├── .gitignore ├── CNAME ├── CONTRIBUTING.md ├── Dockerfile ├── HomebrewFormula └── kasane.rb ├── LICENSE ├── MANIFEST.in ├── README.md ├── _config.yml ├── examples ├── 01-simple-layers │ ├── Kasanefile │ ├── README.md │ ├── first.yaml │ └── second.yaml ├── 02-jsonnet-transformations │ ├── Kasanefile │ ├── README.md │ ├── object.yaml │ └── patch.jsonnet ├── 03-environment │ ├── Kasanefile │ ├── README.md │ ├── object.yaml │ └── patch.jsonnet └── 04-complex-service │ ├── Kasanefile │ ├── README.md │ ├── helpers.libsonnet │ ├── ingress.yaml │ ├── istio-ingress.yaml │ └── service.override.jsonnet ├── kasane ├── __init__.py ├── cmd.py └── ops │ ├── __init__.py │ ├── apply.py │ ├── common.py │ ├── jsonnet.py │ ├── show.py │ └── update.py ├── logo.png ├── setup.py └── tests └── feature ├── cli.feature ├── layers.feature └── test_cli.py /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | Kasanefile.lock 3 | 4 | # Compiled python modules. 5 | *.pyc 6 | .mypy_cache 7 | __pycache__ 8 | /.venv/ 9 | 10 | # Setuptools distribution folder. 11 | /dist/ 12 | /build/ 13 | 14 | # Python egg metadata, regenerated from source files by setuptools. 15 | /*.egg-info 16 | /*.egg 17 | 18 | # Tests & coverage 19 | /.coverage 20 | /.pytest_cache/ 21 | cov.xml 22 | -------------------------------------------------------------------------------- /CNAME: -------------------------------------------------------------------------------- 1 | kasane.app -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | We'd love to accept your patches and contributions to this project. There are 4 | just a few small guidelines you need to follow. 5 | 6 | ## Contributor License Agreement 7 | 8 | Contributions to this project must be accompanied by a Contributor License 9 | Agreement. You (or your employer) retain the copyright to your contribution, 10 | this simply gives us permission to use and redistribute your contributions as 11 | part of the project. Head over to to see 12 | your current agreements on file or to sign a new one. 13 | 14 | You generally only need to submit a CLA once, so if you've already submitted one 15 | (even if it was for a different project), you probably don't need to do it 16 | again. 17 | 18 | ## Code reviews 19 | 20 | All submissions, including submissions by project members, require review. We 21 | use GitHub pull requests for this purpose. Consult 22 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more 23 | information on using pull requests. 24 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:latest 2 | 3 | ADD . /usr/src/kasane 4 | 5 | RUN set -ex; \ 6 | apk add --no-cache --no-progress bash git curl python3 libstdc++; \ 7 | apk add --no-cache --no-progress --virtual .build-deps build-base python3-dev; \ 8 | pip3 install -e /usr/src/kasane; \ 9 | apk --no-progress del .build-deps; \ 10 | curl -L https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/linux/amd64/kubectl > /usr/bin/kubectl; \ 11 | chmod +x /usr/bin/kubectl; 12 | 13 | WORKDIR /app 14 | 15 | CMD ["kasane", "show"] 16 | -------------------------------------------------------------------------------- /HomebrewFormula/kasane.rb: -------------------------------------------------------------------------------- 1 | class Kasane < Formula 2 | include Language::Python::Virtualenv 3 | 4 | desc "Kasane is a layering tool for kubernetes" 5 | homepage "https://github.com/google/kasane" 6 | url "https://github.com/google/kasane/archive/0.1.3.tar.gz" 7 | sha256 "a859e566b588fe74818d4c2270ba66263046f44fed7e9ecd5af582f0955b92e2" 8 | head "https://github.com/google/kasane.git" 9 | 10 | depends_on "python" 11 | 12 | resource "click" do 13 | url "https://files.pythonhosted.org/packages/95/d9/c3336b6b5711c3ab9d1d3a80f1a3e2afeb9d8c02a7166462f6cc96570897/click-6.7.tar.gz" 14 | sha256 "f15516df478d5a56180fbf80e68f206010e6d160fc39fa508b65e035fd75130b" 15 | end 16 | 17 | resource "Jinja2" do 18 | url "https://files.pythonhosted.org/packages/56/e6/332789f295cf22308386cf5bbd1f4e00ed11484299c5d7383378cf48ba47/Jinja2-2.10.tar.gz" 19 | sha256 "f84be1bb0040caca4cea721fcbbbbd61f9be9464ca236387158b0feea01914a4" 20 | end 21 | 22 | resource "MarkupSafe" do 23 | url "https://files.pythonhosted.org/packages/4d/de/32d741db316d8fdb7680822dd37001ef7a448255de9699ab4bfcbdf4172b/MarkupSafe-1.0.tar.gz" 24 | sha256 "a6be69091dac236ea9c6bc7d012beab42010fa914c459791d627dad4910eb665" 25 | end 26 | 27 | resource "jsonnet" do 28 | url "https://files.pythonhosted.org/packages/4a/f5/a0a41ac1f141a62c966291feff15e1829147462e95041bdc5fee1dcd7e0f/jsonnet-0.11.2.tar.gz" 29 | sha256 "3201ca48b0ddc4d65a534686436cd435491addcf26346b07dbd69b38f66f4f8f" 30 | end 31 | 32 | resource "requests" do 33 | url "https://files.pythonhosted.org/packages/54/1f/782a5734931ddf2e1494e4cd615a51ff98e1879cbe9eecbdfeaf09aa75e9/requests-2.19.1.tar.gz" 34 | sha256 "ec22d826a36ed72a7358ff3fe56cbd4ba69dd7a6718ffd450ff0e9df7a47ce6a" 35 | end 36 | 37 | resource "certifi" do 38 | url "https://files.pythonhosted.org/packages/4d/9c/46e950a6f4d6b4be571ddcae21e7bc846fcbb88f1de3eff0f6dd0a6be55d/certifi-2018.4.16.tar.gz" 39 | sha256 "13e698f54293db9f89122b0581843a782ad0934a4fe0172d2a980ba77fc61bb7" 40 | end 41 | 42 | resource "chardet" do 43 | url "https://files.pythonhosted.org/packages/fc/bb/a5768c230f9ddb03acc9ef3f0d4a3cf93462473795d18e9535498c8f929d/chardet-3.0.4.tar.gz" 44 | sha256 "84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae" 45 | end 46 | 47 | resource "idna" do 48 | url "https://files.pythonhosted.org/packages/65/c4/80f97e9c9628f3cac9b98bfca0402ede54e0563b56482e3e6e45c43c4935/idna-2.7.tar.gz" 49 | sha256 "684a38a6f903c1d71d6d5fac066b58d7768af4de2b832e426ec79c30daa94a16" 50 | end 51 | 52 | resource "urllib3" do 53 | url "https://files.pythonhosted.org/packages/3c/d2/dc5471622bd200db1cd9319e02e71bc655e9ea27b8e0ce65fc69de0dac15/urllib3-1.23.tar.gz" 54 | sha256 "a68ac5e15e76e7e5dd2b8f94007233e01effe3e50e8daddf69acfd81cb686baf" 55 | end 56 | 57 | resource "ruamel.yaml" do 58 | url "https://files.pythonhosted.org/packages/63/a5/dba37230d6cf51f4cc19a486faf0f06871d9e87d25df0171b3225d20fc68/ruamel.yaml-0.15.45.tar.gz" 59 | sha256 "096691b0958514da21d19ae40255569f027b5b90530c55faf1d74ff16b2f256b" 60 | end 61 | 62 | def install 63 | virtualenv_install_with_resources 64 | end 65 | 66 | test do 67 | # TODO: version check 68 | # assert_match version.to_s, shell_output("#{bin}/kasane --version") 69 | 70 | mkdir testpath do 71 | open("Kasanefile", "w") do |f| 72 | f.write <<~YAML 73 | layers: 74 | - test.yaml 75 | YAML 76 | end 77 | 78 | open("test.yaml", "w") do |f| 79 | f.write <<~YAML 80 | --- 81 | kind: Dummy 82 | metadata: 83 | name: dummy 84 | YAML 85 | end 86 | 87 | system "#{bin}/kasane", "show" 88 | end 89 | end 90 | end 91 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kasane 2 | 3 | ![](https://raw.githubusercontent.com/google/kasane/master/logo.png) 4 | 5 | kasane [ 重ね ] (n.) pile; heap; layers 6 | 7 | **This is not an official Google product** 8 | 9 | Kasane is a layering tool for kubernetes. It allows you to use the officially published YAML documents and extend them further with your local configuration. 10 | 11 | Kasane can utilise Jsonnet for deep object modification and patching. 12 | 13 | ## Archival notice 14 | 15 | As time goes the k8s infrastructure takes shape. Kasane was an interesting experiment, but practice demonstrated that k8s deployments often need to interact with various third-party services, be those clouds, credential storages, or databases. 16 | 17 | Kasane can be somewhat easily replicated with pulumi. Go try that. 18 | 19 | ## Installation 20 | 21 | Kasane requires Python 3+. Install via pip: 22 | 23 | ```shell 24 | pip install kasane 25 | ``` 26 | 27 | Installation via Homebrew: 28 | 29 | ```shell 30 | brew tap google/kasane https://github.com/google/kasane.git 31 | brew install google/kasane/kasane 32 | ``` 33 | 34 | ## Running from a Docker container 35 | 36 | You can run kasane from a docker container, the official image is `gcr.io/kasaneapp/kasane`. The image is based on alpine and comes pre-packaged with bash, curl, git and kubectl in addition to kasane itself. The workdir is set to `/app` and the default command is `kasane show` so you can quickly examine your local Kasanefiles like this: 37 | 38 | ```bash 39 | $ docker run --rm -ti -v $PWD/examples/03-environment:/app gcr.io/kasaneapp/kasane 40 | config: 41 | defaultFlag: UNRESOLVED_ENV_VAR__DEFAULT_VALUE 42 | defaultFromKasanefile: value 43 | jsonnetEnv: UNRESOLVED_ENV_VAR__OTHER_VALUE 44 | kind: VendoredObject 45 | metadata: 46 | name: PreconfiguredObject 47 | ``` 48 | 49 | Tagged builds for versions starting with 0.1.4 are also available as e.g. `gcr.io/kasaneapp/kasane:0.1.4`. 50 | 51 | ## Examples 52 | 53 | * [Simple Layers](examples/01-simple-layers) is an introduction to kasane features. 54 | * [Jsonnet Transformations](examples/02-jsonnet-transformations) shows how to use Jsonnet to transform objects. 55 | * [Environment](examples/03-environment) explains how to use the external environment for customized pipelines. 56 | * [Complex Service](examples/04-complex-service) shows all the features together by using the upstream configuration for kubernetes dashboard, adding an ingress, and optionally enabling istio sidecar. 57 | 58 | ## Similar tools 59 | 60 | ### Helm 61 | 62 | Helm is fully-featured package management solution for kubernetes. Compared to it, kasane is a swiss army knife. It's simple, lightweight, doesn't install helper code into your production. Kasane allows you to use original YAML files written by application authors, modifying them to your local needs. If you see a `kubectl apply -f http://` example you can turn it into a Kasane deployment with a single line of code and then extend it to your needs. 63 | 64 | Kasane doesn't do any templating, relying on Jsonnet for data manipulation. You won't ever need to count number of spaces to make sure your yaml go template is rendered correctly. 65 | 66 | ### Ksonnet 67 | 68 | Kasane is similar to Ksonnet but is much simpler to use. Kasane allows to re-use original YAML files and minimizes amount of custom Jsonnet code you need to write. Most of the time your Kasane project would consist of a Kasanefile and single yaml or jsonnet file. Still, Kasane allows runtime flexibility with conditional layers and custom environment. 69 | 70 | ## License 71 | 72 | Kasane is distributed under Apache-2 [license](LICENSE). See the [contributing guidelines](CONTRIBUTING.md) on how you can contribute to the project. 73 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-hacker -------------------------------------------------------------------------------- /examples/01-simple-layers/Kasanefile: -------------------------------------------------------------------------------- 1 | layers: 2 | - first.yaml 3 | - https://raw.githubusercontent.com/google/kasane/master/examples/01-simple-layers/second.yaml 4 | -------------------------------------------------------------------------------- /examples/01-simple-layers/README.md: -------------------------------------------------------------------------------- 1 | # Simple Layers 2 | 3 | In the simplest mode of operation Kasane walks through the layers in order and concatenates them. This can be useful in a case when you need to source an external file and then add several local objects. 4 | 5 | Remote files must be vendored prior to use. Kasane verifies the remote hash to keep track of the changing upstream and `kasane update` will sync the state to the latest one. 6 | 7 | ```bash 8 | $ cat Kasanefile 9 | layers: 10 | - first.yaml 11 | - https://raw.githubusercontent.com/google/kasane/master/examples/01-simple-layers/second.yaml 12 | 13 | $ kasane update 14 | 15 | $ kasane show 16 | kind: FakeObject 17 | metadata: 18 | name: fake 19 | --- 20 | kind: FakeObject 21 | metadata: 22 | name: fake2 23 | --- 24 | kind: FakeObject 25 | metadata: 26 | name: fake3 27 | ``` 28 | 29 | The current verison supports only the simple http[s] upstreams. 30 | 31 | Notice how `kasane update` creates `Kasanefile.lock` with the hash of the remote file. 32 | -------------------------------------------------------------------------------- /examples/01-simple-layers/first.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | kind: FakeObject 3 | metadata: 4 | name: fake 5 | --- 6 | kind: FakeObject 7 | metadata: 8 | name: fake2 9 | -------------------------------------------------------------------------------- /examples/01-simple-layers/second.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | kind: FakeObject 3 | metadata: 4 | name: fake3 5 | -------------------------------------------------------------------------------- /examples/02-jsonnet-transformations/Kasanefile: -------------------------------------------------------------------------------- 1 | layers: 2 | - https://raw.githubusercontent.com/google/kasane/master/examples/02-jsonnet-transformations/object.yaml 3 | - patch.jsonnet 4 | -------------------------------------------------------------------------------- /examples/02-jsonnet-transformations/README.md: -------------------------------------------------------------------------------- 1 | # Jsonnet Transformations 2 | 3 | Kasane uses [Jsonnet](http://jsonnet.org/) to transform objects in its pipeline. You can use jsonnet to define new objects, but more commonly you'd use it to make paches to vendored dependencies. This example shows how to modify a vendored dependency. 4 | 5 | ```bash 6 | $ cat Kasanefile 7 | layers: 8 | - https://raw.githubusercontent.com/google/kasane/master/examples/02-jsonnet-transformations/object.yaml 9 | - patch.jsonnet 10 | 11 | $ kasane update 12 | 13 | $ kasane show 14 | kind: VendoredObject 15 | config: 16 | defaultFlag: 42 17 | otherFlag: don't change 18 | metadata: 19 | name: PreconfiguredObject 20 | ``` 21 | 22 | Jsonnet files receive the **array** with all the previous layers concatenated as a function input named `layers` and **must** return an array with the results. It might be a completely different set of objects but it still must be an array. 23 | -------------------------------------------------------------------------------- /examples/02-jsonnet-transformations/object.yaml: -------------------------------------------------------------------------------- 1 | kind: VendoredObject 2 | metadata: 3 | name: PreconfiguredObject 4 | config: 5 | defaultFlag: 10 6 | otherFlag: don't change 7 | -------------------------------------------------------------------------------- /examples/02-jsonnet-transformations/patch.jsonnet: -------------------------------------------------------------------------------- 1 | function (layers) 2 | 3 | [ 4 | layers[0] { 5 | config+: { 6 | defaultFlag: 42, 7 | }, 8 | } 9 | ] 10 | -------------------------------------------------------------------------------- /examples/03-environment/Kasanefile: -------------------------------------------------------------------------------- 1 | layers: 2 | - name: object.yaml 3 | loader: yamlenv 4 | - name: patch.jsonnet 5 | when: not ignore_jsonnet 6 | environment: 7 | DEFAULT_KASANE: value 8 | -------------------------------------------------------------------------------- /examples/03-environment/README.md: -------------------------------------------------------------------------------- 1 | # Environment 2 | 3 | Kasane supports references to the external environment in yaml, jsonnet and Kasanefiles. 4 | 5 | A layer description in Kasanefile can be provided with a simple name that's actually a shorthand for 6 | 7 | ```yaml 8 | - name: layername 9 | ``` 10 | 11 | Two more options supported by layers are `loader` and `when`. 12 | 13 | Loaders allow to transform the loaded data. YAML suppors the loader `yamlenv` that handles a common syntax `${VAR}` found in numerous vendored YAML files. 14 | 15 | Jsonnet supports the environment natively via an exported function `std.native("env")('VAR')`. 16 | 17 | Kasanefiles can skip or include additional layers based on the environment, specified by `when` condition -- if you ever worked with ansible you'll immediately know how it operates. The string in the `when` condition is a jinja2 expression with all the environment options exposed. 18 | 19 | Common environment can be specified inline within Kasanefile. That's handy in case you have several yamlenv files with known defaults. 20 | 21 | ```bash 22 | $ cat Kasanefile 23 | layers: 24 | - name: object.yaml 25 | loader: yamlenv 26 | - name: patch.jsonnet 27 | when: not ignore_jsonnet 28 | environment: 29 | DEFAULT_KASANE: value 30 | 31 | $ kasane show 32 | kind: VendoredObject 33 | config: 34 | defaultFlag: UNRESOLVED_ENV_VAR__DEFAULT_VALUE 35 | defaultFromKasanefile: value 36 | jsonnetEnv: UNRESOLVED_ENV_VAR__OTHER_VALUE 37 | metadata: 38 | name: PreconfiguredObject 39 | ``` 40 | 41 | Notice how by default kasane doesn't fail if the environment is undefined. `kasane show` allows to preview your code quickly but `kasane apply` will fail if environment variables are missing. You can run `kasane show --no-ignore-env` to replicate the same behavior with the show command. 42 | 43 | ```bash 44 | $ kasane -e DEFAULT_VALUE=10 -e OTHER_VALUE=11 show --no-ignore-env 45 | kind: VendoredObject 46 | config: 47 | defaultFlag: 10 48 | defaultFromKasanefile: value 49 | jsonnetEnv: '11' 50 | metadata: 51 | name: PreconfiguredObject 52 | ``` 53 | 54 | Notice how the environment resolves to whatever is contextually sensible in YAML but always to a string in Jsonnet. You must use the appropriate type casting in jsonnet files. 55 | 56 | ```bash 57 | kasane -e DEFAULT_VALUE=10 -e ignore_jsonnet=true show --no-ignore-env 58 | kind: VendoredObject 59 | metadata: 60 | name: PreconfiguredObject 61 | config: 62 | defaultFlag: 10 63 | defaultFromKasanefile: value 64 | ``` 65 | 66 | If the layer is skipped its environment isn't evaluated and it's fine to skip unused environment fields. 67 | 68 | You can also pass the environment via the os environment `KASANE_JSONNET_ENV` variable. It must be a json dictionary: 69 | 70 | ```bash 71 | KASANE_JSONNET_ENV='{"DEFAULT_VALUE":"20"}' kasane show 72 | kind: VendoredObject 73 | config: 74 | defaultFlag: 20 75 | defaultFromKasanefile: value 76 | jsonnetEnv: UNRESOLVED_ENV_VAR__OTHER_VALUE 77 | metadata: 78 | name: PreconfiguredObject 79 | ``` 80 | 81 | Hint: if you keep repeating same loader (e.g. `yamlenv` for a bunch of yaml files) you can specify `default_loader: yamlenv` in Kasanefile. 82 | -------------------------------------------------------------------------------- /examples/03-environment/object.yaml: -------------------------------------------------------------------------------- 1 | kind: VendoredObject 2 | metadata: 3 | name: PreconfiguredObject 4 | config: 5 | defaultFlag: ${DEFAULT_VALUE} 6 | defaultFromKasanefile: ${DEFAULT_KASANE} 7 | -------------------------------------------------------------------------------- /examples/03-environment/patch.jsonnet: -------------------------------------------------------------------------------- 1 | local env(name) = std.native("env")(name); 2 | 3 | function (layers) 4 | 5 | [ 6 | layers[0] { 7 | config+: { 8 | jsonnetEnv: env('OTHER_VALUE'), 9 | }, 10 | } 11 | ] 12 | -------------------------------------------------------------------------------- /examples/04-complex-service/Kasanefile: -------------------------------------------------------------------------------- 1 | layers: 2 | - https://raw.githubusercontent.com/kubernetes/dashboard/master/src/deploy/recommended/kubernetes-dashboard.yaml 3 | - service.override.jsonnet 4 | - name: ingress.yaml 5 | when: not istio 6 | - name: istio-ingress.yaml 7 | when: istio 8 | - name: istio 9 | loader: inject 10 | when: istio 11 | -------------------------------------------------------------------------------- /examples/04-complex-service/README.md: -------------------------------------------------------------------------------- 1 | # Complex Service 2 | 3 | This example demonstrates the use of kasane helpers library and `istio` loader to load kubernetes dashboard and optionally add istio sidecar. 4 | 5 | ```bash 6 | $ kasane show 7 | kasane show -k Ingress 8 | apiVersion: extensions/v1beta1 9 | kind: Ingress 10 | metadata: 11 | name: dashboard-ingress 12 | namespace: kube-system 13 | annotations: 14 | nginx.ingress.kubernetes.io/auth-tls-secret: ingress-nginx/ingress-client-cert 15 | spec: 16 | rules: 17 | - host: dashboard.example.com 18 | http: 19 | paths: 20 | - backend: 21 | serviceName: kubernetes-dashboard 22 | servicePort: 80 23 | tls: 24 | - secretName: wildcard-example-com 25 | hosts: 26 | - dashboard.example.com 27 | ``` 28 | 29 | Notice how you can use `-k $KIND` to quickly filter the output by kubernetes kind field. 30 | -------------------------------------------------------------------------------- /examples/04-complex-service/helpers.libsonnet: -------------------------------------------------------------------------------- 1 | { 2 | patchPodSpec(dep, patch):: 3 | dep { spec+: { template+: { spec+: patch } } }, 4 | 5 | patchContainer(container):: 6 | { 7 | spec+: { template+: { spec+: { 8 | local checkContainerCount = std.assertEqual(std.length(super.continers), 1), 9 | 10 | containers: [super.containers[0] + container], 11 | } } } }, 12 | 13 | patchPort(portNumber, portSpec):: 14 | { 15 | spec+: { 16 | ports: std.map( 17 | function(oldPort) 18 | if oldPort.port == portNumber then oldPort + portSpec else oldPort, 19 | super.ports), 20 | }, 21 | }, 22 | 23 | named(objectlist):: 24 | { 25 | [k.kind + '/' + 26 | (if std.objectHas(k.metadata, 'namespace') then k.metadata.namespace + '/' else '') + 27 | k.metadata.name]: k, 28 | for k in 29 | std.makeArray( 30 | std.length(objectlist), 31 | function(i) 32 | objectlist[i] + {_named_object_index:: i}) 33 | }, 34 | 35 | list(objecthash):: 36 | local objs = [objecthash[f] for f in std.objectFields(objecthash)]; 37 | local objsKeyed = {[std.toString(o._named_object_index)]: o for o in objs}; 38 | [o 39 | for o in std.makeArray( 40 | std.length(objs), 41 | function(i) 42 | local k = std.toString(i); 43 | if std.objectHas(objsKeyed, k) then objsKeyed[k] else null) 44 | if o != null], 45 | 46 | env(name):: 47 | std.native("env")(name), 48 | } 49 | -------------------------------------------------------------------------------- /examples/04-complex-service/ingress.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: extensions/v1beta1 3 | kind: Ingress 4 | metadata: 5 | name: dashboard-ingress 6 | namespace: kube-system 7 | annotations: 8 | nginx.ingress.kubernetes.io/auth-tls-secret: ingress-nginx/ingress-client-cert 9 | spec: 10 | rules: 11 | - host: dashboard.example.com 12 | http: 13 | paths: 14 | - backend: 15 | serviceName: kubernetes-dashboard 16 | servicePort: 80 17 | tls: 18 | - secretName: wildcard-example-com 19 | hosts: 20 | - dashboard.example.com 21 | -------------------------------------------------------------------------------- /examples/04-complex-service/istio-ingress.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: extensions/v1beta1 3 | kind: Ingress 4 | metadata: 5 | name: dashboard-ingress 6 | namespace: kube-system 7 | annotations: 8 | kubernetes.io/ingress.class: istio 9 | spec: 10 | rules: 11 | - host: dashboard.example.com 12 | http: 13 | paths: 14 | - backend: 15 | serviceName: kubernetes-dashboard 16 | servicePort: 80 17 | -------------------------------------------------------------------------------- /examples/04-complex-service/service.override.jsonnet: -------------------------------------------------------------------------------- 1 | local h = import 'helpers.libsonnet'; 2 | 3 | function (layers) 4 | 5 | h.list(h.named(layers) { 6 | 'Service/kube-system/kubernetes-dashboard'+: { 7 | spec+: { 8 | ports: [{ 9 | name: 'http', 10 | port: 80, 11 | targetPort: 9090, 12 | }], 13 | }, 14 | }, 15 | 'Deployment/kube-system/kubernetes-dashboard'+: h.patchContainer({ 16 | args: [ 17 | '--insecure-bind-address=0.0.0.0', 18 | '--insecure-port=9090', 19 | '--enable-insecure-login', 20 | '--heapster-host=http://heapster.kube-system.svc:80', 21 | ], 22 | livenessProbe:: null, 23 | }), 24 | }) 25 | -------------------------------------------------------------------------------- /kasane/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | -------------------------------------------------------------------------------- /kasane/cmd.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | 16 | import os 17 | import json 18 | from typing import List 19 | 20 | import click 21 | 22 | from kasane import ops 23 | 24 | 25 | @click.group() 26 | @click.option('-p', '--path') 27 | @click.option('-l', '--lib') 28 | @click.option('-c', '--config') 29 | @click.option('-e', '--env', multiple=True) 30 | @click.version_option(message=('%(prog)s v%(version)s')) 31 | @click.pass_context 32 | def cli(ctx, path: str, lib: str, config: str, env: List[str]): 33 | if not ctx.obj: 34 | ctx.obj = {} 35 | if not path: 36 | path = os.path.curdir 37 | if not lib: 38 | lib = '' 39 | ctx.obj['path'] = path 40 | ctx.obj['config'] = config 41 | collectedenv = os.getenv('KASANE_JSONNET_ENV', '{ }') 42 | collectedenv = json.loads(collectedenv) 43 | for kv in env: 44 | k, v = kv.split('=', 1) 45 | collectedenv[k] = v 46 | ctx.obj['env'] = collectedenv 47 | ctx.obj['j'] = ops.Jsonnet([path] + [lib], collectedenv) 48 | ctx.obj['rc'] = ops.RuntimeConfig(check_hashes=True, jsonnet=ctx.obj['j'], kubeconfig=ctx.obj['config']) 49 | 50 | 51 | @cli.add_command 52 | @click.command() 53 | @click.pass_context 54 | @click.option('-r', '--raw/--highlight', default=False) 55 | @click.option('--validate-signatures/--no-validate-signatures', default=True) 56 | @click.option('-k') 57 | @click.option('--ignore-env/--no-ignore-env', default=True) 58 | def show(ctx, raw: bool, validate_signatures: bool, k: str, ignore_env: bool) -> None: 59 | '''prints the compiled bundle''' 60 | 61 | if ignore_env: 62 | ctx.obj['j'].ignore_env = True 63 | 64 | ctx.obj['rc'] = ops.common.RuntimeConfig( 65 | check_hashes=validate_signatures, 66 | jsonnet=ctx.obj['rc'].jsonnet, 67 | kubeconfig=ctx.obj['rc'].kubeconfig) 68 | ops.show(ctx.obj['path'], ctx.obj['rc'], not raw, k) 69 | 70 | 71 | @cli.add_command 72 | @click.command() 73 | @click.pass_context 74 | def apply(ctx) -> None: 75 | '''applies the compiled bundle''' 76 | 77 | try: 78 | ops.apply(ctx.obj['path'], ctx.obj['rc']) 79 | except ops.jsonnet.UndefinedEnvError as e: 80 | print(e) 81 | print('known env:', ', '.join(ctx.obj['env'].keys())) 82 | exit(1) 83 | 84 | 85 | @cli.add_command 86 | @click.command() 87 | @click.option('--lock-all/--lock-remote-only', default=False) 88 | @click.pass_context 89 | def update(ctx, lock_all: bool) -> None: 90 | '''updates the bundle''' 91 | 92 | ops.update(ctx.obj['path'], ctx.obj['rc'], lock_all) 93 | 94 | 95 | def main(): 96 | cli() 97 | -------------------------------------------------------------------------------- /kasane/ops/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from .show import show 16 | from .update import update 17 | from .apply import apply 18 | from .jsonnet import Jsonnet 19 | from .common import RuntimeConfig 20 | -------------------------------------------------------------------------------- /kasane/ops/apply.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import subprocess 16 | from io import StringIO 17 | 18 | from ruamel.yaml import YAML 19 | yaml = YAML() 20 | 21 | from . import common, jsonnet 22 | 23 | def apply(path: str, rc: common.RuntimeConfig) -> None: 24 | cfg = common.get_config(path, rc) 25 | data = cfg.run() 26 | val = common.dump_yaml(data) 27 | nsargs = [] 28 | if cfg.namespace: 29 | nsargs += ['-n', cfg.namespace] 30 | if rc.kubeconfig: 31 | nsargs += ['--kubeconfig', rc.kubeconfig] 32 | 33 | subprocess.run(['kubectl', 'apply', *nsargs, '-f', '-'], input=val.encode('utf-8')) 34 | -------------------------------------------------------------------------------- /kasane/ops/common.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import re 16 | from typing import Optional, List, Any, Dict 17 | import os 18 | from collections import namedtuple 19 | import hashlib 20 | from io import StringIO 21 | import subprocess 22 | import pathlib 23 | 24 | from ruamel.yaml import YAML, comments 25 | import json 26 | import requests 27 | from jinja2 import Template 28 | 29 | yaml = YAML() 30 | yaml.allow_duplicate_keys = True # https://github.com/istio/istio/issues/2330 31 | 32 | from .jsonnet import Jsonnet 33 | 34 | CONFIG_NAME = 'Kasanefile' 35 | LOCKFILE_NAME = 'Kasanefile.lock' 36 | 37 | RuntimeConfig = namedtuple('RuntimeConfig', ['check_hashes', 'jsonnet', 'kubeconfig']) 38 | 39 | class RemoteNotVendoredError(RuntimeError): 40 | def __init__(self, layer): 41 | super().__init__() 42 | self.layer = layer 43 | 44 | class Config(object): 45 | def __init__(self, path: str, configdata: dict, lockdata: dict, rc: RuntimeConfig) -> None: 46 | self._configdata = configdata 47 | self._lockdata = lockdata 48 | self.path = path 49 | self.vendor_dir = os.path.join(path, 'vendor') 50 | 51 | self.namespace = configdata.get('namespace') 52 | self.default_loader = configdata.get('default_loader') 53 | self.localenv = configdata.get('environment', {}) 54 | self.layers: List[Layer] = [] 55 | self.rc = rc 56 | 57 | merged_env = dict() 58 | merged_env.update(self.localenv) 59 | merged_env.update(self.rc.jsonnet.env) 60 | self.rc.jsonnet.env = merged_env 61 | 62 | for l in configdata['layers']: 63 | if isinstance(l, str): 64 | l = {'name': l} 65 | d: Dict[str, Any] = l 66 | d.update(next((lk for lk in lockdata['layers'] if l['name'] == lk.get('name')), {})) 67 | self.layers.append(Layer.build(d, self)) 68 | 69 | def run(self) -> Any: 70 | data: List[Any] = [] 71 | for l in self.layers: 72 | if not l.should_run(): 73 | continue 74 | data = l.run(data) 75 | return data 76 | 77 | def write_lockfile(self, newlockfile): 78 | lockpath = os.path.join(self.path, LOCKFILE_NAME) 79 | s = StringIO() 80 | yaml.dump(newlockfile, s) 81 | val = s.getvalue() 82 | with open(lockpath, 'w') as f: 83 | f.write(val) 84 | 85 | class Layer(object): 86 | def __init__(self, data: dict, cfg: Config) -> None: 87 | self.name: str = data['name'] 88 | self.hash: Optional[str] = data.get('hash') 89 | self.loader: Optional[str] = cfg.default_loader 90 | if data.get('loader'): 91 | self.loader = data.get('loader') 92 | self.when: Optional[str] = data.get('when') 93 | self.ignore_namespace: Optional[str] = data.get('ignore_namespace', False) 94 | self.check_hash: str = data.get('check_hash', False) 95 | self.__content: Optional[str] = None 96 | self.__content_digest: Optional[str] = None 97 | self._cfg = cfg 98 | 99 | @staticmethod 100 | def build(data: dict, cfg: Config) -> Any: 101 | name = data['name'] 102 | if data.get('loader') == 'inject': 103 | return InjectLayer(data, cfg) 104 | if name.endswith('.yaml') or name.endswith('.yml'): 105 | return DataLayer(data, cfg) 106 | elif name.endswith('.jsonnet'): 107 | return JsonnetLayer(data, cfg) 108 | else: 109 | return None 110 | 111 | @property 112 | def is_remote(self) -> bool: 113 | if isinstance(self.name, comments.TaggedScalar): 114 | return False 115 | return self.name.startswith('http://') or self.name.startswith('https://') 116 | 117 | @property 118 | def vendored_path(self) -> str: 119 | if not self.is_remote: 120 | raise RuntimeError('only remote layers can be vendored') 121 | if self.name.startswith('http://'): 122 | return os.path.join(self._cfg.vendor_dir, self.name[7:]) 123 | elif self.name.startswith('https://'): 124 | return os.path.join(self._cfg.vendor_dir, self.name[8:]) 125 | else: 126 | raise RuntimeError('don\'t know how to vendor {}'.format(self.name)) 127 | 128 | @property 129 | def content(self) -> str: 130 | if not self.__content: 131 | self.__content = self._load_content() 132 | return self.__content 133 | 134 | @property 135 | def content_digest(self) -> str: 136 | if not self.__content_digest: 137 | m = hashlib.sha256() 138 | m.update(self.content.encode('utf-8')) 139 | self.__content_digest = m.hexdigest() 140 | return self.__content_digest 141 | 142 | def _load_content(self) -> str: 143 | if self.is_remote: 144 | path = self.vendored_path 145 | else: 146 | path = os.path.join(self._cfg.path, self.name) 147 | 148 | try: 149 | return open(path).read() 150 | except FileNotFoundError: 151 | if self.is_remote: 152 | raise RemoteNotVendoredError(self) 153 | raise 154 | 155 | def vendor_content(self): 156 | if not self.is_remote: 157 | return 158 | r = requests.get(self.name) 159 | data = r.text 160 | pathlib.Path(os.path.dirname(self.vendored_path)).mkdir(parents=True, exist_ok=True) 161 | open(self.vendored_path, 'w').write(data) 162 | 163 | def run(self, previous: List[Any]) -> Any: 164 | raise RuntimeError('not implemented') 165 | 166 | def should_run(self) -> bool: 167 | if not self.when: 168 | return True 169 | conditional = '{%% if %s %%}True{%% else %%}False{%% endif %%}' % self.when 170 | return Template(conditional).render(**self._cfg.rc.jsonnet.env) == 'True' 171 | 172 | def _verify_digest(self): 173 | if self._cfg.rc.check_hashes and (self.hash or self.check_hash) and self.hash != self.content_digest: 174 | raise RuntimeError("digest mismatch, got {}, expected {}. Need to run kasane update?".format(self.content_digest, self.hash)) 175 | 176 | class InjectLayer(Layer): 177 | @property 178 | def is_remote(self) -> bool: 179 | return False 180 | 181 | def run(self, previous: List[Any]) -> Any: 182 | if self.name == 'istio': 183 | env = dict(**os.environ) 184 | if self._cfg.rc.kubeconfig: 185 | env['KUBECONFIG'] = self._cfg.rc.kubeconfig 186 | data = subprocess.check_output(['istioctl', 'kube-inject', '-f', '-', '--includeIPRanges=10.200.0.0/16'], universal_newlines=True, input=dump_yaml(previous), env=env) 187 | data = list(yaml.load_all(data)) 188 | data = list(filter(None.__ne__, data)) 189 | else: 190 | raise RuntimeError("don't know how to run {} tag".format(self.name)) 191 | 192 | return data 193 | 194 | RX_ENV = re.compile(r'\$\{([^}]+)\}') 195 | 196 | class DataLayer(Layer): 197 | def run(self, previous: List[Any]) -> Any: 198 | self._verify_digest() 199 | 200 | content = self.content 201 | 202 | if self.loader == 'yamlenv': 203 | def replace(m): 204 | key = m.group(1) 205 | val = self._cfg.rc.jsonnet.get_env(key) 206 | if val.find('\n') != -1: 207 | # if it contains newlines must force a string 208 | val = json.dumps(val) 209 | return val 210 | content = RX_ENV.sub(replace, content) 211 | 212 | data: List[Any] = list(yaml.load_all(content)) 213 | data = list(filter(None.__ne__, data)) 214 | data = previous + data 215 | return data 216 | 217 | class JsonnetLayer(Layer): 218 | def run(self, previous: List[Any]) -> Any: 219 | self._verify_digest() 220 | 221 | return self._cfg.rc.jsonnet.evaluate_snippet(self.name, self.content, tla_codes=dict(layers=json.dumps(previous))) 222 | 223 | def get_config(path: str, rc: RuntimeConfig) -> Config: 224 | cfgpath = os.path.join(path, CONFIG_NAME) 225 | lockpath = os.path.join(path, LOCKFILE_NAME) 226 | if not os.path.isfile(cfgpath): 227 | raise RuntimeError("config {} isn't found at {} or is not a file".format(CONFIG_NAME, cfgpath)) 228 | 229 | cfg = yaml.load(open(cfgpath)) 230 | lock = None 231 | 232 | if os.path.isfile(lockpath): 233 | lock = yaml.load(open(lockpath)) 234 | 235 | if not lock: 236 | lock = {} 237 | 238 | if not 'layers' in lock: 239 | lock['layers'] = [] 240 | 241 | return Config(path, cfg, lock, rc) 242 | 243 | def dump_yaml(data): 244 | s = StringIO() 245 | yaml.dump_all(data, s) 246 | return s.getvalue() 247 | -------------------------------------------------------------------------------- /kasane/ops/jsonnet.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import json 16 | import os 17 | 18 | import _jsonnet 19 | 20 | class UndefinedEnvError(RuntimeError): 21 | def __init__(self, var): 22 | super().__init__('unresolved environment variable ' + var) 23 | self.var = var 24 | 25 | class Jsonnet(object): 26 | def __init__(self, search_dirs, env): 27 | self.ignore_env = False 28 | self.env = env 29 | self._search_dirs = search_dirs 30 | self._native_callbacks = { 31 | 'env': (('name', ), self.get_env), 32 | } 33 | 34 | def get_env(self, name): 35 | e = self.env.get(name) 36 | if e is not None: 37 | return e 38 | if self.ignore_env: 39 | return 'UNRESOLVED_ENV_VAR__' + name 40 | raise UndefinedEnvError(name) 41 | 42 | def evaluate(self, *args, **kwargs): 43 | kwargs['import_callback'] = self._import_callback 44 | kwargs['native_callbacks'] = self._native_callbacks 45 | return json.loads(_jsonnet.evaluate_file(*args, **kwargs)) 46 | 47 | def evaluate_snippet(self, *args, **kwargs): 48 | kwargs['import_callback'] = self._import_callback 49 | kwargs['native_callbacks'] = self._native_callbacks 50 | return json.loads(_jsonnet.evaluate_snippet(*args, **kwargs)) 51 | 52 | def _import_callback(self, localdir, rel): 53 | if not rel: 54 | raise RuntimeError('Got invalid filename (empty string).') 55 | 56 | if rel[-1] == '/': 57 | raise RuntimeError('Attempted to import a directory') 58 | 59 | if rel[0] == '.': 60 | full_path, content = self._try_path(os.path.join(localdir, rel)) 61 | if content: 62 | return full_path, content 63 | 64 | if rel[0] == '/': 65 | full_path, content = self._try_path(rel) 66 | if content: 67 | return full_path, content 68 | 69 | full_path, content = self._try_path(os.path.join(localdir, rel)) 70 | if content: 71 | return full_path, content 72 | 73 | for d in self._search_dirs: 74 | d = os.path.abspath(d) 75 | full_path, content = self._try_path(os.path.join(d, rel)) 76 | if content: 77 | return full_path, content 78 | 79 | raise RuntimeError('File not found') 80 | 81 | def _try_path(self, full_path): 82 | if not os.path.isfile(full_path): 83 | return full_path, None 84 | with open(full_path) as f: 85 | return full_path, f.read() 86 | 87 | -------------------------------------------------------------------------------- /kasane/ops/show.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import click 16 | import shutil 17 | import subprocess 18 | from io import StringIO 19 | 20 | from ruamel.yaml import YAML 21 | yaml = YAML() 22 | 23 | from . import common, jsonnet 24 | 25 | def show(path: str, rc: common.RuntimeConfig, highlight: bool, filter_kind: str) -> None: 26 | cfg = common.get_config(path, rc) 27 | try: 28 | data = cfg.run() 29 | except common.RemoteNotVendoredError as e: 30 | print("layer {name} isn't vendored yet. Run kasane update.".format(name=e.layer.name)) 31 | exit(1) 32 | s = StringIO() 33 | if filter_kind: 34 | filter_kind = filter_kind.lower() 35 | data = list(filter(lambda o: o['kind'].lower() == filter_kind, data)) 36 | yaml.dump_all(data, s) 37 | val = s.getvalue() 38 | if highlight and shutil.which('highlight') != None: 39 | subprocess.run(['highlight', '-l', 'yaml'], input=val.encode('utf-8')) 40 | else: 41 | click.echo(val, nl=False) 42 | -------------------------------------------------------------------------------- /kasane/ops/update.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import subprocess 16 | from io import StringIO 17 | 18 | from ruamel.yaml import YAML 19 | yaml = YAML() 20 | 21 | from . import common, jsonnet 22 | 23 | def update(path: str, rc: common.RuntimeConfig, lock_all: bool) -> None: 24 | cfg = common.get_config(path, rc) 25 | lockedlayers = [] 26 | for l in cfg.layers: 27 | if not lock_all and not (l.is_remote or l.check_hash): 28 | continue 29 | l.vendor_content() 30 | lockedlayers.append(dict( 31 | name=l.name, 32 | hash=l.content_digest, 33 | )) 34 | 35 | lockfile = dict(layers=lockedlayers) 36 | 37 | cfg.write_lockfile(lockfile) 38 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/google/kasane/bc81e9cfff3ffee6cb4ba55660f43e4fd7683fb4/logo.png -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # Copyright 2018 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | from setuptools import setup, find_packages 16 | 17 | def readme(): 18 | with open('README.md') as f: 19 | return f.read() 20 | 21 | setup(name='kasane', 22 | version='0.1.3', 23 | description='A simple kubernets deployment manager', 24 | long_description=readme(), 25 | long_description_content_type='text/markdown', 26 | url='https://github.com/google/kasane', 27 | author='Vladimir Pouzanov', 28 | author_email='farcaller@gmail.com', 29 | keywords='kubernetes helm package-manager docker jsonnet', 30 | license='Apache-2', 31 | packages=find_packages(), 32 | install_requires=[ 33 | 'click', 34 | 'ruamel.yaml', 35 | 'requests', 36 | 'jinja2', 37 | 'jsonnet', 38 | ], 39 | entry_points = { 40 | 'console_scripts': ['kasane=kasane.cmd:main'], 41 | }, 42 | include_package_data=True, 43 | zip_safe=False) 44 | -------------------------------------------------------------------------------- /tests/feature/cli.feature: -------------------------------------------------------------------------------- 1 | Feature: cli 2 | Command-line interface to kasane 3 | 4 | Scenario: Showing a simple Kasanefile 5 | Given Kasanefile: 6 | layers: 7 | - test.yaml 8 | When a layer named 'test.yaml' contains: 9 | --- 10 | kind: FakeObject 11 | metadata: 12 | name: fake 13 | And user runs kasane show 14 | Then kasane outputs: 15 | kind: FakeObject 16 | metadata: 17 | name: fake 18 | 19 | Scenario: Showing a remote layer fails when the layer isn't vendored 20 | Given Kasanefile: 21 | layers: 22 | - https://raw.githubusercontent.com/google/kasane/master/examples/01-simple-layers/second.yaml 23 | When user runs kasane show 24 | Then kasane fails with a message 'layer https://raw.githubusercontent.com/google/kasane/master/examples/01-simple-layers/second.yaml isn't vendored yet. Run kasane update.' 25 | 26 | Scenario: Showing a remote layer works 27 | Given Kasanefile: 28 | layers: 29 | - https://raw.githubusercontent.com/google/kasane/master/examples/01-simple-layers/second.yaml 30 | When user runs kasane update 31 | And user runs kasane show 32 | Then kasane outputs: 33 | kind: FakeObject 34 | metadata: 35 | name: fake3 36 | -------------------------------------------------------------------------------- /tests/feature/layers.feature: -------------------------------------------------------------------------------- 1 | Feature: layers 2 | 3 | Scenario: Combining two layers together 4 | Given Kasanefile: 5 | layers: 6 | - test.yaml 7 | - test2.yaml 8 | When a layer named 'test.yaml' contains: 9 | --- 10 | kind: FakeObject 11 | metadata: 12 | name: fake 13 | And a layer named 'test2.yaml' contains: 14 | --- 15 | kind: FakeObject 16 | metadata: 17 | name: fake2 18 | And user runs kasane show 19 | Then kasane outputs: 20 | kind: FakeObject 21 | metadata: 22 | name: fake 23 | --- 24 | kind: FakeObject 25 | metadata: 26 | name: fake2 27 | 28 | Scenario: Using yamlenv loader substitutes ${ENV} with env values 29 | Given Kasanefile: 30 | layers: 31 | - name: test.yaml 32 | loader: yamlenv 33 | environment: 34 | TEST: case 35 | When a layer named 'test.yaml' contains: 36 | --- 37 | kind: FakeObject 38 | metadata: 39 | name: ${TEST} 40 | And user runs kasane show 41 | Then kasane outputs: 42 | kind: FakeObject 43 | metadata: 44 | name: case 45 | -------------------------------------------------------------------------------- /tests/feature/test_cli.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | """cli feature tests.""" 3 | 4 | from pytest_bdd import (parsers, scenario, given, when, then) 5 | 6 | from kasane import cmd 7 | 8 | 9 | @scenario('cli.feature', "Showing a simple Kasanefile") 10 | def test_showing_a_simple_kasanefile(): 11 | pass 12 | 13 | @scenario('cli.feature', "Showing a remote layer fails when the layer isn't vendored") 14 | def test_fetiching_remote_layer_fails_when_not_vendored(): 15 | pass 16 | 17 | @scenario('cli.feature', "Showing a remote layer works") 18 | def test_showing_remote_layer_works(): 19 | pass 20 | 21 | @scenario('layers.feature', "Combining two layers together") 22 | def test_combining_two_layers_together(): 23 | pass 24 | 25 | @scenario('layers.feature', "Using yamlenv loader substitutes ${ENV} with env values") 26 | def test_yamlenv_loader(): 27 | pass 28 | 29 | ### 30 | 31 | @given(parsers.parse("Kasanefile:\n{text}")) 32 | def kasanefile(tmpdir, text): 33 | tmpdir.join('Kasanefile').write(text) 34 | return {'output': None} 35 | 36 | ### 37 | 38 | @when(parsers.parse("a layer named '{name}' contains:\n{text}")) 39 | def named_layer(tmpdir, name, text): 40 | tmpdir.join(name).write(text) 41 | 42 | @when(parsers.parse("user runs kasane {action}")) 43 | def run_kasane_action(kasanefile, cli_runner, tmpdir, action): 44 | try: 45 | old_dir = tmpdir.chdir() 46 | 47 | # TODO(farcaller): fix the highlight breakage 48 | if action == 'show': 49 | args = ['-r'] 50 | else: 51 | args = [] 52 | 53 | kasanefile['output'] = cli_runner.invoke(cmd.cli, [action, *args]) 54 | except: 55 | raise 56 | finally: 57 | old_dir.chdir() 58 | 59 | ### 60 | 61 | @then(parsers.parse("kasane outputs:\n{text}")) 62 | def kasane_outputs(kasanefile, text): 63 | assert kasanefile['output'].output == text + '\n' 64 | 65 | @then(parsers.parse("kasane fails with a message '{message}'")) 66 | def kasane_fails_with_message(kasanefile, message): 67 | assert kasanefile['output'].exit_code == 1 68 | assert kasanefile['output'].output == message + '\n' 69 | --------------------------------------------------------------------------------