├── example ├── app3 │ ├── values.yaml │ ├── test │ │ ├── ok.yaml │ │ └── __snapshots__ │ │ │ └── ok.snap │ ├── templates │ │ └── secret.yaml │ ├── __snapshots__ │ │ ├── default.snap │ │ └── default.snap.yaml │ └── Chart.yaml ├── app1 │ ├── test_v1 │ │ ├── test_certmanager_enabled.yaml │ │ ├── test_hpa_enabled.yaml │ │ ├── .chartsnap.yaml │ │ └── test_ingress_enabled.yaml │ ├── test_v2 │ │ ├── test_certmanager_enabled.yaml │ │ ├── test_hpa_enabled.yaml │ │ ├── .chartsnap.yaml │ │ ├── test_ingress_enabled.yaml │ │ └── __snapshots__ │ │ │ ├── test_certmanager_enabled.snap │ │ │ ├── test_hpa_enabled.snap │ │ │ └── test_ingress_enabled.snap │ ├── test_v3 │ │ ├── test_certmanager_enabled.yaml │ │ ├── test_hpa_enabled.yaml │ │ ├── .chartsnap.yaml │ │ ├── test_ingress_enabled.yaml │ │ └── __snapshots__ │ │ │ ├── test_certmanager_enabled.snap │ │ │ └── test_hpa_enabled.snap │ ├── testfail │ │ ├── test_certmanager_enabled.yaml │ │ ├── test_hpa_enabled.yaml │ │ ├── .chartsnap.yaml │ │ ├── test_ingress_enabled.yaml │ │ └── __snapshots__ │ │ │ ├── test_certmanager_enabled.snap │ │ │ ├── test_ingress_enabled.snap │ │ │ └── test_hpa_enabled.snap │ ├── test_latest │ │ ├── test_certmanager_enabled.yaml │ │ ├── test_hpa_enabled.yaml │ │ ├── test_hpa_enabled.yml │ │ ├── test_hpa_enabled_20.yml │ │ ├── .chartsnap.yaml │ │ ├── test_ingress_enabled.yaml │ │ └── __snapshots__ │ │ │ ├── test_certmanager_enabled.snap │ │ │ ├── test_hpa_enabled.snap │ │ │ └── test_hpa_enabled_20.snap │ ├── templates │ │ ├── service.yaml │ │ ├── serviceaccount.yaml │ │ ├── tests │ │ │ └── test-connection.yaml │ │ ├── cert.yaml │ │ ├── hpa.yaml │ │ ├── NOTES.txt │ │ ├── ingress.yaml │ │ ├── deployment.yaml │ │ └── _helpers.tpl │ ├── test_wildcard │ │ ├── wildcard_test.yaml │ │ └── __snapshots__ │ │ │ └── wildcard_test.snap │ ├── .helmignore │ ├── Makefile │ ├── README.md │ ├── Chart.yaml │ └── values.yaml ├── app2 │ ├── __snapshots__ │ │ └── default.snap │ ├── .helmignore │ ├── Chart.yaml │ ├── templates │ │ ├── NOTES.txt │ │ ├── _helpers.tpl │ │ └── ingress.yaml │ └── values.yaml └── remote │ ├── nginx-gateway-fabric.values.yaml │ ├── ingress-nginx.values.yaml │ ├── Makefile │ ├── cilium.values.yaml │ └── README.md ├── pkg ├── charts │ ├── testdata │ │ ├── helm_empty.bash │ │ ├── .chartsnap.yaml │ │ ├── helm_cmd.bash │ │ ├── snap_values.yaml │ │ ├── testspec_values.yaml │ │ └── testspec_test.yaml │ ├── __snapshots__ │ │ ├── empty.yaml │ │ ├── helm-error.snap │ │ ├── helm_test.snap │ │ ├── helm_stub_snap_unmatch_v3.yaml │ │ ├── helm_stub_snap_unmatch_v2.yaml │ │ └── helm_stub_snap_v2.yaml │ ├── helm.go │ └── helm_test.go ├── snap │ ├── __snapshot__ │ │ ├── json.snap │ │ ├── single.snap │ │ └── multi.snap │ ├── gomega │ │ ├── object_snapshot.go │ │ ├── snap_test.go │ │ └── snap.go │ ├── cachefs.go │ └── cachefs_test.go ├── api │ └── v1alpha1 │ │ ├── testdata │ │ ├── .chartsnap.yaml │ │ ├── testspec_values.yaml │ │ └── testspec_values_invalid.yaml │ │ ├── header.go │ │ ├── header_test.go │ │ ├── testspec.go │ │ ├── unknown_types.go │ │ ├── __snapshots__ │ │ └── testspec_test.snap │ │ └── unknown_types_test.go ├── jsonpatch │ ├── jsonpatch.go │ └── jsonpatch_test.go ├── unstructured │ ├── testdata │ │ ├── testspec_test.yaml │ │ ├── expected.snap │ │ └── actual.snap │ ├── v1 │ │ └── legacy.go │ └── suite_test.go └── yaml │ ├── diff_test.go │ ├── testdata │ ├── expected.snap │ └── actual.snap │ └── yaml.go ├── .gitignore ├── docs ├── screenshot.png ├── screenshot-seq.png ├── CONTRIBUTING.md └── test_style_guide.md ├── .gitmodules ├── plugin.yaml ├── .goreleaser.yml ├── .github ├── dependabot.yml └── workflows │ ├── ci.yaml │ ├── release.yaml │ ├── check-for-updates.yml │ └── codeql.yml ├── hack ├── test │ ├── test-kong-chart.sh │ └── test-kong-chart-manual.sh ├── crd │ └── helm-chartsnap.jlandowner.dev_unknowns.yaml ├── helm-template-help-snapshot │ └── main.go └── helm-template-diff │ └── main.go ├── CONTRIBUTING.md ├── LICENSE ├── scripts └── install_plugin.sh └── go.mod /example/app3/values.yaml: -------------------------------------------------------------------------------- 1 | apiKey: -------------------------------------------------------------------------------- /example/app3/test/ok.yaml: -------------------------------------------------------------------------------- 1 | apiKey: xxxxxxx -------------------------------------------------------------------------------- /pkg/charts/testdata/helm_empty.bash: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | echo >/dev/null -------------------------------------------------------------------------------- /example/app1/test_v1/test_certmanager_enabled.yaml: -------------------------------------------------------------------------------- 1 | certManager: 2 | enabled: true -------------------------------------------------------------------------------- /example/app1/test_v2/test_certmanager_enabled.yaml: -------------------------------------------------------------------------------- 1 | certManager: 2 | enabled: true -------------------------------------------------------------------------------- /example/app1/test_v3/test_certmanager_enabled.yaml: -------------------------------------------------------------------------------- 1 | certManager: 2 | enabled: true -------------------------------------------------------------------------------- /example/app1/testfail/test_certmanager_enabled.yaml: -------------------------------------------------------------------------------- 1 | certManager: 2 | enabled: true -------------------------------------------------------------------------------- /pkg/charts/__snapshots__/empty.yaml: -------------------------------------------------------------------------------- 1 | # chartsnap: snapshot_version=v3 2 | --- 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | *.bk 3 | dist/ 4 | 5 | dist/ 6 | cover.out 7 | 8 | bin 9 | releases -------------------------------------------------------------------------------- /example/app1/test_latest/test_certmanager_enabled.yaml: -------------------------------------------------------------------------------- 1 | certManager: 2 | enabled: true -------------------------------------------------------------------------------- /example/app2/__snapshots__/default.snap: -------------------------------------------------------------------------------- 1 | # chartsnap: snapshot_version=v3 2 | --- 3 | -------------------------------------------------------------------------------- /docs/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jlandowner/helm-chartsnap/HEAD/docs/screenshot.png -------------------------------------------------------------------------------- /docs/screenshot-seq.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jlandowner/helm-chartsnap/HEAD/docs/screenshot-seq.png -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "hack/test/charts"] 2 | path = hack/test/charts 3 | url = https://github.com/Kong/charts 4 | -------------------------------------------------------------------------------- /pkg/snap/__snapshot__/json.snap: -------------------------------------------------------------------------------- 1 | [json] 2 | SnapShot = """ 3 | { 4 | \"name\": \"John\", 5 | \"age\": 30 6 | } 7 | """ 8 | -------------------------------------------------------------------------------- /example/app1/test_v1/test_hpa_enabled.yaml: -------------------------------------------------------------------------------- 1 | autoscaling: 2 | enabled: true 3 | maxReplicas: 10 4 | targetCPUUtilizationPercentage: 65 5 | -------------------------------------------------------------------------------- /example/app1/test_v2/test_hpa_enabled.yaml: -------------------------------------------------------------------------------- 1 | autoscaling: 2 | enabled: true 3 | maxReplicas: 10 4 | targetCPUUtilizationPercentage: 65 5 | -------------------------------------------------------------------------------- /example/app1/test_v3/test_hpa_enabled.yaml: -------------------------------------------------------------------------------- 1 | autoscaling: 2 | enabled: true 3 | maxReplicas: 10 4 | targetCPUUtilizationPercentage: 65 5 | -------------------------------------------------------------------------------- /example/app1/testfail/test_hpa_enabled.yaml: -------------------------------------------------------------------------------- 1 | autoscaling: 2 | enabled: true 3 | maxReplicas: 10 4 | targetCPUUtilizationPercentage: 65 5 | -------------------------------------------------------------------------------- /example/app1/test_latest/test_hpa_enabled.yaml: -------------------------------------------------------------------------------- 1 | autoscaling: 2 | enabled: true 3 | maxReplicas: 10 4 | targetCPUUtilizationPercentage: 65 5 | -------------------------------------------------------------------------------- /example/remote/nginx-gateway-fabric.values.yaml: -------------------------------------------------------------------------------- 1 | service: 2 | annotations: 3 | service.beta.kubernetes.io/aws-load-balancer-type: "internal" -------------------------------------------------------------------------------- /example/app1/test_latest/test_hpa_enabled.yml: -------------------------------------------------------------------------------- 1 | # This file is a duplicate of test_hpa_enabled.yaml to test .yml extension support. 2 | autoscaling: 3 | enabled: true 4 | maxReplicas: 10 5 | targetCPUUtilizationPercentage: 65 6 | -------------------------------------------------------------------------------- /example/app1/test_latest/test_hpa_enabled_20.yml: -------------------------------------------------------------------------------- 1 | # This file is a duplicate of test_hpa_enabled.yaml to test .yml extension support. 2 | autoscaling: 3 | enabled: true 4 | maxReplicas: 20 5 | targetCPUUtilizationPercentage: 65 6 | -------------------------------------------------------------------------------- /example/app3/templates/secret.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: key-secret 5 | namespace: {{ .Release.Namespace }} 6 | type: kubernetes.io/tls 7 | data: 8 | apiKey: {{ required "apiKey is required" .Values.apiKey }} -------------------------------------------------------------------------------- /pkg/charts/testdata/.chartsnap.yaml: -------------------------------------------------------------------------------- 1 | dynamicFields: 2 | - apiVersion: v1 3 | kind: Secret 4 | name: app1-cert 5 | jsonPath: 6 | - /data/ca.crt 7 | - /data/tls.crt 8 | - /data/tls.key 9 | base64: true 10 | -------------------------------------------------------------------------------- /pkg/charts/testdata/helm_cmd.bash: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Output arguments 4 | echo "Arguments for helm: $@" 5 | 6 | # Output environment variables starting with "HELM_" 7 | echo "Environment variables starting with HELM_:" 8 | env | grep '^HELM_' -------------------------------------------------------------------------------- /pkg/api/v1alpha1/testdata/.chartsnap.yaml: -------------------------------------------------------------------------------- 1 | dynamicFields: 2 | - apiVersion: v1 3 | kind: Secret 4 | name: app1-cert 5 | jsonPath: 6 | - /data/ca.crt 7 | - /data/tls.crt 8 | - /data/tls.key 9 | base64: true 10 | -------------------------------------------------------------------------------- /pkg/charts/testdata/snap_values.yaml: -------------------------------------------------------------------------------- 1 | testSpec: 2 | dynamicFields: 3 | - apiVersion: v1 4 | kind: Secret 5 | name: app1-cert 6 | jsonPath: 7 | - /data/tls.key 8 | - /data/tls.crt 9 | - /data/ca.crt 10 | base64: true 11 | -------------------------------------------------------------------------------- /pkg/charts/testdata/testspec_values.yaml: -------------------------------------------------------------------------------- 1 | testSpec: 2 | dynamicFields: 3 | - apiVersion: v1 4 | kind: Secret 5 | name: app1-cert 6 | jsonPath: 7 | - /data/ca.crt 8 | - /data/tls.crt 9 | - /data/tls.key 10 | base64: true 11 | -------------------------------------------------------------------------------- /pkg/api/v1alpha1/testdata/testspec_values.yaml: -------------------------------------------------------------------------------- 1 | testSpec: 2 | dynamicFields: 3 | - apiVersion: v1 4 | kind: Secret 5 | name: app1-cert 6 | jsonPath: 7 | - /data/ca.crt 8 | - /data/tls.crt 9 | - /data/tls.key 10 | base64: true 11 | -------------------------------------------------------------------------------- /example/app3/test/__snapshots__/ok.snap: -------------------------------------------------------------------------------- 1 | # chartsnap: snapshot_version=v3 2 | --- 3 | # Source: app3/templates/secret.yaml 4 | apiVersion: v1 5 | kind: Secret 6 | metadata: 7 | name: key-secret 8 | namespace: default 9 | type: kubernetes.io/tls 10 | data: 11 | apiKey: xxxxxxx 12 | -------------------------------------------------------------------------------- /pkg/api/v1alpha1/testdata/testspec_values_invalid.yaml: -------------------------------------------------------------------------------- 1 | testSpec: 2 | dynamicFields: 3 | - apiVersion: v1 4 | kind: Secret 5 | name: app1-cert 6 | jsonPath: 7 | - /data/ca.crt 8 | - /data/tls.crt 9 | - /data/tls.key 10 | base64 11 | -------------------------------------------------------------------------------- /pkg/charts/__snapshots__/helm-error.snap: -------------------------------------------------------------------------------- 1 | # chartsnap: snapshot_version=v3 2 | --- 3 | apiVersion: helm-chartsnap.jlandowner.dev/v1alpha1 4 | kind: Unknown 5 | metadata: 6 | name: helm-output 7 | raw: | 8 | Error: non-absolute URLs should be in form of repo_name/path_to_chart, got: notfound 9 | -------------------------------------------------------------------------------- /example/app1/test_v1/.chartsnap.yaml: -------------------------------------------------------------------------------- 1 | # This file define common behavior of the chart snapshots in the test directory. 2 | dynamicFields: 3 | - apiVersion: v1 4 | kind: Secret 5 | name: app1-cert 6 | jsonPath: 7 | - /data/ca.crt 8 | - /data/tls.crt 9 | - /data/tls.key 10 | base64: true 11 | -------------------------------------------------------------------------------- /example/app1/test_v2/.chartsnap.yaml: -------------------------------------------------------------------------------- 1 | # This file define common behavior of the chart snapshots in the test directory. 2 | dynamicFields: 3 | - apiVersion: v1 4 | kind: Secret 5 | name: app1-cert 6 | jsonPath: 7 | - /data/ca.crt 8 | - /data/tls.crt 9 | - /data/tls.key 10 | base64: true 11 | -------------------------------------------------------------------------------- /example/app1/test_v3/.chartsnap.yaml: -------------------------------------------------------------------------------- 1 | # This file define common behavior of the chart snapshots in the test directory. 2 | dynamicFields: 3 | - apiVersion: v1 4 | kind: Secret 5 | name: app1-cert 6 | jsonPath: 7 | - /data/ca.crt 8 | - /data/tls.crt 9 | - /data/tls.key 10 | base64: true 11 | -------------------------------------------------------------------------------- /example/app1/testfail/.chartsnap.yaml: -------------------------------------------------------------------------------- 1 | # This file define common behavior of the chart snapshots in the test directory. 2 | dynamicFields: 3 | - apiVersion: v1 4 | kind: Secret 5 | name: app1-cert 6 | jsonPath: 7 | - /data/ca.crt 8 | - /data/tls.crt 9 | - /data/tls.key 10 | base64: true 11 | -------------------------------------------------------------------------------- /example/app1/test_latest/.chartsnap.yaml: -------------------------------------------------------------------------------- 1 | # This file define common behavior of the chart snapshots in the test directory. 2 | dynamicFields: 3 | - apiVersion: v1 4 | kind: Secret 5 | name: app1-cert 6 | jsonPath: 7 | - /data/ca.crt 8 | - /data/tls.crt 9 | - /data/tls.key 10 | base64: true 11 | -------------------------------------------------------------------------------- /example/app3/__snapshots__/default.snap: -------------------------------------------------------------------------------- 1 | # chartsnap: snapshot_version=v3 2 | --- 3 | apiVersion: helm-chartsnap.jlandowner.dev/v1alpha1 4 | kind: Unknown 5 | metadata: 6 | name: helm-output 7 | raw: | 8 | Error: execution error at (app3/templates/secret.yaml:8:13): apiKey is required 9 | 10 | Use --debug flag to render out invalid YAML 11 | -------------------------------------------------------------------------------- /example/app3/__snapshots__/default.snap.yaml: -------------------------------------------------------------------------------- 1 | # chartsnap: snapshot_version=v3 2 | --- 3 | apiVersion: helm-chartsnap.jlandowner.dev/v1alpha1 4 | kind: Unknown 5 | metadata: 6 | name: helm-output 7 | raw: | 8 | Error: execution error at (app3/templates/secret.yaml:8:13): apiKey is required 9 | 10 | Use --debug flag to render out invalid YAML 11 | -------------------------------------------------------------------------------- /example/remote/ingress-nginx.values.yaml: -------------------------------------------------------------------------------- 1 | testSpec: 2 | snapshotFileExt: yaml 3 | 4 | # https://github.com/kubernetes/ingress-nginx/blob/main/charts/ingress-nginx/values.yaml#L518 5 | controller: 6 | service: 7 | internal: 8 | enabled: true 9 | annotations: 10 | # Create internal NLB 11 | service.beta.kubernetes.io/aws-load-balancer-scheme: "internal" 12 | -------------------------------------------------------------------------------- /plugin.yaml: -------------------------------------------------------------------------------- 1 | # helm plugin config file 2 | # Ref: https://helm.sh/docs/topics/plugins/ 3 | name: chartsnap 4 | version: 0.6.0 5 | usage: Snapshot testing for Helm charts 6 | description: Snapshot testing for Helm charts 7 | command: "$HELM_PLUGIN_DIR/bin/chartsnap" 8 | ignoreFlags: false 9 | hooks: 10 | install: "cd $HELM_PLUGIN_DIR; scripts/install_plugin.sh" 11 | update: "cd $HELM_PLUGIN_DIR; scripts/install_plugin.sh" 12 | -------------------------------------------------------------------------------- /example/app1/templates/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ include "app1.fullname" . }} 5 | labels: 6 | {{- include "app1.labels" . | nindent 4 }} 7 | spec: 8 | type: {{ .Values.service.type }} 9 | ports: 10 | - port: {{ .Values.service.port }} 11 | targetPort: http 12 | protocol: TCP 13 | name: http 14 | selector: 15 | {{- include "app1.selectorLabels" . | nindent 4 }} 16 | -------------------------------------------------------------------------------- /example/app1/test_wildcard/wildcard_test.yaml: -------------------------------------------------------------------------------- 1 | autoscaling: 2 | enabled: true 3 | maxReplicas: 10 4 | targetCPUUtilizationPercentage: 65 5 | 6 | testSpec: 7 | dynamicFields: 8 | - jsonPath: 9 | - /metadata/labels/helm.sh~1chart 10 | value: '###CHART_VERSION###' 11 | - kind: Secret 12 | jsonPath: 13 | - /data/ca.crt 14 | - /data/tls.crt 15 | - /data/tls.key 16 | value: '###CUSTOM_CERT_DATA###' -------------------------------------------------------------------------------- /example/app1/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *.orig 18 | *~ 19 | # Various IDEs 20 | .project 21 | .idea/ 22 | *.tmproj 23 | .vscode/ 24 | -------------------------------------------------------------------------------- /example/app1/templates/serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.serviceAccount.create -}} 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: {{ include "app1.serviceAccountName" . }} 6 | labels: 7 | {{- include "app1.labels" . | nindent 4 }} 8 | {{- with .Values.serviceAccount.annotations }} 9 | annotations: 10 | {{- toYaml . | nindent 4 }} 11 | {{- end }} 12 | automountServiceAccountToken: {{ .Values.serviceAccount.automount }} 13 | {{- end }} 14 | -------------------------------------------------------------------------------- /example/app2/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *.orig 18 | *~ 19 | # Various IDEs 20 | .project 21 | .idea/ 22 | *.tmproj 23 | .vscode/ 24 | -------------------------------------------------------------------------------- /example/app1/templates/tests/test-connection.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | name: "{{ include "app1.fullname" . }}-test-connection" 5 | labels: 6 | {{- include "app1.labels" . | nindent 4 }} 7 | annotations: 8 | "helm.sh/hook": test 9 | spec: 10 | containers: 11 | - name: wget 12 | image: busybox 13 | command: ['wget'] 14 | args: ['{{ include "app1.fullname" . }}:{{ .Values.service.port }}'] 15 | restartPolicy: Never 16 | -------------------------------------------------------------------------------- /example/app1/Makefile: -------------------------------------------------------------------------------- 1 | all: dir 2 | 3 | OPT ?= # --debug 4 | 5 | .PHONY: file 6 | file: 7 | -helm chartsnap $(OPT) -c . -f test/test_ingress_enabled.yaml 8 | 9 | .PHONY: dir 10 | dir: 11 | -helm chartsnap $(OPT) -c . -f test/ 12 | 13 | .PHONY: update 14 | update: 15 | -helm chartsnap $(OPT) -c . -f test/ -u 16 | 17 | helm: 18 | curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash 19 | 20 | helm-chartsnap: 21 | helm plugin install https://github.com/jlandowner/helm-chartsnap 22 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | before: 2 | hooks: 3 | - go mod tidy 4 | builds: 5 | - id: chartsnap 6 | main: ./main.go 7 | binary: chartsnap 8 | env: 9 | - CGO_ENABLED=0 10 | goos: 11 | - linux 12 | - darwin 13 | - windows 14 | goarch: 15 | - amd64 16 | - arm64 17 | archives: 18 | - builds: 19 | - chartsnap 20 | name_template: "chartsnap_{{ .Tag }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}" 21 | wrap_in_directory: false 22 | format: tar.gz 23 | release: 24 | draft: true 25 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "gomod" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /example/app1/test_v1/test_ingress_enabled.yaml: -------------------------------------------------------------------------------- 1 | testSpec: 2 | dynamicFields: 3 | - apiVersion: v1 4 | kind: Secret 5 | name: app1-cert 6 | jsonPath: 7 | - /data/ca.crt 8 | - /data/tls.crt 9 | - /data/tls.key 10 | base64: true 11 | 12 | ingress: 13 | enabled: true 14 | className: "nginx" 15 | annotations: 16 | cert-manager.io/cluster-issuer: nameOfClusterIssuer 17 | hosts: 18 | - host: chart-example.local 19 | paths: 20 | - path: / 21 | pathType: ImplementationSpecific 22 | tls: 23 | - secretName: chart-example-tls 24 | hosts: 25 | - chart-example.local -------------------------------------------------------------------------------- /example/app1/test_v2/test_ingress_enabled.yaml: -------------------------------------------------------------------------------- 1 | testSpec: 2 | dynamicFields: 3 | - apiVersion: v1 4 | kind: Secret 5 | name: app1-cert 6 | jsonPath: 7 | - /data/ca.crt 8 | - /data/tls.crt 9 | - /data/tls.key 10 | base64: true 11 | 12 | ingress: 13 | enabled: true 14 | className: "nginx" 15 | annotations: 16 | cert-manager.io/cluster-issuer: nameOfClusterIssuer 17 | hosts: 18 | - host: chart-example.local 19 | paths: 20 | - path: / 21 | pathType: ImplementationSpecific 22 | tls: 23 | - secretName: chart-example-tls 24 | hosts: 25 | - chart-example.local -------------------------------------------------------------------------------- /example/app1/test_v3/test_ingress_enabled.yaml: -------------------------------------------------------------------------------- 1 | testSpec: 2 | dynamicFields: 3 | - apiVersion: v1 4 | kind: Secret 5 | name: app1-cert 6 | jsonPath: 7 | - /data/ca.crt 8 | - /data/tls.crt 9 | - /data/tls.key 10 | base64: true 11 | 12 | ingress: 13 | enabled: true 14 | className: "nginx" 15 | annotations: 16 | cert-manager.io/cluster-issuer: nameOfClusterIssuer 17 | hosts: 18 | - host: chart-example.local 19 | paths: 20 | - path: / 21 | pathType: ImplementationSpecific 22 | tls: 23 | - secretName: chart-example-tls 24 | hosts: 25 | - chart-example.local -------------------------------------------------------------------------------- /example/app1/testfail/test_ingress_enabled.yaml: -------------------------------------------------------------------------------- 1 | testSpec: 2 | dynamicFields: 3 | - apiVersion: v1 4 | kind: Secret 5 | name: app1-cert 6 | jsonPath: 7 | - /data/ca.crt 8 | - /data/tls.crt 9 | - /data/tls.key 10 | base64: true 11 | 12 | ingress: 13 | enabled: true 14 | className: "nginx" 15 | annotations: 16 | cert-manager.io/cluster-issuer: nameOfClusterIssuer 17 | hosts: 18 | - host: chart-example.local 19 | paths: 20 | - path: / 21 | pathType: ImplementationSpecific 22 | tls: 23 | - secretName: chart-example-tls 24 | hosts: 25 | - chart-example.local -------------------------------------------------------------------------------- /example/app1/test_latest/test_ingress_enabled.yaml: -------------------------------------------------------------------------------- 1 | testSpec: 2 | dynamicFields: 3 | - apiVersion: v1 4 | kind: Secret 5 | name: app1-cert 6 | jsonPath: 7 | - /data/ca.crt 8 | - /data/tls.crt 9 | - /data/tls.key 10 | base64: true 11 | 12 | ingress: 13 | enabled: true 14 | className: "nginx" 15 | annotations: 16 | cert-manager.io/cluster-issuer: nameOfClusterIssuer 17 | hosts: 18 | - host: chart-example.local 19 | paths: 20 | - path: / 21 | pathType: ImplementationSpecific 22 | tls: 23 | - secretName: chart-example-tls 24 | hosts: 25 | - chart-example.local -------------------------------------------------------------------------------- /hack/test/test-kong-chart.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Update submodule to the latest commit 4 | git submodule update --init --recursive --remote --merge 5 | 6 | # Loop through each directory in charts/charts and run helm dependency update 7 | for dir in charts/charts/*; do 8 | if [ -d "$dir" ]; then 9 | cd "$dir" 10 | yq e '.dependencies[] | .name + " " + .repository' Chart.lock | while read line; do 11 | helm repo add $line --force-update 12 | done 13 | helm dependency build 14 | cd - 15 | fi 16 | done 17 | 18 | # Run make test.golden and check return code 19 | make -C charts/ test.golden 20 | if [ $? -eq 0 ]; then 21 | echo "Tests passed successfully."; exit 0 22 | else 23 | echo "Tests failed."; exit 1 24 | fi -------------------------------------------------------------------------------- /pkg/snap/gomega/object_snapshot.go: -------------------------------------------------------------------------------- 1 | package gomega 2 | 3 | import ( 4 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 5 | "k8s.io/apimachinery/pkg/types" 6 | "sigs.k8s.io/controller-runtime/pkg/client" 7 | ) 8 | 9 | func ObjectSnapshot(obj client.Object) client.Object { 10 | t := obj.DeepCopyObject() 11 | o := t.(client.Object) 12 | RemoveDynamicFields(o) 13 | return o 14 | } 15 | 16 | func RemoveDynamicFields(o client.Object) { 17 | o.SetCreationTimestamp(metav1.Time{}) 18 | o.SetResourceVersion("") 19 | o.SetGeneration(0) 20 | o.SetUID(types.UID("")) 21 | o.SetManagedFields(nil) 22 | 23 | ownerRefs := make([]metav1.OwnerReference, len(o.GetOwnerReferences())) 24 | for i, v := range o.GetOwnerReferences() { 25 | v.UID = "" 26 | ownerRefs[i] = v 27 | } 28 | o.SetOwnerReferences(ownerRefs) 29 | } 30 | -------------------------------------------------------------------------------- /example/remote/Makefile: -------------------------------------------------------------------------------- 1 | all: ingress-nginx cilium nginx-gateway-fabric 2 | 3 | OPT ?= # -u 4 | 5 | .PHONY: ingress-nginx 6 | ingress-nginx: 7 | -helm chartsnap $(OPT) -c ingress-nginx -f ingress-nginx.values.yaml -- --repo https://kubernetes.github.io/ingress-nginx --namespace ingress-nginx --skip-tests 8 | 9 | .PHONY: cilium 10 | cilium: 11 | -helm chartsnap $(OPT) -c cilium -f cilium.values.yaml -- --repo https://helm.cilium.io --namespace kube-system 12 | 13 | .PHONY: nginx-gateway-fabric 14 | nginx-gateway-fabric: 15 | -helm chartsnap $(OPT) -c oci://ghcr.io/nginxinc/charts/nginx-gateway-fabric -n nginx-gateway -f nginx-gateway-fabric.values.yaml 16 | 17 | helm: 18 | curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash 19 | 20 | helm-chartsnap: 21 | helm plugin install https://github.com/jlandowner/helm-chartsnap 22 | -------------------------------------------------------------------------------- /pkg/jsonpatch/jsonpatch.go: -------------------------------------------------------------------------------- 1 | package jsonpatch 2 | 3 | import "strings" 4 | 5 | // From http://tools.ietf.org/html/rfc6901#section-4 : 6 | // 7 | // Evaluation of each reference token begins by decoding any escaped 8 | // character sequence. This is performed by first transforming any 9 | // occurrence of the sequence '~1' to '/', and then transforming any 10 | // occurrence of the sequence '~0' to '~'. 11 | 12 | var ( 13 | RFC6901Decoder = strings.NewReplacer("~1", "/", "~0", "~") 14 | ) 15 | 16 | func DecodePatchKey(k string) string { 17 | return RFC6901Decoder.Replace(k) 18 | } 19 | 20 | func SplitPathDecoded(path string) []string { 21 | split := strings.Split(path, "/") 22 | if len(split) < 2 { 23 | return nil 24 | } 25 | parts := split[1:] 26 | for i := 0; i < len(parts); i++ { 27 | parts[i] = DecodePatchKey(parts[i]) 28 | } 29 | return parts 30 | } 31 | -------------------------------------------------------------------------------- /pkg/snap/gomega/snap_test.go: -------------------------------------------------------------------------------- 1 | package gomega 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | . "github.com/onsi/ginkgo/v2" 8 | . "github.com/onsi/gomega" 9 | 10 | "github.com/jlandowner/helm-chartsnap/pkg/unstructured" 11 | ) 12 | 13 | func TestSnap(t *testing.T) { 14 | RegisterFailHandler(Fail) 15 | RunSpecs(t, "Snap Suite") 16 | } 17 | 18 | var _ = Describe("Snap", func() { 19 | It("takes a full snapshot", func() { 20 | b, err := os.ReadFile("testdata/pod.yaml") 21 | Expect(err).NotTo(HaveOccurred()) 22 | Expect(string(b)).To(MatchSnapShot()) 23 | }) 24 | 25 | It("takes a snapshot without dynamic values", func() { 26 | b, err := os.ReadFile("testdata/pod.yaml") 27 | Expect(err).NotTo(HaveOccurred()) 28 | 29 | _, obj, err := unstructured.BytesToUnstructured(b) 30 | Expect(err).NotTo(HaveOccurred()) 31 | 32 | Expect(ObjectSnapshot(obj)).To(MatchSnapShot()) 33 | }) 34 | }) 35 | -------------------------------------------------------------------------------- /example/app1/README.md: -------------------------------------------------------------------------------- 1 | # Example of snapshot testing for local Helm chart 2 | 3 | This chart repository is default helm repository created by `helm create app1` command. 4 | 5 | And add [`test`](test) directory to place test patterns of the chart. 6 | 7 | There is the example test values: 8 | 9 | - [`test_hpa_enabled.yaml`](test/test_hpa_enabled.yaml) 10 | - [`test_ingress_enabled.yaml`](test/test_ingress_enabled.yaml) 11 | 12 | Do snapshot with the specific test values 📸 13 | 14 | ```sh 15 | helm chartsnap -c . -f test/test_ingress_enabled.yaml 16 | ``` 17 | 18 | Or do snapshot for all test values 📸 19 | 20 | ```sh 21 | helm chartsnap -c . -f test/ # specify directory for -f 22 | ``` 23 | 24 | Probably you will see the failure that does not match the snapshot with the above commands. 25 | 26 | Then, update the snapshot with `-u` options. 27 | 28 | ```sh 29 | helm chartsnap -c . -f test/ -u 30 | ``` 31 | -------------------------------------------------------------------------------- /pkg/api/v1alpha1/header.go: -------------------------------------------------------------------------------- 1 | package v1alpha1 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "strings" 7 | ) 8 | 9 | type Header struct { 10 | SnapshotVersion string `header:"snapshot_version"` 11 | } 12 | 13 | func (h *Header) ToString() string { 14 | return fmt.Sprintf("# chartsnap: snapshot_version=%s\n---\n", h.SnapshotVersion) 15 | } 16 | 17 | func ParseHeader(line string) *Header { 18 | h := Header{} 19 | ht := reflect.TypeOf(h) 20 | hv := reflect.ValueOf(&h).Elem() 21 | 22 | split := strings.Split(string([]byte(line)[1:]), " ") 23 | for _, v := range split { 24 | s := strings.Split(v, "=") 25 | if len(s) != 2 { 26 | continue 27 | } 28 | 29 | headerName := strings.TrimSpace(s[0]) 30 | headerValue := strings.TrimSpace(s[1]) 31 | 32 | for i := 0; i < hv.NumField(); i++ { 33 | field := ht.Field(i) 34 | if tag, ok := field.Tag.Lookup("header"); ok && tag == headerName { 35 | hv.Field(i).SetString(headerValue) 36 | } 37 | } 38 | } 39 | return &h 40 | } 41 | -------------------------------------------------------------------------------- /example/remote/cilium.values.yaml: -------------------------------------------------------------------------------- 1 | testSpec: 2 | dynamicFields: 3 | - apiVersion: v1 4 | kind: Secret 5 | name: cilium-ca 6 | jsonPath: 7 | - /data/ca.crt 8 | - /data/ca.key 9 | base64: true 10 | - apiVersion: v1 11 | kind: Secret 12 | name: hubble-relay-client-certs 13 | jsonPath: 14 | - /data/ca.crt 15 | - /data/tls.crt 16 | - /data/tls.key 17 | base64: true 18 | - apiVersion: v1 19 | kind: Secret 20 | name: hubble-server-certs 21 | jsonPath: 22 | - /data/ca.crt 23 | - /data/tls.crt 24 | - /data/tls.key 25 | base64: true 26 | 27 | # https://docs.cilium.io/en/stable/installation/k8s-install-helm/ 28 | # EKS 29 | 30 | eni: 31 | enabled: true 32 | 33 | ipam: 34 | mode: eni 35 | 36 | egressMasqueradeInterfaces: eth0 37 | 38 | routingMode: native 39 | 40 | # https://docs.cilium.io/en/stable/gettingstarted/hubble/#hubble-ui 41 | # Enable Hubble UI 42 | hubble: 43 | relay: 44 | enabled: true 45 | ui: 46 | enabled: true -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Pre-requisites 4 | - Go 1.20 or later 5 | - yq 4.0 or later 6 | 7 | ## Development Environment Setup 8 | 9 | To set up your development environment, run the following command: 10 | 11 | ```sh 12 | make setup 13 | ``` 14 | 15 | This command will: 16 | 17 | 1. Download the necessary Go version. 18 | 2. Install Helm v3. 19 | 20 | After running `make setup`, you can run the tests to verify the setup: 21 | 22 | ```sh 23 | make test 24 | ``` 25 | 26 | ## Testing 27 | 28 | - Unit Test 29 | ```sh 30 | make test 31 | ``` 32 | 33 | - Integration Test 34 | ```sh 35 | # build the plugin from source and place it in the local Helm plugins directory 36 | make integ-test 37 | ``` 38 | 39 | - Integration Test (Snapshot not matched) 40 | ```sh 41 | # This will test fail cases where the snapshot does not match the current output. 42 | make integ-test-fail 43 | ``` 44 | 45 | - Integration Test (Kong) 46 | ```sh 47 | # This will run the integration tests against a Kong chart submodule. 48 | make integ-test-kong 49 | ``` 50 | -------------------------------------------------------------------------------- /pkg/snap/gomega/snap.go: -------------------------------------------------------------------------------- 1 | package gomega 2 | 3 | import ( 4 | "fmt" 5 | "path/filepath" 6 | "regexp" 7 | "strings" 8 | 9 | "github.com/onsi/ginkgo/v2" 10 | "github.com/onsi/gomega/types" 11 | 12 | "github.com/jlandowner/helm-chartsnap/pkg/snap" 13 | ) 14 | 15 | var ( 16 | shotCountMap = map[string]int{} 17 | trimSpace = regexp.MustCompile(` +`) 18 | ) 19 | 20 | // MatchSnapShot returns a Gomega matcher that compares the actual value with the snapshot file. 21 | func MatchSnapShot() types.GomegaMatcher { 22 | 23 | testFile := ginkgo.CurrentSpecReport().FileName() 24 | path := filepath.Dir(testFile) 25 | file := filepath.Base(testFile) 26 | snapFile := filepath.Join(path, "__snapshots__", strings.TrimSuffix(file, ".go")+".snap") 27 | 28 | testLabel := ginkgo.CurrentSpecReport().FullText() 29 | testLabel = trimSpace.ReplaceAllString(testLabel, " ") 30 | 31 | count := shotCountMap[testLabel] 32 | count++ 33 | shotCountMap[testLabel] = count 34 | snapId := fmt.Sprintf("%s %d", testLabel, count) 35 | 36 | return snap.SnapshotMatcher(snapFile, snap.WithSnapshotID(snapId)) 37 | } 38 | -------------------------------------------------------------------------------- /example/app1/templates/cert.yaml: -------------------------------------------------------------------------------- 1 | {{ $tls := fromYaml ( include "app1.gen-certs" . ) }} 2 | --- 3 | {{- if not $.Values.certManager.enabled }} 4 | apiVersion: v1 5 | kind: Secret 6 | metadata: 7 | name: app1-cert 8 | namespace: {{ .Release.Namespace }} 9 | labels: 10 | {{- include "app1.labels" . | nindent 4 }} 11 | type: kubernetes.io/tls 12 | data: 13 | ca.crt: {{ $tls.caCert }} 14 | tls.crt: {{ $tls.clientCert }} 15 | tls.key: {{ $tls.clientKey }} 16 | {{- else }} 17 | apiVersion: cert-manager.io/v1 18 | kind: Certificate 19 | metadata: 20 | labels: 21 | {{- include "app1.labels" . | nindent 4 }} 22 | name: app1-cert 23 | namespace: {{ .Release.Namespace }} 24 | spec: 25 | dnsNames: 26 | - {{ include "app1.fullname" . }}.{{.Release.Namespace}}.svc 27 | - {{ include "app1.fullname" . }}.{{.Release.Namespace}}.svc.cluster.local 28 | issuerRef: 29 | {{- if .Values.certManager.issuer.clusterIssuer }} 30 | kind: ClusterIssuer 31 | {{- else }} 32 | kind: Issuer 33 | {{- end }} 34 | name: {{ .Values.certManager.issuer.name }} 35 | secretName: app1-cert 36 | {{- end }} -------------------------------------------------------------------------------- /example/app1/templates/hpa.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.autoscaling.enabled }} 2 | apiVersion: autoscaling/v2 3 | kind: HorizontalPodAutoscaler 4 | metadata: 5 | name: {{ include "app1.fullname" . }} 6 | labels: 7 | {{- include "app1.labels" . | nindent 4 }} 8 | spec: 9 | scaleTargetRef: 10 | apiVersion: apps/v1 11 | kind: Deployment 12 | name: {{ include "app1.fullname" . }} 13 | minReplicas: {{ .Values.autoscaling.minReplicas }} 14 | maxReplicas: {{ .Values.autoscaling.maxReplicas }} 15 | metrics: 16 | {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} 17 | - type: Resource 18 | resource: 19 | name: cpu 20 | target: 21 | type: Utilization 22 | averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} 23 | {{- end }} 24 | {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} 25 | - type: Resource 26 | resource: 27 | name: memory 28 | target: 29 | type: Utilization 30 | averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} 31 | {{- end }} 32 | {{- end }} 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 jlandowner 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /pkg/charts/__snapshots__/helm_test.snap: -------------------------------------------------------------------------------- 1 | ['Helm test mocks should execute as helm cmd 1'] 2 | SnapShot = '' 3 | 4 | ['Helm when Execute should execute with expected args and env 1'] 5 | SnapShot = """ 6 | Arguments for helm: template aaa ccc --namespace=bbb --values=ddd 7 | Environment variables starting with HELM_: 8 | HELM_DEBUG=false 9 | """ 10 | 11 | ['Helm when Execute with additional args should execute with expected args and env 1'] 12 | SnapShot = """ 13 | Arguments for helm: template chartsnap postgres --namespace=xxx --values=postgres.values.yaml --repo https://charts.bitnami.com/bitnami --skip-tests 14 | Environment variables starting with HELM_: 15 | HELM_DEBUG=false 16 | """ 17 | 18 | ['Helm when Execute without namespace should execute with expected args and env 1'] 19 | SnapShot = """ 20 | Arguments for helm: template chartsnap charts/app1/ --values=charts/app1/test/test.values.yaml 21 | Environment variables starting with HELM_: 22 | HELM_DEBUG=false 23 | """ 24 | 25 | ['Helm when Execute without values should execute with expected args and env 1'] 26 | SnapShot = """ 27 | Arguments for helm: template chartsnap charts/app1/ --namespace=default 28 | Environment variables starting with HELM_: 29 | HELM_DEBUG=false 30 | """ 31 | -------------------------------------------------------------------------------- /example/app1/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: app1 3 | description: A Helm chart for Kubernetes 4 | 5 | # A chart can be either an 'application' or a 'library' chart. 6 | # 7 | # Application charts are a collection of templates that can be packaged into versioned archives 8 | # to be deployed. 9 | # 10 | # Library charts provide useful utilities or functions for the chart developer. They're included as 11 | # a dependency of application charts to inject those utilities and functions into the rendering 12 | # pipeline. Library charts do not define any templates and therefore cannot be deployed. 13 | type: application 14 | 15 | # This is the chart version. This version number should be incremented each time you make changes 16 | # to the chart and its templates, including the app version. 17 | # Versions are expected to follow Semantic Versioning (https://semver.org/) 18 | version: 0.1.0 19 | 20 | # This is the version number of the application being deployed. This version number should be 21 | # incremented each time you make changes to the application. Versions are not expected to 22 | # follow Semantic Versioning. They should reflect the version the application is using. 23 | # It is recommended to use it with quotes. 24 | appVersion: "1.16.0" 25 | -------------------------------------------------------------------------------- /example/app2/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: app2 3 | description: A Helm chart for Kubernetes 4 | 5 | # A chart can be either an 'application' or a 'library' chart. 6 | # 7 | # Application charts are a collection of templates that can be packaged into versioned archives 8 | # to be deployed. 9 | # 10 | # Library charts provide useful utilities or functions for the chart developer. They're included as 11 | # a dependency of application charts to inject those utilities and functions into the rendering 12 | # pipeline. Library charts do not define any templates and therefore cannot be deployed. 13 | type: application 14 | 15 | # This is the chart version. This version number should be incremented each time you make changes 16 | # to the chart and its templates, including the app version. 17 | # Versions are expected to follow Semantic Versioning (https://semver.org/) 18 | version: 0.1.0 19 | 20 | # This is the version number of the application being deployed. This version number should be 21 | # incremented each time you make changes to the application. Versions are not expected to 22 | # follow Semantic Versioning. They should reflect the version the application is using. 23 | # It is recommended to use it with quotes. 24 | appVersion: "1.16.0" 25 | -------------------------------------------------------------------------------- /example/app3/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: app3 3 | description: A Helm chart for Kubernetes 4 | 5 | # A chart can be either an 'application' or a 'library' chart. 6 | # 7 | # Application charts are a collection of templates that can be packaged into versioned archives 8 | # to be deployed. 9 | # 10 | # Library charts provide useful utilities or functions for the chart developer. They're included as 11 | # a dependency of application charts to inject those utilities and functions into the rendering 12 | # pipeline. Library charts do not define any templates and therefore cannot be deployed. 13 | type: application 14 | 15 | # This is the chart version. This version number should be incremented each time you make changes 16 | # to the chart and its templates, including the app version. 17 | # Versions are expected to follow Semantic Versioning (https://semver.org/) 18 | version: 0.1.0 19 | 20 | # This is the version number of the application being deployed. This version number should be 21 | # incremented each time you make changes to the application. Versions are not expected to 22 | # follow Semantic Versioning. They should reflect the version the application is using. 23 | # It is recommended to use it with quotes. 24 | appVersion: "1.16.0" 25 | -------------------------------------------------------------------------------- /pkg/charts/helm.go: -------------------------------------------------------------------------------- 1 | package charts 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | "os/exec" 8 | ) 9 | 10 | type HelmTemplateCmdOptions struct { 11 | HelmPath string 12 | ReleaseName string 13 | Namespace string 14 | Chart string 15 | ValuesFile string 16 | AdditionalArgs []string 17 | } 18 | 19 | func (o *HelmTemplateCmdOptions) Args() []string { 20 | args := []string{ 21 | "template", o.ReleaseName, o.Chart, 22 | } 23 | if o.Namespace != "" { 24 | args = append(args, fmt.Sprintf("--namespace=%s", o.Namespace)) 25 | } 26 | if o.ValuesFile != "" { 27 | args = append(args, fmt.Sprintf("--values=%s", o.ValuesFile)) 28 | } 29 | if len(o.AdditionalArgs) > 0 { 30 | args = append(args, o.AdditionalArgs...) 31 | } 32 | return args 33 | } 34 | 35 | func (o *HelmTemplateCmdOptions) Execute(ctx context.Context) ([]byte, error) { 36 | args := o.Args() 37 | log().DebugContext(ctx, "executing 'helm template' command", "args", args, "additionalArgs", o.AdditionalArgs) 38 | 39 | // helm template should not be executed in debug mode because YAML parser fails. 40 | os.Setenv("HELM_DEBUG", "false") 41 | 42 | cmd := exec.CommandContext(ctx, o.HelmPath, args...) 43 | out, err := cmd.CombinedOutput() 44 | return out, err 45 | } 46 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: 8 | - main 9 | workflow_dispatch: 10 | 11 | jobs: 12 | unittest: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout code 16 | uses: actions/checkout@v4 17 | 18 | - name: Setup Go 19 | uses: actions/setup-go@v5 20 | with: 21 | go-version-file: go.mod 22 | - run: go version 23 | 24 | - name: Unittest 25 | run: | 26 | make test GO=go 27 | 28 | - name: Upload coverage reports to Codecov 29 | uses: codecov/codecov-action@v5 30 | with: 31 | token: ${{ secrets.CODECOV_TOKEN }} 32 | file: ./coverage.txt 33 | 34 | integ-test: 35 | runs-on: ubuntu-latest 36 | steps: 37 | - name: Checkout code 38 | uses: actions/checkout@v4 39 | 40 | - name: Setup Go 41 | uses: actions/setup-go@v5 42 | with: 43 | go-version-file: go.mod 44 | - run: go version 45 | 46 | - name: Integration test 47 | run: | 48 | FORCE_COLOR=1 make integ-test GO=go 49 | 50 | - name: Integration test kong 51 | run: | 52 | FORCE_COLOR=1 make integ-test-kong GO=go 53 | -------------------------------------------------------------------------------- /docs/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Helm Chartsnap 2 | 3 | We appreciate your contributions and look forward to collaborating with you! 4 | 5 | ## How to Contribute 6 | - **Report Bugs**: Use the [GitHub Issues](https://github.com/your-repo/issues) to report bugs. 7 | - **Suggest Features**: Open a feature request in the Issues section. 8 | - **Submit Pull Requests**: Follow the guidelines below to submit changes. 9 | 10 | --- 11 | 12 | ## Development Setup 13 | 1. Clone the repository: 14 | ```bash 15 | git clone https://github.com/your-repo/helm-chartsnap.git 16 | cd helm-chartsnap 17 | ``` 18 | 19 | 2. Install dependencies: 20 | ```bash 21 | make install 22 | ``` 23 | 24 | 3. Run tests to ensure everything is working: 25 | ```bash 26 | (make test && make integ-test && make integ-test-fail && make integ-test-kong) \ 27 | && echo "All tests passed" || echo "Some tests failed" 28 | ``` 29 | 30 | --- 31 | 32 | ## Testing 33 | - Run unit tests: 34 | ```bash 35 | make test 36 | ``` 37 | 38 | - Run integration tests: 39 | ```bash 40 | make integ-test 41 | ``` 42 | 43 | - Run integration tests with failure: 44 | ```bash 45 | make integ-test-fail 46 | ``` 47 | 48 | - Run integration tests using Kong chart: 49 | ```bash 50 | make integ-test-kong 51 | ``` 52 | -------------------------------------------------------------------------------- /pkg/api/v1alpha1/header_test.go: -------------------------------------------------------------------------------- 1 | package v1alpha1 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestHeader_ToString(t *testing.T) { 9 | type fields struct { 10 | Version string 11 | SnapshotVersion string 12 | Chart string 13 | Values string 14 | } 15 | tests := []struct { 16 | name string 17 | fields fields 18 | want string 19 | }{ 20 | { 21 | name: "Test Header ToString", 22 | fields: fields{ 23 | SnapshotVersion: "v3", 24 | }, 25 | want: "# chartsnap: snapshot_version=v3\n---\n", 26 | }, 27 | } 28 | for _, tt := range tests { 29 | t.Run(tt.name, func(t *testing.T) { 30 | h := &Header{ 31 | SnapshotVersion: tt.fields.SnapshotVersion, 32 | } 33 | if got := h.ToString(); got != tt.want { 34 | t.Errorf("Header.ToString() = %v, want %v", got, tt.want) 35 | } 36 | }) 37 | } 38 | } 39 | 40 | func TestParseHeader(t *testing.T) { 41 | type args struct { 42 | line string 43 | } 44 | tests := []struct { 45 | name string 46 | args args 47 | want *Header 48 | }{ 49 | { 50 | name: "Test ParseHeader", 51 | args: args{ 52 | line: "# chartsnap: snapshot_version=v3\n", 53 | }, 54 | want: &Header{ 55 | SnapshotVersion: "v3", 56 | }, 57 | }, 58 | } 59 | for _, tt := range tests { 60 | t.Run(tt.name, func(t *testing.T) { 61 | if got := ParseHeader(tt.args.line); !reflect.DeepEqual(got, tt.want) { 62 | t.Errorf("ParseHeader() = %v, want %v", got, tt.want) 63 | } 64 | }) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /pkg/snap/cachefs.go: -------------------------------------------------------------------------------- 1 | package snap 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | "path/filepath" 8 | "time" 9 | 10 | "github.com/spf13/afero" 11 | ) 12 | 13 | var ( 14 | cacheFs = afero.NewCacheOnReadFs( 15 | afero.NewOsFs(), 16 | afero.NewMemMapFs(), 17 | time.Minute, 18 | ) 19 | ) 20 | 21 | func WriteFile(path string, data []byte) error { 22 | log().Debug("write file by cacheFs", "path", path) 23 | 24 | if err := cacheFs.MkdirAll(filepath.Dir(path), os.ModePerm); err != nil { 25 | return fmt.Errorf("create shopshot directory error: %w", err) 26 | } 27 | file, err := cacheFs.Create(path) 28 | if err != nil { 29 | return fmt.Errorf("open shopshot file error: %w", err) 30 | } 31 | defer file.Close() 32 | 33 | if _, err := file.Write(data); err != nil { 34 | return fmt.Errorf("write shopshot file error: %w", err) 35 | } 36 | return nil 37 | } 38 | 39 | func ReadFile(path string) ([]byte, error) { 40 | log().Debug("read file by cacheFs", "path", path) 41 | 42 | exists, err := afero.Exists(cacheFs, path) 43 | if err != nil { 44 | return nil, fmt.Errorf("file check error: %w", err) 45 | } 46 | if !exists { 47 | return nil, afero.ErrFileNotFound 48 | } 49 | 50 | file, err := cacheFs.Open(path) 51 | if err != nil { 52 | return nil, fmt.Errorf("file open error: %w", err) 53 | } 54 | defer file.Close() 55 | 56 | b, err := io.ReadAll(file) 57 | if err != nil { 58 | return nil, fmt.Errorf("file read error: %w", err) 59 | } 60 | return b, nil 61 | } 62 | 63 | func RemoveFile(path string) error { 64 | log().Debug("remove file by cacheFs", "path", path) 65 | return cacheFs.Remove(path) 66 | } 67 | -------------------------------------------------------------------------------- /example/app1/templates/NOTES.txt: -------------------------------------------------------------------------------- 1 | 1. Get the application URL by running these commands: 2 | {{- if .Values.ingress.enabled }} 3 | {{- range $host := .Values.ingress.hosts }} 4 | {{- range .paths }} 5 | http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} 6 | {{- end }} 7 | {{- end }} 8 | {{- else if contains "NodePort" .Values.service.type }} 9 | export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "app1.fullname" . }}) 10 | export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") 11 | echo http://$NODE_IP:$NODE_PORT 12 | {{- else if contains "LoadBalancer" .Values.service.type }} 13 | NOTE: It may take a few minutes for the LoadBalancer IP to be available. 14 | You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "app1.fullname" . }}' 15 | export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "app1.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") 16 | echo http://$SERVICE_IP:{{ .Values.service.port }} 17 | {{- else if contains "ClusterIP" .Values.service.type }} 18 | export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "app1.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") 19 | export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") 20 | echo "Visit http://127.0.0.1:8080 to use your application" 21 | kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT 22 | {{- end }} 23 | -------------------------------------------------------------------------------- /example/app2/templates/NOTES.txt: -------------------------------------------------------------------------------- 1 | 1. Get the application URL by running these commands: 2 | {{- if .Values.ingress.enabled }} 3 | {{- range $host := .Values.ingress.hosts }} 4 | {{- range .paths }} 5 | http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} 6 | {{- end }} 7 | {{- end }} 8 | {{- else if contains "NodePort" .Values.service.type }} 9 | export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "app2.fullname" . }}) 10 | export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") 11 | echo http://$NODE_IP:$NODE_PORT 12 | {{- else if contains "LoadBalancer" .Values.service.type }} 13 | NOTE: It may take a few minutes for the LoadBalancer IP to be available. 14 | You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "app2.fullname" . }}' 15 | export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "app2.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") 16 | echo http://$SERVICE_IP:{{ .Values.service.port }} 17 | {{- else if contains "ClusterIP" .Values.service.type }} 18 | export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "app2.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") 19 | export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") 20 | echo "Visit http://127.0.0.1:8080 to use your application" 21 | kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT 22 | {{- end }} 23 | -------------------------------------------------------------------------------- /hack/crd/helm-chartsnap.jlandowner.dev_unknowns.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | annotations: 6 | controller-gen.kubebuilder.io/version: v0.15.0 7 | name: unknowns.helm-chartsnap.jlandowner.dev 8 | spec: 9 | group: helm-chartsnap.jlandowner.dev 10 | names: 11 | kind: Unknown 12 | listKind: UnknownList 13 | plural: unknowns 14 | singular: unknown 15 | scope: Namespaced 16 | versions: 17 | - name: v1alpha1 18 | schema: 19 | openAPIV3Schema: 20 | description: Unknown is a placeholder for an unrecognized resource in stdout/stderr 21 | of helm template command output. 22 | properties: 23 | apiVersion: 24 | description: |- 25 | APIVersion defines the versioned schema of this representation of an object. 26 | Servers should convert recognized schemas to the latest internal value, and 27 | may reject unrecognized values. 28 | More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources 29 | type: string 30 | kind: 31 | description: |- 32 | Kind is a string value representing the REST resource this object represents. 33 | Servers may infer this from the endpoint the client submits requests to. 34 | Cannot be updated. 35 | In CamelCase. 36 | More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds 37 | type: string 38 | metadata: 39 | type: object 40 | raw: 41 | description: Raw is the raw string of the helm output. 42 | type: string 43 | type: object 44 | served: true 45 | storage: true 46 | -------------------------------------------------------------------------------- /example/app2/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* 2 | Expand the name of the chart. 3 | */}} 4 | {{- define "app2.name" -}} 5 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} 6 | {{- end }} 7 | 8 | {{/* 9 | Create a default fully qualified app name. 10 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 11 | If release name contains chart name it will be used as a full name. 12 | */}} 13 | {{- define "app2.fullname" -}} 14 | {{- if .Values.fullnameOverride }} 15 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} 16 | {{- else }} 17 | {{- $name := default .Chart.Name .Values.nameOverride }} 18 | {{- if contains $name .Release.Name }} 19 | {{- .Release.Name | trunc 63 | trimSuffix "-" }} 20 | {{- else }} 21 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} 22 | {{- end }} 23 | {{- end }} 24 | {{- end }} 25 | 26 | {{/* 27 | Create chart name and version as used by the chart label. 28 | */}} 29 | {{- define "app2.chart" -}} 30 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} 31 | {{- end }} 32 | 33 | {{/* 34 | Common labels 35 | */}} 36 | {{- define "app2.labels" -}} 37 | helm.sh/chart: {{ include "app2.chart" . }} 38 | {{ include "app2.selectorLabels" . }} 39 | {{- if .Chart.AppVersion }} 40 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 41 | {{- end }} 42 | app.kubernetes.io/managed-by: {{ .Release.Service }} 43 | {{- end }} 44 | 45 | {{/* 46 | Selector labels 47 | */}} 48 | {{- define "app2.selectorLabels" -}} 49 | app.kubernetes.io/name: {{ include "app2.name" . }} 50 | app.kubernetes.io/instance: {{ .Release.Name }} 51 | {{- end }} 52 | 53 | {{/* 54 | Create the name of the service account to use 55 | */}} 56 | {{- define "app2.serviceAccountName" -}} 57 | {{- if .Values.serviceAccount.create }} 58 | {{- default (include "app2.fullname" .) .Values.serviceAccount.name }} 59 | {{- else }} 60 | {{- default "default" .Values.serviceAccount.name }} 61 | {{- end }} 62 | {{- end }} 63 | -------------------------------------------------------------------------------- /pkg/jsonpatch/jsonpatch_test.go: -------------------------------------------------------------------------------- 1 | package jsonpatch 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestDecodePatchKey(t *testing.T) { 9 | type args struct { 10 | k string 11 | } 12 | tests := []struct { 13 | name string 14 | args args 15 | want string 16 | }{ 17 | { 18 | name: "test1", 19 | args: args{ 20 | k: "/metadata/annotations/field~1name", 21 | }, 22 | want: "/metadata/annotations/field/name", 23 | }, 24 | { 25 | name: "test2", 26 | args: args{ 27 | k: "/metadata/annotations/field~0name", 28 | }, 29 | want: "/metadata/annotations/field~name", 30 | }, 31 | } 32 | for _, tt := range tests { 33 | t.Run(tt.name, func(t *testing.T) { 34 | if got := DecodePatchKey(tt.args.k); got != tt.want { 35 | t.Errorf("DecodePatchKey() = %v, want %v", got, tt.want) 36 | } 37 | }) 38 | } 39 | } 40 | 41 | func TestSplitPathDecoded(t *testing.T) { 42 | type args struct { 43 | path string 44 | } 45 | tests := []struct { 46 | name string 47 | args args 48 | want []string 49 | }{ 50 | { 51 | name: "test1", 52 | args: args{ 53 | path: "/metadata/annotations/field~1name", 54 | }, 55 | want: []string{"metadata", "annotations", "field/name"}, 56 | }, 57 | { 58 | name: "test2", 59 | args: args{ 60 | path: "/metadata/annotations/field~0name", 61 | }, 62 | want: []string{"metadata", "annotations", "field~name"}, 63 | }, 64 | { 65 | name: "test3", 66 | args: args{ 67 | path: "/metadata", 68 | }, 69 | want: []string{"metadata"}, 70 | }, 71 | { 72 | name: "test4", 73 | args: args{ 74 | path: "/", 75 | }, 76 | want: []string{""}, 77 | }, 78 | { 79 | name: "test5", 80 | args: args{ 81 | path: "", 82 | }, 83 | want: nil, 84 | }, 85 | } 86 | for _, tt := range tests { 87 | t.Run(tt.name, func(t *testing.T) { 88 | if got := SplitPathDecoded(tt.args.path); !reflect.DeepEqual(got, tt.want) { 89 | t.Errorf("SplitPathDecoded() = %v, want %v", got, tt.want) 90 | } 91 | }) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 🚀 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | semver: 6 | type: string 7 | required: true 8 | description: 'Semver for release. (e.g. 0.0.1) Not prefix v!' 9 | 10 | jobs: 11 | create-tag: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4 16 | with: 17 | fetch-depth: 0 18 | ref: main 19 | 20 | - name: Try to update inline versions 21 | run: | 22 | make update-versions VERSION=${SEMVER} 23 | env: 24 | SEMVER: ${{ github.event.inputs.semver }} 25 | 26 | - name: Check Diff 27 | id: diff 28 | run: | 29 | git add -N . 30 | git diff --name-only --exit-code 31 | continue-on-error: true 32 | 33 | - name: Commit & Push 34 | if: steps.diff.outcome == 'failure' 35 | run: | 36 | git config user.name github-actions[bot] 37 | git config user.email 41898282+github-actions[bot]@users.noreply.github.com 38 | git add . 39 | git commit --author=. -m 'Auto Update by Release Action' 40 | git push 41 | 42 | - name: Create tag 43 | run: | 44 | git tag ${GIT_TAG} 45 | git push origin ${GIT_TAG} 46 | env: 47 | GIT_TAG: v${{ github.event.inputs.semver }} 48 | 49 | create-release: 50 | runs-on: ubuntu-latest 51 | needs: create-tag 52 | steps: 53 | - name: Checkout 54 | uses: actions/checkout@v4 55 | with: 56 | fetch-depth: 0 57 | ref: main 58 | 59 | - name: Setup Go 60 | uses: actions/setup-go@v5 61 | with: 62 | go-version-file: go.mod 63 | - run: go version 64 | 65 | - name: GoReleaser 66 | uses: goreleaser/goreleaser-action@v5 67 | with: 68 | distribution: goreleaser 69 | version: latest 70 | args: release --clean 71 | env: 72 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 73 | -------------------------------------------------------------------------------- /.github/workflows/check-for-updates.yml: -------------------------------------------------------------------------------- 1 | name: Check for updates of remote charts 2 | 3 | on: 4 | schedule: 5 | - cron: "0 0 * * 5" # every Friday at 00:00 6 | 7 | jobs: 8 | ingress-nginx: 9 | runs-on: ubuntu-latest 10 | name: Do snapshot ingress-nginx and create PR if snapshot changed 11 | permissions: 12 | contents: write 13 | pull-requests: write 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v4 17 | 18 | - name: Update Snapshot 19 | uses: jlandowner/helm-chartsnap-action@v1 20 | id: helm-chartsnap-action 21 | with: 22 | chart: ingress-nginx 23 | repo: https://kubernetes.github.io/ingress-nginx 24 | values: example/remote/ingress-nginx.values.yaml 25 | additional_args: "--namespace ingress-nginx --skip-tests" 26 | update_snapshot: true 27 | 28 | cilium: 29 | runs-on: ubuntu-latest 30 | name: Do snapshot cilium and create PR if snapshot changed 31 | permissions: 32 | contents: write 33 | pull-requests: write 34 | steps: 35 | - name: Checkout 36 | uses: actions/checkout@v4 37 | 38 | - name: Update Snapshot 39 | uses: jlandowner/helm-chartsnap-action@v1 40 | id: helm-chartsnap-action 41 | with: 42 | chart: cilium 43 | repo: https://helm.cilium.io 44 | values: example/remote/cilium.values.yaml 45 | additional_args: "--namespace kube-system" 46 | update_snapshot: true 47 | 48 | nginx-gateway-fabric: 49 | runs-on: ubuntu-latest 50 | name: Do snapshot nginx-gateway-fabric and create PR if snapshot changed 51 | permissions: 52 | contents: write 53 | pull-requests: write 54 | steps: 55 | - name: Checkout 56 | uses: actions/checkout@v4 57 | 58 | - name: Update Snapshot 59 | uses: jlandowner/helm-chartsnap-action@v1 60 | id: helm-chartsnap-action 61 | with: 62 | chart: oci://ghcr.io/nginxinc/charts/nginx-gateway-fabric 63 | values: example/remote/nginx-gateway-fabric.values.yaml 64 | additional_args: "--namespace nginx-gateway" 65 | update_snapshot: true 66 | -------------------------------------------------------------------------------- /hack/test/test-kong-chart-manual.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Update submodule to the latest commit 4 | git submodule update --init --recursive --remote --merge 5 | 6 | # Update charts repo to the latest main 7 | cd charts 8 | git pull origin main --rebase 9 | 10 | # ---------------------------------- 11 | # Same test in kong golden tests 12 | # ---------------------------------- 13 | helm chartsnap \ 14 | -c ./charts/kong \ 15 | -f ./charts/kong/ci/ \ 16 | \ 17 | -- \ 18 | --api-versions cert-manager.io/v1 \ 19 | --api-versions gateway.networking.k8s.io/v1 \ 20 | --api-versions gateway.networking.k8s.io/v1beta1 \ 21 | --api-versions gateway.networking.k8s.io/v1alpha2 \ 22 | --api-versions admissionregistration.k8s.io/v1/ValidatingAdmissionPolicy \ 23 | --api-versions admissionregistration.k8s.io/v1/ValidatingAdmissionPolicyBinding 24 | 25 | helm chartsnap \ 26 | -c ./charts/ingress \ 27 | -f ./charts/ingress/ci/ \ 28 | \ 29 | -- \ 30 | --api-versions cert-manager.io/v1 \ 31 | --api-versions gateway.networking.k8s.io/v1 \ 32 | --api-versions gateway.networking.k8s.io/v1beta1 \ 33 | --api-versions gateway.networking.k8s.io/v1alpha2 \ 34 | --api-versions admissionregistration.k8s.io/v1/ValidatingAdmissionPolicy \ 35 | --api-versions admissionregistration.k8s.io/v1/ValidatingAdmissionPolicyBinding 36 | 37 | helm chartsnap \ 38 | -c ./charts/gateway-operator \ 39 | -f ./charts/gateway-operator/ci/ \ 40 | \ 41 | -- \ 42 | --api-versions cert-manager.io/v1 \ 43 | --api-versions gateway.networking.k8s.io/v1 \ 44 | --api-versions gateway.networking.k8s.io/v1beta1 \ 45 | --api-versions gateway.networking.k8s.io/v1alpha2 \ 46 | --api-versions admissionregistration.k8s.io/v1/ValidatingAdmissionPolicy \ 47 | --api-versions admissionregistration.k8s.io/v1/ValidatingAdmissionPolicyBinding -------------------------------------------------------------------------------- /pkg/api/v1alpha1/testspec.go: -------------------------------------------------------------------------------- 1 | package v1alpha1 2 | 3 | import ( 4 | "encoding/base64" 5 | "fmt" 6 | "os" 7 | 8 | yaml "go.yaml.in/yaml/v3" 9 | ) 10 | 11 | func FromFile[T SnapshotValues | SnapshotConfig](filePath string, out *T) error { 12 | f, err := os.Open(filePath) 13 | if err != nil { 14 | return fmt.Errorf("failed to open file '%s': %w", filePath, err) 15 | } 16 | defer f.Close() 17 | 18 | err = yaml.NewDecoder(f).Decode(out) 19 | if err != nil { 20 | return fmt.Errorf("failed to decode file '%s': %w", filePath, err) 21 | } 22 | return nil 23 | } 24 | 25 | type SnapshotValues struct { 26 | TestSpec SnapshotConfig `yaml:"testSpec,omitempty"` 27 | } 28 | 29 | type SnapshotConfig struct { 30 | DynamicFields []ManifestPath `yaml:"dynamicFields,omitempty"` 31 | SnapshotFileExt string `yaml:"snapshotFileExt,omitempty"` 32 | SnapshotVersion string `yaml:"snapshotVersion,omitempty"` 33 | } 34 | 35 | type ManifestPath struct { 36 | Kind string `yaml:"kind,omitempty"` 37 | APIVersion string `yaml:"apiVersion,omitempty"` 38 | Name string `yaml:"name,omitempty"` 39 | JSONPath []string `yaml:"jsonPath,omitempty"` 40 | Base64 bool `yaml:"base64,omitempty"` 41 | Value string `yaml:"value,omitempty"` 42 | } 43 | 44 | const DynamicValue = "###DYNAMIC_FIELD###" 45 | 46 | func (v *ManifestPath) DynamicValue() string { 47 | value := DynamicValue 48 | if v.Value != "" { 49 | value = v.Value 50 | } 51 | 52 | if v.Base64 { 53 | return base64.StdEncoding.EncodeToString([]byte(value)) 54 | } else { 55 | return value 56 | } 57 | } 58 | 59 | // Merge merges the snapshot configs into the current snapshot config 60 | // The current snapshot config has higher priority than the given snapshot config 61 | func (t *SnapshotConfig) Merge(cfg SnapshotConfig) { 62 | // For DynamicFields, it doesn't matter if the same field is replaced with a fixed value several times 63 | // But the current snapshot config has higher priority than the given snapshot config 64 | t.DynamicFields = append(cfg.DynamicFields, t.DynamicFields...) 65 | if cfg.SnapshotFileExt != "" { 66 | t.SnapshotFileExt = cfg.SnapshotFileExt 67 | } 68 | if cfg.SnapshotVersion != "" { 69 | t.SnapshotVersion = cfg.SnapshotVersion 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /hack/helm-template-help-snapshot/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log/slog" 5 | "os" 6 | "os/exec" 7 | "regexp" 8 | 9 | "github.com/jlandowner/helm-chartsnap/pkg/snap" 10 | ) 11 | 12 | func main() { 13 | snapshot("helm-version", execute("helm", "version")) 14 | snapshot("helm-template", execute("helm", "template", "--help")) 15 | } 16 | 17 | func execute(cmd ...string) string { 18 | out, err := exec.Command(cmd[0], cmd[1:]...).CombinedOutput() 19 | if err != nil { 20 | slog.Error("exec error", "err", err) 21 | os.Exit(9) 22 | } 23 | return replaceHomeDir(replaceHelmEnv(string(out))) 24 | } 25 | 26 | func replaceHomeDir(bs string) string { 27 | // Get os.UserHomeDir() and replace it with "###HOME_DIR###" 28 | home, err := os.UserHomeDir() 29 | if err != nil { 30 | panic(err) 31 | } 32 | re := regexp.MustCompile(home) 33 | return re.ReplaceAllString(bs, "###HOME_DIR###") 34 | } 35 | 36 | func replaceHelmEnv(bs string) string { 37 | // Get helm env and replace HELM_REGISTRY_CONFIG, HELM_REPOSITORY_CACHE and HELM_REPOSITORY_CONFIG 38 | envs := parseHelmEnvOutput() 39 | re := regexp.MustCompile(envs["HELM_REGISTRY_CONFIG"]) 40 | bs = re.ReplaceAllString(bs, "###HELM_REGISTRY_CONFIG###") 41 | re = regexp.MustCompile(envs["HELM_REPOSITORY_CACHE"]) 42 | bs = re.ReplaceAllString(bs, "###HELM_REPOSITORY_CACHE###") 43 | re = regexp.MustCompile(envs["HELM_REPOSITORY_CONFIG"]) 44 | bs = re.ReplaceAllString(bs, "###HELM_REPOSITORY_CONFIG###") 45 | return bs 46 | } 47 | 48 | func snapshot(id, data string) { 49 | s := snap.SnapshotMatcher("helm-template.snap", snap.WithSnapshotID(id)) 50 | match, err := s.Match(data) 51 | 52 | if err != nil { 53 | slog.Error("snapshot error", "err", err) 54 | os.Exit(9) 55 | } 56 | if !match { 57 | slog.Error(s.FailureMessage(nil)) 58 | os.Exit(1) 59 | } 60 | } 61 | 62 | func parseHelmEnvOutput() map[string]string { 63 | out, err := exec.Command("helm", "env").CombinedOutput() 64 | if err != nil { 65 | slog.Error("exec error", "err", err) 66 | os.Exit(9) 67 | } 68 | re := regexp.MustCompile(`(.*)="(.*)"`) 69 | matches := re.FindAllStringSubmatch(string(out), -1) 70 | envs := make(map[string]string) 71 | for _, match := range matches { 72 | envs[match[1]] = match[2] 73 | } 74 | return envs 75 | } 76 | -------------------------------------------------------------------------------- /example/app1/templates/ingress.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.ingress.enabled -}} 2 | {{- $fullName := include "app1.fullname" . -}} 3 | {{- $svcPort := .Values.service.port -}} 4 | {{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} 5 | {{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }} 6 | {{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}} 7 | {{- end }} 8 | {{- end }} 9 | {{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} 10 | apiVersion: networking.k8s.io/v1 11 | {{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} 12 | apiVersion: networking.k8s.io/v1beta1 13 | {{- else -}} 14 | apiVersion: extensions/v1beta1 15 | {{- end }} 16 | kind: Ingress 17 | metadata: 18 | name: {{ $fullName }} 19 | labels: 20 | {{- include "app1.labels" . | nindent 4 }} 21 | {{- with .Values.ingress.annotations }} 22 | annotations: 23 | {{- toYaml . | nindent 4 }} 24 | {{- end }} 25 | spec: 26 | {{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} 27 | ingressClassName: {{ .Values.ingress.className }} 28 | {{- end }} 29 | {{- if .Values.ingress.tls }} 30 | tls: 31 | {{- range .Values.ingress.tls }} 32 | - hosts: 33 | {{- range .hosts }} 34 | - {{ . | quote }} 35 | {{- end }} 36 | secretName: {{ .secretName }} 37 | {{- end }} 38 | {{- end }} 39 | rules: 40 | {{- range .Values.ingress.hosts }} 41 | - host: {{ .host | quote }} 42 | http: 43 | paths: 44 | {{- range .paths }} 45 | - path: {{ .path }} 46 | {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }} 47 | pathType: {{ .pathType }} 48 | {{- end }} 49 | backend: 50 | {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} 51 | service: 52 | name: {{ $fullName }} 53 | port: 54 | number: {{ $svcPort }} 55 | {{- else }} 56 | serviceName: {{ $fullName }} 57 | servicePort: {{ $svcPort }} 58 | {{- end }} 59 | {{- end }} 60 | {{- end }} 61 | {{- end }} 62 | -------------------------------------------------------------------------------- /example/app2/templates/ingress.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.ingress.enabled -}} 2 | {{- $fullName := include "app2.fullname" . -}} 3 | {{- $svcPort := .Values.service.port -}} 4 | {{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} 5 | {{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }} 6 | {{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}} 7 | {{- end }} 8 | {{- end }} 9 | {{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} 10 | apiVersion: networking.k8s.io/v1 11 | {{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} 12 | apiVersion: networking.k8s.io/v1beta1 13 | {{- else -}} 14 | apiVersion: extensions/v1beta1 15 | {{- end }} 16 | kind: Ingress 17 | metadata: 18 | name: {{ $fullName }} 19 | labels: 20 | {{- include "app2.labels" . | nindent 4 }} 21 | {{- with .Values.ingress.annotations }} 22 | annotations: 23 | {{- toYaml . | nindent 4 }} 24 | {{- end }} 25 | spec: 26 | {{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} 27 | ingressClassName: {{ .Values.ingress.className }} 28 | {{- end }} 29 | {{- if .Values.ingress.tls }} 30 | tls: 31 | {{- range .Values.ingress.tls }} 32 | - hosts: 33 | {{- range .hosts }} 34 | - {{ . | quote }} 35 | {{- end }} 36 | secretName: {{ .secretName }} 37 | {{- end }} 38 | {{- end }} 39 | rules: 40 | {{- range .Values.ingress.hosts }} 41 | - host: {{ .host | quote }} 42 | http: 43 | paths: 44 | {{- range .paths }} 45 | - path: {{ .path }} 46 | {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }} 47 | pathType: {{ .pathType }} 48 | {{- end }} 49 | backend: 50 | {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} 51 | service: 52 | name: {{ $fullName }} 53 | port: 54 | number: {{ $svcPort }} 55 | {{- else }} 56 | serviceName: {{ $fullName }} 57 | servicePort: {{ $svcPort }} 58 | {{- end }} 59 | {{- end }} 60 | {{- end }} 61 | {{- end }} 62 | -------------------------------------------------------------------------------- /example/remote/README.md: -------------------------------------------------------------------------------- 1 | # Example of snapshot testing for remote Helm repositories 2 | 3 | ## ingress-nginx 4 | 5 | Docs: https://artifacthub.io/packages/helm/ingress-nginx/ingress-nginx 6 | 7 | For example, install ingress-nginx which Service is bound to Network Load Balancer in Amazon EKS. 8 | 9 | ```yaml 10 | # https://github.com/kubernetes/ingress-nginx/blob/main/charts/ingress-nginx/values.yaml#L518 11 | controller: 12 | service: 13 | internal: 14 | enabled: true 15 | annotations: 16 | # Create internal NLB 17 | service.beta.kubernetes.io/aws-load-balancer-scheme: "internal" 18 | 19 | ``` 20 | 21 | Do snapshot 📸 22 | 23 | ```sh 24 | helm chartsnap -c ingress-nginx -f ingress-nginx.values.yaml -- --repo https://kubernetes.github.io/ingress-nginx --namespace ingress-nginx 25 | ``` 26 | 27 | ## cilium 28 | 29 | Docs: https://docs.cilium.io/en/stable/installation/k8s-install-helm/ 30 | 31 | For example, install cilium as AWS ENI mode and enable Hubble UI. 32 | 33 | ```yaml 34 | # https://docs.cilium.io/en/stable/installation/k8s-install-helm/ 35 | # EKS 36 | 37 | eni: 38 | enabled: true 39 | 40 | ipam: 41 | mode: eni 42 | 43 | egressMasqueradeInterfaces: eth0 44 | 45 | routingMode: native 46 | 47 | # https://docs.cilium.io/en/stable/gettingstarted/hubble/#hubble-ui 48 | # Enable Hubble UI 49 | hubble: 50 | relay: 51 | enabled: true 52 | ui: 53 | enabled: true 54 | ``` 55 | 56 | Do snapshot 📸 57 | 58 | ```sh 59 | helm chartsnap -c cilium -f cilium.values.yaml -- --repo https://helm.cilium.io --namespace kube-system 60 | ``` 61 | 62 | However probably you will see the failure that does not match the snapshot of the certificate in Secrets. 63 | 64 | Then add the followings to the value file and re-run the snapshot. 65 | 66 | ```yaml 67 | # Change to fixed values 68 | testSpec: 69 | dynamicFields: 70 | - apiVersion: v1 71 | kind: Secret 72 | name: cilium-ca 73 | jsonPath: 74 | - /data/ca.crt 75 | - /data/ca.key 76 | - apiVersion: v1 77 | kind: Secret 78 | name: hubble-relay-client-certs 79 | jsonPath: 80 | - /data/ca.crt 81 | - /data/tls.crt 82 | - /data/tls.key 83 | - apiVersion: v1 84 | kind: Secret 85 | name: hubble-server-certs 86 | jsonPath: 87 | - /data/ca.crt 88 | - /data/tls.crt 89 | - /data/tls.key 90 | ``` -------------------------------------------------------------------------------- /docs/test_style_guide.md: -------------------------------------------------------------------------------- 1 | # Test Writing Style Guide 2 | 3 | This project uses the **Ginkgo** and **Gomega** testing frameworks for behavior-driven development (BDD). Below are the key conventions and practices for writing tests: 4 | 5 | ## Frameworks and Libraries 6 | - **Ginkgo**: Provides BDD-style constructs like `Describe`, `Context`, and `It` for organizing tests. 7 | - **Gomega**: Offers expressive matchers for assertions. 8 | 9 | ## Test Structure 10 | 1. **Describe**: Groups related tests, typically for a function or feature. 11 | 2. **Context**: Defines specific scenarios or conditions under which tests are executed. 12 | 3. **It**: Represents individual test cases with a clear description of the expected behavior. 13 | 14 | ### Example: 15 | ```go 16 | Describe("FeatureName", func() { 17 | Context("when condition A is met", func() { 18 | It("should behave as expected", func() { 19 | Expect(actual).To(Equal(expected)) 20 | }) 21 | }) 22 | }) 23 | ``` 24 | 25 | ## Parameterized Tests 26 | - Use `DescribeTable` for tests with multiple input-output combinations. 27 | - Define a `struct` for test cases and use `Entry` to specify individual cases. 28 | 29 | ### Example: 30 | ```go 31 | DescribeTable("function behavior", 32 | func(tc testCase) { 33 | result := functionUnderTest(tc.input) 34 | Expect(result).To(Equal(tc.expected)) 35 | }, 36 | Entry("case 1", testCase{input: "A", expected: "B"}), 37 | Entry("case 2", testCase{input: "X", expected: "Y"}), 38 | ) 39 | ``` 40 | 41 | ## Snapshot Testing 42 | - Use `MatchSnapShot` to compare outputs against pre-recorded snapshots. 43 | 44 | ### Example: 45 | ```go 46 | Ω(output.String()).To(MatchSnapShot()) 47 | ``` 48 | 49 | ## Cleanup 50 | - Use `DeferCleanup` to restore global states or clean up resources after tests. 51 | 52 | ### Example: 53 | ```go 54 | DeferCleanup(func() { 55 | // Cleanup logic 56 | }) 57 | ``` 58 | 59 | ## Error Handling 60 | - Use `Expect(err).To(HaveOccurred())` for expected errors. 61 | - Use `Expect(err).ShouldNot(HaveOccurred())` for successful operations. 62 | 63 | ## Test Execution 64 | - Register the test suite using `RegisterFailHandler` and `RunSpecs`. 65 | 66 | ### Example: 67 | ```go 68 | func TestMain(t *testing.T) { 69 | RegisterFailHandler(Fail) 70 | RunSpecs(t, "Main Suite") 71 | } 72 | ``` 73 | 74 | Follow these conventions to ensure consistency and readability in the test suite. 75 | -------------------------------------------------------------------------------- /example/app1/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: {{ include "app1.fullname" . }} 5 | labels: 6 | {{- include "app1.labels" . | nindent 4 }} 7 | spec: 8 | {{- if not .Values.autoscaling.enabled }} 9 | replicas: {{ .Values.replicaCount }} 10 | {{- end }} 11 | selector: 12 | matchLabels: 13 | {{- include "app1.selectorLabels" . | nindent 6 }} 14 | template: 15 | metadata: 16 | {{- with .Values.podAnnotations }} 17 | annotations: 18 | {{- toYaml . | nindent 8 }} 19 | {{- end }} 20 | labels: 21 | {{- include "app1.labels" . | nindent 8 }} 22 | {{- with .Values.podLabels }} 23 | {{- toYaml . | nindent 8 }} 24 | {{- end }} 25 | spec: 26 | {{- with .Values.imagePullSecrets }} 27 | imagePullSecrets: 28 | {{- toYaml . | nindent 8 }} 29 | {{- end }} 30 | serviceAccountName: {{ include "app1.serviceAccountName" . }} 31 | securityContext: 32 | {{- toYaml .Values.podSecurityContext | nindent 8 }} 33 | containers: 34 | - name: {{ .Chart.Name }} 35 | securityContext: 36 | {{- toYaml .Values.securityContext | nindent 12 }} 37 | image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" 38 | imagePullPolicy: {{ .Values.image.pullPolicy }} 39 | ports: 40 | - name: http 41 | containerPort: {{ .Values.service.port }} 42 | protocol: TCP 43 | livenessProbe: 44 | httpGet: 45 | path: / 46 | port: http 47 | readinessProbe: 48 | httpGet: 49 | path: / 50 | port: http 51 | resources: 52 | {{- toYaml .Values.resources | nindent 12 }} 53 | {{- with .Values.volumeMounts }} 54 | volumeMounts: 55 | {{- toYaml . | nindent 12 }} 56 | {{- end }} 57 | {{- with .Values.volumes }} 58 | volumes: 59 | {{- toYaml . | nindent 8 }} 60 | {{- end }} 61 | {{- with .Values.nodeSelector }} 62 | nodeSelector: 63 | {{- toYaml . | nindent 8 }} 64 | {{- end }} 65 | {{- with .Values.affinity }} 66 | affinity: 67 | {{- toYaml . | nindent 8 }} 68 | {{- end }} 69 | {{- with .Values.tolerations }} 70 | tolerations: 71 | {{- toYaml . | nindent 8 }} 72 | {{- end }} 73 | -------------------------------------------------------------------------------- /example/app1/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* 2 | Expand the name of the chart. 3 | */}} 4 | {{- define "app1.name" -}} 5 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} 6 | {{- end }} 7 | 8 | {{/* 9 | Create a default fully qualified app name. 10 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 11 | If release name contains chart name it will be used as a full name. 12 | */}} 13 | {{- define "app1.fullname" -}} 14 | {{- if .Values.fullnameOverride }} 15 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} 16 | {{- else }} 17 | {{- $name := default .Chart.Name .Values.nameOverride }} 18 | {{- if contains $name .Release.Name }} 19 | {{- .Release.Name | trunc 63 | trimSuffix "-" }} 20 | {{- else }} 21 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} 22 | {{- end }} 23 | {{- end }} 24 | {{- end }} 25 | 26 | {{/* 27 | Create chart name and version as used by the chart label. 28 | */}} 29 | {{- define "app1.chart" -}} 30 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} 31 | {{- end }} 32 | 33 | {{/* 34 | Common labels 35 | */}} 36 | {{- define "app1.labels" -}} 37 | helm.sh/chart: {{ include "app1.chart" . }} 38 | {{ include "app1.selectorLabels" . }} 39 | {{- if .Chart.AppVersion }} 40 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 41 | {{- end }} 42 | app.kubernetes.io/managed-by: {{ .Release.Service }} 43 | {{- end }} 44 | 45 | {{/* 46 | Selector labels 47 | */}} 48 | {{- define "app1.selectorLabels" -}} 49 | app.kubernetes.io/name: {{ include "app1.name" . }} 50 | app.kubernetes.io/instance: {{ .Release.Name }} 51 | {{- end }} 52 | 53 | {{/* 54 | Create the name of the service account to use 55 | */}} 56 | {{- define "app1.serviceAccountName" -}} 57 | {{- if .Values.serviceAccount.create }} 58 | {{- default (include "app1.fullname" .) .Values.serviceAccount.name }} 59 | {{- else }} 60 | {{- default "default" .Values.serviceAccount.name }} 61 | {{- end }} 62 | {{- end }} 63 | 64 | {{/* 65 | Generate certificates 66 | */}} 67 | {{- define "app1.gen-certs" -}} 68 | {{- $altNames := list ( printf "%s.%s.svc" ( include "app1.fullname" . ) .Release.Namespace ) ( printf "%s.%s.svc.cluster.local" ( include "app1.fullname" . ) .Release.Namespace ) -}} 69 | {{- $ca := genCA "app1-ca" 3650 -}} 70 | {{- $cert := genSignedCert ( include "app1.fullname" . ) nil $altNames 3650 $ca -}} 71 | caCert: {{ $ca.Cert | b64enc }} 72 | clientCert: {{ $cert.Cert | b64enc }} 73 | clientKey: {{ $cert.Key | b64enc }} 74 | {{- end }} -------------------------------------------------------------------------------- /pkg/charts/helm_test.go: -------------------------------------------------------------------------------- 1 | package charts 2 | 3 | import ( 4 | "context" 5 | 6 | . "github.com/jlandowner/helm-chartsnap/pkg/snap/gomega" 7 | . "github.com/onsi/ginkgo/v2" 8 | . "github.com/onsi/gomega" 9 | ) 10 | 11 | var _ = Describe("Helm", func() { 12 | Context("when Execute", func() { 13 | It("should execute with expected args and env", func() { 14 | o := &HelmTemplateCmdOptions{ 15 | HelmPath: "./testdata/helm_cmd.bash", 16 | ReleaseName: "aaa", 17 | Namespace: "bbb", 18 | Chart: "ccc", 19 | ValuesFile: "ddd", 20 | } 21 | 22 | out, err := o.Execute(context.Background()) 23 | Expect(err).NotTo(HaveOccurred()) 24 | Expect(out).To(MatchSnapShot()) 25 | }) 26 | }) 27 | 28 | Context("when Execute without namespace", func() { 29 | It("should execute with expected args and env", func() { 30 | o := &HelmTemplateCmdOptions{ 31 | HelmPath: "./testdata/helm_cmd.bash", 32 | ReleaseName: "chartsnap", 33 | Chart: "charts/app1/", 34 | ValuesFile: "charts/app1/test/test.values.yaml", 35 | } 36 | 37 | out, err := o.Execute(context.Background()) 38 | Expect(err).NotTo(HaveOccurred()) 39 | Expect(out).To(MatchSnapShot()) 40 | }) 41 | }) 42 | 43 | Context("when Execute without values", func() { 44 | It("should execute with expected args and env", func() { 45 | o := &HelmTemplateCmdOptions{ 46 | HelmPath: "./testdata/helm_cmd.bash", 47 | ReleaseName: "chartsnap", 48 | Namespace: "default", 49 | Chart: "charts/app1/", 50 | } 51 | 52 | out, err := o.Execute(context.Background()) 53 | Expect(err).NotTo(HaveOccurred()) 54 | Expect(out).To(MatchSnapShot()) 55 | }) 56 | }) 57 | 58 | Context("when Execute with additional args", func() { 59 | It("should execute with expected args and env", func() { 60 | o := &HelmTemplateCmdOptions{ 61 | HelmPath: "./testdata/helm_cmd.bash", 62 | ReleaseName: "chartsnap", 63 | Namespace: "xxx", 64 | Chart: "postgres", 65 | ValuesFile: "postgres.values.yaml", 66 | AdditionalArgs: []string{"--repo", "https://charts.bitnami.com/bitnami", "--skip-tests"}, 67 | } 68 | 69 | out, err := o.Execute(context.Background()) 70 | Expect(err).NotTo(HaveOccurred()) 71 | Expect(out).To(MatchSnapShot()) 72 | }) 73 | }) 74 | 75 | Context("test mocks", func() { 76 | It("should execute as helm cmd", func() { 77 | o := &HelmTemplateCmdOptions{ 78 | HelmPath: "./testdata/helm_empty.bash", 79 | ReleaseName: "release", 80 | Chart: "chart", 81 | } 82 | out, err := o.Execute(context.Background()) 83 | Expect(err).NotTo(HaveOccurred()) 84 | Expect(out).To(MatchSnapShot()) 85 | }) 86 | }) 87 | }) 88 | -------------------------------------------------------------------------------- /pkg/api/v1alpha1/unknown_types.go: -------------------------------------------------------------------------------- 1 | // +kubebuilder:object:generate=true 2 | // +groupName=helm-chartsnap.jlandowner.dev 3 | package v1alpha1 4 | 5 | import ( 6 | "bytes" 7 | "fmt" 8 | 9 | yaml "go.yaml.in/yaml/v3" 10 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 11 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 12 | "k8s.io/apimachinery/pkg/runtime/schema" 13 | ) 14 | 15 | var ( 16 | // GroupVersion is group version used to register these objects 17 | GroupVersion = schema.GroupVersion{Group: "helm-chartsnap.jlandowner.dev", Version: "v1alpha1"} 18 | UnknownKind = "Unknown" 19 | ) 20 | 21 | func NewUnknownError(raw string) *Unknown { 22 | return &Unknown{ 23 | ObjectMeta: metav1.ObjectMeta{ 24 | Name: "helm-output", 25 | }, 26 | Raw: raw, 27 | } 28 | } 29 | 30 | // +kubebuilder:object:root=true 31 | // Unknown is a placeholder for an unrecognized resource in stdout/stderr of helm template command output. 32 | type Unknown struct { 33 | metav1.TypeMeta `json:",inline"` 34 | metav1.ObjectMeta `json:"metadata,omitempty"` 35 | 36 | // Raw is the raw string of the helm output. 37 | Raw string `json:"raw,omitempty"` 38 | } 39 | 40 | func (e *Unknown) Error() string { 41 | return fmt.Sprintf("failed to recognize a resource in stdout/stderr of helm template command output. snapshot it as Unknown: \n---\n%s\n---", e.Raw) 42 | } 43 | 44 | func (e *Unknown) Unstructured() *unstructured.Unstructured { 45 | return &unstructured.Unstructured{ 46 | Object: map[string]interface{}{ 47 | "apiVersion": GroupVersion.String(), 48 | "kind": "Unknown", 49 | "metadata": map[string]interface{}{ 50 | "name": "helm-output", 51 | }, 52 | "raw": e.Raw, 53 | }, 54 | } 55 | } 56 | 57 | func (e *Unknown) Node() *yaml.Node { 58 | return &yaml.Node{ 59 | Kind: yaml.MappingNode, Content: []*yaml.Node{ 60 | {Kind: yaml.ScalarNode, Value: "apiVersion"}, 61 | {Kind: yaml.ScalarNode, Value: GroupVersion.String()}, 62 | {Kind: yaml.ScalarNode, Value: "kind"}, 63 | {Kind: yaml.ScalarNode, Value: "Unknown"}, 64 | {Kind: yaml.ScalarNode, Value: "metadata"}, 65 | {Kind: yaml.MappingNode, Content: []*yaml.Node{ 66 | {Kind: yaml.ScalarNode, Value: "name"}, 67 | {Kind: yaml.ScalarNode, Value: "helm-output"}, 68 | }}, 69 | {Kind: yaml.ScalarNode, Value: "raw"}, 70 | {Kind: yaml.ScalarNode, Value: e.Raw, Style: yaml.LiteralStyle}, 71 | }, 72 | } 73 | } 74 | 75 | func (e *Unknown) String() (string, error) { 76 | var buf bytes.Buffer 77 | enc := yaml.NewEncoder(&buf) 78 | enc.SetIndent(2) 79 | err := enc.Encode(e.Node()) 80 | return buf.String(), err 81 | } 82 | 83 | func (e *Unknown) MustString() string { 84 | s, err := e.String() 85 | if err != nil { 86 | panic(fmt.Sprintf("failed to encode Unknown to YAML: %v", err)) 87 | } 88 | return s 89 | } 90 | -------------------------------------------------------------------------------- /example/app2/values.yaml: -------------------------------------------------------------------------------- 1 | # Default values for app2. 2 | # This is a YAML-formatted file. 3 | # Declare variables to be passed into your templates. 4 | 5 | replicaCount: 1 6 | 7 | image: 8 | repository: nginx 9 | pullPolicy: IfNotPresent 10 | # Overrides the image tag whose default is the chart appVersion. 11 | tag: "" 12 | 13 | imagePullSecrets: [] 14 | nameOverride: "" 15 | fullnameOverride: "" 16 | 17 | serviceAccount: 18 | # Specifies whether a service account should be created 19 | create: true 20 | # Automatically mount a ServiceAccount's API credentials? 21 | automount: true 22 | # Annotations to add to the service account 23 | annotations: {} 24 | # The name of the service account to use. 25 | # If not set and create is true, a name is generated using the fullname template 26 | name: "" 27 | 28 | podAnnotations: {} 29 | podLabels: {} 30 | 31 | podSecurityContext: {} 32 | # fsGroup: 2000 33 | 34 | securityContext: {} 35 | # capabilities: 36 | # drop: 37 | # - ALL 38 | # readOnlyRootFilesystem: true 39 | # runAsNonRoot: true 40 | # runAsUser: 1000 41 | 42 | service: 43 | type: ClusterIP 44 | port: 80 45 | 46 | ingress: 47 | enabled: false 48 | className: "" 49 | annotations: {} 50 | # kubernetes.io/ingress.class: nginx 51 | # kubernetes.io/tls-acme: "true" 52 | hosts: 53 | - host: chart-example.local 54 | paths: 55 | - path: / 56 | pathType: ImplementationSpecific 57 | tls: [] 58 | # - secretName: chart-example-tls 59 | # hosts: 60 | # - chart-example.local 61 | 62 | resources: {} 63 | # We usually recommend not to specify default resources and to leave this as a conscious 64 | # choice for the user. This also increases chances charts run on environments with little 65 | # resources, such as Minikube. If you do want to specify resources, uncomment the following 66 | # lines, adjust them as necessary, and remove the curly braces after 'resources:'. 67 | # limits: 68 | # cpu: 100m 69 | # memory: 128Mi 70 | # requests: 71 | # cpu: 100m 72 | # memory: 128Mi 73 | 74 | livenessProbe: 75 | httpGet: 76 | path: / 77 | port: http 78 | readinessProbe: 79 | httpGet: 80 | path: / 81 | port: http 82 | 83 | autoscaling: 84 | enabled: false 85 | minReplicas: 1 86 | maxReplicas: 100 87 | targetCPUUtilizationPercentage: 80 88 | # targetMemoryUtilizationPercentage: 80 89 | 90 | # Additional volumes on the output Deployment definition. 91 | volumes: [] 92 | # - name: foo 93 | # secret: 94 | # secretName: mysecret 95 | # optional: false 96 | 97 | # Additional volumeMounts on the output Deployment definition. 98 | volumeMounts: [] 99 | # - name: foo 100 | # mountPath: "/etc/foo" 101 | # readOnly: true 102 | 103 | nodeSelector: {} 104 | 105 | tolerations: [] 106 | 107 | affinity: {} 108 | -------------------------------------------------------------------------------- /pkg/api/v1alpha1/__snapshots__/testspec_test.snap: -------------------------------------------------------------------------------- 1 | ['TestSpec FromFile when loading .chartsnap.yaml should load config 1'] 2 | SnapShot = """ 3 | { 4 | \"DynamicFields\": [ 5 | { 6 | \"Kind\": \"Secret\", 7 | \"APIVersion\": \"v1\", 8 | \"Name\": \"app1-cert\", 9 | \"JSONPath\": [ 10 | \"/data/ca.crt\", 11 | \"/data/tls.crt\", 12 | \"/data/tls.key\" 13 | ], 14 | \"Base64\": true, 15 | \"Value\": \"\" 16 | } 17 | ], 18 | \"SnapshotFileExt\": \"\", 19 | \"SnapshotVersion\": \"\" 20 | } 21 | """ 22 | 23 | ['TestSpec FromFile when loading invalid yaml should not load config 1'] 24 | SnapShot = """ 25 | failed to decode file 'testdata/testspec_values_invalid.yaml': yaml: line 10: could not find expected ':'""" 26 | 27 | ['TestSpec FromFile when loading not found should not load config 1'] 28 | SnapShot = """ 29 | failed to open file 'testdata/notfound.yaml': open testdata/notfound.yaml: no such file or directory""" 30 | 31 | ['TestSpec FromFile when values.yaml has testSpec should load config 1'] 32 | SnapShot = """ 33 | { 34 | \"TestSpec\": { 35 | \"DynamicFields\": [ 36 | { 37 | \"Kind\": \"Secret\", 38 | \"APIVersion\": \"v1\", 39 | \"Name\": \"app1-cert\", 40 | \"JSONPath\": [ 41 | \"/data/ca.crt\", 42 | \"/data/tls.crt\", 43 | \"/data/tls.key\" 44 | ], 45 | \"Base64\": true, 46 | \"Value\": \"\" 47 | } 48 | ], 49 | \"SnapshotFileExt\": \"\", 50 | \"SnapshotVersion\": \"\" 51 | } 52 | } 53 | """ 54 | 55 | ['TestSpec Merge should merge dynamic fields 1'] 56 | SnapShot = """ 57 | { 58 | \"DynamicFields\": [ 59 | { 60 | \"Kind\": \"service\", 61 | \"APIVersion\": \"v1\", 62 | \"Name\": \"chartsnap-app1\", 63 | \"JSONPath\": [ 64 | \"/spec/ports/0/targetPort\" 65 | ], 66 | \"Base64\": false, 67 | \"Value\": \"\" 68 | }, 69 | { 70 | \"Kind\": \"Pod\", 71 | \"APIVersion\": \"v1\", 72 | \"Name\": \"chartsnap-app1-test-connection\", 73 | \"JSONPath\": [ 74 | \"/metadata/name\" 75 | ], 76 | \"Base64\": false, 77 | \"Value\": \"\" 78 | }, 79 | { 80 | \"Kind\": \"service\", 81 | \"APIVersion\": \"v1\", 82 | \"Name\": \"chartsnap-app1\", 83 | \"JSONPath\": [ 84 | \"/spec/ports/0/targetPort\" 85 | ], 86 | \"Base64\": false, 87 | \"Value\": \"\" 88 | }, 89 | { 90 | \"Kind\": \"Service\", 91 | \"APIVersion\": \"v1\", 92 | \"Name\": \"chartsnap-app1\", 93 | \"JSONPath\": [ 94 | \"/spec/ports/1/targetPort\" 95 | ], 96 | \"Base64\": false, 97 | \"Value\": \"\" 98 | } 99 | ], 100 | \"SnapshotFileExt\": \"\", 101 | \"SnapshotVersion\": \"\" 102 | } 103 | """ 104 | -------------------------------------------------------------------------------- /pkg/charts/testdata/testspec_test.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | labels: 5 | app.kubernetes.io/instance: chartsnap 6 | app.kubernetes.io/managed-by: Helm 7 | app.kubernetes.io/name: app1 8 | app.kubernetes.io/version: 1.16.0 9 | helm.sh/chart: app1-0.1.0 10 | name: chartsnap-app1 11 | spec: 12 | replicas: 1 13 | selector: 14 | matchLabels: 15 | app.kubernetes.io/instance: chartsnap 16 | app.kubernetes.io/name: app1 17 | template: 18 | metadata: 19 | labels: 20 | app.kubernetes.io/instance: chartsnap 21 | app.kubernetes.io/managed-by: Helm 22 | app.kubernetes.io/name: app1 23 | app.kubernetes.io/version: 1.16.0 24 | helm.sh/chart: app1-0.1.0 25 | spec: 26 | containers: 27 | - image: nginx:1.16.0 28 | imagePullPolicy: IfNotPresent 29 | livenessProbe: 30 | httpGet: 31 | path: / 32 | port: http 33 | name: app1 34 | ports: 35 | - containerPort: 80 36 | name: http 37 | protocol: TCP 38 | readinessProbe: 39 | httpGet: 40 | path: / 41 | port: http 42 | resources: {} 43 | securityContext: {} 44 | securityContext: {} 45 | serviceAccountName: chartsnap-app1 46 | --- 47 | apiVersion: v1 48 | kind: Pod 49 | metadata: 50 | annotations: 51 | helm.sh/hook: test 52 | labels: 53 | app.kubernetes.io/instance: chartsnap 54 | app.kubernetes.io/managed-by: Helm 55 | app.kubernetes.io/name: app1 56 | app.kubernetes.io/version: 1.16.0 57 | helm.sh/chart: app1-0.1.0 58 | name: chartsnap-app1-test-connection 59 | spec: 60 | containers: 61 | - args: 62 | - chartsnap-app1:80 63 | command: 64 | - wget 65 | image: busybox 66 | name: wget 67 | restartPolicy: Never 68 | --- 69 | apiVersion: v1 70 | kind: Service 71 | metadata: 72 | labels: 73 | app.kubernetes.io/instance: chartsnap 74 | app.kubernetes.io/managed-by: Helm 75 | app.kubernetes.io/name: app1 76 | app.kubernetes.io/version: 1.16.0 77 | helm.sh/chart: app1-0.1.0 78 | name: chartsnap-app1 79 | spec: 80 | ports: 81 | - name: http 82 | port: 80 83 | protocol: TCP 84 | targetPort: http 85 | - name: https 86 | port: 443 87 | protocol: TCP 88 | targetPort: https 89 | selector: 90 | app.kubernetes.io/instance: chartsnap 91 | app.kubernetes.io/name: app1 92 | type: ClusterIP 93 | --- 94 | apiVersion: v1 95 | automountServiceAccountToken: true 96 | kind: ServiceAccount 97 | metadata: 98 | labels: 99 | app.kubernetes.io/instance: chartsnap 100 | app.kubernetes.io/managed-by: Helm 101 | app.kubernetes.io/name: app1 102 | app.kubernetes.io/version: 1.16.0 103 | helm.sh/chart: app1-0.1.0 104 | name: chartsnap-app1 105 | -------------------------------------------------------------------------------- /pkg/unstructured/testdata/testspec_test.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | labels: 5 | app.kubernetes.io/instance: chartsnap 6 | app.kubernetes.io/managed-by: Helm 7 | app.kubernetes.io/name: app1 8 | app.kubernetes.io/version: 1.16.0 9 | helm.sh/chart: app1-0.1.0 10 | name: chartsnap-app1 11 | spec: 12 | replicas: 1 13 | selector: 14 | matchLabels: 15 | app.kubernetes.io/instance: chartsnap 16 | app.kubernetes.io/name: app1 17 | template: 18 | metadata: 19 | labels: 20 | app.kubernetes.io/instance: chartsnap 21 | app.kubernetes.io/managed-by: Helm 22 | app.kubernetes.io/name: app1 23 | app.kubernetes.io/version: 1.16.0 24 | helm.sh/chart: app1-0.1.0 25 | spec: 26 | containers: 27 | - image: nginx:1.16.0 28 | imagePullPolicy: IfNotPresent 29 | livenessProbe: 30 | httpGet: 31 | path: / 32 | port: http 33 | name: app1 34 | ports: 35 | - containerPort: 80 36 | name: http 37 | protocol: TCP 38 | readinessProbe: 39 | httpGet: 40 | path: / 41 | port: http 42 | resources: {} 43 | securityContext: {} 44 | securityContext: {} 45 | serviceAccountName: chartsnap-app1 46 | --- 47 | apiVersion: v1 48 | kind: Pod 49 | metadata: 50 | annotations: 51 | helm.sh/hook: test 52 | labels: 53 | app.kubernetes.io/instance: chartsnap 54 | app.kubernetes.io/managed-by: Helm 55 | app.kubernetes.io/name: app1 56 | app.kubernetes.io/version: 1.16.0 57 | helm.sh/chart: app1-0.1.0 58 | name: chartsnap-app1-test-connection 59 | spec: 60 | containers: 61 | - args: 62 | - chartsnap-app1:80 63 | command: 64 | - wget 65 | image: busybox 66 | name: wget 67 | restartPolicy: Never 68 | --- 69 | apiVersion: v1 70 | kind: Service 71 | metadata: 72 | labels: 73 | app.kubernetes.io/instance: chartsnap 74 | app.kubernetes.io/managed-by: Helm 75 | app.kubernetes.io/name: app1 76 | app.kubernetes.io/version: 1.16.0 77 | helm.sh/chart: app1-0.1.0 78 | name: chartsnap-app1 79 | spec: 80 | ports: 81 | - name: http 82 | port: 80 83 | protocol: TCP 84 | targetPort: http 85 | - name: https 86 | port: 443 87 | protocol: TCP 88 | targetPort: https 89 | selector: 90 | app.kubernetes.io/instance: chartsnap 91 | app.kubernetes.io/name: app1 92 | type: ClusterIP 93 | --- 94 | apiVersion: v1 95 | automountServiceAccountToken: true 96 | kind: ServiceAccount 97 | metadata: 98 | labels: 99 | app.kubernetes.io/instance: chartsnap 100 | app.kubernetes.io/managed-by: Helm 101 | app.kubernetes.io/name: app1 102 | app.kubernetes.io/version: 1.16.0 103 | helm.sh/chart: app1-0.1.0 104 | name: chartsnap-app1 105 | -------------------------------------------------------------------------------- /scripts/install_plugin.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | [ "$HELM_DEBUG" != "false" ] && set -x && (printenv | grep HELM) 5 | 6 | 7 | # Function to print error message and exit 8 | error_exit() { 9 | echo "$1" >&2 10 | exit 1 11 | } 12 | 13 | # Function to validate command availability 14 | validate_command() { 15 | command -v "$1" >/dev/null 2>&1 || error_exit "Required command '$1' not found. Please install it." 16 | } 17 | 18 | # Function to detect the specified version from plugin.yaml 19 | get_plugin_version() { 20 | cat $HELM_PLUGIN_DIR/plugin.yaml | grep version: | cut -d " " -f 2 21 | } 22 | 23 | # Function to download and install the plugin 24 | install_plugin() { 25 | local plugin_version="$1" 26 | local plugin_url="$2" 27 | local plugin_filename="$3" 28 | local plugin_directory="$4" 29 | 30 | # Download the plugin archive 31 | if validate_command "curl"; then 32 | curl --fail -sSL "${plugin_url}" -o "${plugin_filename}" 33 | elif validate_command "wget"; then 34 | wget -q "${plugin_url}" -O "${plugin_filename}" 35 | else 36 | error_exit "Both 'curl' and 'wget' commands not found. Please install either one." 37 | fi 38 | 39 | # Extract and install the plugin 40 | tar xzf "${plugin_filename}" -C "${plugin_directory}" 41 | mv "${plugin_directory}/${name}" "bin/${name}" || mv "${plugin_directory}/${name}.exe" "bin/${name}" 42 | } 43 | 44 | # Main script 45 | name="chartsnap" 46 | repo_name="helm-chartsnap" 47 | repo_owner="jlandowner" 48 | repo="https://github.com/${repo_owner}/${repo_name}" 49 | HELM_PUSH_PLUGIN_NO_INSTALL_HOOK="${HELM_PUSH_PLUGIN_NO_INSTALL_HOOK:-}" 50 | 51 | # Check if in development mode 52 | if [ -n "$HELM_PUSH_PLUGIN_NO_INSTALL_HOOK" ]; then 53 | echo "Development mode: not downloading versioned release." 54 | exit 0 55 | fi 56 | 57 | # Autodetect the latest version 58 | version=$(get_plugin_version) 59 | echo "Downloading and installing ${name} v${version} ..." 60 | 61 | # Convert architecture of the target system to a compatible GOARCH value 62 | case $(uname -m) in 63 | x86_64) 64 | arch="amd64" 65 | ;; 66 | aarch64 | arm64) 67 | arch="arm64" 68 | ;; 69 | *) 70 | error_exit "Failed to detect target architecture" 71 | ;; 72 | esac 73 | 74 | # Construct the plugin download URL 75 | if [ "$(uname)" = "Darwin" ]; then 76 | url="${repo}/releases/download/v${version}/${name}_v${version}_darwin_${arch}.tar.gz" 77 | elif [ "$(uname)" = "Linux" ] ; then 78 | url="${repo}/releases/download/v${version}/${name}_v${version}_linux_${arch}.tar.gz" 79 | else 80 | url="${repo}/releases/download/v${version}/${name}_v${version}_windows_${arch}.tar.gz" 81 | fi 82 | 83 | echo "$url" 84 | 85 | mkdir -p "bin" 86 | mkdir -p "releases/${version}" 87 | 88 | install_plugin "$version" "$url" "releases/${version}.tar.gz" "releases/${version}" 89 | 90 | echo 91 | echo "${name} is installed. To start it, run the following command:" 92 | echo " helm chartsnap" 93 | echo 94 | -------------------------------------------------------------------------------- /hack/helm-template-diff/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log/slog" 6 | "os" 7 | "os/exec" 8 | "regexp" 9 | 10 | "github.com/jlandowner/helm-chartsnap/pkg/snap" 11 | ) 12 | 13 | func main() { 14 | cases := []struct { 15 | snap string 16 | helm string 17 | }{ 18 | { 19 | snap: "../../example/app1/__snapshots__/default.snap", 20 | helm: "helm template chartsnap ../../example/app1 -n default", 21 | }, 22 | { 23 | snap: "../../example/app1/test_latest/__snapshots__/test_ingress_enabled.snap", 24 | helm: "helm template chartsnap ../../example/app1 -f ../../example/app1/test_latest/test_ingress_enabled.yaml -n default", 25 | }, 26 | { 27 | snap: "../../example/app1/test_latest/__snapshots__/test_hpa_enabled.snap", 28 | helm: "helm template chartsnap ../../example/app1 -f ../../example/app1/test_latest/test_hpa_enabled.yaml -n default", 29 | }, 30 | { 31 | snap: "../../example/app1/test_latest/__snapshots__/test_certmanager_enabled.snap", 32 | helm: "helm template chartsnap ../../example/app1 -f ../../example/app1/test_latest/test_certmanager_enabled.yaml -n default", 33 | }, 34 | { 35 | snap: "../../example/remote/__snapshots__/nginx-gateway-fabric.values.snap", 36 | helm: "helm template chartsnap oci://ghcr.io/nginxinc/charts/nginx-gateway-fabric -f ../../example/remote/nginx-gateway-fabric.values.yaml -n nginx-gateway", 37 | }, 38 | { 39 | snap: "../../example/remote/__snapshots__/cilium.values.snap", 40 | helm: "helm template chartsnap cilium -f ../../example/remote/cilium.values.yaml -n kube-system --repo https://helm.cilium.io", 41 | }, 42 | { 43 | snap: "../../example/remote/__snapshots__/ingress-nginx.values.snap", 44 | helm: "helm template chartsnap ingress-nginx -f ../../example/remote/ingress-nginx.values.yaml --repo https://kubernetes.github.io/ingress-nginx -n ingress-nginx --skip-tests", 45 | }, 46 | } 47 | 48 | for _, c := range cases { 49 | out := execute("sh", "-c", c.helm) 50 | 51 | re := regexp.MustCompile(`LS0tLS1CRUdJTiB[^ \n]*`) 52 | replaced := re.ReplaceAll(out, []byte("'###DYNAMIC_FIELD###'")) 53 | 54 | f, err := os.CreateTemp("", "helm-template-diff") 55 | if err != nil { 56 | slog.Error("create temp file error", "err", err) 57 | os.Exit(9) 58 | } 59 | defer os.Remove(f.Name()) 60 | 61 | _, err = f.Write(replaced) 62 | if err != nil { 63 | slog.Error("write temp file error", "err", err) 64 | os.Exit(9) 65 | } 66 | 67 | snapshot(fmt.Sprintf("sdiff <(%s) %s", c.helm, c.snap), string(execute("sdiff", f.Name(), c.snap))) 68 | } 69 | } 70 | 71 | func execute(cmd ...string) []byte { 72 | out, _ := exec.Command(cmd[0], cmd[1:]...).CombinedOutput() 73 | return out 74 | } 75 | 76 | func snapshot(id, data string) { 77 | s := snap.SnapshotMatcher("helm-template.snap", snap.WithSnapshotID(id)) 78 | match, err := s.Match(data) 79 | 80 | if err != nil { 81 | slog.Error("snapshot error", "err", err) 82 | os.Exit(9) 83 | } 84 | if !match { 85 | slog.Error(s.FailureMessage(nil)) 86 | os.Exit(1) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /example/app1/values.yaml: -------------------------------------------------------------------------------- 1 | # Default values for app1. 2 | # This is a YAML-formatted file. 3 | # Declare variables to be passed into your templates. 4 | 5 | replicaCount: 1 6 | 7 | image: 8 | repository: nginx 9 | pullPolicy: IfNotPresent 10 | # Overrides the image tag whose default is the chart appVersion. 11 | tag: "" 12 | 13 | imagePullSecrets: [] 14 | nameOverride: "" 15 | fullnameOverride: "" 16 | 17 | serviceAccount: 18 | # Specifies whether a service account should be created 19 | create: true 20 | # Automatically mount a ServiceAccount's API credentials? 21 | automount: true 22 | # Annotations to add to the service account 23 | annotations: {} 24 | # The name of the service account to use. 25 | # If not set and create is true, a name is generated using the fullname template 26 | name: "" 27 | 28 | podAnnotations: {} 29 | podLabels: {} 30 | 31 | podSecurityContext: {} 32 | # fsGroup: 2000 33 | 34 | securityContext: {} 35 | # capabilities: 36 | # drop: 37 | # - ALL 38 | # readOnlyRootFilesystem: true 39 | # runAsNonRoot: true 40 | # runAsUser: 1000 41 | 42 | service: 43 | type: ClusterIP 44 | port: 80 45 | 46 | ingress: 47 | enabled: false 48 | className: "" 49 | annotations: {} 50 | # kubernetes.io/ingress.class: nginx 51 | # kubernetes.io/tls-acme: "true" 52 | hosts: 53 | - host: chart-example.local 54 | paths: 55 | - path: / 56 | pathType: ImplementationSpecific 57 | tls: [] 58 | # - secretName: chart-example-tls 59 | # hosts: 60 | # - chart-example.local 61 | 62 | # Configure when using cert-manager for self-signed certificates: https://cert-manager.io/docs/installation/helm/ 63 | # If cert-manager disabled, helm cert generation feature is used 64 | certManager: 65 | enabled: false 66 | issuer: 67 | create: true 68 | clusterIssuer: false 69 | name: nameOfClusterIssuer 70 | 71 | resources: {} 72 | # We usually recommend not to specify default resources and to leave this as a conscious 73 | # choice for the user. This also increases chances charts run on environments with little 74 | # resources, such as Minikube. If you do want to specify resources, uncomment the following 75 | # lines, adjust them as necessary, and remove the curly braces after 'resources:'. 76 | # limits: 77 | # cpu: 100m 78 | # memory: 128Mi 79 | # requests: 80 | # cpu: 100m 81 | # memory: 128Mi 82 | 83 | autoscaling: 84 | enabled: false 85 | minReplicas: 1 86 | maxReplicas: 100 87 | targetCPUUtilizationPercentage: 80 88 | # targetMemoryUtilizationPercentage: 80 89 | 90 | # Additional volumes on the output Deployment definition. 91 | volumes: [] 92 | # - name: foo 93 | # secret: 94 | # secretName: mysecret 95 | # optional: false 96 | 97 | # Additional volumeMounts on the output Deployment definition. 98 | volumeMounts: [] 99 | # - name: foo 100 | # mountPath: "/etc/foo" 101 | # readOnly: true 102 | 103 | nodeSelector: {} 104 | 105 | tolerations: [] 106 | 107 | affinity: {} 108 | -------------------------------------------------------------------------------- /pkg/yaml/diff_test.go: -------------------------------------------------------------------------------- 1 | package yaml 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | . "github.com/jlandowner/helm-chartsnap/pkg/snap/gomega" 8 | . "github.com/onsi/ginkgo/v2" 9 | . "github.com/onsi/gomega" 10 | 11 | "github.com/aryann/difflib" 12 | ) 13 | 14 | var _ = Describe("Diff", func() { 15 | Context("DiffContextLineN is 3", func() { 16 | It("should return the extracted diff with previous/next 3 lines", func() { 17 | expectedSnap := mustReadFile("testdata/expected.snap") 18 | actualSnap := mustReadFile("testdata/actual.snap") 19 | 20 | d := DiffOptions{ 21 | ContextLineN: 3, 22 | } 23 | diff := d.Diff(expectedSnap, actualSnap) 24 | Ω(diff).To(MatchSnapShot()) 25 | }) 26 | }) 27 | 28 | Context("DiffContextLineN is 0", func() { 29 | It("should return all diff", func() { 30 | expectedSnap := mustReadFile("testdata/expected.snap") 31 | actualSnap := mustReadFile("testdata/actual.snap") 32 | 33 | d := DiffOptions{ 34 | ContextLineN: 0, 35 | } 36 | diff := d.Diff(expectedSnap, actualSnap) 37 | Ω(diff).To(MatchSnapShot()) 38 | }) 39 | }) 40 | }) 41 | 42 | func TestMergeDiffOptions(t *testing.T) { 43 | opts := []DiffOptions{ 44 | {ContextLineN: 5}, 45 | {ContextLineN: 3}, 46 | {ContextLineN: 7}, 47 | } 48 | 49 | merged := MergeDiffOptions(opts) 50 | 51 | if merged.ContextLineN != 7 { 52 | t.Errorf("Expected DiffContextLineN to be 7, got %d", merged.ContextLineN) 53 | } 54 | } 55 | 56 | func Test_printDiff(t *testing.T) { 57 | d := difflib.DiffRecord{ 58 | Delta: difflib.LeftOnly, 59 | Payload: "abc", 60 | } 61 | 62 | want := "- abc\n" 63 | 64 | if got := printDiff(d); got != want { 65 | t.Errorf("printDiff() = %v, want %v", got, want) 66 | } 67 | } 68 | 69 | func Test_printHeader(t *testing.T) { 70 | kind := "TestKind" 71 | name := "TestName" 72 | lineN := 5 73 | 74 | want := "@@ KIND=TestKind NAME=TestName LINE=5\n" 75 | 76 | if got := printHeader(kind, name, lineN); got != want { 77 | t.Errorf("printHeader() = %v, want %v", got, want) 78 | } 79 | } 80 | 81 | func Test_findNextKind(t *testing.T) { 82 | diffs := []difflib.DiffRecord{ 83 | {Delta: difflib.Common, Payload: "abc"}, 84 | {Delta: difflib.Common, Payload: "kind: Pod"}, 85 | {Delta: difflib.Common, Payload: "def"}, 86 | } 87 | 88 | want := "Pod" 89 | 90 | if got := findNextKind(diffs); got != want { 91 | t.Errorf("findNextKind() = %v, want %v", got, want) 92 | } 93 | } 94 | 95 | func Test_findNextName(t *testing.T) { 96 | diffs := []difflib.DiffRecord{ 97 | {Delta: difflib.Common, Payload: "abc"}, 98 | {Delta: difflib.Common, Payload: "metadata:"}, 99 | {Delta: difflib.Common, Payload: " name: TestName"}, 100 | {Delta: difflib.Common, Payload: "def"}, 101 | } 102 | 103 | want := "TestName" 104 | 105 | if got := findNextName(diffs); got != want { 106 | t.Errorf("findNextName() = %v, want %v", got, want) 107 | } 108 | } 109 | 110 | func mustReadFile(path string) string { 111 | data, err := os.ReadFile(path) 112 | if err != nil { 113 | panic(err) 114 | } 115 | return string(data) 116 | } 117 | -------------------------------------------------------------------------------- /example/app1/test_v2/__snapshots__/test_certmanager_enabled.snap: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | automountServiceAccountToken: true 3 | kind: ServiceAccount 4 | metadata: 5 | labels: 6 | app.kubernetes.io/instance: chartsnap 7 | app.kubernetes.io/managed-by: Helm 8 | app.kubernetes.io/name: app1 9 | app.kubernetes.io/version: 1.16.0 10 | helm.sh/chart: app1-0.1.0 11 | name: chartsnap-app1 12 | --- 13 | apiVersion: v1 14 | kind: Service 15 | metadata: 16 | labels: 17 | app.kubernetes.io/instance: chartsnap 18 | app.kubernetes.io/managed-by: Helm 19 | app.kubernetes.io/name: app1 20 | app.kubernetes.io/version: 1.16.0 21 | helm.sh/chart: app1-0.1.0 22 | name: chartsnap-app1 23 | spec: 24 | ports: 25 | - name: http 26 | port: 80 27 | protocol: TCP 28 | targetPort: http 29 | selector: 30 | app.kubernetes.io/instance: chartsnap 31 | app.kubernetes.io/name: app1 32 | type: ClusterIP 33 | --- 34 | apiVersion: apps/v1 35 | kind: Deployment 36 | metadata: 37 | labels: 38 | app.kubernetes.io/instance: chartsnap 39 | app.kubernetes.io/managed-by: Helm 40 | app.kubernetes.io/name: app1 41 | app.kubernetes.io/version: 1.16.0 42 | helm.sh/chart: app1-0.1.0 43 | name: chartsnap-app1 44 | spec: 45 | replicas: 1 46 | selector: 47 | matchLabels: 48 | app.kubernetes.io/instance: chartsnap 49 | app.kubernetes.io/name: app1 50 | template: 51 | metadata: 52 | labels: 53 | app.kubernetes.io/instance: chartsnap 54 | app.kubernetes.io/managed-by: Helm 55 | app.kubernetes.io/name: app1 56 | app.kubernetes.io/version: 1.16.0 57 | helm.sh/chart: app1-0.1.0 58 | spec: 59 | containers: 60 | - image: nginx:1.16.0 61 | imagePullPolicy: IfNotPresent 62 | livenessProbe: 63 | httpGet: 64 | path: / 65 | port: http 66 | name: app1 67 | ports: 68 | - containerPort: 80 69 | name: http 70 | protocol: TCP 71 | readinessProbe: 72 | httpGet: 73 | path: / 74 | port: http 75 | resources: {} 76 | securityContext: {} 77 | securityContext: {} 78 | serviceAccountName: chartsnap-app1 79 | --- 80 | apiVersion: cert-manager.io/v1 81 | kind: Certificate 82 | metadata: 83 | labels: 84 | app.kubernetes.io/instance: chartsnap 85 | app.kubernetes.io/managed-by: Helm 86 | app.kubernetes.io/name: app1 87 | app.kubernetes.io/version: 1.16.0 88 | helm.sh/chart: app1-0.1.0 89 | name: app1-cert 90 | namespace: default 91 | spec: 92 | dnsNames: 93 | - chartsnap-app1.default.svc 94 | - chartsnap-app1.default.svc.cluster.local 95 | issuerRef: 96 | kind: Issuer 97 | name: nameOfClusterIssuer 98 | secretName: app1-cert 99 | --- 100 | apiVersion: v1 101 | kind: Pod 102 | metadata: 103 | annotations: 104 | helm.sh/hook: test 105 | labels: 106 | app.kubernetes.io/instance: chartsnap 107 | app.kubernetes.io/managed-by: Helm 108 | app.kubernetes.io/name: app1 109 | app.kubernetes.io/version: 1.16.0 110 | helm.sh/chart: app1-0.1.0 111 | name: chartsnap-app1-test-connection 112 | spec: 113 | containers: 114 | - args: 115 | - chartsnap-app1:80 116 | command: 117 | - wget 118 | image: busybox 119 | name: wget 120 | restartPolicy: Never 121 | -------------------------------------------------------------------------------- /example/app1/testfail/__snapshots__/test_certmanager_enabled.snap: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | automountServiceAccountToken: true 3 | kind: ServiceAccount 4 | metadata: 5 | labels: 6 | app.kubernetes.io/instance: chartsnap 7 | app.kubernetes.io/managed-by: Helm 8 | app.kubernetes.io/name: app1 9 | app.kubernetes.io/version: 1.16.0 10 | helm.sh/chart: app1-0.1.0 11 | name: chartsnap-app1 12 | --- 13 | apiVersion: v1 14 | kind: Service 15 | metadata: 16 | labels: 17 | app.kubernetes.io/instance: chartsnap 18 | app.kubernetes.io/managed-by: Helm 19 | app.kubernetes.io/name: app1 20 | app.kubernetes.io/version: 1.16.0 21 | helm.sh/chart: app1-0.1.0 22 | name: chartsnap-app1 23 | spec: 24 | ports: 25 | - name: http 26 | port: 80 27 | protocol: TCP 28 | targetPort: http 29 | selector: 30 | app.kubernetes.io/instance: chartsnap 31 | app.kubernetes.io/name: app1 32 | type: ClusterIP 33 | --- 34 | apiVersion: apps/v1 35 | kind: Deployment 36 | metadata: 37 | labels: 38 | app.kubernetes.io/instance: chartsnap 39 | app.kubernetes.io/managed-by: Helm 40 | app.kubernetes.io/name: app1 41 | app.kubernetes.io/version: 1.16.0 42 | helm.sh/chart: app1-0.1.0 43 | name: chartsnap-app1 44 | spec: 45 | replicas: 1 46 | selector: 47 | matchLabels: 48 | app.kubernetes.io/instance: chartsnap 49 | app.kubernetes.io/name: app1 50 | template: 51 | metadata: 52 | labels: 53 | app.kubernetes.io/instance: chartsnap 54 | app.kubernetes.io/managed-by: Helm 55 | app.kubernetes.io/name: app1 56 | app.kubernetes.io/version: 1.16.0 57 | helm.sh/chart: app1-0.1.0 58 | spec: 59 | containers: 60 | - image: nginx:1.16.0 61 | imagePullPolicy: IfNotPresent 62 | livenessProbe: 63 | httpGet: 64 | path: / 65 | port: http 66 | name: app1 67 | ports: 68 | - containerPort: 80 69 | name: http 70 | protocol: TCP 71 | readinessProbe: 72 | httpGet: 73 | path: / 74 | port: http 75 | resources: {} 76 | securityContext: {} 77 | securityContext: {} 78 | serviceAccountName: chartsnap-app1 79 | --- 80 | apiVersion: cert-manager.io/v1 81 | kind: Certificate 82 | metadata: 83 | labels: 84 | app.kubernetes.io/instance: chartsnap 85 | app.kubernetes.io/managed-by: Helm 86 | app.kubernetes.io/name: app1 87 | app.kubernetes.io/version: 1.16.0 88 | helm.sh/chart: app1-0.1.0 89 | name: app1-cert 90 | namespace: default 91 | spec: 92 | dnsNames: 93 | - chartsnap-app1.default.svc 94 | - chartsnap-app1.default.svc.cluster.local 95 | issuerRef: 96 | kind: Issuer 97 | name: nameOfClusterIssuer 98 | secretName: app1-cert 99 | --- 100 | apiVersion: v1 101 | kind: Pod 102 | metadata: 103 | annotations: 104 | helm.sh/hook: test 105 | labels: 106 | app.kubernetes.io/instance: chartsnap 107 | app.kubernetes.io/managed-by: Helm 108 | app.kubernetes.io/name: app1 109 | app.kubernetes.io/version: 1.16.0 110 | helm.sh/chart: app1-0.1.0 111 | name: chartsnap-app1-test-connection 112 | spec: 113 | containers: 114 | - args: 115 | - chartsnap-app1:80 116 | command: 117 | - wget 118 | image: busybox 119 | name: wget 120 | restartPolicy: Never 121 | -------------------------------------------------------------------------------- /example/app1/test_latest/__snapshots__/test_certmanager_enabled.snap: -------------------------------------------------------------------------------- 1 | # chartsnap: snapshot_version=v3 2 | --- 3 | # Source: app1/templates/serviceaccount.yaml 4 | apiVersion: v1 5 | kind: ServiceAccount 6 | metadata: 7 | name: chartsnap-app1 8 | labels: 9 | helm.sh/chart: app1-0.1.0 10 | app.kubernetes.io/name: app1 11 | app.kubernetes.io/instance: chartsnap 12 | app.kubernetes.io/version: "1.16.0" 13 | app.kubernetes.io/managed-by: Helm 14 | automountServiceAccountToken: true 15 | --- 16 | # Source: app1/templates/service.yaml 17 | apiVersion: v1 18 | kind: Service 19 | metadata: 20 | name: chartsnap-app1 21 | labels: 22 | helm.sh/chart: app1-0.1.0 23 | app.kubernetes.io/name: app1 24 | app.kubernetes.io/instance: chartsnap 25 | app.kubernetes.io/version: "1.16.0" 26 | app.kubernetes.io/managed-by: Helm 27 | spec: 28 | type: ClusterIP 29 | ports: 30 | - port: 80 31 | targetPort: http 32 | protocol: TCP 33 | name: http 34 | selector: 35 | app.kubernetes.io/name: app1 36 | app.kubernetes.io/instance: chartsnap 37 | --- 38 | # Source: app1/templates/deployment.yaml 39 | apiVersion: apps/v1 40 | kind: Deployment 41 | metadata: 42 | name: chartsnap-app1 43 | labels: 44 | helm.sh/chart: app1-0.1.0 45 | app.kubernetes.io/name: app1 46 | app.kubernetes.io/instance: chartsnap 47 | app.kubernetes.io/version: "1.16.0" 48 | app.kubernetes.io/managed-by: Helm 49 | spec: 50 | replicas: 1 51 | selector: 52 | matchLabels: 53 | app.kubernetes.io/name: app1 54 | app.kubernetes.io/instance: chartsnap 55 | template: 56 | metadata: 57 | labels: 58 | helm.sh/chart: app1-0.1.0 59 | app.kubernetes.io/name: app1 60 | app.kubernetes.io/instance: chartsnap 61 | app.kubernetes.io/version: "1.16.0" 62 | app.kubernetes.io/managed-by: Helm 63 | spec: 64 | serviceAccountName: chartsnap-app1 65 | securityContext: {} 66 | containers: 67 | - name: app1 68 | securityContext: {} 69 | image: "nginx:1.16.0" 70 | imagePullPolicy: IfNotPresent 71 | ports: 72 | - name: http 73 | containerPort: 80 74 | protocol: TCP 75 | livenessProbe: 76 | httpGet: 77 | path: / 78 | port: http 79 | readinessProbe: 80 | httpGet: 81 | path: / 82 | port: http 83 | resources: {} 84 | --- 85 | # Source: app1/templates/cert.yaml 86 | apiVersion: cert-manager.io/v1 87 | kind: Certificate 88 | metadata: 89 | labels: 90 | helm.sh/chart: app1-0.1.0 91 | app.kubernetes.io/name: app1 92 | app.kubernetes.io/instance: chartsnap 93 | app.kubernetes.io/version: "1.16.0" 94 | app.kubernetes.io/managed-by: Helm 95 | name: app1-cert 96 | namespace: default 97 | spec: 98 | dnsNames: 99 | - chartsnap-app1.default.svc 100 | - chartsnap-app1.default.svc.cluster.local 101 | issuerRef: 102 | kind: Issuer 103 | name: nameOfClusterIssuer 104 | secretName: app1-cert 105 | --- 106 | # Source: app1/templates/tests/test-connection.yaml 107 | apiVersion: v1 108 | kind: Pod 109 | metadata: 110 | name: "chartsnap-app1-test-connection" 111 | labels: 112 | helm.sh/chart: app1-0.1.0 113 | app.kubernetes.io/name: app1 114 | app.kubernetes.io/instance: chartsnap 115 | app.kubernetes.io/version: "1.16.0" 116 | app.kubernetes.io/managed-by: Helm 117 | annotations: 118 | "helm.sh/hook": test 119 | spec: 120 | containers: 121 | - name: wget 122 | image: busybox 123 | command: ['wget'] 124 | args: ['chartsnap-app1:80'] 125 | restartPolicy: Never 126 | -------------------------------------------------------------------------------- /example/app1/test_v3/__snapshots__/test_certmanager_enabled.snap: -------------------------------------------------------------------------------- 1 | # chartsnap: snapshot_version=v3 2 | --- 3 | # Source: app1/templates/serviceaccount.yaml 4 | apiVersion: v1 5 | kind: ServiceAccount 6 | metadata: 7 | name: chartsnap-app1 8 | labels: 9 | helm.sh/chart: app1-0.1.0 10 | app.kubernetes.io/name: app1 11 | app.kubernetes.io/instance: chartsnap 12 | app.kubernetes.io/version: "1.16.0" 13 | app.kubernetes.io/managed-by: Helm 14 | automountServiceAccountToken: true 15 | --- 16 | # Source: app1/templates/service.yaml 17 | apiVersion: v1 18 | kind: Service 19 | metadata: 20 | name: chartsnap-app1 21 | labels: 22 | helm.sh/chart: app1-0.1.0 23 | app.kubernetes.io/name: app1 24 | app.kubernetes.io/instance: chartsnap 25 | app.kubernetes.io/version: "1.16.0" 26 | app.kubernetes.io/managed-by: Helm 27 | spec: 28 | type: ClusterIP 29 | ports: 30 | - port: 80 31 | targetPort: http 32 | protocol: TCP 33 | name: http 34 | selector: 35 | app.kubernetes.io/name: app1 36 | app.kubernetes.io/instance: chartsnap 37 | --- 38 | # Source: app1/templates/deployment.yaml 39 | apiVersion: apps/v1 40 | kind: Deployment 41 | metadata: 42 | name: chartsnap-app1 43 | labels: 44 | helm.sh/chart: app1-0.1.0 45 | app.kubernetes.io/name: app1 46 | app.kubernetes.io/instance: chartsnap 47 | app.kubernetes.io/version: "1.16.0" 48 | app.kubernetes.io/managed-by: Helm 49 | spec: 50 | replicas: 1 51 | selector: 52 | matchLabels: 53 | app.kubernetes.io/name: app1 54 | app.kubernetes.io/instance: chartsnap 55 | template: 56 | metadata: 57 | labels: 58 | helm.sh/chart: app1-0.1.0 59 | app.kubernetes.io/name: app1 60 | app.kubernetes.io/instance: chartsnap 61 | app.kubernetes.io/version: "1.16.0" 62 | app.kubernetes.io/managed-by: Helm 63 | spec: 64 | serviceAccountName: chartsnap-app1 65 | securityContext: {} 66 | containers: 67 | - name: app1 68 | securityContext: {} 69 | image: "nginx:1.16.0" 70 | imagePullPolicy: IfNotPresent 71 | ports: 72 | - name: http 73 | containerPort: 80 74 | protocol: TCP 75 | livenessProbe: 76 | httpGet: 77 | path: / 78 | port: http 79 | readinessProbe: 80 | httpGet: 81 | path: / 82 | port: http 83 | resources: {} 84 | --- 85 | # Source: app1/templates/cert.yaml 86 | apiVersion: cert-manager.io/v1 87 | kind: Certificate 88 | metadata: 89 | labels: 90 | helm.sh/chart: app1-0.1.0 91 | app.kubernetes.io/name: app1 92 | app.kubernetes.io/instance: chartsnap 93 | app.kubernetes.io/version: "1.16.0" 94 | app.kubernetes.io/managed-by: Helm 95 | name: app1-cert 96 | namespace: default 97 | spec: 98 | dnsNames: 99 | - chartsnap-app1.default.svc 100 | - chartsnap-app1.default.svc.cluster.local 101 | issuerRef: 102 | kind: Issuer 103 | name: nameOfClusterIssuer 104 | secretName: app1-cert 105 | --- 106 | # Source: app1/templates/tests/test-connection.yaml 107 | apiVersion: v1 108 | kind: Pod 109 | metadata: 110 | name: "chartsnap-app1-test-connection" 111 | labels: 112 | helm.sh/chart: app1-0.1.0 113 | app.kubernetes.io/name: app1 114 | app.kubernetes.io/instance: chartsnap 115 | app.kubernetes.io/version: "1.16.0" 116 | app.kubernetes.io/managed-by: Helm 117 | annotations: 118 | "helm.sh/hook": test 119 | spec: 120 | containers: 121 | - name: wget 122 | image: busybox 123 | command: ['wget'] 124 | args: ['chartsnap-app1:80'] 125 | restartPolicy: Never 126 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ "main" ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ "main" ] 20 | schedule: 21 | - cron: '44 7 * * 3' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | # Runner size impacts CodeQL analysis time. To learn more, please see: 27 | # - https://gh.io/recommended-hardware-resources-for-running-codeql 28 | # - https://gh.io/supported-runners-and-hardware-resources 29 | # - https://gh.io/using-larger-runners 30 | # Consider using larger runners for possible analysis time improvements. 31 | runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} 32 | timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }} 33 | permissions: 34 | actions: read 35 | contents: read 36 | security-events: write 37 | 38 | strategy: 39 | fail-fast: false 40 | matrix: 41 | language: [ 'go' ] 42 | # CodeQL supports [ 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' ] 43 | # Use only 'java-kotlin' to analyze code written in Java, Kotlin or both 44 | # Use only 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both 45 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 46 | 47 | steps: 48 | - name: Checkout repository 49 | uses: actions/checkout@v4 50 | 51 | # Initializes the CodeQL tools for scanning. 52 | - name: Initialize CodeQL 53 | uses: github/codeql-action/init@v2 54 | with: 55 | languages: ${{ matrix.language }} 56 | # If you wish to specify custom queries, you can do so here or in a config file. 57 | # By default, queries listed here will override any specified in a config file. 58 | # Prefix the list here with "+" to use these queries and those in the config file. 59 | 60 | # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 61 | # queries: security-extended,security-and-quality 62 | 63 | 64 | # Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift). 65 | # If this step fails, then you should remove it and run the build manually (see below) 66 | - name: Autobuild 67 | uses: github/codeql-action/autobuild@v2 68 | 69 | # ℹ️ Command-line programs to run using the OS shell. 70 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 71 | 72 | # If the Autobuild fails above, remove it and uncomment the following three lines. 73 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 74 | 75 | # - run: | 76 | # echo "Run, Build Application using script" 77 | # ./location_of_script_within_repo/buildscript.sh 78 | 79 | - name: Perform CodeQL Analysis 80 | uses: github/codeql-action/analyze@v2 81 | with: 82 | category: "/language:${{matrix.language}}" 83 | -------------------------------------------------------------------------------- /pkg/snap/__snapshot__/single.snap: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | automountServiceAccountToken: true 3 | kind: ServiceAccount 4 | metadata: 5 | labels: 6 | app.kubernetes.io/instance: chartsnap 7 | app.kubernetes.io/managed-by: Helm 8 | app.kubernetes.io/name: app1 9 | app.kubernetes.io/version: 1.16.0 10 | helm.sh/chart: app1-0.1.0 11 | name: chartsnap-app1 12 | --- 13 | apiVersion: v1 14 | kind: Namespace 15 | metadata: 16 | annotations: 17 | helm.sh/hook: test 18 | labels: 19 | app.kubernetes.io/instance: chartsnap 20 | app.kubernetes.io/managed-by: Helm 21 | app.kubernetes.io/name: app1 22 | app.kubernetes.io/version: 1.16.0 23 | helm.sh/chart: app1-0.1.0 24 | name: chartsnap-app1-namespace 25 | --- 26 | apiVersion: v1 27 | data: 28 | ca.crt: '###DYNAMIC_FIELD###' 29 | tls.crt: '###DYNAMIC_FIELD###' 30 | tls.key: '###DYNAMIC_FIELD###' 31 | kind: Secret 32 | metadata: 33 | labels: 34 | app.kubernetes.io/instance: chartsnap 35 | app.kubernetes.io/managed-by: Helm 36 | app.kubernetes.io/name: app1 37 | app.kubernetes.io/version: 1.16.0 38 | helm.sh/chart: app1-0.1.0 39 | name: app1-cert 40 | namespace: default 41 | type: kubernetes.io/tls 42 | --- 43 | apiVersion: v1 44 | kind: Service 45 | metadata: 46 | labels: 47 | app.kubernetes.io/instance: chartsnap 48 | app.kubernetes.io/managed-by: Helm 49 | app.kubernetes.io/name: app1 50 | app.kubernetes.io/version: 1.16.0 51 | helm.sh/chart: app1-0.1.0 52 | name: chartsnap-app1 53 | spec: 54 | ports: 55 | - name: http 56 | port: 80 57 | protocol: TCP 58 | targetPort: http 59 | selector: 60 | app.kubernetes.io/instance: chartsnap 61 | app.kubernetes.io/name: app1 62 | type: LoadBalancer 63 | --- 64 | apiVersion: apps/v1 65 | kind: Deployment 66 | metadata: 67 | labels: 68 | app.kubernetes.io/instance: chartsnap 69 | app.kubernetes.io/managed-by: Helm 70 | app.kubernetes.io/name: app1 71 | app.kubernetes.io/version: 1.15.0 72 | helm.sh/chart: app1-0.1.0 73 | name: chartsnap-app1 74 | spec: 75 | replicas: 1 76 | selector: 77 | matchLabels: 78 | app.kubernetes.io/instance: chartsnap 79 | app.kubernetes.io/name: app1 80 | template: 81 | metadata: 82 | labels: 83 | app.kubernetes.io/instance: chartsnap 84 | app.kubernetes.io/managed-by: Helm 85 | app.kubernetes.io/name: app1 86 | app.kubernetes.io/version: 1.16.0 87 | helm.sh/chart: app1-0.1.0 88 | spec: 89 | containers: 90 | - image: nginx:1.16.0 91 | imagePullPolicy: IfNotPresent 92 | livenessProbe: 93 | httpGet: 94 | path: / 95 | port: http 96 | name: app1 97 | ports: 98 | - containerPort: 80 99 | name: http 100 | protocol: TCP 101 | readinessProbe: 102 | httpGet: 103 | path: / 104 | port: http 105 | resources: {} 106 | securityContext: {} 107 | securityContext: {} 108 | serviceAccountName: chartsnap-app1 109 | --- 110 | apiVersion: v1 111 | kind: Pod 112 | metadata: 113 | annotations: 114 | helm.sh/hook: test 115 | labels: 116 | app.kubernetes.io/instance: chartsnap 117 | app.kubernetes.io/managed-by: Helm 118 | app.kubernetes.io/name: app1 119 | app.kubernetes.io/version: 1.16.0 120 | helm.sh/chart: app1-0.1.0 121 | name: chartsnap-app1-test-connection 122 | spec: 123 | containers: 124 | - args: 125 | - chartsnap-app1:80 126 | command: 127 | - wget 128 | image: busybox 129 | name: wget 130 | restartPolicy: Never 131 | -------------------------------------------------------------------------------- /pkg/yaml/testdata/expected.snap: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | automountServiceAccountToken: true 3 | kind: ServiceAccount 4 | metadata: 5 | labels: 6 | app.kubernetes.io/instance: chartsnap 7 | app.kubernetes.io/managed-by: Helm 8 | app.kubernetes.io/name: app1 9 | app.kubernetes.io/version: 1.16.0 10 | helm.sh/chart: app1-0.1.0 11 | name: chartsnap-app1 12 | --- 13 | apiVersion: v1 14 | kind: Namespace 15 | metadata: 16 | annotations: 17 | helm.sh/hook: test 18 | labels: 19 | app.kubernetes.io/instance: chartsnap 20 | app.kubernetes.io/managed-by: Helm 21 | app.kubernetes.io/name: app1 22 | app.kubernetes.io/version: 1.16.0 23 | helm.sh/chart: app1-0.1.0 24 | name: chartsnap-app1-namespace 25 | --- 26 | apiVersion: v1 27 | data: 28 | ca.crt: '###DYNAMIC_FIELD###' 29 | tls.crt: '###DYNAMIC_FIELD###' 30 | tls.key: '###DYNAMIC_FIELD###' 31 | kind: Secret 32 | metadata: 33 | labels: 34 | app.kubernetes.io/instance: chartsnap 35 | app.kubernetes.io/managed-by: Helm 36 | app.kubernetes.io/name: app1 37 | app.kubernetes.io/version: 1.16.0 38 | helm.sh/chart: app1-0.1.0 39 | name: app1-cert 40 | namespace: default 41 | type: kubernetes.io/tls 42 | --- 43 | apiVersion: v1 44 | kind: Service 45 | metadata: 46 | labels: 47 | app.kubernetes.io/instance: chartsnap 48 | app.kubernetes.io/managed-by: Helm 49 | app.kubernetes.io/name: app1 50 | app.kubernetes.io/version: 1.16.0 51 | helm.sh/chart: app1-0.1.0 52 | name: chartsnap-app1 53 | spec: 54 | ports: 55 | - name: http 56 | port: 80 57 | protocol: TCP 58 | targetPort: http 59 | selector: 60 | app.kubernetes.io/instance: chartsnap 61 | app.kubernetes.io/name: app1 62 | type: LoadBalancer 63 | --- 64 | apiVersion: apps/v1 65 | kind: Deployment 66 | metadata: 67 | labels: 68 | app.kubernetes.io/instance: chartsnap 69 | app.kubernetes.io/managed-by: Helm 70 | app.kubernetes.io/name: app1 71 | app.kubernetes.io/version: 1.15.0 72 | helm.sh/chart: app1-0.1.0 73 | name: chartsnap-app1 74 | spec: 75 | replicas: 1 76 | selector: 77 | matchLabels: 78 | app.kubernetes.io/instance: chartsnap 79 | app.kubernetes.io/name: app1 80 | template: 81 | metadata: 82 | labels: 83 | app.kubernetes.io/instance: chartsnap 84 | app.kubernetes.io/managed-by: Helm 85 | app.kubernetes.io/name: app1 86 | app.kubernetes.io/version: 1.16.0 87 | helm.sh/chart: app1-0.1.0 88 | spec: 89 | containers: 90 | - image: nginx:1.16.0 91 | imagePullPolicy: IfNotPresent 92 | livenessProbe: 93 | httpGet: 94 | path: / 95 | port: http 96 | name: app1 97 | ports: 98 | - containerPort: 80 99 | name: http 100 | protocol: TCP 101 | readinessProbe: 102 | httpGet: 103 | path: / 104 | port: http 105 | resources: {} 106 | securityContext: {} 107 | securityContext: {} 108 | serviceAccountName: chartsnap-app1 109 | --- 110 | apiVersion: v1 111 | kind: Pod 112 | metadata: 113 | annotations: 114 | helm.sh/hook: test 115 | labels: 116 | app.kubernetes.io/instance: chartsnap 117 | app.kubernetes.io/managed-by: Helm 118 | app.kubernetes.io/name: app1 119 | app.kubernetes.io/version: 1.16.0 120 | helm.sh/chart: app1-0.1.0 121 | name: chartsnap-app1-test-connection 122 | spec: 123 | containers: 124 | - args: 125 | - chartsnap-app1:80 126 | command: 127 | - wget 128 | image: busybox 129 | name: wget 130 | restartPolicy: Never 131 | a -------------------------------------------------------------------------------- /pkg/unstructured/testdata/expected.snap: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | automountServiceAccountToken: true 3 | kind: ServiceAccount 4 | metadata: 5 | labels: 6 | app.kubernetes.io/instance: chartsnap 7 | app.kubernetes.io/managed-by: Helm 8 | app.kubernetes.io/name: app1 9 | app.kubernetes.io/version: 1.16.0 10 | helm.sh/chart: app1-0.1.0 11 | name: chartsnap-app1 12 | --- 13 | apiVersion: v1 14 | kind: Namespace 15 | metadata: 16 | annotations: 17 | helm.sh/hook: test 18 | labels: 19 | app.kubernetes.io/instance: chartsnap 20 | app.kubernetes.io/managed-by: Helm 21 | app.kubernetes.io/name: app1 22 | app.kubernetes.io/version: 1.16.0 23 | helm.sh/chart: app1-0.1.0 24 | name: chartsnap-app1-namespace 25 | --- 26 | apiVersion: v1 27 | data: 28 | ca.crt: '###DYNAMIC_FIELD###' 29 | tls.crt: '###DYNAMIC_FIELD###' 30 | tls.key: '###DYNAMIC_FIELD###' 31 | kind: Secret 32 | metadata: 33 | labels: 34 | app.kubernetes.io/instance: chartsnap 35 | app.kubernetes.io/managed-by: Helm 36 | app.kubernetes.io/name: app1 37 | app.kubernetes.io/version: 1.16.0 38 | helm.sh/chart: app1-0.1.0 39 | name: app1-cert 40 | namespace: default 41 | type: kubernetes.io/tls 42 | --- 43 | apiVersion: v1 44 | kind: Service 45 | metadata: 46 | labels: 47 | app.kubernetes.io/instance: chartsnap 48 | app.kubernetes.io/managed-by: Helm 49 | app.kubernetes.io/name: app1 50 | app.kubernetes.io/version: 1.16.0 51 | helm.sh/chart: app1-0.1.0 52 | name: chartsnap-app1 53 | spec: 54 | ports: 55 | - name: http 56 | port: 80 57 | protocol: TCP 58 | targetPort: http 59 | selector: 60 | app.kubernetes.io/instance: chartsnap 61 | app.kubernetes.io/name: app1 62 | type: LoadBalancer 63 | --- 64 | apiVersion: apps/v1 65 | kind: Deployment 66 | metadata: 67 | labels: 68 | app.kubernetes.io/instance: chartsnap 69 | app.kubernetes.io/managed-by: Helm 70 | app.kubernetes.io/name: app1 71 | app.kubernetes.io/version: 1.15.0 72 | helm.sh/chart: app1-0.1.0 73 | name: chartsnap-app1 74 | spec: 75 | replicas: 1 76 | selector: 77 | matchLabels: 78 | app.kubernetes.io/instance: chartsnap 79 | app.kubernetes.io/name: app1 80 | template: 81 | metadata: 82 | labels: 83 | app.kubernetes.io/instance: chartsnap 84 | app.kubernetes.io/managed-by: Helm 85 | app.kubernetes.io/name: app1 86 | app.kubernetes.io/version: 1.16.0 87 | helm.sh/chart: app1-0.1.0 88 | spec: 89 | containers: 90 | - image: nginx:1.16.0 91 | imagePullPolicy: IfNotPresent 92 | livenessProbe: 93 | httpGet: 94 | path: / 95 | port: http 96 | name: app1 97 | ports: 98 | - containerPort: 80 99 | name: http 100 | protocol: TCP 101 | readinessProbe: 102 | httpGet: 103 | path: / 104 | port: http 105 | resources: {} 106 | securityContext: {} 107 | securityContext: {} 108 | serviceAccountName: chartsnap-app1 109 | --- 110 | apiVersion: v1 111 | kind: Pod 112 | metadata: 113 | annotations: 114 | helm.sh/hook: test 115 | labels: 116 | app.kubernetes.io/instance: chartsnap 117 | app.kubernetes.io/managed-by: Helm 118 | app.kubernetes.io/name: app1 119 | app.kubernetes.io/version: 1.16.0 120 | helm.sh/chart: app1-0.1.0 121 | name: chartsnap-app1-test-connection 122 | spec: 123 | containers: 124 | - args: 125 | - chartsnap-app1:80 126 | command: 127 | - wget 128 | image: busybox 129 | name: wget 130 | restartPolicy: Never 131 | a -------------------------------------------------------------------------------- /pkg/api/v1alpha1/unknown_types_test.go: -------------------------------------------------------------------------------- 1 | package v1alpha1 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | yaml "go.yaml.in/yaml/v3" 8 | metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 9 | ) 10 | 11 | func TestUnknown_Unstructured(t *testing.T) { 12 | raw := "some raw data" 13 | err := NewUnknownError(raw) 14 | 15 | obj := err.Unstructured() 16 | expectedObj := &metaV1.Unstructured{ 17 | Object: map[string]interface{}{ 18 | "apiVersion": "helm-chartsnap.jlandowner.dev/v1alpha1", 19 | "kind": "Unknown", 20 | "metadata": map[string]interface{}{ 21 | "name": "helm-output", 22 | }, 23 | "raw": "some raw data", 24 | }, 25 | } 26 | 27 | if !reflect.DeepEqual(obj, expectedObj) { 28 | t.Errorf("Expected obj to be %v, but got %v", err, obj) 29 | } 30 | 31 | } 32 | 33 | func TestUnknown_Error(t *testing.T) { 34 | type fields struct { 35 | Raw string 36 | } 37 | tests := []struct { 38 | name string 39 | fields fields 40 | want string 41 | }{ 42 | { 43 | name: "Test Unknown Error", 44 | fields: fields{ 45 | Raw: "some raw data", 46 | }, 47 | want: "failed to recognize a resource in stdout/stderr of helm template command output. snapshot it as Unknown: \n---\nsome raw data\n---", 48 | }, 49 | } 50 | for _, tt := range tests { 51 | t.Run(tt.name, func(t *testing.T) { 52 | e := &Unknown{ 53 | Raw: tt.fields.Raw, 54 | } 55 | if got := e.Error(); got != tt.want { 56 | t.Errorf("Unknown.Error() = %v, want %v", got, tt.want) 57 | } 58 | }) 59 | } 60 | } 61 | 62 | func TestUnknown_Node(t *testing.T) { 63 | type fields struct { 64 | Raw string 65 | } 66 | tests := []struct { 67 | name string 68 | fields fields 69 | want *yaml.Node 70 | }{ 71 | { 72 | name: "Test Unknown Node", 73 | fields: fields{ 74 | Raw: `some raw 75 | data 76 | `, 77 | }, 78 | want: &yaml.Node{ 79 | Kind: yaml.MappingNode, Content: []*yaml.Node{ 80 | {Kind: yaml.ScalarNode, Value: "apiVersion"}, 81 | {Kind: yaml.ScalarNode, Value: "helm-chartsnap.jlandowner.dev/v1alpha1"}, 82 | {Kind: yaml.ScalarNode, Value: "kind"}, 83 | {Kind: yaml.ScalarNode, Value: "Unknown"}, 84 | {Kind: yaml.ScalarNode, Value: "metadata"}, 85 | {Kind: yaml.MappingNode, Content: []*yaml.Node{ 86 | {Kind: yaml.ScalarNode, Value: "name"}, 87 | {Kind: yaml.ScalarNode, Value: "helm-output"}, 88 | }}, 89 | {Kind: yaml.ScalarNode, Value: "raw"}, 90 | {Kind: yaml.ScalarNode, Value: "some raw\ndata\n", Style: yaml.LiteralStyle}, 91 | }, 92 | }, 93 | }, 94 | } 95 | for _, tt := range tests { 96 | t.Run(tt.name, func(t *testing.T) { 97 | e := &Unknown{ 98 | Raw: tt.fields.Raw, 99 | } 100 | if got := e.Node(); !reflect.DeepEqual(got, tt.want) { 101 | t.Errorf("Unknown.Node() = %v, want %v", got, tt.want) 102 | } 103 | }) 104 | } 105 | } 106 | 107 | func TestUnknown_MustString(t *testing.T) { 108 | type fields struct { 109 | Raw string 110 | } 111 | tests := []struct { 112 | name string 113 | fields fields 114 | want string 115 | }{ 116 | { 117 | name: "Test Unknown String", 118 | fields: fields{ 119 | Raw: `some raw 120 | data 121 | `, 122 | }, 123 | want: `apiVersion: helm-chartsnap.jlandowner.dev/v1alpha1 124 | kind: Unknown 125 | metadata: 126 | name: helm-output 127 | raw: | 128 | some raw 129 | data 130 | `, 131 | }, 132 | } 133 | for _, tt := range tests { 134 | t.Run(tt.name, func(t *testing.T) { 135 | e := &Unknown{ 136 | Raw: tt.fields.Raw, 137 | } 138 | got := e.MustString() 139 | if got != tt.want { 140 | t.Errorf("Unknown.String() = %v, want %v", got, tt.want) 141 | } 142 | }) 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /pkg/snap/cachefs_test.go: -------------------------------------------------------------------------------- 1 | package snap 2 | 3 | import ( 4 | "crypto/rand" 5 | "encoding/hex" 6 | "os" 7 | "path/filepath" 8 | 9 | . "github.com/onsi/ginkgo/v2" 10 | . "github.com/onsi/gomega" 11 | ) 12 | 13 | var _ = Describe("CacheFS", func() { 14 | Context("CacheFS", func() { 15 | It("WriteFile", func() { 16 | snapTempDir := filepath.Join(os.TempDir(), "__snapshot__") 17 | os.RemoveAll(snapTempDir) 18 | 19 | filePath := filepath.Join(snapTempDir, generateRandomFileName()) 20 | defer os.RemoveAll(snapTempDir) 21 | 22 | data := []byte("Hello, World! 0") 23 | 24 | err := WriteFile(filePath, data) 25 | Expect(err).NotTo(HaveOccurred()) 26 | 27 | fileContent, err := os.ReadFile(filePath) 28 | Expect(err).NotTo(HaveOccurred()) 29 | Expect(fileContent).To(Equal(data)) 30 | }) 31 | 32 | It("ReadFile", func() { 33 | tmpDir := createTempDir() 34 | defer os.RemoveAll(tmpDir) 35 | 36 | filePath := filepath.Join(tmpDir, "test.txt") 37 | data := []byte("Hello, World! 1") 38 | 39 | err := os.WriteFile(filePath, data, 0644) 40 | Expect(err).NotTo(HaveOccurred()) 41 | 42 | fileContent, err := ReadFile(filePath) 43 | Expect(err).NotTo(HaveOccurred()) 44 | Expect(fileContent).To(Equal(data)) 45 | 46 | // Remove the file directly 47 | err = os.Remove(filePath) 48 | Expect(err).NotTo(HaveOccurred()) 49 | 50 | // Verify that the file has been removed 51 | _, err = os.Stat(filePath) 52 | Expect(os.IsNotExist(err)).To(BeTrue()) 53 | 54 | // Verify that cacheFs still keeps the file 55 | fileContent, err = ReadFile(filePath) 56 | Expect(err).NotTo(HaveOccurred()) 57 | Expect(fileContent).To(Equal(data)) 58 | }) 59 | 60 | It("RemoveFile", func() { 61 | tmpDir := createTempDir() 62 | defer os.RemoveAll(tmpDir) 63 | 64 | filePath := filepath.Join(tmpDir, "test.txt") 65 | data := []byte("Hello, World! 2") 66 | 67 | err := os.WriteFile(filePath, data, 0644) 68 | Expect(err).NotTo(HaveOccurred()) 69 | 70 | fileContent, err := ReadFile(filePath) 71 | Expect(err).NotTo(HaveOccurred()) 72 | Expect(fileContent).To(Equal(data)) 73 | 74 | // Remove the file via cacheFs 75 | err = RemoveFile(filePath) 76 | Expect(err).NotTo(HaveOccurred()) 77 | 78 | // Verify that the file has been removed 79 | _, err = os.Stat(filePath) 80 | Expect(os.IsNotExist(err)).To(BeTrue()) 81 | 82 | // Verify that the file has been removed from cacheFs 83 | _, err = ReadFile(filePath) 84 | Expect(os.IsNotExist(err)).To(BeTrue()) 85 | }) 86 | 87 | It("WriteFile with permission error", func() { 88 | // Try to write to a directory that doesn't exist and can't be created 89 | invalidPath := "/root/nonexistent/test.txt" 90 | data := []byte("test data") 91 | 92 | err := WriteFile(invalidPath, data) 93 | // Should handle error gracefully even if directory creation fails 94 | // The exact behavior depends on the system, but we expect it to handle errors 95 | Expect(err).To(HaveOccurred()) 96 | }) 97 | 98 | It("ReadFile with non-existent file", func() { 99 | nonExistentPath := "/tmp/non-existent-file.txt" 100 | 101 | _, err := ReadFile(nonExistentPath) 102 | Expect(os.IsNotExist(err)).To(BeTrue()) 103 | }) 104 | 105 | }) 106 | }) 107 | 108 | // Helper function to create a temporary directory for testing 109 | func createTempDir() string { 110 | tmpDir, err := os.MkdirTemp("", "test") 111 | if err != nil { 112 | panic(err) 113 | } 114 | return tmpDir 115 | } 116 | 117 | func generateRandomFileName() string { 118 | randBytes := make([]byte, 16) 119 | _, err := rand.Read(randBytes) 120 | if err != nil { 121 | panic(err) 122 | } 123 | return hex.EncodeToString(randBytes) 124 | } 125 | -------------------------------------------------------------------------------- /example/app1/testfail/__snapshots__/test_ingress_enabled.snap: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | automountServiceAccountToken: true 3 | kind: ServiceAccount 4 | metadata: 5 | labels: 6 | app.kubernetes.io/instance: chartsnap 7 | app.kubernetes.io/managed-by: Helm 8 | app.kubernetes.io/name: app1 9 | app.kubernetes.io/version: 1.16.0 10 | helm.sh/chart: app1-0.1.0 11 | name: chartsnap-app1 12 | --- 13 | apiVersion: v1 14 | kind: Namespace 15 | metadata: 16 | annotations: 17 | helm.sh/hook: test 18 | labels: 19 | app.kubernetes.io/instance: chartsnap 20 | app.kubernetes.io/managed-by: Helm 21 | app.kubernetes.io/name: app1 22 | app.kubernetes.io/version: 1.16.0 23 | helm.sh/chart: app1-0.1.0 24 | name: chartsnap-app1-namespace 25 | --- 26 | apiVersion: v1 27 | data: 28 | ca.crt: '###DYNAMIC_FIELD###' 29 | tls.crt: '###DYNAMIC_FIELD###' 30 | tls.key: '###DYNAMIC_FIELD###' 31 | kind: Secret 32 | metadata: 33 | labels: 34 | app.kubernetes.io/instance: chartsnap 35 | app.kubernetes.io/managed-by: Helm 36 | app.kubernetes.io/name: app1 37 | app.kubernetes.io/version: 1.16.0 38 | helm.sh/chart: app1-0.1.0 39 | name: app1-cert 40 | namespace: default 41 | type: kubernetes.io/tls 42 | --- 43 | apiVersion: v1 44 | kind: Service 45 | metadata: 46 | labels: 47 | app.kubernetes.io/instance: chartsnap 48 | app.kubernetes.io/managed-by: Helm 49 | app.kubernetes.io/name: app1 50 | app.kubernetes.io/version: 1.16.0 51 | helm.sh/chart: app1-0.1.0 52 | name: chartsnap-app1 53 | spec: 54 | ports: 55 | - name: http 56 | port: 80 57 | protocol: TCP 58 | targetPort: http 59 | selector: 60 | app.kubernetes.io/instance: chartsnap 61 | app.kubernetes.io/name: app1 62 | type: LoadBalancer 63 | --- 64 | apiVersion: apps/v1 65 | kind: Deployment 66 | metadata: 67 | labels: 68 | app.kubernetes.io/instance: chartsnap 69 | app.kubernetes.io/managed-by: Helm 70 | app.kubernetes.io/name: app1 71 | app.kubernetes.io/version: 1.15.0 72 | helm.sh/chart: app1-0.1.0 73 | name: chartsnap-app1 74 | spec: 75 | replicas: 1 76 | selector: 77 | matchLabels: 78 | app.kubernetes.io/instance: chartsnap 79 | app.kubernetes.io/name: app1 80 | template: 81 | metadata: 82 | labels: 83 | app.kubernetes.io/instance: chartsnap 84 | app.kubernetes.io/managed-by: Helm 85 | app.kubernetes.io/name: app1 86 | app.kubernetes.io/version: 1.16.0 87 | helm.sh/chart: app1-0.1.0 88 | spec: 89 | containers: 90 | - image: nginx:1.16.0 91 | imagePullPolicy: IfNotPresent 92 | livenessProbe: 93 | httpGet: 94 | path: / 95 | port: http 96 | name: app1 97 | ports: 98 | - containerPort: 80 99 | name: http 100 | protocol: TCP 101 | readinessProbe: 102 | httpGet: 103 | path: / 104 | port: http 105 | resources: {} 106 | securityContext: {} 107 | securityContext: {} 108 | serviceAccountName: chartsnap-app1 109 | --- 110 | apiVersion: v1 111 | kind: Pod 112 | metadata: 113 | annotations: 114 | helm.sh/hook: test 115 | labels: 116 | app.kubernetes.io/instance: chartsnap 117 | app.kubernetes.io/managed-by: Helm 118 | app.kubernetes.io/name: app1 119 | app.kubernetes.io/version: 1.16.0 120 | helm.sh/chart: app1-0.1.0 121 | name: chartsnap-app1-test-connection 122 | spec: 123 | containers: 124 | - args: 125 | - chartsnap-app1:80 126 | command: 127 | - wget 128 | image: busybox 129 | name: wget 130 | restartPolicy: Never 131 | a 132 | -------------------------------------------------------------------------------- /pkg/snap/__snapshot__/multi.snap: -------------------------------------------------------------------------------- 1 | [default] 2 | SnapShot = """ 3 | apiVersion: v2 4 | automountServiceAccountToken: true 5 | kind: ServiceAccount 6 | metadata: 7 | labels: 8 | app.kubernetes.io/instance: chartsnap 9 | app.kubernetes.io/managed-by: Helm 10 | app.kubernetes.io/name: app1 11 | app.kubernetes.io/version: 1.16.0 12 | helm.sh/chart: app1-0.1.0 13 | name: chartsnap-app1 14 | --- 15 | apiVersion: v1 16 | kind: Namespace 17 | metadata: 18 | annotations: 19 | helm.sh/hook: test 20 | labels: 21 | app.kubernetes.io/instance: chartsnap 22 | app.kubernetes.io/managed-by: Helm 23 | app.kubernetes.io/name: app1 24 | app.kubernetes.io/version: 1.16.0 25 | helm.sh/chart: app1-0.1.0 26 | name: chartsnap-app1-namespace 27 | --- 28 | apiVersion: v1 29 | data: 30 | ca.crt: '###DYNAMIC_FIELD###' 31 | tls.crt: '###DYNAMIC_FIELD###' 32 | tls.key: '###DYNAMIC_FIELD###' 33 | kind: Secret 34 | metadata: 35 | labels: 36 | app.kubernetes.io/instance: chartsnap 37 | app.kubernetes.io/managed-by: Helm 38 | app.kubernetes.io/name: app1 39 | app.kubernetes.io/version: 1.16.0 40 | helm.sh/chart: app1-0.1.0 41 | name: app1-cert 42 | namespace: default 43 | type: kubernetes.io/tls 44 | --- 45 | apiVersion: v1 46 | kind: Service 47 | metadata: 48 | labels: 49 | app.kubernetes.io/instance: chartsnap 50 | app.kubernetes.io/managed-by: Helm 51 | app.kubernetes.io/name: app1 52 | app.kubernetes.io/version: 1.16.0 53 | helm.sh/chart: app1-0.1.0 54 | name: chartsnap-app1 55 | spec: 56 | ports: 57 | - name: http 58 | port: 80 59 | protocol: TCP 60 | targetPort: http 61 | selector: 62 | app.kubernetes.io/instance: chartsnap 63 | app.kubernetes.io/name: app1 64 | type: LoadBalancer 65 | --- 66 | apiVersion: apps/v1 67 | kind: Deployment 68 | metadata: 69 | labels: 70 | app.kubernetes.io/instance: chartsnap 71 | app.kubernetes.io/managed-by: Helm 72 | app.kubernetes.io/name: app1 73 | app.kubernetes.io/version: 1.15.0 74 | helm.sh/chart: app1-0.1.0 75 | name: chartsnap-app1 76 | spec: 77 | replicas: 1 78 | selector: 79 | matchLabels: 80 | app.kubernetes.io/instance: chartsnap 81 | app.kubernetes.io/name: app1 82 | template: 83 | metadata: 84 | labels: 85 | app.kubernetes.io/instance: chartsnap 86 | app.kubernetes.io/managed-by: Helm 87 | app.kubernetes.io/name: app1 88 | app.kubernetes.io/version: 1.16.0 89 | helm.sh/chart: app1-0.1.0 90 | spec: 91 | containers: 92 | - image: nginx:1.16.0 93 | imagePullPolicy: IfNotPresent 94 | livenessProbe: 95 | httpGet: 96 | path: / 97 | port: http 98 | name: app1 99 | ports: 100 | - containerPort: 80 101 | name: http 102 | protocol: TCP 103 | readinessProbe: 104 | httpGet: 105 | path: / 106 | port: http 107 | resources: {} 108 | securityContext: {} 109 | securityContext: {} 110 | serviceAccountName: chartsnap-app1 111 | --- 112 | apiVersion: v1 113 | kind: Pod 114 | metadata: 115 | annotations: 116 | helm.sh/hook: test 117 | labels: 118 | app.kubernetes.io/instance: chartsnap 119 | app.kubernetes.io/managed-by: Helm 120 | app.kubernetes.io/name: app1 121 | app.kubernetes.io/version: 1.16.0 122 | helm.sh/chart: app1-0.1.0 123 | name: chartsnap-app1-test-connection 124 | spec: 125 | containers: 126 | - args: 127 | - chartsnap-app1:80 128 | command: 129 | - wget 130 | image: busybox 131 | name: wget 132 | restartPolicy: Never 133 | """ 134 | -------------------------------------------------------------------------------- /pkg/unstructured/v1/legacy.go: -------------------------------------------------------------------------------- 1 | package unstructured 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "sort" 7 | "strings" 8 | 9 | "github.com/aryann/difflib" 10 | "github.com/fatih/color" 11 | yaml "go.yaml.in/yaml/v3" 12 | metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 13 | ) 14 | 15 | // Encode encoding legacy formatted yaml 16 | func Encode(arr []metaV1.Unstructured) ([]byte, error) { 17 | sort.SliceStable(arr, func(i, j int) bool { 18 | if arr[i].GetAPIVersion() != arr[j].GetAPIVersion() { 19 | return arr[i].GetAPIVersion() < arr[j].GetAPIVersion() 20 | } 21 | if arr[i].GetKind() != arr[j].GetKind() { 22 | return arr[i].GetKind() < arr[j].GetKind() 23 | } 24 | return arr[i].GetName() < arr[j].GetName() 25 | }) 26 | return yaml.Marshal(arr) 27 | } 28 | 29 | // extract kind value 30 | func findKind(diffs []difflib.DiffRecord) string { 31 | kindExp := regexp.MustCompile(`^ kind: (.+)$`) 32 | for i := 0; i < len(diffs); i++ { 33 | kindMatch := kindExp.FindStringSubmatch(diffs[i].String()) 34 | if len(kindMatch) > 0 { 35 | return kindMatch[1] 36 | } 37 | } 38 | return "" 39 | } 40 | 41 | // extract name value 42 | func findName(diffs []difflib.DiffRecord) string { 43 | metaExp := regexp.MustCompile(`^ metadata:$`) 44 | nameExp := regexp.MustCompile(`^ name: (.+)$`) 45 | for i := 0; i < len(diffs); i++ { 46 | if metaExp.Match([]byte(diffs[i].String())) { 47 | for j := i + 1; j < len(diffs)-i; j++ { 48 | nameMatch := nameExp.FindStringSubmatch(diffs[j].String()) 49 | if len(nameMatch) > 0 { 50 | return nameMatch[1] 51 | } 52 | } 53 | return "" 54 | } 55 | } 56 | return "" 57 | } 58 | 59 | type DiffOptions struct { 60 | ContextLineN int 61 | } 62 | 63 | func (o *DiffOptions) Diff(x, y string) string { 64 | divExp := regexp.MustCompile(`^ - object:$`) 65 | diffs := difflib.Diff(strings.Split(x, "\n"), strings.Split(y, "\n")) 66 | 67 | var ( 68 | sb strings.Builder 69 | isDiffSequence bool 70 | currentKind string 71 | currentName string 72 | ) 73 | 74 | for i, v := range diffs { 75 | if o.ContextLineN < 1 { 76 | // all records 77 | sb.WriteString(diffString(v)) 78 | continue 79 | } 80 | 81 | if divExp.Match([]byte(v.String())) { 82 | isDiffSequence = false 83 | currentKind, currentName = findKind(diffs[i:]), findName(diffs[i:]) 84 | } 85 | 86 | if v.Delta != difflib.Common { 87 | isDiffSequence = true 88 | 89 | // if first diff, add a header and previous lines 90 | if i > 0 && diffs[i-1].Delta == difflib.Common { 91 | // header 92 | sb.WriteString(color.New(color.FgCyan).Sprintf("@@ KIND=%s NAME=%s LINE=%d\n", currentKind, currentName, i)) 93 | 94 | // previous lines 95 | for j := intInRange(0, len(diffs), i-o.ContextLineN); j < i; j++ { 96 | sb.WriteString(fmt.Sprintf("%s\n", diffs[j])) 97 | } 98 | } 99 | sb.WriteString(diffString(v)) 100 | 101 | } else { 102 | if isDiffSequence { 103 | isDiffSequence = false 104 | 105 | // subsequent lines 106 | for j := i; j < intInRange(0, len(diffs), i+o.ContextLineN); j++ { 107 | sb.WriteString(fmt.Sprintf("%s\n", diffs[j])) 108 | } 109 | // divider 110 | sb.WriteString("\n") 111 | } 112 | } 113 | } 114 | return sb.String() 115 | } 116 | 117 | func intInRange(min, max, v int) int { 118 | if v >= min && v <= max { 119 | return v 120 | } else if v < min { 121 | return min 122 | } else { 123 | return max 124 | } 125 | } 126 | 127 | func diffString(d difflib.DiffRecord) string { 128 | switch d.Delta { 129 | case difflib.LeftOnly: 130 | return color.New(color.FgRed).Sprintf("%s\n", d) 131 | case difflib.RightOnly: 132 | return color.New(color.FgGreen).Sprintf("%s\n", d) 133 | default: 134 | return fmt.Sprintf("%s\n", d) 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /pkg/yaml/yaml.go: -------------------------------------------------------------------------------- 1 | package yaml 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "log/slog" 7 | "regexp" 8 | "strings" 9 | "sync" 10 | 11 | "sigs.k8s.io/kustomize/kyaml/kio" 12 | "sigs.k8s.io/kustomize/kyaml/yaml" 13 | 14 | "github.com/jlandowner/helm-chartsnap/pkg/api/v1alpha1" 15 | "github.com/jlandowner/helm-chartsnap/pkg/jsonpatch" 16 | ) 17 | 18 | var ( 19 | logger *slog.Logger 20 | mutex sync.Mutex 21 | ) 22 | 23 | func SetLogger(slogr *slog.Logger) { 24 | mutex.Lock() 25 | defer mutex.Unlock() 26 | logger = slogr 27 | } 28 | 29 | func log() *slog.Logger { 30 | mutex.Lock() 31 | defer mutex.Unlock() 32 | if logger == nil { 33 | logger = slog.Default() 34 | } 35 | return logger 36 | } 37 | 38 | func Encode(resources []*yaml.RNode) ([]byte, error) { 39 | // ref: kio.StringAll() 40 | var b bytes.Buffer 41 | err := (&kio.ByteWriter{Writer: &b}).Write(resources) 42 | return b.Bytes(), err 43 | } 44 | 45 | func Decode(bs []byte) (out []*yaml.RNode, err error) { 46 | out, err = decode(bs) 47 | if err != nil { 48 | log().Debug(fmt.Sprintf("failed to decode YAML: %v", err)) 49 | out, err = decode(convertInvalidYAMLToUnknown(bs)) 50 | if err != nil { 51 | return nil, fmt.Errorf("failed to decode YAML: %w", err) 52 | } 53 | } 54 | 55 | err = convertToUnknownNode(out) 56 | if err != nil { 57 | return nil, fmt.Errorf("failed to convert scaler node to unknown node: %w", err) 58 | } 59 | return out, nil 60 | } 61 | 62 | func decode(bs []byte) ([]*yaml.RNode, error) { 63 | // ref: kio.FromBytes() 64 | return (&kio.ByteReader{ 65 | OmitReaderAnnotations: true, 66 | AnchorsAweigh: true, 67 | Reader: bytes.NewBuffer(bs), 68 | }).Read() 69 | } 70 | 71 | func ApplyFixedValueToDynamicFields(t v1alpha1.SnapshotConfig, docs []*yaml.RNode) error { 72 | for _, v := range t.DynamicFields { 73 | for i, doc := range docs { 74 | if (v.APIVersion == "" || v.APIVersion == doc.GetApiVersion()) && 75 | (v.Kind == "" || v.Kind == doc.GetKind()) && 76 | (v.Name == "" || v.Name == doc.GetName()) { 77 | for _, p := range v.JSONPath { 78 | err := Replace(docs[i], p, v.DynamicValue()) 79 | if err != nil { 80 | return fmt.Errorf("failed to replace json path: %w", err) 81 | } 82 | } 83 | } 84 | } 85 | } 86 | return nil 87 | } 88 | 89 | func convertInvalidYAMLToUnknown(bs []byte) []byte { 90 | splitString := regexp.MustCompile(`(?m)^---$`).Split(string(bs), -1) 91 | 92 | docs := make([]string, 0, len(splitString)) 93 | for _, v := range splitString { 94 | if err := yaml.NewDecoder(bytes.NewBufferString(v)).Decode(&yaml.Node{}); err == nil { 95 | docs = append(docs, v) 96 | } else { 97 | unknown := v1alpha1.NewUnknownError(v) 98 | log().Warn(unknown.Error()) 99 | docs = append(docs, unknown.MustString()) 100 | } 101 | } 102 | return []byte(strings.Join(docs, "\n---\n")) 103 | } 104 | 105 | func convertToUnknownNode(docs []*yaml.RNode) error { 106 | for i, v := range docs { 107 | if v.IsStringValue() { 108 | vv := v.YNode().Value 109 | unknown := v1alpha1.NewUnknownError(vv) 110 | log().Warn(unknown.Error()) 111 | docs[i] = yaml.NewRNode(unknown.Node()) 112 | docs[i].ShouldKeep = true 113 | } else if v.GetApiVersion() == "" && v.GetKind() == "" { 114 | vv, _ := Encode([]*yaml.RNode{v}) 115 | unknown := v1alpha1.NewUnknownError(string(vv)) 116 | log().Warn(unknown.Error()) 117 | docs[i] = yaml.NewRNode(unknown.Node()) 118 | docs[i].ShouldKeep = true 119 | } 120 | } 121 | return nil 122 | } 123 | 124 | // Replace replaces the value at the given jsonpath with the given value. 125 | // jsonpath is a RFC 6901 JSON path. 126 | func Replace(doc *yaml.RNode, path, value string) error { 127 | return doc.PipeE(yaml.LookupCreate(yaml.ScalarNode, (jsonpatch.SplitPathDecoded(path))...), yaml.Set(yaml.NewStringRNode(value))) 128 | } 129 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/jlandowner/helm-chartsnap 2 | 3 | go 1.24.3 4 | 5 | require ( 6 | github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b 7 | github.com/evanphx/json-patch/v5 v5.9.11 8 | github.com/fatih/color v1.18.0 9 | github.com/google/go-cmp v0.7.0 10 | github.com/m-mizutani/clog v0.1.0 11 | github.com/onsi/ginkgo/v2 v2.25.3 12 | github.com/onsi/gomega v1.38.2 13 | github.com/pelletier/go-toml/v2 v2.2.4 14 | github.com/spf13/afero v1.15.0 15 | github.com/spf13/cobra v1.10.1 16 | go.yaml.in/yaml/v3 v3.0.4 17 | golang.org/x/sync v0.17.0 18 | k8s.io/apimachinery v0.34.1 19 | sigs.k8s.io/controller-runtime v0.22.1 20 | sigs.k8s.io/kustomize/kyaml v0.20.1 21 | ) 22 | 23 | require ( 24 | github.com/Masterminds/semver/v3 v3.4.0 // indirect 25 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 26 | github.com/emicklei/go-restful/v3 v3.13.0 // indirect 27 | github.com/fxamacker/cbor/v2 v2.9.0 // indirect 28 | github.com/go-errors/errors v1.5.1 // indirect 29 | github.com/go-logr/logr v1.4.3 // indirect 30 | github.com/go-openapi/jsonpointer v0.22.0 // indirect 31 | github.com/go-openapi/jsonreference v0.21.1 // indirect 32 | github.com/go-openapi/swag v0.24.1 // indirect 33 | github.com/go-openapi/swag/cmdutils v0.24.0 // indirect 34 | github.com/go-openapi/swag/conv v0.24.0 // indirect 35 | github.com/go-openapi/swag/fileutils v0.24.0 // indirect 36 | github.com/go-openapi/swag/jsonname v0.24.0 // indirect 37 | github.com/go-openapi/swag/jsonutils v0.24.0 // indirect 38 | github.com/go-openapi/swag/loading v0.24.0 // indirect 39 | github.com/go-openapi/swag/mangling v0.24.0 // indirect 40 | github.com/go-openapi/swag/netutils v0.24.0 // indirect 41 | github.com/go-openapi/swag/stringutils v0.24.0 // indirect 42 | github.com/go-openapi/swag/typeutils v0.24.0 // indirect 43 | github.com/go-openapi/swag/yamlutils v0.24.0 // indirect 44 | github.com/go-task/slim-sprig/v3 v3.0.0 // indirect 45 | github.com/gogo/protobuf v1.3.2 // indirect 46 | github.com/google/gnostic-models v0.7.0 // indirect 47 | github.com/google/pprof v0.0.0-20250919162441-8b542baf5bcf // indirect 48 | github.com/google/uuid v1.6.0 // indirect 49 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 50 | github.com/josharian/intern v1.0.0 // indirect 51 | github.com/json-iterator/go v1.1.12 // indirect 52 | github.com/k0kubun/pp/v3 v3.5.0 // indirect 53 | github.com/m-mizutani/goerr/v2 v2.0.0 // indirect 54 | github.com/mailru/easyjson v0.9.1 // indirect 55 | github.com/mattn/go-colorable v0.1.14 // indirect 56 | github.com/mattn/go-isatty v0.0.20 // indirect 57 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 58 | github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect 59 | github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect 60 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 61 | github.com/spf13/pflag v1.0.10 // indirect 62 | github.com/x448/float16 v0.8.4 // indirect 63 | github.com/xlab/treeprint v1.2.0 // indirect 64 | go.uber.org/automaxprocs v1.6.0 // indirect 65 | go.yaml.in/yaml/v2 v2.4.3 // indirect 66 | golang.org/x/net v0.44.0 // indirect 67 | golang.org/x/oauth2 v0.31.0 // indirect 68 | golang.org/x/sys v0.36.0 // indirect 69 | golang.org/x/term v0.35.0 // indirect 70 | golang.org/x/text v0.29.0 // indirect 71 | golang.org/x/time v0.13.0 // indirect 72 | golang.org/x/tools v0.37.0 // indirect 73 | google.golang.org/protobuf v1.36.9 // indirect 74 | gopkg.in/inf.v0 v0.9.1 // indirect 75 | gopkg.in/yaml.v3 v3.0.1 // indirect 76 | k8s.io/api v0.34.1 // indirect 77 | k8s.io/client-go v0.34.1 // indirect 78 | k8s.io/klog/v2 v2.130.1 // indirect 79 | k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect 80 | k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d // indirect 81 | sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect 82 | sigs.k8s.io/randfill v1.0.0 // indirect 83 | sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect 84 | sigs.k8s.io/yaml v1.6.0 // indirect 85 | ) 86 | -------------------------------------------------------------------------------- /example/app1/test_v2/__snapshots__/test_hpa_enabled.snap: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | automountServiceAccountToken: true 3 | kind: ServiceAccount 4 | metadata: 5 | labels: 6 | app.kubernetes.io/instance: chartsnap 7 | app.kubernetes.io/managed-by: Helm 8 | app.kubernetes.io/name: app1 9 | app.kubernetes.io/version: 1.16.0 10 | helm.sh/chart: app1-0.1.0 11 | name: chartsnap-app1 12 | --- 13 | apiVersion: v1 14 | data: 15 | ca.crt: IyMjRFlOQU1JQ19GSUVMRCMjIw== 16 | tls.crt: IyMjRFlOQU1JQ19GSUVMRCMjIw== 17 | tls.key: IyMjRFlOQU1JQ19GSUVMRCMjIw== 18 | kind: Secret 19 | metadata: 20 | labels: 21 | app.kubernetes.io/instance: chartsnap 22 | app.kubernetes.io/managed-by: Helm 23 | app.kubernetes.io/name: app1 24 | app.kubernetes.io/version: 1.16.0 25 | helm.sh/chart: app1-0.1.0 26 | name: app1-cert 27 | namespace: default 28 | type: kubernetes.io/tls 29 | --- 30 | apiVersion: v1 31 | kind: Service 32 | metadata: 33 | labels: 34 | app.kubernetes.io/instance: chartsnap 35 | app.kubernetes.io/managed-by: Helm 36 | app.kubernetes.io/name: app1 37 | app.kubernetes.io/version: 1.16.0 38 | helm.sh/chart: app1-0.1.0 39 | name: chartsnap-app1 40 | spec: 41 | ports: 42 | - name: http 43 | port: 80 44 | protocol: TCP 45 | targetPort: http 46 | selector: 47 | app.kubernetes.io/instance: chartsnap 48 | app.kubernetes.io/name: app1 49 | type: ClusterIP 50 | --- 51 | apiVersion: apps/v1 52 | kind: Deployment 53 | metadata: 54 | labels: 55 | app.kubernetes.io/instance: chartsnap 56 | app.kubernetes.io/managed-by: Helm 57 | app.kubernetes.io/name: app1 58 | app.kubernetes.io/version: 1.16.0 59 | helm.sh/chart: app1-0.1.0 60 | name: chartsnap-app1 61 | spec: 62 | selector: 63 | matchLabels: 64 | app.kubernetes.io/instance: chartsnap 65 | app.kubernetes.io/name: app1 66 | template: 67 | metadata: 68 | labels: 69 | app.kubernetes.io/instance: chartsnap 70 | app.kubernetes.io/managed-by: Helm 71 | app.kubernetes.io/name: app1 72 | app.kubernetes.io/version: 1.16.0 73 | helm.sh/chart: app1-0.1.0 74 | spec: 75 | containers: 76 | - image: nginx:1.16.0 77 | imagePullPolicy: IfNotPresent 78 | livenessProbe: 79 | httpGet: 80 | path: / 81 | port: http 82 | name: app1 83 | ports: 84 | - containerPort: 80 85 | name: http 86 | protocol: TCP 87 | readinessProbe: 88 | httpGet: 89 | path: / 90 | port: http 91 | resources: {} 92 | securityContext: {} 93 | securityContext: {} 94 | serviceAccountName: chartsnap-app1 95 | --- 96 | apiVersion: autoscaling/v2 97 | kind: HorizontalPodAutoscaler 98 | metadata: 99 | labels: 100 | app.kubernetes.io/instance: chartsnap 101 | app.kubernetes.io/managed-by: Helm 102 | app.kubernetes.io/name: app1 103 | app.kubernetes.io/version: 1.16.0 104 | helm.sh/chart: app1-0.1.0 105 | name: chartsnap-app1 106 | spec: 107 | maxReplicas: 10 108 | metrics: 109 | - resource: 110 | name: cpu 111 | target: 112 | averageUtilization: 65 113 | type: Utilization 114 | type: Resource 115 | minReplicas: 1 116 | scaleTargetRef: 117 | apiVersion: apps/v1 118 | kind: Deployment 119 | name: chartsnap-app1 120 | --- 121 | apiVersion: v1 122 | kind: Pod 123 | metadata: 124 | annotations: 125 | helm.sh/hook: test 126 | labels: 127 | app.kubernetes.io/instance: chartsnap 128 | app.kubernetes.io/managed-by: Helm 129 | app.kubernetes.io/name: app1 130 | app.kubernetes.io/version: 1.16.0 131 | helm.sh/chart: app1-0.1.0 132 | name: chartsnap-app1-test-connection 133 | spec: 134 | containers: 135 | - args: 136 | - chartsnap-app1:80 137 | command: 138 | - wget 139 | image: busybox 140 | name: wget 141 | restartPolicy: Never 142 | -------------------------------------------------------------------------------- /example/app1/testfail/__snapshots__/test_hpa_enabled.snap: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | automountServiceAccountToken: true 3 | kind: ServiceAccount 4 | metadata: 5 | labels: 6 | app.kubernetes.io/instance: chartsnap 7 | app.kubernetes.io/managed-by: Helm 8 | app.kubernetes.io/name: app1 9 | app.kubernetes.io/version: 1.16.0 10 | helm.sh/chart: app1-0.1.0 11 | name: chartsnap-app1 12 | --- 13 | apiVersion: v1 14 | data: 15 | ca.crt: IyMjRFlOQU1JQ19GSUVMRCMjIw== 16 | tls.crt: IyMjRFlOQU1JQ19GSUVMRCMjIw== 17 | tls.key: IyMjRFlOQU1JQ19GSUVMRCMjIw== 18 | kind: Secret 19 | metadata: 20 | labels: 21 | app.kubernetes.io/instance: chartsnap 22 | app.kubernetes.io/managed-by: Helm 23 | app.kubernetes.io/name: app1 24 | app.kubernetes.io/version: 1.16.0 25 | helm.sh/chart: app1-0.1.0 26 | name: app1-cert 27 | namespace: default 28 | type: kubernetes.io/tls 29 | --- 30 | apiVersion: v1 31 | kind: Service 32 | metadata: 33 | labels: 34 | app.kubernetes.io/instance: chartsnap 35 | app.kubernetes.io/managed-by: Helm 36 | app.kubernetes.io/name: app1 37 | app.kubernetes.io/version: 1.16.0 38 | helm.sh/chart: app1-0.1.0 39 | name: chartsnap-app1 40 | spec: 41 | ports: 42 | - name: http 43 | port: 80 44 | protocol: TCP 45 | targetPort: http 46 | selector: 47 | app.kubernetes.io/instance: chartsnap 48 | app.kubernetes.io/name: app1 49 | type: ClusterIP 50 | --- 51 | apiVersion: apps/v1 52 | kind: Deployment 53 | metadata: 54 | labels: 55 | app.kubernetes.io/instance: chartsnap 56 | app.kubernetes.io/managed-by: Helm 57 | app.kubernetes.io/name: app1 58 | app.kubernetes.io/version: 1.16.0 59 | helm.sh/chart: app1-0.1.0 60 | name: chartsnap-app1 61 | spec: 62 | selector: 63 | matchLabels: 64 | app.kubernetes.io/instance: chartsnap 65 | app.kubernetes.io/name: app1 66 | template: 67 | metadata: 68 | labels: 69 | app.kubernetes.io/instance: chartsnap 70 | app.kubernetes.io/managed-by: Helm 71 | app.kubernetes.io/name: app1 72 | app.kubernetes.io/version: 1.16.0 73 | helm.sh/chart: app1-0.1.0 74 | spec: 75 | containers: 76 | - image: nginx:1.16.0 77 | imagePullPolicy: IfNotPresent 78 | livenessProbe: 79 | httpGet: 80 | path: / 81 | port: http 82 | name: app1 83 | ports: 84 | - containerPort: 80 85 | name: http 86 | protocol: TCP 87 | readinessProbe: 88 | httpGet: 89 | path: / 90 | port: http 91 | resources: {} 92 | securityContext: {} 93 | securityContext: {} 94 | serviceAccountName: chartsnap-app1 95 | --- 96 | apiVersion: autoscaling/v2 97 | kind: HorizontalPodAutoscaler 98 | metadata: 99 | labels: 100 | app.kubernetes.io/instance: chartsnap 101 | app.kubernetes.io/managed-by: Helm 102 | app.kubernetes.io/name: app1 103 | app.kubernetes.io/version: 1.16.0 104 | helm.sh/chart: app1-0.1.0 105 | name: chartsnap-app1 106 | spec: 107 | maxReplicas: 10 108 | metrics: 109 | - resource: 110 | name: cpu 111 | target: 112 | averageUtilization: 65 113 | type: Utilization 114 | type: Resource 115 | minReplicas: 1 116 | scaleTargetRef: 117 | apiVersion: apps/v1 118 | kind: Deployment 119 | name: chartsnap-app1 120 | --- 121 | apiVersion: v1 122 | kind: Pod 123 | metadata: 124 | annotations: 125 | helm.sh/hook: test 126 | labels: 127 | app.kubernetes.io/instance: chartsnap 128 | app.kubernetes.io/managed-by: Helm 129 | app.kubernetes.io/name: app1 130 | app.kubernetes.io/version: 1.16.0 131 | helm.sh/chart: app1-0.1.0 132 | name: chartsnap-app1-test-connection 133 | spec: 134 | containers: 135 | - args: 136 | - chartsnap-app1:80 137 | command: 138 | - wget 139 | image: busybox 140 | name: wget 141 | restartPolicy: Never 142 | -------------------------------------------------------------------------------- /pkg/unstructured/suite_test.go: -------------------------------------------------------------------------------- 1 | package unstructured 2 | 3 | import ( 4 | "io" 5 | "os" 6 | "testing" 7 | 8 | . "github.com/jlandowner/helm-chartsnap/pkg/snap/gomega" 9 | . "github.com/onsi/ginkgo/v2" 10 | . "github.com/onsi/gomega" 11 | metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 12 | 13 | "github.com/jlandowner/helm-chartsnap/pkg/api/v1alpha1" 14 | ) 15 | 16 | func TestUnstructured(t *testing.T) { 17 | RegisterFailHandler(Fail) 18 | RunSpecs(t, "Unstructured Suite") 19 | } 20 | 21 | var _ = Describe("Diff", func() { 22 | Context("DiffContextLineN is 3", func() { 23 | It("should return the extracted diff with previous/next 3 lines", func() { 24 | expectedSnap := mustReadFile("testdata/expected.snap") 25 | actualSnap := mustReadFile("testdata/actual.snap") 26 | 27 | d := DiffOptions{ 28 | ContextLineN: 3, 29 | } 30 | diff := d.Diff(expectedSnap, actualSnap) 31 | Ω(diff).To(MatchSnapShot()) 32 | }) 33 | }) 34 | 35 | Context("DiffContextLineN is 0", func() { 36 | It("should return all diff", func() { 37 | expectedSnap := mustReadFile("testdata/expected.snap") 38 | actualSnap := mustReadFile("testdata/actual.snap") 39 | 40 | d := DiffOptions{ 41 | ContextLineN: 0, 42 | } 43 | diff := d.Diff(expectedSnap, actualSnap) 44 | Ω(diff).To(MatchSnapShot()) 45 | }) 46 | }) 47 | }) 48 | 49 | var _ = Describe("Unknown", func() { 50 | Context("OK", func() { 51 | It("report unknown as warning", func() { 52 | raw := `some: raw data 53 | raw: 54 | data: here` 55 | err := v1alpha1.NewUnknownError(raw) 56 | 57 | Ω(err.Error()).To(MatchSnapShot()) 58 | }) 59 | }) 60 | }) 61 | 62 | var _ = Describe("ApplyDynamicFields", func() { 63 | load := func(filePath string) []metaV1.Unstructured { 64 | f, err := os.Open(filePath) 65 | Expect(err).NotTo(HaveOccurred()) 66 | defer f.Close() 67 | 68 | buf, err := io.ReadAll(f) 69 | Expect(err).NotTo(HaveOccurred()) 70 | 71 | manifests, errs := Decode(string(buf)) 72 | Expect(len(errs)).To(BeZero()) 73 | 74 | return manifests 75 | } 76 | 77 | It("should replace specified fields", func() { 78 | cfg := v1alpha1.SnapshotConfig{ 79 | DynamicFields: []v1alpha1.ManifestPath{ 80 | { 81 | APIVersion: "v1", 82 | Kind: "Service", 83 | Name: "chartsnap-app2", 84 | JSONPath: []string{ 85 | "/spec/ports/0/targetPort", 86 | }, 87 | }, 88 | { 89 | APIVersion: "v2", 90 | Kind: "Service", 91 | Name: "chartsnap-app1", 92 | JSONPath: []string{ 93 | "/spec/ports/0/targetPort", 94 | }, 95 | }, 96 | { 97 | APIVersion: "v1", 98 | Kind: "service", 99 | Name: "chartsnap-app1", 100 | JSONPath: []string{ 101 | "/spec/ports/0/targetPort", 102 | }, 103 | }, 104 | { 105 | APIVersion: "v1", 106 | Kind: "Service", 107 | Name: "chartsnap-app1", 108 | JSONPath: []string{ 109 | "/spec/ports/1/targetPort", 110 | }, 111 | }, 112 | { 113 | APIVersion: "apps/v1", 114 | Kind: "Deployment", 115 | Name: "chartsnap-app1", 116 | JSONPath: []string{ 117 | "/metadata/labels/app.kubernetes.io~1version", 118 | }, 119 | }, 120 | { 121 | APIVersion: "v1", 122 | Kind: "Pod", 123 | Name: "chartsnap-app1-test-connection", 124 | JSONPath: []string{ 125 | "/metadata/name", 126 | }, 127 | }, 128 | { 129 | APIVersion: "apps/v1", 130 | Kind: "Deployment", 131 | Name: "chartsnap-app1", 132 | JSONPath: []string{ 133 | "/spec/template/spec/serviceAccountName", 134 | }, 135 | Base64: true, 136 | }, 137 | }, 138 | } 139 | manifests := load("testdata/testspec_test.yaml") 140 | err := ApplyFixedValue(cfg, manifests) 141 | Expect(err).NotTo(HaveOccurred()) 142 | Expect(manifests).To(MatchSnapShot()) 143 | }) 144 | }) 145 | 146 | func mustReadFile(path string) string { 147 | data, err := os.ReadFile(path) 148 | if err != nil { 149 | panic(err) 150 | } 151 | return string(data) 152 | } 153 | -------------------------------------------------------------------------------- /pkg/charts/__snapshots__/helm_stub_snap_unmatch_v3.yaml: -------------------------------------------------------------------------------- 1 | # chartsnap: snapshot_version=v3 2 | --- 3 | # Source: app1/templates/serviceaccount.yaml 4 | apiVersion: v1 5 | kind: ServiceAccount 6 | metadata: 7 | labels: 8 | app.kubernetes.io/instance: chartsnap 9 | app.kubernetes.io/managed-by: Helm 10 | app.kubernetes.io/name: app1 11 | app.kubernetes.io/version: 1.15.0 12 | helm.sh/chart: app1-0.1.0 13 | name: chartsnap-app1 14 | automountServiceAccountToken: true 15 | --- 16 | # Source: app1/templates/secret.yaml 17 | apiVersion: v1 18 | kind: Secret 19 | metadata: 20 | labels: 21 | app.kubernetes.io/instance: chartsnap 22 | app.kubernetes.io/managed-by: Helm 23 | app.kubernetes.io/name: app1 24 | app.kubernetes.io/version: 1.15.0 25 | helm.sh/chart: app1-0.1.0 26 | name: app1-cert 27 | namespace: default 28 | data: 29 | ca.crt: IyMjRFlOQU1JQ19GSUVMRCMjIw== 30 | tls.crt: IyMjRFlOQU1JQ19GSUVMRCMjIw== 31 | tls.key: IyMjRFlOQU1JQ19GSUVMRCMjIw== 32 | type: kubernetes.io/tls 33 | --- 34 | # Source: app1/templates/service.yaml 35 | apiVersion: v1 36 | kind: Service 37 | metadata: 38 | labels: 39 | app.kubernetes.io/instance: chartsnap 40 | app.kubernetes.io/managed-by: Helm 41 | app.kubernetes.io/name: app1 42 | app.kubernetes.io/version: 1.15.0 43 | helm.sh/chart: app1-0.1.0 44 | name: chartsnap-app1 45 | spec: 46 | ports: 47 | - name: http 48 | port: 80 49 | protocol: TCP 50 | targetPort: http 51 | selector: 52 | app.kubernetes.io/instance: chartsnap 53 | app.kubernetes.io/name: app1 54 | type: ClusterIP 55 | --- 56 | # Source: app1/templates/deployment.yaml 57 | apiVersion: apps/v1 58 | kind: Deployment 59 | metadata: 60 | labels: 61 | app.kubernetes.io/instance: chartsnap 62 | app.kubernetes.io/managed-by: Helm 63 | app.kubernetes.io/name: app1 64 | app.kubernetes.io/version: 1.15.0 65 | helm.sh/chart: app1-0.1.0 66 | name: chartsnap-app1 67 | spec: 68 | selector: 69 | matchLabels: 70 | app.kubernetes.io/instance: chartsnap 71 | app.kubernetes.io/name: app1 72 | template: 73 | metadata: 74 | labels: 75 | app.kubernetes.io/instance: chartsnap 76 | app.kubernetes.io/managed-by: Helm 77 | app.kubernetes.io/name: app1 78 | app.kubernetes.io/version: 1.15.0 79 | helm.sh/chart: app1-0.1.0 80 | spec: 81 | containers: 82 | - image: nginx:1.15.0 83 | imagePullPolicy: IfNotPresent 84 | name: app1 85 | ports: 86 | - containerPort: 80 87 | name: http 88 | protocol: TCP 89 | readinessProbe: 90 | httpGet: 91 | path: / 92 | port: http 93 | resources: {} 94 | securityContext: {} 95 | securityContext: {} 96 | serviceAccountName: chartsnap-app1 97 | --- 98 | # Source: app1/templates/hpa.yaml 99 | apiVersion: autoscaling/v2 100 | kind: HorizontalPodAutoscaler 101 | metadata: 102 | labels: 103 | app.kubernetes.io/instance: chartsnap 104 | app.kubernetes.io/managed-by: Helm 105 | app.kubernetes.io/name: app1 106 | app.kubernetes.io/version: 1.15.0 107 | helm.sh/chart: app1-0.1.0 108 | name: chartsnap-app1 109 | spec: 110 | maxReplicas: 10 111 | metrics: 112 | - resource: 113 | name: cpu 114 | target: 115 | averageUtilization: 65 116 | type: Utilization 117 | type: Resource 118 | minReplicas: 1 119 | scaleTargetRef: 120 | apiVersion: apps/v1 121 | kind: Deployment 122 | name: chartsnap-app1 123 | --- 124 | # Source: app1/templates/tests/test-connection.yaml 125 | apiVersion: v1 126 | kind: Pod 127 | metadata: 128 | annotations: 129 | helm.sh/hook: test 130 | labels: 131 | app.kubernetes.io/instance: chartsnap 132 | app.kubernetes.io/managed-by: Helm 133 | app.kubernetes.io/name: app1 134 | app.kubernetes.io/version: 1.15.0 135 | helm.sh/chart: app1-0.1.0 136 | name: chartsnap-app1-test-connection 137 | spec: 138 | containers: 139 | - args: 140 | - chartsnap-app1:80 141 | command: 142 | - wget 143 | image: busybox 144 | name: wget 145 | restartPolicy: Never 146 | -------------------------------------------------------------------------------- /pkg/yaml/testdata/actual.snap: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | automountServiceAccountToken: true 3 | kind: ServiceAccount 4 | metadata: 5 | labels: 6 | app.kubernetes.io/instance: chartsnap 7 | app.kubernetes.io/managed-by: Helm 8 | app.kubernetes.io/name: app1 9 | app.kubernetes.io/version: 1.16.0 10 | helm.sh/chart: app1-0.1.0 11 | name: chartsnap-app1 12 | --- 13 | apiVersion: v1 14 | data: 15 | ca.crt: IyMjRFlOQU1JQ19GSUVMRCMjIw== 16 | tls.crt: IyMjRFlOQU1JQ19GSUVMRCMjIw== 17 | tls.key: IyMjRFlOQU1JQ19GSUVMRCMjIw== 18 | kind: Secret 19 | metadata: 20 | labels: 21 | app.kubernetes.io/instance: chartsnap 22 | app.kubernetes.io/managed-by: Helm 23 | app.kubernetes.io/name: app1 24 | app.kubernetes.io/version: 1.16.0 25 | helm.sh/chart: app1-0.1.0 26 | name: app1-cert 27 | namespace: default 28 | type: kubernetes.io/tls 29 | --- 30 | apiVersion: v1 31 | kind: Service 32 | metadata: 33 | labels: 34 | app.kubernetes.io/instance: chartsnap 35 | app.kubernetes.io/managed-by: Helm 36 | app.kubernetes.io/name: app1 37 | app.kubernetes.io/version: 1.16.0 38 | helm.sh/chart: app1-0.1.0 39 | name: chartsnap-app1 40 | spec: 41 | ports: 42 | - name: http 43 | port: 80 44 | protocol: TCP 45 | targetPort: http 46 | selector: 47 | app.kubernetes.io/instance: chartsnap 48 | app.kubernetes.io/name: app1 49 | type: ClusterIP 50 | --- 51 | apiVersion: apps/v1 52 | kind: Deployment 53 | metadata: 54 | labels: 55 | app.kubernetes.io/instance: chartsnap 56 | app.kubernetes.io/managed-by: Helm 57 | app.kubernetes.io/name: app1 58 | app.kubernetes.io/version: 1.16.0 59 | helm.sh/chart: app1-0.1.0 60 | name: chartsnap-app1 61 | spec: 62 | replicas: 1 63 | selector: 64 | matchLabels: 65 | app.kubernetes.io/instance: chartsnap 66 | app.kubernetes.io/name: app1 67 | template: 68 | metadata: 69 | labels: 70 | app.kubernetes.io/instance: chartsnap 71 | app.kubernetes.io/managed-by: Helm 72 | app.kubernetes.io/name: app1 73 | app.kubernetes.io/version: 1.16.0 74 | helm.sh/chart: app1-0.1.0 75 | spec: 76 | containers: 77 | - image: nginx:1.16.0 78 | imagePullPolicy: IfNotPresent 79 | livenessProbe: 80 | httpGet: 81 | path: / 82 | port: http 83 | name: app1 84 | ports: 85 | - containerPort: 80 86 | name: http 87 | protocol: TCP 88 | readinessProbe: 89 | httpGet: 90 | path: / 91 | port: http 92 | resources: {} 93 | securityContext: {} 94 | securityContext: {} 95 | serviceAccountName: chartsnap-app1 96 | --- 97 | apiVersion: networking.k8s.io/v1 98 | kind: Ingress 99 | annotations: 100 | cert-manager.io/cluster-issuer: nameOfClusterIssuer 101 | labels: 102 | app.kubernetes.io/instance: chartsnap 103 | app.kubernetes.io/managed-by: Helm 104 | app.kubernetes.io/name: app1 105 | app.kubernetes.io/version: 1.16.0 106 | helm.sh/chart: app1-0.1.0 107 | name: chartsnap-app1 108 | ingressClassName: nginx 109 | rules: 110 | - host: chart-example.local 111 | http: 112 | paths: 113 | - backend: 114 | service: 115 | name: chartsnap-app1 116 | port: 117 | number: 80 118 | path: / 119 | pathType: ImplementationSpecific 120 | tls: 121 | - hosts: 122 | - chart-example.local 123 | secretName: chart-example-tls 124 | --- 125 | apiVersion: v1 126 | kind: Pod 127 | metadata: 128 | annotations: 129 | helm.sh/hook: test 130 | labels: 131 | app.kubernetes.io/instance: chartsnap 132 | app.kubernetes.io/managed-by: Helm 133 | app.kubernetes.io/name: app1 134 | app.kubernetes.io/version: 1.16.0 135 | helm.sh/chart: app1-0.1.0 136 | name: chartsnap-app1-test-connection 137 | spec: 138 | containers: 139 | - args: 140 | - chartsnap-app1:80 141 | command: 142 | - wget 143 | image: busybox 144 | name: wget 145 | restartPolicy: Never 146 | -------------------------------------------------------------------------------- /pkg/unstructured/testdata/actual.snap: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | automountServiceAccountToken: true 3 | kind: ServiceAccount 4 | metadata: 5 | labels: 6 | app.kubernetes.io/instance: chartsnap 7 | app.kubernetes.io/managed-by: Helm 8 | app.kubernetes.io/name: app1 9 | app.kubernetes.io/version: 1.16.0 10 | helm.sh/chart: app1-0.1.0 11 | name: chartsnap-app1 12 | --- 13 | apiVersion: v1 14 | data: 15 | ca.crt: IyMjRFlOQU1JQ19GSUVMRCMjIw== 16 | tls.crt: IyMjRFlOQU1JQ19GSUVMRCMjIw== 17 | tls.key: IyMjRFlOQU1JQ19GSUVMRCMjIw== 18 | kind: Secret 19 | metadata: 20 | labels: 21 | app.kubernetes.io/instance: chartsnap 22 | app.kubernetes.io/managed-by: Helm 23 | app.kubernetes.io/name: app1 24 | app.kubernetes.io/version: 1.16.0 25 | helm.sh/chart: app1-0.1.0 26 | name: app1-cert 27 | namespace: default 28 | type: kubernetes.io/tls 29 | --- 30 | apiVersion: v1 31 | kind: Service 32 | metadata: 33 | labels: 34 | app.kubernetes.io/instance: chartsnap 35 | app.kubernetes.io/managed-by: Helm 36 | app.kubernetes.io/name: app1 37 | app.kubernetes.io/version: 1.16.0 38 | helm.sh/chart: app1-0.1.0 39 | name: chartsnap-app1 40 | spec: 41 | ports: 42 | - name: http 43 | port: 80 44 | protocol: TCP 45 | targetPort: http 46 | selector: 47 | app.kubernetes.io/instance: chartsnap 48 | app.kubernetes.io/name: app1 49 | type: ClusterIP 50 | --- 51 | apiVersion: apps/v1 52 | kind: Deployment 53 | metadata: 54 | labels: 55 | app.kubernetes.io/instance: chartsnap 56 | app.kubernetes.io/managed-by: Helm 57 | app.kubernetes.io/name: app1 58 | app.kubernetes.io/version: 1.16.0 59 | helm.sh/chart: app1-0.1.0 60 | name: chartsnap-app1 61 | spec: 62 | replicas: 1 63 | selector: 64 | matchLabels: 65 | app.kubernetes.io/instance: chartsnap 66 | app.kubernetes.io/name: app1 67 | template: 68 | metadata: 69 | labels: 70 | app.kubernetes.io/instance: chartsnap 71 | app.kubernetes.io/managed-by: Helm 72 | app.kubernetes.io/name: app1 73 | app.kubernetes.io/version: 1.16.0 74 | helm.sh/chart: app1-0.1.0 75 | spec: 76 | containers: 77 | - image: nginx:1.16.0 78 | imagePullPolicy: IfNotPresent 79 | livenessProbe: 80 | httpGet: 81 | path: / 82 | port: http 83 | name: app1 84 | ports: 85 | - containerPort: 80 86 | name: http 87 | protocol: TCP 88 | readinessProbe: 89 | httpGet: 90 | path: / 91 | port: http 92 | resources: {} 93 | securityContext: {} 94 | securityContext: {} 95 | serviceAccountName: chartsnap-app1 96 | --- 97 | apiVersion: networking.k8s.io/v1 98 | kind: Ingress 99 | annotations: 100 | cert-manager.io/cluster-issuer: nameOfClusterIssuer 101 | labels: 102 | app.kubernetes.io/instance: chartsnap 103 | app.kubernetes.io/managed-by: Helm 104 | app.kubernetes.io/name: app1 105 | app.kubernetes.io/version: 1.16.0 106 | helm.sh/chart: app1-0.1.0 107 | name: chartsnap-app1 108 | ingressClassName: nginx 109 | rules: 110 | - host: chart-example.local 111 | http: 112 | paths: 113 | - backend: 114 | service: 115 | name: chartsnap-app1 116 | port: 117 | number: 80 118 | path: / 119 | pathType: ImplementationSpecific 120 | tls: 121 | - hosts: 122 | - chart-example.local 123 | secretName: chart-example-tls 124 | --- 125 | apiVersion: v1 126 | kind: Pod 127 | metadata: 128 | annotations: 129 | helm.sh/hook: test 130 | labels: 131 | app.kubernetes.io/instance: chartsnap 132 | app.kubernetes.io/managed-by: Helm 133 | app.kubernetes.io/name: app1 134 | app.kubernetes.io/version: 1.16.0 135 | helm.sh/chart: app1-0.1.0 136 | name: chartsnap-app1-test-connection 137 | spec: 138 | containers: 139 | - args: 140 | - chartsnap-app1:80 141 | command: 142 | - wget 143 | image: busybox 144 | name: wget 145 | restartPolicy: Never 146 | -------------------------------------------------------------------------------- /pkg/charts/__snapshots__/helm_stub_snap_unmatch_v2.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: helm-chartsnap.jlandowner.dev/v1alpha1 2 | kind: Unknown 3 | metadata: 4 | name: helm-output 5 | raw: | 6 | this is warning message of helm 7 | --- 8 | apiVersion: v1 9 | automountServiceAccountToken: true 10 | kind: ServiceAccount 11 | metadata: 12 | labels: 13 | app.kubernetes.io/instance: chartsnap 14 | app.kubernetes.io/managed-by: Helm 15 | app.kubernetes.io/name: app1 16 | app.kubernetes.io/version: 1.15.0 17 | helm.sh/chart: app1-0.1.0 18 | name: chartsnap-app1 19 | --- 20 | apiVersion: v1 21 | data: 22 | ca.crt: IyMjRFlOQU1JQ19GSUVMRCMjIw== 23 | tls.crt: IyMjRFlOQU1JQ19GSUVMRCMjIw== 24 | tls.key: IyMjRFlOQU1JQ19GSUVMRCMjIw== 25 | kind: Secret 26 | metadata: 27 | labels: 28 | app.kubernetes.io/instance: chartsnap 29 | app.kubernetes.io/managed-by: Helm 30 | app.kubernetes.io/name: app1 31 | app.kubernetes.io/version: 1.15.0 32 | helm.sh/chart: app1-0.1.0 33 | name: app1-cert 34 | namespace: default 35 | type: kubernetes.io/tls 36 | --- 37 | apiVersion: v1 38 | kind: Service 39 | metadata: 40 | labels: 41 | app.kubernetes.io/instance: chartsnap 42 | app.kubernetes.io/managed-by: Helm 43 | app.kubernetes.io/name: app1 44 | app.kubernetes.io/version: 1.15.0 45 | helm.sh/chart: app1-0.1.0 46 | name: chartsnap-app1 47 | spec: 48 | ports: 49 | - name: http 50 | port: 80 51 | protocol: TCP 52 | targetPort: http 53 | selector: 54 | app.kubernetes.io/instance: chartsnap 55 | app.kubernetes.io/name: app1 56 | type: ClusterIP 57 | --- 58 | apiVersion: apps/v1 59 | kind: Deployment 60 | metadata: 61 | labels: 62 | app.kubernetes.io/instance: chartsnap 63 | app.kubernetes.io/managed-by: Helm 64 | app.kubernetes.io/name: app1 65 | app.kubernetes.io/version: 1.15.0 66 | helm.sh/chart: app1-0.1.0 67 | name: chartsnap-app1 68 | spec: 69 | selector: 70 | matchLabels: 71 | app.kubernetes.io/instance: chartsnap 72 | app.kubernetes.io/name: app1 73 | template: 74 | metadata: 75 | labels: 76 | app.kubernetes.io/instance: chartsnap 77 | app.kubernetes.io/managed-by: Helm 78 | app.kubernetes.io/name: app1 79 | app.kubernetes.io/version: 1.15.0 80 | helm.sh/chart: app1-0.1.0 81 | spec: 82 | containers: 83 | - image: nginx:1.15.0 84 | imagePullPolicy: IfNotPresent 85 | livenessProbe: 86 | httpGet: 87 | path: / 88 | port: http 89 | name: app1 90 | ports: 91 | - containerPort: 80 92 | name: http 93 | protocol: TCP 94 | readinessProbe: 95 | httpGet: 96 | path: / 97 | port: http 98 | resources: {} 99 | securityContext: {} 100 | securityContext: {} 101 | serviceAccountName: chartsnap-app1 102 | --- 103 | apiVersion: autoscaling/v2 104 | kind: HorizontalPodAutoscaler 105 | metadata: 106 | labels: 107 | app.kubernetes.io/instance: chartsnap 108 | app.kubernetes.io/managed-by: Helm 109 | app.kubernetes.io/name: app1 110 | app.kubernetes.io/version: 1.15.0 111 | helm.sh/chart: app1-0.1.0 112 | name: chartsnap-app1 113 | spec: 114 | maxReplicas: 10 115 | metrics: 116 | - resource: 117 | name: cpu 118 | target: 119 | averageUtilization: 65 120 | type: Utilization 121 | type: Resource 122 | minReplicas: 1 123 | scaleTargetRef: 124 | apiVersion: apps/v1 125 | kind: Deployment 126 | name: chartsnap-app1 127 | --- 128 | apiVersion: v1 129 | kind: Pod 130 | metadata: 131 | annotations: 132 | helm.sh/hook: test 133 | labels: 134 | app.kubernetes.io/instance: chartsnap 135 | app.kubernetes.io/managed-by: Helm 136 | app.kubernetes.io/name: app1 137 | app.kubernetes.io/version: 1.15.0 138 | helm.sh/chart: app1-0.1.0 139 | name: chartsnap-app1-test-connection 140 | spec: 141 | containers: 142 | - args: 143 | - chartsnap-app1:80 144 | command: 145 | - wget 146 | image: busybox 147 | name: wget 148 | restartPolicy: Never 149 | -------------------------------------------------------------------------------- /pkg/charts/__snapshots__/helm_stub_snap_v2.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: helm-chartsnap.jlandowner.dev/v1alpha1 2 | kind: Unknown 3 | metadata: 4 | name: helm-output 5 | raw: | 6 | this is warning message of helm 7 | --- 8 | apiVersion: v1 9 | automountServiceAccountToken: true 10 | kind: ServiceAccount 11 | metadata: 12 | labels: 13 | app.kubernetes.io/instance: chartsnap 14 | app.kubernetes.io/managed-by: Helm 15 | app.kubernetes.io/name: app1 16 | app.kubernetes.io/version: 1.16.0 17 | helm.sh/chart: app1-0.1.0 18 | name: chartsnap-app1 19 | --- 20 | apiVersion: v1 21 | data: 22 | ca.crt: IyMjRFlOQU1JQ19GSUVMRCMjIw== 23 | tls.crt: IyMjRFlOQU1JQ19GSUVMRCMjIw== 24 | tls.key: IyMjRFlOQU1JQ19GSUVMRCMjIw== 25 | kind: Secret 26 | metadata: 27 | labels: 28 | app.kubernetes.io/instance: chartsnap 29 | app.kubernetes.io/managed-by: Helm 30 | app.kubernetes.io/name: app1 31 | app.kubernetes.io/version: 1.16.0 32 | helm.sh/chart: app1-0.1.0 33 | name: app1-cert 34 | namespace: default 35 | type: kubernetes.io/tls 36 | --- 37 | apiVersion: v1 38 | kind: Service 39 | metadata: 40 | labels: 41 | app.kubernetes.io/instance: chartsnap 42 | app.kubernetes.io/managed-by: Helm 43 | app.kubernetes.io/name: app1 44 | app.kubernetes.io/version: 1.16.0 45 | helm.sh/chart: app1-0.1.0 46 | name: chartsnap-app1 47 | spec: 48 | ports: 49 | - name: http 50 | port: 80 51 | protocol: TCP 52 | targetPort: http 53 | selector: 54 | app.kubernetes.io/instance: chartsnap 55 | app.kubernetes.io/name: app1 56 | type: '###DYNAMIC_FIELD###' 57 | --- 58 | apiVersion: apps/v1 59 | kind: Deployment 60 | metadata: 61 | labels: 62 | app.kubernetes.io/instance: chartsnap 63 | app.kubernetes.io/managed-by: Helm 64 | app.kubernetes.io/name: app1 65 | app.kubernetes.io/version: 1.16.0 66 | helm.sh/chart: app1-0.1.0 67 | name: chartsnap-app1 68 | spec: 69 | selector: 70 | matchLabels: 71 | app.kubernetes.io/instance: chartsnap 72 | app.kubernetes.io/name: app1 73 | template: 74 | metadata: 75 | labels: 76 | app.kubernetes.io/instance: chartsnap 77 | app.kubernetes.io/managed-by: Helm 78 | app.kubernetes.io/name: app1 79 | app.kubernetes.io/version: 1.16.0 80 | helm.sh/chart: app1-0.1.0 81 | spec: 82 | containers: 83 | - image: nginx:1.16.0 84 | imagePullPolicy: IfNotPresent 85 | livenessProbe: 86 | httpGet: 87 | path: / 88 | port: http 89 | name: app1 90 | ports: 91 | - containerPort: 80 92 | name: http 93 | protocol: TCP 94 | readinessProbe: 95 | httpGet: 96 | path: / 97 | port: http 98 | resources: {} 99 | securityContext: {} 100 | securityContext: {} 101 | serviceAccountName: chartsnap-app1 102 | --- 103 | apiVersion: autoscaling/v2 104 | kind: HorizontalPodAutoscaler 105 | metadata: 106 | labels: 107 | app.kubernetes.io/instance: chartsnap 108 | app.kubernetes.io/managed-by: Helm 109 | app.kubernetes.io/name: app1 110 | app.kubernetes.io/version: 1.16.0 111 | helm.sh/chart: app1-0.1.0 112 | name: chartsnap-app1 113 | spec: 114 | maxReplicas: 10 115 | metrics: 116 | - resource: 117 | name: cpu 118 | target: 119 | averageUtilization: 65 120 | type: Utilization 121 | type: Resource 122 | minReplicas: 1 123 | scaleTargetRef: 124 | apiVersion: apps/v1 125 | kind: Deployment 126 | name: chartsnap-app1 127 | --- 128 | apiVersion: v1 129 | kind: Pod 130 | metadata: 131 | annotations: 132 | helm.sh/hook: test 133 | labels: 134 | app.kubernetes.io/instance: chartsnap 135 | app.kubernetes.io/managed-by: Helm 136 | app.kubernetes.io/name: app1 137 | app.kubernetes.io/version: 1.16.0 138 | helm.sh/chart: app1-0.1.0 139 | name: chartsnap-app1-test-connection 140 | spec: 141 | containers: 142 | - args: 143 | - chartsnap-app1:80 144 | command: 145 | - wget 146 | image: busybox 147 | name: wget 148 | restartPolicy: Never 149 | -------------------------------------------------------------------------------- /example/app1/test_v2/__snapshots__/test_ingress_enabled.snap: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | automountServiceAccountToken: true 3 | kind: ServiceAccount 4 | metadata: 5 | labels: 6 | app.kubernetes.io/instance: chartsnap 7 | app.kubernetes.io/managed-by: Helm 8 | app.kubernetes.io/name: app1 9 | app.kubernetes.io/version: 1.16.0 10 | helm.sh/chart: app1-0.1.0 11 | name: chartsnap-app1 12 | --- 13 | apiVersion: v1 14 | data: 15 | ca.crt: IyMjRFlOQU1JQ19GSUVMRCMjIw== 16 | tls.crt: IyMjRFlOQU1JQ19GSUVMRCMjIw== 17 | tls.key: IyMjRFlOQU1JQ19GSUVMRCMjIw== 18 | kind: Secret 19 | metadata: 20 | labels: 21 | app.kubernetes.io/instance: chartsnap 22 | app.kubernetes.io/managed-by: Helm 23 | app.kubernetes.io/name: app1 24 | app.kubernetes.io/version: 1.16.0 25 | helm.sh/chart: app1-0.1.0 26 | name: app1-cert 27 | namespace: default 28 | type: kubernetes.io/tls 29 | --- 30 | apiVersion: v1 31 | kind: Service 32 | metadata: 33 | labels: 34 | app.kubernetes.io/instance: chartsnap 35 | app.kubernetes.io/managed-by: Helm 36 | app.kubernetes.io/name: app1 37 | app.kubernetes.io/version: 1.16.0 38 | helm.sh/chart: app1-0.1.0 39 | name: chartsnap-app1 40 | spec: 41 | ports: 42 | - name: http 43 | port: 80 44 | protocol: TCP 45 | targetPort: http 46 | selector: 47 | app.kubernetes.io/instance: chartsnap 48 | app.kubernetes.io/name: app1 49 | type: ClusterIP 50 | --- 51 | apiVersion: apps/v1 52 | kind: Deployment 53 | metadata: 54 | labels: 55 | app.kubernetes.io/instance: chartsnap 56 | app.kubernetes.io/managed-by: Helm 57 | app.kubernetes.io/name: app1 58 | app.kubernetes.io/version: 1.16.0 59 | helm.sh/chart: app1-0.1.0 60 | name: chartsnap-app1 61 | spec: 62 | replicas: 1 63 | selector: 64 | matchLabels: 65 | app.kubernetes.io/instance: chartsnap 66 | app.kubernetes.io/name: app1 67 | template: 68 | metadata: 69 | labels: 70 | app.kubernetes.io/instance: chartsnap 71 | app.kubernetes.io/managed-by: Helm 72 | app.kubernetes.io/name: app1 73 | app.kubernetes.io/version: 1.16.0 74 | helm.sh/chart: app1-0.1.0 75 | spec: 76 | containers: 77 | - image: nginx:1.16.0 78 | imagePullPolicy: IfNotPresent 79 | livenessProbe: 80 | httpGet: 81 | path: / 82 | port: http 83 | name: app1 84 | ports: 85 | - containerPort: 80 86 | name: http 87 | protocol: TCP 88 | readinessProbe: 89 | httpGet: 90 | path: / 91 | port: http 92 | resources: {} 93 | securityContext: {} 94 | securityContext: {} 95 | serviceAccountName: chartsnap-app1 96 | --- 97 | apiVersion: networking.k8s.io/v1 98 | kind: Ingress 99 | metadata: 100 | annotations: 101 | cert-manager.io/cluster-issuer: nameOfClusterIssuer 102 | labels: 103 | app.kubernetes.io/instance: chartsnap 104 | app.kubernetes.io/managed-by: Helm 105 | app.kubernetes.io/name: app1 106 | app.kubernetes.io/version: 1.16.0 107 | helm.sh/chart: app1-0.1.0 108 | name: chartsnap-app1 109 | spec: 110 | ingressClassName: nginx 111 | rules: 112 | - host: chart-example.local 113 | http: 114 | paths: 115 | - backend: 116 | service: 117 | name: chartsnap-app1 118 | port: 119 | number: 80 120 | path: / 121 | pathType: ImplementationSpecific 122 | tls: 123 | - hosts: 124 | - chart-example.local 125 | secretName: chart-example-tls 126 | --- 127 | apiVersion: v1 128 | kind: Pod 129 | metadata: 130 | annotations: 131 | helm.sh/hook: test 132 | labels: 133 | app.kubernetes.io/instance: chartsnap 134 | app.kubernetes.io/managed-by: Helm 135 | app.kubernetes.io/name: app1 136 | app.kubernetes.io/version: 1.16.0 137 | helm.sh/chart: app1-0.1.0 138 | name: chartsnap-app1-test-connection 139 | spec: 140 | containers: 141 | - args: 142 | - chartsnap-app1:80 143 | command: 144 | - wget 145 | image: busybox 146 | name: wget 147 | restartPolicy: Never 148 | -------------------------------------------------------------------------------- /example/app1/test_v3/__snapshots__/test_hpa_enabled.snap: -------------------------------------------------------------------------------- 1 | # chartsnap: snapshot_version=v3 2 | --- 3 | # Source: app1/templates/serviceaccount.yaml 4 | apiVersion: v1 5 | kind: ServiceAccount 6 | metadata: 7 | name: chartsnap-app1 8 | labels: 9 | helm.sh/chart: app1-0.1.0 10 | app.kubernetes.io/name: app1 11 | app.kubernetes.io/instance: chartsnap 12 | app.kubernetes.io/version: "1.16.0" 13 | app.kubernetes.io/managed-by: Helm 14 | automountServiceAccountToken: true 15 | --- 16 | # Source: app1/templates/cert.yaml 17 | apiVersion: v1 18 | kind: Secret 19 | metadata: 20 | name: app1-cert 21 | namespace: default 22 | labels: 23 | helm.sh/chart: app1-0.1.0 24 | app.kubernetes.io/name: app1 25 | app.kubernetes.io/instance: chartsnap 26 | app.kubernetes.io/version: "1.16.0" 27 | app.kubernetes.io/managed-by: Helm 28 | type: kubernetes.io/tls 29 | data: 30 | ca.crt: IyMjRFlOQU1JQ19GSUVMRCMjIw== 31 | tls.crt: IyMjRFlOQU1JQ19GSUVMRCMjIw== 32 | tls.key: IyMjRFlOQU1JQ19GSUVMRCMjIw== 33 | --- 34 | # Source: app1/templates/service.yaml 35 | apiVersion: v1 36 | kind: Service 37 | metadata: 38 | name: chartsnap-app1 39 | labels: 40 | helm.sh/chart: app1-0.1.0 41 | app.kubernetes.io/name: app1 42 | app.kubernetes.io/instance: chartsnap 43 | app.kubernetes.io/version: "1.16.0" 44 | app.kubernetes.io/managed-by: Helm 45 | spec: 46 | type: ClusterIP 47 | ports: 48 | - port: 80 49 | targetPort: http 50 | protocol: TCP 51 | name: http 52 | selector: 53 | app.kubernetes.io/name: app1 54 | app.kubernetes.io/instance: chartsnap 55 | --- 56 | # Source: app1/templates/deployment.yaml 57 | apiVersion: apps/v1 58 | kind: Deployment 59 | metadata: 60 | name: chartsnap-app1 61 | labels: 62 | helm.sh/chart: app1-0.1.0 63 | app.kubernetes.io/name: app1 64 | app.kubernetes.io/instance: chartsnap 65 | app.kubernetes.io/version: "1.16.0" 66 | app.kubernetes.io/managed-by: Helm 67 | spec: 68 | selector: 69 | matchLabels: 70 | app.kubernetes.io/name: app1 71 | app.kubernetes.io/instance: chartsnap 72 | template: 73 | metadata: 74 | labels: 75 | helm.sh/chart: app1-0.1.0 76 | app.kubernetes.io/name: app1 77 | app.kubernetes.io/instance: chartsnap 78 | app.kubernetes.io/version: "1.16.0" 79 | app.kubernetes.io/managed-by: Helm 80 | spec: 81 | serviceAccountName: chartsnap-app1 82 | securityContext: {} 83 | containers: 84 | - name: app1 85 | securityContext: {} 86 | image: "nginx:1.16.0" 87 | imagePullPolicy: IfNotPresent 88 | ports: 89 | - name: http 90 | containerPort: 80 91 | protocol: TCP 92 | livenessProbe: 93 | httpGet: 94 | path: / 95 | port: http 96 | readinessProbe: 97 | httpGet: 98 | path: / 99 | port: http 100 | resources: {} 101 | --- 102 | # Source: app1/templates/hpa.yaml 103 | apiVersion: autoscaling/v2 104 | kind: HorizontalPodAutoscaler 105 | metadata: 106 | name: chartsnap-app1 107 | labels: 108 | helm.sh/chart: app1-0.1.0 109 | app.kubernetes.io/name: app1 110 | app.kubernetes.io/instance: chartsnap 111 | app.kubernetes.io/version: "1.16.0" 112 | app.kubernetes.io/managed-by: Helm 113 | spec: 114 | scaleTargetRef: 115 | apiVersion: apps/v1 116 | kind: Deployment 117 | name: chartsnap-app1 118 | minReplicas: 1 119 | maxReplicas: 10 120 | metrics: 121 | - type: Resource 122 | resource: 123 | name: cpu 124 | target: 125 | type: Utilization 126 | averageUtilization: 65 127 | --- 128 | # Source: app1/templates/tests/test-connection.yaml 129 | apiVersion: v1 130 | kind: Pod 131 | metadata: 132 | name: "chartsnap-app1-test-connection" 133 | labels: 134 | helm.sh/chart: app1-0.1.0 135 | app.kubernetes.io/name: app1 136 | app.kubernetes.io/instance: chartsnap 137 | app.kubernetes.io/version: "1.16.0" 138 | app.kubernetes.io/managed-by: Helm 139 | annotations: 140 | "helm.sh/hook": test 141 | spec: 142 | containers: 143 | - name: wget 144 | image: busybox 145 | command: ['wget'] 146 | args: ['chartsnap-app1:80'] 147 | restartPolicy: Never 148 | -------------------------------------------------------------------------------- /example/app1/test_latest/__snapshots__/test_hpa_enabled.snap: -------------------------------------------------------------------------------- 1 | # chartsnap: snapshot_version=v3 2 | --- 3 | # Source: app1/templates/serviceaccount.yaml 4 | apiVersion: v1 5 | kind: ServiceAccount 6 | metadata: 7 | name: chartsnap-app1 8 | labels: 9 | helm.sh/chart: app1-0.1.0 10 | app.kubernetes.io/name: app1 11 | app.kubernetes.io/instance: chartsnap 12 | app.kubernetes.io/version: "1.16.0" 13 | app.kubernetes.io/managed-by: Helm 14 | automountServiceAccountToken: true 15 | --- 16 | # Source: app1/templates/cert.yaml 17 | apiVersion: v1 18 | kind: Secret 19 | metadata: 20 | name: app1-cert 21 | namespace: default 22 | labels: 23 | helm.sh/chart: app1-0.1.0 24 | app.kubernetes.io/name: app1 25 | app.kubernetes.io/instance: chartsnap 26 | app.kubernetes.io/version: "1.16.0" 27 | app.kubernetes.io/managed-by: Helm 28 | type: kubernetes.io/tls 29 | data: 30 | ca.crt: IyMjRFlOQU1JQ19GSUVMRCMjIw== 31 | tls.crt: IyMjRFlOQU1JQ19GSUVMRCMjIw== 32 | tls.key: IyMjRFlOQU1JQ19GSUVMRCMjIw== 33 | --- 34 | # Source: app1/templates/service.yaml 35 | apiVersion: v1 36 | kind: Service 37 | metadata: 38 | name: chartsnap-app1 39 | labels: 40 | helm.sh/chart: app1-0.1.0 41 | app.kubernetes.io/name: app1 42 | app.kubernetes.io/instance: chartsnap 43 | app.kubernetes.io/version: "1.16.0" 44 | app.kubernetes.io/managed-by: Helm 45 | spec: 46 | type: ClusterIP 47 | ports: 48 | - port: 80 49 | targetPort: http 50 | protocol: TCP 51 | name: http 52 | selector: 53 | app.kubernetes.io/name: app1 54 | app.kubernetes.io/instance: chartsnap 55 | --- 56 | # Source: app1/templates/deployment.yaml 57 | apiVersion: apps/v1 58 | kind: Deployment 59 | metadata: 60 | name: chartsnap-app1 61 | labels: 62 | helm.sh/chart: app1-0.1.0 63 | app.kubernetes.io/name: app1 64 | app.kubernetes.io/instance: chartsnap 65 | app.kubernetes.io/version: "1.16.0" 66 | app.kubernetes.io/managed-by: Helm 67 | spec: 68 | selector: 69 | matchLabels: 70 | app.kubernetes.io/name: app1 71 | app.kubernetes.io/instance: chartsnap 72 | template: 73 | metadata: 74 | labels: 75 | helm.sh/chart: app1-0.1.0 76 | app.kubernetes.io/name: app1 77 | app.kubernetes.io/instance: chartsnap 78 | app.kubernetes.io/version: "1.16.0" 79 | app.kubernetes.io/managed-by: Helm 80 | spec: 81 | serviceAccountName: chartsnap-app1 82 | securityContext: {} 83 | containers: 84 | - name: app1 85 | securityContext: {} 86 | image: "nginx:1.16.0" 87 | imagePullPolicy: IfNotPresent 88 | ports: 89 | - name: http 90 | containerPort: 80 91 | protocol: TCP 92 | livenessProbe: 93 | httpGet: 94 | path: / 95 | port: http 96 | readinessProbe: 97 | httpGet: 98 | path: / 99 | port: http 100 | resources: {} 101 | --- 102 | # Source: app1/templates/hpa.yaml 103 | apiVersion: autoscaling/v2 104 | kind: HorizontalPodAutoscaler 105 | metadata: 106 | name: chartsnap-app1 107 | labels: 108 | helm.sh/chart: app1-0.1.0 109 | app.kubernetes.io/name: app1 110 | app.kubernetes.io/instance: chartsnap 111 | app.kubernetes.io/version: "1.16.0" 112 | app.kubernetes.io/managed-by: Helm 113 | spec: 114 | scaleTargetRef: 115 | apiVersion: apps/v1 116 | kind: Deployment 117 | name: chartsnap-app1 118 | minReplicas: 1 119 | maxReplicas: 10 120 | metrics: 121 | - type: Resource 122 | resource: 123 | name: cpu 124 | target: 125 | type: Utilization 126 | averageUtilization: 65 127 | --- 128 | # Source: app1/templates/tests/test-connection.yaml 129 | apiVersion: v1 130 | kind: Pod 131 | metadata: 132 | name: "chartsnap-app1-test-connection" 133 | labels: 134 | helm.sh/chart: app1-0.1.0 135 | app.kubernetes.io/name: app1 136 | app.kubernetes.io/instance: chartsnap 137 | app.kubernetes.io/version: "1.16.0" 138 | app.kubernetes.io/managed-by: Helm 139 | annotations: 140 | "helm.sh/hook": test 141 | spec: 142 | containers: 143 | - name: wget 144 | image: busybox 145 | command: ['wget'] 146 | args: ['chartsnap-app1:80'] 147 | restartPolicy: Never 148 | -------------------------------------------------------------------------------- /example/app1/test_latest/__snapshots__/test_hpa_enabled_20.snap: -------------------------------------------------------------------------------- 1 | # chartsnap: snapshot_version=v3 2 | --- 3 | # Source: app1/templates/serviceaccount.yaml 4 | apiVersion: v1 5 | kind: ServiceAccount 6 | metadata: 7 | name: chartsnap-app1 8 | labels: 9 | helm.sh/chart: app1-0.1.0 10 | app.kubernetes.io/name: app1 11 | app.kubernetes.io/instance: chartsnap 12 | app.kubernetes.io/version: "1.16.0" 13 | app.kubernetes.io/managed-by: Helm 14 | automountServiceAccountToken: true 15 | --- 16 | # Source: app1/templates/cert.yaml 17 | apiVersion: v1 18 | kind: Secret 19 | metadata: 20 | name: app1-cert 21 | namespace: default 22 | labels: 23 | helm.sh/chart: app1-0.1.0 24 | app.kubernetes.io/name: app1 25 | app.kubernetes.io/instance: chartsnap 26 | app.kubernetes.io/version: "1.16.0" 27 | app.kubernetes.io/managed-by: Helm 28 | type: kubernetes.io/tls 29 | data: 30 | ca.crt: IyMjRFlOQU1JQ19GSUVMRCMjIw== 31 | tls.crt: IyMjRFlOQU1JQ19GSUVMRCMjIw== 32 | tls.key: IyMjRFlOQU1JQ19GSUVMRCMjIw== 33 | --- 34 | # Source: app1/templates/service.yaml 35 | apiVersion: v1 36 | kind: Service 37 | metadata: 38 | name: chartsnap-app1 39 | labels: 40 | helm.sh/chart: app1-0.1.0 41 | app.kubernetes.io/name: app1 42 | app.kubernetes.io/instance: chartsnap 43 | app.kubernetes.io/version: "1.16.0" 44 | app.kubernetes.io/managed-by: Helm 45 | spec: 46 | type: ClusterIP 47 | ports: 48 | - port: 80 49 | targetPort: http 50 | protocol: TCP 51 | name: http 52 | selector: 53 | app.kubernetes.io/name: app1 54 | app.kubernetes.io/instance: chartsnap 55 | --- 56 | # Source: app1/templates/deployment.yaml 57 | apiVersion: apps/v1 58 | kind: Deployment 59 | metadata: 60 | name: chartsnap-app1 61 | labels: 62 | helm.sh/chart: app1-0.1.0 63 | app.kubernetes.io/name: app1 64 | app.kubernetes.io/instance: chartsnap 65 | app.kubernetes.io/version: "1.16.0" 66 | app.kubernetes.io/managed-by: Helm 67 | spec: 68 | selector: 69 | matchLabels: 70 | app.kubernetes.io/name: app1 71 | app.kubernetes.io/instance: chartsnap 72 | template: 73 | metadata: 74 | labels: 75 | helm.sh/chart: app1-0.1.0 76 | app.kubernetes.io/name: app1 77 | app.kubernetes.io/instance: chartsnap 78 | app.kubernetes.io/version: "1.16.0" 79 | app.kubernetes.io/managed-by: Helm 80 | spec: 81 | serviceAccountName: chartsnap-app1 82 | securityContext: {} 83 | containers: 84 | - name: app1 85 | securityContext: {} 86 | image: "nginx:1.16.0" 87 | imagePullPolicy: IfNotPresent 88 | ports: 89 | - name: http 90 | containerPort: 80 91 | protocol: TCP 92 | livenessProbe: 93 | httpGet: 94 | path: / 95 | port: http 96 | readinessProbe: 97 | httpGet: 98 | path: / 99 | port: http 100 | resources: {} 101 | --- 102 | # Source: app1/templates/hpa.yaml 103 | apiVersion: autoscaling/v2 104 | kind: HorizontalPodAutoscaler 105 | metadata: 106 | name: chartsnap-app1 107 | labels: 108 | helm.sh/chart: app1-0.1.0 109 | app.kubernetes.io/name: app1 110 | app.kubernetes.io/instance: chartsnap 111 | app.kubernetes.io/version: "1.16.0" 112 | app.kubernetes.io/managed-by: Helm 113 | spec: 114 | scaleTargetRef: 115 | apiVersion: apps/v1 116 | kind: Deployment 117 | name: chartsnap-app1 118 | minReplicas: 1 119 | maxReplicas: 20 120 | metrics: 121 | - type: Resource 122 | resource: 123 | name: cpu 124 | target: 125 | type: Utilization 126 | averageUtilization: 65 127 | --- 128 | # Source: app1/templates/tests/test-connection.yaml 129 | apiVersion: v1 130 | kind: Pod 131 | metadata: 132 | name: "chartsnap-app1-test-connection" 133 | labels: 134 | helm.sh/chart: app1-0.1.0 135 | app.kubernetes.io/name: app1 136 | app.kubernetes.io/instance: chartsnap 137 | app.kubernetes.io/version: "1.16.0" 138 | app.kubernetes.io/managed-by: Helm 139 | annotations: 140 | "helm.sh/hook": test 141 | spec: 142 | containers: 143 | - name: wget 144 | image: busybox 145 | command: ['wget'] 146 | args: ['chartsnap-app1:80'] 147 | restartPolicy: Never 148 | -------------------------------------------------------------------------------- /example/app1/test_wildcard/__snapshots__/wildcard_test.snap: -------------------------------------------------------------------------------- 1 | # chartsnap: snapshot_version=v3 2 | --- 3 | # Source: app1/templates/serviceaccount.yaml 4 | apiVersion: v1 5 | kind: ServiceAccount 6 | metadata: 7 | name: chartsnap-app1 8 | labels: 9 | helm.sh/chart: '###CHART_VERSION###' 10 | app.kubernetes.io/name: app1 11 | app.kubernetes.io/instance: chartsnap 12 | app.kubernetes.io/version: "1.16.0" 13 | app.kubernetes.io/managed-by: Helm 14 | automountServiceAccountToken: true 15 | --- 16 | # Source: app1/templates/cert.yaml 17 | apiVersion: v1 18 | kind: Secret 19 | metadata: 20 | name: app1-cert 21 | namespace: default 22 | labels: 23 | helm.sh/chart: '###CHART_VERSION###' 24 | app.kubernetes.io/name: app1 25 | app.kubernetes.io/instance: chartsnap 26 | app.kubernetes.io/version: "1.16.0" 27 | app.kubernetes.io/managed-by: Helm 28 | type: kubernetes.io/tls 29 | data: 30 | ca.crt: '###CUSTOM_CERT_DATA###' 31 | tls.crt: '###CUSTOM_CERT_DATA###' 32 | tls.key: '###CUSTOM_CERT_DATA###' 33 | --- 34 | # Source: app1/templates/service.yaml 35 | apiVersion: v1 36 | kind: Service 37 | metadata: 38 | name: chartsnap-app1 39 | labels: 40 | helm.sh/chart: '###CHART_VERSION###' 41 | app.kubernetes.io/name: app1 42 | app.kubernetes.io/instance: chartsnap 43 | app.kubernetes.io/version: "1.16.0" 44 | app.kubernetes.io/managed-by: Helm 45 | spec: 46 | type: ClusterIP 47 | ports: 48 | - port: 80 49 | targetPort: http 50 | protocol: TCP 51 | name: http 52 | selector: 53 | app.kubernetes.io/name: app1 54 | app.kubernetes.io/instance: chartsnap 55 | --- 56 | # Source: app1/templates/deployment.yaml 57 | apiVersion: apps/v1 58 | kind: Deployment 59 | metadata: 60 | name: chartsnap-app1 61 | labels: 62 | helm.sh/chart: '###CHART_VERSION###' 63 | app.kubernetes.io/name: app1 64 | app.kubernetes.io/instance: chartsnap 65 | app.kubernetes.io/version: "1.16.0" 66 | app.kubernetes.io/managed-by: Helm 67 | spec: 68 | selector: 69 | matchLabels: 70 | app.kubernetes.io/name: app1 71 | app.kubernetes.io/instance: chartsnap 72 | template: 73 | metadata: 74 | labels: 75 | helm.sh/chart: app1-0.1.0 76 | app.kubernetes.io/name: app1 77 | app.kubernetes.io/instance: chartsnap 78 | app.kubernetes.io/version: "1.16.0" 79 | app.kubernetes.io/managed-by: Helm 80 | spec: 81 | serviceAccountName: chartsnap-app1 82 | securityContext: {} 83 | containers: 84 | - name: app1 85 | securityContext: {} 86 | image: "nginx:1.16.0" 87 | imagePullPolicy: IfNotPresent 88 | ports: 89 | - name: http 90 | containerPort: 80 91 | protocol: TCP 92 | livenessProbe: 93 | httpGet: 94 | path: / 95 | port: http 96 | readinessProbe: 97 | httpGet: 98 | path: / 99 | port: http 100 | resources: {} 101 | --- 102 | # Source: app1/templates/hpa.yaml 103 | apiVersion: autoscaling/v2 104 | kind: HorizontalPodAutoscaler 105 | metadata: 106 | name: chartsnap-app1 107 | labels: 108 | helm.sh/chart: '###CHART_VERSION###' 109 | app.kubernetes.io/name: app1 110 | app.kubernetes.io/instance: chartsnap 111 | app.kubernetes.io/version: "1.16.0" 112 | app.kubernetes.io/managed-by: Helm 113 | spec: 114 | scaleTargetRef: 115 | apiVersion: apps/v1 116 | kind: Deployment 117 | name: chartsnap-app1 118 | minReplicas: 1 119 | maxReplicas: 10 120 | metrics: 121 | - type: Resource 122 | resource: 123 | name: cpu 124 | target: 125 | type: Utilization 126 | averageUtilization: 65 127 | --- 128 | # Source: app1/templates/tests/test-connection.yaml 129 | apiVersion: v1 130 | kind: Pod 131 | metadata: 132 | name: "chartsnap-app1-test-connection" 133 | labels: 134 | helm.sh/chart: '###CHART_VERSION###' 135 | app.kubernetes.io/name: app1 136 | app.kubernetes.io/instance: chartsnap 137 | app.kubernetes.io/version: "1.16.0" 138 | app.kubernetes.io/managed-by: Helm 139 | annotations: 140 | "helm.sh/hook": test 141 | spec: 142 | containers: 143 | - name: wget 144 | image: busybox 145 | command: ['wget'] 146 | args: ['chartsnap-app1:80'] 147 | restartPolicy: Never 148 | --------------------------------------------------------------------------------