From 61691312b731591af208b9dc1e415d926e3d0bad Mon Sep 17 00:00:00 2001 From: jlandowner Date: Sat, 6 Apr 2024 15:52:18 +0900 Subject: [PATCH] Snapshot V2 --- example/app1/__snapshots__/default.snap | 17 + example/app1/test/.chartsnap.yaml | 1 + .../test_certmanager_enabled.snap | 238 ++++--- .../test/__snapshots__/test_hpa_enabled.snap | 280 ++++---- .../__snapshots__/test_ingress_enabled.snap | 292 +++++---- example/app1/test/test_ingress_enabled.yaml | 1 + example/app1/testfail/.chartsnap.yaml | 1 + .../test_certmanager_enabled.snap | 238 ++++--- .../__snapshots__/test_hpa_enabled.snap | 280 ++++---- .../__snapshots__/test_ingress_enabled.snap | 276 ++++---- .../app1/testfail/test_ingress_enabled.yaml | 1 + hack/helm-template-help-snapshot/main.go | 2 +- main.go | 70 +- pkg/charts/__snapshots__/helm_stub_snap.yaml | 146 +++++ .../__snapshots__/helm_stub_snap_unmatch.yaml | 146 +++++ .../__snapshots__/helm_stub_snap_v1.toml} | 135 ++-- pkg/charts/__snapshots__/helm_test.snap | 27 + pkg/charts/__snapshots__/snap_test.snap | 94 +++ pkg/charts/__snapshots__/testspec_test.snap | 292 ++++++++- pkg/charts/helm_test.go | 106 ++-- pkg/charts/snap.go | 120 +++- pkg/charts/snap_test.go | 101 ++- pkg/charts/testdata/.chartsnap.yaml | 9 + pkg/charts/testdata/helm_cmd.bash | 8 + pkg/charts/testdata/helm_stub.bash | 149 +++++ pkg/charts/testdata/snap_values.yaml | 10 + pkg/charts/testdata/testspec_values.yaml | 10 + pkg/charts/testspec.go | 39 +- pkg/charts/testspec_test.go | 238 ++++--- pkg/snap/__snapshot__/json.snap | 7 + pkg/snap/__snapshot__/multi.snap | 133 ++++ .../single.snap} | 108 ++-- pkg/snap/__snapshots__/snapshot_test.snap | 27 - pkg/snap/cachefs.go | 66 ++ pkg/snap/cachefs_test.go | 106 ++++ pkg/snap/diff.go | 100 --- pkg/snap/diff_test.go | 226 ------- pkg/snap/gomega/__snapshots__/snap_test.snap | 600 ++++++++++++++++++ pkg/snap/{ => gomega}/object_snapshot.go | 2 +- pkg/snap/gomega/snap.go | 37 ++ pkg/snap/gomega/snap_test.go | 34 + pkg/snap/gomega/testdata/pod.yaml | 235 +++++++ pkg/snap/object_snapshot_test.go | 45 -- pkg/snap/snapshot.go | 240 +++---- pkg/snap/snapshot_create_test.go | 30 - pkg/snap/snapshot_test.go | 240 ++++++- pkg/snap/testdata/diff.snap | 79 --- pkg/snap/testdata/diff.txt | 76 --- pkg/snap/testdata/diff_diff.txt | 76 --- pkg/snap/unstructured_test.go | 220 ------- .../__snapshots__/suite_test.snap | 275 ++++++++ pkg/unstructured/diff.go | 181 ++++++ pkg/unstructured/diff_test.go | 235 +++++++ pkg/unstructured/suite_test.go | 64 ++ pkg/unstructured/testdata/actual.snap | 145 +++++ .../testdata/expected.snap} | 123 ++-- pkg/unstructured/unknown.go | 4 +- pkg/unstructured/unknown_test.go | 27 + pkg/unstructured/unstructured.go | 44 +- pkg/unstructured/unstructured_test.go | 19 +- .../v1/legacy.go} | 73 ++- 61 files changed, 4908 insertions(+), 2296 deletions(-) create mode 100644 pkg/charts/__snapshots__/helm_stub_snap.yaml create mode 100644 pkg/charts/__snapshots__/helm_stub_snap_unmatch.yaml rename pkg/{snap/testdata/unstructured.snap => charts/__snapshots__/helm_stub_snap_v1.toml} (58%) create mode 100644 pkg/charts/__snapshots__/helm_test.snap create mode 100644 pkg/charts/__snapshots__/snap_test.snap create mode 100644 pkg/charts/testdata/.chartsnap.yaml create mode 100755 pkg/charts/testdata/helm_cmd.bash create mode 100755 pkg/charts/testdata/helm_stub.bash create mode 100644 pkg/charts/testdata/snap_values.yaml create mode 100644 pkg/charts/testdata/testspec_values.yaml create mode 100644 pkg/snap/__snapshot__/json.snap create mode 100644 pkg/snap/__snapshot__/multi.snap rename pkg/snap/{testdata/unstructured.yaml => __snapshot__/single.snap} (72%) delete mode 100644 pkg/snap/__snapshots__/snapshot_test.snap create mode 100644 pkg/snap/cachefs.go create mode 100644 pkg/snap/cachefs_test.go delete mode 100644 pkg/snap/diff.go delete mode 100644 pkg/snap/diff_test.go create mode 100644 pkg/snap/gomega/__snapshots__/snap_test.snap rename pkg/snap/{ => gomega}/object_snapshot.go (97%) create mode 100644 pkg/snap/gomega/snap.go create mode 100644 pkg/snap/gomega/snap_test.go create mode 100644 pkg/snap/gomega/testdata/pod.yaml delete mode 100644 pkg/snap/object_snapshot_test.go delete mode 100644 pkg/snap/snapshot_create_test.go delete mode 100644 pkg/snap/testdata/diff.snap delete mode 100644 pkg/snap/testdata/diff.txt delete mode 100644 pkg/snap/testdata/diff_diff.txt delete mode 100644 pkg/snap/unstructured_test.go create mode 100644 pkg/unstructured/__snapshots__/suite_test.snap create mode 100644 pkg/unstructured/diff.go create mode 100644 pkg/unstructured/diff_test.go create mode 100644 pkg/unstructured/suite_test.go create mode 100644 pkg/unstructured/testdata/actual.snap rename pkg/{snap/testdata/unstructured_diff.yaml => unstructured/testdata/expected.snap} (61%) create mode 100644 pkg/unstructured/unknown_test.go rename pkg/{snap/unstructured.go => unstructured/v1/legacy.go} (60%) diff --git a/example/app1/__snapshots__/default.snap b/example/app1/__snapshots__/default.snap index 884b298..8910262 100644 --- a/example/app1/__snapshots__/default.snap +++ b/example/app1/__snapshots__/default.snap @@ -68,6 +68,23 @@ SnapShot = """ image: busybox name: wget restartPolicy: Never +- object: + apiVersion: v1 + data: + ca.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURFRENDQWZpZ0F3SUJBZ0lSQUxFK1NwRWlFZ1RBMWZhRmkzTW5yYVV3RFFZSktvWklodmNOQVFFTEJRQXcKRWpFUU1BNEdBMVVFQXhNSFlYQndNUzFqWVRBZUZ3MHlOREEwTURjeE56TTNNakJhRncwek5EQTBNRFV4TnpNMwpNakJhTUJJeEVEQU9CZ05WQkFNVEIyRndjREV0WTJFd2dnRWlNQTBHQ1NxR1NJYjNEUUVCQVFVQUE0SUJEd0F3CmdnRUtBb0lCQVFEV1YrMnBPSTVYdjAxbGJkZnBRVWsxaTBJTTEvNXkrTjVxRUtndG5TcStQZHhzMmpzTGgxcWEKWVlzcEtHbFUrMTUwNkExZUJrTmFzZXVuWStldEtvU1gwbzN6S2VoTkRscE9qRERqbXY3TTRUTTNudHlaYXFKUAp2R0JMREhLZjc2Mk9QdWZBakY2ZUxKRy9CcHlsR2NkZitMdjM1MHRUUlZaWmY0NUV5MStFUGlQR0hrVjRPVmd5Clo5ZkxHd0FTcFlzZGUzL2IvRWxQL3BmSXNwalRlU1pVSVpDRFVENlc2Tng2d05IVTBWQWhBQ1FHOHBXOENTMDMKc0lhVklFL2pTYXhvM2MrL2hvYWJWWlBoNnV0ME9lR0hLRGVManZBbTdBbjBtTlAybXJUT2VEbTlwdlhWYkR4VQpjZ0ErQ2lwMFc2cWRFS0pXVkFiSVY2YVM4WHJrc1YrL0FnTUJBQUdqWVRCZk1BNEdBMVVkRHdFQi93UUVBd0lDCnBEQWRCZ05WSFNVRUZqQVVCZ2dyQmdFRkJRY0RBUVlJS3dZQkJRVUhBd0l3RHdZRFZSMFRBUUgvQkFVd0F3RUIKL3pBZEJnTlZIUTRFRmdRVVVZaWhETmlUWXVDY28zSWhaU2daaXY4VDRVTXdEUVlKS29aSWh2Y05BUUVMQlFBRApnZ0VCQUFZdmFHalFKTmR2bDdEWGUzUFhwMDc1NFUyOHF3UGlkYU42OG9nVVdFUUQ2a0RidEg5SzF0ODIydC9OCjhiYUNINms1MEZjVmZaT0dyMW8yMnV2UjEraFB3U3U4NC82R2ZlVUR3L2dNL3NXdENZaE1CZW5DRThha0lHQWYKQ3Y2bi9YSzRoVzBBRUUzZWRodDVPcStUcUVmMFAxN3V6Rzd6b0JHeTFRSHhnbEVwcmpvNlU5OHNGelpMKzUzRwpZSTdUa0o3bHYrak1SQ0w5VVdLTU9ocGF4aTYzRzRDQU9STGhCcDdsR0E5MVYxTHRrbFJ6T0VnNmwrdm5rTFBNCi9UZnJiMjhvNmFxcXJUSjUycUd4NjJsRDkrcWhwOWl2dUtaYUlZcHlQbFI0VWJFZUFEUW44Q0w4Nkt0WTRRQTUKckd4VnA3RnVicjlIQUs0YzJUMkROdlcrOVBjPQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg== + tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURhRENDQWxDZ0F3SUJBZ0lRVUtTTUpJbkl3aDkvREFaWUxCaGVSREFOQmdrcWhraUc5dzBCQVFzRkFEQVMKTVJBd0RnWURWUVFERXdkaGNIQXhMV05oTUI0WERUSTBNRFF3TnpFM016Y3lNVm9YRFRNME1EUXdOVEUzTXpjeQpNVm93R1RFWE1CVUdBMVVFQXhNT1kyaGhjblJ6Ym1Gd0xXRndjREV3Z2dFaU1BMEdDU3FHU0liM0RRRUJBUVVBCkE0SUJEd0F3Z2dFS0FvSUJBUURBWWpvSDdNd3JCNXl4RUcwSFZ2QXFWUTJOTzVkSXZ1YmdHSGs4a3pGMEswOGYKcnVPZGN6QnlkUE9lSkVvZmRINzBoa2FMelQwb1JHWEtOYkpxbndvVjRobVJMbGZLMXNha1VSWXVoNFhSN3ZaUQpqNHdtTjFyS0c0YkliK0gvVFpWamxqSTR5b052Q2NkY0w5QlhMYkowWGgrbkxFSEc1dXMvVkdhdVVKUUp3RjhLCld3SEJJMXg4UDhESDJVeStyMWhzVy9ReE85a2UzU2RCZElEK3Z3Ly9IWWFmdzNtY0R1UFd3ZmkzN0lRSlFBcmkKV1lYZ0ZmZ2NWVzBibS9FTnpRN3BZMm1YdDdaYzlBTnBQWTdCeGs2aysvMDdnRkZ6SUFFblBNNms5aDl4UEpGOApNYnY5cnN1OVNFaFEyNmI1eFVYRnF4cEFIUHlwUVF4NW9qTTI1S2paQWdNQkFBR2pnYkl3Z2E4d0RnWURWUjBQCkFRSC9CQVFEQWdXZ01CMEdBMVVkSlFRV01CUUdDQ3NHQVFVRkJ3TUJCZ2dyQmdFRkJRY0RBakFNQmdOVkhSTUIKQWY4RUFqQUFNQjhHQTFVZEl3UVlNQmFBRkZHSW9RellrMkxnbktOeUlXVW9HWXIvRStGRE1FOEdBMVVkRVFSSQpNRWFDR21Ob1lYSjBjMjVoY0MxaGNIQXhMbVJsWm1GMWJIUXVjM1pqZ2loamFHRnlkSE51WVhBdFlYQndNUzVrClpXWmhkV3gwTG5OMll5NWpiSFZ6ZEdWeUxteHZZMkZzTUEwR0NTcUdTSWIzRFFFQkN3VUFBNElCQVFBRk8xeFEKdkh2dXA4bkFncit0emh5ZkcwNVlhaE51UThOV0UwZ2NLTjlMUDl6Y0pGR0NNay9FQWJFakNPQVhWeHFZY08zSwowdFJNc0hGN2l6aDZoeWJRTU43cmJ4NVhLeUxNTURCRmZ4UUhuRmc5ZzJXQUlXZE8zWFZzZTBLSEkxZUxiU0tHCm5Bb2hsVndQMEQrdWx3U3NPbEE5MHVUbEFINHBUa0E3Tzh1QTU3Yk9GQmptZDhnSktEeElYMHpXakI5V1I1czgKSHFxSDFva1E4b1FZcVh5MUN2bnlrbElnNnNsK0d5KytKQ3lCckZaZUdidXZmZm56WktOdktSUDNDVE5jRVB0eQoxelZ1cjZjSmRDcEM4RGhNN0p0VE4wQ0pKY2FHRWxHNHg1b2UyN0ZpY3loYi80SHhxTzFZdWVHK1hURTNuODRQCi9lZjc0eUlGWDdab0JjaGYKLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo= + tls.key: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFb3dJQkFBS0NBUUVBd0dJNkIrek1Ld2Vjc1JCdEIxYndLbFVOalR1WFNMN200Qmg1UEpNeGRDdFBINjdqCm5YTXdjblR6bmlSS0gzUis5SVpHaTgwOUtFUmx5ald5YXA4S0ZlSVprUzVYeXRiR3BGRVdMb2VGMGU3MlVJK00KSmpkYXlodUd5Ry9oLzAyVlk1WXlPTXFEYnduSFhDL1FWeTJ5ZEY0ZnB5eEJ4dWJyUDFSbXJsQ1VDY0JmQ2xzQgp3U05jZkQvQXg5bE12cTlZYkZ2ME1UdlpIdDBuUVhTQS9yOFAveDJHbjhONW5BN2oxc0g0dCt5RUNVQUs0bG1GCjRCWDRIRlZ0RzV2eERjME82V05wbDdlMlhQUURhVDJPd2NaT3BQdjlPNEJSY3lBQkp6ek9wUFlmY1R5UmZERzcKL2E3THZVaElVTnVtK2NWRnhhc2FRQno4cVVFTWVhSXpOdVNvMlFJREFRQUJBb0lCQVFDQkVpVys1V2NCUTRVago0bEpUeDBjd0Q0b2RCQ3IyZW1XcFdhSVZPdWZGK2J5SEZDM1BsOVdjSk16QmY3VmZMeWh4NDVoMitRYWIrbStVClg3eEkvbFNrNCtHbFhzTTE2aXl4VjFtYmMvOGJIc2lRdWc0Y0lhMCt4WU1DL05WU0ZQb3lLeldjbG5uaHlGekIKZzY2eW5vMEl0NUZpOWpWWFBkdjh6Q0pydHRIclM4TndJeFVKdUVYOGFEMlZkcmZWczdJZkV0MEN1K3Fodko0Qgp2amYwajRLV0tYcXhhdW04Q3pBZmsxVm9neUZtTkdkc1VNUEZ4bzVzUm54cFBQYVkyN3NZV0NnR2dkYTZ0Mm84ClhrMlNmVnlrY3YvcktzeEZ6aUZvRTc3cTAvQ2NDZnRkS3lKWldCdWVFTEZPRjY5elpWQlg3Z0NNM2FLb1hNaGYKdlh5MHRMWTFBb0dCQVBEVjk2RStyMmxIRW5LNndtdWQ1UktaMzA3cVNoVU94RXVncmNoRlhsTVByUEVrNzgydApWalJvdWppSDg2QWo0OUo4aVJYTjl5b2N6N2NtRndVWS9hbzgxQjQyTTJCOEpyRzBoNzF6YWFxTk05UjhpUGJiCkhuVkJkUVBkSnE2M0hsUXN4SS8rVTdLUlB6S3A3VWx0OE93b3c1Z0hoTlI0bndwNm83WUVJSlozQW9HQkFNeC8KUW4zZzJvZGJpMlNldWVuTGhvZ1FUVnRGdlZjRGRoZDJKK1EweThNc1Frd3ZCR1c5UXZhcXRSdjgvVHAwTVpNQgpkMDlMUTU0cHlXdUJXMDRURDVFV0JHNUxGWitVUmVrY0hkNDBJNEdTM0hpUmlWNU9ESTRnVlpJUmNKTklTM3RyCjUvcHc5U1VBZTZYQ3U0b2ZBeXNtVUdUeWlLTTIvL2Fwc1UvazNIOHZBb0dBV3NER1o4U3ZaUVNiTnhDWll1UkYKQmhWbHlOOFF1NDZzK2JLNnlkVWFEa0xCOEx6eWdKYm8vU2JaeGFPMWNvc1R0cVduSXNoU2MxUVlFZlFRaUtNNgpNNFJvaWxueVVsRjJZMUNjTmcvZnFaMDhBcjVLL25yanAxdmJOSEdKdWh6WEdQRWx3UDBkblJTT3RCREVrZjhUCjRtb2FDcGdLdmVZV1NHU2VmR0JoeDFFQ2dZQkVUUWpuUkMrWTRBR0pwTjRSY1ZISXBqRkFGK1hxWnhTTk40Q1IKWHZUamhpZktqRFdheVlEUkpDa0RaUmNxNjk0VzdIbHQrWVJuRWl2ZEJVSjZyREVaMDFHOWlNUjdIU25RZHZ5ZQoxNms0UU5YMFN4K25hTWdXdkVQNFdtelFOR2hKbTd2S1VPbi81czVsaWNuYmt3b2E0bHdkcTBmcHc1ZndTYk1ZCkxTZGNMd0tCZ0Voay83emdxK2tydnZCMkQreENPYjdVWkJqT1BuUVQ4eVdqRW1BWkVJNTNDTHVrRGkzckFwSnMKTDhUNTJDeFAvMHNkNWFkYzV2ZGhLVzduemdsUmNuMFI3a0hBSFhzdmRFbEJ4SVhXY0NjS29xblRjMk5OYjhsZQowejFvS0hWcnordTF2alAxUitaalMzUUdDWTNONGVXUVYwZkpaY21BRkZYSVkyM3daelpSCi0tLS0tRU5EIFJTQSBQUklWQVRFIEtFWS0tLS0tCg== + kind: Secret + metadata: + labels: + app.kubernetes.io/instance: chartsnap + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: app1 + app.kubernetes.io/version: 1.16.0 + helm.sh/chart: app1-0.1.0 + name: app1-cert + namespace: default + type: kubernetes.io/tls - object: apiVersion: v1 kind: Service diff --git a/example/app1/test/.chartsnap.yaml b/example/app1/test/.chartsnap.yaml index e35a096..0880502 100644 --- a/example/app1/test/.chartsnap.yaml +++ b/example/app1/test/.chartsnap.yaml @@ -7,3 +7,4 @@ dynamicFields: - /data/ca.crt - /data/tls.crt - /data/tls.key + base64: true diff --git a/example/app1/test/__snapshots__/test_certmanager_enabled.snap b/example/app1/test/__snapshots__/test_certmanager_enabled.snap index f8d6208..fdd48b4 100644 --- a/example/app1/test/__snapshots__/test_certmanager_enabled.snap +++ b/example/app1/test/__snapshots__/test_certmanager_enabled.snap @@ -1,124 +1,120 @@ -[test_certmanager_enabled] -SnapShot = """ -- object: - apiVersion: apps/v1 - kind: Deployment +apiVersion: v1 +automountServiceAccountToken: true +kind: ServiceAccount +metadata: + labels: + app.kubernetes.io/instance: chartsnap + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: app1 + app.kubernetes.io/version: 1.16.0 + helm.sh/chart: app1-0.1.0 + name: chartsnap-app1 +--- +apiVersion: v1 +kind: Service +metadata: + labels: + app.kubernetes.io/instance: chartsnap + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: app1 + app.kubernetes.io/version: 1.16.0 + helm.sh/chart: app1-0.1.0 + name: chartsnap-app1 +spec: + ports: + - name: http + port: 80 + protocol: TCP + targetPort: http + selector: + app.kubernetes.io/instance: chartsnap + app.kubernetes.io/name: app1 + type: ClusterIP +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app.kubernetes.io/instance: chartsnap + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: app1 + app.kubernetes.io/version: 1.16.0 + helm.sh/chart: app1-0.1.0 + name: chartsnap-app1 +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/instance: chartsnap + app.kubernetes.io/name: app1 + template: metadata: - labels: - app.kubernetes.io/instance: chartsnap - app.kubernetes.io/managed-by: Helm - app.kubernetes.io/name: app1 - app.kubernetes.io/version: 1.16.0 - helm.sh/chart: app1-0.1.0 - name: chartsnap-app1 + labels: + app.kubernetes.io/instance: chartsnap + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: app1 + app.kubernetes.io/version: 1.16.0 + helm.sh/chart: app1-0.1.0 spec: - replicas: 1 - selector: - matchLabels: - app.kubernetes.io/instance: chartsnap - app.kubernetes.io/name: app1 - template: - metadata: - labels: - app.kubernetes.io/instance: chartsnap - app.kubernetes.io/managed-by: Helm - app.kubernetes.io/name: app1 - app.kubernetes.io/version: 1.16.0 - helm.sh/chart: app1-0.1.0 - spec: - containers: - - image: nginx:1.16.0 - imagePullPolicy: IfNotPresent - livenessProbe: - httpGet: - path: / - port: http - name: app1 - ports: - - containerPort: 80 - name: http - protocol: TCP - readinessProbe: - httpGet: - path: / - port: http - resources: {} - securityContext: {} - securityContext: {} - serviceAccountName: chartsnap-app1 -- object: - apiVersion: cert-manager.io/v1 - kind: Certificate - metadata: - labels: - app.kubernetes.io/instance: chartsnap - app.kubernetes.io/managed-by: Helm - app.kubernetes.io/name: app1 - app.kubernetes.io/version: 1.16.0 - helm.sh/chart: app1-0.1.0 - name: app1-cert - namespace: default - spec: - dnsNames: - - chartsnap-app1.default.svc - - chartsnap-app1.default.svc.cluster.local - issuerRef: - kind: Issuer - name: nameOfClusterIssuer - secretName: app1-cert -- object: - apiVersion: v1 - kind: Pod - metadata: - annotations: - helm.sh/hook: test - labels: - app.kubernetes.io/instance: chartsnap - app.kubernetes.io/managed-by: Helm - app.kubernetes.io/name: app1 - app.kubernetes.io/version: 1.16.0 - helm.sh/chart: app1-0.1.0 - name: chartsnap-app1-test-connection - spec: - containers: - - args: - - chartsnap-app1:80 - command: - - wget - image: busybox - name: wget - restartPolicy: Never -- object: - apiVersion: v1 - kind: Service - metadata: - labels: - app.kubernetes.io/instance: chartsnap - app.kubernetes.io/managed-by: Helm - app.kubernetes.io/name: app1 - app.kubernetes.io/version: 1.16.0 - helm.sh/chart: app1-0.1.0 - name: chartsnap-app1 - spec: - ports: - - name: http - port: 80 + containers: + - image: nginx:1.16.0 + imagePullPolicy: IfNotPresent + livenessProbe: + httpGet: + path: / + port: http + name: app1 + ports: + - containerPort: 80 + name: http protocol: TCP - targetPort: http - selector: - app.kubernetes.io/instance: chartsnap - app.kubernetes.io/name: app1 - type: ClusterIP -- object: - apiVersion: v1 - automountServiceAccountToken: true - kind: ServiceAccount - metadata: - labels: - app.kubernetes.io/instance: chartsnap - app.kubernetes.io/managed-by: Helm - app.kubernetes.io/name: app1 - app.kubernetes.io/version: 1.16.0 - helm.sh/chart: app1-0.1.0 - name: chartsnap-app1 -""" + readinessProbe: + httpGet: + path: / + port: http + resources: {} + securityContext: {} + securityContext: {} + serviceAccountName: chartsnap-app1 +--- +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + labels: + app.kubernetes.io/instance: chartsnap + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: app1 + app.kubernetes.io/version: 1.16.0 + helm.sh/chart: app1-0.1.0 + name: app1-cert + namespace: default +spec: + dnsNames: + - chartsnap-app1.default.svc + - chartsnap-app1.default.svc.cluster.local + issuerRef: + kind: Issuer + name: nameOfClusterIssuer + secretName: app1-cert +--- +apiVersion: v1 +kind: Pod +metadata: + annotations: + helm.sh/hook: test + labels: + app.kubernetes.io/instance: chartsnap + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: app1 + app.kubernetes.io/version: 1.16.0 + helm.sh/chart: app1-0.1.0 + name: chartsnap-app1-test-connection +spec: + containers: + - args: + - chartsnap-app1:80 + command: + - wget + image: busybox + name: wget + restartPolicy: Never diff --git a/example/app1/test/__snapshots__/test_hpa_enabled.snap b/example/app1/test/__snapshots__/test_hpa_enabled.snap index ac9d3f2..6b85720 100644 --- a/example/app1/test/__snapshots__/test_hpa_enabled.snap +++ b/example/app1/test/__snapshots__/test_hpa_enabled.snap @@ -1,145 +1,141 @@ -[test_hpa_enabled] -SnapShot = """ -- object: - apiVersion: apps/v1 - kind: Deployment - metadata: - labels: - app.kubernetes.io/instance: chartsnap - app.kubernetes.io/managed-by: Helm - app.kubernetes.io/name: app1 - app.kubernetes.io/version: 1.16.0 - helm.sh/chart: app1-0.1.0 - name: chartsnap-app1 - spec: - selector: - matchLabels: - app.kubernetes.io/instance: chartsnap - app.kubernetes.io/name: app1 - template: - metadata: - labels: - app.kubernetes.io/instance: chartsnap - app.kubernetes.io/managed-by: Helm - app.kubernetes.io/name: app1 - app.kubernetes.io/version: 1.16.0 - helm.sh/chart: app1-0.1.0 - spec: - containers: - - image: nginx:1.16.0 - imagePullPolicy: IfNotPresent - livenessProbe: - httpGet: - path: / - port: http - name: app1 - ports: - - containerPort: 80 - name: http - protocol: TCP - readinessProbe: - httpGet: - path: / - port: http - resources: {} - securityContext: {} - securityContext: {} - serviceAccountName: chartsnap-app1 -- object: - apiVersion: autoscaling/v2 - kind: HorizontalPodAutoscaler - metadata: - labels: - app.kubernetes.io/instance: chartsnap - app.kubernetes.io/managed-by: Helm - app.kubernetes.io/name: app1 - app.kubernetes.io/version: 1.16.0 - helm.sh/chart: app1-0.1.0 - name: chartsnap-app1 - spec: - maxReplicas: 10 - metrics: - - resource: - name: cpu - target: - averageUtilization: 65 - type: Utilization - type: Resource - minReplicas: 1 - scaleTargetRef: - apiVersion: apps/v1 - kind: Deployment - name: chartsnap-app1 -- object: - apiVersion: v1 - kind: Pod +apiVersion: v1 +automountServiceAccountToken: true +kind: ServiceAccount +metadata: + labels: + app.kubernetes.io/instance: chartsnap + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: app1 + app.kubernetes.io/version: 1.16.0 + helm.sh/chart: app1-0.1.0 + name: chartsnap-app1 +--- +apiVersion: v1 +data: + ca.crt: IyMjRFlOQU1JQ19GSUVMRCMjIw== + tls.crt: IyMjRFlOQU1JQ19GSUVMRCMjIw== + tls.key: IyMjRFlOQU1JQ19GSUVMRCMjIw== +kind: Secret +metadata: + labels: + app.kubernetes.io/instance: chartsnap + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: app1 + app.kubernetes.io/version: 1.16.0 + helm.sh/chart: app1-0.1.0 + name: app1-cert + namespace: default +type: kubernetes.io/tls +--- +apiVersion: v1 +kind: Service +metadata: + labels: + app.kubernetes.io/instance: chartsnap + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: app1 + app.kubernetes.io/version: 1.16.0 + helm.sh/chart: app1-0.1.0 + name: chartsnap-app1 +spec: + ports: + - name: http + port: 80 + protocol: TCP + targetPort: http + selector: + app.kubernetes.io/instance: chartsnap + app.kubernetes.io/name: app1 + type: ClusterIP +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app.kubernetes.io/instance: chartsnap + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: app1 + app.kubernetes.io/version: 1.16.0 + helm.sh/chart: app1-0.1.0 + name: chartsnap-app1 +spec: + selector: + matchLabels: + app.kubernetes.io/instance: chartsnap + app.kubernetes.io/name: app1 + template: metadata: - annotations: - helm.sh/hook: test - labels: - app.kubernetes.io/instance: chartsnap - app.kubernetes.io/managed-by: Helm - app.kubernetes.io/name: app1 - app.kubernetes.io/version: 1.16.0 - helm.sh/chart: app1-0.1.0 - name: chartsnap-app1-test-connection + labels: + app.kubernetes.io/instance: chartsnap + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: app1 + app.kubernetes.io/version: 1.16.0 + helm.sh/chart: app1-0.1.0 spec: - containers: - - args: - - chartsnap-app1:80 - command: - - wget - image: busybox - name: wget - restartPolicy: Never -- object: - apiVersion: v1 - data: - ca.crt: '###DYNAMIC_FIELD###' - tls.crt: '###DYNAMIC_FIELD###' - tls.key: '###DYNAMIC_FIELD###' - kind: Secret - metadata: - labels: - app.kubernetes.io/instance: chartsnap - app.kubernetes.io/managed-by: Helm - app.kubernetes.io/name: app1 - app.kubernetes.io/version: 1.16.0 - helm.sh/chart: app1-0.1.0 - name: app1-cert - namespace: default - type: kubernetes.io/tls -- object: - apiVersion: v1 - kind: Service - metadata: - labels: - app.kubernetes.io/instance: chartsnap - app.kubernetes.io/managed-by: Helm - app.kubernetes.io/name: app1 - app.kubernetes.io/version: 1.16.0 - helm.sh/chart: app1-0.1.0 - name: chartsnap-app1 - spec: - ports: - - name: http - port: 80 + containers: + - image: nginx:1.16.0 + imagePullPolicy: IfNotPresent + livenessProbe: + httpGet: + path: / + port: http + name: app1 + ports: + - containerPort: 80 + name: http protocol: TCP - targetPort: http - selector: - app.kubernetes.io/instance: chartsnap - app.kubernetes.io/name: app1 - type: ClusterIP -- object: - apiVersion: v1 - automountServiceAccountToken: true - kind: ServiceAccount - metadata: - labels: - app.kubernetes.io/instance: chartsnap - app.kubernetes.io/managed-by: Helm - app.kubernetes.io/name: app1 - app.kubernetes.io/version: 1.16.0 - helm.sh/chart: app1-0.1.0 - name: chartsnap-app1 -""" + readinessProbe: + httpGet: + path: / + port: http + resources: {} + securityContext: {} + securityContext: {} + serviceAccountName: chartsnap-app1 +--- +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + labels: + app.kubernetes.io/instance: chartsnap + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: app1 + app.kubernetes.io/version: 1.16.0 + helm.sh/chart: app1-0.1.0 + name: chartsnap-app1 +spec: + maxReplicas: 10 + metrics: + - resource: + name: cpu + target: + averageUtilization: 65 + type: Utilization + type: Resource + minReplicas: 1 + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: chartsnap-app1 +--- +apiVersion: v1 +kind: Pod +metadata: + annotations: + helm.sh/hook: test + labels: + app.kubernetes.io/instance: chartsnap + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: app1 + app.kubernetes.io/version: 1.16.0 + helm.sh/chart: app1-0.1.0 + name: chartsnap-app1-test-connection +spec: + containers: + - args: + - chartsnap-app1:80 + command: + - wget + image: busybox + name: wget + restartPolicy: Never diff --git a/example/app1/test/__snapshots__/test_ingress_enabled.snap b/example/app1/test/__snapshots__/test_ingress_enabled.snap index a4e078d..b26e4ef 100644 --- a/example/app1/test/__snapshots__/test_ingress_enabled.snap +++ b/example/app1/test/__snapshots__/test_ingress_enabled.snap @@ -1,151 +1,147 @@ -[test_ingress_enabled] -SnapShot = """ -- object: - apiVersion: apps/v1 - kind: Deployment +apiVersion: v1 +automountServiceAccountToken: true +kind: ServiceAccount +metadata: + labels: + app.kubernetes.io/instance: chartsnap + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: app1 + app.kubernetes.io/version: 1.16.0 + helm.sh/chart: app1-0.1.0 + name: chartsnap-app1 +--- +apiVersion: v1 +data: + ca.crt: IyMjRFlOQU1JQ19GSUVMRCMjIw== + tls.crt: IyMjRFlOQU1JQ19GSUVMRCMjIw== + tls.key: IyMjRFlOQU1JQ19GSUVMRCMjIw== +kind: Secret +metadata: + labels: + app.kubernetes.io/instance: chartsnap + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: app1 + app.kubernetes.io/version: 1.16.0 + helm.sh/chart: app1-0.1.0 + name: app1-cert + namespace: default +type: kubernetes.io/tls +--- +apiVersion: v1 +kind: Service +metadata: + labels: + app.kubernetes.io/instance: chartsnap + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: app1 + app.kubernetes.io/version: 1.16.0 + helm.sh/chart: app1-0.1.0 + name: chartsnap-app1 +spec: + ports: + - name: http + port: 80 + protocol: TCP + targetPort: http + selector: + app.kubernetes.io/instance: chartsnap + app.kubernetes.io/name: app1 + type: ClusterIP +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app.kubernetes.io/instance: chartsnap + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: app1 + app.kubernetes.io/version: 1.16.0 + helm.sh/chart: app1-0.1.0 + name: chartsnap-app1 +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/instance: chartsnap + app.kubernetes.io/name: app1 + template: metadata: - labels: - app.kubernetes.io/instance: chartsnap - app.kubernetes.io/managed-by: Helm - app.kubernetes.io/name: app1 - app.kubernetes.io/version: 1.16.0 - helm.sh/chart: app1-0.1.0 - name: chartsnap-app1 + labels: + app.kubernetes.io/instance: chartsnap + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: app1 + app.kubernetes.io/version: 1.16.0 + helm.sh/chart: app1-0.1.0 spec: - replicas: 1 - selector: - matchLabels: - app.kubernetes.io/instance: chartsnap - app.kubernetes.io/name: app1 - template: - metadata: - labels: - app.kubernetes.io/instance: chartsnap - app.kubernetes.io/managed-by: Helm - app.kubernetes.io/name: app1 - app.kubernetes.io/version: 1.16.0 - helm.sh/chart: app1-0.1.0 - spec: - containers: - - image: nginx:1.16.0 - imagePullPolicy: IfNotPresent - livenessProbe: - httpGet: - path: / - port: http - name: app1 - ports: - - containerPort: 80 - name: http - protocol: TCP - readinessProbe: - httpGet: - path: / - port: http - resources: {} - securityContext: {} - securityContext: {} - serviceAccountName: chartsnap-app1 -- object: - apiVersion: networking.k8s.io/v1 - kind: Ingress - metadata: - annotations: - cert-manager.io/cluster-issuer: nameOfClusterIssuer - labels: - app.kubernetes.io/instance: chartsnap - app.kubernetes.io/managed-by: Helm - app.kubernetes.io/name: app1 - app.kubernetes.io/version: 1.16.0 - helm.sh/chart: app1-0.1.0 - name: chartsnap-app1 - spec: - ingressClassName: nginx - rules: - - host: chart-example.local - http: - paths: - - backend: - service: - name: chartsnap-app1 - port: - number: 80 - path: / - pathType: ImplementationSpecific - tls: - - hosts: - - chart-example.local - secretName: chart-example-tls -- object: - apiVersion: v1 - kind: Pod - metadata: - annotations: - helm.sh/hook: test - labels: - app.kubernetes.io/instance: chartsnap - app.kubernetes.io/managed-by: Helm - app.kubernetes.io/name: app1 - app.kubernetes.io/version: 1.16.0 - helm.sh/chart: app1-0.1.0 - name: chartsnap-app1-test-connection - spec: - containers: - - args: - - chartsnap-app1:80 - command: - - wget - image: busybox - name: wget - restartPolicy: Never -- object: - apiVersion: v1 - data: - ca.crt: '###DYNAMIC_FIELD###' - tls.crt: '###DYNAMIC_FIELD###' - tls.key: '###DYNAMIC_FIELD###' - kind: Secret - metadata: - labels: - app.kubernetes.io/instance: chartsnap - app.kubernetes.io/managed-by: Helm - app.kubernetes.io/name: app1 - app.kubernetes.io/version: 1.16.0 - helm.sh/chart: app1-0.1.0 - name: app1-cert - namespace: default - type: kubernetes.io/tls -- object: - apiVersion: v1 - kind: Service - metadata: - labels: - app.kubernetes.io/instance: chartsnap - app.kubernetes.io/managed-by: Helm - app.kubernetes.io/name: app1 - app.kubernetes.io/version: 1.16.0 - helm.sh/chart: app1-0.1.0 - name: chartsnap-app1 - spec: - ports: - - name: http - port: 80 + containers: + - image: nginx:1.16.0 + imagePullPolicy: IfNotPresent + livenessProbe: + httpGet: + path: / + port: http + name: app1 + ports: + - containerPort: 80 + name: http protocol: TCP - targetPort: http - selector: - app.kubernetes.io/instance: chartsnap - app.kubernetes.io/name: app1 - type: ClusterIP -- object: - apiVersion: v1 - automountServiceAccountToken: true - kind: ServiceAccount - metadata: - labels: - app.kubernetes.io/instance: chartsnap - app.kubernetes.io/managed-by: Helm - app.kubernetes.io/name: app1 - app.kubernetes.io/version: 1.16.0 - helm.sh/chart: app1-0.1.0 - name: chartsnap-app1 -""" + readinessProbe: + httpGet: + path: / + port: http + resources: {} + securityContext: {} + securityContext: {} + serviceAccountName: chartsnap-app1 +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + annotations: + cert-manager.io/cluster-issuer: nameOfClusterIssuer + labels: + app.kubernetes.io/instance: chartsnap + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: app1 + app.kubernetes.io/version: 1.16.0 + helm.sh/chart: app1-0.1.0 + name: chartsnap-app1 +spec: + ingressClassName: nginx + rules: + - host: chart-example.local + http: + paths: + - backend: + service: + name: chartsnap-app1 + port: + number: 80 + path: / + pathType: ImplementationSpecific + tls: + - hosts: + - chart-example.local + secretName: chart-example-tls +--- +apiVersion: v1 +kind: Pod +metadata: + annotations: + helm.sh/hook: test + labels: + app.kubernetes.io/instance: chartsnap + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: app1 + app.kubernetes.io/version: 1.16.0 + helm.sh/chart: app1-0.1.0 + name: chartsnap-app1-test-connection +spec: + containers: + - args: + - chartsnap-app1:80 + command: + - wget + image: busybox + name: wget + restartPolicy: Never diff --git a/example/app1/test/test_ingress_enabled.yaml b/example/app1/test/test_ingress_enabled.yaml index 0b42399..5a45e1a 100644 --- a/example/app1/test/test_ingress_enabled.yaml +++ b/example/app1/test/test_ingress_enabled.yaml @@ -7,6 +7,7 @@ testSpec: - /data/ca.crt - /data/tls.crt - /data/tls.key + base64: true ingress: enabled: true diff --git a/example/app1/testfail/.chartsnap.yaml b/example/app1/testfail/.chartsnap.yaml index e35a096..0880502 100644 --- a/example/app1/testfail/.chartsnap.yaml +++ b/example/app1/testfail/.chartsnap.yaml @@ -7,3 +7,4 @@ dynamicFields: - /data/ca.crt - /data/tls.crt - /data/tls.key + base64: true diff --git a/example/app1/testfail/__snapshots__/test_certmanager_enabled.snap b/example/app1/testfail/__snapshots__/test_certmanager_enabled.snap index 8e76a15..fdd48b4 100644 --- a/example/app1/testfail/__snapshots__/test_certmanager_enabled.snap +++ b/example/app1/testfail/__snapshots__/test_certmanager_enabled.snap @@ -1,124 +1,120 @@ -[test_certmanager_enabled] -SnapShot = """ -- object: - apiVersion: apps/v1 - kind: Deployment +apiVersion: v1 +automountServiceAccountToken: true +kind: ServiceAccount +metadata: + labels: + app.kubernetes.io/instance: chartsnap + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: app1 + app.kubernetes.io/version: 1.16.0 + helm.sh/chart: app1-0.1.0 + name: chartsnap-app1 +--- +apiVersion: v1 +kind: Service +metadata: + labels: + app.kubernetes.io/instance: chartsnap + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: app1 + app.kubernetes.io/version: 1.16.0 + helm.sh/chart: app1-0.1.0 + name: chartsnap-app1 +spec: + ports: + - name: http + port: 80 + protocol: TCP + targetPort: http + selector: + app.kubernetes.io/instance: chartsnap + app.kubernetes.io/name: app1 + type: ClusterIP +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app.kubernetes.io/instance: chartsnap + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: app1 + app.kubernetes.io/version: 1.16.0 + helm.sh/chart: app1-0.1.0 + name: chartsnap-app1 +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/instance: chartsnap + app.kubernetes.io/name: app1 + template: metadata: - labels: - app.kubernetes.io/instance: chartsnap - app.kubernetes.io/managed-by: Helm - app.kubernetes.io/name: app1 - app.kubernetes.io/version: 1.16.0 - helm.sh/chart: app1-0.1.0 - name: chartsnap-app1 + labels: + app.kubernetes.io/instance: chartsnap + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: app1 + app.kubernetes.io/version: 1.16.0 + helm.sh/chart: app1-0.1.0 spec: - replicas: 1 - selector: - matchLabels: - app.kubernetes.io/instance: chartsnap - app.kubernetes.io/name: app1 - template: - metadata: - labels: - app.kubernetes.io/instance: chartsnap - app.kubernetes.io/managed-by: Helm - app.kubernetes.io/name: app1 - app.kubernetes.io/version: 1.16.0 - helm.sh/chart: app1-0.1.0 - spec: - containers: - - image: nginx:1.16.0 - imagePullPolicy: IfNotPresent - livenessProbe: - httpGet: - path: / - port: http - name: app1 - ports: - - containerPort: 80 - name: http - protocol: TCP - readinessProbe: - httpGet: - path: / - port: http - resources: {} - securityContext: {} - securityContext: {} - serviceAccountName: chartsnap-app1 -- object: - apiVersion: cert-manager.io/v1 - kind: Certificate - metadata: - labels: - app.kubernetes.io/instance: chartsnap - app.kubernetes.io/managed-by: Helm - app.kubernetes.io/name: app1 - app.kubernetes.io/version: 1.16.0 - helm.sh/chart: app1-0.1.0 - name: app1-cert - namespace: code-server - spec: - dnsNames: - - chartsnap-app1.code-server.svc - - chartsnap-app1.code-server.svc.cluster.local - issuerRef: - kind: Issuer - name: nameOfClusterIssuer - secretName: app1-cert -- object: - apiVersion: v1 - kind: Pod - metadata: - annotations: - helm.sh/hook: test - labels: - app.kubernetes.io/instance: chartsnap - app.kubernetes.io/managed-by: Helm - app.kubernetes.io/name: app1 - app.kubernetes.io/version: 1.16.0 - helm.sh/chart: app1-0.1.0 - name: chartsnap-app1-test-connection - spec: - containers: - - args: - - chartsnap-app1:80 - command: - - wget - image: busybox - name: wget - restartPolicy: Never -- object: - apiVersion: v1 - kind: Service - metadata: - labels: - app.kubernetes.io/instance: chartsnap - app.kubernetes.io/managed-by: Helm - app.kubernetes.io/name: app1 - app.kubernetes.io/version: 1.16.0 - helm.sh/chart: app1-0.1.0 - name: chartsnap-app1 - spec: - ports: - - name: http - port: 80 + containers: + - image: nginx:1.16.0 + imagePullPolicy: IfNotPresent + livenessProbe: + httpGet: + path: / + port: http + name: app1 + ports: + - containerPort: 80 + name: http protocol: TCP - targetPort: http - selector: - app.kubernetes.io/instance: chartsnap - app.kubernetes.io/name: app1 - type: ClusterIP -- object: - apiVersion: v1 - automountServiceAccountToken: true - kind: ServiceAccount - metadata: - labels: - app.kubernetes.io/instance: chartsnap - app.kubernetes.io/managed-by: Helm - app.kubernetes.io/name: app1 - app.kubernetes.io/version: 1.16.0 - helm.sh/chart: app1-0.1.0 - name: chartsnap-app1 -""" + readinessProbe: + httpGet: + path: / + port: http + resources: {} + securityContext: {} + securityContext: {} + serviceAccountName: chartsnap-app1 +--- +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + labels: + app.kubernetes.io/instance: chartsnap + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: app1 + app.kubernetes.io/version: 1.16.0 + helm.sh/chart: app1-0.1.0 + name: app1-cert + namespace: default +spec: + dnsNames: + - chartsnap-app1.default.svc + - chartsnap-app1.default.svc.cluster.local + issuerRef: + kind: Issuer + name: nameOfClusterIssuer + secretName: app1-cert +--- +apiVersion: v1 +kind: Pod +metadata: + annotations: + helm.sh/hook: test + labels: + app.kubernetes.io/instance: chartsnap + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: app1 + app.kubernetes.io/version: 1.16.0 + helm.sh/chart: app1-0.1.0 + name: chartsnap-app1-test-connection +spec: + containers: + - args: + - chartsnap-app1:80 + command: + - wget + image: busybox + name: wget + restartPolicy: Never diff --git a/example/app1/testfail/__snapshots__/test_hpa_enabled.snap b/example/app1/testfail/__snapshots__/test_hpa_enabled.snap index 05b9e0d..6b85720 100644 --- a/example/app1/testfail/__snapshots__/test_hpa_enabled.snap +++ b/example/app1/testfail/__snapshots__/test_hpa_enabled.snap @@ -1,145 +1,141 @@ -[test_hpa_enabled] -SnapShot = """ -- object: - apiVersion: apps/v1 - kind: Deployment - metadata: - labels: - app.kubernetes.io/instance: chartsnap - app.kubernetes.io/managed-by: Helm - app.kubernetes.io/name: app1 - app.kubernetes.io/version: 1.16.0 - helm.sh/chart: app1-0.1.0 - name: chartsnap-app1 - spec: - selector: - matchLabels: - app.kubernetes.io/instance: chartsnap - app.kubernetes.io/name: app1 - template: - metadata: - labels: - app.kubernetes.io/instance: chartsnap - app.kubernetes.io/managed-by: Helm - app.kubernetes.io/name: app1 - app.kubernetes.io/version: 1.16.0 - helm.sh/chart: app1-0.1.0 - spec: - containers: - - image: nginx:1.16.0 - imagePullPolicy: IfNotPresent - livenessProbe: - httpGet: - path: / - port: http - name: app1 - ports: - - containerPort: 80 - name: http - protocol: TCP - readinessProbe: - httpGet: - path: / - port: http - resources: {} - securityContext: {} - securityContext: {} - serviceAccountName: chartsnap-app1 -- object: - apiVersion: autoscaling/v2 - kind: HorizontalPodAutoscaler - metadata: - labels: - app.kubernetes.io/instance: chartsnap - app.kubernetes.io/managed-by: Helm - app.kubernetes.io/name: app1 - app.kubernetes.io/version: 1.16.0 - helm.sh/chart: app1-0.1.0 - name: chartsnap-app1 - spec: - maxReplicas: 10 - metrics: - - resource: - name: cpu - target: - averageUtilization: 65 - type: Utilization - type: Resource - minReplicas: 1 - scaleTargetRef: - apiVersion: apps/v1 - kind: Deployment - name: chartsnap-app1 -- object: - apiVersion: v1 - kind: Pod +apiVersion: v1 +automountServiceAccountToken: true +kind: ServiceAccount +metadata: + labels: + app.kubernetes.io/instance: chartsnap + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: app1 + app.kubernetes.io/version: 1.16.0 + helm.sh/chart: app1-0.1.0 + name: chartsnap-app1 +--- +apiVersion: v1 +data: + ca.crt: IyMjRFlOQU1JQ19GSUVMRCMjIw== + tls.crt: IyMjRFlOQU1JQ19GSUVMRCMjIw== + tls.key: IyMjRFlOQU1JQ19GSUVMRCMjIw== +kind: Secret +metadata: + labels: + app.kubernetes.io/instance: chartsnap + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: app1 + app.kubernetes.io/version: 1.16.0 + helm.sh/chart: app1-0.1.0 + name: app1-cert + namespace: default +type: kubernetes.io/tls +--- +apiVersion: v1 +kind: Service +metadata: + labels: + app.kubernetes.io/instance: chartsnap + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: app1 + app.kubernetes.io/version: 1.16.0 + helm.sh/chart: app1-0.1.0 + name: chartsnap-app1 +spec: + ports: + - name: http + port: 80 + protocol: TCP + targetPort: http + selector: + app.kubernetes.io/instance: chartsnap + app.kubernetes.io/name: app1 + type: ClusterIP +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app.kubernetes.io/instance: chartsnap + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: app1 + app.kubernetes.io/version: 1.16.0 + helm.sh/chart: app1-0.1.0 + name: chartsnap-app1 +spec: + selector: + matchLabels: + app.kubernetes.io/instance: chartsnap + app.kubernetes.io/name: app1 + template: metadata: - annotations: - helm.sh/hook: test - labels: - app.kubernetes.io/instance: chartsnap - app.kubernetes.io/managed-by: Helm - app.kubernetes.io/name: app1 - app.kubernetes.io/version: 1.16.0 - helm.sh/chart: app1-0.1.0 - name: chartsnap-app1-test-connection + labels: + app.kubernetes.io/instance: chartsnap + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: app1 + app.kubernetes.io/version: 1.16.0 + helm.sh/chart: app1-0.1.0 spec: - containers: - - args: - - chartsnap-app1:80 - command: - - wget - image: busybox - name: wget - restartPolicy: Never -- object: - apiVersion: v1 - data: - ca.crt: '###DYNAMIC_FIELD###' - tls.crt: '###DYNAMIC_FIELD###' - tls.key: '###DYNAMIC_FIELD###' - kind: Secret - metadata: - labels: - app.kubernetes.io/instance: chartsnap - app.kubernetes.io/managed-by: Helm - app.kubernetes.io/name: app1 - app.kubernetes.io/version: 1.16.0 - helm.sh/chart: app1-0.1.0 - name: app1-cert - namespace: code-server - type: kubernetes.io/tls -- object: - apiVersion: v1 - kind: Service - metadata: - labels: - app.kubernetes.io/instance: chartsnap - app.kubernetes.io/managed-by: Helm - app.kubernetes.io/name: app1 - app.kubernetes.io/version: 1.16.0 - helm.sh/chart: app1-0.1.0 - name: chartsnap-app1 - spec: - ports: - - name: http - port: 80 + containers: + - image: nginx:1.16.0 + imagePullPolicy: IfNotPresent + livenessProbe: + httpGet: + path: / + port: http + name: app1 + ports: + - containerPort: 80 + name: http protocol: TCP - targetPort: http - selector: - app.kubernetes.io/instance: chartsnap - app.kubernetes.io/name: app1 - type: ClusterIP -- object: - apiVersion: v1 - automountServiceAccountToken: true - kind: ServiceAccount - metadata: - labels: - app.kubernetes.io/instance: chartsnap - app.kubernetes.io/managed-by: Helm - app.kubernetes.io/name: app1 - app.kubernetes.io/version: 1.16.0 - helm.sh/chart: app1-0.1.0 - name: chartsnap-app1 -""" + readinessProbe: + httpGet: + path: / + port: http + resources: {} + securityContext: {} + securityContext: {} + serviceAccountName: chartsnap-app1 +--- +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + labels: + app.kubernetes.io/instance: chartsnap + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: app1 + app.kubernetes.io/version: 1.16.0 + helm.sh/chart: app1-0.1.0 + name: chartsnap-app1 +spec: + maxReplicas: 10 + metrics: + - resource: + name: cpu + target: + averageUtilization: 65 + type: Utilization + type: Resource + minReplicas: 1 + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: chartsnap-app1 +--- +apiVersion: v1 +kind: Pod +metadata: + annotations: + helm.sh/hook: test + labels: + app.kubernetes.io/instance: chartsnap + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: app1 + app.kubernetes.io/version: 1.16.0 + helm.sh/chart: app1-0.1.0 + name: chartsnap-app1-test-connection +spec: + containers: + - args: + - chartsnap-app1:80 + command: + - wget + image: busybox + name: wget + restartPolicy: Never diff --git a/example/app1/testfail/__snapshots__/test_ingress_enabled.snap b/example/app1/testfail/__snapshots__/test_ingress_enabled.snap index fab9727..86a3cf0 100644 --- a/example/app1/testfail/__snapshots__/test_ingress_enabled.snap +++ b/example/app1/testfail/__snapshots__/test_ingress_enabled.snap @@ -1,151 +1,131 @@ -[test_ingress_enabled] -SnapShot = """ -- object: - apiVersion: apps/v1 - kind: Deployment +apiVersion: v2 +automountServiceAccountToken: true +kind: ServiceAccount +metadata: + labels: + app.kubernetes.io/instance: chartsnap + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: app1 + app.kubernetes.io/version: 1.16.0 + helm.sh/chart: app1-0.1.0 + name: chartsnap-app1 +--- +apiVersion: v1 +kind: Namespace +metadata: + annotations: + helm.sh/hook: test + labels: + app.kubernetes.io/instance: chartsnap + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: app1 + app.kubernetes.io/version: 1.16.0 + helm.sh/chart: app1-0.1.0 + name: chartsnap-app1-namespace +--- +apiVersion: v1 +data: + ca.crt: '###DYNAMIC_FIELD###' + tls.crt: '###DYNAMIC_FIELD###' + tls.key: '###DYNAMIC_FIELD###' +kind: Secret +metadata: + labels: + app.kubernetes.io/instance: chartsnap + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: app1 + app.kubernetes.io/version: 1.16.0 + helm.sh/chart: app1-0.1.0 + name: app1-cert + namespace: default +type: kubernetes.io/tls +--- +apiVersion: v1 +kind: Service +metadata: + labels: + app.kubernetes.io/instance: chartsnap + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: app1 + app.kubernetes.io/version: 1.16.0 + helm.sh/chart: app1-0.1.0 + name: chartsnap-app1 +spec: + ports: + - name: http + port: 80 + protocol: TCP + targetPort: http + selector: + app.kubernetes.io/instance: chartsnap + app.kubernetes.io/name: app1 + type: LoadBalancer +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app.kubernetes.io/instance: chartsnap + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: app1 + app.kubernetes.io/version: 1.15.0 + helm.sh/chart: app1-0.1.0 + name: chartsnap-app1 +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/instance: chartsnap + app.kubernetes.io/name: app1 + template: metadata: - labels: - app.kubernetes.io/instance: chartsnap - app.kubernetes.io/managed-by: Helm - app.kubernetes.io/name: app1 - app.kubernetes.io/version: 1.16.0 - helm.sh/chart: app1-0.1.0 - name: chartsnap-app1 + labels: + app.kubernetes.io/instance: chartsnap + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: app1 + app.kubernetes.io/version: 1.16.0 + helm.sh/chart: app1-0.1.0 spec: - replicas: 1 - selector: - matchLabels: - app.kubernetes.io/instance: chartsnap - app.kubernetes.io/name: app1 - template: - metadata: - labels: - app.kubernetes.io/instance: chartsnap - app.kubernetes.io/managed-by: Helm - app.kubernetes.io/name: app1 - app.kubernetes.io/version: 1.16.0 - helm.sh/chart: app1-0.1.0 - spec: - containers: - - image: nginx:1.15.0 - imagePullPolicy: IfNotPresent - livenessProbe: - httpGet: - path: / - port: http - name: app1 - ports: - - containerPort: 80 - name: http - protocol: TCP - readinessProbe: - httpGet: - path: / - port: http - resources: {} - securityContext: {} - securityContext: {} - serviceAccountName: chartsnap-app1 -- object: - apiVersion: networking.k8s.io/v1 - kind: Ingress - metadata: - annotations: - cert-manager.io/cluster-issuer: nameOfClusterIssuer - labels: - app.kubernetes.io/instance: chartsnap - app.kubernetes.io/managed-by: Helm - app.kubernetes.io/name: app1 - app.kubernetes.io/version: 1.16.0 - helm.sh/chart: app1-0.1.0 - name: chartsnap-app1 - spec: - ingressClassName: nginx - rules: - - host: chart-example.local - http: - paths: - - backend: - service: - name: chartsnap-app1 - port: - number: 80 - path: / - pathType: Prefix - tls: - - hosts: - - chart-example.local - secretName: chart-example-tls -- object: - apiVersion: v1 - kind: Pod - metadata: - annotations: - helm.sh/hook: test - labels: - app.kubernetes.io/instance: chartsnap - app.kubernetes.io/managed-by: Helm - app.kubernetes.io/name: app1 - app.kubernetes.io/version: 1.16.0 - helm.sh/chart: app1-0.1.0 - name: chartsnap-app1-test-connection - spec: - containers: - - args: - - chartsnap-app1:80 - command: - - wget - image: busybox - name: wget - restartPolicy: Never -- object: - apiVersion: v1 - data: - ca.crt: '###DYNAMIC_FIELD###' - tls.crt: '###DYNAMIC_FIELD###' - tls.key: '###DYNAMIC_FIELD###' - kind: Secret - metadata: - labels: - app.kubernetes.io/instance: chartsnap - app.kubernetes.io/managed-by: Helm - app.kubernetes.io/name: app1 - app.kubernetes.io/version: 1.16.0 - helm.sh/chart: app1-0.1.0 - name: app1-cert - namespace: code-server - type: kubernetes.io/tls -- object: - apiVersion: v1 - kind: Service - metadata: - labels: - app.kubernetes.io/instance: chartsnap - app.kubernetes.io/managed-by: Helm - app.kubernetes.io/name: app1 - app.kubernetes.io/version: 1.16.0 - helm.sh/chart: app1-0.1.0 - name: chartsnap-app1 - spec: - ports: - - name: http - port: 80 + containers: + - image: nginx:1.16.0 + imagePullPolicy: IfNotPresent + livenessProbe: + httpGet: + path: / + port: http + name: app1 + ports: + - containerPort: 80 + name: http protocol: TCP - targetPort: http - selector: - app.kubernetes.io/instance: chartsnap - app.kubernetes.io/name: app1 - type: ClusterIP -- object: - apiVersion: v1 - automountServiceAccountToken: true - kind: ServiceAccount - metadata: - labels: - app.kubernetes.io/instance: chartsnap - app.kubernetes.io/managed-by: Helm - app.kubernetes.io/name: app1 - app.kubernetes.io/version: 1.16.0 - helm.sh/chart: app1-0.1.0 - name: chartsnap-app1 -""" + readinessProbe: + httpGet: + path: / + port: http + resources: {} + securityContext: {} + securityContext: {} + serviceAccountName: chartsnap-app1 +--- +apiVersion: v1 +kind: Pod +metadata: + annotations: + helm.sh/hook: test + labels: + app.kubernetes.io/instance: chartsnap + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: app1 + app.kubernetes.io/version: 1.16.0 + helm.sh/chart: app1-0.1.0 + name: chartsnap-app1-test-connection +spec: + containers: + - args: + - chartsnap-app1:80 + command: + - wget + image: busybox + name: wget + restartPolicy: Never +a diff --git a/example/app1/testfail/test_ingress_enabled.yaml b/example/app1/testfail/test_ingress_enabled.yaml index 0b42399..5a45e1a 100644 --- a/example/app1/testfail/test_ingress_enabled.yaml +++ b/example/app1/testfail/test_ingress_enabled.yaml @@ -7,6 +7,7 @@ testSpec: - /data/ca.crt - /data/tls.crt - /data/tls.key + base64: true ingress: enabled: true diff --git a/hack/helm-template-help-snapshot/main.go b/hack/helm-template-help-snapshot/main.go index d7db38a..fc8d42e 100644 --- a/hack/helm-template-help-snapshot/main.go +++ b/hack/helm-template-help-snapshot/main.go @@ -23,7 +23,7 @@ func execute(cmd ...string) string { } func snapshot(id, data string) { - s := snap.SnapShotMatcher("helm-template.snap", id) + s := snap.SnapshotMatcher("helm-template.snap", snap.WithSnapshotID(id)) match, err := s.Match(data) if err != nil { diff --git a/main.go b/main.go index 84da7af..2eebbef 100644 --- a/main.go +++ b/main.go @@ -10,9 +10,11 @@ import ( "strings" "github.com/fatih/color" - "github.com/jlandowner/helm-chartsnap/pkg/charts" "github.com/spf13/cobra" "golang.org/x/sync/errgroup" + + "github.com/jlandowner/helm-chartsnap/pkg/charts" + "github.com/jlandowner/helm-chartsnap/pkg/snap" ) var ( @@ -35,6 +37,7 @@ type option struct { FailFast bool Parallelism int ConfigFile string + LegacySnapshot bool // Below properties are the same as helm global options // They are passed to the plugin as environment variables @@ -167,6 +170,7 @@ MIT 2023 jlandowner/helm-chartsnap if err := rootCmd.MarkPersistentFlagFilename("config-file"); err != nil { panic(err) } + rootCmd.PersistentFlags().BoolVar(&o.LegacySnapshot, "legacy-snapshot", false, "use toml-based legacy snapshot format") if err := rootCmd.Execute(); err != nil { slog.New(slogHandler()).Error(err.Error()) @@ -193,6 +197,19 @@ func prerun(cmd *cobra.Command, args []string) error { return nil } +func loadSnapshotConfig(file string, cfg *charts.SnapshotConfig) error { + err := charts.LoadSnapshotConfig(file, cfg) + if err != nil && !os.IsNotExist(err) { + if o.FailFast { + return fmt.Errorf("failed to load snapshot config: %w", err) + } else { + log.Error("WARNING: failed to load snapshot config", "path", file, "err", err) + } + } + log.Debug("snapshot config", "cfg", cfg) + return nil +} + func run(cmd *cobra.Command, args []string) error { log = slog.New(slogHandler()) log.Debug("options", printOptions(*o)...) @@ -206,13 +223,24 @@ func run(cmd *cobra.Command, args []string) error { } var cfg charts.SnapshotConfig + if _, err := os.Stat(o.ConfigFile); err == nil { + if err := loadSnapshotConfig(o.ConfigFile, &cfg); err != nil { + return err + } + } if o.ValuesFile == "" { values = []string{""} } else { - if s, err := os.Stat(o.ValuesFile); os.IsNotExist(err) { - return fmt.Errorf("values file '%s' not found", o.ValuesFile) - } else if s.IsDir() { + stat, err := os.Stat(o.ValuesFile) + if err != nil { + if os.IsNotExist(err) { + return fmt.Errorf("values file '%s' not found", o.ValuesFile) + } + return fmt.Errorf("failed to stat values file %s: %w", o.ValuesFile, err) + } + + if stat.IsDir() { // get all values files in the directory files, err := os.ReadDir(o.ValuesFile) if err != nil { @@ -222,13 +250,8 @@ func run(cmd *cobra.Command, args []string) error { for _, f := range files { // pick config file in a test values directory if f.Name() == o.ConfigFile { - cfg, err = charts.LoadSnapshotConfig(path.Join(o.ValuesFile, f.Name())) - if err != nil { - if o.FailFast { - return fmt.Errorf("failed to load snapshot config: %w", err) - } else { - log.Error("warning: failed to load snapshot config", "path", path.Join(o.ValuesFile, f.Name()), "err", err) - } + if err = loadSnapshotConfig(path.Join(o.ValuesFile, f.Name()), &cfg); err != nil { + return err } continue } @@ -240,6 +263,10 @@ func run(cmd *cobra.Command, args []string) error { } } else { values = []string{o.ValuesFile} + // load snapshot config in the same directory as the values file + if err := loadSnapshotConfig(path.Join(path.Dir(o.ValuesFile), o.ConfigFile), &cfg); err != nil { + return err + } } } @@ -284,26 +311,37 @@ func run(cmd *cobra.Command, args []string) error { } if o.UpdateSnapshot { - err := os.Remove(snapshotFilePath) + // v1 format is multi snapshot format with encoding legacy formatted yaml + if snap.IsMultiSnapshots(snapshotFilePath) && !o.LegacySnapshot { + log.Info("WARNING: snapshot is updated to a latest format. if you want to keep a legacy format, please run with --legacy-snapshot flag", "path", snapshotFilePath) + } + err := snap.RemoveFile(snapshotFilePath) if err != nil && !os.IsNotExist(err) { return fmt.Errorf("failed to replace snapshot file: %w", err) } } - opts := charts.ChartSnapOptions{ + var version string + // use v1 snapshot format if legacy snapshot format is enabled + if o.LegacySnapshot { + version = charts.SnapshotVersionV1 + } + + snapshotter := charts.ChartSnapshotter{ HelmTemplateCmdOptions: ht, SnapshotConfig: cfg, SnapshotFile: snapshotFilePath, + SnapshotVersion: version, DiffContextLineN: o.DiffContextLineN, } - matched, failureMessage, err := charts.Snap(ctx, opts) + result, err := snapshotter.Snap(ctx) if err != nil { bannerPrintln("FAIL", fmt.Sprintf("chart=%s values=%s err=%v", ht.Chart, ht.ValuesFile, err), color.FgRed, color.BgRed) return fmt.Errorf("failed to get snapshot chart=%s values=%s: %w", ht.Chart, ht.ValuesFile, err) } - if !matched { + if !result.Match { bannerPrintln("FAIL", fmt.Sprintf("Snapshot does not match chart=%s values=%s", ht.Chart, ht.ValuesFile), color.FgRed, color.BgRed) - fmt.Println(failureMessage) + fmt.Println(result.FailureMessage) return fmt.Errorf("snapshot does not match chart=%s values=%s", ht.Chart, ht.ValuesFile) } bannerPrintln("PASS", fmt.Sprintf("Snapshot %s chart=%s values=%s", o.OK(), ht.Chart, ht.ValuesFile), color.FgGreen, color.BgGreen) diff --git a/pkg/charts/__snapshots__/helm_stub_snap.yaml b/pkg/charts/__snapshots__/helm_stub_snap.yaml new file mode 100644 index 0000000..2242214 --- /dev/null +++ b/pkg/charts/__snapshots__/helm_stub_snap.yaml @@ -0,0 +1,146 @@ +apiVersion: helm-chartsnap.jlandowner.dev/v1alpha1 +kind: Unknown +raw: | + this is warning message of helm +--- +apiVersion: v1 +automountServiceAccountToken: true +kind: ServiceAccount +metadata: + labels: + app.kubernetes.io/instance: chartsnap + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: app1 + app.kubernetes.io/version: 1.16.0 + helm.sh/chart: app1-0.1.0 + name: chartsnap-app1 +--- +apiVersion: v1 +data: + ca.crt: IyMjRFlOQU1JQ19GSUVMRCMjIw== + tls.crt: IyMjRFlOQU1JQ19GSUVMRCMjIw== + tls.key: IyMjRFlOQU1JQ19GSUVMRCMjIw== +kind: Secret +metadata: + labels: + app.kubernetes.io/instance: chartsnap + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: app1 + app.kubernetes.io/version: 1.16.0 + helm.sh/chart: app1-0.1.0 + name: app1-cert + namespace: default +type: kubernetes.io/tls +--- +apiVersion: v1 +kind: Service +metadata: + labels: + app.kubernetes.io/instance: chartsnap + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: app1 + app.kubernetes.io/version: 1.16.0 + helm.sh/chart: app1-0.1.0 + name: chartsnap-app1 +spec: + ports: + - name: http + port: 80 + protocol: TCP + targetPort: http + selector: + app.kubernetes.io/instance: chartsnap + app.kubernetes.io/name: app1 + type: '###DYNAMIC_FIELD###' +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app.kubernetes.io/instance: chartsnap + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: app1 + app.kubernetes.io/version: 1.16.0 + helm.sh/chart: app1-0.1.0 + name: chartsnap-app1 +spec: + selector: + matchLabels: + app.kubernetes.io/instance: chartsnap + app.kubernetes.io/name: app1 + template: + metadata: + labels: + app.kubernetes.io/instance: chartsnap + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: app1 + app.kubernetes.io/version: 1.16.0 + helm.sh/chart: app1-0.1.0 + spec: + containers: + - image: nginx:1.16.0 + imagePullPolicy: IfNotPresent + livenessProbe: + httpGet: + path: / + port: http + name: app1 + ports: + - containerPort: 80 + name: http + protocol: TCP + readinessProbe: + httpGet: + path: / + port: http + resources: {} + securityContext: {} + securityContext: {} + serviceAccountName: chartsnap-app1 +--- +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + labels: + app.kubernetes.io/instance: chartsnap + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: app1 + app.kubernetes.io/version: 1.16.0 + helm.sh/chart: app1-0.1.0 + name: chartsnap-app1 +spec: + maxReplicas: 10 + metrics: + - resource: + name: cpu + target: + averageUtilization: 65 + type: Utilization + type: Resource + minReplicas: 1 + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: chartsnap-app1 +--- +apiVersion: v1 +kind: Pod +metadata: + annotations: + helm.sh/hook: test + labels: + app.kubernetes.io/instance: chartsnap + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: app1 + app.kubernetes.io/version: 1.16.0 + helm.sh/chart: app1-0.1.0 + name: chartsnap-app1-test-connection +spec: + containers: + - args: + - chartsnap-app1:80 + command: + - wget + image: busybox + name: wget + restartPolicy: Never diff --git a/pkg/charts/__snapshots__/helm_stub_snap_unmatch.yaml b/pkg/charts/__snapshots__/helm_stub_snap_unmatch.yaml new file mode 100644 index 0000000..21c0276 --- /dev/null +++ b/pkg/charts/__snapshots__/helm_stub_snap_unmatch.yaml @@ -0,0 +1,146 @@ +apiVersion: helm-chartsnap.jlandowner.dev/v1alpha1 +kind: Unknown +raw: | + this is warning message of helm +--- +apiVersion: v1 +automountServiceAccountToken: true +kind: ServiceAccount +metadata: + labels: + app.kubernetes.io/instance: chartsnap + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: app1 + app.kubernetes.io/version: 1.15.0 + helm.sh/chart: app1-0.1.0 + name: chartsnap-app1 +--- +apiVersion: v1 +data: + ca.crt: IyMjRFlOQU1JQ19GSUVMRCMjIw== + tls.crt: IyMjRFlOQU1JQ19GSUVMRCMjIw== + tls.key: IyMjRFlOQU1JQ19GSUVMRCMjIw== +kind: Secret +metadata: + labels: + app.kubernetes.io/instance: chartsnap + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: app1 + app.kubernetes.io/version: 1.15.0 + helm.sh/chart: app1-0.1.0 + name: app1-cert + namespace: default +type: kubernetes.io/tls +--- +apiVersion: v1 +kind: Service +metadata: + labels: + app.kubernetes.io/instance: chartsnap + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: app1 + app.kubernetes.io/version: 1.15.0 + helm.sh/chart: app1-0.1.0 + name: chartsnap-app1 +spec: + ports: + - name: http + port: 80 + protocol: TCP + targetPort: http + selector: + app.kubernetes.io/instance: chartsnap + app.kubernetes.io/name: app1 + type: ClusterIP +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app.kubernetes.io/instance: chartsnap + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: app1 + app.kubernetes.io/version: 1.15.0 + helm.sh/chart: app1-0.1.0 + name: chartsnap-app1 +spec: + selector: + matchLabels: + app.kubernetes.io/instance: chartsnap + app.kubernetes.io/name: app1 + template: + metadata: + labels: + app.kubernetes.io/instance: chartsnap + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: app1 + app.kubernetes.io/version: 1.15.0 + helm.sh/chart: app1-0.1.0 + spec: + containers: + - image: nginx:1.15.0 + imagePullPolicy: IfNotPresent + livenessProbe: + httpGet: + path: / + port: http + name: app1 + ports: + - containerPort: 80 + name: http + protocol: TCP + readinessProbe: + httpGet: + path: / + port: http + resources: {} + securityContext: {} + securityContext: {} + serviceAccountName: chartsnap-app1 +--- +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + labels: + app.kubernetes.io/instance: chartsnap + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: app1 + app.kubernetes.io/version: 1.15.0 + helm.sh/chart: app1-0.1.0 + name: chartsnap-app1 +spec: + maxReplicas: 10 + metrics: + - resource: + name: cpu + target: + averageUtilization: 65 + type: Utilization + type: Resource + minReplicas: 1 + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: chartsnap-app1 +--- +apiVersion: v1 +kind: Pod +metadata: + annotations: + helm.sh/hook: test + labels: + app.kubernetes.io/instance: chartsnap + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: app1 + app.kubernetes.io/version: 1.15.0 + helm.sh/chart: app1-0.1.0 + name: chartsnap-app1-test-connection +spec: + containers: + - args: + - chartsnap-app1:80 + command: + - wget + image: busybox + name: wget + restartPolicy: Never diff --git a/pkg/snap/testdata/unstructured.snap b/pkg/charts/__snapshots__/helm_stub_snap_v1.toml similarity index 58% rename from pkg/snap/testdata/unstructured.snap rename to pkg/charts/__snapshots__/helm_stub_snap_v1.toml index 7009655..73dd991 100644 --- a/pkg/snap/testdata/unstructured.snap +++ b/pkg/charts/__snapshots__/helm_stub_snap_v1.toml @@ -1,4 +1,4 @@ -[default] +[snap_values] SnapShot = """ - object: apiVersion: apps/v1 @@ -12,7 +12,6 @@ SnapShot = """ helm.sh/chart: app1-0.1.0 name: chartsnap-app1 spec: - replicas: 1 selector: matchLabels: app.kubernetes.io/instance: chartsnap @@ -47,30 +46,8 @@ SnapShot = """ securityContext: {} serviceAccountName: chartsnap-app1 - object: - apiVersion: v1 - kind: Pod - metadata: - annotations: - helm.sh/hook: test - labels: - app.kubernetes.io/instance: chartsnap - app.kubernetes.io/managed-by: Helm - app.kubernetes.io/name: app1 - app.kubernetes.io/version: 1.16.0 - helm.sh/chart: app1-0.1.0 - name: chartsnap-app1-test-connection - spec: - containers: - - args: - - chartsnap-app1:80 - command: - - wget - image: busybox - name: wget - restartPolicy: Never -- object: - apiVersion: v1 - kind: Service + apiVersion: autoscaling/v2 + kind: HorizontalPodAutoscaler metadata: labels: app.kubernetes.io/instance: chartsnap @@ -80,77 +57,24 @@ SnapShot = """ helm.sh/chart: app1-0.1.0 name: chartsnap-app1 spec: - ports: - - name: http - port: 80 - protocol: TCP - targetPort: http - selector: - app.kubernetes.io/instance: chartsnap - app.kubernetes.io/name: app1 - type: ClusterIP + maxReplicas: 10 + metrics: + - resource: + name: cpu + target: + averageUtilization: 65 + type: Utilization + type: Resource + minReplicas: 1 + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: chartsnap-app1 - object: - apiVersion: v1 - automountServiceAccountToken: true - kind: ServiceAccount - metadata: - labels: - app.kubernetes.io/instance: chartsnap - app.kubernetes.io/managed-by: Helm - app.kubernetes.io/name: app1 - app.kubernetes.io/version: 1.16.0 - helm.sh/chart: app1-0.1.0 - name: chartsnap-app1 -""" - -[unstructured] -SnapShot = """ -- object: - apiVersion: apps/v1 - kind: Deployment - metadata: - labels: - app.kubernetes.io/instance: chartsnap - app.kubernetes.io/managed-by: Helm - app.kubernetes.io/name: app1 - app.kubernetes.io/version: 1.16.0 - helm.sh/chart: app1-0.1.0 - name: chartsnap-app1 - spec: - replicas: 1 - selector: - matchLabels: - app.kubernetes.io/instance: chartsnap - app.kubernetes.io/name: app1 - template: - metadata: - labels: - app.kubernetes.io/instance: chartsnap - app.kubernetes.io/managed-by: Helm - app.kubernetes.io/name: app1 - app.kubernetes.io/version: 1.16.0 - helm.sh/chart: app1-0.1.0 - spec: - containers: - - image: nginx:1.16.0 - imagePullPolicy: IfNotPresent - livenessProbe: - httpGet: - path: / - port: http - name: app1 - ports: - - containerPort: 80 - name: http - protocol: TCP - readinessProbe: - httpGet: - path: / - port: http - resources: {} - securityContext: {} - securityContext: {} - serviceAccountName: chartsnap-app1 + apiVersion: helm-chartsnap.jlandowner.dev/v1alpha1 + kind: Unknown + raw: | + this is warning message of helm - object: apiVersion: v1 kind: Pod @@ -173,6 +97,23 @@ SnapShot = """ image: busybox name: wget restartPolicy: Never +- object: + apiVersion: v1 + data: + ca.crt: IyMjRFlOQU1JQ19GSUVMRCMjIw== + tls.crt: IyMjRFlOQU1JQ19GSUVMRCMjIw== + tls.key: IyMjRFlOQU1JQ19GSUVMRCMjIw== + kind: Secret + metadata: + labels: + app.kubernetes.io/instance: chartsnap + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: app1 + app.kubernetes.io/version: 1.16.0 + helm.sh/chart: app1-0.1.0 + name: app1-cert + namespace: default + type: kubernetes.io/tls - object: apiVersion: v1 kind: Service @@ -193,7 +134,7 @@ SnapShot = """ selector: app.kubernetes.io/instance: chartsnap app.kubernetes.io/name: app1 - type: ClusterIP + type: '###DYNAMIC_FIELD###' - object: apiVersion: v1 automountServiceAccountToken: true diff --git a/pkg/charts/__snapshots__/helm_test.snap b/pkg/charts/__snapshots__/helm_test.snap new file mode 100644 index 0000000..34dc61a --- /dev/null +++ b/pkg/charts/__snapshots__/helm_test.snap @@ -0,0 +1,27 @@ +['Helm when Execute should execute with expected args and env 1'] +SnapShot = """ +Arguments for helm: template aaa ccc --namespace=bbb --values=ddd +Environment variables starting with HELM_: +HELM_DEBUG=false +""" + +['Helm when Execute with additional args should execute with expected args and env 1'] +SnapShot = """ +Arguments for helm: template chartsnap postgres --namespace=xxx --values=postgres.values.yaml --repo https://charts.bitnami.com/bitnami --skip-tests +Environment variables starting with HELM_: +HELM_DEBUG=false +""" + +['Helm when Execute without namespace should execute with expected args and env 1'] +SnapShot = """ +Arguments for helm: template chartsnap charts/app1/ --values=charts/app1/test/test.values.yaml +Environment variables starting with HELM_: +HELM_DEBUG=false +""" + +['Helm when Execute without values should execute with expected args and env 1'] +SnapShot = """ +Arguments for helm: template chartsnap charts/app1/ --namespace=default +Environment variables starting with HELM_: +HELM_DEBUG=false +""" diff --git a/pkg/charts/__snapshots__/snap_test.snap b/pkg/charts/__snapshots__/snap_test.snap new file mode 100644 index 0000000..5d0837d --- /dev/null +++ b/pkg/charts/__snapshots__/snap_test.snap @@ -0,0 +1,94 @@ +['Snap snapshot is v1 should return success response 1'] +SnapShot = """ + +""" + +['Snap snapshot matched should return success response 1'] +SnapShot = """ + +""" + +['Snap snapshot not matched should return unmatched response 1'] +SnapShot = """ +@@ KIND=ServiceAccount NAME=chartsnap-app1 LINE=14 + app.kubernetes.io/instance: chartsnap + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: app1 +- app.kubernetes.io/version: 1.15.0 ++ app.kubernetes.io/version: 1.16.0 + helm.sh/chart: app1-0.1.0 + name: chartsnap-app1 + --- + +@@ KIND=Secret NAME=app1-cert LINE=29 + app.kubernetes.io/instance: chartsnap + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: app1 +- app.kubernetes.io/version: 1.15.0 ++ app.kubernetes.io/version: 1.16.0 + helm.sh/chart: app1-0.1.0 + name: app1-cert + namespace: default + +@@ KIND=Service NAME=chartsnap-app1 LINE=42 + app.kubernetes.io/instance: chartsnap + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: app1 +- app.kubernetes.io/version: 1.15.0 ++ app.kubernetes.io/version: 1.16.0 + helm.sh/chart: app1-0.1.0 + name: chartsnap-app1 + spec: + +@@ KIND=Deployment NAME=chartsnap-app1 LINE=63 + app.kubernetes.io/instance: chartsnap + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: app1 +- app.kubernetes.io/version: 1.15.0 ++ app.kubernetes.io/version: 1.16.0 + helm.sh/chart: app1-0.1.0 + name: chartsnap-app1 + spec: + +@@ KIND=Deployment NAME=chartsnap-app1 LINE=77 + app.kubernetes.io/instance: chartsnap + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: app1 +- app.kubernetes.io/version: 1.15.0 ++ app.kubernetes.io/version: 1.16.0 + helm.sh/chart: app1-0.1.0 + spec: + containers: + +@@ KIND=Deployment NAME=chartsnap-app1 LINE=81 + helm.sh/chart: app1-0.1.0 + spec: + containers: +- - image: nginx:1.15.0 ++ - image: nginx:1.16.0 + imagePullPolicy: IfNotPresent + livenessProbe: + httpGet: + +@@ KIND=HorizontalPodAutoscaler NAME=chartsnap-app1 LINE=108 + app.kubernetes.io/instance: chartsnap + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: app1 +- app.kubernetes.io/version: 1.15.0 ++ app.kubernetes.io/version: 1.16.0 + helm.sh/chart: app1-0.1.0 + name: chartsnap-app1 + spec: + +@@ KIND=Pod NAME=chartsnap-app1-test-connection LINE=135 + app.kubernetes.io/instance: chartsnap + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: app1 +- app.kubernetes.io/version: 1.15.0 ++ app.kubernetes.io/version: 1.16.0 + helm.sh/chart: app1-0.1.0 + name: chartsnap-app1-test-connection + spec: + + +""" diff --git a/pkg/charts/__snapshots__/testspec_test.snap b/pkg/charts/__snapshots__/testspec_test.snap index 2aad563..9f16fa6 100644 --- a/pkg/charts/__snapshots__/testspec_test.snap +++ b/pkg/charts/__snapshots__/testspec_test.snap @@ -62,7 +62,7 @@ SnapShot = """ } ], \"securityContext\": {}, - \"serviceAccountName\": \"chartsnap-app1\" + \"serviceAccountName\": \"IyMjRFlOQU1JQ19GSUVMRCMjIw==\" } } } @@ -152,6 +152,46 @@ SnapShot = """ ] """ +['LoadSnapshotConfig when loading .chartsnap.yaml should load config 1'] +SnapShot = """ +{ + \"DynamicFields\": [ + { + \"Kind\": \"Secret\", + \"APIVersion\": \"v1\", + \"Name\": \"app1-cert\", + \"JSONPath\": [ + \"/data/ca.crt\", + \"/data/tls.crt\", + \"/data/tls.key\" + ], + \"Base64\": true + } + ] +} +""" + +['LoadSnapshotConfig when values.yaml has testSpec should load config 1'] +SnapShot = """ +{ + \"TestSpec\": { + \"DynamicFields\": [ + { + \"Kind\": \"Secret\", + \"APIVersion\": \"v1\", + \"Name\": \"app1-cert\", + \"JSONPath\": [ + \"/data/ca.crt\", + \"/data/tls.crt\", + \"/data/tls.key\" + ], + \"Base64\": true + } + ] + } +} +""" + ['Merge should merge dynamic fields 1'] SnapShot = """ { @@ -162,7 +202,26 @@ SnapShot = """ \"Name\": \"chartsnap-app1\", \"JSONPath\": [ \"/spec/ports/0/targetPort\" - ] + ], + \"Base64\": false + }, + { + \"Kind\": \"Pod\", + \"APIVersion\": \"v1\", + \"Name\": \"chartsnap-app1-test-connection\", + \"JSONPath\": [ + \"/metadata/name\" + ], + \"Base64\": false + }, + { + \"Kind\": \"service\", + \"APIVersion\": \"v1\", + \"Name\": \"chartsnap-app1\", + \"JSONPath\": [ + \"/spec/ports/0/targetPort\" + ], + \"Base64\": false }, { \"Kind\": \"Service\", @@ -170,15 +229,219 @@ SnapShot = """ \"Name\": \"chartsnap-app1\", \"JSONPath\": [ \"/spec/ports/1/targetPort\" - ] + ], + \"Base64\": false + } + ] +} +""" + +['TestSpec ApplyDynamicFields should replace specified fields 1'] +SnapShot = """ +[ + { + \"apiVersion\": \"apps/v1\", + \"kind\": \"Deployment\", + \"metadata\": { + \"labels\": { + \"app.kubernetes.io/instance\": \"chartsnap\", + \"app.kubernetes.io/managed-by\": \"Helm\", + \"app.kubernetes.io/name\": \"app1\", + \"app.kubernetes.io/version\": \"###DYNAMIC_FIELD###\", + \"helm.sh/chart\": \"app1-0.1.0\" + }, + \"name\": \"chartsnap-app1\" }, + \"spec\": { + \"replicas\": 1, + \"selector\": { + \"matchLabels\": { + \"app.kubernetes.io/instance\": \"chartsnap\", + \"app.kubernetes.io/name\": \"app1\" + } + }, + \"template\": { + \"metadata\": { + \"labels\": { + \"app.kubernetes.io/instance\": \"chartsnap\", + \"app.kubernetes.io/managed-by\": \"Helm\", + \"app.kubernetes.io/name\": \"app1\", + \"app.kubernetes.io/version\": \"1.16.0\", + \"helm.sh/chart\": \"app1-0.1.0\" + } + }, + \"spec\": { + \"containers\": [ + { + \"image\": \"nginx:1.16.0\", + \"imagePullPolicy\": \"IfNotPresent\", + \"livenessProbe\": { + \"httpGet\": { + \"path\": \"/\", + \"port\": \"http\" + } + }, + \"name\": \"app1\", + \"ports\": [ + { + \"containerPort\": 80, + \"name\": \"http\", + \"protocol\": \"TCP\" + } + ], + \"readinessProbe\": { + \"httpGet\": { + \"path\": \"/\", + \"port\": \"http\" + } + }, + \"resources\": {}, + \"securityContext\": {} + } + ], + \"securityContext\": {}, + \"serviceAccountName\": \"IyMjRFlOQU1JQ19GSUVMRCMjIw==\" + } + } + } + }, + { + \"apiVersion\": \"v1\", + \"kind\": \"Pod\", + \"metadata\": { + \"annotations\": { + \"helm.sh/hook\": \"test\" + }, + \"labels\": { + \"app.kubernetes.io/instance\": \"chartsnap\", + \"app.kubernetes.io/managed-by\": \"Helm\", + \"app.kubernetes.io/name\": \"app1\", + \"app.kubernetes.io/version\": \"1.16.0\", + \"helm.sh/chart\": \"app1-0.1.0\" + }, + \"name\": \"###DYNAMIC_FIELD###\" + }, + \"spec\": { + \"containers\": [ + { + \"args\": [ + \"chartsnap-app1:80\" + ], + \"command\": [ + \"wget\" + ], + \"image\": \"busybox\", + \"name\": \"wget\" + } + ], + \"restartPolicy\": \"Never\" + } + }, + { + \"apiVersion\": \"v1\", + \"kind\": \"Service\", + \"metadata\": { + \"labels\": { + \"app.kubernetes.io/instance\": \"chartsnap\", + \"app.kubernetes.io/managed-by\": \"Helm\", + \"app.kubernetes.io/name\": \"app1\", + \"app.kubernetes.io/version\": \"1.16.0\", + \"helm.sh/chart\": \"app1-0.1.0\" + }, + \"name\": \"chartsnap-app1\" + }, + \"spec\": { + \"ports\": [ + { + \"name\": \"http\", + \"port\": 80, + \"protocol\": \"TCP\", + \"targetPort\": \"http\" + }, + { + \"name\": \"https\", + \"port\": 443, + \"protocol\": \"TCP\", + \"targetPort\": \"###DYNAMIC_FIELD###\" + } + ], + \"selector\": { + \"app.kubernetes.io/instance\": \"chartsnap\", + \"app.kubernetes.io/name\": \"app1\" + }, + \"type\": \"ClusterIP\" + } + }, + { + \"apiVersion\": \"v1\", + \"automountServiceAccountToken\": true, + \"kind\": \"ServiceAccount\", + \"metadata\": { + \"labels\": { + \"app.kubernetes.io/instance\": \"chartsnap\", + \"app.kubernetes.io/managed-by\": \"Helm\", + \"app.kubernetes.io/name\": \"app1\", + \"app.kubernetes.io/version\": \"1.16.0\", + \"helm.sh/chart\": \"app1-0.1.0\" + }, + \"name\": \"chartsnap-app1\" + } + } +] +""" + +['TestSpec LoadSnapshotConfig when loading .chartsnap.yaml should load config 1'] +SnapShot = """ +{ + \"DynamicFields\": [ + { + \"Kind\": \"Secret\", + \"APIVersion\": \"v1\", + \"Name\": \"app1-cert\", + \"JSONPath\": [ + \"/data/ca.crt\", + \"/data/tls.crt\", + \"/data/tls.key\" + ], + \"Base64\": true + } + ] +} +""" + +['TestSpec LoadSnapshotConfig when values.yaml has testSpec should load config 1'] +SnapShot = """ +{ + \"TestSpec\": { + \"DynamicFields\": [ + { + \"Kind\": \"Secret\", + \"APIVersion\": \"v1\", + \"Name\": \"app1-cert\", + \"JSONPath\": [ + \"/data/ca.crt\", + \"/data/tls.crt\", + \"/data/tls.key\" + ], + \"Base64\": true + } + ] + } +} +""" + +['TestSpec Merge should merge dynamic fields 1'] +SnapShot = """ +{ + \"DynamicFields\": [ { \"Kind\": \"service\", \"APIVersion\": \"v1\", \"Name\": \"chartsnap-app1\", \"JSONPath\": [ \"/spec/ports/0/targetPort\" - ] + ], + \"Base64\": false }, { \"Kind\": \"Pod\", @@ -186,7 +449,26 @@ SnapShot = """ \"Name\": \"chartsnap-app1-test-connection\", \"JSONPath\": [ \"/metadata/name\" - ] + ], + \"Base64\": false + }, + { + \"Kind\": \"service\", + \"APIVersion\": \"v1\", + \"Name\": \"chartsnap-app1\", + \"JSONPath\": [ + \"/spec/ports/0/targetPort\" + ], + \"Base64\": false + }, + { + \"Kind\": \"Service\", + \"APIVersion\": \"v1\", + \"Name\": \"chartsnap-app1\", + \"JSONPath\": [ + \"/spec/ports/1/targetPort\" + ], + \"Base64\": false } ] } diff --git a/pkg/charts/helm_test.go b/pkg/charts/helm_test.go index 78e2c6c..c75be5a 100644 --- a/pkg/charts/helm_test.go +++ b/pkg/charts/helm_test.go @@ -1,62 +1,74 @@ package charts import ( - "reflect" - "testing" + "context" + + . "github.com/jlandowner/helm-chartsnap/pkg/snap/gomega" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" ) -func TestHelmTemplateCmdOptions_Args(t *testing.T) { - type fields struct { - HelmPath string - ReleaseName string - Namespace string - Chart string - ValuesFile string - AdditionalArgs []string - } - tests := []struct { - name string - fields fields - want []string - }{ - { - name: "default", - fields: fields{ - HelmPath: "helm", +var _ = Describe("Helm", func() { + Context("when Execute", func() { + It("should execute with expected args and env", func() { + o := &HelmTemplateCmdOptions{ + HelmPath: "./testdata/helm_cmd.bash", + ReleaseName: "aaa", + Namespace: "bbb", + Chart: "ccc", + ValuesFile: "ddd", + } + + out, err := o.Execute(context.Background()) + Expect(err).NotTo(HaveOccurred()) + Expect(out).To(MatchSnapShot()) + }) + }) + + Context("when Execute without namespace", func() { + It("should execute with expected args and env", func() { + o := &HelmTemplateCmdOptions{ + HelmPath: "./testdata/helm_cmd.bash", ReleaseName: "chartsnap", - Namespace: "default", Chart: "charts/app1/", ValuesFile: "charts/app1/test/test.values.yaml", - }, - want: []string{"template", "chartsnap", "charts/app1/", "--namespace=default", "--values=charts/app1/test/test.values.yaml"}, - }, - { - name: "additional args", - fields: fields{ - HelmPath: "helm", + } + + out, err := o.Execute(context.Background()) + Expect(err).NotTo(HaveOccurred()) + Expect(out).To(MatchSnapShot()) + }) + }) + + Context("when Execute without values", func() { + It("should execute with expected args and env", func() { + o := &HelmTemplateCmdOptions{ + HelmPath: "./testdata/helm_cmd.bash", + ReleaseName: "chartsnap", + Namespace: "default", + Chart: "charts/app1/", + } + + out, err := o.Execute(context.Background()) + Expect(err).NotTo(HaveOccurred()) + Expect(out).To(MatchSnapShot()) + }) + }) + + Context("when Execute with additional args", func() { + It("should execute with expected args and env", func() { + o := &HelmTemplateCmdOptions{ + HelmPath: "./testdata/helm_cmd.bash", ReleaseName: "chartsnap", Namespace: "xxx", Chart: "postgres", ValuesFile: "postgres.values.yaml", AdditionalArgs: []string{"--repo", "https://charts.bitnami.com/bitnami", "--skip-tests"}, - }, - want: []string{"template", "chartsnap", "postgres", "--namespace=xxx", "--values=postgres.values.yaml", "--repo", "https://charts.bitnami.com/bitnami", "--skip-tests"}, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - o := &HelmTemplateCmdOptions{ - HelmPath: tt.fields.HelmPath, - ReleaseName: tt.fields.ReleaseName, - Namespace: tt.fields.Namespace, - Chart: tt.fields.Chart, - ValuesFile: tt.fields.ValuesFile, - AdditionalArgs: tt.fields.AdditionalArgs, - } - got := o.Args() - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("HelmTemplateCmdOptions.Args() = %v, want %v", got, tt.want) } + + out, err := o.Execute(context.Background()) + Expect(err).NotTo(HaveOccurred()) + Expect(out).To(MatchSnapShot()) }) - } -} + }) +}) diff --git a/pkg/charts/snap.go b/pkg/charts/snap.go index bd48983..a87f431 100644 --- a/pkg/charts/snap.go +++ b/pkg/charts/snap.go @@ -8,9 +8,16 @@ import ( "path" "strings" + metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "github.com/jlandowner/helm-chartsnap/pkg/snap" - "github.com/jlandowner/helm-chartsnap/pkg/unstructured" - yaml "sigs.k8s.io/yaml/goyaml.v3" + unstV2 "github.com/jlandowner/helm-chartsnap/pkg/unstructured" + unstV1 "github.com/jlandowner/helm-chartsnap/pkg/unstructured/v1" +) + +const ( + SnapshotVersionV1 = "v1" + SnapshotVersionV2 = "v2" ) var logger *slog.Logger @@ -26,70 +33,107 @@ func log() *slog.Logger { return logger } -type ChartSnapOptions struct { +type ChartSnapshotter struct { HelmTemplateCmdOptions HelmTemplateCmdOptions SnapshotConfig SnapshotConfig SnapshotFile string + SnapshotVersion string DiffContextLineN int } -func Snap(ctx context.Context, o ChartSnapOptions) (match bool, failureMessage string, err error) { +type SnapshotResult struct { + Match bool + FailureMessage string +} + +func (o *ChartSnapshotter) Snap(ctx context.Context) (result *SnapshotResult, err error) { + // override snapshot config within values file's test spec sv := SnapshotValues{} if o.HelmTemplateCmdOptions.ValuesFile != "" { - f, err := os.Open(o.HelmTemplateCmdOptions.ValuesFile) + err = LoadSnapshotConfig(o.HelmTemplateCmdOptions.ValuesFile, &sv) if err != nil { - return match, "", fmt.Errorf("failed to open values file: %w", err) - } - defer f.Close() - - err = yaml.NewDecoder(f).Decode(&sv) - if err != nil { - return match, "", fmt.Errorf("failed to decode values file: %w", err) + return nil, fmt.Errorf("failed to decode values file: %w", err) } } - - // merge snapshot config file and config in snapshot values file + log().Debug("loaded values", "values", sv, "path", o.SnapshotFile) sv.TestSpec.Merge(o.SnapshotConfig) - log().Debug("test spec from values file", "spec", sv.TestSpec) - + // execute helm template command out, err := o.HelmTemplateCmdOptions.Execute(ctx) if err != nil { - return match, "", fmt.Errorf("'helm template' command failed: %w: %s", err, out) + return nil, fmt.Errorf("'helm template' command failed: %w: %s", err, out) } - log().Debug("helm template output", "output", string(out)) + log().Debug("helm template output", "output", string(out), "path", o.SnapshotFile) - manifests, decodeErrs := unstructured.Decode(string(out)) + // decode helm output + manifests, decodeErrs := unstV2.Decode(string(out)) if len(decodeErrs) > 0 { for _, err := range decodeErrs { - log().Info("loading helm output is done with warning") + log().Info("WARNING: loading helm output is done with warning") fmt.Println(err) } } + // apply fixed values to dynamic fields if err := sv.TestSpec.ApplyFixedValue(manifests); err != nil { - return match, "", fmt.Errorf("failed to replace json path: %w", err) + return nil, fmt.Errorf("failed to replace json path: %w", err) } + // if snapshot file is v1 format, fallback to v1 snapshot matcher + if snap.IsMultiSnapshots(o.SnapshotFile) { + o.SnapshotVersion = SnapshotVersionV1 + } + + // take snapshot + if o.SnapshotVersion == SnapshotVersionV1 { + log().Info("WARNING: legacy format snapshot. it will be deprecated in the future version. please update the snapshots to the latest format", "path", o.SnapshotFile) + return o.snapV1(manifests) + } else { + return o.snapV2(manifests) + } +} + +func (o *ChartSnapshotter) snapV1(manifests []metaV1.Unstructured) (result *SnapshotResult, err error) { snap.SetLogger(log()) - s := snap.UnstructuredSnapShotMatcher( - o.SnapshotFile, - SnapshotID(o.HelmTemplateCmdOptions.ValuesFile), - snap.WithDiffContextLineN(o.DiffContextLineN)) - match, err = snap.UnstructuredMatch(s, manifests) + raw, err := unstV1.Encode(manifests) if err != nil { - return match, "", fmt.Errorf("failed to get snapshot: %w", err) + return nil, fmt.Errorf("failed to encode manifests: %w", err) } - return match, s.FailureMessage(nil), nil + + // v1 snapshot is multi snapshot format with encoding legacy formatted yaml + matcher := snap.SnapshotMatcher(o.SnapshotFile, + snap.WithSnapshotID(SnapshotFileName(o.HelmTemplateCmdOptions.ValuesFile)), + snap.WithDiffFunc((&unstV1.DiffOptions{ContextLineN: o.DiffContextLineN}).Diff)) + + match, err := matcher.Match(raw) + if err != nil { + return nil, fmt.Errorf("failed to get snapshot: %w", err) + } + return &SnapshotResult{ + Match: match, + FailureMessage: matcher.FailureMessage(nil), + }, nil } -func SnapshotID(valuesFile string) string { - if valuesFile != "" { - return strings.ReplaceAll(path.Base(valuesFile), ".yaml", "") - } else { - return "default" +func (o *ChartSnapshotter) snapV2(manifests []metaV1.Unstructured) (result *SnapshotResult, err error) { + snap.SetLogger(log()) + unstV2.SetLogger(log()) + + raw, err := unstV2.Encode(manifests) + if err != nil { + return nil, fmt.Errorf("failed to encode manifests: %w", err) + } + matcher := snap.SnapshotMatcher(o.SnapshotFile, snap.WithDiffFunc((&unstV2.DiffOptions{ContextLineN: o.DiffContextLineN}).Diff)) + + match, err := matcher.Match(raw) + if err != nil { + return nil, fmt.Errorf("failed to get snapshot: %w", err) } + return &SnapshotResult{ + Match: match, + FailureMessage: matcher.FailureMessage(nil), + }, nil } func DefaultSnapshotFilePath(chartPath, valuesFile string) string { @@ -106,6 +150,14 @@ func DefaultSnapshotFilePath(chartPath, valuesFile string) string { } } +func SnapshotFileName(valuesFile string) string { + if valuesFile != "" { + return strings.ReplaceAll(path.Base(valuesFile), ".yaml", "") + } else { + return "default" + } +} + func SnapshotFilePath(dir, valuesFile string) string { - return path.Join(dir, "__snapshots__", SnapshotID(valuesFile)+".snap") + return path.Join(dir, "__snapshots__", SnapshotFileName(valuesFile)+".snap") } diff --git a/pkg/charts/snap_test.go b/pkg/charts/snap_test.go index 5b9c1ce..2c7725f 100644 --- a/pkg/charts/snap_test.go +++ b/pkg/charts/snap_test.go @@ -1,10 +1,105 @@ package charts import ( + "context" "testing" + + . "github.com/jlandowner/helm-chartsnap/pkg/snap/gomega" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" ) -func TestSnapshotID(t *testing.T) { +var _ = Describe("Snap", func() { + Context("snapshot matched", func() { + It("should return success response", func() { + ss := &ChartSnapshotter{ + HelmTemplateCmdOptions: HelmTemplateCmdOptions{ + HelmPath: "./testdata/helm_stub.bash", + ReleaseName: "aaa", + Namespace: "bbb", + Chart: "ccc", + ValuesFile: "./testdata/snap_values.yaml", + }, + SnapshotConfig: SnapshotConfig{ + DynamicFields: []ManifestPath{ + { + APIVersion: "v1", + Kind: "Service", + Name: "chartsnap-app1", + JSONPath: []string{ + "/spec/type", + }, + }, + }, + }, + SnapshotFile: "__snapshots__/helm_stub_snap.yaml", + SnapshotVersion: "", + DiffContextLineN: 3, + } + res, err := ss.Snap(context.Background()) + Expect(err).NotTo(HaveOccurred()) + Expect(res.Match).To(BeTrue()) + Expect(res.FailureMessage).To(MatchSnapShot()) + }) + }) + + Context("snapshot is v1", func() { + It("should return success response", func() { + ss := &ChartSnapshotter{ + HelmTemplateCmdOptions: HelmTemplateCmdOptions{ + HelmPath: "./testdata/helm_stub.bash", + ReleaseName: "aaa", + Namespace: "bbb", + Chart: "ccc", + ValuesFile: "./testdata/snap_values.yaml", + }, + SnapshotConfig: SnapshotConfig{ + DynamicFields: []ManifestPath{ + { + APIVersion: "v1", + Kind: "Service", + Name: "chartsnap-app1", + JSONPath: []string{ + "/spec/type", + }, + }, + }, + }, + SnapshotFile: "__snapshots__/helm_stub_snap_v1.toml", + SnapshotVersion: "v1", + DiffContextLineN: 3, + } + res, err := ss.Snap(context.Background()) + Expect(err).NotTo(HaveOccurred()) + Expect(res.Match).To(BeTrue()) + Expect(res.FailureMessage).To(MatchSnapShot()) + }) + }) + + Context("snapshot not matched", func() { + It("should return unmatched response", func() { + ss := &ChartSnapshotter{ + HelmTemplateCmdOptions: HelmTemplateCmdOptions{ + HelmPath: "./testdata/helm_stub.bash", + ReleaseName: "aaa", + Namespace: "bbb", + Chart: "ccc", + ValuesFile: "./testdata/snap_values.yaml", + }, + SnapshotFile: "__snapshots__/helm_stub_snap_unmatch.yaml", + SnapshotVersion: "", + DiffContextLineN: 3, + } + res, err := ss.Snap(context.Background()) + Expect(err).NotTo(HaveOccurred()) + Expect(res.Match).To(BeFalse()) + Expect(res.FailureMessage).To(MatchSnapShot()) + }) + }) + +}) + +func TestSnapshotFileName(t *testing.T) { type args struct { valuesFile string } @@ -37,8 +132,8 @@ func TestSnapshotID(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if got := SnapshotID(tt.args.valuesFile); got != tt.want { - t.Errorf("SnapshotID() = %v, want %v", got, tt.want) + if got := SnapshotFileName(tt.args.valuesFile); got != tt.want { + t.Errorf("SnapshotFileName() = %v, want %v", got, tt.want) } }) } diff --git a/pkg/charts/testdata/.chartsnap.yaml b/pkg/charts/testdata/.chartsnap.yaml new file mode 100644 index 0000000..f02d3eb --- /dev/null +++ b/pkg/charts/testdata/.chartsnap.yaml @@ -0,0 +1,9 @@ +dynamicFields: + - apiVersion: v1 + kind: Secret + name: app1-cert + jsonPath: + - /data/ca.crt + - /data/tls.crt + - /data/tls.key + base64: true diff --git a/pkg/charts/testdata/helm_cmd.bash b/pkg/charts/testdata/helm_cmd.bash new file mode 100755 index 0000000..23d3a09 --- /dev/null +++ b/pkg/charts/testdata/helm_cmd.bash @@ -0,0 +1,8 @@ +#!/bin/bash + +# Output arguments +echo "Arguments for helm: $@" + +# Output environment variables starting with "HELM_" +echo "Environment variables starting with HELM_:" +env | grep '^HELM_' \ No newline at end of file diff --git a/pkg/charts/testdata/helm_stub.bash b/pkg/charts/testdata/helm_stub.bash new file mode 100755 index 0000000..a11fff6 --- /dev/null +++ b/pkg/charts/testdata/helm_stub.bash @@ -0,0 +1,149 @@ +#!/bin/bash + + + +cat < merged.DiffContextLineN { - merged.DiffContextLineN = v.DiffContextLineN - } - } - return merged -} - -func Diff(x, y string, o DiffOptions) string { - diffs := difflib.Diff(strings.Split(x, "\n"), strings.Split(y, "\n")) - - var ( - sb strings.Builder - isDiffSequence bool - ) - - for i, v := range diffs { - if o.ContextLineN() < 1 { - // all records - sb.WriteString(diffString(v)) - continue - } - - if v.Delta != difflib.Common { - isDiffSequence = true - - // if first diff, add a header and previous lines - if i > 0 && diffs[i-1].Delta == difflib.Common { - // header - sb.WriteString(color.New(color.FgCyan).Sprintf("--- line=%d\n", i)) - - // previous lines - for j := intInRange(0, len(diffs), i-o.DiffContextLineN); j < i; j++ { - sb.WriteString(fmt.Sprintf("%s\n", diffs[j])) - } - } - sb.WriteString(diffString(v)) - } else { - if isDiffSequence { - isDiffSequence = false - - // subsequent lines - for j := i; j < intInRange(0, len(diffs), i+o.DiffContextLineN); j++ { - sb.WriteString(fmt.Sprintf("%s\n", diffs[j])) - } - // divider - sb.WriteString("\n") - } - } - } - return sb.String() -} - -func intInRange(min, max, v int) int { - if v >= min && v <= max { - return v - } else if v < min { - return min - } else { - return max - } -} - -func diffString(d difflib.DiffRecord) string { - switch d.Delta { - case difflib.LeftOnly: - return color.New(color.FgRed).Sprintf("%s\n", d) - case difflib.RightOnly: - return color.New(color.FgGreen).Sprintf("%s\n", d) - default: - return fmt.Sprintf("%s\n", d) - } -} diff --git a/pkg/snap/diff_test.go b/pkg/snap/diff_test.go deleted file mode 100644 index 86a7eea..0000000 --- a/pkg/snap/diff_test.go +++ /dev/null @@ -1,226 +0,0 @@ -package snap - -import ( - "io" - "os" - "reflect" - "testing" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - - "github.com/aryann/difflib" -) - -var _ = Describe("Snapshot", func() { - f := func(m OmegaMatcher, filePath string) (success bool, err error) { - f, err := os.Open(filePath) - Expect(err).NotTo(HaveOccurred()) - defer f.Close() - - buf, err := io.ReadAll(f) - Expect(err).NotTo(HaveOccurred()) - - return m.Match(string(buf)) - } - - It("should match", func() { - m := SnapShotMatcher("testdata/diff.snap", "default") - success, err := f(m, "testdata/diff.txt") - Expect(err).NotTo(HaveOccurred()) - Expect(success).To(BeTrue()) - }) - - It("should not match and output diff N=1", func() { - m := SnapShotMatcher("testdata/diff.snap", "default", WithDiffContextLineN(1)) - success, err := f(m, "testdata/diff_diff.txt") - Expect(err).NotTo(HaveOccurred()) - Expect(success).To(BeFalse()) - - Expect(m.FailureMessage(nil)).Should(Equal(`Expected to match ---- line=13 - --ca-file string verify certificates of HTTPS-enabled servers using this CA bundle -- --cert-file string identify HTTPS client using this SSL certificate file - --create-namespace create the release namespace if not present - ---- line=23 - -g, --generate-name generate the name (and omit the NAME parameter) -- -h, --help help for template -+ -h, --help help for templates - --include-crds include CRDs in the templated output - ---- line=56 - -f, --values strings specify values in a YAML file or a URL (can specify multiple) -+ --fake fake - --verify verify the package before using it - - -`)) - }) -}) - -func TestDiffOptions_ContextLineN(t *testing.T) { - type fields struct { - DiffContextLineN int - } - tests := []struct { - name string - fields fields - want int - }{ - { - name: "zero", - fields: fields{DiffContextLineN: -1}, - want: 0, - }, - { - name: "default", - fields: fields{DiffContextLineN: 3}, - want: 3, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - o := &DiffOptions{ - DiffContextLineN: tt.fields.DiffContextLineN, - } - if got := o.ContextLineN(); got != tt.want { - t.Errorf("DiffOptions.ContextLineN() = %v, want %v", got, tt.want) - } - }) - } -} - -func Test_mergeDiffOpts(t *testing.T) { - type args struct { - opts []DiffOptions - } - tests := []struct { - name string - args args - want DiffOptions - }{ - { - name: "single: 0", - args: args{opts: []DiffOptions{{DiffContextLineN: 0}}}, - want: DiffOptions{DiffContextLineN: 0}, - }, - { - name: "single: 3", - args: args{opts: []DiffOptions{{DiffContextLineN: 3}}}, - want: DiffOptions{DiffContextLineN: 3}, - }, - { - name: "multiple: max 1", - args: args{ - opts: []DiffOptions{ - {DiffContextLineN: 3}, - {DiffContextLineN: 2}, - }, - }, - want: DiffOptions{DiffContextLineN: 3}, - }, - { - name: "multiple: max 2", - args: args{ - opts: []DiffOptions{ - {DiffContextLineN: 2}, - {DiffContextLineN: 3}, - }, - }, - want: DiffOptions{DiffContextLineN: 3}, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := mergeDiffOpts(tt.args.opts); !reflect.DeepEqual(got, tt.want) { - t.Errorf("mergeDiffOpts() = %v, want %v", got, tt.want) - } - }) - } -} - -func Test_intInRange(t *testing.T) { - type args struct { - min int - max int - v int - } - tests := []struct { - name string - args args - want int - }{ - { - name: "min", - args: args{min: 0, max: 10, v: -1}, - want: 0, - }, - { - name: "max", - args: args{min: 0, max: 10, v: 11}, - want: 10, - }, - { - name: "in range", - args: args{min: 0, max: 10, v: 5}, - want: 5, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := intInRange(tt.args.min, tt.args.max, tt.args.v); got != tt.want { - t.Errorf("intInRange() = %v, want %v", got, tt.want) - } - }) - } -} - -func Test_diffString(t *testing.T) { - type args struct { - d difflib.DiffRecord - } - tests := []struct { - name string - args args - want string - }{ - { - name: "+", - args: args{ - difflib.DiffRecord{ - Payload: "###", - Delta: difflib.RightOnly, - }, - }, - want: "+ ###\n", - }, - { - name: "-", - args: args{ - difflib.DiffRecord{ - Payload: "###", - Delta: difflib.LeftOnly, - }, - }, - want: "- ###\n", - }, - { - name: "eq", - args: args{ - difflib.DiffRecord{ - Payload: "###", - Delta: difflib.Common, - }, - }, - want: " ###\n", - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := diffString(tt.args.d); got != tt.want { - t.Errorf("diffString() = %v, want %v", got, tt.want) - } - }) - } -} diff --git a/pkg/snap/gomega/__snapshots__/snap_test.snap b/pkg/snap/gomega/__snapshots__/snap_test.snap new file mode 100644 index 0000000..caa044a --- /dev/null +++ b/pkg/snap/gomega/__snapshots__/snap_test.snap @@ -0,0 +1,600 @@ +['Snap takes a full snapshot 1'] +SnapShot = """ +apiVersion: v1 +kind: Pod +metadata: + annotations: + kubectl.kubernetes.io/restartedAt: \"2023-02-26T13:52:41Z\" + creationTimestamp: \"2023-12-16T15:22:57Z\" + generateName: ingress-nginx-controller- + labels: + app.kubernetes.io/component: controller + app.kubernetes.io/instance: ingress-nginx + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: ingress-nginx + app.kubernetes.io/part-of: ingress-nginx + app.kubernetes.io/version: 1.9.4 + controller-revision-hash: 58c9997466 + helm.sh/chart: ingress-nginx-4.8.4 + pod-template-generation: \"5\" + name: ingress-nginx-controller-2rqc5 + namespace: ingress-nginx + ownerReferences: + - apiVersion: apps/v1 + blockOwnerDeletion: true + controller: true + kind: DaemonSet + name: ingress-nginx-controller + uid: xxxxx + resourceVersion: \"11111111\" + uid: xxxxx +spec: + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchFields: + - key: metadata.name + operator: In + values: + - usagi + containers: + - args: + - /nginx-ingress-controller + - --publish-service=$(POD_NAMESPACE)/ingress-nginx-controller + - --election-id=ingress-nginx-leader + - --controller-class=k8s.io/ingress-nginx + - --ingress-class=nginx + - --configmap=$(POD_NAMESPACE)/ingress-nginx-controller + - --validating-webhook=:8443 + - --validating-webhook-certificate=/usr/local/certificates/cert + - --validating-webhook-key=/usr/local/certificates/key + env: + - name: POD_NAME + valueFrom: + fieldRef: + apiVersion: v1 + fieldPath: metadata.name + - name: POD_NAMESPACE + valueFrom: + fieldRef: + apiVersion: v1 + fieldPath: metadata.namespace + - name: LD_PRELOAD + value: /usr/local/lib/libmimalloc.so + image: registry.k8s.io/ingress-nginx/controller:v1.9.4@sha256:5b161f051d017e55d358435f295f5e9a297e66158f136321d9b04520ec6c48a3 + imagePullPolicy: IfNotPresent + lifecycle: + preStop: + exec: + command: + - /wait-shutdown + livenessProbe: + failureThreshold: 5 + httpGet: + path: /healthz + port: 10254 + scheme: HTTP + initialDelaySeconds: 10 + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 + name: controller + ports: + - containerPort: 80 + hostPort: 80 + name: http + protocol: TCP + - containerPort: 443 + hostPort: 443 + name: https + protocol: TCP + - containerPort: 8443 + hostPort: 8443 + name: webhook + protocol: TCP + readinessProbe: + failureThreshold: 3 + httpGet: + path: /healthz + port: 10254 + scheme: HTTP + initialDelaySeconds: 10 + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 + resources: + requests: + cpu: 100m + memory: 90Mi + securityContext: + allowPrivilegeEscalation: false + capabilities: + add: + - NET_BIND_SERVICE + drop: + - ALL + readOnlyRootFilesystem: false + runAsNonRoot: true + runAsUser: 101 + seccompProfile: + type: RuntimeDefault + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + volumeMounts: + - mountPath: /usr/local/certificates/ + name: webhook-cert + readOnly: true + - mountPath: /var/run/secrets/kubernetes.io/serviceaccount + name: kube-api-access-c92mq + readOnly: true + dnsPolicy: ClusterFirst + enableServiceLinks: true + hostNetwork: true + nodeName: usagi + nodeSelector: + kubernetes.io/os: linux + preemptionPolicy: PreemptLowerPriority + priority: 0 + restartPolicy: Always + schedulerName: default-scheduler + securityContext: {} + serviceAccount: ingress-nginx + serviceAccountName: ingress-nginx + terminationGracePeriodSeconds: 300 + tolerations: + - effect: NoExecute + key: node.kubernetes.io/not-ready + operator: Exists + - effect: NoExecute + key: node.kubernetes.io/unreachable + operator: Exists + - effect: NoSchedule + key: node.kubernetes.io/disk-pressure + operator: Exists + - effect: NoSchedule + key: node.kubernetes.io/memory-pressure + operator: Exists + - effect: NoSchedule + key: node.kubernetes.io/pid-pressure + operator: Exists + - effect: NoSchedule + key: node.kubernetes.io/unschedulable + operator: Exists + - effect: NoSchedule + key: node.kubernetes.io/network-unavailable + operator: Exists + volumes: + - name: webhook-cert + secret: + defaultMode: 420 + items: + - key: tls.crt + path: cert + - key: tls.key + path: key + secretName: ingress-nginx-admission + - name: kube-api-access-c92mq + projected: + defaultMode: 420 + sources: + - serviceAccountToken: + expirationSeconds: 3607 + path: token + - configMap: + items: + - key: ca.crt + path: ca.crt + name: kube-root-ca.crt + - downwardAPI: + items: + - fieldRef: + apiVersion: v1 + fieldPath: metadata.namespace + path: namespace +status: + conditions: + - lastProbeTime: null + lastTransitionTime: \"2023-12-16T15:22:57Z\" + status: \"True\" + type: Initialized + - lastProbeTime: null + lastTransitionTime: \"2024-03-25T07:14:16Z\" + status: \"True\" + type: Ready + - lastProbeTime: null + lastTransitionTime: \"2024-03-25T07:14:16Z\" + status: \"True\" + type: ContainersReady + - lastProbeTime: null + lastTransitionTime: \"2023-12-16T15:22:57Z\" + status: \"True\" + type: PodScheduled + containerStatuses: + - containerID: containerd://xxx + image: sha256:5aa0bf4798fa2300b97564cc77480e6d0abac88f8bdc001c01eb4ab3b98b2fbf + imageID: registry.k8s.io/ingress-nginx/controller@sha256:5b161f051d017e55d358435f295f5e9a297e66158f136321d9b04520ec6c48a3 + lastState: + terminated: + containerID: containerd://xxx + exitCode: 255 + finishedAt: \"2024-03-25T07:08:20Z\" + reason: Unknown + startedAt: \"2024-02-12T08:14:25Z\" + name: controller + ready: true + restartCount: 4 + started: true + state: + running: + startedAt: \"2024-03-25T07:13:37Z\" + hostIP: 192.168.0.10 + phase: Running + podIP: 192.168.0.10 + podIPs: + - ip: 192.168.0.10 + qosClass: Burstable + startTime: \"2023-12-16T15:22:57Z\" +""" + +['Snap takes a snapshot without dynamic values 1'] +SnapShot = """ +{ + \"apiVersion\": \"v1\", + \"kind\": \"Pod\", + \"metadata\": { + \"annotations\": { + \"kubectl.kubernetes.io/restartedAt\": \"2023-02-26T13:52:41Z\" + }, + \"generateName\": \"ingress-nginx-controller-\", + \"labels\": { + \"app.kubernetes.io/component\": \"controller\", + \"app.kubernetes.io/instance\": \"ingress-nginx\", + \"app.kubernetes.io/managed-by\": \"Helm\", + \"app.kubernetes.io/name\": \"ingress-nginx\", + \"app.kubernetes.io/part-of\": \"ingress-nginx\", + \"app.kubernetes.io/version\": \"1.9.4\", + \"controller-revision-hash\": \"58c9997466\", + \"helm.sh/chart\": \"ingress-nginx-4.8.4\", + \"pod-template-generation\": \"5\" + }, + \"name\": \"ingress-nginx-controller-2rqc5\", + \"namespace\": \"ingress-nginx\", + \"ownerReferences\": [ + { + \"apiVersion\": \"apps/v1\", + \"blockOwnerDeletion\": true, + \"controller\": true, + \"kind\": \"DaemonSet\", + \"name\": \"ingress-nginx-controller\", + \"uid\": \"\" + } + ] + }, + \"spec\": { + \"affinity\": { + \"nodeAffinity\": { + \"requiredDuringSchedulingIgnoredDuringExecution\": { + \"nodeSelectorTerms\": [ + { + \"matchFields\": [ + { + \"key\": \"metadata.name\", + \"operator\": \"In\", + \"values\": [ + \"usagi\" + ] + } + ] + } + ] + } + } + }, + \"containers\": [ + { + \"args\": [ + \"/nginx-ingress-controller\", + \"--publish-service=$(POD_NAMESPACE)/ingress-nginx-controller\", + \"--election-id=ingress-nginx-leader\", + \"--controller-class=k8s.io/ingress-nginx\", + \"--ingress-class=nginx\", + \"--configmap=$(POD_NAMESPACE)/ingress-nginx-controller\", + \"--validating-webhook=:8443\", + \"--validating-webhook-certificate=/usr/local/certificates/cert\", + \"--validating-webhook-key=/usr/local/certificates/key\" + ], + \"env\": [ + { + \"name\": \"POD_NAME\", + \"valueFrom\": { + \"fieldRef\": { + \"apiVersion\": \"v1\", + \"fieldPath\": \"metadata.name\" + } + } + }, + { + \"name\": \"POD_NAMESPACE\", + \"valueFrom\": { + \"fieldRef\": { + \"apiVersion\": \"v1\", + \"fieldPath\": \"metadata.namespace\" + } + } + }, + { + \"name\": \"LD_PRELOAD\", + \"value\": \"/usr/local/lib/libmimalloc.so\" + } + ], + \"image\": \"registry.k8s.io/ingress-nginx/controller:v1.9.4@sha256:5b161f051d017e55d358435f295f5e9a297e66158f136321d9b04520ec6c48a3\", + \"imagePullPolicy\": \"IfNotPresent\", + \"lifecycle\": { + \"preStop\": { + \"exec\": { + \"command\": [ + \"/wait-shutdown\" + ] + } + } + }, + \"livenessProbe\": { + \"failureThreshold\": 5, + \"httpGet\": { + \"path\": \"/healthz\", + \"port\": 10254, + \"scheme\": \"HTTP\" + }, + \"initialDelaySeconds\": 10, + \"periodSeconds\": 10, + \"successThreshold\": 1, + \"timeoutSeconds\": 1 + }, + \"name\": \"controller\", + \"ports\": [ + { + \"containerPort\": 80, + \"hostPort\": 80, + \"name\": \"http\", + \"protocol\": \"TCP\" + }, + { + \"containerPort\": 443, + \"hostPort\": 443, + \"name\": \"https\", + \"protocol\": \"TCP\" + }, + { + \"containerPort\": 8443, + \"hostPort\": 8443, + \"name\": \"webhook\", + \"protocol\": \"TCP\" + } + ], + \"readinessProbe\": { + \"failureThreshold\": 3, + \"httpGet\": { + \"path\": \"/healthz\", + \"port\": 10254, + \"scheme\": \"HTTP\" + }, + \"initialDelaySeconds\": 10, + \"periodSeconds\": 10, + \"successThreshold\": 1, + \"timeoutSeconds\": 1 + }, + \"resources\": { + \"requests\": { + \"cpu\": \"100m\", + \"memory\": \"90Mi\" + } + }, + \"securityContext\": { + \"allowPrivilegeEscalation\": false, + \"capabilities\": { + \"add\": [ + \"NET_BIND_SERVICE\" + ], + \"drop\": [ + \"ALL\" + ] + }, + \"readOnlyRootFilesystem\": false, + \"runAsNonRoot\": true, + \"runAsUser\": 101, + \"seccompProfile\": { + \"type\": \"RuntimeDefault\" + } + }, + \"terminationMessagePath\": \"/dev/termination-log\", + \"terminationMessagePolicy\": \"File\", + \"volumeMounts\": [ + { + \"mountPath\": \"/usr/local/certificates/\", + \"name\": \"webhook-cert\", + \"readOnly\": true + }, + { + \"mountPath\": \"/var/run/secrets/kubernetes.io/serviceaccount\", + \"name\": \"kube-api-access-c92mq\", + \"readOnly\": true + } + ] + } + ], + \"dnsPolicy\": \"ClusterFirst\", + \"enableServiceLinks\": true, + \"hostNetwork\": true, + \"nodeName\": \"usagi\", + \"nodeSelector\": { + \"kubernetes.io/os\": \"linux\" + }, + \"preemptionPolicy\": \"PreemptLowerPriority\", + \"priority\": 0, + \"restartPolicy\": \"Always\", + \"schedulerName\": \"default-scheduler\", + \"securityContext\": {}, + \"serviceAccount\": \"ingress-nginx\", + \"serviceAccountName\": \"ingress-nginx\", + \"terminationGracePeriodSeconds\": 300, + \"tolerations\": [ + { + \"effect\": \"NoExecute\", + \"key\": \"node.kubernetes.io/not-ready\", + \"operator\": \"Exists\" + }, + { + \"effect\": \"NoExecute\", + \"key\": \"node.kubernetes.io/unreachable\", + \"operator\": \"Exists\" + }, + { + \"effect\": \"NoSchedule\", + \"key\": \"node.kubernetes.io/disk-pressure\", + \"operator\": \"Exists\" + }, + { + \"effect\": \"NoSchedule\", + \"key\": \"node.kubernetes.io/memory-pressure\", + \"operator\": \"Exists\" + }, + { + \"effect\": \"NoSchedule\", + \"key\": \"node.kubernetes.io/pid-pressure\", + \"operator\": \"Exists\" + }, + { + \"effect\": \"NoSchedule\", + \"key\": \"node.kubernetes.io/unschedulable\", + \"operator\": \"Exists\" + }, + { + \"effect\": \"NoSchedule\", + \"key\": \"node.kubernetes.io/network-unavailable\", + \"operator\": \"Exists\" + } + ], + \"volumes\": [ + { + \"name\": \"webhook-cert\", + \"secret\": { + \"defaultMode\": 420, + \"items\": [ + { + \"key\": \"tls.crt\", + \"path\": \"cert\" + }, + { + \"key\": \"tls.key\", + \"path\": \"key\" + } + ], + \"secretName\": \"ingress-nginx-admission\" + } + }, + { + \"name\": \"kube-api-access-c92mq\", + \"projected\": { + \"defaultMode\": 420, + \"sources\": [ + { + \"serviceAccountToken\": { + \"expirationSeconds\": 3607, + \"path\": \"token\" + } + }, + { + \"configMap\": { + \"items\": [ + { + \"key\": \"ca.crt\", + \"path\": \"ca.crt\" + } + ], + \"name\": \"kube-root-ca.crt\" + } + }, + { + \"downwardAPI\": { + \"items\": [ + { + \"fieldRef\": { + \"apiVersion\": \"v1\", + \"fieldPath\": \"metadata.namespace\" + }, + \"path\": \"namespace\" + } + ] + } + } + ] + } + } + ] + }, + \"status\": { + \"conditions\": [ + { + \"lastProbeTime\": null, + \"lastTransitionTime\": \"2023-12-16T15:22:57Z\", + \"status\": \"True\", + \"type\": \"Initialized\" + }, + { + \"lastProbeTime\": null, + \"lastTransitionTime\": \"2024-03-25T07:14:16Z\", + \"status\": \"True\", + \"type\": \"Ready\" + }, + { + \"lastProbeTime\": null, + \"lastTransitionTime\": \"2024-03-25T07:14:16Z\", + \"status\": \"True\", + \"type\": \"ContainersReady\" + }, + { + \"lastProbeTime\": null, + \"lastTransitionTime\": \"2023-12-16T15:22:57Z\", + \"status\": \"True\", + \"type\": \"PodScheduled\" + } + ], + \"containerStatuses\": [ + { + \"containerID\": \"containerd://xxx\", + \"image\": \"sha256:5aa0bf4798fa2300b97564cc77480e6d0abac88f8bdc001c01eb4ab3b98b2fbf\", + \"imageID\": \"registry.k8s.io/ingress-nginx/controller@sha256:5b161f051d017e55d358435f295f5e9a297e66158f136321d9b04520ec6c48a3\", + \"lastState\": { + \"terminated\": { + \"containerID\": \"containerd://xxx\", + \"exitCode\": 255, + \"finishedAt\": \"2024-03-25T07:08:20Z\", + \"reason\": \"Unknown\", + \"startedAt\": \"2024-02-12T08:14:25Z\" + } + }, + \"name\": \"controller\", + \"ready\": true, + \"restartCount\": 4, + \"started\": true, + \"state\": { + \"running\": { + \"startedAt\": \"2024-03-25T07:13:37Z\" + } + } + } + ], + \"hostIP\": \"192.168.0.10\", + \"phase\": \"Running\", + \"podIP\": \"192.168.0.10\", + \"podIPs\": [ + { + \"ip\": \"192.168.0.10\" + } + ], + \"qosClass\": \"Burstable\", + \"startTime\": \"2023-12-16T15:22:57Z\" + } +} +""" diff --git a/pkg/snap/object_snapshot.go b/pkg/snap/gomega/object_snapshot.go similarity index 97% rename from pkg/snap/object_snapshot.go rename to pkg/snap/gomega/object_snapshot.go index e937d7b..29f16ce 100644 --- a/pkg/snap/object_snapshot.go +++ b/pkg/snap/gomega/object_snapshot.go @@ -1,4 +1,4 @@ -package snap +package gomega import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" diff --git a/pkg/snap/gomega/snap.go b/pkg/snap/gomega/snap.go new file mode 100644 index 0000000..7385875 --- /dev/null +++ b/pkg/snap/gomega/snap.go @@ -0,0 +1,37 @@ +package gomega + +import ( + "fmt" + "path/filepath" + "regexp" + "strings" + + "github.com/onsi/ginkgo/v2" + "github.com/onsi/gomega/types" + + "github.com/jlandowner/helm-chartsnap/pkg/snap" +) + +var ( + shotCountMap = map[string]int{} + trimSpace = regexp.MustCompile(` +`) +) + +// MatchSnapShot returns a Gomega matcher that compares the actual value with the snapshot file. +func MatchSnapShot() types.GomegaMatcher { + + testFile := ginkgo.CurrentSpecReport().FileName() + path := filepath.Dir(testFile) + file := filepath.Base(testFile) + snapFile := filepath.Join(path, "__snapshots__", strings.TrimSuffix(file, ".go")+".snap") + + testLabel := ginkgo.CurrentSpecReport().FullText() + testLabel = trimSpace.ReplaceAllString(testLabel, " ") + + count := shotCountMap[testLabel] + count++ + shotCountMap[testLabel] = count + snapId := fmt.Sprintf("%s %d", testLabel, count) + + return snap.SnapshotMatcher(snapFile, snap.WithSnapshotID(snapId)) +} diff --git a/pkg/snap/gomega/snap_test.go b/pkg/snap/gomega/snap_test.go new file mode 100644 index 0000000..4410b13 --- /dev/null +++ b/pkg/snap/gomega/snap_test.go @@ -0,0 +1,34 @@ +package gomega + +import ( + "os" + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/jlandowner/helm-chartsnap/pkg/unstructured" +) + +func TestSnap(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Snap Suite") +} + +var _ = Describe("Snap", func() { + It("takes a full snapshot", func() { + b, err := os.ReadFile("testdata/pod.yaml") + Expect(err).NotTo(HaveOccurred()) + Expect(string(b)).To(MatchSnapShot()) + }) + + It("takes a snapshot without dynamic values", func() { + b, err := os.ReadFile("testdata/pod.yaml") + Expect(err).NotTo(HaveOccurred()) + + _, obj, err := unstructured.BytesToUnstructured(b) + Expect(err).NotTo(HaveOccurred()) + + Expect(ObjectSnapshot(obj)).To(MatchSnapShot()) + }) +}) diff --git a/pkg/snap/gomega/testdata/pod.yaml b/pkg/snap/gomega/testdata/pod.yaml new file mode 100644 index 0000000..24b2a59 --- /dev/null +++ b/pkg/snap/gomega/testdata/pod.yaml @@ -0,0 +1,235 @@ +apiVersion: v1 +kind: Pod +metadata: + annotations: + kubectl.kubernetes.io/restartedAt: "2023-02-26T13:52:41Z" + creationTimestamp: "2023-12-16T15:22:57Z" + generateName: ingress-nginx-controller- + labels: + app.kubernetes.io/component: controller + app.kubernetes.io/instance: ingress-nginx + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: ingress-nginx + app.kubernetes.io/part-of: ingress-nginx + app.kubernetes.io/version: 1.9.4 + controller-revision-hash: 58c9997466 + helm.sh/chart: ingress-nginx-4.8.4 + pod-template-generation: "5" + name: ingress-nginx-controller-2rqc5 + namespace: ingress-nginx + ownerReferences: + - apiVersion: apps/v1 + blockOwnerDeletion: true + controller: true + kind: DaemonSet + name: ingress-nginx-controller + uid: xxxxx + resourceVersion: "11111111" + uid: xxxxx +spec: + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchFields: + - key: metadata.name + operator: In + values: + - usagi + containers: + - args: + - /nginx-ingress-controller + - --publish-service=$(POD_NAMESPACE)/ingress-nginx-controller + - --election-id=ingress-nginx-leader + - --controller-class=k8s.io/ingress-nginx + - --ingress-class=nginx + - --configmap=$(POD_NAMESPACE)/ingress-nginx-controller + - --validating-webhook=:8443 + - --validating-webhook-certificate=/usr/local/certificates/cert + - --validating-webhook-key=/usr/local/certificates/key + env: + - name: POD_NAME + valueFrom: + fieldRef: + apiVersion: v1 + fieldPath: metadata.name + - name: POD_NAMESPACE + valueFrom: + fieldRef: + apiVersion: v1 + fieldPath: metadata.namespace + - name: LD_PRELOAD + value: /usr/local/lib/libmimalloc.so + image: registry.k8s.io/ingress-nginx/controller:v1.9.4@sha256:5b161f051d017e55d358435f295f5e9a297e66158f136321d9b04520ec6c48a3 + imagePullPolicy: IfNotPresent + lifecycle: + preStop: + exec: + command: + - /wait-shutdown + livenessProbe: + failureThreshold: 5 + httpGet: + path: /healthz + port: 10254 + scheme: HTTP + initialDelaySeconds: 10 + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 + name: controller + ports: + - containerPort: 80 + hostPort: 80 + name: http + protocol: TCP + - containerPort: 443 + hostPort: 443 + name: https + protocol: TCP + - containerPort: 8443 + hostPort: 8443 + name: webhook + protocol: TCP + readinessProbe: + failureThreshold: 3 + httpGet: + path: /healthz + port: 10254 + scheme: HTTP + initialDelaySeconds: 10 + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 + resources: + requests: + cpu: 100m + memory: 90Mi + securityContext: + allowPrivilegeEscalation: false + capabilities: + add: + - NET_BIND_SERVICE + drop: + - ALL + readOnlyRootFilesystem: false + runAsNonRoot: true + runAsUser: 101 + seccompProfile: + type: RuntimeDefault + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + volumeMounts: + - mountPath: /usr/local/certificates/ + name: webhook-cert + readOnly: true + - mountPath: /var/run/secrets/kubernetes.io/serviceaccount + name: kube-api-access-c92mq + readOnly: true + dnsPolicy: ClusterFirst + enableServiceLinks: true + hostNetwork: true + nodeName: usagi + nodeSelector: + kubernetes.io/os: linux + preemptionPolicy: PreemptLowerPriority + priority: 0 + restartPolicy: Always + schedulerName: default-scheduler + securityContext: {} + serviceAccount: ingress-nginx + serviceAccountName: ingress-nginx + terminationGracePeriodSeconds: 300 + tolerations: + - effect: NoExecute + key: node.kubernetes.io/not-ready + operator: Exists + - effect: NoExecute + key: node.kubernetes.io/unreachable + operator: Exists + - effect: NoSchedule + key: node.kubernetes.io/disk-pressure + operator: Exists + - effect: NoSchedule + key: node.kubernetes.io/memory-pressure + operator: Exists + - effect: NoSchedule + key: node.kubernetes.io/pid-pressure + operator: Exists + - effect: NoSchedule + key: node.kubernetes.io/unschedulable + operator: Exists + - effect: NoSchedule + key: node.kubernetes.io/network-unavailable + operator: Exists + volumes: + - name: webhook-cert + secret: + defaultMode: 420 + items: + - key: tls.crt + path: cert + - key: tls.key + path: key + secretName: ingress-nginx-admission + - name: kube-api-access-c92mq + projected: + defaultMode: 420 + sources: + - serviceAccountToken: + expirationSeconds: 3607 + path: token + - configMap: + items: + - key: ca.crt + path: ca.crt + name: kube-root-ca.crt + - downwardAPI: + items: + - fieldRef: + apiVersion: v1 + fieldPath: metadata.namespace + path: namespace +status: + conditions: + - lastProbeTime: null + lastTransitionTime: "2023-12-16T15:22:57Z" + status: "True" + type: Initialized + - lastProbeTime: null + lastTransitionTime: "2024-03-25T07:14:16Z" + status: "True" + type: Ready + - lastProbeTime: null + lastTransitionTime: "2024-03-25T07:14:16Z" + status: "True" + type: ContainersReady + - lastProbeTime: null + lastTransitionTime: "2023-12-16T15:22:57Z" + status: "True" + type: PodScheduled + containerStatuses: + - containerID: containerd://xxx + image: sha256:5aa0bf4798fa2300b97564cc77480e6d0abac88f8bdc001c01eb4ab3b98b2fbf + imageID: registry.k8s.io/ingress-nginx/controller@sha256:5b161f051d017e55d358435f295f5e9a297e66158f136321d9b04520ec6c48a3 + lastState: + terminated: + containerID: containerd://xxx + exitCode: 255 + finishedAt: "2024-03-25T07:08:20Z" + reason: Unknown + startedAt: "2024-02-12T08:14:25Z" + name: controller + ready: true + restartCount: 4 + started: true + state: + running: + startedAt: "2024-03-25T07:13:37Z" + hostIP: 192.168.0.10 + phase: Running + podIP: 192.168.0.10 + podIPs: + - ip: 192.168.0.10 + qosClass: Burstable + startTime: "2023-12-16T15:22:57Z" diff --git a/pkg/snap/object_snapshot_test.go b/pkg/snap/object_snapshot_test.go deleted file mode 100644 index 296bf61..0000000 --- a/pkg/snap/object_snapshot_test.go +++ /dev/null @@ -1,45 +0,0 @@ -package snap - -import ( - "reflect" - "testing" - - "sigs.k8s.io/controller-runtime/pkg/client" -) - -func TestObjectSnapshot(t *testing.T) { - type args struct { - obj client.Object - } - tests := []struct { - name string - args args - want client.Object - }{ - // TODO: Add test cases. - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := ObjectSnapshot(tt.args.obj); !reflect.DeepEqual(got, tt.want) { - t.Errorf("ObjectSnapshot() = %v, want %v", got, tt.want) - } - }) - } -} - -func TestRemoveDynamicFields(t *testing.T) { - type args struct { - o client.Object - } - tests := []struct { - name string - args args - }{ - // TODO: Add test cases. - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - RemoveDynamicFields(tt.args.o) - }) - } -} diff --git a/pkg/snap/snapshot.go b/pkg/snap/snapshot.go index 9d62a85..29418e6 100644 --- a/pkg/snap/snapshot.go +++ b/pkg/snap/snapshot.go @@ -6,15 +6,8 @@ import ( "errors" "fmt" "log/slog" - "os" - "path/filepath" - "regexp" - "strings" - "time" "github.com/google/go-cmp/cmp" - "github.com/onsi/ginkgo/v2" - "github.com/onsi/gomega/types" "github.com/pelletier/go-toml/v2" "github.com/spf13/afero" ) @@ -32,63 +25,64 @@ func log() *slog.Logger { return logger } -var ( - shotCountMap = map[string]int{} - cacheFs = afero.NewCacheOnReadFs( - afero.NewOsFs(), - afero.NewMemMapFs(), - time.Minute, - ) - trimSpace = regexp.MustCompile(` +`) -) - -func MatchSnapShot(options ...Option) types.GomegaMatcher { - - testFile := ginkgo.CurrentSpecReport().FileName() - path := filepath.Dir(testFile) - file := filepath.Base(testFile) - snapFile := filepath.Join(path, "__snapshots__", strings.TrimSuffix(file, ".go")+".snap") - - testLabel := ginkgo.CurrentSpecReport().FullText() - testLabel = trimSpace.ReplaceAllString(testLabel, " ") +func defaultDiffFunc(x, y string) string { + var act interface{} + if err := json.Unmarshal([]byte(x), &act); err != nil { + return "Expected to match\n" + cmp.Diff(x, y) + } + var exp interface{} + if err := json.Unmarshal([]byte(y), &exp); err != nil { + return "Expected to match\n" + cmp.Diff(x, y) + } + return "Expected to JSON match\n" + cmp.Diff(exp, act) +} - count := shotCountMap[testLabel] - count++ - shotCountMap[testLabel] = count - snapId := fmt.Sprintf("%s %d", testLabel, count) +// Option is a functional option for snapshotMatcher. +type Option func(m *snapshotMatcher) - return SnapShotMatcher(snapFile, snapId) +func WithDiffFunc(f DiffFunc) Option { + return func(m *snapshotMatcher) { + m.diffFunc = f + } } -func SnapShotMatcher(snapFile string, snapId string, diffOpts ...DiffOptions) *snapShotMatcher { - o := mergeDiffOpts(diffOpts) +// WithSnapshotID is an option to specify the snapshot ID. If this option is set, the snapshot file is treated as a multi-snapshot file. +func WithSnapshotID(id string) Option { + return func(m *snapshotMatcher) { + m.snapID = id + } +} - return &snapShotMatcher{ +// SnapshotMatcher returns a matcher that compares the actual value with the snapshot file. +func SnapshotMatcher(snapFile string, options ...Option) *snapshotMatcher { + m := &snapshotMatcher{ snapFilePath: snapFile, - snapId: snapId, - fs: cacheFs, - diffFunc: Diff, - diffOptions: o, + diffFunc: defaultDiffFunc, } + + for _, opt := range options { + opt(m) + } + return m } -type Option func(m *snapShotMatcher) +type DiffFunc func(x, y string) string -type snapShotMatcher struct { - snapFilePath string - snapId string - fs afero.Fs - expectedJson string - actualJson string - diffFunc func(x, y string, opts DiffOptions) string - diffOptions DiffOptions +type snapshotMatcher struct { + snapFilePath string + snapID string + expectedString string + actualString string + diffFunc DiffFunc } -func (m *snapShotMatcher) Match(actual interface{}) (success bool, err error) { - +func (m *snapshotMatcher) Match(actual interface{}) (success bool, err error) { + // prepare actual switch v := actual.(type) { case string: - m.actualJson = v + m.actualString = v + case []byte: + m.actualString = string(v) default: var buf bytes.Buffer enc := json.NewEncoder(&buf) @@ -97,117 +91,125 @@ func (m *snapShotMatcher) Match(actual interface{}) (success bool, err error) { if err := enc.Encode(actual); err != nil { return false, fmt.Errorf("json encode error: %w", err) } - m.actualJson = buf.String() + m.actualString = buf.String() } - snap, err := m.ReadSnapShot() + // prepare expected from snapshot + snap, err := m.readSnapshot() if errors.Is(err, afero.ErrFileNotFound) { - err = m.WriteSnapShot([]byte(m.actualJson)) + // take new snapshot + err = m.writeSnapshot([]byte(m.actualString)) if err == nil { return true, nil } } if err != nil { - return false, err + return false, fmt.Errorf("preparing expected from snapshot error: %w", err) } - m.expectedJson = *snap + m.expectedString = string(snap) - return m.actualJson == m.expectedJson, nil + return m.actualString == m.expectedString, nil } -func (m *snapShotMatcher) FailureMessage(actual interface{}) (message string) { - return "Expected to match\n" + m.diffFunc(m.expectedJson, m.actualJson, m.diffOptions) + "\n" -} - -func (m *snapShotMatcher) NegatedFailureMessage(actual interface{}) (message string) { - var act interface{} - if err := json.Unmarshal([]byte(m.actualJson), &act); err != nil { - return fmt.Errorf("json decode error: %w", err).Error() - } - var exp interface{} - if err := json.Unmarshal([]byte(m.expectedJson), &exp); err != nil { - return fmt.Errorf("json decode error: %w", err).Error() - } - return "Expected not to match\n" + cmp.Diff(exp, act) + "\n" +// FailureMessage returns a string that describes the failure of the matcher. +// actual must be always nil because the actual value is already parsed and stored in the matcher. +func (m *snapshotMatcher) FailureMessage(actual interface{}) (message string) { + return m.diffFunc(m.expectedString, m.actualString) + "\n" } -//----------------------------------------------------------- - -type data struct { - SnapShot interface{} `toml:"SnapShot,multiline,omitempty"` +// NegatedFailureMessage returns a string that describes the failure of the negated matcher. +// actual must be always nil because the actual value is already parsed and stored in the matcher. +func (m *snapshotMatcher) NegatedFailureMessage(actual interface{}) (message string) { + return m.diffFunc(m.expectedString, m.actualString) + "\n" } -func (m *snapShotMatcher) ReadSnapShot() (*string, error) { - - snapFileData, err := m.readSnapFileData() +func (m *snapshotMatcher) readSnapshot() ([]byte, error) { + raw, err := ReadFile(m.snapFilePath) if err != nil { return nil, err } + if m.snapID == "" { + return raw, nil + } - if snap, ok := (*snapFileData)[m.snapId]; ok { - a := snap.SnapShot.(string) - return &a, nil + log().Debug("read multi snapshot", "snapFilePath", m.snapFilePath, "snapID", m.snapID) + snaps, err := DecodeMultiSnapshots(raw) + if err != nil { + return nil, err + } + if snap, ok := snaps[m.snapID]; ok { + s := snap.Snapshot.(string) + return []byte(s), nil } else { return nil, afero.ErrFileNotFound } } -func (m *snapShotMatcher) WriteSnapShot(snap []byte) error { +func (m *snapshotMatcher) writeSnapshot(snapFileData []byte) error { + if m.snapID == "" { + return WriteFile(m.snapFilePath, snapFileData) - snapFileData, err := m.readSnapFileData() - if err != nil { - return err } - (*snapFileData)[m.snapId] = data{SnapShot: string(snap)} - if err := m.writeSnapFileData(snapFileData); err != nil { - return err + log().Debug("write multi snapshot", "snapFilePath", m.snapFilePath, "snapID", m.snapID) + var snaps multiSnap + raw, err := ReadFile(m.snapFilePath) + if err == nil { + snaps, err = DecodeMultiSnapshots(raw) + if err != nil { + return fmt.Errorf("decode snapshot file error: %w", err) + } } - return nil -} - -func (m *snapShotMatcher) readSnapFileData() (*map[string]data, error) { - exists, err := afero.Exists(m.fs, m.snapFilePath) if err != nil { - return nil, fmt.Errorf("file check error: %w", err) - } - if !exists { - return &map[string]data{}, nil + if errors.Is(err, afero.ErrFileNotFound) { + snaps = make(multiSnap) + } else { + return fmt.Errorf("read snapshot file error: %w", err) + } } - file, err := m.fs.Open(m.snapFilePath) + snaps[m.snapID] = snap{Snapshot: string(snapFileData)} + + data, err := EncodeMultiSnapshots(snaps) if err != nil { - return nil, fmt.Errorf("file open error: %w", err) + return err } + return WriteFile(m.snapFilePath, data) +} + +type multiSnap map[string]snap - defer file.Close() +type snap struct { + Snapshot interface{} `toml:"SnapShot,multiline,omitempty"` +} - var datas map[string]data - err = toml.NewDecoder(file).Decode(&datas) +func IsMultiSnapshots(filePath string) bool { + raw, err := ReadFile(filePath) if err != nil { - return nil, fmt.Errorf("toml decode error: %w", err) - } - if len(datas) == 0 { - return &map[string]data{}, nil + return false } - return &datas, nil + _, err = DecodeMultiSnapshots(raw) + return err == nil } -func (m *snapShotMatcher) writeSnapFileData(snapFileData *map[string]data) error { - if err := m.fs.MkdirAll(filepath.Dir(m.snapFilePath), os.ModePerm); err != nil { - return fmt.Errorf("create snapfile directory error: %w", err) - } - file, err := m.fs.Create(m.snapFilePath) +func DecodeMultiSnapshots(raw []byte) (multiSnap, error) { + snaps := make(multiSnap) + err := toml.NewDecoder(bytes.NewReader(raw)).Decode(&snaps) if err != nil { - return fmt.Errorf("open snapfile error: %w", err) + return nil, fmt.Errorf("toml decode error: %w", err) } - defer file.Close() - - enc := toml.NewEncoder(file) - enc.SetArraysMultiline(true) + if len(snaps) == 0 { + return snaps, nil + } + return snaps, nil +} - if err := enc.Encode(snapFileData); err != nil { - return fmt.Errorf("toml encode error: %w", err) +func EncodeMultiSnapshots(snaps multiSnap) ([]byte, error) { + buf := bytes.Buffer{} + tomlEnc := toml.NewEncoder(&buf) + tomlEnc.SetArraysMultiline(true) + if err := tomlEnc.Encode(snaps); err != nil { + return nil, fmt.Errorf("toml encode error: %w", err) } - return nil + return buf.Bytes(), nil } diff --git a/pkg/snap/snapshot_create_test.go b/pkg/snap/snapshot_create_test.go deleted file mode 100644 index 05a2f8d..0000000 --- a/pkg/snap/snapshot_create_test.go +++ /dev/null @@ -1,30 +0,0 @@ -package snap - -import ( - "os" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" -) - -var _ = Describe("Snapshot", func() { - AfterEach(func() { - os.Remove("__snapshots__/snapshot_create_test.snap") - }) - - It("should match", func() { - testdata := ` -Helm is a tool for managing Charts. Charts are packages of pre-configured Kubernetes resources. - -Use Helm to: - -- Find and use popular software packaged as Helm Charts to run in Kubernetes -- Share your own applications as Helm Charts -- Create reproducible builds of your Kubernetes applications -- Intelligently manage your Kubernetes manifest files -- Manage releases of Helm packages -` - // create snapshot file - Expect(testdata).To(MatchSnapShot()) - }) -}) diff --git a/pkg/snap/snapshot_test.go b/pkg/snap/snapshot_test.go index 1196d9e..c486f52 100644 --- a/pkg/snap/snapshot_test.go +++ b/pkg/snap/snapshot_test.go @@ -1,31 +1,239 @@ package snap import ( + "fmt" + "os" + "path/filepath" "testing" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" ) -func TestSnapshot(t *testing.T) { +var ( + pwd string +) + +func TestSnap(t *testing.T) { RegisterFailHandler(Fail) - RunSpecs(t, "Snapshot Suite") + RunSpecs(t, "Snap Suite") } -var _ = Describe("Snapshot", func() { - It("should match", func() { - testdata := ` -Helm is a tool for managing Charts. Charts are packages of pre-configured Kubernetes resources. - -Use Helm to: - -- Find and use popular software packaged as Helm Charts to run in Kubernetes -- Share your own applications as Helm Charts -- Create reproducible builds of your Kubernetes applications -- Intelligently manage your Kubernetes manifest files -- Manage releases of Helm packages +var _ = Describe("Snap", func() { + BeforeEach(func() { + _pwd, err := os.Getwd() + Expect(err).NotTo(HaveOccurred()) + pwd = _pwd + }) + + AfterEach(func() { + logger = nil + }) + + Describe("SnapshotMatcher", func() { + Context("snapshot file does not exist", func() { + It("should pass and create snapshot file", func() { + var ( + snapFile = "not_found.snap" + snapFilePath = filepath.Join(pwd, "__snapshot__", snapFile) + snapData = ` +Beautiful is better than ugly. +Explicit is better than implicit. +Simple is better than complex. +Complex is better than complicated. +Flat is better than nested. +Sparse is better than dense. +Readability counts. +Special cases aren't special enough to break the rules. +Although practicality beats purity. +Errors should never pass silently. +Unless explicitly silenced. +In the face of ambiguity, refuse the temptation to guess. +There should be one-- and preferably only one --obvious way to do it. +Although that way may not be obvious at first unless you're Dutch. +Now is better than never. +Although never is often better than *right* now. +If the implementation is hard to explain, it's a bad idea. +If the implementation is easy to explain, it may be a good idea. +Namespaces are one honking great idea -- let's do more of those! +` + ) + defer os.Remove(snapFilePath) + + _, err := os.Stat(snapFilePath) + Expect(os.IsNotExist(err)).To(BeTrue()) + + matcher := SnapshotMatcher(snapFilePath) + success, err := matcher.Match(snapData) + Expect(err).NotTo(HaveOccurred()) + Expect(success).To(BeTrue()) + + fileContent, err := os.ReadFile(snapFilePath) + Expect(err).NotTo(HaveOccurred()) + Expect(string(fileContent)).To(Equal(snapData)) + }) + }) + + Context("snapshot file exist", func() { + It("match snapshot", func() { + var ( + snapFile = "single.snap" + snapFilePath = filepath.Join(pwd, "__snapshot__", snapFile) + ) + + _, err := os.Stat(snapFilePath) + Expect(err).NotTo(HaveOccurred()) + + fileContent, err := os.ReadFile(snapFilePath) + Expect(err).NotTo(HaveOccurred()) + + matcher := SnapshotMatcher(snapFilePath) + success, err := matcher.Match(fileContent) + Expect(err).NotTo(HaveOccurred()) + Expect(success).To(BeTrue()) + }) + }) + + Context("multi-formatted snapshot file exist", func() { + It("match snapshot", func() { + var ( + singleSnapFile = "single.snap" + singleSnapFilePath = filepath.Join(pwd, "__snapshot__", singleSnapFile) + + snapFile = "multi.snap" + snapFilePath = filepath.Join(pwd, "__snapshot__", snapFile) + ) + + _, err := os.Stat(snapFilePath) + Expect(err).NotTo(HaveOccurred()) + + // file content is the same as single.snap + fileContent, err := os.ReadFile(singleSnapFilePath) + Expect(err).NotTo(HaveOccurred()) + + matcher := SnapshotMatcher(snapFilePath, WithSnapshotID("default")) + success, err := matcher.Match(fileContent) + Expect(err).NotTo(HaveOccurred()) + Expect(success).To(BeTrue()) + }) + }) + + Context("multi-formatted snapshot file", func() { + It("append new snapID", func() { + snapFilePath := filepath.Join(pwd, "__snapshot__", "multi-not-found.snap") + existingFileData := `[xxx] +SnapShot = """ +apiVersion: v1 +kind: Namespace +metadata: + name: default +""" ` - // match snapshot file - Expect(testdata).To(MatchSnapShot()) + yyySnapshot := `apiVersion: v1 +kind: Pod +metadata: + name: nginx +` + expectedFileData := `[xxx] +SnapShot = """ +apiVersion: v1 +kind: Namespace +metadata: + name: default +""" + +[yyy] +SnapShot = """ +apiVersion: v1 +kind: Pod +metadata: + name: nginx +""" +` + + _, err := os.Stat(snapFilePath) + Expect(os.IsNotExist(err)).To(BeTrue()) + + os.WriteFile(snapFilePath, []byte(existingFileData), 0644) + defer os.Remove(snapFilePath) + + matcher := SnapshotMatcher(snapFilePath, WithSnapshotID("yyy")) + success, err := matcher.Match(yyySnapshot) + Expect(err).NotTo(HaveOccurred()) + Expect(success).To(BeTrue()) + + fileContent, err := os.ReadFile(snapFilePath) + Expect(err).NotTo(HaveOccurred()) + Expect(string(fileContent)).To(Equal(expectedFileData)) + }) + }) + }) + + Context("json snapshot file", func() { + It("match snapshot", func() { + testStruct := struct { + Name string `json:"name"` + Age int `json:"age"` + }{Name: "John", Age: 30} + + snapFilePath := filepath.Join(pwd, "__snapshot__", "json.snap") + + // _, err := os.Stat(snapFilePath) + // Expect(err).NotTo(HaveOccurred()) + + matcher := SnapshotMatcher(snapFilePath, WithSnapshotID("json")) + success, err := matcher.Match(testStruct) + Expect(err).NotTo(HaveOccurred()) + fmt.Println(matcher.FailureMessage(nil)) + Expect(success).To(BeTrue()) + }) }) }) + +func TestIsMultiSnapshots(t *testing.T) { + // table test + tests := []struct { + name string + snapFile string + want bool + }{ + { + name: "single snapshot", + snapFile: "single.snap", + want: false, + }, + { + name: "multi snapshot", + snapFile: "multi.snap", + want: true, + }, + } + for _, tt := range tests { + got := IsMultiSnapshots(filepath.Join(pwd, "__snapshot__", tt.snapFile)) + if got != tt.want { + t.Errorf("Expected file to contain multiple snapshots got %v, want %v", got, tt.want) + } + } +} + +func TestDiffFunc(t *testing.T) { + // Set up test data + x := "test1" + y := "test2" + + // Define the expected diff function + expectedDiffFunc := func(x, y string) string { + return "diff" + } + + // Create a snapshot matcher with the diff function + matcher := SnapshotMatcher("test_snap.toml", WithDiffFunc(expectedDiffFunc)) + + // Get the actual diff function + actualDiffFunc := matcher.diffFunc + + // Check the result + if actualDiffFunc(x, y) != expectedDiffFunc(x, y) { + t.Errorf("Diff function does not match expected") + } +} diff --git a/pkg/snap/testdata/diff.snap b/pkg/snap/testdata/diff.snap deleted file mode 100644 index 670e113..0000000 --- a/pkg/snap/testdata/diff.snap +++ /dev/null @@ -1,79 +0,0 @@ -[default] -SnapShot = """ -Render chart templates locally and display the output. - -Any values that would normally be looked up or retrieved in-cluster will be -faked locally. Additionally, none of the server-side testing of chart validity -(e.g. whether an API is supported) is done. - -Usage: - helm template [NAME] [CHART] [flags] - -Flags: - -a, --api-versions strings Kubernetes api versions used for Capabilities.APIVersions - --atomic if set, the installation process deletes the installation on failure. The --wait flag will be set automatically if --atomic is used - --ca-file string verify certificates of HTTPS-enabled servers using this CA bundle - --cert-file string identify HTTPS client using this SSL certificate file - --create-namespace create the release namespace if not present - --dependency-update update dependencies if they are missing before installing the chart - --description string add a custom description - --devel use development versions, too. Equivalent to version '>0.0.0-0'. If --version is set, this is ignored - --disable-openapi-validation if set, the installation process will not validate rendered templates against the Kubernetes OpenAPI Schema - --dry-run string[=\\\"client\\\"] simulate an install. If --dry-run is set with no option being specified or as '--dry-run=client', it will not attempt cluster connections. Setting '--dry-run=server' allows attempting cluster connections. - --enable-dns enable DNS lookups when rendering templates - --force force resource updates through a replacement strategy - -g, --generate-name generate the name (and omit the NAME parameter) - -h, --help help for template - --include-crds include CRDs in the templated output - --insecure-skip-tls-verify skip tls certificate checks for the chart download - --is-upgrade set .Release.IsUpgrade instead of .Release.IsInstall - --key-file string identify HTTPS client using this SSL key file - --keyring string location of public keys used for verification (default \\\"/home/coder/.gnupg/pubring.gpg\\\") - --kube-version string Kubernetes version used for Capabilities.KubeVersion - -l, --labels stringToString Labels that would be added to release metadata. Should be divided by comma. (default []) - --name-template string specify template used to name the release - --no-hooks prevent hooks from running during install - --output-dir string writes the executed templates to files in output-dir instead of stdout - --pass-credentials pass credentials to all domains - --password string chart repository password where to locate the requested chart - --plain-http use insecure HTTP connections for the chart download - --post-renderer postRendererString the path to an executable to be used for post rendering. If it exists in $PATH, the binary will be used, otherwise it will try to look for the executable at the given path - --post-renderer-args postRendererArgsSlice an argument to the post-renderer (can specify multiple) (default []) - --release-name use release name in the output-dir path. - --render-subchart-notes if set, render subchart notes along with the parent - --replace re-use the given name, only if that name is a deleted release which remains in the history. This is unsafe in production - --repo string chart repository url where to locate the requested chart - --set stringArray set values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2) - --set-file stringArray set values from respective files specified via the command line (can specify multiple or separate values with commas: key1=path1,key2=path2) - --set-json stringArray set JSON values on the command line (can specify multiple or separate values with commas: key1=jsonval1,key2=jsonval2) - --set-literal stringArray set a literal STRING value on the command line - --set-string stringArray set STRING values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2) - -s, --show-only stringArray only show manifests rendered from the given templates - --skip-crds if set, no CRDs will be installed. By default, CRDs are installed if not already present - --skip-tests skip tests from templated output - --timeout duration time to wait for any individual Kubernetes operation (like Jobs for hooks) (default 5m0s) - --username string chart repository username where to locate the requested chart - --validate validate your manifests against the Kubernetes cluster you are currently pointing at. This is the same validation performed on an install - -f, --values strings specify values in a YAML file or a URL (can specify multiple) - --verify verify the package before using it - --version string specify a version constraint for the chart version to use. This constraint can be a specific tag (e.g. 1.1.1) or it may reference a valid range (e.g. ^2.0.0). If this is not specified, the latest version is used - --wait if set, will wait until all Pods, PVCs, Services, and minimum number of Pods of a Deployment, StatefulSet, or ReplicaSet are in a ready state before marking the release as successful. It will wait for as long as --timeout - --wait-for-jobs if set and --wait enabled, will wait until all Jobs have been completed before marking the release as successful. It will wait for as long as --timeout - -Global Flags: - --burst-limit int client-side default throttling limit (default 100) - --debug enable verbose output - --kube-apiserver string the address and the port for the Kubernetes API server - --kube-as-group stringArray group to impersonate for the operation, this flag can be repeated to specify multiple groups. - --kube-as-user string username to impersonate for the operation - --kube-ca-file string the certificate authority file for the Kubernetes API server connection - --kube-context string name of the kubeconfig context to use - --kube-insecure-skip-tls-verify if true, the Kubernetes API server's certificate will not be checked for validity. This will make your HTTPS connections insecure - --kube-tls-server-name string server name to use for Kubernetes API server certificate validation. If it is not provided, the hostname used to contact the server is used - --kube-token string bearer token used for authentication - --kubeconfig string path to the kubeconfig file - -n, --namespace string namespace scope for this request - --registry-config string path to the registry config file (default \\\"/home/coder/.config/helm/registry/config.json\\\") - --repository-cache string path to the file containing cached repository indexes (default \\\"/home/coder/.cache/helm/repository\\\") - --repository-config string path to the file containing repository names and URLs (default \\\"/home/coder/.config/helm/repositories.yaml\\\") -""" diff --git a/pkg/snap/testdata/diff.txt b/pkg/snap/testdata/diff.txt deleted file mode 100644 index 9f609ad..0000000 --- a/pkg/snap/testdata/diff.txt +++ /dev/null @@ -1,76 +0,0 @@ -Render chart templates locally and display the output. - -Any values that would normally be looked up or retrieved in-cluster will be -faked locally. Additionally, none of the server-side testing of chart validity -(e.g. whether an API is supported) is done. - -Usage: - helm template [NAME] [CHART] [flags] - -Flags: - -a, --api-versions strings Kubernetes api versions used for Capabilities.APIVersions - --atomic if set, the installation process deletes the installation on failure. The --wait flag will be set automatically if --atomic is used - --ca-file string verify certificates of HTTPS-enabled servers using this CA bundle - --cert-file string identify HTTPS client using this SSL certificate file - --create-namespace create the release namespace if not present - --dependency-update update dependencies if they are missing before installing the chart - --description string add a custom description - --devel use development versions, too. Equivalent to version '>0.0.0-0'. If --version is set, this is ignored - --disable-openapi-validation if set, the installation process will not validate rendered templates against the Kubernetes OpenAPI Schema - --dry-run string[=\"client\"] simulate an install. If --dry-run is set with no option being specified or as '--dry-run=client', it will not attempt cluster connections. Setting '--dry-run=server' allows attempting cluster connections. - --enable-dns enable DNS lookups when rendering templates - --force force resource updates through a replacement strategy - -g, --generate-name generate the name (and omit the NAME parameter) - -h, --help help for template - --include-crds include CRDs in the templated output - --insecure-skip-tls-verify skip tls certificate checks for the chart download - --is-upgrade set .Release.IsUpgrade instead of .Release.IsInstall - --key-file string identify HTTPS client using this SSL key file - --keyring string location of public keys used for verification (default \"/home/coder/.gnupg/pubring.gpg\") - --kube-version string Kubernetes version used for Capabilities.KubeVersion - -l, --labels stringToString Labels that would be added to release metadata. Should be divided by comma. (default []) - --name-template string specify template used to name the release - --no-hooks prevent hooks from running during install - --output-dir string writes the executed templates to files in output-dir instead of stdout - --pass-credentials pass credentials to all domains - --password string chart repository password where to locate the requested chart - --plain-http use insecure HTTP connections for the chart download - --post-renderer postRendererString the path to an executable to be used for post rendering. If it exists in $PATH, the binary will be used, otherwise it will try to look for the executable at the given path - --post-renderer-args postRendererArgsSlice an argument to the post-renderer (can specify multiple) (default []) - --release-name use release name in the output-dir path. - --render-subchart-notes if set, render subchart notes along with the parent - --replace re-use the given name, only if that name is a deleted release which remains in the history. This is unsafe in production - --repo string chart repository url where to locate the requested chart - --set stringArray set values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2) - --set-file stringArray set values from respective files specified via the command line (can specify multiple or separate values with commas: key1=path1,key2=path2) - --set-json stringArray set JSON values on the command line (can specify multiple or separate values with commas: key1=jsonval1,key2=jsonval2) - --set-literal stringArray set a literal STRING value on the command line - --set-string stringArray set STRING values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2) - -s, --show-only stringArray only show manifests rendered from the given templates - --skip-crds if set, no CRDs will be installed. By default, CRDs are installed if not already present - --skip-tests skip tests from templated output - --timeout duration time to wait for any individual Kubernetes operation (like Jobs for hooks) (default 5m0s) - --username string chart repository username where to locate the requested chart - --validate validate your manifests against the Kubernetes cluster you are currently pointing at. This is the same validation performed on an install - -f, --values strings specify values in a YAML file or a URL (can specify multiple) - --verify verify the package before using it - --version string specify a version constraint for the chart version to use. This constraint can be a specific tag (e.g. 1.1.1) or it may reference a valid range (e.g. ^2.0.0). If this is not specified, the latest version is used - --wait if set, will wait until all Pods, PVCs, Services, and minimum number of Pods of a Deployment, StatefulSet, or ReplicaSet are in a ready state before marking the release as successful. It will wait for as long as --timeout - --wait-for-jobs if set and --wait enabled, will wait until all Jobs have been completed before marking the release as successful. It will wait for as long as --timeout - -Global Flags: - --burst-limit int client-side default throttling limit (default 100) - --debug enable verbose output - --kube-apiserver string the address and the port for the Kubernetes API server - --kube-as-group stringArray group to impersonate for the operation, this flag can be repeated to specify multiple groups. - --kube-as-user string username to impersonate for the operation - --kube-ca-file string the certificate authority file for the Kubernetes API server connection - --kube-context string name of the kubeconfig context to use - --kube-insecure-skip-tls-verify if true, the Kubernetes API server's certificate will not be checked for validity. This will make your HTTPS connections insecure - --kube-tls-server-name string server name to use for Kubernetes API server certificate validation. If it is not provided, the hostname used to contact the server is used - --kube-token string bearer token used for authentication - --kubeconfig string path to the kubeconfig file - -n, --namespace string namespace scope for this request - --registry-config string path to the registry config file (default \"/home/coder/.config/helm/registry/config.json\") - --repository-cache string path to the file containing cached repository indexes (default \"/home/coder/.cache/helm/repository\") - --repository-config string path to the file containing repository names and URLs (default \"/home/coder/.config/helm/repositories.yaml\") diff --git a/pkg/snap/testdata/diff_diff.txt b/pkg/snap/testdata/diff_diff.txt deleted file mode 100644 index d7acbf9..0000000 --- a/pkg/snap/testdata/diff_diff.txt +++ /dev/null @@ -1,76 +0,0 @@ -Render chart templates locally and display the output. - -Any values that would normally be looked up or retrieved in-cluster will be -faked locally. Additionally, none of the server-side testing of chart validity -(e.g. whether an API is supported) is done. - -Usage: - helm template [NAME] [CHART] [flags] - -Flags: - -a, --api-versions strings Kubernetes api versions used for Capabilities.APIVersions - --atomic if set, the installation process deletes the installation on failure. The --wait flag will be set automatically if --atomic is used - --ca-file string verify certificates of HTTPS-enabled servers using this CA bundle - --create-namespace create the release namespace if not present - --dependency-update update dependencies if they are missing before installing the chart - --description string add a custom description - --devel use development versions, too. Equivalent to version '>0.0.0-0'. If --version is set, this is ignored - --disable-openapi-validation if set, the installation process will not validate rendered templates against the Kubernetes OpenAPI Schema - --dry-run string[=\"client\"] simulate an install. If --dry-run is set with no option being specified or as '--dry-run=client', it will not attempt cluster connections. Setting '--dry-run=server' allows attempting cluster connections. - --enable-dns enable DNS lookups when rendering templates - --force force resource updates through a replacement strategy - -g, --generate-name generate the name (and omit the NAME parameter) - -h, --help help for templates - --include-crds include CRDs in the templated output - --insecure-skip-tls-verify skip tls certificate checks for the chart download - --is-upgrade set .Release.IsUpgrade instead of .Release.IsInstall - --key-file string identify HTTPS client using this SSL key file - --keyring string location of public keys used for verification (default \"/home/coder/.gnupg/pubring.gpg\") - --kube-version string Kubernetes version used for Capabilities.KubeVersion - -l, --labels stringToString Labels that would be added to release metadata. Should be divided by comma. (default []) - --name-template string specify template used to name the release - --no-hooks prevent hooks from running during install - --output-dir string writes the executed templates to files in output-dir instead of stdout - --pass-credentials pass credentials to all domains - --password string chart repository password where to locate the requested chart - --plain-http use insecure HTTP connections for the chart download - --post-renderer postRendererString the path to an executable to be used for post rendering. If it exists in $PATH, the binary will be used, otherwise it will try to look for the executable at the given path - --post-renderer-args postRendererArgsSlice an argument to the post-renderer (can specify multiple) (default []) - --release-name use release name in the output-dir path. - --render-subchart-notes if set, render subchart notes along with the parent - --replace re-use the given name, only if that name is a deleted release which remains in the history. This is unsafe in production - --repo string chart repository url where to locate the requested chart - --set stringArray set values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2) - --set-file stringArray set values from respective files specified via the command line (can specify multiple or separate values with commas: key1=path1,key2=path2) - --set-json stringArray set JSON values on the command line (can specify multiple or separate values with commas: key1=jsonval1,key2=jsonval2) - --set-literal stringArray set a literal STRING value on the command line - --set-string stringArray set STRING values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2) - -s, --show-only stringArray only show manifests rendered from the given templates - --skip-crds if set, no CRDs will be installed. By default, CRDs are installed if not already present - --skip-tests skip tests from templated output - --timeout duration time to wait for any individual Kubernetes operation (like Jobs for hooks) (default 5m0s) - --username string chart repository username where to locate the requested chart - --validate validate your manifests against the Kubernetes cluster you are currently pointing at. This is the same validation performed on an install - -f, --values strings specify values in a YAML file or a URL (can specify multiple) - --fake fake - --verify verify the package before using it - --version string specify a version constraint for the chart version to use. This constraint can be a specific tag (e.g. 1.1.1) or it may reference a valid range (e.g. ^2.0.0). If this is not specified, the latest version is used - --wait if set, will wait until all Pods, PVCs, Services, and minimum number of Pods of a Deployment, StatefulSet, or ReplicaSet are in a ready state before marking the release as successful. It will wait for as long as --timeout - --wait-for-jobs if set and --wait enabled, will wait until all Jobs have been completed before marking the release as successful. It will wait for as long as --timeout - -Global Flags: - --burst-limit int client-side default throttling limit (default 100) - --debug enable verbose output - --kube-apiserver string the address and the port for the Kubernetes API server - --kube-as-group stringArray group to impersonate for the operation, this flag can be repeated to specify multiple groups. - --kube-as-user string username to impersonate for the operation - --kube-ca-file string the certificate authority file for the Kubernetes API server connection - --kube-context string name of the kubeconfig context to use - --kube-insecure-skip-tls-verify if true, the Kubernetes API server's certificate will not be checked for validity. This will make your HTTPS connections insecure - --kube-tls-server-name string server name to use for Kubernetes API server certificate validation. If it is not provided, the hostname used to contact the server is used - --kube-token string bearer token used for authentication - --kubeconfig string path to the kubeconfig file - -n, --namespace string namespace scope for this request - --registry-config string path to the registry config file (default \"/home/coder/.config/helm/registry/config.json\") - --repository-cache string path to the file containing cached repository indexes (default \"/home/coder/.cache/helm/repository\") - --repository-config string path to the file containing repository names and URLs (default \"/home/coder/.config/helm/repositories.yaml\") diff --git a/pkg/snap/unstructured_test.go b/pkg/snap/unstructured_test.go deleted file mode 100644 index 3768803..0000000 --- a/pkg/snap/unstructured_test.go +++ /dev/null @@ -1,220 +0,0 @@ -package snap - -import ( - "io" - "os" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - - "github.com/jlandowner/helm-chartsnap/pkg/unstructured" -) - -var _ = Describe("Unstructured Snapshot", func() { - f := func(m OmegaMatcher, filePath string) (success bool, err error) { - f, err := os.Open(filePath) - Expect(err).NotTo(HaveOccurred()) - defer f.Close() - - buf, err := io.ReadAll(f) - Expect(err).NotTo(HaveOccurred()) - - manifests, errs := unstructured.Decode(string(buf)) - Expect(len(errs)).To(BeZero()) - - return UnstructuredMatch(m, manifests) - } - - It("should match", func() { - m := UnstructuredSnapShotMatcher("testdata/unstructured.snap", "default") - success, err := f(m, "testdata/unstructured.yaml") - Expect(err).NotTo(HaveOccurred()) - Expect(success).To(BeTrue()) - }) - - It("should not match and output diff N=1", func() { - m := UnstructuredSnapShotMatcher("testdata/unstructured.snap", "default", WithDiffContextLineN(1)) - success, err := f(m, "testdata/unstructured_diff.yaml") - Expect(err).NotTo(HaveOccurred()) - Expect(success).To(BeFalse()) - - Expect(m.FailureMessage(nil)).Should(Equal(`Expected to match ---- kind=Deployment name=chartsnap-app1 line=8 - app.kubernetes.io/name: app1 -- app.kubernetes.io/version: 1.16.0 -+ app.kubernetes.io/version: 1.21.0 - helm.sh/chart: app1-0.1.0 - ---- kind=Deployment name=chartsnap-app1 line=24 - app.kubernetes.io/name: app1 -- app.kubernetes.io/version: 1.16.0 -+ app.kubernetes.io/version: 1.21.0 - helm.sh/chart: app1-0.1.0 - ---- kind=Deployment name=chartsnap-app1 line=29 - containers: -- - image: nginx:1.16.0 -+ - image: nginx:1.21.0 - imagePullPolicy: IfNotPresent - ---- kind=Deployment name=chartsnap-app1 line=32 - imagePullPolicy: IfNotPresent -- livenessProbe: -- httpGet: -- path: / -- port: http - name: app1 - ---- kind=Pod name=chartsnap-app1-test-connection line=59 - app.kubernetes.io/name: app1 -- app.kubernetes.io/version: 1.16.0 -+ app.kubernetes.io/version: 1.21.0 - helm.sh/chart: app1-0.1.0 - ---- kind=Service name=chartsnap-app1 line=80 - app.kubernetes.io/name: app1 -- app.kubernetes.io/version: 1.16.0 -+ app.kubernetes.io/version: 1.21.0 - helm.sh/chart: app1-0.1.0 - ---- kind=Service name=chartsnap-app1 line=90 - targetPort: http -+ - name: https -+ port: 443 -+ protocol: TCP -+ targetPort: https - selector: - ---- kind=ServiceAccount name= line=107 - app.kubernetes.io/name: app1 -- app.kubernetes.io/version: 1.16.0 -+ app.kubernetes.io/version: 1.21.0 - helm.sh/chart: app1-0.1.0 - - -`)) - }) - - It("should not match and output full diff", func() { - m := UnstructuredSnapShotMatcher("testdata/unstructured.snap", "default") - success, err := f(m, "testdata/unstructured_diff.yaml") - Expect(err).NotTo(HaveOccurred()) - Expect(success).To(BeFalse()) - - Expect(m.FailureMessage(nil)).Should(Equal(`Expected to match - - object: - apiVersion: apps/v1 - kind: Deployment - metadata: - labels: - app.kubernetes.io/instance: chartsnap - app.kubernetes.io/managed-by: Helm - app.kubernetes.io/name: app1 -- app.kubernetes.io/version: 1.16.0 -+ app.kubernetes.io/version: 1.21.0 - helm.sh/chart: app1-0.1.0 - name: chartsnap-app1 - spec: - replicas: 1 - selector: - matchLabels: - app.kubernetes.io/instance: chartsnap - app.kubernetes.io/name: app1 - template: - metadata: - labels: - app.kubernetes.io/instance: chartsnap - app.kubernetes.io/managed-by: Helm - app.kubernetes.io/name: app1 -- app.kubernetes.io/version: 1.16.0 -+ app.kubernetes.io/version: 1.21.0 - helm.sh/chart: app1-0.1.0 - spec: - containers: -- - image: nginx:1.16.0 -+ - image: nginx:1.21.0 - imagePullPolicy: IfNotPresent -- livenessProbe: -- httpGet: -- path: / -- port: http - name: app1 - ports: - - containerPort: 80 - name: http - protocol: TCP - readinessProbe: - httpGet: - path: / - port: http - resources: {} - securityContext: {} - securityContext: {} - serviceAccountName: chartsnap-app1 - - object: - apiVersion: v1 - kind: Pod - metadata: - annotations: - helm.sh/hook: test - labels: - app.kubernetes.io/instance: chartsnap - app.kubernetes.io/managed-by: Helm - app.kubernetes.io/name: app1 -- app.kubernetes.io/version: 1.16.0 -+ app.kubernetes.io/version: 1.21.0 - helm.sh/chart: app1-0.1.0 - name: chartsnap-app1-test-connection - spec: - containers: - - args: - - chartsnap-app1:80 - command: - - wget - image: busybox - name: wget - restartPolicy: Never - - object: - apiVersion: v1 - kind: Service - metadata: - labels: - app.kubernetes.io/instance: chartsnap - app.kubernetes.io/managed-by: Helm - app.kubernetes.io/name: app1 -- app.kubernetes.io/version: 1.16.0 -+ app.kubernetes.io/version: 1.21.0 - helm.sh/chart: app1-0.1.0 - name: chartsnap-app1 - spec: - ports: - - name: http - port: 80 - protocol: TCP - targetPort: http -+ - name: https -+ port: 443 -+ protocol: TCP -+ targetPort: https - selector: - app.kubernetes.io/instance: chartsnap - app.kubernetes.io/name: app1 - type: ClusterIP - - object: - apiVersion: v1 - automountServiceAccountToken: true - kind: ServiceAccount - metadata: - labels: - app.kubernetes.io/instance: chartsnap - app.kubernetes.io/managed-by: Helm - app.kubernetes.io/name: app1 -- app.kubernetes.io/version: 1.16.0 -+ app.kubernetes.io/version: 1.21.0 - helm.sh/chart: app1-0.1.0 - name: chartsnap-app1 - - -`)) - }) -}) diff --git a/pkg/unstructured/__snapshots__/suite_test.snap b/pkg/unstructured/__snapshots__/suite_test.snap new file mode 100644 index 0000000..1a34092 --- /dev/null +++ b/pkg/unstructured/__snapshots__/suite_test.snap @@ -0,0 +1,275 @@ +['Diff DiffContextLineN is 0 should return all diff 1'] +SnapShot = """ +- apiVersion: v2 ++ apiVersion: v1 + automountServiceAccountToken: true + kind: ServiceAccount + metadata: + labels: + app.kubernetes.io/instance: chartsnap + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: app1 + app.kubernetes.io/version: 1.16.0 + helm.sh/chart: app1-0.1.0 + name: chartsnap-app1 +- --- +- apiVersion: v1 +- kind: Namespace +- metadata: +- annotations: +- helm.sh/hook: test +- labels: +- app.kubernetes.io/instance: chartsnap +- app.kubernetes.io/managed-by: Helm +- app.kubernetes.io/name: app1 +- app.kubernetes.io/version: 1.16.0 +- helm.sh/chart: app1-0.1.0 +- name: chartsnap-app1-namespace + --- + apiVersion: v1 + data: +- ca.crt: '###DYNAMIC_FIELD###' +- tls.crt: '###DYNAMIC_FIELD###' +- tls.key: '###DYNAMIC_FIELD###' ++ ca.crt: IyMjRFlOQU1JQ19GSUVMRCMjIw== ++ tls.crt: IyMjRFlOQU1JQ19GSUVMRCMjIw== ++ tls.key: IyMjRFlOQU1JQ19GSUVMRCMjIw== + kind: Secret + metadata: + labels: + app.kubernetes.io/instance: chartsnap + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: app1 + app.kubernetes.io/version: 1.16.0 + helm.sh/chart: app1-0.1.0 + name: app1-cert + namespace: default + type: kubernetes.io/tls + --- + apiVersion: v1 + kind: Service + metadata: + labels: + app.kubernetes.io/instance: chartsnap + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: app1 + app.kubernetes.io/version: 1.16.0 + helm.sh/chart: app1-0.1.0 + name: chartsnap-app1 + spec: + ports: + - name: http + port: 80 + protocol: TCP + targetPort: http + selector: + app.kubernetes.io/instance: chartsnap + app.kubernetes.io/name: app1 +- type: LoadBalancer ++ type: ClusterIP + --- + apiVersion: apps/v1 + kind: Deployment + metadata: + labels: + app.kubernetes.io/instance: chartsnap + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: app1 +- app.kubernetes.io/version: 1.15.0 ++ app.kubernetes.io/version: 1.16.0 + helm.sh/chart: app1-0.1.0 + name: chartsnap-app1 + spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/instance: chartsnap + app.kubernetes.io/name: app1 + template: + metadata: + labels: + app.kubernetes.io/instance: chartsnap + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: app1 + app.kubernetes.io/version: 1.16.0 + helm.sh/chart: app1-0.1.0 + spec: + containers: + - image: nginx:1.16.0 + imagePullPolicy: IfNotPresent + livenessProbe: + httpGet: + path: / + port: http + name: app1 + ports: + - containerPort: 80 + name: http + protocol: TCP + readinessProbe: + httpGet: + path: / + port: http + resources: {} + securityContext: {} + securityContext: {} + serviceAccountName: chartsnap-app1 ++ --- ++ apiVersion: networking.k8s.io/v1 ++ kind: Ingress ++ annotations: ++ cert-manager.io/cluster-issuer: nameOfClusterIssuer ++ labels: ++ app.kubernetes.io/instance: chartsnap ++ app.kubernetes.io/managed-by: Helm ++ app.kubernetes.io/name: app1 ++ app.kubernetes.io/version: 1.16.0 ++ helm.sh/chart: app1-0.1.0 ++ name: chartsnap-app1 ++ ingressClassName: nginx ++ rules: ++ - host: chart-example.local ++ http: ++ paths: ++ - backend: ++ service: ++ name: chartsnap-app1 ++ port: ++ number: 80 ++ path: / ++ pathType: ImplementationSpecific ++ tls: ++ - hosts: ++ - chart-example.local ++ secretName: chart-example-tls + --- + apiVersion: v1 + kind: Pod + metadata: + annotations: + helm.sh/hook: test + labels: + app.kubernetes.io/instance: chartsnap + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: app1 + app.kubernetes.io/version: 1.16.0 + helm.sh/chart: app1-0.1.0 + name: chartsnap-app1-test-connection + spec: + containers: + - args: + - chartsnap-app1:80 + command: + - wget + image: busybox + name: wget + restartPolicy: Never +- a ++ +""" + +['Diff DiffContextLineN is 3 should return the extracted diff with previous/next 3 lines 1'] +SnapShot = """ +@@ KIND=ServiceAccount NAME=chartsnap-app1 LINE=1 +- apiVersion: v2 ++ apiVersion: v1 + automountServiceAccountToken: true + kind: ServiceAccount + metadata: + +@@ KIND=Namespace NAME=chartsnap-app1-namespace LINE=12 +- --- +- apiVersion: v1 +- kind: Namespace +- metadata: +- annotations: +- helm.sh/hook: test +- labels: +- app.kubernetes.io/instance: chartsnap +- app.kubernetes.io/managed-by: Helm +- app.kubernetes.io/name: app1 +- app.kubernetes.io/version: 1.16.0 +- helm.sh/chart: app1-0.1.0 +- name: chartsnap-app1-namespace + +@@ KIND=Secret NAME=app1-cert LINE=28 + apiVersion: v1 + data: +- ca.crt: '###DYNAMIC_FIELD###' +- tls.crt: '###DYNAMIC_FIELD###' +- tls.key: '###DYNAMIC_FIELD###' ++ ca.crt: IyMjRFlOQU1JQ19GSUVMRCMjIw== ++ tls.crt: IyMjRFlOQU1JQ19GSUVMRCMjIw== ++ tls.key: IyMjRFlOQU1JQ19GSUVMRCMjIw== + kind: Secret + metadata: + labels: + +@@ KIND=Service NAME=chartsnap-app1 LINE=62 + selector: + app.kubernetes.io/instance: chartsnap + app.kubernetes.io/name: app1 +- type: LoadBalancer ++ type: ClusterIP + +@@ KIND=Deployment NAME=chartsnap-app1 LINE=71 + app.kubernetes.io/instance: chartsnap + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: app1 +- app.kubernetes.io/version: 1.15.0 ++ app.kubernetes.io/version: 1.16.0 + helm.sh/chart: app1-0.1.0 + name: chartsnap-app1 + spec: + +@@ KIND=Ingress NAME= LINE=108 ++ --- ++ apiVersion: networking.k8s.io/v1 ++ kind: Ingress ++ annotations: ++ cert-manager.io/cluster-issuer: nameOfClusterIssuer ++ labels: ++ app.kubernetes.io/instance: chartsnap ++ app.kubernetes.io/managed-by: Helm ++ app.kubernetes.io/name: app1 ++ app.kubernetes.io/version: 1.16.0 ++ helm.sh/chart: app1-0.1.0 ++ name: chartsnap-app1 ++ ingressClassName: nginx ++ rules: ++ - host: chart-example.local ++ http: ++ paths: ++ - backend: ++ service: ++ name: chartsnap-app1 ++ port: ++ number: 80 ++ path: / ++ pathType: ImplementationSpecific ++ tls: ++ - hosts: ++ - chart-example.local ++ secretName: chart-example-tls + +@@ KIND=Pod NAME=chartsnap-app1-test-connection LINE=131 + image: busybox + name: wget + restartPolicy: Never +- a ++ +""" + +['Unknown OK report unknown as warning 1'] +SnapShot = """ +WARN: failed to recognize a resource in stdout/stderr of helm template command output. snapshot it as Unknown: +--- +object: + apiVersion: helm-chartsnap.jlandowner.dev/v1alpha1 + kind: Unknown + raw: |- + some: raw data + raw: + data: here + +---""" diff --git a/pkg/unstructured/diff.go b/pkg/unstructured/diff.go new file mode 100644 index 0000000..b327fe8 --- /dev/null +++ b/pkg/unstructured/diff.go @@ -0,0 +1,181 @@ +package unstructured + +import ( + "fmt" + "regexp" + "strings" + + "github.com/aryann/difflib" + "github.com/fatih/color" +) + +func MergeDiffOptions(opts []DiffOptions) DiffOptions { + var merged DiffOptions + for _, v := range opts { + if v.ContextLineN > merged.ContextLineN { + merged.ContextLineN = v.ContextLineN + } + } + return merged +} + +type DiffOptions struct { + ContextLineN int +} + +func (o *DiffOptions) Diff(x, y string) string { + var ( + sb strings.Builder + curr sequence = sequence{} + ) + diffs := difflib.Diff(strings.Split(x, "\n"), strings.Split(y, "\n")) + curr.reset(diffs, 0) + + for i, v := range diffs { + if o.ContextLineN < 1 { + // add all records + sb.WriteString(printDiff(v)) + continue + } + + // check if current cursor is divider + if divExp.MatchString(v.Payload) { + // if diff sequence is in progress, stop it + if curr.isDiffSequence { + curr.stopDiff() + + // add divider + sb.WriteString("\n") + } + + // reset cursor for next yaml sequence + // current cursor is divider so reset with next index + curr.reset(diffs, i+1) + } + curr.incrementLineN(v) + + log().Debug("processing diff line", "index", i, "lineN", curr.fileLineN, "kind", curr.kind, + "name", curr.name, "preIsDiff", curr.isDiffSequence, "delta", v.Delta, "payload", v.Payload) + + if v.Delta != difflib.Common { + curr.recordDiff() + + // if first diff, add a header and previous lines + if i == 0 || diffs[i-1].Delta == difflib.Common { + // add header + sb.WriteString(curr.header()) + + // add previous lines + for j := numInRange(curr.startIndex, len(diffs), i-o.ContextLineN); j < i; j++ { + sb.WriteString(fmt.Sprintf("%s\n", diffs[j])) + } + } + sb.WriteString(printDiff(v)) + + } else { + if curr.isDiffSequence { + curr.stopDiff() + + // add subsequent lines + for j := i; j < numInRange(0, len(diffs), i+o.ContextLineN); j++ { + sb.WriteString(fmt.Sprintf("%s\n", diffs[j])) + } + // add divider + sb.WriteString("\n") + } + } + } + return sb.String() +} + +var ( + divExp = regexp.MustCompile(`^---$`) + kindExp = regexp.MustCompile(`^kind: (.+)$`) + metaExp = regexp.MustCompile(`^metadata:$`) + nameExp = regexp.MustCompile(`^ name: (.+)$`) +) + +type sequence struct { + kind string + name string + startIndex int + isDiffSequence bool + + fileLineN int +} + +func (s *sequence) incrementLineN(r difflib.DiffRecord) { + // record actual snapshot file line number + // right only means new line added so it should not be considered + if r.Delta != difflib.RightOnly { + s.fileLineN++ + } +} + +func (s *sequence) reset(diffs []difflib.DiffRecord, i int) { + s.kind, s.name = findNextKind(diffs[i:]), findNextName(diffs[i:]) + s.startIndex = i + s.isDiffSequence = false +} + +func (s *sequence) recordDiff() { + s.isDiffSequence = true +} + +func (s *sequence) stopDiff() { + s.isDiffSequence = false +} + +func (s *sequence) header() string { + return printHeader(s.kind, s.name, s.fileLineN) +} + +func numInRange(min, max, v int) int { + if v >= min && v <= max { + return v + } else if v < min { + return min + } else { + return max + } +} + +func printDiff(d difflib.DiffRecord) string { + switch d.Delta { + case difflib.LeftOnly: + return color.New(color.FgRed).Sprintf("%s\n", d) + case difflib.RightOnly: + return color.New(color.FgGreen).Sprintf("%s\n", d) + default: + return fmt.Sprintf("%s\n", d) + } +} + +func printHeader(kind, name string, lineN int) string { + return color.New(color.FgCyan, color.Bold, color.Italic).Sprintf("@@ KIND=%s NAME=%s LINE=%d\n", kind, name, lineN) +} + +func findNextKind(diffs []difflib.DiffRecord) string { + for i := 0; i < len(diffs); i++ { + kindMatch := kindExp.FindStringSubmatch(diffs[i].Payload) + if len(kindMatch) > 0 { + return kindMatch[1] + } + } + return "" +} + +func findNextName(diffs []difflib.DiffRecord) string { + for i := 0; i < len(diffs); i++ { + if metaExp.MatchString(diffs[i].Payload) { + for j := i + 1; j < len(diffs)-i; j++ { + nameMatch := nameExp.FindStringSubmatch(diffs[j].Payload) + if len(nameMatch) > 0 { + return nameMatch[1] + } + } + return "" + } + } + return "" +} diff --git a/pkg/unstructured/diff_test.go b/pkg/unstructured/diff_test.go new file mode 100644 index 0000000..c4ef9ad --- /dev/null +++ b/pkg/unstructured/diff_test.go @@ -0,0 +1,235 @@ +package unstructured + +import ( + "testing" + + "github.com/aryann/difflib" +) + +var ( + testdata = []difflib.DiffRecord{ + {Delta: difflib.LeftOnly, Payload: "apiVersion: v2"}, + {Delta: difflib.RightOnly, Payload: "apiVersion: v1"}, + {Delta: difflib.Common, Payload: "automountServiceAccountToken: true"}, + {Delta: difflib.Common, Payload: "kind: ServiceAccount"}, + {Delta: difflib.Common, Payload: " labels:"}, + {Delta: difflib.Common, Payload: " app.kubernetes.io/instance: chartsnap"}, + {Delta: difflib.Common, Payload: " app.kubernetes.io/managed-by: Helm"}, + {Delta: difflib.Common, Payload: " app.kubernetes.io/name: app1"}, + {Delta: difflib.Common, Payload: " app.kubernetes.io/version: 1.16.0"}, + {Delta: difflib.Common, Payload: " helm.sh/chart: app1-0.1.0"}, + {Delta: difflib.Common, Payload: " name: chartsnap-app1"}, + {Delta: difflib.Common, Payload: "---"}, + {Delta: difflib.LeftOnly, Payload: "apiVersion: v1"}, + {Delta: difflib.LeftOnly, Payload: "kind: Namespace"}, + {Delta: difflib.LeftOnly, Payload: " annotations:"}, + {Delta: difflib.LeftOnly, Payload: " helm.sh/hook: test"}, + {Delta: difflib.LeftOnly, Payload: " labels:"}, + {Delta: difflib.LeftOnly, Payload: " app.kubernetes.io/instance: chartsnap"}, + {Delta: difflib.LeftOnly, Payload: " app.kubernetes.io/managed-by: Helm"}, + {Delta: difflib.LeftOnly, Payload: " app.kubernetes.io/name: app1"}, + {Delta: difflib.LeftOnly, Payload: " app.kubernetes.io/version: 1.16.0"}, + {Delta: difflib.LeftOnly, Payload: " helm.sh/chart: app1-0.1.0"}, + {Delta: difflib.LeftOnly, Payload: " name: chartsnap-app1-namespace"}, + {Delta: difflib.Common, Payload: "---"}, + {Delta: difflib.Common, Payload: "apiVersion: v1"}, + {Delta: difflib.LeftOnly, Payload: " ca.crt: '###DYNAMIC_FIELD###'"}, + {Delta: difflib.LeftOnly, Payload: " tls.crt: '###DYNAMIC_FIELD###'"}, + {Delta: difflib.LeftOnly, Payload: " tls.key: '###DYNAMIC_FIELD###'"}, + {Delta: difflib.RightOnly, Payload: " ca.crt: IyMjRFlOQU1JQ19GSUVMRCMjIw=="}, + {Delta: difflib.RightOnly, Payload: " tls.crt: IyMjRFlOQU1JQ19GSUVMRCMjIw=="}, + {Delta: difflib.RightOnly, Payload: " tls.key: IyMjRFlOQU1JQ19GSUVMRCMjIw=="}, + {Delta: difflib.Common, Payload: "kind: Secret"}, + {Delta: difflib.Common, Payload: " labels:"}, + {Delta: difflib.Common, Payload: " app.kubernetes.io/instance: chartsnap"}, + {Delta: difflib.Common, Payload: " app.kubernetes.io/managed-by: Helm"}, + {Delta: difflib.Common, Payload: " app.kubernetes.io/name: app1"}, + {Delta: difflib.Common, Payload: " app.kubernetes.io/version: 1.16.0"}, + {Delta: difflib.Common, Payload: " helm.sh/chart: app1-0.1.0"}, + {Delta: difflib.Common, Payload: " name: app1-cert"}, + {Delta: difflib.Common, Payload: " namespace: default"}, + {Delta: difflib.Common, Payload: "type: kubernetes.io/tls"}, + {Delta: difflib.Common, Payload: "---"}, + {Delta: difflib.Common, Payload: "apiVersion: v1"}, + {Delta: difflib.Common, Payload: "kind: Service"}, + {Delta: difflib.Common, Payload: " labels:"}, + {Delta: difflib.Common, Payload: " app.kubernetes.io/instance: chartsnap"}, + {Delta: difflib.Common, Payload: " app.kubernetes.io/managed-by: Helm"}, + {Delta: difflib.Common, Payload: " app.kubernetes.io/name: app1"}, + {Delta: difflib.Common, Payload: " app.kubernetes.io/version: 1.16.0"}, + {Delta: difflib.Common, Payload: " helm.sh/chart: app1-0.1.0"}, + {Delta: difflib.Common, Payload: " name: chartsnap-app1"}, + {Delta: difflib.Common, Payload: " ports:"}, + {Delta: difflib.Common, Payload: " - name: http"}, + {Delta: difflib.Common, Payload: " port: 80"}, + {Delta: difflib.Common, Payload: " protocol: TCP"}, + {Delta: difflib.Common, Payload: " targetPort: http"}, + {Delta: difflib.Common, Payload: " selector:"}, + {Delta: difflib.Common, Payload: " app.kubernetes.io/instance: chartsnap"}, + {Delta: difflib.Common, Payload: " app.kubernetes.io/name: app1"}, + {Delta: difflib.LeftOnly, Payload: " type: LoadBalancer"}, + {Delta: difflib.RightOnly, Payload: " type: ClusterIP"}, + {Delta: difflib.Common, Payload: "---"}, + {Delta: difflib.Common, Payload: "apiVersion: apps/v1"}, + {Delta: difflib.Common, Payload: "kind: Deployment"}, + {Delta: difflib.Common, Payload: " labels:"}, + {Delta: difflib.Common, Payload: " app.kubernetes.io/instance: chartsnap"}, + {Delta: difflib.Common, Payload: " app.kubernetes.io/managed-by: Helm"}, + {Delta: difflib.Common, Payload: " app.kubernetes.io/name: app1"}, + {Delta: difflib.LeftOnly, Payload: " app.kubernetes.io/version: 1.15.0"}, + {Delta: difflib.RightOnly, Payload: " app.kubernetes.io/version: 1.16.0"}, + {Delta: difflib.Common, Payload: " helm.sh/chart: app1-0.1.0"}, + {Delta: difflib.Common, Payload: " name: chartsnap-app1"}, + {Delta: difflib.Common, Payload: " replicas: 1"}, + {Delta: difflib.Common, Payload: " selector:"}, + {Delta: difflib.Common, Payload: " matchLabels:"}, + {Delta: difflib.Common, Payload: " app.kubernetes.io/instance: chartsnap"}, + {Delta: difflib.Common, Payload: " app.kubernetes.io/name: app1"}, + {Delta: difflib.Common, Payload: " template:"}, + {Delta: difflib.Common, Payload: " metadata:"}, + {Delta: difflib.Common, Payload: " labels:"}, + {Delta: difflib.Common, Payload: " app.kubernetes.io/instance: chartsnap"}, + {Delta: difflib.Common, Payload: " app.kubernetes.io/managed-by: Helm"}, + {Delta: difflib.Common, Payload: " app.kubernetes.io/name: app1"}, + {Delta: difflib.Common, Payload: " app.kubernetes.io/version: 1.16.0"}, + {Delta: difflib.Common, Payload: " helm.sh/chart: app1-0.1.0"}, + {Delta: difflib.Common, Payload: " spec:"}, + {Delta: difflib.Common, Payload: " containers:"}, + {Delta: difflib.Common, Payload: " - image: nginx:1.16.0"}, + {Delta: difflib.Common, Payload: " imagePullPolicy: IfNotPresent"}, + {Delta: difflib.Common, Payload: " livenessProbe:"}, + {Delta: difflib.Common, Payload: " httpGet:"}, + {Delta: difflib.Common, Payload: " path: /"}, + {Delta: difflib.Common, Payload: " port: http"}, + {Delta: difflib.Common, Payload: " name: app1"}, + {Delta: difflib.Common, Payload: " ports:"}, + {Delta: difflib.Common, Payload: " - containerPort: 80"}, + {Delta: difflib.Common, Payload: " name: http"}, + {Delta: difflib.Common, Payload: " protocol: TCP"}, + {Delta: difflib.Common, Payload: " readinessProbe:"}, + {Delta: difflib.Common, Payload: " httpGet:"}, + {Delta: difflib.Common, Payload: " path: /"}, + {Delta: difflib.Common, Payload: " port: http"}, + {Delta: difflib.Common, Payload: " resources: {}"}, + {Delta: difflib.Common, Payload: " securityContext: {}"}, + {Delta: difflib.Common, Payload: " securityContext: {}"}, + {Delta: difflib.Common, Payload: " serviceAccountName: chartsnap-app1"}, + {Delta: difflib.RightOnly, Payload: "---"}, + {Delta: difflib.RightOnly, Payload: "apiVersion: networking.k8s.io/v1"}, + {Delta: difflib.RightOnly, Payload: "kind: Ingress"}, + {Delta: difflib.RightOnly, Payload: " annotations:"}, + {Delta: difflib.RightOnly, Payload: " cert-manager.io/cluster-issuer: nameOfClusterIssuer"}, + {Delta: difflib.RightOnly, Payload: " labels:"}, + {Delta: difflib.RightOnly, Payload: " app.kubernetes.io/instance: chartsnap"}, + {Delta: difflib.RightOnly, Payload: " app.kubernetes.io/managed-by: Helm"}, + {Delta: difflib.RightOnly, Payload: " app.kubernetes.io/name: app1"}, + {Delta: difflib.RightOnly, Payload: " app.kubernetes.io/version: 1.16.0"}, + {Delta: difflib.RightOnly, Payload: " helm.sh/chart: app1-0.1.0"}, + {Delta: difflib.RightOnly, Payload: " name: chartsnap-app1"}, + {Delta: difflib.RightOnly, Payload: " ingressClassName: nginx"}, + {Delta: difflib.RightOnly, Payload: " rules:"}, + {Delta: difflib.RightOnly, Payload: " - host: chart-example.local"}, + {Delta: difflib.RightOnly, Payload: " http:"}, + {Delta: difflib.RightOnly, Payload: " paths:"}, + {Delta: difflib.RightOnly, Payload: " - backend:"}, + {Delta: difflib.RightOnly, Payload: " service:"}, + {Delta: difflib.RightOnly, Payload: " name: chartsnap-app1"}, + {Delta: difflib.RightOnly, Payload: " port:"}, + {Delta: difflib.RightOnly, Payload: " number: 80"}, + {Delta: difflib.RightOnly, Payload: " path: /"}, + {Delta: difflib.RightOnly, Payload: " pathType: ImplementationSpecific"}, + {Delta: difflib.RightOnly, Payload: " tls:"}, + {Delta: difflib.RightOnly, Payload: " - hosts:"}, + {Delta: difflib.RightOnly, Payload: " - chart-example.local"}, + {Delta: difflib.RightOnly, Payload: " secretName: chart-example-tls"}, + {Delta: difflib.Common, Payload: "---"}, + {Delta: difflib.Common, Payload: "apiVersion: v1"}, + {Delta: difflib.Common, Payload: "kind: Pod"}, + {Delta: difflib.Common, Payload: " annotations:"}, + {Delta: difflib.Common, Payload: " helm.sh/hook: test"}, + {Delta: difflib.Common, Payload: " labels:"}, + {Delta: difflib.Common, Payload: " app.kubernetes.io/instance: chartsnap"}, + {Delta: difflib.Common, Payload: " app.kubernetes.io/managed-by: Helm"}, + {Delta: difflib.Common, Payload: " app.kubernetes.io/name: app1"}, + {Delta: difflib.Common, Payload: " app.kubernetes.io/version: 1.16.0"}, + {Delta: difflib.Common, Payload: " helm.sh/chart: app1-0.1.0"}, + {Delta: difflib.Common, Payload: " name: chartsnap-app1-test-connection"}, + {Delta: difflib.Common, Payload: " containers:"}, + {Delta: difflib.Common, Payload: " - args:"}, + {Delta: difflib.Common, Payload: " - chartsnap-app1:80"}, + {Delta: difflib.Common, Payload: " command:"}, + {Delta: difflib.Common, Payload: " - wget"}, + {Delta: difflib.Common, Payload: " image: busybox"}, + {Delta: difflib.Common, Payload: " name: wget"}, + {Delta: difflib.Common, Payload: " restartPolicy: Never"}, + {Delta: difflib.LeftOnly, Payload: "a"}, + } +) + +func TestMergeDiffOptions(t *testing.T) { + opts := []DiffOptions{ + {ContextLineN: 5}, + {ContextLineN: 3}, + {ContextLineN: 7}, + } + + merged := MergeDiffOptions(opts) + + if merged.ContextLineN != 7 { + t.Errorf("Expected DiffContextLineN to be 7, got %d", merged.ContextLineN) + } +} + +func Test_printDiff(t *testing.T) { + d := difflib.DiffRecord{ + Delta: difflib.LeftOnly, + Payload: "abc", + } + + want := "- abc\n" + + if got := printDiff(d); got != want { + t.Errorf("printDiff() = %v, want %v", got, want) + } +} + +func Test_printHeader(t *testing.T) { + kind := "TestKind" + name := "TestName" + lineN := 5 + + want := "@@ KIND=TestKind NAME=TestName LINE=5\n" + + if got := printHeader(kind, name, lineN); got != want { + t.Errorf("printHeader() = %v, want %v", got, want) + } +} + +func Test_findNextKind(t *testing.T) { + diffs := []difflib.DiffRecord{ + {Delta: difflib.Common, Payload: "abc"}, + {Delta: difflib.Common, Payload: "kind: Pod"}, + {Delta: difflib.Common, Payload: "def"}, + } + + want := "Pod" + + if got := findNextKind(diffs); got != want { + t.Errorf("findNextKind() = %v, want %v", got, want) + } +} + +func Test_findNextName(t *testing.T) { + diffs := []difflib.DiffRecord{ + {Delta: difflib.Common, Payload: "abc"}, + {Delta: difflib.Common, Payload: "metadata:"}, + {Delta: difflib.Common, Payload: " name: TestName"}, + {Delta: difflib.Common, Payload: "def"}, + } + + want := "TestName" + + if got := findNextName(diffs); got != want { + t.Errorf("findNextName() = %v, want %v", got, want) + } +} diff --git a/pkg/unstructured/suite_test.go b/pkg/unstructured/suite_test.go new file mode 100644 index 0000000..4c7af0f --- /dev/null +++ b/pkg/unstructured/suite_test.go @@ -0,0 +1,64 @@ +package unstructured + +import ( + "os" + "testing" + + . "github.com/jlandowner/helm-chartsnap/pkg/snap/gomega" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestUnstructured(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Unstructured Suite") +} + +var _ = Describe("Diff", func() { + Context("DiffContextLineN is 3", func() { + It("should return the extracted diff with previous/next 3 lines", func() { + expectedSnap := mustReadFile("testdata/expected.snap") + actualSnap := mustReadFile("testdata/actual.snap") + + d := DiffOptions{ + ContextLineN: 3, + } + diff := d.Diff(expectedSnap, actualSnap) + Ω(diff).To(MatchSnapShot()) + }) + }) + + Context("DiffContextLineN is 0", func() { + It("should return all diff", func() { + expectedSnap := mustReadFile("testdata/expected.snap") + actualSnap := mustReadFile("testdata/actual.snap") + + d := DiffOptions{ + ContextLineN: 0, + } + diff := d.Diff(expectedSnap, actualSnap) + Ω(diff).To(MatchSnapShot()) + }) + }) +}) + +var _ = Describe("Unknown", func() { + Context("OK", func() { + It("report unknown as warning", func() { + raw := `some: raw data +raw: + data: here` + err := NewUnknownError(raw) + + Ω(err.Error()).To(MatchSnapShot()) + }) + }) +}) + +func mustReadFile(path string) string { + data, err := os.ReadFile(path) + if err != nil { + panic(err) + } + return string(data) +} diff --git a/pkg/unstructured/testdata/actual.snap b/pkg/unstructured/testdata/actual.snap new file mode 100644 index 0000000..de6ba12 --- /dev/null +++ b/pkg/unstructured/testdata/actual.snap @@ -0,0 +1,145 @@ +apiVersion: v1 +automountServiceAccountToken: true +kind: ServiceAccount +metadata: + labels: + app.kubernetes.io/instance: chartsnap + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: app1 + app.kubernetes.io/version: 1.16.0 + helm.sh/chart: app1-0.1.0 + name: chartsnap-app1 +--- +apiVersion: v1 +data: + ca.crt: IyMjRFlOQU1JQ19GSUVMRCMjIw== + tls.crt: IyMjRFlOQU1JQ19GSUVMRCMjIw== + tls.key: IyMjRFlOQU1JQ19GSUVMRCMjIw== +kind: Secret +metadata: + labels: + app.kubernetes.io/instance: chartsnap + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: app1 + app.kubernetes.io/version: 1.16.0 + helm.sh/chart: app1-0.1.0 + name: app1-cert + namespace: default +type: kubernetes.io/tls +--- +apiVersion: v1 +kind: Service +metadata: + labels: + app.kubernetes.io/instance: chartsnap + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: app1 + app.kubernetes.io/version: 1.16.0 + helm.sh/chart: app1-0.1.0 + name: chartsnap-app1 +spec: + ports: + - name: http + port: 80 + protocol: TCP + targetPort: http + selector: + app.kubernetes.io/instance: chartsnap + app.kubernetes.io/name: app1 + type: ClusterIP +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app.kubernetes.io/instance: chartsnap + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: app1 + app.kubernetes.io/version: 1.16.0 + helm.sh/chart: app1-0.1.0 + name: chartsnap-app1 +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/instance: chartsnap + app.kubernetes.io/name: app1 + template: + metadata: + labels: + app.kubernetes.io/instance: chartsnap + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: app1 + app.kubernetes.io/version: 1.16.0 + helm.sh/chart: app1-0.1.0 + spec: + containers: + - image: nginx:1.16.0 + imagePullPolicy: IfNotPresent + livenessProbe: + httpGet: + path: / + port: http + name: app1 + ports: + - containerPort: 80 + name: http + protocol: TCP + readinessProbe: + httpGet: + path: / + port: http + resources: {} + securityContext: {} + securityContext: {} + serviceAccountName: chartsnap-app1 +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress + annotations: + cert-manager.io/cluster-issuer: nameOfClusterIssuer + labels: + app.kubernetes.io/instance: chartsnap + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: app1 + app.kubernetes.io/version: 1.16.0 + helm.sh/chart: app1-0.1.0 + name: chartsnap-app1 + ingressClassName: nginx + rules: + - host: chart-example.local + http: + paths: + - backend: + service: + name: chartsnap-app1 + port: + number: 80 + path: / + pathType: ImplementationSpecific + tls: + - hosts: + - chart-example.local + secretName: chart-example-tls +--- +apiVersion: v1 +kind: Pod +metadata: + annotations: + helm.sh/hook: test + labels: + app.kubernetes.io/instance: chartsnap + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: app1 + app.kubernetes.io/version: 1.16.0 + helm.sh/chart: app1-0.1.0 + name: chartsnap-app1-test-connection +spec: + containers: + - args: + - chartsnap-app1:80 + command: + - wget + image: busybox + name: wget + restartPolicy: Never diff --git a/pkg/snap/testdata/unstructured_diff.yaml b/pkg/unstructured/testdata/expected.snap similarity index 61% rename from pkg/snap/testdata/unstructured_diff.yaml rename to pkg/unstructured/testdata/expected.snap index 9c17e15..61008d0 100644 --- a/pkg/snap/testdata/unstructured_diff.yaml +++ b/pkg/unstructured/testdata/expected.snap @@ -1,3 +1,66 @@ +apiVersion: v2 +automountServiceAccountToken: true +kind: ServiceAccount +metadata: + labels: + app.kubernetes.io/instance: chartsnap + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: app1 + app.kubernetes.io/version: 1.16.0 + helm.sh/chart: app1-0.1.0 + name: chartsnap-app1 +--- +apiVersion: v1 +kind: Namespace +metadata: + annotations: + helm.sh/hook: test + labels: + app.kubernetes.io/instance: chartsnap + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: app1 + app.kubernetes.io/version: 1.16.0 + helm.sh/chart: app1-0.1.0 + name: chartsnap-app1-namespace +--- +apiVersion: v1 +data: + ca.crt: '###DYNAMIC_FIELD###' + tls.crt: '###DYNAMIC_FIELD###' + tls.key: '###DYNAMIC_FIELD###' +kind: Secret +metadata: + labels: + app.kubernetes.io/instance: chartsnap + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: app1 + app.kubernetes.io/version: 1.16.0 + helm.sh/chart: app1-0.1.0 + name: app1-cert + namespace: default +type: kubernetes.io/tls +--- +apiVersion: v1 +kind: Service +metadata: + labels: + app.kubernetes.io/instance: chartsnap + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/name: app1 + app.kubernetes.io/version: 1.16.0 + helm.sh/chart: app1-0.1.0 + name: chartsnap-app1 +spec: + ports: + - name: http + port: 80 + protocol: TCP + targetPort: http + selector: + app.kubernetes.io/instance: chartsnap + app.kubernetes.io/name: app1 + type: LoadBalancer +--- apiVersion: apps/v1 kind: Deployment metadata: @@ -5,7 +68,7 @@ metadata: app.kubernetes.io/instance: chartsnap app.kubernetes.io/managed-by: Helm app.kubernetes.io/name: app1 - app.kubernetes.io/version: 1.21.0 + app.kubernetes.io/version: 1.15.0 helm.sh/chart: app1-0.1.0 name: chartsnap-app1 spec: @@ -20,17 +83,21 @@ spec: app.kubernetes.io/instance: chartsnap app.kubernetes.io/managed-by: Helm app.kubernetes.io/name: app1 - app.kubernetes.io/version: 1.21.0 + app.kubernetes.io/version: 1.16.0 helm.sh/chart: app1-0.1.0 spec: containers: - - image: nginx:1.21.0 + - image: nginx:1.16.0 imagePullPolicy: IfNotPresent + livenessProbe: + httpGet: + path: / + port: http name: app1 ports: - - containerPort: 80 - name: http - protocol: TCP + - containerPort: 80 + name: http + protocol: TCP readinessProbe: httpGet: path: / @@ -49,52 +116,16 @@ metadata: app.kubernetes.io/instance: chartsnap app.kubernetes.io/managed-by: Helm app.kubernetes.io/name: app1 - app.kubernetes.io/version: 1.21.0 + app.kubernetes.io/version: 1.16.0 helm.sh/chart: app1-0.1.0 name: chartsnap-app1-test-connection spec: containers: - args: - - chartsnap-app1:80 + - chartsnap-app1:80 command: - - wget + - wget image: busybox name: wget restartPolicy: Never ---- -apiVersion: v1 -kind: Service -metadata: - labels: - app.kubernetes.io/instance: chartsnap - app.kubernetes.io/managed-by: Helm - app.kubernetes.io/name: app1 - app.kubernetes.io/version: 1.21.0 - helm.sh/chart: app1-0.1.0 - name: chartsnap-app1 -spec: - ports: - - name: http - port: 80 - protocol: TCP - targetPort: http - - name: https - port: 443 - protocol: TCP - targetPort: https - selector: - app.kubernetes.io/instance: chartsnap - app.kubernetes.io/name: app1 - type: ClusterIP ---- -apiVersion: v1 -automountServiceAccountToken: true -kind: ServiceAccount -metadata: - labels: - app.kubernetes.io/instance: chartsnap - app.kubernetes.io/managed-by: Helm - app.kubernetes.io/name: app1 - app.kubernetes.io/version: 1.21.0 - helm.sh/chart: app1-0.1.0 - name: chartsnap-app1 +a \ No newline at end of file diff --git a/pkg/unstructured/unknown.go b/pkg/unstructured/unknown.go index 1fe1388..1ca5b99 100644 --- a/pkg/unstructured/unknown.go +++ b/pkg/unstructured/unknown.go @@ -24,9 +24,9 @@ type UnknownError struct { func (e *UnknownError) Error() string { out, err := yaml.Marshal(e.Unstructured()) if err != nil { - return "xxx" + panic(err) } - return fmt.Sprintf("WARN: failed to recognize a resource. snapshot as Unknown: \n---\n%s\n---", out) + return fmt.Sprintf("WARN: failed to recognize a resource in stdout/stderr of helm template command output. snapshot it as Unknown: \n---\n%s\n---", out) } func (e *UnknownError) Unstructured() *metaV1.Unstructured { diff --git a/pkg/unstructured/unknown_test.go b/pkg/unstructured/unknown_test.go new file mode 100644 index 0000000..c1c84b6 --- /dev/null +++ b/pkg/unstructured/unknown_test.go @@ -0,0 +1,27 @@ +package unstructured + +import ( + "reflect" + "testing" + + metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +func TestUnknownError_Unstructured(t *testing.T) { + raw := "some raw data" + err := NewUnknownError(raw) + + obj := err.Unstructured() + expectedObj := &metaV1.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "helm-chartsnap.jlandowner.dev/v1alpha1", + "kind": "Unknown", + "raw": "some raw data", + }, + } + + if !reflect.DeepEqual(obj, expectedObj) { + t.Errorf("Expected obj to be %v, but got %v", err, obj) + } + +} diff --git a/pkg/unstructured/unstructured.go b/pkg/unstructured/unstructured.go index 87ddfd4..7056251 100644 --- a/pkg/unstructured/unstructured.go +++ b/pkg/unstructured/unstructured.go @@ -1,30 +1,48 @@ package unstructured import ( + "bytes" "encoding/json" "fmt" + "log/slog" "regexp" - "sort" "strings" jsonpatch "github.com/evanphx/json-patch/v5" metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apimachinery/pkg/runtime/serializer/yaml" - yamlv3 "sigs.k8s.io/yaml/goyaml.v3" + yamlUtil "k8s.io/apimachinery/pkg/runtime/serializer/yaml" + yaml "sigs.k8s.io/yaml/goyaml.v3" ) +var logger *slog.Logger + +func SetLogger(slogr *slog.Logger) { + logger = slogr +} + +func log() *slog.Logger { + if logger == nil { + logger = slog.Default() + } + return logger +} + func Encode(arr []metaV1.Unstructured) ([]byte, error) { - sort.SliceStable(arr, func(i, j int) bool { - if arr[i].GetAPIVersion() != arr[j].GetAPIVersion() { - return arr[i].GetAPIVersion() < arr[j].GetAPIVersion() - } - if arr[i].GetKind() != arr[j].GetKind() { - return arr[i].GetKind() < arr[j].GetKind() + var buf bytes.Buffer + enc := yaml.NewEncoder(&buf) + enc.SetIndent(2) + + for _, d := range arr { + if err := enc.Encode(d.Object); err != nil { + return nil, fmt.Errorf("failed to encode unstructured to YAML: %w", err) } - return arr[i].GetName() < arr[j].GetName() - }) - return yamlv3.Marshal(arr) + } + if err := enc.Close(); err != nil { + return nil, fmt.Errorf("failed to close encoder: %w", err) + } + + return buf.Bytes(), nil } func Decode(source string) ([]metaV1.Unstructured, []error) { @@ -90,7 +108,7 @@ func StringToUnstructured(data string) (*schema.GroupVersionKind, *metaV1.Unstru func BytesToUnstructured(data []byte) (*schema.GroupVersionKind, *metaV1.Unstructured, error) { obj := &metaV1.Unstructured{} - dec := yaml.NewDecodingSerializer(metaV1.UnstructuredJSONScheme) + dec := yamlUtil.NewDecodingSerializer(metaV1.UnstructuredJSONScheme) _, gvk, err := dec.Decode([]byte(data), nil, obj) if err != nil { return nil, nil, err diff --git a/pkg/unstructured/unstructured_test.go b/pkg/unstructured/unstructured_test.go index b7cd486..d1c3fe3 100644 --- a/pkg/unstructured/unstructured_test.go +++ b/pkg/unstructured/unstructured_test.go @@ -41,16 +41,15 @@ func TestEncode(t *testing.T) { }, }, }, - want: `- object: - apiVersion: v1 - kind: Pod - metadata: - name: pod1 -- object: - apiVersion: v1 - kind: Service - metadata: - name: service1 + want: `apiVersion: v1 +kind: Pod +metadata: + name: pod1 +--- +apiVersion: v1 +kind: Service +metadata: + name: service1 `, }, } diff --git a/pkg/snap/unstructured.go b/pkg/unstructured/v1/legacy.go similarity index 60% rename from pkg/snap/unstructured.go rename to pkg/unstructured/v1/legacy.go index 23105e2..ea0752c 100644 --- a/pkg/snap/unstructured.go +++ b/pkg/unstructured/v1/legacy.go @@ -1,36 +1,29 @@ -package snap +package unstructured import ( "fmt" "regexp" + "sort" "strings" "github.com/aryann/difflib" "github.com/fatih/color" - gomegatypes "github.com/onsi/gomega/types" metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - - unstructutils "github.com/jlandowner/helm-chartsnap/pkg/unstructured" + yaml "sigs.k8s.io/yaml/goyaml.v3" ) -func UnstructuredSnapShotMatcher(snapFile string, snapId string, diffOpts ...DiffOptions) *snapShotMatcher { - o := mergeDiffOpts(diffOpts) - - return &snapShotMatcher{ - snapFilePath: snapFile, - snapId: snapId, - fs: cacheFs, - diffFunc: UnstructuredSnapshotDiff, - diffOptions: o, - } -} - -func UnstructuredMatch(matcher gomegatypes.GomegaMatcher, manifests []metaV1.Unstructured) (success bool, err error) { - res, err := unstructutils.Encode(manifests) - if err != nil { - return false, fmt.Errorf("failed to encode manifests: %w", err) - } - return matcher.Match(string(res)) +// Encode encoding legacy formatted yaml +func Encode(arr []metaV1.Unstructured) ([]byte, error) { + sort.SliceStable(arr, func(i, j int) bool { + if arr[i].GetAPIVersion() != arr[j].GetAPIVersion() { + return arr[i].GetAPIVersion() < arr[j].GetAPIVersion() + } + if arr[i].GetKind() != arr[j].GetKind() { + return arr[i].GetKind() < arr[j].GetKind() + } + return arr[i].GetName() < arr[j].GetName() + }) + return yaml.Marshal(arr) } // extract kind value @@ -63,7 +56,11 @@ func findName(diffs []difflib.DiffRecord) string { return "" } -func UnstructuredSnapshotDiff(x, y string, o DiffOptions) string { +type DiffOptions struct { + ContextLineN int +} + +func (o *DiffOptions) Diff(x, y string) string { divExp := regexp.MustCompile(`^ - object:$`) diffs := difflib.Diff(strings.Split(x, "\n"), strings.Split(y, "\n")) @@ -75,7 +72,7 @@ func UnstructuredSnapshotDiff(x, y string, o DiffOptions) string { ) for i, v := range diffs { - if o.ContextLineN() < 1 { + if o.ContextLineN < 1 { // all records sb.WriteString(diffString(v)) continue @@ -84,7 +81,6 @@ func UnstructuredSnapshotDiff(x, y string, o DiffOptions) string { if divExp.Match([]byte(v.String())) { isDiffSequence = false currentKind, currentName = findKind(diffs[i:]), findName(diffs[i:]) - log().Debug("div match", "kind", currentKind, "name", currentName, "index", i) } if v.Delta != difflib.Common { @@ -93,10 +89,10 @@ func UnstructuredSnapshotDiff(x, y string, o DiffOptions) string { // if first diff, add a header and previous lines if i > 0 && diffs[i-1].Delta == difflib.Common { // header - sb.WriteString(color.New(color.FgCyan).Sprintf("--- kind=%s name=%s line=%d\n", currentKind, currentName, i)) + sb.WriteString(color.New(color.FgCyan).Sprintf("@@ KIND=%s NAME=%s LINE=%d\n", currentKind, currentName, i)) // previous lines - for j := intInRange(0, len(diffs), i-o.DiffContextLineN); j < i; j++ { + for j := intInRange(0, len(diffs), i-o.ContextLineN); j < i; j++ { sb.WriteString(fmt.Sprintf("%s\n", diffs[j])) } } @@ -107,7 +103,7 @@ func UnstructuredSnapshotDiff(x, y string, o DiffOptions) string { isDiffSequence = false // subsequent lines - for j := i; j < intInRange(0, len(diffs), i+o.DiffContextLineN); j++ { + for j := i; j < intInRange(0, len(diffs), i+o.ContextLineN); j++ { sb.WriteString(fmt.Sprintf("%s\n", diffs[j])) } // divider @@ -117,3 +113,24 @@ func UnstructuredSnapshotDiff(x, y string, o DiffOptions) string { } return sb.String() } + +func intInRange(min, max, v int) int { + if v >= min && v <= max { + return v + } else if v < min { + return min + } else { + return max + } +} + +func diffString(d difflib.DiffRecord) string { + switch d.Delta { + case difflib.LeftOnly: + return color.New(color.FgRed).Sprintf("%s\n", d) + case difflib.RightOnly: + return color.New(color.FgGreen).Sprintf("%s\n", d) + default: + return fmt.Sprintf("%s\n", d) + } +}