devops/shuttles/terraform/terraform-provider-helm/helm/resource_helm_release.go

2272 lines
72 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package helm
import (
"context"
"encoding/json"
"fmt"
"net/url"
"os"
pathpkg "path"
"strings"
"time"
"github.com/hashicorp/terraform-plugin-framework-validators/int64validator"
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/identityschema"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/defaults"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/schema/validator"
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
"github.com/hashicorp/terraform-plugin-log/tflog"
"github.com/pkg/errors"
"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"
"helm.sh/helm/v3/pkg/registry"
"helm.sh/helm/v3/pkg/release"
"k8s.io/helm/pkg/strvals"
"sigs.k8s.io/yaml"
)
var (
_ resource.Resource = &HelmRelease{}
_ resource.ResourceWithModifyPlan = &HelmRelease{}
_ resource.ResourceWithImportState = &HelmRelease{}
_ resource.ResourceWithIdentity = &HelmRelease{}
_ resource.ResourceWithUpgradeState = &HelmRelease{}
)
type HelmRelease struct {
meta *Meta
}
func NewHelmRelease() resource.Resource {
return &HelmRelease{}
}
type HelmReleaseIdentityModel struct {
Namespace types.String `tfsdk:"namespace"`
ReleaseName types.String `tfsdk:"release_name"`
}
type HelmReleaseModel struct {
Atomic types.Bool `tfsdk:"atomic"`
Chart types.String `tfsdk:"chart"`
CleanupOnFail types.Bool `tfsdk:"cleanup_on_fail"`
CreateNamespace types.Bool `tfsdk:"create_namespace"`
DependencyUpdate types.Bool `tfsdk:"dependency_update"`
Description types.String `tfsdk:"description"`
Devel types.Bool `tfsdk:"devel"`
DisableCrdHooks types.Bool `tfsdk:"disable_crd_hooks"`
DisableOpenapiValidation types.Bool `tfsdk:"disable_openapi_validation"`
DisableWebhooks types.Bool `tfsdk:"disable_webhooks"`
ForceUpdate types.Bool `tfsdk:"force_update"`
ID types.String `tfsdk:"id"`
Keyring types.String `tfsdk:"keyring"`
Lint types.Bool `tfsdk:"lint"`
Manifest types.String `tfsdk:"manifest"`
MaxHistory types.Int64 `tfsdk:"max_history"`
Metadata types.Object `tfsdk:"metadata"`
Name types.String `tfsdk:"name"`
Namespace types.String `tfsdk:"namespace"`
PassCredentials types.Bool `tfsdk:"pass_credentials"`
PostRender *PostRenderModel `tfsdk:"postrender"`
RecreatePods types.Bool `tfsdk:"recreate_pods"`
Replace types.Bool `tfsdk:"replace"`
RenderSubchartNotes types.Bool `tfsdk:"render_subchart_notes"`
Repository types.String `tfsdk:"repository"`
RepositoryCaFile types.String `tfsdk:"repository_ca_file"`
RepositoryCertFile types.String `tfsdk:"repository_cert_file"`
RepositoryKeyFile types.String `tfsdk:"repository_key_file"`
RepositoryPassword types.String `tfsdk:"repository_password"`
RepositoryUsername types.String `tfsdk:"repository_username"`
ResetValues types.Bool `tfsdk:"reset_values"`
ReuseValues types.Bool `tfsdk:"reuse_values"`
SetWO types.List `tfsdk:"set_wo"`
SetWORevision types.Int64 `tfsdk:"set_wo_revision"`
Set types.List `tfsdk:"set"`
SetList types.List `tfsdk:"set_list"`
SetSensitive types.List `tfsdk:"set_sensitive"`
SkipCrds types.Bool `tfsdk:"skip_crds"`
Status types.String `tfsdk:"status"`
Timeout types.Int64 `tfsdk:"timeout"`
Values types.List `tfsdk:"values"`
Verify types.Bool `tfsdk:"verify"`
Version types.String `tfsdk:"version"`
Wait types.Bool `tfsdk:"wait"`
WaitForJobs types.Bool `tfsdk:"wait_for_jobs"`
}
var defaultAttributes = map[string]interface{}{
"atomic": false,
"cleanup_on_fail": false,
"create_namespace": false,
"dependency_update": false,
"disable_crd_hooks": false,
"disable_openapi_validation": false,
"disable_webhooks": false,
"force_update": false,
"lint": false,
"max_history": int64(0),
"pass_credentials": false,
"recreate_pods": false,
"render_subchart_notes": true,
"replace": false,
"reset_values": false,
"reuse_values": false,
"skip_crds": false,
"timeout": int64(300),
"verify": false,
"wait": true,
"wait_for_jobs": false,
}
type releaseMetaData struct {
AppVersion types.String `tfsdk:"app_version"`
Chart types.String `tfsdk:"chart"`
Name types.String `tfsdk:"name"`
Namespace types.String `tfsdk:"namespace"`
Revision types.Int64 `tfsdk:"revision"`
Version types.String `tfsdk:"version"`
Values types.String `tfsdk:"values"`
FirstDeployed types.Int64 `tfsdk:"first_deployed"`
LastDeployed types.Int64 `tfsdk:"last_deployed"`
Notes types.String `tfsdk:"notes"`
}
type setResourceModel struct {
Name types.String `tfsdk:"name"`
Type types.String `tfsdk:"type"`
Value types.String `tfsdk:"value"`
}
type set_listResourceModel struct {
Name types.String `tfsdk:"name"`
Value types.List `tfsdk:"value"`
}
type PostRenderModel struct {
Args types.List `tfsdk:"args"`
BinaryPath types.String `tfsdk:"binary_path"`
}
type suppressDescriptionPlanModifier struct{}
func (m suppressDescriptionPlanModifier) Description(ctx context.Context) string {
return "Suppress changes if the new description is an empty string"
}
func (m suppressDescriptionPlanModifier) MarkdownDescription(ctx context.Context) string {
return m.Description(ctx)
}
func (m suppressDescriptionPlanModifier) PlanModifyString(ctx context.Context, req planmodifier.StringRequest, resp *planmodifier.StringResponse) {
if req.PlanValue.IsNull() || req.PlanValue.ValueString() == "" {
resp.PlanValue = req.StateValue
}
}
func suppressDescription() planmodifier.String {
return suppressDescriptionPlanModifier{}
}
type suppressDevelPlanModifier struct{}
func (m suppressDevelPlanModifier) Description(ctx context.Context) string {
return "Suppress changes if the version is set"
}
func (m suppressDevelPlanModifier) MarkdownDescription(ctx context.Context) string {
return m.Description(ctx)
}
func (m suppressDevelPlanModifier) PlanModifyBool(ctx context.Context, req planmodifier.BoolRequest, resp *planmodifier.BoolResponse) {
var version types.String
req.Plan.GetAttribute(ctx, path.Root("version"), &version)
if !version.IsNull() && version.ValueString() != "" {
resp.PlanValue = req.StateValue
}
}
func suppressDevel() planmodifier.Bool {
return suppressDevelPlanModifier{}
}
// Supress Keyring
type suppressKeyringPlanModifier struct{}
func (m suppressKeyringPlanModifier) Description(ctx context.Context) string {
return "Suppress changes if verify is false"
}
func (m suppressKeyringPlanModifier) MarkdownDescription(ctx context.Context) string {
return m.Description(ctx)
}
func (m suppressKeyringPlanModifier) PlanModifyString(ctx context.Context, req planmodifier.StringRequest, resp *planmodifier.StringResponse) {
var verify types.Bool
req.Plan.GetAttribute(ctx, path.Root("verify"), &verify)
if !verify.IsNull() && !verify.ValueBool() {
resp.PlanValue = req.StateValue
}
}
func suppressKeyring() planmodifier.String {
return suppressKeyringPlanModifier{}
}
func namespaceDefault() defaults.String {
return namespaceDefaultValue{}
}
type namespaceDefaultValue struct{}
func (d namespaceDefaultValue) Description(ctx context.Context) string {
return "If namespace is not provided, defaults to HELM_NAMESPACE environment variable or 'default'."
}
func (d namespaceDefaultValue) MarkdownDescription(ctx context.Context) string {
return d.Description(ctx)
}
func (d namespaceDefaultValue) DefaultString(ctx context.Context, req defaults.StringRequest, resp *defaults.StringResponse) {
envNamespace := os.Getenv("HELM_NAMESPACE")
if envNamespace == "" {
envNamespace = "default"
}
resp.PlanValue = types.StringValue(envNamespace)
}
func (r *HelmRelease) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_release"
}
func (r *HelmRelease) UpgradeState(ctx context.Context) map[int64]resource.StateUpgrader {
return r.buildUpgradeStateMap(ctx)
}
func (r *HelmRelease) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
Description: "Schema to define attributes that are available in the resource",
Attributes: map[string]schema.Attribute{
"atomic": schema.BoolAttribute{
Optional: true,
Computed: true,
Default: booldefault.StaticBool(defaultAttributes["atomic"].(bool)),
Description: "If set, installation process purges chart on fail. The wait flag will be set automatically if atomic is used",
},
"chart": schema.StringAttribute{
Required: true,
Description: "Chart name to be installed. A path may be used",
},
"cleanup_on_fail": schema.BoolAttribute{
Optional: true,
Computed: true,
Default: booldefault.StaticBool(defaultAttributes["cleanup_on_fail"].(bool)),
Description: "Allow deletion of new resources created in this upgrade when upgrade fails",
},
"create_namespace": schema.BoolAttribute{
Optional: true,
Computed: true,
Default: booldefault.StaticBool(defaultAttributes["create_namespace"].(bool)),
Description: "Create the namespace if it does not exist",
},
"dependency_update": schema.BoolAttribute{
Optional: true,
Computed: true,
Default: booldefault.StaticBool(defaultAttributes["dependency_update"].(bool)),
Description: "Run helm dependency update before installing the chart",
},
"description": schema.StringAttribute{
Optional: true,
Description: "Add a custom description",
PlanModifiers: []planmodifier.String{
suppressDescription(),
},
},
"devel": schema.BoolAttribute{
Optional: true,
Description: "Use chart development versions, too. Equivalent to version '>0.0.0-0'. If 'version' is set, this is ignored",
PlanModifiers: []planmodifier.Bool{
suppressDevel(),
},
},
"disable_crd_hooks": schema.BoolAttribute{
Optional: true,
Computed: true,
Default: booldefault.StaticBool(defaultAttributes["disable_crd_hooks"].(bool)),
Description: "Prevent CRD hooks from running, but run other hooks. See helm install --no-crd-hook",
},
"disable_openapi_validation": schema.BoolAttribute{
Optional: true,
Computed: true,
Default: booldefault.StaticBool(defaultAttributes["disable_openapi_validation"].(bool)),
Description: "If set, the installation process will not validate rendered templates against the Kubernetes OpenAPI Schema",
},
"disable_webhooks": schema.BoolAttribute{
Optional: true,
Computed: true,
Default: booldefault.StaticBool(defaultAttributes["disable_webhooks"].(bool)),
Description: "Prevent hooks from running",
},
"force_update": schema.BoolAttribute{
Optional: true,
Computed: true,
Default: booldefault.StaticBool(defaultAttributes["force_update"].(bool)),
Description: "Force resource update through delete/recreate if needed.",
},
"id": schema.StringAttribute{
Computed: true,
},
"keyring": schema.StringAttribute{
Optional: true,
Description: "Location of public keys used for verification, Used only if 'verify is true'",
PlanModifiers: []planmodifier.String{
suppressKeyring(),
},
},
"lint": schema.BoolAttribute{
Optional: true,
Computed: true,
Default: booldefault.StaticBool(defaultAttributes["lint"].(bool)),
Description: "Run helm lint when planning",
},
"manifest": schema.StringAttribute{
Description: "The rendered manifest as JSON.",
Computed: true,
},
"max_history": schema.Int64Attribute{
Optional: true,
Computed: true,
Default: int64default.StaticInt64(defaultAttributes["max_history"].(int64)),
Description: "Limit the maximum number of revisions saved per release. Use 0 for no limit",
},
"metadata": schema.SingleNestedAttribute{
Description: "Status of the deployed release.",
Computed: true,
Attributes: map[string]schema.Attribute{
"app_version": schema.StringAttribute{
Computed: true,
Description: "The version number of the application being deployed",
},
"chart": schema.StringAttribute{
Computed: true,
Description: "The name of the chart",
},
"first_deployed": schema.Int64Attribute{
Computed: true,
Description: "FirstDeployed is an int64 which represents timestamp when the release was first deployed.",
},
"last_deployed": schema.Int64Attribute{
Computed: true,
Description: "LastDeployed is an int64 which represents timestamp when the release was last deployed.",
},
"name": schema.StringAttribute{
Computed: true,
Description: "Name is the name of the release",
},
"namespace": schema.StringAttribute{
Computed: true,
Description: "Namespace is the kubernetes namespace of the release",
},
"notes": schema.StringAttribute{
Computed: true,
Description: "Notes is the description of the deployed release, rendered from templates.",
},
"revision": schema.Int64Attribute{
Computed: true,
Description: "Version is an int32 which represents the version of the release",
},
"values": schema.StringAttribute{
Computed: true,
Description: "Set of extra values. added to the chart. The sensitive data is cloaked. JSON encoded.",
},
"version": schema.StringAttribute{
Computed: true,
Description: "A SemVer 2 conformant version string of the chart",
},
},
},
"name": schema.StringAttribute{
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
Validators: []validator.String{
stringvalidator.LengthBetween(1, 53),
},
Description: "Release name. The length must not be longer than 53 characters",
},
"namespace": schema.StringAttribute{
Optional: true,
Computed: true,
Default: namespaceDefault(),
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
Description: "Namespace to install the release into",
},
"pass_credentials": schema.BoolAttribute{
Optional: true,
Description: "Pass credentials to all domains",
Computed: true,
Default: booldefault.StaticBool(defaultAttributes["pass_credentials"].(bool)),
},
"recreate_pods": schema.BoolAttribute{
Optional: true,
Computed: true,
Default: booldefault.StaticBool(defaultAttributes["recreate_pods"].(bool)),
Description: "Perform pods restart during upgrade/rollback",
},
"render_subchart_notes": schema.BoolAttribute{
Optional: true,
Computed: true,
Default: booldefault.StaticBool(defaultAttributes["render_subchart_notes"].(bool)),
Description: "If set, render subchart notes along with the parent",
},
"replace": schema.BoolAttribute{
Optional: true,
Computed: true,
Default: booldefault.StaticBool(defaultAttributes["replace"].(bool)),
Description: "Re-use the given name, even if that name is already used. This is unsafe in production",
},
"repository": schema.StringAttribute{
Optional: true,
Description: "Repository where to locate the requested chart. If it is a URL, the chart is installed without installing the repository",
},
"repository_ca_file": schema.StringAttribute{
Optional: true,
Description: "The Repositories CA file",
},
"repository_cert_file": schema.StringAttribute{
Optional: true,
Description: "The repositories cert file",
},
"repository_key_file": schema.StringAttribute{
Optional: true,
Description: "The repositories cert key file",
},
"repository_password": schema.StringAttribute{
Optional: true,
Sensitive: true,
Description: "Password for HTTP basic authentication",
},
"repository_username": schema.StringAttribute{
Optional: true,
Description: "Username for HTTP basic authentication",
},
"reset_values": schema.BoolAttribute{
Optional: true,
Computed: true,
Description: "When upgrading, reset the values to the ones built into the chart",
Default: booldefault.StaticBool(defaultAttributes["reset_values"].(bool)),
},
"reuse_values": schema.BoolAttribute{
Optional: true,
Computed: true,
Description: "When upgrading, reuse the last release's values and merge in any overrides. If 'reset_values' is specified, this is ignored",
Default: booldefault.StaticBool(defaultAttributes["reuse_values"].(bool)),
},
"skip_crds": schema.BoolAttribute{
Optional: true,
Computed: true,
Default: booldefault.StaticBool(defaultAttributes["skip_crds"].(bool)),
Description: "If set, no CRDs will be installed. By default, CRDs are installed if not already present",
},
"status": schema.StringAttribute{
Computed: true,
Description: "Status of the release",
},
"timeout": schema.Int64Attribute{
Optional: true,
Computed: true,
Default: int64default.StaticInt64(defaultAttributes["timeout"].(int64)),
Description: "Time in seconds to wait for any individual kubernetes operation",
},
"values": schema.ListAttribute{
Optional: true,
Description: "List of values in raw YAML format to pass to helm",
ElementType: types.StringType,
},
"verify": schema.BoolAttribute{
Optional: true,
Computed: true,
Default: booldefault.StaticBool(defaultAttributes["verify"].(bool)),
Description: "Verify the package before installing it.",
},
"version": schema.StringAttribute{
Optional: true,
Computed: true,
Description: "Specify the exact chart version to install. If this is not specified, the latest version is installed",
},
"wait": schema.BoolAttribute{
Optional: true,
Computed: true,
Default: booldefault.StaticBool(defaultAttributes["wait"].(bool)),
Description: "Will wait until all resources are in a ready state before marking the release as successful.",
},
"wait_for_jobs": schema.BoolAttribute{
Optional: true,
Computed: true,
Default: booldefault.StaticBool(defaultAttributes["wait_for_jobs"].(bool)),
Description: "If wait is enabled, will wait until all Jobs have been completed before marking the release as successful.",
},
"set": schema.ListNestedAttribute{
Description: "Custom values to be merged with the values",
Optional: true,
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
"name": schema.StringAttribute{
Required: true,
},
"value": schema.StringAttribute{
Optional: true,
},
"type": schema.StringAttribute{
Optional: true,
Computed: true,
Default: stringdefault.StaticString(""),
Validators: []validator.String{
stringvalidator.OneOf("auto", "string", "literal"),
},
},
},
},
},
"set_wo": schema.ListNestedAttribute{
Description: "Custom values to be merged with the values",
Optional: true,
WriteOnly: true,
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
"name": schema.StringAttribute{
Required: true,
WriteOnly: true,
},
"value": schema.StringAttribute{
Required: true,
WriteOnly: true,
},
"type": schema.StringAttribute{
Optional: true,
WriteOnly: true,
Validators: []validator.String{
stringvalidator.OneOf("auto", "string"),
},
},
},
},
},
"set_wo_revision": schema.Int64Attribute{
Optional: true,
Description: `The current revision of the write-only "set_wo" attribute. Incrementing this integer value will cause Terraform to update the write-only value.`,
Validators: []validator.Int64{
int64validator.AtLeast(1),
},
},
"set_list": schema.ListNestedAttribute{
Description: "Custom sensitive values to be merged with the values",
Optional: true,
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
"name": schema.StringAttribute{
Required: true,
},
"value": schema.ListAttribute{
Required: true,
ElementType: types.StringType,
},
},
},
},
"set_sensitive": schema.ListNestedAttribute{
Description: "Custom sensitive values to be merged with the values",
Optional: true,
NestedObject: schema.NestedAttributeObject{
Attributes: map[string]schema.Attribute{
"name": schema.StringAttribute{
Required: true,
},
"value": schema.StringAttribute{
Required: true,
Sensitive: true,
},
"type": schema.StringAttribute{
Optional: true,
Validators: []validator.String{
stringvalidator.OneOf("auto", "string", "literal"),
},
},
},
},
},
"postrender": schema.SingleNestedAttribute{
Description: "Postrender command config",
Optional: true,
Attributes: map[string]schema.Attribute{
"args": schema.ListAttribute{
Optional: true,
Description: "An argument to the post-renderer (can specify multiple)",
ElementType: types.StringType,
},
"binary_path": schema.StringAttribute{
Required: true,
Description: "The common binary path",
},
},
},
},
Version: 2,
}
}
func (r *HelmRelease) IdentitySchema(ctx context.Context, req resource.IdentitySchemaRequest, resp *resource.IdentitySchemaResponse) {
resp.IdentitySchema = identityschema.Schema{
Version: 0,
Attributes: map[string]identityschema.Attribute{
"namespace": identityschema.StringAttribute{
// use "default" if not specified
OptionalForImport: true,
},
"release_name": identityschema.StringAttribute{
RequiredForImport: true,
},
},
}
}
func (r *HelmRelease) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
// Ensure that the ProviderData is not nil
if req.ProviderData == nil {
return
}
// Assert that the ProviderData is of type *Meta
meta, ok := req.ProviderData.(*Meta)
if !ok {
resp.Diagnostics.AddError(
"Provider Configuration Error",
fmt.Sprintf("Unexpected ProviderData type: %T", req.ProviderData),
)
return
}
tflog.Debug(ctx, fmt.Sprintf("Configured meta: %+v", meta))
r.meta = meta
}
const sensitiveContentValue = "(sensitive value)"
func cloakSetValue(values map[string]interface{}, valuePath string) {
pathKeys := strings.Split(valuePath, ".")
sensitiveKey := pathKeys[len(pathKeys)-1]
parentPathKeys := pathKeys[:len(pathKeys)-1]
m := values
for _, key := range parentPathKeys {
v, ok := m[key].(map[string]interface{})
if !ok {
return
}
m = v
}
m[sensitiveKey] = sensitiveContentValue
}
func mergeMaps(a, b map[string]interface{}) map[string]interface{} {
out := make(map[string]interface{}, len(a))
for k, v := range a {
out[k] = v
}
for k, v := range b {
if vMap, ok := v.(map[string]interface{}); ok {
if bv, ok := out[k]; ok {
if bvMap, ok := bv.(map[string]interface{}); ok {
out[k] = mergeMaps(bvMap, vMap)
continue
}
}
}
out[k] = v
}
return out
}
func (r *HelmRelease) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var state HelmReleaseModel
diags := req.Plan.Get(ctx, &state)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
var config HelmReleaseModel
diags = req.Config.Get(ctx, &config)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
tflog.Debug(ctx, fmt.Sprintf("Plan state on Create: %+v", state))
meta := r.meta
if meta == nil {
resp.Diagnostics.AddError("Initialization Error", "Meta instance is not initialized")
return
}
namespace := state.Namespace.ValueString()
actionConfig, err := meta.GetHelmConfiguration(ctx, namespace)
if err != nil {
resp.Diagnostics.AddError("Error getting helm configuration", fmt.Sprintf("Unable to get Helm configuration for namespace %s: %s", namespace, err))
return
}
ociDiags := OCIRegistryLogin(ctx, meta, actionConfig, meta.RegistryClient, state.Repository.ValueString(), state.Chart.ValueString(), state.RepositoryUsername.ValueString(), state.RepositoryPassword.ValueString())
resp.Diagnostics.Append(ociDiags...)
if resp.Diagnostics.HasError() {
return
}
client := action.NewInstall(actionConfig)
cpo, chartName, cpoDiags := chartPathOptions(&state, meta, &client.ChartPathOptions)
resp.Diagnostics.Append(cpoDiags...)
if resp.Diagnostics.HasError() {
return
}
c, cpath, chartDiags := getChart(ctx, &state, meta, chartName, cpo)
resp.Diagnostics.Append(chartDiags...)
if resp.Diagnostics.HasError() {
return
}
updated, depDiags := checkChartDependencies(ctx, &state, c, cpath, meta)
resp.Diagnostics.Append(depDiags...)
if resp.Diagnostics.HasError() {
return
} else if updated {
c, err = loader.Load(cpath)
if err != nil {
resp.Diagnostics.AddError("Error loading chart", fmt.Sprintf("Could not load chart: %s", err))
return
}
}
values, valuesDiags := getValues(ctx, &state)
resp.Diagnostics.Append(valuesDiags...)
if resp.Diagnostics.HasError() {
return
}
if config.SetWORevision.ValueInt64() > 0 {
woValues, woDiags := getWriteOnlyValues(ctx, &config)
resp.Diagnostics.Append(woDiags...)
if resp.Diagnostics.HasError() {
return
}
if len(woValues) > 0 {
values = mergeMaps(values, woValues)
}
}
err = isChartInstallable(c)
if err != nil {
resp.Diagnostics.AddError("Error checking if chart is installable", fmt.Sprintf("Chart is not installable: %s", err))
return
}
client.ClientOnly = false
client.DryRun = false
client.DisableHooks = state.DisableWebhooks.ValueBool()
client.Wait = state.Wait.ValueBool()
client.WaitForJobs = state.WaitForJobs.ValueBool()
client.Devel = state.Devel.ValueBool()
client.DependencyUpdate = state.DependencyUpdate.ValueBool()
client.Timeout = time.Duration(state.Timeout.ValueInt64()) * time.Second
client.Namespace = state.Namespace.ValueString()
client.ReleaseName = state.Name.ValueString()
client.Atomic = state.Atomic.ValueBool()
client.SkipCRDs = state.SkipCrds.ValueBool()
client.SubNotes = state.RenderSubchartNotes.ValueBool()
client.DisableOpenAPIValidation = state.DisableOpenapiValidation.ValueBool()
client.Replace = state.Replace.ValueBool()
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 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
}
if err != nil && rel != nil {
exists, existsDiags := resourceReleaseExists(ctx, state.Name.ValueString(), state.Namespace.ValueString(), meta)
resp.Diagnostics.Append(existsDiags...)
if resp.Diagnostics.HasError() {
return
}
if !exists {
resp.Diagnostics.AddError("installation failed", err.Error())
return
}
diags := setReleaseAttributes(ctx, &state, resp.Identity, rel, meta)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
resp.Diagnostics.Append(diag.NewWarningDiagnostic("Helm release created with warnings", fmt.Sprintf("Helm release %q was created but has a failed status. Use the `helm` command to investigate the error, correct it, then run Terraform again.", client.ReleaseName)))
resp.Diagnostics.Append(diag.NewErrorDiagnostic("Helm release error", err.Error()))
return
}
diags = setReleaseAttributes(ctx, &state, resp.Identity, rel, meta)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
diags = resp.State.Set(ctx, &state)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
}
func (r *HelmRelease) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var state HelmReleaseModel
diags := req.State.Get(ctx, &state)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
tflog.Debug(ctx, fmt.Sprintf("Current state before changes: %+v", state))
meta := r.meta
if meta == nil {
resp.Diagnostics.AddError(
"Meta not set",
"The meta information is not set for the resource",
)
return
}
exists, diags := resourceReleaseExists(ctx, state.Name.ValueString(), state.Namespace.ValueString(), meta)
if !exists {
resp.State.RemoveResource(ctx)
return
}
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
logID := fmt.Sprintf("[resourceReleaseRead: %s]", state.Name.ValueString())
tflog.Debug(ctx, fmt.Sprintf("%s Started", logID))
c, err := meta.GetHelmConfiguration(ctx, state.Namespace.ValueString())
if err != nil {
resp.Diagnostics.AddError(
"Error getting helm configuration",
fmt.Sprintf("Unable to get Helm configuration for namespace %s: %s", state.Namespace.ValueString(), err),
)
return
}
release, err := getRelease(ctx, meta, c, state.Name.ValueString())
if err != nil {
resp.Diagnostics.AddError(
"Error getting release",
fmt.Sprintf("Unable to get Helm release %s: %s", state.Name.ValueString(), err.Error()),
)
return
}
diags = setReleaseAttributes(ctx, &state, resp.Identity, release, meta)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
resp.Diagnostics.AddError(
"Error setting release attributes",
fmt.Sprintf("Unable to set attributes for helm release %s", state.Name.ValueString()),
)
return
}
resp.Diagnostics.Append(resp.State.Set(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}
}
func (r *HelmRelease) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var plan HelmReleaseModel
diags := req.Plan.Get(ctx, &plan)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
// Current state of the resource before update operation is applied
var state HelmReleaseModel
diags = req.State.Get(ctx, &state)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
var config HelmReleaseModel
diags = req.Config.Get(ctx, &config)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
logID := fmt.Sprintf("[resourceReleaseUpdate: %s]", state.Name.ValueString())
tflog.Debug(ctx, fmt.Sprintf("%s Started", logID))
meta := r.meta
namespace := state.Namespace.ValueString()
tflog.Debug(ctx, fmt.Sprintf("%s Getting helm configuration for namespace: %s", logID, namespace))
actionConfig, err := meta.GetHelmConfiguration(ctx, namespace)
if err != nil {
tflog.Debug(ctx, fmt.Sprintf("%s Failed to get helm configuration: %v", logID, err))
resp.Diagnostics.AddError("Error getting helm configuration", fmt.Sprintf("Unable to get Helm configuration for namespace %s: %s", namespace, err))
return
}
ociDiags := OCIRegistryLogin(ctx, meta, actionConfig, meta.RegistryClient, state.Repository.ValueString(), state.Chart.ValueString(), state.RepositoryUsername.ValueString(), state.RepositoryPassword.ValueString())
resp.Diagnostics.Append(ociDiags...)
if resp.Diagnostics.HasError() {
return
}
client := action.NewUpgrade(actionConfig)
cpo, chartName, cpoDiags := chartPathOptions(&plan, meta, &client.ChartPathOptions)
resp.Diagnostics.Append(cpoDiags...)
if resp.Diagnostics.HasError() {
return
}
c, path, chartDiags := getChart(ctx, &plan, meta, chartName, cpo)
resp.Diagnostics.Append(chartDiags...)
if resp.Diagnostics.HasError() {
return
}
// Check and update the chart's depenedcies if it's needed
updated, depDiags := checkChartDependencies(ctx, &plan, c, path, meta)
resp.Diagnostics.Append(depDiags...)
if resp.Diagnostics.HasError() {
return
} else if updated {
c, err = loader.Load(path)
if err != nil {
resp.Diagnostics.AddError("Error loading chart", fmt.Sprintf("Could not load chart: %s", err))
return
}
}
client.Devel = plan.Devel.ValueBool()
client.Namespace = plan.Namespace.ValueString()
client.Timeout = time.Duration(plan.Timeout.ValueInt64()) * time.Second
client.Wait = plan.Wait.ValueBool()
client.WaitForJobs = plan.WaitForJobs.ValueBool()
client.DryRun = false
client.DisableHooks = plan.DisableWebhooks.ValueBool()
client.Atomic = plan.Atomic.ValueBool()
client.SkipCRDs = plan.SkipCrds.ValueBool()
client.SubNotes = plan.RenderSubchartNotes.ValueBool()
client.DisableOpenAPIValidation = plan.DisableOpenapiValidation.ValueBool()
client.Force = plan.ForceUpdate.ValueBool()
client.ResetValues = plan.ResetValues.ValueBool()
client.ReuseValues = plan.ReuseValues.ValueBool()
client.Recreate = plan.RecreatePods.ValueBool()
client.MaxHistory = int(plan.MaxHistory.ValueInt64())
client.CleanupOnFail = plan.CleanupOnFail.ValueBool()
client.Description = plan.Description.ValueString()
if plan.PostRender != nil {
binaryPath := plan.PostRender.BinaryPath.ValueString()
argsList := plan.PostRender.Args.Elements()
var args []string
for _, arg := range argsList {
args = append(args, arg.(basetypes.StringValue).ValueString())
}
tflog.Debug(ctx, fmt.Sprintf("Binary path update method: %s, 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
}
values, valuesDiags := getValues(ctx, &plan)
resp.Diagnostics.Append(valuesDiags...)
if resp.Diagnostics.HasError() {
return
}
if plan.SetWORevision.ValueInt64() > state.SetWORevision.ValueInt64() {
woValues, woDiags := getWriteOnlyValues(ctx, &config)
resp.Diagnostics.Append(woDiags...)
if resp.Diagnostics.HasError() {
return
}
if len(woValues) > 0 {
values = mergeMaps(values, woValues)
}
}
name := plan.Name.ValueString()
release, err := client.Run(name, c, values)
if err != nil {
resp.Diagnostics.AddError("Error upgrading chart", fmt.Sprintf("Upgrade failed: %s", err))
return
}
diags = setReleaseAttributes(ctx, &plan, resp.Identity, release, meta)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
diags = resp.State.Set(ctx, &plan)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
}
func (r *HelmRelease) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
var state HelmReleaseModel
diags := req.State.Get(ctx, &state)
for _, diag := range diags {
tflog.Debug(ctx, fmt.Sprintf("Diagnostics after state get: %s", diag.Detail()))
}
// Append diagnostics to response
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
tflog.Error(ctx, fmt.Sprintf("Error retrieving state: %v", resp.Diagnostics))
return
}
tflog.Debug(ctx, fmt.Sprintf("Retrieved state: %+v", state))
// Check if meta is set
meta := r.meta
if meta == nil {
resp.Diagnostics.AddError(
"Meta not set",
"The meta information is not set for the resource",
)
tflog.Error(ctx, "Meta information is not set for the resource")
return
}
name := state.Name.ValueString()
namespace := state.Namespace.ValueString()
exists, diags := resourceReleaseExists(ctx, name, namespace, meta)
if !exists {
return
}
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
// Get Helm configuration
actionConfig, err := meta.GetHelmConfiguration(ctx, namespace)
if err != nil {
resp.Diagnostics.AddError(
"Error getting helm configuration",
fmt.Sprintf("Unable to get Helm configuration for namespace %s: %s", namespace, err),
)
tflog.Error(ctx, fmt.Sprintf("Unable to get Helm configuration for namespace %s: %s", namespace, err))
return
}
tflog.Debug(ctx, fmt.Sprintf("Retrieved Helm configuration for namespace: %s", namespace))
// Initialize uninstall action
uninstall := action.NewUninstall(actionConfig)
uninstall.Wait = state.Wait.ValueBool()
uninstall.DisableHooks = state.DisableWebhooks.ValueBool()
uninstall.Timeout = time.Duration(state.Timeout.ValueInt64()) * time.Second
// Uninstall the release
tflog.Info(ctx, fmt.Sprintf("Uninstalling Helm release: %s", name))
res, err := uninstall.Run(name)
if err != nil {
resp.Diagnostics.AddError(
"Error uninstalling release",
fmt.Sprintf("Unable to uninstall Helm release %s: %s", name, err),
)
tflog.Error(ctx, fmt.Sprintf("Unable to uninstall Helm release %s: %s", name, err))
return
}
if res.Info != "" {
resp.Diagnostics.Append(diag.NewWarningDiagnostic(
"Helm uninstall returned an information message",
res.Info,
))
}
}
func chartPathOptions(model *HelmReleaseModel, meta *Meta, cpo *action.ChartPathOptions) (*action.ChartPathOptions, string, diag.Diagnostics) {
var diags diag.Diagnostics
chartName := model.Chart.ValueString()
repository := model.Repository.ValueString()
var repositoryURL string
if registry.IsOCI(repository) {
// LocateChart expects the chart name to contain the full OCI path
u, err := url.Parse(repository)
if err != nil {
diags.AddError("Invalid Repository URL", fmt.Sprintf("Failed to parse repository URL %s: %s", repository, err))
return nil, "", diags
}
u.Path = pathpkg.Join(u.Path, chartName)
chartName = u.String()
} else {
var err error
repositoryURL, chartName, err = buildChartNameWithRepository(repository, strings.TrimSpace(chartName))
if err != nil {
diags.AddError("Error building Chart Name With Repository", fmt.Sprintf("Could not build Chart Name With Repository %s and chart %s: %s", repository, chartName, err))
return nil, "", diags
}
}
version := getVersion(model)
cpo.CaFile = model.RepositoryCaFile.ValueString()
cpo.CertFile = model.RepositoryCertFile.ValueString()
cpo.KeyFile = model.RepositoryKeyFile.ValueString()
cpo.Keyring = model.Keyring.ValueString()
cpo.RepoURL = repositoryURL
cpo.Verify = model.Verify.ValueBool()
if !useChartVersion(chartName, cpo.RepoURL) {
cpo.Version = version
}
cpo.Username = model.RepositoryUsername.ValueString()
cpo.Password = model.RepositoryPassword.ValueString()
cpo.PassCredentialsAll = model.PassCredentials.ValueBool()
return cpo, chartName, diags
}
func useChartVersion(chart string, repo string) bool {
// checks if chart is a URL or OCI registry
if _, err := url.ParseRequestURI(chart); err == nil && !registry.IsOCI(chart) {
return true
}
// checks if chart is a local chart
if _, err := os.Stat(chart); err == nil {
return true
}
// checks if repo is a local chart
if _, err := os.Stat(repo); err == nil {
return true
}
return false
}
func buildChartNameWithRepository(repository, name string) (string, string, error) {
_, err := url.ParseRequestURI(repository)
if err == nil {
return repository, name, nil
}
if strings.Index(name, "/") == -1 && repository != "" {
name = fmt.Sprintf("%s/%s", repository, name)
}
return "", name, nil
}
func getVersion(model *HelmReleaseModel) string {
version := model.Version.ValueString()
if version == "" && model.Devel.ValueBool() {
return ">0.0.0-0"
}
return strings.TrimSpace(version)
}
func isChartInstallable(ch *chart.Chart) error {
switch ch.Metadata.Type {
case "", "application":
return nil
}
return errors.Errorf("%s charts are not installable", ch.Metadata.Type)
}
func getChart(ctx context.Context, model *HelmReleaseModel, m *Meta, name string, cpo *action.ChartPathOptions) (*chart.Chart, string, diag.Diagnostics) {
var diags diag.Diagnostics
tflog.Debug(ctx, fmt.Sprintf("Helm settings: %+v", m.Settings))
path, err := cpo.LocateChart(name, m.Settings)
if err != nil {
diags.AddError("Error locating chart", fmt.Sprintf("Unable to locate chart %s: %s", name, err))
return nil, "", diags
}
c, err := loader.Load(path)
if err != nil {
diags.AddError("Error loading chart", fmt.Sprintf("Unable to load chart %s: %s", path, err))
return nil, "", diags
}
return c, path, diags
}
func getWriteOnlyValues(ctx context.Context, model *HelmReleaseModel) (map[string]interface{}, diag.Diagnostics) {
base := map[string]interface{}{}
diags := diag.Diagnostics{}
if !model.SetWO.IsUnknown() && !model.SetWO.IsNull() {
tflog.Debug(ctx, "Processing SetWO attribute")
var setvals []setResourceModel
setDiags := model.SetWO.ElementsAs(ctx, &setvals, false)
diags.Append(setDiags...)
if diags.HasError() {
return nil, diags
}
for _, set := range setvals {
setDiags := getValue(base, set)
diags.Append(setDiags...)
if diags.HasError() {
return nil, diags
}
}
}
return base, diags
}
func getValues(ctx context.Context, model *HelmReleaseModel) (map[string]interface{}, diag.Diagnostics) {
base := map[string]interface{}{}
var diags diag.Diagnostics
// Processing "values" attribute
for _, raw := range model.Values.Elements() {
if raw.IsNull() {
continue
}
value, ok := raw.(types.String)
if !ok {
diags.AddError("Type Error", fmt.Sprintf("Expected types.String, got %T", raw))
return nil, diags
}
values := value.ValueString()
if values == "" {
continue
}
currentMap := map[string]interface{}{}
if err := yaml.Unmarshal([]byte(values), &currentMap); err != nil {
diags.AddError("Error unmarshaling values", fmt.Sprintf("---> %v %s", err, values))
return nil, diags
}
base = mergeMaps(base, currentMap)
}
// Processing "set" attribute
if !model.Set.IsNull() {
tflog.Debug(ctx, "Processing Set attribute")
var setList []setResourceModel
setDiags := model.Set.ElementsAs(ctx, &setList, false)
diags.Append(setDiags...)
if diags.HasError() {
return nil, diags
}
for i, set := range setList {
tflog.Debug(ctx, fmt.Sprintf("Processing Set element at index %d: %v", i, set))
setDiags := getValue(base, set)
diags.Append(setDiags...)
if diags.HasError() {
tflog.Debug(ctx, fmt.Sprintf("Error occurred while processing Set element at index %d", i))
return nil, diags
}
}
}
// Processing "set_list" attribute
if !model.SetList.IsUnknown() {
tflog.Debug(ctx, "Processing Set_list attribute")
var setListSlice []set_listResourceModel
setListDiags := model.SetList.ElementsAs(ctx, &setListSlice, false)
diags.Append(setListDiags...)
if diags.HasError() {
tflog.Debug(ctx, "Error occurred while processing Set_list attribute")
return nil, diags
}
for i, setList := range setListSlice {
tflog.Debug(ctx, fmt.Sprintf("Processing Set_list element at index %d: %v", i, setList))
setListDiags := getListValue(ctx, base, setList)
diags.Append(setListDiags...)
if diags.HasError() {
tflog.Debug(ctx, fmt.Sprintf("Error occurred while processing Set_list element at index %d", i))
return nil, diags
}
}
}
// Processing "set_sensitive" attribute
if !model.SetSensitive.IsNull() {
tflog.Debug(ctx, "Processing Set_Sensitive attribute")
var setSensitiveList []setResourceModel
setSensitiveDiags := model.SetSensitive.ElementsAs(ctx, &setSensitiveList, false)
diags.Append(setSensitiveDiags...)
if diags.HasError() {
tflog.Debug(ctx, "Error occurred while processing Set_Sensitive attribute")
return nil, diags
}
for i, setSensitive := range setSensitiveList {
tflog.Debug(ctx, fmt.Sprintf("Processing Set_Sensitive element at index %d: %v", i, setSensitive))
setSensitiveDiags := getValue(base, setSensitive)
diags.Append(setSensitiveDiags...)
if diags.HasError() {
tflog.Debug(ctx, fmt.Sprintf("Error occurred while processing Set_Sensitive element at index %d", i))
return nil, diags
}
}
}
tflog.Debug(ctx, fmt.Sprintf("Final merged values: %v", base))
logDiags := logValues(ctx, base, model)
diags.Append(logDiags...)
if diags.HasError() {
tflog.Debug(ctx, "Error occurred while logging values")
return nil, diags
}
return base, diags
}
func getValue(base map[string]interface{}, set setResourceModel) diag.Diagnostics {
var diags diag.Diagnostics
name := set.Name.ValueString()
value := set.Value.ValueString()
valueType := set.Type.ValueString()
switch valueType {
case "auto", "":
if err := strvals.ParseInto(fmt.Sprintf("%s=%s", name, value), base); err != nil {
diags.AddError("Failed parsing value", fmt.Sprintf("Failed parsing key %q with value %s: %s", name, value, err))
return diags
}
case "string":
if err := strvals.ParseIntoString(fmt.Sprintf("%s=%s", name, value), base); err != nil {
diags.AddError("Failed parsing string value", fmt.Sprintf("Failed parsing key %q with value %s: %s", name, value, err))
return diags
}
case "literal":
var literal interface{}
if err := yaml.Unmarshal([]byte(fmt.Sprintf("%s: %s", name, value)), &literal); err != nil {
diags.AddError("Failed parsing literal value", fmt.Sprintf("Key %q with literal value %s: %s", name, value, err))
return diags
}
if m, ok := literal.(map[string]interface{}); ok {
base[name] = m[name]
} else {
base[name] = literal
}
default:
diags.AddError("Unexpected type", fmt.Sprintf("Unexpected type: %s", valueType))
return diags
}
return diags
}
// deepCloneMap creates a deep copy of a map[string]interface{}
func deepCloneMap(m map[string]interface{}) map[string]interface{} {
if m == nil {
return nil
}
clone := make(map[string]interface{}, len(m))
for k, v := range m {
switch val := v.(type) {
case map[string]interface{}:
clone[k] = deepCloneMap(val)
default:
clone[k] = v
}
}
return clone
}
func logValues(ctx context.Context, values map[string]interface{}, state *HelmReleaseModel) diag.Diagnostics {
var diags diag.Diagnostics
// Deep cloning values map to avoid modifying the original
c := deepCloneMap(values)
cloakSetValues(c, state)
y, err := yaml.Marshal(c)
if err != nil {
diags.AddError("Error marshaling map to YAML", fmt.Sprintf("Failed to marshal map to YAML: %s", err))
return diags
}
tflog.Debug(ctx, fmt.Sprintf("---[ values.yaml ]-----------------------------------\n%s\n", string(y)))
return diags
}
func cloakSetValues(config map[string]interface{}, state *HelmReleaseModel) {
if !state.SetSensitive.IsNull() {
var setSensitiveList []setResourceModel
diags := state.SetSensitive.ElementsAs(context.Background(), &setSensitiveList, false)
if diags.HasError() {
// Handle diagnostics error
return
}
for _, set := range setSensitiveList {
cloakSetValue(config, set.Name.ValueString())
}
}
}
func getListValue(ctx context.Context, base map[string]interface{}, set set_listResourceModel) diag.Diagnostics {
var diags diag.Diagnostics
name := set.Name.ValueString()
if set.Value.IsNull() {
diags.AddError("Null List Value", "The list value is null.")
return diags
}
// Get the elements of the ListValue
elements := set.Value.Elements()
// Convert elements to a list of strings
listStringArray := make([]string, 0, len(elements))
for _, element := range elements {
if !element.IsNull() {
strValue := element.(types.String).ValueString()
listStringArray = append(listStringArray, strValue)
}
}
// Join the list into a single string
listString := strings.Join(listStringArray, ",")
if err := strvals.ParseInto(fmt.Sprintf("%s={%s}", name, listString), base); err != nil {
diags.AddError("Error parsing list value", fmt.Sprintf("Failed parsing key %q with value %s: %s", name, listString, err))
return diags
}
return diags
}
func versionsEqual(a, b string) bool {
return strings.TrimPrefix(a, "v") == strings.TrimPrefix(b, "v")
}
func setReleaseAttributes(ctx context.Context, state *HelmReleaseModel, identity *tfsdk.ResourceIdentity, r *release.Release, meta *Meta) diag.Diagnostics {
var diags diag.Diagnostics
// Update state with attributes from the helm release
state.Name = types.StringValue(r.Name)
version := r.Chart.Metadata.Version
if !versionsEqual(version, state.Version.ValueString()) {
state.Version = types.StringValue(version)
}
state.Namespace = types.StringValue(r.Namespace)
state.Status = types.StringValue(r.Info.Status.String())
state.ID = types.StringValue(r.Name)
rid := HelmReleaseIdentityModel{
Namespace: types.StringValue(r.Namespace),
ReleaseName: types.StringValue(r.Name),
}
diags = identity.Set(ctx, rid)
if diags.HasError() {
return diags
}
// Cloak sensitive values in the release config
values := "{}"
if r.Config != nil {
// Deep clone the config to avoid modifying the original
configClone := deepCloneMap(r.Config)
cloakSetValues(configClone, state)
v, err := json.Marshal(configClone)
if err != nil {
diags.AddError(
"Error marshaling values",
fmt.Sprintf("unable to marshal values: %s", err),
)
return diags
}
values = string(v)
}
// Handling the helm release if manifest experiment is enabled
if meta.ExperimentEnabled("manifest") {
jsonManifest, err := convertYAMLManifestToJSON(r.Manifest)
if err != nil {
diags.AddError(
"Error converting manifest to JSON",
fmt.Sprintf("Unable to convert manifest to JSON: %s", err),
)
return diags
}
sensitiveValues := extractSensitiveValues(state)
manifest := redactSensitiveValues(string(jsonManifest), sensitiveValues)
state.Manifest = types.StringValue(manifest)
}
// NOTE Don't retrieve values if write-only is being used.
// It is not possible to pick out which values are write-only
// at read time because write-only values are ephemeral
valuesstr := types.StringValue("{}")
if state.SetWORevision.ValueInt64() <= 0 {
valuesstr = types.StringValue(values)
}
// Create metadata as a slice of maps
metadata := map[string]attr.Value{
"name": types.StringValue(r.Name),
"revision": types.Int64Value(int64(r.Version)),
"namespace": types.StringValue(r.Namespace),
"chart": types.StringValue(r.Chart.Metadata.Name),
"version": types.StringValue(r.Chart.Metadata.Version),
"app_version": types.StringValue(r.Chart.Metadata.AppVersion),
"values": valuesstr,
"first_deployed": types.Int64Value(r.Info.FirstDeployed.Unix()),
"last_deployed": types.Int64Value(r.Info.LastDeployed.Unix()),
"notes": types.StringValue(r.Info.Notes),
}
// Convert the list of ObjectValues to a ListValue
metadataObject, diag := types.ObjectValue(metadataAttrTypes(), metadata)
diags.Append(diag...)
if diags.HasError() {
tflog.Error(ctx, "Error converting metadata to ListValue", map[string]interface{}{
"metadata": metadata,
"error": diags,
})
return diags
}
// Log metadata after conversion
tflog.Debug(ctx, fmt.Sprintf("Metadata after conversion: %+v", metadataObject))
state.Metadata = metadataObject
return diags
}
func metadataAttrTypes() map[string]attr.Type {
return map[string]attr.Type{
"name": types.StringType,
"revision": types.Int64Type,
"namespace": types.StringType,
"chart": types.StringType,
"version": types.StringType,
"app_version": types.StringType,
"values": types.StringType,
"first_deployed": types.Int64Type,
"last_deployed": types.Int64Type,
"notes": types.StringType,
}
}
func extractSensitiveValues(state *HelmReleaseModel) map[string]string {
sensitiveValues := make(map[string]string)
if !state.SetSensitive.IsNull() {
var setSensitiveList []setResourceModel
diags := state.SetSensitive.ElementsAs(context.Background(), &setSensitiveList, false)
if diags.HasError() {
return sensitiveValues
}
for _, set := range setSensitiveList {
sensitiveValues[set.Name.ValueString()] = "(sensitive value)"
}
}
return sensitiveValues
}
func (m *Meta) ExperimentEnabled(name string) bool {
if enabled, exists := m.Experiments[name]; exists {
return enabled
}
return false
}
// c
func resourceReleaseExists(ctx context.Context, name, namespace string, meta *Meta) (bool, diag.Diagnostics) {
logID := fmt.Sprintf("[resourceReleaseExists: %s]", name)
tflog.Debug(ctx, fmt.Sprintf("%s Start", logID))
var diags diag.Diagnostics
c, err := meta.GetHelmConfiguration(ctx, namespace)
if err != nil {
diags.AddError(
"Error getting helm configuration",
fmt.Sprintf("Unable to get Helm configuration for namespace %s: %s", namespace, err),
)
return false, diags
}
_, err = getRelease(ctx, meta, c, name)
tflog.Debug(ctx, fmt.Sprintf("%s Done", logID))
if err == nil {
return true, diags
}
if err == errReleaseNotFound {
return false, diags
}
diags.AddError(
"Error checking release existence",
fmt.Sprintf("Error checking release %s in namespace %s: %s", name, namespace, err),
)
return false, diags
}
var errReleaseNotFound = fmt.Errorf("release: not found")
// c
func getRelease(ctx context.Context, m *Meta, cfg *action.Configuration, name string) (*release.Release, error) {
get := action.NewGet(cfg)
tflog.Debug(ctx, fmt.Sprintf("%s getRelease post action created", name))
res, err := get.Run(name)
tflog.Debug(ctx, fmt.Sprintf("%s getRelease post run", name))
if err != nil {
tflog.Debug(ctx, fmt.Sprintf("getRelease for %s occurred", name))
tflog.Debug(ctx, fmt.Sprintf("%v", err))
if strings.Contains(err.Error(), "release: not found") {
tflog.Error(ctx, errReleaseNotFound.Error())
return nil, errReleaseNotFound
}
tflog.Debug(ctx, fmt.Sprintf("Could not get release %s", err))
tflog.Error(ctx, err.Error())
return nil, err
}
tflog.Debug(ctx, fmt.Sprintf("%s getRelease completed", name))
return res, nil
}
// c
func checkChartDependencies(ctx context.Context, model *HelmReleaseModel, c *chart.Chart, path string, m *Meta) (bool, diag.Diagnostics) {
var diags diag.Diagnostics
p := getter.All(m.Settings)
if req := c.Metadata.Dependencies; req != nil {
err := action.CheckDependencies(c, req)
if err != nil {
if model.DependencyUpdate.ValueBool() {
man := &downloader.Manager{
Out: os.Stdout,
ChartPath: path,
Keyring: model.Keyring.ValueString(),
SkipUpdate: false,
Getters: p,
RepositoryConfig: m.Settings.RepositoryConfig,
RepositoryCache: m.Settings.RepositoryCache,
Debug: m.Settings.Debug,
}
tflog.Debug(ctx, "Downloading chart dependencies...")
if err := man.Update(); err != nil {
diags.AddError("", fmt.Sprintf("Failed to update chart dependencies: %s", err))
return true, diags
}
return true, diags
}
diags.AddError("", "Found in Chart.yaml, but missing in charts/ directory")
return false, diags
}
}
tflog.Debug(ctx, "Chart dependencies are up to date.")
return false, diags
}
func (r *HelmRelease) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) {
if req.Plan.Raw.IsNull() {
// resource is being destroyed
return
}
var plan HelmReleaseModel
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
if resp.Diagnostics.HasError() {
return
}
var state *HelmReleaseModel
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
if resp.Diagnostics.HasError() {
return
}
var config HelmReleaseModel
resp.Diagnostics.Append(req.Config.Get(ctx, &config)...)
if resp.Diagnostics.HasError() {
return
}
tflog.Debug(ctx, fmt.Sprintf("Plan state on ModifyPlan: %+v", plan))
tflog.Debug(ctx, fmt.Sprintf("Actual state on ModifyPlan: %+v", state))
logID := fmt.Sprintf("[resourceDiff: %s]", plan.Name.ValueString())
tflog.Debug(ctx, fmt.Sprintf("%s Start", logID))
meta := r.meta
name := plan.Name.ValueString()
namespace := plan.Namespace.ValueString()
actionConfig, err := meta.GetHelmConfiguration(ctx, namespace)
if err != nil {
resp.Diagnostics.AddError("Error getting Helm configuration", err.Error())
return
}
tflog.Debug(ctx, fmt.Sprintf("%s Initial Values: Name=%s, Namespace=%s, Repository=%s, Repository_Username=%s, Repository_Password=%s, Chart=%s", logID,
name, namespace, plan.Repository.ValueString(), plan.RepositoryUsername.ValueString(), plan.RepositoryPassword.ValueString(), plan.Chart.ValueString()))
repositoryURL := plan.Repository.ValueString()
repositoryUsername := plan.RepositoryUsername.ValueString()
repositoryPassword := plan.RepositoryPassword.ValueString()
chartName := plan.Chart.ValueString()
ociDiags := OCIRegistryLogin(ctx, meta, actionConfig, meta.RegistryClient, repositoryURL, chartName, repositoryUsername, repositoryPassword)
resp.Diagnostics.Append(ociDiags...)
if resp.Diagnostics.HasError() {
return
}
// Always set desired state to DEPLOYED
plan.Status = types.StringValue(release.StatusDeployed.String())
if recomputeMetadata(plan, state) {
tflog.Debug(ctx, fmt.Sprintf("%s Metadata has changes, setting to unknown", logID))
plan.Metadata = types.ObjectUnknown(metadataAttrTypes())
}
if !useChartVersion(plan.Chart.ValueString(), plan.Repository.ValueString()) {
// Check if version has changed
if state != nil && !plan.Version.Equal(state.Version) {
// Ensure trimming 'v' prefix correctly
oldVersionStr := strings.TrimPrefix(state.Version.String(), "v")
newVersionStr := strings.TrimPrefix(plan.Version.String(), "v")
if oldVersionStr != newVersionStr && newVersionStr != "" {
// Setting Metadata to a computed value
plan.Metadata = types.ObjectUnknown(metadataAttrTypes())
}
}
}
client := action.NewInstall(actionConfig)
cpo, chartName, diags := chartPathOptions(&plan, meta, &client.ChartPathOptions)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
chart, path, diags := getChart(ctx, &plan, meta, chartName, cpo)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
tflog.Debug(ctx, fmt.Sprintf("%s Got chart", logID))
updated, diags := checkChartDependencies(ctx, &plan, chart, path, meta)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
} else if updated {
chart, err = loader.Load(path)
if err != nil {
resp.Diagnostics.AddError("Error loading chart", err.Error())
return
}
}
if plan.Lint.ValueBool() {
diags := resourceReleaseValidate(ctx, &plan, meta, cpo)
if diags.HasError() {
resp.Diagnostics.Append(diags...)
return
}
}
tflog.Debug(ctx, fmt.Sprintf("%s Release validated", logID))
if meta.ExperimentEnabled("manifest") {
// Check if all necessary values are known
if valuesUnknown(plan) {
tflog.Debug(ctx, "not all values are known, skipping dry run to render manifest")
plan.Manifest = types.StringNull()
plan.Version = types.StringNull()
return
}
if plan.PostRender != nil {
binaryPath := plan.PostRender.BinaryPath.ValueString()
argsList := plan.PostRender.Args.Elements()
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("Error creating post-renderer", fmt.Sprintf("Could not create post-renderer: %s", err))
return
}
client.PostRenderer = pr
}
if state == nil {
install := action.NewInstall(actionConfig)
install.ChartPathOptions = *cpo
install.DryRun = true
install.DisableHooks = plan.DisableWebhooks.ValueBool()
install.Wait = plan.Wait.ValueBool()
install.WaitForJobs = plan.WaitForJobs.ValueBool()
install.Devel = plan.Devel.ValueBool()
install.DependencyUpdate = plan.DependencyUpdate.ValueBool()
install.Timeout = time.Duration(plan.Timeout.ValueInt64()) * time.Second
install.Namespace = plan.Namespace.ValueString()
install.ReleaseName = plan.Name.ValueString()
install.Atomic = plan.Atomic.ValueBool()
install.SkipCRDs = plan.SkipCrds.ValueBool()
install.SubNotes = plan.RenderSubchartNotes.ValueBool()
install.DisableOpenAPIValidation = plan.DisableOpenapiValidation.ValueBool()
install.Replace = plan.Replace.ValueBool()
install.Description = plan.Description.ValueString()
install.CreateNamespace = plan.CreateNamespace.ValueBool()
install.PostRenderer = client.PostRenderer
values, diags := getValues(ctx, &plan)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
tflog.Debug(ctx, fmt.Sprintf("%s performing dry run install", logID))
dry, err := install.Run(chart, values)
if err != nil {
// NOTE if the cluster is not reachable then we can't run the install
// this will happen if the user has their cluster creation in the
// same apply. We are catching this case here and marking manifest
// as computed to avoid breaking existing configs
if strings.Contains(err.Error(), "Kubernetes cluster unreachable") {
resp.Diagnostics.AddError("cluster was unreachable at create time, marking manifest as computed", err.Error())
plan.Manifest = types.StringNull()
return
}
resp.Diagnostics.AddError("Error performing dry run install", err.Error())
return
}
jsonManifest, err := convertYAMLManifestToJSON(dry.Manifest)
if err != nil {
resp.Diagnostics.AddError("Error converting YAML manifest to JSON", err.Error())
return
}
valuesMap := make(map[string]string)
if !plan.SetSensitive.IsNull() {
var setSensitiveList []setResourceModel
setSensitiveDiags := plan.SetSensitive.ElementsAs(ctx, &setSensitiveList, false)
resp.Diagnostics.Append(setSensitiveDiags...)
if resp.Diagnostics.HasError() {
return
}
for _, set := range setSensitiveList {
valuesMap[set.Name.ValueString()] = set.Value.ValueString()
}
}
manifest := redactSensitiveValues(string(jsonManifest), valuesMap)
plan.Manifest = types.StringValue(manifest)
return
}
_, err = getRelease(ctx, meta, actionConfig, name)
if err == errReleaseNotFound {
if len(chart.Metadata.Version) > 0 {
plan.Version = types.StringValue(chart.Metadata.Version)
}
plan.Manifest = types.StringNull()
return
} else if err != nil {
resp.Diagnostics.AddError("Error retrieving old release for a diff", err.Error())
return
}
upgrade := action.NewUpgrade(actionConfig)
upgrade.ChartPathOptions = *cpo
upgrade.Devel = plan.Devel.ValueBool()
upgrade.Namespace = plan.Namespace.ValueString()
upgrade.Timeout = time.Duration(plan.Timeout.ValueInt64()) * time.Second
upgrade.Wait = plan.Wait.ValueBool()
upgrade.DryRun = true
upgrade.DisableHooks = plan.DisableWebhooks.ValueBool()
upgrade.Atomic = plan.Atomic.ValueBool()
upgrade.SubNotes = plan.RenderSubchartNotes.ValueBool()
upgrade.WaitForJobs = plan.WaitForJobs.ValueBool()
upgrade.Force = plan.ForceUpdate.ValueBool()
upgrade.ResetValues = plan.ResetValues.ValueBool()
upgrade.ReuseValues = plan.ReuseValues.ValueBool()
upgrade.Recreate = plan.RecreatePods.ValueBool()
upgrade.MaxHistory = int(plan.MaxHistory.ValueInt64())
upgrade.CleanupOnFail = plan.CleanupOnFail.ValueBool()
upgrade.Description = plan.Description.ValueString()
upgrade.PostRenderer = client.PostRenderer
values, diags := getValues(ctx, &plan)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
tflog.Debug(ctx, fmt.Sprintf("%s performing dry run upgrade", logID))
dry, err := upgrade.Run(name, chart, values)
if err != nil && strings.Contains(err.Error(), "has no deployed releases") {
if len(chart.Metadata.Version) > 0 && cpo.Version != "" {
plan.Version = types.StringValue(chart.Metadata.Version)
}
plan.Version = types.StringNull()
plan.Manifest = types.StringNull()
return
} else if err != nil {
resp.Diagnostics.AddError("Error running dry run for a diff", err.Error())
return
}
jsonManifest, err := convertYAMLManifestToJSON(dry.Manifest)
if err != nil {
resp.Diagnostics.AddError("Error converting YAML manifest to JSON", err.Error())
return
}
valuesMap := make(map[string]string)
if !plan.SetSensitive.IsNull() {
var setSensitiveList []setResourceModel
setSensitiveDiags := plan.SetSensitive.ElementsAs(ctx, &setSensitiveList, false)
resp.Diagnostics.Append(setSensitiveDiags...)
if resp.Diagnostics.HasError() {
return
}
for _, set := range setSensitiveList {
valuesMap[set.Name.ValueString()] = set.Value.ValueString()
}
}
manifest := redactSensitiveValues(string(jsonManifest), valuesMap)
plan.Manifest = types.StringValue(manifest)
tflog.Debug(ctx, fmt.Sprintf("%s set manifest: %s", logID, jsonManifest))
} else {
plan.Manifest = types.StringNull()
}
tflog.Debug(ctx, fmt.Sprintf("%s Done", logID))
if len(chart.Metadata.Version) > 0 {
plan.Version = types.StringValue(chart.Metadata.Version)
} else {
plan.Version = types.StringNull()
}
if !config.Version.IsNull() && !config.Version.Equal(plan.Version) {
if versionsEqual(config.Version.ValueString(), plan.Version.ValueString()) {
plan.Version = config.Version
} else {
resp.Diagnostics.AddError(
"Planned version is different from configured version",
fmt.Sprintf(`The version in the configuration is %q but the planned version is %q.
You should update the version in your configuration to %[2]q, or remove the version attribute from your configuration.`, config.Version.ValueString(), plan.Version.ValueString()))
return
}
}
resp.Plan.Set(ctx, &plan)
}
// TODO: write unit test, always returns true for recomputing the metadata
// returns true if any metadata fields have changed
func recomputeMetadata(plan HelmReleaseModel, state *HelmReleaseModel) bool {
if state == nil {
return true
}
if !plan.Chart.Equal(state.Chart) {
return true
}
if !plan.Repository.Equal(state.Repository) {
return true
}
if !plan.Values.Equal(state.Values) {
return true
}
if !plan.Set.Equal(state.Set) {
return true
}
if !plan.SetSensitive.Equal(state.SetSensitive) {
return true
}
if !plan.SetList.Equal(state.SetList) {
return true
}
return false
}
func resourceReleaseValidate(ctx context.Context, model *HelmReleaseModel, meta *Meta, cpo *action.ChartPathOptions) diag.Diagnostics {
var diags diag.Diagnostics
cpo, name, chartDiags := chartPathOptions(model, meta, cpo)
diags.Append(chartDiags...)
if diags.HasError() {
diags.AddError("Malformed values", fmt.Sprintf("Chart path options error: %s", chartDiags))
return diags
}
values, valuesDiags := getValues(ctx, model)
diags.Append(valuesDiags...)
if diags.HasError() {
return diags
}
lintDiags := lintChart(meta, name, cpo, values)
if lintDiags != nil {
diagnostic := diag.NewErrorDiagnostic("Lint Error", lintDiags.Error())
diags = append(diags, diagnostic)
}
return diags
}
func lintChart(m *Meta, name string, cpo *action.ChartPathOptions, values map[string]interface{}) error {
path, err := cpo.LocateChart(name, m.Settings)
if err != nil {
return err
}
l := action.NewLint()
result := l.Run([]string{path}, values)
return resultToError(result)
}
func resultToError(r *action.LintResult) error {
if len(r.Errors) == 0 {
return nil
}
messages := []string{}
for _, msg := range r.Messages {
for _, err := range r.Errors {
if err == msg.Err {
messages = append(messages, fmt.Sprintf("%s: %s", msg.Path, msg.Err))
break
}
}
}
return fmt.Errorf("malformed chart or values: \n\t%s", strings.Join(messages, "\n\t"))
}
func (r *HelmRelease) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
var namespace, name string
if req.ID != "" {
tflog.Debug(ctx, "Using ID string for import")
var err error
namespace, name, err = parseImportIdentifier(req.ID)
if err != nil {
resp.Diagnostics.AddError(
"Unable to parse import identifier",
fmt.Sprintf("Unable to parse identifier %s: %s", req.ID, err),
)
return
}
} else {
tflog.Debug(ctx, "Using Resource Identity for Import")
rid := HelmReleaseIdentityModel{
Namespace: types.StringValue("default"),
}
diags := req.Identity.Get(ctx, &rid)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
namespace = rid.Namespace.ValueString()
name = rid.ReleaseName.ValueString()
}
meta := r.meta
if meta == nil {
resp.Diagnostics.AddError(
"Meta not set",
"The meta information is not set for the resource",
)
return
}
actionConfig, err := meta.GetHelmConfiguration(ctx, namespace)
if err != nil {
resp.Diagnostics.AddError(
"Error getting helm configuration",
fmt.Sprintf("Unable to get Helm configuration for namespace %s: %s", namespace, err),
)
return
}
release, err := getRelease(ctx, meta, actionConfig, name)
if err != nil {
resp.Diagnostics.AddError(
"Error getting release",
fmt.Sprintf("Unable to get Helm release %s: %s", name, err.Error()),
)
return
}
var state HelmReleaseModel
state.Name = types.StringValue(release.Name)
state.Description = types.StringValue(release.Info.Description)
state.Chart = types.StringValue(release.Chart.Metadata.Name)
// Set release-specific attributes using the helper function
diags := setReleaseAttributes(ctx, &state, resp.Identity, release, meta)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
return
}
state.Set = types.ListNull(types.ObjectType{
AttrTypes: map[string]attr.Type{
"name": types.StringType,
"type": types.StringType,
"value": types.StringType,
},
})
state.SetWO = types.ListNull(types.ObjectType{
AttrTypes: map[string]attr.Type{
"name": types.StringType,
"type": types.StringType,
"value": types.StringType,
},
})
state.SetSensitive = types.ListNull(types.ObjectType{
AttrTypes: map[string]attr.Type{
"name": types.StringType,
"type": types.StringType,
"value": types.StringType,
},
})
state.SetList = types.ListNull(types.ObjectType{
AttrTypes: map[string]attr.Type{
"name": types.StringType,
"value": types.ListType{
ElemType: types.StringType,
},
},
})
state.Values = types.ListNull(types.StringType)
tflog.Debug(ctx, fmt.Sprintf("Setting final state: %+v", state))
diags = resp.State.Set(ctx, &state)
resp.Diagnostics.Append(diags...)
if diags.HasError() {
fmt.Println("DOH")
tflog.Error(ctx, "Error setting final state", map[string]interface{}{
"state": state,
"diagnostics": diags,
})
return
}
// Set default attributes
for key, value := range defaultAttributes {
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root(key), value)...)
if resp.Diagnostics.HasError() {
return
}
}
}
func parseImportIdentifier(id string) (string, string, error) {
parts := strings.Split(id, "/")
if len(parts) != 2 {
err := errors.Errorf("Unexpected ID format (%q), expected namespace/name", id)
return "", "", err
}
return parts[0], parts[1], nil
}
// returns true if any values, set_list, set, set_sensitive are unknown
func valuesUnknown(plan HelmReleaseModel) bool {
if plan.Values.IsUnknown() {
return true
}
if plan.SetList.IsUnknown() {
return true
}
if plan.Set.IsUnknown() {
return true
}
if plan.SetSensitive.IsUnknown() {
return true
}
return false
}