Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
3 changes: 3 additions & 0 deletions .changelog/1675.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:enhancement:
`helm_release`: adds support for the `upgrade_install` field
```
141 changes: 124 additions & 17 deletions helm/resource_helm_release.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ import (
"helm.sh/helm/v3/pkg/action"
"helm.sh/helm/v3/pkg/chart"
"helm.sh/helm/v3/pkg/chart/loader"

"helm.sh/helm/v3/pkg/downloader"
"helm.sh/helm/v3/pkg/getter"
"helm.sh/helm/v3/pkg/postrender"
Expand Down Expand Up @@ -108,6 +107,7 @@ type HelmReleaseModel struct {
SkipCrds types.Bool `tfsdk:"skip_crds"`
Status types.String `tfsdk:"status"`
Timeout types.Int64 `tfsdk:"timeout"`
UpgradeInstall types.Bool `tfsdk:"upgrade_install"`
Values types.List `tfsdk:"values"`
Verify types.Bool `tfsdk:"verify"`
Version types.String `tfsdk:"version"`
Expand Down Expand Up @@ -137,6 +137,7 @@ var defaultAttributes = map[string]interface{}{
"verify": false,
"wait": true,
"wait_for_jobs": false,
"upgrade_install": false,
}

type releaseMetaData struct {
Expand Down Expand Up @@ -619,6 +620,12 @@ func (r *HelmRelease) Schema(ctx context.Context, req resource.SchemaRequest, re
},
},
},
"upgrade_install": schema.BoolAttribute{
Optional: true,
Computed: true,
Default: booldefault.StaticBool(defaultAttributes["upgrade_install"].(bool)),
Description: "If true, the provider will install the release at the specified version even if a release not controlled by the provider is present. This is equivalent to running 'helm upgrade --install'. WARNING: this may not be suitable for production use -- see the 'Upgrade Mode' note in the provider documentation. Defaults to `false`.",
},
"postrender": schema.SingleNestedAttribute{
Description: "Postrender command config",
Optional: true,
Expand Down Expand Up @@ -709,6 +716,25 @@ func mergeMaps(a, b map[string]interface{}) map[string]interface{} {
return out
}

func getInstalledReleaseVersion(ctx context.Context, m *Meta, cfg *action.Configuration, name string) (string, error) {
logID := fmt.Sprintf("[getInstalledReleaseVersion: %s]", name)
histClient := action.NewHistory(cfg)
histClient.Max = 1

hist, err := histClient.Run(name)
if err != nil {
if strings.Contains(err.Error(), "not found") {
tflog.Debug(ctx, fmt.Sprintf("%s Chart %s is not yet installed", logID, name))
return "", nil
}
return "", err
}

installedVersion := hist[0].Chart.Metadata.Version
tflog.Debug(ctx, fmt.Sprintf("%s Chart %s is installed as release %s", logID, name, installedVersion))
return installedVersion, nil
}

func (r *HelmRelease) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var state HelmReleaseModel
diags := req.Plan.Get(ctx, &state)
Expand Down Expand Up @@ -808,27 +834,87 @@ func (r *HelmRelease) Create(ctx context.Context, req resource.CreateRequest, re
client.Description = state.Description.ValueString()
client.CreateNamespace = state.CreateNamespace.ValueBool()

if state.PostRender != nil {
binaryPath := state.PostRender.BinaryPath.ValueString()
argsList := state.PostRender.Args.Elements()
var releaseAlreadyExists bool
var installedVersion string
var rel *release.Release

if binaryPath != "" {
var args []string
for _, arg := range argsList {
args = append(args, arg.(basetypes.StringValue).ValueString())
}
tflog.Debug(ctx, fmt.Sprintf("Creating post-renderer with binary path: %s and args: %v", binaryPath, args))
pr, err := postrender.NewExec(binaryPath, args...)
if err != nil {
resp.Diagnostics.AddError("Error creating post-renderer", fmt.Sprintf("Could not create post-renderer: %s", err))
return
}
releaseName := state.Name.ValueString()

client.PostRenderer = pr
if state.UpgradeInstall.ValueBool() {
tflog.Debug(ctx, fmt.Sprintf("Checking if %q is already installed", releaseName))

ver, err := getInstalledReleaseVersion(ctx, meta, actionConfig, releaseName)
if err != nil {
resp.Diagnostics.AddError("Error checking installed release", fmt.Sprintf("Failed to determine if release exists: %s", err))
return
}
installedVersion = ver
if installedVersion != "" {
tflog.Debug(ctx, fmt.Sprintf("Release %q is installed (version: %s)", releaseName, installedVersion))
releaseAlreadyExists = true
} else {
tflog.Debug(ctx, fmt.Sprintf("Release %q is not installed", releaseName))
}
}

rel, err := client.Run(c, values)
if state.UpgradeInstall.ValueBool() && releaseAlreadyExists {
tflog.Debug(ctx, fmt.Sprintf("Upgrade-installing chart %q", releaseName))

upgradeClient := action.NewUpgrade(actionConfig)
upgradeClient.ChartPathOptions = *cpo
upgradeClient.DryRun = false
upgradeClient.DisableHooks = state.DisableWebhooks.ValueBool()
upgradeClient.Wait = state.Wait.ValueBool()
upgradeClient.Devel = state.Devel.ValueBool()
upgradeClient.Timeout = time.Duration(state.Timeout.ValueInt64()) * time.Second
upgradeClient.Namespace = state.Namespace.ValueString()
upgradeClient.Atomic = state.Atomic.ValueBool()
upgradeClient.SkipCRDs = state.SkipCrds.ValueBool()
upgradeClient.SubNotes = state.RenderSubchartNotes.ValueBool()
upgradeClient.DisableOpenAPIValidation = state.DisableOpenapiValidation.ValueBool()
upgradeClient.Description = state.Description.ValueString()

if state.PostRender != nil {
binaryPath := state.PostRender.BinaryPath.ValueString()
argsList := state.PostRender.Args.Elements()
if binaryPath != "" {
var args []string
for _, arg := range argsList {
args = append(args, arg.(basetypes.StringValue).ValueString())
}
pr, err := postrender.NewExec(binaryPath, args...)
if err != nil {
resp.Diagnostics.AddError("Post-render Error", fmt.Sprintf("Could not create post-renderer: %s", err))
return
}
upgradeClient.PostRenderer = pr
}
}

rel, err = upgradeClient.Run(releaseName, c, values)
} else {
tflog.Debug(ctx, fmt.Sprintf("Installing chart %q", releaseName))
if state.PostRender != nil {
binaryPath := state.PostRender.BinaryPath.ValueString()
argsList := state.PostRender.Args.Elements()

if binaryPath != "" {
var args []string
for _, arg := range argsList {
args = append(args, arg.(basetypes.StringValue).ValueString())
}
tflog.Debug(ctx, fmt.Sprintf("Creating post-renderer with binary path: %s and args: %v", binaryPath, args))
pr, err := postrender.NewExec(binaryPath, args...)
if err != nil {
resp.Diagnostics.AddError("Error creating post-renderer", fmt.Sprintf("Could not create post-renderer: %s", err))
return
}

client.PostRenderer = pr
}
}
rel, err = client.Run(c, values)
}
if err != nil && rel == nil {
resp.Diagnostics.AddError("installation failed", err.Error())
return
Expand Down Expand Up @@ -2030,6 +2116,27 @@ func (r *HelmRelease) ModifyPlan(ctx context.Context, req resource.ModifyPlanReq

tflog.Debug(ctx, fmt.Sprintf("%s Done", logID))

if plan.UpgradeInstall.ValueBool() && plan.Version.IsNull() {
tflog.Debug(ctx, fmt.Sprintf("%s upgrade_install is enabled and version attribute is empty", logID))

installedVersion, err := getInstalledReleaseVersion(ctx, meta, actionConfig, name)
if err != nil {
resp.Diagnostics.AddError("Failed to check installed release version", err.Error())
return
}

if installedVersion != "" {
tflog.Debug(ctx, fmt.Sprintf("%s setting version to installed version %s", logID, installedVersion))
plan.Version = types.StringValue(installedVersion)
} else if len(chart.Metadata.Version) > 0 {
tflog.Debug(ctx, fmt.Sprintf("%s setting version to chart version %s", logID, chart.Metadata.Version))
plan.Version = types.StringValue(chart.Metadata.Version)
} else {
tflog.Debug(ctx, fmt.Sprintf("%s setting version to computed", logID))
plan.Version = types.StringNull()
}
}

if len(chart.Metadata.Version) > 0 {
plan.Version = types.StringValue(chart.Metadata.Version)
} else {
Expand Down
4 changes: 3 additions & 1 deletion helm/resource_helm_release_stateupgrader.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ func (r *HelmRelease) buildUpgradeStateMap(_ context.Context) map[int64]resource
"skip_crds": tftypes.Bool,
"status": tftypes.String,
"timeout": tftypes.Number,
"upgrade_install": tftypes.String,
"upgrade_install": tftypes.Bool,
"values": tftypes.List{ElementType: tftypes.String},
"verify": tftypes.Bool,
"version": tftypes.String,
Expand Down Expand Up @@ -249,6 +249,7 @@ func (r *HelmRelease) buildUpgradeStateMap(_ context.Context) map[int64]resource
"set_wo_revision": tftypes.Number,
"status": tftypes.String,
"timeout": tftypes.Number,
"upgrade_install": tftypes.Bool,
"values": tftypes.List{ElementType: tftypes.String},
"verify": tftypes.Bool,
"version": tftypes.String,
Expand Down Expand Up @@ -303,6 +304,7 @@ func (r *HelmRelease) buildUpgradeStateMap(_ context.Context) map[int64]resource
"skip_crds": oldState["skip_crds"],
"status": oldState["status"],
"timeout": oldState["timeout"],
"upgrade_install": oldState["upgrade_install"],
"values": oldState["values"],
"verify": oldState["verify"],
"version": oldState["version"],
Expand Down
114 changes: 114 additions & 0 deletions helm/resource_helm_release_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -592,6 +592,120 @@ func TestAccResourceRelease_updateAfterFail(t *testing.T) {
})
}

func TestAccResourceRelease_upgradeInstall_coldstart(t *testing.T) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

In the SDKv2 codebase this feature has an additional test: 723cb76#diff-0c630859c0a0648a5a7cd178d9b8d011108f16648cd8fc6b651390cec95ebc56R151

name := randName("coldstart")
namespace := createRandomNamespace(t)
defer deleteNamespace(t, namespace)

resource.Test(t, resource.TestCase{
ProtoV6ProviderFactories: protoV6ProviderFactories(),
Steps: []resource.TestStep{
{
Config: testAccHelmReleaseConfigWithUpgradeInstall(testResourceName, namespace, name, "1.2.3", true),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("helm_release.test", "metadata.name", name),
resource.TestCheckResourceAttr("helm_release.test", "metadata.namespace", namespace),
resource.TestCheckResourceAttr("helm_release.test", "metadata.revision", "1"),
resource.TestCheckResourceAttr("helm_release.test", "status", release.StatusDeployed.String()),
resource.TestCheckResourceAttr("helm_release.test", "description", "Test"),
resource.TestCheckResourceAttr("helm_release.test", "metadata.chart", "test-chart"),
resource.TestCheckResourceAttr("helm_release.test", "metadata.version", "1.2.3"),
resource.TestCheckResourceAttr("helm_release.test", "metadata.app_version", "1.19.5"),
),
},
{
Config: testAccHelmReleaseConfigWithUpgradeInstall(testResourceName, namespace, name, "1.2.3", true),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("helm_release.test", "metadata.revision", "1"),
),
},
},
})
}

func testAccHelmReleaseConfigWithUpgradeInstall(resource, ns, name, version string, upgradeInstall bool) string {
return fmt.Sprintf(`
resource "helm_release" "%s" {
name = %q
namespace = %q
chart = "test-chart"
repository = %q
version = %q
description = "Test"
upgrade_install = %t

set = [
{
name = "foo"
value = "qux"
},
{
name = "qux.bar"
value = "1"
},
{
name = "master.persistence.enabled"
value = "false"
},
{
name = "replication.enabled"
value = "false"
}
]
}
`, resource, name, ns, testRepositoryURL, version, upgradeInstall)
}

func TestAccResourceRelease_upgradeInstall_warmstart(t *testing.T) {
name := randName("warmstart")
namespace := createRandomNamespace(t)
defer deleteNamespace(t, namespace)

cmd := exec.Command("helm", "install", name, "test-chart",
"--repo", testRepositoryURL,
"--version", "1.2.3",
"-n", namespace, "--create-namespace")
if out, err := cmd.CombinedOutput(); err != nil {
t.Fatalf("failed to preinstall release: %s\n%s", err, out)
}

resource.Test(t, resource.TestCase{
ProtoV6ProviderFactories: protoV6ProviderFactories(),
Steps: []resource.TestStep{
{
Config: testAccHelmReleaseWarmstart(testResourceName, namespace, name, "1.2.3"),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr("helm_release.test", "metadata.revision", "2"),
resource.TestCheckResourceAttr("helm_release.test", "metadata.version", "1.2.3"),
resource.TestCheckResourceAttr("helm_release.test", "metadata.values", `{"foo":"bar"}`),
resource.TestCheckResourceAttr("helm_release.test", "status", release.StatusDeployed.String()),
),
},
},
})
}

func testAccHelmReleaseWarmstart(resource, ns, name, version string) string {
return fmt.Sprintf(`
resource "helm_release" "%s" {
name = %q
namespace = %q
chart = "test-chart"
repository = %q
version = %q
description = "Test"
upgrade_install = true

set = [
{
name = "foo"
value = "bar"
}
]
}
`, resource, name, ns, testRepositoryURL, version)
}

func TestAccResourceRelease_updateExistingFailed(t *testing.T) {
name := randName("test-update-existing-failed")
namespace := createRandomNamespace(t)
Expand Down
Loading