Alpha (#4)
Some checks failed
CI Workflow / CI (push) Has been cancelled
CI Workflow / Check GoReleaser config (push) Has been cancelled
CI Workflow / Code coverage report (push) Has been cancelled

* 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:
Marcel S. Henselin 2025-12-19 11:37:53 +01:00 committed by GitHub
parent 45073a716b
commit 2733834fc9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
351 changed files with 62744 additions and 3 deletions

View 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
}

View 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)
}
}
}
})
}
}

View 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))
}

View 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)
}
})
}
}

View 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)...)
}

View 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)
}
})
}
}

View 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
}
}

View 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)
}
})
}
}

View 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)...)
}
}

View 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)
}
})
}
}