From c7cc0c269099ac2bd40b1787ed22f0331d99d448 Mon Sep 17 00:00:00 2001 From: Eron Wright Date: Wed, 13 Sep 2023 16:33:36 -0700 Subject: [PATCH 1/9] Compute a checksum of template output --- provider/pkg/provider/helm_release.go | 35 +++++++++++++++++++++------ 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/provider/pkg/provider/helm_release.go b/provider/pkg/provider/helm_release.go index b94fd12316..290fc0c224 100644 --- a/provider/pkg/provider/helm_release.go +++ b/provider/pkg/provider/helm_release.go @@ -16,6 +16,8 @@ package provider import ( "context" + "crypto/sha256" + "encoding/hex" "encoding/json" "errors" "fmt" @@ -132,6 +134,8 @@ type Release struct { // Manifest map[string]interface{} `json:"manifest,omitempty"` // Names of resources created by the release grouped by "kind/version". ResourceNames map[string][]string `json:"resourceNames,omitempty"` + // The checksum of the rendered template (to detect changes) + Checksum string `json:"checksum,omitempty"` // Status of the deployed release. Status *ReleaseStatus `json:"status,omitempty"` } @@ -353,19 +357,23 @@ func (r *helmReleaseProvider) Check(ctx context.Context, req *pulumirpc.CheckReq return nil, err } - resourceNames, err := r.computeResourceNames(new, r.clientSet) + resourceInfo, err := r.computeResourceInfo(new, r.clientSet) if err != nil && errors.Is(err, fs.ErrNotExist) { // Likely because the chart is not readily available (e.g. import of chart where no repo info is stored). // Declare bankruptcy in being able to determine the underlying resources and hope for the best // further down the operations. - resourceNames = nil + resourceInfo = nil } else if err != nil { return nil, err } - if len(new.ResourceNames) == 0 { - new.ResourceNames = resourceNames + if resourceInfo != nil { + new.Checksum = resourceInfo.checksum + if len(new.ResourceNames) == 0 { + new.ResourceNames = resourceInfo.resourceNames + } } + logger.V(9).Infof("New: %+v", new) news = resource.NewPropertyMap(new) } @@ -1089,8 +1097,14 @@ func (r *helmReleaseProvider) Delete(ctx context.Context, req *pulumirpc.DeleteR return &pbempty.Empty{}, nil } -func (r *helmReleaseProvider) computeResourceNames(rel *Release, clientSet *clients.DynamicClientSet) (map[string][]string, error) { - logger.V(9).Infof("Looking up resource names for release: %q: %#v", rel.Name, rel) +type resourceInfo struct { + resourceNames map[string][]string + checksum string +} + +func (r *helmReleaseProvider) computeResourceInfo(rel *Release, clientSet *clients.DynamicClientSet) (*resourceInfo, error) { + logger.V(9).Infof("Computing resource info for release: %q: %#v", rel.Name, rel) + result := &resourceInfo{} helmChartOpts := r.chartOptsFromRelease(rel) logger.V(9).Infof("About to template: %+v", helmChartOpts) @@ -1099,12 +1113,19 @@ func (r *helmReleaseProvider) computeResourceNames(rel *Release, clientSet *clie return nil, err } + // compute the resource names _, resourceNames, err := convertYAMLManifestToJSON(templ) if err != nil { return nil, err } + result.resourceNames = resourceNames + + // compute the checksum of the rendered output, to be able to detect changes + h := sha256.New() + h.Write([]byte(templ)) + result.checksum = hex.EncodeToString(h.Sum(nil)) - return resourceNames, nil + return result, nil } func (r *helmReleaseProvider) chartOptsFromRelease(rel *Release) HelmChartOpts { From 9a434099869ccb5343ef80bfe30ac9a0fd296571 Mon Sep 17 00:00:00 2001 From: Eron Wright Date: Thu, 14 Sep 2023 00:08:21 -0700 Subject: [PATCH 2/9] Avoid spurious release --- provider/pkg/provider/helm_release.go | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/provider/pkg/provider/helm_release.go b/provider/pkg/provider/helm_release.go index 290fc0c224..6f541bc5ca 100644 --- a/provider/pkg/provider/helm_release.go +++ b/provider/pkg/provider/helm_release.go @@ -682,6 +682,15 @@ func (r *helmReleaseProvider) Diff(ctx context.Context, req *pulumirpc.DiffReque // Extract old inputs from the `__inputs` field of the old state. oldInputs, _ := parseCheckpointRelease(olds) + + // if the checkpointed inputs have no checksum, then assume no change since checksums were introduced, + // to avoid making a spurious release. + checksum, ok := oldInputs["checksum"] + if !ok || (checksum.IsString() && checksum.StringValue() == "") { + oldInputs["checksum"] = news["checksum"] + } + + // Calculate the diff between the old and new inputs diff := oldInputs.Diff(news) if diff == nil { logger.V(9).Infof("No diff found for %q", req.GetUrn()) @@ -1122,7 +1131,10 @@ func (r *helmReleaseProvider) computeResourceInfo(rel *Release, clientSet *clien // compute the checksum of the rendered output, to be able to detect changes h := sha256.New() - h.Write([]byte(templ)) + _, err = h.Write([]byte(templ)) + if err != nil { + return nil, err + } result.checksum = hex.EncodeToString(h.Sum(nil)) return result, nil From 7c5cd1e1b8a0d2cef31b1fc657d9401d5036308a Mon Sep 17 00:00:00 2001 From: Eron Wright Date: Thu, 14 Sep 2023 14:05:27 -0700 Subject: [PATCH 3/9] Test for helmv3:Release with local chart updates --- tests/sdk/go/go_test.go | 21 + .../go/helm-release-local/step1/Pulumi.yaml | 2 +- tests/sdk/go/helm-release-local/step1/main.go | 15 + .../step2/nginx/values.yaml | 510 ++++++++++++++++++ 4 files changed, 547 insertions(+), 1 deletion(-) create mode 100644 tests/sdk/go/helm-release-local/step2/nginx/values.yaml diff --git a/tests/sdk/go/go_test.go b/tests/sdk/go/go_test.go index ee44c5ef16..fdf900d712 100644 --- a/tests/sdk/go/go_test.go +++ b/tests/sdk/go/go_test.go @@ -189,10 +189,31 @@ func TestGo(t *testing.T) { }) t.Run("Helm Release Local", func(t *testing.T) { + validateReplicas := func(t *testing.T, stack integration.RuntimeValidationStackInfo, expected float64) { + actual, ok := stack.Outputs["replicas"].(float64) + if !ok { + t.Fatalf("expected a replicas output") + } + assert.Equal(t, expected, actual, "expected replicas to be %d", expected) + } + options := baseOptions.With(integration.ProgramTestOptions{ Dir: filepath.Join(cwd, "helm-release-local", "step1"), Quick: true, ExpectRefreshChanges: true, + ExtraRuntimeValidation: func(t *testing.T, stack integration.RuntimeValidationStackInfo) { + validateReplicas(t, stack, 1) + }, + EditDirs: []integration.EditDir{ + { + Dir: filepath.Join("helm-release-local", "step2"), + Additive: true, + ExtraRuntimeValidation: func(t *testing.T, stack integration.RuntimeValidationStackInfo) { + validateReplicas(t, stack, 2) + }, + ExpectFailure: false, + }, + }, }) integration.ProgramTest(t, &options) }) diff --git a/tests/sdk/go/helm-release-local/step1/Pulumi.yaml b/tests/sdk/go/helm-release-local/step1/Pulumi.yaml index 8bf5e3c131..39772624dc 100644 --- a/tests/sdk/go/helm-release-local/step1/Pulumi.yaml +++ b/tests/sdk/go/helm-release-local/step1/Pulumi.yaml @@ -1,3 +1,3 @@ name: go_helm_release -description: Test Kubernetes Helm Release resource with remote Chart. +description: Test Kubernetes Helm Release resource with local Chart. runtime: go diff --git a/tests/sdk/go/helm-release-local/step1/main.go b/tests/sdk/go/helm-release-local/step1/main.go index ca61ce59c7..9aec183831 100644 --- a/tests/sdk/go/helm-release-local/step1/main.go +++ b/tests/sdk/go/helm-release-local/step1/main.go @@ -3,6 +3,7 @@ package main import ( "fmt" + appsv1 "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/apps/v1" corev1 "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/core/v1" "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/helm/v3" "github.com/pulumi/pulumi/sdk/v3/go/pulumi" @@ -24,6 +25,20 @@ func main() { if err != nil { return err } + + replicas := pulumi.All(rel.Status.Namespace(), rel.Status.Name()). + ApplyT(func(r any) (any, error) { + arr := r.([]any) + namespace := arr[0].(*string) + name := arr[1].(*string) + svc, err := appsv1.GetDeployment(ctx, "deployment", pulumi.ID(fmt.Sprintf("%s/%s-nginx", *namespace, *name)), nil) + if err != nil { + return "", nil + } + return svc.Spec.Replicas(), nil + }) + ctx.Export("replicas", replicas) + svc := pulumi.All(rel.Status.Namespace(), rel.Status.Name()). ApplyT(func(r any) (any, error) { arr := r.([]any) diff --git a/tests/sdk/go/helm-release-local/step2/nginx/values.yaml b/tests/sdk/go/helm-release-local/step2/nginx/values.yaml new file mode 100644 index 0000000000..f30118c709 --- /dev/null +++ b/tests/sdk/go/helm-release-local/step2/nginx/values.yaml @@ -0,0 +1,510 @@ +## Global Docker image parameters +## Please, note that this will override the image parameters, including dependencies, configured to use the global value +## Current available global Docker image parameters: imageRegistry and imagePullSecrets +## +# global: +# imageRegistry: myRegistryName +# imagePullSecrets: +# - myRegistryKeySecretName + +## Bitnami NGINX image version +## ref: https://hub.docker.com/r/bitnami/nginx/tags/ +## +image: + registry: docker.io + repository: bitnami/nginx + tag: 1.19.1-debian-10-r23 + ## Specify a imagePullPolicy + ## Defaults to 'Always' if image tag is 'latest', else set to 'IfNotPresent' + ## ref: http://kubernetes.io/docs/user-guide/images/#pre-pulling-images + ## + pullPolicy: IfNotPresent + ## Optionally specify an array of imagePullSecrets. + ## Secrets must be manually created in the namespace. + ## ref: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/ + ## + # pullSecrets: + # - myRegistryKeySecretName + +## String to partially override nginx.fullname template (will maintain the release name) +## +# nameOverride: + +## String to fully override nginx.fullname template +## +# fullnameOverride: + +## Name of existing ConfigMap with the server static site content +## +# staticSiteConfigmap + +## Name of existing PVC with the server static site content +## NOTE: This will override staticSiteConfigmap +## +# staticSitePVC + +## Get the server static content from a git repository +## NOTE: This will override staticSiteConfigmap and staticSitePVC +## +cloneStaticSiteFromGit: + enabled: false + ## Bitnami Git image version + ## ref: https://hub.docker.com/r/bitnami/git/tags/ + ## + image: + registry: docker.io + repository: bitnami/git + tag: 2.28.0-debian-10-r6 + ## Specify a imagePullPolicy + ## Defaults to 'Always' if image tag is 'latest', else set to 'IfNotPresent' + ## ref: http://kubernetes.io/docs/user-guide/images/#pre-pulling-images + ## + pullPolicy: IfNotPresent + ## Optionally specify an array of imagePullSecrets. + ## Secrets must be manually created in the namespace. + ## ref: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/ + ## + # pullSecrets: + # - myRegistryKeySecretName + ## Repository to clone static content from + ## + # repository: + ## Branch inside the git repository + ## + # branch: + ## Interval for sidecar container pull from the repository + ## + interval: 60 + +## Custom server block to be added to NGINX configuration +## PHP-FPM example server block: +## serverBlock: |- +## server { +## listen 0.0.0.0:8080; +## root /app; +## location / { +## index index.html index.php; +## } +## location ~ \.php$ { +## fastcgi_pass phpfpm-server:9000; +## fastcgi_index index.php; +## include fastcgi.conf; +## } +## } +## +# serverBlock: + +## ConfigMap with custom server block to be added to NGINX configuration +## NOTE: This will override serverBlock +## +# existingServerBlockConfigmap: + +## Number of replicas to deploy +## +replicaCount: 2 + +## Deployment Container Port +## +containerPort: 8080 + +## If you would like to serve tls in the cluster set containerTlsPort +## This is required for extra serverBlocks that serve https. +## +## containerTlsPort: 8443 + +## Pod annotations +## ref: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/ +## +podAnnotations: {} + +## Affinity for pod assignment +## Ref: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#affinity-and-anti-affinity +## +affinity: {} + +## Node labels for pod assignment. Evaluated as a template. +## Ref: https://kubernetes.io/docs/user-guide/node-selection/ +## +nodeSelector: {} + +## Tolerations for pod assignment. Evaluated as a template. +## Ref: https://kubernetes.io/docs/concepts/configuration/taint-and-toleration/ +## +tolerations: {} + +## NGINX containers' resource requests and limits +## ref: http://kubernetes.io/docs/user-guide/compute-resources/ +## +resources: + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + limits: {} + # cpu: 100m + # memory: 128Mi + requests: {} + # cpu: 100m + # memory: 128Mi + +## NGINX containers' liveness and readiness probes +## ref: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#container-probes +## +livenessProbe: + tcpSocket: + port: http + initialDelaySeconds: 30 + timeoutSeconds: 5 + failureThreshold: 6 +readinessProbe: + tcpSocket: + port: http + initialDelaySeconds: 5 + timeoutSeconds: 3 + periodSeconds: 5 + +## NGINX Service properties +## +service: + ## Service type + ## + type: LoadBalancer + + ## HTTP Port + ## + port: 80 + + ## HTTPS Port + ## + httpsPort: 443 + + ## Specify the nodePort(s) value(s) for the LoadBalancer and NodePort service types. + ## ref: https://kubernetes.io/docs/concepts/services-networking/service/#type-nodeport + ## + nodePorts: + http: "" + https: "" + + ## Set the LoadBalancer service type to internal only. + ## ref: https://kubernetes.io/docs/concepts/services-networking/service/#internal-load-balancer + ## + # loadBalancerIP: + + ## Provide any additional annotations which may be required. This can be used to + ## set the LoadBalancer service type to internal only. + ## ref: https://kubernetes.io/docs/concepts/services-networking/service/#internal-load-balancer + ## + annotations: {} + + ## Enable client source IP preservation + ## ref http://kubernetes.io/docs/tasks/access-application-cluster/create-external-load-balancer/#preserving-the-client-source-ip + ## + externalTrafficPolicy: Cluster + + +## LDAP Auth Daemon Properties +## +## Daemon that will proxy LDAP requests +## between NGINX and a given LDAP Server +## +ldapDaemon: + + enabled: false + + ## Bitnami NGINX LDAP Auth Daemon image + ## ref: https://hub.docker.com/r/bitnami/nginx-ldap-auth-daemon/tags/ + ## + image: + registry: docker.io + repository: bitnami/nginx-ldap-auth-daemon + tag: 0.20200116.0-debian-10-r83 + pullPolicy: IfNotPresent + + ## LDAP Daemon port + ## + port: 8888 + + ## LDAP Auth Daemon Configuration + ## + ## These different properties define the form of requests performed + ## against the given LDAP server + ## + ## BEWARE THAT THESE VALUES WILL BE IGNORED IF A CUSTOM LDAP SERVER BLOCK + ## ALREADY SPECIFIES THEM. + ## + ## + ldapConfig: + + ## LDAP URI where to query the server + ## Must follow the pattern -> ldap[s]:/: + uri: "" + + ## LDAP search base DN + baseDN: "" + + ## LDAP bind DN + bindDN: "" + + ## LDAP bind Password + bindPassword: "" + + ## LDAP search filter + filter: "" + + ## LDAP auth realm + httpRealm: "" + + ## LDAP cookie name + httpCookieName: "" + + ## NGINX Configuration File containing the directives (that define + ## how LDAP requests are performed) and tells NGINX to use the LDAP Daemon + ## as proxy. Besides, it defines the routes that will require of LDAP auth + ## in order to be accessed. + ## + ## If LDAP directives are provided, they will take precedence over + ## the ones specified in ldapConfig. + ## + ## This will be evaluated as a template. + ## + ## + + nginxServerBlock: |- + server { + listen 0.0.0.0:{{ .Values.containerPort }}; + + # You can provide a special subPath or the root + location = / { + auth_request /auth-proxy; + } + + location = /auth-proxy { + internal; + + proxy_pass http://127.0.0.1:{{ .Values.ldapDaemon.port }}; + + ############################################################### + # YOU SHOULD CHANGE THE FOLLOWING TO YOUR LDAP CONFIGURATION # + ############################################################### + + # URL and port for connecting to the LDAP server + proxy_set_header X-Ldap-URL "ldap://YOUR_LDAP_SERVER_IP:YOUR_LDAP_SERVER_PORT"; + + # Base DN + proxy_set_header X-Ldap-BaseDN "dc=example,dc=org"; + + # Bind DN + proxy_set_header X-Ldap-BindDN "cn=admin,dc=example,dc=org"; + + # Bind password + proxy_set_header X-Ldap-BindPass "adminpassword"; + } + } + + ## Use an existing Secret holding an NGINX Configuration file that + ## configures LDAP requests. (will be evaluated as a template) + ## + ## If provided, both nginxServerBlock and ldapConfig properties are ignored. + ## + existingNginxServerBlockSecret: + + ## LDAP Auth Daemon's liveness and readiness probes + ## ref: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#container-probes + ## + livenessProbe: + tcpSocket: + port: ldap-daemon + initialDelaySeconds: 30 + timeoutSeconds: 5 + failureThreshold: 6 + + readinessProbe: + tcpSocket: + port: ldap-daemon + initialDelaySeconds: 5 + timeoutSeconds: 3 + periodSeconds: 5 + +## Ingress paramaters +## +ingress: + ## Set to true to enable ingress record generation + ## + enabled: false + + ## Set this to true in order to add the corresponding annotations for cert-manager + ## + certManager: false + + ## When the ingress is enabled, a host pointing to this will be created + ## + # hostname: example.local + + ## The list of hosts and paths to be covered into ingress rules if more than one hosts + ## or only a host with a path is needed, this is an array + ## hosts: + ## - name: example.local + ## path: / + + ## Ingress annotations done as key:value pairs + ## For a full list of possible ingress annotations, please see + ## ref: https://github.com/kubernetes/ingress-nginx/blob/master/docs/user-guide/nginx-configuration/annotations.md + ## + ## If tls is set to true, annotation ingress.kubernetes.io/secure-backends: "true" will automatically be set + ## If certManager is set to true, annotation kubernetes.io/tls-acme: "true" will automatically be set + annotations: {} + # kubernetes.io/ingress.class: nginx + + ## The tls configuration for the ingress + ## ref: https://kubernetes.io/docs/concepts/services-networking/ingress/#tls + ## + tls: + - hosts: + - example.local + secretName: example.local-tls + + +healthIngress: + ## Set to true to enable health ingress record generation + ## + enabled: false + + ## Set this to true in order to add the corresponding annotations for cert-manager + ## + certManager: false + + ## When the health ingress is enabled, a host pointing to this will be created + ## + hostname: example.local + + ## Health Ingress annotations done as key:value pairs + ## For a full list of possible ingress annotations, please see + ## ref: https://github.com/kubernetes/ingress-nginx/blob/master/docs/user-guide/nginx-configuration/annotations.md + ## + ## If tls is set to true, annotation ingress.kubernetes.io/secure-backends: "true" will automatically be set + ## If certManager is set to true, annotation kubernetes.io/tls-acme: "true" will automatically be set + annotations: {} + # kubernetes.io/ingress.class: nginx + + ## The list of additional hostnames to be covered with this health ingress record. + ## Most likely the hostname above will be enough, but in the event more hosts are needed, this is an array + ## hosts: + ## - name: example.local + ## path: / + + ## The tls configuration for the health ingress + ## ref: https://kubernetes.io/docs/concepts/services-networking/ingress/#tls + ## + tls: + - hosts: + - example.local + secretName: example.local-tls + + secrets: + ## If you're providing your own certificates, please use this to add the certificates as secrets + ## key and certificate should start with -----BEGIN CERTIFICATE----- or + ## -----BEGIN RSA PRIVATE KEY----- + ## + ## name should line up with a tlsSecret set further up + ## If you're using cert-manager, this is unneeded, as it will create the secret for you if it is not set + ## + ## It is also possible to create and manage the certificates outside of this helm chart + ## Please see README.md for more information + # - name: example.local-tls + # key: + # certificate: + +## Prometheus Exporter / Metrics +## +metrics: + enabled: false + + ## Bitnami NGINX Prometheus Exporter image + ## ref: https://hub.docker.com/r/bitnami/nginx-exporter/tags/ + ## + image: + registry: docker.io + repository: bitnami/nginx-exporter + tag: 0.8.0-debian-10-r41 + pullPolicy: IfNotPresent + ## Optionally specify an array of imagePullSecrets. + ## Secrets must be manually created in the namespace. + ## ref: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/ + ## + # pullSecrets: + # - myRegistryKeySecretName + + ## Prometheus exporter pods' annotation and labels + ## ref: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/ + ## + podAnnotations: + prometheus.io/scrape: "true" + prometheus.io/port: "9113" + + ## Prometheus exporter service parameters + ## + service: + ## NGINX Prometheus exporter port + ## + port: 9113 + ## Annotations for the Prometheus exporter service + ## + annotations: + prometheus.io/scrape: "true" + prometheus.io/port: "{{ .Values.metrics.service.port }}" + + ## NGINX Prometheus exporter resource requests and limits + ## ref: http://kubernetes.io/docs/user-guide/compute-resources/ + ## + resources: + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + limits: {} + # cpu: 100m + # memory: 128Mi + requests: {} + # cpu: 100m + # memory: 128Mi + + ## Prometheus Operator ServiceMonitor configuration + ## + serviceMonitor: + enabled: false + ## Namespace in which Prometheus is running + ## + # namespace: monitoring + + ## Interval at which metrics should be scraped. + ## ref: https://github.com/coreos/prometheus-operator/blob/master/Documentation/api.md#endpoint + ## + # interval: 10s + + ## Timeout after which the scrape is ended + ## ref: https://github.com/coreos/prometheus-operator/blob/master/Documentation/api.md#endpoint + ## + # scrapeTimeout: 10s + + ## ServiceMonitor selector labels + ## ref: https://github.com/bitnami/charts/tree/master/bitnami/prometheus-operator#prometheus-configuration + ## + # selector: + # prometheus: my-prometheus + +## Autoscaling parameters +## +autoscaling: + enabled: false + # minReplicas: 1 + # maxReplicas: 10 + # targetCPU: 50 + # targetMemory: 50 + +## Array to add extra volumes (evaluated as a template) +## +extraVolumes: [] + +## Array to add extra mounts (normally used with extraVolumes, evaluated as a template) +## +extraVolumeMounts: [] From b334937711f58b46c3eeaf8ac33f316678492174 Mon Sep 17 00:00:00 2001 From: Eron Wright Date: Thu, 14 Sep 2023 15:16:56 -0700 Subject: [PATCH 4/9] CHANGELOG --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f1610859b..5f9516eff1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ - Return mapping information for terraform conversions (https://github.com/pulumi/pulumi-kubernetes/pull/2457) +- helm.v3.Release: Detect changes to local charts (https://github.com/pulumi/pulumi-kubernetes/pull/2568) + ## 4.1.1 (August 23, 2023) - Revert the switch to pyproject.toml and wheel-based PyPI publishing as it impacts users that run pip with --no-binary From 47637f5534b589620f78dd8e5e842f9d00e6d10a Mon Sep 17 00:00:00 2001 From: Eron Wright Date: Fri, 15 Sep 2023 11:20:51 -0700 Subject: [PATCH 5/9] Remove optimization for migration case --- provider/pkg/provider/helm_release.go | 7 ------- 1 file changed, 7 deletions(-) diff --git a/provider/pkg/provider/helm_release.go b/provider/pkg/provider/helm_release.go index 6f541bc5ca..cdee2a4c61 100644 --- a/provider/pkg/provider/helm_release.go +++ b/provider/pkg/provider/helm_release.go @@ -683,13 +683,6 @@ func (r *helmReleaseProvider) Diff(ctx context.Context, req *pulumirpc.DiffReque // Extract old inputs from the `__inputs` field of the old state. oldInputs, _ := parseCheckpointRelease(olds) - // if the checkpointed inputs have no checksum, then assume no change since checksums were introduced, - // to avoid making a spurious release. - checksum, ok := oldInputs["checksum"] - if !ok || (checksum.IsString() && checksum.StringValue() == "") { - oldInputs["checksum"] = news["checksum"] - } - // Calculate the diff between the old and new inputs diff := oldInputs.Diff(news) if diff == nil { From 995b110bc2765a6a0bf98f40135e2c078fa8bbe7 Mon Sep 17 00:00:00 2001 From: Eron Wright Date: Fri, 15 Sep 2023 15:33:58 -0700 Subject: [PATCH 6/9] Rename a test case for clarity --- tests/sdk/go/go_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/sdk/go/go_test.go b/tests/sdk/go/go_test.go index fdf900d712..bcca9226e0 100644 --- a/tests/sdk/go/go_test.go +++ b/tests/sdk/go/go_test.go @@ -112,7 +112,7 @@ func TestGo(t *testing.T) { integration.ProgramTest(t, &options) }) - t.Run("Helm Import", func(t *testing.T) { + t.Run("Helm Release Import", func(t *testing.T) { baseDir := filepath.Join(cwd, "helm-release-import", "step1") namespace := getRandomNamespace("importtest") require.NoError(t, createRelease("mynginx", namespace, baseDir, true)) From bb5dd6170243532ac56f03d84dda1c0346cc1f5b Mon Sep 17 00:00:00 2001 From: Eron Wright Date: Fri, 15 Sep 2023 15:35:32 -0700 Subject: [PATCH 7/9] Calculate checksum during import --- provider/pkg/provider/helm_release.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/provider/pkg/provider/helm_release.go b/provider/pkg/provider/helm_release.go index cdee2a4c61..e26ab6be33 100644 --- a/provider/pkg/provider/helm_release.go +++ b/provider/pkg/provider/helm_release.go @@ -941,7 +941,14 @@ func (r *helmReleaseProvider) Read(ctx context.Context, req *pulumirpc.ReadReque oldInputs, _ := parseCheckpointRelease(oldState) if oldInputs == nil { // No old inputs suggests this is an import. Hydrate the imports from the current live object - logger.V(9).Infof("existingRelease: %#v", existingRelease) + resourceInfo, err := r.computeResourceInfo(existingRelease, r.clientSet) + if err != nil { + return nil, err + } + existingRelease.Checksum = resourceInfo.checksum + existingRelease.ResourceNames = resourceInfo.resourceNames + logger.V(9).Infof("%s Imported release: %#v", label, existingRelease) + oldInputs = r.serializeImportInputs(existingRelease) r.setDefaults(oldInputs) } From 3ac46f79bf8ab7ab562fa5d3b3aa3c61716a807a Mon Sep 17 00:00:00 2001 From: Eron Wright Date: Fri, 15 Sep 2023 16:18:23 -0700 Subject: [PATCH 8/9] support for ignoreChanges --- provider/pkg/provider/helm_release.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/provider/pkg/provider/helm_release.go b/provider/pkg/provider/helm_release.go index e26ab6be33..13c2f44e8c 100644 --- a/provider/pkg/provider/helm_release.go +++ b/provider/pkg/provider/helm_release.go @@ -683,6 +683,13 @@ func (r *helmReleaseProvider) Diff(ctx context.Context, req *pulumirpc.DiffReque // Extract old inputs from the `__inputs` field of the old state. oldInputs, _ := parseCheckpointRelease(olds) + // apply ignoreChanges + for _, ignore := range req.GetIgnoreChanges() { + if ignore == "checksum" { + news["checksum"] = oldInputs["checksum"] + } + } + // Calculate the diff between the old and new inputs diff := oldInputs.Diff(news) if diff == nil { From 523e22ae8855ee21d7b1a6d1e3c9ab733ef59be0 Mon Sep 17 00:00:00 2001 From: Eron Wright Date: Sat, 16 Sep 2023 16:41:25 -0700 Subject: [PATCH 9/9] integration test for ignorechanges support --- tests/sdk/go/go_test.go | 11 + tests/sdk/go/helm-release-local/step3/main.go | 57 ++ .../step3/nginx/values.yaml | 510 ++++++++++++++++++ 3 files changed, 578 insertions(+) create mode 100644 tests/sdk/go/helm-release-local/step3/main.go create mode 100644 tests/sdk/go/helm-release-local/step3/nginx/values.yaml diff --git a/tests/sdk/go/go_test.go b/tests/sdk/go/go_test.go index bcca9226e0..955ae3e936 100644 --- a/tests/sdk/go/go_test.go +++ b/tests/sdk/go/go_test.go @@ -209,6 +209,17 @@ func TestGo(t *testing.T) { Dir: filepath.Join("helm-release-local", "step2"), Additive: true, ExtraRuntimeValidation: func(t *testing.T, stack integration.RuntimeValidationStackInfo) { + // expect the change in values.yaml (replicaCount: 2) to be detected + validateReplicas(t, stack, 2) + }, + ExpectFailure: false, + }, + { + Dir: filepath.Join("helm-release-local", "step3"), + Additive: true, + ExtraRuntimeValidation: func(t *testing.T, stack integration.RuntimeValidationStackInfo) { + // note the resource option: pulumi.IgnoreChanges([]string{"checksum"}) + // expect the change in values.yaml (replicaCount: 3) to be ignored validateReplicas(t, stack, 2) }, ExpectFailure: false, diff --git a/tests/sdk/go/helm-release-local/step3/main.go b/tests/sdk/go/helm-release-local/step3/main.go new file mode 100644 index 0000000000..c9ca1bb4f1 --- /dev/null +++ b/tests/sdk/go/helm-release-local/step3/main.go @@ -0,0 +1,57 @@ +package main + +import ( + "fmt" + + appsv1 "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/apps/v1" + corev1 "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/core/v1" + "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/helm/v3" + "github.com/pulumi/pulumi/sdk/v3/go/pulumi" +) + +func main() { + pulumi.Run(func(ctx *pulumi.Context) error { + ns, err := corev1.NewNamespace(ctx, "test", &corev1.NamespaceArgs{}) + if err != nil { + return err + } + + rel, err := helm.NewRelease(ctx, "test", &helm.ReleaseArgs{ + Chart: pulumi.String("nginx"), + Namespace: ns.Metadata.Name(), + Values: pulumi.Map{"service": pulumi.StringMap{"type": pulumi.String("ClusterIP")}}, + Timeout: pulumi.Int(300), + }, pulumi.IgnoreChanges([]string{"checksum"})) + if err != nil { + return err + } + + replicas := pulumi.All(rel.Status.Namespace(), rel.Status.Name()). + ApplyT(func(r any) (any, error) { + arr := r.([]any) + namespace := arr[0].(*string) + name := arr[1].(*string) + svc, err := appsv1.GetDeployment(ctx, "deployment", pulumi.ID(fmt.Sprintf("%s/%s-nginx", *namespace, *name)), nil) + if err != nil { + return "", nil + } + return svc.Spec.Replicas(), nil + }) + ctx.Export("replicas", replicas) + + svc := pulumi.All(rel.Status.Namespace(), rel.Status.Name()). + ApplyT(func(r any) (any, error) { + arr := r.([]any) + namespace := arr[0].(*string) + name := arr[1].(*string) + svc, err := corev1.GetService(ctx, "svc", pulumi.ID(fmt.Sprintf("%s/%s-nginx", *namespace, *name)), nil) + if err != nil { + return "", nil + } + return svc.Spec.ClusterIP(), nil + }) + ctx.Export("svc_ip", svc) + + return nil + }) +} diff --git a/tests/sdk/go/helm-release-local/step3/nginx/values.yaml b/tests/sdk/go/helm-release-local/step3/nginx/values.yaml new file mode 100644 index 0000000000..a9d1d78ed2 --- /dev/null +++ b/tests/sdk/go/helm-release-local/step3/nginx/values.yaml @@ -0,0 +1,510 @@ +## Global Docker image parameters +## Please, note that this will override the image parameters, including dependencies, configured to use the global value +## Current available global Docker image parameters: imageRegistry and imagePullSecrets +## +# global: +# imageRegistry: myRegistryName +# imagePullSecrets: +# - myRegistryKeySecretName + +## Bitnami NGINX image version +## ref: https://hub.docker.com/r/bitnami/nginx/tags/ +## +image: + registry: docker.io + repository: bitnami/nginx + tag: 1.19.1-debian-10-r23 + ## Specify a imagePullPolicy + ## Defaults to 'Always' if image tag is 'latest', else set to 'IfNotPresent' + ## ref: http://kubernetes.io/docs/user-guide/images/#pre-pulling-images + ## + pullPolicy: IfNotPresent + ## Optionally specify an array of imagePullSecrets. + ## Secrets must be manually created in the namespace. + ## ref: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/ + ## + # pullSecrets: + # - myRegistryKeySecretName + +## String to partially override nginx.fullname template (will maintain the release name) +## +# nameOverride: + +## String to fully override nginx.fullname template +## +# fullnameOverride: + +## Name of existing ConfigMap with the server static site content +## +# staticSiteConfigmap + +## Name of existing PVC with the server static site content +## NOTE: This will override staticSiteConfigmap +## +# staticSitePVC + +## Get the server static content from a git repository +## NOTE: This will override staticSiteConfigmap and staticSitePVC +## +cloneStaticSiteFromGit: + enabled: false + ## Bitnami Git image version + ## ref: https://hub.docker.com/r/bitnami/git/tags/ + ## + image: + registry: docker.io + repository: bitnami/git + tag: 2.28.0-debian-10-r6 + ## Specify a imagePullPolicy + ## Defaults to 'Always' if image tag is 'latest', else set to 'IfNotPresent' + ## ref: http://kubernetes.io/docs/user-guide/images/#pre-pulling-images + ## + pullPolicy: IfNotPresent + ## Optionally specify an array of imagePullSecrets. + ## Secrets must be manually created in the namespace. + ## ref: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/ + ## + # pullSecrets: + # - myRegistryKeySecretName + ## Repository to clone static content from + ## + # repository: + ## Branch inside the git repository + ## + # branch: + ## Interval for sidecar container pull from the repository + ## + interval: 60 + +## Custom server block to be added to NGINX configuration +## PHP-FPM example server block: +## serverBlock: |- +## server { +## listen 0.0.0.0:8080; +## root /app; +## location / { +## index index.html index.php; +## } +## location ~ \.php$ { +## fastcgi_pass phpfpm-server:9000; +## fastcgi_index index.php; +## include fastcgi.conf; +## } +## } +## +# serverBlock: + +## ConfigMap with custom server block to be added to NGINX configuration +## NOTE: This will override serverBlock +## +# existingServerBlockConfigmap: + +## Number of replicas to deploy +## +replicaCount: 3 + +## Deployment Container Port +## +containerPort: 8080 + +## If you would like to serve tls in the cluster set containerTlsPort +## This is required for extra serverBlocks that serve https. +## +## containerTlsPort: 8443 + +## Pod annotations +## ref: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/ +## +podAnnotations: {} + +## Affinity for pod assignment +## Ref: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#affinity-and-anti-affinity +## +affinity: {} + +## Node labels for pod assignment. Evaluated as a template. +## Ref: https://kubernetes.io/docs/user-guide/node-selection/ +## +nodeSelector: {} + +## Tolerations for pod assignment. Evaluated as a template. +## Ref: https://kubernetes.io/docs/concepts/configuration/taint-and-toleration/ +## +tolerations: {} + +## NGINX containers' resource requests and limits +## ref: http://kubernetes.io/docs/user-guide/compute-resources/ +## +resources: + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + limits: {} + # cpu: 100m + # memory: 128Mi + requests: {} + # cpu: 100m + # memory: 128Mi + +## NGINX containers' liveness and readiness probes +## ref: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#container-probes +## +livenessProbe: + tcpSocket: + port: http + initialDelaySeconds: 30 + timeoutSeconds: 5 + failureThreshold: 6 +readinessProbe: + tcpSocket: + port: http + initialDelaySeconds: 5 + timeoutSeconds: 3 + periodSeconds: 5 + +## NGINX Service properties +## +service: + ## Service type + ## + type: LoadBalancer + + ## HTTP Port + ## + port: 80 + + ## HTTPS Port + ## + httpsPort: 443 + + ## Specify the nodePort(s) value(s) for the LoadBalancer and NodePort service types. + ## ref: https://kubernetes.io/docs/concepts/services-networking/service/#type-nodeport + ## + nodePorts: + http: "" + https: "" + + ## Set the LoadBalancer service type to internal only. + ## ref: https://kubernetes.io/docs/concepts/services-networking/service/#internal-load-balancer + ## + # loadBalancerIP: + + ## Provide any additional annotations which may be required. This can be used to + ## set the LoadBalancer service type to internal only. + ## ref: https://kubernetes.io/docs/concepts/services-networking/service/#internal-load-balancer + ## + annotations: {} + + ## Enable client source IP preservation + ## ref http://kubernetes.io/docs/tasks/access-application-cluster/create-external-load-balancer/#preserving-the-client-source-ip + ## + externalTrafficPolicy: Cluster + + +## LDAP Auth Daemon Properties +## +## Daemon that will proxy LDAP requests +## between NGINX and a given LDAP Server +## +ldapDaemon: + + enabled: false + + ## Bitnami NGINX LDAP Auth Daemon image + ## ref: https://hub.docker.com/r/bitnami/nginx-ldap-auth-daemon/tags/ + ## + image: + registry: docker.io + repository: bitnami/nginx-ldap-auth-daemon + tag: 0.20200116.0-debian-10-r83 + pullPolicy: IfNotPresent + + ## LDAP Daemon port + ## + port: 8888 + + ## LDAP Auth Daemon Configuration + ## + ## These different properties define the form of requests performed + ## against the given LDAP server + ## + ## BEWARE THAT THESE VALUES WILL BE IGNORED IF A CUSTOM LDAP SERVER BLOCK + ## ALREADY SPECIFIES THEM. + ## + ## + ldapConfig: + + ## LDAP URI where to query the server + ## Must follow the pattern -> ldap[s]:/: + uri: "" + + ## LDAP search base DN + baseDN: "" + + ## LDAP bind DN + bindDN: "" + + ## LDAP bind Password + bindPassword: "" + + ## LDAP search filter + filter: "" + + ## LDAP auth realm + httpRealm: "" + + ## LDAP cookie name + httpCookieName: "" + + ## NGINX Configuration File containing the directives (that define + ## how LDAP requests are performed) and tells NGINX to use the LDAP Daemon + ## as proxy. Besides, it defines the routes that will require of LDAP auth + ## in order to be accessed. + ## + ## If LDAP directives are provided, they will take precedence over + ## the ones specified in ldapConfig. + ## + ## This will be evaluated as a template. + ## + ## + + nginxServerBlock: |- + server { + listen 0.0.0.0:{{ .Values.containerPort }}; + + # You can provide a special subPath or the root + location = / { + auth_request /auth-proxy; + } + + location = /auth-proxy { + internal; + + proxy_pass http://127.0.0.1:{{ .Values.ldapDaemon.port }}; + + ############################################################### + # YOU SHOULD CHANGE THE FOLLOWING TO YOUR LDAP CONFIGURATION # + ############################################################### + + # URL and port for connecting to the LDAP server + proxy_set_header X-Ldap-URL "ldap://YOUR_LDAP_SERVER_IP:YOUR_LDAP_SERVER_PORT"; + + # Base DN + proxy_set_header X-Ldap-BaseDN "dc=example,dc=org"; + + # Bind DN + proxy_set_header X-Ldap-BindDN "cn=admin,dc=example,dc=org"; + + # Bind password + proxy_set_header X-Ldap-BindPass "adminpassword"; + } + } + + ## Use an existing Secret holding an NGINX Configuration file that + ## configures LDAP requests. (will be evaluated as a template) + ## + ## If provided, both nginxServerBlock and ldapConfig properties are ignored. + ## + existingNginxServerBlockSecret: + + ## LDAP Auth Daemon's liveness and readiness probes + ## ref: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#container-probes + ## + livenessProbe: + tcpSocket: + port: ldap-daemon + initialDelaySeconds: 30 + timeoutSeconds: 5 + failureThreshold: 6 + + readinessProbe: + tcpSocket: + port: ldap-daemon + initialDelaySeconds: 5 + timeoutSeconds: 3 + periodSeconds: 5 + +## Ingress paramaters +## +ingress: + ## Set to true to enable ingress record generation + ## + enabled: false + + ## Set this to true in order to add the corresponding annotations for cert-manager + ## + certManager: false + + ## When the ingress is enabled, a host pointing to this will be created + ## + # hostname: example.local + + ## The list of hosts and paths to be covered into ingress rules if more than one hosts + ## or only a host with a path is needed, this is an array + ## hosts: + ## - name: example.local + ## path: / + + ## Ingress annotations done as key:value pairs + ## For a full list of possible ingress annotations, please see + ## ref: https://github.com/kubernetes/ingress-nginx/blob/master/docs/user-guide/nginx-configuration/annotations.md + ## + ## If tls is set to true, annotation ingress.kubernetes.io/secure-backends: "true" will automatically be set + ## If certManager is set to true, annotation kubernetes.io/tls-acme: "true" will automatically be set + annotations: {} + # kubernetes.io/ingress.class: nginx + + ## The tls configuration for the ingress + ## ref: https://kubernetes.io/docs/concepts/services-networking/ingress/#tls + ## + tls: + - hosts: + - example.local + secretName: example.local-tls + + +healthIngress: + ## Set to true to enable health ingress record generation + ## + enabled: false + + ## Set this to true in order to add the corresponding annotations for cert-manager + ## + certManager: false + + ## When the health ingress is enabled, a host pointing to this will be created + ## + hostname: example.local + + ## Health Ingress annotations done as key:value pairs + ## For a full list of possible ingress annotations, please see + ## ref: https://github.com/kubernetes/ingress-nginx/blob/master/docs/user-guide/nginx-configuration/annotations.md + ## + ## If tls is set to true, annotation ingress.kubernetes.io/secure-backends: "true" will automatically be set + ## If certManager is set to true, annotation kubernetes.io/tls-acme: "true" will automatically be set + annotations: {} + # kubernetes.io/ingress.class: nginx + + ## The list of additional hostnames to be covered with this health ingress record. + ## Most likely the hostname above will be enough, but in the event more hosts are needed, this is an array + ## hosts: + ## - name: example.local + ## path: / + + ## The tls configuration for the health ingress + ## ref: https://kubernetes.io/docs/concepts/services-networking/ingress/#tls + ## + tls: + - hosts: + - example.local + secretName: example.local-tls + + secrets: + ## If you're providing your own certificates, please use this to add the certificates as secrets + ## key and certificate should start with -----BEGIN CERTIFICATE----- or + ## -----BEGIN RSA PRIVATE KEY----- + ## + ## name should line up with a tlsSecret set further up + ## If you're using cert-manager, this is unneeded, as it will create the secret for you if it is not set + ## + ## It is also possible to create and manage the certificates outside of this helm chart + ## Please see README.md for more information + # - name: example.local-tls + # key: + # certificate: + +## Prometheus Exporter / Metrics +## +metrics: + enabled: false + + ## Bitnami NGINX Prometheus Exporter image + ## ref: https://hub.docker.com/r/bitnami/nginx-exporter/tags/ + ## + image: + registry: docker.io + repository: bitnami/nginx-exporter + tag: 0.8.0-debian-10-r41 + pullPolicy: IfNotPresent + ## Optionally specify an array of imagePullSecrets. + ## Secrets must be manually created in the namespace. + ## ref: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/ + ## + # pullSecrets: + # - myRegistryKeySecretName + + ## Prometheus exporter pods' annotation and labels + ## ref: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/ + ## + podAnnotations: + prometheus.io/scrape: "true" + prometheus.io/port: "9113" + + ## Prometheus exporter service parameters + ## + service: + ## NGINX Prometheus exporter port + ## + port: 9113 + ## Annotations for the Prometheus exporter service + ## + annotations: + prometheus.io/scrape: "true" + prometheus.io/port: "{{ .Values.metrics.service.port }}" + + ## NGINX Prometheus exporter resource requests and limits + ## ref: http://kubernetes.io/docs/user-guide/compute-resources/ + ## + resources: + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + limits: {} + # cpu: 100m + # memory: 128Mi + requests: {} + # cpu: 100m + # memory: 128Mi + + ## Prometheus Operator ServiceMonitor configuration + ## + serviceMonitor: + enabled: false + ## Namespace in which Prometheus is running + ## + # namespace: monitoring + + ## Interval at which metrics should be scraped. + ## ref: https://github.com/coreos/prometheus-operator/blob/master/Documentation/api.md#endpoint + ## + # interval: 10s + + ## Timeout after which the scrape is ended + ## ref: https://github.com/coreos/prometheus-operator/blob/master/Documentation/api.md#endpoint + ## + # scrapeTimeout: 10s + + ## ServiceMonitor selector labels + ## ref: https://github.com/bitnami/charts/tree/master/bitnami/prometheus-operator#prometheus-configuration + ## + # selector: + # prometheus: my-prometheus + +## Autoscaling parameters +## +autoscaling: + enabled: false + # minReplicas: 1 + # maxReplicas: 10 + # targetCPU: 50 + # targetMemory: 50 + +## Array to add extra volumes (evaluated as a template) +## +extraVolumes: [] + +## Array to add extra mounts (normally used with extraVolumes, evaluated as a template) +## +extraVolumeMounts: []