Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Helm Release detect changes to local chart #2568

Merged
merged 9 commits into from
Sep 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
56 changes: 48 additions & 8 deletions provider/pkg/provider/helm_release.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ package provider

import (
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
Expand Down Expand Up @@ -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"`
EronWright marked this conversation as resolved.
Show resolved Hide resolved
// Status of the deployed release.
Status *ReleaseStatus `json:"status,omitempty"`
}
Expand Down Expand Up @@ -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 {
EronWright marked this conversation as resolved.
Show resolved Hide resolved
new.Checksum = resourceInfo.checksum
if len(new.ResourceNames) == 0 {
new.ResourceNames = resourceInfo.resourceNames
}
}

logger.V(9).Infof("New: %+v", new)
news = resource.NewPropertyMap(new)
}
Expand Down Expand Up @@ -674,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)

// apply ignoreChanges
for _, ignore := range req.GetIgnoreChanges() {
if ignore == "checksum" {
news["checksum"] = oldInputs["checksum"]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add a testcase here for this functionality as well?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

}
}

// 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())
Expand Down Expand Up @@ -931,7 +948,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)
}
Expand Down Expand Up @@ -1089,8 +1113,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)
Expand All @@ -1099,12 +1129,22 @@ 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()
EronWright marked this conversation as resolved.
Show resolved Hide resolved
_, err = h.Write([]byte(templ))
if err != nil {
return nil, err
}
result.checksum = hex.EncodeToString(h.Sum(nil))

return resourceNames, nil
return result, nil
}

func (r *helmReleaseProvider) chartOptsFromRelease(rel *Release) HelmChartOpts {
Expand Down
34 changes: 33 additions & 1 deletion tests/sdk/go/go_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -189,10 +189,42 @@ 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) {
// 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,
},
},
})
integration.ProgramTest(t, &options)
})
Expand Down
2 changes: 1 addition & 1 deletion tests/sdk/go/helm-release-local/step1/Pulumi.yaml
Original file line number Diff line number Diff line change
@@ -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
15 changes: 15 additions & 0 deletions tests/sdk/go/helm-release-local/step1/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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)
Expand Down
Loading
Loading