From aa006128573b99df93bf13303565749fe8547392 Mon Sep 17 00:00:00 2001 From: Erik Sundell Date: Tue, 14 Aug 2018 21:53:29 +0200 Subject: [PATCH] Improved template linting/validation + CI integration --- .gitignore | 5 +- .travis.yml | 2 +- ci/install.sh | 9 + ci/minikube.env | 1 + dev-requirements.txt | 13 +- jupyterhub/Chart.yaml | 6 +- jupyterhub/values.yaml | 13 +- tools/lint-chart-values.yaml | 192 ------------ tools/lint.py | 58 ---- tools/templates/lint-and-validate-values.yaml | 274 ++++++++++++++++++ tools/templates/lint-and-validate.py | 87 ++++++ .../yamllint-config.yaml} | 2 +- 12 files changed, 399 insertions(+), 263 deletions(-) delete mode 100644 tools/lint-chart-values.yaml delete mode 100755 tools/lint.py create mode 100644 tools/templates/lint-and-validate-values.yaml create mode 100755 tools/templates/lint-and-validate.py rename tools/{lint-config.yaml => templates/yamllint-config.yaml} (96%) diff --git a/.gitignore b/.gitignore index f004016a25..2a48495156 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,9 @@ ### Zero to JupyterHub Kubernetes ### -tools/lint-output/ +tools/templates/rendered-templates/ +bin/ +ci/.vagrant +.vscode ### macOS ### *.DS_Store diff --git a/.travis.yml b/.travis.yml index 0965170c2a..7372c188b6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,6 +13,7 @@ script: - chartpress --commit-range ${TRAVIS_COMMIT_RANGE} - git diff - ./ci/test.sh +- python3 tools/templates/lint-and-validate.py deploy: provider: script @@ -21,7 +22,6 @@ deploy: on: branch: - master - - v0.7-dev condition: "$KUBE_VERSION = 1.9.4" env: matrix: diff --git a/ci/install.sh b/ci/install.sh index 1e8a92a509..e9cfb4a081 100755 --- a/ci/install.sh +++ b/ci/install.sh @@ -23,6 +23,15 @@ curl -Lo minikube https://storage.googleapis.com/minikube/releases/v${MINIKUBE_V chmod +x minikube mv minikube bin/ +echo "installing kubeval" +if ! [ -f bin/kubeval-${KUBEVAL_VERSION} ]; then + curl -sSLo bin/kubeval-${KUBEVAL_VERSION}.tar.gz https://github.com/garethr/kubeval/releases/download/${KUBEVAL_VERSION}/kubeval-linux-amd64.tar.gz + tar --extract --file bin/kubeval-${KUBEVAL_VERSION}.tar.gz --directory bin + rm bin/kubeval-${KUBEVAL_VERSION}.tar.gz + mv bin/kubeval bin/kubeval-${KUBEVAL_VERSION} +fi +cp bin/kubeval-${KUBEVAL_VERSION} bin/kubeval + echo "starting minikube with RBAC" sudo CHANGE_MINIKUBE_NONE_USER=true $PWD/bin/minikube start --vm-driver=none --kubernetes-version=v${KUBE_VERSION} --extra-config=apiserver.Authorization.Mode=RBAC --bootstrapper=localkube minikube update-context diff --git a/ci/minikube.env b/ci/minikube.env index a83417e369..48d0421e5c 100644 --- a/ci/minikube.env +++ b/ci/minikube.env @@ -1,3 +1,4 @@ export MINIKUBE_VERSION=0.28.0 export HELM_VERSION=2.9.1 +export KUBEVAL_VERSION=0.7.1 export PATH="$PWD/bin:$PATH" diff --git a/dev-requirements.txt b/dev-requirements.txt index 2dd03bd03e..beb70e512b 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,3 +1,14 @@ +# chartpress is important for local development, CI and CD +# - builds images and can push them also (--push) +# - updates image names and tags in values.yaml +# - can publish the built Helm chart (--publish) +chartpress==0.2.1 + +# yamllint is important for local development, CI +# - used within tools/templates/lint-and-validate.py +yamllint>=1.1.1 + +# PyGithub and python-dateutil is rarely used +# - used within tools/contributors.py PyGithub>=1 python-dateutil>=2 -chartpress==0.2.1 diff --git a/jupyterhub/Chart.yaml b/jupyterhub/Chart.yaml index 9026ed4cb1..0d19e032cc 100644 --- a/jupyterhub/Chart.yaml +++ b/jupyterhub/Chart.yaml @@ -1,10 +1,10 @@ name: jupyterhub version: v0.7-dev -appVersion: v0.8.1 +appVersion: v0.9.2 description: Multi-user Jupyter installation home: https://z2jh.jupyter.org sources: - https://github.com/jupyterhub/zero-to-jupyterhub-k8s icon: https://jupyter.org/assets/hublogo.svg -kubeVersion: ">=1.8.0-0" -tillerVersion: ">=2.7.0-0" +kubeVersion: '>=1.8.0-0' +tillerVersion: '>=2.7.0-0' diff --git a/jupyterhub/values.yaml b/jupyterhub/values.yaml index fd52f06ebd..20b19f1b05 100644 --- a/jupyterhub/values.yaml +++ b/jupyterhub/values.yaml @@ -16,7 +16,7 @@ hub: # sqlite-pvc backed hub requires Recreate strategy to work type: Recreate # This is required for upgrading to work - rollingUpdate: null + rollingUpdate: db: type: sqlite-pvc upgrade: @@ -31,7 +31,7 @@ hub: url: labels: {} annotations: - prometheus.io/scrape: "true" + prometheus.io/scrape: 'true' prometheus.io/path: /hub/metrics extraConfig: {} extraConfigMap: {} @@ -92,8 +92,9 @@ proxy: image: name: jetstack/kube-lego tag: 0.1.6 + pullPolicy: IfNotPresent resources: {} - labels: + labels: {} nodeSelector: {} pdb: enabled: true @@ -107,9 +108,9 @@ proxy: key: cert: secret: - name: "" - key: "" - crt: "" + name: '' + key: '' + crt: '' hosts: [] networkPolicy: enabled: false diff --git a/tools/lint-chart-values.yaml b/tools/lint-chart-values.yaml deleted file mode 100644 index b74a02cb9a..0000000000 --- a/tools/lint-chart-values.yaml +++ /dev/null @@ -1,192 +0,0 @@ -hub: - service: - ports: - nodePort: - baseUrl: / - cookieSecret: '' - publicURL: - nodeSelector: {} - concurrentSpawnLimit: 64 - activeServerLimit: - db: - type: sqlite-pvc - upgrade: - pvc: - annotations: {} - selector: {} - subPath: - storageClassName: - url: - labels: - test-label: test-value - annotations: - test-annotation: test-value - extraConfig: {} - extraConfigMap: {} - extraEnv: {} - extraContainers: [] - extraVolumes: [] - extraVolumeMounts: [] - services: {} - imagePullPolicy: IfNotPresent - pdb: - enabled: true - networkPolicy: - enabled: true - egress: - - to: - - ipBlock: - cidr: 0.0.0.0/0 - - -rbac: - enabled: true - - -proxy: - secretToken: '0000000000000000000000000000000000000000000000000000000000000000' - service: - type: LoadBalancer - labels: {} - annotations: {} - nodePorts: - http: - https: - nginx: - image: - name: quay.io/kubernetes-ingress-controller/nginx-ingress-controller - tag: 0.9.0 - pullPolicy: IfNotPresent - proxyBodySize: 64m - resources: {} - lego: - image: - name: jetstack/kube-lego - # We need a couple of fixes related to ingress.provider that are in master - # When 0.1.6 is released, switch to that! - tag: master-2368 - resources: {} - labels: {} - annotations: {} - nodeSelector: {} - pdb: - enabled: true - https: - enabled: true - type: letsencrypt - letsencrypt: - contactEmail: '' - manual: - key: - cert: - hosts: [] - networkPolicy: - enabled: true - egress: - - to: - - ipBlock: - cidr: 0.0.0.0/0 - - -auth: - type: dummy - whitelist: - users: - admin: - access: true - users: - dummy: - password: - state: - enabled: true - cryptoKey: '0000000000000000000000000000000000000000000000000000000000000000' - - -singleuser: - networkTools: - image: - name: jupyterhub/k8s-network-tools - tag: generated-by-chartpress - cloudMetadata: - enabled: true - ip: 169.254.169.254 - networkPolicy: - enabled: true - egress: - # Required egress is handled by other rules so it's safe to modify this - - to: - - ipBlock: - cidr: 0.0.0.0/0 - except: - - 169.254.169.254/32 - extraLabels: {} - extraEnv: {} - lifecycleHooks: - initContainers: - nodeSelector: {} - uid: 1000 - fsGid: 100 - serviceAccountName: - schedulerStrategy: - storage: - type: dynamic - extraVolumes: [] - extraVolumeMounts: [] - static: - pvcName: - subPath: '{username}' - capacity: 10Gi - homeMountPath: /home/jovyan - dynamic: - storageClass: - pvcNameTemplate: claim-{username}{servername} - volumeNameTemplate: volume-{username}{servername} - storageAccessModes: - - ReadWriteOnce - image: - name: jupyterhub/k8s-singleuser-sample - tag: generated-by-chartpress - pullPolicy: IfNotPresent - startTimeout: 300 - cpu: - limit: - guarantee: - memory: - limit: - guarantee: 1G - extraResource: - limits: {} - guarantees: {} - cmd: jupyterhub-singleuser - defaultUrl: - - -prePuller: - hook: - enabled: true - extraEnv: {} - image: - name: jupyterhub/k8s-image-awaiter - tag: generated-by-chartpress - continuous: - enabled: true - pause: - image: - name: gcr.io/google_containers/pause - tag: '3.0' - - -ingress: - enabled: true - annotations: {} - hosts: [] - tls: - - -cull: - enabled: true - users: false - - -debug: - enabled: true diff --git a/tools/lint.py b/tools/lint.py deleted file mode 100755 index 30521d4ad4..0000000000 --- a/tools/lint.py +++ /dev/null @@ -1,58 +0,0 @@ -#!/usr/bin/env python3 -""" -Lints the chart's yaml files without any cluster interaction. For this script to -function, you must install yamllint and kubeval. - -- https://github.com/adrienverge/yamllint -- https://github.com/garethr/kubeval -""" - - -import argparse -import glob -import subprocess - -def lint(config, values, kubernetes_version): - """Calls `helm lint`, `helm template` and `yamllint`.""" - - output = 'lint-output' - - print("### helm lint") - subprocess.check_call([ - 'helm', 'lint', '../jupyterhub', - '--values', values, - ]) - - print("### helm template") - subprocess.check_call([ - 'helm', 'template', '../jupyterhub', - '--values', values, - '--output-dir', output - ]) - - print("### yamllint") - subprocess.check_call([ - 'yamllint', '-c', config, output - ]) - - print("### kubeval") - for filename in glob.iglob(output + '/**/*.yaml', recursive=True): - subprocess.check_call([ - 'kubeval', filename, - '--kubernetes-version', kubernetes_version, - '--strict' - ]) - - print() - print("### All good!") - - -if __name__ == '__main__': - argparser = argparse.ArgumentParser() - argparser.add_argument('--config', default='lint-config.yaml', help='Specify the yamllint config') - argparser.add_argument('--values', default='lint-chart-values.yaml', help='Specify additional chart value files') - argparser.add_argument('--output', default='lint-output', help='Specify an output directory') - argparser.add_argument('--kubernetes-version', default='1.8.0', help='Validate against this kubernetes version') - args = argparser.parse_args() - - lint(args.config, args.values, args.kubernetes_version) diff --git a/tools/templates/lint-and-validate-values.yaml b/tools/templates/lint-and-validate-values.yaml new file mode 100644 index 0000000000..d7984103c0 --- /dev/null +++ b/tools/templates/lint-and-validate-values.yaml @@ -0,0 +1,274 @@ +hub: + service: + type: ClusterIP + ports: + nodePort: + baseUrl: / + cookieSecret: mock + publicURL: mock-public-url + nodeSelector: + node-type: mock + activeServerLimit: 3 + deploymentStrategy: + type: Recreate + rollingUpdate: + db: + type: sqlite-pvc + upgrade: + pvc: + annotations: + mock-annotation: mock + selector: + matchLabels: + mock-selector: mock + accessModes: + - ReadWriteOnce + storage: 1Gi + subPath: /mock + storageClassName: custom-storage-class + url: custom-db-url + labels: + mock: mock + annotations: + mock: mock + extraConfig: + test: |- + c.Spawner.cmd = 'mock' + extraConfigMap: + mock.entry: mock-config-map-entry + extraEnv: + MOCK_HUB_ENV: mock + extraContainers: [] + extraVolumes: [] + extraVolumeMounts: [] + resources: + requests: + cpu: 100m + memory: 512Mi + services: + tests: + apiToken: mocked-api-token + imagePullPolicy: Always + pdb: + enabled: true + networkPolicy: + enabled: true + egress: + - to: + - ipBlock: + cidr: 0.0.0.0/0 + allowNamedServers: true + + +rbac: + enabled: true + + +proxy: + secretToken: '0000000000000000000000000000000000000000000000000000000000000000' + service: + type: LoadBalancer + labels: + MOCK_PROXY_ENV: mock + annotations: + hub.jupyter.org/mock-proxy-annotation: mock + nodePorts: + http: + https: + chp: + resources: + requests: + cpu: 100m + memory: 512Mi + nginx: + proxyBodySize: 64m + resources: + requests: + cpu: 100m + memory: 512Mi + limits: + cpu: 200m + memory: 1Gi + lego: + resources: + requests: + cpu: 100m + memory: 512Mi + limits: + cpu: 200m + memory: 1Gi + labels: + mock-proxy-label: mock + nodeSelector: + mock-proxy-node-selector: mock + pdb: + enabled: true + https: + enabled: true + type: manual + #type: letsencrypt, manual, secret + letsencrypt: + contactEmail: 'e@domain.com' + manual: + key: mock-key + cert: mock-cert + secret: + name: 'mock-secret-name' + key: 'mock-key' + crt: 'mock-crt' + hosts: [domain.com] + networkPolicy: + enabled: true + egress: + - to: + - ipBlock: + cidr: 0.0.0.0/0 + + +auth: + type: dummy + whitelist: + users: + - mock + admin: + access: true + users: + - mock + dummy: + password: + ldap: + dn: + search: {} + user: {} + user: {} + state: + enabled: true + cryptoKey: mock-crypto-key + + +singleuser: + nodeSelector: + mock-node-selector: mock + extraTolerations: [] + extraNodeAffinity: + required: [] + preferred: [] + extraPodAffinity: + required: [] + preferred: [] + extraPodAntiAffinity: + required: [] + preferred: [] + cloudMetadata: + enabled: true + ip: 169.254.169.254 + networkPolicy: + enabled: true + egress: + - to: + - ipBlock: + cidr: 0.0.0.0/0 + except: + - 169.254.169.254/32 + events: true + extraLabels: {} + storageExtraLabels: {} + extraEnv: {} + lifecycleHooks: + initContainers: + uid: 1000 + fsGid: 100 + serviceAccountName: + storage: + type: dynamic + extraVolumes: [] + extraVolumeMounts: [] + static: + pvcName: + subPath: '{username}' + capacity: 10Gi + homeMountPath: /home/jovyan + dynamic: + storageClass: + pvcNameTemplate: claim-{username}{servername} + volumeNameTemplate: volume-{username}{servername} + storageAccessModes: [ReadWriteOnce] + imagePullSecret: + enabled: true + registry: R + username: U + email: e@domain.com + password: P + startTimeout: 300 + cpu: + guarantees: 1G + limits: 5 + memory: + guarantees: 1G + limits: + extraResource: + guarantees: + mock: 3 + limits: + mock: 1 + cmd: jupyterhub-singleuser + defaultUrl: / + + +scheduling: + userScheduler: + enabled: true + podPriority: + enabled: true + userPlaceholder: + enabled: true + resources: + requests: + cpu: 1 + memory: 1G + userDummy: + enabled: true + resources: + limits: + cpu: 2 + corePods: + nodeAffinity: + matchNodePurpose: require + userPods: + nodeAffinity: + matchNodePurpose: require + podAffinity: + preferScheduleNextToRealUsers: true + + +prePuller: + hook: + enabled: true + extraEnv: + MOCK_ENV: mock + continuous: + enabled: true + extraImages: + - name: mock-user/mock-image + tag: mock-tag + + +ingress: + enabled: true + annotations: + mock: mock + hosts: [] + tls: + + +cull: + enabled: true + users: true + timeout: 3600 + every: 600 + concurrency: 10 + maxAge: 3600*8 + + +debug: + enabled: true diff --git a/tools/templates/lint-and-validate.py b/tools/templates/lint-and-validate.py new file mode 100755 index 0000000000..d99075a1b3 --- /dev/null +++ b/tools/templates/lint-and-validate.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python3 +""" +Lints and validates the chart's template files and their rendered output without +any cluster interaction. For this script to function, you must install yamllint +and kubeval. + +- https://github.com/adrienverge/yamllint + +pip install yamllint + +- https://github.com/garethr/kubeval + +LATEST=curl --silent "https://api.github.com/repos/garethr/kubeval/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/' +wget https://github.com/garethr/kubeval/releases/download/$LATEST/kubeval-linux-amd64.tar.gz +tar xf kubeval-darwin-amd64.tar.gz +mv kubeval /usr/local/bin +""" + + +import os +import sys +import argparse +import glob +import subprocess + +os.chdir(os.path.dirname(sys.argv[0])) + +def lint(yamllint_config, values, kubernetes_version, output_dir, debug): + """Calls `helm lint`, `helm template`, `yamllint` and `kubeval`.""" + + print("### Clearing output directory") + subprocess.check_call([ + 'mkdir', '-p', output_dir, + ]) + subprocess.check_call([ + 'rm', '-rf', output_dir + '/*', + ]) + + print("### Linting started") + print("### 1/4 - helm lint") + helm_lint_cmd = [ + 'helm', 'lint', '../../jupyterhub', + '--values', values, + ] + if debug: + helm_lint_cmd.append('--debug') + subprocess.check_call(helm_lint_cmd) + + print("### 2/4 - helm template") + helm_template_cmd = [ + 'helm', 'template', '../../jupyterhub', + '--values', values, + '--output-dir', output_dir + ] + if debug: + helm_template_cmd.append('--debug') + subprocess.check_call(helm_template_cmd) + + print("### 3/4 - yamllint") + subprocess.check_call([ + 'yamllint', '-c', yamllint_config, output_dir + ]) + + print("### 4/4 - kubeval") + for filename in glob.iglob(output_dir + '/**/*.yaml', recursive=True): + subprocess.check_call([ + 'kubeval', filename, + '--kubernetes-version', kubernetes_version, + '--strict', + '--schema-location', 'https://raw.githubusercontent.com/consideRatio' + ]) + + print() + print("### Linting and validation of templates finished: All good!") + + +if __name__ == '__main__': + argparser = argparse.ArgumentParser() + argparser.add_argument('--debug', action='store_true', help='Run helm lint and helm template with the --debug flag') + argparser.add_argument('--values', default='lint-and-validate-values.yaml', help='Specify Helm values in a YAML file (can specify multiple)') + argparser.add_argument('--kubernetes-version', default='1.11.0', help='Version of Kubernetes to validate against') + argparser.add_argument('--output-dir', default='rendered-templates', help='Output directory for the rendered templates. Warning: content in this will be wiped.') + argparser.add_argument('--yamllint-config', default='yamllint-config.yaml', help='Specify a yamllint config') + + args = argparser.parse_args() + + lint(args.yamllint_config, args.values, args.kubernetes_version, args.output_dir, args.debug) diff --git a/tools/lint-config.yaml b/tools/templates/yamllint-config.yaml similarity index 96% rename from tools/lint-config.yaml rename to tools/templates/yamllint-config.yaml index a1bc16cd82..beb6e4d6d2 100644 --- a/tools/lint-config.yaml +++ b/tools/templates/yamllint-config.yaml @@ -17,7 +17,7 @@ rules: min-spaces-after: 1 max-spaces-after: 1 comments: - require-starting-space: false # Default: true (*) + require-starting-space: false # Default: true (*) min-spaces-from-content: 2 comments-indentation: {} document-end: disable