Skip to content

Commit

Permalink
Merge pull request #492 from MusicDin/cp/null-config
Browse files Browse the repository at this point in the history
Support null values in config - from Incus
  • Loading branch information
simondeziel authored Jul 8, 2024
2 parents c8d7eca + be88006 commit f7f0752
Show file tree
Hide file tree
Showing 11 changed files with 121 additions and 40 deletions.
76 changes: 67 additions & 9 deletions internal/common/lxd_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,61 @@ func ToConfigMap(ctx context.Context, configMap types.Map) (map[string]string, d
return make(map[string]string), nil
}

config := make(map[string]string, len(configMap.Elements()))
diags := configMap.ElementsAs(ctx, &config, false)
return config, diags
// Convert to an intermediate nullable type.
tfConfig := make(map[string]*string, len(configMap.Elements()))
diags := configMap.ElementsAs(ctx, &tfConfig, false)
if diags != nil {
return nil, diags
}

// Then convert to our native type.
config := make(map[string]string, len(tfConfig))
for k, v := range tfConfig {
if v == nil {
continue
}

config[k] = *v
}

return config, nil
}

// ToConfigMapType converts map[string]string into config of type types.Map.
func ToConfigMapType(ctx context.Context, config map[string]string) (types.Map, diag.Diagnostics) {
func ToConfigMapType(ctx context.Context, config map[string]*string, modelConfig types.Map) (types.Map, diag.Diagnostics) {
// Add any missing nil values.
nullConfig := map[string]*string{}
if !modelConfig.IsNull() && !modelConfig.IsUnknown() {
_ = modelConfig.ElementsAs(context.Background(), &nullConfig, false)
}

for k, v := range nullConfig {
if v != nil {
continue
}

_, ok := config[k]
if !ok {
config[k] = nil
}
}

return types.MapValueFrom(ctx, types.StringType, config)
}

// ToNullableConfig converts map[string]string to map[string]*string.
func ToNullableConfig(config map[string]string) map[string]*string {
nullConfig := make(map[string]*string, len(config))

for k := range config {
// Copy the value.
v := string(config[k])
nullConfig[k] = &v
}

return nullConfig
}

// MergeConfig merges resource (existing) configuration with user defined
// configuration. Non-empty resource config entries that are contained in
// the provided computed keys are inserted in the user config.
Expand Down Expand Up @@ -55,14 +100,20 @@ func MergeConfig(resConfig map[string]string, usrConfig map[string]string, compu
// file in order to be able to produce a consistent Terraform plan. If there
// is a non-computed-key entry, it will be retained in the configuration and
// will trigger an error.
func StripConfig(resConfig map[string]string, usrConfig map[string]string, computedKeys []string) map[string]string {
config := make(map[string]string)
func StripConfig(resConfig map[string]string, modelConfig types.Map, computedKeys []string) map[string]*string {
// Handle nulls in modelConfig.
usrConfig := map[string]*string{}
if !modelConfig.IsNull() && !modelConfig.IsUnknown() {
_ = modelConfig.ElementsAs(context.Background(), &usrConfig, false)
}

// Populate empty values from user config, so they do not "disappear"
// from the state.
config := make(map[string]*string)

for k, v := range usrConfig {
if v == "" {
config[k] = v
if v == nil {
config[k] = nil
}
}

Expand All @@ -76,7 +127,14 @@ func StripConfig(resConfig map[string]string, usrConfig map[string]string, compu

_, ok := usrConfig[k]
if ok || !isComputedKey(k, computedKeys) {
config[k] = v
if usrConfig[k] == nil && isComputedKey(k, computedKeys) {
// Keep as null.
config[k] = nil
} else {
// Copy the value.
v := string(resConfig[k])
config[k] = &v
}
}
}

Expand Down
11 changes: 4 additions & 7 deletions internal/instance/resource_instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -1112,26 +1112,23 @@ func (r InstanceResource) SyncState(ctx context.Context, tfState *tfsdk.State, s
}

// Extract user defined config and merge it with current resource config.
usrConfig, diags := common.ToConfigMap(ctx, m.Config)
respDiags.Append(diags...)

stateConfig := common.StripConfig(instance.Config, usrConfig, m.ComputedKeys())
stateConfig := common.StripConfig(instance.Config, m.Config, m.ComputedKeys())

// Extract enteries with "limits." prefix.
instanceLimits := make(map[string]string)
for k, v := range stateConfig {
key, ok := strings.CutPrefix(k, "limits.")
if ok {
instanceLimits[key] = v
instanceLimits[key] = *v
delete(stateConfig, k)
}
}

// Convert config, limits, profiles, and devices into schema type.
config, diags := common.ToConfigMapType(ctx, stateConfig)
config, diags := common.ToConfigMapType(ctx, stateConfig, m.Config)
respDiags.Append(diags...)

limits, diags := common.ToConfigMapType(ctx, instanceLimits)
limits, diags := common.ToConfigMapType(ctx, common.ToNullableConfig(instanceLimits), m.Config)
respDiags.Append(diags...)

profiles, diags := ToProfileListType(ctx, instance.Profiles)
Expand Down
7 changes: 2 additions & 5 deletions internal/network/resource_network.go
Original file line number Diff line number Diff line change
Expand Up @@ -323,13 +323,10 @@ func (r NetworkResource) SyncState(ctx context.Context, tfState *tfsdk.State, se
}

// Extract user defined config and merge it with current config state.
usrConfig, diags := common.ToConfigMap(ctx, m.Config)
respDiags.Append(diags...)

stateConfig := common.StripConfig(network.Config, usrConfig, m.ComputedKeys())
stateConfig := common.StripConfig(network.Config, m.Config, m.ComputedKeys())

// Convert config state into schema type.
config, diags := common.ToConfigMapType(ctx, stateConfig)
config, diags := common.ToConfigMapType(ctx, stateConfig, m.Config)
respDiags.Append(diags...)

m.Name = types.StringValue(network.Name)
Expand Down
2 changes: 1 addition & 1 deletion internal/network/resource_network_lb.go
Original file line number Diff line number Diff line change
Expand Up @@ -368,7 +368,7 @@ func (r LxdNetworkLBResource) SyncState(ctx context.Context, tfState *tfsdk.Stat
ports, diags := ToLBPortSetType(ctx, lb.Ports)
respDiags.Append(diags...)

config, diags := common.ToConfigMapType(ctx, lb.Config)
config, diags := common.ToConfigMapType(ctx, common.ToNullableConfig(lb.Config), m.Config)
respDiags.Append(diags...)

m.Description = types.StringValue(lb.Description)
Expand Down
38 changes: 38 additions & 0 deletions internal/network/resource_network_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,27 @@ func TestAccNetwork_description(t *testing.T) {
})
}

func TestAccNetwork_nullable(t *testing.T) {
networkName := acctest.GenerateName(2, "-")

resource.Test(t, resource.TestCase{
PreCheck: func() { acctest.PreCheck(t) },
ProtoV6ProviderFactories: acctest.ProtoV6ProviderFactories,
Steps: []resource.TestStep{
{
Config: testAccNetwork_nullable(networkName),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr("lxd_network.network", "name", networkName),
resource.TestCheckResourceAttr("lxd_network.network", "type", "bridge"),
resource.TestCheckResourceAttr("lxd_network.network", "config.%", "2"),
resource.TestCheckNoResourceAttr("lxd_network.network", "config.ipv4.address"),
resource.TestCheckResourceAttr("lxd_network.network", "config.ipv6.address", "none"),
),
},
},
})
}

func TestAccNetwork_attach(t *testing.T) {
networkName := acctest.GenerateName(2, "-")
profileName := acctest.GenerateName(2, "-")
Expand Down Expand Up @@ -276,6 +297,23 @@ resource "lxd_network" "network" {
`, networkName)
}

func testAccNetwork_nullable(networkName string) string {
return fmt.Sprintf(`
locals {
foo = "bar"
}
resource "lxd_network" "network" {
name = "%s"
config = {
"ipv4.address" = local.foo == "bar" ? null : "10.0.0.1/24"
"ipv6.address" = "none"
}
}
`, networkName)
}

func testAccNetwork_attach(networkName string, profileName string, instanceName string) string {
return fmt.Sprintf(`
resource "lxd_network" "network" {
Expand Down
2 changes: 1 addition & 1 deletion internal/network/resource_network_zone.go
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,7 @@ func (r NetworkZoneResource) SyncState(ctx context.Context, tfState *tfsdk.State
}

// Convert config state into schema type.
config, diags := common.ToConfigMapType(ctx, zone.Config)
config, diags := common.ToConfigMapType(ctx, common.ToNullableConfig(zone.Config), m.Config)
if diags.HasError() {
return diags
}
Expand Down
2 changes: 1 addition & 1 deletion internal/network/resource_network_zone_record.go
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +341,7 @@ func (r NetworkZoneRecordResource) SyncState(ctx context.Context, tfState *tfsdk
entries, diags := ToZoneRecordEntrySetType(ctx, record.Entries)
respDiags.Append(diags...)

config, diags := common.ToConfigMapType(ctx, record.Config)
config, diags := common.ToConfigMapType(ctx, common.ToNullableConfig(record.Config), m.Config)
respDiags.Append(diags...)

m.Zone = types.StringValue(zoneName)
Expand Down
2 changes: 1 addition & 1 deletion internal/profile/resource_profile.go
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,7 @@ func (r ProfileResource) SyncState(ctx context.Context, tfState *tfsdk.State, se
}

// Convert config state and devices into schema types.
config, diags := common.ToConfigMapType(ctx, profile.Config)
config, diags := common.ToConfigMapType(ctx, common.ToNullableConfig(profile.Config), m.Config)
respDiags.Append(diags...)

devices, diags := common.ToDeviceSetType(ctx, profile.Devices)
Expand Down
7 changes: 2 additions & 5 deletions internal/project/resource_project.go
Original file line number Diff line number Diff line change
Expand Up @@ -277,13 +277,10 @@ func (r ProjectResource) SyncState(ctx context.Context, tfState *tfsdk.State, se
}

// Extract user defined config and merge it with current config state.
usrConfig, diags := common.ToConfigMap(ctx, m.Config)
respDiags.Append(diags...)

stateConfig := common.StripConfig(project.Config, usrConfig, m.ComputedKeys())
stateConfig := common.StripConfig(project.Config, m.Config, m.ComputedKeys())

// Convert config state into schema type.
config, diags := common.ToConfigMapType(ctx, stateConfig)
config, diags := common.ToConfigMapType(ctx, stateConfig, m.Config)
respDiags.Append(diags...)

m.Name = types.StringValue(project.Name)
Expand Down
7 changes: 2 additions & 5 deletions internal/storage/resource_storage_pool.go
Original file line number Diff line number Diff line change
Expand Up @@ -328,13 +328,10 @@ func (r StoragePoolResource) SyncState(ctx context.Context, tfState *tfsdk.State
}

// Extract user defined config and merge it with current config state.
userConfig, diags := common.ToConfigMap(ctx, m.Config)
respDiags.Append(diags...)

stateConfig := common.StripConfig(pool.Config, userConfig, m.ComputedKeys(pool.Driver))
stateConfig := common.StripConfig(pool.Config, m.Config, m.ComputedKeys(pool.Driver))

// Convert config state into schema type.
config, diags := common.ToConfigMapType(ctx, stateConfig)
config, diags := common.ToConfigMapType(ctx, stateConfig, m.Config)
respDiags.Append(diags...)

m.Name = types.StringValue(pool.Name)
Expand Down
7 changes: 2 additions & 5 deletions internal/storage/resource_storage_volume.go
Original file line number Diff line number Diff line change
Expand Up @@ -349,12 +349,9 @@ func (r StorageVolumeResource) SyncState(ctx context.Context, tfState *tfsdk.Sta
}

// Extract user defined config and merge it with current config state.
userConfig, diags := common.ToConfigMap(ctx, m.Config)
respDiags.Append(diags...)

stateConfig := common.StripConfig(vol.Config, userConfig, m.ComputedKeys())
stateConfig := common.StripConfig(vol.Config, m.Config, m.ComputedKeys())

config, diags := common.ToConfigMapType(ctx, stateConfig)
config, diags := common.ToConfigMapType(ctx, stateConfig, m.Config)
respDiags.Append(diags...)

m.Name = types.StringValue(vol.Name)
Expand Down

0 comments on commit f7f0752

Please sign in to comment.