Alpha (#4)
* chore: initial push to be able to work together * chore: add missing wait folder * chore: add missing folders * chore: cleanup alpha branch * feat: mssql alpha instance (#2) * fix: remove unused attribute types and functions from backup models * fix: update API client references to use sqlserverflexalpha package * fix: update package references to use sqlserverflexalpha and modify user data source model * fix: add sqlserverflexalpha user data source to provider * fix: add sqlserverflexalpha user resource and update related functionality * chore: add stackit_sqlserverflexalpha_user resource and instance_id variable * fix: refactor sqlserverflexalpha user resource and enhance schema with status and default_database --------- Co-authored-by: Andre Harms <andre.harms@stackit.cloud> Co-authored-by: Marcel S. Henselin <marcel.henselin@stackit.cloud> * feat: add sqlserver instance * chore: fixing tests * chore: update docs --------- Co-authored-by: Marcel S. Henselin <marcel.henselin@stackit.cloud> Co-authored-by: Andre Harms <andre.harms@stackit.cloud>
This commit is contained in:
parent
45073a716b
commit
2733834fc9
351 changed files with 62744 additions and 3 deletions
48
stackit/internal/utils/attributes.go
Normal file
48
stackit/internal/utils/attributes.go
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
// Copyright (c) STACKIT
|
||||
|
||||
package utils
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/terraform-plugin-framework/diag"
|
||||
"github.com/hashicorp/terraform-plugin-framework/path"
|
||||
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core"
|
||||
)
|
||||
|
||||
type attributeGetter interface {
|
||||
GetAttribute(ctx context.Context, attributePath path.Path, target interface{}) diag.Diagnostics
|
||||
}
|
||||
|
||||
func ToTime(ctx context.Context, format string, val types.String, target *time.Time) (diags diag.Diagnostics) {
|
||||
var err error
|
||||
text := val.ValueString()
|
||||
*target, err = time.Parse(format, text)
|
||||
if err != nil {
|
||||
core.LogAndAddError(ctx, &diags, "cannot parse date", fmt.Sprintf("cannot parse date %q with format %q: %v", text, format, err))
|
||||
return diags
|
||||
}
|
||||
return diags
|
||||
}
|
||||
|
||||
// GetTimeFromStringAttribute retrieves a string attribute from e.g. a [plan.Plan], [tfsdk.Config] or a [tfsdk.State] and
|
||||
// converts it to a [time.Time] object with a given format, if possible.
|
||||
func GetTimeFromStringAttribute(ctx context.Context, attributePath path.Path, source attributeGetter, dateFormat string, target *time.Time) (diags diag.Diagnostics) {
|
||||
var date types.String
|
||||
diags.Append(source.GetAttribute(ctx, attributePath, &date)...)
|
||||
if diags.HasError() {
|
||||
return diags
|
||||
}
|
||||
if date.IsNull() || date.IsUnknown() {
|
||||
return diags
|
||||
}
|
||||
diags.Append(ToTime(ctx, dateFormat, date, target)...)
|
||||
if diags.HasError() {
|
||||
return diags
|
||||
}
|
||||
|
||||
return diags
|
||||
}
|
||||
90
stackit/internal/utils/attributes_test.go
Normal file
90
stackit/internal/utils/attributes_test.go
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
// Copyright (c) STACKIT
|
||||
|
||||
package utils
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/terraform-plugin-framework/diag"
|
||||
"github.com/hashicorp/terraform-plugin-framework/path"
|
||||
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||
)
|
||||
|
||||
type attributeGetterFunc func(ctx context.Context, attributePath path.Path, target interface{}) diag.Diagnostics
|
||||
|
||||
func (a attributeGetterFunc) GetAttribute(ctx context.Context, attributePath path.Path, target interface{}) diag.Diagnostics {
|
||||
return a(ctx, attributePath, target)
|
||||
}
|
||||
|
||||
func mustLocation(name string) *time.Location {
|
||||
loc, err := time.LoadLocation(name)
|
||||
if err != nil {
|
||||
log.Panicf("cannot load location %s: %v", name, err)
|
||||
}
|
||||
return loc
|
||||
}
|
||||
|
||||
func TestGetTimeFromString(t *testing.T) {
|
||||
type args struct {
|
||||
path path.Path
|
||||
source attributeGetterFunc
|
||||
dateFormat string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantErr bool
|
||||
want time.Time
|
||||
}{
|
||||
{
|
||||
name: "simple string",
|
||||
args: args{
|
||||
path: path.Root("foo"),
|
||||
source: func(_ context.Context, _ path.Path, target interface{}) diag.Diagnostics {
|
||||
t, ok := target.(*types.String)
|
||||
if !ok {
|
||||
log.Panicf("wrong type %T", target)
|
||||
}
|
||||
*t = types.StringValue("2025-02-06T09:41:00+01:00")
|
||||
return nil
|
||||
},
|
||||
dateFormat: time.RFC3339,
|
||||
},
|
||||
want: time.Date(2025, 2, 6, 9, 41, 0, 0, mustLocation("Europe/Berlin")),
|
||||
},
|
||||
{
|
||||
name: "invalid type",
|
||||
args: args{
|
||||
path: path.Root("foo"),
|
||||
source: func(_ context.Context, p path.Path, _ interface{}) (diags diag.Diagnostics) {
|
||||
diags.AddAttributeError(p, "kapow", "kapow")
|
||||
return diags
|
||||
},
|
||||
dateFormat: time.RFC3339,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var target time.Time
|
||||
gotDiags := GetTimeFromStringAttribute(context.Background(), tt.args.path, tt.args.source, tt.args.dateFormat, &target)
|
||||
if tt.wantErr {
|
||||
if !gotDiags.HasError() {
|
||||
t.Errorf("expected error")
|
||||
}
|
||||
} else {
|
||||
if gotDiags.HasError() {
|
||||
t.Errorf("expected no errors, but got %v", gotDiags)
|
||||
} else {
|
||||
if want, got := tt.want, target; !want.Equal(got) {
|
||||
t.Errorf("got wrong date, want %s but got %s", want, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
13
stackit/internal/utils/headers.go
Normal file
13
stackit/internal/utils/headers.go
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
// Copyright (c) STACKIT
|
||||
|
||||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/stackitcloud/stackit-sdk-go/core/config"
|
||||
)
|
||||
|
||||
func UserAgentConfigOption(providerVersion string) config.ConfigurationOption {
|
||||
return config.WithUserAgent(fmt.Sprintf("stackit-terraform-provider/%s", providerVersion))
|
||||
}
|
||||
48
stackit/internal/utils/headers_test.go
Normal file
48
stackit/internal/utils/headers_test.go
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
// Copyright (c) STACKIT
|
||||
|
||||
package utils
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/stackitcloud/stackit-sdk-go/core/config"
|
||||
)
|
||||
|
||||
func TestUserAgentConfigOption(t *testing.T) {
|
||||
type args struct {
|
||||
providerVersion string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want config.ConfigurationOption
|
||||
}{
|
||||
{
|
||||
name: "TestUserAgentConfigOption",
|
||||
args: args{
|
||||
providerVersion: "1.0.0",
|
||||
},
|
||||
want: config.WithUserAgent("stackit-terraform-provider/1.0.0"),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
clientConfigActual := config.Configuration{}
|
||||
err := tt.want(&clientConfigActual)
|
||||
if err != nil {
|
||||
t.Errorf("error applying configuration: %v", err)
|
||||
}
|
||||
|
||||
clientConfigExpected := config.Configuration{}
|
||||
err = UserAgentConfigOption(tt.args.providerVersion)(&clientConfigExpected)
|
||||
if err != nil {
|
||||
t.Errorf("error applying configuration: %v", err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(clientConfigActual, clientConfigExpected) {
|
||||
t.Errorf("UserAgentConfigOption() = %v, want %v", clientConfigActual, clientConfigExpected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
38
stackit/internal/utils/regions.go
Normal file
38
stackit/internal/utils/regions.go
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
// Copyright (c) STACKIT
|
||||
|
||||
package utils
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/hashicorp/terraform-plugin-framework/path"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource"
|
||||
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core"
|
||||
)
|
||||
|
||||
// AdaptRegion rewrites the region of a terraform plan
|
||||
func AdaptRegion(ctx context.Context, configRegion types.String, planRegion *types.String, defaultRegion string, resp *resource.ModifyPlanResponse) {
|
||||
// Get the intended region. This is either set directly set in the individual
|
||||
// config or the provider region has to be used
|
||||
var intendedRegion types.String
|
||||
if configRegion.IsNull() {
|
||||
if defaultRegion == "" {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "set region", "no region defined in config or provider")
|
||||
return
|
||||
}
|
||||
intendedRegion = types.StringValue(defaultRegion)
|
||||
} else {
|
||||
intendedRegion = configRegion
|
||||
}
|
||||
|
||||
// check if the currently configured region corresponds to the planned region
|
||||
// on mismatch override the planned region with the intended region
|
||||
// and force a replacement of the resource
|
||||
p := path.Root("region")
|
||||
if !intendedRegion.Equal(*planRegion) {
|
||||
resp.RequiresReplace.Append(p)
|
||||
*planRegion = intendedRegion
|
||||
}
|
||||
resp.Diagnostics.Append(resp.Plan.SetAttribute(ctx, p, *planRegion)...)
|
||||
}
|
||||
89
stackit/internal/utils/regions_test.go
Normal file
89
stackit/internal/utils/regions_test.go
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
// Copyright (c) STACKIT
|
||||
|
||||
package utils
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/terraform-plugin-framework/provider/schema"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource"
|
||||
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
|
||||
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||
)
|
||||
|
||||
func TestAdaptRegion(t *testing.T) {
|
||||
type model struct {
|
||||
Region types.String `tfsdk:"region"`
|
||||
}
|
||||
type args struct {
|
||||
configRegion types.String
|
||||
defaultRegion string
|
||||
}
|
||||
testcases := []struct {
|
||||
name string
|
||||
args args
|
||||
wantErr bool
|
||||
wantRegion types.String
|
||||
}{
|
||||
{
|
||||
"no configured region, use provider region",
|
||||
args{
|
||||
types.StringNull(),
|
||||
"eu01",
|
||||
},
|
||||
false,
|
||||
types.StringValue("eu01"),
|
||||
},
|
||||
{
|
||||
"no configured region, no provider region => want error",
|
||||
args{
|
||||
types.StringNull(),
|
||||
"",
|
||||
},
|
||||
true,
|
||||
types.StringNull(),
|
||||
},
|
||||
{
|
||||
"configuration region overrides provider region",
|
||||
args{
|
||||
types.StringValue("eu01-m"),
|
||||
"eu01",
|
||||
},
|
||||
false,
|
||||
types.StringValue("eu01-m"),
|
||||
},
|
||||
}
|
||||
for _, tc := range testcases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
plan := tfsdk.Plan{
|
||||
Schema: schema.Schema{
|
||||
Attributes: map[string]schema.Attribute{
|
||||
"region": schema.StringAttribute{
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if diags := plan.Set(context.Background(), model{types.StringValue("unknown")}); diags.HasError() {
|
||||
t.Fatalf("cannot create test model: %v", diags)
|
||||
}
|
||||
resp := resource.ModifyPlanResponse{
|
||||
Plan: plan,
|
||||
}
|
||||
|
||||
configModel := model{
|
||||
Region: tc.args.configRegion,
|
||||
}
|
||||
planModel := model{}
|
||||
AdaptRegion(context.Background(), configModel.Region, &planModel.Region, tc.args.defaultRegion, &resp)
|
||||
if diags := resp.Diagnostics; tc.wantErr != diags.HasError() {
|
||||
t.Errorf("unexpected diagnostics: want err: %v, actual %v", tc.wantErr, diags.Errors())
|
||||
}
|
||||
if expected, actual := tc.wantRegion, planModel.Region; !expected.Equal(actual) {
|
||||
t.Errorf("wrong result region. expect %s but got %s", expected, actual)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
71
stackit/internal/utils/use_state_for_unknown_if.go
Normal file
71
stackit/internal/utils/use_state_for_unknown_if.go
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
// Copyright (c) STACKIT
|
||||
|
||||
package utils
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/hashicorp/terraform-plugin-framework/diag"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
|
||||
)
|
||||
|
||||
type UseStateForUnknownFuncResponse struct {
|
||||
UseStateForUnknown bool
|
||||
Diagnostics diag.Diagnostics
|
||||
}
|
||||
|
||||
// UseStateForUnknownIfFunc is a conditional function used in UseStateForUnknownIf
|
||||
type UseStateForUnknownIfFunc func(context.Context, planmodifier.StringRequest, *UseStateForUnknownFuncResponse)
|
||||
|
||||
type useStateForUnknownIf struct {
|
||||
ifFunc UseStateForUnknownIfFunc
|
||||
description string
|
||||
}
|
||||
|
||||
// UseStateForUnknownIf returns a plan modifier similar to UseStateForUnknown with a conditional
|
||||
func UseStateForUnknownIf(f UseStateForUnknownIfFunc, description string) planmodifier.String {
|
||||
return useStateForUnknownIf{
|
||||
ifFunc: f,
|
||||
description: description,
|
||||
}
|
||||
}
|
||||
|
||||
func (m useStateForUnknownIf) Description(context.Context) string {
|
||||
return m.description
|
||||
}
|
||||
|
||||
func (m useStateForUnknownIf) MarkdownDescription(ctx context.Context) string {
|
||||
return m.Description(ctx)
|
||||
}
|
||||
|
||||
func (m useStateForUnknownIf) PlanModifyString(ctx context.Context, req planmodifier.StringRequest, resp *planmodifier.StringResponse) { // nolint:gocritic // function signature required by Terraform
|
||||
// Do nothing if there is no state value.
|
||||
if req.StateValue.IsNull() {
|
||||
return
|
||||
}
|
||||
|
||||
// Do nothing if there is a known planned value.
|
||||
if !req.PlanValue.IsUnknown() {
|
||||
return
|
||||
}
|
||||
|
||||
// Do nothing if there is an unknown configuration value, otherwise interpolation gets messed up.
|
||||
if req.ConfigValue.IsUnknown() {
|
||||
return
|
||||
}
|
||||
|
||||
// The above checks are taken from the UseStateForUnknown plan modifier implementation
|
||||
// (https://github.com/hashicorp/terraform-plugin-framework/blob/44348af3923c82a93c64ae7dca906d9850ba956b/resource/schema/stringplanmodifier/use_state_for_unknown.go#L38)
|
||||
|
||||
funcResponse := &UseStateForUnknownFuncResponse{}
|
||||
m.ifFunc(ctx, req, funcResponse)
|
||||
|
||||
resp.Diagnostics.Append(funcResponse.Diagnostics...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
if funcResponse.UseStateForUnknown {
|
||||
resp.PlanValue = req.StateValue
|
||||
}
|
||||
}
|
||||
130
stackit/internal/utils/use_state_for_unknown_if_test.go
Normal file
130
stackit/internal/utils/use_state_for_unknown_if_test.go
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
// Copyright (c) STACKIT
|
||||
|
||||
package utils
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
|
||||
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||
)
|
||||
|
||||
func TestUseStateForUnknownIf_PlanModifyString(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
stateValue types.String
|
||||
planValue types.String
|
||||
configValue types.String
|
||||
ifFunc UseStateForUnknownIfFunc
|
||||
expectedPlanValue types.String
|
||||
expectedError bool
|
||||
}{
|
||||
{
|
||||
name: "State is Null (Creation)",
|
||||
stateValue: types.StringNull(),
|
||||
planValue: types.StringUnknown(),
|
||||
configValue: types.StringValue("some-config"),
|
||||
ifFunc: func(_ context.Context, _ planmodifier.StringRequest, resp *UseStateForUnknownFuncResponse) {
|
||||
// This should not be reached because the state is null
|
||||
resp.UseStateForUnknown = true
|
||||
},
|
||||
expectedPlanValue: types.StringUnknown(),
|
||||
},
|
||||
{
|
||||
name: "Plan is already known - (User updated the value)",
|
||||
stateValue: types.StringValue("old-state"),
|
||||
planValue: types.StringValue("new-plan"),
|
||||
configValue: types.StringValue("new-plan"),
|
||||
ifFunc: func(_ context.Context, _ planmodifier.StringRequest, resp *UseStateForUnknownFuncResponse) {
|
||||
// This should not be reached because the plan is known
|
||||
resp.UseStateForUnknown = true
|
||||
},
|
||||
expectedPlanValue: types.StringValue("new-plan"),
|
||||
},
|
||||
{
|
||||
name: "Config is Unknown (Interpolation)",
|
||||
stateValue: types.StringValue("old-state"),
|
||||
planValue: types.StringUnknown(),
|
||||
configValue: types.StringUnknown(),
|
||||
ifFunc: func(_ context.Context, _ planmodifier.StringRequest, resp *UseStateForUnknownFuncResponse) {
|
||||
// This should not be reached
|
||||
resp.UseStateForUnknown = true
|
||||
},
|
||||
expectedPlanValue: types.StringUnknown(),
|
||||
},
|
||||
{
|
||||
name: "Condition returns False (Do not use state)",
|
||||
stateValue: types.StringValue("old-state"),
|
||||
planValue: types.StringUnknown(),
|
||||
configValue: types.StringNull(), // Simulating computed only
|
||||
ifFunc: func(_ context.Context, _ planmodifier.StringRequest, resp *UseStateForUnknownFuncResponse) {
|
||||
resp.UseStateForUnknown = false
|
||||
},
|
||||
expectedPlanValue: types.StringUnknown(),
|
||||
},
|
||||
{
|
||||
name: "Condition returns True (Use state)",
|
||||
stateValue: types.StringValue("old-state"),
|
||||
planValue: types.StringUnknown(),
|
||||
configValue: types.StringNull(),
|
||||
ifFunc: func(_ context.Context, _ planmodifier.StringRequest, resp *UseStateForUnknownFuncResponse) {
|
||||
resp.UseStateForUnknown = true
|
||||
},
|
||||
expectedPlanValue: types.StringValue("old-state"),
|
||||
},
|
||||
{
|
||||
name: "Func returns Error",
|
||||
stateValue: types.StringValue("old-state"),
|
||||
planValue: types.StringUnknown(),
|
||||
configValue: types.StringNull(),
|
||||
ifFunc: func(_ context.Context, _ planmodifier.StringRequest, resp *UseStateForUnknownFuncResponse) {
|
||||
resp.Diagnostics.AddError("Test Error", "Something went wrong")
|
||||
},
|
||||
expectedPlanValue: types.StringUnknown(),
|
||||
expectedError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Initialize the modifier
|
||||
modifier := UseStateForUnknownIf(tt.ifFunc, "test description")
|
||||
|
||||
// Construct request
|
||||
req := planmodifier.StringRequest{
|
||||
StateValue: tt.stateValue,
|
||||
PlanValue: tt.planValue,
|
||||
ConfigValue: tt.configValue,
|
||||
}
|
||||
|
||||
// Construct response
|
||||
// Note: In the framework, resp.PlanValue is initialized to req.PlanValue
|
||||
// before the modifier is called. We must simulate this.
|
||||
resp := &planmodifier.StringResponse{
|
||||
PlanValue: tt.planValue,
|
||||
}
|
||||
|
||||
// Run the modifier
|
||||
modifier.PlanModifyString(ctx, req, resp)
|
||||
|
||||
// Check Errors
|
||||
if tt.expectedError {
|
||||
if !resp.Diagnostics.HasError() {
|
||||
t.Error("Expected error, got none")
|
||||
}
|
||||
} else {
|
||||
if resp.Diagnostics.HasError() {
|
||||
t.Errorf("Unexpected error: %s", resp.Diagnostics)
|
||||
}
|
||||
}
|
||||
|
||||
// Check Plan Value
|
||||
if !resp.PlanValue.Equal(tt.expectedPlanValue) {
|
||||
t.Errorf("PlanValue mismatch.\nExpected: %s\nGot: %s", tt.expectedPlanValue, resp.PlanValue)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
186
stackit/internal/utils/utils.go
Normal file
186
stackit/internal/utils/utils.go
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
// Copyright (c) STACKIT
|
||||
|
||||
package utils
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
|
||||
|
||||
"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/types"
|
||||
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
|
||||
"github.com/hashicorp/terraform-plugin-log/tflog"
|
||||
"github.com/stackitcloud/stackit-sdk-go/core/oapierror"
|
||||
"github.com/stackitcloud/stackit-sdk-go/core/utils"
|
||||
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core"
|
||||
)
|
||||
|
||||
const (
|
||||
SKEServiceId = "cloud.stackit.ske"
|
||||
ModelServingServiceId = "cloud.stackit.model-serving"
|
||||
)
|
||||
|
||||
var (
|
||||
LegacyProjectRoles = []string{"project.admin", "project.auditor", "project.member", "project.owner"}
|
||||
)
|
||||
|
||||
// ReconcileStringSlices reconciles two string lists by removing elements from the
|
||||
// first list that are not in the second list and appending elements from the
|
||||
// second list that are not in the first list.
|
||||
// This preserves the order of the elements in the first list that are also in
|
||||
// the second list, which is useful when using ListAttributes in Terraform.
|
||||
// The source of truth for the order is the first list and the source of truth for the content is the second list.
|
||||
func ReconcileStringSlices(list1, list2 []string) []string {
|
||||
// Create a copy of list1 to avoid modifying the original list
|
||||
list1Copy := append([]string{}, list1...)
|
||||
|
||||
// Create a map to quickly check if an element is in list2
|
||||
inList2 := make(map[string]bool)
|
||||
for _, elem := range list2 {
|
||||
inList2[elem] = true
|
||||
}
|
||||
|
||||
// Remove elements from list1Copy that are not in list2
|
||||
i := 0
|
||||
for _, elem := range list1Copy {
|
||||
if inList2[elem] {
|
||||
list1Copy[i] = elem
|
||||
i++
|
||||
}
|
||||
}
|
||||
list1Copy = list1Copy[:i]
|
||||
|
||||
// Append elements to list1Copy that are in list2 but not in list1Copy
|
||||
inList1 := make(map[string]bool)
|
||||
for _, elem := range list1Copy {
|
||||
inList1[elem] = true
|
||||
}
|
||||
for _, elem := range list2 {
|
||||
if !inList1[elem] {
|
||||
list1Copy = append(list1Copy, elem)
|
||||
}
|
||||
}
|
||||
|
||||
return list1Copy
|
||||
}
|
||||
|
||||
func ListValuetoStringSlice(list basetypes.ListValue) ([]string, error) {
|
||||
result := []string{}
|
||||
for _, el := range list.Elements() {
|
||||
elStr, ok := el.(types.String)
|
||||
if !ok {
|
||||
return result, fmt.Errorf("expected record to be of type %T, got %T", types.String{}, elStr)
|
||||
}
|
||||
result = append(result, elStr.ValueString())
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// SimplifyBackupSchedule removes leading 0s from backup schedule numbers (e.g. "00 00 * * *" becomes "0 0 * * *")
|
||||
// Needed as the API does it internally and would otherwise cause inconsistent result in Terraform
|
||||
func SimplifyBackupSchedule(schedule string) string {
|
||||
regex := regexp.MustCompile(`0+\d+`) // Matches series of one or more zeros followed by a series of one or more digits
|
||||
simplifiedSchedule := regex.ReplaceAllStringFunc(schedule, func(match string) string {
|
||||
simplified := strings.TrimLeft(match, "0")
|
||||
if simplified == "" {
|
||||
simplified = "0"
|
||||
}
|
||||
return simplified
|
||||
})
|
||||
return simplifiedSchedule
|
||||
}
|
||||
|
||||
// ConvertPointerSliceToStringSlice safely converts a slice of string pointers to a slice of strings.
|
||||
func ConvertPointerSliceToStringSlice(pointerSlice []*string) []string {
|
||||
if pointerSlice == nil {
|
||||
return []string{}
|
||||
}
|
||||
stringSlice := make([]string, 0, len(pointerSlice))
|
||||
for _, strPtr := range pointerSlice {
|
||||
if strPtr != nil { // Safely skip any nil pointers in the list
|
||||
stringSlice = append(stringSlice, *strPtr)
|
||||
}
|
||||
}
|
||||
return stringSlice
|
||||
}
|
||||
|
||||
func IsLegacyProjectRole(role string) bool {
|
||||
return utils.Contains(LegacyProjectRoles, role)
|
||||
}
|
||||
|
||||
type value interface {
|
||||
IsUnknown() bool
|
||||
IsNull() bool
|
||||
}
|
||||
|
||||
// IsUndefined checks if a passed value is unknown or null
|
||||
func IsUndefined(val value) bool {
|
||||
return val.IsUnknown() || val.IsNull()
|
||||
}
|
||||
|
||||
// LogError logs errors. In descriptions different messages for http status codes can be passed. When no one matches the defaultDescription will be used
|
||||
func LogError(ctx context.Context, inputDiags *diag.Diagnostics, err error, summary, defaultDescription string, descriptions map[int]string) {
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
tflog.Error(ctx, fmt.Sprintf("%s. Err: %v", summary, err))
|
||||
|
||||
var oapiErr *oapierror.GenericOpenAPIError
|
||||
ok := errors.As(err, &oapiErr)
|
||||
if !ok {
|
||||
core.LogAndAddError(ctx, inputDiags, summary, fmt.Sprintf("Calling API: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
var description string
|
||||
if len(descriptions) != 0 {
|
||||
description, ok = descriptions[oapiErr.StatusCode]
|
||||
}
|
||||
if !ok || description == "" {
|
||||
description = defaultDescription
|
||||
}
|
||||
core.LogAndAddError(ctx, inputDiags, summary, description)
|
||||
}
|
||||
|
||||
// FormatPossibleValues formats a slice into a comma-separated-list for usage in the provider docs
|
||||
func FormatPossibleValues(values ...string) string {
|
||||
var formattedValues []string
|
||||
for _, value := range values {
|
||||
formattedValues = append(formattedValues, fmt.Sprintf("`%v`", value))
|
||||
}
|
||||
return fmt.Sprintf("Possible values are: %s.", strings.Join(formattedValues, ", "))
|
||||
}
|
||||
|
||||
func BuildInternalTerraformId(idParts ...string) types.String {
|
||||
return types.StringValue(strings.Join(idParts, core.Separator))
|
||||
}
|
||||
|
||||
// If a List was completely removed from the terraform config this is not recognized by terraform.
|
||||
// This helper function checks if that is the case and adjusts the plan accordingly.
|
||||
func CheckListRemoval(ctx context.Context, configModelList, planModelList types.List, destination path.Path, listType attr.Type, createEmptyList bool, resp *resource.ModifyPlanResponse) {
|
||||
if configModelList.IsNull() && !planModelList.IsNull() {
|
||||
if createEmptyList {
|
||||
emptyList, _ := types.ListValueFrom(ctx, listType, []string{})
|
||||
resp.Diagnostics.Append(resp.Plan.SetAttribute(ctx, destination, emptyList)...)
|
||||
} else {
|
||||
resp.Diagnostics.Append(resp.Plan.SetAttribute(ctx, destination, types.ListNull(listType))...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SetAndLogStateFields writes the given map of key-value pairs to the state
|
||||
func SetAndLogStateFields(ctx context.Context, diags *diag.Diagnostics, state *tfsdk.State, values map[string]any) {
|
||||
for key, val := range values {
|
||||
ctx = tflog.SetField(ctx, key, val)
|
||||
diags.Append(state.SetAttribute(ctx, path.Root(key), val)...)
|
||||
}
|
||||
}
|
||||
614
stackit/internal/utils/utils_test.go
Normal file
614
stackit/internal/utils/utils_test.go
Normal file
|
|
@ -0,0 +1,614 @@
|
|||
// Copyright (c) STACKIT
|
||||
|
||||
package utils
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/terraform-plugin-framework/diag"
|
||||
"github.com/hashicorp/terraform-plugin-go/tftypes"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/hashicorp/terraform-plugin-framework/attr"
|
||||
"github.com/hashicorp/terraform-plugin-framework/datasource/schema"
|
||||
"github.com/hashicorp/terraform-plugin-framework/path"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource"
|
||||
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
|
||||
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
|
||||
"github.com/stackitcloud/stackit-sdk-go/core/utils"
|
||||
)
|
||||
|
||||
func TestReconcileStrLists(t *testing.T) {
|
||||
tests := []struct {
|
||||
description string
|
||||
list1 []string
|
||||
list2 []string
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
"empty lists",
|
||||
[]string{},
|
||||
[]string{},
|
||||
[]string{},
|
||||
},
|
||||
{
|
||||
"list1 empty",
|
||||
[]string{},
|
||||
[]string{"a", "b", "c"},
|
||||
[]string{"a", "b", "c"},
|
||||
},
|
||||
{
|
||||
"list2 empty",
|
||||
[]string{"a", "b", "c"},
|
||||
[]string{},
|
||||
[]string{},
|
||||
},
|
||||
{
|
||||
"no common elements",
|
||||
[]string{"a", "b", "c"},
|
||||
[]string{"d", "e", "f"},
|
||||
[]string{"d", "e", "f"},
|
||||
},
|
||||
{
|
||||
"common elements",
|
||||
[]string{"d", "a", "c"},
|
||||
[]string{"b", "c", "d", "e"},
|
||||
[]string{"d", "c", "b", "e"},
|
||||
},
|
||||
{
|
||||
"common elements with empty string",
|
||||
[]string{"d", "", "c"},
|
||||
[]string{"", "c", "d"},
|
||||
[]string{"d", "", "c"},
|
||||
},
|
||||
{
|
||||
"common elements with duplicates",
|
||||
[]string{"a", "b", "c", "c"},
|
||||
[]string{"b", "c", "d", "e"},
|
||||
[]string{"b", "c", "c", "d", "e"},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.description, func(t *testing.T) {
|
||||
output := ReconcileStringSlices(tt.list1, tt.list2)
|
||||
diff := cmp.Diff(output, tt.expected)
|
||||
if diff != "" {
|
||||
t.Fatalf("Data does not match: %s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestListValuetoStrSlice(t *testing.T) {
|
||||
tests := []struct {
|
||||
description string
|
||||
input basetypes.ListValue
|
||||
expected []string
|
||||
isValid bool
|
||||
}{
|
||||
{
|
||||
description: "empty list",
|
||||
input: types.ListValueMust(types.StringType, []attr.Value{}),
|
||||
expected: []string{},
|
||||
isValid: true,
|
||||
},
|
||||
{
|
||||
description: "values ok",
|
||||
input: types.ListValueMust(types.StringType, []attr.Value{
|
||||
types.StringValue("a"),
|
||||
types.StringValue("b"),
|
||||
types.StringValue("c"),
|
||||
}),
|
||||
expected: []string{"a", "b", "c"},
|
||||
isValid: true,
|
||||
},
|
||||
{
|
||||
description: "different type",
|
||||
input: types.ListValueMust(types.Int64Type, []attr.Value{
|
||||
types.Int64Value(12),
|
||||
}),
|
||||
isValid: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.description, func(t *testing.T) {
|
||||
output, err := ListValuetoStringSlice(tt.input)
|
||||
if err != nil {
|
||||
if !tt.isValid {
|
||||
return
|
||||
}
|
||||
t.Fatalf("Should not have failed: %v", err)
|
||||
}
|
||||
if !tt.isValid {
|
||||
t.Fatalf("Should have failed")
|
||||
}
|
||||
diff := cmp.Diff(output, tt.expected)
|
||||
if diff != "" {
|
||||
t.Fatalf("Data does not match: %s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertPointerSliceToStringSlice(t *testing.T) {
|
||||
tests := []struct {
|
||||
description string
|
||||
input []*string
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
description: "nil slice",
|
||||
input: nil,
|
||||
expected: []string{},
|
||||
},
|
||||
{
|
||||
description: "empty slice",
|
||||
input: []*string{},
|
||||
expected: []string{},
|
||||
},
|
||||
{
|
||||
description: "slice with valid pointers",
|
||||
input: []*string{utils.Ptr("apple"), utils.Ptr("banana"), utils.Ptr("cherry")},
|
||||
expected: []string{"apple", "banana", "cherry"},
|
||||
},
|
||||
{
|
||||
description: "slice with some nil pointers",
|
||||
input: []*string{utils.Ptr("apple"), nil, utils.Ptr("cherry"), nil},
|
||||
expected: []string{"apple", "cherry"},
|
||||
},
|
||||
{
|
||||
description: "slice with all nil pointers",
|
||||
input: []*string{nil, nil, nil},
|
||||
expected: []string{},
|
||||
},
|
||||
{
|
||||
description: "slice with a pointer to an empty string",
|
||||
input: []*string{utils.Ptr("apple"), utils.Ptr(""), utils.Ptr("cherry")},
|
||||
expected: []string{"apple", "", "cherry"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.description, func(t *testing.T) {
|
||||
output := ConvertPointerSliceToStringSlice(tt.input)
|
||||
diff := cmp.Diff(output, tt.expected)
|
||||
if diff != "" {
|
||||
t.Fatalf("Data does not match: %s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSimplifyBackupSchedule(t *testing.T) {
|
||||
tests := []struct {
|
||||
description string
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
"simple schedule",
|
||||
"0 0 * * *",
|
||||
"0 0 * * *",
|
||||
},
|
||||
{
|
||||
"schedule with leading zeros",
|
||||
"00 00 * * *",
|
||||
"0 0 * * *",
|
||||
},
|
||||
{
|
||||
"schedule with leading zeros 2",
|
||||
"00 001 * * *",
|
||||
"0 1 * * *",
|
||||
},
|
||||
{
|
||||
"schedule with leading zeros 3",
|
||||
"00 0010 * * *",
|
||||
"0 10 * * *",
|
||||
},
|
||||
{
|
||||
"simple schedule with slash",
|
||||
"0 0/6 * * *",
|
||||
"0 0/6 * * *",
|
||||
},
|
||||
{
|
||||
"schedule with leading zeros and slash",
|
||||
"00 00/6 * * *",
|
||||
"0 0/6 * * *",
|
||||
},
|
||||
{
|
||||
"schedule with leading zeros and slash 2",
|
||||
"00 001/06 * * *",
|
||||
"0 1/6 * * *",
|
||||
},
|
||||
{
|
||||
"simple schedule with comma",
|
||||
"0 10,15 * * *",
|
||||
"0 10,15 * * *",
|
||||
},
|
||||
{
|
||||
"schedule with leading zeros and comma",
|
||||
"0 010,0015 * * *",
|
||||
"0 10,15 * * *",
|
||||
},
|
||||
{
|
||||
"simple schedule with comma and slash",
|
||||
"0 0-11/10 * * *",
|
||||
"0 0-11/10 * * *",
|
||||
},
|
||||
{
|
||||
"schedule with leading zeros, comma, and slash",
|
||||
"00 000-011/010 * * *",
|
||||
"0 0-11/10 * * *",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.description, func(t *testing.T) {
|
||||
output := SimplifyBackupSchedule(tt.input)
|
||||
if output != tt.expected {
|
||||
t.Fatalf("Data does not match: %s", output)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsLegacyProjectRole(t *testing.T) {
|
||||
tests := []struct {
|
||||
description string
|
||||
role string
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
"non legacy role",
|
||||
"owner",
|
||||
false,
|
||||
},
|
||||
{
|
||||
"leagcy role",
|
||||
"project.owner",
|
||||
true,
|
||||
},
|
||||
{
|
||||
"leagcy role 2",
|
||||
"project.admin",
|
||||
true,
|
||||
},
|
||||
{
|
||||
"leagcy role 3",
|
||||
"project.member",
|
||||
true,
|
||||
},
|
||||
{
|
||||
"leagcy role 4",
|
||||
"project.auditor",
|
||||
true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.description, func(t *testing.T) {
|
||||
output := IsLegacyProjectRole(tt.role)
|
||||
if output != tt.expected {
|
||||
t.Fatalf("Data does not match: %v", output)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatPossibleValues(t *testing.T) {
|
||||
gotPrefix := "Possible values are:"
|
||||
|
||||
type args struct {
|
||||
values []string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "single string value",
|
||||
args: args{
|
||||
values: []string{"foo"},
|
||||
},
|
||||
want: fmt.Sprintf("%s `foo`.", gotPrefix),
|
||||
},
|
||||
{
|
||||
name: "multiple string value",
|
||||
args: args{
|
||||
values: []string{"foo", "bar", "trololol"},
|
||||
},
|
||||
want: fmt.Sprintf("%s `foo`, `bar`, `trololol`.", gotPrefix),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := FormatPossibleValues(tt.args.values...); got != tt.want {
|
||||
t.Errorf("FormatPossibleValues() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsUndefined(t *testing.T) {
|
||||
type args struct {
|
||||
val value
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "undefined value",
|
||||
args: args{
|
||||
val: types.StringNull(),
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "unknown value",
|
||||
args: args{
|
||||
val: types.StringUnknown(),
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "string value",
|
||||
args: args{
|
||||
val: types.StringValue(""),
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := IsUndefined(tt.args.val); got != tt.want {
|
||||
t.Errorf("IsUndefined() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildInternalTerraformId(t *testing.T) {
|
||||
type args struct {
|
||||
idParts []string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want types.String
|
||||
}{
|
||||
{
|
||||
name: "no id parts",
|
||||
args: args{
|
||||
idParts: []string{},
|
||||
},
|
||||
want: types.StringValue(""),
|
||||
},
|
||||
{
|
||||
name: "multiple id parts",
|
||||
args: args{
|
||||
idParts: []string{"abc", "foo", "bar", "xyz"},
|
||||
},
|
||||
want: types.StringValue("abc,foo,bar,xyz"),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := BuildInternalTerraformId(tt.args.idParts...); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("BuildInternalTerraformId() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckListRemoval(t *testing.T) {
|
||||
type model struct {
|
||||
AllowedAddresses types.List `tfsdk:"allowed_addresses"`
|
||||
}
|
||||
tests := []struct {
|
||||
description string
|
||||
configModelList types.List
|
||||
planModelList types.List
|
||||
path path.Path
|
||||
listType attr.Type
|
||||
createEmptyList bool
|
||||
expectedAdjustedResp bool
|
||||
}{
|
||||
{
|
||||
"config and plan are the same - no change",
|
||||
types.ListValueMust(types.StringType, []attr.Value{
|
||||
types.StringValue("value1"),
|
||||
}),
|
||||
types.ListValueMust(types.StringType, []attr.Value{
|
||||
types.StringValue("value1"),
|
||||
}),
|
||||
path.Root("allowed_addresses"),
|
||||
types.StringType,
|
||||
false,
|
||||
false,
|
||||
},
|
||||
{
|
||||
"list was removed from config",
|
||||
types.ListNull(types.StringType),
|
||||
types.ListValueMust(types.StringType, []attr.Value{
|
||||
types.StringValue("value1"),
|
||||
}),
|
||||
path.Root("allowed_addresses"),
|
||||
types.StringType,
|
||||
false,
|
||||
true,
|
||||
},
|
||||
{
|
||||
"list was added to config",
|
||||
types.ListValueMust(types.StringType, []attr.Value{
|
||||
types.StringValue("value1"),
|
||||
}),
|
||||
types.ListNull(types.StringType),
|
||||
path.Root("allowed_addresses"),
|
||||
types.StringType,
|
||||
false,
|
||||
false,
|
||||
},
|
||||
{
|
||||
"no list provided at all",
|
||||
types.ListNull(types.StringType),
|
||||
types.ListNull(types.StringType),
|
||||
path.Root("allowed_addresses"),
|
||||
types.StringType,
|
||||
false,
|
||||
false,
|
||||
},
|
||||
{
|
||||
"create empty list test - list was removed from config",
|
||||
types.ListNull(types.StringType),
|
||||
types.ListValueMust(types.StringType, []attr.Value{
|
||||
types.StringValue("value1"),
|
||||
}),
|
||||
path.Root("allowed_addresses"),
|
||||
types.StringType,
|
||||
true,
|
||||
true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.description, func(t *testing.T) {
|
||||
// create resp
|
||||
plan := tfsdk.Plan{
|
||||
Schema: schema.Schema{
|
||||
Attributes: map[string]schema.Attribute{
|
||||
"allowed_addresses": schema.ListAttribute{
|
||||
ElementType: basetypes.StringType{},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// set input planModelList to plan
|
||||
if diags := plan.Set(context.Background(), model{tt.planModelList}); diags.HasError() {
|
||||
t.Fatalf("cannot create test model: %v", diags)
|
||||
}
|
||||
resp := resource.ModifyPlanResponse{
|
||||
Plan: plan,
|
||||
}
|
||||
|
||||
CheckListRemoval(context.Background(), tt.configModelList, tt.planModelList, tt.path, tt.listType, tt.createEmptyList, &resp)
|
||||
// check targetList
|
||||
var respList types.List
|
||||
resp.Plan.GetAttribute(context.Background(), tt.path, &respList)
|
||||
|
||||
if tt.createEmptyList {
|
||||
emptyList, _ := types.ListValueFrom(context.Background(), tt.listType, []string{})
|
||||
diffEmptyList := cmp.Diff(emptyList, respList)
|
||||
if diffEmptyList != "" {
|
||||
t.Fatalf("an empty list should have been created but was not: %s", diffEmptyList)
|
||||
}
|
||||
}
|
||||
|
||||
// compare planModelList and resp list
|
||||
diff := cmp.Diff(tt.planModelList, respList)
|
||||
if tt.expectedAdjustedResp {
|
||||
if diff == "" {
|
||||
t.Fatalf("plan should be adjusted but was not")
|
||||
}
|
||||
} else {
|
||||
if diff != "" {
|
||||
t.Fatalf("plan should not be adjusted but diff is: %s", diff)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetAndLogStateFields(t *testing.T) {
|
||||
testSchema := schema.Schema{
|
||||
Attributes: map[string]schema.Attribute{
|
||||
"project_id": schema.StringAttribute{},
|
||||
"instance_id": schema.StringAttribute{},
|
||||
},
|
||||
}
|
||||
|
||||
type args struct {
|
||||
diags *diag.Diagnostics
|
||||
state *tfsdk.State
|
||||
values map[string]interface{}
|
||||
}
|
||||
type want struct {
|
||||
hasError bool
|
||||
state *tfsdk.State
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want want
|
||||
}{
|
||||
{
|
||||
name: "empty map",
|
||||
args: args{
|
||||
diags: &diag.Diagnostics{},
|
||||
state: &tfsdk.State{},
|
||||
values: map[string]interface{}{},
|
||||
},
|
||||
want: want{
|
||||
hasError: false,
|
||||
state: &tfsdk.State{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "base",
|
||||
args: args{
|
||||
diags: &diag.Diagnostics{},
|
||||
state: func() *tfsdk.State {
|
||||
ctx := context.Background()
|
||||
state := tfsdk.State{
|
||||
Raw: tftypes.NewValue(testSchema.Type().TerraformType(ctx), map[string]tftypes.Value{
|
||||
"project_id": tftypes.NewValue(tftypes.String, "9b15d120-86f8-45f5-81d8-a554f09c7582"),
|
||||
"instance_id": tftypes.NewValue(tftypes.String, nil),
|
||||
}),
|
||||
Schema: testSchema,
|
||||
}
|
||||
return &state
|
||||
}(),
|
||||
values: map[string]interface{}{
|
||||
"project_id": "a414f971-3f7a-4e9a-8671-51a8acb7bcc8",
|
||||
"instance_id": "97073250-8cad-46c3-8424-6258ac0b3731",
|
||||
},
|
||||
},
|
||||
want: want{
|
||||
hasError: false,
|
||||
state: func() *tfsdk.State {
|
||||
ctx := context.Background()
|
||||
state := tfsdk.State{
|
||||
Raw: tftypes.NewValue(testSchema.Type().TerraformType(ctx), map[string]tftypes.Value{
|
||||
"project_id": tftypes.NewValue(tftypes.String, nil),
|
||||
"instance_id": tftypes.NewValue(tftypes.String, nil),
|
||||
}),
|
||||
Schema: testSchema,
|
||||
}
|
||||
state.SetAttribute(ctx, path.Root("project_id"), "a414f971-3f7a-4e9a-8671-51a8acb7bcc8")
|
||||
state.SetAttribute(ctx, path.Root("instance_id"), "97073250-8cad-46c3-8424-6258ac0b3731")
|
||||
return &state
|
||||
}(),
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
SetAndLogStateFields(ctx, tt.args.diags, tt.args.state, tt.args.values)
|
||||
|
||||
if tt.args.diags.HasError() != tt.want.hasError {
|
||||
t.Errorf("TestSetAndLogStateFields() error count = %v, hasErr %v", tt.args.diags.ErrorsCount(), tt.want.hasError)
|
||||
}
|
||||
|
||||
diff := cmp.Diff(tt.args.state, tt.want.state)
|
||||
if diff != "" {
|
||||
t.Fatalf("Data does not match: %s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue