about summary refs log tree commit diff
path: root/ops/kontemplate
diff options
context:
space:
mode:
Diffstat (limited to 'ops/kontemplate')
-rw-r--r--ops/kontemplate/.gitignore2
-rw-r--r--ops/kontemplate/LICENSE674
-rw-r--r--ops/kontemplate/README.md185
-rwxr-xr-xops/kontemplate/build-release.sh75
-rw-r--r--ops/kontemplate/context/context.go266
-rw-r--r--ops/kontemplate/context/context_test.go353
-rw-r--r--ops/kontemplate/context/testdata/collections-test.yaml15
-rw-r--r--ops/kontemplate/context/testdata/default-loading.yaml6
-rw-r--r--ops/kontemplate/context/testdata/default/default.yaml2
-rw-r--r--ops/kontemplate/context/testdata/explicit-path.yaml11
-rw-r--r--ops/kontemplate/context/testdata/explicit-subresource-path.yaml8
-rw-r--r--ops/kontemplate/context/testdata/flat-test.yaml10
-rw-r--r--ops/kontemplate/context/testdata/flat-with-args-test.yaml9
-rw-r--r--ops/kontemplate/context/testdata/import-vars-simple.yaml5
-rw-r--r--ops/kontemplate/context/testdata/merging/context.yaml15
-rw-r--r--ops/kontemplate/context/testdata/merging/import-vars.yaml4
-rw-r--r--ops/kontemplate/context/testdata/merging/resource/default.yaml5
-rw-r--r--ops/kontemplate/context/testdata/merging/resource/output.yaml5
-rw-r--r--ops/kontemplate/context/testdata/parent-variable-override.yaml10
-rw-r--r--ops/kontemplate/context/testdata/parent-variables.yaml10
-rw-r--r--ops/kontemplate/context/testdata/test-vars-override.yaml3
-rw-r--r--ops/kontemplate/context/testdata/test-vars.yaml5
-rw-r--r--ops/kontemplate/default.nix36
-rw-r--r--ops/kontemplate/deps.nix111
-rw-r--r--ops/kontemplate/docs/cluster-config.md106
-rw-r--r--ops/kontemplate/docs/resource-sets.md170
-rw-r--r--ops/kontemplate/docs/templates.md153
-rw-r--r--ops/kontemplate/docs/tips-and-tricks.md77
-rw-r--r--ops/kontemplate/example/other-config.yaml7
-rw-r--r--ops/kontemplate/example/prod-cluster.json16
-rw-r--r--ops/kontemplate/example/prod-cluster.yaml17
-rw-r--r--ops/kontemplate/example/some-api/some-api.yaml52
-rw-r--r--ops/kontemplate/example/some-api/some.cfg4
-rw-r--r--ops/kontemplate/image/Dockerfile15
-rw-r--r--ops/kontemplate/image/README.md12
-rw-r--r--ops/kontemplate/image/hashes2
-rw-r--r--ops/kontemplate/main.go242
-rw-r--r--ops/kontemplate/release.nix54
-rw-r--r--ops/kontemplate/templater/dns.go35
-rw-r--r--ops/kontemplate/templater/pass.go34
-rw-r--r--ops/kontemplate/templater/templater.go236
-rw-r--r--ops/kontemplate/templater/templater_test.go205
-rw-r--r--ops/kontemplate/templater/testdata/test-default.txt1
-rw-r--r--ops/kontemplate/templater/testdata/test-insertTemplate.txt1
-rw-r--r--ops/kontemplate/templater/testdata/test-template.txt1
-rw-r--r--ops/kontemplate/util/util.go58
-rw-r--r--ops/kontemplate/util/util_test.go83
47 files changed, 3406 insertions, 0 deletions
diff --git a/ops/kontemplate/.gitignore b/ops/kontemplate/.gitignore
new file mode 100644
index 000000000000..53a04aab3a89
--- /dev/null
+++ b/ops/kontemplate/.gitignore
@@ -0,0 +1,2 @@
+.idea/
+release/
diff --git a/ops/kontemplate/LICENSE b/ops/kontemplate/LICENSE
new file mode 100644
index 000000000000..94a9ed024d38
--- /dev/null
+++ b/ops/kontemplate/LICENSE
@@ -0,0 +1,674 @@
+                    GNU GENERAL PUBLIC LICENSE
+                       Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+  The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works.  By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.  We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors.  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+  To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights.  Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received.  You must make sure that they, too, receive
+or can get the source code.  And you must show them these terms so they
+know their rights.
+
+  Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+  For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software.  For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+  Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so.  This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software.  The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable.  Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products.  If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+  Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary.  To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                       TERMS AND CONDITIONS
+
+  0. Definitions.
+
+  "This License" refers to version 3 of the GNU General Public License.
+
+  "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+  "The Program" refers to any copyrightable work licensed under this
+License.  Each licensee is addressed as "you".  "Licensees" and
+"recipients" may be individuals or organizations.
+
+  To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy.  The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+  A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+  To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy.  Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+  To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies.  Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+  An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License.  If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+  1. Source Code.
+
+  The "source code" for a work means the preferred form of the work
+for making modifications to it.  "Object code" means any non-source
+form of a work.
+
+  A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+  The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form.  A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+  The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities.  However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work.  For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+  The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+  The Corresponding Source for a work in source code form is that
+same work.
+
+  2. Basic Permissions.
+
+  All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met.  This License explicitly affirms your unlimited
+permission to run the unmodified Program.  The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work.  This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+  You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force.  You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright.  Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+  Conveying under any other circumstances is permitted solely under
+the conditions stated below.  Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+  3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+  No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+  When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+  4. Conveying Verbatim Copies.
+
+  You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+  You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+  5. Conveying Modified Source Versions.
+
+  You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+    a) The work must carry prominent notices stating that you modified
+    it, and giving a relevant date.
+
+    b) The work must carry prominent notices stating that it is
+    released under this License and any conditions added under section
+    7.  This requirement modifies the requirement in section 4 to
+    "keep intact all notices".
+
+    c) You must license the entire work, as a whole, under this
+    License to anyone who comes into possession of a copy.  This
+    License will therefore apply, along with any applicable section 7
+    additional terms, to the whole of the work, and all its parts,
+    regardless of how they are packaged.  This License gives no
+    permission to license the work in any other way, but it does not
+    invalidate such permission if you have separately received it.
+
+    d) If the work has interactive user interfaces, each must display
+    Appropriate Legal Notices; however, if the Program has interactive
+    interfaces that do not display Appropriate Legal Notices, your
+    work need not make them do so.
+
+  A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit.  Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+  6. Conveying Non-Source Forms.
+
+  You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+    a) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by the
+    Corresponding Source fixed on a durable physical medium
+    customarily used for software interchange.
+
+    b) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by a
+    written offer, valid for at least three years and valid for as
+    long as you offer spare parts or customer support for that product
+    model, to give anyone who possesses the object code either (1) a
+    copy of the Corresponding Source for all the software in the
+    product that is covered by this License, on a durable physical
+    medium customarily used for software interchange, for a price no
+    more than your reasonable cost of physically performing this
+    conveying of source, or (2) access to copy the
+    Corresponding Source from a network server at no charge.
+
+    c) Convey individual copies of the object code with a copy of the
+    written offer to provide the Corresponding Source.  This
+    alternative is allowed only occasionally and noncommercially, and
+    only if you received the object code with such an offer, in accord
+    with subsection 6b.
+
+    d) Convey the object code by offering access from a designated
+    place (gratis or for a charge), and offer equivalent access to the
+    Corresponding Source in the same way through the same place at no
+    further charge.  You need not require recipients to copy the
+    Corresponding Source along with the object code.  If the place to
+    copy the object code is a network server, the Corresponding Source
+    may be on a different server (operated by you or a third party)
+    that supports equivalent copying facilities, provided you maintain
+    clear directions next to the object code saying where to find the
+    Corresponding Source.  Regardless of what server hosts the
+    Corresponding Source, you remain obligated to ensure that it is
+    available for as long as needed to satisfy these requirements.
+
+    e) Convey the object code using peer-to-peer transmission, provided
+    you inform other peers where the object code and Corresponding
+    Source of the work are being offered to the general public at no
+    charge under subsection 6d.
+
+  A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+  A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling.  In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage.  For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product.  A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+  "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source.  The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+  If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information.  But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+  The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed.  Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+  Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+  7. Additional Terms.
+
+  "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law.  If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+  When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it.  (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.)  You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+  Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+    a) Disclaiming warranty or limiting liability differently from the
+    terms of sections 15 and 16 of this License; or
+
+    b) Requiring preservation of specified reasonable legal notices or
+    author attributions in that material or in the Appropriate Legal
+    Notices displayed by works containing it; or
+
+    c) Prohibiting misrepresentation of the origin of that material, or
+    requiring that modified versions of such material be marked in
+    reasonable ways as different from the original version; or
+
+    d) Limiting the use for publicity purposes of names of licensors or
+    authors of the material; or
+
+    e) Declining to grant rights under trademark law for use of some
+    trade names, trademarks, or service marks; or
+
+    f) Requiring indemnification of licensors and authors of that
+    material by anyone who conveys the material (or modified versions of
+    it) with contractual assumptions of liability to the recipient, for
+    any liability that these contractual assumptions directly impose on
+    those licensors and authors.
+
+  All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10.  If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term.  If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+  If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+  Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+  8. Termination.
+
+  You may not propagate or modify a covered work except as expressly
+provided under this License.  Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+  However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+  Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+  Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License.  If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+  9. Acceptance Not Required for Having Copies.
+
+  You are not required to accept this License in order to receive or
+run a copy of the Program.  Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance.  However,
+nothing other than this License grants you permission to propagate or
+modify any covered work.  These actions infringe copyright if you do
+not accept this License.  Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+  10. Automatic Licensing of Downstream Recipients.
+
+  Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License.  You are not responsible
+for enforcing compliance by third parties with this License.
+
+  An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations.  If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+  You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License.  For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+  11. Patents.
+
+  A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based.  The
+work thus licensed is called the contributor's "contributor version".
+
+  A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version.  For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+  Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+  In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement).  To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+  If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients.  "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+  If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+  A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License.  You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+  Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+  12. No Surrender of Others' Freedom.
+
+  If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all.  For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+  13. Use with the GNU Affero General Public License.
+
+  Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work.  The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+  14. Revised Versions of this License.
+
+  The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+  Each version is given a distinguishing version number.  If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation.  If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+  If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+  Later license versions may give you additional or different
+permissions.  However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+  15. Disclaimer of Warranty.
+
+  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. Limitation of Liability.
+
+  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+  17. Interpretation of Sections 15 and 16.
+
+  If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+                     END OF TERMS AND CONDITIONS
+
+            How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+  If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+    <program>  Copyright (C) <year>  <name of author>
+    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+  You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+<http://www.gnu.org/licenses/>.
+
+  The GNU General Public License does not permit incorporating your program
+into proprietary programs.  If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library.  If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.  But first, please read
+<http://www.gnu.org/philosophy/why-not-lgpl.html>.
diff --git a/ops/kontemplate/README.md b/ops/kontemplate/README.md
new file mode 100644
index 000000000000..803a1c4f16fb
--- /dev/null
+++ b/ops/kontemplate/README.md
@@ -0,0 +1,185 @@
+Kontemplate - A simple Kubernetes templater
+===========================================
+
+Kontemplate is a simple CLI tool that can take sets of Kubernetes resource files
+with placeholders and insert values per environment.
+
+This tool was made because in many cases all I want in terms of Kubernetes
+configuration is simple value interpolation per environment (i.e. Kubernetes
+cluster), but with the same deployment files.
+
+In my experience this is often enough and more complex solutions such as
+[Helm][] are not required.
+
+Check out a Kontemplate setup example and the feature list below!
+
+<!-- markdown-toc start - Don't edit this section. Run M-x markdown-toc-refresh-toc -->
+**Table of Contents**
+
+- [Kontemplate - A simple Kubernetes templater](#kontemplate---a-simple-kubernetes-templater)
+    - [Features](#features)
+    - [Example](#example)
+    - [Installation](#installation)
+        - [Homebrew](#homebrew)
+        - [Arch Linux](#arch-linux)
+        - [Building repeatably from source](#building-repeatably-from-source)
+        - [Building from source](#building-from-source)
+    - [Usage](#usage)
+    - [Contributing](#contributing)
+
+<!-- markdown-toc end -->
+
+## Features
+
+* [Simple, yet powerful templates](docs/templates.md)
+* [Clean cluster configuration files](docs/cluster-config.md)
+* [Resources organised as simple resource sets](docs/resource-sets.md)
+* Integration with pass
+* Integration with kubectl
+
+## Example
+
+Kontemplate lets you describe resources as you normally would in a simple folder structure:
+
+```
+.
+├── prod-cluster.yaml
+└── some-api
+    ├── deployment.yaml
+    └── service.yaml
+```
+
+This example has all resources belonging to `some-api` (no file naming conventions enforced at all!) in the `some-api`
+folder and the configuration for the cluster `prod-cluster` in the corresponding file.
+
+Lets take a short look at `prod-cluster.yaml`:
+
+```yaml
+---
+context: k8s.prod.mydomain.com
+global:
+  globalVar: lizards
+include:
+  - name: some-api
+    values:
+      version: 1.0-0e6884d
+      importantFeature: true
+      apiPort: 4567
+```
+
+Those values are then templated into the resource files of `some-api`. That's it!
+
+You can also set up more complicated folder structures for organisation, for example:
+
+```
+.
+├── api
+│   ├── image-api
+│   │   └── deployment.yaml
+│   └── music-api
+│       └── deployment.yaml
+│   │   └── default.json
+├── frontend
+│   ├── main-app
+│   │   ├── deployment.yaml
+│   │   └── service.yaml
+│   └── user-page
+│       ├── deployment.yaml
+│       └── service.yaml
+├── prod-cluster.yaml
+└── test-cluster.yaml
+```
+
+And selectively template or apply resources with a command such as
+`kontemplate apply test-cluster.yaml --include api --include frontend/user-page`
+to only update the `api` resource sets and the `frontend/user-page` resource set.
+
+## Installation
+
+It is recommended to install Kontemplate from the [Nix](https://nixos.org/) package set,
+where it is available since NixOS 17.09 as `kontemplate`.
+
+If using Nix is not an option for you, several other methods of installation are
+available:
+
+### Binary releases
+
+Signed binary releases are available on the [releases page][] for Linux, OS X, FreeBSD and
+Windows.
+
+Releases are signed with the GPG key `DCF34CFAC1AC44B87E26333136EE34814F6D294A`.
+
+### Building from source
+
+You can clone Kontemplate either by cloning the full TVL
+[depot][https://code.tvl.fyi] or by just cloning the kontemplate
+subtree like so:
+
+    git clone https://code.tvl.fyi/depot.git:/ops/kontemplate.git
+
+The `go` tooling can be used as normal with this cloned repository. In
+a full clone of the depot, Nix can be used to build Kontemplate:
+
+    nix-build -A ops.kontemplate
+
+## Usage
+
+You must have `kubectl` installed to use Kontemplate effectively.
+
+```
+usage: kontemplate [<flags>] <command> [<args> ...]
+
+simple Kubernetes resource templating
+
+Flags:
+  -h, --help                 Show context-sensitive help (also try --help-long and --help-man).
+  -i, --include=INCLUDE ...  Resource sets to include explicitly
+  -e, --exclude=EXCLUDE ...  Resource sets to exclude explicitly
+
+Commands:
+  help [<command>...]
+    Show help.
+
+  template <file>
+    Template resource sets and print them
+
+  apply [<flags>] <file>
+    Template resources and pass to 'kubectl apply'
+
+  replace <file>
+    Template resources and pass to 'kubectl replace'
+
+  delete <file>
+    Template resources and pass to 'kubectl delete'
+
+  create <file>
+    Template resources and pass to 'kubectl create'
+
+```
+
+Examples:
+
+```
+# Look at output for a specific resource set and check to see if it's correct ...
+kontemplate template example/prod-cluster.yaml -i some-api
+
+# ... maybe do a dry-run to see what kubectl would do:
+kontemplate apply example/prod-cluster.yaml --dry-run
+
+# And actually apply it if you like what you see:
+kontemplate apply example/prod-cluster.yaml
+```
+
+Check out the feature list and the individual feature documentation above. Then you should be good to go!
+
+## Contributing
+
+Feel free to contribute pull requests, file bugs and open issues with feature suggestions!
+
+Kontemplate is licensed under the GPLv3, a copy of the license and its terms can be found
+in the `LICENSE` file.
+
+Please follow the [code of conduct](CODE_OF_CONDUCT.md).
+
+[Helm]: https://helm.sh/
+[releases page]: https://github.com/tazjin/kontemplate/releases
diff --git a/ops/kontemplate/build-release.sh b/ops/kontemplate/build-release.sh
new file mode 100755
index 000000000000..e4258c53dd09
--- /dev/null
+++ b/ops/kontemplate/build-release.sh
@@ -0,0 +1,75 @@
+#!/usr/bin/env bash
+set -ueo pipefail
+
+# Copyright (C) 2016-2019  Vincent Ambo <mail@tazj.in>
+#
+# This file is part of Kontemplate.
+#
+# Kontemplate is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+
+readonly GIT_HASH="$(git rev-parse --short HEAD)"
+readonly LDFLAGS="-X main.gitHash=${GIT_HASH} -w -s"
+readonly VERSION="1.8.0-${GIT_HASH}"
+
+function binary-name() {
+    local os="${1}"
+    local target="${2}"
+    if [ "${os}" = "windows" ]; then
+        echo -n "${target}/kontemplate.exe"
+    else
+        echo -n "${target}/kontemplate"
+    fi
+}
+
+function build-for() {
+    local os="${1}"
+    local arch="${2}"
+    local target="release/${os}/${arch}"
+    local bin=$(binary-name "${os}" "${target}")
+
+    echo "Building kontemplate for ${os}-${arch} in ${target}"
+
+    mkdir -p "${target}"
+
+    env GOOS="${os}" GOARCH="${arch}" go build \
+        -ldflags "${LDFLAGS}" \
+        -o "${bin}" \
+        -tags netgo
+}
+
+function sign-for() {
+    local os="${1}"
+    local arch="${2}"
+    local target="release/${os}/${arch}"
+    local bin=$(binary-name "${os}" "${target}")
+    local tar="release/kontemplate-${VERSION}-${os}-${arch}.tar.gz"
+
+    echo "Packing release into ${tar}"
+    tar czvf "${tar}" -C "${target}" $(basename "${bin}")
+
+    local hash=$(sha256sum "${tar}")
+    echo "Signing kontemplate release tarball for ${os}-${arch} with SHA256 ${hash}"
+    gpg --armor --detach-sig --sign "${tar}"
+}
+
+case "${1}" in
+    "build")
+        # Build releases for various operating systems:
+        build-for "linux" "amd64"
+        build-for "darwin" "amd64"
+        build-for "windows" "amd64"
+        build-for "freebsd" "amd64"
+        exit 0
+        ;;
+    "sign")
+        # Bundle and sign releases:
+        sign-for "linux" "amd64"
+        sign-for "darwin" "amd64"
+        sign-for "windows" "amd64"
+        sign-for "freebsd" "amd64"
+        exit 0
+        ;;
+esac
diff --git a/ops/kontemplate/context/context.go b/ops/kontemplate/context/context.go
new file mode 100644
index 000000000000..2d0378a0ec23
--- /dev/null
+++ b/ops/kontemplate/context/context.go
@@ -0,0 +1,266 @@
+// Copyright (C) 2016-2019  Vincent Ambo <mail@tazj.in>
+//
+// This file is part of Kontemplate.
+//
+// Kontemplate is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+package context
+
+import (
+	"fmt"
+	"path"
+	"strings"
+
+	"github.com/tazjin/kontemplate/util"
+)
+
+type ResourceSet struct {
+	// Name of the resource set. This can be used in include/exclude statements during kontemplate runs.
+	Name string `json:"name"`
+
+	// Path to the folder containing the files for this resource set. This defaults to the value of the 'name' field
+	// if unset.
+	Path string `json:"path"`
+
+	// Values to include when interpolating resources from this resource set.
+	Values map[string]interface{} `json:"values"`
+
+	// Args to pass on to kubectl for this resource set.
+	Args []string `json:"args"`
+
+	// Nested resource sets to include
+	Include []ResourceSet `json:"include"`
+
+	// Parent resource set for flattened resource sets. Should not be manually specified.
+	Parent string
+}
+
+type Context struct {
+	// The name of the kubectl context
+	Name string `json:"context"`
+
+	// Global variables that should be accessible by all resource sets
+	Global map[string]interface{} `json:"global"`
+
+	// File names of YAML or JSON files including extra variables that should be globally accessible
+	VariableImportFiles []string `json:"import"`
+
+	// The resource sets to include in this context
+	ResourceSets []ResourceSet `json:"include"`
+
+	// Variables imported from additional files
+	ImportedVars map[string]interface{}
+
+	// Explicitly set variables (via `--var`) that should override all others
+	ExplicitVars map[string]interface{}
+
+	// This field represents the absolute path to the context base directory and should not be manually specified.
+	BaseDir string
+}
+
+func contextLoadingError(filename string, cause error) error {
+	return fmt.Errorf("Context loading failed on file %s due to: \n%v", filename, cause)
+}
+
+// Attempt to load and deserialise a Context from the specified file.
+func LoadContext(filename string, explicitVars *[]string) (*Context, error) {
+	var ctx Context
+	err := util.LoadData(filename, &ctx)
+
+	if err != nil {
+		return nil, contextLoadingError(filename, err)
+	}
+
+	ctx.BaseDir = path.Dir(filename)
+
+	// Prepare the resource sets by resolving parents etc.
+	ctx.ResourceSets = flattenPrepareResourceSetPaths(&ctx.BaseDir, &ctx.ResourceSets)
+
+	// Add variables explicitly specified on the command line
+	ctx.ExplicitVars, err = loadExplicitVars(explicitVars)
+	if err != nil {
+		return nil, fmt.Errorf("Error setting explicit variables: %v\n", err)
+	}
+
+	// Add variables loaded from import files
+	ctx.ImportedVars, err = ctx.loadImportedVariables()
+	if err != nil {
+		return nil, contextLoadingError(filename, err)
+	}
+
+	// Merge variables defined at different levels. The
+	// `mergeContextValues` function is documented with the merge
+	// hierarchy.
+	ctx.ResourceSets = ctx.mergeContextValues()
+
+	if err != nil {
+		return nil, contextLoadingError(filename, err)
+	}
+
+	return &ctx, nil
+}
+
+// Kontemplate supports specifying additional variable files with the
+// `import` keyword. This function loads those variable files and
+// merges them together with the context's other global variables.
+func (ctx *Context) loadImportedVariables() (map[string]interface{}, error) {
+	allImportedVars := make(map[string]interface{})
+
+	for _, file := range ctx.VariableImportFiles {
+		// Ensure that the filename is not merged with the baseDir if
+		// it is set to an absolute path.
+		var filePath string
+		if path.IsAbs(file) {
+			filePath = file
+		} else {
+			filePath = path.Join(ctx.BaseDir, file)
+		}
+
+		var importedVars map[string]interface{}
+		err := util.LoadData(filePath, &importedVars)
+
+		if err != nil {
+			return nil, err
+		}
+
+		allImportedVars = *util.Merge(&allImportedVars, &importedVars)
+	}
+
+	return allImportedVars, nil
+}
+
+// Correctly prepares the file paths for resource sets by inferring implicit paths and flattening resource set
+// collections, i.e. resource sets that themselves have an additional 'include' field set.
+// Those will be regarded as a short-hand for including multiple resource sets from a subfolder.
+// See https://github.com/tazjin/kontemplate/issues/9 for more information.
+func flattenPrepareResourceSetPaths(baseDir *string, rs *[]ResourceSet) []ResourceSet {
+	flattened := make([]ResourceSet, 0)
+
+	for _, r := range *rs {
+		// If a path is not explicitly specified it should default to the resource set name.
+		// This is also the classic behaviour prior to kontemplate 1.2
+		if r.Path == "" {
+			r.Path = r.Name
+		}
+
+		// Paths are made absolute by resolving them relative to the context base,
+		// unless absolute paths were specified.
+		if !path.IsAbs(r.Path) {
+			r.Path = path.Join(*baseDir, r.Path)
+		}
+
+		if len(r.Include) == 0 {
+			flattened = append(flattened, r)
+		} else {
+			for _, subResourceSet := range r.Include {
+				if subResourceSet.Path == "" {
+					subResourceSet.Path = subResourceSet.Name
+				}
+
+				subResourceSet.Parent = r.Name
+				subResourceSet.Name = path.Join(r.Name, subResourceSet.Name)
+				subResourceSet.Path = path.Join(r.Path, subResourceSet.Path)
+				subResourceSet.Values = *util.Merge(&r.Values, &subResourceSet.Values)
+				flattened = append(flattened, subResourceSet)
+			}
+		}
+	}
+
+	return flattened
+}
+
+// Merges the context and resource set variables according in the
+// desired precedence order.
+//
+// For now the reasoning behind the merge order is from least specific
+// in relation to the cluster configuration, which means that the
+// precedence is (in ascending order):
+//
+// 1. Default values in resource sets.
+// 2. Values imported from files (via `import:`)
+// 3. Global values in a cluster configuration
+// 4. Values set in a resource set's `include`-section
+// 5. Explicit values set on the CLI (`--var`)
+//
+// For a discussion on the reasoning behind this order, please consult
+// https://github.com/tazjin/kontemplate/issues/142
+func (ctx *Context) mergeContextValues() []ResourceSet {
+	updated := make([]ResourceSet, len(ctx.ResourceSets))
+
+	// Merging has to happen separately for every individual
+	// resource set to make use of the default values:
+	for i, rs := range ctx.ResourceSets {
+		// Begin by loading default values from the resource
+		// sets configuration.
+		//
+		// Resource sets are used across different cluster
+		// contexts and the default values in them have the
+		// lowest precedence.
+		defaultValues := loadDefaultValues(&rs, ctx)
+
+		// Continue by merging default values with values
+		// imported from external files. Those values are also
+		// used across cluster contexts, but have higher
+		// precedence than defaults.
+		merged := util.Merge(defaultValues, &ctx.ImportedVars)
+
+		// Merge global values defined in the cluster context:
+		merged = util.Merge(merged, &ctx.Global)
+
+		// Merge values configured in the resource set's
+		// `include` section:
+		merged = util.Merge(merged, &rs.Values)
+
+		// Merge values defined explicitly on the CLI:
+		merged = util.Merge(merged, &ctx.ExplicitVars)
+
+		// Continue with the newly merged resource set:
+		rs.Values = *merged
+		updated[i] = rs
+	}
+
+	return updated
+}
+
+// Loads default values for a resource set collection from
+// path/to/set/default.{json|yaml}.
+func loadDefaultValues(rs *ResourceSet, c *Context) *map[string]interface{} {
+	var defaultVars map[string]interface{}
+
+	for _, filename := range util.DefaultFilenames {
+		err := util.LoadData(path.Join(rs.Path, filename), &defaultVars)
+		if err == nil {
+			return &defaultVars
+		}
+	}
+
+	// The actual error is not inspected here. The reasoning for
+	// this is that in case of serious problems (e.g. permission
+	// issues with the folder / folder not existing) failure will
+	// occur a bit later anyways.
+	//
+	// Otherwise we'd have to differentiate between
+	// file-not-found-errors (no default values specified) and
+	// other errors here.
+	return &rs.Values
+}
+
+// Prepares the variables specified explicitly via `--var` when
+// executing kontemplate for adding to the context.
+func loadExplicitVars(vars *[]string) (map[string]interface{}, error) {
+	explicitVars := make(map[string]interface{}, len(*vars))
+
+	for _, v := range *vars {
+		varParts := strings.SplitN(v, "=", 2)
+		if len(varParts) != 2 {
+			return nil, fmt.Errorf(`invalid explicit variable provided (%s), name and value should be separated with "="`, v)
+		}
+
+		explicitVars[varParts[0]] = varParts[1]
+	}
+
+	return explicitVars, nil
+}
diff --git a/ops/kontemplate/context/context_test.go b/ops/kontemplate/context/context_test.go
new file mode 100644
index 000000000000..7ecd9d587d24
--- /dev/null
+++ b/ops/kontemplate/context/context_test.go
@@ -0,0 +1,353 @@
+// Copyright (C) 2016-2019  Vincent Ambo <mail@tazj.in>
+//
+// This file is part of Kontemplate.
+//
+// Kontemplate is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+package context
+
+import (
+	"reflect"
+	"testing"
+)
+
+var noExplicitVars []string = make([]string, 0)
+
+func TestLoadFlatContextFromFile(t *testing.T) {
+	ctx, err := LoadContext("testdata/flat-test.yaml", &noExplicitVars)
+
+	if err != nil {
+		t.Error(err)
+		t.Fail()
+	}
+
+	expected := Context{
+		Name: "k8s.prod.mydomain.com",
+		Global: map[string]interface{}{
+			"globalVar": "lizards",
+		},
+		ResourceSets: []ResourceSet{
+			{
+				Name: "some-api",
+				Path: "testdata/some-api",
+				Values: map[string]interface{}{
+					"apiPort":          float64(4567), // yep!
+					"importantFeature": true,
+					"version":          "1.0-0e6884d",
+					"globalVar":        "lizards",
+				},
+				Include: nil,
+				Parent:  "",
+			},
+		},
+		BaseDir:      "testdata",
+		ImportedVars: make(map[string]interface{}, 0),
+		ExplicitVars: make(map[string]interface{}, 0),
+	}
+
+	if !reflect.DeepEqual(*ctx, expected) {
+		t.Error("Loaded context and expected context did not match")
+		t.Fail()
+	}
+}
+
+func TestLoadContextWithArgs(t *testing.T) {
+	ctx, err := LoadContext("testdata/flat-with-args-test.yaml", &noExplicitVars)
+
+	if err != nil {
+		t.Error(err)
+		t.Fail()
+	}
+
+	expected := Context{
+		Name: "k8s.prod.mydomain.com",
+		ResourceSets: []ResourceSet{
+			{
+				Name:   "some-api",
+				Path:   "testdata/some-api",
+				Values: make(map[string]interface{}, 0),
+				Args: []string{
+					"--as=some-user",
+					"--as-group=hello:world",
+					"--as-banana",
+					"true",
+				},
+				Include: nil,
+				Parent:  "",
+			},
+		},
+		BaseDir:      "testdata",
+		ImportedVars: make(map[string]interface{}, 0),
+		ExplicitVars: make(map[string]interface{}, 0),
+	}
+
+	if !reflect.DeepEqual(*ctx, expected) {
+		t.Error("Loaded context and expected context did not match")
+		t.Fail()
+	}
+}
+
+func TestLoadContextWithResourceSetCollections(t *testing.T) {
+	ctx, err := LoadContext("testdata/collections-test.yaml", &noExplicitVars)
+
+	if err != nil {
+		t.Error(err)
+		t.Fail()
+	}
+
+	expected := Context{
+		Name: "k8s.prod.mydomain.com",
+		Global: map[string]interface{}{
+			"globalVar": "lizards",
+		},
+		ResourceSets: []ResourceSet{
+			{
+				Name: "some-api",
+				Path: "testdata/some-api",
+				Values: map[string]interface{}{
+					"apiPort":          float64(4567), // yep!
+					"importantFeature": true,
+					"version":          "1.0-0e6884d",
+					"globalVar":        "lizards",
+				},
+				Include: nil,
+				Parent:  "",
+			},
+			{
+				Name: "collection/nested",
+				Path: "testdata/collection/nested",
+				Values: map[string]interface{}{
+					"lizards":   "good",
+					"globalVar": "lizards",
+				},
+				Include: nil,
+				Parent:  "collection",
+			},
+		},
+		BaseDir:      "testdata",
+		ImportedVars: make(map[string]interface{}, 0),
+		ExplicitVars: make(map[string]interface{}, 0),
+	}
+
+	if !reflect.DeepEqual(*ctx, expected) {
+		t.Error("Loaded context and expected context did not match")
+		t.Fail()
+	}
+
+}
+
+func TestSubresourceVariableInheritance(t *testing.T) {
+	ctx, err := LoadContext("testdata/parent-variables.yaml", &noExplicitVars)
+
+	if err != nil {
+		t.Error(err)
+		t.Fail()
+	}
+
+	expected := Context{
+		Name: "k8s.prod.mydomain.com",
+		ResourceSets: []ResourceSet{
+			{
+				Name: "parent/child",
+				Path: "testdata/parent/child",
+				Values: map[string]interface{}{
+					"foo": "bar",
+					"bar": "baz",
+				},
+				Include: nil,
+				Parent:  "parent",
+			},
+		},
+		BaseDir:      "testdata",
+		ImportedVars: make(map[string]interface{}, 0),
+		ExplicitVars: make(map[string]interface{}, 0),
+	}
+
+	if !reflect.DeepEqual(*ctx, expected) {
+		t.Error("Loaded and expected context did not match")
+		t.Fail()
+	}
+}
+
+func TestSubresourceVariableInheritanceOverride(t *testing.T) {
+	ctx, err := LoadContext("testdata/parent-variable-override.yaml", &noExplicitVars)
+
+	if err != nil {
+		t.Error(err)
+		t.Fail()
+	}
+
+	expected := Context{
+		Name: "k8s.prod.mydomain.com",
+		ResourceSets: []ResourceSet{
+			{
+				Name: "parent/child",
+				Path: "testdata/parent/child",
+				Values: map[string]interface{}{
+					"foo": "newvalue",
+				},
+				Include: nil,
+				Parent:  "parent",
+			},
+		},
+		BaseDir:      "testdata",
+		ImportedVars: make(map[string]interface{}, 0),
+		ExplicitVars: make(map[string]interface{}, 0),
+	}
+
+	if !reflect.DeepEqual(*ctx, expected) {
+		t.Error("Loaded and expected context did not match")
+		t.Fail()
+	}
+}
+
+func TestDefaultValuesLoading(t *testing.T) {
+	ctx, err := LoadContext("testdata/default-loading.yaml", &noExplicitVars)
+	if err != nil {
+		t.Error(err)
+		t.Fail()
+	}
+
+	rs := ctx.ResourceSets[0]
+	if rs.Values["defaultValues"] != "loaded" {
+		t.Errorf("Default values not loaded from YAML file")
+		t.Fail()
+	}
+
+	if rs.Values["override"] != "notAtAll" {
+		t.Error("Default values should not override other values")
+		t.Fail()
+	}
+}
+
+func TestImportValuesLoading(t *testing.T) {
+	ctx, err := LoadContext("testdata/import-vars-simple.yaml", &noExplicitVars)
+	if err != nil {
+		t.Error(err)
+		t.Fail()
+	}
+
+	expected := map[string]interface{}{
+		"override": "true",
+		"music": map[string]interface{}{
+			"artist": "Pallida",
+			"track":  "Tractor Beam",
+		},
+	}
+
+	if !reflect.DeepEqual(ctx.ImportedVars, expected) {
+		t.Error("Expected imported values after loading imports did not match!")
+		t.Fail()
+	}
+}
+
+func TestExplicitPathLoading(t *testing.T) {
+	ctx, err := LoadContext("testdata/explicit-path.yaml", &noExplicitVars)
+	if err != nil {
+		t.Error(err)
+		t.Fail()
+	}
+
+	expected := Context{
+		Name: "k8s.prod.mydomain.com",
+		ResourceSets: []ResourceSet{
+			{
+				Name: "some-api-europe",
+				Path: "testdata/some-api",
+				Values: map[string]interface{}{
+					"location": "europe",
+				},
+				Include: nil,
+				Parent:  "",
+			},
+			{
+				Name: "some-api-asia",
+				Path: "testdata/some-api",
+				Values: map[string]interface{}{
+					"location": "asia",
+				},
+				Include: nil,
+				Parent:  "",
+			},
+		},
+		BaseDir:      "testdata",
+		ImportedVars: make(map[string]interface{}, 0),
+		ExplicitVars: make(map[string]interface{}, 0),
+	}
+
+	if !reflect.DeepEqual(*ctx, expected) {
+		t.Error("Loaded context and expected context did not match")
+		t.Fail()
+	}
+}
+
+func TestExplicitSubresourcePathLoading(t *testing.T) {
+	ctx, err := LoadContext("testdata/explicit-subresource-path.yaml", &noExplicitVars)
+	if err != nil {
+		t.Error(err)
+		t.Fail()
+	}
+
+	expected := Context{
+		Name: "k8s.prod.mydomain.com",
+		ResourceSets: []ResourceSet{
+			{
+				Name:   "parent/child",
+				Path:   "testdata/parent-path/child-path",
+				Parent: "parent",
+				Values: make(map[string]interface{}, 0),
+			},
+		},
+		BaseDir:      "testdata",
+		ImportedVars: make(map[string]interface{}, 0),
+		ExplicitVars: make(map[string]interface{}, 0),
+	}
+
+	if !reflect.DeepEqual(*ctx, expected) {
+		t.Error("Loaded context and expected context did not match")
+		t.Fail()
+	}
+}
+
+func TestSetVariablesFromArguments(t *testing.T) {
+	vars := []string{"version=some-service-version"}
+	ctx, _ := LoadContext("testdata/default-loading.yaml", &vars)
+
+	if version := ctx.ExplicitVars["version"]; version != "some-service-version" {
+		t.Errorf(`Expected variable "version" to have value "some-service-version" but was "%s"`, version)
+	}
+}
+
+func TestSetInvalidVariablesFromArguments(t *testing.T) {
+	vars := []string{"version: some-service-version"}
+	_, err := LoadContext("testdata/default-loading.yaml", &vars)
+
+	if err == nil {
+		t.Error("Expected invalid variable to return an error")
+	}
+}
+
+// This test ensures that variables are merged in the correct order.
+// Please consult the test data in `testdata/merging`.
+func TestValueMergePrecedence(t *testing.T) {
+	cliVars:= []string{"cliVar=cliVar"}
+	ctx, _ := LoadContext("testdata/merging/context.yaml", &cliVars)
+
+	expected := map[string]interface{}{
+		"defaultVar": "defaultVar",
+		"importVar": "importVar",
+		"globalVar": "globalVar",
+		"includeVar": "includeVar",
+		"cliVar": "cliVar",
+	}
+
+	result := ctx.ResourceSets[0].Values
+
+	if !reflect.DeepEqual(expected, result) {
+		t.Errorf("Merged values did not match expected result: \n%v", result)
+		t.Fail()
+	}
+}
diff --git a/ops/kontemplate/context/testdata/collections-test.yaml b/ops/kontemplate/context/testdata/collections-test.yaml
new file mode 100644
index 000000000000..a619c8cfddcc
--- /dev/null
+++ b/ops/kontemplate/context/testdata/collections-test.yaml
@@ -0,0 +1,15 @@
+---
+context: k8s.prod.mydomain.com
+global:
+  globalVar: lizards
+include:
+  - name: some-api
+    values:
+      version: 1.0-0e6884d
+      importantFeature: true
+      apiPort: 4567
+  - name: collection
+    include:
+      - name: nested
+        values:
+          lizards: good
diff --git a/ops/kontemplate/context/testdata/default-loading.yaml b/ops/kontemplate/context/testdata/default-loading.yaml
new file mode 100644
index 000000000000..d589c99b4eaf
--- /dev/null
+++ b/ops/kontemplate/context/testdata/default-loading.yaml
@@ -0,0 +1,6 @@
+---
+context: default-loading
+include:
+  - name: default
+    values:
+      override: notAtAll
\ No newline at end of file
diff --git a/ops/kontemplate/context/testdata/default/default.yaml b/ops/kontemplate/context/testdata/default/default.yaml
new file mode 100644
index 000000000000..0ffa3cd81f24
--- /dev/null
+++ b/ops/kontemplate/context/testdata/default/default.yaml
@@ -0,0 +1,2 @@
+defaultValues: loaded
+override: noop
\ No newline at end of file
diff --git a/ops/kontemplate/context/testdata/explicit-path.yaml b/ops/kontemplate/context/testdata/explicit-path.yaml
new file mode 100644
index 000000000000..2c81f83c0919
--- /dev/null
+++ b/ops/kontemplate/context/testdata/explicit-path.yaml
@@ -0,0 +1,11 @@
+---
+context: k8s.prod.mydomain.com
+include:
+  - name: some-api-europe
+    path: some-api
+    values:
+      location: europe
+  - name: some-api-asia
+    path: some-api
+    values:
+      location: asia
diff --git a/ops/kontemplate/context/testdata/explicit-subresource-path.yaml b/ops/kontemplate/context/testdata/explicit-subresource-path.yaml
new file mode 100644
index 000000000000..6cf86183229e
--- /dev/null
+++ b/ops/kontemplate/context/testdata/explicit-subresource-path.yaml
@@ -0,0 +1,8 @@
+---
+context: k8s.prod.mydomain.com
+include:
+  - name: parent
+    path: parent-path
+    include:
+      - name: child
+        path: child-path
diff --git a/ops/kontemplate/context/testdata/flat-test.yaml b/ops/kontemplate/context/testdata/flat-test.yaml
new file mode 100644
index 000000000000..dd7804f719c3
--- /dev/null
+++ b/ops/kontemplate/context/testdata/flat-test.yaml
@@ -0,0 +1,10 @@
+---
+context: k8s.prod.mydomain.com
+global:
+  globalVar: lizards
+include:
+  - name: some-api
+    values:
+      version: 1.0-0e6884d
+      importantFeature: true
+      apiPort: 4567
diff --git a/ops/kontemplate/context/testdata/flat-with-args-test.yaml b/ops/kontemplate/context/testdata/flat-with-args-test.yaml
new file mode 100644
index 000000000000..29d3334fb54d
--- /dev/null
+++ b/ops/kontemplate/context/testdata/flat-with-args-test.yaml
@@ -0,0 +1,9 @@
+---
+context: k8s.prod.mydomain.com
+include:
+  - name: some-api
+    args:
+      - --as=some-user
+      - --as-group=hello:world
+      - --as-banana
+      - "true"
diff --git a/ops/kontemplate/context/testdata/import-vars-simple.yaml b/ops/kontemplate/context/testdata/import-vars-simple.yaml
new file mode 100644
index 000000000000..12244e1ab174
--- /dev/null
+++ b/ops/kontemplate/context/testdata/import-vars-simple.yaml
@@ -0,0 +1,5 @@
+---
+context: k8s.prod.mydomain.com
+import:
+  - test-vars.yaml
+include: []
diff --git a/ops/kontemplate/context/testdata/merging/context.yaml b/ops/kontemplate/context/testdata/merging/context.yaml
new file mode 100644
index 000000000000..df30d3d8cbe3
--- /dev/null
+++ b/ops/kontemplate/context/testdata/merging/context.yaml
@@ -0,0 +1,15 @@
+# This context file is intended to test the merge hierarchy of
+# variables defined at different levels.
+---
+context: merging.in.kontemplate.works
+global:
+  globalVar: globalVar
+  includeVar: should be overridden (global)
+  cliVar: should be overridden (global)
+import:
+  - import-vars.yaml
+include:
+  - name: resource
+    values:
+      includeVar: includeVar
+      cliVar: should be overridden (include)
diff --git a/ops/kontemplate/context/testdata/merging/import-vars.yaml b/ops/kontemplate/context/testdata/merging/import-vars.yaml
new file mode 100644
index 000000000000..2a51352571a6
--- /dev/null
+++ b/ops/kontemplate/context/testdata/merging/import-vars.yaml
@@ -0,0 +1,4 @@
+importVar: importVar
+globalVar: should be overridden (import)
+includeVar: should be overridden (import)
+cliVar: should be overridden (import)
diff --git a/ops/kontemplate/context/testdata/merging/resource/default.yaml b/ops/kontemplate/context/testdata/merging/resource/default.yaml
new file mode 100644
index 000000000000..040a19aaba25
--- /dev/null
+++ b/ops/kontemplate/context/testdata/merging/resource/default.yaml
@@ -0,0 +1,5 @@
+defaultVar: defaultVar
+importVar: should be overridden (default)
+globalVar: should be overridden (default)
+includeVar: should be overridden (default)
+cliVar: should be overridden (default)
diff --git a/ops/kontemplate/context/testdata/merging/resource/output.yaml b/ops/kontemplate/context/testdata/merging/resource/output.yaml
new file mode 100644
index 000000000000..5920b2720780
--- /dev/null
+++ b/ops/kontemplate/context/testdata/merging/resource/output.yaml
@@ -0,0 +1,5 @@
+defaultVar: {{ .defaultVar }}
+importVar: {{ .importVar }}
+globalVar: {{ .globalVar }}
+includeVar: {{ .includeVar }}
+cliVar: {{ .cliVar }}
diff --git a/ops/kontemplate/context/testdata/parent-variable-override.yaml b/ops/kontemplate/context/testdata/parent-variable-override.yaml
new file mode 100644
index 000000000000..42676c3028fe
--- /dev/null
+++ b/ops/kontemplate/context/testdata/parent-variable-override.yaml
@@ -0,0 +1,10 @@
+---
+context: k8s.prod.mydomain.com
+include:
+  - name: parent
+    values:
+      foo: bar
+    include:
+      - name: child
+        values:
+          foo: newvalue
diff --git a/ops/kontemplate/context/testdata/parent-variables.yaml b/ops/kontemplate/context/testdata/parent-variables.yaml
new file mode 100644
index 000000000000..8459fd30405b
--- /dev/null
+++ b/ops/kontemplate/context/testdata/parent-variables.yaml
@@ -0,0 +1,10 @@
+---
+context: k8s.prod.mydomain.com
+include:
+  - name: parent
+    values:
+      foo: bar
+    include:
+      - name: child
+        values:
+          bar: baz
diff --git a/ops/kontemplate/context/testdata/test-vars-override.yaml b/ops/kontemplate/context/testdata/test-vars-override.yaml
new file mode 100644
index 000000000000..5215c559c136
--- /dev/null
+++ b/ops/kontemplate/context/testdata/test-vars-override.yaml
@@ -0,0 +1,3 @@
+---
+override: 3
+place: Oslo
diff --git a/ops/kontemplate/context/testdata/test-vars.yaml b/ops/kontemplate/context/testdata/test-vars.yaml
new file mode 100644
index 000000000000..af27bdc455bf
--- /dev/null
+++ b/ops/kontemplate/context/testdata/test-vars.yaml
@@ -0,0 +1,5 @@
+---
+override: 'true'
+music:
+  artist: Pallida
+  track: Tractor Beam
diff --git a/ops/kontemplate/default.nix b/ops/kontemplate/default.nix
new file mode 100644
index 000000000000..1190869c3fda
--- /dev/null
+++ b/ops/kontemplate/default.nix
@@ -0,0 +1,36 @@
+# Copyright (C) 2016-2021  Vincent Ambo <mail@tazj.in>
+#
+# This file is part of Kontemplate.
+#
+# Kontemplate is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This file is the Nix derivation used to install Kontemplate on
+# Nix-based systems.
+
+{ lib, pkgs, ... }:
+
+pkgs.buildGoPackage rec {
+  name = "kontemplate-${version}";
+  version = "canon";
+  src = ./.;
+  goPackagePath = "github.com/tazjin/kontemplate";
+  goDeps = ./deps.nix;
+  buildInputs = [ pkgs.parallel ];
+
+  # Enable checks and configure check-phase to include vet:
+  doCheck = true;
+  preCheck = ''
+    for pkg in $(getGoDirs ""); do
+      buildGoDir vet "$pkg"
+    done
+  '';
+
+  meta = with lib; {
+    description = "A resource templating helper for Kubernetes";
+    homepage = "http://kontemplate.works/";
+    license = licenses.gpl3;
+  };
+}
diff --git a/ops/kontemplate/deps.nix b/ops/kontemplate/deps.nix
new file mode 100644
index 000000000000..7693968bd550
--- /dev/null
+++ b/ops/kontemplate/deps.nix
@@ -0,0 +1,111 @@
+# This file was generated by https://github.com/kamilchm/go2nix v1.3.0
+[
+  {
+    goPackagePath = "github.com/Masterminds/goutils";
+    fetch = {
+      type = "git";
+      url = "https://github.com/Masterminds/goutils";
+      rev = "41ac8693c5c10a92ea1ff5ac3a7f95646f6123b0";
+      sha256 = "180px47gj936qyk5bkv5mbbgiil9abdjq6kwkf7sq70vyi9mcfiq";
+    };
+  }
+  {
+    goPackagePath = "github.com/Masterminds/semver";
+    fetch = {
+      type = "git";
+      url = "https://github.com/Masterminds/semver";
+      rev = "5bc3b9184d48f1412b300b87a200cf020d9254cf";
+      sha256 = "1vdfm653v50jf63cw0kg2hslx50cn4mk6lj3p51bi11jrg48kfng";
+    };
+  }
+  {
+    goPackagePath = "github.com/Masterminds/sprig";
+    fetch = {
+      type = "git";
+      url = "https://github.com/Masterminds/sprig";
+      rev = "6f509977777c33eae63b2136d97f7b976cb971cc";
+      sha256 = "05h9k6fhjxnpwlihj3z02q9kvqvnq53jix0ab84sx0666bci3cdh";
+    };
+  }
+  {
+    goPackagePath = "github.com/alecthomas/template";
+    fetch = {
+      type = "git";
+      url = "https://github.com/alecthomas/template";
+      rev = "fb15b899a75114aa79cc930e33c46b577cc664b1";
+      sha256 = "1vlasv4dgycydh5wx6jdcvz40zdv90zz1h7836z7lhsi2ymvii26";
+    };
+  }
+  {
+    goPackagePath = "github.com/alecthomas/units";
+    fetch = {
+      type = "git";
+      url = "https://github.com/alecthomas/units";
+      rev = "c3de453c63f4bdb4dadffab9805ec00426c505f7";
+      sha256 = "0js37zlgv37y61j4a2d46jh72xm5kxmpaiw0ya9v944bjpc386my";
+    };
+  }
+  {
+    goPackagePath = "github.com/ghodss/yaml";
+    fetch = {
+      type = "git";
+      url = "https://github.com/ghodss/yaml";
+      rev = "25d852aebe32c875e9c044af3eef9c7dc6bc777f";
+      sha256 = "1w9yq0bxzygc4qwkwwiy7k1k1yviaspcqqv18255k2xkjv5ipccz";
+    };
+  }
+  {
+    goPackagePath = "github.com/google/uuid";
+    fetch = {
+      type = "git";
+      url = "https://github.com/google/uuid";
+      rev = "c2e93f3ae59f2904160ceaab466009f965df46d6";
+      sha256 = "0zw8fvl6jqg0fmv6kmvhss0g4gkrbvgyvl2zgy5wdbdlgp4fja0h";
+    };
+  }
+  {
+    goPackagePath = "github.com/huandu/xstrings";
+    fetch = {
+      type = "git";
+      url = "https://github.com/huandu/xstrings";
+      rev = "8bbcf2f9ccb55755e748b7644164cd4bdce94c1d";
+      sha256 = "1ivvc95514z63k7cpz71l0dwlanffmsh1pijhaqmp41kfiby8rsx";
+    };
+  }
+  {
+    goPackagePath = "github.com/imdario/mergo";
+    fetch = {
+      type = "git";
+      url = "https://github.com/imdario/mergo";
+      rev = "4c317f2286be3bd0c4f1a0e622edc6398ec4656d";
+      sha256 = "0bihha1qsgfjk14yv1hwddv3d8dzxpbjlaxwwyys6lhgxz1cr9h9";
+    };
+  }
+  {
+    goPackagePath = "golang.org/x/crypto";
+    fetch = {
+      type = "git";
+      url = "https://go.googlesource.com/crypto";
+      rev = "9756ffdc24725223350eb3266ffb92590d28f278";
+      sha256 = "0q7hxaaq6lp0v8qqzifvysl47z5rfdlrxkh3d29vsl3wyby3dxl8";
+    };
+  }
+  {
+    goPackagePath = "gopkg.in/alecthomas/kingpin.v2";
+    fetch = {
+      type = "git";
+      url = "https://gopkg.in/alecthomas/kingpin.v2";
+      rev = "947dcec5ba9c011838740e680966fd7087a71d0d";
+      sha256 = "0mndnv3hdngr3bxp7yxfd47cas4prv98sqw534mx7vp38gd88n5r";
+    };
+  }
+  {
+    goPackagePath = "gopkg.in/yaml.v2";
+    fetch = {
+      type = "git";
+      url = "https://gopkg.in/yaml.v2";
+      rev = "51d6538a90f86fe93ac480b35f37b2be17fef232";
+      sha256 = "01wj12jzsdqlnidpyjssmj0r4yavlqy7dwrg7adqd8dicjc4ncsa";
+    };
+  }
+]
diff --git a/ops/kontemplate/docs/cluster-config.md b/ops/kontemplate/docs/cluster-config.md
new file mode 100644
index 000000000000..4e870161796d
--- /dev/null
+++ b/ops/kontemplate/docs/cluster-config.md
@@ -0,0 +1,106 @@
+Cluster configuration
+==========================
+
+Every cluster (or "environment") that requires individual configuration is specified in
+a very simple YAML file in Kontemplate.
+
+An example file for a hypothetical test environment could look like this:
+
+```yaml
+---
+context: k8s.test.mydomain.com
+global:
+  clusterName: test-cluster
+  defaultReplicas: 2
+import:
+  - test-secrets.yaml
+include:
+  - name: gateway
+    path: tools/nginx
+    values:
+      tlsDomains:
+        - test.oslo.pub
+        - test.tazj.in
+  - path: backend
+    values:
+      env: test
+    include:
+      - name: blog
+        values:
+          url: test.tazj.in
+      - name: pub-service
+```
+
+<!-- markdown-toc start - Don't edit this section. Run M-x markdown-toc-refresh-toc -->
+**Table of Contents**
+
+- [Cluster configuration](#cluster-configuration)
+    - [Fields](#fields)
+        - [`context`](#context)
+        - [`global`](#global)
+        - [`import`](#import)
+        - [`include`](#include)
+    - [External variables](#external-variables)
+
+<!-- markdown-toc end -->
+
+## Fields
+
+This is documentation for the individual fields in a cluster context file.
+
+### `context`
+
+The `context` field contains the name of the kubectl-context. You can list context names with
+'kubectl config get-contexts'.
+
+This must be set here so that Kontemplate can use the correct context when calling kubectl.
+
+This field is **required** for `kubectl`-wrapping commands. It can be left out if only the `template`-command is used.
+
+### `global`
+
+The `global` field contains a key/value map of variables that should be available to all resource
+sets in the cluster.
+
+This field is **optional**.
+
+### `import`
+
+The `import` field contains the file names of additional YAML or JSON files from which global
+variables should be loaded. Using this field makes it possible to keep certain configuration that
+is the same for some, but not all, clusters in a common place.
+
+This field is **optional**.
+
+### `include`
+
+The `include` field contains the actual resource sets to be included in the cluster.
+
+Information about the structure of resource sets can be found in the [resource set documentation][].
+
+This field is **required**.
+
+## External variables
+
+As mentioned above, extra variables can be loaded from additional YAML or JSON files. Assuming you
+have a file called `test-secrets.yaml` which contains variables that should be shared between a `test`
+and `dev` cluster, you could import it in your context as such:
+
+```yaml
+# test-secrets.yaml:
+mySecretVar: foo-bar-12345
+
+# test-cluster.yaml:
+context: k8s.test.mydomain.com
+import:
+  - test-secrets.yaml
+
+# dev-cluster.yaml:
+context: k8s.dev.mydomain.com
+import:
+  - test-secrets.yaml
+```
+
+The variable `mySecretVar` is then available as a global variable.
+
+[resource set documentation]: resource-sets.md
diff --git a/ops/kontemplate/docs/resource-sets.md b/ops/kontemplate/docs/resource-sets.md
new file mode 100644
index 000000000000..1444dd491249
--- /dev/null
+++ b/ops/kontemplate/docs/resource-sets.md
@@ -0,0 +1,170 @@
+Resource Sets
+================
+
+Resource sets are collections of Kubernetes resources that should be passed to `kubectl` together.
+
+Technically a resource set is simply a folder with a few YAML and/or JSON templates in it.
+
+<!-- markdown-toc start - Don't edit this section. Run M-x markdown-toc-refresh-toc -->
+**Table of Contents**
+
+- [Resource Sets](#resource-sets)
+- [Creating resource sets](#creating-resource-sets)
+    - [Default variables](#default-variables)
+- [Including resource sets](#including-resource-sets)
+    - [Fields](#fields)
+        - [`name`](#name)
+        - [`path`](#path)
+        - [`values`](#values)
+        - [`args`](#args)
+        - [`include`](#include)
+    - [Multiple includes](#multiple-includes)
+    - [Nesting resource sets](#nesting-resource-sets)
+        - [Caveats](#caveats)
+
+<!-- markdown-toc end -->
+
+# Creating resource sets
+
+Simply create a folder in your Kontemplate repository and place a YAML or JSON file in it. These
+files get interpreted as [templates][] during Kontemplate runs and variables (as well as template
+logic or functions) will be interpolated.
+
+Refer to the template documentation for information on how to write templates.
+
+## Default variables
+
+Sometimes it is useful to specify default values for variables that should be interpolated during
+a run if the [cluster configuration][] does not specify a variable explicitly.
+
+This can be done simply by placing a `default.yaml` or `default.json` file in the resource set
+folder and filling it with key/value pairs of the intended default variables.
+
+Kontemplate will error during interpolation if any variables are left unspecified.
+
+# Including resource sets
+
+Under the cluster configuration `include` key resource sets are included and required variables
+are specified. For example:
+
+```yaml
+include:
+  - name: some-api
+    values:
+      version: 1.2-SNAPSHOT
+```
+
+This will include a resource set from a folder called `some-api` and set the specified `version` variable.
+
+## Fields
+
+The available fields when including a resource set are these:
+
+### `name`
+
+The `name` field contains the name of the resource set. This name can be used to refer to the resource set
+when specifying explicit includes or excludes during a run.
+
+By default it is assumed that the `name` is the path to the resource set folder, but this can be overridden.
+
+This field is **required**.
+
+### `path`
+
+The `path` field specifies an explicit path to a resource set folder in the case that it should differ from
+the resource set's `name`.
+
+This field is **optional**.
+
+### `values`
+
+The `values` field specifies key/values pairs of variables that should be available during templating.
+
+This field is **optional**.
+
+### `args`
+
+The `args` field specifies a list of arguments that should be passed to `kubectl`.
+
+This field is **optional**.
+
+### `include`
+
+The `include` field specifies additional resource sets that should be included and that should inherit the
+variables of this resource set.
+
+The fully qualified names of "nested" resource sets are set to `${PARENT_NAME}/${CHILD_NAME}` and paths are
+merged in the same way.
+
+This makes it easy to organise different resource sets as "groups" to include / exclude them collectively
+during runs.
+
+This field is **optional**.
+
+## Multiple includes
+
+Resource sets can be included multiple times with different configurations. In this case it is recommended
+to set the `path` and `name` fields explicitly. For example:
+
+```yaml
+include:
+  - name: forwarder-europe
+    path: tools/forwarder
+    values:
+      source: europe
+  - name: forwarder-asia
+    path: tools/forwarder
+    values:
+      source: asia
+```
+
+The two different configurations can be referred to by their set names, but will use the same resource
+templates with different configurations.
+
+## Nesting resource sets
+
+As mentioned above for the `include` field, resource sets can be nested. This lets users group resource
+sets in logical ways using simple folder structures.
+
+Assuming a folder structure like:
+
+```
+├── backend
+│   ├── auth-api
+│   ├── message-api
+│   └── order-api
+└── frontend
+    ├── app-page
+    └── login-page
+```
+
+With each of these folders being a resource set, they could be included in a cluster configuration like so:
+
+```yaml
+include:
+  - name: backend
+    include:
+      - name: auth-api
+      - name: message-api
+      - name: order-api
+  - name: frontend:
+    include:
+      - name: app-page
+      - name: login-page
+```
+
+Kontemplate could then be run with, for example, `--include backend` to only include the resource sets nested
+in the backend group. Specific resource sets can also be targeted, for example as `--include backend/order-api`.
+
+Variables specified in the parent resource set are inherited by the children.
+
+### Caveats
+
+Two caveats apply that users should be aware of:
+
+1. The parent resource set can not contain any resource templates itself.
+
+2. Only one level of nesting is supported. Specifying `include` again on a nested resource set will be ignored.
+
+[templates]: templates.md
+[cluster configuration]: cluster-config.md
diff --git a/ops/kontemplate/docs/templates.md b/ops/kontemplate/docs/templates.md
new file mode 100644
index 000000000000..32da205108de
--- /dev/null
+++ b/ops/kontemplate/docs/templates.md
@@ -0,0 +1,153 @@
+Kontemplate templates
+=====================
+
+The template file format is based on Go's [templating engine][] in combination
+with a small extension library called [sprig][] that adds additional template
+functions.
+
+Go templates can either simply display variables or build more complicated
+*pipelines* in which variables are passed to functions for further processing,
+or in which conditionals are evaluated for more complex template logic.
+
+It is recommended that you check out the Golang [documentation][] for the templating
+engine in addition to the cherry-picked features listed here.
+
+<!-- markdown-toc start - Don't edit this section. Run M-x markdown-toc-refresh-toc -->
+**Table of Contents**
+
+- [Kontemplate templates](#kontemplate-templates)
+    - [Basic variable interpolation](#basic-variable-interpolation)
+        - [Example:](#example)
+    - [Template functions](#template-functions)
+    - [Examples:](#examples)
+    - [Conditionals & ranges](#conditionals--ranges)
+    - [Caveats](#caveats)
+
+<!-- markdown-toc end -->
+
+## Basic variable interpolation
+
+The basic template format uses `{{ .variableName }}` as the interpolation format.
+
+### Example:
+
+Assuming that you include a resource set as such:
+
+```
+- name: api-gateway
+  values:
+    internalHost: http://my-internal-host/
+```
+
+And the api-gateway resource set includes a ConfigMap (some fields left out for
+the example):
+
+```
+# api-gateway/configmap.yaml:
+---
+kind: ConfigMap
+metadata:
+  name: api-gateway-config
+data:
+  internalHost: {{ .internalHost }}
+```
+
+The resulting output will be:
+
+```
+
+---
+kind: ConfigMap
+metadata:
+  name: api-gateway-config
+data:
+  internalHost: http://my-internal-host/
+```
+
+## Template functions
+
+Go templates support template functions which you can think of as a sort of
+shell-like pipeline where text flows through transformations from left to
+right.
+
+Some template functions come from Go's standard library and are listed in the
+[Go documentation][]. In addition the functions declared by [sprig][] are
+available in kontemplate, as well as five custom functions:
+
+* `json`: Encodes any supplied data structure as JSON.
+* `gitHEAD`: Retrieves the commit hash at Git `HEAD`.
+* `passLookup`: Looks up the supplied key in [pass][].
+* `insertFile`: Insert the contents of the given file in the resource
+  set folder as a string.
+* `insertTemplate`: Insert the contents of the given template in the resource
+  set folder as a string.
+
+## Examples:
+
+```
+# With the following values:
+name: Donald
+certKeyPath: my-website/cert-key
+
+# The following interpolations are possible:
+
+{{ .name | upper }}
+-> DONALD
+
+{{ .name | upper | repeat 2 }}
+-> DONALD DONALD
+
+{{ .certKeyPath | passLookup }}
+-> Returns content of 'my-website/cert-key' from pass
+
+{{ gitHEAD }}
+-> Returns the Git commit hash at HEAD.
+```
+
+## Conditionals & ranges
+
+Some logic is supported in Golang templates and can be used in Kontemplate, too.
+
+With the following values:
+
+```
+useKube2IAM: true
+servicePorts:
+  - 8080
+  - 9090
+```
+
+The following interpolations are possible:
+
+```
+# Conditionally insert something in the template:
+metadata:
+  annotations:
+    foo: bar
+    {{ if .useKube2IAM -}} iam.amazonaws.com/role: my-api {{- end }}
+```
+
+```
+# Iterate over a list of values
+ports:
+  {{ range .servicePorts }}
+  - port: {{ . }}
+  {{ end }}
+```
+
+Check out the Golang documentation (linked above) for more information about template logic.
+
+## Caveats
+
+Kontemplate does not by itself parse any of the content of the templates, which
+means that it does not validate whether the resources you supply are valid YAML
+or JSON.
+
+You can perform some validation by using `kontemplate apply --dry-run` which
+will make use of the Dry-Run functionality in `kubectl`.
+
+[templating engine]: https://golang.org/pkg/text/template/
+[documentation]: https://golang.org/pkg/text/template/
+[sprig]: http://masterminds.github.io/sprig/
+[Go documentation]: https://golang.org/pkg/text/template/#hdr-Functions
+[pass]: https://www.passwordstore.org/
diff --git a/ops/kontemplate/docs/tips-and-tricks.md b/ops/kontemplate/docs/tips-and-tricks.md
new file mode 100644
index 000000000000..5401ac91e5a3
--- /dev/null
+++ b/ops/kontemplate/docs/tips-and-tricks.md
@@ -0,0 +1,77 @@
+Kontemplate tips & tricks
+=========================
+
+<!-- markdown-toc start - Don't edit this section. Run M-x markdown-toc-refresh-toc -->
+**Table of Contents**
+
+- [Kontemplate tips & tricks](#kontemplate-tips--tricks)
+    - [Update Deployments when ConfigMaps change](#update-deployments-when-configmaps-change)
+    - [direnv & pass](#direnv--pass)
+
+<!-- markdown-toc end -->
+
+## Update Deployments when ConfigMaps change
+
+Kubernetes does [not currently][] have the ability to perform rolling updates
+of Deployments and other resource types when `ConfigMap` or `Secret` objects
+are updated.
+
+It is possible to make use of annotations and templating functions in
+Kontemplate to force updates to these resources anyways.
+ 
+For example:
+
+```yaml
+# A ConfigMap that contains some configuration for your app
+---
+kind: ConfigMap
+metadata:
+  name: app-config
+data:
+  app.conf: |
+    name: {{ .appName }}
+    foo: bar
+```
+
+Now whenever the `appName` variable changes or we make an edit to the
+`ConfigMap` we would like to update the `Deployment` making use of it, too. We
+can do this by adding a hash of the parsed template to the annotations of the
+created `Pod` objects:
+
+```yaml
+
+---
+kind: Deployment
+metadata:
+  name: app
+spec:
+  template:
+    metadata:
+      annotations:
+        configHash: {{ insertTemplate "app-config.yaml" | sha256sum }}
+    spec:
+      containers:
+        - name: app
+          # Some details omitted ... 
+          volumeMounts:
+            - name: config
+              mountPath: /etc/app/
+      volumes:
+        - name: config
+          configMap:
+            name: app-config
+```
+
+Now any change to the `ConfigMap` - either by directly editing the yaml file or
+via a changed template variable - will cause the annotation to change,
+triggering a rolling update of all relevant pods.
+
+## direnv & pass
+
+Users of `pass` may have multiple different password stores on their machines.
+Assuming that `kontemplate` configuration exists somewhere on the filesystem
+per project, it is easy to use [direnv][] to switch to the correct
+`PASSWORD_STORE_DIR` variable when entering the folder.
+
+[not currently]: https://github.com/kubernetes/kubernetes/issues/22368
+[direnv]: https://direnv.net/
diff --git a/ops/kontemplate/example/other-config.yaml b/ops/kontemplate/example/other-config.yaml
new file mode 100644
index 000000000000..87370569c46e
--- /dev/null
+++ b/ops/kontemplate/example/other-config.yaml
@@ -0,0 +1,7 @@
+---
+apiVersion: extensions/v1beta1
+kind: ConfigMap
+metadata:
+  name: other-config
+data:
+  globalData: {{ .globalVar }}
diff --git a/ops/kontemplate/example/prod-cluster.json b/ops/kontemplate/example/prod-cluster.json
new file mode 100644
index 000000000000..70e2365f1710
--- /dev/null
+++ b/ops/kontemplate/example/prod-cluster.json
@@ -0,0 +1,16 @@
+{
+  "context": "k8s.prod.mydomain.com",
+  "global": {
+    "globalVar": "lizards"
+  },
+  "include": [
+    {
+      "name": "some-api",
+      "values": {
+        "version": "1.0-SNAPSHOT-0e6884d",
+        "importantFeature": true,
+        "apiPort": 4567
+      }
+    }
+  ]
+}
diff --git a/ops/kontemplate/example/prod-cluster.yaml b/ops/kontemplate/example/prod-cluster.yaml
new file mode 100644
index 000000000000..9f300a492080
--- /dev/null
+++ b/ops/kontemplate/example/prod-cluster.yaml
@@ -0,0 +1,17 @@
+---
+context: k8s.prod.mydomain.com
+global:
+  globalVar: lizards
+include:
+  # By default resource sets are included from a folder with the same
+  # name as the resource set's name
+  - name: some-api
+    values:
+      version: 1.0-0e6884d
+      importantFeature: true
+      apiPort: 4567
+
+  # Paths can also be specified manually (and point at single template
+  # files!)
+  - name: other-config
+    path: other-config.yaml
diff --git a/ops/kontemplate/example/some-api/some-api.yaml b/ops/kontemplate/example/some-api/some-api.yaml
new file mode 100644
index 000000000000..f0188f9dbde3
--- /dev/null
+++ b/ops/kontemplate/example/some-api/some-api.yaml
@@ -0,0 +1,52 @@
+---
+apiVersion: v1
+kind: Secret
+metadata:
+  name: secret-certificate
+data:
+  cert.pem: {{ passLookup "my/secret/certificate" | b64enc }}
+---
+apiVersion: extensions/v1beta1
+kind: ConfigMap
+metadata:
+  name: some-config
+data:
+  # The content of the example configuration file is templated in here
+  # by the 'insertFile' function and indented for YAML-compatibility
+  # with the 'indent' function:
+  some.cfg: |
+{{ insertFile "some.cfg" | indent 4 }}
+---
+apiVersion: extensions/v1beta1
+kind: Deployment
+metadata:
+  name: some-api
+spec:
+  replicas: 1
+  template:
+    metadata:
+      labels:
+        app: some-api
+    spec:
+      containers:
+        - image: my.container.repo/some-api:{{ .version }}
+          name: some-api
+          env:
+            - name: ENABLE_IMPORTANT_FEATURE
+              value: {{ .importantFeature }}
+            - name: SOME_GLOBAL_VAR
+              value: {{ .globalVar }}
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: some-api
+  labels:
+    app: some-api
+spec:
+  selector:
+    app: some-api
+  ports:
+    - port: 80
+      targetPort: {{ .apiPort }}
+      name: http
diff --git a/ops/kontemplate/example/some-api/some.cfg b/ops/kontemplate/example/some-api/some.cfg
new file mode 100644
index 000000000000..733d5e16789a
--- /dev/null
+++ b/ops/kontemplate/example/some-api/some.cfg
@@ -0,0 +1,4 @@
+{
+  "something": 1542,
+  "other-thing": "да"
+}
diff --git a/ops/kontemplate/image/Dockerfile b/ops/kontemplate/image/Dockerfile
new file mode 100644
index 000000000000..a40fa83b0867
--- /dev/null
+++ b/ops/kontemplate/image/Dockerfile
@@ -0,0 +1,15 @@
+FROM alpine:3.10
+
+ADD hashes /root/hashes
+ADD https://storage.googleapis.com/kubernetes-release/release/v1.15.3/bin/linux/amd64/kubectl /usr/bin/kubectl
+ADD https://github.com/tazjin/kontemplate/releases/download/v1.8.0/kontemplate-1.8.0-6c3b299-linux-amd64.tar.gz /tmp/kontemplate.tar.gz
+
+# Pass release version is 1.7.3
+ADD https://raw.githubusercontent.com/zx2c4/password-store/74fdfb5022f317ad48d449e29543710bdad1afda/src/password-store.sh /usr/bin/pass
+
+RUN sha256sum -c /root/hashes && \
+    apk add -U bash tree gnupg git && \
+    chmod +x /usr/bin/kubectl /usr/bin/pass && \
+    tar xzvf /tmp/kontemplate.tar.gz && \
+    mv kontemplate /usr/bin/kontemplate && \
+    /usr/bin/kontemplate version
diff --git a/ops/kontemplate/image/README.md b/ops/kontemplate/image/README.md
new file mode 100644
index 000000000000..fe04765401be
--- /dev/null
+++ b/ops/kontemplate/image/README.md
@@ -0,0 +1,12 @@
+Kontemplate Docker image
+========================
+
+This builds a simple Docker image available on the Docker Hub as `tazjin/kontemplate`.
+
+Builds are automated based on the Dockerfile contained here.
+
+It contains both `kontemplate` and `kubectl` and can be used as part of container-based
+CI pipelines.
+
+`pass` and its dependencies are also installed to enable the use of the `passLookup`
+template function if desired.
diff --git a/ops/kontemplate/image/hashes b/ops/kontemplate/image/hashes
new file mode 100644
index 000000000000..bfd87c0201e7
--- /dev/null
+++ b/ops/kontemplate/image/hashes
@@ -0,0 +1,2 @@
+a39dfdd77e4655acaabe301285cf389cb5fc8145060f5677dc93db1cc20911a4  /tmp/kontemplate.tar.gz
+6e805054a1fb2280abb53f75b57a1b92bf9c66ffe0d2cdcd46e81b079d93c322  /usr/bin/kubectl
diff --git a/ops/kontemplate/main.go b/ops/kontemplate/main.go
new file mode 100644
index 000000000000..e55d42465c6b
--- /dev/null
+++ b/ops/kontemplate/main.go
@@ -0,0 +1,242 @@
+// Copyright (C) 2016-2019  Vincent Ambo <mail@tazj.in>
+//
+// Kontemplate is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+package main
+
+import (
+	"fmt"
+	"os"
+	"os/exec"
+	"strings"
+
+	"github.com/tazjin/kontemplate/context"
+	"github.com/tazjin/kontemplate/templater"
+	"gopkg.in/alecthomas/kingpin.v2"
+)
+
+const version string = "1.8.0"
+
+// This variable will be initialised by the Go linker during the builder
+var gitHash string
+
+var (
+	app = kingpin.New("kontemplate", "simple Kubernetes resource templating")
+
+	// Global flags
+	includes   = app.Flag("include", "Resource sets to include explicitly").Short('i').Strings()
+	excludes   = app.Flag("exclude", "Resource sets to exclude explicitly").Short('e').Strings()
+	variables  = app.Flag("var", "Provide variables to templates explicitly").Strings()
+	kubectlBin = app.Flag("kubectl", "Path to the kubectl binary (default 'kubectl')").Default("kubectl").String()
+
+	// Commands
+	template          = app.Command("template", "Template resource sets and print them")
+	templateFile      = template.Arg("file", "Cluster configuration file to use").Required().String()
+	templateOutputDir = template.Flag("output", "Output directory in which to save templated files instead of printing them").Short('o').String()
+
+	apply       = app.Command("apply", "Template resources and pass to 'kubectl apply'")
+	applyFile   = apply.Arg("file", "Cluster configuration file to use").Required().String()
+	applyDryRun = apply.Flag("dry-run", "Print remote operations without executing them").Default("false").Bool()
+
+	replace     = app.Command("replace", "Template resources and pass to 'kubectl replace'")
+	replaceFile = replace.Arg("file", "Cluster configuration file to use").Required().String()
+
+	delete     = app.Command("delete", "Template resources and pass to 'kubectl delete'")
+	deleteFile = delete.Arg("file", "Cluster configuration file to use").Required().String()
+
+	create     = app.Command("create", "Template resources and pass to 'kubectl create'")
+	createFile = create.Arg("file", "Cluster configuration file to use").Required().String()
+
+	versionCmd = app.Command("version", "Show kontemplate version")
+)
+
+func main() {
+	app.HelpFlag.Short('h')
+
+	switch kingpin.MustParse(app.Parse(os.Args[1:])) {
+	case template.FullCommand():
+		templateCommand()
+
+	case apply.FullCommand():
+		applyCommand()
+
+	case replace.FullCommand():
+		replaceCommand()
+
+	case delete.FullCommand():
+		deleteCommand()
+
+	case create.FullCommand():
+		createCommand()
+
+	case versionCmd.FullCommand():
+		versionCommand()
+	}
+}
+
+func versionCommand() {
+	if gitHash == "" {
+		fmt.Printf("Kontemplate version %s (git commit unknown)\n", version)
+	} else {
+		fmt.Printf("Kontemplate version %s (git commit: %s)\n", version, gitHash)
+	}
+}
+
+func templateCommand() {
+	_, resourceSets := loadContextAndResources(templateFile)
+
+	for _, rs := range *resourceSets {
+		if len(rs.Resources) == 0 {
+			fmt.Fprintf(os.Stderr, "Warning: Resource set '%s' does not exist or contains no valid templates\n", rs.Name)
+			continue
+		}
+
+		if *templateOutputDir != "" {
+			templateIntoDirectory(templateOutputDir, rs)
+		} else {
+			for _, r := range rs.Resources {
+				fmt.Fprintf(os.Stderr, "Rendered file %s/%s:\n", rs.Name, r.Filename)
+				fmt.Println(r.Rendered)
+			}
+		}
+	}
+}
+
+func templateIntoDirectory(outputDir *string, rs templater.RenderedResourceSet) {
+	// Attempt to create the output directory if it does not
+	// already exist:
+	if err := os.MkdirAll(*templateOutputDir, 0775); err != nil {
+		app.Fatalf("Could not create output directory: %v\n", err)
+	}
+
+	// Nested resource sets may contain slashes in their names.
+	// These are replaced with dashes for the purpose of writing a
+	// flat list of output files:
+	setName := strings.Replace(rs.Name, "/", "-", -1)
+
+	for _, r := range rs.Resources {
+		filename := fmt.Sprintf("%s/%s-%s", *templateOutputDir, setName, r.Filename)
+		fmt.Fprintf(os.Stderr, "Writing file %s\n", filename)
+
+		file, err := os.Create(filename)
+		if err != nil {
+			app.Fatalf("Could not create file %s: %v\n", filename, err)
+		}
+
+		_, err = fmt.Fprintf(file, r.Rendered)
+		if err != nil {
+			app.Fatalf("Error writing file %s: %v\n", filename, err)
+		}
+	}
+}
+
+func applyCommand() {
+	ctx, resources := loadContextAndResources(applyFile)
+
+	var kubectlArgs []string
+
+	if *applyDryRun {
+		kubectlArgs = []string{"apply", "-f", "-", "--dry-run"}
+	} else {
+		kubectlArgs = []string{"apply", "-f", "-"}
+	}
+
+	if err := runKubectlWithResources(ctx, &kubectlArgs, resources); err != nil {
+		failWithKubectlError(err)
+	}
+}
+
+func replaceCommand() {
+	ctx, resources := loadContextAndResources(replaceFile)
+	args := []string{"replace", "--save-config=true", "-f", "-"}
+
+	if err := runKubectlWithResources(ctx, &args, resources); err != nil {
+		failWithKubectlError(err)
+	}
+}
+
+func deleteCommand() {
+	ctx, resources := loadContextAndResources(deleteFile)
+	args := []string{"delete", "-f", "-"}
+
+	if err := runKubectlWithResources(ctx, &args, resources); err != nil {
+		failWithKubectlError(err)
+	}
+}
+
+func createCommand() {
+	ctx, resources := loadContextAndResources(createFile)
+	args := []string{"create", "--save-config=true", "-f", "-"}
+
+	if err := runKubectlWithResources(ctx, &args, resources); err != nil {
+		failWithKubectlError(err)
+	}
+}
+
+func loadContextAndResources(file *string) (*context.Context, *[]templater.RenderedResourceSet) {
+	ctx, err := context.LoadContext(*file, variables)
+	if err != nil {
+		app.Fatalf("Error loading context: %v\n", err)
+	}
+
+	resources, err := templater.LoadAndApplyTemplates(includes, excludes, ctx)
+	if err != nil {
+		app.Fatalf("Error templating resource sets: %v\n", err)
+	}
+
+	return ctx, &resources
+}
+
+func runKubectlWithResources(c *context.Context, kubectlArgs *[]string, resourceSets *[]templater.RenderedResourceSet) error {
+	argsWithContext := append(*kubectlArgs, fmt.Sprintf("--context=%s", c.Name))
+
+	for _, rs := range *resourceSets {
+		if len(rs.Resources) == 0 {
+			fmt.Fprintf(os.Stderr, "Warning: Resource set '%s' contains no valid templates\n", rs.Name)
+			continue
+		}
+
+		argsWithResourceSetArgs := append(argsWithContext, rs.Args...)
+
+		kubectl := exec.Command(*kubectlBin, argsWithResourceSetArgs...)
+
+		stdin, err := kubectl.StdinPipe()
+		if err != nil {
+			return fmt.Errorf("kubectl error: %v", err)
+		}
+
+		kubectl.Stdout = os.Stdout
+		kubectl.Stderr = os.Stderr
+
+		if err = kubectl.Start(); err != nil {
+			return fmt.Errorf("kubectl error: %v", err)
+		}
+
+		for _, r := range rs.Resources {
+			fmt.Printf("Passing file %s/%s to kubectl\n", rs.Name, r.Filename)
+			fmt.Fprintln(stdin, r.Rendered)
+		}
+		stdin.Close()
+
+		if err = kubectl.Wait(); err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+func failWithKubectlError(err error) {
+	app.Fatalf("Kubectl error: %v\n", err)
+}
diff --git a/ops/kontemplate/release.nix b/ops/kontemplate/release.nix
new file mode 100644
index 000000000000..8a04109526cf
--- /dev/null
+++ b/ops/kontemplate/release.nix
@@ -0,0 +1,54 @@
+# Copyright (C) 2016-2019  Vincent Ambo <mail@tazj.in>
+#
+# This file is part of Kontemplate.
+#
+# Kontemplate is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This file is the Nix derivation used to build release binaries for
+# several different architectures and operating systems.
+
+let pkgs = import ((import <nixpkgs> {}).fetchFromGitHub {
+  owner = "NixOS";
+  repo = "nixpkgs-channels";
+  rev = "541d9cce8af7a490fb9085305939569567cb58e6";
+  sha256 = "0jgz72hhzkd5vyq5v69vpljjlnf0lqaz7fh327bvb3cvmwbfxrja";
+}) {};
+in with pkgs; buildGoPackage rec {
+  name = "kontemplate-${version}";
+  version = "canon";
+  src = ./.;
+  goPackagePath = "github.com/tazjin/kontemplate";
+  goDeps = ./deps.nix;
+
+  # This configuration enables the building of statically linked
+  # executables. For some reason, those will have multiple references
+  # to the Go compiler's installation path in them, which is the
+  # reason for setting the 'allowGoReference' flag.
+  dontStrip = true; # Linker configuration handles stripping
+  allowGoReference = true;
+  CGO_ENABLED="0";
+  GOCACHE="off";
+
+  # Configure release builds via the "build-matrix" script:
+  buildInputs = [ git ];
+  buildPhase = ''
+    cd go/src/${goPackagePath}
+    patchShebangs build-release.sh
+    ./build-release.sh build
+  '';
+
+  outputs = [ "out" ];
+  installPhase = ''
+    mkdir $out
+    cp -r release/ $out
+  '';
+
+  meta = with lib; {
+    description = "A resource templating helper for Kubernetes";
+    homepage = "http://kontemplate.works/";
+    license = licenses.gpl3;
+  };
+}
diff --git a/ops/kontemplate/templater/dns.go b/ops/kontemplate/templater/dns.go
new file mode 100644
index 000000000000..6cd974dd9324
--- /dev/null
+++ b/ops/kontemplate/templater/dns.go
@@ -0,0 +1,35 @@
+// Copyright (C) 2016-2019  Vincent Ambo <mail@tazj.in>
+//
+// This file is part of Kontemplate.
+//
+// Kontemplate is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This file contains the implementation of a template function for retrieving
+// IP addresses from DNS
+
+package templater
+
+import (
+	"fmt"
+	"net"
+	"os"
+)
+
+func GetIPsFromDNS(host string) ([]interface{}, error) {
+	fmt.Fprintf(os.Stderr, "Attempting to look up IP for %s in DNS\n", host)
+	ips, err := net.LookupIP(host)
+
+	if err != nil {
+		return nil, fmt.Errorf("IP address lookup failed: %v", err)
+	}
+
+	var result []interface{} = make([]interface{}, len(ips))
+	for i, ip := range ips {
+		result[i] = ip
+	}
+
+	return result, nil
+}
diff --git a/ops/kontemplate/templater/pass.go b/ops/kontemplate/templater/pass.go
new file mode 100644
index 000000000000..f7fbcb433de5
--- /dev/null
+++ b/ops/kontemplate/templater/pass.go
@@ -0,0 +1,34 @@
+// Copyright (C) 2016-2019  Vincent Ambo <mail@tazj.in>
+//
+// This file is part of Kontemplate.
+//
+// Kontemplate is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This file contains the implementation of a template function for retrieving
+// variables from 'pass', the standard UNIX password manager.
+
+package templater
+
+import (
+	"fmt"
+	"os"
+	"os/exec"
+	"strings"
+)
+
+func GetFromPass(key string) (string, error) {
+	fmt.Fprintf(os.Stderr, "Attempting to look up %s in pass\n", key)
+	pass := exec.Command("pass", "show", key)
+
+	output, err := pass.CombinedOutput()
+	if err != nil {
+		return "", fmt.Errorf("Pass lookup failed: %s (%v)", output, err)
+	}
+
+	trimmed := strings.TrimSpace(string(output))
+
+	return trimmed, nil
+}
diff --git a/ops/kontemplate/templater/templater.go b/ops/kontemplate/templater/templater.go
new file mode 100644
index 000000000000..a8f0c670a603
--- /dev/null
+++ b/ops/kontemplate/templater/templater.go
@@ -0,0 +1,236 @@
+// Copyright (C) 2016-2019  Vincent Ambo <mail@tazj.in>
+//
+// This file is part of Kontemplate.
+//
+// Kontemplate is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+package templater
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"io/ioutil"
+	"os"
+	"os/exec"
+	"path"
+	"strings"
+	"text/template"
+
+	"github.com/Masterminds/sprig"
+	"github.com/tazjin/kontemplate/context"
+	"github.com/tazjin/kontemplate/util"
+)
+
+const failOnMissingKeys string = "missingkey=error"
+
+type RenderedResource struct {
+	Filename string
+	Rendered string
+}
+
+type RenderedResourceSet struct {
+	Name      string
+	Resources []RenderedResource
+	Args      []string
+}
+
+func LoadAndApplyTemplates(include *[]string, exclude *[]string, c *context.Context) ([]RenderedResourceSet, error) {
+	limitedResourceSets := applyLimits(&c.ResourceSets, include, exclude)
+	renderedResourceSets := make([]RenderedResourceSet, 0)
+
+	if len(*limitedResourceSets) == 0 {
+		return renderedResourceSets, fmt.Errorf("No valid resource sets included!")
+	}
+
+	for _, rs := range *limitedResourceSets {
+		set, err := processResourceSet(c, &rs)
+
+		if err != nil {
+			return nil, err
+		}
+
+		renderedResourceSets = append(renderedResourceSets, *set)
+	}
+
+	return renderedResourceSets, nil
+}
+
+func processResourceSet(ctx *context.Context, rs *context.ResourceSet) (*RenderedResourceSet, error) {
+	fmt.Fprintf(os.Stderr, "Loading resources for %s\n", rs.Name)
+
+	fileInfo, err := os.Stat(rs.Path)
+	if err != nil {
+		return nil, err
+	}
+
+	var files []os.FileInfo
+	var resources []RenderedResource
+
+	// Treat single-file resource paths separately from resource
+	// sets containing multiple templates
+	if fileInfo.IsDir() {
+		// Explicitly discard this error, which will give us an empty
+		// list of files instead.
+		// This will end up printing a warning to the user, but it
+		// won't stop the rest of the process.
+		files, _ = ioutil.ReadDir(rs.Path)
+		resources, err = processFiles(ctx, rs, files)
+		if err != nil {
+			return nil, err
+		}
+	} else {
+		resource, err := templateFile(ctx, rs, rs.Path)
+		if err != nil {
+			return nil, err
+		}
+
+		resources = []RenderedResource{resource}
+	}
+
+	return &RenderedResourceSet{
+		Name:      rs.Name,
+		Resources: resources,
+		Args:      rs.Args,
+	}, nil
+}
+
+func processFiles(ctx *context.Context, rs *context.ResourceSet, files []os.FileInfo) ([]RenderedResource, error) {
+	resources := make([]RenderedResource, 0)
+
+	for _, file := range files {
+		if !file.IsDir() && isResourceFile(file) {
+			path := path.Join(rs.Path, file.Name())
+			res, err := templateFile(ctx, rs, path)
+
+			if err != nil {
+				return resources, err
+			}
+
+			resources = append(resources, res)
+		}
+	}
+
+	return resources, nil
+}
+
+func templateFile(ctx *context.Context, rs *context.ResourceSet, filepath string) (RenderedResource, error) {
+	var resource RenderedResource
+
+	tpl, err := template.New(path.Base(filepath)).Funcs(templateFuncs(ctx, rs)).Option(failOnMissingKeys).ParseFiles(filepath)
+	if err != nil {
+		return resource, fmt.Errorf("Could not load template %s: %v", filepath, err)
+	}
+
+	var b bytes.Buffer
+	err = tpl.Execute(&b, rs.Values)
+	if err != nil {
+		return resource, fmt.Errorf("Error while templating %s: %v", filepath, err)
+	}
+
+	resource = RenderedResource{
+		Filename: path.Base(filepath),
+		Rendered: b.String(),
+	}
+
+	return resource, nil
+}
+
+// Applies the limits of explicitly included or excluded resources and returns the updated resource set.
+// Exclude takes priority over include
+func applyLimits(rs *[]context.ResourceSet, include *[]string, exclude *[]string) *[]context.ResourceSet {
+	if len(*include) == 0 && len(*exclude) == 0 {
+		return rs
+	}
+
+	// Exclude excluded resource sets
+	excluded := make([]context.ResourceSet, 0)
+	for _, r := range *rs {
+		if !matchesResourceSet(exclude, &r) {
+			excluded = append(excluded, r)
+		}
+	}
+
+	// Include included resource sets
+	if len(*include) == 0 {
+		return &excluded
+	}
+	included := make([]context.ResourceSet, 0)
+	for _, r := range excluded {
+		if matchesResourceSet(include, &r) {
+			included = append(included, r)
+		}
+	}
+
+	return &included
+}
+
+// Check whether an include/exclude string slice matches a resource set
+func matchesResourceSet(s *[]string, rs *context.ResourceSet) bool {
+	for _, r := range *s {
+		r = strings.TrimSuffix(r, "/")
+		if r == rs.Name || r == rs.Parent {
+			return true
+		}
+	}
+
+	return false
+}
+
+func templateFuncs(c *context.Context, rs *context.ResourceSet) template.FuncMap {
+	m := sprig.TxtFuncMap()
+	m["json"] = func(data interface{}) string {
+		b, _ := json.Marshal(data)
+		return string(b)
+	}
+	m["passLookup"] = GetFromPass
+	m["gitHEAD"] = func() (string, error) {
+		out, err := exec.Command("git", "-C", c.BaseDir, "rev-parse", "HEAD").Output()
+		if err != nil {
+			return "", err
+		}
+		output := strings.TrimSpace(string(out))
+		return output, nil
+	}
+	m["lookupIPAddr"] = GetIPsFromDNS
+	m["insertFile"] = func(file string) (string, error) {
+		data, err := ioutil.ReadFile(path.Join(rs.Path, file))
+		if err != nil {
+			return "", err
+		}
+
+		return string(data), nil
+	}
+	m["insertTemplate"] = func(file string) (string, error) {
+		data, err := templateFile(c, rs, path.Join(rs.Path, file))
+		if err != nil {
+			return "", err
+		}
+
+		return data.Rendered, nil
+	}
+	m["default"] = func(defaultVal interface{}, varName string) interface{} {
+		if val, ok := rs.Values[varName]; ok {
+			return val
+		}
+
+		return defaultVal
+	}
+	return m
+}
+
+// Checks whether a file is a resource file (i.e. is YAML or JSON) and not a default values file.
+func isResourceFile(f os.FileInfo) bool {
+	for _, defaultFile := range util.DefaultFilenames {
+		if f.Name() == defaultFile {
+			return false
+		}
+	}
+
+	return strings.HasSuffix(f.Name(), "yaml") ||
+		strings.HasSuffix(f.Name(), "yml") ||
+		strings.HasSuffix(f.Name(), "json")
+}
diff --git a/ops/kontemplate/templater/templater_test.go b/ops/kontemplate/templater/templater_test.go
new file mode 100644
index 000000000000..c20858c203b4
--- /dev/null
+++ b/ops/kontemplate/templater/templater_test.go
@@ -0,0 +1,205 @@
+// Copyright (C) 2016-2019  Vincent Ambo <mail@tazj.in>
+//
+// This file is part of Kontemplate.
+//
+// Kontemplate is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+package templater
+
+import (
+	"github.com/tazjin/kontemplate/context"
+	"reflect"
+	"strings"
+	"testing"
+)
+
+func TestApplyNoLimits(t *testing.T) {
+	resources := []context.ResourceSet{
+		{
+			Name: "testResourceSet1",
+		},
+		{
+			Name: "testResourceSet2",
+		},
+	}
+
+	result := applyLimits(&resources, &[]string{}, &[]string{})
+
+	if !reflect.DeepEqual(resources, *result) {
+		t.Error("Resource set slice changed, but shouldn't have.")
+		t.Errorf("Expected: %v\nResult: %v\n", resources, *result)
+		t.Fail()
+	}
+}
+
+func TestApplyIncludeLimits(t *testing.T) {
+	resources := []context.ResourceSet{
+		{
+			Name: "testResourceSet1",
+		},
+		{
+			Name: "testResourceSet2",
+		},
+		{
+			Name:   "testResourceSet3",
+			Parent: "included",
+		},
+	}
+
+	includes := []string{"testResourceSet1", "included"}
+
+	result := applyLimits(&resources, &includes, &[]string{})
+
+	expected := []context.ResourceSet{
+		{
+			Name: "testResourceSet1",
+		},
+		{
+			Name:   "testResourceSet3",
+			Parent: "included",
+		},
+	}
+
+	if !reflect.DeepEqual(expected, *result) {
+		t.Error("Result does not contain expected resource sets.")
+		t.Errorf("Expected: %v\nResult: %v\n", expected, *result)
+		t.Fail()
+	}
+}
+
+func TestApplyExcludeLimits(t *testing.T) {
+	resources := []context.ResourceSet{
+		{
+			Name: "testResourceSet1",
+		},
+		{
+			Name: "testResourceSet2",
+		},
+		{
+			Name:   "testResourceSet3",
+			Parent: "included",
+		},
+	}
+
+	exclude := []string{"testResourceSet2"}
+
+	result := applyLimits(&resources, &[]string{}, &exclude)
+
+	expected := []context.ResourceSet{
+		{
+			Name: "testResourceSet1",
+		},
+		{
+			Name:   "testResourceSet3",
+			Parent: "included",
+		},
+	}
+
+	if !reflect.DeepEqual(expected, *result) {
+		t.Error("Result does not contain expected resource sets.")
+		t.Errorf("Expected: %v\nResult: %v\n", expected, *result)
+		t.Fail()
+	}
+}
+
+func TestApplyLimitsExcludeIncludePrecedence(t *testing.T) {
+	resources := []context.ResourceSet{
+		{
+			Name:   "collection/nested1",
+			Parent: "collection",
+		},
+		{
+			Name:   "collection/nested2",
+			Parent: "collection",
+		},
+		{
+			Name:   "collection/nested3",
+			Parent: "collection",
+		},
+		{
+			Name: "something-else",
+		},
+	}
+
+	include := []string{"collection"}
+	exclude := []string{"collection/nested2"}
+
+	result := applyLimits(&resources, &include, &exclude)
+
+	expected := []context.ResourceSet{
+		{
+			Name:   "collection/nested1",
+			Parent: "collection",
+		},
+		{
+			Name:   "collection/nested3",
+			Parent: "collection",
+		},
+	}
+
+	if !reflect.DeepEqual(expected, *result) {
+		t.Error("Result does not contain expected resource sets.")
+		t.Errorf("Expected: %v\nResult: %v\n", expected, *result)
+		t.Fail()
+	}
+}
+
+func TestFailOnMissingKeys(t *testing.T) {
+	ctx := context.Context{}
+	resourceSet := context.ResourceSet{}
+
+	_, err := templateFile(&ctx, &resourceSet, "testdata/test-template.txt")
+
+	if err == nil {
+		t.Errorf("Template with missing keys should have failed.\n")
+		t.Fail()
+	}
+
+	if !strings.Contains(err.Error(), "map has no entry for key \"testName\"") {
+		t.Errorf("Templating failed with unexpected error: %v\n", err)
+	}
+}
+
+func TestDefaultTemplateFunction(t *testing.T) {
+	ctx := context.Context{}
+	resourceSet := context.ResourceSet{}
+
+	res, err := templateFile(&ctx, &resourceSet, "testdata/test-default.txt")
+
+	if err != nil {
+		t.Errorf("Templating with default values should have succeeded.\n")
+		t.Fail()
+	}
+
+	if res.Rendered != "defaultValue\n" {
+		t.Error("Result does not contain expected rendered default value.")
+		t.Fail()
+	}
+}
+
+func TestInsertTemplateFunction(t *testing.T) {
+	ctx := context.Context{}
+	resourceSet := context.ResourceSet{
+		Path: "testdata",
+		Values: map[string]interface{}{
+			"testName":        "TestInsertTemplateFunction",
+		},
+	}
+
+	res, err := templateFile(&ctx, &resourceSet, "testdata/test-insertTemplate.txt")
+
+	if err != nil {
+		t.Error(err)
+		t.Errorf("Templating with an insertTemplate call should have succeeded.\n")
+		t.Fail()
+	}
+
+	if res.Rendered != "Inserting \"Template for test TestInsertTemplateFunction\".\n" {
+		t.Error("Result does not contain expected rendered template value.")
+		t.Error(res.Rendered)
+		t.Fail()
+	}
+}
diff --git a/ops/kontemplate/templater/testdata/test-default.txt b/ops/kontemplate/templater/testdata/test-default.txt
new file mode 100644
index 000000000000..4f7997bd69d2
--- /dev/null
+++ b/ops/kontemplate/templater/testdata/test-default.txt
@@ -0,0 +1 @@
+{{ default "defaultValue" "missingVar" }}
diff --git a/ops/kontemplate/templater/testdata/test-insertTemplate.txt b/ops/kontemplate/templater/testdata/test-insertTemplate.txt
new file mode 100644
index 000000000000..8155e174fe00
--- /dev/null
+++ b/ops/kontemplate/templater/testdata/test-insertTemplate.txt
@@ -0,0 +1 @@
+Inserting "{{ insertTemplate "test-template.txt" | trim }}".
diff --git a/ops/kontemplate/templater/testdata/test-template.txt b/ops/kontemplate/templater/testdata/test-template.txt
new file mode 100644
index 000000000000..06f1cfc630c3
--- /dev/null
+++ b/ops/kontemplate/templater/testdata/test-template.txt
@@ -0,0 +1 @@
+Template for test {{ .testName }}
diff --git a/ops/kontemplate/util/util.go b/ops/kontemplate/util/util.go
new file mode 100644
index 000000000000..56fa1e3fc9c5
--- /dev/null
+++ b/ops/kontemplate/util/util.go
@@ -0,0 +1,58 @@
+// Copyright (C) 2016-2019  Vincent Ambo <mail@tazj.in>
+//
+// This file is part of Kontemplate.
+//
+// Kontemplate is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+package util
+
+import (
+	"io/ioutil"
+
+	"github.com/ghodss/yaml"
+)
+
+// Filenames excluded from templating for the purpose of containing default variable values inside a resource set.
+var DefaultFilenames []string = []string{"default.yml", "default.yaml", "default.json"}
+
+// Merges two maps together. Values from the second map override values in the first map.
+// The returned map is new if anything was changed.
+func Merge(in1 *map[string]interface{}, in2 *map[string]interface{}) *map[string]interface{} {
+	if in1 == nil || len(*in1) == 0 {
+		return in2
+	}
+
+	if in2 == nil || len(*in2) == 0 {
+		return in1
+	}
+
+	new := make(map[string]interface{})
+	for k, v := range *in1 {
+		new[k] = v
+	}
+
+	for k, v := range *in2 {
+		new[k] = v
+	}
+
+	return &new
+}
+
+// Loads either a YAML or JSON file from the specified path and
+// deserialises it into the provided interface.
+func LoadData(filename string, addr interface{}) error {
+	file, err := ioutil.ReadFile(filename)
+	if err != nil {
+		return err
+	}
+
+	err = yaml.Unmarshal(file, addr)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
diff --git a/ops/kontemplate/util/util_test.go b/ops/kontemplate/util/util_test.go
new file mode 100644
index 000000000000..53c56081758c
--- /dev/null
+++ b/ops/kontemplate/util/util_test.go
@@ -0,0 +1,83 @@
+// Copyright (C) 2016-2019  Vincent Ambo <mail@tazj.in>
+//
+// This file is part of Kontemplate.
+//
+// Kontemplate is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+package util
+
+import (
+	"reflect"
+	"testing"
+)
+
+func TestMergeWithEmptyMap(t *testing.T) {
+	testMap := map[string]interface{}{
+		"foo": "bar",
+	}
+
+	empty := make(map[string]interface{})
+
+	res1 := Merge(&testMap, &empty)
+	res2 := Merge(&empty, &testMap)
+
+	if res1 != &testMap || res2 != &testMap {
+		t.Error("A new map was returned incorrectly.")
+		t.Fail()
+	}
+}
+
+func TestMergeWithNilMap(t *testing.T) {
+	testMap := map[string]interface{}{
+		"foo": "bar",
+	}
+
+	res1 := Merge(&testMap, nil)
+	res2 := Merge(nil, &testMap)
+
+	if res1 != &testMap || res2 != &testMap {
+		t.Error("A new map was returned incorrectly.")
+		t.Fail()
+	}
+}
+
+func TestMergeMaps(t *testing.T) {
+	map1 := map[string]interface{}{
+		"foo": "bar",
+	}
+
+	map2 := map[string]interface{}{
+		"bar": "baz",
+	}
+
+	result := Merge(&map1, &map2)
+	expected := map[string]interface{}{
+		"foo": "bar",
+		"bar": "baz",
+	}
+
+	if !reflect.DeepEqual(*result, expected) {
+		t.Error("Maps were merged incorrectly.")
+		t.Fail()
+	}
+}
+
+func TestMergeMapsPrecedence(t *testing.T) {
+	map1 := map[string]interface{}{
+		"foo": "incorrect",
+	}
+
+	map2 := map[string]interface{}{
+		"foo": "correct",
+	}
+
+	result := Merge(&map1, &map2)
+
+	if (*result)["foo"] != "correct" {
+		t.Error("Map merge precedence test failed.")
+		t.Fail()
+	}
+}