diff --git a/stackit/internal/services/ske/cluster/resource.go b/stackit/internal/services/ske/cluster/resource.go index 7e2049b75..b486e37e5 100644 --- a/stackit/internal/services/ske/cluster/resource.go +++ b/stackit/internal/services/ske/cluster/resource.go @@ -63,7 +63,7 @@ type Cluster struct { NodePools types.List `tfsdk:"node_pools"` Maintenance types.Object `tfsdk:"maintenance"` Hibernations types.List `tfsdk:"hibernations"` - Extensions *Extensions `tfsdk:"extensions"` + Extensions types.Object `tfsdk:"extensions"` KubeConfig types.String `tfsdk:"kube_config"` } @@ -147,21 +147,39 @@ var hibernationTypes = map[string]attr.Type{ "timezone": basetypes.StringType{}, } -type Extensions struct { - Argus *ArgusExtension `tfsdk:"argus"` - ACL *ACL `tfsdk:"acl"` +type extensions struct { + Argus types.Object `tfsdk:"argus"` + ACL types.Object `tfsdk:"acl"` } -type ACL struct { +// Types corresponding to extensions +var extensionsTypes = map[string]attr.Type{ + "argus": basetypes.ObjectType{AttrTypes: argusExtensionTypes}, + "acl": basetypes.ObjectType{AttrTypes: aclTypes}, +} + +type acl struct { Enabled types.Bool `tfsdk:"enabled"` AllowedCIDRs types.List `tfsdk:"allowed_cidrs"` } -type ArgusExtension struct { +// Types corresponding to acl +var aclTypes = map[string]attr.Type{ + "enabled": basetypes.BoolType{}, + "allowed_cidrs": basetypes.ListType{ElemType: types.StringType}, +} + +type argusExtension struct { Enabled types.Bool `tfsdk:"enabled"` ArgusInstanceId types.String `tfsdk:"argus_instance_id"` } +// Types corresponding to argusExtension +var argusExtensionTypes = map[string]attr.Type{ + "enabled": basetypes.BoolType{}, + "argus_instance_id": basetypes.StringType{}, +} + // NewClusterResource is a helper function to simplify the provider implementation. func NewClusterResource() resource.Resource { return &clusterResource{} @@ -771,28 +789,54 @@ func toHibernationsPayload(ctx context.Context, m *Cluster) (*ske.Hibernation, e } func toExtensionsPayload(ctx context.Context, m *Cluster) (*ske.Extension, error) { - if m.Extensions == nil { + if m.Extensions.IsNull() || m.Extensions.IsUnknown() { return nil, nil } - ex := &ske.Extension{} - if m.Extensions.Argus != nil { - ex.Argus = &ske.Argus{ - Enabled: conversion.BoolValueToPointer(m.Extensions.Argus.Enabled), - ArgusInstanceId: conversion.StringValueToPointer(m.Extensions.Argus.ArgusInstanceId), - } + ex := extensions{} + diags := m.Extensions.As(ctx, &ex, basetypes.ObjectAsOptions{}) + if diags.HasError() { + return nil, fmt.Errorf("converting extensions object: %v", diags.Errors()) } - if m.Extensions.ACL != nil { + + var skeAcl *ske.ACL + if !(ex.ACL.IsNull() || ex.ACL.IsUnknown()) { + acl := acl{} + diags = ex.ACL.As(ctx, &acl, basetypes.ObjectAsOptions{}) + if diags.HasError() { + return nil, fmt.Errorf("converting extensions.acl object: %v", diags.Errors()) + } + aclEnabled := conversion.BoolValueToPointer(acl.Enabled) + cidrs := []string{} - diags := m.Extensions.ACL.AllowedCIDRs.ElementsAs(ctx, &cidrs, true) + diags = acl.AllowedCIDRs.ElementsAs(ctx, &cidrs, true) if diags.HasError() { - return nil, fmt.Errorf("error in extension object converion %v", diags.Errors()) + return nil, fmt.Errorf("converting extensions.acl.cidrs object: %v", diags.Errors()) } - ex.Acl = &ske.ACL{ - Enabled: conversion.BoolValueToPointer(m.Extensions.ACL.Enabled), + skeAcl = &ske.ACL{ + Enabled: aclEnabled, AllowedCidrs: &cidrs, } } - return ex, nil + + var skeArgusExtension *ske.Argus + if !(ex.Argus.IsNull() || ex.Argus.IsUnknown()) { + argus := argusExtension{} + diags = ex.ACL.As(ctx, &argus, basetypes.ObjectAsOptions{}) + if diags.HasError() { + return nil, fmt.Errorf("converting extensions.acl object: %v", diags.Errors()) + } + argusEnabled := conversion.BoolValueToPointer(argus.Enabled) + argusInstanceId := conversion.StringValueToPointer(argus.ArgusInstanceId) + skeArgusExtension = &ske.Argus{ + Enabled: argusEnabled, + ArgusInstanceId: argusInstanceId, + } + } + + return &ske.Extension{ + Acl: skeAcl, + Argus: skeArgusExtension, + }, nil } func toMaintenancePayload(ctx context.Context, m *Cluster) (*ske.Maintenance, error) { @@ -803,7 +847,7 @@ func toMaintenancePayload(ctx context.Context, m *Cluster) (*ske.Maintenance, er maintenance := Maintenance{} diags := m.Maintenance.As(ctx, &maintenance, basetypes.ObjectAsOptions{}) if diags.HasError() { - return nil, fmt.Errorf("error in maintenance object conversion %v", diags.Errors()) + return nil, fmt.Errorf("converting maintenance object: %v", diags.Errors()) } var timeWindowStart *string @@ -870,7 +914,7 @@ func mapFields(ctx context.Context, cl *ske.ClusterResponse, m *Cluster) error { m.AllowPrivilegedContainers = types.BoolPointerValue(cl.Kubernetes.AllowPrivilegedContainers) } - err := mapNodePools(cl, m) + err := mapNodePools(ctx, cl, m) if err != nil { return fmt.Errorf("mapping node_pools: %w", err) } @@ -882,11 +926,14 @@ func mapFields(ctx context.Context, cl *ske.ClusterResponse, m *Cluster) error { if err != nil { return fmt.Errorf("mapping hibernations: %w", err) } - mapExtensions(cl, m) + err = mapExtensions(ctx, cl, m) + if err != nil { + return fmt.Errorf("mapping extensions: %w", err) + } return nil } -func mapNodePools(cl *ske.ClusterResponse, m *Cluster) error { +func mapNodePools(ctx context.Context, cl *ske.ClusterResponse, m *Cluster) error { if cl.Nodepools == nil { m.NodePools = types.ListNull(types.ObjectType{AttrTypes: nodePoolTypes}) return nil @@ -941,11 +988,7 @@ func mapNodePools(cl *ske.ClusterResponse, m *Cluster) error { } if nodePoolResp.AvailabilityZones != nil { - elems := []attr.Value{} - for _, v := range *nodePoolResp.AvailabilityZones { - elems = append(elems, types.StringValue(v)) - } - elemsTF, diags := types.ListValue(types.StringType, elems) + elemsTF, diags := types.ListValueFrom(ctx, types.StringType, *nodePoolResp.AvailabilityZones) if diags.HasError() { return fmt.Errorf("mapping index %d, field availability_zones: %w", i, core.DiagsToError(diags)) } @@ -1026,7 +1069,7 @@ func mapHibernations(cl *ske.ClusterResponse, m *Cluster) error { } func mapMaintenance(ctx context.Context, cl *ske.ClusterResponse, m *Cluster) error { - // Aligned with SKE team that a flattened data structure is fine, because not extensions are planned. + // Aligned with SKE team that a flattened data structure is fine, because no extensions are planned. if cl.Maintenance == nil { m.Maintenance = types.ObjectNull(maintenanceTypes) return nil @@ -1041,7 +1084,7 @@ func mapMaintenance(ctx context.Context, cl *ske.ClusterResponse, m *Cluster) er } startTime, endTime, err := getMaintenanceTimes(ctx, cl, m) if err != nil { - return fmt.Errorf("failed to get maintenance times: %w", err) + return fmt.Errorf("getting maintenance times: %w", err) } maintenanceValues := map[string]attr.Value{ "enable_kubernetes_version_updates": ekvu, @@ -1051,7 +1094,7 @@ func mapMaintenance(ctx context.Context, cl *ske.ClusterResponse, m *Cluster) er } maintenanceObject, diags := types.ObjectValue(maintenanceTypes, maintenanceValues) if diags.HasError() { - return fmt.Errorf("failed to create flavor: %w", core.DiagsToError(diags)) + return fmt.Errorf("creating flavor: %w", core.DiagsToError(diags)) } m.Maintenance = maintenanceObject return nil @@ -1060,11 +1103,11 @@ func mapMaintenance(ctx context.Context, cl *ske.ClusterResponse, m *Cluster) er func getMaintenanceTimes(ctx context.Context, cl *ske.ClusterResponse, m *Cluster) (startTime, endTime string, err error) { startTimeAPI, err := time.Parse(time.RFC3339, *cl.Maintenance.TimeWindow.Start) if err != nil { - return "", "", fmt.Errorf("failed to parse start time '%s' from API response as RFC3339 datetime: %w", *cl.Maintenance.TimeWindow.Start, err) + return "", "", fmt.Errorf("parsing start time '%s' from API response as RFC3339 datetime: %w", *cl.Maintenance.TimeWindow.Start, err) } endTimeAPI, err := time.Parse(time.RFC3339, *cl.Maintenance.TimeWindow.End) if err != nil { - return "", "", fmt.Errorf("failed to parse end time '%s' from API response as RFC3339 datetime: %w", *cl.Maintenance.TimeWindow.End, err) + return "", "", fmt.Errorf("parsing end time '%s' from API response as RFC3339 datetime: %w", *cl.Maintenance.TimeWindow.End, err) } if m.Maintenance.IsNull() || m.Maintenance.IsUnknown() { @@ -1074,7 +1117,7 @@ func getMaintenanceTimes(ctx context.Context, cl *ske.ClusterResponse, m *Cluste maintenance := &Maintenance{} diags := m.Maintenance.As(ctx, maintenance, basetypes.ObjectAsOptions{}) if diags.HasError() { - return "", "", fmt.Errorf("error in maintenance object conversion %w", core.DiagsToError(diags.Errors())) + return "", "", fmt.Errorf("converting maintenance object %w", core.DiagsToError(diags.Errors())) } if maintenance.Start.IsNull() || maintenance.Start.IsUnknown() { @@ -1082,7 +1125,7 @@ func getMaintenanceTimes(ctx context.Context, cl *ske.ClusterResponse, m *Cluste } else { startTimeTF, err := time.Parse("15:04:05Z07:00", maintenance.Start.ValueString()) if err != nil { - return "", "", fmt.Errorf("failed to parse start time '%s' from TF config as RFC time: %w", maintenance.Start.ValueString(), err) + return "", "", fmt.Errorf("parsing start time '%s' from TF config as RFC time: %w", maintenance.Start.ValueString(), err) } if startTimeAPI.Format("15:04:05Z07:00") != startTimeTF.Format("15:04:05Z07:00") { return "", "", fmt.Errorf("start time '%v' from API response doesn't match start time '%v' from TF config", *cl.Maintenance.TimeWindow.Start, maintenance.Start.ValueString()) @@ -1095,7 +1138,7 @@ func getMaintenanceTimes(ctx context.Context, cl *ske.ClusterResponse, m *Cluste } else { endTimeTF, err := time.Parse("15:04:05Z07:00", maintenance.End.ValueString()) if err != nil { - return "", "", fmt.Errorf("failed to parse end time '%s' from TF config as RFC time: %w", maintenance.End.ValueString(), err) + return "", "", fmt.Errorf("parsing end time '%s' from TF config as RFC time: %w", maintenance.End.ValueString(), err) } if endTimeAPI.Format("15:04:05Z07:00") != endTimeTF.Format("15:04:05Z07:00") { return "", "", fmt.Errorf("end time '%v' from API response doesn't match end time '%v' from TF config", *cl.Maintenance.TimeWindow.End, maintenance.End.ValueString()) @@ -1106,32 +1149,70 @@ func getMaintenanceTimes(ctx context.Context, cl *ske.ClusterResponse, m *Cluste return startTime, endTime, nil } -func mapExtensions(cl *ske.ClusterResponse, m *Cluster) { +func mapExtensions(ctx context.Context, cl *ske.ClusterResponse, m *Cluster) error { if cl.Extensions == nil || (cl.Extensions.Argus == nil && cl.Extensions.Acl == nil) { - return + m.Extensions = types.ObjectNull(extensionsTypes) + return nil } - if m.Extensions == nil { - m.Extensions = &Extensions{} + + var diags diag.Diagnostics + acl := types.ObjectNull(aclTypes) + if cl.Extensions.Acl != nil { + enabled := types.BoolNull() + if cl.Extensions.Acl.Enabled != nil { + enabled = types.BoolValue(*cl.Extensions.Acl.Enabled) + } + + cidrsList, diags := types.ListValueFrom(ctx, types.StringType, cl.Extensions.Acl.AllowedCidrs) + if diags.HasError() { + return fmt.Errorf("creating allowed_cidrs list: %w", core.DiagsToError(diags)) + } + + aclValues := map[string]attr.Value{ + "enabled": enabled, + "allowed_cidrs": cidrsList, + } + + acl, diags = types.ObjectValue(aclTypes, aclValues) + if diags.HasError() { + return fmt.Errorf("creating acl: %w", core.DiagsToError(diags)) + } } + + argusExtension := types.ObjectNull(argusExtensionTypes) if cl.Extensions.Argus != nil { - m.Extensions.Argus = &ArgusExtension{ - Enabled: types.BoolPointerValue(cl.Extensions.Argus.Enabled), - ArgusInstanceId: types.StringPointerValue(cl.Extensions.Argus.ArgusInstanceId), + enabled := types.BoolNull() + if cl.Extensions.Argus.Enabled != nil { + enabled = types.BoolValue(*cl.Extensions.Argus.Enabled) } - } - if cl.Extensions.Acl != nil { - cidr := []attr.Value{} - if cl.Extensions.Acl.AllowedCidrs != nil { - for _, v := range *cl.Extensions.Acl.AllowedCidrs { - cidr = append(cidr, types.StringValue(v)) - } + argusInstanceId := types.StringNull() + if cl.Extensions.Argus.ArgusInstanceId != nil { + argusInstanceId = types.StringValue(*cl.Extensions.Argus.ArgusInstanceId) + } + + argusExtensionValues := map[string]attr.Value{ + "enabled": enabled, + "argus_instance_id": argusInstanceId, } - m.Extensions.ACL = &ACL{ - Enabled: types.BoolPointerValue(cl.Extensions.Acl.Enabled), - AllowedCIDRs: types.ListValueMust(types.StringType, cidr), + + argusExtension, diags = types.ObjectValue(argusExtensionTypes, argusExtensionValues) + if diags.HasError() { + return fmt.Errorf("creating argus extension: %w", core.DiagsToError(diags)) } } + + extensionsValues := map[string]attr.Value{ + "acl": acl, + "argus": argusExtension, + } + + extensions, diags := types.ObjectValue(extensionsTypes, extensionsValues) + if diags.HasError() { + return fmt.Errorf("creating extensions: %w", core.DiagsToError(diags)) + } + m.Extensions = extensions + return nil } func toKubernetesPayload(m *Cluster, availableVersions []ske.KubernetesVersion) (kubernetesPayload *ske.Kubernetes, hasDeprecatedVersion bool, err error) { diff --git a/stackit/internal/services/ske/cluster/resource_test.go b/stackit/internal/services/ske/cluster/resource_test.go index 037cbe17f..bc4a7274b 100644 --- a/stackit/internal/services/ske/cluster/resource_test.go +++ b/stackit/internal/services/ske/cluster/resource_test.go @@ -34,7 +34,7 @@ func TestMapFields(t *testing.T) { NodePools: types.ListNull(types.ObjectType{AttrTypes: nodePoolTypes}), Maintenance: types.ObjectNull(maintenanceTypes), Hibernations: types.ListNull(types.ObjectType{AttrTypes: hibernationTypes}), - Extensions: nil, + Extensions: types.ObjectNull(extensionsTypes), KubeConfig: types.StringNull(), }, true, @@ -188,18 +188,18 @@ func TestMapFields(t *testing.T) { ), }, ), - Extensions: &Extensions{ - Argus: &ArgusExtension{ - Enabled: types.BoolValue(true), - ArgusInstanceId: types.StringValue("aid"), - }, - ACL: &ACL{ - Enabled: types.BoolValue(true), - AllowedCIDRs: types.ListValueMust(types.StringType, []attr.Value{ + Extensions: types.ObjectValueMust(extensionsTypes, map[string]attr.Value{ + "acl": types.ObjectValueMust(aclTypes, map[string]attr.Value{ + "enabled": types.BoolValue(true), + "allowed_cidrs": types.ListValueMust(types.StringType, []attr.Value{ types.StringValue("cidr1"), }), - }, - }, + }), + "argus": types.ObjectValueMust(argusExtensionTypes, map[string]attr.Value{ + "enabled": types.BoolValue(true), + "argus_instance_id": types.StringValue("aid"), + }), + }), KubeConfig: types.StringNull(), }, true,