Skip to content

Set attribute defaults conditionally #259

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

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
3 changes: 3 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ jobs:
INCUS_REMOTE: local
INCUS_ADDR: localhost
INCUS_PORT: 8443
INCUS_STORAGE_BUCKETS_PORT: 8444
INCUS_GENERATE_CLIENT_CERTS: "true"
INCUS_ACCEPT_SERVER_CERTIFICATE: "true"
INCUS_SCHEME: https
Expand Down Expand Up @@ -63,6 +64,8 @@ jobs:

incus remote add docker https://docker.io --protocol=oci

incus config set core.storage_buckets_address="$INCUS_ADDR:$INCUS_STORAGE_BUCKETS_PORT"

- name: Configure OVN
run: |
sudo apt-add-repository ppa:stgraber/ovn-stable --yes
Expand Down
142 changes: 142 additions & 0 deletions internal/common/planmodifiers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
package common

import (
"context"
"fmt"

"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/schema/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
"github.com/hashicorp/terraform-plugin-framework/types"
)

func SetDefaultStringIfAllUndefined(defaultValue types.String, expression ...path.Expression) planmodifier.String {
return setDefaultStringIfAllUndefinedModifier{
defaultValue: defaultValue,
expressions: expression,
}
}

func SetDefaultMapIfAllUndefined(defaultValue types.Map, expression ...path.Expression) planmodifier.Map {
return setDefaultMapIfAllUndefinedModifier{
defaultValue: defaultValue,
expressions: expression,
}
}

type setDefaultStringIfAllUndefinedModifier struct {
defaultValue types.String
expressions path.Expressions
}

func (m setDefaultStringIfAllUndefinedModifier) Description(ctx context.Context) string {
return fmt.Sprintf("Sets the default value %q only if none the following are set: %q", m.defaultValue.String(), m.expressions)
}

func (m setDefaultStringIfAllUndefinedModifier) MarkdownDescription(ctx context.Context) string {
return m.Description(ctx)
}

func (m setDefaultStringIfAllUndefinedModifier) PlanModifyString(ctx context.Context, req planmodifier.StringRequest, resp *planmodifier.StringResponse) {
if !req.ConfigValue.IsNull() && !req.ConfigValue.IsUnknown() {
return
}

expressions := req.PathExpression.MergeExpressions(m.expressions...)
allUndefinedReq := allUndefinedRequest{
expressions: expressions,
config: req.Config,
}
allUndefinedResp := &allUndefinedResponse{}

allUndefined(ctx, allUndefinedReq, allUndefinedResp)

resp.Diagnostics.Append(allUndefinedResp.diagnostics...)
if resp.Diagnostics.HasError() {
return
}

if allUndefinedResp.allUndefined {
resp.PlanValue = m.defaultValue
}
}

type setDefaultMapIfAllUndefinedModifier struct {
defaultValue types.Map
expressions path.Expressions
}

func (m setDefaultMapIfAllUndefinedModifier) Description(ctx context.Context) string {
return fmt.Sprintf("Sets the default value %q only if none the following are set: %q", m.defaultValue.String(), m.expressions)
}

func (m setDefaultMapIfAllUndefinedModifier) MarkdownDescription(ctx context.Context) string {
return m.Description(ctx)
}

func (m setDefaultMapIfAllUndefinedModifier) PlanModifyMap(ctx context.Context, req planmodifier.MapRequest, resp *planmodifier.MapResponse) {
if !req.ConfigValue.IsNull() && !req.ConfigValue.IsUnknown() {
return
}

expressions := req.PathExpression.MergeExpressions(m.expressions...)
allUndefinedReq := allUndefinedRequest{
expressions: expressions,
config: req.Config,
}
allUndefinedResp := &allUndefinedResponse{}

allUndefined(ctx, allUndefinedReq, allUndefinedResp)

resp.Diagnostics.Append(allUndefinedResp.diagnostics...)
if resp.Diagnostics.HasError() {
return
}

if allUndefinedResp.allUndefined {
resp.PlanValue = m.defaultValue
}
}

type allUndefinedRequest struct {
expressions path.Expressions
config tfsdk.Config
}

type allUndefinedResponse struct {
diagnostics diag.Diagnostics
allUndefined bool
}

func allUndefined(ctx context.Context, req allUndefinedRequest, resp *allUndefinedResponse) {
resp.allUndefined = true
for _, expression := range req.expressions {
matchedPaths, diags := req.config.PathMatches(ctx, expression)

resp.diagnostics.Append(diags...)

if diags.HasError() {
continue
}

for _, mp := range matchedPaths {
var mpVal attr.Value
diags := req.config.GetAttribute(ctx, mp, &mpVal)
resp.diagnostics.Append(diags...)

if diags.HasError() {
continue
}

if mpVal.IsUnknown() {
continue
}

if !mpVal.IsNull() {
resp.allUndefined = false
}
}
}
}
80 changes: 53 additions & 27 deletions internal/instance/resource_instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"strings"
"time"

"github.com/hashicorp/terraform-plugin-framework-validators/boolvalidator"
"github.com/hashicorp/terraform-plugin-framework-validators/listvalidator"
"github.com/hashicorp/terraform-plugin-framework-validators/mapvalidator"
"github.com/hashicorp/terraform-plugin-framework-validators/objectvalidator"
Expand All @@ -19,10 +20,8 @@ import (
"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/boolplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/mapdefault"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/objectplanmodifier"
"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"
Expand Down Expand Up @@ -129,18 +128,38 @@ func (r InstanceResource) Schema(_ context.Context, _ resource.SchemaRequest, re
"description": schema.StringAttribute{
Optional: true,
Computed: true,
Default: stringdefault.StaticString(""),
PlanModifiers: []planmodifier.String{
common.SetDefaultStringIfAllUndefined(
types.StringValue(""),
path.MatchRoot("source_instance"),
path.MatchRoot("source_file"),
),
},
Validators: []validator.String{
stringvalidator.ConflictsWith(
path.MatchRoot("source_instance"),
path.MatchRoot("source_file"),
),
},
},

"type": schema.StringAttribute{
Optional: true,
Computed: true,
Default: stringdefault.StaticString("container"),
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
common.SetDefaultStringIfAllUndefined(
types.StringValue("container"),
path.MatchRoot("source_instance"),
path.MatchRoot("source_file"),
),
},
Validators: []validator.String{
stringvalidator.OneOf("container", "virtual-machine"),
stringvalidator.ConflictsWith(
path.MatchRoot("source_instance"),
path.MatchRoot("source_file"),
),
},
},

Expand All @@ -151,11 +170,9 @@ func (r InstanceResource) Schema(_ context.Context, _ resource.SchemaRequest, re
},
Validators: []validator.String{
stringvalidator.ConflictsWith(
path.Expressions{
path.MatchRoot("source_instance"),
path.MatchRoot("source_file"),
path.MatchRoot("architecture"),
}...,
path.MatchRoot("source_instance"),
path.MatchRoot("source_file"),
path.MatchRoot("architecture"),
),
},
},
Expand All @@ -167,6 +184,12 @@ func (r InstanceResource) Schema(_ context.Context, _ resource.SchemaRequest, re
PlanModifiers: []planmodifier.Bool{
boolplanmodifier.RequiresReplace(),
},
Validators: []validator.Bool{
boolvalidator.ConflictsWith(
path.MatchRoot("source_file"),
path.MatchRoot("source_instance"),
),
},
},

"running": schema.BoolAttribute{
Expand All @@ -184,6 +207,10 @@ func (r InstanceResource) Schema(_ context.Context, _ resource.SchemaRequest, re
Validators: []validator.List{
// Prevent empty values.
listvalidator.ValueStringsAre(stringvalidator.LengthAtLeast(1)),
listvalidator.ConflictsWith(
path.MatchRoot("source_file"),
path.MatchRoot("source_instance"),
),
},
},

Expand Down Expand Up @@ -219,9 +246,19 @@ func (r InstanceResource) Schema(_ context.Context, _ resource.SchemaRequest, re
Optional: true,
Computed: true,
ElementType: types.StringType,
Default: mapdefault.StaticValue(types.MapValueMust(types.StringType, map[string]attr.Value{})),
PlanModifiers: []planmodifier.Map{
common.SetDefaultMapIfAllUndefined(
types.MapValueMust(types.StringType, map[string]attr.Value{}),
path.MatchRoot("source_instance"),
path.MatchRoot("source_file"),
),
},
Validators: []validator.Map{
mapvalidator.KeysAre(configKeyValidator{}),
mapvalidator.ConflictsWith(
path.MatchRoot("source_file"),
path.MatchRoot("source_instance"),
),
},
},

Expand All @@ -242,12 +279,7 @@ func (r InstanceResource) Schema(_ context.Context, _ resource.SchemaRequest, re
objectplanmodifier.RequiresReplace(),
},
Validators: []validator.Object{
objectvalidator.ConflictsWith(
path.Expressions{
path.MatchRoot("source_file"),
path.MatchRoot("architecture"),
}...,
),
objectvalidator.ConflictsWith(path.MatchRoot("source_file")),
},
},

Expand All @@ -258,17 +290,7 @@ func (r InstanceResource) Schema(_ context.Context, _ resource.SchemaRequest, re
},
Validators: []validator.String{
stringvalidator.LengthAtLeast(1),
stringvalidator.ConflictsWith(
path.Expressions{
path.MatchRoot("description"),
path.MatchRoot("type"),
path.MatchRoot("ephemeral"),
path.MatchRoot("profiles"),
path.MatchRoot("file"),
path.MatchRoot("config"),
path.MatchRoot("architecture"),
}...,
),
stringvalidator.ConflictsWith(path.MatchRoot("source_instance")),
},
},

Expand All @@ -281,6 +303,10 @@ func (r InstanceResource) Schema(_ context.Context, _ resource.SchemaRequest, re
},
Validators: []validator.String{
common.ArchitectureValidator{},
stringvalidator.ConflictsWith(
path.MatchRoot("source_file"),
path.MatchRoot("source_instance"),
),
},
},

Expand Down
Loading