SKE Cluster - Change Extensions field model to use types.Object (#127)

* Convert SKE Extensions field to TF model

* Fix unit test

* Check error

* Update stackit/internal/services/ske/cluster/resource.go

Co-authored-by: João Palet <joao.palet@outlook.com>

* Fix error messages

* Replace ListValueMust with ListValue

* Replace types.ListValue with types.ListValueFrom

* Add context

* Add separator

---------

Co-authored-by: João Palet <joao.palet@outlook.com>
This commit is contained in:
Vicente Pinto 2023-11-06 10:59:36 +00:00 committed by GitHub
parent 1d88c953a0
commit 0d438a4a9c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 147 additions and 66 deletions

View file

@ -63,7 +63,7 @@ type Cluster struct {
NodePools types.List `tfsdk:"node_pools"` NodePools types.List `tfsdk:"node_pools"`
Maintenance types.Object `tfsdk:"maintenance"` Maintenance types.Object `tfsdk:"maintenance"`
Hibernations types.List `tfsdk:"hibernations"` Hibernations types.List `tfsdk:"hibernations"`
Extensions *Extensions `tfsdk:"extensions"` Extensions types.Object `tfsdk:"extensions"`
KubeConfig types.String `tfsdk:"kube_config"` KubeConfig types.String `tfsdk:"kube_config"`
} }
@ -147,21 +147,39 @@ var hibernationTypes = map[string]attr.Type{
"timezone": basetypes.StringType{}, "timezone": basetypes.StringType{},
} }
type Extensions struct { type extensions struct {
Argus *ArgusExtension `tfsdk:"argus"` Argus types.Object `tfsdk:"argus"`
ACL *ACL `tfsdk:"acl"` 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"` Enabled types.Bool `tfsdk:"enabled"`
AllowedCIDRs types.List `tfsdk:"allowed_cidrs"` 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"` Enabled types.Bool `tfsdk:"enabled"`
ArgusInstanceId types.String `tfsdk:"argus_instance_id"` 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. // NewClusterResource is a helper function to simplify the provider implementation.
func NewClusterResource() resource.Resource { func NewClusterResource() resource.Resource {
return &clusterResource{} 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) { func toExtensionsPayload(ctx context.Context, m *Cluster) (*ske.Extension, error) {
if m.Extensions == nil { if m.Extensions.IsNull() || m.Extensions.IsUnknown() {
return nil, nil return nil, nil
} }
ex := &ske.Extension{} ex := extensions{}
if m.Extensions.Argus != nil { diags := m.Extensions.As(ctx, &ex, basetypes.ObjectAsOptions{})
ex.Argus = &ske.Argus{ if diags.HasError() {
Enabled: conversion.BoolValueToPointer(m.Extensions.Argus.Enabled), return nil, fmt.Errorf("converting extensions object: %v", diags.Errors())
ArgusInstanceId: conversion.StringValueToPointer(m.Extensions.Argus.ArgusInstanceId),
}
} }
if m.Extensions.ACL != nil {
cidrs := []string{} var skeAcl *ske.ACL
diags := m.Extensions.ACL.AllowedCIDRs.ElementsAs(ctx, &cidrs, true) if !(ex.ACL.IsNull() || ex.ACL.IsUnknown()) {
acl := acl{}
diags = ex.ACL.As(ctx, &acl, basetypes.ObjectAsOptions{})
if diags.HasError() { if diags.HasError() {
return nil, fmt.Errorf("error in extension object converion %v", diags.Errors()) return nil, fmt.Errorf("converting extensions.acl object: %v", diags.Errors())
} }
ex.Acl = &ske.ACL{ aclEnabled := conversion.BoolValueToPointer(acl.Enabled)
Enabled: conversion.BoolValueToPointer(m.Extensions.ACL.Enabled),
cidrs := []string{}
diags = acl.AllowedCIDRs.ElementsAs(ctx, &cidrs, true)
if diags.HasError() {
return nil, fmt.Errorf("converting extensions.acl.cidrs object: %v", diags.Errors())
}
skeAcl = &ske.ACL{
Enabled: aclEnabled,
AllowedCidrs: &cidrs, 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) { 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{} maintenance := Maintenance{}
diags := m.Maintenance.As(ctx, &maintenance, basetypes.ObjectAsOptions{}) diags := m.Maintenance.As(ctx, &maintenance, basetypes.ObjectAsOptions{})
if diags.HasError() { 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 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) m.AllowPrivilegedContainers = types.BoolPointerValue(cl.Kubernetes.AllowPrivilegedContainers)
} }
err := mapNodePools(cl, m) err := mapNodePools(ctx, cl, m)
if err != nil { if err != nil {
return fmt.Errorf("mapping node_pools: %w", err) 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 { if err != nil {
return fmt.Errorf("mapping hibernations: %w", err) 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 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 { if cl.Nodepools == nil {
m.NodePools = types.ListNull(types.ObjectType{AttrTypes: nodePoolTypes}) m.NodePools = types.ListNull(types.ObjectType{AttrTypes: nodePoolTypes})
return nil return nil
@ -941,11 +988,7 @@ func mapNodePools(cl *ske.ClusterResponse, m *Cluster) error {
} }
if nodePoolResp.AvailabilityZones != nil { if nodePoolResp.AvailabilityZones != nil {
elems := []attr.Value{} elemsTF, diags := types.ListValueFrom(ctx, types.StringType, *nodePoolResp.AvailabilityZones)
for _, v := range *nodePoolResp.AvailabilityZones {
elems = append(elems, types.StringValue(v))
}
elemsTF, diags := types.ListValue(types.StringType, elems)
if diags.HasError() { if diags.HasError() {
return fmt.Errorf("mapping index %d, field availability_zones: %w", i, core.DiagsToError(diags)) 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 { 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 { if cl.Maintenance == nil {
m.Maintenance = types.ObjectNull(maintenanceTypes) m.Maintenance = types.ObjectNull(maintenanceTypes)
return nil return nil
@ -1041,7 +1084,7 @@ func mapMaintenance(ctx context.Context, cl *ske.ClusterResponse, m *Cluster) er
} }
startTime, endTime, err := getMaintenanceTimes(ctx, cl, m) startTime, endTime, err := getMaintenanceTimes(ctx, cl, m)
if err != nil { 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{ maintenanceValues := map[string]attr.Value{
"enable_kubernetes_version_updates": ekvu, "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) maintenanceObject, diags := types.ObjectValue(maintenanceTypes, maintenanceValues)
if diags.HasError() { 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 m.Maintenance = maintenanceObject
return nil 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) { 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) startTimeAPI, err := time.Parse(time.RFC3339, *cl.Maintenance.TimeWindow.Start)
if err != nil { 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) endTimeAPI, err := time.Parse(time.RFC3339, *cl.Maintenance.TimeWindow.End)
if err != nil { 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() { if m.Maintenance.IsNull() || m.Maintenance.IsUnknown() {
@ -1074,7 +1117,7 @@ func getMaintenanceTimes(ctx context.Context, cl *ske.ClusterResponse, m *Cluste
maintenance := &Maintenance{} maintenance := &Maintenance{}
diags := m.Maintenance.As(ctx, maintenance, basetypes.ObjectAsOptions{}) diags := m.Maintenance.As(ctx, maintenance, basetypes.ObjectAsOptions{})
if diags.HasError() { 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() { if maintenance.Start.IsNull() || maintenance.Start.IsUnknown() {
@ -1082,7 +1125,7 @@ func getMaintenanceTimes(ctx context.Context, cl *ske.ClusterResponse, m *Cluste
} else { } else {
startTimeTF, err := time.Parse("15:04:05Z07:00", maintenance.Start.ValueString()) startTimeTF, err := time.Parse("15:04:05Z07:00", maintenance.Start.ValueString())
if err != nil { 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") { 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()) 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 { } else {
endTimeTF, err := time.Parse("15:04:05Z07:00", maintenance.End.ValueString()) endTimeTF, err := time.Parse("15:04:05Z07:00", maintenance.End.ValueString())
if err != nil { 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") { 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()) 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 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) { 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.Argus != nil { if cl.Extensions.Acl != nil {
m.Extensions.Argus = &ArgusExtension{ enabled := types.BoolNull()
Enabled: types.BoolPointerValue(cl.Extensions.Argus.Enabled), if cl.Extensions.Acl.Enabled != nil {
ArgusInstanceId: types.StringPointerValue(cl.Extensions.Argus.ArgusInstanceId), 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))
} }
} }
if cl.Extensions.Acl != nil { argusExtension := types.ObjectNull(argusExtensionTypes)
cidr := []attr.Value{} if cl.Extensions.Argus != nil {
if cl.Extensions.Acl.AllowedCidrs != nil { enabled := types.BoolNull()
for _, v := range *cl.Extensions.Acl.AllowedCidrs { if cl.Extensions.Argus.Enabled != nil {
cidr = append(cidr, types.StringValue(v)) enabled = types.BoolValue(*cl.Extensions.Argus.Enabled)
}
} }
m.Extensions.ACL = &ACL{
Enabled: types.BoolPointerValue(cl.Extensions.Acl.Enabled), argusInstanceId := types.StringNull()
AllowedCIDRs: types.ListValueMust(types.StringType, cidr), if cl.Extensions.Argus.ArgusInstanceId != nil {
argusInstanceId = types.StringValue(*cl.Extensions.Argus.ArgusInstanceId)
}
argusExtensionValues := map[string]attr.Value{
"enabled": enabled,
"argus_instance_id": argusInstanceId,
}
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) { func toKubernetesPayload(m *Cluster, availableVersions []ske.KubernetesVersion) (kubernetesPayload *ske.Kubernetes, hasDeprecatedVersion bool, err error) {

View file

@ -34,7 +34,7 @@ func TestMapFields(t *testing.T) {
NodePools: types.ListNull(types.ObjectType{AttrTypes: nodePoolTypes}), NodePools: types.ListNull(types.ObjectType{AttrTypes: nodePoolTypes}),
Maintenance: types.ObjectNull(maintenanceTypes), Maintenance: types.ObjectNull(maintenanceTypes),
Hibernations: types.ListNull(types.ObjectType{AttrTypes: hibernationTypes}), Hibernations: types.ListNull(types.ObjectType{AttrTypes: hibernationTypes}),
Extensions: nil, Extensions: types.ObjectNull(extensionsTypes),
KubeConfig: types.StringNull(), KubeConfig: types.StringNull(),
}, },
true, true,
@ -188,18 +188,18 @@ func TestMapFields(t *testing.T) {
), ),
}, },
), ),
Extensions: &Extensions{ Extensions: types.ObjectValueMust(extensionsTypes, map[string]attr.Value{
Argus: &ArgusExtension{ "acl": types.ObjectValueMust(aclTypes, map[string]attr.Value{
Enabled: types.BoolValue(true), "enabled": types.BoolValue(true),
ArgusInstanceId: types.StringValue("aid"), "allowed_cidrs": types.ListValueMust(types.StringType, []attr.Value{
},
ACL: &ACL{
Enabled: types.BoolValue(true),
AllowedCIDRs: types.ListValueMust(types.StringType, []attr.Value{
types.StringValue("cidr1"), types.StringValue("cidr1"),
}), }),
}, }),
}, "argus": types.ObjectValueMust(argusExtensionTypes, map[string]attr.Value{
"enabled": types.BoolValue(true),
"argus_instance_id": types.StringValue("aid"),
}),
}),
KubeConfig: types.StringNull(), KubeConfig: types.StringNull(),
}, },
true, true,