Repository: bitnami-labs/kube-libsonnet Branch: master Commit: 6c3dd4a19536 Files: 46 Total size: 136.5 KB Directory structure: gitextract_n5_iu0de/ ├── .gitattributes ├── .travis.yml ├── CODEOWNERS ├── CODE_OF_CONDUCT.md ├── LICENSE ├── Makefile ├── README.md ├── SECURITY.md ├── bitnami.libsonnet ├── examples/ │ ├── guestbook/ │ │ └── guestbook.jsonnet │ └── wordpress/ │ ├── backend.jsonnet │ ├── frontend.jsonnet │ └── wordpress.jsonnet ├── kube-platforms.libsonnet ├── kube.libsonnet ├── tests/ │ ├── Dockerfile │ ├── Makefile │ ├── docker-compose.yaml │ ├── golden/ │ │ ├── init-kube.json │ │ ├── test-Ingress-2ndport.pass.json │ │ ├── test-Ingress-port_num_only.pass.json │ │ ├── test-SealedSecret.pass.json │ │ ├── test-Service-container_index.pass.json │ │ ├── test-gke-ManagedCertificate.pass.json │ │ ├── test-simple-validate.pass.json │ │ └── unittests.pass.json │ ├── init-kube.jsonnet │ ├── k3s-e2e-test.sh │ ├── test-Ingress-2ndport.pass.jsonnet │ ├── test-Ingress-name_port.fail.jsonnet │ ├── test-Ingress-port_num_only.pass.jsonnet │ ├── test-PDB-no-spec.fail.jsonnet │ ├── test-PDB-wrong-spec.fail.jsonnet │ ├── test-Pod-no_containers_array.fail.jsonnet │ ├── test-Pod-no_containers_map.fail.jsonnet │ ├── test-Pod-secretmount.fail.jsonnet │ ├── test-SealedSecret.fail.jsonnet │ ├── test-SealedSecret.pass.json │ ├── test-SealedSecret.pass.jsonnet │ ├── test-Service-container_index.fail.jsonnet │ ├── test-Service-container_index.pass.jsonnet │ ├── test-gke-ManagedCertificate.fail.jsonnet │ ├── test-gke-ManagedCertificate.pass.jsonnet │ ├── test-simple-validate.pass.jsonnet │ └── unittests.pass.jsonnet └── utils.libsonnet ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitattributes ================================================ tests/golden/* linguist-generated ================================================ FILE: .travis.yml ================================================ language: bash os: - linux services: - docker before_install: # update to docker-ce - curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add - - sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) edge" - sudo apt-get update - sudo apt-get -y install docker-ce script: - make tests ================================================ FILE: CODEOWNERS ================================================ * @dbarranco @jbianquetti-nami ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Contributor Code of Conduct As contributors and maintainers of the "Bitnami Charts" project, we pledge to respect everyone who contributes by posting issues, updating documentation, submitting pull requests, providing feedback in comments, and any other activities. Communication through any of Bitnami's channels (GitHub, mailing lists, Twitter, and so on) must be constructive and never resort to personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct. We promise to extend courtesy and respect to everyone involved in this project, regardless of gender, gender identity, sexual orientation, disability, age, race, ethnicity, religion, or level of experience. We expect anyone contributing to the "Bitnami Charts" project to do the same. If any member of the community violates this code of conduct, the maintainers of the "Bitnami Charts" project may take action, including removing issues, comments, and PRs or blocking accounts, as deemed appropriate. If you are subjected to or witness unacceptable behavior, or have any other concerns, please communicate with us. If you have suggestions to improve this Code of Conduct, please submit an issue or PR. **Attribution** This Code of Conduct is adapted from the Angular project available at this page: https://github.com/angular/code-of-conduct/blob/master/CODE_OF_CONDUCT.md ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: Makefile ================================================ # Originally taken from https://github.com/bitnami-labs/kube-manifests/, # trimmed down to only run lib testing. # # Provides 'test' target. Uses docker. all: @echo make tests tests: make -C tests .PHONY: all tests ================================================ FILE: README.md ================================================ ## WARNING: This repository is no longer actively maintained by Bitnami/VMware. We have made the difficult decision to stop driving this project and therefore we will no longer actively respond to issues or pull requests. There is a new initiative to continue maintaining this project outside of Bitnami/VMware at https://github.com/kube-libsonnet/kube-libsonnet [![Build Status](https://travis-ci.org/bitnami-labs/kube-libsonnet.svg?branch=master)](https://travis-ci.org/bitnami-labs/kube-libsonnet) # kube-libsonnet This repo has been originally populated by the `lib/` folder contents from `https://github.com/bitnami-labs/kube-manifests` as of Mar/2018, aiming to provide a library of `jsonnet` manifests for common Kubernetes objects (such as `Deployment`, `Service`, `Ingress`, etc). Accordingly, above `kube-manifests` has been changed to use this repo as a git submodule, i.e.: $ git submodule add https://github.com/bitnami-labs/kube-libsonnet $ cat .gitmodules [submodule "lib"] path = lib url = https://github.com/bitnami-labs/kube-libsonnet ## Testing Unit and e2e-ish testing at tests/, needs usable `docker-compose` setup at node, will run a `k3s` "dummy" container to serve Kube API, "enough" to run `kubecfg validate` against it: make tests If you don't want that full kube-api stack (will then use your "local" kubernetes configured environment), you can run: make -C tests local-tests kube-validate ================================================ FILE: SECURITY.md ================================================ # Security Release Process The community has adopted this security disclosure and response policy to ensure we responsibly handle critical issues. ## Supported Versions For a list of support versions that this project will potentially create security fixes for, please refer to the Releases page on this project's GitHub and/or project related documentation on release cadence and support. ## Reporting a Vulnerability - Private Disclosure Process Security is of the highest importance and all security vulnerabilities or suspected security vulnerabilities should be reported to this project privately, to minimize attacks against current users before they are fixed. Vulnerabilities will be investigated and patched on the next patch (or minor) release as soon as possible. This information could be kept entirely internal to the project. If you know of a publicly disclosed security vulnerability for this project, please **IMMEDIATELY** contact the maintainers of this project privately. The use of encrypted email is encouraged. **IMPORTANT: Do not file public issues on GitHub for security vulnerabilities** To report a vulnerability or a security-related issue, please contact the maintainers with enough details through one of the following channels: * Directly via their individual email addresses * Open a [GitHub Security Advisory](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing/privately-reporting-a-security-vulnerability). This allows for anyone to report security vulnerabilities directly and privately to the maintainers via GitHub. Note that this option may not be present for every repository. The report will be fielded by the maintainers who have committer and release permissions. Feedback will be sent within 3 business days, including a detailed plan to investigate the issue and any potential workarounds to perform in the meantime. Do not report non-security-impacting bugs through this channel. Use GitHub issues for all non-security-impacting bugs. ## Proposed Report Content Provide a descriptive title and in the description of the report include the following information: * Basic identity information, such as your name and your affiliation or company. * Detailed steps to reproduce the vulnerability (POC scripts, screenshots, and logs are all helpful to us). * Description of the effects of the vulnerability on this project and the related hardware and software configurations, so that the maintainers can reproduce it. * How the vulnerability affects this project's usage and an estimation of the attack surface, if there is one. * List other projects or dependencies that were used in conjunction with this project to produce the vulnerability. ## When to report a vulnerability * When you think this project has a potential security vulnerability. * When you suspect a potential vulnerability but you are unsure that it impacts this project. * When you know of or suspect a potential vulnerability on another project that is used by this project. ## Patch, Release, and Disclosure The maintainers will respond to vulnerability reports as follows: 1. The maintainers will investigate the vulnerability and determine its effects and criticality. 2. If the issue is not deemed to be a vulnerability, the maintainers will follow up with a detailed reason for rejection. 3. The maintainers will initiate a conversation with the reporter within 3 business days. 4. If a vulnerability is acknowledged and the timeline for a fix is determined, the maintainers will work on a plan to communicate with the appropriate community, including identifying mitigating steps that affected users can take to protect themselves until the fix is rolled out. 5. The maintainers will also create a [Security Advisory](https://docs.github.com/en/code-security/repository-security-advisories/publishing-a-repository-security-advisory) using the [CVSS Calculator](https://www.first.org/cvss/calculator/3.0), if it is not created yet. The maintainers make the final call on the calculated CVSS; it is better to move quickly than making the CVSS perfect. Issues may also be reported to [Mitre](https://cve.mitre.org/) using this [scoring calculator](https://nvd.nist.gov/vuln-metrics/cvss/v3-calculator). The draft advisory will initially be set to private. 6. The maintainers will work on fixing the vulnerability and perform internal testing before preparing to roll out the fix. 7. Once the fix is confirmed, the maintainers will patch the vulnerability in the next patch or minor release, and backport a patch release into all earlier supported releases. ## Public Disclosure Process The maintainers publish the public advisory to this project's community via GitHub. In most cases, additional communication via Slack, Twitter, mailing lists, blog, and other channels will assist in educating the project's users and rolling out the patched release to affected users. The maintainers will also publish any mitigating steps users can take until the fix can be applied to their instances. This project's distributors will handle creating and publishing their own security advisories. ## Confidentiality, integrity and availability We consider vulnerabilities leading to the compromise of data confidentiality, elevation of privilege, or integrity to be our highest priority concerns. Availability, in particular in areas relating to DoS and resource exhaustion, is also a serious security concern. The maintainer team takes all vulnerabilities, potential vulnerabilities, and suspected vulnerabilities seriously and will investigate them in an urgent and expeditious manner. Note that we do not currently consider the default settings for this project to be secure-by-default. It is necessary for operators to explicitly configure settings, role based access control, and other resource related features in this project to provide a hardened environment. We will not act on any security disclosure that relates to a lack of safe defaults. Over time, we will work towards improved safe-by-default configuration, taking into account backwards compatibility. ================================================ FILE: bitnami.libsonnet ================================================ // Generic stuff is in kube.libsonnet - this file contains // bitnami-specific conventions. local kube = import "kube.libsonnet"; local perCloudSvcAnnotations(cloud, internal, service) = ( { aws: { "service.beta.kubernetes.io/aws-load-balancer-connection-draining-enabled": "true", "service.beta.kubernetes.io/aws-load-balancer-connection-draining-timeout": std.toString(service.target_pod.spec.terminationGracePeriodSeconds), // Use PROXY protocol (nginx supports this too) "service.beta.kubernetes.io/aws-load-balancer-proxy-protocol": "*", // Does LB do NAT or DSR? (OnlyLocal implies DSR) // https://kubernetes.io/docs/tutorials/services/source-ip/ // NB: Don't enable this without modifying set-real-ip-from above! // Not supported on aws in k8s 1.5 - immediate close / serves 503s. //"service.beta.kubernetes.io/external-traffic": "OnlyLocal", }, gke: {}, }[cloud] + if internal then { aws: { "service.beta.kubernetes.io/aws-load-balancer-internal": "0.0.0.0/0", }, gke: { "cloud.google.com/load-balancer-type": "internal", }, }[cloud] else {} ); local perCloudSvcSpec(cloud) = ( { aws: {}, // Required to get real src IP address, which also allows proper // ingress.kubernetes.io/whitelist-source-range matching gke: { externalTrafficPolicy: "Local" }, }[cloud] ); { ElbService(name, cloud, internal): kube.Service(name) { local service = self, metadata+: { annotations+: perCloudSvcAnnotations(cloud, internal, service), }, spec+: { type: "LoadBalancer" } + perCloudSvcSpec(cloud), }, Ingress(name, class=null): kube.Ingress(name) { local ing = self, host:: error "host required", target_svc:: error "target_svc required", // Default to single-service - override if you want something else. paths:: [ { path: "/", backend: ing.target_svc.name_port, pathType: "ImplementationSpecific", }, ], secretName:: "%s-cert" % [ing.metadata.name], // cert_provider can either be: // - "cm-dns": cert-manager using route53 for ACME dns-01 challenge (default) // - "cm-http": cert-manager using ACME http, requires public ingress cert_provider:: $.CertManager.default_ingress_provider, metadata+: $.CertManager.IngressMeta[ing.cert_provider] { annotations+: { // Add ingress class iff specified [if class != null then "kubernetes.io/ingress.class" else null]: class, }, }, spec+: { tls: [ { hosts: std.set([r.host for r in ing.spec.rules]), secretName: ing.secretName, }, ], rules: [ { host: ing.host, http: { paths: ing.paths, }, }, ], }, }, PromScrape(port): { local scrape = self, prom_path:: "/metrics", metadata+: { annotations+: { "prometheus.io/scrape": "true", "prometheus.io/port": std.toString(port), "prometheus.io/path": scrape.prom_path, }, }, }, PodZoneAntiAffinityAnnotation(pod): { podAntiAffinity: { preferredDuringSchedulingIgnoredDuringExecution: [ { weight: 50, podAffinityTerm: { labelSelector: { matchLabels: pod.metadata.labels }, topologyKey: "failure-domain.beta.kubernetes.io/zone", }, }, { weight: 100, podAffinityTerm: { labelSelector: { matchLabels: pod.metadata.labels }, topologyKey: "kubernetes.io/hostname", }, }, ], }, }, CertManager:: { // Deployed cluster issuers' names: cluster_issuers:: { acme_dns:: "letsencrypt-prod-dns", acme_http:: "letsencrypt-prod-http", in_cluster:: "in-cluster-issuer", }, default_ingress_provider:: "cm-dns", IngressMeta:: { "cm-dns":: { annotations+: { "cert-manager.io/cluster-issuer": $.CertManager.cluster_issuers.acme_dns, }, }, "cm-http":: { annotations+: { "cert-manager.io/cluster-issuer": $.CertManager.cluster_issuers.acme_http, }, }, }, // CertManager ClusterIssuer object ClusterIssuer(name):: kube._Object("cert-manager.io/v1alpha2", "ClusterIssuer", name), // CertManager Certificate object Certificate(name):: kube._Object("cert-manager.io/v1alpha2", "Certificate", name) { assert std.objectHas(self.metadata, "namespace") : "Certificate('%s') must set metadata.namespace" % self.metadata.name, }, InCluster:: { // Broadest usage is ["any"], limit to mTLS usage: default_usages:: ["digital signature", "key encipherment"], // Ref to our in-cluster ClusterIssuer cluster_issuer:: $.CertManager.ClusterIssuer($.CertManager.cluster_issuers.in_cluster) { spec+: { selfSigned: {}, }, }, // Use as: // my_cert: kube.CertManager.InCluster.Certificate("my-tls-cert", "my-namespace") // to get a Kubernetes TLS secret named "my-tls-cert" in "my-namespace" Certificate(name, namespace):: $.CertManager.Certificate(name) { metadata+: { namespace: namespace }, spec+: { secretName: name, issuerRef: kube.CrossVersionObjectReference($.CertManager.InCluster.cluster_issuer) { // issuerRef doesn't have the apiVersion field apiVersion:: null, }, commonName: name, dnsNames: [ name, "%s.%s" % [name, namespace], "%s.%s.svc" % [name, namespace], ], usages: $.CertManager.InCluster.default_usages, }, }, }, }, } ================================================ FILE: examples/guestbook/guestbook.jsonnet ================================================ // Copyright 2017 The kubecfg authors // // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Simple to demonstrate kubecfg using kube-libsonnet // This should not necessarily be considered a model jsonnet example // to build upon. // This is a simple port to jsonnet of the standard guestbook example // https://github.com/kubernetes/kubernetes/tree/master/examples/guestbook // // ``` // kubecfg update guestbook.jsonnet // // # poke at // - $(minikube service frontend), etc // - kubectl proxy # then visit http://localhost:8001/api/v1/namespaces/default/services/frontend/proxy/ // kubecfg delete guestbook.jsonnet // ``` local kube = import "../../kube.libsonnet"; { frontend_deployment: kube.Deployment("frontend") { spec+: { local my_spec = self, replicas: 3, template+: { spec+: { containers_+: { gb_fe: kube.Container("gb-frontend") { image: "gcr.io/google-samples/gb-frontend:v4", resources: { requests: { cpu: "100m", memory: "100Mi" } }, env_+: { GET_HOSTS_FROM: "dns", NUMBER_REPLICAS: my_spec.replicas, }, ports_+: { http: { containerPort: 80 } }, }}}}}}, frontend_service: kube.Service("frontend") { target_pod: $.frontend_deployment.spec.template, // spec+: { type: "LoadBalancer" }, }, redis_master_deployment: kube.Deployment("redis-master") { spec+: { template+: { spec+: { containers_+: { redis_master: kube.Container("redis-master") { image: "gcr.io/google_containers/redis:e2e", resources: { requests: { cpu: "100m", memory: "100Mi" } }, ports_+: { redis: { containerPort: 6379 }, }}}}}}}, redis_master_service: kube.Service("redis-master") { target_pod: $.redis_master_deployment.spec.template, }, redis_slave_deployment: kube.Deployment("redis-slave") { spec+: { replicas: 2, template+: { spec+: { containers_+: { redis_slave: kube.Container("redist-slave") { image: "gcr.io/google_samples/gb-redisslave:v1", resources: { requests: { cpu: "100m", memory: "100Mi" }, }, env_: { GET_HOSTS_FROM: "dns", }, ports_+: { redis: { containerPort: 6379 }, }}}}}}}, redis_slave_service: kube.Service("redis-slave") { target_pod: $.redis_slave_deployment.spec.template, }, } ================================================ FILE: examples/wordpress/backend.jsonnet ================================================ local kube = import "../../kube.libsonnet"; local labels = { tier: "backend", }; { backend: { secret: kube.Secret("mariadb") { metadata+: { labels+: labels, }, data_+: { "database_name": "webserver_db", "database_user": "webserver_user", "database_password": "webserver_db_password", "replication_user": "replica_user", "replication_password": "replica_password", "root_user": "root_user", "root_password": "root_password" }}, master: { local masterLabels = labels + { component: "master", }, statefulset: kube.StatefulSet("mariadb-master") { metadata+: { labels+: masterLabels, }, spec+: { template+: { spec+: { securityContext: { runAsUser: 1001, fsGroup: 1001, }, containers_+: { default: kube.Container("mariadb") { image: "bitnami/mariadb", ports_+: { mysql: { containerPort: 3306 } }, env_+: { MARIADB_REPLICATION_MODE: "master", MARIADB_REPLICATION_USER: kube.SecretKeyRef($.backend.secret, "replication_user"), MARIADB_REPLICATION_PASSWORD: kube.SecretKeyRef($.backend.secret, "replication_password"), MARIADB_ROOT_USER: kube.SecretKeyRef($.backend.secret, "root_user"), MARIADB_ROOT_PASSWORD: kube.SecretKeyRef($.backend.secret, "root_password"), MARIADB_USER: kube.SecretKeyRef($.backend.secret, "database_user"), MARIADB_DATABASE: kube.SecretKeyRef($.backend.secret, "database_name"), MARIADB_PASSWORD: kube.SecretKeyRef($.backend.secret, "database_password"), }, livenessProbe: { initialDelaySeconds: 40, exec: { command: [ "sh", "-c", "exec mysqladmin status -u$MARIADB_ROOT_USER -p$MARIADB_ROOT_PASSWORD", ]}}, readinessProbe: self.livenessProbe { initialDelaySeconds: 30, }, volumeMounts_+: { "mariadb-data": { "mountPath": "/bitnami/mariadb", }}}, metrics: kube.Container("metrics") { image: "prom/mysqld-exporter:v0.10.0", command: [ "sh", "-c", "DATA_SOURCE_NAME=\"$MARIADB_ROOT_USER:$MARIADB_ROOT_PASSWORD@(localhost:3306)/\" exec /bin/mysqld_exporter", ], ports_+: { metrics: { containerPort: 9104 } }, env_+: { MARIADB_ROOT_USER: kube.SecretKeyRef($.backend.secret, "root_user"), MARIADB_ROOT_PASSWORD: kube.SecretKeyRef($.backend.secret, "root_password"), }, livenessProbe: { initialDelaySeconds: 15, timeoutSeconds: 1, httpGet: { path: "/metrics", port: 9104, }}, readinessProbe: self.livenessProbe { initialDelaySeconds: 5, timeoutSeconds: 1, }}}}}, volumeClaimTemplates_+: { "mariadb-data": { storage: "10Gi", metadata+: { labels+: masterLabels, }}}}}, service: kube.Service("mariadb-master") { metadata+: { labels+: masterLabels, annotations+: { "prometheus.io/scrape": "true", "prometheus.io/port": "9104", }}, target_pod: $.backend.master.statefulset.spec.template, spec+: { ports: [ { name: "mariadb", port: 3306, targetPort: $.backend.master.statefulset.spec.template.spec.containers[0].ports[0].containerPort, }, { name: "metrics", port: 9104, targetPort: $.backend.master.statefulset.spec.template.spec.containers[1].ports[0].containerPort, }]}}}, slave: { local slaveLabels = labels + { component: "slave", }, statefulset: kube.StatefulSet("mariadb-slave") { metadata+: { labels+: slaveLabels, }, spec+: { template+: { spec+: { securityContext: { runAsUser: 1001, fsGroup: 1001, }, containers_+: { default: kube.Container("mariadb") { image: "bitnami/mariadb", ports_+: { mysql: { containerPort: 3306 } }, env_+: { MARIADB_REPLICATION_MODE: "slave", MARIADB_REPLICATION_USER: kube.SecretKeyRef($.backend.secret, "replication_user"), MARIADB_REPLICATION_PASSWORD: kube.SecretKeyRef($.backend.secret, "replication_password"), MARIADB_MASTER_HOST: $.backend.master.service.metadata.name, MARIADB_MASTER_ROOT_USER: kube.SecretKeyRef($.backend.secret, "root_user"), MARIADB_MASTER_ROOT_PASSWORD: kube.SecretKeyRef($.backend.secret, "root_password"), }, livenessProbe: { initialDelaySeconds: 40, exec: { command: [ "sh", "-c", "exec mysqladmin status -u$MARIADB_MASTER_ROOT_USER -p$MARIADB_MASTER_ROOT_PASSWORD", ]}}, readinessProbe: self.livenessProbe { initialDelaySeconds: 30, }, volumeMounts_+: { "mariadb-data": { "mountPath": "/bitnami/mariadb", }}}, metrics: kube.Container("metrics") { image: "prom/mysqld-exporter:v0.10.0", command: [ "sh", "-c", "DATA_SOURCE_NAME=\"$MARIADB_MASTER_ROOT_USER:$MARIADB_MASTER_ROOT_PASSWORD@(localhost:3306)/\" exec /bin/mysqld_exporter", ], ports_+: { metrics: { containerPort: 9104 } }, env_+: { MARIADB_MASTER_ROOT_USER: kube.SecretKeyRef($.backend.secret, "root_user"), MARIADB_MASTER_ROOT_PASSWORD: kube.SecretKeyRef($.backend.secret, "root_password"), }, livenessProbe: { initialDelaySeconds: 15, timeoutSeconds: 1, httpGet: { path: "/metrics", port: 9104, }}, readinessProbe: self.livenessProbe { initialDelaySeconds: 5, timeoutSeconds: 5, }}}}}, volumeClaimTemplates_+: { "mariadb-data": { storage: "10Gi", metadata+: { labels+: slaveLabels, }}}}}, service: kube.Service("mariadb-slave") { metadata+: { labels+: slaveLabels, annotations+: { "prometheus.io/scrape": "true", "prometheus.io/port": "9104", }}, target_pod: $.backend.slave.statefulset.spec.template, spec+: { ports: [ { name: "mariadb", port: 3306, targetPort: $.backend.slave.statefulset.spec.template.spec.containers[0].ports[0].containerPort, }, { name: "metrics", port: 9104, targetPort: $.backend.slave.statefulset.spec.template.spec.containers[1].ports[0].containerPort, }]}}}}} ================================================ FILE: examples/wordpress/frontend.jsonnet ================================================ local kube = import "../../kube.libsonnet"; local be = import "backend.jsonnet"; local labels = { tier: "frontend", }; { frontend: { pvc: kube.PersistentVolumeClaim("wordpress") { metadata+: { labels+: labels, }, storage:: "10Gi", }, configmap: kube.ConfigMap("wordpress") { metadata+: { labels+: labels, }, data: { "admin_first_name": "Admin", "admin_last_name": "User", "blog_name": "Kubernetes blog!", }}, secret: kube.Secret("wordpress") { metadata+: { labels+: labels, }, data_+: { "user": "user", "password": "bitnami", "mail": "user@example.com", }}, deployment: kube.Deployment("wordpress") { metadata+: { labels+: labels, }, spec+: { template+: { spec+: { containers_+: { default: kube.Container("wordpress") { image: "bitnami/wordpress", ports_+: { http: { containerPort: 80 } }, env_+: { MARIADB_HOST: be.backend.master.service.metadata.name, WORDPRESS_DATABASE_USER: kube.SecretKeyRef(be.backend.secret, "database_user"), WORDPRESS_DATABASE_NAME: kube.SecretKeyRef(be.backend.secret, "database_name"), WORDPRESS_DATABASE_PASSWORD: kube.SecretKeyRef(be.backend.secret, "database_password"), WORDPRESS_USERNAME: kube.SecretKeyRef($.frontend.secret, "user"), WORDPRESS_EMAIL: kube.SecretKeyRef($.frontend.secret, "mail"), WORDPRESS_PASSWORD: kube.SecretKeyRef($.frontend.secret, "password"), WORDPRESS_BLOG_NAME: kube.ConfigMapRef($.frontend.configmap, "blog_name"), WORDPRESS_FIRST_NAME: kube.ConfigMapRef($.frontend.configmap, "admin_first_name"), WORDPRESS_LAST_NAME: kube.ConfigMapRef($.frontend.configmap, "admin_last_name"), }, livenessProbe: { initialDelaySeconds: 120, httpGet: { path: "/wp-login.php", port: 80 }}, readinessProbe: self.livenessProbe { initialDelaySeconds: 60, }, volumeMounts_+: { "wordpress-data": { "mountPath": "/bitnami", }}}}, volumes_+: { "wordpress-data": { "persistentVolumeClaim": { "claimName": "wordpress", }}}}}}}, service: kube.Service("wordpress") { metadata+: { labels+: labels, }, target_pod: $.frontend.deployment.spec.template, }}} ================================================ FILE: examples/wordpress/wordpress.jsonnet ================================================ // Copyright (c) 2018 Bitnami // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // ``` // kubecfg update wordpress.jsonnet // // kubecfg delete wordpress.jsonnet // ``` local kube = import "../../kube.libsonnet"; local fe = import "frontend.jsonnet"; local be = import "backend.jsonnet"; local findObjs(top) = std.flattenArrays([ if (std.objectHas(v, "apiVersion") && std.objectHas(v, "kind")) then [v] else findObjs(v) for v in kube.objectValues(top) ]); kube.List() { items_+: { frontend: fe, backend: be, }, items: findObjs(self.items_), } ================================================ FILE: kube-platforms.libsonnet ================================================ // Extend kube.libsonnet for platform specific CRDs, drop-in usage as: // // local kube = import "kube-platforms.jsonnet"; // { // my_deploy: kube.Deployment(...) { ... } // my_gke_cert: kube.gke.ManagedCertificate(...) { ... } // } (import "kube.libsonnet") { gke:: { ManagedCertificate(name): $._Object("networking.gke.io/v1beta1", "ManagedCertificate", name) { spec: { domains: error "spec.domains array is required", }, assert std.length(self.spec.domains) > 0 : "ManagedCertificate '%s' spec.domains array must not be empty" % self.metadata.name, }, BackendConfig(name): $._Object("cloud.google.com/v1beta1", "BackendConfig", name) { spec: {}, }, }, } ================================================ FILE: kube.libsonnet ================================================ // Generic library of Kubernetes objects (https://github.com/bitnami-labs/kube-libsonnet) // // Objects in this file follow the regular Kubernetes API object // schema with two exceptions: // // ## Optional helpers // // A few objects have defaults or additional "helper" hidden // (double-colon) fields that will help with common situations. For // example, `Service.target_pod` generates suitable `selector` and // `ports` blocks for the common case of a single-pod/single-port // service. If for some reason you don't want the helper, just // provide explicit values for the regular Kubernetes fields that the // helper *would* have generated, and the helper logic will be // ignored. // // ## The Underscore Convention: // // Various constructs in the Kubernetes API use JSON arrays to // represent unordered sets or named key/value maps. This is // particularly annoying with jsonnet since we want to use jsonnet's // powerful object merge operation with these constructs. // // To combat this, this library attempts to provide more "jsonnet // native" variants of these arrays in alternative hidden fields that // end with an underscore. For example, the `env_` block in // `Container`: // ``` // kube.Container("foo") { // env_: { FOO: "bar" }, // } // ``` // ... produces the expected `container.env` JSON array: // ``` // { // "env": [ // { "name": "FOO", "value": "bar" } // ] // } // ``` // // If you are confused by the underscore versions, or don't want them // in your situation then just ignore them and set the regular // non-underscore field as usual. // // // ## TODO // // TODO: Expand this to include all API objects. // // Should probably fill out all the defaults here too, so jsonnet can // reference them. In addition, jsonnet validation is more useful // (client-side, and gives better line information). { // In case you may want/need to skip assertions for speed reasons (rather big configmaps/etc), // load the library with e.g. // local kube = (import "lib/kube.libsonnet") { _assert:: false }; _assert:: true, // resource contructors will use kinds/versions/fields compatible at least with version: minKubeVersion: { major: 1, minor: 19, version: "%s.%s" % [self.major, self.minor], }, // Returns array of values from given object. Does not include hidden fields. objectValues(o):: [o[field] for field in std.objectFields(o)], // Returns array of [key, value] pairs from given object. Does not include hidden fields. objectItems(o):: [[k, o[k]] for k in std.objectFields(o)], // Replace all occurrences of `_` with `-`. hyphenate(s):: std.join("-", std.split(s, "_")), // Convert an octal (as a string) to number, parseOctal(s):: ( local len = std.length(s); local leading = std.substr(s, 0, len - 1); local last = std.parseInt(std.substr(s, len - 1, 1)); assert (!$._assert) || last < 8 : "found '%s' digit >= 8" % [last]; last + (if len > 1 then 8 * $.parseOctal(leading) else 0) ), // Convert {foo: {a: b}} to [{name: foo, a: b}] mapToNamedList(o):: [{ name: $.hyphenate(n) } + o[n] for n in std.objectFields(o)], // Return object containing only these fields elements filterMapByFields(o, fields): { [field]: o[field] for field in std.setInter(std.objectFields(o), fields) }, // Convert from SI unit suffixes to regular number siToNum(n):: ( local convert = if std.endsWith(n, "m") then [1, 0.001] else if std.endsWith(n, "K") then [1, 1e3] else if std.endsWith(n, "M") then [1, 1e6] else if std.endsWith(n, "G") then [1, 1e9] else if std.endsWith(n, "T") then [1, 1e12] else if std.endsWith(n, "P") then [1, 1e15] else if std.endsWith(n, "E") then [1, 1e18] else if std.endsWith(n, "Ki") then [2, std.pow(2, 10)] else if std.endsWith(n, "Mi") then [2, std.pow(2, 20)] else if std.endsWith(n, "Gi") then [2, std.pow(2, 30)] else if std.endsWith(n, "Ti") then [2, std.pow(2, 40)] else if std.endsWith(n, "Pi") then [2, std.pow(2, 50)] else if std.endsWith(n, "Ei") then [2, std.pow(2, 60)] else error "Unknown numerical suffix in " + n; local n_len = std.length(n); std.parseInt(std.substr(n, 0, n_len - convert[0])) * convert[1] ), local remap(v, start, end, newstart) = if v >= start && v <= end then v - start + newstart else v, local remapChar(c, start, end, newstart) = std.char(remap( std.codepoint(c), std.codepoint(start), std.codepoint(end), std.codepoint(newstart) )), toLower(s):: ( std.join("", [remapChar(c, "A", "Z", "a") for c in std.stringChars(s)]) ), toUpper(s):: ( std.join("", [remapChar(c, "a", "z", "A") for c in std.stringChars(s)]) ), boolXor(x, y):: ((if x then 1 else 0) + (if y then 1 else 0) == 1), _Object(apiVersion, kind, name):: { local this = self, apiVersion: apiVersion, kind: kind, metadata: { name: name, labels: { name: std.join("-", std.split(this.metadata.name, ":")) }, annotations: {}, }, }, List(): { apiVersion: "v1", kind: "List", items_:: {}, items: $.objectValues(self.items_), }, Namespace(name): $._Object("v1", "Namespace", name) { }, Endpoints(name): $._Object("v1", "Endpoints", name) { Ip(addr):: { ip: addr }, Port(p):: { port: p }, subsets: [], }, Service(name): $._Object("v1", "Service", name) { local service = self, target_pod:: error "service target_pod required", container_index:: 0, port:: self.target_pod.spec.containers[service.container_index].ports[0].containerPort, // Helpers that format host:port in various ways host:: "%s.%s.svc" % [self.metadata.name, self.metadata.namespace], host_colon_port:: "%s:%s" % [self.host, self.spec.ports[0].port], http_url:: "http://%s/" % self.host_colon_port, proxy_urlpath:: "/api/v1/proxy/namespaces/%s/services/%s/" % [ self.metadata.namespace, self.metadata.name, ], // Useful in Ingress rules // This has been adapted for Ingress with apiVersion: networking.k8s.io/v1 name_port:: { local this = self, default_port:: service.spec.ports[0], port_spec:: if std.objectHas(this.default_port, "name") then { name: this.default_port.name } else { number: this.default_port.port }, service+: { name: service.metadata.name, port+: this.port_spec, }, assert (!$._assert) || $.boolXor( std.objectHas(this.port_spec, "name"), std.objectHas(this.port_spec, "number") ) : "Service '%s' name_port: `name` and `number` are mutually exclusive for Ingress spec" % name, }, spec: { selector: service.target_pod.metadata.labels, ports: [ { port: service.port, name: service.target_pod.spec.containers[service.container_index].ports[0].name, targetPort: service.target_pod.spec.containers[service.container_index].ports[0].containerPort, }, ], type: "ClusterIP", }, }, PersistentVolume(name): $._Object("v1", "PersistentVolume", name) { spec: {}, }, // TODO: This is a terrible name PersistentVolumeClaimVolume(pvc): { persistentVolumeClaim: { claimName: pvc.metadata.name }, }, StorageClass(name): $._Object("storage.k8s.io/v1", "StorageClass", name) { provisioner: error "provisioner required", }, PersistentVolumeClaim(name): $._Object("v1", "PersistentVolumeClaim", name) { local pvc = self, storageClass:: null, storage:: error "storage required", spec: { resources: { requests: { storage: pvc.storage, }, }, accessModes: ["ReadWriteOnce"], [if pvc.storageClass != null then "storageClassName"]: pvc.storageClass, }, }, Container(name): { name: name, image: error "container image value required", imagePullPolicy: if std.endsWith(self.image, ":latest") then "Always" else "IfNotPresent", envList(map):: [ if std.type(map[x]) == "object" then { name: x, valueFrom: map[x], } else { // Let `null` value stay as such (vs string-ified) name: x, value: if map[x] == null then null else std.toString(map[x]), } for x in std.objectFields(map) ], env_:: {}, env: self.envList(self.env_), args_:: {}, args: ["--%s=%s" % kv for kv in $.objectItems(self.args_)], ports_:: {}, ports: $.mapToNamedList(self.ports_), volumeMounts_:: {}, volumeMounts: $.mapToNamedList(self.volumeMounts_), stdin: false, tty: false, assert (!$._assert) || (!self.tty || self.stdin) : "tty=true requires stdin=true", }, PodDisruptionBudget(name): $._Object("policy/v1beta1", "PodDisruptionBudget", name) { local this = self, target_pod:: error "target_pod required", spec: { assert (!$._assert) || $.boolXor( std.objectHas(self, "minAvailable"), std.objectHas(self, "maxUnavailable") ) : "PDB '%s': exactly one of minAvailable/maxUnavailable required" % name, selector: { matchLabels: this.target_pod.metadata.labels, }, }, }, Pod(name): $._Object("v1", "Pod", name) { spec: $.PodSpec, }, PodSpec: { // The 'first' container is used in various defaults in k8s. local container_names = std.objectFields(self.containers_), default_container:: if std.length(container_names) > 1 then "default" else container_names[0], containers_:: {}, local container_names_ordered = [self.default_container] + [n for n in container_names if n != self.default_container], containers: ( assert (!$._assert) || std.length(self.containers_) > 0 : "Pod must have at least one container (via containers_ map)"; [{ name: $.hyphenate(name) } + self.containers_[name] for name in container_names_ordered if self.containers_[name] != null] ), // Note initContainers are inherently ordered, and using this // named object will lose that ordering. If order matters, then // manipulate `initContainers` directly (perhaps // appending/prepending to `super.initContainers` to mix+match // both approaches) initContainers_:: {}, initContainers: [{ name: $.hyphenate(name) } + self.initContainers_[name] for name in std.objectFields(self.initContainers_) if self.initContainers_[name] != null], volumes_:: {}, volumes: $.mapToNamedList(self.volumes_), imagePullSecrets: [], terminationGracePeriodSeconds: 30, assert (!$._assert) || std.length(self.containers) > 0 : "Pod must have at least one container (via containers array)", // Return an array of pod's ports numbers ports(proto):: [ p.containerPort for p in std.flattenArrays([ c.ports for c in self.containers ]) if ( (!(std.objectHas(p, "protocol")) && proto == "TCP") || ((std.objectHas(p, "protocol")) && p.protocol == proto) ) ], }, EmptyDirVolume(): { emptyDir: {}, }, HostPathVolume(path, type=""): { hostPath: { path: path, type: type }, }, GitRepoVolume(repository, revision): { gitRepo: { repository: repository, // "master" is possible, but should be avoided for production revision: revision, }, }, SecretVolume(secret): { secret: { secretName: secret.metadata.name }, }, ConfigMapVolume(configmap): { configMap: { name: configmap.metadata.name }, }, ConfigMap(name): $._Object("v1", "ConfigMap", name) { data: {}, // I keep thinking data values can be any JSON type. This check // will remind me that they must be strings :( local nonstrings = [ k for k in std.objectFields(self.data) if std.type(self.data[k]) != "string" ], assert (!$._assert) || std.length(nonstrings) == 0 : "data contains non-string values: %s" % [nonstrings], }, // subtype of EnvVarSource ConfigMapRef(configmap, key): { assert (!$._assert) || std.objectHas(configmap.data, key) : "ConfigMap '%s' doesn't have '%s' field in configmap.data" % [configmap.metadata.name, key], configMapKeyRef: { name: configmap.metadata.name, key: key, }, }, Secret(name): $._Object("v1", "Secret", name) { local secret = self, type: "Opaque", data_:: {}, data: { [k]: std.base64(secret.data_[k]) for k in std.objectFields(secret.data_) }, }, // subtype of EnvVarSource SecretKeyRef(secret, key): { assert (!$._assert) || std.objectHas(secret.data, key) : "Secret '%s' doesn't have '%s' field in secret.data" % [secret.metadata.name, key], secretKeyRef: { name: secret.metadata.name, key: key, }, }, // subtype of EnvVarSource FieldRef(key): { fieldRef: { apiVersion: "v1", fieldPath: key, }, }, // subtype of EnvVarSource ResourceFieldRef(key, divisor="1"): { resourceFieldRef: { resource: key, divisor: std.toString(divisor), }, }, Deployment(name): $._Object("apps/v1", "Deployment", name) { local deployment = self, spec: { template: { spec: $.PodSpec, metadata: { labels: deployment.metadata.labels, annotations: {}, }, }, selector: { matchLabels: deployment.spec.template.metadata.labels, }, strategy: { type: "RollingUpdate", local pvcs = [ v for v in deployment.spec.template.spec.volumes if std.objectHas(v, "persistentVolumeClaim") ], local is_stateless = std.length(pvcs) == 0, // Apps trying to maintain a majority quorum or similar will // want to tune these carefully. // NB: Upstream default is surge=1 unavail=1 rollingUpdate: if is_stateless then { maxSurge: "25%", // rounds up maxUnavailable: "25%", // rounds down } else { // Poor-man's StatelessSet. Useful mostly with replicas=1. maxSurge: 0, maxUnavailable: 1, }, }, // NB: Upstream default is 0 minReadySeconds: 30, // NB: Regular k8s default is to keep all revisions revisionHistoryLimit: 10, }, }, CrossVersionObjectReference(target): { apiVersion: target.apiVersion, kind: target.kind, name: target.metadata.name, }, HorizontalPodAutoscaler(name): $._Object("autoscaling/v1", "HorizontalPodAutoscaler", name) { local hpa = self, target:: error "target required", spec: { scaleTargetRef: $.CrossVersionObjectReference(hpa.target), minReplicas: hpa.target.spec.replicas, maxReplicas: error "maxReplicas required", assert (!$._assert) || self.maxReplicas >= self.minReplicas, }, }, StatefulSet(name): $._Object("apps/v1", "StatefulSet", name) { local sset = self, spec: { serviceName: name, updateStrategy: { type: "RollingUpdate", rollingUpdate: { partition: 0, }, }, template: { spec: $.PodSpec, metadata: { labels: sset.metadata.labels, annotations: {}, }, }, selector: { matchLabels: sset.spec.template.metadata.labels, }, volumeClaimTemplates_:: {}, volumeClaimTemplates: [ // StatefulSet is overly fussy about "changes" (even when // they're no-ops). // In particular annotations={} is apparently a "change", // since the comparison is ignorant of defaults. std.prune($.PersistentVolumeClaim($.hyphenate(kv[0])) + { apiVersion:: null, kind:: null } + kv[1]) for kv in $.objectItems(self.volumeClaimTemplates_) ], replicas: 1, assert (!$._assert) || self.replicas >= 1, }, }, Job(name): $._Object("batch/v1", "Job", name) { local job = self, spec: $.JobSpec { template+: { metadata+: { labels: job.metadata.labels, }, }, }, }, CronJob(name): $._Object("batch/v1beta1", "CronJob", name) { local cronjob = self, spec: { jobTemplate: { spec: $.JobSpec { template+: { metadata+: { labels: cronjob.metadata.labels, }, }, }, }, schedule: error "Need to provide spec.schedule", successfulJobsHistoryLimit: 10, failedJobsHistoryLimit: 20, // NB: upstream concurrencyPolicy default is "Allow" concurrencyPolicy: "Forbid", }, }, JobSpec: { local this = self, template: { spec: $.PodSpec { restartPolicy: "OnFailure", }, }, completions: 1, parallelism: 1, }, DaemonSet(name): $._Object("apps/v1", "DaemonSet", name) { local ds = self, spec: { updateStrategy: { type: "RollingUpdate", rollingUpdate: { maxUnavailable: 1, }, }, template: { metadata: { labels: ds.metadata.labels, annotations: {}, }, spec: $.PodSpec, }, selector: { matchLabels: ds.spec.template.metadata.labels, }, }, }, Ingress(name): $._Object("networking.k8s.io/v1", "Ingress", name) { spec: {}, local rel_paths = [ p.path for r in self.spec.rules for p in r.http.paths if std.objectHas(p, "path") && !std.startsWith(p.path, "/") ], assert (!$._assert) || std.length(rel_paths) == 0 : "paths must be absolute: " + rel_paths, }, ThirdPartyResource(name): $._Object("extensions/v1beta1", "ThirdPartyResource", name) { versions_:: [], versions: [{ name: n } for n in self.versions_], }, CustomResourceDefinition(group, version, kind): { local this = self, apiVersion: "apiextensions.k8s.io/v1", kind: "CustomResourceDefinition", metadata+: { name: this.spec.names.plural + "." + this.spec.group, }, spec: { scope: "Namespaced", group: group, versions_:: { // Create an opinionated default_spec for the version, easy to override by the user, // specially if they had several versions to derived from the same "skeleton". default_spec:: { served: true, storage: true, schema: { openAPIV3Schema: { type: "object", properties: { spec: { type: "object", }, }, }, }, }, [version]: self.default_spec, }, versions: $.mapToNamedList(self.versions_), names: { kind: kind, singular: $.toLower(self.kind), plural: self.singular + "s", listKind: self.kind + "List", }, }, }, ServiceAccount(name): $._Object("v1", "ServiceAccount", name) { }, Role(name): $._Object("rbac.authorization.k8s.io/v1", "Role", name) { rules: [], }, ClusterRole(name): $.Role(name) { kind: "ClusterRole", }, Group(name): { kind: "Group", name: name, apiGroup: "rbac.authorization.k8s.io", }, User(name): { kind: "User", name: name, apiGroup: "rbac.authorization.k8s.io", }, RoleBinding(name): $._Object("rbac.authorization.k8s.io/v1", "RoleBinding", name) { local rb = self, subjects_:: [], subjects: [{ kind: o.kind, namespace: o.metadata.namespace, name: o.metadata.name, } for o in self.subjects_], roleRef_:: error "roleRef is required", roleRef: { apiGroup: "rbac.authorization.k8s.io", kind: rb.roleRef_.kind, name: rb.roleRef_.metadata.name, }, }, ClusterRoleBinding(name): $.RoleBinding(name) { kind: "ClusterRoleBinding", }, // NB: encryptedData can be imported into a SealedSecret as follows: // kubectl get secret ... -ojson mysec | kubeseal | jq -r .spec.encryptedData > sealedsecret.json // encryptedData: std.parseJson(importstr "sealedsecret.json") SealedSecret(name): $._Object("bitnami.com/v1alpha1", "SealedSecret", name) { spec: { encryptedData: {}, }, assert (!$._assert) || std.length(std.objectFields(self.spec.encryptedData)) != 0 : "SealedSecret '%s' has empty encryptedData field" % name, }, // NB: helper method to access several Kubernetes objects podRef, // used below to extract its labels podRef(obj):: ({ Pod: obj, Deployment: obj.spec.template, StatefulSet: obj.spec.template, DaemonSet: obj.spec.template, Job: obj.spec.template, CronJob: obj.spec.jobTemplate.spec.template, }[obj.kind]), // NB: return a { podSelector: ... } ready to use for e.g. NSPs (see below) // pod labels can be optionally filtered by their label name 2nd array arg podLabelsSelector(obj, filter=null):: { podSelector: std.prune({ matchLabels: if filter != null then $.filterMapByFields($.podRef(obj).metadata.labels, filter) else $.podRef(obj).metadata.labels, }), }, // NB: Returns an array as [{ port: num, protocol: "PROTO" }, {...}, ... ] // Need to split TCP, UDP logic to be able to dedup each set of protocol ports podsPorts(obj_list):: std.flattenArrays([ [ { port: port, protocol: protocol } for port in std.set( std.flattenArrays([$.podRef(obj).spec.ports(protocol) for obj in obj_list]) ) ] for protocol in ["TCP", "UDP"] ]), // NB: most of the "helper" stuff comes from above (podLabelsSelector, podsPorts), // NetworkPolicy returned object will have "Ingress", "Egress" policyTypes auto-set // based on populated spec.ingress or spec.egress // See tests/test-simple-validate.jsonnet for example(s). NetworkPolicy(name): $._Object("networking.k8s.io/v1", "NetworkPolicy", name) { local networkpolicy = self, spec: { policyTypes: std.prune([ if networkpolicy.spec.ingress != [] then "Ingress" else null, if networkpolicy.spec.egress != [] then "Egress" else null, ]), ingress: $.objectValues(self.ingress_), ingress_:: {}, egress: $.objectValues(self.egress_), egress_:: {}, podSelector: {}, }, }, VerticalPodAutoscaler(name):: $._Object("autoscaling.k8s.io/v1beta2", "VerticalPodAutoscaler", name) { local vpa = self, target:: error "target required", spec+: { targetRef: $.CrossVersionObjectReference(vpa.target), updatePolicy: { updateMode: "Auto", }, }, }, // Helper function to ease VPA creation as e.g.: // foo_vpa:: kube.createVPAFor($.foo_deploy) createVPAFor(target, mode="Auto"):: $.VerticalPodAutoscaler(target.metadata.name) { target:: target, metadata+: { namespace: target.metadata.namespace, labels+: target.metadata.labels, }, spec+: { updatePolicy+: { updateMode: mode, }, }, }, } ================================================ FILE: tests/Dockerfile ================================================ FROM bitnami/minideb:buster MAINTAINER sre@bitnami.com ARG jsonnet_version=0.14.0 ARG kubectl_version=v1.17.0 ARG kubecfg_version=v0.14.0 RUN install_packages jq make curl ca-certificates RUN adduser --home /home/user --disabled-password --gecos User user RUN curl -sLo /tmp/jsonnet-v${jsonnet_version}.tar.gz https://github.com/google/jsonnet/releases/download/v${jsonnet_version}/jsonnet-bin-v${jsonnet_version}-linux.tar.gz RUN tar -zxf /tmp/jsonnet-v${jsonnet_version}.tar.gz -C /tmp && mv /tmp/jsonnet /tmp/jsonnetfmt /usr/local/bin RUN curl -sLo /usr/local/bin/kubectl https://storage.googleapis.com/kubernetes-release/release/${kubectl_version}/bin/linux/amd64/kubectl RUN chmod +x /usr/local/bin/kubectl RUN curl -sLo /usr/local/bin/kubecfg https://github.com/bitnami/kubecfg/releases/download/${kubecfg_version}/kubecfg-linux-amd64 RUN chmod +x /usr/local/bin/kubecfg USER user WORKDIR /home/user CMD ["/bin/bash", "-l"] ================================================ FILE: tests/Makefile ================================================ # K3S_ as a "mapping" from KUBE_MAJOR_RELEASE to k3s tag, # from https://hub.docker.com/r/rancher/k3s/tags # K3S_V1_13=v0.3.0 # no-more to support v1.18 previous Ingress apiVersion deprecation K3S_V1_14=v0.8.0 K3S_V1_15=v0.9.0 K3S_V1_16=v1.0.1 # Since v1.17 k3s image tags follow Kubernetes releases numbering K3S_V1_17=v1.17.17-k3s1 K3S_V1_18=v1.18.19-k3s1 K3S_V1_19=v1.19.11-k3s1 K3S_V1_20=v1.20.7-k3s1 K3S_V1_21=v1.21.1-k3s1 K3S_V1_22=v1.22.2-k3s1 # # Since https://github.com/bitnami-labs/kube-libsonnet/issues/32 we only support # kubernetes v1.14+ (Ingress apiVersion deprecated in v1.18, available since v1.14). # # Kubernetes releases we cover with e2e testing, # we'll run `docker-compose` for each to below rancher/k3s versions (tags) E2E_K3S_VERSIONS=$(K3S_V1_19) $(K3S_V1_20) $(K3S_V1_21) $(K3S_V1_22) SHELL=/bin/bash # Rather arbitrary Bitnami style choice JSONNET_FMT=--indent 2 --string-style d --comment-style s --no-pad-arrays --pad-objects --pretty-field-names LIB_JSONNET=$(wildcard ../*.libsonnet) # jsonnet manifests for kube-validate target, exercising `kubecfg validate ...` # against a live Kube API endpoint ALL_K8S_VALIDATE_JSONNET=$(wildcard *-validate.pass.jsonnet) # Filenames provide expected testing result (pass|fail) PASS_JSONNET=$(sort $(wildcard test*pass.jsonnet)) FAIL_JSONNET=$(sort $(wildcard test*fail.jsonnet)) UNIT_JSONNET=$(sort $(wildcard unittest*.jsonnet)) ALL_JSONNET=$(sort $(wildcard *.jsonnet)) # Phony %.diff and golden/*.json targets PHONY_GOLDEN=$(patsubst %.jsonnet,golden/%.json,$(PASS_JSONNET)) PHONY_DIFF=$(patsubst %.jsonnet,%.diff,$(PASS_JSONNET)) # Phony %.eval-pass and %.eval-fail targets PHONY_EVAL_PASS=$(patsubst %.jsonnet,%.eval-pass,$(PASS_JSONNET)) PHONY_EVAL_FAIL=$(patsubst %.jsonnet,%.eval-fail,$(FAIL_JSONNET)) ## NOTE: below values need to be in-sync with docker-compose.yaml ## (not automated to avoid over-engineering the manifests) DOCKER_E2E=e2e-test TMP_RANCHER=./tmp-rancher PROJECT=kubelibsonnet # All tests, run from docker-compose built docker containers, # to (also) avoid the need for local tools install, see ./Dockerfile tests: $(patsubst %,e2e-tests-%,$(E2E_K3S_VERSIONS)) @echo "SUCCESS: verified Kubernetes versions:" @cat $(TMP_RANCHER)/report.txt @rm -rf ./$(TMP_RANCHER) # These target is dynamically driven from e2e-test-% above, run as # e.g. `e2e-tests-v1.17.2-k3s1` e2e-tests-%: req-docker req-docker-compose install -d $(TMP_RANCHER)/root/etc && touch $(TMP_RANCHER)/root/etc/k3s.yaml env USERID=$$(id -u) K3S_VERSION=$(*) docker-compose -p $(PROJECT) up -d rc=$$(timeout 60s docker wait $(DOCKER_E2E)) || rc=255 ;\ test $$rc -ne 0 && docker logs k3s-api;\ docker logs $(DOCKER_E2E); \ exit $$rc @# Peek e2e test output for Kubernetes versions tested docker logs $(DOCKER_E2E)| egrep '^Server.Version.+' | sort -u >> $(TMP_RANCHER)/report.txt docker-compose -p $(PROJECT) down rm -rf ./$(TMP_RANCHER)/root # Tests safe to run without a live Kube API endpoint, # but still requiring local `jsonnet` install local-tests: unittests lint assertion-tests golden-diff # NB: unittest jsonnet files are also covered by eval-pass and diff targets, # called out here for convenience unittests: req-jsonnet jsonnet $(UNIT_JSONNET) lint: req-jsonnetfmt @set -e; errs=0; \ for f in $(ALL_JSONNET) $(LIB_JSONNET); do \ if ! jsonnetfmt --test $(JSONNET_FMT) -- $$f; then \ echo "FAILED lint: $$f" >&2; \ errs=$$(( $$errs + 1 )); \ fi; \ done; \ if [ $$errs -gt 0 ]; then \ echo "NOTE: if the 'lint' target fails, run:"; \ echo " $(MAKE) fix-lint lint"; \ exit 1; \ fi assertion-tests: req-jsonnet $(PHONY_EVAL_PASS) $(PHONY_EVAL_FAIL) golden-diff: diff-help $(PHONY_DIFF) # Used to initialize docker'ized KubeAPI via k3s kube-init: req-kubectl req-kubecfg kubectl version --short | grep k3s # void falsely initializing live clusters kubecfg update init-kube.jsonnet kube-validate: req-kubectl req-kubecfg timeout 10 kubectl api-versions > /dev/null \ || { echo "WARNING: no usable runtime kube context, skipping."; exit 0 ;} \ && kubectl version --short && kubecfg version && kubecfg validate --ignore-unknown=false $(ALL_K8S_VALIDATE_JSONNET) %.diff: %.jsonnet diff -u golden/$(*).json <(jsonnet $(<)) %.eval-pass: %.jsonnet @echo "INFO: must PASS: $(<)" @(jsonnet $(<) > /dev/null) @echo "OK[PASS]: $(<)" %.eval-fail: %.jsonnet @echo "INFO: must FAIL: $(<)" @echo -n "| "; (jsonnet $(<) > /dev/null) 2>&1 | grep RUNTIME.ERROR @echo "OK[FAIL]: $(<)" golden/%.json: %.jsonnet jsonnet $(<) > $(@) diff-help: @echo "NOTE: if the 'golden-diff' target fails, review output and run:" @echo " $(MAKE) gen-golden golden-diff" @echo fix-lint: req-jsonnetfmt @set -e; \ for f in $(ALL_JSONNET) $(LIB_JSONNET); do \ echo jsonnetfmt -i $(JSONNET_FMT) -- $$f; \ jsonnetfmt -i $(JSONNET_FMT) -- $$f; \ done req-%: @which $(*) >/dev/null && exit 0; echo "ERROR: '$(*)' is required in PATH"; exit 1 gen-golden: $(PHONY_GOLDEN) .PHONY: unittests lint eval-pass eval-fail validate golden-diff %.eval-pass %.evail-fail %.diff golden/%.json diff-help fix-lint gen-golden ================================================ FILE: tests/docker-compose.yaml ================================================ version: "3" services: kube-api: image: rancher/k3s:${K3S_VERSION} command: server --disable-agent container_name: k3s-api # Volume mapping to "capture" k3s admin credentials # (aka "admin.conf" KUBECONFIG) volumes: - ./tmp-rancher/root:/.kube - ./tmp-rancher/root:/.rancher - ./tmp-rancher/root/etc:/etc/rancher/k3s expose: - 6443 user: "${USERID}" environment: - USER=nobody - HOME=/ tmpfs: - /var/run - /run - /tmp e2e-test: build: . container_name: e2e-test links: - "kube-api:kube-api" depends_on: - kube-api volumes: - ./tmp-rancher/root:/tmp/rancher - ..:/work working_dir: /work environment: - HOME=/ user: "${USERID}" command: - tests/k3s-e2e-test.sh ================================================ FILE: tests/golden/init-kube.json ================================================ { "vpa_crd": { "apiVersion": "apiextensions.k8s.io/v1beta1", "kind": "CustomResourceDefinition", "metadata": { "name": "verticalpodautoscalers.autoscaling.k8s.io" }, "spec": { "group": "autoscaling.k8s.io", "names": { "kind": "VerticalPodAutoscaler", "listKind": "VerticalPodAutoscalerList", "plural": "verticalpodautoscalers", "singular": "verticalpodautoscaler" }, "scope": "Namespaced", "version": "v1beta1", "versions": [ { "name": "v1beta1", "served": true, "storage": false }, { "name": "v1beta2", "served": true, "storage": true } ] } } } ================================================ FILE: tests/golden/test-Ingress-2ndport.pass.json ================================================ { "deploy": { "apiVersion": "apps/v1", "kind": "Deployment", "metadata": { "annotations": { }, "labels": { "name": "test-Ingress-pass-deploy" }, "name": "test-Ingress-pass-deploy" }, "spec": { "minReadySeconds": 30, "replicas": 1, "revisionHistoryLimit": 10, "selector": { "matchLabels": { "name": "test-Ingress-pass-deploy" } }, "strategy": { "rollingUpdate": { "maxSurge": "25%", "maxUnavailable": "25%" }, "type": "RollingUpdate" }, "template": { "metadata": { "annotations": { }, "labels": { "name": "test-Ingress-pass-deploy" } }, "spec": { "containers": [ { "args": [ ], "env": [ ], "image": "nginx:1.12", "imagePullPolicy": "IfNotPresent", "name": "test-Ingress-pass", "ports": [ { "containerPort": 80, "name": "http" }, { "containerPort": 9099, "name": "metrics" } ], "stdin": false, "tty": false, "volumeMounts": [ ] } ], "imagePullSecrets": [ ], "initContainers": [ ], "terminationGracePeriodSeconds": 30, "volumes": [ ] } } } }, "ingress": { "apiVersion": "networking.k8s.io/v1", "kind": "Ingress", "metadata": { "annotations": { "cert-manager.io/cluster-issuer": "letsencrypt-prod-dns" }, "labels": { "name": "test-Ingress-pass-ingress" }, "name": "test-Ingress-pass-ingress" }, "spec": { "rules": [ { "host": "foo.g.dev.bitnami.net", "http": { "paths": [ { "backend": { "service": { "name": "test-Ingress-pass-svc", "port": { "name": "metrics" } } }, "path": "/", "pathType": "ImplementationSpecific" } ] } } ], "tls": [ { "hosts": [ "foo.g.dev.bitnami.net" ], "secretName": "test-Ingress-pass-ingress-cert" } ] } }, "pod": { "apiVersion": "v1", "kind": "Pod", "metadata": { "annotations": { }, "labels": { "name": "test-Ingress-pass-pod" }, "name": "test-Ingress-pass-pod" }, "spec": { "containers": [ { "args": [ ], "env": [ ], "image": "nginx:1.12", "imagePullPolicy": "IfNotPresent", "name": "test-Ingress-pass", "ports": [ { "containerPort": 80, "name": "http" }, { "containerPort": 9099, "name": "metrics" } ], "stdin": false, "tty": false, "volumeMounts": [ ] } ], "imagePullSecrets": [ ], "initContainers": [ ], "terminationGracePeriodSeconds": 30, "volumes": [ ] } }, "service": { "apiVersion": "v1", "kind": "Service", "metadata": { "annotations": { }, "labels": { "name": "test-Ingress-pass-svc" }, "name": "test-Ingress-pass-svc" }, "spec": { "ports": [ { "name": "http", "port": 80, "targetPort": 80 } ], "selector": { "name": "test-Ingress-pass-deploy" }, "type": "ClusterIP" } } } ================================================ FILE: tests/golden/test-Ingress-port_num_only.pass.json ================================================ { "deploy": { "apiVersion": "apps/v1", "kind": "Deployment", "metadata": { "annotations": { }, "labels": { "name": "test-Ingress-pass-deploy" }, "name": "test-Ingress-pass-deploy" }, "spec": { "minReadySeconds": 30, "replicas": 1, "revisionHistoryLimit": 10, "selector": { "matchLabels": { "name": "test-Ingress-pass-deploy" } }, "strategy": { "rollingUpdate": { "maxSurge": "25%", "maxUnavailable": "25%" }, "type": "RollingUpdate" }, "template": { "metadata": { "annotations": { }, "labels": { "name": "test-Ingress-pass-deploy" } }, "spec": { "containers": [ { "args": [ ], "env": [ ], "image": "nginx:1.12", "imagePullPolicy": "IfNotPresent", "name": "test-Ingress-pass", "ports": [ { "containerPort": 80, "name": "http" }, { "containerPort": 9099, "name": "metrics" } ], "stdin": false, "tty": false, "volumeMounts": [ ] } ], "imagePullSecrets": [ ], "initContainers": [ ], "terminationGracePeriodSeconds": 30, "volumes": [ ] } } } }, "ingress": { "apiVersion": "networking.k8s.io/v1", "kind": "Ingress", "metadata": { "annotations": { "cert-manager.io/cluster-issuer": "letsencrypt-prod-dns" }, "labels": { "name": "test-Ingress-pass-ingress" }, "name": "test-Ingress-pass-ingress" }, "spec": { "rules": [ { "host": "foo.g.dev.bitnami.net", "http": { "paths": [ { "backend": { "service": { "name": "test-Ingress-pass-svc", "port": { "number": 4242 } } }, "path": "/", "pathType": "ImplementationSpecific" } ] } } ], "tls": [ { "hosts": [ "foo.g.dev.bitnami.net" ], "secretName": "test-Ingress-pass-ingress-cert" } ] } }, "pod": { "apiVersion": "v1", "kind": "Pod", "metadata": { "annotations": { }, "labels": { "name": "test-Ingress-pass-pod" }, "name": "test-Ingress-pass-pod" }, "spec": { "containers": [ { "args": [ ], "env": [ ], "image": "nginx:1.12", "imagePullPolicy": "IfNotPresent", "name": "test-Ingress-pass", "ports": [ { "containerPort": 80, "name": "http" }, { "containerPort": 9099, "name": "metrics" } ], "stdin": false, "tty": false, "volumeMounts": [ ] } ], "imagePullSecrets": [ ], "initContainers": [ ], "terminationGracePeriodSeconds": 30, "volumes": [ ] } }, "service": { "apiVersion": "v1", "kind": "Service", "metadata": { "annotations": { }, "labels": { "name": "test-Ingress-pass-svc" }, "name": "test-Ingress-pass-svc" }, "spec": { "ports": [ { "name": "http", "port": 80, "targetPort": 80 } ], "selector": { "name": "test-Ingress-pass-deploy" }, "type": "ClusterIP" } } } ================================================ FILE: tests/golden/test-SealedSecret.pass.json ================================================ { "apiVersion": "v1", "items": [ { "apiVersion": "bitnami.com/v1alpha1", "kind": "SealedSecret", "metadata": { "annotations": { }, "labels": { "name": "foo" }, "name": "foo" }, "spec": { "encryptedData": { "another_key": "dGVzdAo=", "some_key": "dGVzdAo=" } } } ], "kind": "List" } ================================================ FILE: tests/golden/test-Service-container_index.pass.json ================================================ { "deploy": { "apiVersion": "apps/v1", "kind": "Deployment", "metadata": { "annotations": { }, "labels": { "name": "test-Service-pass-deploy" }, "name": "test-Service-pass-deploy" }, "spec": { "minReadySeconds": 30, "replicas": 1, "revisionHistoryLimit": 10, "selector": { "matchLabels": { "name": "test-Service-pass-deploy" } }, "strategy": { "rollingUpdate": { "maxSurge": "25%", "maxUnavailable": "25%" }, "type": "RollingUpdate" }, "template": { "metadata": { "annotations": { }, "labels": { "name": "test-Service-pass-deploy" } }, "spec": { "containers": [ { "args": [ ], "env": [ ], "image": "nginx:1.12", "imagePullPolicy": "IfNotPresent", "name": "test-Service-pass-default", "ports": [ { "containerPort": 80, "name": "http" }, { "containerPort": 9099, "name": "metrics" } ], "stdin": false, "tty": false, "volumeMounts": [ ] }, { "args": [ ], "env": [ ], "image": "nginx:1.12", "imagePullPolicy": "IfNotPresent", "name": "test-Service-pass-sidecar", "ports": [ { "containerPort": 80, "name": "http-sidecar" }, { "containerPort": 9099, "name": "metrics" } ], "stdin": false, "tty": false, "volumeMounts": [ ] } ], "imagePullSecrets": [ ], "initContainers": [ ], "terminationGracePeriodSeconds": 30, "volumes": [ ] } } } }, "pod": { "apiVersion": "v1", "kind": "Pod", "metadata": { "annotations": { }, "labels": { "name": "test-Service-pass-pod" }, "name": "test-Service-pass-pod" }, "spec": { "containers": [ { "args": [ ], "env": [ ], "image": "nginx:1.12", "imagePullPolicy": "IfNotPresent", "name": "test-Service-pass-default", "ports": [ { "containerPort": 80, "name": "http" }, { "containerPort": 9099, "name": "metrics" } ], "stdin": false, "tty": false, "volumeMounts": [ ] }, { "args": [ ], "env": [ ], "image": "nginx:1.12", "imagePullPolicy": "IfNotPresent", "name": "test-Service-pass-sidecar", "ports": [ { "containerPort": 80, "name": "http-sidecar" }, { "containerPort": 9099, "name": "metrics" } ], "stdin": false, "tty": false, "volumeMounts": [ ] } ], "imagePullSecrets": [ ], "initContainers": [ ], "terminationGracePeriodSeconds": 30, "volumes": [ ] } }, "service": { "apiVersion": "v1", "kind": "Service", "metadata": { "annotations": { }, "labels": { "name": "test-Service-pass-svc" }, "name": "test-Service-pass-svc" }, "spec": { "ports": [ { "name": "http-sidecar", "port": 80, "targetPort": 80 } ], "selector": { "name": "test-Service-pass-deploy" }, "type": "ClusterIP" } } } ================================================ FILE: tests/golden/test-gke-ManagedCertificate.pass.json ================================================ { "apiVersion": "v1", "items": [ { "apiVersion": "networking.gke.io/v1beta1", "kind": "ManagedCertificate", "metadata": { "annotations": { }, "labels": { "name": "foo" }, "name": "foo" }, "spec": { "domains": [ "foo.example.com" ] } } ], "kind": "List" } ================================================ FILE: tests/golden/test-simple-validate.pass.json ================================================ { "apiVersion": "v1", "items": [ { "apiVersion": "v1", "data": { "foo_key": "bar_val" }, "kind": "ConfigMap", "metadata": { "annotations": { }, "labels": { "name": "foo-config" }, "name": "foo-config", "namespace": "foons" } }, { "apiVersion": "batch/v1beta1", "kind": "CronJob", "metadata": { "annotations": { }, "labels": { "name": "foo-cronjob" }, "name": "foo-cronjob", "namespace": "foons" }, "spec": { "concurrencyPolicy": "Forbid", "failedJobsHistoryLimit": 20, "jobTemplate": { "spec": { "completions": 1, "parallelism": 1, "template": { "metadata": { "labels": { "name": "foo-cronjob" } }, "spec": { "containers": [ { "args": [ ], "env": [ ], "image": "busybox", "imagePullPolicy": "IfNotPresent", "name": "foo", "ports": [ ], "stdin": false, "tty": false, "volumeMounts": [ ] } ], "imagePullSecrets": [ ], "initContainers": [ ], "restartPolicy": "OnFailure", "terminationGracePeriodSeconds": 30, "volumes": [ ] } } } }, "schedule": "0 * * * *", "successfulJobsHistoryLimit": 10 } }, { "apiVersion": "apps/v1", "kind": "Deployment", "metadata": { "annotations": { }, "labels": { "name": "foo-deploy" }, "name": "foo-deploy", "namespace": "foons" }, "spec": { "minReadySeconds": 30, "replicas": 1, "revisionHistoryLimit": 10, "selector": { "matchLabels": { "name": "foo-deploy" } }, "strategy": { "rollingUpdate": { "maxSurge": "25%", "maxUnavailable": "25%" }, "type": "RollingUpdate" }, "template": { "metadata": { "annotations": { }, "labels": { "name": "foo-deploy" } }, "spec": { "affinity": { "podAntiAffinity": { "preferredDuringSchedulingIgnoredDuringExecution": [ { "podAffinityTerm": { "labelSelector": { "matchLabels": { "name": "foo-deploy" } }, "topologyKey": "kubernetes.io/hostname" }, "weight": 70 }, { "podAffinityTerm": { "labelSelector": { "matchLabels": { "name": "foo-deploy" } }, "topologyKey": "failure-domain.beta.kubernetes.io/zone" }, "weight": 70 }, { "podAffinityTerm": { "labelSelector": { "matchLabels": { "name": "foo-deploy" } }, "topologyKey": "failure-domain.beta.kubernetes.io/region" }, "weight": 70 } ] } }, "containers": [ { "args": [ ], "env": [ { "name": "my_secret", "valueFrom": { "secretKeyRef": { "key": "sec_key", "name": "foo-secret" } } }, { "name": "other_key", "value": null } ], "image": "nginx:1.12", "imagePullPolicy": "IfNotPresent", "name": "foo", "ports": [ { "containerPort": 80, "name": "http" }, { "containerPort": 888, "name": "udp-port", "protocol": "UDP" } ], "stdin": false, "tty": false, "volumeMounts": [ { "mountPath": "/config", "name": "config-vol" } ] } ], "imagePullSecrets": [ ], "initContainers": [ ], "serviceAccountName": "foo-sa", "terminationGracePeriodSeconds": 30, "volumes": [ { "configMap": { "name": "foo-config" }, "name": "config-vol" } ] } } } }, { "apiVersion": "policy/v1beta1", "kind": "PodDisruptionBudget", "metadata": { "annotations": { }, "labels": { "name": "foo-deploy-pdb" }, "name": "foo-deploy-pdb" }, "spec": { "minAvailable": 1, "selector": { "matchLabels": { "name": "foo-deploy" } } } }, { "apiVersion": "autoscaling.k8s.io/v1beta2", "kind": "VerticalPodAutoscaler", "metadata": { "annotations": { }, "labels": { "name": "foo-deploy" }, "name": "foo-deploy", "namespace": "foons" }, "spec": { "targetRef": { "apiVersion": "apps/v1", "kind": "Deployment", "name": "foo-deploy" }, "updatePolicy": { "updateMode": "Auto" } } }, { "apiVersion": "apps/v1", "kind": "DaemonSet", "metadata": { "annotations": { }, "labels": { "name": "foo-ds" }, "name": "foo-ds", "namespace": "foons" }, "spec": { "selector": { "matchLabels": { "name": "foo-ds" } }, "template": { "metadata": { "annotations": { }, "labels": { "name": "foo-ds" } }, "spec": { "containers": [ { "args": [ ], "env": [ { "name": "my_secret", "valueFrom": { "secretKeyRef": { "key": "sec_key", "name": "foo-secret" } } }, { "name": "other_key", "value": null } ], "image": "nginx:1.12", "imagePullPolicy": "IfNotPresent", "name": "foo", "ports": [ { "containerPort": 80, "name": "http" }, { "containerPort": 888, "name": "udp-port", "protocol": "UDP" } ], "stdin": false, "tty": false, "volumeMounts": [ { "mountPath": "/config", "name": "config-vol" } ] } ], "imagePullSecrets": [ ], "initContainers": [ ], "terminationGracePeriodSeconds": 30, "volumes": [ { "configMap": { "name": "foo-config" }, "name": "config-vol" } ] } }, "updateStrategy": { "rollingUpdate": { "maxUnavailable": 1 }, "type": "RollingUpdate" } } }, { "apiVersion": "networking.k8s.io/v1", "kind": "Ingress", "metadata": { "annotations": { "cert-manager.io/cluster-issuer": "letsencrypt-prod-dns" }, "labels": { "name": "foo-ingress" }, "name": "foo-ingress", "namespace": "foons" }, "spec": { "rules": [ { "host": "foo.g.dev.bitnami.net", "http": { "paths": [ { "backend": { "service": { "name": "foo-svc", "port": { "name": "http" } } }, "path": "/", "pathType": "ImplementationSpecific" } ] } } ], "tls": [ { "hosts": [ "foo.g.dev.bitnami.net" ], "secretName": "foo-ingress-cert" } ] } }, { "apiVersion": "batch/v1", "kind": "Job", "metadata": { "annotations": { }, "labels": { "name": "foo-job" }, "name": "foo-job", "namespace": "foons" }, "spec": { "completions": 1, "parallelism": 1, "template": { "metadata": { "labels": { "name": "foo-job" } }, "spec": { "containers": [ { "args": [ ], "env": [ ], "image": "busybox", "imagePullPolicy": "IfNotPresent", "name": "foo", "ports": [ ], "stdin": false, "tty": false, "volumeMounts": [ ] } ], "imagePullSecrets": [ ], "initContainers": [ ], "restartPolicy": "OnFailure", "terminationGracePeriodSeconds": 30, "volumes": [ ] } } } }, { "apiVersion": "v1", "kind": "Namespace", "metadata": { "annotations": { }, "labels": { "name": "foons" }, "name": "foons" } }, { "apiVersion": "networking.k8s.io/v1", "kind": "NetworkPolicy", "metadata": { "annotations": { }, "labels": { "name": "foo-nsp-pods" }, "name": "foo-nsp-pods", "namespace": "foons" }, "spec": { "egress": [ { "ports": [ { "port": 53, "protocol": "UDP" } ], "to": [ { "namespaceSelector": { "matchLabels": { "name": "kube-system" } } } ] }, { "ports": [ { "port": 80, "protocol": "TCP" }, { "port": 888, "protocol": "UDP" } ], "to": [ { "podSelector": { "matchLabels": { "name": "foo-sts" } } } ] } ], "ingress": [ { "from": [ { "podSelector": { "matchLabels": { "name": "foo-job" } } }, { "podSelector": { "matchLabels": { "name": "foo-cronjob" } } }, { "namespaceSelector": { "matchLabels": { "name": "nginx-ingress" } } } ], "ports": [ { "port": 80, "protocol": "TCP" }, { "port": 888, "protocol": "UDP" } ] } ], "podSelector": { "matchLabels": { "name": "foo-deploy" } }, "policyTypes": [ "Ingress", "Egress" ] } }, { "apiVersion": "networking.k8s.io/v1", "kind": "Ingress", "metadata": { "annotations": { }, "labels": { "name": "foo-pathless-ingress" }, "name": "foo-pathless-ingress", "namespace": "foons" }, "spec": { "rules": [ { "host": "a.example.com", "http": { "paths": [ { "backend": { "service": { "name": "service-a", "port": { "name": "web" } } }, "pathType": "ImplementationSpecific" } ] } }, { "host": "b.example.com", "http": { "paths": [ { "backend": { "service": { "name": "service-2", "port": { "name": "web" } } }, "pathType": "ImplementationSpecific" } ] } } ] } }, { "apiVersion": "v1", "kind": "Pod", "metadata": { "annotations": { }, "labels": { "name": "foo-pod" }, "name": "foo-pod", "namespace": "foons" }, "spec": { "containers": [ { "args": [ ], "env": [ { "name": "my_secret", "valueFrom": { "secretKeyRef": { "key": "sec_key", "name": "foo-secret" } } }, { "name": "other_key", "value": null } ], "image": "nginx:1.12", "imagePullPolicy": "IfNotPresent", "name": "foo", "ports": [ { "containerPort": 80, "name": "http" }, { "containerPort": 888, "name": "udp-port", "protocol": "UDP" } ], "stdin": false, "tty": false, "volumeMounts": [ { "mountPath": "/config", "name": "config-vol" } ] } ], "imagePullSecrets": [ ], "initContainers": [ ], "terminationGracePeriodSeconds": 30, "volumes": [ { "configMap": { "name": "foo-config" }, "name": "config-vol" } ] } }, { "apiVersion": "rbac.authorization.k8s.io/v1", "kind": "Role", "metadata": { "annotations": { }, "labels": { "name": "foo-role" }, "name": "foo-role", "namespace": "foons" }, "rules": [ { "apiGroups": [ "" ], "resources": [ "pods", "secrets", "configmaps", "persistentvolumeclaims" ], "verbs": [ "get" ] }, { "apiGroups": [ "" ], "resources": [ "pods" ], "verbs": [ "patch" ] } ] }, { "apiVersion": "rbac.authorization.k8s.io/v1", "kind": "RoleBinding", "metadata": { "annotations": { }, "labels": { "name": "foo-rolebinding" }, "name": "foo-rolebinding", "namespace": "foons" }, "roleRef": { "apiGroup": "rbac.authorization.k8s.io", "kind": "Role", "name": "foo-role" }, "subjects": [ { "kind": "ServiceAccount", "name": "foo-sa", "namespace": "foons" } ] }, { "apiVersion": "v1", "kind": "ServiceAccount", "metadata": { "annotations": { }, "labels": { "name": "foo-sa" }, "name": "foo-sa", "namespace": "foons" } }, { "apiVersion": "v1", "data": { "sec_key": "c2VjcmV0Cg==" }, "kind": "Secret", "metadata": { "annotations": { }, "labels": { "name": "foo-secret" }, "name": "foo-secret", "namespace": "foons" }, "type": "Opaque" }, { "apiVersion": "v1", "kind": "Service", "metadata": { "annotations": { }, "labels": { "name": "foo-svc" }, "name": "foo-svc", "namespace": "foons" }, "spec": { "ports": [ { "name": "http", "port": 80, "targetPort": 80 } ], "selector": { "name": "foo-deploy" }, "type": "ClusterIP" } }, { "apiVersion": "apps/v1", "kind": "StatefulSet", "metadata": { "annotations": { }, "labels": { "name": "foo-sts" }, "name": "foo-sts", "namespace": "foons" }, "spec": { "replicas": 1, "selector": { "matchLabels": { "name": "foo-sts" } }, "serviceName": "foo-sts", "template": { "metadata": { "annotations": { }, "labels": { "name": "foo-sts" } }, "spec": { "containers": [ { "args": [ ], "env": [ { "name": "my_secret", "valueFrom": { "secretKeyRef": { "key": "sec_key", "name": "foo-secret" } } }, { "name": "other_key", "value": null } ], "image": "nginx:1.12", "imagePullPolicy": "IfNotPresent", "name": "foo", "ports": [ { "containerPort": 80, "name": "http" }, { "containerPort": 888, "name": "udp-port", "protocol": "UDP" } ], "stdin": false, "tty": false, "volumeMounts": [ { "mountPath": "/config", "name": "config-vol" }, { "mountPath": "/foo/data", "name": "datadir" } ] } ], "imagePullSecrets": [ ], "initContainers": [ ], "serviceAccountName": "foo-sa", "terminationGracePeriodSeconds": 30, "volumes": [ { "configMap": { "name": "foo-config" }, "name": "config-vol" } ] } }, "updateStrategy": { "rollingUpdate": { "partition": 0 }, "type": "RollingUpdate" }, "volumeClaimTemplates": [ { "metadata": { "labels": { "name": "datadir" }, "name": "datadir", "namespace": "foons" }, "spec": { "accessModes": [ "ReadWriteOnce" ], "resources": { "requests": { "storage": "10Gi" } } } } ] } }, { "apiVersion": "cert-manager.io/v1alpha2", "kind": "Certificate", "metadata": { "annotations": { }, "labels": { "name": "foo-cert" }, "name": "foo-cert", "namespace": "foons" }, "spec": { "commonName": "foo-cert", "dnsNames": [ "foo-cert", "foo-cert.foons", "foo-cert.foons.svc" ], "issuerRef": { "kind": "ClusterIssuer", "name": "in-cluster-issuer" }, "secretName": "foo-cert", "usages": [ "digital signature", "key encipherment" ] } }, { "apiVersion": "autoscaling.k8s.io/v1beta2", "kind": "VerticalPodAutoscaler", "metadata": { "annotations": { }, "labels": { "name": "foo-vpa" }, "name": "foo-vpa" }, "spec": { "targetRef": { "apiVersion": "apps/v1", "kind": "Deployment", "name": "foo-deploy" }, "updatePolicy": { "updateMode": "Auto" } } } ], "kind": "List" } ================================================ FILE: tests/golden/unittests.pass.json ================================================ true ================================================ FILE: tests/init-kube.jsonnet ================================================ local kube = import "../kube.libsonnet"; local crds = { // A simplified VPA CRD from https://github.com/kubernetes/autoscaler/tree/master/vertical-pod-autoscaler vpa_crd: kube.CustomResourceDefinition("autoscaling.k8s.io", "v1beta1", "VerticalPodAutoscaler") { metadata+: { annotations: { "api-approved.kubernetes.io": "https://github.com/kubernetes/kubernetes/pull/78458", }, }, spec+: { versions_+: { v1beta2: self.default_spec { storage: false }, }, }, }, // Simplified cert-manager CRD from https://github.com/jetstack/cert-manager/blob/master/deploy/crds/crd-certificates.yaml, // enough to test bitnami.CertManager object(s) cm_certificate_crd: kube.CustomResourceDefinition("cert-manager.io", "v1alpha2", "Certificate"), }; crds ================================================ FILE: tests/k3s-e2e-test.sh ================================================ #!/bin/sh set -eu echo "INFO: Starting tests: unit, lint ..." (set -x make -C tests local-tests ) export KUBECONFIG=/tmp/kubeconfig echo "INFO: Waiting for kube-api to be available ..." # Busy loop waiting for: # - rancher/k3s to have written the k3s.yaml with admin creds # - kube API to be ready (set +e until kubectl get nodes; do sleep 1 # Found that k3s releases create k3s.yaml under diff paths, # redirecting stderr just to avoid red-herrings errors sed -e s/localhost/kube-api/ -e s/127.0.0.1/kube-api/ \ /tmp/rancher/k3s.yaml /tmp/rancher/etc/k3s.yaml \ > ${KUBECONFIG:?} 2>/dev/null done ) echo "INFO: initializing kube cluster: ..." (set -x make -C tests kube-init ) echo "INFO: Starting tests: test-kube ..." (set -x make -C tests kube-validate ) ================================================ FILE: tests/test-Ingress-2ndport.pass.jsonnet ================================================ local bitnami = import "../bitnami.libsonnet"; local kube = import "../kube.libsonnet"; local stack = { name:: "test-Ingress-pass", pod: kube.Pod($.name + "-pod") { spec+: { containers_+: { foo_cont: kube.Container($.name) { image: "nginx:1.12", ports_+: { http: { containerPort: 80 }, metrics: { containerPort: 9099 }, }, }, }, }, }, deploy: kube.Deployment($.name + "-deploy") { local this = self, spec+: { template+: { spec+: $.pod.spec { }, }, }, }, service: kube.Service($.name + "-svc") { local this = self, target_pod: $.deploy.spec.template, name_port+:: { // Override default_port to 2nd port instead of 1st (default), default_port:: this.target_pod.spec.containers[0].ports[1], }, }, ingress: bitnami.Ingress($.name + "-ingress") { host: "foo.g.dev.bitnami.net", target_svc: $.service, }, }; stack { // Assert we got 2nd port dubbed "metrics" for Ingress assert (stack.ingress.spec.rules[0].http.paths[0].backend.service.port == { name: "metrics" }), } ================================================ FILE: tests/test-Ingress-name_port.fail.jsonnet ================================================ local bitnami = import "../bitnami.libsonnet"; local kube = import "../kube.libsonnet"; local stack = { name:: "test-Ingress-fail", pod: kube.Pod($.name + "-pod") { spec+: { containers_+: { foo_cont: kube.Container($.name) { image: "nginx:1.12", ports_+: { http: { containerPort: 80 }, metrics: { containerPort: 9099 }, }, }, }, }, }, deploy: kube.Deployment($.name + "-deploy") { local this = self, spec+: { template+: { spec+: $.pod.spec { }, }, }, }, service: kube.Service($.name + "-svc") { local this = self, target_pod: $.deploy.spec.template, name_port+:: { // Force failing from having `name` _and_ `number` rendered for the // ingress spec (via name_port, see kube.libsonnet) port_spec+:: { number: 4242 }, }, }, ingress: bitnami.Ingress($.name + "-ingress") { host: "foo.g.dev.bitnami.net", target_svc: $.service, }, }; stack ================================================ FILE: tests/test-Ingress-port_num_only.pass.jsonnet ================================================ local bitnami = import "../bitnami.libsonnet"; local kube = import "../kube.libsonnet"; local stack = { name:: "test-Ingress-pass", pod: kube.Pod($.name + "-pod") { spec+: { containers_+: { foo_cont: kube.Container($.name) { image: "nginx:1.12", ports_+: { http: { containerPort: 80 }, metrics: { containerPort: 9099 }, }, }, }, }, }, deploy: kube.Deployment($.name + "-deploy") { local this = self, spec+: { template+: { spec+: $.pod.spec { }, }, }, }, service: kube.Service($.name + "-svc") { local this = self, target_pod: $.deploy.spec.template, name_port+:: { // Force port to _only_ be below number (note no `+::` construct) port_spec:: { number: 4242 }, }, }, ingress: bitnami.Ingress($.name + "-ingress") { host: "foo.g.dev.bitnami.net", target_svc: $.service, }, }; stack { // Assert we got expected port number for Ingress assert (stack.ingress.spec.rules[0].http.paths[0].backend.service.port == { number: 4242 }), } ================================================ FILE: tests/test-PDB-no-spec.fail.jsonnet ================================================ local kube = import "../kube.libsonnet"; local simple_validate = (import "test-simple-validate.pass.jsonnet").items_; simple_validate { deploy_pdb+: { spec+: { minAvailable:: null, }, }, } ================================================ FILE: tests/test-PDB-wrong-spec.fail.jsonnet ================================================ local kube = import "../kube.libsonnet"; local simple_validate = (import "test-simple-validate.pass.jsonnet").items_; simple_validate { deploy_pdb+: { spec+: { minAvailable: 1, maxUnavailable: 2, }, }, } ================================================ FILE: tests/test-Pod-no_containers_array.fail.jsonnet ================================================ local kube = import "../kube.libsonnet"; local simple_validate = (import "test-simple-validate.pass.jsonnet").items_; simple_validate { pod+: { spec+: { containers: [], }, }, } ================================================ FILE: tests/test-Pod-no_containers_map.fail.jsonnet ================================================ local kube = import "../kube.libsonnet"; local simple_validate = (import "test-simple-validate.pass.jsonnet").items_; simple_validate { pod+: { spec+: { containers_:: {}, }, }, } ================================================ FILE: tests/test-Pod-secretmount.fail.jsonnet ================================================ local kube = import "../kube.libsonnet"; local simple_validate = (import "test-simple-validate.pass.jsonnet").items_; simple_validate { pod+: { metadata+: { spec+: { containers_+: { foo_cont+: { env_+: { my_secret: kube.SecretKeyRef($.secret, "sec_key_nopes"), }, }, }, }, }, }, } ================================================ FILE: tests/test-SealedSecret.fail.jsonnet ================================================ local kube = import "../kube.libsonnet"; local stack = { sealedsecret: kube.SealedSecret("foo") { spec+: { bar: std.parseJson(importstr "test-sealedsecrets.json"), }, }, }; kube.List() { items_+: stack, } ================================================ FILE: tests/test-SealedSecret.pass.json ================================================ { "some_key": "dGVzdAo=", "another_key": "dGVzdAo=" } ================================================ FILE: tests/test-SealedSecret.pass.jsonnet ================================================ local kube = import "../kube.libsonnet"; local stack = { sealedsecret: kube.SealedSecret("foo") { spec+: { encryptedData: std.parseJson(importstr "test-SealedSecret.pass.json"), }, }, }; kube.List() { items_+: stack, } ================================================ FILE: tests/test-Service-container_index.fail.jsonnet ================================================ local bitnami = import "../bitnami.libsonnet"; local kube = import "../kube.libsonnet"; local stack = { name:: "test-Service-pass", pod: kube.Pod($.name + "-pod") { spec+: { containers_+: { default: kube.Container($.name + "-default") { image: "nginx:1.12", ports_+: { http: { containerPort: 80 }, metrics: { containerPort: 9099 }, }, }, sidecar: kube.Container($.name + "-sidecar") { image: "nginx:1.12", ports_+: { sidecar: { containerPort: 80 }, metrics: { containerPort: 9099 }, }, }, }, }, }, deploy: kube.Deployment($.name + "-deploy") { local this = self, spec+: { template+: { spec+: $.pod.spec, }, }, }, service: kube.Service($.name + "-svc") { local this = self, target_pod: $.deploy.spec.template, // Force fail by selecting an index out of range. container_index: 3, }, }; stack ================================================ FILE: tests/test-Service-container_index.pass.jsonnet ================================================ local bitnami = import "../bitnami.libsonnet"; local kube = import "../kube.libsonnet"; local stack = { name:: "test-Service-pass", pod: kube.Pod($.name + "-pod") { spec+: { containers_+: { default: kube.Container($.name + "-default") { image: "nginx:1.12", ports_+: { http: { containerPort: 80 }, metrics: { containerPort: 9099 }, }, }, sidecar: kube.Container($.name + "-sidecar") { image: "nginx:1.12", ports_+: { http_sidecar: { containerPort: 80 }, metrics: { containerPort: 9099 }, }, }, }, }, }, deploy: kube.Deployment($.name + "-deploy") { local this = self, spec+: { template+: { spec+: $.pod.spec, }, }, }, service: kube.Service($.name + "-svc") { local this = self, target_pod: $.deploy.spec.template, container_index: 1, }, }; stack { // Assert we got the 2nd containers port named "http" for service assert (stack.service.spec.ports[0].name == "http-sidecar") : "Expected service port http-sidecar.", } ================================================ FILE: tests/test-gke-ManagedCertificate.fail.jsonnet ================================================ local kube = import "../kube-platforms.libsonnet"; local stack = { foocert: kube.gke.ManagedCertificate("foo") { spec+: { domains: [], }, }, }; kube.List() { items_+: stack, } ================================================ FILE: tests/test-gke-ManagedCertificate.pass.jsonnet ================================================ local kube = import "../kube-platforms.libsonnet"; local stack = { foocert: kube.gke.ManagedCertificate("foo") { spec+: { domains: ["foo.example.com"], }, }, }; kube.List() { items_+: stack, } ================================================ FILE: tests/test-simple-validate.pass.jsonnet ================================================ local bitnami = import "../bitnami.libsonnet"; local kube = import "../kube.libsonnet"; local utils = import "../utils.libsonnet"; // Just a simple stack to exercise our kube.libsonnet // objects, output is saved to tests/golden/test-simple-validate.pass.json // to assert textual diff output. local stack = { namespace:: "foons", name:: "foo", ns: kube.Namespace($.namespace), sa: kube.ServiceAccount($.name + "-sa") { metadata+: { namespace: $.namespace }, }, role: kube.Role($.name + "-role") { metadata+: { namespace: $.namespace }, rules: [{ apiGroups: [""], resources: ["pods", "secrets", "configmaps", "persistentvolumeclaims"], verbs: ["get"], }, { apiGroups: [""], resources: ["pods"], verbs: ["patch"], }], }, rolebinding: kube.RoleBinding($.name + "-rolebinding") { metadata+: { namespace: $.namespace }, roleRef_: $.role, subjects_+: [$.sa], }, config: kube.ConfigMap($.name + "-config") { metadata+: { namespace: $.namespace }, data: { foo_key: "bar_val", }, }, secret: kube.Secret($.name + "-secret") { metadata+: { namespace: $.namespace }, data: { sec_key: "c2VjcmV0Cg==", }, }, // NB: making up an Ingress pointing to $.deploy Pod service: kube.Service($.name + "-svc") { metadata+: { namespace: $.namespace }, target_pod: $.deploy.spec.template, }, ingress: bitnami.Ingress($.name + "-ingress") { metadata+: { namespace: $.namespace }, host: "foo.g.dev.bitnami.net", target_svc: $.service, }, // An ingress with multiple hosts but none of the hosts specifies a path (#54). pathlessIngress: kube.Ingress($.name + "-pathless-ingress") { metadata+: { namespace: $.namespace }, spec+: { rules+: [ { host: "a.example.com", http: { paths: [{ pathType: "ImplementationSpecific", backend: { service: { name: "service-a", port: { name: "web", }, }, }, }], }, }, { host: "b.example.com", http: { paths: [{ pathType: "ImplementationSpecific", backend: { service: { name: "service-2", port: { name: "web", }, }, }, }], }, }, ], }, }, // NB: just a simple example pod pod: kube.Pod($.name + "-pod") { metadata+: { namespace: $.namespace }, spec+: { containers_+: { foo_cont: kube.Container($.name) { image: "nginx:1.12", env_+: { my_secret: kube.SecretKeyRef($.secret, "sec_key"), other_key: null, }, ports_+: { http: { containerPort: 80 }, udp_port: { containerPort: 888, protocol: "UDP" }, }, volumeMounts_+: { config_vol: { mountPath: "/config" }, }, }, }, volumes_+: { config_vol: kube.ConfigMapVolume($.config), }, }, }, // NB: all object below needing to spec a Pod will just // use above particular pod manifest just for convenience deploy: kube.Deployment($.name + "-deploy") { local this = self, metadata+: { namespace: $.namespace }, spec+: { template+: { spec+: $.pod.spec { affinity+: utils.weakNodeDiversity(this.spec.selector), serviceAccountName: $.sa.metadata.name, }, }, }, }, deploy_pdb: kube.PodDisruptionBudget($.name + "-deploy-pdb") { target_pod: $.deploy.spec.template, spec+: { minAvailable: 1, }, }, sts: kube.StatefulSet($.name + "-sts") { metadata+: { namespace: $.namespace }, spec+: { template+: { spec+: $.pod.spec { serviceAccountName: $.sa.metadata.name, containers_+: { foo_cont+: { volumeMounts_+: { datadir: { mountPath: "/foo/data" }, }, }, }, }, }, volumeClaimTemplates_+: { datadir: kube.PersistentVolumeClaim("datadir") { metadata+: { namespace: $.namespace }, storage: "10Gi", }, }, }, }, ds: kube.DaemonSet($.name + "-ds") { metadata+: { namespace: $.namespace }, spec+: { template+: { spec: $.pod.spec, }, }, }, job: kube.Job($.name + "-job") { metadata+: { namespace: $.namespace }, spec+: { template+: { spec+: { containers_+: { foo_cont: kube.Container($.name) { image: "busybox", }, }, }, }, }, }, cronjob: kube.CronJob($.name + "-cronjob") { metadata+: { namespace: $.namespace }, spec+: { jobTemplate+: { spec+: { template+: { spec+: { containers_+: { foo_cont: kube.Container($.name) { image: "busybox", }, }, }, }, }, }, schedule: "0 * * * *", }, }, // NB: create NSP from $.deploy Pod ref nsp_pods: kube.NetworkPolicy($.name + "-nsp-pods") { metadata+: { namespace: $.namespace }, // NB: $.deploy has unique "foo-deploy" label (as well as other // podLabelsSelector() arg) spec+: kube.podLabelsSelector($.deploy) { // NB: making up $.deploy needing to get reached by $job, $.cronjob // and nginx-ingress-controller (running in its own NS named "nginx-ingress" ingress_: { from_jobs_and_ingressctl: { from: [ kube.podLabelsSelector($.job), kube.podLabelsSelector($.cronjob), { namespaceSelector: { matchLabels: { name: "nginx-ingress" } } }, ], ports: kube.podsPorts([$.deploy]), }, }, // NB: making up $.deploy needing to connect to $.sts, and // "kube-system" NS for DNS services egress_: { to_sts: { to: [ kube.podLabelsSelector($.sts), ], ports: kube.podsPorts([$.sts]), }, to_kube_dns: { to: [ { namespaceSelector: { matchLabels: { name: "kube-system" } } }, ], ports: [{ port: 53, protocol: "UDP" }], }, }, }, }, // NB: these VPAs need the VPA CRD added to the cluster, for local k3s testing // we add it via the `init-kube` Makefile target using `init-kube.jsonnet` vpa: kube.VerticalPodAutoscaler($.name + "-vpa") { spec+: { targetRef: { apiVersion: "apps/v1", kind: "Deployment", name: "foo-deploy", }, }, }, deploy_vpa: kube.createVPAFor($.deploy), tls_cert: bitnami.CertManager.InCluster.Certificate("foo-cert", $.namespace), }; kube.List() { items_+: stack, } ================================================ FILE: tests/unittests.pass.jsonnet ================================================ local kube = import "../kube.libsonnet"; local utils = import "../utils.libsonnet"; local an_obj = kube._Object("v1", "Gentle", "foo"); local a_pod = kube.Pod("foo") { metadata+: { labels+: { foo: "bar", bar: "qxx" } }, spec+: { containers_+: { foo: kube.Container("foo") { image: "nginx", ports_: { http: { containerPort: 8080 }, https: { containerPort: 8443 }, udp: { containerPort: 5353, protocol: "UDP" }, }, }, }, }, }; local a_deploy = kube.Deployment("foo") { spec+: { template+: { metadata+: a_pod.metadata, spec+: a_pod.spec } }, }; // Basic unittesting for methods that are not exercised by the other e2e-ish tests // kube.libsonnet std.assertEqual(kube.objectValues({ a: 1, b: 2 }), [1, 2]) && std.assertEqual(kube.objectItems({ a: 1, b: 2 }), [["a", 1], ["b", 2]]) && std.assertEqual(kube.hyphenate("foo_bar_baz"), ("foo-bar-baz")) && std.assertEqual(kube.mapToNamedList({ foo: { a: "b" } }), [{ name: "foo", a: "b" }]) && std.assertEqual(kube.filterMapByFields({ a: 1, b: 2, c: 3 }, ["a", "c", "d"]), { a: 1, c: 3 }) && std.assertEqual(kube.parseOctal("755"), 493) && std.assertEqual(kube.siToNum("42G"), 42 * 1e9) && std.assertEqual(kube.siToNum("42Gi"), 42 * std.pow(2, 30)) && std.assertEqual(kube.toUpper("ForTy 2"), "FORTY 2") && std.assertEqual(kube.toLower("ForTy 2"), "forty 2") && std.assertEqual(an_obj, { apiVersion: "v1", kind: "Gentle", metadata: { name: "foo", labels: { name: "foo" }, annotations: {} }, }) && std.assertEqual( [kube.podRef(a_deploy).spec.ports("TCP"), kube.podRef(a_deploy).spec.ports("UDP")], [[8080, 8443], [5353]] ) && std.assertEqual( // latest kubecfg produces stable output from maps hashes, so below shouldn't be flaky kube.podsPorts([a_deploy]), [ { port: 8080, protocol: "TCP" }, { port: 8443, protocol: "TCP" }, { port: 5353, protocol: "UDP" }, ] ) && std.assertEqual( kube.podLabelsSelector(a_deploy), { podSelector: { matchLabels: { name: "foo", foo: "bar", bar: "qxx" } } } ) && // utils.libsonnet std.assertEqual( [utils.path_join("foo", "bar"), utils.path_join("foo/", "bar")], ["foo/bar", "foo/bar"] ) && std.assertEqual( utils.trimUrl("http://example.com/foo/"), "http://example.com/foo" ) && std.assertEqual( std.parseJson(utils.toJson('{ "foo": "bar\nqqq" }')), '{ "foo": "bar\nqqq" }', ) && std.assertEqual( utils.parentDomain("foo.example.com"), "example.com" ) && std.assertEqual( utils.parentDomain("foo.example.com"), "example.com" ) && std.assertEqual( std.uniq([ x.podAffinityTerm.labelSelector for x in utils.weakNodeDiversity({ foo: "bar" }).podAntiAffinity.preferredDuringSchedulingIgnoredDuringExecution ]), [{ foo: "bar" }] ) && std.assertEqual( (utils.HashedConfigMap("hashed-cm")) { data+: { foo: "bar" } }.metadata.name, "hashed-cm-94232c5", ) && std.assertEqual( ( (utils.HashedConfigMap("hashed-cm")) { data+: { foo: "bar" }, }.metadata.name == (utils.HashedConfigMap("hashed-cm")) { data+: { foo: "baz" }, }.metadata.name ), false, ) && std.assertEqual( (utils.HashedSecret("hashed-secret")) { data+: { foo: std.base64("bar") } }.metadata.name, "hashed-secret-16f81db", ) && std.assertEqual( ( (utils.HashedSecret("hashed-secret")) { data+: { foo: std.base64("bar") }, }.metadata.name == (utils.HashedSecret("hashed-secret")) { data+: { foo: std.base64("baz") }, }.metadata.name ), false, ) && true ================================================ FILE: utils.libsonnet ================================================ /* * kube-libsonnet - A jsonnet helper library for Kubernetes * * Copyright 2018-2020 VMware Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ // Various opinionated helper functions, that might not be generally // useful in other deployments. local kube = import "kube.libsonnet"; { path_join(prefix, suffix):: ( if std.endsWith(prefix, "/") then prefix + suffix else prefix + "/" + suffix ), trimUrl(str):: ( if std.endsWith(str, "/") then std.substr(str, 0, std.length(str) - 1) else str ), toJson(x):: ( if std.type(x) == "string" then std.escapeStringJson(x) else std.toString(x) ), parentDomain(fqdn):: ( local parts = std.split(fqdn, "."); local tail = [parts[i] for i in std.range(1, std.length(parts) - 1)]; assert std.length(tail) >= 1 : "Tried to use parent of top-level DNS domain %s" % fqdn; std.join(".", tail) ), // affinity=weakNodeDiversity to Try to spread across separate // nodes/zones (for fault-tolerance) weakNodeDiversity(selector):: { podAntiAffinity+: { preferredDuringSchedulingIgnoredDuringExecution+: [{ weight: 70, podAffinityTerm: { labelSelector: selector, topologyKey: k, }, } for k in [ "kubernetes.io/hostname", "failure-domain.beta.kubernetes.io/zone", "failure-domain.beta.kubernetes.io/region", ]], }, }, TlsIngress(name):: kube.Ingress(name) { local this = self, metadata+: { annotations+: { "kubernetes.io/tls-acme": "true", "kubernetes.io/ingress.class": "nginx", }, }, spec+: { tls+: [{ hosts: std.set([r.host for r in this.spec.rules]), secretName: this.metadata.name + "-tls", }], }, }, AuthIngress(name):: $.TlsIngress(name) { local this = self, host:: error "host is required", authHost:: "auth." + $.parentDomain(this.host), metadata+: { annotations+: { // NB: Our nginx-ingress no-auth-locations includes "/oauth2" "nginx.ingress.kubernetes.io/auth-signin": "https://%s/oauth2/start?rd=%%2F$server_name$escaped_request_uri" % this.authHost, "nginx.ingress.kubernetes.io/auth-url": "https://%s/oauth2/auth" % this.authHost, "nginx.ingress.kubernetes.io/auth-response-headers": "X-Auth-Request-User, X-Auth-Request-Email", }, }, }, local hashed = { local this = self, metadata+: { local hash = std.substr(std.md5(std.toString(this.data)), 0, 7), local orig_name = super.name, name: orig_name + "-" + hash, labels+: { name: orig_name }, }, }, HashedConfigMap(name):: kube.ConfigMap(name) + hashed, HashedSecret(name):: kube.Secret(name) + hashed, }