diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..537f51d --- /dev/null +++ b/.editorconfig @@ -0,0 +1,17 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true +charset = utf-8 +indent_style = space +indent_size = 4 +trim_trailing_whitespace = true +max_line_length = 110 +quote_type = single + +[*.{yaml,md}] +indent_size = 2 + +[Makefile] +indent_style = tab diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml new file mode 100644 index 0000000..73dc436 --- /dev/null +++ b/.github/dependabot.yaml @@ -0,0 +1,8 @@ +version: 2 +updates: + - package-ecosystem: pip + directory: '/ci' + schedule: + interval: daily + ignore: + - dependency-name: none diff --git a/.github/workflows/dependabot-auto-merge.yaml b/.github/workflows/dependabot-auto-merge.yaml new file mode 100644 index 0000000..1c2353d --- /dev/null +++ b/.github/workflows/dependabot-auto-merge.yaml @@ -0,0 +1,20 @@ +--- +name: Auto merge Dependabot updates + +on: + workflow_run: + workflows: + - Continuous integration + types: + - completed + +jobs: + auto-merge: + name: Auto merge Dependabot updates + runs-on: ubuntu-20.04 + timeout-minutes: 5 + steps: + - name: Auto merge + uses: ridedott/dependabot-auto-merge-action@master + with: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml new file mode 100644 index 0000000..4eee500 --- /dev/null +++ b/.github/workflows/main.yaml @@ -0,0 +1,50 @@ +--- +name: Continuous integration + +on: + push: + pull_request: + +env: + HAS_SECRETS: ${{ secrets.HAS_SECRETS }} + +jobs: + main: + runs-on: ubuntu-20.04 + name: Continuous integration + timeout-minutes: 20 + if: "!startsWith(github.event.head_commit.message, '[skip ci] ')" + + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - uses: camptocamp/initialise-gopass-summon-action@v2 + with: + ci-gpg-private-key: ${{secrets.CI_GPG_PRIVATE_KEY}} + github-gopass-ci-token: ${{secrets.GOPASS_CI_GITHUB_TOKEN}} + if: env.HAS_SECRETS == 'HAS_SECRETS' + + - run: echo "${HOME}/.local/bin" >> ${GITHUB_PATH} + - run: python3 -m pip install --user --requirement=ci/requirements.txt + + - name: Checks + run: c2cciutils-checks + + - name: Install helm + uses: azure/setup-helm@v1 + - run: helm dependency update . + - run: helm lint . + - run: helm lint --values=tests/values.yaml . + - run: helm template --namespace=default --values=tests/values.yaml custom . > tests/actual.yaml + - run: diff --ignore-trailing-space tests/actual.yaml tests/expected.yaml + + - name: Setup k3s/k3d + run: c2cciutils-k8s-install + + - name: Apply + run: kubectl apply -f tests/expected.yaml + + - name: Publish + run: c2cciutils-publish diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2a30108 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/Chart.lock +/charts/ +/tests/*actual.yaml +*/__pycache__/* diff --git a/.helmignore b/.helmignore new file mode 100644 index 0000000..e9d0e20 --- /dev/null +++ b/.helmignore @@ -0,0 +1,24 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ + +tests/ diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..4ad46ee --- /dev/null +++ b/.prettierignore @@ -0,0 +1,2 @@ +templates/**/*.yaml +tests/*expected.yaml diff --git a/Chart.yaml b/Chart.yaml new file mode 100644 index 0000000..8be0eb7 --- /dev/null +++ b/Chart.yaml @@ -0,0 +1,9 @@ +apiVersion: v2 +appVersion: '1.0' +description: A custom pod with everything needed +name: custom-pod +version: 0.1.0 +dependencies: + - name: common + repository: https://camptocamp.github.io/helm-common + version: 0.1.1 diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..a41d9dd --- /dev/null +++ b/Makefile @@ -0,0 +1,5 @@ +HELM != helm3 + +gen-expected: + ${HELM} template --namespace=default --values=tests/values.yaml custom . > tests/expected.yaml + sed -i 's/[[:blank:]]\+$$//g' tests/expected.yaml diff --git a/README.md b/README.md new file mode 100644 index 0000000..3bd2675 --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +# [Kubernetes](https://kubernetes.io/) [HELM chart](https://helm.sh/) for a simple custom application + +With this chart you can easily deploy a simple custom application on Kubernetes, with only configuration. + +[See as example](./tests/values.yaml). diff --git a/ci/config.yaml b/ci/config.yaml new file mode 100644 index 0000000..57e148a --- /dev/null +++ b/ci/config.yaml @@ -0,0 +1,11 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/camptocamp/c2cciutils/master/c2cciutils/schema.json + +checks: + required_workflows: + clean.yaml: false + audit.yaml: false + backport.yaml: false + codeql.yaml: false + black: + ignore_patterns_re: + - .*\.yaml diff --git a/ci/requirements.txt b/ci/requirements.txt new file mode 100644 index 0000000..0a645df --- /dev/null +++ b/ci/requirements.txt @@ -0,0 +1 @@ +c2cciutils==1.1.dev20211021112135 diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..c2691b0 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,3 @@ +[tool.black] +line-length = 110 +target-version = ['py38'] diff --git a/templates/NOTES.txt b/templates/NOTES.txt new file mode 100644 index 0000000..3e8cebc --- /dev/null +++ b/templates/NOTES.txt @@ -0,0 +1,21 @@ +1. Get the application URL by running these commands: +{{- if .Values.ingress.enabled }} +{{- range $host := .Values.ingress.hosts }} + {{- range .paths }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ . }} + {{- end }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "common.fullname" ( dict "root" . "service" .Values ) }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "common.fullname" ( dict "root" . "service" .Values ) }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "common.fullname" ( dict "root" . "service" .Values ) }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "common.name" ( dict "root" . "service" .Values ) }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl port-forward $POD_NAME 8080:80 +{{- end }} diff --git a/templates/deployment.yaml b/templates/deployment.yaml new file mode 100644 index 0000000..36151af --- /dev/null +++ b/templates/deployment.yaml @@ -0,0 +1,68 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "common.fullname" ( dict "root" . "service" .Values ) }} + labels: {{ include "common.labels" ( dict "root" . "service" .Values ) | nindent 4 }} +spec: + replicas: {{ .Values.replicaCount }} + strategy: + type: RollingUpdate + selector: + matchLabels: {{- include "common.selectorLabels" ( dict "root" . "service" .Values ) | nindent 6 }} + template: + metadata: + labels: {{- include "common.selectorLabels" ( dict "root" . "service" .Values ) | nindent 8 }} + spec: {{- include "common.podConfig" ( dict "root" . "service" .Values ) | indent 6 }} + {{- if .Values.initContainers }} + initContainers: + {{- range $name, $config := .Values.initContainers }} + - name: {{ $name }} + {{- include "common.containerConfig" ( dict "root" $ "container" $config ) | nindent 10 }} + {{- with $config.command }} + command: {{ $config.command | toYaml | nindent 12 }} + {{- end }} + {{- with $config.args }} + args: {{ $config.args | toYaml | nindent 12 }} + {{- end }} + {{- with $config.volumeMounts }} + volumeMounts: {{ $config.volumeMounts | toYaml | nindent 12 }} + {{- end }} + {{- end }} + {{- end }} + containers: + {{- range $name, $config := .Values.containers }} + - name: {{ $name }} + {{- include "common.containerConfig" ( dict "root" $ "container" $config ) | nindent 10 }} + {{- with $config.command }} + command: + {{- . | toYaml | nindent 12 }} + {{- end }} + {{- with $config.args }} + args: + {{- . | toYaml | nindent 12 }} + {{- end }} + {{- with $config.volumeMounts }} + volumeMounts: + {{- . | toYaml | nindent 12 }} + {{- end }} + {{- with $config.ports }} + ports: + {{- $config.ports | toYaml | nindent 12 }} + {{- end }} + {{- with $config.livenessProbe }} + livenessProbe: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with $config.readinessProbe }} + readinessProbe: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with $config.readinessProbe }} + startupProbe: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- end }} + {{- with .Values.volumes }} + volumes: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/templates/ingress.yaml b/templates/ingress.yaml new file mode 100644 index 0000000..532a1de --- /dev/null +++ b/templates/ingress.yaml @@ -0,0 +1,53 @@ +{{- if .Values.ingress.enabled -}} +{{- $fullName := include "common.fullname" ( dict "root" . "service" .Values ) -}} +{{- $svcPort := $.Values.ingress.servicePort -}} +{{- range $ingress_host := .Values.ingress.hosts }} +--- +{{- if semverCompare ">=1.19.0" ( trimPrefix "v" $.Capabilities.KubeVersion.Version ) }} +apiVersion: networking.k8s.io/v1 +{{- else -}} +{{- if semverCompare ">=1.14-0" $.Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1beta1 +{{- else -}} +apiVersion: extensions/v1beta1 +{{- end }} +{{- end }} +kind: Ingress +metadata: + name: {{ $fullName }}-{{ $ingress_host.name }} + labels: {{ include "common.labels" ( dict "root" $ "service" $.Values ) | nindent 4 }} + {{- with $.Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: +{{- if $ingress_host.tls }} + tls: + {{- range $ingress_host.tls }} + - hosts: + - {{ $ingress_host.host }} + secretName: {{ $ingress_host.tls.secretName }} + {{- end }} +{{- end }} + rules: + - host: {{ $ingress_host.host }} + http: + paths: + {{- range $.Values.ingress.paths }} + - path: {{ . }} + {{- if semverCompare ">=1.14-0" $.Capabilities.KubeVersion.GitVersion }} + pathType: Prefix + {{- end }} + backend: + {{- if semverCompare ">=1.14-0" $.Capabilities.KubeVersion.GitVersion }} + service: + name: {{ $fullName }} + port: + number: {{ $svcPort }} + {{- else }} + serviceName: {{ $fullName }} + servicePort: {{ $svcPort }} + {{- end -}} + {{- end }} +{{- end }} +{{- end }} diff --git a/templates/pdb.yaml b/templates/pdb.yaml new file mode 100644 index 0000000..c50eb3e --- /dev/null +++ b/templates/pdb.yaml @@ -0,0 +1,9 @@ +apiVersion: policy/v1beta1 +kind: PodDisruptionBudget +metadata: + name: {{ include "common.fullname" ( dict "root" . "service" .Values ) }} + labels: {{- include "common.selectorLabels" ( dict "root" . "service" .Values ) | nindent 4 }} +spec: + maxUnavailable: 1 + selector: + matchLabels: {{- include "common.selectorLabels" ( dict "root" . "service" .Values ) | nindent 6 }} diff --git a/templates/service.yaml b/templates/service.yaml new file mode 100644 index 0000000..98f4e22 --- /dev/null +++ b/templates/service.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "common.fullname" ( dict "root" . "service" .Values ) }} + labels: {{ include "common.labels" ( dict "root" . "service" .Values ) | nindent 4 }} + prometheus: "true" +spec: + type: {{ .Values.service.type }} + {{- with .Values.service.ports }} + ports: + {{- . | toYaml | nindent 4 }} + {{- end }} + selector: {{- include "common.selectorLabels" ( dict "root" . "service" .Values ) | nindent 4 }} diff --git a/templates/serviceaccount.yaml b/templates/serviceaccount.yaml new file mode 100644 index 0000000..0782e5f --- /dev/null +++ b/templates/serviceaccount.yaml @@ -0,0 +1,7 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "common.serviceAccountName" ( dict "root" . "service" .Values ) }} + labels: {{ include "common.labels" ( dict "root" . "service" .Values ) | nindent 4 }} +{{- end }} diff --git a/tests/expected.yaml b/tests/expected.yaml new file mode 100644 index 0000000..f41668b --- /dev/null +++ b/tests/expected.yaml @@ -0,0 +1,242 @@ +--- +# Source: custom-pod/templates/pdb.yaml +apiVersion: policy/v1beta1 +kind: PodDisruptionBudget +metadata: + name: custom-custom-pod + labels: + app.kubernetes.io/name: custom-pod + app.kubernetes.io/instance: custom + app.kubernetes.io/component: main +spec: + maxUnavailable: 1 + selector: + matchLabels: + app.kubernetes.io/name: custom-pod + app.kubernetes.io/instance: custom + app.kubernetes.io/component: main +--- +# Source: custom-pod/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: custom-custom-pod + labels: + helm.sh/chart: custom-pod-0.1.0 + app.kubernetes.io/version: "1.0" + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: custom-pod + app.kubernetes.io/instance: custom + app.kubernetes.io/component: main + prometheus: "true" +spec: + type: ClusterIP + ports: + - name: http + port: 8080 + protocol: TCP + targetPort: http + selector: + app.kubernetes.io/name: custom-pod + app.kubernetes.io/instance: custom + app.kubernetes.io/component: main +--- +# Source: custom-pod/templates/deployment.yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: custom-custom-pod + labels: + helm.sh/chart: custom-pod-0.1.0 + app.kubernetes.io/version: "1.0" + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: custom-pod + app.kubernetes.io/instance: custom + app.kubernetes.io/component: main +spec: + replicas: 1 + strategy: + type: RollingUpdate + selector: + matchLabels: + app.kubernetes.io/name: custom-pod + app.kubernetes.io/instance: custom + app.kubernetes.io/component: main + template: + metadata: + labels: + app.kubernetes.io/name: custom-pod + app.kubernetes.io/instance: custom + app.kubernetes.io/component: main + spec: + serviceAccountName: default + securityContext: + runAsNonRoot: true + runAsUser: 33 + affinity: + {} + initContainers: + - name: aa + securityContext: + runAsNonRoot: true + runAsUser: 33 + image: "camptocamp/custom-aa:latest" + imagePullPolicy: IfNotPresent + env: + - name: "TEST" + value: "aa" + terminationMessagePolicy: FallbackToLogsOnError + resources: + limits: + cpu: 100m + memory: 50Mi + requests: + cpu: 100m + memory: 50Mi + command: + - sleep + - "3600" + - name: bb + securityContext: + runAsNonRoot: true + runAsUser: 33 + image: "camptocamp/custom-bb:latest" + imagePullPolicy: IfNotPresent + terminationMessagePolicy: FallbackToLogsOnError + resources: + null + args: + - sleep + - "3600" + volumeMounts: + - mountPath: /tmp/my-volume + name: my-volume + containers: + - name: cc + securityContext: + runAsNonRoot: true + runAsUser: 33 + image: "camptocamp/custom-aa:latest" + imagePullPolicy: IfNotPresent + env: + - name: "TEST" + value: "aa" + terminationMessagePolicy: FallbackToLogsOnError + resources: + limits: + cpu: 100m + memory: 50Mi + requests: + cpu: 100m + memory: 50Mi + command: + - sleep + - "3600" + - name: dd + securityContext: + runAsNonRoot: true + runAsUser: 33 + image: "camptocamp/custom-bb:latest" + imagePullPolicy: IfNotPresent + terminationMessagePolicy: FallbackToLogsOnError + resources: + null + args: + - sleep + - "3600" + volumeMounts: + - mountPath: /tmp/my-volume + name: my-volume + ports: + - containerPort: 8080 + name: http + protocol: TCP + livenessProbe: + httpGet: + path: / + port: http + readinessProbe: + httpGet: + path: / + port: http + initialDelaySeconds: 5 + periodSeconds: 20 + timeoutSeconds: 10 + startupProbe: + httpGet: + path: / + port: http + initialDelaySeconds: 5 + periodSeconds: 20 + timeoutSeconds: 10 + volumes: + - emptyDir: {} + name: my-volume +--- +# Source: custom-pod/templates/ingress.yaml +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: custom-custom-pod-int + labels: + helm.sh/chart: custom-pod-0.1.0 + app.kubernetes.io/version: "1.0" + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: custom-pod + app.kubernetes.io/instance: custom + app.kubernetes.io/component: main +spec: + rules: + - host: int.local + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: custom-custom-pod + port: + number: 8080 + - path: /custom + pathType: Prefix + backend: + service: + name: custom-custom-pod + port: + number: 8080 +--- +# Source: custom-pod/templates/ingress.yaml +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: custom-custom-pod-prod + labels: + helm.sh/chart: custom-pod-0.1.0 + app.kubernetes.io/version: "1.0" + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: custom-pod + app.kubernetes.io/instance: custom + app.kubernetes.io/component: main +spec: + tls: + - hosts: + - prod.local + secretName: prod-custom + rules: + - host: prod.local + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: custom-custom-pod + port: + number: 8080 + - path: /custom + pathType: Prefix + backend: + service: + name: custom-custom-pod + port: + number: 8080 diff --git a/tests/mapserver-expected.yaml b/tests/mapserver-expected.yaml new file mode 100644 index 0000000..e69de29 diff --git a/tests/values.yaml b/tests/values.yaml new file mode 100644 index 0000000..aa616c8 --- /dev/null +++ b/tests/values.yaml @@ -0,0 +1,84 @@ +initContainers: + aa: &aa + image: + repository: camptocamp/custom-aa + tag: latest + sha: + command: + - sleep + - '3600' + resources: + limits: + cpu: '100m' + memory: '50Mi' + requests: + cpu: '100m' + memory: '50Mi' + env: + TEST: + value: 'aa' + bb: &bb + image: + repository: camptocamp/custom-bb + tag: latest + sha: + args: + - sleep + - '3600' + volumeMounts: + - name: my-volume + mountPath: /tmp/my-volume + +containers: + cc: + <<: *aa + dd: + <<: *bb + livenessProbe: + httpGet: + path: / + port: http + readinessProbe: + httpGet: + path: / + port: http + initialDelaySeconds: 5 + timeoutSeconds: 10 + periodSeconds: 20 + startupProbe: + httpGet: + path: / + port: http + ports: + - name: http + containerPort: 8080 + protocol: TCP + +volumes: + - name: my-volume + emptyDir: {} + +securityContext: + runAsNonRoot: true + runAsUser: 33 # www-data + +ingress: + enabled: true + servicePort: 8080 + paths: + - / + - /custom + hosts: + - name: int + host: int.local + - name: prod + host: prod.local + tls: + secretName: prod-custom + +service: + ports: + - name: http + port: 8080 + protocol: TCP + targetPort: http diff --git a/values.yaml b/values.yaml new file mode 100644 index 0000000..4932de4 --- /dev/null +++ b/values.yaml @@ -0,0 +1,55 @@ +nameOverride: '' +fullnameOverride: '' + +imagePullSecrets: [] +replicaCount: 1 +image: + pullPolicy: IfNotPresent + +# ConfigMap or secret from env name override +configMapNameOverride: {} + +initContainers: [] +containers: [] + +serviceAccount: + # Specifies whether a service account should be created + create: false + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: default + +podSecurityContext: + {} + # fsGroup: 2000 + +securityContext: + {} + # runAsNonRoot: true + # runAsUser: 33 # www-data + # readOnlyRootFilesystem: true + # capabilities: + # drop: + # - ALL + +service: + type: ClusterIP + ports: [] + +ingress: + enabled: false + annotations: + {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + paths: [] + hosts: + - host: chart-example.local + # tls: + # - secretName: chart-example-tls + +nodeSelector: {} + +tolerations: [] + +affinity: {}