diff --git a/golang-ci.yaml b/golang-ci.yaml index 3487f7456..be069a611 100644 --- a/golang-ci.yaml +++ b/golang-ci.yaml @@ -1,95 +1,289 @@ -# This file contains all available configuration options -# with their default values. - -# options for analysis running +version: "2" run: - # default concurrency is a available CPU number - concurrency: 4 - - # timeout for analysis, e.g. 30s, 5m, default is 1m - timeout: 5m -linters-settings: - goimports: - # put imports beginning with prefix after 3rd-party packages; - # it's a comma-separated list of prefixes - local-prefixes: github.com/freiheit-com/nmww - depguard: - rules: - main: - list-mode: lax # Everything is allowed unless it is denied - deny: - - pkg: "github.com/stretchr/testify" - desc: Do not use a testing framework - misspell: - # Correct spellings using locale preferences for US or UK. - # Default is to use a neutral variety of English. - # Setting locale to US will correct the British spelling of 'colour' to 'color'. - locale: US - golint: - min-confidence: 0.8 - gosec: - excludes: - # Suppressions: (see https://github.com/securego/gosec#available-rules for details) - - G104 # "Audit errors not checked" -> which we don't need and is a badly implemented version of errcheck - - G102 # "Bind to all interfaces" -> since this is normal in k8s - - G304 # "File path provided as taint input" -> too many false positives - - G307 # "Deferring unsafe method "Close" on type "io.ReadCloser" -> false positive when calling defer resp.Body.Close() - nakedret: - max-func-lines: 0 - revive: - ignore-generated-header: true - severity: error - # https://github.com/mgechev/revive - rules: - - name: errorf - - name: context-as-argument - - name: error-return - - name: increment-decrement - - name: indent-error-flow - - name: superfluous-else - - name: unused-parameter - - name: unreachable-code - - name: atomic - - name: empty-lines - - name: early-return - gocritic: - enabled-tags: - - performance - - style - - experimental - disabled-checks: - - wrapperFunc - - typeDefFirst - - ifElseChain - - dupImport # https://github.com/go-critic/go-critic/issues/845 + build-tags: + - integration + issues-exit-code: 1 + tests: true linters: + default: none enable: - # https://golangci-lint.run/usage/linters/ - # default linters - - gosimple - - govet - - ineffassign - - staticcheck - - typecheck - - unused - # additional linters + - asciicheck + - bodyclose + - cyclop + - depguard + - dogsled + - dupl + - durationcheck + - err113 + - errcheck - errorlint + - exhaustive + - forbidigo + - forcetypeassert + - funlen + - gochecknoglobals - gochecknoinits + - gocognit + - goconst - gocritic - - gofmt - - goimports + - gocyclo + - godot + - gomoddirectives + - gomodguard + - goprintffuncname - gosec + - govet + - importas + - ineffassign + - lll + - makezero - misspell + - mnd - nakedret + - nestif + - nilerr + - nlreturn + - noctx + - nolintlint + - prealloc + - predeclared + - promlinter - revive - - depguard - - bodyclose + - rowserrcheck + - sloglint - sqlclosecheck + - staticcheck + - tparallel + - unconvert + - unparam + - unused - wastedassign - - forcetypeassert - - errcheck - disable: - - noctx # false positive: finds errors with http.NewRequest that dont make sense - - unparam # false positives + - whitespace + - wsl + settings: + cyclop: + max-complexity: 20 + package-average: 0 + depguard: + rules: + all: + deny: + - pkg: github.com/sirupsen/logrus + desc: logging is done using the log/slog package + - pkg: log$ + desc: logging is done using the log/slog package + - pkg: go.uber.org/zap + desc: logging is done using the log/slog package + dogsled: + max-blank-identifiers: 2 + dupl: + threshold: 150 + errcheck: + check-type-assertions: true + check-blank: true + exhaustive: + default-signifies-exhaustive: true + funlen: + lines: 100 + statements: 50 + gocognit: + min-complexity: 30 + goconst: + min-len: 3 + min-occurrences: 2 + gocritic: + disabled-checks: + - dupImport + - octalLiteral + - unnamedResult + enabled-tags: + - diagnostic + - experimental + - opinionated + - performance + - style + settings: + hugeParam: + sizeThreshold: 80 + gocyclo: + min-complexity: 30 + govet: + disable: + - fieldalignment + enable-all: true + lll: + line-length: 140 + tab-width: 1 + misspell: + locale: US + mnd: + checks: + - argument + - case + - condition + - return + nakedret: + max-func-lines: 5 + nlreturn: + block-size: 5 + nolintlint: + require-explanation: true + require-specific: true + allow-unused: false + prealloc: + simple: true + range-loops: true + for-loops: true + revive: + rules: + - name: context-keys-type + disabled: false + - name: time-naming + disabled: false + - name: var-declaration + disabled: false + - name: unexported-return + disabled: false + - name: errorf + disabled: false + - name: blank-imports + disabled: false + - name: context-as-argument + disabled: false + - name: dot-imports + disabled: false + - name: error-return + disabled: false + - name: error-strings + disabled: false + - name: error-naming + disabled: false + - name: exported + disabled: false + - name: increment-decrement + disabled: false + - name: var-naming + disabled: false + - name: package-comments + disabled: false + - name: range + disabled: false + - name: receiver-naming + disabled: false + - name: indent-error-flow + disabled: false + sloglint: + no-mixed-args: true + no-global: all + context: scope + static-msg: true + forbidden-keys: + - time + - level + - msg + - source + staticcheck: + initialisms: + - ACL + - API + - ASCII + - CPU + - CSS + - DNS + - EOF + - GUID + - HTML + - HTTP + - HTTPS + - ID + - IP + - JSON + - QPS + - RAM + - RPC + - SLA + - SMTP + - SQL + - SSH + - TCP + - TLS + - TTL + - UDP + - UI + - GID + - UID + - UUID + - URI + - URL + - UTF8 + - VM + - XML + - XMPP + - XSRF + - XSS + - SIP + - RTP + - AMQP + - DB + - TS + unparam: + check-exported: false + unused: + exported-fields-are-used: true + whitespace: + multi-if: false + multi-func: false + exclusions: + generated: lax + presets: + - comments + - common-false-positives + - legacy + - std-error-handling + rules: + - linters: + - dogsled + - dupl + - err113 + - forcetypeassert + - funlen + - gochecknoglobals + - goconst + - mnd + - noctx + - unparam + path: _test\.go + - linters: + - govet + text: declaration of "err" shadows declaration + paths: + - third_party$ + - builtin$ + - examples$ issues: - exclude-use-default: false + max-issues-per-linter: 0 + max-same-issues: 0 +formatters: + enable: + - gci + - gofmt + - gofumpt + - goimports + settings: + gci: + sections: + - standard + - default + - prefix(dev.azure.com/schwarzit) + custom-order: true + gofmt: + simplify: true + goimports: + local-prefixes: + - dev.azure.com/schwarzit + exclusions: + generated: lax + paths: + - third_party$ + - builtin$ + - examples$ diff --git a/main.go b/main.go index 2b958c430..255fe8fc8 100644 --- a/main.go +++ b/main.go @@ -9,15 +9,15 @@ import ( "github.com/stackitcloud/terraform-provider-stackit/stackit" ) -var ( - // goreleaser configuration will override this value - version string = "dev" -) +// goreleaser configuration will override this value. +var version string = "dev" func main() { var debug bool + flag.BoolVar(&debug, "debug", false, "allows debugging the provider") flag.Parse() + err := providerserver.Serve(context.Background(), stackit.New(version), providerserver.ServeOpts{ Address: "registry.terraform.io/stackitcloud/stackit", Debug: debug, diff --git a/scripts/project.sh b/scripts/project.sh index 91bb1efd3..a7d45abab 100755 --- a/scripts/project.sh +++ b/scripts/project.sh @@ -16,7 +16,7 @@ elif [ "$action" = "tools" ]; then go mod download - go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.62.0 + go install github.com/golangci/golangci-lint/cmd/golangci-lint@v2.6.0 go install github.com/hashicorp/terraform-plugin-docs/cmd/tfplugindocs@v0.21.0 else echo "Invalid action: '$action', please use $0 help for help" diff --git a/stackit/internal/conversion/conversion.go b/stackit/internal/conversion/conversion.go index 312535f01..f03206bed 100644 --- a/stackit/internal/conversion/conversion.go +++ b/stackit/internal/conversion/conversion.go @@ -4,9 +4,8 @@ import ( "context" "fmt" - "github.com/hashicorp/terraform-plugin-framework/diag" - "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" @@ -16,27 +15,33 @@ func ToString(ctx context.Context, v attr.Value) (string, error) { if t := v.Type(ctx); t != types.StringType { return "", fmt.Errorf("type mismatch. expected 'types.StringType' but got '%s'", t.String()) } + if v.IsNull() || v.IsUnknown() { return "", fmt.Errorf("value is unknown or null") } + tv, err := v.ToTerraformValue(ctx) if err != nil { return "", err } + var s string if err := tv.Copy().As(&s); err != nil { return "", err } + return s, nil } func ToOptStringMap(tfMap map[string]attr.Value) (*map[string]string, error) { //nolint: gocritic //pointer needed to map optional fields labels := make(map[string]string, len(tfMap)) + for l, v := range tfMap { valueString, ok := v.(types.String) if !ok { return nil, fmt.Errorf("error converting map value: expected to string, got %v", v) } + labels[l] = valueString.ValueString() } @@ -44,15 +49,18 @@ func ToOptStringMap(tfMap map[string]attr.Value) (*map[string]string, error) { / if len(labels) == 0 { labelsPointer = nil } + return labelsPointer, nil } func ToTerraformStringMap(ctx context.Context, m map[string]string) (basetypes.MapValue, error) { labels := make(map[string]attr.Value, len(m)) + for l, v := range m { stringValue := types.StringValue(v) labels[l] = stringValue } + res, diags := types.MapValueFrom(ctx, types.StringType, m) if diags.HasError() { return types.MapNull(types.StringType), fmt.Errorf("converting to MapValue: %v", diags.Errors()) @@ -64,6 +72,7 @@ func ToTerraformStringMap(ctx context.Context, m map[string]string) (basetypes.M // ToStringInterfaceMap converts a basetypes.MapValue of Strings to a map[string]interface{}. func ToStringInterfaceMap(ctx context.Context, m basetypes.MapValue) (map[string]interface{}, error) { labels := map[string]string{} + diags := m.ElementsAs(ctx, &labels, false) if diags.HasError() { return nil, fmt.Errorf("converting from MapValue: %w", core.DiagsToError(diags)) @@ -83,7 +92,9 @@ func StringValueToPointer(s basetypes.StringValue) *string { if s.IsNull() || s.IsUnknown() { return nil } + value := s.ValueString() + return &value } @@ -93,7 +104,9 @@ func Int64ValueToPointer(s basetypes.Int64Value) *int64 { if s.IsNull() || s.IsUnknown() { return nil } + value := s.ValueInt64() + return &value } @@ -103,7 +116,9 @@ func Float64ValueToPointer(s basetypes.Float64Value) *float64 { if s.IsNull() || s.IsUnknown() { return nil } + value := s.ValueFloat64() + return &value } @@ -113,7 +128,9 @@ func BoolValueToPointer(s basetypes.BoolValue) *bool { if s.IsNull() || s.IsUnknown() { return nil } + value := s.ValueBool() + return &value } @@ -125,11 +142,13 @@ func StringListToPointer(list basetypes.ListValue) (*[]string, error) { } listStr := []string{} + for i, el := range list.Elements() { elStr, ok := el.(types.String) if !ok { return nil, fmt.Errorf("element %d is not a string", i) } + listStr = append(listStr, elStr.ValueString()) } @@ -139,7 +158,7 @@ func StringListToPointer(list basetypes.ListValue) (*[]string, error) { // ToJSONMApPartialUpdatePayload returns a map[string]interface{} to be used in a PATCH request payload. // It takes a current map as it is in the terraform state and a desired map as it is in the user configuratiom // and builds a map which sets to null keys that should be removed, updates the values of existing keys and adds new keys -// This method is needed because in partial updates, e.g. if the key is not provided it is ignored and not removed +// This method is needed because in partial updates, e.g. if the key is not provided it is ignored and not removed. func ToJSONMapPartialUpdatePayload(ctx context.Context, current, desired types.Map) (map[string]interface{}, error) { currentMap, err := ToStringInterfaceMap(ctx, current) if err != nil { @@ -167,6 +186,7 @@ func ToJSONMapPartialUpdatePayload(ctx context.Context, current, desired types.M mapPayload[k] = desiredValue } } + return mapPayload, nil } @@ -181,5 +201,6 @@ func ParseProviderData(ctx context.Context, providerData any, diags *diag.Diagno core.LogAndAddError(ctx, diags, "Error configuring API client", fmt.Sprintf("Expected configure type stackit.ProviderData, got %T", providerData)) return core.ProviderData{}, false } + return stackitProviderData, true } diff --git a/stackit/internal/conversion/conversion_test.go b/stackit/internal/conversion/conversion_test.go index 1d652aead..ba2beaf19 100644 --- a/stackit/internal/conversion/conversion_test.go +++ b/stackit/internal/conversion/conversion_test.go @@ -5,13 +5,12 @@ import ( "reflect" "testing" - "github.com/hashicorp/terraform-plugin-framework/diag" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/google/go-cmp/cmp" "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" ) func TestFromTerraformStringMapToInterfaceMap(t *testing.T) { @@ -19,6 +18,7 @@ func TestFromTerraformStringMapToInterfaceMap(t *testing.T) { ctx context.Context m basetypes.MapValue } + tests := []struct { name string args args @@ -78,6 +78,7 @@ func TestFromTerraformStringMapToInterfaceMap(t *testing.T) { t.Errorf("FromTerraformStringMapToInterfaceMap() error = %v, wantErr %v", err, tt.wantErr) return } + if !reflect.DeepEqual(got, tt.want) { t.Errorf("FromTerraformStringMapToInterfaceMap() = %v, want %v", got, tt.want) } @@ -208,9 +209,11 @@ func TestToJSONMapUpdatePayload(t *testing.T) { if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(output, tt.expected) if diff != "" { @@ -225,10 +228,12 @@ func TestParseProviderData(t *testing.T) { type args struct { providerData any } + type want struct { ok bool providerData core.ProviderData } + tests := []struct { name string args args @@ -292,12 +297,15 @@ func TestParseProviderData(t *testing.T) { diags := diag.Diagnostics{} actual, ok := ParseProviderData(ctx, tt.args.providerData, &diags) + if diags.HasError() != tt.wantErr { t.Errorf("ConfigureClient() error = %v, want %v", diags.HasError(), tt.wantErr) } + if ok != tt.want.ok { t.Errorf("ParseProviderData() got = %v, want %v", ok, tt.want.ok) } + if !reflect.DeepEqual(actual, tt.want.providerData) { t.Errorf("ParseProviderData() got = %v, want %v", actual, tt.want) } diff --git a/stackit/internal/core/core.go b/stackit/internal/core/core.go index d993cddb3..2b9f9a6ec 100644 --- a/stackit/internal/core/core.go +++ b/stackit/internal/core/core.go @@ -6,13 +6,12 @@ import ( "net/http" "strings" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" ) -// Separator used for concatenation of TF-internal resource ID +// Separator used for concatenation of TF-internal resource ID. const Separator = "," type ResourceType string @@ -59,7 +58,7 @@ type ProviderData struct { Version string // version of the STACKIT Terraform provider } -// GetRegion returns the effective region for the provider, falling back to the deprecated _region_ attribute +// GetRegion returns the effective region for the provider, falling back to the deprecated _region_ attribute. func (pd *ProviderData) GetRegion() string { if pd.DefaultRegion != "" { return pd.DefaultRegion @@ -74,11 +73,12 @@ func (pd *ProviderData) GetRegionWithOverride(overrideRegion types.String) strin if overrideRegion.IsUnknown() || overrideRegion.IsNull() { return pd.GetRegion() } + return overrideRegion.ValueString() } // DiagsToError Converts TF diagnostics' errors into an error with a human-readable description. -// If there are no errors, the output is nil +// If there are no errors, the output is nil. func DiagsToError(diags diag.Diagnostics) error { if !diags.HasError() { return nil @@ -86,6 +86,7 @@ func DiagsToError(diags diag.Diagnostics) error { diagsError := diags.Errors() diagsStrings := make([]string, 0) + for _, diagnostic := range diagsError { diagsStrings = append(diagsStrings, fmt.Sprintf( "(%s) %s", @@ -93,16 +94,17 @@ func DiagsToError(diags diag.Diagnostics) error { diagnostic.Detail(), )) } + return fmt.Errorf("%s", strings.Join(diagsStrings, ";")) } -// LogAndAddError Logs the error and adds it to the diags +// LogAndAddError Logs the error and adds it to the diags. func LogAndAddError(ctx context.Context, diags *diag.Diagnostics, summary, detail string) { tflog.Error(ctx, fmt.Sprintf("%s | %s", summary, detail)) diags.AddError(summary, detail) } -// LogAndAddWarning Logs the warning and adds it to the diags +// LogAndAddWarning Logs the warning and adds it to the diags. func LogAndAddWarning(ctx context.Context, diags *diag.Diagnostics, summary, detail string) { tflog.Warn(ctx, fmt.Sprintf("%s | %s", summary, detail)) diags.AddWarning(summary, detail) diff --git a/stackit/internal/core/core_test.go b/stackit/internal/core/core_test.go index 8824e870c..79369664e 100644 --- a/stackit/internal/core/core_test.go +++ b/stackit/internal/core/core_test.go @@ -10,6 +10,7 @@ func TestProviderData_GetRegionWithOverride(t *testing.T) { type args struct { overrideRegion types.String } + tests := []struct { name string providerData *ProviderData diff --git a/stackit/internal/features/beta.go b/stackit/internal/features/beta.go index 4354fbd5c..fe6127a68 100644 --- a/stackit/internal/features/beta.go +++ b/stackit/internal/features/beta.go @@ -21,9 +21,11 @@ func BetaResourcesEnabled(ctx context.Context, data *core.ProviderData, diags *d if strings.EqualFold(value, "true") { return true } + if strings.EqualFold(value, "false") { return false } + warnDetails := fmt.Sprintf(`The value of the environment variable that enables beta functionality must be either "true" or "false", got %q. Defaulting to the provider feature flag.`, value) core.LogAndAddWarning(ctx, diags, "Invalid value for STACKIT_TF_ENABLE_BETA_RESOURCES environment variable.", warnDetails) @@ -32,6 +34,7 @@ Defaulting to the provider feature flag.`, value) if data == nil { return false } + return data.EnableBetaResources } @@ -44,6 +47,7 @@ func CheckBetaResourcesEnabled(ctx context.Context, data *core.ProviderData, dia core.LogAndAddErrorBeta(ctx, diags, resourceName, resourceType) return } + core.LogAndAddWarningBeta(ctx, diags, resourceName, resourceType) } diff --git a/stackit/internal/features/beta_test.go b/stackit/internal/features/beta_test.go index 242636c50..edb358ce2 100644 --- a/stackit/internal/features/beta_test.go +++ b/stackit/internal/features/beta_test.go @@ -149,6 +149,7 @@ func TestBetaResourcesEnabled(t *testing.T) { if tt.envSet { t.Setenv("STACKIT_TF_ENABLE_BETA_RESOURCES", tt.envValue) } + diags := diag.Diagnostics{} result := BetaResourcesEnabled(context.Background(), tt.data, &diags) @@ -159,6 +160,7 @@ func TestBetaResourcesEnabled(t *testing.T) { if tt.expectWarn && diags.WarningsCount() == 0 { t.Fatalf("Expected warning, got none") } + if !tt.expectWarn && diags.WarningsCount() > 0 { t.Fatalf("Expected no warning, got %d", diags.WarningsCount()) } @@ -193,6 +195,7 @@ func TestCheckBetaResourcesEnabled(t *testing.T) { } else { envValue = "false" } + t.Setenv("STACKIT_TF_ENABLE_BETA_RESOURCES", envValue) diags := diag.Diagnostics{} @@ -201,6 +204,7 @@ func TestCheckBetaResourcesEnabled(t *testing.T) { if tt.expectError && diags.ErrorsCount() == 0 { t.Fatalf("Expected error, got none") } + if !tt.expectError && diags.ErrorsCount() > 0 { t.Fatalf("Expected no error, got %d", diags.ErrorsCount()) } @@ -208,6 +212,7 @@ func TestCheckBetaResourcesEnabled(t *testing.T) { if tt.expectWarn && diags.WarningsCount() == 0 { t.Fatalf("Expected warning, got none") } + if !tt.expectWarn && diags.WarningsCount() > 0 { t.Fatalf("Expected no warning, got %d", diags.WarningsCount()) } diff --git a/stackit/internal/features/experiments.go b/stackit/internal/features/experiments.go index 351930484..b70b88ca9 100644 --- a/stackit/internal/features/experiments.go +++ b/stackit/internal/features/experiments.go @@ -36,6 +36,7 @@ func CheckExperimentEnabled(ctx context.Context, data *core.ProviderData, experi if CheckExperimentEnabledWithoutError(ctx, data, experiment, resourceName, resourceType, diags) { return } + errTitle := fmt.Sprintf("%s is part of the %s experiment, which is currently disabled by default", resourceName, experiment) errContent := fmt.Sprintf(`Enable the %s experiment by adding it into your provider block.`, experiment) tflog.Error(ctx, fmt.Sprintf("%s | %s", errTitle, errContent)) @@ -47,8 +48,10 @@ func CheckExperimentEnabledWithoutError(ctx context.Context, data *core.Provider errTitle := fmt.Sprintf("The experiment %s does not exist.", experiment) errContent := "This is a bug in the STACKIT Terraform Provider. Please open an issue here: https://github.com/stackitcloud/terraform-provider-stackit/issues" diags.AddError(errTitle, errContent) + return false } + experimentActive := slices.ContainsFunc(data.Experiments, func(e string) bool { return strings.EqualFold(e, experiment) }) @@ -58,8 +61,10 @@ func CheckExperimentEnabledWithoutError(ctx context.Context, data *core.Provider warnContent := fmt.Sprintf("This %s is part of the %s experiment and is likely going to undergo significant changes or be removed in the future. Use it at your own discretion.", resourceType, experiment) tflog.Warn(ctx, fmt.Sprintf("%s | %s", warnTitle, warnContent)) diags.AddWarning(warnTitle, warnContent) + return true } + return false } diff --git a/stackit/internal/features/experiments_test.go b/stackit/internal/features/experiments_test.go index f86925978..76b3a918b 100644 --- a/stackit/internal/features/experiments_test.go +++ b/stackit/internal/features/experiments_test.go @@ -13,6 +13,7 @@ func TestValidExperiment(t *testing.T) { experiment string diags *diag.Diagnostics } + tests := []struct { name string args args @@ -53,6 +54,7 @@ func TestCheckExperimentEnabled(t *testing.T) { resourceType core.ResourceType diags *diag.Diagnostics } + tests := []struct { name string args args @@ -133,9 +135,11 @@ func TestCheckExperimentEnabled(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { CheckExperimentEnabled(tt.args.ctx, tt.args.data, tt.args.experiment, tt.args.resourceName, tt.args.resourceType, tt.args.diags) + if got := tt.args.diags.HasError(); got != tt.wantDiagsErr { t.Errorf("CheckExperimentEnabled() diags.HasError() = %v, want %v", got, tt.wantDiagsErr) } + if got := tt.args.diags.WarningsCount() > 0; got != tt.wantDiagsWarning { t.Errorf("CheckExperimentEnabled() diags.WarningsCount() > 0 = %v, want %v", got, tt.wantDiagsErr) } @@ -152,6 +156,7 @@ func TestCheckExperimentEnabledWithoutError(t *testing.T) { resourceType core.ResourceType diags *diag.Diagnostics } + tests := []struct { name string args args @@ -159,7 +164,6 @@ func TestCheckExperimentEnabledWithoutError(t *testing.T) { wantDiagsErr bool wantDiagsWarning bool }{ - { name: "enabled", args: args{ @@ -241,9 +245,11 @@ func TestCheckExperimentEnabledWithoutError(t *testing.T) { if got := CheckExperimentEnabledWithoutError(tt.args.ctx, tt.args.data, tt.args.experiment, tt.args.resourceName, tt.args.resourceType, tt.args.diags); got != tt.wantEnabled { t.Errorf("CheckExperimentEnabledWithoutError() = %v, want %v", got, tt.wantEnabled) } + if got := tt.args.diags.HasError(); got != tt.wantDiagsErr { t.Errorf("CheckExperimentEnabled() diags.HasError() = %v, want %v", got, tt.wantDiagsErr) } + if got := tt.args.diags.WarningsCount() > 0; got != tt.wantDiagsWarning { t.Errorf("CheckExperimentEnabled() diags.WarningsCount() > 0 = %v, want %v", got, tt.wantDiagsErr) } diff --git a/stackit/internal/services/authorization/authorization_acc_test.go b/stackit/internal/services/authorization/authorization_acc_test.go index 7fcede14d..ddd094fff 100644 --- a/stackit/internal/services/authorization/authorization_acc_test.go +++ b/stackit/internal/services/authorization/authorization_acc_test.go @@ -2,14 +2,13 @@ package authorization_test import ( "context" + _ "embed" "errors" "fmt" "regexp" "slices" "testing" - _ "embed" - "github.com/hashicorp/terraform-plugin-testing/config" "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/terraform" @@ -54,7 +53,6 @@ func TestAccProjectRoleAssignmentResource(t *testing.T) { } members, err := client.ListMembers(context.TODO(), "project", testutil.ProjectId).Execute() - if err != nil { return err } @@ -65,6 +63,7 @@ func TestAccProjectRoleAssignmentResource(t *testing.T) { t.Log(members.Members) return errors.New("Membership not found") } + return nil }, }, @@ -97,6 +96,7 @@ func TestAccProjectRoleAssignmentResource(t *testing.T) { func authApiClient() (*authorization.APIClient, error) { var client *authorization.APIClient + var err error if testutil.AuthorizationCustomEndpoint == "" { client, err = authorization.NewAPIClient( @@ -107,8 +107,10 @@ func authApiClient() (*authorization.APIClient, error) { stackitSdkConfig.WithEndpoint(testutil.AuthorizationCustomEndpoint), ) } + if err != nil { return nil, fmt.Errorf("creating client: %w", err) } + return client, nil } diff --git a/stackit/internal/services/authorization/roleassignments/resource.go b/stackit/internal/services/authorization/roleassignments/resource.go index 32d5909df..ce296ab9b 100644 --- a/stackit/internal/services/authorization/roleassignments/resource.go +++ b/stackit/internal/services/authorization/roleassignments/resource.go @@ -7,11 +7,6 @@ import ( "fmt" "strings" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - authorizationUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/authorization/utils" - "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" @@ -21,12 +16,15 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/stackitcloud/stackit-sdk-go/services/authorization" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/features" + authorizationUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/authorization/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" ) -// List of permission assignments targets in form [TF resource name]:[api name] +// List of permission assignments targets in form [TF resource name]:[api name]. var roleTargets = []string{ "project", "organization", @@ -39,10 +37,10 @@ var ( _ resource.ResourceWithImportState = &roleAssignmentResource{} errRoleAssignmentNotFound = errors.New("response members did not contain expected role assignment") - errRoleAssignmentDuplicateFound = errors.New("found a duplicate role assignment.") + errRoleAssignmentDuplicateFound = errors.New("found a duplicate role assignment") ) -// Provider's internal model +// Provider's internal model. type Model struct { Id types.String `tfsdk:"id"` // needed by TF ResourceId types.String `tfsdk:"resource_id"` @@ -60,6 +58,7 @@ func NewRoleAssignmentResources() []func() resource.Resource { } }) } + return resources } @@ -82,14 +81,17 @@ func (r *roleAssignmentResource) Configure(ctx context.Context, req resource.Con } features.CheckExperimentEnabled(ctx, &providerData, features.IamExperiment, fmt.Sprintf("stackit_authorization_%s_role_assignment", r.apiName), core.Resource, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } apiClient := authorizationUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + r.authorizationClient = apiClient tflog.Info(ctx, fmt.Sprintf("Resource Manager %s Role Assignment client configured", r.apiName)) } @@ -144,10 +146,11 @@ func (r *roleAssignmentResource) Schema(_ context.Context, _ resource.SchemaRequ } // Create creates the resource and sets the initial Terraform state. -func (r *roleAssignmentResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *roleAssignmentResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -165,6 +168,7 @@ func (r *roleAssignmentResource) Create(ctx context.Context, req resource.Create core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credential", fmt.Sprintf("Creating API payload: %v", err)) return } + createResp, err := r.authorizationClient.AddMembers(ctx, model.ResourceId.ValueString()).AddMembersPayload(*payload).Execute() if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, fmt.Sprintf("Error creating %s role assignment", r.apiName), fmt.Sprintf("Calling API: %v", err)) @@ -177,19 +181,23 @@ func (r *roleAssignmentResource) Create(ctx context.Context, req resource.Create core.LogAndAddError(ctx, &resp.Diagnostics, fmt.Sprintf("Error creating %s role assignment", r.apiName), fmt.Sprintf("Processing API payload: %v", err)) return } + diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, fmt.Sprintf("%s role assignment created", r.apiName)) } // Read refreshes the Terraform state with the latest data. -func (r *roleAssignmentResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (r *roleAssignmentResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -212,22 +220,25 @@ func (r *roleAssignmentResource) Read(ctx context.Context, req resource.ReadRequ // Set refreshed state diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, fmt.Sprintf("%s role assignment read successful", r.apiName)) } // Update updates the resource and sets the updated Terraform state on success. -func (r *roleAssignmentResource) Update(_ context.Context, _ resource.UpdateRequest, _ *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *roleAssignmentResource) Update(_ context.Context, _ resource.UpdateRequest, _ *resource.UpdateResponse) { //nolint:gocritic // function signature required by Terraform // does nothing since resource updates should always trigger resource replacement } // Delete deletes the resource and removes the Terraform state on success. -func (r *roleAssignmentResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform +func (r *roleAssignmentResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -251,7 +262,7 @@ func (r *roleAssignmentResource) Delete(ctx context.Context, req resource.Delete } // ImportState imports a resource into the Terraform state on success. -// The expected format of the project role assignment resource import identifier is: resource_id,role,subject +// The expected format of the project role assignment resource import identifier is: resource_id,role,subject. func (r *roleAssignmentResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { idParts := strings.Split(req.ID, core.Separator) if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" { @@ -259,6 +270,7 @@ func (r *roleAssignmentResource) ImportState(ctx context.Context, req resource.I fmt.Sprintf("Error importing %s role assignment", r.apiName), fmt.Sprintf("Expected import identifier with format [resource_id],[role],[subject], got %q", req.ID), ) + return } @@ -273,9 +285,11 @@ func mapListMembersResponse(resp *authorization.ListMembersResponse, model *Mode if resp == nil { return fmt.Errorf("response input is nil") } + if resp.Members == nil { return fmt.Errorf("response members are nil") } + if model == nil { return fmt.Errorf("model input is nil") } @@ -287,9 +301,11 @@ func mapListMembersResponse(resp *authorization.ListMembersResponse, model *Mode if *m.Role == model.Role.ValueString() && *m.Subject == model.Subject.ValueString() { model.Role = types.StringPointerValue(m.Role) model.Subject = types.StringPointerValue(m.Subject) + return nil } } + return errRoleAssignmentNotFound } @@ -298,24 +314,28 @@ func mapMembersResponse(resp *authorization.MembersResponse, model *Model) error if err != nil { return err } + return mapListMembersResponse(listMembersResponse, model) } -// Helper to convert objects with equal JSON tags +// Helper to convert objects with equal JSON tags. func typeConverter[R any](data any) (*R, error) { var result R + b, err := json.Marshal(&data) if err != nil { return nil, err } + err = json.Unmarshal(b, &result) if err != nil { return nil, err } + return &result, err } -// Build Createproject role assignmentPayload from provider's model +// Build Createproject role assignmentPayload from provider's model. func (r *roleAssignmentResource) toCreatePayload(model *Model) (*authorization.AddMembersPayload, error) { if model == nil { return nil, fmt.Errorf("nil model") @@ -335,10 +355,11 @@ func (r *roleAssignmentResource) annotateLogger(ctx context.Context, model *Mode ctx = tflog.SetField(ctx, "subject", model.Subject.ValueString()) ctx = tflog.SetField(ctx, "role", model.Role.ValueString()) ctx = tflog.SetField(ctx, "resource_type", r.apiName) + return ctx } -// returns an error if duplicate role assignment exists +// returns an error if duplicate role assignment exists. func (r *roleAssignmentResource) checkDuplicate(ctx context.Context, model Model) error { //nolint:gocritic // A read only copy is required since an api response is parsed into the model and this check should not affect the model parameter listResp, err := r.authorizationClient.ListMembers(ctx, r.apiName, model.ResourceId.ValueString()).Subject(model.Subject.ValueString()).Execute() if err != nil { @@ -347,12 +368,13 @@ func (r *roleAssignmentResource) checkDuplicate(ctx context.Context, model Model // Map response body to schema err = mapListMembersResponse(listResp, &model) - if err != nil { if errors.Is(err, errRoleAssignmentNotFound) { return nil } + return err } + return errRoleAssignmentDuplicateFound } diff --git a/stackit/internal/services/authorization/utils/util.go b/stackit/internal/services/authorization/utils/util.go index 99694780a..1a0b521d9 100644 --- a/stackit/internal/services/authorization/utils/util.go +++ b/stackit/internal/services/authorization/utils/util.go @@ -19,6 +19,7 @@ func ConfigureClient(ctx context.Context, providerData *core.ProviderData, diags if providerData.AuthorizationCustomEndpoint != "" { apiClientConfigOptions = append(apiClientConfigOptions, config.WithEndpoint(providerData.AuthorizationCustomEndpoint)) } + apiClient, err := authorization.NewAPIClient(apiClientConfigOptions...) if err != nil { core.LogAndAddError(ctx, diags, "Error configuring API client", fmt.Sprintf("Configuring client: %v. This is an error related to the provider configuration, not to the resource configuration", err)) diff --git a/stackit/internal/services/authorization/utils/util_test.go b/stackit/internal/services/authorization/utils/util_test.go index 794f255a6..a369ef3d6 100644 --- a/stackit/internal/services/authorization/utils/util_test.go +++ b/stackit/internal/services/authorization/utils/util_test.go @@ -22,6 +22,7 @@ const ( func TestConfigureClient(t *testing.T) { /* mock authentication by setting service account token env variable */ os.Clearenv() + err := os.Setenv(sdkClients.ServiceAccountToken, "mock-val") if err != nil { t.Errorf("error setting env variable: %v", err) @@ -30,6 +31,7 @@ func TestConfigureClient(t *testing.T) { type args struct { providerData *core.ProviderData } + tests := []struct { name string args args @@ -50,6 +52,7 @@ func TestConfigureClient(t *testing.T) { if err != nil { t.Errorf("error configuring client: %v", err) } + return apiClient }(), wantErr: false, @@ -70,6 +73,7 @@ func TestConfigureClient(t *testing.T) { if err != nil { t.Errorf("error configuring client: %v", err) } + return apiClient }(), wantErr: false, @@ -81,6 +85,7 @@ func TestConfigureClient(t *testing.T) { diags := diag.Diagnostics{} actual := ConfigureClient(ctx, tt.args.providerData, &diags) + if diags.HasError() != tt.wantErr { t.Errorf("ConfigureClient() error = %v, want %v", diags.HasError(), tt.wantErr) } diff --git a/stackit/internal/services/cdn/cdn_acc_test.go b/stackit/internal/services/cdn/cdn_acc_test.go index 0dd031a5b..a55046b20 100644 --- a/stackit/internal/services/cdn/cdn_acc_test.go +++ b/stackit/internal/services/cdn/cdn_acc_test.go @@ -42,6 +42,7 @@ func configResources(regions string, geofencingCountries []string) string { } geofencingList := strings.Join(quotedCountries, ",") + return fmt.Sprintf(` %s @@ -117,11 +118,13 @@ func configDatasources(regions, cert, key string, geofencingCountries []string) } `, configCustomDomainResources(regions, cert, key, geofencingCountries)) } + func makeCertAndKey(t *testing.T, organization string) (cert, key []byte) { privateKey, err := rsa.GenerateKey(cryptoRand.Reader, 2048) if err != nil { t.Fatalf("failed to generate key: %s", err.Error()) } + template := x509.Certificate{ SerialNumber: big.NewInt(1), Issuer: pkix.Name{CommonName: organization}, @@ -135,6 +138,7 @@ func makeCertAndKey(t *testing.T, organization string) (cert, key []byte) { ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, BasicConstraintsValid: true, } + cert, err = x509.CreateCertificate( cryptoRand.Reader, &template, @@ -154,6 +158,7 @@ func makeCertAndKey(t *testing.T, organization string) (cert, key []byte) { Bytes: x509.MarshalPKCS1PrivateKey(privateKey), }) } + func TestAccCDNDistributionResource(t *testing.T) { fullDomainName := fmt.Sprintf("%s.%s", instanceResource["custom_domain_prefix"], instanceResource["dns_name"]) organization := fmt.Sprintf("organization-%s", uuid.NewString()) @@ -333,9 +338,12 @@ func TestAccCDNDistributionResource(t *testing.T) { }, }) } + func testAccCheckCDNDistributionDestroy(s *terraform.State) error { ctx := context.Background() + var client *cdn.APIClient + var err error if testutil.MongoDBFlexCustomEndpoint == "" { client, err = cdn.NewAPIClient() @@ -344,15 +352,18 @@ func testAccCheckCDNDistributionDestroy(s *terraform.State) error { config.WithEndpoint(testutil.MongoDBFlexCustomEndpoint), ) } + if err != nil { return fmt.Errorf("creating client: %w", err) } distributionsToDestroy := []string{} + for _, rs := range s.RootModule().Resources { if rs.Type != "stackit_mongodbflex_instance" { continue } + distributionId := strings.Split(rs.Primary.ID, core.Separator)[1] distributionsToDestroy = append(distributionsToDestroy, distributionId) } @@ -362,11 +373,13 @@ func testAccCheckCDNDistributionDestroy(s *terraform.State) error { if err != nil { return fmt.Errorf("destroying CDN distribution %s during CheckDestroy: %w", dist, err) } + _, err = wait.DeleteDistributionWaitHandler(ctx, client, testutil.ProjectId, dist).WaitWithContext(ctx) if err != nil { return fmt.Errorf("destroying CDN distribution %s during CheckDestroy: waiting for deletion %w", dist, err) } } + return nil } @@ -382,26 +395,34 @@ func blockUntilDomainResolves(domain string) (net.IP, error) { if err != nil { return nil, fmt.Errorf("error looking up IP for domain %s: %w", domain, err) } + for _, ip := range ips { if ip.String() != "" { return ip, nil } } + return nil, fmt.Errorf("no IP for domain: %v", domain) } + return retry(recordCheckAttempts, recordCheckInterval, isReady) } func retry[T any](attempts int, sleep time.Duration, f func() (T, error)) (T, error) { var zero T + var errOuter error + for i := 0; i < attempts; i++ { dist, err := f() if err == nil { return dist, nil } + errOuter = err + time.Sleep(sleep) } + return zero, fmt.Errorf("retry timed out, last error: %w", errOuter) } diff --git a/stackit/internal/services/cdn/customdomain/datasource.go b/stackit/internal/services/cdn/customdomain/datasource.go index 14946b1e4..2117d4d28 100644 --- a/stackit/internal/services/cdn/customdomain/datasource.go +++ b/stackit/internal/services/cdn/customdomain/datasource.go @@ -6,9 +6,6 @@ import ( "fmt" "net/http" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - cdnUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/cdn/utils" - "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" @@ -17,8 +14,10 @@ import ( "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/stackitcloud/stackit-sdk-go/core/oapierror" "github.com/stackitcloud/stackit-sdk-go/services/cdn" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/features" + cdnUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/cdn/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" ) @@ -57,15 +56,19 @@ func (d *customDomainDataSource) Configure(ctx context.Context, req datasource.C } features.CheckBetaResourcesEnabled(ctx, &providerData, &resp.Diagnostics, "stackit_cdn_custom_domain", core.Datasource) + if resp.Diagnostics.HasError() { return } apiClient := cdnUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + d.client = apiClient + tflog.Info(ctx, "CDN client configured") } @@ -118,10 +121,11 @@ func (r *customDomainDataSource) Schema(_ context.Context, _ datasource.SchemaRe } } -func (r *customDomainDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (r *customDomainDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model customDomainDataSourceModel // Use the new data source model diags := req.Config.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -142,7 +146,9 @@ func (r *customDomainDataSource) Read(ctx context.Context, req datasource.ReadRe return } } + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading CDN custom domain", fmt.Sprintf("Calling API: %v", err)) + return } @@ -156,9 +162,11 @@ func (r *customDomainDataSource) Read(ctx context.Context, req datasource.ReadRe // Set refreshed state diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "CDN custom domain read") } @@ -166,6 +174,7 @@ func mapCustomDomainDataSourceFields(customDomainResponse *cdn.GetCustomDomainRe if customDomainResponse == nil { return fmt.Errorf("response input is nil") } + if model == nil { return fmt.Errorf("model input is nil") } @@ -177,6 +186,7 @@ func mapCustomDomainDataSourceFields(customDomainResponse *cdn.GetCustomDomainRe if customDomainResponse.CustomDomain.Name == nil { return fmt.Errorf("name is missing in response") } + if customDomainResponse.CustomDomain.Status == nil { return fmt.Errorf("status missing in response") } @@ -202,6 +212,7 @@ func mapCustomDomainDataSourceFields(customDomainResponse *cdn.GetCustomDomainRe if diags.HasError() { return fmt.Errorf("failed to map certificate: %w", core.DiagsToError(diags)) } + model.Certificate = certificateObj } @@ -209,18 +220,22 @@ func mapCustomDomainDataSourceFields(customDomainResponse *cdn.GetCustomDomainRe model.Status = types.StringValue(string(*customDomainResponse.CustomDomain.Status)) customDomainErrors := []attr.Value{} + if customDomainResponse.CustomDomain.Errors != nil { for _, e := range *customDomainResponse.CustomDomain.Errors { if e.En == nil { return fmt.Errorf("error description missing") } + customDomainErrors = append(customDomainErrors, types.StringValue(*e.En)) } } + modelErrors, diags := types.ListValue(types.StringType, customDomainErrors) if diags.HasError() { return core.DiagsToError(diags) } + model.Errors = modelErrors // Also map the fields back to the model from the config diff --git a/stackit/internal/services/cdn/customdomain/datasource_test.go b/stackit/internal/services/cdn/customdomain/datasource_test.go index 0a823b27e..dcb0615d8 100644 --- a/stackit/internal/services/cdn/customdomain/datasource_test.go +++ b/stackit/internal/services/cdn/customdomain/datasource_test.go @@ -32,6 +32,7 @@ func TestMapDataSourceFields(t *testing.T) { for _, mod := range mods { mod(model) } + return model } @@ -67,6 +68,7 @@ func TestMapDataSourceFields(t *testing.T) { for _, mod := range mods { mod(customDomainResponse) } + return customDomainResponse } @@ -123,9 +125,11 @@ func TestMapDataSourceFields(t *testing.T) { if err != nil && tc.IsValid { t.Fatalf("Error mapping fields: %v", err) } + if err == nil && !tc.IsValid { t.Fatalf("Should have failed") } + if tc.IsValid { diff := cmp.Diff(tc.Expected, model) if diff != "" { diff --git a/stackit/internal/services/cdn/customdomain/resource.go b/stackit/internal/services/cdn/customdomain/resource.go index f974348e8..99c167059 100644 --- a/stackit/internal/services/cdn/customdomain/resource.go +++ b/stackit/internal/services/cdn/customdomain/resource.go @@ -9,11 +9,6 @@ import ( "strings" "time" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - cdnUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/cdn/utils" - "github.com/google/uuid" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/path" @@ -28,8 +23,11 @@ import ( "github.com/stackitcloud/stackit-sdk-go/core/oapierror" "github.com/stackitcloud/stackit-sdk-go/services/cdn" "github.com/stackitcloud/stackit-sdk-go/services/cdn/wait" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/features" + cdnUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/cdn/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" ) @@ -39,6 +37,7 @@ var ( _ resource.ResourceWithConfigure = &customDomainResource{} _ resource.ResourceWithImportState = &customDomainResource{} ) + var certificateSchemaDescriptions = map[string]string{ "main": "The TLS certificate for the custom domain. If omitted, a managed certificate will be used. If the block is specified, a custom certificate is used.", "certificate": "The PEM-encoded TLS certificate. Required for custom certificates.", @@ -96,15 +95,19 @@ func (r *customDomainResource) Configure(ctx context.Context, req resource.Confi } features.CheckBetaResourcesEnabled(ctx, &providerData, &resp.Diagnostics, "stackit_cdn_custom_domain", "resource") + if resp.Diagnostics.HasError() { return } apiClient := cdnUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + r.client = apiClient + tflog.Info(ctx, "CDN client configured") } @@ -179,13 +182,15 @@ func (r *customDomainResource) Schema(_ context.Context, _ resource.SchemaReques } } -func (r *customDomainResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *customDomainResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { //nolint:gocritic // function signature required by Terraform var model CustomDomainModel diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) distributionId := model.DistributionId.ValueString() @@ -202,11 +207,13 @@ func (r *customDomainResource) Create(ctx context.Context, req resource.CreateRe IntentId: cdn.PtrString(uuid.NewString()), Certificate: certificate, } + _, err = r.client.PutCustomDomain(ctx, projectId, distributionId, name).PutCustomDomainPayload(payload).Execute() if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating CDN custom domain", fmt.Sprintf("Calling API: %v", err)) return } + _, err = wait.CreateCDNCustomDomainWaitHandler(ctx, r.client, projectId, distributionId, name).SetTimeout(5 * time.Minute).WaitWithContext(ctx) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating CDN custom domain", fmt.Sprintf("Waiting for create: %v", err)) @@ -218,6 +225,7 @@ func (r *customDomainResource) Create(ctx context.Context, req resource.CreateRe core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating CDN custom domain", fmt.Sprintf("Calling API: %v", err)) return } + err = mapCustomDomainResourceFields(respCustomDomain, &model) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating CDN custom domain", fmt.Sprintf("Processing API payload: %v", err)) @@ -226,16 +234,19 @@ func (r *customDomainResource) Create(ctx context.Context, req resource.CreateRe diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "CDN custom domain created") } -func (r *customDomainResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (r *customDomainResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model CustomDomainModel diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -248,7 +259,6 @@ func (r *customDomainResource) Read(ctx context.Context, req resource.ReadReques ctx = tflog.SetField(ctx, "name", name) customDomainResp, err := r.client.GetCustomDomain(ctx, projectId, distributionId, name).Execute() - if err != nil { var oapiErr *oapierror.GenericOpenAPIError // n.b. err is caught here if of type *oapierror.GenericOpenAPIError, which the stackit SDK client returns @@ -258,9 +268,12 @@ func (r *customDomainResource) Read(ctx context.Context, req resource.ReadReques return } } + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading CDN custom domain", fmt.Sprintf("Calling API: %v", err)) + return } + err = mapCustomDomainResourceFields(customDomainResp, &model) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading CDN custom domain", fmt.Sprintf("Processing API payload: %v", err)) @@ -269,16 +282,19 @@ func (r *customDomainResource) Read(ctx context.Context, req resource.ReadReques // Set refreshed state diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "CDN custom domain read") } -func (r *customDomainResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *customDomainResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { //nolint:gocritic // function signature required by Terraform var model CustomDomainModel diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -300,6 +316,7 @@ func (r *customDomainResource) Update(ctx context.Context, req resource.UpdateRe IntentId: cdn.PtrString(uuid.NewString()), Certificate: certificate, } + _, err = r.client.PutCustomDomain(ctx, projectId, distributionId, name).PutCustomDomainPayload(payload).Execute() if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating CDN custom domain certificate", fmt.Sprintf("Calling API: %v", err)) @@ -317,23 +334,28 @@ func (r *customDomainResource) Update(ctx context.Context, req resource.UpdateRe core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating CDN custom domain certificate", fmt.Sprintf("Calling API to read final state: %v", err)) return } + err = mapCustomDomainResourceFields(respCustomDomain, &model) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating CDN custom domain certificate", fmt.Sprintf("Processing API payload: %v", err)) return } + diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "CDN custom domain certificate updated") } -func (r *customDomainResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform +func (r *customDomainResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { //nolint:gocritic // function signature required by Terraform var model CustomDomainModel diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -349,11 +371,13 @@ func (r *customDomainResource) Delete(ctx context.Context, req resource.DeleteRe if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Delete CDN custom domain", fmt.Sprintf("Delete custom domain: %v", err)) } + _, err = wait.DeleteCDNCustomDomainWaitHandler(ctx, r.client, projectId, distributionId, name).WaitWithContext(ctx) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Delete CDN custom domain", fmt.Sprintf("Waiting for deletion: %v", err)) return } + tflog.Info(ctx, "CDN custom domain deleted") } @@ -363,6 +387,7 @@ func (r *customDomainResource) ImportState(ctx context.Context, req resource.Imp if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" { core.LogAndAddError(ctx, &resp.Diagnostics, "Error importing CDN custom domain", fmt.Sprintf("Expected import identifier on the format: [project_id]%q[distribution_id]%q[custom_domain_name], got %q", core.Separator, core.Separator, req.ID)) } + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), idParts[0])...) resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("distribution_id"), idParts[1])...) resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("name"), idParts[2])...) @@ -371,11 +396,13 @@ func (r *customDomainResource) ImportState(ctx context.Context, req resource.Imp func normalizeCertificate(certInput cdn.GetCustomDomainResponseGetCertificateAttributeType) (Certificate, error) { var customCert *cdn.GetCustomDomainCustomCertificate + var managedCert *cdn.GetCustomDomainManagedCertificate if certInput == nil { return Certificate{}, errors.New("input of type GetCustomDomainResponseCertificate is nil") } + customCert = certInput.GetCustomDomainCustomCertificate managedCert = certInput.GetCustomDomainManagedCertificate @@ -404,6 +431,7 @@ func toCertificatePayload(ctx context.Context, model *CustomDomainModel) (*cdn.P if model.Certificate.IsNull() { managedCert := cdn.NewPutCustomDomainManagedCertificate("managed") certPayload := cdn.PutCustomDomainManagedCertificateAsPutCustomDomainPayloadCertificate(managedCert) + return &certPayload, nil } @@ -439,6 +467,7 @@ func mapCustomDomainResourceFields(customDomainResponse *cdn.GetCustomDomainResp if customDomainResponse == nil { return fmt.Errorf("response input is nil") } + if model == nil { return fmt.Errorf("model input is nil") } @@ -446,6 +475,7 @@ func mapCustomDomainResourceFields(customDomainResponse *cdn.GetCustomDomainResp if customDomainResponse.CustomDomain == nil { return fmt.Errorf("CustomDomain is missing in response") } + if customDomainResponse.CustomDomain.Name == nil { return fmt.Errorf("name is missing in response") } @@ -453,6 +483,7 @@ func mapCustomDomainResourceFields(customDomainResponse *cdn.GetCustomDomainResp if customDomainResponse.CustomDomain.Status == nil { return fmt.Errorf("status missing in response") } + normalizedCert, err := normalizeCertificate(customDomainResponse.Certificate) if err != nil { return fmt.Errorf("Certificate error in normalizer: %w", err) @@ -476,6 +507,7 @@ func mapCustomDomainResourceFields(customDomainResponse *cdn.GetCustomDomainResp if val, ok := existingAttrs["certificate"]; ok { certAttributes["certificate"] = val } + if val, ok := existingAttrs["private_key"]; ok { certAttributes["private_key"] = val } @@ -490,6 +522,7 @@ func mapCustomDomainResourceFields(customDomainResponse *cdn.GetCustomDomainResp if diags.HasError() { return fmt.Errorf("failed to map certificate: %w", core.DiagsToError(diags)) } + model.Certificate = certificateObj } @@ -497,18 +530,22 @@ func mapCustomDomainResourceFields(customDomainResponse *cdn.GetCustomDomainResp model.Status = types.StringValue(string(*customDomainResponse.CustomDomain.Status)) customDomainErrors := []attr.Value{} + if customDomainResponse.CustomDomain.Errors != nil { for _, e := range *customDomainResponse.CustomDomain.Errors { if e.En == nil { return fmt.Errorf("error description missing") } + customDomainErrors = append(customDomainErrors, types.StringValue(*e.En)) } } + modelErrors, diags := types.ListValue(types.StringType, customDomainErrors) if diags.HasError() { return core.DiagsToError(diags) } + model.Errors = modelErrors return nil diff --git a/stackit/internal/services/cdn/customdomain/resource_test.go b/stackit/internal/services/cdn/customdomain/resource_test.go index 28aff2942..852c50041 100644 --- a/stackit/internal/services/cdn/customdomain/resource_test.go +++ b/stackit/internal/services/cdn/customdomain/resource_test.go @@ -30,6 +30,7 @@ func TestMapFields(t *testing.T) { } const dummyCert = "dummy-cert-pem" + const dummyKey = "dummy-key-pem" emtpyErrorsList := types.ListValueMust(types.StringType, []attr.Value{}) @@ -54,6 +55,7 @@ func TestMapFields(t *testing.T) { for _, mod := range mods { mod(model) } + return model } @@ -87,6 +89,7 @@ func TestMapFields(t *testing.T) { for _, mod := range mods { mod(customDomainResponse) } + return customDomainResponse } @@ -159,13 +162,16 @@ func TestMapFields(t *testing.T) { model := tc.InitialModel model.DistributionId = tc.Expected.DistributionId model.ProjectId = tc.Expected.ProjectId + err := mapCustomDomainResourceFields(tc.Input, model) if err != nil && tc.IsValid { t.Fatalf("Error mapping fields: %v", err) } + if err == nil && !tc.IsValid { t.Fatalf("Should have failed") } + if tc.IsValid { diff := cmp.Diff(tc.Expected, model) if diff != "" { @@ -181,6 +187,7 @@ func makeCertAndKey(t *testing.T, organization string) (cert, key []byte) { if err != nil { t.Fatalf("failed to generate key: %s", err.Error()) } + template := x509.Certificate{ SerialNumber: big.NewInt(1), Issuer: pkix.Name{CommonName: organization}, @@ -194,6 +201,7 @@ func makeCertAndKey(t *testing.T, organization string) (cert, key []byte) { ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, BasicConstraintsValid: true, } + cert, err = x509.CreateCertificate( cryptoRand.Reader, &template, @@ -213,6 +221,7 @@ func makeCertAndKey(t *testing.T, organization string) (cert, key []byte) { Bytes: x509.MarshalPKCS1PrivateKey(privateKey), }) } + func TestToCertificatePayload(t *testing.T) { organization := fmt.Sprintf("organization-%s", uuid.NewString()) cert, key := makeCertAndKey(t, organization) @@ -290,9 +299,11 @@ func TestToCertificatePayload(t *testing.T) { if err == nil { t.Fatalf("expected err, but got none") } + if err.Error() != tt.expectedErrMsg { t.Fatalf("expected err '%s', got '%s'", tt.expectedErrMsg, err.Error()) } + return // Test ends here for failing cases } diff --git a/stackit/internal/services/cdn/distribution/datasource.go b/stackit/internal/services/cdn/distribution/datasource.go index c111fcc9d..8a6c7dab0 100644 --- a/stackit/internal/services/cdn/distribution/datasource.go +++ b/stackit/internal/services/cdn/distribution/datasource.go @@ -4,17 +4,16 @@ import ( "context" "fmt" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - cdnUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/cdn/utils" - "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/stackitcloud/stackit-sdk-go/services/cdn" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/features" + cdnUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/cdn/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" ) @@ -39,15 +38,19 @@ func (d *distributionDataSource) Configure(ctx context.Context, req datasource.C } features.CheckBetaResourcesEnabled(ctx, &providerData, &resp.Diagnostics, "stackit_cdn_distribution", "datasource") + if resp.Diagnostics.HasError() { return } apiClient := cdnUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + d.client = apiClient + tflog.Info(ctx, "Service Account client configured") } @@ -176,10 +179,11 @@ func (r *distributionDataSource) Schema(_ context.Context, _ datasource.SchemaRe } } -func (r *distributionDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (r *distributionDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.Config.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -197,13 +201,16 @@ func (r *distributionDataSource) Read(ctx context.Context, req datasource.ReadRe map[int]string{}, ) resp.State.RemoveResource(ctx) + return } + err = mapFields(ctx, distributionResp.Distribution, &model) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading CDN distribution", fmt.Sprintf("Error processing API response: %v", err)) return } + diags = resp.State.Set(ctx, &model) resp.Diagnostics.Append(diags...) } diff --git a/stackit/internal/services/cdn/distribution/resource.go b/stackit/internal/services/cdn/distribution/resource.go index a9d03cefa..6a9a386a8 100644 --- a/stackit/internal/services/cdn/distribution/resource.go +++ b/stackit/internal/services/cdn/distribution/resource.go @@ -136,21 +136,26 @@ func NewDistributionResource() resource.Resource { func (r *distributionResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { var ok bool + r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) if !ok { return } features.CheckBetaResourcesEnabled(ctx, &r.providerData, &resp.Diagnostics, "stackit_cdn_distribution", "resource") + if resp.Diagnostics.HasError() { return } apiClient := cdnUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + r.client = apiClient + tflog.Info(ctx, "CDN client configured") } @@ -292,7 +297,9 @@ func (r *distributionResource) Schema(_ context.Context, _ resource.SchemaReques func (r *distributionResource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) { var model Model + resp.Diagnostics.Append(req.Config.Get(ctx, &model)...) + if resp.Diagnostics.HasError() { return } @@ -304,12 +311,14 @@ func (r *distributionResource) ValidateConfig(ctx context.Context, req resource. if diags.HasError() { return } + if geofencing := config.Backend.Geofencing; geofencing != nil { for url, region := range *geofencing { if region == nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Invalid geofencing config", fmt.Sprintf("The list of countries for URL %q must not be null.", url)) continue } + if len(region) == 0 { core.LogAndAddError(ctx, &resp.Diagnostics, "Invalid geofencing config", fmt.Sprintf("The list of countries for URL %q must not be empty.", url)) continue @@ -327,13 +336,15 @@ func (r *distributionResource) ValidateConfig(ctx context.Context, req resource. } } -func (r *distributionResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *distributionResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) @@ -348,6 +359,7 @@ func (r *distributionResource) Create(ctx context.Context, req resource.CreateRe core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating CDN distribution", fmt.Sprintf("Calling API: %v", err)) return } + waitResp, err := wait.CreateDistributionPoolWaitHandler(ctx, r.client, projectId, *createResp.Distribution.Id).SetTimeout(5 * time.Minute).WaitWithContext(ctx) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating CDN distribution", fmt.Sprintf("Waiting for create: %v", err)) @@ -362,16 +374,19 @@ func (r *distributionResource) Create(ctx context.Context, req resource.CreateRe diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "CDN distribution created") } -func (r *distributionResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (r *distributionResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -391,9 +406,12 @@ func (r *distributionResource) Read(ctx context.Context, req resource.ReadReques return } } + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading CDN distribution", fmt.Sprintf("Calling API: %v", err)) + return } + err = mapFields(ctx, cdnResp.Distribution, &model) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading CDN ditribution", fmt.Sprintf("Processing API payload: %v", err)) @@ -402,16 +420,19 @@ func (r *distributionResource) Read(ctx context.Context, req resource.ReadReques // Set refreshed state diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "CDN distribution read") } -func (r *distributionResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *distributionResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -422,6 +443,7 @@ func (r *distributionResource) Update(ctx context.Context, req resource.UpdateRe ctx = tflog.SetField(ctx, "distribution_id", distributionId) configModel := distributionConfig{} + diags = model.Config.As(ctx, &configModel, basetypes.ObjectAsOptions{ UnhandledNullAsEmpty: false, UnhandledUnknownAsEmpty: false, @@ -432,18 +454,21 @@ func (r *distributionResource) Update(ctx context.Context, req resource.UpdateRe } regions := []cdn.Region{} + for _, r := range *configModel.Regions { regionEnum, err := cdn.NewRegionFromValue(r) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Update CDN distribution", fmt.Sprintf("Map regions: %v", err)) return } + regions = append(regions, *regionEnum) } // blockedCountries // Use a pointer to a slice to distinguish between an empty list (unblock all) and nil (no change). var blockedCountries *[]string + if configModel.BlockedCountries != nil { // Use a temporary slice tempBlockedCountries := []string{} @@ -454,6 +479,7 @@ func (r *distributionResource) Update(ctx context.Context, req resource.UpdateRe core.LogAndAddError(ctx, &resp.Diagnostics, "Update CDN distribution", fmt.Sprintf("Blocked countries: %v", err)) return } + tempBlockedCountries = append(tempBlockedCountries, validatedBlockedCountry) } @@ -462,19 +488,25 @@ func (r *distributionResource) Update(ctx context.Context, req resource.UpdateRe } geofencingPatch := map[string][]string{} + if configModel.Backend.Geofencing != nil { gf := make(map[string][]string) + for url, countries := range *configModel.Backend.Geofencing { countryStrings := make([]string, len(countries)) + for i, countryPtr := range countries { if countryPtr == nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Update CDN distribution", fmt.Sprintf("Geofencing url %q has a null value", url)) return } + countryStrings[i] = *countryPtr } + gf[url] = countryStrings } + geofencingPatch = gf } @@ -504,6 +536,7 @@ func (r *distributionResource) Update(ctx context.Context, req resource.UpdateRe if !utils.IsUndefined(optimizerModel.Enabled) { optimizer.SetEnabled(optimizerModel.Enabled.ValueBool()) } + configPatch.Optimizer = optimizer } @@ -530,19 +563,23 @@ func (r *distributionResource) Update(ctx context.Context, req resource.UpdateRe diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "CDN distribution updated") } -func (r *distributionResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform +func (r *distributionResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() distributionId := model.DistributionId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) @@ -552,11 +589,13 @@ func (r *distributionResource) Delete(ctx context.Context, req resource.DeleteRe if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Delete CDN distribution", fmt.Sprintf("Delete distribution: %v", err)) } + _, err = wait.DeleteDistributionWaitHandler(ctx, r.client, projectId, distributionId).WaitWithContext(ctx) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Delete CDN distribution", fmt.Sprintf("Waiting for deletion: %v", err)) return } + tflog.Info(ctx, "CDN distribution deleted") } @@ -566,6 +605,7 @@ func (r *distributionResource) ImportState(ctx context.Context, req resource.Imp if len(idParts) != 2 || idParts[0] == "" || idParts[1] == "" { core.LogAndAddError(ctx, &resp.Diagnostics, "Error importing CDN distribution", fmt.Sprintf("Expected import identifier on the format: [project_id]%q[distribution_id], got %q", core.Separator, req.ID)) } + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), idParts[0])...) resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("distribution_id"), idParts[1])...) tflog.Info(ctx, "CDN distribution state imported") @@ -575,28 +615,29 @@ func mapFields(ctx context.Context, distribution *cdn.Distribution, model *Model if distribution == nil { return fmt.Errorf("response input is nil") } + if model == nil { return fmt.Errorf("model input is nil") } if distribution.ProjectId == nil { - return fmt.Errorf("Project ID not present") + return fmt.Errorf("project ID not present") } if distribution.Id == nil { - return fmt.Errorf("CDN distribution ID not present") + return fmt.Errorf("cDN distribution ID not present") } if distribution.CreatedAt == nil { - return fmt.Errorf("CreatedAt missing in response") + return fmt.Errorf("createdAt missing in response") } if distribution.UpdatedAt == nil { - return fmt.Errorf("UpdatedAt missing in response") + return fmt.Errorf("updatedAt missing in response") } if distribution.Status == nil { - return fmt.Errorf("Status missing in response") + return fmt.Errorf("status missing in response") } model.ID = utils.BuildInternalTerraformId(*distribution.ProjectId, *distribution.Id) @@ -608,15 +649,18 @@ func mapFields(ctx context.Context, distribution *cdn.Distribution, model *Model // distributionErrors distributionErrors := []attr.Value{} + if distribution.Errors != nil { for _, e := range *distribution.Errors { distributionErrors = append(distributionErrors, types.StringValue(*e.En)) } } + modelErrors, diags := types.ListValue(types.StringType, distributionErrors) if diags.HasError() { return core.DiagsToError(diags) } + model.Errors = modelErrors // regions @@ -624,6 +668,7 @@ func mapFields(ctx context.Context, distribution *cdn.Distribution, model *Model for _, r := range *distribution.Config.Regions { regions = append(regions, types.StringValue(string(r))) } + modelRegions, diags := types.ListValue(types.StringType, regions) if diags.HasError() { return core.DiagsToError(diags) @@ -631,6 +676,7 @@ func mapFields(ctx context.Context, distribution *cdn.Distribution, model *Model // blockedCountries var blockedCountries []attr.Value + if distribution.Config != nil && distribution.Config.BlockedCountries != nil { for _, c := range *distribution.Config.BlockedCountries { blockedCountries = append(blockedCountries, types.StringValue(string(c))) @@ -644,13 +690,16 @@ func mapFields(ctx context.Context, distribution *cdn.Distribution, model *Model // originRequestHeaders originRequestHeaders := types.MapNull(types.StringType) + if origHeaders := distribution.Config.Backend.HttpBackend.OriginRequestHeaders; origHeaders != nil && len(*origHeaders) > 0 { headers := map[string]attr.Value{} for k, v := range *origHeaders { headers[k] = types.StringValue(v) } + mappedHeaders, diags := types.MapValue(types.StringType, headers) originRequestHeaders = mappedHeaders + if diags.HasError() { return core.DiagsToError(diags) } @@ -658,18 +707,22 @@ func mapFields(ctx context.Context, distribution *cdn.Distribution, model *Model // geofencing var oldConfig distributionConfig + oldGeofencingMap := make(map[string][]*string) + if !model.Config.IsNull() { diags = model.Config.As(ctx, &oldConfig, basetypes.ObjectAsOptions{}) if diags.HasError() { return core.DiagsToError(diags) } + if oldConfig.Backend.Geofencing != nil { oldGeofencingMap = *oldConfig.Backend.Geofencing } } reconciledGeofencingData := make(map[string][]string) + if geofencingAPI := distribution.Config.Backend.HttpBackend.Geofencing; geofencingAPI != nil && len(*geofencingAPI) > 0 { newGeofencingMap := *geofencingAPI for url, newCountries := range newGeofencingMap { @@ -683,21 +736,26 @@ func mapFields(ctx context.Context, distribution *cdn.Distribution, model *Model } geofencingVal := types.MapNull(geofencingTypes.ElemType) + if len(reconciledGeofencingData) > 0 { geofencingMapElems := make(map[string]attr.Value) + for url, countries := range reconciledGeofencingData { listVal, diags := types.ListValueFrom(ctx, types.StringType, countries) if diags.HasError() { return core.DiagsToError(diags) } + geofencingMapElems[url] = listVal } var mappedGeofencing basetypes.MapValue + mappedGeofencing, diags = types.MapValue(geofencingTypes.ElemType, geofencingMapElems) if diags.HasError() { return core.DiagsToError(diags) } + geofencingVal = mappedGeofencing } @@ -713,10 +771,12 @@ func mapFields(ctx context.Context, distribution *cdn.Distribution, model *Model } optimizerVal := types.ObjectNull(optimizerTypes) + if o := distribution.Config.Optimizer; o != nil { optimizerEnabled, ok := o.GetEnabledOk() if ok { var diags diag.Diagnostics + optimizerVal, diags = types.ObjectValue(optimizerTypes, map[string]attr.Value{ "enabled": types.BoolValue(optimizerEnabled), }) @@ -725,6 +785,7 @@ func mapFields(ctx context.Context, distribution *cdn.Distribution, model *Model } } } + cfg, diags := types.ObjectValue(configTypes, map[string]attr.Value{ "backend": backend, "regions": modelRegions, @@ -734,27 +795,34 @@ func mapFields(ctx context.Context, distribution *cdn.Distribution, model *Model if diags.HasError() { return core.DiagsToError(diags) } + model.Config = cfg domains := []attr.Value{} + if distribution.Domains != nil { for _, d := range *distribution.Domains { domainErrors := []attr.Value{} + if d.Errors != nil { for _, e := range *d.Errors { if e.En == nil { return fmt.Errorf("error description missing") } + domainErrors = append(domainErrors, types.StringValue(*e.En)) } } + modelDomainErrors, diags := types.ListValue(types.StringType, domainErrors) if diags.HasError() { return core.DiagsToError(diags) } + if d.Name == nil || d.Status == nil || d.Type == nil { return fmt.Errorf("domain entry incomplete") } + modelDomain, diags := types.ObjectValue(domainTypes, map[string]attr.Value{ "name": types.StringValue(*d.Name), "status": types.StringValue(string(*d.Status)), @@ -773,7 +841,9 @@ func mapFields(ctx context.Context, distribution *cdn.Distribution, model *Model if diags.HasError() { return core.DiagsToError(diags) } + model.Domains = modelDomains + return nil } @@ -781,10 +851,12 @@ func toCreatePayload(ctx context.Context, model *Model) (*cdn.CreateDistribution if model == nil { return nil, fmt.Errorf("missing model") } + cfg, err := convertConfig(ctx, model) if err != nil { return nil, err } + var optimizer *cdn.Optimizer if cfg.Optimizer != nil { optimizer = cdn.NewOptimizer(cfg.Optimizer.GetEnabled()) @@ -807,10 +879,13 @@ func convertConfig(ctx context.Context, model *Model) (*cdn.Config, error) { if model == nil { return nil, errors.New("model cannot be nil") } + if model.Config.IsNull() || model.Config.IsUnknown() { return nil, errors.New("config cannot be nil or unknown") } + configModel := distributionConfig{} + diags := model.Config.As(ctx, &configModel, basetypes.ObjectAsOptions{ UnhandledNullAsEmpty: false, UnhandledUnknownAsEmpty: false, @@ -821,47 +896,57 @@ func convertConfig(ctx context.Context, model *Model) (*cdn.Config, error) { // regions regions := []cdn.Region{} + for _, r := range *configModel.Regions { regionEnum, err := cdn.NewRegionFromValue(r) if err != nil { return nil, err } + regions = append(regions, *regionEnum) } // blockedCountries var blockedCountries []string + if configModel.BlockedCountries != nil { for _, blockedCountry := range *configModel.BlockedCountries { validatedBlockedCountry, err := validateCountryCode(blockedCountry) if err != nil { return nil, err } + blockedCountries = append(blockedCountries, validatedBlockedCountry) } } // geofencing geofencing := map[string][]string{} + if configModel.Backend.Geofencing != nil { for endpoint, countryCodes := range *configModel.Backend.Geofencing { geofencingCountry := make([]string, len(countryCodes)) + for i, countryCodePtr := range countryCodes { if countryCodePtr == nil { return nil, fmt.Errorf("geofencing url %q has a null value", endpoint) } + validatedCountry, err := validateCountryCode(*countryCodePtr) if err != nil { return nil, err } + geofencingCountry[i] = validatedCountry } + geofencing[endpoint] = geofencingCountry } } // originRequestHeaders originRequestHeaders := map[string]string{} + if configModel.Backend.OriginRequestHeaders != nil { for k, v := range *configModel.Backend.OriginRequestHeaders { originRequestHeaders[k] = v @@ -883,6 +968,7 @@ func convertConfig(ctx context.Context, model *Model) (*cdn.Config, error) { if !utils.IsUndefined(configModel.Optimizer) { var optimizerModel optimizerConfig + diags := configModel.Optimizer.As(ctx, &optimizerModel, basetypes.ObjectAsOptions{}) if diags.HasError() { return nil, core.DiagsToError(diags) @@ -911,7 +997,7 @@ func validateCountryCode(country string) (string, error) { char1 := upperCountry[0] char2 := upperCountry[1] - if !((char1 >= 'A' && char1 <= 'Z') && (char2 >= 'A' && char2 <= 'Z')) { + if char1 < 'A' || char1 > 'Z' || char2 < 'A' || char2 > 'Z' { return "", fmt.Errorf("country code '%s' must consist of two alphabetical letters (A-Z or a-z)", country) } diff --git a/stackit/internal/services/cdn/distribution/resource_test.go b/stackit/internal/services/cdn/distribution/resource_test.go index 1a6394361..8e1b83425 100644 --- a/stackit/internal/services/cdn/distribution/resource_test.go +++ b/stackit/internal/services/cdn/distribution/resource_test.go @@ -52,8 +52,10 @@ func TestToCreatePayload(t *testing.T) { for _, mod := range mods { mod(model) } + return model } + tests := map[string]struct { Input *Model Expected *cdn.CreateDistributionPayload @@ -118,9 +120,11 @@ func TestToCreatePayload(t *testing.T) { if err != nil && tc.IsValid { t.Fatalf("Error converting model to create payload: %v", err) } + if err == nil && !tc.IsValid { t.Fatalf("Should have failed") } + if tc.IsValid { // set generated ID before diffing tc.Expected.IntentId = res.IntentId @@ -173,8 +177,10 @@ func TestConvertConfig(t *testing.T) { for _, mod := range mods { mod(model) } + return model } + tests := map[string]struct { Input *Model Expected *cdn.Config @@ -249,9 +255,11 @@ func TestConvertConfig(t *testing.T) { if err != nil && tc.IsValid { t.Fatalf("Error converting model to create payload: %v", err) } + if err == nil && !tc.IsValid { t.Fatalf("Should have failed") } + if tc.IsValid { diff := cmp.Diff(res, tc.Expected) if diff != "" { @@ -318,6 +326,7 @@ func TestMapFields(t *testing.T) { for _, mod := range mods { mod(model) } + return model } distributionFixture := func(mods ...func(*cdn.Distribution)) *cdn.Distribution { @@ -353,8 +362,10 @@ func TestMapFields(t *testing.T) { for _, mod := range mods { mod(distribution) } + return distribution } + tests := map[string]struct { Input *cdn.Distribution Expected *Model @@ -466,13 +477,16 @@ func TestMapFields(t *testing.T) { for tn, tc := range tests { t.Run(tn, func(t *testing.T) { model := &Model{} + err := mapFields(context.Background(), tc.Input, model) if err != nil && tc.IsValid { t.Fatalf("Error mapping fields: %v", err) } + if err == nil && !tc.IsValid { t.Fatalf("Should have failed") } + if tc.IsValid { diff := cmp.Diff(model, tc.Expected) if diff != "" { @@ -573,6 +587,7 @@ func TestValidateCountryCode(t *testing.T) { } else if err.Error() != tc.expectedError { t.Errorf("for input '%s', expected error '%s', but got '%s'", tc.inputCountry, tc.expectedError, err.Error()) } + if gotOutput != "" { t.Errorf("expected empty string on error, but got '%s'", gotOutput) } @@ -580,6 +595,7 @@ func TestValidateCountryCode(t *testing.T) { if err != nil { t.Errorf("did not expect an error for input '%s', but got: %v", tc.inputCountry, err) } + if gotOutput != tc.wantOutput { t.Errorf("for input '%s', expected output '%s', but got '%s'", tc.inputCountry, tc.wantOutput, gotOutput) } diff --git a/stackit/internal/services/cdn/utils/util.go b/stackit/internal/services/cdn/utils/util.go index e03f68d6f..9c9d1c1d4 100644 --- a/stackit/internal/services/cdn/utils/util.go +++ b/stackit/internal/services/cdn/utils/util.go @@ -19,6 +19,7 @@ func ConfigureClient(ctx context.Context, providerData *core.ProviderData, diags if providerData.CdnCustomEndpoint != "" { apiClientConfigOptions = append(apiClientConfigOptions, config.WithEndpoint(providerData.CdnCustomEndpoint)) } + apiClient, err := cdn.NewAPIClient(apiClientConfigOptions...) if err != nil { core.LogAndAddError(ctx, diags, "Error configuring API client", fmt.Sprintf("Configuring client: %v. This is an error related to the provider configuration, not to the resource configuration", err)) diff --git a/stackit/internal/services/cdn/utils/util_test.go b/stackit/internal/services/cdn/utils/util_test.go index 576d02476..da0450140 100644 --- a/stackit/internal/services/cdn/utils/util_test.go +++ b/stackit/internal/services/cdn/utils/util_test.go @@ -22,6 +22,7 @@ const ( func TestConfigureClient(t *testing.T) { /* mock authentication by setting service account token env variable */ os.Clearenv() + err := os.Setenv(sdkClients.ServiceAccountToken, "mock-val") if err != nil { t.Errorf("error setting env variable: %v", err) @@ -30,6 +31,7 @@ func TestConfigureClient(t *testing.T) { type args struct { providerData *core.ProviderData } + tests := []struct { name string args args @@ -50,6 +52,7 @@ func TestConfigureClient(t *testing.T) { if err != nil { t.Errorf("error configuring client: %v", err) } + return apiClient }(), wantErr: false, @@ -70,6 +73,7 @@ func TestConfigureClient(t *testing.T) { if err != nil { t.Errorf("error configuring client: %v", err) } + return apiClient }(), wantErr: false, @@ -81,6 +85,7 @@ func TestConfigureClient(t *testing.T) { diags := diag.Diagnostics{} actual := ConfigureClient(ctx, tt.args.providerData, &diags) + if diags.HasError() != tt.wantErr { t.Errorf("ConfigureClient() error = %v, want %v", diags.HasError(), tt.wantErr) } diff --git a/stackit/internal/services/dns/dns_acc_test.go b/stackit/internal/services/dns/dns_acc_test.go index 201ab118a..cf1bb6a64 100644 --- a/stackit/internal/services/dns/dns_acc_test.go +++ b/stackit/internal/services/dns/dns_acc_test.go @@ -66,6 +66,7 @@ var testConfigVarsMax = config.Variables{ func configVarsInvalid(vars config.Variables) config.Variables { tempConfig := maps.Clone(vars) tempConfig["dns_name"] = config.StringVariable("foo") + return tempConfig } @@ -79,6 +80,7 @@ func configVarsMinUpdated() config.Variables { func configVarsMaxUpdated() config.Variables { tempConfig := maps.Clone(testConfigVarsMax) tempConfig["record_record1"] = config.StringVariable("1.2.3.5") + return tempConfig } @@ -495,7 +497,9 @@ func TestAccDnsMaxResource(t *testing.T) { func testAccCheckDnsDestroy(s *terraform.State) error { ctx := context.Background() + var client *dns.APIClient + var err error if testutil.DnsCustomEndpoint == "" { client, err = dns.NewAPIClient() @@ -504,11 +508,13 @@ func testAccCheckDnsDestroy(s *terraform.State) error { core_config.WithEndpoint(testutil.DnsCustomEndpoint), ) } + if err != nil { return fmt.Errorf("creating client: %w", err) } zonesToDestroy := []string{} + for _, rs := range s.RootModule().Resources { if rs.Type != "stackit_dns_zone" { continue @@ -531,11 +537,13 @@ func testAccCheckDnsDestroy(s *terraform.State) error { if err != nil { return fmt.Errorf("destroying zone %s during CheckDestroy: %w", *zones[i].Id, err) } + _, err = wait.DeleteZoneWaitHandler(ctx, client, testutil.ProjectId, id).WaitWithContext(ctx) if err != nil { return fmt.Errorf("destroying zone %s during CheckDestroy: waiting for deletion %w", *zones[i].Id, err) } } } + return nil } diff --git a/stackit/internal/services/dns/recordset/datasource.go b/stackit/internal/services/dns/recordset/datasource.go index 792872611..f305a7aca 100644 --- a/stackit/internal/services/dns/recordset/datasource.go +++ b/stackit/internal/services/dns/recordset/datasource.go @@ -5,16 +5,15 @@ import ( "fmt" "net/http" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - dnsUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/dns/utils" - "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/stackitcloud/stackit-sdk-go/services/dns" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + dnsUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/dns/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" ) @@ -47,10 +46,13 @@ func (d *recordSetDataSource) Configure(ctx context.Context, req datasource.Conf } apiClient := dnsUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + d.client = apiClient + tflog.Info(ctx, "DNS record set client configured") } @@ -129,13 +131,15 @@ func (d *recordSetDataSource) Schema(_ context.Context, _ datasource.SchemaReque } // Read refreshes the Terraform state with the latest data. -func (d *recordSetDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (d *recordSetDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.Config.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() zoneId := model.ZoneId.ValueString() recordSetId := model.RecordSetId.ValueString() @@ -155,11 +159,14 @@ func (d *recordSetDataSource) Read(ctx context.Context, req datasource.ReadReque }, ) resp.State.RemoveResource(ctx) + return } + if recordSetResp != nil && recordSetResp.Rrset.State != nil && *recordSetResp.Rrset.State == dns.RECORDSETSTATE_DELETE_SUCCEEDED { resp.State.RemoveResource(ctx) core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading record set", "Record set was deleted successfully") + return } @@ -168,10 +175,13 @@ func (d *recordSetDataSource) Read(ctx context.Context, req datasource.ReadReque core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading record set", fmt.Sprintf("Processing API payload: %v", err)) return } + diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "DNS record set read") } diff --git a/stackit/internal/services/dns/recordset/resource.go b/stackit/internal/services/dns/recordset/resource.go index 9a9046350..61d6bbd01 100644 --- a/stackit/internal/services/dns/recordset/resource.go +++ b/stackit/internal/services/dns/recordset/resource.go @@ -71,10 +71,13 @@ func (r *recordSetResource) Configure(ctx context.Context, req resource.Configur } apiClient := dnsUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + r.client = apiClient + tflog.Info(ctx, "DNS record set client configured") } @@ -191,11 +194,12 @@ func (r *recordSetResource) Schema(_ context.Context, _ resource.SchemaRequest, } // Create creates the resource and sets the initial Terraform state. -func (r *recordSetResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *recordSetResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { //nolint:gocritic // function signature required by Terraform // Retrieve values from plan var model Model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -224,6 +228,7 @@ func (r *recordSetResource) Create(ctx context.Context, req resource.CreateReque "zone_id": zoneId, "record_set_id": *recordSetResp.Rrset.Id, }) + if resp.Diagnostics.HasError() { return } @@ -243,20 +248,24 @@ func (r *recordSetResource) Create(ctx context.Context, req resource.CreateReque // Set state to fully populated data diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "DNS record set created") } // Read refreshes the Terraform state with the latest data. -func (r *recordSetResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (r *recordSetResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() zoneId := model.ZoneId.ValueString() recordSetId := model.RecordSetId.ValueString() @@ -269,6 +278,7 @@ func (r *recordSetResource) Read(ctx context.Context, req resource.ReadRequest, core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading record set", fmt.Sprintf("Calling API: %v", err)) return } + if recordSetResp != nil && recordSetResp.Rrset.State != nil && *recordSetResp.Rrset.State == dns.RECORDSETSTATE_DELETE_SUCCEEDED { resp.State.RemoveResource(ctx) return @@ -284,18 +294,21 @@ func (r *recordSetResource) Read(ctx context.Context, req resource.ReadRequest, // Set refreshed state diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "DNS record set read") } // Update updates the resource and sets the updated Terraform state on success. -func (r *recordSetResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *recordSetResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { //nolint:gocritic // function signature required by Terraform // Retrieve values from plan var model Model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -319,6 +332,7 @@ func (r *recordSetResource) Update(ctx context.Context, req resource.UpdateReque core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating record set", err.Error()) return } + waitResp, err := wait.PartialUpdateRecordSetWaitHandler(ctx, r.client, projectId, zoneId, recordSetId).WaitWithContext(ctx) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating record set", fmt.Sprintf("Instance update waiting: %v", err)) @@ -330,20 +344,24 @@ func (r *recordSetResource) Update(ctx context.Context, req resource.UpdateReque core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating record set", fmt.Sprintf("Processing API payload: %v", err)) return } + diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "DNS record set updated") } // Delete deletes the resource and removes the Terraform state on success. -func (r *recordSetResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform +func (r *recordSetResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { //nolint:gocritic // function signature required by Terraform // Retrieve values from plan var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -360,16 +378,18 @@ func (r *recordSetResource) Delete(ctx context.Context, req resource.DeleteReque if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting record set", fmt.Sprintf("Calling API: %v", err)) } + _, err = wait.DeleteRecordSetWaitHandler(ctx, r.client, projectId, zoneId, recordSetId).WaitWithContext(ctx) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting record set", fmt.Sprintf("Instance deletion waiting: %v", err)) return } + tflog.Info(ctx, "DNS record set deleted") } // ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: project_id,zone_id,record_set_id +// The expected format of the resource import identifier is: project_id,zone_id,record_set_id. func (r *recordSetResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { idParts := strings.Split(req.ID, core.Separator) if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" { @@ -377,6 +397,7 @@ func (r *recordSetResource) ImportState(ctx context.Context, req resource.Import "Error importing record set", fmt.Sprintf("Expected import identifier with format [project_id],[zone_id],[record_set_id], got %q", req.ID), ) + return } @@ -392,9 +413,11 @@ func mapFields(ctx context.Context, recordSetResp *dns.RecordSetResponse, model if recordSetResp == nil || recordSetResp.Rrset == nil { return fmt.Errorf("response input is nil") } + if model == nil { return fmt.Errorf("model input is nil") } + recordSet := recordSetResp.Rrset var recordSetId string @@ -429,6 +452,7 @@ func mapFields(ctx context.Context, recordSetResp *dns.RecordSetResponse, model model.Records = recordsTF } + model.Id = utils.BuildInternalTerraformId( model.ProjectId.ValueString(), model.ZoneId.ValueString(), recordSetId, ) @@ -436,13 +460,16 @@ func mapFields(ctx context.Context, recordSetResp *dns.RecordSetResponse, model model.Active = types.BoolPointerValue(recordSet.Active) model.Comment = types.StringPointerValue(recordSet.Comment) model.Error = types.StringPointerValue(recordSet.Error) + if model.Name.IsNull() || model.Name.IsUnknown() { model.Name = types.StringPointerValue(recordSet.Name) } + model.FQDN = types.StringPointerValue(recordSet.Name) model.State = types.StringValue(string(recordSet.GetState())) model.TTL = types.Int64PointerValue(recordSet.Ttl) model.Type = types.StringValue(string(recordSet.GetType())) + return nil } @@ -452,11 +479,13 @@ func toCreatePayload(model *Model) (*dns.CreateRecordSetPayload, error) { } records := []dns.RecordPayload{} + for i, record := range model.Records.Elements() { recordString, ok := record.(types.String) if !ok { return nil, fmt.Errorf("expected record at index %d to be of type %T, got %T", i, types.String{}, record) } + records = append(records, dns.RecordPayload{ Content: conversion.StringValueToPointer(recordString), }) @@ -477,11 +506,13 @@ func toUpdatePayload(model *Model) (*dns.PartialUpdateRecordSetPayload, error) { } records := []dns.RecordPayload{} + for i, record := range model.Records.Elements() { recordString, ok := record.(types.String) if !ok { return nil, fmt.Errorf("expected record at index %d to be of type %T, got %T", i, types.String{}, record) } + records = append(records, dns.RecordPayload{ Content: conversion.StringValueToPointer(recordString), }) diff --git a/stackit/internal/services/dns/recordset/resource_test.go b/stackit/internal/services/dns/recordset/resource_test.go index f73309a03..f642bcc90 100644 --- a/stackit/internal/services/dns/recordset/resource_test.go +++ b/stackit/internal/services/dns/recordset/resource_test.go @@ -199,9 +199,11 @@ func TestMapFields(t *testing.T) { if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(tt.state, tt.expected) if diff != "" { @@ -282,9 +284,11 @@ func TestToCreatePayload(t *testing.T) { if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(output, tt.expected) if diff != "" { @@ -361,9 +365,11 @@ func TestToUpdatePayload(t *testing.T) { if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(output, tt.expected) if diff != "" { diff --git a/stackit/internal/services/dns/utils/util.go b/stackit/internal/services/dns/utils/util.go index ca0e4889f..9ad1ef213 100644 --- a/stackit/internal/services/dns/utils/util.go +++ b/stackit/internal/services/dns/utils/util.go @@ -19,6 +19,7 @@ func ConfigureClient(ctx context.Context, providerData *core.ProviderData, diags if providerData.DnsCustomEndpoint != "" { apiClientConfigOptions = append(apiClientConfigOptions, config.WithEndpoint(providerData.DnsCustomEndpoint)) } + apiClient, err := dns.NewAPIClient(apiClientConfigOptions...) if err != nil { core.LogAndAddError(ctx, diags, "Error configuring API client", fmt.Sprintf("Configuring client: %v. This is an error related to the provider configuration, not to the resource configuration", err)) diff --git a/stackit/internal/services/dns/utils/util_test.go b/stackit/internal/services/dns/utils/util_test.go index 31e61382a..3522d7bca 100644 --- a/stackit/internal/services/dns/utils/util_test.go +++ b/stackit/internal/services/dns/utils/util_test.go @@ -22,6 +22,7 @@ const ( func TestConfigureClient(t *testing.T) { /* mock authentication by setting service account token env variable */ os.Clearenv() + err := os.Setenv(sdkClients.ServiceAccountToken, "mock-val") if err != nil { t.Errorf("error setting env variable: %v", err) @@ -30,6 +31,7 @@ func TestConfigureClient(t *testing.T) { type args struct { providerData *core.ProviderData } + tests := []struct { name string args args @@ -50,6 +52,7 @@ func TestConfigureClient(t *testing.T) { if err != nil { t.Errorf("error configuring client: %v", err) } + return apiClient }(), wantErr: false, @@ -70,6 +73,7 @@ func TestConfigureClient(t *testing.T) { if err != nil { t.Errorf("error configuring client: %v", err) } + return apiClient }(), wantErr: false, @@ -81,6 +85,7 @@ func TestConfigureClient(t *testing.T) { diags := diag.Diagnostics{} actual := ConfigureClient(ctx, tt.args.providerData, &diags) + if diags.HasError() != tt.wantErr { t.Errorf("ConfigureClient() error = %v, want %v", diags.HasError(), tt.wantErr) } diff --git a/stackit/internal/services/dns/zone/datasource.go b/stackit/internal/services/dns/zone/datasource.go index 682c87df2..4a2219db2 100644 --- a/stackit/internal/services/dns/zone/datasource.go +++ b/stackit/internal/services/dns/zone/datasource.go @@ -6,17 +6,16 @@ import ( "net/http" "github.com/hashicorp/terraform-plugin-framework-validators/datasourcevalidator" - "github.com/hashicorp/terraform-plugin-framework/path" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - dnsUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/dns/utils" - "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/stackitcloud/stackit-sdk-go/services/dns" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + dnsUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/dns/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" ) @@ -41,7 +40,7 @@ func (d *zoneDataSource) Metadata(_ context.Context, req datasource.MetadataRequ resp.TypeName = req.ProviderTypeName + "_dns_zone" } -// ConfigValidators validates the resource configuration +// ConfigValidators validates the resource configuration. func (d *zoneDataSource) ConfigValidators(_ context.Context) []datasource.ConfigValidator { return []datasource.ConfigValidator{ datasourcevalidator.ExactlyOneOf( @@ -58,10 +57,13 @@ func (d *zoneDataSource) Configure(ctx context.Context, req datasource.Configure } apiClient := dnsUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + d.client = apiClient + tflog.Info(ctx, "DNS zone client configured") } @@ -172,13 +174,15 @@ func (d *zoneDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, r } // Read refreshes the Terraform state with the latest data. -func (d *zoneDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (d *zoneDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.Config.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() zoneId := model.ZoneId.ValueString() dnsName := model.DnsName.ValueString() @@ -187,6 +191,7 @@ func (d *zoneDataSource) Read(ctx context.Context, req datasource.ReadRequest, r ctx = tflog.SetField(ctx, "dns_name", dnsName) var zoneResp *dns.ZoneResponse + var err error if zoneId != "" { @@ -203,6 +208,7 @@ func (d *zoneDataSource) Read(ctx context.Context, req datasource.ReadRequest, r }, ) resp.State.RemoveResource(ctx) + return } } else { @@ -222,8 +228,10 @@ func (d *zoneDataSource) Read(ctx context.Context, req datasource.ReadRequest, r }, ) resp.State.RemoveResource(ctx) + return } + if *listZoneResp.TotalItems != 1 { utils.LogError( ctx, @@ -234,8 +242,10 @@ func (d *zoneDataSource) Read(ctx context.Context, req datasource.ReadRequest, r nil, ) resp.State.RemoveResource(ctx) + return } + zones := *listZoneResp.Zones zoneResp = dns.NewZoneResponse(zones[0]) } @@ -243,6 +253,7 @@ func (d *zoneDataSource) Read(ctx context.Context, req datasource.ReadRequest, r if zoneResp != nil && zoneResp.Zone.State != nil && *zoneResp.Zone.State == dns.ZONESTATE_DELETE_SUCCEEDED { resp.State.RemoveResource(ctx) core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading zone", "Zone was deleted successfully") + return } @@ -251,10 +262,13 @@ func (d *zoneDataSource) Read(ctx context.Context, req datasource.ReadRequest, r core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading zone", fmt.Sprintf("Processing API payload: %v", err)) return } + diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "DNS zone read") } diff --git a/stackit/internal/services/dns/zone/resource.go b/stackit/internal/services/dns/zone/resource.go index 4b05814cf..b83a6ce80 100644 --- a/stackit/internal/services/dns/zone/resource.go +++ b/stackit/internal/services/dns/zone/resource.go @@ -6,8 +6,6 @@ import ( "math" "strings" - dnsUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/dns/utils" - "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" @@ -25,6 +23,7 @@ import ( "github.com/stackitcloud/stackit-sdk-go/services/dns/wait" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + dnsUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/dns/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" ) @@ -84,10 +83,13 @@ func (r *zoneResource) Configure(ctx context.Context, req resource.ConfigureRequ } apiClient := dnsUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + r.client = apiClient + tflog.Info(ctx, "DNS zone client configured") } @@ -276,10 +278,12 @@ func (r *zoneResource) Schema(_ context.Context, _ resource.SchemaRequest, resp } // Create creates the resource and sets the initial Terraform state. -func (r *zoneResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *zoneResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { //nolint:gocritic // function signature required by Terraform // Retrieve values from plan var model Model + resp.Diagnostics.Append(req.Plan.Get(ctx, &model)...) + if resp.Diagnostics.HasError() { return } @@ -306,6 +310,7 @@ func (r *zoneResource) Create(ctx context.Context, req resource.CreateRequest, r "project_id": projectId, "zone_id": zoneId, }) + if resp.Diagnostics.HasError() { return } @@ -324,20 +329,24 @@ func (r *zoneResource) Create(ctx context.Context, req resource.CreateRequest, r } // Set state to fully populated data resp.Diagnostics.Append(resp.State.Set(ctx, model)...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "DNS zone created") } // Read refreshes the Terraform state with the latest data. -func (r *zoneResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (r *zoneResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() zoneId := model.ZoneId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) @@ -348,6 +357,7 @@ func (r *zoneResource) Read(ctx context.Context, req resource.ReadRequest, resp core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading zone", fmt.Sprintf("Calling API: %v", err)) return } + if zoneResp != nil && zoneResp.Zone.State != nil && *zoneResp.Zone.State == dns.ZONESTATE_DELETE_SUCCEEDED { resp.State.RemoveResource(ctx) return @@ -362,21 +372,25 @@ func (r *zoneResource) Read(ctx context.Context, req resource.ReadRequest, resp // Set refreshed state diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "DNS zone read") } // Update updates the resource and sets the updated Terraform state on success. -func (r *zoneResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *zoneResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { //nolint:gocritic // function signature required by Terraform // Retrieve values from plan var model Model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() zoneId := model.ZoneId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) @@ -394,6 +408,7 @@ func (r *zoneResource) Update(ctx context.Context, req resource.UpdateRequest, r core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating zone", fmt.Sprintf("Calling API: %v", err)) return } + waitResp, err := wait.PartialUpdateZoneWaitHandler(ctx, r.client, projectId, zoneId).WaitWithContext(ctx) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating zone", fmt.Sprintf("Zone update waiting: %v", err)) @@ -405,20 +420,24 @@ func (r *zoneResource) Update(ctx context.Context, req resource.UpdateRequest, r core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating zone", fmt.Sprintf("Processing API payload: %v", err)) return } + diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "DNS zone updated") } // Delete deletes the resource and removes the Terraform state on success. -func (r *zoneResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform +func (r *zoneResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { //nolint:gocritic // function signature required by Terraform // Retrieve values from state var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -434,6 +453,7 @@ func (r *zoneResource) Delete(ctx context.Context, req resource.DeleteRequest, r core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting zone", fmt.Sprintf("Calling API: %v", err)) return } + _, err = wait.DeleteZoneWaitHandler(ctx, r.client, projectId, zoneId).WaitWithContext(ctx) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting zone", fmt.Sprintf("Zone deletion waiting: %v", err)) @@ -444,7 +464,7 @@ func (r *zoneResource) Delete(ctx context.Context, req resource.DeleteRequest, r } // ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: project_id,zone_id +// The expected format of the resource import identifier is: project_id,zone_id. func (r *zoneResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { idParts := strings.Split(req.ID, core.Separator) @@ -453,6 +473,7 @@ func (r *zoneResource) ImportState(ctx context.Context, req resource.ImportState "Error importing zone", fmt.Sprintf("Expected import identifier with format: [project_id],[zone_id] Got: %q", req.ID), ) + return } @@ -468,12 +489,15 @@ func mapFields(ctx context.Context, zoneResp *dns.ZoneResponse, model *Model) er if zoneResp == nil || zoneResp.Zone == nil { return fmt.Errorf("response input is nil") } + if model == nil { return fmt.Errorf("model input is nil") } + z := zoneResp.Zone var rc *int64 + if z.RecordCount != nil { recordCount64 := int64(*z.RecordCount) rc = &recordCount64 @@ -496,6 +520,7 @@ func mapFields(ctx context.Context, zoneResp *dns.ZoneResponse, model *Model) er model.Primaries = types.ListNull(types.StringType) } else { respPrimaries := *z.Primaries + modelPrimaries, err := utils.ListValuetoStringSlice(model.Primaries) if err != nil { return err @@ -510,6 +535,7 @@ func mapFields(ctx context.Context, zoneResp *dns.ZoneResponse, model *Model) er model.Primaries = primariesTF } + model.ZoneId = types.StringValue(zoneId) model.Description = types.StringPointerValue(z.Description) model.Acl = types.StringPointerValue(z.Acl) @@ -529,6 +555,7 @@ func mapFields(ctx context.Context, zoneResp *dns.ZoneResponse, model *Model) er model.State = types.StringValue(string(z.GetState())) model.Type = types.StringValue(string(z.GetType())) model.Visibility = types.StringValue(string(z.GetVisibility())) + return nil } @@ -538,13 +565,16 @@ func toCreatePayload(model *Model) (*dns.CreateZonePayload, error) { } modelPrimaries := []string{} + for _, primary := range model.Primaries.Elements() { primaryString, ok := primary.(types.String) if !ok { return nil, fmt.Errorf("type assertion failed") } + modelPrimaries = append(modelPrimaries, primaryString.ValueString()) } + return &dns.CreateZonePayload{ Name: conversion.StringValueToPointer(model.Name), DnsName: conversion.StringValueToPointer(model.DnsName), diff --git a/stackit/internal/services/dns/zone/resource_test.go b/stackit/internal/services/dns/zone/resource_test.go index d12cd90de..de23b4b64 100644 --- a/stackit/internal/services/dns/zone/resource_test.go +++ b/stackit/internal/services/dns/zone/resource_test.go @@ -270,9 +270,11 @@ func TestMapFields(t *testing.T) { if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(tt.state, tt.expected) if diff != "" { @@ -352,9 +354,11 @@ func TestToCreatePayload(t *testing.T) { if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(output, tt.expected) if diff != "" { @@ -423,9 +427,11 @@ func TestToPayloadUpdate(t *testing.T) { if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(output, tt.expected) if diff != "" { diff --git a/stackit/internal/services/git/git_acc_test.go b/stackit/internal/services/git/git_acc_test.go index a4a87d00d..8b8d6c7ec 100644 --- a/stackit/internal/services/git/git_acc_test.go +++ b/stackit/internal/services/git/git_acc_test.go @@ -25,11 +25,13 @@ var resourceMin string //go:embed testdata/resource-max.tf var resourceMax string -var nameMin = fmt.Sprintf("git-min-%s-instance", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum)) -var nameMinUpdated = fmt.Sprintf("git-min-%s-instance", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum)) -var nameMax = fmt.Sprintf("git-max-%s-instance", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum)) -var nameMaxUpdated = fmt.Sprintf("git-max-%s-instance", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum)) -var aclUpdated = "192.168.1.0/32" +var ( + nameMin = fmt.Sprintf("git-min-%s-instance", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum)) + nameMinUpdated = fmt.Sprintf("git-min-%s-instance", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum)) + nameMax = fmt.Sprintf("git-max-%s-instance", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum)) + nameMaxUpdated = fmt.Sprintf("git-max-%s-instance", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum)) + aclUpdated = "192.168.1.0/32" +) var testConfigVarsMin = config.Variables{ "project_id": config.StringVariable(testutil.ProjectId), @@ -49,6 +51,7 @@ func testConfigVarsMinUpdated() config.Variables { // update git instance to a new name // should trigger creating a new instance tempConfig["name"] = config.StringVariable(nameMinUpdated) + return tempConfig } @@ -150,6 +153,7 @@ func TestAccGitMin(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute instance_id") } + return fmt.Sprintf("%s,%s", testutil.ProjectId, instanceId), nil }, ImportState: true, @@ -268,6 +272,7 @@ func TestAccGitMax(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute instance_id") } + return fmt.Sprintf("%s,%s", testutil.ProjectId, instanceId), nil }, ImportState: true, @@ -297,7 +302,9 @@ func TestAccGitMax(t *testing.T) { func testAccCheckGitInstanceDestroy(s *terraform.State) error { ctx := context.Background() + var client *git.APIClient + var err error if testutil.GitCustomEndpoint == "" { @@ -313,10 +320,12 @@ func testAccCheckGitInstanceDestroy(s *terraform.State) error { } var instancesToDestroy []string + for _, rs := range s.RootModule().Resources { if rs.Type != "stackit_git" { continue } + instanceId := strings.Split(rs.Primary.ID, core.Separator)[1] instancesToDestroy = append(instancesToDestroy, instanceId) } @@ -331,6 +340,7 @@ func testAccCheckGitInstanceDestroy(s *terraform.State) error { if gitInstances[i].Id == nil { continue } + if utils.Contains(instancesToDestroy, *gitInstances[i].Id) { err := client.DeleteInstance(ctx, testutil.ProjectId, *gitInstances[i].Id).Execute() if err != nil { @@ -338,5 +348,6 @@ func testAccCheckGitInstanceDestroy(s *terraform.State) error { } } } + return nil } diff --git a/stackit/internal/services/git/instance/datasource.go b/stackit/internal/services/git/instance/datasource.go index d331810c1..c8a93dee6 100644 --- a/stackit/internal/services/git/instance/datasource.go +++ b/stackit/internal/services/git/instance/datasource.go @@ -6,9 +6,6 @@ import ( "fmt" "net/http" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - gitUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/git/utils" - "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" @@ -16,8 +13,10 @@ import ( "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/stackitcloud/stackit-sdk-go/core/oapierror" "github.com/stackitcloud/stackit-sdk-go/services/git" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/features" + gitUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/git/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" ) @@ -44,15 +43,19 @@ func (g *gitDataSource) Configure(ctx context.Context, req datasource.ConfigureR } features.CheckBetaResourcesEnabled(ctx, &providerData, &resp.Diagnostics, "stackit_git", "datasource") + if resp.Diagnostics.HasError() { return } apiClient := gitUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + g.client = apiClient + tflog.Info(ctx, "git client configured") } @@ -124,10 +127,11 @@ func (g *gitDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, re } } -func (g *gitDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (g *gitDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.Config.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -140,12 +144,15 @@ func (g *gitDataSource) Read(ctx context.Context, req datasource.ReadRequest, re gitInstanceResp, err := g.client.GetInstance(ctx, projectId, instanceId).Execute() if err != nil { var oapiErr *oapierror.GenericOpenAPIError + ok := errors.As(err, &oapiErr) if ok && oapiErr.StatusCode == http.StatusNotFound { resp.State.RemoveResource(ctx) return } + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading git instance", fmt.Sprintf("Calling API: %v", err)) + return } diff --git a/stackit/internal/services/git/instance/resource.go b/stackit/internal/services/git/instance/resource.go index 178ce1fe9..81aee5f44 100644 --- a/stackit/internal/services/git/instance/resource.go +++ b/stackit/internal/services/git/instance/resource.go @@ -61,7 +61,7 @@ type gitResource struct { client *git.APIClient } -// descriptions for the attributes in the Schema +// descriptions for the attributes in the Schema. var descriptions = map[string]string{ "id": "Terraform's internal resource ID, structured as \"`project_id`,`instance_id`\".", "acl": "Restricted ACL for instance access.", @@ -84,15 +84,19 @@ func (g *gitResource) Configure(ctx context.Context, req resource.ConfigureReque } features.CheckBetaResourcesEnabled(ctx, &providerData, &resp.Diagnostics, "stackit_git", "resource") + if resp.Diagnostics.HasError() { return } apiClient := gitUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + g.client = apiClient + tflog.Info(ctx, "git client configured") } @@ -186,11 +190,12 @@ func (g *gitResource) Schema(_ context.Context, _ resource.SchemaRequest, resp * } // Create creates the resource and sets the initial Terraform state for the git instance. -func (g *gitResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform +func (g *gitResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { //nolint:gocritic // function signature required by Terraform // Retrieve the planned values for the resource. var model Model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -203,6 +208,7 @@ func (g *gitResource) Create(ctx context.Context, req resource.CreateRequest, re payload, diags := toCreatePayload(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -217,6 +223,7 @@ func (g *gitResource) Create(ctx context.Context, req resource.CreateRequest, re } gitInstanceId := *gitInstanceResp.Id + _, err = wait.CreateGitInstanceWaitHandler(ctx, g.client, projectId, gitInstanceId).WaitWithContext(ctx) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating git instance", fmt.Sprintf("Git instance creation waiting: %v", err)) @@ -232,18 +239,21 @@ func (g *gitResource) Create(ctx context.Context, req resource.CreateRequest, re // Set the state with fully populated data. diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "Git Instance created") } // Read refreshes the Terraform state with the latest git instance data. -func (g *gitResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (g *gitResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { //nolint:gocritic // function signature required by Terraform // Retrieve the current state of the resource. var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -256,12 +266,15 @@ func (g *gitResource) Read(ctx context.Context, req resource.ReadRequest, resp * gitInstanceResp, err := g.client.GetInstance(ctx, projectId, instanceId).Execute() if err != nil { var oapiErr *oapierror.GenericOpenAPIError + ok := errors.As(err, &oapiErr) if ok && oapiErr.StatusCode == http.StatusNotFound { resp.State.RemoveResource(ctx) return } + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading git instance", fmt.Sprintf("Calling API: %v", err)) + return } @@ -283,17 +296,18 @@ func (g *gitResource) Read(ctx context.Context, req resource.ReadRequest, resp * // As a result, the Update function is redundant since any modifications will // automatically trigger a resource recreation through Terraform's built-in // lifecycle management. -func (g *gitResource) Update(ctx context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform +func (g *gitResource) Update(ctx context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { //nolint:gocritic // function signature required by Terraform // git instances cannot be updated, so we log an error. core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating git instance", "Git Instance can't be updated") } // Delete deletes the git instance and removes it from the Terraform state on success. -func (g *gitResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform +func (g *gitResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { //nolint:gocritic // function signature required by Terraform // Retrieve current state of the resource. var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -320,7 +334,7 @@ func (g *gitResource) Delete(ctx context.Context, req resource.DeleteRequest, re } // ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: project_id,instance_id +// The expected format of the resource import identifier is: project_id,instance_id. func (g *gitResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { // Split the import identifier to extract project ID and email. idParts := strings.Split(req.ID, core.Separator) @@ -331,6 +345,7 @@ func (g *gitResource) ImportState(ctx context.Context, req resource.ImportStateR "Error importing git instance", fmt.Sprintf("Expected import identifier with format: [project_id],[instance_id] Got: %q", req.ID), ) + return } @@ -348,6 +363,7 @@ func mapFields(ctx context.Context, resp *git.Instance, model *Model) error { if resp == nil { return fmt.Errorf("response input is nil") } + if model == nil { return fmt.Errorf("model input is nil") } @@ -357,7 +373,9 @@ func mapFields(ctx context.Context, resp *git.Instance, model *Model) error { } aclList := types.ListNull(types.StringType) + var diags diag.Diagnostics + if resp.Acl != nil && len(*resp.Acl) > 0 { aclList, diags = types.ListValueFrom(ctx, types.StringType, resp.Acl) if diags.HasError() { @@ -384,7 +402,7 @@ func mapFields(ctx context.Context, resp *git.Instance, model *Model) error { return nil } -// toCreatePayload creates the payload to create a git instance +// toCreatePayload creates the payload to create a git instance. func toCreatePayload(ctx context.Context, model *Model) (git.CreateInstancePayload, diag.Diagnostics) { diags := diag.Diagnostics{} @@ -396,16 +414,17 @@ func toCreatePayload(ctx context.Context, model *Model) (git.CreateInstancePaylo Name: model.Name.ValueStringPointer(), } - if !(model.ACL.IsNull() || model.ACL.IsUnknown()) { + if !model.ACL.IsNull() && !model.ACL.IsUnknown() { var acl []string aclDiags := model.ACL.ElementsAs(ctx, &acl, false) diags.Append(aclDiags...) + if !aclDiags.HasError() { payload.Acl = &acl } } - if !(model.Flavor.IsNull() || model.Flavor.IsUnknown()) { + if !model.Flavor.IsNull() && !model.Flavor.IsUnknown() { payload.Flavor = git.CreateInstancePayloadGetFlavorAttributeType(model.Flavor.ValueStringPointer()) } diff --git a/stackit/internal/services/git/instance/resource_test.go b/stackit/internal/services/git/instance/resource_test.go index 585f21edc..4527cf6fd 100644 --- a/stackit/internal/services/git/instance/resource_test.go +++ b/stackit/internal/services/git/instance/resource_test.go @@ -130,14 +130,17 @@ func TestMapFields(t *testing.T) { if tt.expected != nil { state.ProjectId = tt.expected.ProjectId } + err := mapFields(context.Background(), tt.input, state) if tt.isValid && err != nil { t.Fatalf("expected success, got error: %v", err) } + if !tt.isValid && err == nil { t.Fatalf("expected error, got nil") } + if tt.isValid { if diff := cmp.Diff(tt.expected, state); diff != "" { t.Errorf("unexpected diff (-want +got):\n%s", diff) diff --git a/stackit/internal/services/git/utils/util.go b/stackit/internal/services/git/utils/util.go index e55f2351e..c6fd645d0 100644 --- a/stackit/internal/services/git/utils/util.go +++ b/stackit/internal/services/git/utils/util.go @@ -19,6 +19,7 @@ func ConfigureClient(ctx context.Context, providerData *core.ProviderData, diags if providerData.GitCustomEndpoint != "" { apiClientConfigOptions = append(apiClientConfigOptions, config.WithEndpoint(providerData.GitCustomEndpoint)) } + apiClient, err := git.NewAPIClient(apiClientConfigOptions...) if err != nil { core.LogAndAddError(ctx, diags, "Error configuring API client", fmt.Sprintf("Configuring client: %v. This is an error related to the provider configuration, not to the resource configuration", err)) diff --git a/stackit/internal/services/git/utils/util_test.go b/stackit/internal/services/git/utils/util_test.go index 92c812023..27875b76e 100644 --- a/stackit/internal/services/git/utils/util_test.go +++ b/stackit/internal/services/git/utils/util_test.go @@ -22,6 +22,7 @@ const ( func TestConfigureClient(t *testing.T) { /* mock authentication by setting service account token env variable */ os.Clearenv() + err := os.Setenv(sdkClients.ServiceAccountToken, "mock-val") if err != nil { t.Errorf("error setting env variable: %v", err) @@ -30,6 +31,7 @@ func TestConfigureClient(t *testing.T) { type args struct { providerData *core.ProviderData } + tests := []struct { name string args args @@ -50,6 +52,7 @@ func TestConfigureClient(t *testing.T) { if err != nil { t.Errorf("error configuring client: %v", err) } + return apiClient }(), wantErr: false, @@ -70,6 +73,7 @@ func TestConfigureClient(t *testing.T) { if err != nil { t.Errorf("error configuring client: %v", err) } + return apiClient }(), wantErr: false, @@ -81,6 +85,7 @@ func TestConfigureClient(t *testing.T) { diags := diag.Diagnostics{} actual := ConfigureClient(ctx, tt.args.providerData, &diags) + if diags.HasError() != tt.wantErr { t.Errorf("ConfigureClient() error = %v, want %v", diags.HasError(), tt.wantErr) } diff --git a/stackit/internal/services/iaas/affinitygroup/datasource.go b/stackit/internal/services/iaas/affinitygroup/datasource.go index ed4507001..d47a97bfb 100644 --- a/stackit/internal/services/iaas/affinitygroup/datasource.go +++ b/stackit/internal/services/iaas/affinitygroup/datasource.go @@ -6,13 +6,6 @@ import ( "net/http" "regexp" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/datasource" @@ -21,6 +14,11 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/stackitcloud/stackit-sdk-go/services/iaas" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" ) var ( @@ -43,10 +41,13 @@ func (d *affinityGroupDatasource) Configure(ctx context.Context, req datasource. } apiClient := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + d.client = apiClient + tflog.Info(ctx, "iaas client configured") } @@ -109,13 +110,15 @@ func (d *affinityGroupDatasource) Schema(_ context.Context, _ datasource.SchemaR } } -func (d *affinityGroupDatasource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (d *affinityGroupDatasource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.Config.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() affinityGroupId := model.AffinityGroupId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) @@ -134,6 +137,7 @@ func (d *affinityGroupDatasource) Read(ctx context.Context, req datasource.ReadR }, ) resp.State.RemoveResource(ctx) + return } @@ -144,8 +148,10 @@ func (d *affinityGroupDatasource) Read(ctx context.Context, req datasource.ReadR diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "Affinity group read") } diff --git a/stackit/internal/services/iaas/affinitygroup/resource.go b/stackit/internal/services/iaas/affinitygroup/resource.go index 1110e4296..22c3a3abd 100644 --- a/stackit/internal/services/iaas/affinitygroup/resource.go +++ b/stackit/internal/services/iaas/affinitygroup/resource.go @@ -7,14 +7,6 @@ import ( "regexp" "strings" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - - iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/path" @@ -27,6 +19,11 @@ import ( "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/stackitcloud/stackit-sdk-go/core/oapierror" "github.com/stackitcloud/stackit-sdk-go/services/iaas" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" ) var ( @@ -35,7 +32,7 @@ var ( _ resource.ResourceWithImportState = &affinityGroupResource{} ) -// Model is the provider's internal model +// Model is the provider's internal model. type Model struct { Id types.String `tfsdk:"id"` ProjectId types.String `tfsdk:"project_id"` @@ -67,10 +64,13 @@ func (r *affinityGroupResource) Configure(ctx context.Context, req resource.Conf } apiClient := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + r.client = apiClient + tflog.Info(ctx, "iaas client configured") } @@ -146,13 +146,15 @@ func (r *affinityGroupResource) Schema(_ context.Context, _ resource.SchemaReque } // Create creates the resource and sets the initial Terraform state. -func (r *affinityGroupResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *affinityGroupResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.Config.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) @@ -162,11 +164,13 @@ func (r *affinityGroupResource) Create(ctx context.Context, req resource.CreateR core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating affinity group", fmt.Sprintf("Creating API payload: %v", err)) return } + affinityGroupResp, err := r.client.CreateAffinityGroup(ctx, projectId).CreateAffinityGroupPayload(*payload).Execute() if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating affinity group", fmt.Sprintf("Calling API: %v", err)) return } + ctx = tflog.SetField(ctx, "affinity_group_id", affinityGroupResp.Id) // Map response body to schema @@ -178,20 +182,24 @@ func (r *affinityGroupResource) Create(ctx context.Context, req resource.CreateR diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "Affinity group created") } // Read refreshes the Terraform state with the latest data. -func (r *affinityGroupResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (r *affinityGroupResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() affinityGroupId := model.AffinityGroupId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) @@ -204,7 +212,9 @@ func (r *affinityGroupResource) Read(ctx context.Context, req resource.ReadReque resp.State.RemoveResource(ctx) return } + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading affinity group", fmt.Sprintf("Call API: %v", err)) + return } @@ -215,22 +225,25 @@ func (r *affinityGroupResource) Read(ctx context.Context, req resource.ReadReque // Set refreshed state diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "Affinity group read") } -func (r *affinityGroupResource) Update(_ context.Context, _ resource.UpdateRequest, _ *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *affinityGroupResource) Update(_ context.Context, _ resource.UpdateRequest, _ *resource.UpdateResponse) { //nolint:gocritic // function signature required by Terraform // Update is not supported, all fields require replace } // Delete deletes the resource and removes the Terraform state on success. -func (r *affinityGroupResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform +func (r *affinityGroupResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { //nolint:gocritic // function signature required by Terraform // Retrieve values from state var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -258,6 +271,7 @@ func (r *affinityGroupResource) ImportState(ctx context.Context, req resource.Im "Error importing affinity group", fmt.Sprintf("Expected import indentifier with format: [project_id],[affinity_group_id], got: %q", req.ID), ) + return } @@ -310,6 +324,7 @@ func mapFields(ctx context.Context, affinityGroupResp *iaas.AffinityGroup, model if diags.HasError() { return fmt.Errorf("convert members to StringValue list: %w", core.DiagsToError(diags)) } + model.Members = members } else if model.Members.IsNull() { model.Members = types.ListNull(types.StringType) diff --git a/stackit/internal/services/iaas/affinitygroup/resource_test.go b/stackit/internal/services/iaas/affinitygroup/resource_test.go index a4e203910..83e789ee6 100644 --- a/stackit/internal/services/iaas/affinitygroup/resource_test.go +++ b/stackit/internal/services/iaas/affinitygroup/resource_test.go @@ -60,9 +60,11 @@ func TestMapFields(t *testing.T) { if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed") } + if tt.isValid { diff := cmp.Diff(tt.state, tt.expected) if diff != "" { @@ -100,9 +102,11 @@ func TestToCreatePayload(t *testing.T) { if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(output, tt.expected) if diff != "" { diff --git a/stackit/internal/services/iaas/iaas_acc_test.go b/stackit/internal/services/iaas/iaas_acc_test.go index dda89f70a..a9e419c36 100644 --- a/stackit/internal/services/iaas/iaas_acc_test.go +++ b/stackit/internal/services/iaas/iaas_acc_test.go @@ -101,7 +101,7 @@ var ( const ( keypairPublicKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIDsPd27M449akqCtdFg2+AmRVJz6eWio0oMP9dVg7XZ" - // TODO: create network area using terraform resource instead once it's out of experimental stage and GA + // TODO: create network area using terraform resource instead once it's out of experimental stage and GA. testNetworkAreaId = "25bbf23a-8134-4439-9f5e-1641caf8354e" ) @@ -119,6 +119,7 @@ var testConfigServerVarsMinUpdated = func() config.Variables { } updatedConfig["name"] = config.StringVariable(testutil.ProjectId) updatedConfig["machine_type"] = config.StringVariable("t1.2") + return updatedConfig }() @@ -147,6 +148,7 @@ var testConfigServerVarsMaxUpdated = func() config.Variables { updatedConfig["machine_type"] = config.StringVariable("t1.2") updatedConfig["label"] = config.StringVariable("updated") updatedConfig["desired_status"] = config.StringVariable("inactive") + return updatedConfig }() @@ -159,6 +161,7 @@ var testConfigServerVarsMaxUpdatedDesiredStatus = func() config.Variables { updatedConfig["machine_type"] = config.StringVariable("t1.2") updatedConfig["label"] = config.StringVariable("updated") updatedConfig["desired_status"] = config.StringVariable("deallocated") + return updatedConfig }() @@ -192,6 +195,7 @@ var testConfigNetworkInterfaceVarsMaxUpdated = func() config.Variables { updatedConfig["ipv4"] = config.StringVariable("10.2.10.21") updatedConfig["security"] = config.BoolVariable(false) updatedConfig["label"] = config.StringVariable("updated") + return updatedConfig }() @@ -207,6 +211,7 @@ var testConfigVolumeVarsMinUpdated = func() config.Variables { updatedConfig[k] = v } updatedConfig["size"] = config.IntegerVariable(20) + return updatedConfig }() @@ -229,6 +234,7 @@ var testConfigVolumeVarsMaxUpdated = func() config.Variables { updatedConfig["name"] = config.StringVariable(fmt.Sprintf("%s-updated", testutil.ConvertConfigVariable(testConfigVolumeVarsMax["name"]))) updatedConfig["description"] = config.StringVariable(fmt.Sprintf("%s-updated", testutil.ConvertConfigVariable(testConfigVolumeVarsMax["description"]))) updatedConfig["label"] = config.StringVariable("updated") + return updatedConfig }() @@ -258,6 +264,7 @@ var testConfigNetworkV1VarsMaxUpdated = func() config.Variables { updatedConfig["ipv4_gateway"] = config.StringVariable("") updatedConfig["ipv4_nameserver_0"] = config.StringVariable("10.2.2.10") updatedConfig["label"] = config.StringVariable("updated") + return updatedConfig }() @@ -294,6 +301,7 @@ var testConfigNetworkV2VarsMaxUpdated = func() config.Variables { updatedConfig["ipv4_gateway"] = config.StringVariable("") updatedConfig["ipv4_nameserver_0"] = config.StringVariable("10.2.2.10") updatedConfig["label"] = config.StringVariable("updated") + return updatedConfig }() @@ -313,6 +321,7 @@ var testConfigNetworkAreaVarsMinUpdated = func() config.Variables { } updatedConfig["name"] = config.StringVariable(fmt.Sprintf("%s-updated", testutil.ConvertConfigVariable(updatedConfig["name"]))) updatedConfig["network_ranges_prefix"] = config.StringVariable("10.0.0.0/18") + return updatedConfig }() @@ -342,6 +351,7 @@ var testConfigNetworkAreaVarsMaxUpdated = func() config.Variables { updatedConfig["max_prefix_length"] = config.IntegerVariable(25) updatedConfig["min_prefix_length"] = config.IntegerVariable(20) updatedConfig["label"] = config.StringVariable("updated") + return updatedConfig }() @@ -356,7 +366,9 @@ func testConfigSecurityGroupsVarsMinUpdated() config.Variables { for k, v := range testConfigSecurityGroupsVarsMin { updatedConfig[k] = v } + updatedConfig["name"] = config.StringVariable(fmt.Sprintf("%s-updated", testutil.ConvertConfigVariable(updatedConfig["name"]))) + return updatedConfig } @@ -382,6 +394,7 @@ func testConfigSecurityGroupsVarsMaxUpdated() config.Variables { for k, v := range testConfigSecurityGroupsVarsMax { updatedConfig[k] = v } + updatedConfig["name"] = config.StringVariable(fmt.Sprintf("%s-updated", testutil.ConvertConfigVariable(updatedConfig["name"]))) updatedConfig["name_remote"] = config.StringVariable(fmt.Sprintf("%s-updated", testutil.ConvertConfigVariable(updatedConfig["name_remote"]))) updatedConfig["description"] = config.StringVariable(fmt.Sprintf("%s-updated", testutil.ConvertConfigVariable(updatedConfig["description"]))) @@ -400,6 +413,7 @@ var testConfigImageVarsMin = func() config.Variables { } localFilePath = filePath } + return config.Variables{ "project_id": config.StringVariable(testutil.ProjectId), "name": config.StringVariable(fmt.Sprintf("tf-acc-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlpha))), @@ -414,6 +428,7 @@ var testConfigImageVarsMinUpdated = func() config.Variables { updatedConfig[k] = v } updatedConfig["name"] = config.StringVariable(fmt.Sprintf("%s-updated", testutil.ConvertConfigVariable(updatedConfig["name"]))) + return updatedConfig }() @@ -427,6 +442,7 @@ var testConfigImageVarsMax = func() config.Variables { } localFilePath = filePath } + return config.Variables{ "project_id": config.StringVariable(testutil.ProjectId), "name": config.StringVariable(fmt.Sprintf("tf-acc-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlpha))), @@ -473,6 +489,7 @@ var testConfigImageVarsMaxUpdated = func() config.Variables { updatedConfig["uefi"] = config.BoolVariable(false) updatedConfig["video_model"] = config.StringVariable("virtio") updatedConfig["virtio_scsi"] = config.BoolVariable(false) + return updatedConfig }() @@ -493,6 +510,7 @@ var testConfigKeyPairMaxUpdated = func() config.Variables { updatedConfig[k] = v } updatedConfig["label"] = config.StringVariable("updated") + return updatedConfig }() @@ -500,7 +518,7 @@ var testConfigMachineTypeVars = config.Variables{ "project_id": config.StringVariable(testutil.ProjectId), } -// if no local file is provided the test should create a default file and work with this instead of failing +// if no local file is provided the test should create a default file and work with this instead of failing. var localFileForIaasImage os.File func TestAccNetworkV1Min(t *testing.T) { @@ -559,6 +577,7 @@ func TestAccNetworkV1Min(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute network_id") } + return fmt.Sprintf("%s,%s", testutil.ProjectId, networkId), nil }, ImportState: true, @@ -682,6 +701,7 @@ func TestAccNetworkV1Max(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute network_id") } + return fmt.Sprintf("%s,%s", testutil.ProjectId, networkId), nil }, ImportState: true, @@ -712,6 +732,7 @@ func TestAccNetworkV1Max(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute network_id") } + return fmt.Sprintf("%s,%s", testutil.ProjectId, networkId), nil }, ImportState: true, @@ -835,6 +856,7 @@ func TestAccNetworkV2Min(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute network_id") } + return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, region, networkId), nil }, ImportState: true, @@ -1057,6 +1079,7 @@ func TestAccNetworkV2Max(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute network_id") } + return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, region, networkId), nil }, ImportState: true, @@ -1233,6 +1256,7 @@ func TestAccNetworkAreaMin(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute network_area_id") } + return fmt.Sprintf("%s,%s", testutil.OrganizationId, networkAreaId), nil }, ImportState: true, @@ -1254,6 +1278,7 @@ func TestAccNetworkAreaMin(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute network_area_route_id") } + return fmt.Sprintf("%s,%s,%s", testutil.OrganizationId, networkAreaId, networkAreaRouteId), nil }, ImportState: true, @@ -1401,6 +1426,7 @@ func TestAccNetworkAreaMax(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute network_area_id") } + return fmt.Sprintf("%s,%s", testutil.OrganizationId, networkAreaId), nil }, ImportState: true, @@ -1422,6 +1448,7 @@ func TestAccNetworkAreaMax(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute network_area_route_id") } + return fmt.Sprintf("%s,%s,%s", testutil.OrganizationId, networkAreaId, networkAreaRouteId), nil }, ImportState: true, @@ -1560,6 +1587,7 @@ func TestAccVolumeMin(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute volume_id") } + return fmt.Sprintf("%s,%s", testutil.ProjectId, volumeId), nil }, ImportState: true, @@ -1577,6 +1605,7 @@ func TestAccVolumeMin(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute volume_id") } + return fmt.Sprintf("%s,%s", testutil.ProjectId, volumeId), nil }, ImportState: true, @@ -1726,6 +1755,7 @@ func TestAccVolumeMax(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute volume_id") } + return fmt.Sprintf("%s,%s", testutil.ProjectId, volumeId), nil }, ImportState: true, @@ -1743,6 +1773,7 @@ func TestAccVolumeMax(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute volume_id") } + return fmt.Sprintf("%s,%s", testutil.ProjectId, volumeId), nil }, ImportState: true, @@ -1885,6 +1916,7 @@ func TestAccServerMin(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute server_id") } + return fmt.Sprintf("%s,%s", testutil.ProjectId, serverId), nil }, ImportState: true, @@ -2121,6 +2153,7 @@ func TestAccServerMax(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute affinity_group_id") } + return fmt.Sprintf("%s,%s", testutil.ProjectId, affinityGroupId), nil }, ImportState: true, @@ -2138,6 +2171,7 @@ func TestAccServerMax(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute volume_id") } + return fmt.Sprintf("%s,%s", testutil.ProjectId, volumeId), nil }, ImportState: true, @@ -2155,6 +2189,7 @@ func TestAccServerMax(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute volume_id") } + return fmt.Sprintf("%s,%s", testutil.ProjectId, volumeId), nil }, ImportState: true, @@ -2176,6 +2211,7 @@ func TestAccServerMax(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute volume_id") } + return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, serverId, volumeId), nil }, ImportState: true, @@ -2193,6 +2229,7 @@ func TestAccServerMax(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute network_id") } + return fmt.Sprintf("%s,%s", testutil.ProjectId, networkId), nil }, ImportState: true, @@ -2215,6 +2252,7 @@ func TestAccServerMax(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute network_interface_id") } + return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, networkId, networkInterfaceId), nil }, ImportState: true, @@ -2236,6 +2274,7 @@ func TestAccServerMax(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute network_interface_id") } + return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, networkId, networkInterfaceId), nil }, ImportState: true, @@ -2257,6 +2296,7 @@ func TestAccServerMax(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute network_interface_id") } + return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, serverId, networkInterfaceId), nil }, ImportState: true, @@ -2274,6 +2314,7 @@ func TestAccServerMax(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute name") } + return keyPairName, nil }, ImportState: true, @@ -2295,6 +2336,7 @@ func TestAccServerMax(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute volume_id") } + return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, serverId, serviceAccountEmail), nil }, ImportState: true, @@ -2312,6 +2354,7 @@ func TestAccServerMax(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute server_id") } + return fmt.Sprintf("%s,%s", testutil.ProjectId, serverId), nil }, ImportState: true, @@ -2587,6 +2630,7 @@ func TestAccAffinityGroupMin(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute affinity_group_id") } + return fmt.Sprintf("%s,%s", testutil.ProjectId, affinityGroupId), nil }, ImportState: true, @@ -2604,7 +2648,6 @@ func TestAccIaaSSecurityGroupMin(t *testing.T) { ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, CheckDestroy: testAccCheckDestroy, Steps: []resource.TestStep{ - // Creation { ConfigVariables: testConfigSecurityGroupsVarsMin, @@ -2684,6 +2727,7 @@ func TestAccIaaSSecurityGroupMin(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute security_group_id") } + return fmt.Sprintf("%s,%s", testutil.ProjectId, securityGroupId), nil }, ImportState: true, @@ -2705,6 +2749,7 @@ func TestAccIaaSSecurityGroupMin(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute security_group_rule_id") } + return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, securityGroupId, securityGroupRuleId), nil }, ImportState: true, @@ -2745,7 +2790,6 @@ func TestAccIaaSSecurityGroupMax(t *testing.T) { ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, CheckDestroy: testAccCheckDestroy, Steps: []resource.TestStep{ - // Creation { ConfigVariables: testConfigSecurityGroupsVarsMax, @@ -2992,6 +3036,7 @@ func TestAccIaaSSecurityGroupMax(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute security_group_id") } + return fmt.Sprintf("%s,%s", testutil.ProjectId, securityGroupId), nil }, ImportState: true, @@ -3013,6 +3058,7 @@ func TestAccIaaSSecurityGroupMax(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute security_group_rule_id") } + return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, securityGroupId, securityGroupRuleId), nil }, ImportState: true, @@ -3215,6 +3261,7 @@ func TestAccNetworkInterfaceMin(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute network_interface_id") } + return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, networkId, networkInterfaceId), nil }, ImportState: true, @@ -3232,6 +3279,7 @@ func TestAccNetworkInterfaceMin(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute network_id") } + return fmt.Sprintf("%s,%s", testutil.ProjectId, networkId), nil }, ImportState: true, @@ -3249,6 +3297,7 @@ func TestAccNetworkInterfaceMin(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute public_ip_id") } + return fmt.Sprintf("%s,%s", testutil.ProjectId, publicIpId), nil }, ImportState: true, @@ -3472,6 +3521,7 @@ func TestAccNetworkInterfaceMax(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute network_interface_id") } + return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, networkId, networkInterfaceId), nil }, ImportState: true, @@ -3489,6 +3539,7 @@ func TestAccNetworkInterfaceMax(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute network_id") } + return fmt.Sprintf("%s,%s", testutil.ProjectId, networkId), nil }, ImportState: true, @@ -3506,6 +3557,7 @@ func TestAccNetworkInterfaceMax(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute public_ip_id") } + return fmt.Sprintf("%s,%s", testutil.ProjectId, publicIpId), nil }, ImportState: true, @@ -3527,6 +3579,7 @@ func TestAccNetworkInterfaceMax(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute network_interface_id") } + return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, networkId, networkInterfaceId), nil }, ImportState: true, @@ -3544,6 +3597,7 @@ func TestAccNetworkInterfaceMax(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute public_ip_id") } + return fmt.Sprintf("%s,%s", testutil.ProjectId, publicIpId), nil }, ImportState: true, @@ -3565,6 +3619,7 @@ func TestAccNetworkInterfaceMax(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute network_interface_id") } + return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, publicIpId, networkInterfaceId), nil }, ImportState: true, @@ -3705,6 +3760,7 @@ func TestAccKeyPairMin(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute name") } + return keyPairName, nil }, ImportState: true, @@ -3771,6 +3827,7 @@ func TestAccKeyPairMax(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute name") } + return keyPairName, nil }, ImportState: true, @@ -3797,7 +3854,6 @@ func TestAccImageMin(t *testing.T) { ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, CheckDestroy: testAccCheckDestroy, Steps: []resource.TestStep{ - // Creation { ConfigVariables: testConfigImageVarsMin, @@ -3860,6 +3916,7 @@ func TestAccImageMin(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute image_id") } + return fmt.Sprintf("%s,%s", testutil.ProjectId, imageId), nil }, ImportState: true, @@ -3895,7 +3952,6 @@ func TestAccImageMax(t *testing.T) { ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, CheckDestroy: testAccCheckDestroy, Steps: []resource.TestStep{ - // Creation { ConfigVariables: testConfigImageVarsMax, @@ -3990,6 +4046,7 @@ func TestAccImageMax(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute image_id") } + return fmt.Sprintf("%s,%s", testutil.ProjectId, imageId), nil }, ImportState: true, @@ -4267,6 +4324,7 @@ func testAccCheckDestroy(s *terraform.State) error { testAccCheckIaaSKeyPairDestroy, testAccCheckIaaSImageDestroy, } + var errs []error wg := sync.WaitGroup{} @@ -4278,16 +4336,21 @@ func testAccCheckDestroy(s *terraform.State) error { if err != nil { errs = append(errs, err) } + wg.Done() }() } + wg.Wait() + return errors.Join(errs...) } func testAccCheckNetworkV1Destroy(s *terraform.State) error { ctx := context.Background() + var client *iaas.APIClient + var err error if testutil.IaaSCustomEndpoint == "" { client, err = iaas.NewAPIClient( @@ -4298,6 +4361,7 @@ func testAccCheckNetworkV1Destroy(s *terraform.State) error { stackitSdkConfig.WithEndpoint(testutil.IaaSCustomEndpoint), ) } + if err != nil { return fmt.Errorf("creating client: %w", err) } @@ -4308,7 +4372,9 @@ func testAccCheckNetworkV1Destroy(s *terraform.State) error { if rs.Type != "stackit_network" { continue } + networkId := strings.Split(rs.Primary.ID, core.Separator)[1] + err := client.DeleteNetworkExecute(ctx, testutil.ProjectId, networkId) if err != nil { var oapiErr *oapierror.GenericOpenAPIError @@ -4317,8 +4383,10 @@ func testAccCheckNetworkV1Destroy(s *terraform.State) error { continue } } + errs = append(errs, fmt.Errorf("cannot trigger network deletion %q: %w", networkId, err)) } + _, err = wait.DeleteNetworkWaitHandler(ctx, client, testutil.ProjectId, networkId).WaitWithContext(ctx) if err != nil { errs = append(errs, fmt.Errorf("cannot delete network %q: %w", networkId, err)) @@ -4330,7 +4398,9 @@ func testAccCheckNetworkV1Destroy(s *terraform.State) error { func testAccCheckNetworkV2Destroy(s *terraform.State) error { ctx := context.Background() + var client *iaasalpha.APIClient + var err error if testutil.IaaSCustomEndpoint == "" { client, err = iaasalpha.NewAPIClient() @@ -4339,6 +4409,7 @@ func testAccCheckNetworkV2Destroy(s *terraform.State) error { stackitSdkConfig.WithEndpoint(testutil.IaaSCustomEndpoint), ) } + if err != nil { return fmt.Errorf("creating client: %w", err) } @@ -4349,8 +4420,10 @@ func testAccCheckNetworkV2Destroy(s *terraform.State) error { if rs.Type != "stackit_network" { continue } + region := strings.Split(rs.Primary.ID, core.Separator)[1] networkId := strings.Split(rs.Primary.ID, core.Separator)[2] + err := client.DeleteNetworkExecute(ctx, testutil.ProjectId, region, networkId) if err != nil { var oapiErr *oapierror.GenericOpenAPIError @@ -4359,8 +4432,10 @@ func testAccCheckNetworkV2Destroy(s *terraform.State) error { continue } } + errs = append(errs, fmt.Errorf("cannot trigger network deletion %q: %w", networkId, err)) } + _, err = waitAlpha.DeleteNetworkWaitHandler(ctx, client, testutil.ProjectId, region, networkId).WaitWithContext(ctx) if err != nil { errs = append(errs, fmt.Errorf("cannot delete network %q: %w", networkId, err)) @@ -4372,7 +4447,9 @@ func testAccCheckNetworkV2Destroy(s *terraform.State) error { func testAccCheckNetworkInterfaceDestroy(s *terraform.State) error { ctx := context.Background() + var client *iaas.APIClient + var err error if testutil.IaaSCustomEndpoint == "" { client, err = iaas.NewAPIClient( @@ -4383,6 +4460,7 @@ func testAccCheckNetworkInterfaceDestroy(s *terraform.State) error { stackitSdkConfig.WithEndpoint(testutil.IaaSCustomEndpoint), ) } + if err != nil { return fmt.Errorf("creating client: %w", err) } @@ -4393,9 +4471,11 @@ func testAccCheckNetworkInterfaceDestroy(s *terraform.State) error { if rs.Type != "stackit_network_interface" { continue } + ids := strings.Split(rs.Primary.ID, core.Separator) networkId := ids[1] networkInterfaceId := ids[2] + err := client.DeleteNicExecute(ctx, testutil.ProjectId, networkId, networkInterfaceId) if err != nil { var oapiErr *oapierror.GenericOpenAPIError @@ -4404,8 +4484,10 @@ func testAccCheckNetworkInterfaceDestroy(s *terraform.State) error { continue } } + errs = append(errs, fmt.Errorf("cannot trigger network interface deletion %q: %w", networkInterfaceId, err)) } + if err != nil { errs = append(errs, fmt.Errorf("cannot delete network interface %q: %w", networkInterfaceId, err)) } @@ -4416,7 +4498,9 @@ func testAccCheckNetworkInterfaceDestroy(s *terraform.State) error { func testAccCheckNetworkAreaDestroy(s *terraform.State) error { ctx := context.Background() + var client *iaas.APIClient + var err error if testutil.IaaSCustomEndpoint == "" { client, err = iaas.NewAPIClient( @@ -4427,16 +4511,19 @@ func testAccCheckNetworkAreaDestroy(s *terraform.State) error { stackitSdkConfig.WithEndpoint(testutil.IaaSCustomEndpoint), ) } + if err != nil { return fmt.Errorf("creating client: %w", err) } // network areas networkAreasToDestroy := []string{} + for _, rs := range s.RootModule().Resources { if rs.Type != "stackit_network_area" { continue } + networkAreaId := strings.Split(rs.Primary.ID, core.Separator)[1] networkAreasToDestroy = append(networkAreasToDestroy, networkAreaId) } @@ -4451,6 +4538,7 @@ func testAccCheckNetworkAreaDestroy(s *terraform.State) error { if networkAreas[i].AreaId == nil { continue } + if utils.Contains(networkAreasToDestroy, *networkAreas[i].AreaId) { err := client.DeleteNetworkAreaExecute(ctx, testutil.OrganizationId, *networkAreas[i].AreaId) if err != nil { @@ -4458,12 +4546,15 @@ func testAccCheckNetworkAreaDestroy(s *terraform.State) error { } } } + return nil } func testAccCheckIaaSVolumeDestroy(s *terraform.State) error { ctx := context.Background() + var client *iaas.APIClient + var err error if testutil.IaaSCustomEndpoint == "" { client, err = iaas.NewAPIClient( @@ -4474,11 +4565,13 @@ func testAccCheckIaaSVolumeDestroy(s *terraform.State) error { stackitSdkConfig.WithEndpoint(testutil.IaaSCustomEndpoint), ) } + if err != nil { return fmt.Errorf("creating client: %w", err) } volumesToDestroy := []string{} + for _, rs := range s.RootModule().Resources { if rs.Type != "stackit_volume" { continue @@ -4498,6 +4591,7 @@ func testAccCheckIaaSVolumeDestroy(s *terraform.State) error { if volumes[i].Id == nil { continue } + if utils.Contains(volumesToDestroy, *volumes[i].Id) { err := client.DeleteVolumeExecute(ctx, testutil.ProjectId, *volumes[i].Id) if err != nil { @@ -4505,14 +4599,19 @@ func testAccCheckIaaSVolumeDestroy(s *terraform.State) error { } } } + return nil } func testAccCheckServerDestroy(s *terraform.State) error { ctx := context.Background() + var alphaClient *iaas.APIClient + var client *iaas.APIClient + var err error + var alphaErr error if testutil.IaaSCustomEndpoint == "" { alphaClient, alphaErr = iaas.NewAPIClient( @@ -4529,6 +4628,7 @@ func testAccCheckServerDestroy(s *terraform.State) error { stackitSdkConfig.WithRegion("eu01"), ) } + if err != nil || alphaErr != nil { return fmt.Errorf("creating client: %w, %w", err, alphaErr) } @@ -4536,6 +4636,7 @@ func testAccCheckServerDestroy(s *terraform.State) error { // Servers serversToDestroy := []string{} + for _, rs := range s.RootModule().Resources { if rs.Type != "stackit_server" { continue @@ -4555,6 +4656,7 @@ func testAccCheckServerDestroy(s *terraform.State) error { if servers[i].Id == nil { continue } + if utils.Contains(serversToDestroy, *servers[i].Id) { err := alphaClient.DeleteServerExecute(ctx, testutil.ProjectId, *servers[i].Id) if err != nil { @@ -4566,6 +4668,7 @@ func testAccCheckServerDestroy(s *terraform.State) error { // Networks networksToDestroy := []string{} + for _, rs := range s.RootModule().Resources { if rs.Type != "stackit_network" { continue @@ -4585,6 +4688,7 @@ func testAccCheckServerDestroy(s *terraform.State) error { if networks[i].NetworkId == nil { continue } + if utils.Contains(networksToDestroy, *networks[i].NetworkId) { err := client.DeleteNetworkExecute(ctx, testutil.ProjectId, *networks[i].NetworkId) if err != nil { @@ -4598,7 +4702,9 @@ func testAccCheckServerDestroy(s *terraform.State) error { func testAccCheckAffinityGroupDestroy(s *terraform.State) error { ctx := context.Background() + var client *iaas.APIClient + var err error if testutil.IaaSCustomEndpoint == "" { client, err = iaas.NewAPIClient( @@ -4609,11 +4715,13 @@ func testAccCheckAffinityGroupDestroy(s *terraform.State) error { stackitSdkConfig.WithEndpoint(testutil.IaaSCustomEndpoint), ) } + if err != nil { return fmt.Errorf("creating client: %w", err) } affinityGroupsToDestroy := []string{} + for _, rs := range s.RootModule().Resources { if rs.Type != "stackit_affinity_group" { continue @@ -4633,6 +4741,7 @@ func testAccCheckAffinityGroupDestroy(s *terraform.State) error { if affinityGroups[i].Id == nil { continue } + if utils.Contains(affinityGroupsToDestroy, *affinityGroups[i].Id) { err := client.DeleteAffinityGroupExecute(ctx, testutil.ProjectId, *affinityGroups[i].Id) if err != nil { @@ -4640,12 +4749,15 @@ func testAccCheckAffinityGroupDestroy(s *terraform.State) error { } } } + return nil } func testAccCheckIaaSSecurityGroupDestroy(s *terraform.State) error { ctx := context.Background() + var client *iaas.APIClient + var err error if testutil.IaaSCustomEndpoint == "" { client, err = iaas.NewAPIClient( @@ -4656,11 +4768,13 @@ func testAccCheckIaaSSecurityGroupDestroy(s *terraform.State) error { stackitSdkConfig.WithEndpoint(testutil.IaaSCustomEndpoint), ) } + if err != nil { return fmt.Errorf("creating client: %w", err) } securityGroupsToDestroy := []string{} + for _, rs := range s.RootModule().Resources { if rs.Type != "stackit_security_group" { continue @@ -4680,6 +4794,7 @@ func testAccCheckIaaSSecurityGroupDestroy(s *terraform.State) error { if securityGroups[i].Id == nil { continue } + if utils.Contains(securityGroupsToDestroy, *securityGroups[i].Id) { err := client.DeleteSecurityGroupExecute(ctx, testutil.ProjectId, *securityGroups[i].Id) if err != nil { @@ -4687,12 +4802,15 @@ func testAccCheckIaaSSecurityGroupDestroy(s *terraform.State) error { } } } + return nil } func testAccCheckIaaSPublicIpDestroy(s *terraform.State) error { ctx := context.Background() + var client *iaas.APIClient + var err error if testutil.IaaSCustomEndpoint == "" { client, err = iaas.NewAPIClient( @@ -4703,11 +4821,13 @@ func testAccCheckIaaSPublicIpDestroy(s *terraform.State) error { stackitSdkConfig.WithEndpoint(testutil.IaaSCustomEndpoint), ) } + if err != nil { return fmt.Errorf("creating client: %w", err) } publicIpsToDestroy := []string{} + for _, rs := range s.RootModule().Resources { if rs.Type != "stackit_public_ip" { continue @@ -4727,6 +4847,7 @@ func testAccCheckIaaSPublicIpDestroy(s *terraform.State) error { if publicIps[i].Id == nil { continue } + if utils.Contains(publicIpsToDestroy, *publicIps[i].Id) { err := client.DeletePublicIPExecute(ctx, testutil.ProjectId, *publicIps[i].Id) if err != nil { @@ -4734,12 +4855,15 @@ func testAccCheckIaaSPublicIpDestroy(s *terraform.State) error { } } } + return nil } func testAccCheckIaaSKeyPairDestroy(s *terraform.State) error { ctx := context.Background() + var client *iaas.APIClient + var err error if testutil.IaaSCustomEndpoint == "" { client, err = iaas.NewAPIClient( @@ -4750,11 +4874,13 @@ func testAccCheckIaaSKeyPairDestroy(s *terraform.State) error { stackitSdkConfig.WithEndpoint(testutil.IaaSCustomEndpoint), ) } + if err != nil { return fmt.Errorf("creating client: %w", err) } keyPairsToDestroy := []string{} + for _, rs := range s.RootModule().Resources { if rs.Type != "stackit_key_pair" { continue @@ -4773,6 +4899,7 @@ func testAccCheckIaaSKeyPairDestroy(s *terraform.State) error { if keyPairs[i].Name == nil { continue } + if utils.Contains(keyPairsToDestroy, *keyPairs[i].Name) { err := client.DeleteKeyPairExecute(ctx, *keyPairs[i].Name) if err != nil { @@ -4780,12 +4907,15 @@ func testAccCheckIaaSKeyPairDestroy(s *terraform.State) error { } } } + return nil } func testAccCheckIaaSImageDestroy(s *terraform.State) error { ctx := context.Background() + var client *iaas.APIClient + var err error if testutil.IaaSCustomEndpoint == "" { @@ -4797,11 +4927,13 @@ func testAccCheckIaaSImageDestroy(s *terraform.State) error { stackitSdkConfig.WithEndpoint(testutil.IaaSCustomEndpoint), ) } + if err != nil { return fmt.Errorf("creating client: %w", err) } imagesToDestroy := []string{} + for _, rs := range s.RootModule().Resources { if rs.Type != "stackit_image" { continue @@ -4821,6 +4953,7 @@ func testAccCheckIaaSImageDestroy(s *terraform.State) error { if images[i].Id == nil { continue } + if utils.Contains(imagesToDestroy, *images[i].Id) { err := client.DeleteImageExecute(ctx, testutil.ProjectId, *images[i].Id) if err != nil { @@ -4828,5 +4961,6 @@ func testAccCheckIaaSImageDestroy(s *terraform.State) error { } } } + return nil } diff --git a/stackit/internal/services/iaas/image/datasource.go b/stackit/internal/services/iaas/image/datasource.go index 4b24a8ff7..43385c43f 100644 --- a/stackit/internal/services/iaas/image/datasource.go +++ b/stackit/internal/services/iaas/image/datasource.go @@ -5,9 +5,6 @@ import ( "fmt" "net/http" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" - "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" @@ -17,7 +14,9 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types/basetypes" "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/stackitcloud/stackit-sdk-go/services/iaas" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" ) @@ -64,10 +63,13 @@ func (d *imageDataSource) Configure(ctx context.Context, req datasource.Configur } apiClient := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + d.client = apiClient + tflog.Info(ctx, "iaas client configured") } @@ -204,13 +206,15 @@ func (r *imageDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, } // // Read refreshes the Terraform state with the latest data. -func (r *imageDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (r *imageDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model DataSourceModel diags := req.Config.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() imageId := model.ImageId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) @@ -229,6 +233,7 @@ func (r *imageDataSource) Read(ctx context.Context, req datasource.ReadRequest, }, ) resp.State.RemoveResource(ctx) + return } @@ -241,9 +246,11 @@ func (r *imageDataSource) Read(ctx context.Context, req datasource.ReadRequest, // Set refreshed state diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "image read") } @@ -251,6 +258,7 @@ func mapDataSourceFields(ctx context.Context, imageResp *iaas.Image, model *Data if imageResp == nil { return fmt.Errorf("response input is nil") } + if model == nil { return fmt.Errorf("model input is nil") } @@ -267,9 +275,12 @@ func mapDataSourceFields(ctx context.Context, imageResp *iaas.Image, model *Data model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), imageId) // Map config - var configModel = &configModel{} + configModel := &configModel{} + var configObject basetypes.ObjectValue + diags := diag.Diagnostics{} + if imageResp.Config != nil { configModel.BootMenu = types.BoolPointerValue(imageResp.Config.BootMenu) configModel.CDROMBus = types.StringPointerValue(imageResp.Config.GetCdromBus()) @@ -303,13 +314,16 @@ func mapDataSourceFields(ctx context.Context, imageResp *iaas.Image, model *Data } else { configObject = types.ObjectNull(configTypes) } + if diags.HasError() { return fmt.Errorf("creating config: %w", core.DiagsToError(diags)) } // Map checksum - var checksumModel = &checksumModel{} + checksumModel := &checksumModel{} + var checksumObject basetypes.ObjectValue + if imageResp.Checksum != nil { checksumModel.Algorithm = types.StringPointerValue(imageResp.Checksum.Algorithm) checksumModel.Digest = types.StringPointerValue(imageResp.Checksum.Digest) @@ -320,6 +334,7 @@ func mapDataSourceFields(ctx context.Context, imageResp *iaas.Image, model *Data } else { checksumObject = types.ObjectNull(checksumTypes) } + if diags.HasError() { return fmt.Errorf("creating checksum: %w", core.DiagsToError(diags)) } @@ -340,5 +355,6 @@ func mapDataSourceFields(ctx context.Context, imageResp *iaas.Image, model *Data model.Labels = labels model.Config = configObject model.Checksum = checksumObject + return nil } diff --git a/stackit/internal/services/iaas/image/datasource_test.go b/stackit/internal/services/iaas/image/datasource_test.go index a16120ac9..c84ccbcc5 100644 --- a/stackit/internal/services/iaas/image/datasource_test.go +++ b/stackit/internal/services/iaas/image/datasource_test.go @@ -149,9 +149,11 @@ func TestMapDataSourceFields(t *testing.T) { if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(tt.state, tt.expected) if diff != "" { diff --git a/stackit/internal/services/iaas/image/resource.go b/stackit/internal/services/iaas/image/resource.go index 2d9716836..8f1a7e2c4 100644 --- a/stackit/internal/services/iaas/image/resource.go +++ b/stackit/internal/services/iaas/image/resource.go @@ -9,10 +9,6 @@ import ( "strings" "time" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - - iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" - "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/path" @@ -32,6 +28,8 @@ import ( "github.com/stackitcloud/stackit-sdk-go/services/iaas/wait" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" ) @@ -58,7 +56,7 @@ type Model struct { LocalFilePath types.String `tfsdk:"local_file_path"` } -// Struct corresponding to Model.Config +// Struct corresponding to Model.Config. type configModel struct { BootMenu types.Bool `tfsdk:"boot_menu"` CDROMBus types.String `tfsdk:"cdrom_bus"` @@ -75,7 +73,7 @@ type configModel struct { VirtioScsi types.Bool `tfsdk:"virtio_scsi"` } -// Types corresponding to configModel +// Types corresponding to configModel. var configTypes = map[string]attr.Type{ "boot_menu": basetypes.BoolType{}, "cdrom_bus": basetypes.StringType{}, @@ -92,13 +90,13 @@ var configTypes = map[string]attr.Type{ "virtio_scsi": basetypes.BoolType{}, } -// Struct corresponding to Model.Checksum +// Struct corresponding to Model.Checksum. type checksumModel struct { Algorithm types.String `tfsdk:"algorithm"` Digest types.String `tfsdk:"digest"` } -// Types corresponding to checksumModel +// Types corresponding to checksumModel. var checksumTypes = map[string]attr.Type{ "algorithm": basetypes.StringType{}, "digest": basetypes.StringType{}, @@ -127,10 +125,13 @@ func (r *imageResource) Configure(ctx context.Context, req resource.ConfigureReq } apiClient := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + r.client = apiClient + tflog.Info(ctx, "iaas client configured") } @@ -368,11 +369,12 @@ func (r *imageResource) Schema(_ context.Context, _ resource.SchemaRequest, resp } // Create creates the resource and sets the initial Terraform state. -func (r *imageResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *imageResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { //nolint:gocritic // function signature required by Terraform // Retrieve values from plan var model Model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -393,6 +395,7 @@ func (r *imageResource) Create(ctx context.Context, req resource.CreateRequest, core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating image", fmt.Sprintf("Calling API: %v", err)) return } + ctx = tflog.SetField(ctx, "image_id", *imageCreateResp.Id) // Get the image object, as the create response does not contain all fields @@ -412,6 +415,7 @@ func (r *imageResource) Create(ctx context.Context, req resource.CreateRequest, // Set state to partially populated data diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -426,6 +430,7 @@ func (r *imageResource) Create(ctx context.Context, req resource.CreateRequest, // Wait for image to become available waiter := wait.UploadImageWaitHandler(ctx, r.client, projectId, *imageCreateResp.Id) waiter = waiter.SetTimeout(7 * 24 * time.Hour) // Set timeout to one week, to make the timeout useless + waitResp, err := waiter.WaitWithContext(ctx) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating image", fmt.Sprintf("Waiting for image to become available: %v", err)) @@ -442,20 +447,24 @@ func (r *imageResource) Create(ctx context.Context, req resource.CreateRequest, // Set state to fully populated data diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "Image created") } // // Read refreshes the Terraform state with the latest data. -func (r *imageResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (r *imageResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() imageId := model.ImageId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) @@ -468,7 +477,9 @@ func (r *imageResource) Read(ctx context.Context, req resource.ReadRequest, resp resp.State.RemoveResource(ctx) return } + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading image", fmt.Sprintf("Calling API: %v", err)) + return } @@ -481,21 +492,25 @@ func (r *imageResource) Read(ctx context.Context, req resource.ReadRequest, resp // Set refreshed state diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "Image read") } // Update updates the resource and sets the updated Terraform state on success. -func (r *imageResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *imageResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { //nolint:gocritic // function signature required by Terraform // Retrieve values from plan var model Model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() imageId := model.ImageId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) @@ -505,6 +520,7 @@ func (r *imageResource) Update(ctx context.Context, req resource.UpdateRequest, var stateModel Model diags = req.State.Get(ctx, &stateModel) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -527,20 +543,24 @@ func (r *imageResource) Update(ctx context.Context, req resource.UpdateRequest, core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating image", fmt.Sprintf("Processing API payload: %v", err)) return } + diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "Image updated") } // Delete deletes the resource and removes the Terraform state on success. -func (r *imageResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform +func (r *imageResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { //nolint:gocritic // function signature required by Terraform // Retrieve values from state var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -556,6 +576,7 @@ func (r *imageResource) Delete(ctx context.Context, req resource.DeleteRequest, core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting image", fmt.Sprintf("Calling API: %v", err)) return } + _, err = wait.DeleteImageWaitHandler(ctx, r.client, projectId, imageId).WaitWithContext(ctx) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting image", fmt.Sprintf("image deletion waiting: %v", err)) @@ -566,7 +587,7 @@ func (r *imageResource) Delete(ctx context.Context, req resource.DeleteRequest, } // ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: project_id,image_id +// The expected format of the resource import identifier is: project_id,image_id. func (r *imageResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { idParts := strings.Split(req.ID, core.Separator) @@ -575,6 +596,7 @@ func (r *imageResource) ImportState(ctx context.Context, req resource.ImportStat "Error importing image", fmt.Sprintf("Expected import identifier with format: [project_id],[image_id] Got: %q", req.ID), ) + return } @@ -592,6 +614,7 @@ func mapFields(ctx context.Context, imageResp *iaas.Image, model *Model) error { if imageResp == nil { return fmt.Errorf("response input is nil") } + if model == nil { return fmt.Errorf("model input is nil") } @@ -608,9 +631,12 @@ func mapFields(ctx context.Context, imageResp *iaas.Image, model *Model) error { model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), imageId) // Map config - var configModel = &configModel{} + configModel := &configModel{} + var configObject basetypes.ObjectValue + diags := diag.Diagnostics{} + if imageResp.Config != nil { configModel.BootMenu = types.BoolPointerValue(imageResp.Config.BootMenu) configModel.CDROMBus = types.StringPointerValue(imageResp.Config.GetCdromBus()) @@ -644,13 +670,16 @@ func mapFields(ctx context.Context, imageResp *iaas.Image, model *Model) error { } else { configObject = types.ObjectNull(configTypes) } + if diags.HasError() { return fmt.Errorf("creating config: %w", core.DiagsToError(diags)) } // Map checksum - var checksumModel = &checksumModel{} + checksumModel := &checksumModel{} + var checksumObject basetypes.ObjectValue + if imageResp.Checksum != nil { checksumModel.Algorithm = types.StringPointerValue(imageResp.Checksum.Algorithm) checksumModel.Digest = types.StringPointerValue(imageResp.Checksum.Digest) @@ -661,6 +690,7 @@ func mapFields(ctx context.Context, imageResp *iaas.Image, model *Model) error { } else { checksumObject = types.ObjectNull(checksumTypes) } + if diags.HasError() { return fmt.Errorf("creating checksum: %w", core.DiagsToError(diags)) } @@ -681,6 +711,7 @@ func mapFields(ctx context.Context, imageResp *iaas.Image, model *Model) error { model.Labels = labels model.Config = configObject model.Checksum = checksumObject + return nil } @@ -689,8 +720,8 @@ func toCreatePayload(ctx context.Context, model *Model) (*iaas.CreateImagePayloa return nil, fmt.Errorf("nil model") } - var configModel = &configModel{} - if !(model.Config.IsNull() || model.Config.IsUnknown()) { + configModel := &configModel{} + if !model.Config.IsNull() && !model.Config.IsUnknown() { diags := model.Config.As(ctx, configModel, basetypes.ObjectAsOptions{}) if diags.HasError() { return nil, fmt.Errorf("convert boot volume object to struct: %w", core.DiagsToError(diags)) @@ -734,8 +765,8 @@ func toUpdatePayload(ctx context.Context, model *Model, currentLabels types.Map) return nil, fmt.Errorf("nil model") } - var configModel = &configModel{} - if !(model.Config.IsNull() || model.Config.IsUnknown()) { + configModel := &configModel{} + if !model.Config.IsNull() && !model.Config.IsUnknown() { diags := model.Config.As(ctx, configModel, basetypes.ObjectAsOptions{}) if diags.HasError() { return nil, fmt.Errorf("convert boot volume object to struct: %w", core.DiagsToError(diags)) @@ -779,6 +810,7 @@ func uploadImage(ctx context.Context, diags *diag.Diagnostics, filePath, uploadU if filePath == "" { return fmt.Errorf("file path is empty") } + if uploadURL == "" { return fmt.Errorf("upload URL is empty") } @@ -787,6 +819,7 @@ func uploadImage(ctx context.Context, diags *diag.Diagnostics, filePath, uploadU if err != nil { return fmt.Errorf("open file: %w", err) } + stat, err := file.Stat() if err != nil { return fmt.Errorf("stat file: %w", err) @@ -796,14 +829,17 @@ func uploadImage(ctx context.Context, diags *diag.Diagnostics, filePath, uploadU if err != nil { return fmt.Errorf("create upload request: %w", err) } + req.Header.Set("Content-Type", "application/octet-stream") req.ContentLength = stat.Size() client := &http.Client{} + resp, err := client.Do(req) if err != nil { return fmt.Errorf("upload image: %w", err) } + defer func() { err = resp.Body.Close() if err != nil { diff --git a/stackit/internal/services/iaas/image/resource_test.go b/stackit/internal/services/iaas/image/resource_test.go index 23b894dfc..da2902dd8 100644 --- a/stackit/internal/services/iaas/image/resource_test.go +++ b/stackit/internal/services/iaas/image/resource_test.go @@ -154,9 +154,11 @@ func TestMapFields(t *testing.T) { if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(tt.state, tt.expected) if diff != "" { @@ -238,9 +240,11 @@ func TestToCreatePayload(t *testing.T) { if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(output, tt.expected, cmp.AllowUnexported(iaas.NullableString{})) if diff != "" { @@ -321,9 +325,11 @@ func TestToUpdatePayload(t *testing.T) { if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(output, tt.expected, cmp.AllowUnexported(iaas.NullableString{})) if diff != "" { @@ -367,14 +373,18 @@ func Test_UploadImage(t *testing.T) { if tt.uploadFails { w.WriteHeader(http.StatusInternalServerError) _, _ = fmt.Fprintln(w, `{"status":"some error occurred"}`) + return } + w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) _, _ = fmt.Fprintln(w, `{"status":"ok"}`) }) + server := httptest.NewServer(handler) defer server.Close() + uploadURL, err := url.Parse(server.URL) if err != nil { t.Error(err) diff --git a/stackit/internal/services/iaas/imagev2/datasource.go b/stackit/internal/services/iaas/imagev2/datasource.go index e39562393..2aaab2048 100644 --- a/stackit/internal/services/iaas/imagev2/datasource.go +++ b/stackit/internal/services/iaas/imagev2/datasource.go @@ -8,24 +8,22 @@ import ( "sort" "github.com/hashicorp/terraform-plugin-framework-validators/datasourcevalidator" - "github.com/hashicorp/terraform-plugin-framework/path" - "github.com/stackitcloud/stackit-sdk-go/services/iaas" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/features" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "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/services/iaas" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/features" iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" ) // Ensure the implementation satisfies the expected interfaces. @@ -60,7 +58,7 @@ type Filter struct { SecureBoot types.Bool `tfsdk:"secure_boot"` } -// Struct corresponding to Model.Config +// Struct corresponding to Model.Config. type configModel struct { BootMenu types.Bool `tfsdk:"boot_menu"` CDROMBus types.String `tfsdk:"cdrom_bus"` @@ -77,7 +75,7 @@ type configModel struct { VirtioScsi types.Bool `tfsdk:"virtio_scsi"` } -// Types corresponding to configModel +// Types corresponding to configModel. var configTypes = map[string]attr.Type{ "boot_menu": basetypes.BoolType{}, "cdrom_bus": basetypes.StringType{}, @@ -94,13 +92,13 @@ var configTypes = map[string]attr.Type{ "virtio_scsi": basetypes.BoolType{}, } -// Struct corresponding to Model.Checksum +// Struct corresponding to Model.Checksum. type checksumModel struct { Algorithm types.String `tfsdk:"algorithm"` Digest types.String `tfsdk:"digest"` } -// Types corresponding to checksumModel +// Types corresponding to checksumModel. var checksumTypes = map[string]attr.Type{ "algorithm": basetypes.StringType{}, "digest": basetypes.StringType{}, @@ -128,16 +126,19 @@ func (d *imageDataV2Source) Configure(ctx context.Context, req datasource.Config } features.CheckBetaResourcesEnabled(ctx, &providerData, &resp.Diagnostics, "stackit_image_v2", "datasource") + if resp.Diagnostics.HasError() { return } apiClient := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } d.client = apiClient + tflog.Info(ctx, "iaas client configured") } @@ -348,10 +349,11 @@ func (d *imageDataV2Source) Schema(_ context.Context, _ datasource.SchemaRequest } // Read refreshes the Terraform state with the latest data. -func (d *imageDataV2Source) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (d *imageDataV2Source) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model DataSourceModel diags := req.Config.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -377,6 +379,7 @@ func (d *imageDataV2Source) Read(ctx context.Context, req datasource.ReadRequest ctx = tflog.SetField(ctx, "sort_ascending", sortAscending) var imageResp *iaas.Image + var err error // Case 1: Direct lookup by image ID @@ -389,11 +392,11 @@ func (d *imageDataV2Source) Read(ctx context.Context, req datasource.ReadRequest http.StatusForbidden: fmt.Sprintf("Project with ID %q not found or forbidden access", projectID), }) resp.State.RemoveResource(ctx) + return } } else { // Case 2: Lookup by name or name_regex - // Compile regex var compiledRegex *regexp.Regexp if nameRegex != "" { @@ -413,11 +416,13 @@ func (d *imageDataV2Source) Read(ctx context.Context, req datasource.ReadRequest // Step 1: Match images by name or regular expression (name or name_regex, if provided) var matchedImages []*iaas.Image + for i := range *imageList.Items { img := &(*imageList.Items)[i] if name != "" && img.Name != nil && *img.Name == name { matchedImages = append(matchedImages, img) } + if compiledRegex != nil && img.Name != nil && compiledRegex.MatchString(*img.Name) { matchedImages = append(matchedImages, img) } @@ -434,6 +439,7 @@ func (d *imageDataV2Source) Read(ctx context.Context, req datasource.ReadRequest // Step 3: Apply additional filtering based on OS, distro, version, UEFI, secure boot, etc. var filteredImages []*iaas.Image + for _, img := range matchedImages { if imageMatchesFilter(img, &filter) { filteredImages = append(filteredImages, img) @@ -460,6 +466,7 @@ func (d *imageDataV2Source) Read(ctx context.Context, req datasource.ReadRequest // Set refreshed state diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -471,6 +478,7 @@ func mapDataSourceFields(ctx context.Context, imageResp *iaas.Image, model *Data if imageResp == nil { return fmt.Errorf("response input is nil") } + if model == nil { return fmt.Errorf("model input is nil") } @@ -487,9 +495,12 @@ func mapDataSourceFields(ctx context.Context, imageResp *iaas.Image, model *Data model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), imageId) // Map config - var configModel = &configModel{} + configModel := &configModel{} + var configObject basetypes.ObjectValue + diags := diag.Diagnostics{} + if imageResp.Config != nil { configModel.BootMenu = types.BoolPointerValue(imageResp.Config.BootMenu) configModel.CDROMBus = types.StringPointerValue(imageResp.Config.GetCdromBus()) @@ -523,13 +534,16 @@ func mapDataSourceFields(ctx context.Context, imageResp *iaas.Image, model *Data } else { configObject = types.ObjectNull(configTypes) } + if diags.HasError() { return fmt.Errorf("creating config: %w", core.DiagsToError(diags)) } // Map checksum - var checksumModel = &checksumModel{} + checksumModel := &checksumModel{} + var checksumObject basetypes.ObjectValue + if imageResp.Checksum != nil { checksumModel.Algorithm = types.StringPointerValue(imageResp.Checksum.Algorithm) checksumModel.Digest = types.StringPointerValue(imageResp.Checksum.Digest) @@ -540,6 +554,7 @@ func mapDataSourceFields(ctx context.Context, imageResp *iaas.Image, model *Data } else { checksumObject = types.ObjectNull(checksumTypes) } + if diags.HasError() { return fmt.Errorf("creating checksum: %w", core.DiagsToError(diags)) } @@ -560,6 +575,7 @@ func mapDataSourceFields(ctx context.Context, imageResp *iaas.Image, model *Data model.Labels = labels model.Config = configObject model.Checksum = checksumObject + return nil } diff --git a/stackit/internal/services/iaas/imagev2/datasource_test.go b/stackit/internal/services/iaas/imagev2/datasource_test.go index 56b9930b1..02600866f 100644 --- a/stackit/internal/services/iaas/imagev2/datasource_test.go +++ b/stackit/internal/services/iaas/imagev2/datasource_test.go @@ -149,9 +149,11 @@ func TestMapDataSourceFields(t *testing.T) { if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(tt.state, tt.expected) if diff != "" { @@ -455,6 +457,7 @@ func TestSortImagesByName(t *testing.T) { sortImagesByName(tc.input, tc.ascending) gotNames := make([]string, len(tc.input)) + for i, img := range tc.input { if img.Name == nil { gotNames[i] = "" diff --git a/stackit/internal/services/iaas/keypair/datasource.go b/stackit/internal/services/iaas/keypair/datasource.go index 513607f5b..6f4a99c6b 100644 --- a/stackit/internal/services/iaas/keypair/datasource.go +++ b/stackit/internal/services/iaas/keypair/datasource.go @@ -4,15 +4,14 @@ import ( "context" "fmt" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" - "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/stackitcloud/stackit-sdk-go/services/iaas" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" ) @@ -43,10 +42,13 @@ func (d *keyPairDataSource) Configure(ctx context.Context, req datasource.Config } apiClient := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + d.client = apiClient + tflog.Info(ctx, "iaas client configured") } @@ -84,13 +86,15 @@ func (r *keyPairDataSource) Schema(_ context.Context, _ datasource.SchemaRequest } // Read refreshes the Terraform state with the latest data. -func (r *keyPairDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (r *keyPairDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.Config.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + name := model.Name.ValueString() ctx = tflog.SetField(ctx, "name", name) @@ -105,6 +109,7 @@ func (r *keyPairDataSource) Read(ctx context.Context, req datasource.ReadRequest nil, ) resp.State.RemoveResource(ctx) + return } @@ -117,8 +122,10 @@ func (r *keyPairDataSource) Read(ctx context.Context, req datasource.ReadRequest // Set refreshed state diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "Key pair read") } diff --git a/stackit/internal/services/iaas/keypair/resource.go b/stackit/internal/services/iaas/keypair/resource.go index ccae2565c..3add148de 100644 --- a/stackit/internal/services/iaas/keypair/resource.go +++ b/stackit/internal/services/iaas/keypair/resource.go @@ -6,8 +6,6 @@ import ( "net/http" "strings" - iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" - "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" @@ -19,6 +17,7 @@ import ( "github.com/stackitcloud/stackit-sdk-go/services/iaas" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" ) // Ensure the implementation satisfies the expected interfaces. @@ -59,10 +58,13 @@ func (r *keyPairResource) Configure(ctx context.Context, req resource.ConfigureR } apiClient := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + r.client = apiClient + tflog.Info(ctx, "iaas client configured") } @@ -114,7 +116,7 @@ func (r *keyPairResource) Schema(_ context.Context, _ resource.SchemaRequest, re // ModifyPlan will be called in the Plan phase. // It will check if the plan contains a change that requires replacement. If yes, it will show a warning to the user. -func (r *keyPairResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform +func (r *keyPairResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { //nolint:gocritic // function signature required by Terraform // If the state is empty we are creating a new resource // If the plan is empty we are deleting the resource // In both cases we don't need to check for replacement @@ -136,11 +138,12 @@ func (r *keyPairResource) ModifyPlan(ctx context.Context, req resource.ModifyPla } // Create creates the resource and sets the initial Terraform state. -func (r *keyPairResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *keyPairResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { //nolint:gocritic // function signature required by Terraform // Retrieve values from plan var model Model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -172,20 +175,24 @@ func (r *keyPairResource) Create(ctx context.Context, req resource.CreateRequest // Set state to fully populated data diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "Key pair created") } // Read refreshes the Terraform state with the latest data. -func (r *keyPairResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (r *keyPairResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + name := model.Name.ValueString() ctx = tflog.SetField(ctx, "name", name) @@ -196,7 +203,9 @@ func (r *keyPairResource) Read(ctx context.Context, req resource.ReadRequest, re resp.State.RemoveResource(ctx) return } + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading key pair", fmt.Sprintf("Calling API: %v", err)) + return } @@ -209,21 +218,25 @@ func (r *keyPairResource) Read(ctx context.Context, req resource.ReadRequest, re // Set refreshed state diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "Key pair read") } // Update updates the resource and sets the updated Terraform state on success. -func (r *keyPairResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *keyPairResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { //nolint:gocritic // function signature required by Terraform // Retrieve values from plan var model Model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + name := model.Name.ValueString() ctx = tflog.SetField(ctx, "name", name) @@ -231,6 +244,7 @@ func (r *keyPairResource) Update(ctx context.Context, req resource.UpdateRequest var stateModel Model diags = req.State.Get(ctx, &stateModel) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -253,20 +267,24 @@ func (r *keyPairResource) Update(ctx context.Context, req resource.UpdateRequest core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating key pair", fmt.Sprintf("Processing API payload: %v", err)) return } + diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "key pair updated") } // Delete deletes the resource and removes the Terraform state on success. -func (r *keyPairResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform +func (r *keyPairResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { //nolint:gocritic // function signature required by Terraform // Retrieve values from state var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -285,7 +303,7 @@ func (r *keyPairResource) Delete(ctx context.Context, req resource.DeleteRequest } // ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: project_id,key_pair_id +// The expected format of the resource import identifier is: project_id,key_pair_id. func (r *keyPairResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { idParts := strings.Split(req.ID, core.Separator) @@ -294,6 +312,7 @@ func (r *keyPairResource) ImportState(ctx context.Context, req resource.ImportSt "Error importing key pair", fmt.Sprintf("Expected import identifier with format: [name] Got: %q", req.ID), ) + return } @@ -308,6 +327,7 @@ func mapFields(ctx context.Context, keyPairResp *iaas.Keypair, model *Model) err if keyPairResp == nil { return fmt.Errorf("response input is nil") } + if model == nil { return fmt.Errorf("model input is nil") } @@ -326,6 +346,7 @@ func mapFields(ctx context.Context, keyPairResp *iaas.Keypair, model *Model) err model.Fingerprint = types.StringPointerValue(keyPairResp.Fingerprint) var err error + model.Labels, err = iaasUtils.MapLabels(ctx, keyPairResp.Labels, model.Labels) if err != nil { return err diff --git a/stackit/internal/services/iaas/keypair/resource_test.go b/stackit/internal/services/iaas/keypair/resource_test.go index ed3af09ac..965c42b72 100644 --- a/stackit/internal/services/iaas/keypair/resource_test.go +++ b/stackit/internal/services/iaas/keypair/resource_test.go @@ -105,9 +105,11 @@ func TestMapFields(t *testing.T) { if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(tt.state, tt.expected) if diff != "" { @@ -152,9 +154,11 @@ func TestToCreatePayload(t *testing.T) { if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(output, tt.expected, cmp.AllowUnexported(iaas.NullableString{})) if diff != "" { @@ -197,9 +201,11 @@ func TestToUpdatePayload(t *testing.T) { if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(output, tt.expected, cmp.AllowUnexported(iaas.NullableString{})) if diff != "" { diff --git a/stackit/internal/services/iaas/machinetype/datasource.go b/stackit/internal/services/iaas/machinetype/datasource.go index ed2c1c9d1..e43551a98 100644 --- a/stackit/internal/services/iaas/machinetype/datasource.go +++ b/stackit/internal/services/iaas/machinetype/datasource.go @@ -38,7 +38,7 @@ type DataSourceModel struct { Vcpus types.Int64 `tfsdk:"vcpus"` } -// NewMachineTypeDataSource instantiates the data source +// NewMachineTypeDataSource instantiates the data source. func NewMachineTypeDataSource() datasource.DataSource { return &machineTypeDataSource{} } @@ -58,14 +58,17 @@ func (d *machineTypeDataSource) Configure(ctx context.Context, req datasource.Co } features.CheckBetaResourcesEnabled(ctx, &providerData, &resp.Diagnostics, "stackit_machine_type", "datasource") + if resp.Diagnostics.HasError() { return } client := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + d.client = client tflog.Info(ctx, "IAAS client configured") @@ -134,9 +137,11 @@ func (d *machineTypeDataSource) Schema(_ context.Context, _ datasource.SchemaReq } } -func (d *machineTypeDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (d *machineTypeDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model DataSourceModel + resp.Diagnostics.Append(req.Config.Get(ctx, &model)...) + if resp.Diagnostics.HasError() { return } @@ -163,6 +168,7 @@ func (d *machineTypeDataSource) Read(ctx context.Context, req datasource.ReadReq }, ) resp.State.RemoveResource(ctx) + return } @@ -189,9 +195,11 @@ func (d *machineTypeDataSource) Read(ctx context.Context, req datasource.ReadReq } resp.Diagnostics.Append(resp.State.Set(ctx, model)...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "Successfully read machine type") } @@ -212,14 +220,18 @@ func mapDataSourceFields(ctx context.Context, machineType *iaas.MachineType, mod model.Vcpus = types.Int64PointerValue(machineType.Vcpus) extra := types.MapNull(types.StringType) + if machineType.ExtraSpecs != nil && len(*machineType.ExtraSpecs) > 0 { var diags diag.Diagnostics + extra, diags = types.MapValueFrom(ctx, types.StringType, *machineType.ExtraSpecs) if diags.HasError() { return fmt.Errorf("converting extraspecs: %w", core.DiagsToError(diags)) } } + model.ExtraSpecs = extra + return nil } @@ -230,6 +242,7 @@ func sortMachineTypeByName(input []*iaas.MachineType, ascending bool) ([]*iaas.M // Filter out nil or missing name var filtered []*iaas.MachineType + for _, m := range input { if m != nil && m.Name != nil { filtered = append(filtered, m) @@ -240,6 +253,7 @@ func sortMachineTypeByName(input []*iaas.MachineType, ascending bool) ([]*iaas.M if ascending { return *filtered[i].Name < *filtered[j].Name } + return *filtered[i].Name > *filtered[j].Name }) diff --git a/stackit/internal/services/iaas/machinetype/datasource_test.go b/stackit/internal/services/iaas/machinetype/datasource_test.go index 3fde4794d..f1997697c 100644 --- a/stackit/internal/services/iaas/machinetype/datasource_test.go +++ b/stackit/internal/services/iaas/machinetype/datasource_test.go @@ -150,6 +150,7 @@ func TestMapDataSourceFields(t *testing.T) { if err == nil { t.Errorf("expected error but got none") } + return } @@ -219,6 +220,7 @@ func TestSortMachineTypeByName(t *testing.T) { if err == nil { t.Errorf("expected error but got none") } + return } @@ -227,6 +229,7 @@ func TestSortMachineTypeByName(t *testing.T) { } var result []string + for _, mt := range sorted { if mt.Name != nil { result = append(result, *mt.Name) diff --git a/stackit/internal/services/iaas/network/datasource.go b/stackit/internal/services/iaas/network/datasource.go index a78d11b9b..40708e07c 100644 --- a/stackit/internal/services/iaas/network/datasource.go +++ b/stackit/internal/services/iaas/network/datasource.go @@ -3,14 +3,6 @@ package network import ( "context" - "github.com/stackitcloud/stackit-sdk-go/services/iaasalpha" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/features" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/network/utils/v1network" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/network/utils/v2network" - iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" - iaasAlphaUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaasalpha/utils" - "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" @@ -18,7 +10,14 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/stackitcloud/stackit-sdk-go/services/iaas" + "github.com/stackitcloud/stackit-sdk-go/services/iaasalpha" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/features" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/network/utils/v1network" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/network/utils/v2network" + iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" + iaasAlphaUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaasalpha/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" ) @@ -48,29 +47,36 @@ func (d *networkDataSource) Metadata(_ context.Context, req datasource.MetadataR func (d *networkDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { var ok bool + d.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) if !ok { return } d.isExperimental = features.CheckExperimentEnabledWithoutError(ctx, &d.providerData, features.NetworkExperiment, "stackit_network", core.Datasource, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } if d.isExperimental { alphaApiClient := iaasAlphaUtils.ConfigureClient(ctx, &d.providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + d.alphaClient = alphaApiClient } else { apiClient := iaasUtils.ConfigureClient(ctx, &d.providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + d.client = apiClient } + tflog.Info(ctx, "IaaS client configured") } @@ -196,7 +202,7 @@ func (d *networkDataSource) Schema(_ context.Context, _ datasource.SchemaRequest } // Read refreshes the Terraform state with the latest data. -func (d *networkDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (d *networkDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { //nolint:gocritic // function signature required by Terraform if !d.isExperimental { v1network.DatasourceRead(ctx, req, resp, d.client) } else { diff --git a/stackit/internal/services/iaas/network/resource.go b/stackit/internal/services/iaas/network/resource.go index 365a303ed..6e135ea4b 100644 --- a/stackit/internal/services/iaas/network/resource.go +++ b/stackit/internal/services/iaas/network/resource.go @@ -59,61 +59,75 @@ func (r *networkResource) Metadata(_ context.Context, req resource.MetadataReque // Configure adds the provider configured client to the resource. func (r *networkResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { var ok bool + r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) if !ok { return } r.isExperimental = features.CheckExperimentEnabledWithoutError(ctx, &r.providerData, features.NetworkExperiment, "stackit_network", core.Resource, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } if r.isExperimental { alphaApiClient := iaasAlphaUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + r.alphaClient = alphaApiClient } else { apiClient := iaasUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + r.client = apiClient } + tflog.Info(ctx, "IaaS client configured") } // ModifyPlan implements resource.ResourceWithModifyPlan. // Use the modifier to set the effective region in the current plan. -func (r *networkResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform +func (r *networkResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { //nolint:gocritic // function signature required by Terraform // If the v1 api is used, it's not required to get the fallback region because it isn't used if !r.isExperimental { return } + var configModel model.Model // skip initial empty configuration to avoid follow-up errors if req.Config.Raw.IsNull() { return } + resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...) + if resp.Diagnostics.HasError() { return } var planModel model.Model + resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) + if resp.Diagnostics.HasError() { return } utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp) + if resp.Diagnostics.HasError() { return } resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...) + if resp.Diagnostics.HasError() { return } @@ -121,7 +135,9 @@ func (r *networkResource) ModifyPlan(ctx context.Context, req resource.ModifyPla func (r *networkResource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) { var resourceModel model.Model + resp.Diagnostics.Append(req.Config.Get(ctx, &resourceModel)...) + if resp.Diagnostics.HasError() { return } @@ -129,17 +145,19 @@ func (r *networkResource) ValidateConfig(ctx context.Context, req resource.Valid if !resourceModel.Nameservers.IsUnknown() && !resourceModel.IPv4Nameservers.IsUnknown() && !resourceModel.Nameservers.IsNull() && !resourceModel.IPv4Nameservers.IsNull() { core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring network", "You cannot provide both the `nameservers` and `ipv4_nameservers` fields simultaneously. Please remove the deprecated `nameservers` field, and use `ipv4_nameservers` to configure nameservers for IPv4.") } + if !r.isExperimental { if !utils.IsUndefined(resourceModel.Region) { core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring network", "Setting the `region` is not supported yet. This can only be configured when the experiments `network` is set.") } + if !utils.IsUndefined(resourceModel.RoutingTableID) { core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring network", "Setting the field `routing_table_id` is not supported yet. This can only be configured when the experiments `network` is set.") } } } -// ConfigValidators validates the resource configuration +// ConfigValidators validates the resource configuration. func (r *networkResource) ConfigValidators(_ context.Context) []resource.ConfigValidator { return []resource.ConfigValidator{ resourcevalidator.Conflicting( @@ -365,7 +383,7 @@ func (r *networkResource) Schema(_ context.Context, _ resource.SchemaRequest, re } // Create creates the resource and sets the initial Terraform state. -func (r *networkResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *networkResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { //nolint:gocritic // function signature required by Terraform if !r.isExperimental { v1network.Create(ctx, req, resp, r.client) } else { @@ -374,7 +392,7 @@ func (r *networkResource) Create(ctx context.Context, req resource.CreateRequest } // Read refreshes the Terraform state with the latest data. -func (r *networkResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (r *networkResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { //nolint:gocritic // function signature required by Terraform if !r.isExperimental { v1network.Read(ctx, req, resp, r.client) } else { @@ -383,7 +401,7 @@ func (r *networkResource) Read(ctx context.Context, req resource.ReadRequest, re } // Update updates the resource and sets the updated Terraform state on success. -func (r *networkResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *networkResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { //nolint:gocritic // function signature required by Terraform if !r.isExperimental { v1network.Update(ctx, req, resp, r.client) } else { @@ -392,7 +410,7 @@ func (r *networkResource) Update(ctx context.Context, req resource.UpdateRequest } // Delete deletes the resource and removes the Terraform state on success. -func (r *networkResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform +func (r *networkResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { //nolint:gocritic // function signature required by Terraform if !r.isExperimental { v1network.Delete(ctx, req, resp, r.client) } else { @@ -401,7 +419,7 @@ func (r *networkResource) Delete(ctx context.Context, req resource.DeleteRequest } // ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: project_id,network_id +// The expected format of the resource import identifier is: project_id,network_id. func (r *networkResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { if !r.isExperimental { v1network.ImportState(ctx, req, resp) diff --git a/stackit/internal/services/iaas/network/utils/v1network/datasource.go b/stackit/internal/services/iaas/network/utils/v1network/datasource.go index 08f8da5bd..9b71cfc79 100644 --- a/stackit/internal/services/iaas/network/utils/v1network/datasource.go +++ b/stackit/internal/services/iaas/network/utils/v1network/datasource.go @@ -16,13 +16,15 @@ import ( "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" ) -func DatasourceRead(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse, client *iaas.APIClient) { // nolint:gocritic // function signature required by Terraform +func DatasourceRead(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse, client *iaas.APIClient) { //nolint:gocritic // function signature required by Terraform var model networkModel.DataSourceModel diags := req.Config.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() networkId := model.NetworkId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) @@ -41,6 +43,7 @@ func DatasourceRead(ctx context.Context, req datasource.ReadRequest, resp *datas }, ) resp.State.RemoveResource(ctx) + return } @@ -49,11 +52,14 @@ func DatasourceRead(ctx context.Context, req datasource.ReadRequest, resp *datas core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading network", fmt.Sprintf("Processing API payload: %v", err)) return } + diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "Network read") } @@ -61,6 +67,7 @@ func mapDataSourceFields(ctx context.Context, networkResp *iaas.Network, model * if networkResp == nil { return fmt.Errorf("response input is nil") } + if model == nil { return fmt.Errorf("model input is nil") } @@ -90,9 +97,11 @@ func mapDataSourceFields(ctx context.Context, networkResp *iaas.Network, model * respNameservers := *networkResp.Nameservers modelNameservers, err := utils.ListValuetoStringSlice(model.Nameservers) modelIPv4Nameservers, errIpv4 := utils.ListValuetoStringSlice(model.IPv4Nameservers) + if err != nil { return fmt.Errorf("get current network nameservers from model: %w", err) } + if errIpv4 != nil { return fmt.Errorf("get current IPv4 network nameservers from model: %w", errIpv4) } @@ -102,9 +111,11 @@ func mapDataSourceFields(ctx context.Context, networkResp *iaas.Network, model * nameserversTF, diags := types.ListValueFrom(ctx, types.StringType, reconciledNameservers) ipv4NameserversTF, ipv4Diags := types.ListValueFrom(ctx, types.StringType, reconciledIPv4Nameservers) + if diags.HasError() { return fmt.Errorf("map network nameservers: %w", core.DiagsToError(diags)) } + if ipv4Diags.HasError() { return fmt.Errorf("map IPv4 network nameservers: %w", core.DiagsToError(ipv4Diags)) } @@ -118,13 +129,16 @@ func mapDataSourceFields(ctx context.Context, networkResp *iaas.Network, model * model.IPv4Prefixes = types.ListNull(types.StringType) } else { respPrefixes := *networkResp.Prefixes + prefixesTF, diags := types.ListValueFrom(ctx, types.StringType, respPrefixes) if diags.HasError() { return fmt.Errorf("map network prefixes: %w", core.DiagsToError(diags)) } + if len(respPrefixes) > 0 { model.IPv4Prefix = types.StringValue(respPrefixes[0]) _, netmask, err := net.ParseCIDR(respPrefixes[0]) + if err != nil { // silently ignore parsing error for the netmask model.IPv4PrefixLength = types.Int64Null() @@ -149,6 +163,7 @@ func mapDataSourceFields(ctx context.Context, networkResp *iaas.Network, model * model.IPv6Nameservers = types.ListNull(types.StringType) } else { respIPv6Nameservers := *networkResp.NameserversV6 + modelIPv6Nameservers, errIpv6 := utils.ListValuetoStringSlice(model.IPv6Nameservers) if errIpv6 != nil { return fmt.Errorf("get current IPv6 network nameservers from model: %w", errIpv6) @@ -168,13 +183,16 @@ func mapDataSourceFields(ctx context.Context, networkResp *iaas.Network, model * model.IPv6Prefixes = types.ListNull(types.StringType) } else { respPrefixesV6 := *networkResp.PrefixesV6 + prefixesV6TF, diags := types.ListValueFrom(ctx, types.StringType, respPrefixesV6) if diags.HasError() { return fmt.Errorf("map network IPv6 prefixes: %w", core.DiagsToError(diags)) } + if len(respPrefixesV6) > 0 { model.IPv6Prefix = types.StringValue(respPrefixesV6[0]) _, netmask, err := net.ParseCIDR(respPrefixesV6[0]) + if err != nil { // silently ignore parsing error for the netmask model.IPv6PrefixLength = types.Int64Null() @@ -183,6 +201,7 @@ func mapDataSourceFields(ctx context.Context, networkResp *iaas.Network, model * model.IPv6PrefixLength = types.Int64Value(int64(ones)) } } + model.IPv6Prefixes = prefixesV6TF } diff --git a/stackit/internal/services/iaas/network/utils/v1network/datasource_test.go b/stackit/internal/services/iaas/network/utils/v1network/datasource_test.go index 2ce9b5c96..c052fcbdb 100644 --- a/stackit/internal/services/iaas/network/utils/v1network/datasource_test.go +++ b/stackit/internal/services/iaas/network/utils/v1network/datasource_test.go @@ -338,9 +338,11 @@ func TestMapDataSourceFields(t *testing.T) { if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(tt.state, tt.expected) if diff != "" { diff --git a/stackit/internal/services/iaas/network/utils/v1network/resource.go b/stackit/internal/services/iaas/network/utils/v1network/resource.go index ddb91ed6d..27138ae80 100644 --- a/stackit/internal/services/iaas/network/utils/v1network/resource.go +++ b/stackit/internal/services/iaas/network/utils/v1network/resource.go @@ -22,11 +22,12 @@ import ( "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" ) -func Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse, client *iaas.APIClient) { // nolint:gocritic // function signature required by Terraform +func Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse, client *iaas.APIClient) { //nolint:gocritic // function signature required by Terraform // Retrieve values from plan var model networkModel.Model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -50,6 +51,7 @@ func Create(ctx context.Context, req resource.CreateRequest, resp *resource.Crea } networkId := *network.NetworkId + network, err = wait.CreateNetworkWaitHandler(ctx, client, projectId, networkId).WaitWithContext(ctx) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating network", fmt.Sprintf("Network creation waiting: %v", err)) @@ -67,19 +69,23 @@ func Create(ctx context.Context, req resource.CreateRequest, resp *resource.Crea // Set state to fully populated data diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "Network created") } -func Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse, client *iaas.APIClient) { // nolint:gocritic // function signature required by Terraform +func Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse, client *iaas.APIClient) { //nolint:gocritic // function signature required by Terraform var model networkModel.Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() networkId := model.NetworkId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) @@ -92,7 +98,9 @@ func Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResp resp.State.RemoveResource(ctx) return } + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading network", fmt.Sprintf("Calling API: %v", err)) + return } @@ -105,20 +113,24 @@ func Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResp // Set refreshed state diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "Network read") } -func Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse, client *iaas.APIClient) { // nolint:gocritic // function signature required by Terraform +func Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse, client *iaas.APIClient) { //nolint:gocritic // function signature required by Terraform // Retrieve values from plan var model networkModel.Model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() networkId := model.NetworkId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) @@ -128,6 +140,7 @@ func Update(ctx context.Context, req resource.UpdateRequest, resp *resource.Upda var stateModel networkModel.Model diags = req.State.Get(ctx, &stateModel) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -144,6 +157,7 @@ func Update(ctx context.Context, req resource.UpdateRequest, resp *resource.Upda core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating network", fmt.Sprintf("Calling API: %v", err)) return } + waitResp, err := wait.UpdateNetworkWaitHandler(ctx, client, projectId, networkId).WaitWithContext(ctx) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating network", fmt.Sprintf("Network update waiting: %v", err)) @@ -155,19 +169,23 @@ func Update(ctx context.Context, req resource.UpdateRequest, resp *resource.Upda core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating network", fmt.Sprintf("Processing API payload: %v", err)) return } + diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "Network updated") } -func Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse, client *iaas.APIClient) { // nolint:gocritic // function signature required by Terraform +func Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse, client *iaas.APIClient) { //nolint:gocritic // function signature required by Terraform // Retrieve values from state var model networkModel.Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -183,6 +201,7 @@ func Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.Dele core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting network", fmt.Sprintf("Calling API: %v", err)) return } + _, err = wait.DeleteNetworkWaitHandler(ctx, client, projectId, networkId).WaitWithContext(ctx) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting network", fmt.Sprintf("Network deletion waiting: %v", err)) @@ -193,7 +212,7 @@ func Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.Dele } // ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: project_id,network_id +// The expected format of the resource import identifier is: project_id,network_id. func ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { idParts := strings.Split(req.ID, core.Separator) @@ -202,6 +221,7 @@ func ImportState(ctx context.Context, req resource.ImportStateRequest, resp *res "Error importing network", fmt.Sprintf("Expected import identifier with format: [project_id],[network_id] Got: %q", req.ID), ) + return } @@ -219,6 +239,7 @@ func mapFields(ctx context.Context, networkResp *iaas.Network, model *networkMod if networkResp == nil { return fmt.Errorf("response input is nil") } + if model == nil { return fmt.Errorf("model input is nil") } @@ -247,9 +268,11 @@ func mapFields(ctx context.Context, networkResp *iaas.Network, model *networkMod respNameservers := *networkResp.Nameservers modelNameservers, err := utils.ListValuetoStringSlice(model.Nameservers) modelIPv4Nameservers, errIpv4 := utils.ListValuetoStringSlice(model.IPv4Nameservers) + if err != nil { return fmt.Errorf("get current network nameservers from model: %w", err) } + if errIpv4 != nil { return fmt.Errorf("get current IPv4 network nameservers from model: %w", errIpv4) } @@ -259,9 +282,11 @@ func mapFields(ctx context.Context, networkResp *iaas.Network, model *networkMod nameserversTF, diags := types.ListValueFrom(ctx, types.StringType, reconciledNameservers) ipv4NameserversTF, ipv4Diags := types.ListValueFrom(ctx, types.StringType, reconciledIPv4Nameservers) + if diags.HasError() { return fmt.Errorf("map network nameservers: %w", core.DiagsToError(diags)) } + if ipv4Diags.HasError() { return fmt.Errorf("map IPv4 network nameservers: %w", core.DiagsToError(ipv4Diags)) } @@ -275,13 +300,16 @@ func mapFields(ctx context.Context, networkResp *iaas.Network, model *networkMod model.IPv4Prefixes = types.ListNull(types.StringType) } else { respPrefixes := *networkResp.Prefixes + prefixesTF, diags := types.ListValueFrom(ctx, types.StringType, respPrefixes) if diags.HasError() { return fmt.Errorf("map network prefixes: %w", core.DiagsToError(diags)) } + if len(respPrefixes) > 0 { model.IPv4Prefix = types.StringValue(respPrefixes[0]) _, netmask, err := net.ParseCIDR(respPrefixes[0]) + if err != nil { // silently ignore parsing error for the netmask model.IPv4PrefixLength = types.Int64Null() @@ -307,6 +335,7 @@ func mapFields(ctx context.Context, networkResp *iaas.Network, model *networkMod model.IPv6Nameservers = types.ListNull(types.StringType) } else { respIPv6Nameservers := *networkResp.NameserversV6 + modelIPv6Nameservers, errIpv6 := utils.ListValuetoStringSlice(model.IPv6Nameservers) if errIpv6 != nil { return fmt.Errorf("get current IPv6 network nameservers from model: %w", errIpv6) @@ -326,13 +355,16 @@ func mapFields(ctx context.Context, networkResp *iaas.Network, model *networkMod model.IPv6Prefixes = types.ListNull(types.StringType) } else { respPrefixesV6 := *networkResp.PrefixesV6 + prefixesV6TF, diags := types.ListValueFrom(ctx, types.StringType, respPrefixesV6) if diags.HasError() { return fmt.Errorf("map network IPv6 prefixes: %w", core.DiagsToError(diags)) } + if len(respPrefixesV6) > 0 { model.IPv6Prefix = types.StringValue(respPrefixesV6[0]) _, netmask, err := net.ParseCIDR(respPrefixesV6[0]) + if err != nil { // silently ignore parsing error for the netmask model.IPv6PrefixLength = types.Int64Null() @@ -341,6 +373,7 @@ func mapFields(ctx context.Context, networkResp *iaas.Network, model *networkMod model.IPv6PrefixLength = types.Int64Value(int64(ones)) } } + model.IPv6Prefixes = prefixesV6TF } @@ -365,18 +398,21 @@ func toCreatePayload(ctx context.Context, model *networkModel.Model) (*iaas.Crea if model == nil { return nil, fmt.Errorf("nil model") } + addressFamily := &iaas.CreateNetworkAddressFamily{} modelIPv6Nameservers := []string{} + for _, ipv6ns := range model.IPv6Nameservers.Elements() { ipv6NameserverString, ok := ipv6ns.(types.String) if !ok { return nil, fmt.Errorf("type assertion failed") } + modelIPv6Nameservers = append(modelIPv6Nameservers, ipv6NameserverString.ValueString()) } - if !(model.IPv6Prefix.IsNull() || model.IPv6PrefixLength.IsNull() || model.IPv6Nameservers.IsNull()) { + if !model.IPv6Prefix.IsNull() && !model.IPv6PrefixLength.IsNull() && !model.IPv6Nameservers.IsNull() { addressFamily.Ipv6 = &iaas.CreateNetworkIPv6Body{ Nameservers: &modelIPv6Nameservers, Prefix: conversion.StringValueToPointer(model.IPv6Prefix), @@ -385,15 +421,16 @@ func toCreatePayload(ctx context.Context, model *networkModel.Model) (*iaas.Crea if model.NoIPv6Gateway.ValueBool() { addressFamily.Ipv6.Gateway = iaas.NewNullableString(nil) - } else if !(model.IPv6Gateway.IsUnknown() || model.IPv6Gateway.IsNull()) { + } else if !model.IPv6Gateway.IsUnknown() && !model.IPv6Gateway.IsNull() { addressFamily.Ipv6.Gateway = iaas.NewNullableString(conversion.StringValueToPointer(model.IPv6Gateway)) } } modelIPv4Nameservers := []string{} + var modelIPv4List []attr.Value - if !(model.IPv4Nameservers.IsNull() || model.IPv4Nameservers.IsUnknown()) { + if !model.IPv4Nameservers.IsNull() && !model.IPv4Nameservers.IsUnknown() { modelIPv4List = model.IPv4Nameservers.Elements() } else { modelIPv4List = model.Nameservers.Elements() @@ -404,6 +441,7 @@ func toCreatePayload(ctx context.Context, model *networkModel.Model) (*iaas.Crea if !ok { return nil, fmt.Errorf("type assertion failed") } + modelIPv4Nameservers = append(modelIPv4Nameservers, ipv4NameserverString.ValueString()) } @@ -416,7 +454,7 @@ func toCreatePayload(ctx context.Context, model *networkModel.Model) (*iaas.Crea if model.NoIPv4Gateway.ValueBool() { addressFamily.Ipv4.Gateway = iaas.NewNullableString(nil) - } else if !(model.IPv4Gateway.IsUnknown() || model.IPv4Gateway.IsNull()) { + } else if !model.IPv4Gateway.IsUnknown() && !model.IPv4Gateway.IsNull() { addressFamily.Ipv4.Gateway = iaas.NewNullableString(conversion.StringValueToPointer(model.IPv4Gateway)) } } @@ -443,42 +481,48 @@ func toUpdatePayload(ctx context.Context, model, stateModel *networkModel.Model) if model == nil { return nil, fmt.Errorf("nil model") } + addressFamily := &iaas.UpdateNetworkAddressFamily{} modelIPv6Nameservers := []string{} + for _, ipv6ns := range model.IPv6Nameservers.Elements() { ipv6NameserverString, ok := ipv6ns.(types.String) if !ok { return nil, fmt.Errorf("type assertion failed") } + modelIPv6Nameservers = append(modelIPv6Nameservers, ipv6NameserverString.ValueString()) } - if !(model.IPv6Nameservers.IsNull() || model.IPv6Nameservers.IsUnknown()) { + if !model.IPv6Nameservers.IsNull() && !model.IPv6Nameservers.IsUnknown() { addressFamily.Ipv6 = &iaas.UpdateNetworkIPv6Body{ Nameservers: &modelIPv6Nameservers, } if model.NoIPv6Gateway.ValueBool() { addressFamily.Ipv6.Gateway = iaas.NewNullableString(nil) - } else if !(model.IPv6Gateway.IsUnknown() || model.IPv6Gateway.IsNull()) { + } else if !model.IPv6Gateway.IsUnknown() && !model.IPv6Gateway.IsNull() { addressFamily.Ipv6.Gateway = iaas.NewNullableString(conversion.StringValueToPointer(model.IPv6Gateway)) } } modelIPv4Nameservers := []string{} + var modelIPv4List []attr.Value - if !(model.IPv4Nameservers.IsNull() || model.IPv4Nameservers.IsUnknown()) { + if !model.IPv4Nameservers.IsNull() && !model.IPv4Nameservers.IsUnknown() { modelIPv4List = model.IPv4Nameservers.Elements() } else { modelIPv4List = model.Nameservers.Elements() } + for _, ipv4ns := range modelIPv4List { ipv4NameserverString, ok := ipv4ns.(types.String) if !ok { return nil, fmt.Errorf("type assertion failed") } + modelIPv4Nameservers = append(modelIPv4Nameservers, ipv4NameserverString.ValueString()) } @@ -489,11 +533,13 @@ func toUpdatePayload(ctx context.Context, model, stateModel *networkModel.Model) if model.NoIPv4Gateway.ValueBool() { addressFamily.Ipv4.Gateway = iaas.NewNullableString(nil) - } else if !(model.IPv4Gateway.IsUnknown() || model.IPv4Gateway.IsNull()) { + } else if !model.IPv4Gateway.IsUnknown() && !model.IPv4Gateway.IsNull() { addressFamily.Ipv4.Gateway = iaas.NewNullableString(conversion.StringValueToPointer(model.IPv4Gateway)) } } + currentLabels := stateModel.Labels + labels, err := conversion.ToJSONMapPartialUpdatePayload(ctx, currentLabels, model.Labels) if err != nil { return nil, fmt.Errorf("converting to Go map: %w", err) diff --git a/stackit/internal/services/iaas/network/utils/v1network/resource_test.go b/stackit/internal/services/iaas/network/utils/v1network/resource_test.go index 21db0a1d1..70b6bf6b9 100644 --- a/stackit/internal/services/iaas/network/utils/v1network/resource_test.go +++ b/stackit/internal/services/iaas/network/utils/v1network/resource_test.go @@ -338,9 +338,11 @@ func TestMapFields(t *testing.T) { if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(tt.state, tt.expected) if diff != "" { @@ -473,9 +475,11 @@ func TestToCreatePayload(t *testing.T) { if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(output, tt.expected, cmp.AllowUnexported(iaas.NullableString{})) if diff != "" { @@ -677,9 +681,11 @@ func TestToUpdatePayload(t *testing.T) { if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(output, tt.expected, cmp.AllowUnexported(iaas.NullableString{})) if diff != "" { diff --git a/stackit/internal/services/iaas/network/utils/v2network/datasource.go b/stackit/internal/services/iaas/network/utils/v2network/datasource.go index bc447b825..761648201 100644 --- a/stackit/internal/services/iaas/network/utils/v2network/datasource.go +++ b/stackit/internal/services/iaas/network/utils/v2network/datasource.go @@ -16,13 +16,15 @@ import ( "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" ) -func DatasourceRead(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse, client *iaasalpha.APIClient, providerData core.ProviderData) { // nolint:gocritic // function signature required by Terraform +func DatasourceRead(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse, client *iaasalpha.APIClient, providerData core.ProviderData) { //nolint:gocritic // function signature required by Terraform var model networkModel.DataSourceModel diags := req.Config.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() networkId := model.NetworkId.ValueString() region := providerData.GetRegionWithOverride(model.Region) @@ -42,6 +44,7 @@ func DatasourceRead(ctx context.Context, req datasource.ReadRequest, resp *datas }, ) resp.State.RemoveResource(ctx) + return } @@ -50,11 +53,14 @@ func DatasourceRead(ctx context.Context, req datasource.ReadRequest, resp *datas core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading network", fmt.Sprintf("Processing API payload: %v", err)) return } + diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "Network read") } @@ -62,6 +68,7 @@ func mapDataSourceFields(ctx context.Context, networkResp *iaasalpha.Network, mo if networkResp == nil { return fmt.Errorf("response input is nil") } + if model == nil { return fmt.Errorf("model input is nil") } @@ -91,9 +98,11 @@ func mapDataSourceFields(ctx context.Context, networkResp *iaasalpha.Network, mo respNameservers := *networkResp.Ipv4.Nameservers modelNameservers, err := utils.ListValuetoStringSlice(model.Nameservers) modelIPv4Nameservers, errIpv4 := utils.ListValuetoStringSlice(model.IPv4Nameservers) + if err != nil { return fmt.Errorf("get current network nameservers from model: %w", err) } + if errIpv4 != nil { return fmt.Errorf("get current IPv4 network nameservers from model: %w", errIpv4) } @@ -103,9 +112,11 @@ func mapDataSourceFields(ctx context.Context, networkResp *iaasalpha.Network, mo nameserversTF, diags := types.ListValueFrom(ctx, types.StringType, reconciledNameservers) ipv4NameserversTF, ipv4Diags := types.ListValueFrom(ctx, types.StringType, reconciledIPv4Nameservers) + if diags.HasError() { return fmt.Errorf("map network nameservers: %w", core.DiagsToError(diags)) } + if ipv4Diags.HasError() { return fmt.Errorf("map IPv4 network nameservers: %w", core.DiagsToError(ipv4Diags)) } @@ -119,13 +130,16 @@ func mapDataSourceFields(ctx context.Context, networkResp *iaasalpha.Network, mo model.IPv4Prefixes = types.ListNull(types.StringType) } else { respPrefixes := *networkResp.Ipv4.Prefixes + prefixesTF, diags := types.ListValueFrom(ctx, types.StringType, respPrefixes) if diags.HasError() { return fmt.Errorf("map network prefixes: %w", core.DiagsToError(diags)) } + if len(respPrefixes) > 0 { model.IPv4Prefix = types.StringValue(respPrefixes[0]) _, netmask, err := net.ParseCIDR(respPrefixes[0]) + if err != nil { // silently ignore parsing error for the netmask model.IPv4PrefixLength = types.Int64Null() @@ -157,6 +171,7 @@ func mapDataSourceFields(ctx context.Context, networkResp *iaasalpha.Network, mo model.IPv6Nameservers = types.ListNull(types.StringType) } else { respIPv6Nameservers := *networkResp.Ipv6.Nameservers + modelIPv6Nameservers, errIpv6 := utils.ListValuetoStringSlice(model.IPv6Nameservers) if errIpv6 != nil { return fmt.Errorf("get current IPv6 network nameservers from model: %w", errIpv6) @@ -176,13 +191,16 @@ func mapDataSourceFields(ctx context.Context, networkResp *iaasalpha.Network, mo model.IPv6Prefixes = types.ListNull(types.StringType) } else { respPrefixesV6 := *networkResp.Ipv6.Prefixes + prefixesV6TF, diags := types.ListValueFrom(ctx, types.StringType, respPrefixesV6) if diags.HasError() { return fmt.Errorf("map network IPv6 prefixes: %w", core.DiagsToError(diags)) } + if len(respPrefixesV6) > 0 { model.IPv6Prefix = types.StringValue(respPrefixesV6[0]) _, netmask, err := net.ParseCIDR(respPrefixesV6[0]) + if err != nil { // silently ignore parsing error for the netmask model.IPv6PrefixLength = types.Int64Null() @@ -191,6 +209,7 @@ func mapDataSourceFields(ctx context.Context, networkResp *iaasalpha.Network, mo model.IPv6PrefixLength = types.Int64Value(int64(ones)) } } + model.IPv6Prefixes = prefixesV6TF } diff --git a/stackit/internal/services/iaas/network/utils/v2network/datasource_test.go b/stackit/internal/services/iaas/network/utils/v2network/datasource_test.go index eba9d4117..f181fc436 100644 --- a/stackit/internal/services/iaas/network/utils/v2network/datasource_test.go +++ b/stackit/internal/services/iaas/network/utils/v2network/datasource_test.go @@ -373,9 +373,11 @@ func TestMapDataSourceFields(t *testing.T) { if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(tt.state, tt.expected) if diff != "" { diff --git a/stackit/internal/services/iaas/network/utils/v2network/resource.go b/stackit/internal/services/iaas/network/utils/v2network/resource.go index 2bfbe0d53..1eac8e577 100644 --- a/stackit/internal/services/iaas/network/utils/v2network/resource.go +++ b/stackit/internal/services/iaas/network/utils/v2network/resource.go @@ -22,11 +22,12 @@ import ( "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" ) -func Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse, client *iaasalpha.APIClient) { // nolint:gocritic // function signature required by Terraform +func Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse, client *iaasalpha.APIClient) { //nolint:gocritic // function signature required by Terraform // Retrieve values from plan var model networkModel.Model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -52,6 +53,7 @@ func Create(ctx context.Context, req resource.CreateRequest, resp *resource.Crea } networkId := *network.Id + network, err = wait.CreateNetworkWaitHandler(ctx, client, projectId, region, networkId).WaitWithContext(ctx) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating network", fmt.Sprintf("Network creation waiting: %v", err)) @@ -69,19 +71,23 @@ func Create(ctx context.Context, req resource.CreateRequest, resp *resource.Crea // Set state to fully populated data diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "Network created") } -func Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse, client *iaasalpha.APIClient, providerData core.ProviderData) { // nolint:gocritic // function signature required by Terraform +func Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse, client *iaasalpha.APIClient, providerData core.ProviderData) { //nolint:gocritic // function signature required by Terraform var model networkModel.Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() networkId := model.NetworkId.ValueString() region := providerData.GetRegionWithOverride(model.Region) @@ -96,7 +102,9 @@ func Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResp resp.State.RemoveResource(ctx) return } + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading network", fmt.Sprintf("Calling API: %v", err)) + return } @@ -109,20 +117,24 @@ func Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResp // Set refreshed state diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "Network read") } -func Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse, client *iaasalpha.APIClient) { // nolint:gocritic // function signature required by Terraform +func Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse, client *iaasalpha.APIClient) { //nolint:gocritic // function signature required by Terraform // Retrieve values from plan var model networkModel.Model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() networkId := model.NetworkId.ValueString() region := model.Region.ValueString() @@ -134,6 +146,7 @@ func Update(ctx context.Context, req resource.UpdateRequest, resp *resource.Upda var stateModel networkModel.Model diags = req.State.Get(ctx, &stateModel) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -150,6 +163,7 @@ func Update(ctx context.Context, req resource.UpdateRequest, resp *resource.Upda core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating network", fmt.Sprintf("Calling API: %v", err)) return } + waitResp, err := wait.UpdateNetworkWaitHandler(ctx, client, projectId, region, networkId).WaitWithContext(ctx) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating network", fmt.Sprintf("Network update waiting: %v", err)) @@ -161,19 +175,23 @@ func Update(ctx context.Context, req resource.UpdateRequest, resp *resource.Upda core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating network", fmt.Sprintf("Processing API payload: %v", err)) return } + diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "Network updated") } -func Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse, client *iaasalpha.APIClient) { // nolint:gocritic // function signature required by Terraform +func Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse, client *iaasalpha.APIClient) { //nolint:gocritic // function signature required by Terraform // Retrieve values from state var model networkModel.Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -191,6 +209,7 @@ func Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.Dele core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting network", fmt.Sprintf("Calling API: %v", err)) return } + _, err = wait.DeleteNetworkWaitHandler(ctx, client, projectId, region, networkId).WaitWithContext(ctx) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting network", fmt.Sprintf("Network deletion waiting: %v", err)) @@ -201,7 +220,7 @@ func Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.Dele } // ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: project_id,region,network_id +// The expected format of the resource import identifier is: project_id,region,network_id. func ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { idParts := strings.Split(req.ID, core.Separator) @@ -210,6 +229,7 @@ func ImportState(ctx context.Context, req resource.ImportStateRequest, resp *res "Error importing network", fmt.Sprintf("Expected import identifier with format: [project_id],[region],[network_id] Got: %q", req.ID), ) + return } @@ -230,6 +250,7 @@ func mapFields(ctx context.Context, networkResp *iaasalpha.Network, model *netwo if networkResp == nil { return fmt.Errorf("response input is nil") } + if model == nil { return fmt.Errorf("model input is nil") } @@ -259,9 +280,11 @@ func mapFields(ctx context.Context, networkResp *iaasalpha.Network, model *netwo respNameservers := *networkResp.Ipv4.Nameservers modelNameservers, err := utils.ListValuetoStringSlice(model.Nameservers) modelIPv4Nameservers, errIpv4 := utils.ListValuetoStringSlice(model.IPv4Nameservers) + if err != nil { return fmt.Errorf("get current network nameservers from model: %w", err) } + if errIpv4 != nil { return fmt.Errorf("get current IPv4 network nameservers from model: %w", errIpv4) } @@ -271,9 +294,11 @@ func mapFields(ctx context.Context, networkResp *iaasalpha.Network, model *netwo nameserversTF, diags := types.ListValueFrom(ctx, types.StringType, reconciledNameservers) ipv4NameserversTF, ipv4Diags := types.ListValueFrom(ctx, types.StringType, reconciledIPv4Nameservers) + if diags.HasError() { return fmt.Errorf("map network nameservers: %w", core.DiagsToError(diags)) } + if ipv4Diags.HasError() { return fmt.Errorf("map IPv4 network nameservers: %w", core.DiagsToError(ipv4Diags)) } @@ -287,12 +312,15 @@ func mapFields(ctx context.Context, networkResp *iaasalpha.Network, model *netwo model.IPv4Prefixes = types.ListNull(types.StringType) } else { respPrefixes := *networkResp.Ipv4.Prefixes + prefixesTF, diags := types.ListValueFrom(ctx, types.StringType, respPrefixes) if diags.HasError() { return fmt.Errorf("map network prefixes: %w", core.DiagsToError(diags)) } + if len(respPrefixes) > 0 { model.IPv4Prefix = types.StringValue(respPrefixes[0]) + _, netmask, err := net.ParseCIDR(respPrefixes[0]) if err != nil { tflog.Error(ctx, fmt.Sprintf("ipv4_prefix_length: %+v", err)) @@ -326,6 +354,7 @@ func mapFields(ctx context.Context, networkResp *iaasalpha.Network, model *netwo model.IPv6Nameservers = types.ListNull(types.StringType) } else { respIPv6Nameservers := *networkResp.Ipv6.Nameservers + modelIPv6Nameservers, errIpv6 := utils.ListValuetoStringSlice(model.IPv6Nameservers) if errIpv6 != nil { return fmt.Errorf("get current IPv6 network nameservers from model: %w", errIpv6) @@ -345,13 +374,16 @@ func mapFields(ctx context.Context, networkResp *iaasalpha.Network, model *netwo model.IPv6Prefixes = types.ListNull(types.StringType) } else { respPrefixesV6 := *networkResp.Ipv6.Prefixes + prefixesV6TF, diags := types.ListValueFrom(ctx, types.StringType, respPrefixesV6) if diags.HasError() { return fmt.Errorf("map network IPv6 prefixes: %w", core.DiagsToError(diags)) } + if len(respPrefixesV6) > 0 { model.IPv6Prefix = types.StringValue(respPrefixesV6[0]) _, netmask, err := net.ParseCIDR(respPrefixesV6[0]) + if err != nil { // silently ignore parsing error for the netmask model.IPv6PrefixLength = types.Int64Null() @@ -360,6 +392,7 @@ func mapFields(ctx context.Context, networkResp *iaasalpha.Network, model *netwo model.IPv6PrefixLength = types.Int64Value(int64(ones)) } } + model.IPv6Prefixes = prefixesV6TF } @@ -390,11 +423,13 @@ func toCreatePayload(ctx context.Context, model *networkModel.Model) (*iaasalpha } modelIPv6Nameservers := []string{} + for _, ipv6ns := range model.IPv6Nameservers.Elements() { ipv6NameserverString, ok := ipv6ns.(types.String) if !ok { return nil, fmt.Errorf("type assertion failed") } + modelIPv6Nameservers = append(modelIPv6Nameservers, ipv6NameserverString.ValueString()) } @@ -410,7 +445,7 @@ func toCreatePayload(ctx context.Context, model *networkModel.Model) (*iaasalpha var gateway *iaasalpha.NullableString if model.NoIPv6Gateway.ValueBool() { gateway = iaasalpha.NewNullableString(nil) - } else if !(model.IPv6Gateway.IsUnknown() || model.IPv6Gateway.IsNull()) { + } else if !model.IPv6Gateway.IsUnknown() && !model.IPv6Gateway.IsNull() { gateway = iaasalpha.NewNullableString(conversion.StringValueToPointer(model.IPv6Gateway)) } @@ -424,9 +459,10 @@ func toCreatePayload(ctx context.Context, model *networkModel.Model) (*iaasalpha } modelIPv4Nameservers := []string{} + var modelIPv4List []attr.Value - if !(model.IPv4Nameservers.IsNull() || model.IPv4Nameservers.IsUnknown()) { + if !model.IPv4Nameservers.IsNull() && !model.IPv4Nameservers.IsUnknown() { modelIPv4List = model.IPv4Nameservers.Elements() } else { modelIPv4List = model.Nameservers.Elements() @@ -437,6 +473,7 @@ func toCreatePayload(ctx context.Context, model *networkModel.Model) (*iaasalpha if !ok { return nil, fmt.Errorf("type assertion failed") } + modelIPv4Nameservers = append(modelIPv4Nameservers, ipv4NameserverString.ValueString()) } @@ -452,7 +489,7 @@ func toCreatePayload(ctx context.Context, model *networkModel.Model) (*iaasalpha var gateway *iaasalpha.NullableString if model.NoIPv4Gateway.ValueBool() { gateway = iaasalpha.NewNullableString(nil) - } else if !(model.IPv4Gateway.IsUnknown() || model.IPv4Gateway.IsNull()) { + } else if !model.IPv4Gateway.IsUnknown() && !model.IPv4Gateway.IsNull() { gateway = iaasalpha.NewNullableString(conversion.StringValueToPointer(model.IPv4Gateway)) } @@ -488,40 +525,45 @@ func toUpdatePayload(ctx context.Context, model, stateModel *networkModel.Model) } modelIPv6Nameservers := []string{} + for _, ipv6ns := range model.IPv6Nameservers.Elements() { ipv6NameserverString, ok := ipv6ns.(types.String) if !ok { return nil, fmt.Errorf("type assertion failed") } + modelIPv6Nameservers = append(modelIPv6Nameservers, ipv6NameserverString.ValueString()) } var ipv6Body *iaasalpha.UpdateNetworkIPv6Body - if !(model.IPv6Nameservers.IsNull() || model.IPv6Nameservers.IsUnknown()) { + if !model.IPv6Nameservers.IsNull() && !model.IPv6Nameservers.IsUnknown() { ipv6Body = &iaasalpha.UpdateNetworkIPv6Body{ Nameservers: &modelIPv6Nameservers, } if model.NoIPv6Gateway.ValueBool() { ipv6Body.Gateway = iaasalpha.NewNullableString(nil) - } else if !(model.IPv6Gateway.IsUnknown() || model.IPv6Gateway.IsNull()) { + } else if !model.IPv6Gateway.IsUnknown() && !model.IPv6Gateway.IsNull() { ipv6Body.Gateway = iaasalpha.NewNullableString(conversion.StringValueToPointer(model.IPv6Gateway)) } } modelIPv4Nameservers := []string{} + var modelIPv4List []attr.Value - if !(model.IPv4Nameservers.IsNull() || model.IPv4Nameservers.IsUnknown()) { + if !model.IPv4Nameservers.IsNull() && !model.IPv4Nameservers.IsUnknown() { modelIPv4List = model.IPv4Nameservers.Elements() } else { modelIPv4List = model.Nameservers.Elements() } + for _, ipv4ns := range modelIPv4List { ipv4NameserverString, ok := ipv4ns.(types.String) if !ok { return nil, fmt.Errorf("type assertion failed") } + modelIPv4Nameservers = append(modelIPv4Nameservers, ipv4NameserverString.ValueString()) } @@ -533,11 +575,13 @@ func toUpdatePayload(ctx context.Context, model, stateModel *networkModel.Model) if model.NoIPv4Gateway.ValueBool() { ipv4Body.Gateway = iaasalpha.NewNullableString(nil) - } else if !(model.IPv4Gateway.IsUnknown() || model.IPv4Gateway.IsNull()) { + } else if !model.IPv4Gateway.IsUnknown() && !model.IPv4Gateway.IsNull() { ipv4Body.Gateway = iaasalpha.NewNullableString(conversion.StringValueToPointer(model.IPv4Gateway)) } } + currentLabels := stateModel.Labels + labels, err := conversion.ToJSONMapPartialUpdatePayload(ctx, currentLabels, model.Labels) if err != nil { return nil, fmt.Errorf("converting to Go map: %w", err) diff --git a/stackit/internal/services/iaas/network/utils/v2network/resource_test.go b/stackit/internal/services/iaas/network/utils/v2network/resource_test.go index 93575f07e..4dcc85672 100644 --- a/stackit/internal/services/iaas/network/utils/v2network/resource_test.go +++ b/stackit/internal/services/iaas/network/utils/v2network/resource_test.go @@ -14,6 +14,7 @@ import ( func TestMapFields(t *testing.T) { const testRegion = "region" + tests := []struct { description string state model.Model @@ -370,9 +371,11 @@ func TestMapFields(t *testing.T) { if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(tt.state, tt.expected) if diff != "" { @@ -499,9 +502,11 @@ func TestToCreatePayload(t *testing.T) { if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(output, tt.expected, cmp.AllowUnexported(iaasalpha.NullableString{})) if diff != "" { @@ -693,9 +698,11 @@ func TestToUpdatePayload(t *testing.T) { if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(output, tt.expected, cmp.AllowUnexported(iaasalpha.NullableString{})) if diff != "" { diff --git a/stackit/internal/services/iaas/networkarea/datasource.go b/stackit/internal/services/iaas/networkarea/datasource.go index 15deb0886..243b62174 100644 --- a/stackit/internal/services/iaas/networkarea/datasource.go +++ b/stackit/internal/services/iaas/networkarea/datasource.go @@ -5,9 +5,6 @@ import ( "fmt" "net/http" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" - "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" @@ -17,7 +14,9 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/stackitcloud/stackit-sdk-go/services/iaas" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" ) @@ -49,10 +48,13 @@ func (d *networkAreaDataSource) Configure(ctx context.Context, req datasource.Co } apiClient := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + d.client = apiClient + tflog.Info(ctx, "IaaS client configured") } @@ -163,13 +165,15 @@ func (d *networkAreaDataSource) Schema(_ context.Context, _ datasource.SchemaReq } // Read refreshes the Terraform state with the latest data. -func (d *networkAreaDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (d *networkAreaDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.Config.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + organizationId := model.OrganizationId.ValueString() networkAreaId := model.NetworkAreaId.ValueString() ctx = tflog.SetField(ctx, "organization_id", organizationId) @@ -188,6 +192,7 @@ func (d *networkAreaDataSource) Read(ctx context.Context, req datasource.ReadReq }, ) resp.State.RemoveResource(ctx) + return } @@ -198,10 +203,13 @@ func (d *networkAreaDataSource) Read(ctx context.Context, req datasource.ReadReq core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading network area", fmt.Sprintf("Processing API payload: %v", err)) return } + diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "Network area read") } diff --git a/stackit/internal/services/iaas/networkarea/resource.go b/stackit/internal/services/iaas/networkarea/resource.go index a688e7d88..9870b0171 100644 --- a/stackit/internal/services/iaas/networkarea/resource.go +++ b/stackit/internal/services/iaas/networkarea/resource.go @@ -6,10 +6,6 @@ import ( "net/http" "strings" - "github.com/stackitcloud/stackit-sdk-go/services/resourcemanager" - iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" - resourcemanagerUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/resourcemanager/utils" - "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" @@ -28,8 +24,11 @@ import ( sdkUtils "github.com/stackitcloud/stackit-sdk-go/core/utils" "github.com/stackitcloud/stackit-sdk-go/services/iaas" "github.com/stackitcloud/stackit-sdk-go/services/iaas/wait" + "github.com/stackitcloud/stackit-sdk-go/services/resourcemanager" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" + resourcemanagerUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/resourcemanager/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" ) @@ -56,13 +55,13 @@ type Model struct { Labels types.Map `tfsdk:"labels"` } -// Struct corresponding to Model.NetworkRanges[i] +// Struct corresponding to Model.NetworkRanges[i]. type networkRange struct { Prefix types.String `tfsdk:"prefix"` NetworkRangeId types.String `tfsdk:"network_range_id"` } -// Types corresponding to networkRanges +// Types corresponding to networkRanges. var networkRangeTypes = map[string]attr.Type{ "prefix": types.StringType, "network_range_id": types.StringType, @@ -92,15 +91,20 @@ func (r *networkAreaResource) Configure(ctx context.Context, req resource.Config } apiClient := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + r.client = apiClient resourceManagerClient := resourcemanagerUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + r.resourceManagerClient = resourceManagerClient + tflog.Info(ctx, "IaaS client configured") } @@ -230,11 +234,12 @@ func (r *networkAreaResource) Schema(_ context.Context, _ resource.SchemaRequest } // Create creates the resource and sets the initial Terraform state. -func (r *networkAreaResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *networkAreaResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { //nolint:gocritic // function signature required by Terraform // Retrieve values from plan var model Model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -261,6 +266,7 @@ func (r *networkAreaResource) Create(ctx context.Context, req resource.CreateReq core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating network area", fmt.Sprintf("Network area creation waiting: %v", err)) return } + networkAreaId := *networkArea.AreaId ctx = tflog.SetField(ctx, "network_area_id", networkAreaId) @@ -275,20 +281,24 @@ func (r *networkAreaResource) Create(ctx context.Context, req resource.CreateReq // Set state to fully populated data diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "Network area created") } // Read refreshes the Terraform state with the latest data. -func (r *networkAreaResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (r *networkAreaResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + organizationId := model.OrganizationId.ValueString() networkAreaId := model.NetworkAreaId.ValueString() ctx = tflog.SetField(ctx, "organization_id", organizationId) @@ -301,7 +311,9 @@ func (r *networkAreaResource) Read(ctx context.Context, req resource.ReadRequest resp.State.RemoveResource(ctx) return } + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading network area", fmt.Sprintf("Calling API: %v", err)) + return } @@ -316,30 +328,35 @@ func (r *networkAreaResource) Read(ctx context.Context, req resource.ReadRequest // Set refreshed state diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "Network area read") } // Update updates the resource and sets the updated Terraform state on success. -func (r *networkAreaResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *networkAreaResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { //nolint:gocritic // function signature required by Terraform // Retrieve values from plan var model Model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + organizationId := model.OrganizationId.ValueString() networkAreaId := model.NetworkAreaId.ValueString() ctx = tflog.SetField(ctx, "organization_id", organizationId) ctx = tflog.SetField(ctx, "network_area_id", networkAreaId) ranges := []networkRange{} - if !(model.NetworkRanges.IsNull() || model.NetworkRanges.IsUnknown()) { + if !model.NetworkRanges.IsNull() && !model.NetworkRanges.IsUnknown() { diags = model.NetworkRanges.ElementsAs(ctx, &ranges, false) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -349,6 +366,7 @@ func (r *networkAreaResource) Update(ctx context.Context, req resource.UpdateReq var stateModel Model diags = req.State.Get(ctx, &stateModel) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -365,6 +383,7 @@ func (r *networkAreaResource) Update(ctx context.Context, req resource.UpdateReq core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating network area", fmt.Sprintf("Calling API: %v", err)) return } + waitResp, err := wait.UpdateNetworkAreaWaitHandler(ctx, r.client, organizationId, networkAreaId).WaitWithContext(ctx) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating network area", fmt.Sprintf("Network area update waiting: %v", err)) @@ -385,7 +404,9 @@ func (r *networkAreaResource) Update(ctx context.Context, req resource.UpdateReq resp.State.RemoveResource(ctx) return } + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading network area", fmt.Sprintf("Calling API: %v", err)) + return } @@ -396,20 +417,24 @@ func (r *networkAreaResource) Update(ctx context.Context, req resource.UpdateReq core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating network area", fmt.Sprintf("Processing API payload: %v", err)) return } + diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "Network area updated") } // Delete deletes the resource and removes the Terraform state on success. -func (r *networkAreaResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform +func (r *networkAreaResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { //nolint:gocritic // function signature required by Terraform // Retrieve values from state var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -431,6 +456,7 @@ func (r *networkAreaResource) Delete(ctx context.Context, req resource.DeleteReq core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting network area", fmt.Sprintf("Calling API: %v", err)) return } + _, err = wait.DeleteNetworkAreaWaitHandler(ctx, r.client, organizationId, networkAreaId).WaitWithContext(ctx) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting network area", fmt.Sprintf("Network area deletion waiting: %v", err)) @@ -441,7 +467,7 @@ func (r *networkAreaResource) Delete(ctx context.Context, req resource.DeleteReq } // ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: project_id,network_id +// The expected format of the resource import identifier is: project_id,network_id. func (r *networkAreaResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { idParts := strings.Split(req.ID, core.Separator) @@ -450,6 +476,7 @@ func (r *networkAreaResource) ImportState(ctx context.Context, req resource.Impo "Error importing network area", fmt.Sprintf("Expected import identifier with format: [organization_id],[network_area_id] Got: %q", req.ID), ) + return } @@ -467,6 +494,7 @@ func mapFields(ctx context.Context, networkAreaResp *iaas.NetworkArea, networkAr if networkAreaResp == nil { return fmt.Errorf("response input is nil") } + if model == nil { return fmt.Errorf("model input is nil") } @@ -486,6 +514,7 @@ func mapFields(ctx context.Context, networkAreaResp *iaas.NetworkArea, networkAr model.DefaultNameservers = types.ListNull(types.StringType) } else { respDefaultNameservers := *networkAreaResp.Ipv4.DefaultNameservers + modelDefaultNameservers, err := utils.ListValuetoStringSlice(model.DefaultNameservers) if err != nil { return fmt.Errorf("get current network area default nameservers from model: %w", err) @@ -532,13 +561,14 @@ func mapNetworkRanges(ctx context.Context, networkAreaRangesList *[]iaas.Network if networkAreaRangesList == nil { return fmt.Errorf("nil network area ranges list") } + if len(*networkAreaRangesList) == 0 { model.NetworkRanges = types.ListNull(types.ObjectType{AttrTypes: networkRangeTypes}) return nil } ranges := []networkRange{} - if !(model.NetworkRanges.IsNull() || model.NetworkRanges.IsUnknown()) { + if !model.NetworkRanges.IsNull() && !model.NetworkRanges.IsUnknown() { diags = model.NetworkRanges.ElementsAs(ctx, &ranges, false) if diags.HasError() { return fmt.Errorf("map network ranges: %w", core.DiagsToError(diags)) @@ -558,14 +588,17 @@ func mapNetworkRanges(ctx context.Context, networkAreaRangesList *[]iaas.Network reconciledRangePrefixes := utils.ReconcileStringSlices(modelNetworkRangePrefixes, apiNetworkRangePrefixes) networkRangesList := []attr.Value{} + for i, prefix := range reconciledRangePrefixes { var networkRangeId string + for _, networkRangeElement := range *networkAreaRangesList { if *networkRangeElement.Prefix == prefix { networkRangeId = *networkRangeElement.NetworkRangeId break } } + networkRangeMap := map[string]attr.Value{ "prefix": types.StringValue(prefix), "network_range_id": types.StringValue(networkRangeId), @@ -588,6 +621,7 @@ func mapNetworkRanges(ctx context.Context, networkAreaRangesList *[]iaas.Network } model.NetworkRanges = networkRangesTF + return nil } @@ -597,11 +631,13 @@ func toCreatePayload(ctx context.Context, model *Model) (*iaas.CreateNetworkArea } modelDefaultNameservers := []string{} + for _, ns := range model.DefaultNameservers.Elements() { nameserverString, ok := ns.(types.String) if !ok { return nil, fmt.Errorf("type assertion failed") } + modelDefaultNameservers = append(modelDefaultNameservers, nameserverString.ValueString()) } @@ -637,11 +673,13 @@ func toUpdatePayload(ctx context.Context, model *Model, currentLabels types.Map) } modelDefaultNameservers := []string{} + for _, ns := range model.DefaultNameservers.Elements() { nameserverString, ok := ns.(types.String) if !ok { return nil, fmt.Errorf("type assertion failed") } + modelDefaultNameservers = append(modelDefaultNameservers, nameserverString.ValueString()) } @@ -670,6 +708,7 @@ func toNetworkRangesPayload(ctx context.Context, model *Model) (*[]iaas.NetworkR } networkRangesModel := []networkRange{} + diags := model.NetworkRanges.ElementsAs(ctx, &networkRangesModel, false) if diags.HasError() { return nil, core.DiagsToError(diags) @@ -680,6 +719,7 @@ func toNetworkRangesPayload(ctx context.Context, model *Model) (*[]iaas.NetworkR } payload := []iaas.NetworkRange{} + for i := range networkRangesModel { networkRangeModel := networkRangesModel[i] payload = append(payload, iaas.NetworkRange{ @@ -690,7 +730,7 @@ func toNetworkRangesPayload(ctx context.Context, model *Model) (*[]iaas.NetworkR return &payload, nil } -// updateNetworkRanges creates and deletes network ranges so that network area ranges are the ones in the model +// updateNetworkRanges creates and deletes network ranges so that network area ranges are the ones in the model. func updateNetworkRanges(ctx context.Context, organizationId, networkAreaId string, ranges []networkRange, client *iaas.APIClient) error { // Get network ranges current state currentNetworkRangesResp, err := client.ListNetworkAreaRanges(ctx, organizationId, networkAreaId).Execute() @@ -716,6 +756,7 @@ func updateNetworkRanges(ctx context.Context, organizationId, networkAreaId stri if _, ok := networkRangesState[prefix]; !ok { networkRangesState[prefix] = &networkRangeState{} } + networkRangesState[prefix].isCreated = true networkRangesState[prefix].id = *networkRange.NetworkRangeId } diff --git a/stackit/internal/services/iaas/networkarea/resource_test.go b/stackit/internal/services/iaas/networkarea/resource_test.go index d91044b98..afcc42537 100644 --- a/stackit/internal/services/iaas/networkarea/resource_test.go +++ b/stackit/internal/services/iaas/networkarea/resource_test.go @@ -17,14 +17,16 @@ import ( "github.com/stackitcloud/stackit-sdk-go/services/iaas" ) -var testOrganizationId = uuid.NewString() -var testAreaId = uuid.NewString() -var testRangeId1 = uuid.NewString() -var testRangeId2 = uuid.NewString() -var testRangeId3 = uuid.NewString() -var testRangeId4 = uuid.NewString() -var testRangeId5 = uuid.NewString() -var testRangeId2Repeated = uuid.NewString() +var ( + testOrganizationId = uuid.NewString() + testAreaId = uuid.NewString() + testRangeId1 = uuid.NewString() + testRangeId2 = uuid.NewString() + testRangeId3 = uuid.NewString() + testRangeId4 = uuid.NewString() + testRangeId5 = uuid.NewString() + testRangeId2Repeated = uuid.NewString() +) func TestMapFields(t *testing.T) { tests := []struct { @@ -380,9 +382,11 @@ func TestMapFields(t *testing.T) { if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(tt.state, tt.expected) if diff != "" { @@ -461,9 +465,11 @@ func TestToCreatePayload(t *testing.T) { if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(output, tt.expected) if diff != "" { @@ -522,9 +528,11 @@ func TestToUpdatePayload(t *testing.T) { if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(output, tt.expected) if diff != "" { @@ -556,6 +564,7 @@ func TestUpdateNetworkRanges(t *testing.T) { }, }, } + getAllNetworkRangesRespBytes, err := json.Marshal(getAllNetworkRangesResp) if err != nil { t.Fatalf("Failed to marshal get all network ranges response: %v", err) @@ -856,12 +865,15 @@ func TestUpdateNetworkRanges(t *testing.T) { // Handler for getting all network ranges getAllNetworkRangesHandler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "application/json") + if tt.getAllNetworkRangesFails { w.WriteHeader(http.StatusInternalServerError) + _, err := w.Write(failureRespBytes) if err != nil { t.Errorf("Get all network ranges handler: failed to write bad response: %v", err) } + return } @@ -874,16 +886,20 @@ func TestUpdateNetworkRanges(t *testing.T) { // Handler for creating network range createNetworkRangeHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { decoder := json.NewDecoder(r.Body) + var payload iaas.CreateNetworkAreaRangePayload + err := decoder.Decode(&payload) if err != nil { t.Errorf("Create network range handler: failed to parse payload") return } + if payload.Ipv4 == nil { t.Errorf("Create network range handler: nil Ipv4") return } + ipv4 := *payload.Ipv4 for _, networkRange := range ipv4 { @@ -892,13 +908,17 @@ func TestUpdateNetworkRanges(t *testing.T) { t.Errorf("Create network range handler: attempted to create range '%v' that already exists", *payload.Ipv4) return } + w.Header().Set("Content-Type", "application/json") + if tt.createNetworkRangesFails { w.WriteHeader(http.StatusInternalServerError) + _, err := w.Write(failureRespBytes) if err != nil { t.Errorf("Create network ranges handler: failed to write bad response: %v", err) } + return } @@ -906,15 +926,18 @@ func TestUpdateNetworkRanges(t *testing.T) { Prefix: utils.Ptr("prefix"), NetworkRangeId: utils.Ptr("id-range"), } + respBytes, err := json.Marshal(resp) if err != nil { t.Errorf("Create network range handler: failed to marshal response: %v", err) return } + _, err = w.Write(respBytes) if err != nil { t.Errorf("Create network range handler: failed to write response: %v", err) } + networkRangesStates[prefix] = true } }) @@ -922,6 +945,7 @@ func TestUpdateNetworkRanges(t *testing.T) { // Handler for deleting Network range deleteNetworkRangeHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) + networkRangeId, ok := vars["networkRangeId"] if !ok { t.Errorf("Delete network range handler: no range ID") @@ -929,28 +953,34 @@ func TestUpdateNetworkRanges(t *testing.T) { } var prefix string + for _, rangeItem := range *getAllNetworkRangesResp.Items { if *rangeItem.NetworkRangeId == networkRangeId { prefix = *rangeItem.Prefix } } + prefixExists, prefixWasCreated := networkRangesStates[prefix] if !prefixWasCreated { t.Errorf("Delete network range handler: attempted to delete range '%v' that wasn't created", prefix) return } + if prefixWasCreated && !prefixExists { t.Errorf("Delete network range handler: attempted to delete range '%v' that was already deleted", prefix) return } w.Header().Set("Content-Type", "application/json") + if tt.deleteNetworkRangesFails { w.WriteHeader(http.StatusInternalServerError) + _, err := w.Write(failureRespBytes) if err != nil { t.Errorf("Delete network range handler: failed to write bad response: %v", err) } + return } @@ -958,21 +988,25 @@ func TestUpdateNetworkRanges(t *testing.T) { if err != nil { t.Errorf("Delete network range handler: failed to write response: %v", err) } + networkRangesStates[prefix] = false }) // Setup server and client router := mux.NewRouter() router.HandleFunc("/v1/organizations/{organizationId}/network-areas/{areaId}/network-ranges", func(w http.ResponseWriter, r *http.Request) { - if r.Method == "GET" { + switch r.Method { + case "GET": getAllNetworkRangesHandler(w, r) - } else if r.Method == "POST" { + case "POST": createNetworkRangeHandler(w, r) } }) router.HandleFunc("/v1/organizations/{organizationId}/network-areas/{areaId}/network-ranges/{networkRangeId}", deleteNetworkRangeHandler) + mockedServer := httptest.NewServer(router) defer mockedServer.Close() + client, err := iaas.NewAPIClient( config.WithEndpoint(mockedServer.URL), config.WithoutAuthentication(), @@ -986,9 +1020,11 @@ func TestUpdateNetworkRanges(t *testing.T) { if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(networkRangesStates, tt.expectedNetworkRangesStates) if diff != "" { diff --git a/stackit/internal/services/iaas/networkarearoute/datasource.go b/stackit/internal/services/iaas/networkarearoute/datasource.go index 19b139d3f..36e83f31b 100644 --- a/stackit/internal/services/iaas/networkarearoute/datasource.go +++ b/stackit/internal/services/iaas/networkarearoute/datasource.go @@ -5,16 +5,15 @@ import ( "fmt" "net/http" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" - "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/stackitcloud/stackit-sdk-go/services/iaas" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" ) @@ -46,10 +45,13 @@ func (d *networkAreaRouteDataSource) Configure(ctx context.Context, req datasour } apiClient := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + d.client = apiClient + tflog.Info(ctx, "IaaS client configured") } @@ -106,13 +108,15 @@ func (d *networkAreaRouteDataSource) Schema(_ context.Context, _ datasource.Sche } // Read refreshes the Terraform state with the latest data. -func (d *networkAreaRouteDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (d *networkAreaRouteDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.Config.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + organizationId := model.OrganizationId.ValueString() networkAreaId := model.NetworkAreaId.ValueString() networkAreaRouteId := model.NetworkAreaRouteId.ValueString() @@ -133,6 +137,7 @@ func (d *networkAreaRouteDataSource) Read(ctx context.Context, req datasource.Re }, ) resp.State.RemoveResource(ctx) + return } @@ -141,10 +146,13 @@ func (d *networkAreaRouteDataSource) Read(ctx context.Context, req datasource.Re core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading network area route", fmt.Sprintf("Processing API payload: %v", err)) return } + diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "Network area route read") } diff --git a/stackit/internal/services/iaas/networkarearoute/resource.go b/stackit/internal/services/iaas/networkarearoute/resource.go index e29b3fdc0..698ddf6a8 100644 --- a/stackit/internal/services/iaas/networkarearoute/resource.go +++ b/stackit/internal/services/iaas/networkarearoute/resource.go @@ -6,10 +6,6 @@ import ( "net/http" "strings" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - - iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" - "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" @@ -22,6 +18,8 @@ import ( "github.com/stackitcloud/stackit-sdk-go/services/iaas" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" ) @@ -65,10 +63,13 @@ func (r *networkAreaRouteResource) Configure(ctx context.Context, req resource.C } apiClient := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + r.client = apiClient + tflog.Info(ctx, "IaaS client configured") } @@ -151,11 +152,12 @@ func (r *networkAreaRouteResource) Schema(_ context.Context, _ resource.SchemaRe } // Create creates the resource and sets the initial Terraform state. -func (r *networkAreaRouteResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *networkAreaRouteResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { //nolint:gocritic // function signature required by Terraform // Retrieve values from plan var model Model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -178,6 +180,7 @@ func (r *networkAreaRouteResource) Create(ctx context.Context, req resource.Crea core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating network area route", fmt.Sprintf("Calling API: %v", err)) return } + if routes.Items == nil || len(*routes.Items) == 0 { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating network area route.", "Empty response from API") return @@ -204,20 +207,24 @@ func (r *networkAreaRouteResource) Create(ctx context.Context, req resource.Crea // Set state to fully populated data diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "Network area route created") } // Read refreshes the Terraform state with the latest data. -func (r *networkAreaRouteResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (r *networkAreaRouteResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + organizationId := model.OrganizationId.ValueString() networkAreaId := model.NetworkAreaId.ValueString() networkAreaRouteId := model.NetworkAreaRouteId.ValueString() @@ -232,7 +239,9 @@ func (r *networkAreaRouteResource) Read(ctx context.Context, req resource.ReadRe resp.State.RemoveResource(ctx) return } + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading network area route.", fmt.Sprintf("Calling API: %v", err)) + return } @@ -245,18 +254,21 @@ func (r *networkAreaRouteResource) Read(ctx context.Context, req resource.ReadRe // Set refreshed state diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "Network area route read") } // Delete deletes the resource and removes the Terraform state on success. -func (r *networkAreaRouteResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform +func (r *networkAreaRouteResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { //nolint:gocritic // function signature required by Terraform // Retrieve values from state var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -279,11 +291,12 @@ func (r *networkAreaRouteResource) Delete(ctx context.Context, req resource.Dele } // Update updates the resource and sets the updated Terraform state on success. -func (r *networkAreaRouteResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *networkAreaRouteResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { //nolint:gocritic // function signature required by Terraform // Retrieve values from plan var model Model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -299,6 +312,7 @@ func (r *networkAreaRouteResource) Update(ctx context.Context, req resource.Upda var stateModel Model diags = req.State.Get(ctx, &stateModel) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -321,16 +335,19 @@ func (r *networkAreaRouteResource) Update(ctx context.Context, req resource.Upda core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating network area route", fmt.Sprintf("Processing API payload: %v", err)) return } + diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "Network area route updated") } // ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: organization_id,network_aread_id,network_area_route_id +// The expected format of the resource import identifier is: organization_id,network_aread_id,network_area_route_id. func (r *networkAreaRouteResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { idParts := strings.Split(req.ID, core.Separator) @@ -339,6 +356,7 @@ func (r *networkAreaRouteResource) ImportState(ctx context.Context, req resource "Error importing network area route", fmt.Sprintf("Expected import identifier with format: [organization_id],[network_area_id],[network_area_route_id] Got: %q", req.ID), ) + return } @@ -359,6 +377,7 @@ func mapFields(ctx context.Context, networkAreaRoute *iaas.Route, model *Model) if networkAreaRoute == nil { return fmt.Errorf("response input is nil") } + if model == nil { return fmt.Errorf("model input is nil") } @@ -383,6 +402,7 @@ func mapFields(ctx context.Context, networkAreaRoute *iaas.Route, model *Model) model.NextHop = types.StringPointerValue(networkAreaRoute.Nexthop) model.Prefix = types.StringPointerValue(networkAreaRoute.Prefix) model.Labels = labels + return nil } diff --git a/stackit/internal/services/iaas/networkarearoute/resource_test.go b/stackit/internal/services/iaas/networkarearoute/resource_test.go index d210e0592..c55d0939c 100644 --- a/stackit/internal/services/iaas/networkarearoute/resource_test.go +++ b/stackit/internal/services/iaas/networkarearoute/resource_test.go @@ -99,9 +99,11 @@ func TestMapFields(t *testing.T) { if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(tt.state, tt.expected) if diff != "" { @@ -148,9 +150,11 @@ func TestToCreatePayload(t *testing.T) { if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(output, tt.expected) if diff != "" { @@ -191,9 +195,11 @@ func TestToUpdatePayload(t *testing.T) { if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(output, tt.expected, cmp.AllowUnexported(iaas.NullableString{})) if diff != "" { diff --git a/stackit/internal/services/iaas/networkinterface/datasource.go b/stackit/internal/services/iaas/networkinterface/datasource.go index dd0e38122..b5976f83e 100644 --- a/stackit/internal/services/iaas/networkinterface/datasource.go +++ b/stackit/internal/services/iaas/networkinterface/datasource.go @@ -5,16 +5,15 @@ import ( "fmt" "net/http" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" - "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/stackitcloud/stackit-sdk-go/services/iaas" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" ) @@ -46,10 +45,13 @@ func (d *networkInterfaceDataSource) Configure(ctx context.Context, req datasour } apiClient := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + d.client = apiClient + tflog.Info(ctx, "IaaS client configured") } @@ -134,13 +136,15 @@ func (d *networkInterfaceDataSource) Schema(_ context.Context, _ datasource.Sche } // Read refreshes the Terraform state with the latest data. -func (d *networkInterfaceDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (d *networkInterfaceDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.Config.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() networkId := model.NetworkId.ValueString() networkInterfaceId := model.NetworkInterfaceId.ValueString() @@ -161,6 +165,7 @@ func (d *networkInterfaceDataSource) Read(ctx context.Context, req datasource.Re }, ) resp.State.RemoveResource(ctx) + return } @@ -169,10 +174,13 @@ func (d *networkInterfaceDataSource) Read(ctx context.Context, req datasource.Re core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading network interface", fmt.Sprintf("Processing API payload: %v", err)) return } + diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "Network interface read") } diff --git a/stackit/internal/services/iaas/networkinterface/resource.go b/stackit/internal/services/iaas/networkinterface/resource.go index 1532a9b81..be6183b77 100644 --- a/stackit/internal/services/iaas/networkinterface/resource.go +++ b/stackit/internal/services/iaas/networkinterface/resource.go @@ -7,8 +7,6 @@ import ( "regexp" "strings" - iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" - "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/diag" @@ -24,6 +22,7 @@ import ( "github.com/stackitcloud/stackit-sdk-go/services/iaas" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" ) @@ -63,25 +62,31 @@ type networkInterfaceResource struct { } // ModifyPlan implements resource.ResourceWithModifyPlan. -func (r *networkInterfaceResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform +func (r *networkInterfaceResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { //nolint:gocritic // function signature required by Terraform // skip initial empty configuration to avoid follow-up errors if req.Config.Raw.IsNull() { return } + var configModel Model + resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...) + if resp.Diagnostics.HasError() { return } var planModel Model + resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) + if resp.Diagnostics.HasError() { return } // If allowed_addresses were completly removed from the config this is not recognized by terraform // since this field is optional and computed therefore this plan modifier is needed. utils.CheckListRemoval(ctx, configModel.AllowedAddresses, planModel.AllowedAddresses, path.Root("allowed_addresses"), types.StringType, false, resp) + if resp.Diagnostics.HasError() { return } @@ -89,6 +94,7 @@ func (r *networkInterfaceResource) ModifyPlan(ctx context.Context, req resource. // If security_group_ids were completly removed from the config this is not recognized by terraform // since this field is optional and computed therefore this plan modifier is needed. utils.CheckListRemoval(ctx, configModel.SecurityGroupIds, planModel.SecurityGroupIds, path.Root("security_group_ids"), types.StringType, true, resp) + if resp.Diagnostics.HasError() { return } @@ -107,10 +113,13 @@ func (r *networkInterfaceResource) Configure(ctx context.Context, req resource.C } apiClient := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + r.client = apiClient + tflog.Info(ctx, "iaas client configured") } @@ -248,11 +257,12 @@ func (r *networkInterfaceResource) Schema(_ context.Context, _ resource.SchemaRe } // Create creates the resource and sets the initial Terraform state. -func (r *networkInterfaceResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *networkInterfaceResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { //nolint:gocritic // function signature required by Terraform // Retrieve values from plan var model Model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -289,20 +299,24 @@ func (r *networkInterfaceResource) Create(ctx context.Context, req resource.Crea // Set state to fully populated data diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "Network interface created") } // Read refreshes the Terraform state with the latest data. -func (r *networkInterfaceResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (r *networkInterfaceResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() networkId := model.NetworkId.ValueString() networkInterfaceId := model.NetworkInterfaceId.ValueString() @@ -317,7 +331,9 @@ func (r *networkInterfaceResource) Read(ctx context.Context, req resource.ReadRe resp.State.RemoveResource(ctx) return } + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading network interface", fmt.Sprintf("Calling API: %v", err)) + return } @@ -330,21 +346,25 @@ func (r *networkInterfaceResource) Read(ctx context.Context, req resource.ReadRe // Set refreshed state diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "Network interface read") } // Update updates the resource and sets the updated Terraform state on success. -func (r *networkInterfaceResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *networkInterfaceResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { //nolint:gocritic // function signature required by Terraform // Retrieve values from plan var model Model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() networkId := model.NetworkId.ValueString() networkInterfaceId := model.NetworkInterfaceId.ValueString() @@ -356,6 +376,7 @@ func (r *networkInterfaceResource) Update(ctx context.Context, req resource.Upda var stateModel Model diags = req.State.Get(ctx, &stateModel) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -378,20 +399,24 @@ func (r *networkInterfaceResource) Update(ctx context.Context, req resource.Upda core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating network interface", fmt.Sprintf("Processing API payload: %v", err)) return } + diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "Network interface updated") } // Delete deletes the resource and removes the Terraform state on success. -func (r *networkInterfaceResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform +func (r *networkInterfaceResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { //nolint:gocritic // function signature required by Terraform // Retrieve values from state var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -414,7 +439,7 @@ func (r *networkInterfaceResource) Delete(ctx context.Context, req resource.Dele } // ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: project_id,network_id,network_interface_id +// The expected format of the resource import identifier is: project_id,network_id,network_interface_id. func (r *networkInterfaceResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { idParts := strings.Split(req.ID, core.Separator) @@ -423,6 +448,7 @@ func (r *networkInterfaceResource) ImportState(ctx context.Context, req resource "Error importing network interface", fmt.Sprintf("Expected import identifier with format: [project_id],[network_id],[network_interface_id] Got: %q", req.ID), ) + return } @@ -443,6 +469,7 @@ func mapFields(ctx context.Context, networkInterfaceResp *iaas.NIC, model *Model if networkInterfaceResp == nil { return fmt.Errorf("response input is nil") } + if model == nil { return fmt.Errorf("model input is nil") } @@ -459,7 +486,9 @@ func mapFields(ctx context.Context, networkInterfaceResp *iaas.NIC, model *Model model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), model.NetworkId.ValueString(), networkInterfaceId) respAllowedAddresses := []string{} + var diags diag.Diagnostics + if networkInterfaceResp.AllowedAddresses == nil { // If we send an empty list, the API will send null in the response // We should handle this case and set the value to an empty list @@ -495,6 +524,7 @@ func mapFields(ctx context.Context, networkInterfaceResp *iaas.NIC, model *Model model.SecurityGroupIds = types.ListNull(types.StringType) } else { respSecurityGroups := *networkInterfaceResp.SecurityGroups + modelSecurityGroups, err := utils.ListValuetoStringSlice(model.SecurityGroupIds) if err != nil { return fmt.Errorf("get current network interface security groups from model: %w", err) @@ -540,18 +570,21 @@ func toCreatePayload(ctx context.Context, model *Model) (*iaas.CreateNicPayload, var labelPayload *map[string]interface{} modelSecurityGroups := []string{} - if !(model.SecurityGroupIds.IsNull() || model.SecurityGroupIds.IsUnknown()) { + + if !model.SecurityGroupIds.IsNull() && !model.SecurityGroupIds.IsUnknown() { for _, ns := range model.SecurityGroupIds.Elements() { securityGroupString, ok := ns.(types.String) if !ok { return nil, fmt.Errorf("type assertion failed") } + modelSecurityGroups = append(modelSecurityGroups, securityGroupString.ValueString()) } } allowedAddressesPayload := &[]iaas.AllowedAddressesInner{} - if !(model.AllowedAddresses.IsNull() || model.AllowedAddresses.IsUnknown()) { + + if !model.AllowedAddresses.IsNull() && !model.AllowedAddresses.IsUnknown() { for _, allowedAddressModel := range model.AllowedAddresses.Elements() { allowedAddressString, ok := allowedAddressModel.(types.String) if !ok { @@ -571,6 +604,7 @@ func toCreatePayload(ctx context.Context, model *Model) (*iaas.CreateNicPayload, if err != nil { return nil, fmt.Errorf("mapping labels: %w", err) } + labelPayload = &labelMap } @@ -595,16 +629,19 @@ func toUpdatePayload(ctx context.Context, model *Model, currentLabels types.Map) var labelPayload *map[string]interface{} modelSecurityGroups := []string{} + for _, ns := range model.SecurityGroupIds.Elements() { securityGroupString, ok := ns.(types.String) if !ok { return nil, fmt.Errorf("type assertion failed") } + modelSecurityGroups = append(modelSecurityGroups, securityGroupString.ValueString()) } allowedAddressesPayload := []iaas.AllowedAddressesInner{} // Even if null in the model, we need to send an empty list to the API since it's a PATCH endpoint - if !(model.AllowedAddresses.IsNull() || model.AllowedAddresses.IsUnknown()) { + + if !model.AllowedAddresses.IsNull() && !model.AllowedAddresses.IsUnknown() { for _, allowedAddressModel := range model.AllowedAddresses.Elements() { allowedAddressString, ok := allowedAddressModel.(types.String) if !ok { @@ -622,6 +659,7 @@ func toUpdatePayload(ctx context.Context, model *Model, currentLabels types.Map) if err != nil { return nil, fmt.Errorf("mapping labels: %w", err) } + labelPayload = &labelMap } diff --git a/stackit/internal/services/iaas/networkinterface/resource_test.go b/stackit/internal/services/iaas/networkinterface/resource_test.go index 070c3c28f..93ae23a93 100644 --- a/stackit/internal/services/iaas/networkinterface/resource_test.go +++ b/stackit/internal/services/iaas/networkinterface/resource_test.go @@ -177,9 +177,11 @@ func TestMapFields(t *testing.T) { if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(tt.state, tt.expected) if diff != "" { @@ -253,9 +255,11 @@ func TestToCreatePayload(t *testing.T) { if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(output, tt.expected) if diff != "" { @@ -329,9 +333,11 @@ func TestToUpdatePayload(t *testing.T) { if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(output, tt.expected) if diff != "" { diff --git a/stackit/internal/services/iaas/networkinterfaceattach/resource.go b/stackit/internal/services/iaas/networkinterfaceattach/resource.go index 1517dd80f..4b61ebbde 100644 --- a/stackit/internal/services/iaas/networkinterfaceattach/resource.go +++ b/stackit/internal/services/iaas/networkinterfaceattach/resource.go @@ -6,11 +6,6 @@ import ( "net/http" "strings" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" - "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" @@ -21,7 +16,10 @@ import ( "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/stackitcloud/stackit-sdk-go/core/oapierror" "github.com/stackitcloud/stackit-sdk-go/services/iaas" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" ) @@ -62,10 +60,13 @@ func (r *networkInterfaceAttachResource) Configure(ctx context.Context, req reso } apiClient := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + r.client = apiClient + tflog.Info(ctx, "iaas client configured") } @@ -121,11 +122,12 @@ func (r *networkInterfaceAttachResource) Schema(_ context.Context, _ resource.Sc } // Create creates the resource and sets the initial Terraform state. -func (r *networkInterfaceAttachResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *networkInterfaceAttachResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { //nolint:gocritic // function signature required by Terraform // Retrieve values from plan var model Model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -149,20 +151,24 @@ func (r *networkInterfaceAttachResource) Create(ctx context.Context, req resourc // Set state to fully populated data diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "Network interface attachment created") } // Read refreshes the Terraform state with the latest data. -func (r *networkInterfaceAttachResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (r *networkInterfaceAttachResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) serverId := model.ServerId.ValueString() @@ -177,7 +183,9 @@ func (r *networkInterfaceAttachResource) Read(ctx context.Context, req resource. resp.State.RemoveResource(ctx) return } + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading network interface attachment", fmt.Sprintf("Calling API: %v", err)) + return } @@ -194,10 +202,13 @@ func (r *networkInterfaceAttachResource) Read(ctx context.Context, req resource. // Set refreshed state diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "Network interface attachment read") + return } } @@ -207,16 +218,17 @@ func (r *networkInterfaceAttachResource) Read(ctx context.Context, req resource. } // Update updates the resource and sets the updated Terraform state on success. -func (r *networkInterfaceAttachResource) Update(_ context.Context, _ resource.UpdateRequest, _ *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *networkInterfaceAttachResource) Update(_ context.Context, _ resource.UpdateRequest, _ *resource.UpdateResponse) { //nolint:gocritic // function signature required by Terraform // Update is not supported, all fields require replace } // Delete deletes the resource and removes the Terraform state on success. -func (r *networkInterfaceAttachResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform +func (r *networkInterfaceAttachResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { //nolint:gocritic // function signature required by Terraform // Retrieve values from state var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -239,7 +251,7 @@ func (r *networkInterfaceAttachResource) Delete(ctx context.Context, req resourc } // ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: project_id,server_id +// The expected format of the resource import identifier is: project_id,server_id. func (r *networkInterfaceAttachResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { idParts := strings.Split(req.ID, core.Separator) @@ -248,6 +260,7 @@ func (r *networkInterfaceAttachResource) ImportState(ctx context.Context, req re "Error importing network_interface attachment", fmt.Sprintf("Expected import identifier with format: [project_id],[server_id],[network_interface_id] Got: %q", req.ID), ) + return } diff --git a/stackit/internal/services/iaas/project/datasource.go b/stackit/internal/services/iaas/project/datasource.go index c5be5e8a0..32672270f 100644 --- a/stackit/internal/services/iaas/project/datasource.go +++ b/stackit/internal/services/iaas/project/datasource.go @@ -19,9 +19,7 @@ import ( "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" ) -var ( - _ datasource.DataSourceWithConfigure = &projectDataSource{} -) +var _ datasource.DataSourceWithConfigure = &projectDataSource{} type DatasourceModel struct { Id types.String `tfsdk:"id"` // needed by TF @@ -50,10 +48,13 @@ func (d *projectDataSource) Configure(ctx context.Context, req datasource.Config } apiClient := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + d.client = apiClient + tflog.Info(ctx, "iaas client configured") } @@ -115,13 +116,15 @@ func (d *projectDataSource) Schema(_ context.Context, _ datasource.SchemaRequest } // Read refreshes the Terraform state with the latest data. -func (d *projectDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (d *projectDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model DatasourceModel diags := req.Config.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) @@ -136,6 +139,7 @@ func (d *projectDataSource) Read(ctx context.Context, req datasource.ReadRequest nil, ) resp.State.RemoveResource(ctx) + return } @@ -148,9 +152,11 @@ func (d *projectDataSource) Read(ctx context.Context, req datasource.ReadRequest // Set refreshed state diags = resp.State.Set(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "project read") } @@ -158,6 +164,7 @@ func mapDataSourceFields(projectResp *iaas.Project, model *DatasourceModel) erro if projectResp == nil { return fmt.Errorf("response input is nil") } + if model == nil { return fmt.Errorf("model input is nil") } @@ -175,6 +182,7 @@ func mapDataSourceFields(projectResp *iaas.Project, model *DatasourceModel) erro model.ProjectId = types.StringValue(projectId) var areaId basetypes.StringValue + if projectResp.AreaId != nil { if projectResp.AreaId.String != nil { areaId = types.StringPointerValue(projectResp.AreaId.String) @@ -184,12 +192,14 @@ func mapDataSourceFields(projectResp *iaas.Project, model *DatasourceModel) erro } var createdAt basetypes.StringValue + if projectResp.CreatedAt != nil { createdAtValue := *projectResp.CreatedAt createdAt = types.StringValue(createdAtValue.Format(time.RFC3339)) } var updatedAt basetypes.StringValue + if projectResp.UpdatedAt != nil { updatedAtValue := *projectResp.UpdatedAt updatedAt = types.StringValue(updatedAtValue.Format(time.RFC3339)) @@ -200,5 +210,6 @@ func mapDataSourceFields(projectResp *iaas.Project, model *DatasourceModel) erro model.State = types.StringPointerValue(projectResp.State) model.CreatedAt = createdAt model.UpdatedAt = updatedAt + return nil } diff --git a/stackit/internal/services/iaas/project/datasource_test.go b/stackit/internal/services/iaas/project/datasource_test.go index adbd5ec26..c703da882 100644 --- a/stackit/internal/services/iaas/project/datasource_test.go +++ b/stackit/internal/services/iaas/project/datasource_test.go @@ -21,6 +21,7 @@ func testTimestamp() time.Time { func TestMapDataSourceFields(t *testing.T) { const projectId = "pid" + tests := []struct { description string state *DatasourceModel @@ -106,9 +107,11 @@ func TestMapDataSourceFields(t *testing.T) { if !tt.isValid && err == nil { t.Fatal("should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(tt.expected, tt.state) if diff != "" { diff --git a/stackit/internal/services/iaas/publicip/datasource.go b/stackit/internal/services/iaas/publicip/datasource.go index b9e177cf0..1bf6d76fa 100644 --- a/stackit/internal/services/iaas/publicip/datasource.go +++ b/stackit/internal/services/iaas/publicip/datasource.go @@ -5,16 +5,15 @@ import ( "fmt" "net/http" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" - "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/stackitcloud/stackit-sdk-go/services/iaas" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" ) @@ -46,10 +45,13 @@ func (d *publicIpDataSource) Configure(ctx context.Context, req datasource.Confi } apiClient := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + d.client = apiClient + tflog.Info(ctx, "iaas client configured") } @@ -102,13 +104,15 @@ func (r *publicIpDataSource) Schema(_ context.Context, _ datasource.SchemaReques } // Read refreshes the Terraform state with the latest data. -func (d *publicIpDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (d *publicIpDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.Config.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() publicIpId := model.PublicIpId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) @@ -127,6 +131,7 @@ func (d *publicIpDataSource) Read(ctx context.Context, req datasource.ReadReques }, ) resp.State.RemoveResource(ctx) + return } @@ -135,10 +140,13 @@ func (d *publicIpDataSource) Read(ctx context.Context, req datasource.ReadReques core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading public IP", fmt.Sprintf("Processing API payload: %v", err)) return } + diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "public IP read") } diff --git a/stackit/internal/services/iaas/publicip/resource.go b/stackit/internal/services/iaas/publicip/resource.go index 04e84a663..fdd048c18 100644 --- a/stackit/internal/services/iaas/publicip/resource.go +++ b/stackit/internal/services/iaas/publicip/resource.go @@ -6,10 +6,6 @@ import ( "net/http" "strings" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - - iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" - "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" @@ -22,6 +18,8 @@ import ( "github.com/stackitcloud/stackit-sdk-go/services/iaas" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" ) @@ -64,10 +62,13 @@ func (r *publicIpResource) Configure(ctx context.Context, req resource.Configure } apiClient := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + r.client = apiClient + tflog.Info(ctx, "iaas client configured") } @@ -136,11 +137,12 @@ func (r *publicIpResource) Schema(_ context.Context, _ resource.SchemaRequest, r } // Create creates the resource and sets the initial Terraform state. -func (r *publicIpResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *publicIpResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { //nolint:gocritic // function signature required by Terraform // Retrieve values from plan var model Model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -174,20 +176,24 @@ func (r *publicIpResource) Create(ctx context.Context, req resource.CreateReques // Set state to fully populated data diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "Public IP created") } // Read refreshes the Terraform state with the latest data. -func (r *publicIpResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (r *publicIpResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() publicIpId := model.PublicIpId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) @@ -200,7 +206,9 @@ func (r *publicIpResource) Read(ctx context.Context, req resource.ReadRequest, r resp.State.RemoveResource(ctx) return } + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading public IP", fmt.Sprintf("Calling API: %v", err)) + return } @@ -213,21 +221,25 @@ func (r *publicIpResource) Read(ctx context.Context, req resource.ReadRequest, r // Set refreshed state diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "public IP read") } // Update updates the resource and sets the updated Terraform state on success. -func (r *publicIpResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *publicIpResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { //nolint:gocritic // function signature required by Terraform // Retrieve values from plan var model Model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() publicIpId := model.PublicIpId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) @@ -237,6 +249,7 @@ func (r *publicIpResource) Update(ctx context.Context, req resource.UpdateReques var stateModel Model diags = req.State.Get(ctx, &stateModel) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -259,20 +272,24 @@ func (r *publicIpResource) Update(ctx context.Context, req resource.UpdateReques core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating public IP", fmt.Sprintf("Processing API payload: %v", err)) return } + diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "public IP updated") } // Delete deletes the resource and removes the Terraform state on success. -func (r *publicIpResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform +func (r *publicIpResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { //nolint:gocritic // function signature required by Terraform // Retrieve values from state var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -293,7 +310,7 @@ func (r *publicIpResource) Delete(ctx context.Context, req resource.DeleteReques } // ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: project_id,public_ip_id +// The expected format of the resource import identifier is: project_id,public_ip_id. func (r *publicIpResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { idParts := strings.Split(req.ID, core.Separator) @@ -302,6 +319,7 @@ func (r *publicIpResource) ImportState(ctx context.Context, req resource.ImportS "Error importing public IP", fmt.Sprintf("Expected import identifier with format: [project_id],[public_ip_id] Got: %q", req.ID), ) + return } @@ -319,6 +337,7 @@ func mapFields(ctx context.Context, publicIpResp *iaas.PublicIp, model *Model) e if publicIpResp == nil { return fmt.Errorf("response input is nil") } + if model == nil { return fmt.Errorf("model input is nil") } @@ -341,12 +360,15 @@ func mapFields(ctx context.Context, publicIpResp *iaas.PublicIp, model *Model) e model.PublicIpId = types.StringValue(publicIpId) model.Ip = types.StringPointerValue(publicIpResp.Ip) + if publicIpResp.NetworkInterface != nil { model.NetworkInterfaceId = types.StringPointerValue(publicIpResp.GetNetworkInterface()) } else { model.NetworkInterfaceId = types.StringNull() } + model.Labels = labels + return nil } diff --git a/stackit/internal/services/iaas/publicip/resource_test.go b/stackit/internal/services/iaas/publicip/resource_test.go index 1eda0e8d1..cdbd52275 100644 --- a/stackit/internal/services/iaas/publicip/resource_test.go +++ b/stackit/internal/services/iaas/publicip/resource_test.go @@ -128,9 +128,11 @@ func TestMapFields(t *testing.T) { if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(tt.state, tt.expected) if diff != "" { @@ -190,9 +192,11 @@ func TestToCreatePayload(t *testing.T) { if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(output, tt.expected, cmp.AllowUnexported(iaas.NullableString{})) if diff != "" { @@ -250,9 +254,11 @@ func TestToUpdatePayload(t *testing.T) { if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(output, tt.expected, cmp.AllowUnexported(iaas.NullableString{})) if diff != "" { diff --git a/stackit/internal/services/iaas/publicipassociate/resource.go b/stackit/internal/services/iaas/publicipassociate/resource.go index d19c5de4b..efcfe51e3 100644 --- a/stackit/internal/services/iaas/publicipassociate/resource.go +++ b/stackit/internal/services/iaas/publicipassociate/resource.go @@ -6,10 +6,6 @@ import ( "net/http" "strings" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - - iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" - "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" @@ -22,6 +18,8 @@ import ( "github.com/stackitcloud/stackit-sdk-go/services/iaas" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" ) @@ -63,6 +61,7 @@ func (r *publicIpAssociateResource) Configure(ctx context.Context, req resource. } apiClient := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } @@ -71,6 +70,7 @@ func (r *publicIpAssociateResource) Configure(ctx context.Context, req resource. "Using both resources together for the same public IP or network interface WILL lead to conflicts, as they both have control of the public IP and network interface association.") r.client = apiClient + tflog.Info(ctx, "iaas client configured") } @@ -142,14 +142,16 @@ func (r *publicIpAssociateResource) Schema(_ context.Context, _ resource.SchemaR } // Create creates the resource and sets the initial Terraform state. -func (r *publicIpAssociateResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *publicIpAssociateResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { //nolint:gocritic // function signature required by Terraform // Retrieve values from plan var model Model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() publicIpId := model.PublicIpId.ValueString() networkInterfaceId := model.NetworkInterfaceId.ValueString() @@ -175,22 +177,27 @@ func (r *publicIpAssociateResource) Create(ctx context.Context, req resource.Cre core.LogAndAddError(ctx, &resp.Diagnostics, "Error associating public IP to network interface", fmt.Sprintf("Processing API payload: %v", err)) return } + diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "public IP associated to network interface") } // Read refreshes the Terraform state with the latest data. -func (r *publicIpAssociateResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (r *publicIpAssociateResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() publicIpId := model.PublicIpId.ValueString() networkInterfaceId := model.NetworkInterfaceId.ValueString() @@ -205,7 +212,9 @@ func (r *publicIpAssociateResource) Read(ctx context.Context, req resource.ReadR resp.State.RemoveResource(ctx) return } + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading public IP association", fmt.Sprintf("Calling API: %v", err)) + return } @@ -218,23 +227,26 @@ func (r *publicIpAssociateResource) Read(ctx context.Context, req resource.ReadR // Set refreshed state diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "public IP associate read") } // Update updates the resource and sets the updated Terraform state on success. -func (r *publicIpAssociateResource) Update(_ context.Context, _ resource.UpdateRequest, _ *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *publicIpAssociateResource) Update(_ context.Context, _ resource.UpdateRequest, _ *resource.UpdateResponse) { //nolint:gocritic // function signature required by Terraform // Update is not supported, all fields require replace } // Delete deletes the resource and removes the Terraform state on success. -func (r *publicIpAssociateResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform +func (r *publicIpAssociateResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { //nolint:gocritic // function signature required by Terraform // Retrieve values from state var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -260,7 +272,7 @@ func (r *publicIpAssociateResource) Delete(ctx context.Context, req resource.Del } // ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: project_id,public_ip_id +// The expected format of the resource import identifier is: project_id,public_ip_id. func (r *publicIpAssociateResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { idParts := strings.Split(req.ID, core.Separator) @@ -269,6 +281,7 @@ func (r *publicIpAssociateResource) ImportState(ctx context.Context, req resourc "Error importing public IP associate", fmt.Sprintf("Expected import identifier with format: [project_id],[public_ip_id],[network_interface_id] Got: %q", req.ID), ) + return } @@ -289,6 +302,7 @@ func mapFields(publicIpResp *iaas.PublicIp, model *Model) error { if publicIpResp == nil { return fmt.Errorf("response input is nil") } + if model == nil { return fmt.Errorf("model input is nil") } diff --git a/stackit/internal/services/iaas/publicipassociate/resource_test.go b/stackit/internal/services/iaas/publicipassociate/resource_test.go index a15cf34bc..25ecc3469 100644 --- a/stackit/internal/services/iaas/publicipassociate/resource_test.go +++ b/stackit/internal/services/iaas/publicipassociate/resource_test.go @@ -81,9 +81,11 @@ func TestMapFields(t *testing.T) { if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(tt.state, tt.expected) if diff != "" { @@ -118,9 +120,11 @@ func TestToCreatePayload(t *testing.T) { if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(output, tt.expected, cmp.AllowUnexported(iaas.NullableString{})) if diff != "" { diff --git a/stackit/internal/services/iaas/publicipranges/datasource.go b/stackit/internal/services/iaas/publicipranges/datasource.go index 3daebaaa5..1f6b5f25f 100644 --- a/stackit/internal/services/iaas/publicipranges/datasource.go +++ b/stackit/internal/services/iaas/publicipranges/datasource.go @@ -6,14 +6,6 @@ import ( "net/http" "sort" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" - - "github.com/stackitcloud/stackit-sdk-go/services/iaas" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/datasource" @@ -21,6 +13,12 @@ import ( "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/stackitcloud/stackit-sdk-go/services/iaas" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" ) // Ensure the implementation satisfies the expected interfaces. @@ -60,10 +58,13 @@ func (d *publicIpRangesDataSource) Configure(ctx context.Context, req datasource } apiClient := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + d.client = apiClient + tflog.Info(ctx, "iaas client configured") } @@ -108,13 +109,15 @@ func (d *publicIpRangesDataSource) Schema(_ context.Context, _ datasource.Schema } // Read refreshes the Terraform state with the latest data. -func (d *publicIpRangesDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (d *publicIpRangesDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.Config.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + publicIpRangeResp, err := d.client.ListPublicIPRangesExecute(ctx) if err != nil { utils.LogError( @@ -128,6 +131,7 @@ func (d *publicIpRangesDataSource) Read(ctx context.Context, req datasource.Read }, ) resp.State.RemoveResource(ctx) + return } @@ -137,11 +141,14 @@ func (d *publicIpRangesDataSource) Read(ctx context.Context, req datasource.Read core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading public IP ranges", fmt.Sprintf("Processing API payload: %v", err)) return } + diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "read public IP ranges") } @@ -149,6 +156,7 @@ func mapFields(ctx context.Context, publicIpRangeResp *iaas.PublicNetworkListRes if publicIpRangeResp == nil { return fmt.Errorf("response input is nil") } + if model == nil { return fmt.Errorf("model input is nil") } @@ -157,21 +165,25 @@ func mapFields(ctx context.Context, publicIpRangeResp *iaas.PublicNetworkListRes if err != nil { return fmt.Errorf("error mapping public IP ranges: %w", err) } + return nil } -// mapPublicIpRanges map the response publicIpRanges to the model +// mapPublicIpRanges map the response publicIpRanges to the model. func mapPublicIpRanges(ctx context.Context, publicIpRanges *[]iaas.PublicNetwork, model *Model) error { if publicIpRanges == nil { return fmt.Errorf("publicIpRanges input is nil") } + if len(*publicIpRanges) == 0 { model.PublicIpRanges = types.ListNull(types.ObjectType{AttrTypes: publicIpRangesTypes}) model.CidrList = types.ListNull(types.StringType) + return nil } var apiIpRanges []string + for _, ipRange := range *publicIpRanges { if ipRange.Cidr != nil && *ipRange.Cidr != "" { apiIpRanges = append(apiIpRanges, *ipRange.Cidr) @@ -184,14 +196,17 @@ func mapPublicIpRanges(ctx context.Context, publicIpRanges *[]iaas.PublicNetwork model.Id = utils.BuildInternalTerraformId(apiIpRanges...) var ipRangesList []attr.Value + for _, cidr := range apiIpRanges { ipRangeValues := map[string]attr.Value{ "cidr": types.StringValue(cidr), } + ipRangeObject, diag := types.ObjectValue(publicIpRangesTypes, ipRangeValues) if diag.HasError() { return core.DiagsToError(diag) } + ipRangesList = append(ipRangesList, ipRangeObject) } @@ -209,6 +224,7 @@ func mapPublicIpRanges(ctx context.Context, publicIpRanges *[]iaas.PublicNetwork if diags.HasError() { return core.DiagsToError(diags) } + model.CidrList = cidrListTF return nil diff --git a/stackit/internal/services/iaas/publicipranges/datasource_test.go b/stackit/internal/services/iaas/publicipranges/datasource_test.go index 535df5f74..fb669d70d 100644 --- a/stackit/internal/services/iaas/publicipranges/datasource_test.go +++ b/stackit/internal/services/iaas/publicipranges/datasource_test.go @@ -75,6 +75,7 @@ func TestMapPublicIpRanges(t *testing.T) { }) ipRangesVal, _ := types.ListValue(types.ObjectType{AttrTypes: publicIpRangesTypes}, []attr.Value{ipRange}) cidrListVal, _ := types.ListValueFrom(ctx, types.StringType, cidrs) + return Model{ PublicIpRanges: ipRangesVal, CidrList: cidrListVal, @@ -94,6 +95,7 @@ func TestMapPublicIpRanges(t *testing.T) { if err == nil { t.Fatalf("Expected error but got nil") } + return } else if err != nil { t.Fatalf("Unexpected error: %v", err) diff --git a/stackit/internal/services/iaas/securitygroup/datasource.go b/stackit/internal/services/iaas/securitygroup/datasource.go index 171f5aba6..503adf121 100644 --- a/stackit/internal/services/iaas/securitygroup/datasource.go +++ b/stackit/internal/services/iaas/securitygroup/datasource.go @@ -5,16 +5,15 @@ import ( "fmt" "net/http" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" - "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/stackitcloud/stackit-sdk-go/services/iaas" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" ) @@ -46,10 +45,13 @@ func (d *securityGroupDataSource) Configure(ctx context.Context, req datasource. } apiClient := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + d.client = apiClient + tflog.Info(ctx, "iaas client configured") } @@ -102,13 +104,15 @@ func (r *securityGroupDataSource) Schema(_ context.Context, _ datasource.SchemaR } // Read refreshes the Terraform state with the latest data. -func (d *securityGroupDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (d *securityGroupDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.Config.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() securityGroupId := model.SecurityGroupId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) @@ -127,6 +131,7 @@ func (d *securityGroupDataSource) Read(ctx context.Context, req datasource.ReadR }, ) resp.State.RemoveResource(ctx) + return } @@ -135,10 +140,13 @@ func (d *securityGroupDataSource) Read(ctx context.Context, req datasource.ReadR core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading security group", fmt.Sprintf("Processing API payload: %v", err)) return } + diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "security group read") } diff --git a/stackit/internal/services/iaas/securitygroup/resource.go b/stackit/internal/services/iaas/securitygroup/resource.go index 3663c25b3..bccb18e85 100644 --- a/stackit/internal/services/iaas/securitygroup/resource.go +++ b/stackit/internal/services/iaas/securitygroup/resource.go @@ -7,10 +7,6 @@ import ( "regexp" "strings" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - - iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" - "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" @@ -25,6 +21,8 @@ import ( "github.com/stackitcloud/stackit-sdk-go/services/iaas" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" ) @@ -68,10 +66,13 @@ func (r *securityGroupResource) Configure(ctx context.Context, req resource.Conf } apiClient := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + r.client = apiClient + tflog.Info(ctx, "iaas client configured") } @@ -153,11 +154,12 @@ func (r *securityGroupResource) Schema(_ context.Context, _ resource.SchemaReque } // Create creates the resource and sets the initial Terraform state. -func (r *securityGroupResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *securityGroupResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { //nolint:gocritic // function signature required by Terraform // Retrieve values from plan var model Model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -193,20 +195,24 @@ func (r *securityGroupResource) Create(ctx context.Context, req resource.CreateR // Set state to fully populated data diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "Security group created") } // Read refreshes the Terraform state with the latest data. -func (r *securityGroupResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (r *securityGroupResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() securityGroupId := model.SecurityGroupId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) @@ -219,7 +225,9 @@ func (r *securityGroupResource) Read(ctx context.Context, req resource.ReadReque resp.State.RemoveResource(ctx) return } + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading security group", fmt.Sprintf("Calling API: %v", err)) + return } @@ -232,21 +240,25 @@ func (r *securityGroupResource) Read(ctx context.Context, req resource.ReadReque // Set refreshed state diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "security group read") } // Update updates the resource and sets the updated Terraform state on success. -func (r *securityGroupResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *securityGroupResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { //nolint:gocritic // function signature required by Terraform // Retrieve values from plan var model Model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() securityGroupId := model.SecurityGroupId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) @@ -256,6 +268,7 @@ func (r *securityGroupResource) Update(ctx context.Context, req resource.UpdateR var stateModel Model diags = req.State.Get(ctx, &stateModel) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -278,20 +291,24 @@ func (r *securityGroupResource) Update(ctx context.Context, req resource.UpdateR core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating security group", fmt.Sprintf("Processing API payload: %v", err)) return } + diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "security group updated") } // Delete deletes the resource and removes the Terraform state on success. -func (r *securityGroupResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform +func (r *securityGroupResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { //nolint:gocritic // function signature required by Terraform // Retrieve values from state var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -312,7 +329,7 @@ func (r *securityGroupResource) Delete(ctx context.Context, req resource.DeleteR } // ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: project_id,security_group_id +// The expected format of the resource import identifier is: project_id,security_group_id. func (r *securityGroupResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { idParts := strings.Split(req.ID, core.Separator) @@ -321,6 +338,7 @@ func (r *securityGroupResource) ImportState(ctx context.Context, req resource.Im "Error importing security group", fmt.Sprintf("Expected import identifier with format: [project_id],[security_group_id] Got: %q", req.ID), ) + return } @@ -338,6 +356,7 @@ func mapFields(ctx context.Context, securityGroupResp *iaas.SecurityGroup, model if securityGroupResp == nil { return fmt.Errorf("response input is nil") } + if model == nil { return fmt.Errorf("model input is nil") } diff --git a/stackit/internal/services/iaas/securitygroup/resource_test.go b/stackit/internal/services/iaas/securitygroup/resource_test.go index 2b4c12367..79f1f6425 100644 --- a/stackit/internal/services/iaas/securitygroup/resource_test.go +++ b/stackit/internal/services/iaas/securitygroup/resource_test.go @@ -112,9 +112,11 @@ func TestMapFields(t *testing.T) { if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(tt.state, tt.expected) if diff != "" { @@ -159,9 +161,11 @@ func TestToCreatePayload(t *testing.T) { if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(output, tt.expected) if diff != "" { @@ -204,9 +208,11 @@ func TestToUpdatePayload(t *testing.T) { if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(output, tt.expected) if diff != "" { diff --git a/stackit/internal/services/iaas/securitygrouprule/datasource.go b/stackit/internal/services/iaas/securitygrouprule/datasource.go index 77d385213..c4c48e3c1 100644 --- a/stackit/internal/services/iaas/securitygrouprule/datasource.go +++ b/stackit/internal/services/iaas/securitygrouprule/datasource.go @@ -5,15 +5,14 @@ import ( "fmt" "net/http" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" - "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/stackitcloud/stackit-sdk-go/services/iaas" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" ) @@ -45,10 +44,13 @@ func (d *securityGroupRuleDataSource) Configure(ctx context.Context, req datasou } apiClient := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + d.client = apiClient + tflog.Info(ctx, "iaas client configured") } @@ -156,13 +158,15 @@ func (r *securityGroupRuleDataSource) Schema(_ context.Context, _ datasource.Sch } // Read refreshes the Terraform state with the latest data. -func (d *securityGroupRuleDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (d *securityGroupRuleDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.Config.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() securityGroupId := model.SecurityGroupId.ValueString() securityGroupRuleId := model.SecurityGroupRuleId.ValueString() @@ -183,6 +187,7 @@ func (d *securityGroupRuleDataSource) Read(ctx context.Context, req datasource.R }, ) resp.State.RemoveResource(ctx) + return } @@ -191,10 +196,13 @@ func (d *securityGroupRuleDataSource) Read(ctx context.Context, req datasource.R core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading security group rule", fmt.Sprintf("Processing API payload: %v", err)) return } + diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "security group rule read") } diff --git a/stackit/internal/services/iaas/securitygrouprule/planmodifier.go b/stackit/internal/services/iaas/securitygrouprule/planmodifier.go index 23d879f96..5d92ae3c6 100644 --- a/stackit/internal/services/iaas/securitygrouprule/planmodifier.go +++ b/stackit/internal/services/iaas/securitygrouprule/planmodifier.go @@ -17,7 +17,7 @@ import ( // and Computed attributes to an unknown value "(known after apply)" on update. // To prevent always showing "(known after apply)" on update for an attribute, e.g. port_range, which never changes in case the protocol is a specific one, // we set the value to null. -// Examples: port_range is only computed if protocol is not icmp and icmp_parameters is only computed if protocol is icmp +// Examples: port_range is only computed if protocol is not icmp and icmp_parameters is only computed if protocol is icmp. func UseNullForUnknownBasedOnProtocolModifier() planmodifier.Object { return useNullForUnknownBasedOnProtocolModifier{} } @@ -35,7 +35,7 @@ func (m useNullForUnknownBasedOnProtocolModifier) MarkdownDescription(_ context. } // PlanModifyBool implements the plan modification logic. -func (m useNullForUnknownBasedOnProtocolModifier) PlanModifyObject(ctx context.Context, req planmodifier.ObjectRequest, resp *planmodifier.ObjectResponse) { // nolint:gocritic // function signature required by Terraform +func (m useNullForUnknownBasedOnProtocolModifier) PlanModifyObject(ctx context.Context, req planmodifier.ObjectRequest, resp *planmodifier.ObjectResponse) { //nolint:gocritic // function signature required by Terraform // Check if the resource is being created. if req.State.Raw.IsNull() { return @@ -53,7 +53,9 @@ func (m useNullForUnknownBasedOnProtocolModifier) PlanModifyObject(ctx context.C // If there is an unknown configuration value, check if the value of protocol.name attribute corresponds to an icmp protocol. If it does, set the attribute value to null var model Model + resp.Diagnostics.Append(req.Config.Get(ctx, &model)...) + if resp.Diagnostics.HasError() { return } @@ -66,6 +68,7 @@ func (m useNullForUnknownBasedOnProtocolModifier) PlanModifyObject(ctx context.C protocol := &protocolModel{} diags := model.Protocol.As(ctx, protocol, basetypes.ObjectAsOptions{}) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } diff --git a/stackit/internal/services/iaas/securitygrouprule/resource.go b/stackit/internal/services/iaas/securitygrouprule/resource.go index 735e88cc1..75d82b55d 100644 --- a/stackit/internal/services/iaas/securitygrouprule/resource.go +++ b/stackit/internal/services/iaas/securitygrouprule/resource.go @@ -8,8 +8,6 @@ import ( "slices" "strings" - iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" - "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/attr" @@ -28,6 +26,7 @@ import ( "github.com/stackitcloud/stackit-sdk-go/services/iaas" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" ) @@ -64,7 +63,7 @@ type icmpParametersModel struct { Type types.Int64 `tfsdk:"type"` } -// Types corresponding to icmpParameters +// Types corresponding to icmpParameters. var icmpParametersTypes = map[string]attr.Type{ "code": basetypes.Int64Type{}, "type": basetypes.Int64Type{}, @@ -75,7 +74,7 @@ type portRangeModel struct { Min types.Int64 `tfsdk:"min"` } -// Types corresponding to portRange +// Types corresponding to portRange. var portRangeTypes = map[string]attr.Type{ "max": basetypes.Int64Type{}, "min": basetypes.Int64Type{}, @@ -86,7 +85,7 @@ type protocolModel struct { Number types.Int64 `tfsdk:"number"` } -// Types corresponding to protocol +// Types corresponding to protocol. var protocolTypes = map[string]attr.Type{ "name": basetypes.StringType{}, "number": basetypes.Int64Type{}, @@ -115,10 +114,13 @@ func (r *securityGroupRuleResource) Configure(ctx context.Context, req resource. } apiClient := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + r.client = apiClient + tflog.Info(ctx, "iaas client configured") } @@ -139,6 +141,7 @@ func (r securityGroupRuleResource) ValidateConfig(ctx context.Context, req resou protocol := &protocolModel{} diags := model.Protocol.As(ctx, protocol, basetypes.ObjectAsOptions{}) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -150,7 +153,7 @@ func (r securityGroupRuleResource) ValidateConfig(ctx context.Context, req resou } if slices.Contains(icmpProtocols, *protocolName) { - if !(model.PortRange.IsNull() || model.PortRange.IsUnknown()) { + if !model.PortRange.IsNull() && !model.PortRange.IsUnknown() { resp.Diagnostics.AddAttributeError( path.Root("port_range"), "Conflicting attribute configuration", @@ -158,7 +161,7 @@ func (r securityGroupRuleResource) ValidateConfig(ctx context.Context, req resou ) } } else { - if !(model.IcmpParameters.IsNull() || model.IcmpParameters.IsUnknown()) { + if !model.IcmpParameters.IsNull() && !model.IcmpParameters.IsUnknown() { resp.Diagnostics.AddAttributeError( path.Root("icmp_parameters"), "Conflicting attribute configuration", @@ -380,11 +383,12 @@ func (r *securityGroupRuleResource) Schema(_ context.Context, _ resource.SchemaR } // Create creates the resource and sets the initial Terraform state. -func (r *securityGroupRuleResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *securityGroupRuleResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { //nolint:gocritic // function signature required by Terraform // Retrieve values from plan var model Model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -395,30 +399,33 @@ func (r *securityGroupRuleResource) Create(ctx context.Context, req resource.Cre ctx = tflog.SetField(ctx, "security_group_id", securityGroupId) var icmpParameters *icmpParametersModel - if !(model.IcmpParameters.IsNull() || model.IcmpParameters.IsUnknown()) { + if !model.IcmpParameters.IsNull() && !model.IcmpParameters.IsUnknown() { icmpParameters = &icmpParametersModel{} diags = model.IcmpParameters.As(ctx, icmpParameters, basetypes.ObjectAsOptions{}) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } } var portRange *portRangeModel - if !(model.PortRange.IsNull() || model.PortRange.IsUnknown()) { + if !model.PortRange.IsNull() && !model.PortRange.IsUnknown() { portRange = &portRangeModel{} diags = model.PortRange.As(ctx, portRange, basetypes.ObjectAsOptions{}) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } } var protocol *protocolModel - if !(model.Protocol.IsNull() || model.Protocol.IsUnknown()) { + if !model.Protocol.IsNull() && !model.Protocol.IsUnknown() { protocol = &protocolModel{} diags = model.Protocol.As(ctx, protocol, basetypes.ObjectAsOptions{}) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -449,20 +456,24 @@ func (r *securityGroupRuleResource) Create(ctx context.Context, req resource.Cre // Set state to fully populated data diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "Security group rule created") } // Read refreshes the Terraform state with the latest data. -func (r *securityGroupRuleResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (r *securityGroupRuleResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() securityGroupId := model.SecurityGroupId.ValueString() securityGroupRuleId := model.SecurityGroupRuleId.ValueString() @@ -477,7 +488,9 @@ func (r *securityGroupRuleResource) Read(ctx context.Context, req resource.ReadR resp.State.RemoveResource(ctx) return } + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading security group rule", fmt.Sprintf("Calling API: %v", err)) + return } @@ -490,24 +503,27 @@ func (r *securityGroupRuleResource) Read(ctx context.Context, req resource.ReadR // Set refreshed state diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "security group rule read") } // Update updates the resource and sets the updated Terraform state on success. -func (r *securityGroupRuleResource) Update(ctx context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *securityGroupRuleResource) Update(ctx context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { //nolint:gocritic // function signature required by Terraform // Update shouldn't be called core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating security group rule", "Security group rule can't be updated") } // Delete deletes the resource and removes the Terraform state on success. -func (r *securityGroupRuleResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform +func (r *securityGroupRuleResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { //nolint:gocritic // function signature required by Terraform // Retrieve values from state var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -530,7 +546,7 @@ func (r *securityGroupRuleResource) Delete(ctx context.Context, req resource.Del } // ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: project_id,security_group_id, security_group_rule_id +// The expected format of the resource import identifier is: project_id,security_group_id, security_group_rule_id. func (r *securityGroupRuleResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { idParts := strings.Split(req.ID, core.Separator) @@ -539,6 +555,7 @@ func (r *securityGroupRuleResource) ImportState(ctx context.Context, req resourc "Error importing security group rule", fmt.Sprintf("Expected import identifier with format: [project_id],[security_group_id],[security_group_rule_id] Got: %q", req.ID), ) + return } @@ -559,6 +576,7 @@ func mapFields(securityGroupRuleResp *iaas.SecurityGroupRule, model *Model) erro if securityGroupRuleResp == nil { return fmt.Errorf("response input is nil") } + if model == nil { return fmt.Errorf("model input is nil") } @@ -584,10 +602,12 @@ func mapFields(securityGroupRuleResp *iaas.SecurityGroupRule, model *Model) erro if err != nil { return fmt.Errorf("map icmp_parameters: %w", err) } + err = mapPortRange(securityGroupRuleResp, model) if err != nil { return fmt.Errorf("map port_range: %w", err) } + err = mapProtocol(securityGroupRuleResp, model) if err != nil { return fmt.Errorf("map protocol: %w", err) @@ -611,7 +631,9 @@ func mapIcmpParameters(securityGroupRuleResp *iaas.SecurityGroupRule, m *Model) if diags.HasError() { return fmt.Errorf("create icmpParameters object: %w", core.DiagsToError(diags)) } + m.IcmpParameters = icmpParametersObject + return nil } @@ -641,7 +663,9 @@ func mapPortRange(securityGroupRuleResp *iaas.SecurityGroupRule, m *Model) error if diags.HasError() { return fmt.Errorf("create portRange object: %w", core.DiagsToError(diags)) } + m.PortRange = portRangeObject + return nil } @@ -665,11 +689,14 @@ func mapProtocol(securityGroupRuleResp *iaas.SecurityGroupRule, m *Model) error "name": protocolNameValue, "number": protocolNumberValue, } + protocolObject, diags := types.ObjectValue(protocolTypes, protocolValues) if diags.HasError() { return fmt.Errorf("create protocol object: %w", core.DiagsToError(diags)) } + m.Protocol = protocolObject + return nil } @@ -709,6 +736,7 @@ func toIcmpParametersPayload(icmpParameters *icmpParametersModel) (*iaas.ICMPPar if icmpParameters == nil { return nil, nil } + payloadParams := &iaas.ICMPParameters{} payloadParams.Code = conversion.Int64ValueToPointer(icmpParameters.Code) @@ -721,6 +749,7 @@ func toPortRangePayload(portRange *portRangeModel) (*iaas.PortRange, error) { if portRange == nil { return nil, nil } + payloadPortRange := &iaas.PortRange{} payloadPortRange.Max = conversion.Int64ValueToPointer(portRange.Max) @@ -733,6 +762,7 @@ func toProtocolPayload(protocol *protocolModel) (*iaas.CreateProtocol, error) { if protocol == nil { return nil, nil } + payloadProtocol := &iaas.CreateProtocol{} payloadProtocol.String = conversion.StringValueToPointer(protocol.Name) diff --git a/stackit/internal/services/iaas/securitygrouprule/resource_test.go b/stackit/internal/services/iaas/securitygrouprule/resource_test.go index ef6dc0069..fc6c3e3f6 100644 --- a/stackit/internal/services/iaas/securitygrouprule/resource_test.go +++ b/stackit/internal/services/iaas/securitygrouprule/resource_test.go @@ -205,9 +205,11 @@ func TestMapFields(t *testing.T) { if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(tt.state, tt.expected) if diff != "" { @@ -259,27 +261,33 @@ func TestToCreatePayload(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { var icmpParameters *icmpParametersModel + var portRange *portRangeModel + var protocol *protocolModel + if tt.input != nil { - if !(tt.input.IcmpParameters.IsNull() || tt.input.IcmpParameters.IsUnknown()) { + if !tt.input.IcmpParameters.IsNull() && !tt.input.IcmpParameters.IsUnknown() { icmpParameters = &icmpParametersModel{} + diags := tt.input.IcmpParameters.As(context.Background(), icmpParameters, basetypes.ObjectAsOptions{}) if diags.HasError() { t.Fatalf("Error converting icmp parameters: %v", diags.Errors()) } } - if !(tt.input.PortRange.IsNull() || tt.input.PortRange.IsUnknown()) { + if !tt.input.PortRange.IsNull() && !tt.input.PortRange.IsUnknown() { portRange = &portRangeModel{} + diags := tt.input.PortRange.As(context.Background(), portRange, basetypes.ObjectAsOptions{}) if diags.HasError() { t.Fatalf("Error converting port range: %v", diags.Errors()) } } - if !(tt.input.Protocol.IsNull() || tt.input.Protocol.IsUnknown()) { + if !tt.input.Protocol.IsNull() && !tt.input.Protocol.IsUnknown() { protocol = &protocolModel{} + diags := tt.input.Protocol.As(context.Background(), protocol, basetypes.ObjectAsOptions{}) if diags.HasError() { t.Fatalf("Error converting protocol: %v", diags.Errors()) @@ -291,9 +299,11 @@ func TestToCreatePayload(t *testing.T) { if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(output, tt.expected) if diff != "" { diff --git a/stackit/internal/services/iaas/server/datasource.go b/stackit/internal/services/iaas/server/datasource.go index ce2159613..ea746eba8 100644 --- a/stackit/internal/services/iaas/server/datasource.go +++ b/stackit/internal/services/iaas/server/datasource.go @@ -6,9 +6,6 @@ import ( "net/http" "time" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" - "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" @@ -17,7 +14,9 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types/basetypes" "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/stackitcloud/stackit-sdk-go/services/iaas" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" ) @@ -73,10 +72,13 @@ func (d *serverDataSource) Configure(ctx context.Context, req datasource.Configu } apiClient := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + d.client = apiClient + tflog.Info(ctx, "iaas client configured") } @@ -176,13 +178,15 @@ func (r *serverDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, } // // Read refreshes the Terraform state with the latest data. -func (r *serverDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (r *serverDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model DataSourceModel diags := req.Config.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() serverId := model.ServerId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) @@ -190,6 +194,7 @@ func (r *serverDataSource) Read(ctx context.Context, req datasource.ReadRequest, serverReq := r.client.GetServer(ctx, projectId, serverId) serverReq = serverReq.Details(true) + serverResp, err := serverReq.Execute() if err != nil { utils.LogError( @@ -203,6 +208,7 @@ func (r *serverDataSource) Read(ctx context.Context, req datasource.ReadRequest, }, ) resp.State.RemoveResource(ctx) + return } @@ -215,9 +221,11 @@ func (r *serverDataSource) Read(ctx context.Context, req datasource.ReadRequest, // Set refreshed state diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "server read") } @@ -225,6 +233,7 @@ func mapDataSourceFields(ctx context.Context, serverResp *iaas.Server, model *Da if serverResp == nil { return fmt.Errorf("response input is nil") } + if model == nil { return fmt.Errorf("model input is nil") } @@ -246,25 +255,32 @@ func mapDataSourceFields(ctx context.Context, serverResp *iaas.Server, model *Da } var createdAt basetypes.StringValue + if serverResp.CreatedAt != nil { createdAtValue := *serverResp.CreatedAt createdAt = types.StringValue(createdAtValue.Format(time.RFC3339)) } + var updatedAt basetypes.StringValue + if serverResp.UpdatedAt != nil { updatedAtValue := *serverResp.UpdatedAt updatedAt = types.StringValue(updatedAtValue.Format(time.RFC3339)) } + var launchedAt basetypes.StringValue + if serverResp.LaunchedAt != nil { launchedAtValue := *serverResp.LaunchedAt launchedAt = types.StringValue(launchedAtValue.Format(time.RFC3339)) } + if serverResp.Nics != nil { var respNics []string for _, nic := range *serverResp.Nics { respNics = append(respNics, *nic.NicId) } + nicTF, diags := types.ListValueFrom(ctx, types.StringType, respNics) if diags.HasError() { return fmt.Errorf("failed to map networkInterfaces: %w", core.DiagsToError(diags)) @@ -283,6 +299,7 @@ func mapDataSourceFields(ctx context.Context, serverResp *iaas.Server, model *Da if diags.HasError() { return fmt.Errorf("failed to map bootVolume: %w", core.DiagsToError(diags)) } + model.BootVolume = bootVolume } else { model.BootVolume = types.ObjectNull(bootVolumeDataTypes) diff --git a/stackit/internal/services/iaas/server/datasource_test.go b/stackit/internal/services/iaas/server/datasource_test.go index bb709d15c..2fb336178 100644 --- a/stackit/internal/services/iaas/server/datasource_test.go +++ b/stackit/internal/services/iaas/server/datasource_test.go @@ -148,9 +148,11 @@ func TestMapDataSourceFields(t *testing.T) { if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(tt.state, tt.expected) if diff != "" { diff --git a/stackit/internal/services/iaas/server/resource.go b/stackit/internal/services/iaas/server/resource.go index 5701d9ca3..821409181 100644 --- a/stackit/internal/services/iaas/server/resource.go +++ b/stackit/internal/services/iaas/server/resource.go @@ -9,8 +9,6 @@ import ( "strings" "time" - iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" - "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" "github.com/hashicorp/terraform-plugin-framework-validators/resourcevalidator" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" @@ -33,6 +31,7 @@ import ( "github.com/stackitcloud/stackit-sdk-go/services/iaas/wait" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" ) @@ -73,7 +72,7 @@ type Model struct { DesiredStatus types.String `tfsdk:"desired_status"` } -// Struct corresponding to Model.BootVolume +// Struct corresponding to Model.BootVolume. type bootVolumeModel struct { Id types.String `tfsdk:"id"` PerformanceClass types.String `tfsdk:"performance_class"` @@ -83,7 +82,7 @@ type bootVolumeModel struct { DeleteOnTermination types.Bool `tfsdk:"delete_on_termination"` } -// Types corresponding to bootVolumeModel +// Types corresponding to bootVolumeModel. var bootVolumeTypes = map[string]attr.Type{ "performance_class": basetypes.StringType{}, "size": basetypes.Int64Type{}, @@ -110,14 +109,16 @@ func (r *serverResource) Metadata(_ context.Context, req resource.MetadataReques func (r serverResource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) { var model Model + resp.Diagnostics.Append(req.Config.Get(ctx, &model)...) + if resp.Diagnostics.HasError() { return } // convert boot volume model - var bootVolume = &bootVolumeModel{} - if !(model.BootVolume.IsNull() || model.BootVolume.IsUnknown()) { + bootVolume := &bootVolumeModel{} + if !model.BootVolume.IsNull() && !model.BootVolume.IsUnknown() { diags := model.BootVolume.As(ctx, bootVolume, basetypes.ObjectAsOptions{}) if diags.HasError() { return @@ -131,7 +132,7 @@ func (r serverResource) ValidateConfig(ctx context.Context, req resource.Validat } } -// ConfigValidators validates the resource configuration +// ConfigValidators validates the resource configuration. func (r *serverResource) ConfigValidators(_ context.Context) []resource.ConfigValidator { return []resource.ConfigValidator{ resourcevalidator.AtLeastOneOf( @@ -153,10 +154,13 @@ func (r *serverResource) Configure(ctx context.Context, req resource.ConfigureRe } apiClient := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + r.client = apiClient + tflog.Info(ctx, "iaas client configured") } @@ -382,8 +386,7 @@ func (r *serverResource) Schema(_ context.Context, _ resource.SchemaRequest, res var _ planmodifier.String = desiredStateModifier{} -type desiredStateModifier struct { -} +type desiredStateModifier struct{} // Description implements planmodifier.String. func (d desiredStateModifier) Description(context.Context) string { @@ -402,12 +405,15 @@ func (d desiredStateModifier) PlanModifyString(ctx context.Context, req planmodi planState types.String currentState types.String ) + resp.Diagnostics.Append(req.Plan.GetAttribute(ctx, path.Root("desired_status"), &planState)...) + if resp.Diagnostics.HasError() { return } resp.Diagnostics.Append(req.State.GetAttribute(ctx, path.Root("desired_status"), ¤tState)...) + if resp.Diagnostics.HasError() { return } @@ -418,11 +424,12 @@ func (d desiredStateModifier) PlanModifyString(ctx context.Context, req planmodi } // Create creates the resource and sets the initial Terraform state. -func (r *serverResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *serverResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { //nolint:gocritic // function signature required by Terraform // Retrieve values from plan var model Model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -446,16 +453,19 @@ func (r *serverResource) Create(ctx context.Context, req resource.CreateRequest, } serverId := *server.Id + _, err = wait.CreateServerWaitHandler(ctx, r.client, projectId, serverId).WaitWithContext(ctx) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating server", fmt.Sprintf("server creation waiting: %v", err)) return } + ctx = tflog.SetField(ctx, "server_id", serverId) // Get Server with details serverReq := r.client.GetServer(ctx, projectId, serverId) serverReq = serverReq.Details(true) + server, err = serverReq.Execute() if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating server", fmt.Sprintf("get server details: %v", err)) @@ -476,14 +486,16 @@ func (r *serverResource) Create(ctx context.Context, req resource.CreateRequest, // Set state to fully populated data diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "Server created") } // serverControlClient provides a mockable interface for the necessary -// client operations in [updateServerStatus] +// client operations in [updateServerStatus]. type serverControlClient interface { wait.APIClientInterface StartServerExecute(ctx context.Context, projectId string, serverId string) error @@ -493,46 +505,56 @@ type serverControlClient interface { func startServer(ctx context.Context, client serverControlClient, projectId, serverId string) error { tflog.Debug(ctx, "starting server to enter active state") + if err := client.StartServerExecute(ctx, projectId, serverId); err != nil { return fmt.Errorf("cannot start server: %w", err) } + _, err := wait.StartServerWaitHandler(ctx, client, projectId, serverId).WaitWithContext(ctx) if err != nil { return fmt.Errorf("cannot check started server: %w", err) } + return nil } func stopServer(ctx context.Context, client serverControlClient, projectId, serverId string) error { tflog.Debug(ctx, "stopping server to enter inactive state") + if err := client.StopServerExecute(ctx, projectId, serverId); err != nil { return fmt.Errorf("cannot stop server: %w", err) } + _, err := wait.StopServerWaitHandler(ctx, client, projectId, serverId).WaitWithContext(ctx) if err != nil { return fmt.Errorf("cannot check stopped server: %w", err) } + return nil } func deallocatServer(ctx context.Context, client serverControlClient, projectId, serverId string) error { tflog.Debug(ctx, "deallocating server to enter shelved state") + if err := client.DeallocateServerExecute(ctx, projectId, serverId); err != nil { return fmt.Errorf("cannot deallocate server: %w", err) } + _, err := wait.DeallocateServerWaitHandler(ctx, client, projectId, serverId).WaitWithContext(ctx) if err != nil { return fmt.Errorf("cannot check deallocated server: %w", err) } + return nil } -// updateServerStatus applies the appropriate server state changes for the actual current and the intended state +// updateServerStatus applies the appropriate server state changes for the actual current and the intended state. func updateServerStatus(ctx context.Context, client serverControlClient, currentState *string, model *Model) error { if currentState == nil { tflog.Warn(ctx, "no current state available, not updating server state") return nil } + switch *currentState { case wait.ServerActiveStatus: switch strings.ToUpper(model.DesiredStatus.ValueString()) { @@ -542,12 +564,12 @@ func updateServerStatus(ctx context.Context, client serverControlClient, current } case wait.ServerDeallocatedStatus: - if err := deallocatServer(ctx, client, model.ProjectId.ValueString(), model.ServerId.ValueString()); err != nil { return err } default: tflog.Debug(ctx, fmt.Sprintf("nothing to do for status value %q", model.DesiredStatus.ValueString())) + if _, err := client.GetServerExecute(ctx, model.ProjectId.ValueString(), model.ServerId.ValueString()); err != nil { return err } @@ -565,6 +587,7 @@ func updateServerStatus(ctx context.Context, client serverControlClient, current default: tflog.Debug(ctx, fmt.Sprintf("nothing to do for status value %q", model.DesiredStatus.ValueString())) + if _, err := client.GetServerExecute(ctx, model.ProjectId.ValueString(), model.ServerId.ValueString()); err != nil { return err } @@ -582,6 +605,7 @@ func updateServerStatus(ctx context.Context, client serverControlClient, current } default: tflog.Debug(ctx, fmt.Sprintf("nothing to do for status value %q", model.DesiredStatus.ValueString())) + if _, err := client.GetServerExecute(ctx, model.ProjectId.ValueString(), model.ServerId.ValueString()); err != nil { return err } @@ -594,13 +618,15 @@ func updateServerStatus(ctx context.Context, client serverControlClient, current } // // Read refreshes the Terraform state with the latest data. -func (r *serverResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (r *serverResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() serverId := model.ServerId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) @@ -608,6 +634,7 @@ func (r *serverResource) Read(ctx context.Context, req resource.ReadRequest, res serverReq := r.client.GetServer(ctx, projectId, serverId) serverReq = serverReq.Details(true) + serverResp, err := serverReq.Execute() if err != nil { oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped @@ -615,7 +642,9 @@ func (r *serverResource) Read(ctx context.Context, req resource.ReadRequest, res resp.State.RemoveResource(ctx) return } + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading server", fmt.Sprintf("Calling API: %v", err)) + return } @@ -628,9 +657,11 @@ func (r *serverResource) Read(ctx context.Context, req resource.ReadRequest, res // Set refreshed state diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "server read") } @@ -638,8 +669,9 @@ func (r *serverResource) updateServerAttributes(ctx context.Context, model, stat // Generate API request body from model payload, err := toUpdatePayload(ctx, model, stateModel.Labels) if err != nil { - return nil, fmt.Errorf("Creating API payload: %w", err) + return nil, fmt.Errorf("creating API payload: %w", err) } + projectId := model.ProjectId.ValueString() serverId := model.ServerId.ValueString() @@ -647,7 +679,7 @@ func (r *serverResource) updateServerAttributes(ctx context.Context, model, stat // Update existing server updatedServer, err = r.client.UpdateServer(ctx, projectId, serverId).UpdateServerPayload(*payload).Execute() if err != nil { - return nil, fmt.Errorf("Calling API: %w", err) + return nil, fmt.Errorf("calling API: %w", err) } // Update machine type @@ -656,9 +688,10 @@ func (r *serverResource) updateServerAttributes(ctx context.Context, model, stat payload := iaas.ResizeServerPayload{ MachineType: modelMachineType, } + err := r.client.ResizeServer(ctx, projectId, serverId).ResizeServerPayload(payload).Execute() if err != nil { - return nil, fmt.Errorf("Resizing the server, calling API: %w", err) + return nil, fmt.Errorf("resizing the server, calling API: %w", err) } _, err = wait.ResizeServerWaitHandler(ctx, r.client, projectId, serverId).WaitWithContext(ctx) @@ -668,18 +701,21 @@ func (r *serverResource) updateServerAttributes(ctx context.Context, model, stat // Update server model because the API doesn't return a server object as response updatedServer.MachineType = modelMachineType } + return updatedServer, nil } // Update updates the resource and sets the updated Terraform state on success. -func (r *serverResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *serverResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { //nolint:gocritic // function signature required by Terraform // Retrieve values from plan var model Model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() serverId := model.ServerId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) @@ -689,6 +725,7 @@ func (r *serverResource) Update(ctx context.Context, req resource.UpdateRequest, var stateModel Model diags = req.State.Get(ctx, &stateModel) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -697,6 +734,7 @@ func (r *serverResource) Update(ctx context.Context, req resource.UpdateRequest, server *iaas.Server err error ) + if server, err = r.client.GetServer(ctx, model.ProjectId.ValueString(), model.ServerId.ValueString()).Execute(); err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error retrieving server state", fmt.Sprintf("Getting server state: %v", err)) } @@ -731,6 +769,7 @@ func (r *serverResource) Update(ctx context.Context, req resource.UpdateRequest, // Re-fetch the server data, to get the details values. serverReq := r.client.GetServer(ctx, projectId, serverId) serverReq = serverReq.Details(true) + updatedServer, err := serverReq.Execute() if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating server", fmt.Sprintf("Calling API: %v", err)) @@ -745,18 +784,21 @@ func (r *serverResource) Update(ctx context.Context, req resource.UpdateRequest, diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "server updated") } // Delete deletes the resource and removes the Terraform state on success. -func (r *serverResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform +func (r *serverResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { //nolint:gocritic // function signature required by Terraform // Retrieve values from state var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -772,6 +814,7 @@ func (r *serverResource) Delete(ctx context.Context, req resource.DeleteRequest, core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting server", fmt.Sprintf("Calling API: %v", err)) return } + _, err = wait.DeleteServerWaitHandler(ctx, r.client, projectId, serverId).WaitWithContext(ctx) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting server", fmt.Sprintf("server deletion waiting: %v", err)) @@ -782,7 +825,7 @@ func (r *serverResource) Delete(ctx context.Context, req resource.DeleteRequest, } // ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: project_id,server_id +// The expected format of the resource import identifier is: project_id,server_id. func (r *serverResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { idParts := strings.Split(req.ID, core.Separator) @@ -791,6 +834,7 @@ func (r *serverResource) ImportState(ctx context.Context, req resource.ImportSta "Error importing server", fmt.Sprintf("Expected import identifier with format: [project_id],[server_id] Got: %q", req.ID), ) + return } @@ -808,6 +852,7 @@ func mapFields(ctx context.Context, serverResp *iaas.Server, model *Model) error if serverResp == nil { return fmt.Errorf("response input is nil") } + if model == nil { return fmt.Errorf("model input is nil") } @@ -829,20 +874,26 @@ func mapFields(ctx context.Context, serverResp *iaas.Server, model *Model) error } var createdAt basetypes.StringValue + if serverResp.CreatedAt != nil { createdAtValue := *serverResp.CreatedAt createdAt = types.StringValue(createdAtValue.Format(time.RFC3339)) } + var updatedAt basetypes.StringValue + if serverResp.UpdatedAt != nil { updatedAtValue := *serverResp.UpdatedAt updatedAt = types.StringValue(updatedAtValue.Format(time.RFC3339)) } + var launchedAt basetypes.StringValue + if serverResp.LaunchedAt != nil { launchedAtValue := *serverResp.LaunchedAt launchedAt = types.StringValue(launchedAtValue.Format(time.RFC3339)) } + if serverResp.Nics != nil { var respNics []string for _, nic := range *serverResp.Nics { @@ -850,15 +901,18 @@ func mapFields(ctx context.Context, serverResp *iaas.Server, model *Model) error } var modelNics []string + for _, modelNic := range model.NetworkInterfaces.Elements() { modelNicString, ok := modelNic.(types.String) if !ok { return fmt.Errorf("type assertion for network interfaces failed") } + modelNics = append(modelNics, modelNicString.ValueString()) } var filteredNics []string + for _, modelNic := range modelNics { for _, nic := range respNics { if nic == modelNic { @@ -887,8 +941,8 @@ func mapFields(ctx context.Context, serverResp *iaas.Server, model *Model) error if serverResp.BootVolume != nil { // convert boot volume model - var bootVolumeModel = &bootVolumeModel{} - if !(model.BootVolume.IsNull() || model.BootVolume.IsUnknown()) { + bootVolumeModel := &bootVolumeModel{} + if !model.BootVolume.IsNull() && !model.BootVolume.IsUnknown() { diags := model.BootVolume.As(ctx, bootVolumeModel, basetypes.ObjectAsOptions{}) if diags.HasError() { return fmt.Errorf("failed to map bootVolume: %w", core.DiagsToError(diags)) @@ -908,6 +962,7 @@ func mapFields(ctx context.Context, serverResp *iaas.Server, model *Model) error if diags.HasError() { return fmt.Errorf("failed to map bootVolume: %w", core.DiagsToError(diags)) } + model.BootVolume = bootVolume } else { model.BootVolume = types.ObjectNull(bootVolumeTypes) @@ -928,6 +983,7 @@ func mapFields(ctx context.Context, serverResp *iaas.Server, model *Model) error if serverResp.UserData != nil && len(*serverResp.UserData) > 0 { model.UserData = types.StringValue(string(*serverResp.UserData)) } + model.Name = types.StringPointerValue(serverResp.Name) model.Labels = labels model.ImageId = types.StringPointerValue(serverResp.ImageId) @@ -945,8 +1001,8 @@ func toCreatePayload(ctx context.Context, model *Model) (*iaas.CreateServerPaylo return nil, fmt.Errorf("nil model") } - var bootVolume = &bootVolumeModel{} - if !(model.BootVolume.IsNull() || model.BootVolume.IsUnknown()) { + bootVolume := &bootVolumeModel{} + if !model.BootVolume.IsNull() && !model.BootVolume.IsUnknown() { diags := model.BootVolume.As(ctx, bootVolume, basetypes.ObjectAsOptions{}) if diags.HasError() { return nil, fmt.Errorf("convert boot volume object to struct: %w", core.DiagsToError(diags)) @@ -975,6 +1031,7 @@ func toCreatePayload(ctx context.Context, model *Model) (*iaas.CreateServerPaylo } var userData *[]byte + if !model.UserData.IsNull() && !model.UserData.IsUnknown() { src := []byte(model.UserData.ValueString()) encodedUserData := make([]byte, base64.StdEncoding.EncodedLen(len(src))) @@ -983,13 +1040,16 @@ func toCreatePayload(ctx context.Context, model *Model) (*iaas.CreateServerPaylo } var network *iaas.CreateServerPayloadNetworking + if !model.NetworkInterfaces.IsNull() && !model.NetworkInterfaces.IsUnknown() { var nicIds []string + for _, nic := range model.NetworkInterfaces.Elements() { nicString, ok := nic.(types.String) if !ok { return nil, fmt.Errorf("type assertion failed") } + nicIds = append(nicIds, nicString.ValueString()) } diff --git a/stackit/internal/services/iaas/server/resource_test.go b/stackit/internal/services/iaas/server/resource_test.go index d9dac877b..be8a2eccd 100644 --- a/stackit/internal/services/iaas/server/resource_test.go +++ b/stackit/internal/services/iaas/server/resource_test.go @@ -159,9 +159,11 @@ func TestMapFields(t *testing.T) { if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(tt.state, tt.expected) if diff != "" { @@ -271,9 +273,11 @@ func TestToCreatePayload(t *testing.T) { if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(output, tt.expected) if diff != "" { @@ -314,9 +318,11 @@ func TestToUpdatePayload(t *testing.T) { if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(output, tt.expected) if diff != "" { @@ -330,7 +336,7 @@ func TestToUpdatePayload(t *testing.T) { var _ serverControlClient = (*mockServerControlClient)(nil) // mockServerControlClient mocks the [serverControlClient] interface with -// pluggable functions +// pluggable functions. type mockServerControlClient struct { wait.APIClientInterface startServerCalled int @@ -373,13 +379,16 @@ func (t *mockServerControlClient) StopServerExecute(ctx context.Context, project func Test_serverResource_updateServerStatus(t *testing.T) { projectId := basetypes.NewStringValue("projectId") serverId := basetypes.NewStringValue("serverId") + type fields struct { client *mockServerControlClient } + type args struct { currentState *string model Model } + type want struct { err bool status types.String @@ -388,6 +397,7 @@ func Test_serverResource_updateServerStatus(t *testing.T) { startCount int deallocatedCount int } + tests := []struct { name string fields fields @@ -429,6 +439,7 @@ func Test_serverResource_updateServerStatus(t *testing.T) { } else { state = wait.ServerInactiveStatus } + return &iaas.Server{ Id: utils.Ptr(serverId.ValueString()), Status: &state, @@ -465,6 +476,7 @@ func Test_serverResource_updateServerStatus(t *testing.T) { default: state = wait.ServerDeallocatedStatus } + return &iaas.Server{ Id: utils.Ptr(serverId.ValueString()), Status: &state, @@ -566,10 +578,12 @@ func Test_serverResource_updateServerStatus(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() + err := updateServerStatus(context.Background(), tt.fields.client, tt.args.currentState, &tt.args.model) if (err != nil) != tt.want.err { t.Errorf("inconsistent error, want %v and got %v", tt.want.err, err) } + if expected, actual := tt.want.status, tt.args.model.DesiredStatus; expected != actual { t.Errorf("wanted status %s but got %s", expected, actual) } @@ -577,12 +591,15 @@ func Test_serverResource_updateServerStatus(t *testing.T) { if expected, actual := tt.want.getServerCount, tt.fields.client.getServerCalled; expected != actual { t.Errorf("wrong number of get server calls: Expected %d but got %d", expected, actual) } + if expected, actual := tt.want.startCount, tt.fields.client.startServerCalled; expected != actual { t.Errorf("wrong number of start server calls: Expected %d but got %d", expected, actual) } + if expected, actual := tt.want.stopCount, tt.fields.client.stopServerCalled; expected != actual { t.Errorf("wrong number of stop server calls: Expected %d but got %d", expected, actual) } + if expected, actual := tt.want.deallocatedCount, tt.fields.client.deallocateServerCalled; expected != actual { t.Errorf("wrong number of deallocate server calls: Expected %d but got %d", expected, actual) } diff --git a/stackit/internal/services/iaas/serviceaccountattach/resource.go b/stackit/internal/services/iaas/serviceaccountattach/resource.go index f5eaed4d2..d6b16a50b 100644 --- a/stackit/internal/services/iaas/serviceaccountattach/resource.go +++ b/stackit/internal/services/iaas/serviceaccountattach/resource.go @@ -6,11 +6,6 @@ import ( "net/http" "strings" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" - "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" @@ -21,7 +16,10 @@ import ( "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/stackitcloud/stackit-sdk-go/core/oapierror" "github.com/stackitcloud/stackit-sdk-go/services/iaas" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" ) @@ -62,10 +60,13 @@ func (r *networkInterfaceAttachResource) Configure(ctx context.Context, req reso } apiClient := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + r.client = apiClient + tflog.Info(ctx, "iaas client configured") } @@ -117,11 +118,12 @@ func (r *networkInterfaceAttachResource) Schema(_ context.Context, _ resource.Sc } // Create creates the resource and sets the initial Terraform state. -func (r *networkInterfaceAttachResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *networkInterfaceAttachResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { //nolint:gocritic // function signature required by Terraform // Retrieve values from plan var model Model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -145,20 +147,24 @@ func (r *networkInterfaceAttachResource) Create(ctx context.Context, req resourc // Set state to fully populated data diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "Service account attachment created") } // Read refreshes the Terraform state with the latest data. -func (r *networkInterfaceAttachResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (r *networkInterfaceAttachResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) serverId := model.ServerId.ValueString() @@ -173,7 +179,9 @@ func (r *networkInterfaceAttachResource) Read(ctx context.Context, req resource. resp.State.RemoveResource(ctx) return } + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading service account attachment", fmt.Sprintf("Calling API: %v", err)) + return } @@ -190,10 +198,13 @@ func (r *networkInterfaceAttachResource) Read(ctx context.Context, req resource. // Set refreshed state diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "Service account attachment read") + return } } @@ -203,16 +214,17 @@ func (r *networkInterfaceAttachResource) Read(ctx context.Context, req resource. } // Update updates the resource and sets the updated Terraform state on success. -func (r *networkInterfaceAttachResource) Update(_ context.Context, _ resource.UpdateRequest, _ *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *networkInterfaceAttachResource) Update(_ context.Context, _ resource.UpdateRequest, _ *resource.UpdateResponse) { //nolint:gocritic // function signature required by Terraform // Update is not supported, all fields require replace } // Delete deletes the resource and removes the Terraform state on success. -func (r *networkInterfaceAttachResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform +func (r *networkInterfaceAttachResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { //nolint:gocritic // function signature required by Terraform // Retrieve values from state var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -235,7 +247,7 @@ func (r *networkInterfaceAttachResource) Delete(ctx context.Context, req resourc } // ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: project_id,server_id +// The expected format of the resource import identifier is: project_id,server_id. func (r *networkInterfaceAttachResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { idParts := strings.Split(req.ID, core.Separator) @@ -244,6 +256,7 @@ func (r *networkInterfaceAttachResource) ImportState(ctx context.Context, req re "Error importing service_account attachment", fmt.Sprintf("Expected import identifier with format: [project_id],[server_id],[service_account_email] Got: %q", req.ID), ) + return } diff --git a/stackit/internal/services/iaas/utils/util.go b/stackit/internal/services/iaas/utils/util.go index 7d7a2492e..49404f8ca 100644 --- a/stackit/internal/services/iaas/utils/util.go +++ b/stackit/internal/services/iaas/utils/util.go @@ -4,10 +4,9 @@ import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - - "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/stackitcloud/stackit-sdk-go/core/config" "github.com/stackitcloud/stackit-sdk-go/services/iaas" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" @@ -24,6 +23,7 @@ func ConfigureClient(ctx context.Context, providerData *core.ProviderData, diags } else { apiClientConfigOptions = append(apiClientConfigOptions, config.WithRegion(providerData.GetRegion())) } + apiClient, err := iaas.NewAPIClient(apiClientConfigOptions...) if err != nil { core.LogAndAddError(ctx, diags, "Error configuring API client", fmt.Sprintf("Configuring client: %v. This is an error related to the provider configuration, not to the resource configuration", err)) @@ -41,6 +41,7 @@ func MapLabels(ctx context.Context, responseLabels *map[string]interface{}, curr if responseLabels != nil && len(*responseLabels) != 0 { var diags diag.Diagnostics + labelsTF, diags = types.MapValueFrom(ctx, types.StringType, *responseLabels) if diags.HasError() { return labelsTF, fmt.Errorf("convert labels to StringValue map: %w", core.DiagsToError(diags)) diff --git a/stackit/internal/services/iaas/utils/util_test.go b/stackit/internal/services/iaas/utils/util_test.go index dce0d0365..c82123925 100644 --- a/stackit/internal/services/iaas/utils/util_test.go +++ b/stackit/internal/services/iaas/utils/util_test.go @@ -7,10 +7,9 @@ import ( "testing" "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - - "github.com/hashicorp/terraform-plugin-framework/diag" sdkClients "github.com/stackitcloud/stackit-sdk-go/core/clients" "github.com/stackitcloud/stackit-sdk-go/core/config" "github.com/stackitcloud/stackit-sdk-go/services/iaas" @@ -26,6 +25,7 @@ const ( func TestConfigureClient(t *testing.T) { /* mock authentication by setting service account token env variable */ os.Clearenv() + err := os.Setenv(sdkClients.ServiceAccountToken, "mock-val") if err != nil { t.Errorf("error setting env variable: %v", err) @@ -34,6 +34,7 @@ func TestConfigureClient(t *testing.T) { type args struct { providerData *core.ProviderData } + tests := []struct { name string args args @@ -55,6 +56,7 @@ func TestConfigureClient(t *testing.T) { if err != nil { t.Errorf("error configuring client: %v", err) } + return apiClient }(), wantErr: false, @@ -75,6 +77,7 @@ func TestConfigureClient(t *testing.T) { if err != nil { t.Errorf("error configuring client: %v", err) } + return apiClient }(), wantErr: false, @@ -86,6 +89,7 @@ func TestConfigureClient(t *testing.T) { diags := diag.Diagnostics{} actual := ConfigureClient(ctx, tt.args.providerData, &diags) + if diags.HasError() != tt.wantErr { t.Errorf("ConfigureClient() error = %v, want %v", diags.HasError(), tt.wantErr) } @@ -102,6 +106,7 @@ func TestMapLabels(t *testing.T) { responseLabels *map[string]interface{} currentLabels types.Map } + tests := []struct { name string args args @@ -166,11 +171,13 @@ func TestMapLabels(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ctx := context.Background() + got, err := MapLabels(ctx, tt.args.responseLabels, tt.args.currentLabels) if (err != nil) != tt.wantErr { t.Errorf("MapLabels() error = %v, wantErr %v", err, tt.wantErr) return } + if !reflect.DeepEqual(got, tt.want) { t.Errorf("MapLabels() got = %v, want %v", got, tt.want) } diff --git a/stackit/internal/services/iaas/volume/datasource.go b/stackit/internal/services/iaas/volume/datasource.go index 664772002..1d148f981 100644 --- a/stackit/internal/services/iaas/volume/datasource.go +++ b/stackit/internal/services/iaas/volume/datasource.go @@ -5,16 +5,15 @@ import ( "fmt" "net/http" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" - "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/stackitcloud/stackit-sdk-go/services/iaas" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" ) @@ -46,10 +45,13 @@ func (d *volumeDataSource) Configure(ctx context.Context, req datasource.Configu } apiClient := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + d.client = apiClient + tflog.Info(ctx, "iaas client configured") } @@ -132,13 +134,15 @@ func (r *volumeDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, } // Read refreshes the Terraform state with the latest data. -func (d *volumeDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (d *volumeDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.Config.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() volumeId := model.VolumeId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) @@ -157,6 +161,7 @@ func (d *volumeDataSource) Read(ctx context.Context, req datasource.ReadRequest, }, ) resp.State.RemoveResource(ctx) + return } @@ -165,10 +170,13 @@ func (d *volumeDataSource) Read(ctx context.Context, req datasource.ReadRequest, core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading volume", fmt.Sprintf("Processing API payload: %v", err)) return } + diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "volume read") } diff --git a/stackit/internal/services/iaas/volume/resource.go b/stackit/internal/services/iaas/volume/resource.go index 2b409a174..f96c6a13b 100644 --- a/stackit/internal/services/iaas/volume/resource.go +++ b/stackit/internal/services/iaas/volume/resource.go @@ -7,8 +7,6 @@ import ( "regexp" "strings" - iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" - "github.com/hashicorp/terraform-plugin-framework-validators/resourcevalidator" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/attr" @@ -28,6 +26,7 @@ import ( "github.com/stackitcloud/stackit-sdk-go/services/iaas/wait" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" ) @@ -55,13 +54,13 @@ type Model struct { Source types.Object `tfsdk:"source"` } -// Struct corresponding to Model.Source +// Struct corresponding to Model.Source. type sourceModel struct { Type types.String `tfsdk:"type"` Id types.String `tfsdk:"id"` } -// Types corresponding to sourceModel +// Types corresponding to sourceModel. var sourceTypes = map[string]attr.Type{ "type": basetypes.StringType{}, "id": basetypes.StringType{}, @@ -82,7 +81,7 @@ func (r *volumeResource) Metadata(_ context.Context, req resource.MetadataReques resp.TypeName = req.ProviderTypeName + "_volume" } -// ConfigValidators validates the resource configuration +// ConfigValidators validates the resource configuration. func (r *volumeResource) ConfigValidators(_ context.Context) []resource.ConfigValidator { return []resource.ConfigValidator{ resourcevalidator.AtLeastOneOf( @@ -100,10 +99,13 @@ func (r *volumeResource) Configure(ctx context.Context, req resource.ConfigureRe } apiClient := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + r.client = apiClient + tflog.Info(ctx, "iaas client configured") } @@ -246,8 +248,7 @@ func (r *volumeResource) Schema(_ context.Context, _ resource.SchemaRequest, res var _ planmodifier.Int64 = volumeResizeModifier{} -type volumeResizeModifier struct { -} +type volumeResizeModifier struct{} // Description implements planmodifier.String. func (v volumeResizeModifier) Description(context.Context) string { @@ -260,29 +261,35 @@ func (v volumeResizeModifier) MarkdownDescription(ctx context.Context) string { } // PlanModifyInt64 implements planmodifier.Int64. -func (v volumeResizeModifier) PlanModifyInt64(ctx context.Context, req planmodifier.Int64Request, resp *planmodifier.Int64Response) { // nolint:gocritic // function signature required by Terraform +func (v volumeResizeModifier) PlanModifyInt64(ctx context.Context, req planmodifier.Int64Request, resp *planmodifier.Int64Response) { //nolint:gocritic // function signature required by Terraform var planSize types.Int64 + var currentSize types.Int64 resp.Diagnostics.Append(req.Plan.GetAttribute(ctx, path.Root("size"), &planSize)...) + if resp.Diagnostics.HasError() { return } + resp.Diagnostics.Append(req.State.GetAttribute(ctx, path.Root("size"), ¤tSize)...) + if resp.Diagnostics.HasError() { return } + if planSize.ValueInt64() < currentSize.ValueInt64() { core.LogAndAddError(ctx, &resp.Diagnostics, "Error changing volume size", "A volume cannot be made smaller in order to prevent data loss.") } } // Create creates the resource and sets the initial Terraform state. -func (r *volumeResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *volumeResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { //nolint:gocritic // function signature required by Terraform // Retrieve values from plan var model Model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -290,10 +297,11 @@ func (r *volumeResource) Create(ctx context.Context, req resource.CreateRequest, projectId := model.ProjectId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) - var source = &sourceModel{} - if !(model.Source.IsNull() || model.Source.IsUnknown()) { + source := &sourceModel{} + if !model.Source.IsNull() && !model.Source.IsUnknown() { diags = model.Source.As(ctx, source, basetypes.ObjectAsOptions{}) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -315,6 +323,7 @@ func (r *volumeResource) Create(ctx context.Context, req resource.CreateRequest, } volumeId := *volume.Id + volume, err = wait.CreateVolumeWaitHandler(ctx, r.client, projectId, volumeId).WaitWithContext(ctx) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating volume", fmt.Sprintf("volume creation waiting: %v", err)) @@ -332,20 +341,24 @@ func (r *volumeResource) Create(ctx context.Context, req resource.CreateRequest, // Set state to fully populated data diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "Volume created") } // Read refreshes the Terraform state with the latest data. -func (r *volumeResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (r *volumeResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() volumeId := model.VolumeId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) @@ -358,7 +371,9 @@ func (r *volumeResource) Read(ctx context.Context, req resource.ReadRequest, res resp.State.RemoveResource(ctx) return } + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading volume", fmt.Sprintf("Calling API: %v", err)) + return } @@ -371,21 +386,25 @@ func (r *volumeResource) Read(ctx context.Context, req resource.ReadRequest, res // Set refreshed state diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "volume read") } // Update updates the resource and sets the updated Terraform state on success. -func (r *volumeResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *volumeResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { //nolint:gocritic // function signature required by Terraform // Retrieve values from plan var model Model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() volumeId := model.VolumeId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) @@ -395,6 +414,7 @@ func (r *volumeResource) Update(ctx context.Context, req resource.UpdateRequest, var stateModel Model diags = req.State.Get(ctx, &stateModel) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -422,6 +442,7 @@ func (r *volumeResource) Update(ctx context.Context, req resource.UpdateRequest, payload := iaas.ResizeVolumePayload{ Size: modelSize, } + err := r.client.ResizeVolume(ctx, projectId, volumeId).ResizeVolumePayload(payload).Execute() if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating volume", fmt.Sprintf("Resizing the volume, calling API: %v", err)) @@ -430,25 +451,30 @@ func (r *volumeResource) Update(ctx context.Context, req resource.UpdateRequest, updatedVolume.Size = modelSize } } + err = mapFields(ctx, updatedVolume, &model) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating volume", fmt.Sprintf("Processing API payload: %v", err)) return } + diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "volume updated") } // Delete deletes the resource and removes the Terraform state on success. -func (r *volumeResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform +func (r *volumeResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { //nolint:gocritic // function signature required by Terraform // Retrieve values from state var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -464,6 +490,7 @@ func (r *volumeResource) Delete(ctx context.Context, req resource.DeleteRequest, core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting volume", fmt.Sprintf("Calling API: %v", err)) return } + _, err = wait.DeleteVolumeWaitHandler(ctx, r.client, projectId, volumeId).WaitWithContext(ctx) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting volume", fmt.Sprintf("volume deletion waiting: %v", err)) @@ -474,7 +501,7 @@ func (r *volumeResource) Delete(ctx context.Context, req resource.DeleteRequest, } // ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: project_id,volume_id +// The expected format of the resource import identifier is: project_id,volume_id. func (r *volumeResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { idParts := strings.Split(req.ID, core.Separator) @@ -483,6 +510,7 @@ func (r *volumeResource) ImportState(ctx context.Context, req resource.ImportSta "Error importing volume", fmt.Sprintf("Expected import identifier with format: [project_id],[volume_id] Got: %q", req.ID), ) + return } @@ -500,6 +528,7 @@ func mapFields(ctx context.Context, volumeResp *iaas.Volume, model *Model) error if volumeResp == nil { return fmt.Errorf("response input is nil") } + if model == nil { return fmt.Errorf("model input is nil") } @@ -510,7 +539,7 @@ func mapFields(ctx context.Context, volumeResp *iaas.Volume, model *Model) error } else if volumeResp.Id != nil { volumeId = *volumeResp.Id } else { - return fmt.Errorf("Volume id not present") + return fmt.Errorf("volume id not present") } model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), volumeId) @@ -521,6 +550,7 @@ func mapFields(ctx context.Context, volumeResp *iaas.Volume, model *Model) error } var sourceValues map[string]attr.Value + var sourceObject basetypes.ObjectValue if volumeResp.Source == nil { sourceObject = types.ObjectNull(sourceTypes) @@ -529,7 +559,9 @@ func mapFields(ctx context.Context, volumeResp *iaas.Volume, model *Model) error "type": types.StringPointerValue(volumeResp.Source.Type), "id": types.StringPointerValue(volumeResp.Source.Id), } + var diags diag.Diagnostics + sourceObject, diags = types.ObjectValue(sourceTypes, sourceValues) if diags.HasError() { return fmt.Errorf("creating source: %w", core.DiagsToError(diags)) @@ -544,11 +576,13 @@ func mapFields(ctx context.Context, volumeResp *iaas.Volume, model *Model) error if name := volumeResp.Name; name != nil && *name == "" { model.Name = types.StringNull() } + model.Labels = labels model.PerformanceClass = types.StringPointerValue(volumeResp.PerformanceClass) model.ServerId = types.StringPointerValue(volumeResp.ServerId) model.Size = types.Int64PointerValue(volumeResp.Size) model.Source = sourceObject + return nil } diff --git a/stackit/internal/services/iaas/volume/resource_test.go b/stackit/internal/services/iaas/volume/resource_test.go index 819d594ec..8738d3243 100644 --- a/stackit/internal/services/iaas/volume/resource_test.go +++ b/stackit/internal/services/iaas/volume/resource_test.go @@ -130,9 +130,11 @@ func TestMapFields(t *testing.T) { if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(tt.state, tt.expected) if diff != "" { @@ -194,9 +196,11 @@ func TestToCreatePayload(t *testing.T) { if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(output, tt.expected) if diff != "" { @@ -239,9 +243,11 @@ func TestToUpdatePayload(t *testing.T) { if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(output, tt.expected) if diff != "" { diff --git a/stackit/internal/services/iaas/volumeattach/resource.go b/stackit/internal/services/iaas/volumeattach/resource.go index c5a851cf3..f4984b8cd 100644 --- a/stackit/internal/services/iaas/volumeattach/resource.go +++ b/stackit/internal/services/iaas/volumeattach/resource.go @@ -6,11 +6,6 @@ import ( "net/http" "strings" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" - "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" @@ -23,7 +18,10 @@ import ( sdkUtils "github.com/stackitcloud/stackit-sdk-go/core/utils" "github.com/stackitcloud/stackit-sdk-go/services/iaas" "github.com/stackitcloud/stackit-sdk-go/services/iaas/wait" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" ) @@ -64,10 +62,13 @@ func (r *volumeAttachResource) Configure(ctx context.Context, req resource.Confi } apiClient := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + r.client = apiClient + tflog.Info(ctx, "iaas client configured") } @@ -123,11 +124,12 @@ func (r *volumeAttachResource) Schema(_ context.Context, _ resource.SchemaReques } // Create creates the resource and sets the initial Terraform state. -func (r *volumeAttachResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *volumeAttachResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { //nolint:gocritic // function signature required by Terraform // Retrieve values from plan var model Model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -144,6 +146,7 @@ func (r *volumeAttachResource) Create(ctx context.Context, req resource.CreateRe payload := iaas.AddVolumeToServerPayload{ DeleteOnTermination: sdkUtils.Ptr(false), } + _, err := r.client.AddVolumeToServer(ctx, projectId, serverId, volumeId).AddVolumeToServerPayload(payload).Execute() if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error attaching volume to server", fmt.Sprintf("Calling API: %v", err)) @@ -161,20 +164,24 @@ func (r *volumeAttachResource) Create(ctx context.Context, req resource.CreateRe // Set state to fully populated data diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "Volume attachment created") } // Read refreshes the Terraform state with the latest data. -func (r *volumeAttachResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (r *volumeAttachResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) serverId := model.ServerId.ValueString() @@ -189,30 +196,35 @@ func (r *volumeAttachResource) Read(ctx context.Context, req resource.ReadReques resp.State.RemoveResource(ctx) return } + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading volume attachment", fmt.Sprintf("Calling API: %v", err)) + return } // Set refreshed state diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "Volume attachment read") } // Update updates the resource and sets the updated Terraform state on success. -func (r *volumeAttachResource) Update(_ context.Context, _ resource.UpdateRequest, _ *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *volumeAttachResource) Update(_ context.Context, _ resource.UpdateRequest, _ *resource.UpdateResponse) { //nolint:gocritic // function signature required by Terraform // Update is not supported, all fields require replace } // Delete deletes the resource and removes the Terraform state on success. -func (r *volumeAttachResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform +func (r *volumeAttachResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { //nolint:gocritic // function signature required by Terraform // Retrieve values from state var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -241,7 +253,7 @@ func (r *volumeAttachResource) Delete(ctx context.Context, req resource.DeleteRe } // ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: project_id,server_id +// The expected format of the resource import identifier is: project_id,server_id. func (r *volumeAttachResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { idParts := strings.Split(req.ID, core.Separator) @@ -250,6 +262,7 @@ func (r *volumeAttachResource) ImportState(ctx context.Context, req resource.Imp "Error importing volume attachment", fmt.Sprintf("Expected import identifier with format: [project_id],[server_id],[volume_id] Got: %q", req.ID), ) + return } diff --git a/stackit/internal/services/iaasalpha/iaasalpha_acc_test.go b/stackit/internal/services/iaasalpha/iaasalpha_acc_test.go index dd0e06541..ba79479c9 100644 --- a/stackit/internal/services/iaasalpha/iaasalpha_acc_test.go +++ b/stackit/internal/services/iaasalpha/iaasalpha_acc_test.go @@ -19,11 +19,10 @@ import ( "github.com/stackitcloud/stackit-sdk-go/core/oapierror" "github.com/stackitcloud/stackit-sdk-go/services/iaasalpha" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/testutil" ) -// TODO: create network area using terraform resource instead once it's out of experimental stage and GA +// TODO: create network area using terraform resource instead once it's out of experimental stage and GA. const ( testNetworkAreaId = "25bbf23a-8134-4439-9f5e-1641caf8354e" ) @@ -73,6 +72,7 @@ var testConfigRoutingTableMaxUpdated = func() config.Variables { updatedConfig["name"] = config.StringVariable(fmt.Sprintf("acc-test-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum))) updatedConfig["description"] = config.StringVariable("This is the updated description of the routing table.") updatedConfig["label"] = config.StringVariable("routing-table-updated-label-01") + return updatedConfig }() @@ -111,7 +111,7 @@ var testConfigRoutingTableRouteMaxUpdated = func() config.Variables { return updatedConfig }() -// execute routingtable and routingtable route min and max tests with t.Run() to prevent parallel runs (needed for tests of stackit_routing_tables datasource) +// execute routingtable and routingtable route min and max tests with t.Run() to prevent parallel runs (needed for tests of stackit_routing_tables datasource). func TestAccRoutingTable(t *testing.T) { t.Run("TestAccRoutingTableMin", func(t *testing.T) { t.Logf("TestAccRoutingTableMin name: %s", testutil.ConvertConfigVariable(testConfigRoutingTableMin["name"])) @@ -223,6 +223,7 @@ func TestAccRoutingTable(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute routing_table_id") } + return fmt.Sprintf("%s,%s,%s,%s", testutil.OrganizationId, region, networkAreaId, routingTableId), nil }, ImportState: true, @@ -364,6 +365,7 @@ func TestAccRoutingTable(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute routing_table_id") } + return fmt.Sprintf("%s,%s,%s,%s", testutil.OrganizationId, region, networkAreaId, routingTableId), nil }, ImportState: true, @@ -520,6 +522,7 @@ func TestAccRoutingTable(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute route_id") } + return fmt.Sprintf("%s,%s,%s,%s,%s", testutil.OrganizationId, region, networkAreaId, routingTableId, routeId), nil }, ImportState: true, @@ -689,6 +692,7 @@ func TestAccRoutingTable(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute route_id") } + return fmt.Sprintf("%s,%s,%s,%s,%s", testutil.OrganizationId, region, networkAreaId, routingTableId, routeId), nil }, ImportState: true, @@ -735,6 +739,7 @@ func testAccCheckDestroy(s *terraform.State) error { testAccCheckRoutingTableDestroy, testAccCheckRoutingTableRouteDestroy, } + var errs []error wg := sync.WaitGroup{} @@ -746,16 +751,21 @@ func testAccCheckDestroy(s *terraform.State) error { if err != nil { errs = append(errs, err) } + wg.Done() }() } + wg.Wait() + return errors.Join(errs...) } func testAccCheckRoutingTableDestroy(s *terraform.State) error { ctx := context.Background() + var client *iaasalpha.APIClient + var err error if testutil.IaaSCustomEndpoint == "" { client, err = iaasalpha.NewAPIClient() @@ -764,6 +774,7 @@ func testAccCheckRoutingTableDestroy(s *terraform.State) error { stackitSdkConfig.WithEndpoint(testutil.IaaSCustomEndpoint), ) } + if err != nil { return fmt.Errorf("creating client: %w", err) } @@ -774,8 +785,10 @@ func testAccCheckRoutingTableDestroy(s *terraform.State) error { if rs.Type != "stackit_routing_table" { continue } + routingTableId := strings.Split(rs.Primary.ID, core.Separator)[3] region := strings.Split(rs.Primary.ID, core.Separator)[1] + err := client.DeleteRoutingTableFromAreaExecute(ctx, testutil.OrganizationId, testNetworkAreaId, region, routingTableId) if err != nil { var oapiErr *oapierror.GenericOpenAPIError @@ -784,6 +797,7 @@ func testAccCheckRoutingTableDestroy(s *terraform.State) error { continue } } + errs = append(errs, fmt.Errorf("cannot trigger routing table deletion %q: %w", routingTableId, err)) } } @@ -793,7 +807,9 @@ func testAccCheckRoutingTableDestroy(s *terraform.State) error { func testAccCheckRoutingTableRouteDestroy(s *terraform.State) error { ctx := context.Background() + var client *iaasalpha.APIClient + var err error if testutil.IaaSCustomEndpoint == "" { client, err = iaasalpha.NewAPIClient() @@ -802,6 +818,7 @@ func testAccCheckRoutingTableRouteDestroy(s *terraform.State) error { stackitSdkConfig.WithEndpoint(testutil.IaaSCustomEndpoint), ) } + if err != nil { return fmt.Errorf("creating client: %w", err) } @@ -812,9 +829,11 @@ func testAccCheckRoutingTableRouteDestroy(s *terraform.State) error { if rs.Type != "stackit_routing_table_route" { continue } + routingTableRouteId := strings.Split(rs.Primary.ID, core.Separator)[4] routingTableId := strings.Split(rs.Primary.ID, core.Separator)[3] region := strings.Split(rs.Primary.ID, core.Separator)[1] + err := client.DeleteRouteFromRoutingTableExecute(ctx, testutil.OrganizationId, testNetworkAreaId, region, routingTableId, routingTableRouteId) if err != nil { var oapiErr *oapierror.GenericOpenAPIError @@ -823,6 +842,7 @@ func testAccCheckRoutingTableRouteDestroy(s *terraform.State) error { continue } } + errs = append(errs, fmt.Errorf("cannot trigger routing table route deletion %q: %w", routingTableId, err)) } } diff --git a/stackit/internal/services/iaasalpha/routingtable/route/datasource.go b/stackit/internal/services/iaasalpha/routingtable/route/datasource.go index 51846f69f..7da4762c0 100644 --- a/stackit/internal/services/iaasalpha/routingtable/route/datasource.go +++ b/stackit/internal/services/iaasalpha/routingtable/route/datasource.go @@ -5,8 +5,6 @@ import ( "fmt" "net/http" - shared "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaasalpha/routingtable/shared" - "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-log/tflog" @@ -14,6 +12,7 @@ import ( "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/features" + shared "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaasalpha/routingtable/shared" iaasalphaUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaasalpha/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" ) @@ -41,21 +40,26 @@ func (d *routingTableRouteDataSource) Metadata(_ context.Context, req datasource func (d *routingTableRouteDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { var ok bool + d.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) if !ok { return } features.CheckExperimentEnabled(ctx, &d.providerData, features.RoutingTablesExperiment, "stackit_routing_table_route", core.Datasource, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } apiClient := iaasalphaUtils.ConfigureClient(ctx, &d.providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + d.client = apiClient + tflog.Info(ctx, "IaaS client configured") } @@ -70,10 +74,11 @@ func (d *routingTableRouteDataSource) Schema(_ context.Context, _ datasource.Sch } // Read refreshes the Terraform state with the latest data. -func (d *routingTableRouteDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (d *routingTableRouteDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model shared.RouteModel diags := req.Config.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -103,6 +108,7 @@ func (d *routingTableRouteDataSource) Read(ctx context.Context, req datasource.R }, ) resp.State.RemoveResource(ctx) + return } @@ -111,10 +117,13 @@ func (d *routingTableRouteDataSource) Read(ctx context.Context, req datasource.R core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading routing table route", fmt.Sprintf("Processing API payload: %v", err)) return } + diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "Routing table route read") } diff --git a/stackit/internal/services/iaasalpha/routingtable/route/resource.go b/stackit/internal/services/iaasalpha/routingtable/route/resource.go index a7fb1025e..2a47305d7 100644 --- a/stackit/internal/services/iaasalpha/routingtable/route/resource.go +++ b/stackit/internal/services/iaasalpha/routingtable/route/resource.go @@ -5,8 +5,6 @@ import ( "fmt" "strings" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaasalpha/routingtable/shared" - "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" @@ -21,6 +19,7 @@ import ( "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/features" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaasalpha/routingtable/shared" iaasalphaUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaasalpha/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" @@ -53,49 +52,61 @@ func (r *routeResource) Metadata(_ context.Context, req resource.MetadataRequest // Configure adds the provider configured client to the resource. func (r *routeResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { var ok bool + r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) if !ok { return } features.CheckExperimentEnabled(ctx, &r.providerData, features.RoutingTablesExperiment, "stackit_routing_table_route", core.Resource, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } apiClient := iaasalphaUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + r.client = apiClient + tflog.Info(ctx, "IaaS alpha client configured") } // ModifyPlan implements resource.ResourceWithModifyPlan. // Use the modifier to set the effective region in the current plan. -func (r *routeResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform +func (r *routeResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { //nolint:gocritic // function signature required by Terraform // skip initial empty configuration to avoid follow-up errors if req.Config.Raw.IsNull() { return } var configModel shared.RouteModel + resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...) + if resp.Diagnostics.HasError() { return } + var planModel shared.RouteModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) + if resp.Diagnostics.HasError() { return } utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp) + if resp.Diagnostics.HasError() { return } resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...) + if resp.Diagnostics.HasError() { return } @@ -229,10 +240,11 @@ func (r *routeResource) Schema(_ context.Context, _ resource.SchemaRequest, resp } // Create creates the resource and sets the initial Terraform state. -func (r *routeResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *routeResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { //nolint:gocritic // function signature required by Terraform var model shared.RouteModel diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -266,24 +278,29 @@ func (r *routeResource) Create(ctx context.Context, req resource.CreateRequest, core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating routing table route", fmt.Sprintf("Processing API payload: %v", err)) return } + ctx = tflog.SetField(ctx, "route_id", model.RouteId.ValueString()) diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "Routing table route created") } // Read refreshes the Terraform state with the latest data. -func (r *routeResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (r *routeResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model shared.RouteModel diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + organizationId := model.OrganizationId.ValueString() routingTableId := model.RoutingTableId.ValueString() networkAreaId := model.NetworkAreaId.ValueString() @@ -312,18 +329,21 @@ func (r *routeResource) Read(ctx context.Context, req resource.ReadRequest, resp // Set refreshed state diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "Routing table route read.") } // Update updates the resource and sets the updated Terraform state on success. -func (r *routeResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *routeResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { //nolint:gocritic // function signature required by Terraform // Retrieve values from plan var model shared.RouteModel diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -344,6 +364,7 @@ func (r *routeResource) Update(ctx context.Context, req resource.UpdateRequest, var stateModel shared.RouteModel diags = req.State.Get(ctx, &stateModel) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -370,17 +391,20 @@ func (r *routeResource) Update(ctx context.Context, req resource.UpdateRequest, // Set refreshed state diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "Routing table route updated") } // Delete deletes the resource and removes the Terraform state on success. -func (r *routeResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform +func (r *routeResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { //nolint:gocritic // function signature required by Terraform var model shared.RouteModel diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -407,7 +431,7 @@ func (r *routeResource) Delete(ctx context.Context, req resource.DeleteRequest, } // ImportState imports a resource into the Terraform state on success. -// The expected format of the routing table route resource import identifier is: organization_id,region,network_area_id,routing_table_id,route_id +// The expected format of the routing table route resource import identifier is: organization_id,region,network_area_id,routing_table_id,route_id. func (r *routeResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { idParts := strings.Split(req.ID, core.Separator) @@ -416,6 +440,7 @@ func (r *routeResource) ImportState(ctx context.Context, req resource.ImportStat "Error importing routing table", fmt.Sprintf("Expected import identifier with format: [organization_id],[region],[network_area_id],[routing_table_id],[route_id] Got: %q", req.ID), ) + return } @@ -448,6 +473,7 @@ func mapFieldsFromList(ctx context.Context, routeResp *iaasalpha.RouteListRespon } route := (*routeResp.Items)[0] + return shared.MapRouteModel(ctx, &route, model, region) } @@ -465,6 +491,7 @@ func toCreatePayload(ctx context.Context, model *shared.RouteReadModel) (*iaasal if err != nil { return nil, err } + destinationPayload, err := toDestinationPayload(ctx, model) if err != nil { return nil, err @@ -500,11 +527,13 @@ func toNextHopPayload(ctx context.Context, model *shared.RouteReadModel) (*iaasa if model == nil { return nil, fmt.Errorf("nil model") } + if utils.IsUndefined(model.NextHop) { return nil, nil } nexthopModel := shared.RouteNextHop{} + diags := model.NextHop.As(ctx, &nexthopModel, basetypes.ObjectAsOptions{}) if diags.HasError() { return nil, core.DiagsToError(diags) @@ -520,6 +549,7 @@ func toNextHopPayload(ctx context.Context, model *shared.RouteReadModel) (*iaasa case "ipv6": return sdkUtils.Ptr(iaasalpha.NexthopIPv6AsRouteNexthop(iaasalpha.NewNexthopIPv6("ipv6", nexthopModel.Value.ValueString()))), nil } + return nil, fmt.Errorf("unknown nexthop type: %s", nexthopModel.Type.ValueString()) } @@ -527,11 +557,13 @@ func toDestinationPayload(ctx context.Context, model *shared.RouteReadModel) (*i if model == nil { return nil, fmt.Errorf("nil model") } + if utils.IsUndefined(model.Destination) { return nil, nil } destinationModel := shared.RouteDestination{} + diags := model.Destination.As(ctx, &destinationModel, basetypes.ObjectAsOptions{}) if diags.HasError() { return nil, core.DiagsToError(diags) @@ -543,5 +575,6 @@ func toDestinationPayload(ctx context.Context, model *shared.RouteReadModel) (*i case "cidrv6": return sdkUtils.Ptr(iaasalpha.DestinationCIDRv6AsRouteDestination(iaasalpha.NewDestinationCIDRv6("cidrv6", destinationModel.Value.ValueString()))), nil } + return nil, fmt.Errorf("unknown destination type: %s", destinationModel.Type.ValueString()) } diff --git a/stackit/internal/services/iaasalpha/routingtable/route/resource_test.go b/stackit/internal/services/iaasalpha/routingtable/route/resource_test.go index 9d59f8555..bb8d96bde 100644 --- a/stackit/internal/services/iaasalpha/routingtable/route/resource_test.go +++ b/stackit/internal/services/iaasalpha/routingtable/route/resource_test.go @@ -6,14 +6,13 @@ import ( "reflect" "testing" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaasalpha/routingtable/shared" - "github.com/google/go-cmp/cmp" "github.com/google/uuid" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/stackitcloud/stackit-sdk-go/core/utils" "github.com/stackitcloud/stackit-sdk-go/services/iaasalpha" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaasalpha/routingtable/shared" ) const ( @@ -33,6 +32,7 @@ func Test_mapFieldsFromList(t *testing.T) { model *shared.RouteModel region string } + tests := []struct { name string args args @@ -172,6 +172,7 @@ func Test_toUpdatePayload(t *testing.T) { model *shared.RouteModel currentLabels types.Map } + tests := []struct { name string args args @@ -213,11 +214,13 @@ func Test_toUpdatePayload(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ctx := context.Background() + got, err := toUpdatePayload(ctx, tt.args.model, tt.args.currentLabels) if (err != nil) != tt.wantErr { t.Errorf("toUpdatePayload() error = %v, wantErr %v", err, tt.wantErr) return } + diff := cmp.Diff(got, tt.want) if diff != "" { t.Fatalf("toUpdatePayload(): %s", diff) @@ -230,6 +233,7 @@ func Test_toNextHopPayload(t *testing.T) { type args struct { model *shared.RouteReadModel } + tests := []struct { name string args args @@ -307,11 +311,13 @@ func Test_toNextHopPayload(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ctx := context.Background() + got, err := toNextHopPayload(ctx, tt.args.model) if (err != nil) != tt.wantErr { t.Errorf("toNextHopPayload() error = %v, wantErr %v", err, tt.wantErr) return } + if !reflect.DeepEqual(got, tt.want) { t.Errorf("toNextHopPayload() got = %v, want %v", got, tt.want) } @@ -323,6 +329,7 @@ func Test_toDestinationPayload(t *testing.T) { type args struct { model *shared.RouteReadModel } + tests := []struct { name string args args @@ -370,11 +377,13 @@ func Test_toDestinationPayload(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ctx := context.Background() + got, err := toDestinationPayload(ctx, tt.args.model) if (err != nil) != tt.wantErr { t.Errorf("toDestinationPayload() error = %v, wantErr %v", err, tt.wantErr) return } + if !reflect.DeepEqual(got, tt.want) { t.Errorf("toDestinationPayload() got = %v, want %v", got, tt.want) } @@ -386,6 +395,7 @@ func Test_toCreatePayload(t *testing.T) { type args struct { model *shared.RouteReadModel } + tests := []struct { name string args args @@ -438,11 +448,13 @@ func Test_toCreatePayload(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ctx := context.Background() + got, err := toCreatePayload(ctx, tt.args.model) if (err != nil) != tt.wantErr { t.Errorf("toCreatePayload() error = %v, wantErr %v", err, tt.wantErr) return } + diff := cmp.Diff(got, tt.want) if diff != "" { t.Fatalf("toCreatePayload(): %s", diff) diff --git a/stackit/internal/services/iaasalpha/routingtable/routes/datasource.go b/stackit/internal/services/iaasalpha/routingtable/routes/datasource.go index 26ea2d8a1..373018581 100644 --- a/stackit/internal/services/iaasalpha/routingtable/routes/datasource.go +++ b/stackit/internal/services/iaasalpha/routingtable/routes/datasource.go @@ -52,21 +52,26 @@ func (d *routingTableRoutesDataSource) Metadata(_ context.Context, req datasourc func (d *routingTableRoutesDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { var ok bool + d.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) if !ok { return } features.CheckExperimentEnabled(ctx, &d.providerData, features.RoutingTablesExperiment, "stackit_routing_table_routes", core.Datasource, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } apiClient := iaasalphaUtils.ConfigureClient(ctx, &d.providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + d.client = apiClient + tflog.Info(ctx, "IaaS client configured") } @@ -81,10 +86,11 @@ func (d *routingTableRoutesDataSource) Schema(_ context.Context, _ datasource.Sc } // Read refreshes the Terraform state with the latest data. -func (d *routingTableRoutesDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (d *routingTableRoutesDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model RoutingTableRoutesDataSourceModel diags := req.Config.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -111,6 +117,7 @@ func (d *routingTableRoutesDataSource) Read(ctx context.Context, req datasource. }, ) resp.State.RemoveResource(ctx) + return } @@ -119,11 +126,14 @@ func (d *routingTableRoutesDataSource) Read(ctx context.Context, req datasource. core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading routing table routes", fmt.Sprintf("Processing API payload: %v", err)) return } + diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "Routing table routes read") } @@ -131,9 +141,11 @@ func mapDataSourceRoutingTableRoutes(ctx context.Context, routes *iaasalpha.Rout if routes == nil { return fmt.Errorf("response input is nil") } + if model == nil { return fmt.Errorf("model input is nil") } + if routes.Items == nil { return fmt.Errorf("items input is nil") } @@ -148,8 +160,10 @@ func mapDataSourceRoutingTableRoutes(ctx context.Context, routes *iaasalpha.Rout ) itemsList := []attr.Value{} + for i, route := range *routes.Items { var routeModel shared.RouteReadModel + err := shared.MapRouteReadModel(ctx, &route, &routeModel) if err != nil { return fmt.Errorf("mapping route: %w", err) @@ -168,6 +182,7 @@ func mapDataSourceRoutingTableRoutes(ctx context.Context, routes *iaasalpha.Rout if diags.HasError() { return fmt.Errorf("mapping index %d: %w", i, core.DiagsToError(diags)) } + itemsList = append(itemsList, routeTF) } diff --git a/stackit/internal/services/iaasalpha/routingtable/routes/datasource_test.go b/stackit/internal/services/iaasalpha/routingtable/routes/datasource_test.go index 171abd65d..a9cd54bd9 100644 --- a/stackit/internal/services/iaasalpha/routingtable/routes/datasource_test.go +++ b/stackit/internal/services/iaasalpha/routingtable/routes/datasource_test.go @@ -5,14 +5,13 @@ import ( "fmt" "testing" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaasalpha/routingtable/shared" - "github.com/google/go-cmp/cmp" "github.com/google/uuid" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/stackitcloud/stackit-sdk-go/core/utils" "github.com/stackitcloud/stackit-sdk-go/services/iaasalpha" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaasalpha/routingtable/shared" ) const ( @@ -33,6 +32,7 @@ func Test_mapDataSourceRoutingTableRoutes(t *testing.T) { model *RoutingTableRoutesDataSourceModel region string } + tests := []struct { name string args args diff --git a/stackit/internal/services/iaasalpha/routingtable/shared/route.go b/stackit/internal/services/iaasalpha/routingtable/shared/route.go index e05cf78de..11326555d 100644 --- a/stackit/internal/services/iaasalpha/routingtable/shared/route.go +++ b/stackit/internal/services/iaasalpha/routingtable/shared/route.go @@ -49,28 +49,29 @@ func RouteModelTypes() map[string]attr.Type { modelTypes["routing_table_id"] = types.StringType modelTypes["network_area_id"] = types.StringType modelTypes["region"] = types.StringType + return modelTypes } -// RouteDestination is the struct corresponding to RouteReadModel.Destination +// RouteDestination is the struct corresponding to RouteReadModel.Destination. type RouteDestination struct { Type types.String `tfsdk:"type"` Value types.String `tfsdk:"value"` } -// RouteDestinationTypes Types corresponding to routeDestination +// RouteDestinationTypes Types corresponding to routeDestination. var RouteDestinationTypes = map[string]attr.Type{ "type": types.StringType, "value": types.StringType, } -// RouteNextHop is the struct corresponding to RouteReadModel.NextHop +// RouteNextHop is the struct corresponding to RouteReadModel.NextHop. type RouteNextHop struct { Type types.String `tfsdk:"type"` Value types.String `tfsdk:"value"` } -// RouteNextHopTypes Types corresponding to routeNextHop +// RouteNextHopTypes Types corresponding to routeNextHop. var RouteNextHopTypes = map[string]attr.Type{ "type": types.StringType, "value": types.StringType, @@ -80,6 +81,7 @@ func MapRouteModel(ctx context.Context, route *iaasalpha.Route, model *RouteMode if route == nil { return fmt.Errorf("response input is nil") } + if model == nil { return fmt.Errorf("model input is nil") } @@ -108,6 +110,7 @@ func MapRouteReadModel(ctx context.Context, route *iaasalpha.Route, model *Route if route == nil { return fmt.Errorf("response input is nil") } + if model == nil { return fmt.Errorf("model input is nil") } @@ -128,10 +131,12 @@ func MapRouteReadModel(ctx context.Context, route *iaasalpha.Route, model *Route // created at and updated at createdAtTF, updatedAtTF := types.StringNull(), types.StringNull() + if route.CreatedAt != nil { createdAtValue := *route.CreatedAt createdAtTF = types.StringValue(createdAtValue.Format(time.RFC3339)) } + if route.UpdatedAt != nil { updatedAtValue := *route.UpdatedAt updatedAtTF = types.StringValue(updatedAtValue.Format(time.RFC3339)) @@ -153,6 +158,7 @@ func MapRouteReadModel(ctx context.Context, route *iaasalpha.Route, model *Route model.CreatedAt = createdAtTF model.UpdatedAt = updatedAtTF model.Labels = labels + return nil } diff --git a/stackit/internal/services/iaasalpha/routingtable/shared/route_test.go b/stackit/internal/services/iaasalpha/routingtable/shared/route_test.go index 6997ad228..fd0277e19 100644 --- a/stackit/internal/services/iaasalpha/routingtable/shared/route_test.go +++ b/stackit/internal/services/iaasalpha/routingtable/shared/route_test.go @@ -29,6 +29,7 @@ func Test_MapRouteNextHop(t *testing.T) { type args struct { routeResp *iaasalpha.Route } + tests := []struct { name string args args @@ -134,13 +135,13 @@ func Test_MapRouteDestination(t *testing.T) { type args struct { routeResp *iaasalpha.Route } + tests := []struct { name string args args wantErr bool expected types.Object }{ - { name: "destination is nil", args: args{ @@ -215,6 +216,7 @@ func TestMapRouteModel(t *testing.T) { model *RouteModel region string } + tests := []struct { name string args args diff --git a/stackit/internal/services/iaasalpha/routingtable/shared/shared.go b/stackit/internal/services/iaasalpha/routingtable/shared/shared.go index 04382ad3e..459fe5d20 100644 --- a/stackit/internal/services/iaasalpha/routingtable/shared/shared.go +++ b/stackit/internal/services/iaasalpha/routingtable/shared/shared.go @@ -6,13 +6,12 @@ import ( "maps" "time" - iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" - "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/stackitcloud/stackit-sdk-go/services/iaasalpha" + iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" ) @@ -57,6 +56,7 @@ func GetDatasourceGetAttributes() map[string]schema.Attribute { Description: "Terraform's internal datasource ID. It is structured as \"`organization_id`,`region`,`network_area_id`,`routing_table_id`\".", Computed: true, } + return getAttributes } @@ -75,6 +75,7 @@ func GetRouteDataSourceAttributes() map[string]schema.Attribute { Description: "Terraform's internal datasource ID. It is structured as \"`organization_id`,`region`,`network_area_id`,`routing_table_id`,`route_id`\".", Computed: true, } + return getAttributes } @@ -95,6 +96,7 @@ func GetRoutesDataSourceAttributes() map[string]schema.Attribute { Description: "The datasource region. If not defined, the provider region is used.", Optional: true, } + return getAttributes } @@ -223,6 +225,7 @@ func MapRoutingTableReadModel(ctx context.Context, routingTable *iaasalpha.Routi if routingTable == nil { return fmt.Errorf("response input is nil") } + if model == nil { return fmt.Errorf("model input is nil") } @@ -243,10 +246,12 @@ func MapRoutingTableReadModel(ctx context.Context, routingTable *iaasalpha.Routi // created at and updated at createdAtTF, updatedAtTF := types.StringNull(), types.StringNull() + if routingTable.CreatedAt != nil { createdAtValue := *routingTable.CreatedAt createdAtTF = types.StringValue(createdAtValue.Format(time.RFC3339)) } + if routingTable.UpdatedAt != nil { updatedAtValue := *routingTable.UpdatedAt updatedAtTF = types.StringValue(updatedAtValue.Format(time.RFC3339)) @@ -260,5 +265,6 @@ func MapRoutingTableReadModel(ctx context.Context, routingTable *iaasalpha.Routi model.Labels = labels model.CreatedAt = createdAtTF model.UpdatedAt = updatedAtTF + return nil } diff --git a/stackit/internal/services/iaasalpha/routingtable/table/datasource.go b/stackit/internal/services/iaasalpha/routingtable/table/datasource.go index 61b4ddbe4..082edf709 100644 --- a/stackit/internal/services/iaasalpha/routingtable/table/datasource.go +++ b/stackit/internal/services/iaasalpha/routingtable/table/datasource.go @@ -6,17 +6,15 @@ import ( "net/http" "strings" - "github.com/hashicorp/terraform-plugin-framework/types" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaasalpha/routingtable/shared" - "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/stackitcloud/stackit-sdk-go/services/iaasalpha" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/features" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaasalpha/routingtable/shared" iaasalphaUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaasalpha/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" ) @@ -44,21 +42,26 @@ func (d *routingTableDataSource) Metadata(_ context.Context, req datasource.Meta func (d *routingTableDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { var ok bool + d.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) if !ok { return } features.CheckExperimentEnabled(ctx, &d.providerData, features.RoutingTablesExperiment, "stackit_routing_table", core.Datasource, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } apiClient := iaasalphaUtils.ConfigureClient(ctx, &d.providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + d.client = apiClient + tflog.Info(ctx, "IaaS client configured") } @@ -73,10 +76,11 @@ func (d *routingTableDataSource) Schema(_ context.Context, _ datasource.SchemaRe } // Read refreshes the Terraform state with the latest data. -func (d *routingTableDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (d *routingTableDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model shared.RoutingTableDataSourceModel diags := req.Config.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -103,6 +107,7 @@ func (d *routingTableDataSource) Read(ctx context.Context, req datasource.ReadRe }, ) resp.State.RemoveResource(ctx) + return } @@ -111,11 +116,14 @@ func (d *routingTableDataSource) Read(ctx context.Context, req datasource.ReadRe core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading routing table", fmt.Sprintf("Processing API payload: %v", err)) return } + diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "Routing table read") } @@ -123,6 +131,7 @@ func mapDatasourceFields(ctx context.Context, routingTable *iaasalpha.RoutingTab if routingTable == nil { return fmt.Errorf("response input is nil") } + if model == nil { return fmt.Errorf("model input is nil") } @@ -152,5 +161,6 @@ func mapDatasourceFields(ctx context.Context, routingTable *iaasalpha.RoutingTab } model.Region = types.StringValue(region) + return nil } diff --git a/stackit/internal/services/iaasalpha/routingtable/table/datasource_test.go b/stackit/internal/services/iaasalpha/routingtable/table/datasource_test.go index 4622e1b3e..4bc64940f 100644 --- a/stackit/internal/services/iaasalpha/routingtable/table/datasource_test.go +++ b/stackit/internal/services/iaasalpha/routingtable/table/datasource_test.go @@ -5,14 +5,13 @@ import ( "fmt" "testing" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaasalpha/routingtable/shared" - "github.com/google/go-cmp/cmp" "github.com/google/uuid" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/stackitcloud/stackit-sdk-go/core/utils" "github.com/stackitcloud/stackit-sdk-go/services/iaasalpha" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaasalpha/routingtable/shared" ) const ( @@ -122,9 +121,11 @@ func Test_mapDatasourceFields(t *testing.T) { if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(tt.state, tt.expected) if diff != "" { diff --git a/stackit/internal/services/iaasalpha/routingtable/table/resource.go b/stackit/internal/services/iaasalpha/routingtable/table/resource.go index 43d6d9070..25c487a57 100644 --- a/stackit/internal/services/iaasalpha/routingtable/table/resource.go +++ b/stackit/internal/services/iaasalpha/routingtable/table/resource.go @@ -7,15 +7,12 @@ import ( "strings" "time" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier" - - iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" - "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" "github.com/hashicorp/terraform-plugin-framework/schema/validator" @@ -25,6 +22,7 @@ import ( "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/features" + iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" iaasalphaUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaasalpha/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" @@ -71,49 +69,61 @@ func (r *routingTableResource) Metadata(_ context.Context, req resource.Metadata // Configure adds the provider configured client to the resource. func (r *routingTableResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { var ok bool + r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) if !ok { return } features.CheckExperimentEnabled(ctx, &r.providerData, features.RoutingTablesExperiment, "stackit_routing_table", core.Resource, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } apiClient := iaasalphaUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + r.client = apiClient + tflog.Info(ctx, "IaaS alpha client configured") } // ModifyPlan implements resource.ResourceWithModifyPlan. // Use the modifier to set the effective region in the current plan. -func (r *routingTableResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform +func (r *routingTableResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { //nolint:gocritic // function signature required by Terraform // skip initial empty configuration to avoid follow-up errors if req.Config.Raw.IsNull() { return } + var configModel Model + resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...) + if resp.Diagnostics.HasError() { return } var planModel Model + resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) + if resp.Diagnostics.HasError() { return } utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp) + if resp.Diagnostics.HasError() { return } resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...) + if resp.Diagnostics.HasError() { return } @@ -219,11 +229,12 @@ func (r *routingTableResource) Schema(_ context.Context, _ resource.SchemaReques } // Create creates the resource and sets the initial Terraform state. -func (r *routingTableResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *routingTableResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { //nolint:gocritic // function signature required by Terraform // Retrieve values from plan var model Model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -258,17 +269,20 @@ func (r *routingTableResource) Create(ctx context.Context, req resource.CreateRe // Set state to fully populated data diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "Routing table created") } // Read refreshes the Terraform state with the latest data. -func (r *routingTableResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (r *routingTableResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -296,6 +310,7 @@ func (r *routingTableResource) Read(ctx context.Context, req resource.ReadReques }, ) resp.State.RemoveResource(ctx) + return } @@ -308,18 +323,21 @@ func (r *routingTableResource) Read(ctx context.Context, req resource.ReadReques // Set refreshed state diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "Routing table read") } // Update updates the resource and sets the updated Terraform state on success. -func (r *routingTableResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *routingTableResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { //nolint:gocritic // function signature required by Terraform // Retrieve values from plan var model Model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -338,6 +356,7 @@ func (r *routingTableResource) Update(ctx context.Context, req resource.UpdateRe var stateModel Model diags = req.State.Get(ctx, &stateModel) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -364,18 +383,21 @@ func (r *routingTableResource) Update(ctx context.Context, req resource.UpdateRe // Set refreshed state diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "Routing table updated") } // Delete deletes the resource and removes the Terraform state on success. -func (r *routingTableResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform +func (r *routingTableResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { //nolint:gocritic // function signature required by Terraform // Retrieve values from state var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -400,7 +422,7 @@ func (r *routingTableResource) Delete(ctx context.Context, req resource.DeleteRe } // ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: organization_id,region,network_area_id,routing_table_id +// The expected format of the resource import identifier is: organization_id,region,network_area_id,routing_table_id. func (r *routingTableResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { idParts := strings.Split(req.ID, core.Separator) @@ -409,6 +431,7 @@ func (r *routingTableResource) ImportState(ctx context.Context, req resource.Imp "Error importing routing table", fmt.Sprintf("Expected import identifier with format: [organization_id],[region],[network_area_id],[routing_table_id] Got: %q", req.ID), ) + return } @@ -432,6 +455,7 @@ func mapFields(ctx context.Context, routingTable *iaasalpha.RoutingTable, model if routingTable == nil { return fmt.Errorf("response input is nil") } + if model == nil { return fmt.Errorf("model input is nil") } @@ -454,10 +478,12 @@ func mapFields(ctx context.Context, routingTable *iaasalpha.RoutingTable, model // created at and updated at createdAtTF, updatedAtTF := types.StringNull(), types.StringNull() + if routingTable.CreatedAt != nil { createdAtValue := *routingTable.CreatedAt createdAtTF = types.StringValue(createdAtValue.Format(time.RFC3339)) } + if routingTable.UpdatedAt != nil { updatedAtValue := *routingTable.UpdatedAt updatedAtTF = types.StringValue(updatedAtValue.Format(time.RFC3339)) @@ -471,6 +497,7 @@ func mapFields(ctx context.Context, routingTable *iaasalpha.RoutingTable, model model.SystemRoutes = types.BoolPointerValue(routingTable.SystemRoutes) model.CreatedAt = createdAtTF model.UpdatedAt = updatedAtTF + return nil } diff --git a/stackit/internal/services/iaasalpha/routingtable/table/resource_test.go b/stackit/internal/services/iaasalpha/routingtable/table/resource_test.go index 24b1fef90..8d1df53f5 100644 --- a/stackit/internal/services/iaasalpha/routingtable/table/resource_test.go +++ b/stackit/internal/services/iaasalpha/routingtable/table/resource_test.go @@ -15,6 +15,7 @@ import ( func TestMapFields(t *testing.T) { const testRegion = "eu01" id := fmt.Sprintf("%s,%s,%s,%s", "oid", testRegion, "aid", "rtid") + tests := []struct { description string state Model @@ -104,9 +105,11 @@ func TestMapFields(t *testing.T) { if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(tt.state, tt.expected) if diff != "" { @@ -151,9 +154,11 @@ func TestToCreatePayload(t *testing.T) { if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(output, tt.expected) if diff != "" { @@ -198,9 +203,11 @@ func TestToUpdatePayload(t *testing.T) { if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(output, tt.expected, cmp.AllowUnexported(iaasalpha.NullableString{})) if diff != "" { diff --git a/stackit/internal/services/iaasalpha/routingtable/tables/datasource.go b/stackit/internal/services/iaasalpha/routingtable/tables/datasource.go index eac2257c3..467856532 100644 --- a/stackit/internal/services/iaasalpha/routingtable/tables/datasource.go +++ b/stackit/internal/services/iaasalpha/routingtable/tables/datasource.go @@ -5,8 +5,6 @@ import ( "fmt" "net/http" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaasalpha/routingtable/shared" - "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" @@ -17,6 +15,7 @@ import ( "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/features" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaasalpha/routingtable/shared" iaasalphaUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaasalpha/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" @@ -53,21 +52,26 @@ func (d *routingTablesDataSource) Metadata(_ context.Context, req datasource.Met func (d *routingTablesDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { var ok bool + d.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) if !ok { return } features.CheckExperimentEnabled(ctx, &d.providerData, features.RoutingTablesExperiment, "stackit_routing_tables", core.Datasource, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } apiClient := iaasalphaUtils.ConfigureClient(ctx, &d.providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + d.client = apiClient + tflog.Info(ctx, "IaaS client configured") } @@ -115,10 +119,11 @@ func (d *routingTablesDataSource) Schema(_ context.Context, _ datasource.SchemaR } // Read refreshes the Terraform state with the latest data. -func (d *routingTablesDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (d *routingTablesDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model DataSourceModelTables diags := req.Config.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -143,6 +148,7 @@ func (d *routingTablesDataSource) Read(ctx context.Context, req datasource.ReadR }, ) resp.State.RemoveResource(ctx) + return } @@ -151,11 +157,14 @@ func (d *routingTablesDataSource) Read(ctx context.Context, req datasource.ReadR core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading routing table", fmt.Sprintf("Processing API payload: %v", err)) return } + diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "Routing table read") } @@ -163,9 +172,11 @@ func mapDataSourceRoutingTables(ctx context.Context, routingTables *iaasalpha.Ro if routingTables == nil { return fmt.Errorf("response input is nil") } + if model == nil { return fmt.Errorf("model input is nil") } + if routingTables.Items == nil { return fmt.Errorf("items input is nil") } @@ -176,8 +187,10 @@ func mapDataSourceRoutingTables(ctx context.Context, routingTables *iaasalpha.Ro model.Id = utils.BuildInternalTerraformId(organizationId, region, networkAreaId) itemsList := []attr.Value{} + for i, routingTable := range *routingTables.Items { var routingTableModel shared.RoutingTableReadModel + err := shared.MapRoutingTableReadModel(ctx, &routingTable, &routingTableModel) if err != nil { return fmt.Errorf("mapping routes: %w", err) @@ -198,6 +211,7 @@ func mapDataSourceRoutingTables(ctx context.Context, routingTables *iaasalpha.Ro if diags.HasError() { return fmt.Errorf("mapping index %d: %w", i, core.DiagsToError(diags)) } + itemsList = append(itemsList, routingTableTF) } @@ -205,6 +219,7 @@ func mapDataSourceRoutingTables(ctx context.Context, routingTables *iaasalpha.Ro if diags.HasError() { return core.DiagsToError(diags) } + model.Items = itemsListTF model.Region = types.StringValue(region) diff --git a/stackit/internal/services/iaasalpha/routingtable/tables/datasource_test.go b/stackit/internal/services/iaasalpha/routingtable/tables/datasource_test.go index 2df93e796..9cc0783f7 100644 --- a/stackit/internal/services/iaasalpha/routingtable/tables/datasource_test.go +++ b/stackit/internal/services/iaasalpha/routingtable/tables/datasource_test.go @@ -6,14 +6,13 @@ import ( "testing" "time" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaasalpha/routingtable/shared" - "github.com/google/go-cmp/cmp" "github.com/google/uuid" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/stackitcloud/stackit-sdk-go/core/utils" "github.com/stackitcloud/stackit-sdk-go/services/iaasalpha" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaasalpha/routingtable/shared" ) const ( @@ -161,9 +160,11 @@ func TestMapDataFields(t *testing.T) { if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(tt.state, tt.expected) if diff != "" { diff --git a/stackit/internal/services/iaasalpha/utils/util.go b/stackit/internal/services/iaasalpha/utils/util.go index 40216b924..b6d971269 100644 --- a/stackit/internal/services/iaasalpha/utils/util.go +++ b/stackit/internal/services/iaasalpha/utils/util.go @@ -19,6 +19,7 @@ func ConfigureClient(ctx context.Context, providerData *core.ProviderData, diags if providerData.IaaSCustomEndpoint != "" { apiClientConfigOptions = append(apiClientConfigOptions, config.WithEndpoint(providerData.IaaSCustomEndpoint)) } + apiClient, err := iaasalpha.NewAPIClient(apiClientConfigOptions...) if err != nil { core.LogAndAddError(ctx, diags, "Error configuring API client", fmt.Sprintf("Configuring client: %v. This is an error related to the provider configuration, not to the resource configuration", err)) diff --git a/stackit/internal/services/iaasalpha/utils/util_test.go b/stackit/internal/services/iaasalpha/utils/util_test.go index d4ba46713..655590a31 100644 --- a/stackit/internal/services/iaasalpha/utils/util_test.go +++ b/stackit/internal/services/iaasalpha/utils/util_test.go @@ -22,6 +22,7 @@ const ( func TestConfigureClient(t *testing.T) { /* mock authentication by setting service account token env variable */ os.Clearenv() + err := os.Setenv(sdkClients.ServiceAccountToken, "mock-val") if err != nil { t.Errorf("error setting env variable: %v", err) @@ -30,6 +31,7 @@ func TestConfigureClient(t *testing.T) { type args struct { providerData *core.ProviderData } + tests := []struct { name string args args @@ -50,6 +52,7 @@ func TestConfigureClient(t *testing.T) { if err != nil { t.Errorf("error configuring client: %v", err) } + return apiClient }(), wantErr: false, @@ -70,6 +73,7 @@ func TestConfigureClient(t *testing.T) { if err != nil { t.Errorf("error configuring client: %v", err) } + return apiClient }(), wantErr: false, @@ -81,6 +85,7 @@ func TestConfigureClient(t *testing.T) { diags := diag.Diagnostics{} actual := ConfigureClient(ctx, tt.args.providerData, &diags) + if diags.HasError() != tt.wantErr { t.Errorf("ConfigureClient() error = %v, want %v", diags.HasError(), tt.wantErr) } diff --git a/stackit/internal/services/loadbalancer/loadbalancer/datasource.go b/stackit/internal/services/loadbalancer/loadbalancer/datasource.go index 393ba022c..0fb6d9c4f 100644 --- a/stackit/internal/services/loadbalancer/loadbalancer/datasource.go +++ b/stackit/internal/services/loadbalancer/loadbalancer/datasource.go @@ -5,22 +5,20 @@ import ( "fmt" "net/http" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - loadbalancerUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/loadbalancer/utils" - "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" "github.com/hashicorp/terraform-plugin-framework-validators/setvalidator" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/stackitcloud/stackit-sdk-go/services/loadbalancer" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + loadbalancerUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/loadbalancer/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - - "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/stackitcloud/stackit-sdk-go/services/loadbalancer" ) // Ensure the implementation satisfies the expected interfaces. @@ -47,16 +45,20 @@ func (r *loadBalancerDataSource) Metadata(_ context.Context, req datasource.Meta // Configure adds the provider configured client to the data source. func (r *loadBalancerDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { var ok bool + r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) if !ok { return } apiClient := loadbalancerUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + r.client = apiClient + tflog.Info(ctx, "Load balancer client configured") } @@ -378,13 +380,15 @@ func (r *loadBalancerDataSource) Schema(_ context.Context, _ datasource.SchemaRe } // Read refreshes the Terraform state with the latest data. -func (r *loadBalancerDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (r *loadBalancerDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.Config.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() name := model.Name.ValueString() region := r.providerData.GetRegionWithOverride(model.Region) @@ -405,6 +409,7 @@ func (r *loadBalancerDataSource) Read(ctx context.Context, req datasource.ReadRe }, ) resp.State.RemoveResource(ctx) + return } @@ -418,8 +423,10 @@ func (r *loadBalancerDataSource) Read(ctx context.Context, req datasource.ReadRe // Set refreshed state diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "Load balancer read") } diff --git a/stackit/internal/services/loadbalancer/loadbalancer/resource.go b/stackit/internal/services/loadbalancer/loadbalancer/resource.go index e6aa031f8..cb7b176dc 100644 --- a/stackit/internal/services/loadbalancer/loadbalancer/resource.go +++ b/stackit/internal/services/loadbalancer/loadbalancer/resource.go @@ -7,10 +7,6 @@ import ( "strings" "time" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" - - loadbalancerUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/loadbalancer/utils" - "github.com/google/uuid" "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" "github.com/hashicorp/terraform-plugin-framework-validators/setvalidator" @@ -20,6 +16,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" "github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier" @@ -37,6 +34,7 @@ import ( "github.com/stackitcloud/stackit-sdk-go/services/loadbalancer/wait" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + loadbalancerUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/loadbalancer/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" ) @@ -65,7 +63,7 @@ type Model struct { SecurityGroupId types.String `tfsdk:"security_group_id"` } -// Struct corresponding to Model.Listeners[i] +// Struct corresponding to Model.Listeners[i]. type listener struct { DisplayName types.String `tfsdk:"display_name"` Port types.Int64 `tfsdk:"port"` @@ -76,7 +74,7 @@ type listener struct { UDP types.Object `tfsdk:"udp"` } -// Types corresponding to listener +// Types corresponding to listener. var listenerTypes = map[string]attr.Type{ "display_name": types.StringType, "port": types.Int64Type, @@ -87,12 +85,12 @@ var listenerTypes = map[string]attr.Type{ "udp": types.ObjectType{AttrTypes: udpTypes}, } -// Struct corresponding to listener.ServerNameIndicators[i] +// Struct corresponding to listener.ServerNameIndicators[i]. type serverNameIndicator struct { Name types.String `tfsdk:"name"` } -// Types corresponding to serverNameIndicator +// Types corresponding to serverNameIndicator. var serverNameIndicatorTypes = map[string]attr.Type{ "name": types.StringType, } @@ -113,26 +111,26 @@ var udpTypes = map[string]attr.Type{ "idle_timeout": types.StringType, } -// Struct corresponding to Model.Networks[i] +// Struct corresponding to Model.Networks[i]. type network struct { NetworkId types.String `tfsdk:"network_id"` Role types.String `tfsdk:"role"` } -// Types corresponding to network +// Types corresponding to network. var networkTypes = map[string]attr.Type{ "network_id": types.StringType, "role": types.StringType, } -// Struct corresponding to Model.Options +// Struct corresponding to Model.Options. type options struct { ACL types.Set `tfsdk:"acl"` PrivateNetworkOnly types.Bool `tfsdk:"private_network_only"` Observability types.Object `tfsdk:"observability"` } -// Types corresponding to options +// Types corresponding to options. var optionsTypes = map[string]attr.Type{ "acl": types.SetType{ElemType: types.StringType}, "private_network_only": types.BoolType, @@ -159,7 +157,7 @@ var observabilityOptionTypes = map[string]attr.Type{ "push_url": types.StringType, } -// Struct corresponding to Model.TargetPools[i] +// Struct corresponding to Model.TargetPools[i]. type targetPool struct { ActiveHealthCheck types.Object `tfsdk:"active_health_check"` Name types.String `tfsdk:"name"` @@ -168,7 +166,7 @@ type targetPool struct { SessionPersistence types.Object `tfsdk:"session_persistence"` } -// Types corresponding to targetPool +// Types corresponding to targetPool. var targetPoolTypes = map[string]attr.Type{ "active_health_check": types.ObjectType{AttrTypes: activeHealthCheckTypes}, "name": types.StringType, @@ -177,7 +175,7 @@ var targetPoolTypes = map[string]attr.Type{ "session_persistence": types.ObjectType{AttrTypes: sessionPersistenceTypes}, } -// Struct corresponding to targetPool.ActiveHealthCheck +// Struct corresponding to targetPool.ActiveHealthCheck. type activeHealthCheck struct { HealthyThreshold types.Int64 `tfsdk:"healthy_threshold"` Interval types.String `tfsdk:"interval"` @@ -186,7 +184,7 @@ type activeHealthCheck struct { UnhealthyThreshold types.Int64 `tfsdk:"unhealthy_threshold"` } -// Types corresponding to activeHealthCheck +// Types corresponding to activeHealthCheck. var activeHealthCheckTypes = map[string]attr.Type{ "healthy_threshold": types.Int64Type, "interval": types.StringType, @@ -195,24 +193,24 @@ var activeHealthCheckTypes = map[string]attr.Type{ "unhealthy_threshold": types.Int64Type, } -// Struct corresponding to targetPool.Targets[i] +// Struct corresponding to targetPool.Targets[i]. type target struct { DisplayName types.String `tfsdk:"display_name"` Ip types.String `tfsdk:"ip"` } -// Types corresponding to target +// Types corresponding to target. var targetTypes = map[string]attr.Type{ "display_name": types.StringType, "ip": types.StringType, } -// Struct corresponding to targetPool.SessionPersistence +// Struct corresponding to targetPool.SessionPersistence. type sessionPersistence struct { UseSourceIPAddress types.Bool `tfsdk:"use_source_ip_address"` } -// Types corresponding to SessionPersistence +// Types corresponding to SessionPersistence. var sessionPersistenceTypes = map[string]attr.Type{ "use_source_ip_address": types.BoolType, } @@ -235,38 +233,46 @@ func (r *loadBalancerResource) Metadata(_ context.Context, req resource.Metadata // ModifyPlan implements resource.ResourceWithModifyPlan. // Use the modifier to set the effective region in the current plan. -func (r *loadBalancerResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform +func (r *loadBalancerResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { //nolint:gocritic // function signature required by Terraform var configModel Model // skip initial empty configuration to avoid follow-up errors if req.Config.Raw.IsNull() { return } + resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...) + if resp.Diagnostics.HasError() { return } var planModel Model + resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) + if resp.Diagnostics.HasError() { return } utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp) + if resp.Diagnostics.HasError() { return } resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...) + if resp.Diagnostics.HasError() { return } } -// ConfigValidators validates the resource configuration +// ConfigValidators validates the resource configuration. func (r *loadBalancerResource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) { var model Model + resp.Diagnostics.Append(req.Config.Get(ctx, &model)...) + if resp.Diagnostics.HasError() { return } @@ -284,13 +290,16 @@ func validateConfig(ctx context.Context, diags *diag.Diagnostics, model *Model) if !externalAddressIsSet { core.LogAndAddError(ctx, diags, "Error configuring load balancer", fmt.Sprintf("You need to provide either the `options.private_network_only = true` or `external_address` field. %v", err)) } + return } + if lbOptions.PrivateNetworkOnly == nil || !*lbOptions.PrivateNetworkOnly { // private_network_only is not set or false and external_address is not set if !externalAddressIsSet { core.LogAndAddError(ctx, diags, "Error configuring load balancer", "You need to provide either the `options.private_network_only = true` or `external_address` field.") } + return } @@ -303,16 +312,20 @@ func validateConfig(ctx context.Context, diags *diag.Diagnostics, model *Model) // Configure adds the provider configured client to the resource. func (r *loadBalancerResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { var ok bool + r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) if !ok { return } apiClient := loadbalancerUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + r.client = apiClient + tflog.Info(ctx, "Load Balancer client configured") } @@ -742,14 +755,16 @@ The example below creates the supporting infrastructure using the STACKIT Terraf } // Create creates the resource and sets the initial Terraform state. -func (r *loadBalancerResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *loadBalancerResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { //nolint:gocritic // function signature required by Terraform // Retrieve values from plan var model Model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() region := model.Region.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) @@ -785,6 +800,7 @@ func (r *loadBalancerResource) Create(ctx context.Context, req resource.CreateRe // Set state to fully populated data diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -793,13 +809,15 @@ func (r *loadBalancerResource) Create(ctx context.Context, req resource.CreateRe } // Read refreshes the Terraform state with the latest data. -func (r *loadBalancerResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (r *loadBalancerResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() name := model.Name.ValueString() region := r.providerData.GetRegionWithOverride(model.Region) @@ -815,7 +833,9 @@ func (r *loadBalancerResource) Read(ctx context.Context, req resource.ReadReques resp.State.RemoveResource(ctx) return } + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading load balancer", fmt.Sprintf("Calling API: %v", err)) + return } @@ -829,21 +849,25 @@ func (r *loadBalancerResource) Read(ctx context.Context, req resource.ReadReques // Set refreshed state diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "Load balancer read") } // Update updates the resource and sets the updated Terraform state on success. -func (r *loadBalancerResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *loadBalancerResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { //nolint:gocritic // function signature required by Terraform // Retrieve values from plan var model Model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() name := model.Name.ValueString() region := model.Region.ValueString() @@ -854,9 +878,11 @@ func (r *loadBalancerResource) Update(ctx context.Context, req resource.UpdateRe targetPoolsModel := []targetPool{} diags = model.TargetPools.ElementsAs(ctx, &targetPoolsModel, false) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + for i := range targetPoolsModel { targetPoolModel := targetPoolsModel[i] targetPoolName := targetPoolModel.Name.ValueString() @@ -876,6 +902,7 @@ func (r *loadBalancerResource) Update(ctx context.Context, req resource.UpdateRe return } } + ctx = tflog.SetField(ctx, "target_pool_name", nil) // Get updated load balancer @@ -895,6 +922,7 @@ func (r *loadBalancerResource) Update(ctx context.Context, req resource.UpdateRe // Set state to fully populated data diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -903,13 +931,15 @@ func (r *loadBalancerResource) Update(ctx context.Context, req resource.UpdateRe } // Delete deletes the resource and removes the Terraform state on success. -func (r *loadBalancerResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform +func (r *loadBalancerResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() name := model.Name.ValueString() region := model.Region.ValueString() @@ -934,7 +964,7 @@ func (r *loadBalancerResource) Delete(ctx context.Context, req resource.DeleteRe } // ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: project_id,name +// The expected format of the resource import identifier is: project_id,name. func (r *loadBalancerResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { idParts := strings.Split(req.ID, core.Separator) @@ -943,6 +973,7 @@ func (r *loadBalancerResource) ImportState(ctx context.Context, req resource.Imp "Error importing load balancer", fmt.Sprintf("Expected import identifier with format: [project_id],[region],[name] Got: %q", req.ID), ) + return } @@ -962,14 +993,17 @@ func toCreatePayload(ctx context.Context, model *Model) (*loadbalancer.CreateLoa if err != nil { return nil, fmt.Errorf("converting listeners: %w", err) } + networksPayload, err := toNetworksPayload(ctx, model) if err != nil { return nil, fmt.Errorf("converting networks: %w", err) } + optionsPayload, err := toOptionsPayload(ctx, model) if err != nil { return nil, fmt.Errorf("converting options: %w", err) } + targetPoolsPayload, err := toTargetPoolsPayload(ctx, model) if err != nil { return nil, fmt.Errorf("converting target_pools: %w", err) @@ -993,6 +1027,7 @@ func toListenersPayload(ctx context.Context, model *Model) (*[]loadbalancer.List } listenersModel := []listener{} + diags := model.Listeners.ElementsAs(ctx, &listenersModel, false) if diags.HasError() { return nil, core.DiagsToError(diags) @@ -1003,20 +1038,25 @@ func toListenersPayload(ctx context.Context, model *Model) (*[]loadbalancer.List } payload := []loadbalancer.Listener{} + for i := range listenersModel { listenerModel := listenersModel[i] + serverNameIndicatorsPayload, err := toServerNameIndicatorsPayload(ctx, &listenerModel) if err != nil { return nil, fmt.Errorf("converting index %d: converting server_name_indicator: %w", i, err) } + tcp, err := toTCP(ctx, &listenerModel) if err != nil { return nil, fmt.Errorf("converting index %d: converting tcp: %w", i, err) } + udp, err := toUDP(ctx, &listenerModel) if err != nil { return nil, fmt.Errorf("converting index %d: converting udp: %w", i, err) } + payload = append(payload, loadbalancer.Listener{ DisplayName: conversion.StringValueToPointer(listenerModel.DisplayName), Port: conversion.Int64ValueToPointer(listenerModel.Port), @@ -1037,12 +1077,14 @@ func toServerNameIndicatorsPayload(ctx context.Context, l *listener) (*[]loadbal } serverNameIndicatorsModel := []serverNameIndicator{} + diags := l.ServerNameIndicators.ElementsAs(ctx, &serverNameIndicatorsModel, false) if diags.HasError() { return nil, core.DiagsToError(diags) } payload := []loadbalancer.ServerNameIndicator{} + for i := range serverNameIndicatorsModel { indicatorModel := serverNameIndicatorsModel[i] payload = append(payload, loadbalancer.ServerNameIndicator{ @@ -1059,10 +1101,12 @@ func toTCP(ctx context.Context, listener *listener) (*loadbalancer.OptionsTCP, e } tcp := tcp{} + diags := listener.TCP.As(ctx, &tcp, basetypes.ObjectAsOptions{}) if diags.HasError() { return nil, core.DiagsToError(diags) } + if tcp.IdleTimeout.IsNull() || tcp.IdleTimeout.IsUnknown() { return nil, nil } @@ -1078,10 +1122,12 @@ func toUDP(ctx context.Context, listener *listener) (*loadbalancer.OptionsUDP, e } udp := udp{} + diags := listener.UDP.As(ctx, &udp, basetypes.ObjectAsOptions{}) if diags.HasError() { return nil, core.DiagsToError(diags) } + if udp.IdleTimeout.IsNull() || udp.IdleTimeout.IsUnknown() { return nil, nil } @@ -1097,6 +1143,7 @@ func toNetworksPayload(ctx context.Context, model *Model) (*[]loadbalancer.Netwo } networksModel := []network{} + diags := model.Networks.ElementsAs(ctx, &networksModel, false) if diags.HasError() { return nil, core.DiagsToError(diags) @@ -1107,6 +1154,7 @@ func toNetworksPayload(ctx context.Context, model *Model) (*[]loadbalancer.Netwo } payload := []loadbalancer.Network{} + for i := range networksModel { networkModel := networksModel[i] payload = append(payload, loadbalancer.Network{ @@ -1127,24 +1175,30 @@ func toOptionsPayload(ctx context.Context, model *Model) (*loadbalancer.LoadBala } optionsModel := options{} + diags := model.Options.As(ctx, &optionsModel, basetypes.ObjectAsOptions{}) if diags.HasError() { return nil, core.DiagsToError(diags) } accessControlPayload := &loadbalancer.LoadbalancerOptionAccessControl{} - if !(optionsModel.ACL.IsNull() || optionsModel.ACL.IsUnknown()) { + + if !optionsModel.ACL.IsNull() && !optionsModel.ACL.IsUnknown() { var aclModel []string + diags := optionsModel.ACL.ElementsAs(ctx, &aclModel, false) if diags.HasError() { return nil, fmt.Errorf("converting acl: %w", core.DiagsToError(diags)) } + accessControlPayload.AllowedSourceRanges = &aclModel } observabilityPayload := &loadbalancer.LoadbalancerOptionObservability{} - if !(optionsModel.Observability.IsNull() || optionsModel.Observability.IsUnknown()) { + + if !optionsModel.Observability.IsNull() && !optionsModel.Observability.IsUnknown() { observabilityModel := observability{} + diags := optionsModel.Observability.As(ctx, &observabilityModel, basetypes.ObjectAsOptions{}) if diags.HasError() { return nil, fmt.Errorf("converting observability: %w", core.DiagsToError(diags)) @@ -1152,10 +1206,12 @@ func toOptionsPayload(ctx context.Context, model *Model) (*loadbalancer.LoadBala // observability logs observabilityLogsModel := observabilityOption{} + diags = observabilityModel.Logs.As(ctx, &observabilityLogsModel, basetypes.ObjectAsOptions{}) if diags.HasError() { return nil, fmt.Errorf("converting observability logs: %w", core.DiagsToError(diags)) } + observabilityPayload.Logs = &loadbalancer.LoadbalancerOptionLogs{ CredentialsRef: observabilityLogsModel.CredentialsRef.ValueStringPointer(), PushUrl: observabilityLogsModel.PushUrl.ValueStringPointer(), @@ -1163,10 +1219,12 @@ func toOptionsPayload(ctx context.Context, model *Model) (*loadbalancer.LoadBala // observability metrics observabilityMetricsModel := observabilityOption{} + diags = observabilityModel.Metrics.As(ctx, &observabilityMetricsModel, basetypes.ObjectAsOptions{}) if diags.HasError() { return nil, fmt.Errorf("converting observability metrics: %w", core.DiagsToError(diags)) } + observabilityPayload.Metrics = &loadbalancer.LoadbalancerOptionMetrics{ CredentialsRef: observabilityMetricsModel.CredentialsRef.ValueStringPointer(), PushUrl: observabilityMetricsModel.PushUrl.ValueStringPointer(), @@ -1188,6 +1246,7 @@ func toTargetPoolsPayload(ctx context.Context, model *Model) (*[]loadbalancer.Ta } targetPoolsModel := []targetPool{} + diags := model.TargetPools.ElementsAs(ctx, &targetPoolsModel, false) if diags.HasError() { return nil, core.DiagsToError(diags) @@ -1198,6 +1257,7 @@ func toTargetPoolsPayload(ctx context.Context, model *Model) (*[]loadbalancer.Ta } payload := []loadbalancer.TargetPool{} + for i := range targetPoolsModel { targetPoolModel := targetPoolsModel[i] @@ -1205,10 +1265,12 @@ func toTargetPoolsPayload(ctx context.Context, model *Model) (*[]loadbalancer.Ta if err != nil { return nil, fmt.Errorf("converting index %d: converting active_health_check: %w", i, err) } + sessionPersistencePayload, err := toSessionPersistencePayload(ctx, &targetPoolModel) if err != nil { return nil, fmt.Errorf("converting index %d: converting session_persistence: %w", i, err) } + targetsPayload, err := toTargetsPayload(ctx, &targetPoolModel) if err != nil { return nil, fmt.Errorf("converting index %d: converting targets: %w", i, err) @@ -1235,10 +1297,12 @@ func toTargetPoolUpdatePayload(ctx context.Context, tp *targetPool) (*loadbalanc if err != nil { return nil, fmt.Errorf("converting active_health_check: %w", err) } + sessionPersistencePayload, err := toSessionPersistencePayload(ctx, tp) if err != nil { return nil, fmt.Errorf("converting session_persistence: %w", err) } + targetsPayload, err := toTargetsPayload(ctx, tp) if err != nil { return nil, fmt.Errorf("converting targets: %w", err) @@ -1259,6 +1323,7 @@ func toSessionPersistencePayload(ctx context.Context, tp *targetPool) (*loadbala } sessionPersistenceModel := sessionPersistence{} + diags := tp.SessionPersistence.As(ctx, &sessionPersistenceModel, basetypes.ObjectAsOptions{}) if diags.HasError() { return nil, core.DiagsToError(diags) @@ -1275,6 +1340,7 @@ func toActiveHealthCheckPayload(ctx context.Context, tp *targetPool) (*loadbalan } activeHealthCheckModel := activeHealthCheck{} + diags := tp.ActiveHealthCheck.As(ctx, &activeHealthCheckModel, basetypes.ObjectAsOptions{}) if diags.HasError() { return nil, fmt.Errorf("converting active health check: %w", core.DiagsToError(diags)) @@ -1295,6 +1361,7 @@ func toTargetsPayload(ctx context.Context, tp *targetPool) (*[]loadbalancer.Targ } targetsModel := []target{} + diags := tp.Targets.ElementsAs(ctx, &targetsModel, false) if diags.HasError() { return nil, fmt.Errorf("converting Targets list: %w", core.DiagsToError(diags)) @@ -1305,6 +1372,7 @@ func toTargetsPayload(ctx context.Context, tp *targetPool) (*[]loadbalancer.Targ } payload := []loadbalancer.Target{} + for i := range targetsModel { targetModel := targetsModel[i] payload = append(payload, loadbalancer.Target{ @@ -1321,6 +1389,7 @@ func mapFields(ctx context.Context, lb *loadbalancer.LoadBalancer, m *Model, reg if lb == nil { return fmt.Errorf("response input is nil") } + if m == nil { return fmt.Errorf("model input is nil") } @@ -1333,6 +1402,7 @@ func mapFields(ctx context.Context, lb *loadbalancer.LoadBalancer, m *Model, reg } else { return fmt.Errorf("name not present") } + m.Region = types.StringValue(region) m.Name = types.StringValue(name) m.Id = utils.BuildInternalTerraformId(m.ProjectId.ValueString(), m.Region.ValueString(), name) @@ -1347,18 +1417,22 @@ func mapFields(ctx context.Context, lb *loadbalancer.LoadBalancer, m *Model, reg } else { m.SecurityGroupId = types.StringNull() } + err := mapListeners(lb, m) if err != nil { return fmt.Errorf("mapping listeners: %w", err) } + err = mapNetworks(lb, m) if err != nil { return fmt.Errorf("mapping network: %w", err) } + err = mapOptions(ctx, lb, m) if err != nil { return fmt.Errorf("mapping options: %w", err) } + err = mapTargetPools(lb, m) if err != nil { return fmt.Errorf("mapping target pools: %w", err) @@ -1374,6 +1448,7 @@ func mapListeners(loadBalancerResp *loadbalancer.LoadBalancer, m *Model) error { } listenersList := []attr.Value{} + for i, listenerResp := range *loadBalancerResp.Listeners { listenerMap := map[string]attr.Value{ "display_name": types.StringPointerValue(listenerResp.DisplayName), @@ -1414,6 +1489,7 @@ func mapListeners(loadBalancerResp *loadbalancer.LoadBalancer, m *Model) error { } m.Listeners = listenersTF + return nil } @@ -1424,6 +1500,7 @@ func mapServerNameIndicators(serverNameIndicatorsResp *[]loadbalancer.ServerName } serverNameIndicatorsList := []attr.Value{} + for i, serverNameIndicatorResp := range *serverNameIndicatorsResp { serverNameIndicatorMap := map[string]attr.Value{ "name": types.StringPointerValue(serverNameIndicatorResp.Name), @@ -1446,6 +1523,7 @@ func mapServerNameIndicators(serverNameIndicatorsResp *[]loadbalancer.ServerName } l["server_name_indicators"] = serverNameIndicatorsTF + return nil } @@ -1463,6 +1541,7 @@ func mapTCP(tcp *loadbalancer.OptionsTCP, listener map[string]attr.Value) error } listener["tcp"] = tcpAttr + return nil } @@ -1480,6 +1559,7 @@ func mapUDP(udp *loadbalancer.OptionsUDP, listener map[string]attr.Value) error } listener["udp"] = udpAttr + return nil } @@ -1490,6 +1570,7 @@ func mapNetworks(loadBalancerResp *loadbalancer.LoadBalancer, m *Model) error { } networksList := []attr.Value{} + for i, networkResp := range *loadBalancerResp.Networks { networkMap := map[string]attr.Value{ "network_id": types.StringPointerValue(networkResp.NetworkId), @@ -1513,6 +1594,7 @@ func mapNetworks(loadBalancerResp *loadbalancer.LoadBalancer, m *Model) error { } m.Networks = networksTF + return nil } @@ -1528,10 +1610,12 @@ func mapOptions(ctx context.Context, loadBalancerResp *loadbalancer.LoadBalancer // we set it to false in the TF state to prevent an inconsistent result after apply error if !m.Options.IsNull() && !m.Options.IsUnknown() { optionsModel := options{} + diags := m.Options.As(ctx, &optionsModel, basetypes.ObjectAsOptions{}) if diags.HasError() { return fmt.Errorf("convert options: %w", core.DiagsToError(diags)) } + if loadBalancerResp.Options.PrivateNetworkOnly == nil && !optionsModel.PrivateNetworkOnly.IsNull() && !optionsModel.PrivateNetworkOnly.IsUnknown() && !optionsModel.PrivateNetworkOnly.ValueBool() { privateNetworkOnlyTF = types.BoolValue(false) } @@ -1554,6 +1638,7 @@ func mapOptions(ctx context.Context, loadBalancerResp *loadbalancer.LoadBalancer observabilityLogsMap["credentials_ref"] = types.StringPointerValue(loadBalancerResp.Options.Observability.Logs.CredentialsRef) observabilityLogsMap["push_url"] = types.StringPointerValue(loadBalancerResp.Options.Observability.Logs.PushUrl) } + observabilityLogsTF, diags := types.ObjectValue(observabilityOptionTypes, observabilityLogsMap) if diags.HasError() { return core.DiagsToError(diags) @@ -1567,6 +1652,7 @@ func mapOptions(ctx context.Context, loadBalancerResp *loadbalancer.LoadBalancer observabilityMetricsMap["credentials_ref"] = types.StringPointerValue(loadBalancerResp.Options.Observability.Metrics.CredentialsRef) observabilityMetricsMap["push_url"] = types.StringPointerValue(loadBalancerResp.Options.Observability.Metrics.PushUrl) } + observabilityMetricsTF, diags := types.ObjectValue(observabilityOptionTypes, observabilityMetricsMap) if diags.HasError() { return core.DiagsToError(diags) @@ -1576,10 +1662,12 @@ func mapOptions(ctx context.Context, loadBalancerResp *loadbalancer.LoadBalancer "logs": observabilityLogsTF, "metrics": observabilityMetricsTF, } + observabilityTF, diags := types.ObjectValue(observabilityTypes, observabilityMap) if diags.HasError() { return core.DiagsToError(diags) } + optionsMap["observability"] = observabilityTF optionsTF, diags := types.ObjectValue(optionsTypes, optionsMap) @@ -1588,6 +1676,7 @@ func mapOptions(ctx context.Context, loadBalancerResp *loadbalancer.LoadBalancer } m.Options = optionsTF + return nil } @@ -1598,6 +1687,7 @@ func mapACL(accessControlResp *loadbalancer.LoadbalancerOptionAccessControl, o m } aclList := []attr.Value{} + for _, rangeResp := range *accessControlResp.AllowedSourceRanges { rangeTF := types.StringValue(rangeResp) aclList = append(aclList, rangeTF) @@ -1609,6 +1699,7 @@ func mapACL(accessControlResp *loadbalancer.LoadbalancerOptionAccessControl, o m } o["acl"] = aclTF + return nil } @@ -1619,6 +1710,7 @@ func mapTargetPools(loadBalancerResp *loadbalancer.LoadBalancer, m *Model) error } targetPoolsList := []attr.Value{} + for i, targetPoolResp := range *loadBalancerResp.TargetPools { targetPoolMap := map[string]attr.Value{ "name": types.StringPointerValue(targetPoolResp.Name), @@ -1657,6 +1749,7 @@ func mapTargetPools(loadBalancerResp *loadbalancer.LoadBalancer, m *Model) error } m.TargetPools = targetPoolsTF + return nil } @@ -1680,6 +1773,7 @@ func mapActiveHealthCheck(activeHealthCheckResp *loadbalancer.ActiveHealthCheck, } tp["active_health_check"] = activeHealthCheckTF + return nil } @@ -1690,6 +1784,7 @@ func mapTargets(targetsResp *[]loadbalancer.Target, tp map[string]attr.Value) er } targetsList := []attr.Value{} + for i, targetResp := range *targetsResp { targetMap := map[string]attr.Value{ "display_name": types.StringPointerValue(targetResp.DisplayName), @@ -1713,6 +1808,7 @@ func mapTargets(targetsResp *[]loadbalancer.Target, tp map[string]attr.Value) er } tp["targets"] = targetsTF + return nil } @@ -1732,5 +1828,6 @@ func mapSessionPersistence(sessionPersistenceResp *loadbalancer.SessionPersisten } tp["session_persistence"] = sessionPersistenceTF + return nil } diff --git a/stackit/internal/services/loadbalancer/loadbalancer/resource_test.go b/stackit/internal/services/loadbalancer/loadbalancer/resource_test.go index 831ae1f47..7fe594b02 100644 --- a/stackit/internal/services/loadbalancer/loadbalancer/resource_test.go +++ b/stackit/internal/services/loadbalancer/loadbalancer/resource_test.go @@ -354,9 +354,11 @@ func TestToCreatePayload(t *testing.T) { if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(output, tt.expected) if diff != "" { @@ -437,9 +439,11 @@ func TestToTargetPoolUpdatePayload(t *testing.T) { if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(output, tt.expected) if diff != "" { @@ -453,6 +457,7 @@ func TestToTargetPoolUpdatePayload(t *testing.T) { func TestMapFields(t *testing.T) { const testRegion = "eu01" id := fmt.Sprintf("%s,%s,%s", "pid", testRegion, "name") + tests := []struct { description string input *loadbalancer.LoadBalancer @@ -854,13 +859,16 @@ func TestMapFields(t *testing.T) { }), }) } + err := mapFields(context.Background(), tt.input, model, tt.region) if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(model, tt.expected) if diff != "" { @@ -876,6 +884,7 @@ func Test_validateConfig(t *testing.T) { ExternalAddress *string PrivateNetworkOnly *bool } + tests := []struct { name string args args diff --git a/stackit/internal/services/loadbalancer/loadbalancer_acc_test.go b/stackit/internal/services/loadbalancer/loadbalancer_acc_test.go index 909053384..2448d720b 100644 --- a/stackit/internal/services/loadbalancer/loadbalancer_acc_test.go +++ b/stackit/internal/services/loadbalancer/loadbalancer_acc_test.go @@ -4,6 +4,7 @@ import ( "context" _ "embed" "fmt" + "maps" "strings" "testing" @@ -11,14 +12,11 @@ import ( "github.com/hashicorp/terraform-plugin-testing/helper/acctest" "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/terraform" - "github.com/stackitcloud/stackit-sdk-go/services/loadbalancer/wait" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - - "maps" - stackitSdkConfig "github.com/stackitcloud/stackit-sdk-go/core/config" "github.com/stackitcloud/stackit-sdk-go/core/utils" "github.com/stackitcloud/stackit-sdk-go/services/loadbalancer" + "github.com/stackitcloud/stackit-sdk-go/services/loadbalancer/wait" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/testutil" ) @@ -96,6 +94,7 @@ func configVarsMinUpdated() config.Variables { tempConfig := make(config.Variables, len(testConfigVarsMin)) maps.Copy(tempConfig, testConfigVarsMin) tempConfig["target_port"] = config.StringVariable("5431") + return tempConfig } @@ -103,6 +102,7 @@ func configVarsMaxUpdated() config.Variables { tempConfig := make(config.Variables, len(testConfigVarsMax)) maps.Copy(tempConfig, testConfigVarsMax) tempConfig["sni_target_port"] = config.StringVariable("5431") + return tempConfig } @@ -192,7 +192,8 @@ func TestAccLoadBalancerResourceMin(t *testing.T) { "stackit_loadbalancer.loadbalancer", "security_group_id", "data.stackit_loadbalancer.loadbalancer", "security_group_id", ), - )}, + ), + }, // Import { ConfigVariables: testConfigVarsMin, @@ -210,6 +211,7 @@ func TestAccLoadBalancerResourceMin(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute region") } + return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, region, name), nil }, ImportState: true, @@ -368,7 +370,8 @@ func TestAccLoadBalancerResourceMax(t *testing.T) { "stackit_loadbalancer.loadbalancer", "security_group_id", "data.stackit_loadbalancer.loadbalancer", "security_group_id", ), - )}, + ), + }, // Import { ConfigVariables: testConfigVarsMax, @@ -386,6 +389,7 @@ func TestAccLoadBalancerResourceMax(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute region") } + return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, region, name), nil }, ImportState: true, @@ -409,7 +413,9 @@ func TestAccLoadBalancerResourceMax(t *testing.T) { func testAccCheckLoadBalancerDestroy(s *terraform.State) error { ctx := context.Background() + var client *loadbalancer.APIClient + var err error if testutil.LoadBalancerCustomEndpoint == "" { client, err = loadbalancer.NewAPIClient() @@ -418,6 +424,7 @@ func testAccCheckLoadBalancerDestroy(s *terraform.State) error { stackitSdkConfig.WithEndpoint(testutil.LoadBalancerCustomEndpoint), ) } + if err != nil { return fmt.Errorf("creating client: %w", err) } @@ -426,7 +433,9 @@ func testAccCheckLoadBalancerDestroy(s *terraform.State) error { if testutil.Region != "" { region = testutil.Region } + loadbalancersToDestroy := []string{} + for _, rs := range s.RootModule().Resources { if rs.Type != "stackit_loadbalancer" { continue @@ -451,16 +460,19 @@ func testAccCheckLoadBalancerDestroy(s *terraform.State) error { if items[i].Name == nil { continue } + if utils.Contains(loadbalancersToDestroy, *items[i].Name) { _, err := client.DeleteLoadBalancerExecute(ctx, testutil.ProjectId, region, *items[i].Name) if err != nil { return fmt.Errorf("destroying load balancer %s during CheckDestroy: %w", *items[i].Name, err) } + _, err = wait.DeleteLoadBalancerWaitHandler(ctx, client, testutil.ProjectId, region, *items[i].Name).WaitWithContext(ctx) if err != nil { return fmt.Errorf("destroying load balancer %s during CheckDestroy: waiting for deletion %w", *items[i].Name, err) } } } + return nil } diff --git a/stackit/internal/services/loadbalancer/observability-credential/resource.go b/stackit/internal/services/loadbalancer/observability-credential/resource.go index 8ac96211d..502ff8180 100644 --- a/stackit/internal/services/loadbalancer/observability-credential/resource.go +++ b/stackit/internal/services/loadbalancer/observability-credential/resource.go @@ -6,8 +6,6 @@ import ( "net/http" "strings" - loadbalancerUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/loadbalancer/utils" - "github.com/google/uuid" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" @@ -21,6 +19,7 @@ import ( "github.com/stackitcloud/stackit-sdk-go/services/loadbalancer" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + loadbalancerUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/loadbalancer/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" ) @@ -61,29 +60,35 @@ func (r *observabilityCredentialResource) Metadata(_ context.Context, req resour // ModifyPlan implements resource.ResourceWithModifyPlan. // Use the modifier to set the effective region in the current plan. -func (r *observabilityCredentialResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform +func (r *observabilityCredentialResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { //nolint:gocritic // function signature required by Terraform var configModel Model // skip initial empty configuration to avoid follow-up errors if req.Config.Raw.IsNull() { return } + resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...) + if resp.Diagnostics.HasError() { return } var planModel Model + resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) + if resp.Diagnostics.HasError() { return } utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp) + if resp.Diagnostics.HasError() { return } resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...) + if resp.Diagnostics.HasError() { return } @@ -92,16 +97,20 @@ func (r *observabilityCredentialResource) ModifyPlan(ctx context.Context, req re // Configure adds the provider configured client to the resource. func (r *observabilityCredentialResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { var ok bool + r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) if !ok { return } apiClient := loadbalancerUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + r.client = apiClient + tflog.Info(ctx, "Load Balancer client configured") } @@ -180,14 +189,16 @@ func (r *observabilityCredentialResource) Schema(_ context.Context, _ resource.S } // Create creates the resource and sets the initial Terraform state. -func (r *observabilityCredentialResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *observabilityCredentialResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { //nolint:gocritic // function signature required by Terraform // Retrieve values from plan var model Model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() region := model.Region.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) @@ -206,6 +217,7 @@ func (r *observabilityCredentialResource) Create(ctx context.Context, req resour core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating observability credential", fmt.Sprintf("Calling API: %v", err)) return } + ctx = tflog.SetField(ctx, "credentials_ref", createResp.Credential.CredentialsRef) // Map response body to schema @@ -218,6 +230,7 @@ func (r *observabilityCredentialResource) Create(ctx context.Context, req resour // Set state to fully populated data diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -226,13 +239,15 @@ func (r *observabilityCredentialResource) Create(ctx context.Context, req resour } // Read refreshes the Terraform state with the latest data. -func (r *observabilityCredentialResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (r *observabilityCredentialResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() credentialsRef := model.CredentialsRef.ValueString() region := r.providerData.GetRegionWithOverride(model.Region) @@ -248,7 +263,9 @@ func (r *observabilityCredentialResource) Read(ctx context.Context, req resource resp.State.RemoveResource(ctx) return } + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading observability credential", fmt.Sprintf("Calling API: %v", err)) + return } @@ -262,25 +279,29 @@ func (r *observabilityCredentialResource) Read(ctx context.Context, req resource // Set refreshed state diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "Load balancer observability credential read") } -func (r *observabilityCredentialResource) Update(ctx context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *observabilityCredentialResource) Update(ctx context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { //nolint:gocritic // function signature required by Terraform // Update shouldn't be called core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating observability credential", "Observability credential can't be updated") } // Delete deletes the resource and removes the Terraform state on success. -func (r *observabilityCredentialResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform +func (r *observabilityCredentialResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() credentialsRef := model.CredentialsRef.ValueString() region := model.Region.ValueString() @@ -299,7 +320,7 @@ func (r *observabilityCredentialResource) Delete(ctx context.Context, req resour } // ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: project_id,name +// The expected format of the resource import identifier is: project_id,name. func (r *observabilityCredentialResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { idParts := strings.Split(req.ID, core.Separator) @@ -308,6 +329,7 @@ func (r *observabilityCredentialResource) ImportState(ctx context.Context, req r "Error importing observability credential", fmt.Sprintf("Expected import identifier with format: [project_id],[region],[credentials_ref] Got: %q", req.ID), ) + return } @@ -333,6 +355,7 @@ func mapFields(cred *loadbalancer.CredentialsResponse, m *Model, region string) if cred == nil { return fmt.Errorf("response input is nil") } + if m == nil { return fmt.Errorf("model input is nil") } @@ -345,9 +368,12 @@ func mapFields(cred *loadbalancer.CredentialsResponse, m *Model, region string) } else { return fmt.Errorf("credentials ref not present") } + m.CredentialsRef = types.StringValue(credentialsRef) m.DisplayName = types.StringPointerValue(cred.DisplayName) + var username string + if m.Username.ValueString() != "" { username = m.Username.ValueString() } else if cred.Username != nil { @@ -355,6 +381,7 @@ func mapFields(cred *loadbalancer.CredentialsResponse, m *Model, region string) } else { return fmt.Errorf("username not present") } + m.Username = types.StringValue(username) m.Region = types.StringValue(region) m.Id = utils.BuildInternalTerraformId( diff --git a/stackit/internal/services/loadbalancer/observability-credential/resource_test.go b/stackit/internal/services/loadbalancer/observability-credential/resource_test.go index a6aaffe70..9cddcd957 100644 --- a/stackit/internal/services/loadbalancer/observability-credential/resource_test.go +++ b/stackit/internal/services/loadbalancer/observability-credential/resource_test.go @@ -54,9 +54,11 @@ func TestToCreatePayload(t *testing.T) { if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(output, tt.expected) if diff != "" { @@ -70,6 +72,7 @@ func TestToCreatePayload(t *testing.T) { func TestMapFields(t *testing.T) { const testRegion = "eu01" id := fmt.Sprintf("%s,%s,%s", "pid", testRegion, "credentials_ref") + tests := []struct { description string input *loadbalancer.CredentialsResponse @@ -145,13 +148,16 @@ func TestMapFields(t *testing.T) { model := &Model{ ProjectId: tt.expected.ProjectId, } + err := mapFields(tt.input, model, tt.region) if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(model, tt.expected) if diff != "" { diff --git a/stackit/internal/services/loadbalancer/utils/util.go b/stackit/internal/services/loadbalancer/utils/util.go index 2b84c5667..675973387 100644 --- a/stackit/internal/services/loadbalancer/utils/util.go +++ b/stackit/internal/services/loadbalancer/utils/util.go @@ -4,10 +4,9 @@ import ( "context" "fmt" - "github.com/stackitcloud/stackit-sdk-go/services/loadbalancer" - "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/stackitcloud/stackit-sdk-go/core/config" + "github.com/stackitcloud/stackit-sdk-go/services/loadbalancer" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" ) @@ -20,6 +19,7 @@ func ConfigureClient(ctx context.Context, providerData *core.ProviderData, diags if providerData.LoadBalancerCustomEndpoint != "" { apiClientConfigOptions = append(apiClientConfigOptions, config.WithEndpoint(providerData.LoadBalancerCustomEndpoint)) } + apiClient, err := loadbalancer.NewAPIClient(apiClientConfigOptions...) if err != nil { core.LogAndAddError(ctx, diags, "Error configuring API client", fmt.Sprintf("Configuring client: %v. This is an error related to the provider configuration, not to the resource configuration", err)) diff --git a/stackit/internal/services/loadbalancer/utils/util_test.go b/stackit/internal/services/loadbalancer/utils/util_test.go index b7e118f3b..18cd2a922 100644 --- a/stackit/internal/services/loadbalancer/utils/util_test.go +++ b/stackit/internal/services/loadbalancer/utils/util_test.go @@ -22,6 +22,7 @@ const ( func TestConfigureClient(t *testing.T) { /* mock authentication by setting service account token env variable */ os.Clearenv() + err := os.Setenv(sdkClients.ServiceAccountToken, "mock-val") if err != nil { t.Errorf("error setting env variable: %v", err) @@ -30,6 +31,7 @@ func TestConfigureClient(t *testing.T) { type args struct { providerData *core.ProviderData } + tests := []struct { name string args args @@ -50,6 +52,7 @@ func TestConfigureClient(t *testing.T) { if err != nil { t.Errorf("error configuring client: %v", err) } + return apiClient }(), wantErr: false, @@ -70,6 +73,7 @@ func TestConfigureClient(t *testing.T) { if err != nil { t.Errorf("error configuring client: %v", err) } + return apiClient }(), wantErr: false, @@ -81,6 +85,7 @@ func TestConfigureClient(t *testing.T) { diags := diag.Diagnostics{} actual := ConfigureClient(ctx, tt.args.providerData, &diags) + if diags.HasError() != tt.wantErr { t.Errorf("ConfigureClient() error = %v, want %v", diags.HasError(), tt.wantErr) } diff --git a/stackit/internal/services/logme/credential/datasource.go b/stackit/internal/services/logme/credential/datasource.go index 90f652942..ce374bcfb 100644 --- a/stackit/internal/services/logme/credential/datasource.go +++ b/stackit/internal/services/logme/credential/datasource.go @@ -5,18 +5,16 @@ import ( "fmt" "net/http" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - logmeUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/logme/utils" - "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/stackitcloud/stackit-sdk-go/services/logme" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + logmeUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/logme/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - - "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/stackitcloud/stackit-sdk-go/services/logme" ) // Ensure the implementation satisfies the expected interfaces. @@ -47,10 +45,13 @@ func (r *credentialDataSource) Configure(ctx context.Context, req datasource.Con } apiClient := logmeUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + r.client = apiClient + tflog.Info(ctx, "LogMe credential client configured") } @@ -117,13 +118,15 @@ func (r *credentialDataSource) Schema(_ context.Context, _ datasource.SchemaRequ } // Read refreshes the Terraform state with the latest data. -func (r *credentialDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (r *credentialDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.Config.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() instanceId := model.InstanceId.ValueString() credentialId := model.CredentialId.ValueString() @@ -144,6 +147,7 @@ func (r *credentialDataSource) Read(ctx context.Context, req datasource.ReadRequ }, ) resp.State.RemoveResource(ctx) + return } @@ -157,8 +161,10 @@ func (r *credentialDataSource) Read(ctx context.Context, req datasource.ReadRequ // Set refreshed state diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "LogMe credential read") } diff --git a/stackit/internal/services/logme/credential/resource.go b/stackit/internal/services/logme/credential/resource.go index f98668225..2d4d58abf 100644 --- a/stackit/internal/services/logme/credential/resource.go +++ b/stackit/internal/services/logme/credential/resource.go @@ -6,25 +6,22 @@ import ( "net/http" "strings" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - logmeUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/logme/utils" - - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/stackitcloud/stackit-sdk-go/core/oapierror" "github.com/stackitcloud/stackit-sdk-go/services/logme" "github.com/stackitcloud/stackit-sdk-go/services/logme/wait" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + logmeUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/logme/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" ) // Ensure the implementation satisfies the expected interfaces. @@ -69,10 +66,13 @@ func (r *credentialResource) Configure(ctx context.Context, req resource.Configu } apiClient := logmeUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + r.client = apiClient + tflog.Info(ctx, "LogMe credential client configured") } @@ -153,13 +153,15 @@ func (r *credentialResource) Schema(_ context.Context, _ resource.SchemaRequest, } // Create creates the resource and sets the initial Terraform state. -func (r *credentialResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *credentialResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() instanceId := model.InstanceId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) @@ -171,10 +173,12 @@ func (r *credentialResource) Create(ctx context.Context, req resource.CreateRequ core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credential", fmt.Sprintf("Calling API: %v", err)) return } + if credentialsResp.Id == nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credential", "Got empty credential id") return } + credentialId := *credentialsResp.Id ctx = tflog.SetField(ctx, "credential_id", credentialId) @@ -190,22 +194,27 @@ func (r *credentialResource) Create(ctx context.Context, req resource.CreateRequ core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credential", fmt.Sprintf("Processing API payload: %v", err)) return } + diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "LogMe credential created") } // Read refreshes the Terraform state with the latest data. -func (r *credentialResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (r *credentialResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() instanceId := model.InstanceId.ValueString() credentialId := model.CredentialId.ValueString() @@ -220,7 +229,9 @@ func (r *credentialResource) Read(ctx context.Context, req resource.ReadRequest, resp.State.RemoveResource(ctx) return } + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading credential", fmt.Sprintf("Calling API: %v", err)) + return } @@ -234,23 +245,26 @@ func (r *credentialResource) Read(ctx context.Context, req resource.ReadRequest, // Set refreshed state diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "LogMe credential read") } // Update updates the resource and sets the updated Terraform state on success. -func (r *credentialResource) Update(ctx context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *credentialResource) Update(ctx context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { //nolint:gocritic // function signature required by Terraform // Update shouldn't be called core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating credential", "Credential can't be updated") } // Delete deletes the resource and removes the Terraform state on success. -func (r *credentialResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform +func (r *credentialResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -267,16 +281,18 @@ func (r *credentialResource) Delete(ctx context.Context, req resource.DeleteRequ if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting credential", fmt.Sprintf("Calling API: %v", err)) } + _, err = wait.DeleteCredentialsWaitHandler(ctx, r.client, projectId, instanceId, credentialId).WaitWithContext(ctx) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting credential", fmt.Sprintf("Instance deletion waiting: %v", err)) return } + tflog.Info(ctx, "LogMe credential deleted") } // ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: project_id,instance_id,credential_id +// The expected format of the resource import identifier is: project_id,instance_id,credential_id. func (r *credentialResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { idParts := strings.Split(req.ID, core.Separator) if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" { @@ -284,6 +300,7 @@ func (r *credentialResource) ImportState(ctx context.Context, req resource.Impor "Error importing credential", fmt.Sprintf("Expected import identifier with format [project_id],[instance_id],[credential_id], got %q", req.ID), ) + return } @@ -297,12 +314,15 @@ func mapFields(credentialsResp *logme.CredentialsResponse, model *Model) error { if credentialsResp == nil { return fmt.Errorf("response input is nil") } + if credentialsResp.Raw == nil { return fmt.Errorf("response credentials raw is nil") } + if model == nil { return fmt.Errorf("model input is nil") } + credentials := credentialsResp.Raw.Credentials var credentialId string @@ -316,6 +336,7 @@ func mapFields(credentialsResp *logme.CredentialsResponse, model *Model) error { model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), model.InstanceId.ValueString(), credentialId) model.CredentialId = types.StringValue(credentialId) + if credentials != nil { model.Host = types.StringPointerValue(credentials.Host) model.Password = types.StringPointerValue(credentials.Password) @@ -323,5 +344,6 @@ func mapFields(credentialsResp *logme.CredentialsResponse, model *Model) error { model.Uri = types.StringPointerValue(credentials.Uri) model.Username = types.StringPointerValue(credentials.Username) } + return nil } diff --git a/stackit/internal/services/logme/credential/resource_test.go b/stackit/internal/services/logme/credential/resource_test.go index ce1b51dee..5ec4732b6 100644 --- a/stackit/internal/services/logme/credential/resource_test.go +++ b/stackit/internal/services/logme/credential/resource_test.go @@ -116,13 +116,16 @@ func TestMapFields(t *testing.T) { ProjectId: tt.expected.ProjectId, InstanceId: tt.expected.InstanceId, } + err := mapFields(tt.input, model) if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(model, &tt.expected) if diff != "" { diff --git a/stackit/internal/services/logme/instance/datasource.go b/stackit/internal/services/logme/instance/datasource.go index a7f94ca35..9ddb340b2 100644 --- a/stackit/internal/services/logme/instance/datasource.go +++ b/stackit/internal/services/logme/instance/datasource.go @@ -5,19 +5,17 @@ import ( "fmt" "net/http" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - logmeUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/logme/utils" - "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/stackitcloud/stackit-sdk-go/services/logme" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + logmeUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/logme/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - - "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/stackitcloud/stackit-sdk-go/services/logme" ) // Ensure the implementation satisfies the expected interfaces. @@ -48,10 +46,13 @@ func (r *instanceDataSource) Configure(ctx context.Context, req datasource.Confi } apiClient := logmeUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + r.client = apiClient + tflog.Info(ctx, "LogMe instance client configured") } @@ -239,13 +240,15 @@ func (r *instanceDataSource) Schema(_ context.Context, _ datasource.SchemaReques } // Read refreshes the Terraform state with the latest data. -func (r *instanceDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (r *instanceDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.Config.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() instanceId := model.InstanceId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) @@ -265,6 +268,7 @@ func (r *instanceDataSource) Read(ctx context.Context, req datasource.ReadReques }, ) resp.State.RemoveResource(ctx) + return } @@ -284,8 +288,10 @@ func (r *instanceDataSource) Read(ctx context.Context, req datasource.ReadReques // Set refreshed state diags = resp.State.Set(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "LogMe instance read") } diff --git a/stackit/internal/services/logme/instance/resource.go b/stackit/internal/services/logme/instance/resource.go index 8c1234b33..0cf13d88f 100644 --- a/stackit/internal/services/logme/instance/resource.go +++ b/stackit/internal/services/logme/instance/resource.go @@ -8,28 +8,25 @@ import ( "strings" "time" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - - logmeUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/logme/utils" - "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" "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/services/logme" "github.com/stackitcloud/stackit-sdk-go/services/logme/wait" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + logmeUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/logme/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" ) // Ensure the implementation satisfies the expected interfaces. @@ -55,7 +52,7 @@ type Model struct { PlanId types.String `tfsdk:"plan_id"` } -// Struct corresponding to DataSourceModel.Parameters +// Struct corresponding to DataSourceModel.Parameters. type parametersModel struct { SgwAcl types.String `tfsdk:"sgw_acl"` EnableMonitoring types.Bool `tfsdk:"enable_monitoring"` @@ -81,7 +78,7 @@ type parametersModel struct { Syslog types.List `tfsdk:"syslog"` } -// Types corresponding to parametersModel +// Types corresponding to parametersModel. var parametersTypes = map[string]attr.Type{ "sgw_acl": basetypes.StringType{}, "enable_monitoring": basetypes.BoolType{}, @@ -130,10 +127,13 @@ func (r *instanceResource) Configure(ctx context.Context, req resource.Configure } apiClient := logmeUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + r.client = apiClient + tflog.Info(ctx, "LogMe instance client configured") } @@ -382,21 +382,24 @@ func (r *instanceResource) Schema(_ context.Context, _ resource.SchemaRequest, r } // Create creates the resource and sets the initial Terraform state. -func (r *instanceResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *instanceResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) var parameters *parametersModel - if !(model.Parameters.IsNull() || model.Parameters.IsUnknown()) { + if !model.Parameters.IsNull() && !model.Parameters.IsUnknown() { parameters = ¶metersModel{} diags = model.Parameters.As(ctx, parameters, basetypes.ObjectAsOptions{}) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -420,6 +423,7 @@ func (r *instanceResource) Create(ctx context.Context, req resource.CreateReques core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Calling API: %v", err)) return } + instanceId := *createResp.InstanceId ctx = tflog.SetField(ctx, "instance_id", instanceId) waitResp, err := wait.CreateInstanceWaitHandler(ctx, r.client, projectId, instanceId).SetTimeout(90 * time.Minute).WaitWithContext(ctx) @@ -438,20 +442,24 @@ func (r *instanceResource) Create(ctx context.Context, req resource.CreateReques // Set state to fully populated data diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "LogMe instance created") } // Read refreshes the Terraform state with the latest data. -func (r *instanceResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (r *instanceResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() instanceId := model.InstanceId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) @@ -464,7 +472,9 @@ func (r *instanceResource) Read(ctx context.Context, req resource.ReadRequest, r resp.State.RemoveResource(ctx) return } + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Calling API: %v", err)) + return } @@ -485,30 +495,35 @@ func (r *instanceResource) Read(ctx context.Context, req resource.ReadRequest, r // Set refreshed state diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "LogMe instance read") } // Update updates the resource and sets the updated Terraform state on success. -func (r *instanceResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *instanceResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() instanceId := model.InstanceId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) ctx = tflog.SetField(ctx, "instance_id", instanceId) var parameters *parametersModel - if !(model.Parameters.IsNull() || model.Parameters.IsUnknown()) { + if !model.Parameters.IsNull() && !model.Parameters.IsUnknown() { parameters = ¶metersModel{} diags = model.Parameters.As(ctx, parameters, basetypes.ObjectAsOptions{}) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -532,6 +547,7 @@ func (r *instanceResource) Update(ctx context.Context, req resource.UpdateReques core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Calling API: %v", err)) return } + waitResp, err := wait.PartialUpdateInstanceWaitHandler(ctx, r.client, projectId, instanceId).WaitWithContext(ctx) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Instance update waiting: %v", err)) @@ -547,21 +563,25 @@ func (r *instanceResource) Update(ctx context.Context, req resource.UpdateReques diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "LogMe instance updated") } // Delete deletes the resource and removes the Terraform state on success. -func (r *instanceResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform +func (r *instanceResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { //nolint:gocritic // function signature required by Terraform // Retrieve values from state var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() instanceId := model.InstanceId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) @@ -573,16 +593,18 @@ func (r *instanceResource) Delete(ctx context.Context, req resource.DeleteReques core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting instance", fmt.Sprintf("Calling API: %v", err)) return } + _, err = wait.DeleteInstanceWaitHandler(ctx, r.client, projectId, instanceId).WaitWithContext(ctx) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting instance", fmt.Sprintf("Instance deletion waiting: %v", err)) return } + tflog.Info(ctx, "LogMe instance deleted") } // ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: project_id,instance_id +// The expected format of the resource import identifier is: project_id,instance_id. func (r *instanceResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { idParts := strings.Split(req.ID, core.Separator) @@ -591,6 +613,7 @@ func (r *instanceResource) ImportState(ctx context.Context, req resource.ImportS "Error importing instance", fmt.Sprintf("Expected import identifier with format: [project_id],[instance_id] Got: %q", req.ID), ) + return } @@ -603,6 +626,7 @@ func mapFields(instance *logme.Instance, model *Model) error { if instance == nil { return fmt.Errorf("response input is nil") } + if model == nil { return fmt.Errorf("model input is nil") } @@ -633,15 +657,19 @@ func mapFields(instance *logme.Instance, model *Model) error { if err != nil { return fmt.Errorf("mapping parameters: %w", err) } + model.Parameters = parameters } + return nil } func mapParameters(params map[string]interface{}) (types.Object, error) { attributes := map[string]attr.Value{} + for attribute := range parametersTypes { var valueInterface interface{} + var ok bool // This replacement is necessary because Terraform does not allow hyphens in attribute names @@ -664,6 +692,7 @@ func mapParameters(params map[string]interface{}) (types.Object, error) { } else { valueInterface, ok = params[attribute] } + if !ok { // All fields are optional, so this is ok // Set the value as nil, will be handled accordingly @@ -671,6 +700,7 @@ func mapParameters(params map[string]interface{}) (types.Object, error) { } var value attr.Value + switch parametersTypes[attribute].(type) { default: return types.ObjectNull(parametersTypes), fmt.Errorf("found unexpected attribute type '%T'", parametersTypes[attribute]) @@ -682,6 +712,7 @@ func mapParameters(params map[string]interface{}) (types.Object, error) { if !ok { return types.ObjectNull(parametersTypes), fmt.Errorf("found attribute '%s' of type %T, failed to assert as string", attribute, valueInterface) } + value = types.StringValue(valueString) } case basetypes.BoolType: @@ -692,6 +723,7 @@ func mapParameters(params map[string]interface{}) (types.Object, error) { if !ok { return types.ObjectNull(parametersTypes), fmt.Errorf("found attribute '%s' of type %T, failed to assert as bool", attribute, valueInterface) } + value = types.BoolValue(valueBool) } case basetypes.Int64Type: @@ -701,6 +733,7 @@ func mapParameters(params map[string]interface{}) (types.Object, error) { // This may be int64, int32, int or float64 // We try to assert all 4 var valueInt64 int64 + switch temp := valueInterface.(type) { default: return types.ObjectNull(parametersTypes), fmt.Errorf("found attribute '%s' of type %T, failed to assert as int", attribute, valueInterface) @@ -713,6 +746,7 @@ func mapParameters(params map[string]interface{}) (types.Object, error) { case float64: valueInt64 = int64(temp) } + value = types.Int64Value(valueInt64) } case basetypes.Float64Type: @@ -720,12 +754,14 @@ func mapParameters(params map[string]interface{}) (types.Object, error) { value = types.Float64Null() } else { var valueFloat64 float64 + switch temp := valueInterface.(type) { default: return types.ObjectNull(parametersTypes), fmt.Errorf("found attribute '%s' of type %T, failed to assert as int", attribute, valueInterface) case float64: valueFloat64 = float64(temp) } + value = types.Float64Value(valueFloat64) } case basetypes.ListType: // Assumed to be a list of strings @@ -735,6 +771,7 @@ func mapParameters(params map[string]interface{}) (types.Object, error) { // This may be []string{} or []interface{} // We try to assert all 2 var valueList []attr.Value + switch temp := valueInterface.(type) { default: return types.ObjectNull(parametersTypes), fmt.Errorf("found attribute '%s' of type %T, failed to assert as array of interface", attribute, valueInterface) @@ -748,16 +785,20 @@ func mapParameters(params map[string]interface{}) (types.Object, error) { if !ok { return types.ObjectNull(parametersTypes), fmt.Errorf("found attribute '%s' with element '%s' of type %T, failed to assert as string", attribute, x, x) } + valueList = append(valueList, types.StringValue(xString)) } } + temp2, diags := types.ListValue(types.StringType, valueList) if diags.HasError() { return types.ObjectNull(parametersTypes), fmt.Errorf("failed to map %s: %w", attribute, core.DiagsToError(diags)) } + value = temp2 } } + attributes[attribute] = value } @@ -765,6 +806,7 @@ func mapParameters(params map[string]interface{}) (types.Object, error) { if diags.HasError() { return types.ObjectNull(parametersTypes), fmt.Errorf("failed to create object: %w", core.DiagsToError(diags)) } + return output, nil } @@ -805,6 +847,7 @@ func toInstanceParams(parameters *parametersModel) (*logme.InstanceParameters, e if parameters == nil { return nil, nil } + payloadParams := &logme.InstanceParameters{} payloadParams.SgwAcl = conversion.StringValueToPointer(parameters.SgwAcl) @@ -828,6 +871,7 @@ func toInstanceParams(parameters *parametersModel) (*logme.InstanceParameters, e payloadParams.MonitoringInstanceId = conversion.StringValueToPointer(parameters.MonitoringInstanceId) var err error + payloadParams.OpensearchTlsCiphers, err = conversion.StringListToPointer(parameters.OpensearchTlsCiphers) if err != nil { return nil, fmt.Errorf("convert opensearch_tls_ciphers: %w", err) @@ -848,6 +892,7 @@ func toInstanceParams(parameters *parametersModel) (*logme.InstanceParameters, e func (r *instanceResource) loadPlanId(ctx context.Context, model *Model) error { projectId := model.ProjectId.ValueString() + res, err := r.client.ListOfferings(ctx, projectId).Execute() if err != nil { return fmt.Errorf("getting LogMe offerings: %w", err) @@ -858,21 +903,25 @@ func (r *instanceResource) loadPlanId(ctx context.Context, model *Model) error { availableVersions := "" availablePlanNames := "" isValidVersion := false + for _, offer := range *res.Offerings { if !strings.EqualFold(*offer.Version, version) { availableVersions = fmt.Sprintf("%s\n- %s", availableVersions, *offer.Version) continue } + isValidVersion = true for _, plan := range *offer.Plans { if plan.Name == nil { continue } + if strings.EqualFold(*plan.Name, planName) && plan.Id != nil { model.PlanId = types.StringPointerValue(plan.Id) return nil } + availablePlanNames = fmt.Sprintf("%s\n- %s", availablePlanNames, *plan.Name) } } @@ -880,12 +929,14 @@ func (r *instanceResource) loadPlanId(ctx context.Context, model *Model) error { if !isValidVersion { return fmt.Errorf("couldn't find version '%s', available versions are: %s", version, availableVersions) } + return fmt.Errorf("couldn't find plan_name '%s' for version %s, available names are: %s", planName, version, availablePlanNames) } func loadPlanNameAndVersion(ctx context.Context, client *logme.APIClient, model *Model) error { projectId := model.ProjectId.ValueString() planId := model.PlanId.ValueString() + res, err := client.ListOfferings(ctx, projectId).Execute() if err != nil { return fmt.Errorf("getting LogMe offerings: %w", err) @@ -896,6 +947,7 @@ func loadPlanNameAndVersion(ctx context.Context, client *logme.APIClient, model if strings.EqualFold(*plan.Id, planId) && plan.Id != nil { model.PlanName = types.StringPointerValue(plan.Name) model.Version = types.StringPointerValue(offer.Version) + return nil } } diff --git a/stackit/internal/services/logme/instance/resource_test.go b/stackit/internal/services/logme/instance/resource_test.go index d74d81d18..77dfb7dae 100644 --- a/stackit/internal/services/logme/instance/resource_test.go +++ b/stackit/internal/services/logme/instance/resource_test.go @@ -212,13 +212,16 @@ func TestMapFields(t *testing.T) { ProjectId: tt.expected.ProjectId, InstanceId: tt.expected.InstanceId, } + err := mapFields(tt.input, state) if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(state, &tt.expected) if diff != "" { @@ -292,22 +295,27 @@ func TestToCreatePayload(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { var parameters *parametersModel + if tt.input != nil { - if !(tt.input.Parameters.IsNull() || tt.input.Parameters.IsUnknown()) { + if !tt.input.Parameters.IsNull() && !tt.input.Parameters.IsUnknown() { parameters = ¶metersModel{} + diags := tt.input.Parameters.As(context.Background(), parameters, basetypes.ObjectAsOptions{}) if diags.HasError() { t.Fatalf("Error converting parameters: %v", diags.Errors()) } } } + output, err := toCreatePayload(tt.input, parameters) if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(output, tt.expected) if diff != "" { @@ -375,22 +383,27 @@ func TestToUpdatePayload(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { var parameters *parametersModel + if tt.input != nil { - if !(tt.input.Parameters.IsNull() || tt.input.Parameters.IsUnknown()) { + if !tt.input.Parameters.IsNull() && !tt.input.Parameters.IsUnknown() { parameters = ¶metersModel{} + diags := tt.input.Parameters.As(context.Background(), parameters, basetypes.ObjectAsOptions{}) if diags.HasError() { t.Fatalf("Error converting parameters: %v", diags.Errors()) } } } + output, err := toUpdatePayload(tt.input, parameters) if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(output, tt.expected) if diff != "" { diff --git a/stackit/internal/services/logme/logme_acc_test.go b/stackit/internal/services/logme/logme_acc_test.go index d138380ed..149a3addb 100644 --- a/stackit/internal/services/logme/logme_acc_test.go +++ b/stackit/internal/services/logme/logme_acc_test.go @@ -33,7 +33,7 @@ var ( maxTestName = testutil.ResourceNameWithDateTime("logme-max") ) -// Instance resource data +// Instance resource data. var testConfigVarsMin = config.Variables{ "project_id": config.StringVariable(testutil.ProjectId), "name": config.StringVariable(minTestName), @@ -80,6 +80,7 @@ var testConfigVarsMax = config.Variables{ func configVarsMinUpdated() config.Variables { updatedConfig := maps.Clone(testConfigVarsMax) updatedConfig["name"] = config.StringVariable(minTestName + "-updated") + return updatedConfig } @@ -90,6 +91,7 @@ func configVarsMaxUpdated() config.Variables { updatedConfig["parameters_graphite"] = config.StringVariable("graphite.stackit.cloud:2003") updatedConfig["parameters_sgw_acl"] = config.StringVariable("192.168.1.0/24") updatedConfig["parameters_syslog"] = config.StringVariable("test.log:514") + return updatedConfig } @@ -98,7 +100,6 @@ func TestAccLogMeMinResource(t *testing.T) { ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, CheckDestroy: testAccCheckLogMeDestroy, Steps: []resource.TestStep{ - // Creation { Config: testutil.LogMeProviderConfig() + "\n" + resourceMinConfig, @@ -171,6 +172,7 @@ func TestAccLogMeMinResource(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute instance_id") } + return fmt.Sprintf("%s,%s", testutil.ProjectId, instanceId), nil }, ImportState: true, @@ -193,6 +195,7 @@ func TestAccLogMeMinResource(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute credential_id") } + return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, instanceId, credentialId), nil }, ImportState: true, @@ -221,7 +224,6 @@ func TestAccLogMeMaxResource(t *testing.T) { ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, CheckDestroy: testAccCheckLogMeDestroy, Steps: []resource.TestStep{ - // Creation { Config: testutil.LogMeProviderConfig() + "\n" + resourceMaxConfig, @@ -348,6 +350,7 @@ func TestAccLogMeMaxResource(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute instance_id") } + return fmt.Sprintf("%s,%s", testutil.ProjectId, instanceId), nil }, ImportState: true, @@ -370,6 +373,7 @@ func TestAccLogMeMaxResource(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute credential_id") } + return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, instanceId, credentialId), nil }, ImportState: true, @@ -422,7 +426,9 @@ func TestAccLogMeMaxResource(t *testing.T) { func testAccCheckLogMeDestroy(s *terraform.State) error { ctx := context.Background() + var client *logme.APIClient + var err error if testutil.LogMeCustomEndpoint == "" { client, err = logme.NewAPIClient( @@ -433,11 +439,13 @@ func testAccCheckLogMeDestroy(s *terraform.State) error { core_config.WithEndpoint(testutil.LogMeCustomEndpoint), ) } + if err != nil { return fmt.Errorf("creating client: %w", err) } instancesToDestroy := []string{} + for _, rs := range s.RootModule().Resources { if rs.Type != "stackit_logme_instance" { continue @@ -457,12 +465,14 @@ func testAccCheckLogMeDestroy(s *terraform.State) error { if instances[i].InstanceId == nil { continue } + if utils.Contains(instancesToDestroy, *instances[i].InstanceId) { if !checkInstanceDeleteSuccess(&instances[i]) { err := client.DeleteInstanceExecute(ctx, testutil.ProjectId, *instances[i].InstanceId) if err != nil { return fmt.Errorf("destroying instance %s during CheckDestroy: %w", *instances[i].InstanceId, err) } + _, err = wait.DeleteInstanceWaitHandler(ctx, client, testutil.ProjectId, *instances[i].InstanceId).WaitWithContext(ctx) if err != nil { return fmt.Errorf("destroying instance %s during CheckDestroy: waiting for deletion %w", *instances[i].InstanceId, err) @@ -470,6 +480,7 @@ func testAccCheckLogMeDestroy(s *terraform.State) error { } } } + return nil } @@ -485,5 +496,6 @@ func checkInstanceDeleteSuccess(i *logme.Instance) bool { return false } } + return true } diff --git a/stackit/internal/services/logme/utils/util.go b/stackit/internal/services/logme/utils/util.go index 6da59478a..e00ac4020 100644 --- a/stackit/internal/services/logme/utils/util.go +++ b/stackit/internal/services/logme/utils/util.go @@ -21,6 +21,7 @@ func ConfigureClient(ctx context.Context, providerData *core.ProviderData, diags } else { apiClientConfigOptions = append(apiClientConfigOptions, config.WithRegion(providerData.GetRegion())) } + apiClient, err := logme.NewAPIClient(apiClientConfigOptions...) if err != nil { core.LogAndAddError(ctx, diags, "Error configuring API client", fmt.Sprintf("Configuring client: %v. This is an error related to the provider configuration, not to the resource configuration", err)) diff --git a/stackit/internal/services/logme/utils/util_test.go b/stackit/internal/services/logme/utils/util_test.go index 9fa901993..49285fa0a 100644 --- a/stackit/internal/services/logme/utils/util_test.go +++ b/stackit/internal/services/logme/utils/util_test.go @@ -22,6 +22,7 @@ const ( func TestConfigureClient(t *testing.T) { /* mock authentication by setting service account token env variable */ os.Clearenv() + err := os.Setenv(sdkClients.ServiceAccountToken, "mock-val") if err != nil { t.Errorf("error setting env variable: %v", err) @@ -30,6 +31,7 @@ func TestConfigureClient(t *testing.T) { type args struct { providerData *core.ProviderData } + tests := []struct { name string args args @@ -51,6 +53,7 @@ func TestConfigureClient(t *testing.T) { if err != nil { t.Errorf("error configuring client: %v", err) } + return apiClient }(), wantErr: false, @@ -71,6 +74,7 @@ func TestConfigureClient(t *testing.T) { if err != nil { t.Errorf("error configuring client: %v", err) } + return apiClient }(), wantErr: false, @@ -82,6 +86,7 @@ func TestConfigureClient(t *testing.T) { diags := diag.Diagnostics{} actual := ConfigureClient(ctx, tt.args.providerData, &diags) + if diags.HasError() != tt.wantErr { t.Errorf("ConfigureClient() error = %v, want %v", diags.HasError(), tt.wantErr) } diff --git a/stackit/internal/services/mariadb/credential/datasource.go b/stackit/internal/services/mariadb/credential/datasource.go index d97209c85..8caf4bad1 100644 --- a/stackit/internal/services/mariadb/credential/datasource.go +++ b/stackit/internal/services/mariadb/credential/datasource.go @@ -5,19 +5,17 @@ import ( "fmt" "net/http" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - mariadbUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/mariadb/utils" - "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/stackitcloud/stackit-sdk-go/services/mariadb" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + mariadbUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/mariadb/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - - "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/services/mariadb" ) // Ensure the implementation satisfies the expected interfaces. @@ -48,10 +46,13 @@ func (r *credentialDataSource) Configure(ctx context.Context, req datasource.Con } apiClient := mariadbUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + r.client = apiClient + tflog.Info(ctx, "mariadb credential client configured") } @@ -125,13 +126,15 @@ func (r *credentialDataSource) Schema(_ context.Context, _ datasource.SchemaRequ } // Read refreshes the Terraform state with the latest data. -func (r *credentialDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (r *credentialDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.Config.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() instanceId := model.InstanceId.ValueString() credentialId := model.CredentialId.ValueString() @@ -152,6 +155,7 @@ func (r *credentialDataSource) Read(ctx context.Context, req datasource.ReadRequ }, ) resp.State.RemoveResource(ctx) + return } @@ -165,8 +169,10 @@ func (r *credentialDataSource) Read(ctx context.Context, req datasource.ReadRequ // Set refreshed state diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "mariadb credential read") } diff --git a/stackit/internal/services/mariadb/credential/resource.go b/stackit/internal/services/mariadb/credential/resource.go index 15a7c52ad..046405bf3 100644 --- a/stackit/internal/services/mariadb/credential/resource.go +++ b/stackit/internal/services/mariadb/credential/resource.go @@ -6,24 +6,22 @@ import ( "net/http" "strings" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - mariadbUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/mariadb/utils" - - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/stackitcloud/stackit-sdk-go/core/oapierror" "github.com/stackitcloud/stackit-sdk-go/services/mariadb" "github.com/stackitcloud/stackit-sdk-go/services/mariadb/wait" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + mariadbUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/mariadb/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" ) // Ensure the implementation satisfies the expected interfaces. @@ -70,10 +68,13 @@ func (r *credentialResource) Configure(ctx context.Context, req resource.Configu } apiClient := mariadbUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + r.client = apiClient + tflog.Info(ctx, "MariaDB credential client configured") } @@ -161,13 +162,15 @@ func (r *credentialResource) Schema(_ context.Context, _ resource.SchemaRequest, } // Create creates the resource and sets the initial Terraform state. -func (r *credentialResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *credentialResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() instanceId := model.InstanceId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) @@ -179,10 +182,12 @@ func (r *credentialResource) Create(ctx context.Context, req resource.CreateRequ core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credential", fmt.Sprintf("Calling API: %v", err)) return } + if credentialsResp.Id == nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credential", "Got empty credential id") return } + credentialId := *credentialsResp.Id ctx = tflog.SetField(ctx, "credential_id", credentialId) @@ -198,22 +203,27 @@ func (r *credentialResource) Create(ctx context.Context, req resource.CreateRequ core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credential", fmt.Sprintf("Processing API payload: %v", err)) return } + diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "MariaDB credential created") } // Read refreshes the Terraform state with the latest data. -func (r *credentialResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (r *credentialResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() instanceId := model.InstanceId.ValueString() credentialId := model.CredentialId.ValueString() @@ -228,7 +238,9 @@ func (r *credentialResource) Read(ctx context.Context, req resource.ReadRequest, resp.State.RemoveResource(ctx) return } + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading credential", fmt.Sprintf("Calling API: %v", err)) + return } @@ -242,23 +254,26 @@ func (r *credentialResource) Read(ctx context.Context, req resource.ReadRequest, // Set refreshed state diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "MariaDB credential read") } // Update updates the resource and sets the updated Terraform state on success. -func (r *credentialResource) Update(ctx context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *credentialResource) Update(ctx context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { //nolint:gocritic // function signature required by Terraform // Update shouldn't be called core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating credential", "Credential can't be updated") } // Delete deletes the resource and removes the Terraform state on success. -func (r *credentialResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform +func (r *credentialResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -275,16 +290,18 @@ func (r *credentialResource) Delete(ctx context.Context, req resource.DeleteRequ if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting credential", fmt.Sprintf("Calling API: %v", err)) } + _, err = wait.DeleteCredentialsWaitHandler(ctx, r.client, projectId, instanceId, credentialId).WaitWithContext(ctx) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting credential", fmt.Sprintf("Instance deletion waiting: %v", err)) return } + tflog.Info(ctx, "MariaDB credential deleted") } // ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: project_id,instance_id,credential_id +// The expected format of the resource import identifier is: project_id,instance_id,credential_id. func (r *credentialResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { idParts := strings.Split(req.ID, core.Separator) if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" { @@ -292,6 +309,7 @@ func (r *credentialResource) ImportState(ctx context.Context, req resource.Impor "Error importing credential", fmt.Sprintf("Expected import identifier with format [project_id],[instance_id],[credential_id], got %q", req.ID), ) + return } @@ -305,12 +323,15 @@ func mapFields(ctx context.Context, credentialsResp *mariadb.CredentialsResponse if credentialsResp == nil { return fmt.Errorf("response input is nil") } + if credentialsResp.Raw == nil { return fmt.Errorf("response credentials raw is nil") } + if model == nil { return fmt.Errorf("model input is nil") } + credentials := credentialsResp.Raw.Credentials var credentialId string @@ -335,6 +356,7 @@ func mapFields(ctx context.Context, credentialsResp *mariadb.CredentialsResponse model.Hosts = types.ListNull(types.StringType) model.CredentialId = types.StringValue(credentialId) + if credentials != nil { if credentials.Hosts != nil { respHosts := *credentials.Hosts @@ -348,6 +370,7 @@ func mapFields(ctx context.Context, credentialsResp *mariadb.CredentialsResponse model.Hosts = hostsTF } + model.Host = types.StringPointerValue(credentials.Host) model.Name = types.StringPointerValue(credentials.Name) model.Password = types.StringPointerValue(credentials.Password) @@ -355,5 +378,6 @@ func mapFields(ctx context.Context, credentialsResp *mariadb.CredentialsResponse model.Uri = types.StringPointerValue(credentials.Uri) model.Username = types.StringPointerValue(credentials.Username) } + return nil } diff --git a/stackit/internal/services/mariadb/credential/resource_test.go b/stackit/internal/services/mariadb/credential/resource_test.go index 911b57f3a..38c119116 100644 --- a/stackit/internal/services/mariadb/credential/resource_test.go +++ b/stackit/internal/services/mariadb/credential/resource_test.go @@ -207,9 +207,11 @@ func TestMapFields(t *testing.T) { if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(tt.state, tt.expected) if diff != "" { diff --git a/stackit/internal/services/mariadb/instance/datasource.go b/stackit/internal/services/mariadb/instance/datasource.go index 39fe12703..3ecb5fa18 100644 --- a/stackit/internal/services/mariadb/instance/datasource.go +++ b/stackit/internal/services/mariadb/instance/datasource.go @@ -5,19 +5,17 @@ import ( "fmt" "net/http" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - mariadbUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/mariadb/utils" - "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/stackitcloud/stackit-sdk-go/services/mariadb" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + mariadbUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/mariadb/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - - "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/stackitcloud/stackit-sdk-go/services/mariadb" ) // Ensure the implementation satisfies the expected interfaces. @@ -48,10 +46,13 @@ func (r *instanceDataSource) Configure(ctx context.Context, req datasource.Confi } apiClient := mariadbUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + r.client = apiClient + tflog.Info(ctx, "MariaDB instance client configured") } @@ -175,13 +176,15 @@ func (r *instanceDataSource) Schema(_ context.Context, _ datasource.SchemaReques } // Read refreshes the Terraform state with the latest data. -func (r *instanceDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (r *instanceDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.Config.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() instanceId := model.InstanceId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) @@ -201,6 +204,7 @@ func (r *instanceDataSource) Read(ctx context.Context, req datasource.ReadReques }, ) resp.State.RemoveResource(ctx) + return } @@ -220,8 +224,10 @@ func (r *instanceDataSource) Read(ctx context.Context, req datasource.ReadReques // Set refreshed state diags = resp.State.Set(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "MariaDB instance read") } diff --git a/stackit/internal/services/mariadb/instance/resource.go b/stackit/internal/services/mariadb/instance/resource.go index 39d2f3ecf..7dcfb3ffd 100644 --- a/stackit/internal/services/mariadb/instance/resource.go +++ b/stackit/internal/services/mariadb/instance/resource.go @@ -6,28 +6,25 @@ import ( "net/http" "strings" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - - mariadbUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/mariadb/utils" - "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" "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/services/mariadb" "github.com/stackitcloud/stackit-sdk-go/services/mariadb/wait" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + mariadbUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/mariadb/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" ) // Ensure the implementation satisfies the expected interfaces. @@ -53,7 +50,7 @@ type Model struct { PlanId types.String `tfsdk:"plan_id"` } -// Struct corresponding to DataSourceModel.Parameters +// Struct corresponding to DataSourceModel.Parameters. type parametersModel struct { SgwAcl types.String `tfsdk:"sgw_acl"` EnableMonitoring types.Bool `tfsdk:"enable_monitoring"` @@ -65,7 +62,7 @@ type parametersModel struct { Syslog types.List `tfsdk:"syslog"` } -// Types corresponding to parametersModel +// Types corresponding to parametersModel. var parametersTypes = map[string]attr.Type{ "sgw_acl": basetypes.StringType{}, "enable_monitoring": basetypes.BoolType{}, @@ -100,10 +97,13 @@ func (r *instanceResource) Configure(ctx context.Context, req resource.Configure } apiClient := mariadbUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + r.client = apiClient + tflog.Info(ctx, "MariaDB instance client configured") } @@ -275,21 +275,24 @@ func (r *instanceResource) Schema(_ context.Context, _ resource.SchemaRequest, r } // Create creates the resource and sets the initial Terraform state. -func (r *instanceResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *instanceResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) var parameters *parametersModel - if !(model.Parameters.IsNull() || model.Parameters.IsUnknown()) { + if !model.Parameters.IsNull() && !model.Parameters.IsUnknown() { parameters = ¶metersModel{} diags = model.Parameters.As(ctx, parameters, basetypes.ObjectAsOptions{}) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -313,6 +316,7 @@ func (r *instanceResource) Create(ctx context.Context, req resource.CreateReques core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Calling API: %v", err)) return } + instanceId := *createResp.InstanceId ctx = tflog.SetField(ctx, "instance_id", instanceId) waitResp, err := wait.CreateInstanceWaitHandler(ctx, r.client, projectId, instanceId).WaitWithContext(ctx) @@ -331,20 +335,24 @@ func (r *instanceResource) Create(ctx context.Context, req resource.CreateReques // Set state to fully populated data diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "MariaDB instance created") } // Read refreshes the Terraform state with the latest data. -func (r *instanceResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (r *instanceResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() instanceId := model.InstanceId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) @@ -357,7 +365,9 @@ func (r *instanceResource) Read(ctx context.Context, req resource.ReadRequest, r resp.State.RemoveResource(ctx) return } + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Calling API: %v", err)) + return } @@ -378,30 +388,35 @@ func (r *instanceResource) Read(ctx context.Context, req resource.ReadRequest, r // Set refreshed state diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "MariaDB instance read") } // Update updates the resource and sets the updated Terraform state on success. -func (r *instanceResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *instanceResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() instanceId := model.InstanceId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) ctx = tflog.SetField(ctx, "instance_id", instanceId) var parameters *parametersModel - if !(model.Parameters.IsNull() || model.Parameters.IsUnknown()) { + if !model.Parameters.IsNull() && !model.Parameters.IsUnknown() { parameters = ¶metersModel{} diags = model.Parameters.As(ctx, parameters, basetypes.ObjectAsOptions{}) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -425,6 +440,7 @@ func (r *instanceResource) Update(ctx context.Context, req resource.UpdateReques core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Calling API: %v", err)) return } + waitResp, err := wait.PartialUpdateInstanceWaitHandler(ctx, r.client, projectId, instanceId).WaitWithContext(ctx) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Instance update waiting: %v", err)) @@ -440,21 +456,25 @@ func (r *instanceResource) Update(ctx context.Context, req resource.UpdateReques diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "MariaDB instance updated") } // Delete deletes the resource and removes the Terraform state on success. -func (r *instanceResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform +func (r *instanceResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { //nolint:gocritic // function signature required by Terraform // Retrieve values from state var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() instanceId := model.InstanceId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) @@ -466,16 +486,18 @@ func (r *instanceResource) Delete(ctx context.Context, req resource.DeleteReques core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting instance", fmt.Sprintf("Calling API: %v", err)) return } + _, err = wait.DeleteInstanceWaitHandler(ctx, r.client, projectId, instanceId).WaitWithContext(ctx) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting instance", fmt.Sprintf("Instance deletion waiting: %v", err)) return } + tflog.Info(ctx, "MariaDB instance deleted") } // ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: project_id,instance_id +// The expected format of the resource import identifier is: project_id,instance_id. func (r *instanceResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { idParts := strings.Split(req.ID, core.Separator) @@ -484,6 +506,7 @@ func (r *instanceResource) ImportState(ctx context.Context, req resource.ImportS "Error importing instance", fmt.Sprintf("Expected import identifier with format: [project_id],[instance_id] Got: %q", req.ID), ) + return } @@ -496,6 +519,7 @@ func mapFields(instance *mariadb.Instance, model *Model) error { if instance == nil { return fmt.Errorf("response input is nil") } + if model == nil { return fmt.Errorf("model input is nil") } @@ -526,13 +550,16 @@ func mapFields(instance *mariadb.Instance, model *Model) error { if err != nil { return fmt.Errorf("mapping parameters: %w", err) } + model.Parameters = parameters } + return nil } func mapParameters(params map[string]interface{}) (types.Object, error) { attributes := map[string]attr.Value{} + for attribute := range parametersTypes { valueInterface, ok := params[attribute] if !ok { @@ -542,6 +569,7 @@ func mapParameters(params map[string]interface{}) (types.Object, error) { } var value attr.Value + switch parametersTypes[attribute].(type) { default: return types.ObjectNull(parametersTypes), fmt.Errorf("found unexpected attribute type '%T'", parametersTypes[attribute]) @@ -553,6 +581,7 @@ func mapParameters(params map[string]interface{}) (types.Object, error) { if !ok { return types.ObjectNull(parametersTypes), fmt.Errorf("found attribute '%s' of type %T, failed to assert as string", attribute, valueInterface) } + value = types.StringValue(valueString) } case basetypes.BoolType: @@ -563,6 +592,7 @@ func mapParameters(params map[string]interface{}) (types.Object, error) { if !ok { return types.ObjectNull(parametersTypes), fmt.Errorf("found attribute '%s' of type %T, failed to assert as bool", attribute, valueInterface) } + value = types.BoolValue(valueBool) } case basetypes.Int64Type: @@ -572,6 +602,7 @@ func mapParameters(params map[string]interface{}) (types.Object, error) { // This may be int64, int32, int or float64 // We try to assert all 4 var valueInt64 int64 + switch temp := valueInterface.(type) { default: return types.ObjectNull(parametersTypes), fmt.Errorf("found attribute '%s' of type %T, failed to assert as int", attribute, valueInterface) @@ -584,6 +615,7 @@ func mapParameters(params map[string]interface{}) (types.Object, error) { case float64: valueInt64 = int64(temp) } + value = types.Int64Value(valueInt64) } case basetypes.ListType: // Assumed to be a list of strings @@ -593,6 +625,7 @@ func mapParameters(params map[string]interface{}) (types.Object, error) { // This may be []string{} or []interface{} // We try to assert all 2 var valueList []attr.Value + switch temp := valueInterface.(type) { default: return types.ObjectNull(parametersTypes), fmt.Errorf("found attribute '%s' of type %T, failed to assert as array of interface", attribute, valueInterface) @@ -606,16 +639,20 @@ func mapParameters(params map[string]interface{}) (types.Object, error) { if !ok { return types.ObjectNull(parametersTypes), fmt.Errorf("found attribute '%s' with element '%s' of type %T, failed to assert as string", attribute, x, x) } + valueList = append(valueList, types.StringValue(xString)) } } + temp2, diags := types.ListValue(types.StringType, valueList) if diags.HasError() { return types.ObjectNull(parametersTypes), fmt.Errorf("failed to map %s: %w", attribute, core.DiagsToError(diags)) } + value = temp2 } } + attributes[attribute] = value } @@ -623,6 +660,7 @@ func mapParameters(params map[string]interface{}) (types.Object, error) { if diags.HasError() { return types.ObjectNull(parametersTypes), fmt.Errorf("failed to create object: %w", core.DiagsToError(diags)) } + return output, nil } @@ -630,10 +668,12 @@ func toCreatePayload(model *Model, parameters *parametersModel) (*mariadb.Create if model == nil { return nil, fmt.Errorf("nil model") } + payloadParams, err := toInstanceParams(parameters) if err != nil { return nil, fmt.Errorf("convert parameters: %w", err) } + return &mariadb.CreateInstancePayload{ InstanceName: conversion.StringValueToPointer(model.Name), PlanId: conversion.StringValueToPointer(model.PlanId), @@ -645,10 +685,12 @@ func toUpdatePayload(model *Model, parameters *parametersModel) (*mariadb.Partia if model == nil { return nil, fmt.Errorf("nil model") } + payloadParams, err := toInstanceParams(parameters) if err != nil { return nil, fmt.Errorf("convert parameters: %w", err) } + return &mariadb.PartialUpdateInstancePayload{ PlanId: conversion.StringValueToPointer(model.PlanId), Parameters: payloadParams, @@ -659,6 +701,7 @@ func toInstanceParams(parameters *parametersModel) (*mariadb.InstanceParameters, if parameters == nil { return nil, nil } + payloadParams := &mariadb.InstanceParameters{} payloadParams.SgwAcl = conversion.StringValueToPointer(parameters.SgwAcl) @@ -673,6 +716,7 @@ func toInstanceParams(parameters *parametersModel) (*mariadb.InstanceParameters, if err != nil { return nil, fmt.Errorf("convert syslog: %w", err) } + payloadParams.Syslog = syslog return payloadParams, nil @@ -680,6 +724,7 @@ func toInstanceParams(parameters *parametersModel) (*mariadb.InstanceParameters, func (r *instanceResource) loadPlanId(ctx context.Context, model *Model) error { projectId := model.ProjectId.ValueString() + res, err := r.client.ListOfferings(ctx, projectId).Execute() if err != nil { return fmt.Errorf("getting MariaDB offerings: %w", err) @@ -690,21 +735,25 @@ func (r *instanceResource) loadPlanId(ctx context.Context, model *Model) error { availableVersions := "" availablePlanNames := "" isValidVersion := false + for _, offer := range *res.Offerings { if !strings.EqualFold(*offer.Version, version) { availableVersions = fmt.Sprintf("%s\n- %s", availableVersions, *offer.Version) continue } + isValidVersion = true for _, plan := range *offer.Plans { if plan.Name == nil { continue } + if strings.EqualFold(*plan.Name, planName) && plan.Id != nil { model.PlanId = types.StringPointerValue(plan.Id) return nil } + availablePlanNames = fmt.Sprintf("%s\n- %s", availablePlanNames, *plan.Name) } } @@ -712,12 +761,14 @@ func (r *instanceResource) loadPlanId(ctx context.Context, model *Model) error { if !isValidVersion { return fmt.Errorf("couldn't find version '%s', available versions are: %s", version, availableVersions) } + return fmt.Errorf("couldn't find plan_name '%s' for version %s, available names are: %s", planName, version, availablePlanNames) } func loadPlanNameAndVersion(ctx context.Context, client *mariadb.APIClient, model *Model) error { projectId := model.ProjectId.ValueString() planId := model.PlanId.ValueString() + res, err := client.ListOfferings(ctx, projectId).Execute() if err != nil { return fmt.Errorf("getting MariaDB offerings: %w", err) @@ -728,6 +779,7 @@ func loadPlanNameAndVersion(ctx context.Context, client *mariadb.APIClient, mode if strings.EqualFold(*plan.Id, planId) && plan.Id != nil { model.PlanName = types.StringPointerValue(plan.Name) model.Version = types.StringPointerValue(offer.Version) + return nil } } diff --git a/stackit/internal/services/mariadb/instance/resource_test.go b/stackit/internal/services/mariadb/instance/resource_test.go index c9a1af9de..38a58be88 100644 --- a/stackit/internal/services/mariadb/instance/resource_test.go +++ b/stackit/internal/services/mariadb/instance/resource_test.go @@ -149,13 +149,16 @@ func TestMapFields(t *testing.T) { ProjectId: tt.expected.ProjectId, InstanceId: tt.expected.InstanceId, } + err := mapFields(tt.input, state) if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(state, &tt.expected) if diff != "" { @@ -229,22 +232,27 @@ func TestToCreatePayload(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { var parameters *parametersModel + if tt.input != nil { - if !(tt.input.Parameters.IsNull() || tt.input.Parameters.IsUnknown()) { + if !tt.input.Parameters.IsNull() && !tt.input.Parameters.IsUnknown() { parameters = ¶metersModel{} + diags := tt.input.Parameters.As(context.Background(), parameters, basetypes.ObjectAsOptions{}) if diags.HasError() { t.Fatalf("Error converting parameters: %v", diags.Errors()) } } } + output, err := toCreatePayload(tt.input, parameters) if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(output, tt.expected) if diff != "" { @@ -312,22 +320,27 @@ func TestToUpdatePayload(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { var parameters *parametersModel + if tt.input != nil { - if !(tt.input.Parameters.IsNull() || tt.input.Parameters.IsUnknown()) { + if !tt.input.Parameters.IsNull() && !tt.input.Parameters.IsUnknown() { parameters = ¶metersModel{} + diags := tt.input.Parameters.As(context.Background(), parameters, basetypes.ObjectAsOptions{}) if diags.HasError() { t.Fatalf("Error converting parameters: %v", diags.Errors()) } } } + output, err := toUpdatePayload(tt.input, parameters) if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(output, tt.expected) if diff != "" { diff --git a/stackit/internal/services/mariadb/mariadb_acc_test.go b/stackit/internal/services/mariadb/mariadb_acc_test.go index 3d40b8774..a1e0f7fff 100644 --- a/stackit/internal/services/mariadb/mariadb_acc_test.go +++ b/stackit/internal/services/mariadb/mariadb_acc_test.go @@ -52,15 +52,17 @@ func configVarsMaxUpdated() config.Variables { for k, v := range testConfigVarsMax { updatedConfig[k] = v } + updatedConfig["parameters_max_disk_threshold"] = config.IntegerVariable(85) updatedConfig["parameters_metrics_frequency"] = config.IntegerVariable(10) updatedConfig["parameters_graphite"] = config.StringVariable("graphite.stackit.cloud:2003") updatedConfig["parameters_sgw_acl"] = config.StringVariable("192.168.1.0/24") updatedConfig["parameters_syslog"] = config.StringVariable("test.log:514") + return updatedConfig } -// minimum configuration +// minimum configuration. func TestAccMariaDbResourceMin(t *testing.T) { t.Logf("Maria test instance name: %s", testutil.ConvertConfigVariable(testConfigVarsMin["name"])) resource.ParallelTest(t, resource.TestCase{ @@ -164,6 +166,7 @@ func TestAccMariaDbResourceMin(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute instance_id") } + return fmt.Sprintf("%s,%s", testutil.ProjectId, instanceId), nil }, ImportState: true, @@ -185,6 +188,7 @@ func TestAccMariaDbResourceMin(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute credential_id") } + return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, instanceId, credentialId), nil }, ImportState: true, @@ -196,7 +200,7 @@ func TestAccMariaDbResourceMin(t *testing.T) { }) } -// maximum configuration +// maximum configuration. func TestAccMariaDbResourceMax(t *testing.T) { t.Logf("Maria test instance name: %s", testutil.ConvertConfigVariable(testConfigVarsMax["name"])) resource.ParallelTest(t, resource.TestCase{ @@ -325,6 +329,7 @@ func TestAccMariaDbResourceMax(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute instance_id") } + return fmt.Sprintf("%s,%s", testutil.ProjectId, instanceId), nil }, ImportState: true, @@ -346,6 +351,7 @@ func TestAccMariaDbResourceMax(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute credential_id") } + return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, instanceId, credentialId), nil }, ImportState: true, @@ -409,7 +415,9 @@ func TestAccMariaDbResourceMax(t *testing.T) { func testAccCheckMariaDBDestroy(s *terraform.State) error { ctx := context.Background() + var client *mariadb.APIClient + var err error if testutil.MariaDBCustomEndpoint == "" { client, err = mariadb.NewAPIClient( @@ -420,11 +428,13 @@ func testAccCheckMariaDBDestroy(s *terraform.State) error { stackitSdkConfig.WithEndpoint(testutil.MariaDBCustomEndpoint), ) } + if err != nil { return fmt.Errorf("creating client: %w", err) } instancesToDestroy := []string{} + for _, rs := range s.RootModule().Resources { if rs.Type != "stackit_mariadb_instance" { continue @@ -444,12 +454,14 @@ func testAccCheckMariaDBDestroy(s *terraform.State) error { if instances[i].InstanceId == nil { continue } + if utils.Contains(instancesToDestroy, *instances[i].InstanceId) { if !checkInstanceDeleteSuccess(&instances[i]) { err := client.DeleteInstanceExecute(ctx, testutil.ProjectId, *instances[i].InstanceId) if err != nil { return fmt.Errorf("destroying instance %s during CheckDestroy: %w", *instances[i].InstanceId, err) } + _, err = wait.DeleteInstanceWaitHandler(ctx, client, testutil.ProjectId, *instances[i].InstanceId).WaitWithContext(ctx) if err != nil { return fmt.Errorf("destroying instance %s during CheckDestroy: waiting for deletion %w", *instances[i].InstanceId, err) @@ -457,6 +469,7 @@ func testAccCheckMariaDBDestroy(s *terraform.State) error { } } } + return nil } @@ -472,5 +485,6 @@ func checkInstanceDeleteSuccess(i *mariadb.Instance) bool { return false } } + return true } diff --git a/stackit/internal/services/mariadb/utils/util.go b/stackit/internal/services/mariadb/utils/util.go index 21928e169..44dde1dd6 100644 --- a/stackit/internal/services/mariadb/utils/util.go +++ b/stackit/internal/services/mariadb/utils/util.go @@ -21,6 +21,7 @@ func ConfigureClient(ctx context.Context, providerData *core.ProviderData, diags } else { apiClientConfigOptions = append(apiClientConfigOptions, config.WithRegion(providerData.GetRegion())) } + apiClient, err := mariadb.NewAPIClient(apiClientConfigOptions...) if err != nil { core.LogAndAddError(ctx, diags, "Error configuring API client", fmt.Sprintf("Configuring client: %v. This is an error related to the provider configuration, not to the resource configuration", err)) diff --git a/stackit/internal/services/mariadb/utils/util_test.go b/stackit/internal/services/mariadb/utils/util_test.go index 88dfa102b..a5de654e3 100644 --- a/stackit/internal/services/mariadb/utils/util_test.go +++ b/stackit/internal/services/mariadb/utils/util_test.go @@ -22,6 +22,7 @@ const ( func TestConfigureClient(t *testing.T) { /* mock authentication by setting service account token env variable */ os.Clearenv() + err := os.Setenv(sdkClients.ServiceAccountToken, "mock-val") if err != nil { t.Errorf("error setting env variable: %v", err) @@ -30,6 +31,7 @@ func TestConfigureClient(t *testing.T) { type args struct { providerData *core.ProviderData } + tests := []struct { name string args args @@ -51,6 +53,7 @@ func TestConfigureClient(t *testing.T) { if err != nil { t.Errorf("error configuring client: %v", err) } + return apiClient }(), wantErr: false, @@ -71,6 +74,7 @@ func TestConfigureClient(t *testing.T) { if err != nil { t.Errorf("error configuring client: %v", err) } + return apiClient }(), wantErr: false, @@ -82,6 +86,7 @@ func TestConfigureClient(t *testing.T) { diags := diag.Diagnostics{} actual := ConfigureClient(ctx, tt.args.providerData, &diags) + if diags.HasError() != tt.wantErr { t.Errorf("ConfigureClient() error = %v, want %v", diags.HasError(), tt.wantErr) } diff --git a/stackit/internal/services/modelserving/modelserving_acc_test.go b/stackit/internal/services/modelserving/modelserving_acc_test.go index 4f31d5ca4..6316ec5a4 100644 --- a/stackit/internal/services/modelserving/modelserving_acc_test.go +++ b/stackit/internal/services/modelserving/modelserving_acc_test.go @@ -16,7 +16,7 @@ import ( "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/testutil" ) -// Token resource data +// Token resource data. var tokenResource = map[string]string{ "project_id": testutil.ProjectId, "name": "token01", @@ -93,7 +93,9 @@ func TestAccModelServingTokenResource(t *testing.T) { func testAccCheckModelServingTokenDestroy(s *terraform.State) error { ctx := context.Background() + var client *modelserving.APIClient + var err error if testutil.ModelServingCustomEndpoint == "" { @@ -109,6 +111,7 @@ func testAccCheckModelServingTokenDestroy(s *terraform.State) error { } tokensToDestroy := []string{} + for _, rs := range s.RootModule().Resources { if rs.Type != "stackit_modelserving_token" { continue @@ -119,6 +122,7 @@ func testAccCheckModelServingTokenDestroy(s *terraform.State) error { if len(idParts) != 3 { return fmt.Errorf("invalid ID: %s", rs.Primary.ID) } + if idParts[2] != "" { tokensToDestroy = append(tokensToDestroy, idParts[2]) } @@ -143,16 +147,19 @@ func testAccCheckModelServingTokenDestroy(s *terraform.State) error { if items[i].Name == nil { continue } + if utils.Contains(tokensToDestroy, *items[i].Name) { _, err := client.DeleteToken(ctx, testutil.Region, testutil.ProjectId, *items[i].Id).Execute() if err != nil { return fmt.Errorf("destroying token %s during CheckDestroy: %w", *items[i].Name, err) } + _, err = wait.DeleteModelServingWaitHandler(ctx, client, testutil.Region, testutil.ProjectId, *items[i].Id).WaitWithContext(ctx) if err != nil { return fmt.Errorf("destroying token %s during CheckDestroy: waiting for deletion %w", *items[i].Name, err) } } } + return nil } diff --git a/stackit/internal/services/modelserving/token/resource.go b/stackit/internal/services/modelserving/token/resource.go index 0bdb79f38..001e9a52f 100644 --- a/stackit/internal/services/modelserving/token/resource.go +++ b/stackit/internal/services/modelserving/token/resource.go @@ -8,9 +8,6 @@ import ( "net/http" "time" - modelservingUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/modelserving/utils" - serviceenablementUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/serviceenablement/utils" - "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" @@ -27,6 +24,8 @@ import ( serviceEnablementWait "github.com/stackitcloud/stackit-sdk-go/services/serviceenablement/wait" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + modelservingUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/modelserving/utils" + serviceenablementUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/serviceenablement/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" ) @@ -83,40 +82,50 @@ func (r *tokenResource) Metadata(_ context.Context, req resource.MetadataRequest // Configure adds the provider configured client to the resource. func (r *tokenResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { var ok bool + r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) if !ok { return } apiClient := modelservingUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + serviceEnablementClient := serviceenablementUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + r.client = apiClient r.serviceEnablementClient = serviceEnablementClient + tflog.Info(ctx, "Model-Serving auth token client configured") } // ModifyPlan implements resource.ResourceWithModifyPlan. // Use the modifier to set the effective region in the current plan. -func (r *tokenResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform +func (r *tokenResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { //nolint:gocritic // function signature required by Terraform var configModel Model // skip initial empty configuration to avoid follow-up errors if req.Config.Raw.IsNull() { return } + resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...) + if resp.Diagnostics.HasError() { return } var planModel Model + resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) + if resp.Diagnostics.HasError() { return } @@ -128,11 +137,13 @@ func (r *tokenResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanR r.providerData.GetRegion(), resp, ) + if resp.Diagnostics.HasError() { return } resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...) + if resp.Diagnostics.HasError() { return } @@ -228,11 +239,12 @@ func (r *tokenResource) Schema(_ context.Context, _ resource.SchemaRequest, resp } // Create creates the resource and sets the initial Terraform state. -func (r *tokenResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *tokenResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { //nolint:gocritic // function signature required by Terraform // Retrieve values from plan var model Model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -254,15 +266,18 @@ func (r *tokenResource) Create(ctx context.Context, req resource.CreateRequest, core.LogAndAddError(ctx, &resp.Diagnostics, "Error enabling AI model serving", fmt.Sprintf("Service not available in region %s \n%v", region, err), ) + return } } + core.LogAndAddError( ctx, &resp.Diagnostics, "Error enabling AI model serving", fmt.Sprintf("Error enabling AI model serving: %v", err), ) + return } @@ -275,6 +290,7 @@ func (r *tokenResource) Create(ctx context.Context, req resource.CreateRequest, "Error enabling AI model serving", fmt.Sprintf("Error enabling AI model serving: %v", err), ) + return } @@ -296,6 +312,7 @@ func (r *tokenResource) Create(ctx context.Context, req resource.CreateRequest, "Error creating AI model serving auth token", fmt.Sprintf("Calling API: %v", err), ) + return } @@ -315,6 +332,7 @@ func (r *tokenResource) Create(ctx context.Context, req resource.CreateRequest, // Set state to fully populated data diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -323,10 +341,11 @@ func (r *tokenResource) Create(ctx context.Context, req resource.CreateRequest, } // Read refreshes the Terraform state with the latest data. -func (r *tokenResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (r *tokenResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -352,6 +371,7 @@ func (r *tokenResource) Read(ctx context.Context, req resource.ReadRequest, resp } core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading AI model serving auth token", fmt.Sprintf("Calling API: %v", err)) + return } @@ -359,6 +379,7 @@ func (r *tokenResource) Read(ctx context.Context, req resource.ReadRequest, resp *getTokenResp.Token.State == inactiveState { resp.State.RemoveResource(ctx) core.LogAndAddWarning(ctx, &resp.Diagnostics, "Error reading AI model serving auth token", "AI model serving auth token has expired") + return } @@ -372,6 +393,7 @@ func (r *tokenResource) Read(ctx context.Context, req resource.ReadRequest, resp // Set refreshed state diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -380,11 +402,12 @@ func (r *tokenResource) Read(ctx context.Context, req resource.ReadRequest, resp } // Update updates the resource and sets the updated Terraform state on success. -func (r *tokenResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *tokenResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { //nolint:gocritic // function signature required by Terraform // Retrieve values from plan var model Model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -393,6 +416,7 @@ func (r *tokenResource) Update(ctx context.Context, req resource.UpdateRequest, var state Model diags = req.State.Get(ctx, &state) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -437,6 +461,7 @@ func (r *tokenResource) Update(ctx context.Context, req resource.UpdateRequest, projectId, ), ) + return } @@ -444,6 +469,7 @@ func (r *tokenResource) Update(ctx context.Context, req resource.UpdateRequest, *updateTokenResp.Token.State == inactiveState { resp.State.RemoveResource(ctx) core.LogAndAddWarning(ctx, &resp.Diagnostics, "Error updating AI model serving auth token", "AI model serving auth token has expired") + return } @@ -455,6 +481,7 @@ func (r *tokenResource) Update(ctx context.Context, req resource.UpdateRequest, // Since STACKIT is not saving the content of the token. We have to use it from the state. model.Token = state.Token + err = mapGetResponse(waitResp, &model) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating AI model serving auth token", fmt.Sprintf("Processing API payload: %v", err)) @@ -463,6 +490,7 @@ func (r *tokenResource) Update(ctx context.Context, req resource.UpdateRequest, diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -471,11 +499,12 @@ func (r *tokenResource) Update(ctx context.Context, req resource.UpdateRequest, } // Delete deletes the resource and removes the Terraform state on success. -func (r *tokenResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform +func (r *tokenResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { //nolint:gocritic // function signature required by Terraform // Retrieve values from plan var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -501,6 +530,7 @@ func (r *tokenResource) Delete(ctx context.Context, req resource.DeleteRequest, } core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting AI model serving auth token", fmt.Sprintf("Calling API: %v", err)) + return } @@ -518,6 +548,7 @@ func mapCreateResponse(tokenCreateResp *modelserving.CreateTokenResponse, waitRe if tokenCreateResp == nil || tokenCreateResp.Token == nil { return fmt.Errorf("response input is nil") } + if model == nil { return fmt.Errorf("model input is nil") } @@ -556,6 +587,7 @@ func mapGetResponse(tokenGetResp *modelserving.GetTokenResponse, model *Model) e if tokenGetResp.Token == nil { return fmt.Errorf("response input is nil") } + if model == nil { return fmt.Errorf("model input is nil") } diff --git a/stackit/internal/services/modelserving/utils/util.go b/stackit/internal/services/modelserving/utils/util.go index 5b2bc505a..054d2b861 100644 --- a/stackit/internal/services/modelserving/utils/util.go +++ b/stackit/internal/services/modelserving/utils/util.go @@ -19,6 +19,7 @@ func ConfigureClient(ctx context.Context, providerData *core.ProviderData, diags if providerData.ModelServingCustomEndpoint != "" { apiClientConfigOptions = append(apiClientConfigOptions, config.WithEndpoint(providerData.ModelServingCustomEndpoint)) } + apiClient, err := modelserving.NewAPIClient(apiClientConfigOptions...) if err != nil { core.LogAndAddError(ctx, diags, "Error configuring API client", fmt.Sprintf("Configuring client: %v. This is an error related to the provider configuration, not to the resource configuration", err)) diff --git a/stackit/internal/services/modelserving/utils/util_test.go b/stackit/internal/services/modelserving/utils/util_test.go index e03889e20..6c7749322 100644 --- a/stackit/internal/services/modelserving/utils/util_test.go +++ b/stackit/internal/services/modelserving/utils/util_test.go @@ -22,6 +22,7 @@ const ( func TestConfigureClient(t *testing.T) { /* mock authentication by setting service account token env variable */ os.Clearenv() + err := os.Setenv(sdkClients.ServiceAccountToken, "mock-val") if err != nil { t.Errorf("error setting env variable: %v", err) @@ -30,6 +31,7 @@ func TestConfigureClient(t *testing.T) { type args struct { providerData *core.ProviderData } + tests := []struct { name string args args @@ -50,6 +52,7 @@ func TestConfigureClient(t *testing.T) { if err != nil { t.Errorf("error configuring client: %v", err) } + return apiClient }(), wantErr: false, @@ -70,6 +73,7 @@ func TestConfigureClient(t *testing.T) { if err != nil { t.Errorf("error configuring client: %v", err) } + return apiClient }(), wantErr: false, @@ -81,6 +85,7 @@ func TestConfigureClient(t *testing.T) { diags := diag.Diagnostics{} actual := ConfigureClient(ctx, tt.args.providerData, &diags) + if diags.HasError() != tt.wantErr { t.Errorf("ConfigureClient() error = %v, want %v", diags.HasError(), tt.wantErr) } diff --git a/stackit/internal/services/mongodbflex/instance/datasource.go b/stackit/internal/services/mongodbflex/instance/datasource.go index 3ce92a3f5..5f4ba2695 100644 --- a/stackit/internal/services/mongodbflex/instance/datasource.go +++ b/stackit/internal/services/mongodbflex/instance/datasource.go @@ -5,20 +5,18 @@ import ( "fmt" "net/http" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - mongodbflexUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/mongodbflex/utils" - "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "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/services/mongodbflex" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + mongodbflexUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/mongodbflex/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - - "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex" ) // Ensure the implementation satisfies the expected interfaces. @@ -45,16 +43,20 @@ func (d *instanceDataSource) Metadata(_ context.Context, req datasource.Metadata // Configure adds the provider configured client to the data source. func (d *instanceDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { var ok bool + d.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) if !ok { return } apiClient := mongodbflexUtils.ConfigureClient(ctx, &d.providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + d.client = apiClient + tflog.Info(ctx, "MongoDB Flex instance client configured") } @@ -189,10 +191,11 @@ func (d *instanceDataSource) Schema(_ context.Context, _ datasource.SchemaReques } // Read refreshes the Terraform state with the latest data. -func (d *instanceDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (d *instanceDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.Config.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -216,29 +219,35 @@ func (d *instanceDataSource) Read(ctx context.Context, req datasource.ReadReques }, ) resp.State.RemoveResource(ctx) + return } - var flavor = &flavorModel{} - if !(model.Flavor.IsNull() || model.Flavor.IsUnknown()) { + flavor := &flavorModel{} + if !model.Flavor.IsNull() && !model.Flavor.IsUnknown() { diags = model.Flavor.As(ctx, flavor, basetypes.ObjectAsOptions{}) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } } - var storage = &storageModel{} - if !(model.Storage.IsNull() || model.Storage.IsUnknown()) { + + storage := &storageModel{} + if !model.Storage.IsNull() && !model.Storage.IsUnknown() { diags = model.Storage.As(ctx, storage, basetypes.ObjectAsOptions{}) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } } - var options = &optionsModel{} - if !(model.Options.IsNull() || model.Options.IsUnknown()) { + + options := &optionsModel{} + if !model.Options.IsNull() && !model.Options.IsUnknown() { diags = model.Options.As(ctx, options, basetypes.ObjectAsOptions{}) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -252,8 +261,10 @@ func (d *instanceDataSource) Read(ctx context.Context, req datasource.ReadReques // Set refreshed state diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "MongoDB Flex instance read") } diff --git a/stackit/internal/services/mongodbflex/instance/resource.go b/stackit/internal/services/mongodbflex/instance/resource.go index a4330fd80..46f67e24a 100644 --- a/stackit/internal/services/mongodbflex/instance/resource.go +++ b/stackit/internal/services/mongodbflex/instance/resource.go @@ -9,29 +9,27 @@ import ( "strings" "time" - mongodbflexUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/mongodbflex/utils" - "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "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/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" "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/services/mongodbflex" "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex/wait" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + mongodbflexUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/mongodbflex/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" ) // Ensure the implementation satisfies the expected interfaces. @@ -57,7 +55,7 @@ type Model struct { Region types.String `tfsdk:"region"` } -// Struct corresponding to Model.Flavor +// Struct corresponding to Model.Flavor. type flavorModel struct { Id types.String `tfsdk:"id"` Description types.String `tfsdk:"description"` @@ -65,7 +63,7 @@ type flavorModel struct { RAM types.Int64 `tfsdk:"ram"` } -// Types corresponding to flavorModel +// Types corresponding to flavorModel. var flavorTypes = map[string]attr.Type{ "id": basetypes.StringType{}, "description": basetypes.StringType{}, @@ -73,19 +71,19 @@ var flavorTypes = map[string]attr.Type{ "ram": basetypes.Int64Type{}, } -// Struct corresponding to Model.Storage +// Struct corresponding to Model.Storage. type storageModel struct { Class types.String `tfsdk:"class"` Size types.Int64 `tfsdk:"size"` } -// Types corresponding to storageModel +// Types corresponding to storageModel. var storageTypes = map[string]attr.Type{ "class": basetypes.StringType{}, "size": basetypes.Int64Type{}, } -// Struct corresponding to Model.Options +// Struct corresponding to Model.Options. type optionsModel struct { Type types.String `tfsdk:"type"` SnapshotRetentionDays types.Int64 `tfsdk:"snapshot_retention_days"` @@ -95,7 +93,7 @@ type optionsModel struct { MonthlySnapshotRetentionMonths types.Int64 `tfsdk:"monthly_snapshot_retention_months"` } -// Types corresponding to optionsModel +// Types corresponding to optionsModel. var optionsTypes = map[string]attr.Type{ "type": basetypes.StringType{}, "snapshot_retention_days": basetypes.Int64Type{}, @@ -124,44 +122,54 @@ func (r *instanceResource) Metadata(_ context.Context, req resource.MetadataRequ // Configure adds the provider configured client to the resource. func (r *instanceResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { var ok bool + r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) if !ok { return } apiClient := mongodbflexUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + r.client = apiClient + tflog.Info(ctx, "MongoDB Flex instance client configured") } // ModifyPlan implements resource.ResourceWithModifyPlan. // Use the modifier to set the effective region in the current plan. -func (r *instanceResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform +func (r *instanceResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { //nolint:gocritic // function signature required by Terraform var configModel Model // skip initial empty configuration to avoid follow-up errors if req.Config.Raw.IsNull() { return } + resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...) + if resp.Diagnostics.HasError() { return } var planModel Model + resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) + if resp.Diagnostics.HasError() { return } utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp) + if resp.Diagnostics.HasError() { return } resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...) + if resp.Diagnostics.HasError() { return } @@ -347,53 +355,62 @@ func (r *instanceResource) Schema(_ context.Context, _ resource.SchemaRequest, r } // Create creates the resource and sets the initial Terraform state. -func (r *instanceResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *instanceResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { //nolint:gocritic // function signature required by Terraform // Retrieve values from plan var model Model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() region := r.providerData.GetRegionWithOverride(model.Region) ctx = tflog.SetField(ctx, "project_id", projectId) ctx = tflog.SetField(ctx, "region", region) var acl []string - if !(model.ACL.IsNull() || model.ACL.IsUnknown()) { + if !model.ACL.IsNull() && !model.ACL.IsUnknown() { diags = model.ACL.ElementsAs(ctx, &acl, false) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } } - var flavor = &flavorModel{} - if !(model.Flavor.IsNull() || model.Flavor.IsUnknown()) { + + flavor := &flavorModel{} + if !model.Flavor.IsNull() && !model.Flavor.IsUnknown() { diags = model.Flavor.As(ctx, flavor, basetypes.ObjectAsOptions{}) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + err := loadFlavorId(ctx, r.client, &model, flavor, region) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Loading flavor ID: %v", err)) return } } - var storage = &storageModel{} - if !(model.Storage.IsNull() || model.Storage.IsUnknown()) { + + storage := &storageModel{} + if !model.Storage.IsNull() && !model.Storage.IsUnknown() { diags = model.Storage.As(ctx, storage, basetypes.ObjectAsOptions{}) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } } - var options = &optionsModel{} - if !(model.Options.IsNull() || model.Options.IsUnknown()) { + options := &optionsModel{} + if !model.Options.IsNull() && !model.Options.IsUnknown() { diags = model.Options.As(ctx, options, basetypes.ObjectAsOptions{}) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -411,26 +428,33 @@ func (r *instanceResource) Create(ctx context.Context, req resource.CreateReques core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Calling API: %v", err)) return } + if createResp == nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", "API response is empty") return } + if createResp.Id == nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", "API response does not contain instance id") return } + instanceId := *createResp.Id ctx = tflog.SetField(ctx, "instance_id", instanceId) diags = resp.State.SetAttribute(ctx, path.Root("project_id"), projectId) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + diags = resp.State.SetAttribute(ctx, path.Root("instance_id"), instanceId) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + waitResp, err := wait.CreateInstanceWaitHandler(ctx, r.client, projectId, instanceId, region).WaitWithContext(ctx) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Instance creation waiting: %v", err)) @@ -446,6 +470,7 @@ func (r *instanceResource) Create(ctx context.Context, req resource.CreateReques diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -455,6 +480,7 @@ func (r *instanceResource) Create(ctx context.Context, req resource.CreateReques core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Creating API payload: %v", err)) return } + backupScheduleOptions, err := r.client.UpdateBackupSchedule(ctx, projectId, instanceId, region).UpdateBackupSchedulePayload(*backupScheduleOptionsPayload).Execute() if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Updating options: %v", err)) @@ -470,20 +496,24 @@ func (r *instanceResource) Create(ctx context.Context, req resource.CreateReques // Set state to fully populated data diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "MongoDB Flex instance created") } // Read refreshes the Terraform state with the latest data. -func (r *instanceResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (r *instanceResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() region := r.providerData.GetRegionWithOverride(model.Region) instanceId := model.InstanceId.ValueString() @@ -491,27 +521,31 @@ func (r *instanceResource) Read(ctx context.Context, req resource.ReadRequest, r ctx = tflog.SetField(ctx, "region", region) ctx = tflog.SetField(ctx, "instance_id", instanceId) - var flavor = &flavorModel{} - if !(model.Flavor.IsNull() || model.Flavor.IsUnknown()) { + flavor := &flavorModel{} + if !model.Flavor.IsNull() && !model.Flavor.IsUnknown() { diags = model.Flavor.As(ctx, flavor, basetypes.ObjectAsOptions{}) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } } - var storage = &storageModel{} - if !(model.Storage.IsNull() || model.Storage.IsUnknown()) { + + storage := &storageModel{} + if !model.Storage.IsNull() && !model.Storage.IsUnknown() { diags = model.Storage.As(ctx, storage, basetypes.ObjectAsOptions{}) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } } - var options = &optionsModel{} - if !(model.Options.IsNull() || model.Options.IsUnknown()) { + options := &optionsModel{} + if !model.Options.IsNull() && !model.Options.IsUnknown() { diags = model.Options.As(ctx, options, basetypes.ObjectAsOptions{}) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -524,7 +558,9 @@ func (r *instanceResource) Read(ctx context.Context, req resource.ReadRequest, r resp.State.RemoveResource(ctx) return } + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", err.Error()) + return } @@ -537,21 +573,25 @@ func (r *instanceResource) Read(ctx context.Context, req resource.ReadRequest, r // Set refreshed state diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "MongoDB Flex instance read") } // Update updates the resource and sets the updated Terraform state on success. -func (r *instanceResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *instanceResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { //nolint:gocritic // function signature required by Terraform // Retrieve values from plan var model Model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() region := r.providerData.GetRegionWithOverride(model.Region) instanceId := model.InstanceId.ValueString() @@ -560,39 +600,46 @@ func (r *instanceResource) Update(ctx context.Context, req resource.UpdateReques ctx = tflog.SetField(ctx, "instance_id", instanceId) var acl []string - if !(model.ACL.IsNull() || model.ACL.IsUnknown()) { + if !model.ACL.IsNull() && !model.ACL.IsUnknown() { diags = model.ACL.ElementsAs(ctx, &acl, false) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } } - var flavor = &flavorModel{} - if !(model.Flavor.IsNull() || model.Flavor.IsUnknown()) { + + flavor := &flavorModel{} + if !model.Flavor.IsNull() && !model.Flavor.IsUnknown() { diags = model.Flavor.As(ctx, flavor, basetypes.ObjectAsOptions{}) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + err := loadFlavorId(ctx, r.client, &model, flavor, region) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Loading flavor ID: %v", err)) return } } - var storage = &storageModel{} - if !(model.Storage.IsNull() || model.Storage.IsUnknown()) { + + storage := &storageModel{} + if !model.Storage.IsNull() && !model.Storage.IsUnknown() { diags = model.Storage.As(ctx, storage, basetypes.ObjectAsOptions{}) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } } - var options = &optionsModel{} - if !(model.Options.IsNull() || model.Options.IsUnknown()) { + options := &optionsModel{} + if !model.Options.IsNull() && !model.Options.IsUnknown() { diags = model.Options.As(ctx, options, basetypes.ObjectAsOptions{}) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -610,6 +657,7 @@ func (r *instanceResource) Update(ctx context.Context, req resource.UpdateReques core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", err.Error()) return } + waitResp, err := wait.UpdateInstanceWaitHandler(ctx, r.client, projectId, instanceId, region).WaitWithContext(ctx) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Instance update waiting: %v", err)) @@ -628,6 +676,7 @@ func (r *instanceResource) Update(ctx context.Context, req resource.UpdateReques core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Creating API payload: %v", err)) return } + backupScheduleOptions, err := r.client.UpdateBackupSchedule(ctx, projectId, instanceId, region).UpdateBackupSchedulePayload(*backupScheduleOptionsPayload).Execute() if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Updating options: %v", err)) @@ -642,21 +691,25 @@ func (r *instanceResource) Update(ctx context.Context, req resource.UpdateReques diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "MongoDB Flex instance updated") } // Delete deletes the resource and removes the Terraform state on success. -func (r *instanceResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform +func (r *instanceResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { //nolint:gocritic // function signature required by Terraform // Retrieve values from state var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() region := r.providerData.GetRegionWithOverride(model.Region) instanceId := model.InstanceId.ValueString() @@ -670,6 +723,7 @@ func (r *instanceResource) Delete(ctx context.Context, req resource.DeleteReques core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting instance", fmt.Sprintf("Calling API: %v", err)) return } + _, err = wait.DeleteInstanceWaitHandler(ctx, r.client, projectId, instanceId, region).WaitWithContext(ctx) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting instance", fmt.Sprintf("Instance deletion waiting: %v", err)) @@ -684,7 +738,7 @@ func (r *instanceResource) Delete(ctx context.Context, req resource.DeleteReques } // ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: project_id,instance_id +// The expected format of the resource import identifier is: project_id,instance_id. func (r *instanceResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { idParts := strings.Split(req.ID, core.Separator) @@ -693,6 +747,7 @@ func (r *instanceResource) ImportState(ctx context.Context, req resource.ImportS "Error importing instance", fmt.Sprintf("Expected import identifier with format: [project_id],[region],[instance_id] Got: %q", req.ID), ) + return } @@ -706,12 +761,15 @@ func mapFields(ctx context.Context, resp *mongodbflex.InstanceResponse, model *M if resp == nil { return fmt.Errorf("response input is nil") } + if resp.Item == nil { return fmt.Errorf("no instance provided") } + if model == nil { return fmt.Errorf("model input is nil") } + instance := resp.Item var instanceId string @@ -724,11 +782,14 @@ func mapFields(ctx context.Context, resp *mongodbflex.InstanceResponse, model *M } var aclList basetypes.ListValue + var diags diag.Diagnostics + if instance.Acl == nil || instance.Acl.Items == nil { aclList = types.ListNull(types.StringType) } else { respACL := *instance.Acl.Items + modelACL, err := utils.ListValuetoStringSlice(model.ACL) if err != nil { return err @@ -758,6 +819,7 @@ func mapFields(ctx context.Context, resp *mongodbflex.InstanceResponse, model *M "ram": types.Int64PointerValue(instance.Flavor.Memory), } } + flavorObject, diags := types.ObjectValue(flavorTypes, flavorValues) if diags.HasError() { return fmt.Errorf("creating flavor: %w", core.DiagsToError(diags)) @@ -775,6 +837,7 @@ func mapFields(ctx context.Context, resp *mongodbflex.InstanceResponse, model *M "size": types.Int64PointerValue(instance.Storage.Size), } } + storageObject, diags := types.ObjectValue(storageTypes, storageValues) if diags.HasError() { return fmt.Errorf("creating storage: %w", core.DiagsToError(diags)) @@ -792,26 +855,35 @@ func mapFields(ctx context.Context, resp *mongodbflex.InstanceResponse, model *M } } else { snapshotRetentionDaysStr := (*instance.Options)["snapshotRetentionDays"] + snapshotRetentionDays, err := strconv.ParseInt(snapshotRetentionDaysStr, 10, 64) if err != nil { return fmt.Errorf("parse snapshot retention days: %w", err) } + dailySnapshotRetentionDaysStr := (*instance.Options)["dailySnapshotRetentionDays"] + dailySnapshotRetentionDays, err := strconv.ParseInt(dailySnapshotRetentionDaysStr, 10, 64) if err != nil { return fmt.Errorf("parse daily snapshot retention days: %w", err) } + weeklySnapshotRetentionWeeksStr := (*instance.Options)["weeklySnapshotRetentionWeeks"] + weeklySnapshotRetentionWeeks, err := strconv.ParseInt(weeklySnapshotRetentionWeeksStr, 10, 64) if err != nil { return fmt.Errorf("parse weekly snapshot retention weeks: %w", err) } + monthlySnapshotRetentionMonthsStr := (*instance.Options)["monthlySnapshotRetentionMonths"] + monthlySnapshotRetentionMonths, err := strconv.ParseInt(monthlySnapshotRetentionMonthsStr, 10, 64) if err != nil { return fmt.Errorf("parse monthly snapshot retention months: %w", err) } + pointInTimeWindowHoursStr := (*instance.Options)["pointInTimeWindowHours"] + pointInTimeWindowHours, err := strconv.ParseInt(pointInTimeWindowHoursStr, 10, 64) if err != nil { return fmt.Errorf("parse point in time window hours: %w", err) @@ -826,6 +898,7 @@ func mapFields(ctx context.Context, resp *mongodbflex.InstanceResponse, model *M "point_in_time_window_hours": types.Int64Value(pointInTimeWindowHours), } } + optionsObject, diags := types.ObjectValue(optionsTypes, optionsValues) if diags.HasError() { return fmt.Errorf("creating options: %w", core.DiagsToError(diags)) @@ -848,6 +921,7 @@ func mapFields(ctx context.Context, resp *mongodbflex.InstanceResponse, model *M model.Storage = storageObject model.Version = types.StringPointerValue(instance.Version) model.Options = optionsObject + return nil } @@ -872,11 +946,14 @@ func mapOptions(model *Model, options *optionsModel, backupScheduleOptions *mong "point_in_time_window_hours": types.Int64Value(*backupScheduleOptions.PointInTimeWindowHours), } } + optionsTF, diags := types.ObjectValue(optionsTypes, optionsValues) if diags.HasError() { return fmt.Errorf("creating options: %w", core.DiagsToError(diags)) } + model.Options = optionsTF + return nil } @@ -884,15 +961,19 @@ func toCreatePayload(model *Model, acl []string, flavor *flavorModel, storage *s if model == nil { return nil, fmt.Errorf("nil model") } + if acl == nil { return nil, fmt.Errorf("nil acl") } + if flavor == nil { return nil, fmt.Errorf("nil flavor") } + if storage == nil { return nil, fmt.Errorf("nil storage") } + if options == nil { return nil, fmt.Errorf("nil options") } @@ -923,15 +1004,19 @@ func toUpdatePayload(model *Model, acl []string, flavor *flavorModel, storage *s if model == nil { return nil, fmt.Errorf("nil model") } + if acl == nil { return nil, fmt.Errorf("nil acl") } + if flavor == nil { return nil, fmt.Errorf("nil flavor") } + if storage == nil { return nil, fmt.Errorf("nil storage") } + if options == nil { return nil, fmt.Errorf("nil options") } @@ -963,8 +1048,8 @@ func toUpdateBackupScheduleOptionsPayload(ctx context.Context, model *Model, con return nil, nil } - var currOptions = &optionsModel{} - if !(model.Options.IsNull() || model.Options.IsUnknown()) { + currOptions := &optionsModel{} + if !model.Options.IsNull() && !model.Options.IsUnknown() { diags := model.Options.As(ctx, currOptions, basetypes.ObjectAsOptions{}) if diags.HasError() { return nil, fmt.Errorf("map current options: %w", core.DiagsToError(diags)) @@ -1017,39 +1102,49 @@ func loadFlavorId(ctx context.Context, client mongoDBFlexClient, model *Model, f if model == nil { return fmt.Errorf("nil model") } + if flavor == nil { return fmt.Errorf("nil flavor") } + cpu := conversion.Int64ValueToPointer(flavor.CPU) if cpu == nil { return fmt.Errorf("nil CPU") } + ram := conversion.Int64ValueToPointer(flavor.RAM) if ram == nil { return fmt.Errorf("nil RAM") } projectId := model.ProjectId.ValueString() + res, err := client.ListFlavorsExecute(ctx, projectId, region) if err != nil { return fmt.Errorf("listing mongodbflex flavors: %w", err) } avl := "" + if res.Flavors == nil { return fmt.Errorf("finding flavors for project %s", projectId) } + for _, f := range *res.Flavors { if f.Id == nil || f.Cpu == nil || f.Memory == nil { continue } + if *f.Cpu == *cpu && *f.Memory == *ram { flavor.Id = types.StringValue(*f.Id) flavor.Description = types.StringValue(*f.Description) + break } + avl = fmt.Sprintf("%s\n- %d CPU, %d GB RAM", avl, *f.Cpu, *f.Memory) } + if flavor.Id.ValueString() == "" { return fmt.Errorf("couldn't find flavor, available specs are:%s", avl) } diff --git a/stackit/internal/services/mongodbflex/instance/resource_test.go b/stackit/internal/services/mongodbflex/instance/resource_test.go index 732b5038d..0722ce925 100644 --- a/stackit/internal/services/mongodbflex/instance/resource_test.go +++ b/stackit/internal/services/mongodbflex/instance/resource_test.go @@ -5,10 +5,9 @@ import ( "fmt" "testing" - "github.com/google/uuid" - "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/stackitcloud/stackit-sdk-go/core/utils" @@ -373,9 +372,11 @@ func TestMapFields(t *testing.T) { if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(tt.state, tt.expected) if diff != "" { @@ -444,9 +445,11 @@ func TestMapOptions(t *testing.T) { if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(tt.model, tt.expected, cmpopts.IgnoreFields(Model{}, "ACL", "Flavor", "Replicas", "Storage", "Version")) if diff != "" { @@ -623,9 +626,11 @@ func TestToCreatePayload(t *testing.T) { if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(output, tt.expected) if diff != "" { @@ -802,9 +807,11 @@ func TestToUpdatePayload(t *testing.T) { if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(output, tt.expected) if diff != "" { @@ -926,9 +933,11 @@ func TestToUpdateBackupScheduleOptionsPayload(t *testing.T) { if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(output, tt.expected) if diff != "" { @@ -1075,13 +1084,16 @@ func TestLoadFlavorId(t *testing.T) { CPU: tt.inputFlavor.CPU, RAM: tt.inputFlavor.RAM, } + err := loadFlavorId(context.Background(), client, model, flavorModel, testRegion) if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(flavorModel, tt.expected) if diff != "" { diff --git a/stackit/internal/services/mongodbflex/mongodbflex_acc_test.go b/stackit/internal/services/mongodbflex/mongodbflex_acc_test.go index 0ab9598fd..9648ed08d 100644 --- a/stackit/internal/services/mongodbflex/mongodbflex_acc_test.go +++ b/stackit/internal/services/mongodbflex/mongodbflex_acc_test.go @@ -9,16 +9,15 @@ import ( "github.com/hashicorp/terraform-plugin-testing/helper/acctest" "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/terraform" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/core/config" + "github.com/stackitcloud/stackit-sdk-go/core/utils" "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex" "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex/wait" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/testutil" ) -// Instance resource data +// Instance resource data. var instanceResource = map[string]string{ "project_id": testutil.ProjectId, "name": fmt.Sprintf("tf-acc-%s", acctest.RandStringFromCharSet(7, acctest.CharSetAlphaNum)), @@ -42,7 +41,7 @@ var instanceResource = map[string]string{ "point_in_time_window_hours": "30", } -// User resource data +// User resource data. var userResource = map[string]string{ "username": fmt.Sprintf("tf-acc-user-%s", acctest.RandStringFromCharSet(7, acctest.CharSetAlpha)), "role": "read", @@ -238,6 +237,7 @@ func TestAccMongoDBFlexFlexResource(t *testing.T) { if s[0].Attributes["backup_schedule"] != instanceResource["backup_schedule_read"] { return fmt.Errorf("expected backup_schedule %s, got %s", instanceResource["backup_schedule_read"], s[0].Attributes["backup_schedule"]) } + return nil }, }, @@ -298,7 +298,9 @@ func TestAccMongoDBFlexFlexResource(t *testing.T) { func testAccCheckMongoDBFlexDestroy(s *terraform.State) error { ctx := context.Background() + var client *mongodbflex.APIClient + var err error if testutil.MongoDBFlexCustomEndpoint == "" { client, err = mongodbflex.NewAPIClient() @@ -307,11 +309,13 @@ func testAccCheckMongoDBFlexDestroy(s *terraform.State) error { config.WithEndpoint(testutil.MongoDBFlexCustomEndpoint), ) } + if err != nil { return fmt.Errorf("creating client: %w", err) } instancesToDestroy := []string{} + for _, rs := range s.RootModule().Resources { if rs.Type != "stackit_mongodbflex_instance" { continue @@ -331,16 +335,19 @@ func testAccCheckMongoDBFlexDestroy(s *terraform.State) error { if items[i].Id == nil { continue } + if utils.Contains(instancesToDestroy, *items[i].Id) { err := client.DeleteInstanceExecute(ctx, testutil.ProjectId, *items[i].Id, testutil.Region) if err != nil { return fmt.Errorf("destroying instance %s during CheckDestroy: %w", *items[i].Id, err) } + _, err = wait.DeleteInstanceWaitHandler(ctx, client, testutil.ProjectId, *items[i].Id, testutil.Region).WaitWithContext(ctx) if err != nil { return fmt.Errorf("destroying instance %s during CheckDestroy: waiting for deletion %w", *items[i].Id, err) } } } + return nil } diff --git a/stackit/internal/services/mongodbflex/user/datasource.go b/stackit/internal/services/mongodbflex/user/datasource.go index 3a7e9f029..8bb1c702a 100644 --- a/stackit/internal/services/mongodbflex/user/datasource.go +++ b/stackit/internal/services/mongodbflex/user/datasource.go @@ -5,20 +5,18 @@ import ( "fmt" "net/http" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - mongodbflexUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/mongodbflex/utils" - "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + mongodbflexUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/mongodbflex/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - - "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex" ) // Ensure the implementation satisfies the expected interfaces. @@ -58,16 +56,20 @@ func (d *userDataSource) Metadata(_ context.Context, req datasource.MetadataRequ // Configure adds the provider configured client to the data source. func (d *userDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { var ok bool + d.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) if !ok { return } apiClient := mongodbflexUtils.ConfigureClient(ctx, &d.providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + d.client = apiClient + tflog.Info(ctx, "MongoDB Flex user client configured") } @@ -139,13 +141,15 @@ func (d *userDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, r } // Read refreshes the Terraform state with the latest data. -func (d *userDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (d *userDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model DataSourceModel diags := req.Config.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() region := d.providerData.GetRegionWithOverride(model.Region) instanceId := model.InstanceId.ValueString() @@ -168,6 +172,7 @@ func (d *userDataSource) Read(ctx context.Context, req datasource.ReadRequest, r }, ) resp.State.RemoveResource(ctx) + return } @@ -181,9 +186,11 @@ func (d *userDataSource) Read(ctx context.Context, req datasource.ReadRequest, r // Set refreshed state diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "MongoDB Flex user read") } @@ -191,9 +198,11 @@ func mapDataSourceFields(userResp *mongodbflex.GetUserResponse, model *DataSourc if userResp == nil || userResp.Item == nil { return fmt.Errorf("response is nil") } + if model == nil { return fmt.Errorf("model input is nil") } + user := userResp.Item var userId string @@ -204,6 +213,7 @@ func mapDataSourceFields(userResp *mongodbflex.GetUserResponse, model *DataSourc } else { return fmt.Errorf("user id not present") } + model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), region, model.InstanceId.ValueString(), userId) model.UserId = types.StringValue(userId) model.Username = types.StringPointerValue(user.Username) @@ -216,13 +226,17 @@ func mapDataSourceFields(userResp *mongodbflex.GetUserResponse, model *DataSourc for _, role := range *user.Roles { roles = append(roles, types.StringValue(role)) } + rolesSet, diags := types.SetValue(types.StringType, roles) if diags.HasError() { return fmt.Errorf("mapping roles: %w", core.DiagsToError(diags)) } + model.Roles = rolesSet } + model.Host = types.StringPointerValue(user.Host) model.Port = types.Int64PointerValue(user.Port) + return nil } diff --git a/stackit/internal/services/mongodbflex/user/datasource_test.go b/stackit/internal/services/mongodbflex/user/datasource_test.go index e5ce87cf4..e80807852 100644 --- a/stackit/internal/services/mongodbflex/user/datasource_test.go +++ b/stackit/internal/services/mongodbflex/user/datasource_test.go @@ -128,13 +128,16 @@ func TestMapDataSourceFields(t *testing.T) { InstanceId: tt.expected.InstanceId, UserId: tt.expected.UserId, } + err := mapDataSourceFields(tt.input, state, tt.region) if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(state, &tt.expected) if diff != "" { diff --git a/stackit/internal/services/mongodbflex/user/resource.go b/stackit/internal/services/mongodbflex/user/resource.go index 4a763e175..d65691cc5 100644 --- a/stackit/internal/services/mongodbflex/user/resource.go +++ b/stackit/internal/services/mongodbflex/user/resource.go @@ -6,25 +6,22 @@ import ( "net/http" "strings" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - - mongodbflexUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/mongodbflex/utils" - - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/stackitcloud/stackit-sdk-go/core/oapierror" "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + mongodbflexUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/mongodbflex/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" ) // Ensure the implementation satisfies the expected interfaces. @@ -69,44 +66,54 @@ func (r *userResource) Metadata(_ context.Context, req resource.MetadataRequest, // Configure adds the provider configured client to the resource. func (r *userResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { var ok bool + r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) if !ok { return } apiClient := mongodbflexUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + r.client = apiClient + tflog.Info(ctx, "MongoDB Flex user client configured") } // ModifyPlan implements resource.ResourceWithModifyPlan. // Use the modifier to set the effective region in the current plan. -func (r *userResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform +func (r *userResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { //nolint:gocritic // function signature required by Terraform var configModel Model // skip initial empty configuration to avoid follow-up errors if req.Config.Raw.IsNull() { return } + resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...) + if resp.Diagnostics.HasError() { return } var planModel Model + resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) + if resp.Diagnostics.HasError() { return } utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp) + if resp.Diagnostics.HasError() { return } resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...) + if resp.Diagnostics.HasError() { return } @@ -215,13 +222,15 @@ func (r *userResource) Schema(_ context.Context, _ resource.SchemaRequest, resp } // Create creates the resource and sets the initial Terraform state. -func (r *userResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *userResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() region := r.providerData.GetRegionWithOverride(model.Region) instanceId := model.InstanceId.ValueString() @@ -230,9 +239,10 @@ func (r *userResource) Create(ctx context.Context, req resource.CreateRequest, r ctx = tflog.SetField(ctx, "instance_id", instanceId) var roles []string - if !(model.Roles.IsNull() || model.Roles.IsUnknown()) { + if !model.Roles.IsNull() && !model.Roles.IsUnknown() { diags = model.Roles.ElementsAs(ctx, &roles, false) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -250,10 +260,12 @@ func (r *userResource) Create(ctx context.Context, req resource.CreateRequest, r core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating user", fmt.Sprintf("Calling API: %v", err)) return } + if userResp == nil || userResp.Item == nil || userResp.Item.Id == nil || *userResp.Item.Id == "" { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating user", "API didn't return user ID. A user might have been created") return } + userId := *userResp.Item.Id ctx = tflog.SetField(ctx, "user_id", userId) @@ -266,20 +278,24 @@ func (r *userResource) Create(ctx context.Context, req resource.CreateRequest, r // Set state to fully populated data diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "MongoDB Flex user created") } // Read refreshes the Terraform state with the latest data. -func (r *userResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (r *userResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() region := r.providerData.GetRegionWithOverride(model.Region) instanceId := model.InstanceId.ValueString() @@ -296,7 +312,9 @@ func (r *userResource) Read(ctx context.Context, req resource.ReadRequest, resp resp.State.RemoveResource(ctx) return } + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading user", fmt.Sprintf("Calling API: %v", err)) + return } @@ -310,21 +328,25 @@ func (r *userResource) Read(ctx context.Context, req resource.ReadRequest, resp // Set refreshed state diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "MongoDB Flex user read") } // Update updates the resource and sets the updated Terraform state on success. -func (r *userResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *userResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { //nolint:gocritic // function signature required by Terraform // Retrieve values from plan var model Model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() region := r.providerData.GetRegionWithOverride(model.Region) instanceId := model.InstanceId.ValueString() @@ -338,14 +360,16 @@ func (r *userResource) Update(ctx context.Context, req resource.UpdateRequest, r var stateModel Model diags = req.State.Get(ctx, &stateModel) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } var roles []string - if !(model.Roles.IsNull() || model.Roles.IsUnknown()) { + if !model.Roles.IsNull() && !model.Roles.IsUnknown() { diags = model.Roles.ElementsAs(ctx, &roles, false) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -381,18 +405,21 @@ func (r *userResource) Update(ctx context.Context, req resource.UpdateRequest, r // Set state to fully populated data diags = resp.State.Set(ctx, stateModel) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "MongoDB Flex user updated") } // Delete deletes the resource and removes the Terraform state on success. -func (r *userResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform +func (r *userResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { //nolint:gocritic // function signature required by Terraform // Retrieve values from plan var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -412,11 +439,12 @@ func (r *userResource) Delete(ctx context.Context, req resource.DeleteRequest, r core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting user", fmt.Sprintf("Calling API: %v", err)) return } + tflog.Info(ctx, "MongoDB Flex user deleted") } // ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: project_id,zone_id,record_set_id +// The expected format of the resource import identifier is: project_id,zone_id,record_set_id. func (r *userResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { idParts := strings.Split(req.ID, core.Separator) if len(idParts) != 4 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" || idParts[3] == "" { @@ -424,6 +452,7 @@ func (r *userResource) ImportState(ctx context.Context, req resource.ImportState "Error importing user", fmt.Sprintf("Expected import identifier with format [project_id],[region],[instance_id],[user_id], got %q", req.ID), ) + return } @@ -442,14 +471,17 @@ func mapFieldsCreate(userResp *mongodbflex.CreateUserResponse, model *Model, reg if userResp == nil || userResp.Item == nil { return fmt.Errorf("response is nil") } + if model == nil { return fmt.Errorf("model input is nil") } + user := userResp.Item if user.Id == nil { return fmt.Errorf("user id not present") } + userId := *user.Id model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), region, model.InstanceId.ValueString(), userId) model.Region = types.StringValue(region) @@ -460,6 +492,7 @@ func mapFieldsCreate(userResp *mongodbflex.CreateUserResponse, model *Model, reg if user.Password == nil { return fmt.Errorf("user password not present") } + model.Password = types.StringValue(*user.Password) if user.Roles == nil { @@ -469,15 +502,19 @@ func mapFieldsCreate(userResp *mongodbflex.CreateUserResponse, model *Model, reg for _, role := range *user.Roles { roles = append(roles, types.StringValue(role)) } + rolesSet, diags := types.SetValue(types.StringType, roles) if diags.HasError() { return fmt.Errorf("mapping roles: %w", core.DiagsToError(diags)) } + model.Roles = rolesSet } + model.Host = types.StringPointerValue(user.Host) model.Port = types.Int64PointerValue(user.Port) model.Uri = types.StringPointerValue(user.Uri) + return nil } @@ -485,9 +522,11 @@ func mapFields(userResp *mongodbflex.GetUserResponse, model *Model, region strin if userResp == nil || userResp.Item == nil { return fmt.Errorf("response is nil") } + if model == nil { return fmt.Errorf("model input is nil") } + user := userResp.Item var userId string @@ -498,6 +537,7 @@ func mapFields(userResp *mongodbflex.GetUserResponse, model *Model, region strin } else { return fmt.Errorf("user id not present") } + model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), region, model.InstanceId.ValueString(), userId) model.Region = types.StringValue(region) model.UserId = types.StringValue(userId) @@ -511,14 +551,18 @@ func mapFields(userResp *mongodbflex.GetUserResponse, model *Model, region strin for _, role := range *user.Roles { roles = append(roles, types.StringValue(role)) } + rolesSet, diags := types.SetValue(types.StringType, roles) if diags.HasError() { return fmt.Errorf("mapping roles: %w", core.DiagsToError(diags)) } + model.Roles = rolesSet } + model.Host = types.StringPointerValue(user.Host) model.Port = types.Int64PointerValue(user.Port) + return nil } @@ -526,6 +570,7 @@ func toCreatePayload(model *Model, roles []string) (*mongodbflex.CreateUserPaylo if model == nil { return nil, fmt.Errorf("nil model") } + if roles == nil { return nil, fmt.Errorf("nil roles") } @@ -541,6 +586,7 @@ func toUpdatePayload(model *Model, roles []string) (*mongodbflex.UpdateUserPaylo if model == nil { return nil, fmt.Errorf("nil model") } + if roles == nil { return nil, fmt.Errorf("nil roles") } diff --git a/stackit/internal/services/mongodbflex/user/resource_test.go b/stackit/internal/services/mongodbflex/user/resource_test.go index 53f31ef98..e52379f54 100644 --- a/stackit/internal/services/mongodbflex/user/resource_test.go +++ b/stackit/internal/services/mongodbflex/user/resource_test.go @@ -4,9 +4,8 @@ import ( "fmt" "testing" - "github.com/google/uuid" - "github.com/google/go-cmp/cmp" + "github.com/google/uuid" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/stackitcloud/stackit-sdk-go/core/utils" @@ -167,13 +166,16 @@ func TestMapFieldsCreate(t *testing.T) { ProjectId: tt.expected.ProjectId, InstanceId: tt.expected.InstanceId, } + err := mapFieldsCreate(tt.input, state, tt.region) if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(state, &tt.expected) if diff != "" { @@ -304,13 +306,16 @@ func TestMapFields(t *testing.T) { InstanceId: tt.expected.InstanceId, UserId: tt.expected.UserId, } + err := mapFields(tt.input, state, tt.region) if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(state, &tt.expected) if diff != "" { @@ -399,9 +404,11 @@ func TestToCreatePayload(t *testing.T) { if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(output, tt.expected) if diff != "" { @@ -487,9 +494,11 @@ func TestToUpdatePayload(t *testing.T) { if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(output, tt.expected) if diff != "" { diff --git a/stackit/internal/services/mongodbflex/utils/util_test.go b/stackit/internal/services/mongodbflex/utils/util_test.go index d268d9afb..4310352f4 100644 --- a/stackit/internal/services/mongodbflex/utils/util_test.go +++ b/stackit/internal/services/mongodbflex/utils/util_test.go @@ -22,6 +22,7 @@ const ( func TestConfigureClient(t *testing.T) { /* mock authentication by setting service account token env variable */ os.Clearenv() + err := os.Setenv(sdkClients.ServiceAccountToken, "mock-val") if err != nil { t.Errorf("error setting env variable: %v", err) @@ -30,6 +31,7 @@ func TestConfigureClient(t *testing.T) { type args struct { providerData *core.ProviderData } + tests := []struct { name string args args @@ -50,6 +52,7 @@ func TestConfigureClient(t *testing.T) { if err != nil { t.Errorf("error configuring client: %v", err) } + return apiClient }(), wantErr: false, @@ -70,6 +73,7 @@ func TestConfigureClient(t *testing.T) { if err != nil { t.Errorf("error configuring client: %v", err) } + return apiClient }(), wantErr: false, @@ -81,6 +85,7 @@ func TestConfigureClient(t *testing.T) { diags := diag.Diagnostics{} actual := ConfigureClient(ctx, tt.args.providerData, &diags) + if diags.HasError() != tt.wantErr { t.Errorf("ConfigureClient() error = %v, want %v", diags.HasError(), tt.wantErr) } diff --git a/stackit/internal/services/objectstorage/bucket/datasource.go b/stackit/internal/services/objectstorage/bucket/datasource.go index c541830e5..c0d28b602 100644 --- a/stackit/internal/services/objectstorage/bucket/datasource.go +++ b/stackit/internal/services/objectstorage/bucket/datasource.go @@ -5,18 +5,16 @@ import ( "fmt" "net/http" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - objectstorageUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/objectstorage/utils" - "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/stackitcloud/stackit-sdk-go/services/objectstorage" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + objectstorageUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/objectstorage/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - - "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/stackitcloud/stackit-sdk-go/services/objectstorage" ) // Ensure the implementation satisfies the expected interfaces. @@ -43,16 +41,20 @@ func (r *bucketDataSource) Metadata(_ context.Context, req datasource.MetadataRe // Configure adds the provider configured client to the data source. func (r *bucketDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { var ok bool + r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) if !ok { return } apiClient := objectstorageUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + r.client = apiClient + tflog.Info(ctx, "ObjectStorage bucket client configured") } @@ -106,13 +108,15 @@ func (r *bucketDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, } // Read refreshes the Terraform state with the latest data. -func (r *bucketDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (r *bucketDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.Config.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() bucketName := model.Name.ValueString() region := r.providerData.GetRegionWithOverride(model.Region) @@ -134,6 +138,7 @@ func (r *bucketDataSource) Read(ctx context.Context, req datasource.ReadRequest, }, ) resp.State.RemoveResource(ctx) + return } @@ -147,8 +152,10 @@ func (r *bucketDataSource) Read(ctx context.Context, req datasource.ReadRequest, // Set refreshed state diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "ObjectStorage bucket read") } diff --git a/stackit/internal/services/objectstorage/bucket/resource.go b/stackit/internal/services/objectstorage/bucket/resource.go index 52e15e7f5..cf54deef0 100644 --- a/stackit/internal/services/objectstorage/bucket/resource.go +++ b/stackit/internal/services/objectstorage/bucket/resource.go @@ -7,24 +7,22 @@ import ( "net/http" "strings" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - objectstorageUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/objectstorage/utils" - - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/stackitcloud/stackit-sdk-go/core/oapierror" "github.com/stackitcloud/stackit-sdk-go/services/objectstorage" "github.com/stackitcloud/stackit-sdk-go/services/objectstorage/wait" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + objectstorageUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/objectstorage/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" ) // Ensure the implementation satisfies the expected interfaces. @@ -57,29 +55,35 @@ type bucketResource struct { // ModifyPlan implements resource.ResourceWithModifyPlan. // Use the modifier to set the effective region in the current plan. -func (r *bucketResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform +func (r *bucketResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { //nolint:gocritic // function signature required by Terraform var configModel Model // skip initial empty configuration to avoid follow-up errors if req.Config.Raw.IsNull() { return } + resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...) + if resp.Diagnostics.HasError() { return } var planModel Model + resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) + if resp.Diagnostics.HasError() { return } utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp) + if resp.Diagnostics.HasError() { return } resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...) + if resp.Diagnostics.HasError() { return } @@ -93,16 +97,20 @@ func (r *bucketResource) Metadata(_ context.Context, req resource.MetadataReques // Configure adds the provider configured client to the resource. func (r *bucketResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { var ok bool + r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) if !ok { return } apiClient := objectstorageUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + r.client = apiClient + tflog.Info(ctx, "ObjectStorage bucket client configured") } @@ -171,13 +179,15 @@ func (r *bucketResource) Schema(_ context.Context, _ resource.SchemaRequest, res } // Create creates the resource and sets the initial Terraform state. -func (r *bucketResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *bucketResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() bucketName := model.Name.ValueString() region := model.Region.ValueString() @@ -212,22 +222,27 @@ func (r *bucketResource) Create(ctx context.Context, req resource.CreateRequest, core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating bucket", fmt.Sprintf("Processing API payload: %v", err)) return } + diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "ObjectStorage bucket created") } // Read refreshes the Terraform state with the latest data. -func (r *bucketResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (r *bucketResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() bucketName := model.Name.ValueString() region := r.providerData.GetRegionWithOverride(model.Region) @@ -243,7 +258,9 @@ func (r *bucketResource) Read(ctx context.Context, req resource.ReadRequest, res resp.State.RemoveResource(ctx) return } + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading bucket", fmt.Sprintf("Calling API: %v", err)) + return } @@ -257,26 +274,30 @@ func (r *bucketResource) Read(ctx context.Context, req resource.ReadRequest, res // Set refreshed state diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "ObjectStorage bucket read") } // Update updates the resource and sets the updated Terraform state on success. -func (r *bucketResource) Update(ctx context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *bucketResource) Update(ctx context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { //nolint:gocritic // function signature required by Terraform // Update shouldn't be called core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating bucket", "Bucket can't be updated") } // Delete deletes the resource and removes the Terraform state on success. -func (r *bucketResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform +func (r *bucketResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() bucketName := model.Name.ValueString() region := model.Region.ValueString() @@ -295,18 +316,21 @@ func (r *bucketResource) Delete(ctx context.Context, req resource.DeleteRequest, return } } + core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting bucket", fmt.Sprintf("Calling API: %v", err)) } + _, err = wait.DeleteBucketWaitHandler(ctx, r.client, projectId, region, bucketName).WaitWithContext(ctx) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting bucket", fmt.Sprintf("Bucket deletion waiting: %v", err)) return } + tflog.Info(ctx, "ObjectStorage bucket deleted") } // ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: project_id,name +// The expected format of the resource import identifier is: project_id,name. func (r *bucketResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { idParts := strings.Split(req.ID, core.Separator) if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" { @@ -314,6 +338,7 @@ func (r *bucketResource) ImportState(ctx context.Context, req resource.ImportSta "Error importing bucket", fmt.Sprintf("Expected import identifier with format [project_id],[region],[name], got %q", req.ID), ) + return } @@ -327,18 +352,22 @@ func mapFields(bucketResp *objectstorage.GetBucketResponse, model *Model, region if bucketResp == nil { return fmt.Errorf("response input is nil") } + if bucketResp.Bucket == nil { return fmt.Errorf("response bucket is nil") } + if model == nil { return fmt.Errorf("model input is nil") } + bucket := bucketResp.Bucket model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), region, model.Name.ValueString()) model.URLPathStyle = types.StringPointerValue(bucket.UrlPathStyle) model.URLVirtualHostedStyle = types.StringPointerValue(bucket.UrlVirtualHostedStyle) model.Region = types.StringValue(region) + return nil } @@ -346,7 +375,7 @@ type objectStorageClient interface { EnableServiceExecute(ctx context.Context, projectId, region string) (*objectstorage.ProjectStatus, error) } -// enableProject enables object storage for the specified project. If the project is already enabled, nothing happens +// enableProject enables object storage for the specified project. If the project is already enabled, nothing happens. func enableProject(ctx context.Context, model *Model, region string, client objectStorageClient) error { projectId := model.ProjectId.ValueString() @@ -355,5 +384,6 @@ func enableProject(ctx context.Context, model *Model, region string, client obje if err != nil { return fmt.Errorf("failed to create object storage project: %w", err) } + return nil } diff --git a/stackit/internal/services/objectstorage/bucket/resource_test.go b/stackit/internal/services/objectstorage/bucket/resource_test.go index 876e2fd57..2f0702d10 100644 --- a/stackit/internal/services/objectstorage/bucket/resource_test.go +++ b/stackit/internal/services/objectstorage/bucket/resource_test.go @@ -29,6 +29,7 @@ func (c *objectStorageClientMocked) EnableServiceExecute(_ context.Context, proj func TestMapFields(t *testing.T) { const testRegion = "eu01" id := fmt.Sprintf("%s,%s,%s", "pid", testRegion, "bname") + tests := []struct { description string input *objectstorage.GetBucketResponse @@ -105,13 +106,16 @@ func TestMapFields(t *testing.T) { ProjectId: tt.expected.ProjectId, Name: tt.expected.Name, } + err := mapFields(tt.input, model, "eu01") if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(model, &tt.expected) if diff != "" { @@ -144,10 +148,12 @@ func TestEnableProject(t *testing.T) { client := &objectStorageClientMocked{ returnError: tt.enableFails, } + err := enableProject(context.Background(), &Model{}, "eu01", client) if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } diff --git a/stackit/internal/services/objectstorage/credential/datasource.go b/stackit/internal/services/objectstorage/credential/datasource.go index 0a0df7887..791b1f2c3 100644 --- a/stackit/internal/services/objectstorage/credential/datasource.go +++ b/stackit/internal/services/objectstorage/credential/datasource.go @@ -6,17 +6,15 @@ import ( "net/http" "time" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - objectstorageUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/objectstorage/utils" - "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/stackitcloud/stackit-sdk-go/services/objectstorage" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + objectstorageUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/objectstorage/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - - "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/stackitcloud/stackit-sdk-go/services/objectstorage" ) // Ensure the implementation satisfies the expected interfaces. @@ -53,16 +51,20 @@ func (r *credentialDataSource) Metadata(_ context.Context, req datasource.Metada // Configure adds the provider configured client to the datasource. func (r *credentialDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { var ok bool + r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) if !ok { return } apiClient := objectstorageUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + r.client = apiClient + tflog.Info(ctx, "ObjectStorage credential client configured") } @@ -112,10 +114,11 @@ func (r *credentialDataSource) Schema(_ context.Context, _ datasource.SchemaRequ } // Read refreshes the Terraform state with the latest data. -func (r *credentialDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (r *credentialDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model DataSourceModel diags := req.Config.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -143,8 +146,10 @@ func (r *credentialDataSource) Read(ctx context.Context, req datasource.ReadRequ }, ) resp.State.RemoveResource(ctx) + return } + if credentialsGroupResp == nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Reading credentials", fmt.Sprintf("Response is nil: %v", err)) return @@ -165,9 +170,11 @@ func (r *credentialDataSource) Read(ctx context.Context, req datasource.ReadRequ // Set refreshed state diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "ObjectStorage credential read") } @@ -175,6 +182,7 @@ func mapDataSourceFields(credentialResp *objectstorage.AccessKey, model *DataSou if credentialResp == nil { return fmt.Errorf("response input is nil") } + if model == nil { return fmt.Errorf("model input is nil") } @@ -197,6 +205,7 @@ func mapDataSourceFields(credentialResp *objectstorage.AccessKey, model *DataSou if err != nil { return fmt.Errorf("unable to parse payload expiration timestamp '%v': %w", *credentialResp.Expires, err) } + model.ExpirationTimestamp = types.StringValue(expirationTimestamp.Format(time.RFC3339)) } @@ -206,16 +215,19 @@ func mapDataSourceFields(credentialResp *objectstorage.AccessKey, model *DataSou model.CredentialId = types.StringValue(credentialId) model.Name = types.StringPointerValue(credentialResp.DisplayName) model.Region = types.StringValue(region) + return nil } -// Returns the access key if found otherwise nil +// Returns the access key if found otherwise nil. func findCredential(credentialsGroupResp objectstorage.ListAccessKeysResponse, credentialId string) *objectstorage.AccessKey { for _, credential := range *credentialsGroupResp.AccessKeys { if credential.KeyId == nil || *credential.KeyId != credentialId { continue } + return &credential } + return nil } diff --git a/stackit/internal/services/objectstorage/credential/datasource_test.go b/stackit/internal/services/objectstorage/credential/datasource_test.go index e6ba0539a..a651ce75f 100644 --- a/stackit/internal/services/objectstorage/credential/datasource_test.go +++ b/stackit/internal/services/objectstorage/credential/datasource_test.go @@ -16,6 +16,7 @@ func TestMapDatasourceFields(t *testing.T) { const testRegion = "eu01" id := fmt.Sprintf("%s,%s,%s", "pid", testRegion, "cgid,cid") + tests := []struct { description string input *objectstorage.AccessKey @@ -107,13 +108,16 @@ func TestMapDatasourceFields(t *testing.T) { CredentialsGroupId: tt.expected.CredentialsGroupId, CredentialId: tt.expected.CredentialId, } + err := mapDataSourceFields(tt.input, model, "eu01") if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(model, &tt.expected) if diff != "" { diff --git a/stackit/internal/services/objectstorage/credential/resource.go b/stackit/internal/services/objectstorage/credential/resource.go index e2f4cea0b..00300d01a 100644 --- a/stackit/internal/services/objectstorage/credential/resource.go +++ b/stackit/internal/services/objectstorage/credential/resource.go @@ -7,23 +7,21 @@ import ( "strings" "time" - objectstorageUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/objectstorage/utils" - - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/stackitcloud/stackit-sdk-go/core/oapierror" "github.com/stackitcloud/stackit-sdk-go/services/objectstorage" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + objectstorageUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/objectstorage/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" ) // Ensure the implementation satisfies the expected interfaces. @@ -58,12 +56,15 @@ type credentialResource struct { } // ModifyPlan implements resource.ResourceWithModifyPlan. -func (r *credentialResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform +func (r *credentialResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { //nolint:gocritic // function signature required by Terraform r.modifyPlanRegion(ctx, &req, resp) + if resp.Diagnostics.HasError() { return } + r.modifyPlanExpiration(ctx, &req, resp) + if resp.Diagnostics.HasError() { return } @@ -77,23 +78,29 @@ func (r *credentialResource) modifyPlanRegion(ctx context.Context, req *resource if req.Config.Raw.IsNull() { return } + resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...) + if resp.Diagnostics.HasError() { return } var planModel Model + resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) + if resp.Diagnostics.HasError() { return } utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp) + if resp.Diagnostics.HasError() { return } resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...) + if resp.Diagnostics.HasError() { return } @@ -102,16 +109,20 @@ func (r *credentialResource) modifyPlanRegion(ctx context.Context, req *resource // ModifyPlan implements resource.ResourceWithModifyPlan. func (r *credentialResource) modifyPlanExpiration(ctx context.Context, req *resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { p := path.Root("expiration_timestamp") + var ( stateDate time.Time planDate time.Time ) resp.Diagnostics.Append(utils.GetTimeFromStringAttribute(ctx, p, req.State, time.RFC3339, &stateDate)...) + if resp.Diagnostics.HasError() { return } + resp.Diagnostics.Append(utils.GetTimeFromStringAttribute(ctx, p, resp.Plan, time.RFC3339, &planDate)...) + if resp.Diagnostics.HasError() { return } @@ -121,6 +132,7 @@ func (r *credentialResource) modifyPlanExpiration(ctx context.Context, req *reso // this will prevent no-op updates if stateDate.Equal(planDate) && !stateDate.IsZero() { resp.Diagnostics.Append(resp.Plan.SetAttribute(ctx, p, types.StringValue(stateDate.Format(time.RFC3339)))...) + if resp.Diagnostics.HasError() { return } @@ -135,16 +147,20 @@ func (r *credentialResource) Metadata(_ context.Context, req resource.MetadataRe // Configure adds the provider configured client to the resource. func (r *credentialResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { var ok bool + r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) if !ok { return } apiClient := objectstorageUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + r.client = apiClient + tflog.Info(ctx, "ObjectStorage credential client configured") } @@ -241,13 +257,15 @@ func (r *credentialResource) Schema(_ context.Context, _ resource.SchemaRequest, } // Create creates the resource and sets the initial Terraform state. -func (r *credentialResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *credentialResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() credentialsGroupId := model.CredentialsGroupId.ValueString() region := model.Region.ValueString() @@ -275,10 +293,12 @@ func (r *credentialResource) Create(ctx context.Context, req resource.CreateRequ core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credential", fmt.Sprintf("Calling API: %v", err)) return } + if credentialResp.KeyId == nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credential", "Got empty credential id") return } + credentialId := *credentialResp.KeyId ctx = tflog.SetField(ctx, "credential_id", credentialId) @@ -294,11 +314,15 @@ func (r *credentialResource) Create(ctx context.Context, req resource.CreateRequ actualDate time.Time planDate time.Time ) + resp.Diagnostics.Append(utils.ToTime(ctx, time.RFC3339, model.ExpirationTimestamp, &actualDate)...) + if resp.Diagnostics.HasError() { return } + resp.Diagnostics.Append(utils.GetTimeFromStringAttribute(ctx, path.Root("expiration_timestamp"), req.Plan, time.RFC3339, &planDate)...) + if resp.Diagnostics.HasError() { return } @@ -311,17 +335,20 @@ func (r *credentialResource) Create(ctx context.Context, req resource.CreateRequ diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "ObjectStorage credential created") } // Read refreshes the Terraform state with the latest data. -func (r *credentialResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (r *credentialResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -341,10 +368,12 @@ func (r *credentialResource) Read(ctx context.Context, req resource.ReadRequest, core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading credential", fmt.Sprintf("Finding credential: %v", err)) return } + if !found { resp.State.RemoveResource(ctx) return } + var ( currentApiDate time.Time stateDate time.Time @@ -352,11 +381,13 @@ func (r *credentialResource) Read(ctx context.Context, req resource.ReadRequest, if !utils.IsUndefined(model.ExpirationTimestamp) { resp.Diagnostics.Append(utils.ToTime(ctx, time.RFC3339, model.ExpirationTimestamp, ¤tApiDate)...) + if resp.Diagnostics.HasError() { return } resp.Diagnostics.Append(utils.GetTimeFromStringAttribute(ctx, path.Root("expiration_timestamp"), req.State, time.RFC3339, &stateDate)...) + if resp.Diagnostics.HasError() { return } @@ -371,14 +402,16 @@ func (r *credentialResource) Read(ctx context.Context, req resource.ReadRequest, // Set refreshed state diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "ObjectStorage credential read") } // Update updates the resource and sets the updated Terraform state on success. -func (r *credentialResource) Update(_ context.Context, _ resource.UpdateRequest, _ *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *credentialResource) Update(_ context.Context, _ resource.UpdateRequest, _ *resource.UpdateResponse) { //nolint:gocritic // function signature required by Terraform /* While a credential cannot be updated, the Update call must not be prevented with an error: When the expiration timestamp has been updated to the same point in time, but e.g. with a different timezone, @@ -391,10 +424,11 @@ func (r *credentialResource) Update(_ context.Context, _ resource.UpdateRequest, } // Delete deletes the resource and removes the Terraform state on success. -func (r *credentialResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform +func (r *credentialResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -419,7 +453,7 @@ func (r *credentialResource) Delete(ctx context.Context, req resource.DeleteRequ } // ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: project_id,credentials_group_id,credential_id +// The expected format of the resource import identifier is: project_id,credentials_group_id,credential_id. func (r *credentialResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { idParts := strings.Split(req.ID, core.Separator) if len(idParts) != 4 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" || idParts[3] == "" { @@ -427,6 +461,7 @@ func (r *credentialResource) ImportState(ctx context.Context, req resource.Impor "Error importing credential", fmt.Sprintf("Expected import identifier with format [project_id],[region],[credentials_group_id],[credential_id], got %q", req.ID), ) + return } @@ -441,7 +476,7 @@ type objectStorageClient interface { EnableServiceExecute(ctx context.Context, projectId, region string) (*objectstorage.ProjectStatus, error) } -// enableProject enables object storage for the specified project. If the project is already enabled, nothing happens +// enableProject enables object storage for the specified project. If the project is already enabled, nothing happens. func enableProject(ctx context.Context, model *Model, region string, client objectStorageClient) error { projectId := model.ProjectId.ValueString() @@ -450,6 +485,7 @@ func enableProject(ctx context.Context, model *Model, region string, client obje if err != nil { return fmt.Errorf("failed to create object storage project: %w", err) } + return nil } @@ -466,10 +502,12 @@ func toCreatePayload(model *Model) (*objectstorage.CreateAccessKeyPayload, error if expirationTimestampValue == nil { return &objectstorage.CreateAccessKeyPayload{}, nil } + expirationTimestamp, err := time.Parse(time.RFC3339, *expirationTimestampValue) if err != nil { return nil, fmt.Errorf("unable to parse expiration timestamp '%v': %w", *expirationTimestampValue, err) } + return &objectstorage.CreateAccessKeyPayload{ Expires: &expirationTimestamp, }, nil @@ -479,6 +517,7 @@ func mapFields(credentialResp *objectstorage.CreateAccessKeyResponse, model *Mod if credentialResp == nil { return fmt.Errorf("response input is nil") } + if model == nil { return fmt.Errorf("model input is nil") } @@ -501,6 +540,7 @@ func mapFields(credentialResp *objectstorage.CreateAccessKeyResponse, model *Mod if err != nil { return fmt.Errorf("unable to parse payload expiration timestamp '%v': %w", *credentialResp.Expires, err) } + model.ExpirationTimestamp = types.StringValue(expirationTimestamp.Format(time.RFC3339)) } @@ -512,6 +552,7 @@ func mapFields(credentialResp *objectstorage.CreateAccessKeyResponse, model *Mod model.AccessKey = types.StringPointerValue(credentialResp.AccessKey) model.SecretAccessKey = types.StringPointerValue(credentialResp.SecretAccessKey) model.Region = types.StringValue(region) + return nil } @@ -529,13 +570,16 @@ func readCredentials(ctx context.Context, model *Model, region string, client *o if ok && oapiErr.StatusCode == http.StatusNotFound { return false, nil } + return false, fmt.Errorf("getting credentials groups: %w", err) } + if credentialsGroupResp == nil { return false, fmt.Errorf("getting credentials groups: nil response") } foundCredential := false + for _, credential := range *credentialsGroupResp.AccessKeys { if credential.KeyId == nil || *credential.KeyId != credentialId { continue @@ -555,10 +599,13 @@ func readCredentials(ctx context.Context, model *Model, region string, client *o if err != nil { return foundCredential, fmt.Errorf("unable to parse payload expiration timestamp '%v': %w", *credential.Expires, err) } + model.ExpirationTimestamp = types.StringValue(expirationTimestamp.Format(time.RFC3339)) } + break } + model.Region = types.StringValue(region) return foundCredential, nil diff --git a/stackit/internal/services/objectstorage/credential/resource_test.go b/stackit/internal/services/objectstorage/credential/resource_test.go index 24746aa2f..73a8d49c9 100644 --- a/stackit/internal/services/objectstorage/credential/resource_test.go +++ b/stackit/internal/services/objectstorage/credential/resource_test.go @@ -32,8 +32,10 @@ func (c *objectStorageClientMocked) EnableServiceExecute(_ context.Context, proj func TestMapFields(t *testing.T) { now := time.Now() + const testRegion = "eu01" id := fmt.Sprintf("%s,%s,%s", "pid", testRegion, "cgid,cid") + tests := []struct { description string input *objectstorage.CreateAccessKeyResponse @@ -136,13 +138,16 @@ func TestMapFields(t *testing.T) { CredentialsGroupId: tt.expected.CredentialsGroupId, CredentialId: tt.expected.CredentialId, } + err := mapFields(tt.input, model, "eu01") if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(model, &tt.expected) if diff != "" { @@ -156,6 +161,7 @@ func TestMapFields(t *testing.T) { func TestEnableProject(t *testing.T) { const testRegion = "eu01" id := fmt.Sprintf("%s,%s,%s", "pid", testRegion, "cgid,cid") + tests := []struct { description string expected Model @@ -203,10 +209,12 @@ func TestEnableProject(t *testing.T) { CredentialsGroupId: tt.expected.CredentialsGroupId, CredentialId: tt.expected.CredentialId, } + err := enableProject(context.Background(), model, "eu01", client) if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } @@ -216,6 +224,7 @@ func TestEnableProject(t *testing.T) { func TestReadCredentials(t *testing.T) { now := time.Now() + const testRegion = "eu01" id := fmt.Sprintf("%s,%s,%s", "pid", testRegion, "cgid,cid") tests := []struct { @@ -398,13 +407,16 @@ func TestReadCredentials(t *testing.T) { handler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "application/json") + if tt.getCredentialsFails { w.WriteHeader(http.StatusBadGateway) w.Header().Set("Content-Type", "application/json") + _, err := w.Write([]byte("{\"message\": \"Something bad happened\"")) if err != nil { t.Errorf("Failed to write bad response: %v", err) } + return } @@ -413,8 +425,10 @@ func TestReadCredentials(t *testing.T) { t.Errorf("Failed to write response: %v", err) } }) + mockedServer := httptest.NewServer(handler) defer mockedServer.Close() + client, err := objectstorage.NewAPIClient( config.WithEndpoint(mockedServer.URL), config.WithoutAuthentication(), @@ -428,13 +442,16 @@ func TestReadCredentials(t *testing.T) { CredentialsGroupId: tt.expectedModel.CredentialsGroupId, CredentialId: tt.expectedModel.CredentialId, } + found, err := readCredentials(context.Background(), model, "eu01", client) if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(model, &tt.expectedModel) if diff != "" { diff --git a/stackit/internal/services/objectstorage/credentialsgroup/datasource.go b/stackit/internal/services/objectstorage/credentialsgroup/datasource.go index 72c57d2af..1ef6199fa 100644 --- a/stackit/internal/services/objectstorage/credentialsgroup/datasource.go +++ b/stackit/internal/services/objectstorage/credentialsgroup/datasource.go @@ -4,18 +4,16 @@ import ( "context" "fmt" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - objectstorageUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/objectstorage/utils" - "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/stackitcloud/stackit-sdk-go/services/objectstorage" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + objectstorageUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/objectstorage/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - - "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/stackitcloud/stackit-sdk-go/services/objectstorage" ) // Ensure the implementation satisfies the expected interfaces. @@ -42,16 +40,20 @@ func (r *credentialsGroupDataSource) Metadata(_ context.Context, req datasource. // Configure adds the provider configured client to the data source. func (r *credentialsGroupDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { var ok bool + r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) if !ok { return } apiClient := objectstorageUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + r.client = apiClient + tflog.Info(ctx, "ObjectStorage credentials group client configured") } @@ -104,13 +106,15 @@ func (r *credentialsGroupDataSource) Schema(_ context.Context, _ datasource.Sche } // Read refreshes the Terraform state with the latest data. -func (r *credentialsGroupDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (r *credentialsGroupDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.Config.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() credentialsGroupId := model.CredentialsGroupId.ValueString() region := r.providerData.GetRegionWithOverride(model.Region) @@ -124,9 +128,11 @@ func (r *credentialsGroupDataSource) Read(ctx context.Context, req datasource.Re core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading credentials group", fmt.Sprintf("getting credential group from list of credentials groups: %v", err)) return } + if !found { resp.State.RemoveResource(ctx) core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading credentials group", fmt.Sprintf("Credentials group with ID %q does not exists in project %q", credentialsGroupId, projectId)) + return } @@ -137,8 +143,10 @@ func (r *credentialsGroupDataSource) Read(ctx context.Context, req datasource.Re // Set refreshed state diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "ObjectStorage credentials group read") } diff --git a/stackit/internal/services/objectstorage/credentialsgroup/resource.go b/stackit/internal/services/objectstorage/credentialsgroup/resource.go index 7008d77b2..248286c2b 100644 --- a/stackit/internal/services/objectstorage/credentialsgroup/resource.go +++ b/stackit/internal/services/objectstorage/credentialsgroup/resource.go @@ -6,24 +6,22 @@ import ( "net/http" "strings" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - objectstorageUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/objectstorage/utils" - - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/stackitcloud/stackit-sdk-go/core/oapierror" sdkUtils "github.com/stackitcloud/stackit-sdk-go/core/utils" "github.com/stackitcloud/stackit-sdk-go/services/objectstorage" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + objectstorageUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/objectstorage/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" ) // Ensure the implementation satisfies the expected interfaces. @@ -56,29 +54,35 @@ type credentialsGroupResource struct { // ModifyPlan implements resource.ResourceWithModifyPlan. // Use the modifier to set the effective region in the current plan. -func (r *credentialsGroupResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform +func (r *credentialsGroupResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { //nolint:gocritic // function signature required by Terraform var configModel Model // skip initial empty configuration to avoid follow-up errors if req.Config.Raw.IsNull() { return } + resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...) + if resp.Diagnostics.HasError() { return } var planModel Model + resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) + if resp.Diagnostics.HasError() { return } utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp) + if resp.Diagnostics.HasError() { return } resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...) + if resp.Diagnostics.HasError() { return } @@ -92,16 +96,20 @@ func (r *credentialsGroupResource) Metadata(_ context.Context, req resource.Meta // Configure adds the provider configured client to the resource. func (r *credentialsGroupResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { var ok bool + r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) if !ok { return } apiClient := objectstorageUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + r.client = apiClient + tflog.Info(ctx, "ObjectStorage credentials group client configured") } @@ -169,13 +177,15 @@ func (r *credentialsGroupResource) Schema(_ context.Context, _ resource.SchemaRe } // Create creates the resource and sets the initial Terraform state. -func (r *credentialsGroupResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *credentialsGroupResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() credentialsGroupName := model.Name.ValueString() region := model.Region.ValueString() @@ -208,22 +218,27 @@ func (r *credentialsGroupResource) Create(ctx context.Context, req resource.Crea core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credentialsGroup", fmt.Sprintf("Processing API payload: %v", err)) return } + diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "ObjectStorage credentials group created") } // Read refreshes the Terraform state with the latest data. -func (r *credentialsGroupResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (r *credentialsGroupResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() credentialsGroupId := model.CredentialsGroupId.ValueString() region := r.providerData.GetRegionWithOverride(model.Region) @@ -237,6 +252,7 @@ func (r *credentialsGroupResource) Read(ctx context.Context, req resource.ReadRe core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading credentialsGroup", fmt.Sprintf("getting credential group from list of credentials groups: %v", err)) return } + if !found { resp.State.RemoveResource(ctx) return @@ -247,26 +263,30 @@ func (r *credentialsGroupResource) Read(ctx context.Context, req resource.ReadRe // Set refreshed state diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "ObjectStorage credentials group read") } // Update updates the resource and sets the updated Terraform state on success. -func (r *credentialsGroupResource) Update(ctx context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *credentialsGroupResource) Update(ctx context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { //nolint:gocritic // function signature required by Terraform // Update shouldn't be called core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating credentials group", "CredentialsGroup can't be updated") } // Delete deletes the resource and removes the Terraform state on success. -func (r *credentialsGroupResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform +func (r *credentialsGroupResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() credentialsGroupId := model.CredentialsGroupId.ValueString() region := model.Region.ValueString() @@ -285,7 +305,7 @@ func (r *credentialsGroupResource) Delete(ctx context.Context, req resource.Dele } // ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: project_id, credentials_group_id +// The expected format of the resource import identifier is: project_id, credentials_group_id. func (r *credentialsGroupResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { idParts := strings.Split(req.ID, core.Separator) if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" { @@ -293,6 +313,7 @@ func (r *credentialsGroupResource) ImportState(ctx context.Context, req resource "Error importing credentialsGroup", fmt.Sprintf("Expected import identifier with format [project_id],[region],[credentials_group_id], got %q", req.ID), ) + return } @@ -306,19 +327,24 @@ func mapFields(credentialsGroupResp *objectstorage.CreateCredentialsGroupRespons if credentialsGroupResp == nil { return fmt.Errorf("response input is nil") } + if credentialsGroupResp.CredentialsGroup == nil { return fmt.Errorf("response credentialsGroup is nil") } + if model == nil { return fmt.Errorf("model input is nil") } + credentialsGroup := credentialsGroupResp.CredentialsGroup err := mapCredentialsGroup(*credentialsGroup, model, region) if err != nil { return err } + model.Region = types.StringValue(region) + return nil } @@ -336,6 +362,7 @@ func mapCredentialsGroup(credentialsGroup objectstorage.CredentialsGroup, model model.CredentialsGroupId = types.StringValue(credentialsGroupId) model.URN = types.StringPointerValue(credentialsGroup.Urn) model.Name = types.StringPointerValue(credentialsGroup.DisplayName) + return nil } @@ -344,7 +371,7 @@ type objectStorageClient interface { ListCredentialsGroupsExecute(ctx context.Context, projectId, region string) (*objectstorage.ListCredentialsGroupsResponse, error) } -// enableProject enables object storage for the specified project. If the project is already enabled, nothing happens +// enableProject enables object storage for the specified project. If the project is already enabled, nothing happens. func enableProject(ctx context.Context, model *Model, region string, client objectStorageClient) error { projectId := model.ProjectId.ValueString() @@ -353,6 +380,7 @@ func enableProject(ctx context.Context, model *Model, region string, client obje if err != nil { return fmt.Errorf("failed to create object storage project: %w", err) } + return nil } @@ -372,6 +400,7 @@ func readCredentialsGroups(ctx context.Context, model *Model, region string, cli if ok && oapiErr.StatusCode == http.StatusNotFound { return found, nil } + return found, fmt.Errorf("getting credentials groups: %w", err) } @@ -383,11 +412,13 @@ func readCredentialsGroups(ctx context.Context, model *Model, region string, cli if *credentialsGroup.CredentialsGroupId != model.CredentialsGroupId.ValueString() && *credentialsGroup.DisplayName != model.Name.ValueString() { continue } + found = true err = mapCredentialsGroup(credentialsGroup, model, region) if err != nil { return found, err } + break } diff --git a/stackit/internal/services/objectstorage/credentialsgroup/resource_test.go b/stackit/internal/services/objectstorage/credentialsgroup/resource_test.go index 37b0dae84..0eb2cdf8c 100644 --- a/stackit/internal/services/objectstorage/credentialsgroup/resource_test.go +++ b/stackit/internal/services/objectstorage/credentialsgroup/resource_test.go @@ -37,6 +37,7 @@ func (c *objectStorageClientMocked) ListCredentialsGroupsExecute(_ context.Conte func TestMapFields(t *testing.T) { const testRegion = "eu01" id := fmt.Sprintf("%s,%s,%s", "pid", testRegion, "cid") + tests := []struct { description string input *objectstorage.CreateCredentialsGroupResponse @@ -113,13 +114,16 @@ func TestMapFields(t *testing.T) { ProjectId: tt.expected.ProjectId, CredentialsGroupId: tt.expected.CredentialsGroupId, } + err := mapFields(tt.input, model, "eu01") if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(model, &tt.expected) if diff != "" { @@ -152,10 +156,12 @@ func TestEnableProject(t *testing.T) { client := &objectStorageClientMocked{ returnError: tt.enableFails, } + err := enableProject(context.Background(), &Model{}, "eu01", client) if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } @@ -166,6 +172,7 @@ func TestEnableProject(t *testing.T) { func TestReadCredentialsGroups(t *testing.T) { const testRegion = "eu01" id := fmt.Sprintf("%s,%s,%s", "pid", testRegion, "cid") + tests := []struct { description string mockedResp *objectstorage.ListCredentialsGroupsResponse @@ -295,13 +302,16 @@ func TestReadCredentialsGroups(t *testing.T) { ProjectId: tt.expectedModel.ProjectId, CredentialsGroupId: tt.expectedModel.CredentialsGroupId, } + found, err := readCredentialsGroups(context.Background(), model, "eu01", client) if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(model, &tt.expectedModel) if diff != "" { diff --git a/stackit/internal/services/objectstorage/objectstorage_acc_test.go b/stackit/internal/services/objectstorage/objectstorage_acc_test.go index 4fd117255..cad91398b 100644 --- a/stackit/internal/services/objectstorage/objectstorage_acc_test.go +++ b/stackit/internal/services/objectstorage/objectstorage_acc_test.go @@ -12,7 +12,6 @@ import ( "github.com/hashicorp/terraform-plugin-testing/helper/acctest" "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/terraform" - stackitSdkConfig "github.com/stackitcloud/stackit-sdk-go/core/config" "github.com/stackitcloud/stackit-sdk-go/core/utils" "github.com/stackitcloud/stackit-sdk-go/services/objectstorage" @@ -223,6 +222,7 @@ func TestAccObjectStorageResourceMin(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute credential_id") } + return fmt.Sprintf("%s,%s,%s,%s", testutil.ProjectId, testutil.Region, credentialsGroupId, credentialId), nil }, ImportState: true, @@ -236,7 +236,9 @@ func TestAccObjectStorageResourceMin(t *testing.T) { func testAccCheckObjectStorageDestroy(s *terraform.State) error { ctx := context.Background() + var client *objectstorage.APIClient + var err error if testutil.ObjectStorageCustomEndpoint == "" { client, err = objectstorage.NewAPIClient( @@ -247,11 +249,13 @@ func testAccCheckObjectStorageDestroy(s *terraform.State) error { stackitSdkConfig.WithEndpoint(testutil.ObjectStorageCustomEndpoint), ) } + if err != nil { return fmt.Errorf("creating client: %w", err) } bucketsToDestroy := []string{} + for _, rs := range s.RootModule().Resources { if rs.Type != "stackit_objectstorage_bucket" { continue @@ -271,12 +275,14 @@ func testAccCheckObjectStorageDestroy(s *terraform.State) error { if bucket.Name == nil { continue } + bucketName := *bucket.Name if utils.Contains(bucketsToDestroy, bucketName) { _, err := client.DeleteBucketExecute(ctx, testutil.ProjectId, testutil.Region, bucketName) if err != nil { return fmt.Errorf("destroying bucket %s during CheckDestroy: %w", bucketName, err) } + _, err = wait.DeleteBucketWaitHandler(ctx, client, testutil.ProjectId, testutil.Region, bucketName).WaitWithContext(ctx) if err != nil { return fmt.Errorf("destroying instance %s during CheckDestroy: waiting for deletion %w", bucketName, err) @@ -285,6 +291,7 @@ func testAccCheckObjectStorageDestroy(s *terraform.State) error { } credentialsGroupsToDestroy := []string{} + for _, rs := range s.RootModule().Resources { if rs.Type != "stackit_objectstorage_credentials_group" { continue @@ -304,6 +311,7 @@ func testAccCheckObjectStorageDestroy(s *terraform.State) error { if group.CredentialsGroupId == nil { continue } + groupId := *group.CredentialsGroupId if utils.Contains(credentialsGroupsToDestroy, groupId) { _, err := client.DeleteCredentialsGroupExecute(ctx, testutil.ProjectId, testutil.Region, groupId) @@ -312,5 +320,6 @@ func testAccCheckObjectStorageDestroy(s *terraform.State) error { } } } + return nil } diff --git a/stackit/internal/services/objectstorage/utils/util.go b/stackit/internal/services/objectstorage/utils/util.go index 56a013a20..1b82a6844 100644 --- a/stackit/internal/services/objectstorage/utils/util.go +++ b/stackit/internal/services/objectstorage/utils/util.go @@ -4,10 +4,9 @@ import ( "context" "fmt" - "github.com/stackitcloud/stackit-sdk-go/services/objectstorage" - "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/stackitcloud/stackit-sdk-go/core/config" + "github.com/stackitcloud/stackit-sdk-go/services/objectstorage" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" ) @@ -20,6 +19,7 @@ func ConfigureClient(ctx context.Context, providerData *core.ProviderData, diags if providerData.ObjectStorageCustomEndpoint != "" { apiClientConfigOptions = append(apiClientConfigOptions, config.WithEndpoint(providerData.ObjectStorageCustomEndpoint)) } + apiClient, err := objectstorage.NewAPIClient(apiClientConfigOptions...) if err != nil { core.LogAndAddError(ctx, diags, "Error configuring API client", fmt.Sprintf("Configuring client: %v. This is an error related to the provider configuration, not to the resource configuration", err)) diff --git a/stackit/internal/services/objectstorage/utils/util_test.go b/stackit/internal/services/objectstorage/utils/util_test.go index d31dc8a6f..0ae998d47 100644 --- a/stackit/internal/services/objectstorage/utils/util_test.go +++ b/stackit/internal/services/objectstorage/utils/util_test.go @@ -22,6 +22,7 @@ const ( func TestConfigureClient(t *testing.T) { /* mock authentication by setting service account token env variable */ os.Clearenv() + err := os.Setenv(sdkClients.ServiceAccountToken, "mock-val") if err != nil { t.Errorf("error setting env variable: %v", err) @@ -30,6 +31,7 @@ func TestConfigureClient(t *testing.T) { type args struct { providerData *core.ProviderData } + tests := []struct { name string args args @@ -50,6 +52,7 @@ func TestConfigureClient(t *testing.T) { if err != nil { t.Errorf("error configuring client: %v", err) } + return apiClient }(), wantErr: false, @@ -70,6 +73,7 @@ func TestConfigureClient(t *testing.T) { if err != nil { t.Errorf("error configuring client: %v", err) } + return apiClient }(), wantErr: false, @@ -81,6 +85,7 @@ func TestConfigureClient(t *testing.T) { diags := diag.Diagnostics{} actual := ConfigureClient(ctx, tt.args.providerData, &diags) + if diags.HasError() != tt.wantErr { t.Errorf("ConfigureClient() error = %v, want %v", diags.HasError(), tt.wantErr) } diff --git a/stackit/internal/services/observability/alertgroup/datasource.go b/stackit/internal/services/observability/alertgroup/datasource.go index df7467372..b4542a579 100644 --- a/stackit/internal/services/observability/alertgroup/datasource.go +++ b/stackit/internal/services/observability/alertgroup/datasource.go @@ -6,9 +6,6 @@ import ( "fmt" "net/http" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - observabilityUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/observability/utils" - "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" @@ -17,7 +14,9 @@ import ( "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/stackitcloud/stackit-sdk-go/core/oapierror" "github.com/stackitcloud/stackit-sdk-go/services/observability" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + observabilityUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/observability/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" ) @@ -44,10 +43,13 @@ func (a *alertGroupDataSource) Configure(ctx context.Context, req datasource.Con } apiClient := observabilityUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + a.client = apiClient + tflog.Info(ctx, "Observability alert group client configured") } @@ -130,10 +132,11 @@ func (a *alertGroupDataSource) Schema(_ context.Context, _ datasource.SchemaRequ } } -func (a *alertGroupDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (a *alertGroupDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.Config.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -148,12 +151,15 @@ func (a *alertGroupDataSource) Read(ctx context.Context, req datasource.ReadRequ readAlertGroupResp, err := a.client.GetAlertgroup(ctx, alertGroupName, instanceId, projectId).Execute() if err != nil { var oapiErr *oapierror.GenericOpenAPIError + ok := errors.As(err, &oapiErr) if ok && oapiErr.StatusCode == http.StatusNotFound { resp.State.RemoveResource(ctx) return } + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading alert group", fmt.Sprintf("Calling API: %v", err)) + return } diff --git a/stackit/internal/services/observability/alertgroup/resource.go b/stackit/internal/services/observability/alertgroup/resource.go index 81295320a..201f92156 100644 --- a/stackit/internal/services/observability/alertgroup/resource.go +++ b/stackit/internal/services/observability/alertgroup/resource.go @@ -8,8 +8,6 @@ import ( "regexp" "strings" - observabilityUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/observability/utils" - "github.com/hashicorp/terraform-plugin-framework-validators/mapvalidator" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/attr" @@ -27,6 +25,7 @@ import ( "github.com/stackitcloud/stackit-sdk-go/services/observability" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + observabilityUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/observability/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" ) @@ -100,10 +99,13 @@ func (a *alertGroupResource) Configure(ctx context.Context, req resource.Configu } apiClient := observabilityUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + a.client = apiClient + tflog.Info(ctx, "Observability alert group client configured") } @@ -230,11 +232,12 @@ func (a *alertGroupResource) Schema(_ context.Context, _ resource.SchemaRequest, } // Create creates the resource and sets the initial Terraform state. -func (a *alertGroupResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform +func (a *alertGroupResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { //nolint:gocritic // function signature required by Terraform // Retrieve values from plan var model Model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -274,17 +277,20 @@ func (a *alertGroupResource) Create(ctx context.Context, req resource.CreateRequ // Set the state with fully populated data. diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "alert group created") } // Read refreshes the Terraform state with the latest data. -func (a *alertGroupResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (a *alertGroupResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -299,12 +305,15 @@ func (a *alertGroupResource) Read(ctx context.Context, req resource.ReadRequest, readAlertGroupResp, err := a.client.GetAlertgroup(ctx, alertGroupName, instanceId, projectId).Execute() if err != nil { var oapiErr *oapierror.GenericOpenAPIError + ok := errors.As(err, &oapiErr) if ok && oapiErr.StatusCode == http.StatusNotFound { resp.State.RemoveResource(ctx) return } + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading alert group", fmt.Sprintf("Calling API: %v", err)) + return } @@ -323,16 +332,17 @@ func (a *alertGroupResource) Read(ctx context.Context, req resource.ReadRequest, // The Update function is redundant since any modifications will // automatically trigger a resource recreation through Terraform's built-in // lifecycle management. -func (a *alertGroupResource) Update(ctx context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform +func (a *alertGroupResource) Update(ctx context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { //nolint:gocritic // function signature required by Terraform core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating alert group", "Observability alert groups can't be updated") } // Delete deletes the resource and removes the Terraform state on success. -func (a *alertGroupResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform +func (a *alertGroupResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { //nolint:gocritic // function signature required by Terraform // Retrieve values from state var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -354,7 +364,7 @@ func (a *alertGroupResource) Delete(ctx context.Context, req resource.DeleteRequ } // ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: project_id,instance_id,name +// The expected format of the resource import identifier is: project_id,instance_id,name. func (a *alertGroupResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { idParts := strings.Split(req.ID, core.Separator) @@ -363,6 +373,7 @@ func (a *alertGroupResource) ImportState(ctx context.Context, req resource.Impor "Error importing scrape config", fmt.Sprintf("Expected import identifier with format: [project_id],[instance_id],[name] Got: %q", req.ID), ) + return } @@ -393,6 +404,7 @@ func toCreatePayload(ctx context.Context, model *Model) (*observability.CreateAl if err != nil { return nil, err } + payload.Rules = &rules } @@ -406,12 +418,14 @@ func toRulesPayload(ctx context.Context, model *Model) ([]observability.UpdateAl } var rules []rule + diags := model.Rules.ElementsAs(ctx, &rules, false) if diags.HasError() { return nil, core.DiagsToError(diags) } var oarrs []observability.UpdateAlertgroupsRequestInnerRulesInner + for i := range rules { rule := &rules[i] oarr := observability.UpdateAlertgroupsRequestInnerRulesInner{} @@ -421,6 +435,7 @@ func toRulesPayload(ctx context.Context, model *Model) ([]observability.UpdateAl if alert == nil { return nil, fmt.Errorf("found nil alert for rule[%d]", i) } + oarr.Alert = alert } @@ -429,6 +444,7 @@ func toRulesPayload(ctx context.Context, model *Model) ([]observability.UpdateAl if expression == nil { return nil, fmt.Errorf("found nil expression for rule[%d]", i) } + oarr.Expr = expression } @@ -437,6 +453,7 @@ func toRulesPayload(ctx context.Context, model *Model) ([]observability.UpdateAl if for_ == nil { return nil, fmt.Errorf("found nil expression for for_[%d]", i) } + oarr.For = for_ } @@ -445,6 +462,7 @@ func toRulesPayload(ctx context.Context, model *Model) ([]observability.UpdateAl if err != nil { return nil, fmt.Errorf("converting to Go map: %w", err) } + oarr.Labels = &labels } @@ -453,6 +471,7 @@ func toRulesPayload(ctx context.Context, model *Model) ([]observability.UpdateAl if err != nil { return nil, fmt.Errorf("converting to Go map: %w", err) } + oarr.Annotations = &annotations } @@ -504,6 +523,7 @@ func mapFields(ctx context.Context, alertGroup *observability.AlertGroup, model } else { return fmt.Errorf("found empty interval") } + model.Interval = types.StringValue(interval) if alertGroup.Rules != nil { @@ -534,6 +554,7 @@ func mapRules(_ context.Context, alertGroup *observability.AlertGroup, model *Mo for k, v := range *r.Labels { labelElems[k] = types.StringValue(v) } + ruleMap["labels"] = types.MapValueMust(types.StringType, labelElems) } @@ -542,6 +563,7 @@ func mapRules(_ context.Context, alertGroup *observability.AlertGroup, model *Mo for k, v := range *r.Annotations { annoElems[k] = types.StringValue(v) } + ruleMap["annotations"] = types.MapValueMust(types.StringType, annoElems) } @@ -549,6 +571,7 @@ func mapRules(_ context.Context, alertGroup *observability.AlertGroup, model *Mo if diags.HasError() { return fmt.Errorf("mapping index %d: %w", i, core.DiagsToError(diags)) } + newRules = append(newRules, ruleTf) } @@ -558,5 +581,6 @@ func mapRules(_ context.Context, alertGroup *observability.AlertGroup, model *Mo } model.Rules = rulesTf + return nil } diff --git a/stackit/internal/services/observability/alertgroup/resource_test.go b/stackit/internal/services/observability/alertgroup/resource_test.go index 746974212..f75cd6ebb 100644 --- a/stackit/internal/services/observability/alertgroup/resource_test.go +++ b/stackit/internal/services/observability/alertgroup/resource_test.go @@ -311,6 +311,7 @@ func TestMapFields(t *testing.T) { if diff := cmp.Diff(tt.model.Name.ValueString(), tt.expectedName); diff != "" { t.Errorf("unexpected name (-got +want):\n%s", diff) } + if diff := cmp.Diff(tt.model.Id.ValueString(), tt.expectedID); diff != "" { t.Errorf("unexpected ID (-got +want):\n%s", diff) } diff --git a/stackit/internal/services/observability/credential/resource.go b/stackit/internal/services/observability/credential/resource.go index 8b5ca3f99..233f07e4b 100644 --- a/stackit/internal/services/observability/credential/resource.go +++ b/stackit/internal/services/observability/credential/resource.go @@ -5,9 +5,6 @@ import ( "fmt" "net/http" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - observabilityUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/observability/utils" - "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" @@ -17,7 +14,9 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/stackitcloud/stackit-sdk-go/services/observability" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + observabilityUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/observability/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" ) @@ -60,10 +59,13 @@ func (r *credentialResource) Configure(ctx context.Context, req resource.Configu } apiClient := observabilityUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + r.client = apiClient + tflog.Info(ctx, "Observability credential client configured") } @@ -128,10 +130,11 @@ func (r *credentialResource) Schema(_ context.Context, _ resource.SchemaRequest, } // Create creates the resource and sets the initial Terraform state. -func (r *credentialResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *credentialResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -149,16 +152,20 @@ func (r *credentialResource) Create(ctx context.Context, req resource.CreateRequ core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credential", fmt.Sprintf("Calling API: %v", err)) return } + err = mapFields(got.Credentials, &model) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credential", fmt.Sprintf("Processing API payload: %v", err)) return } + diags = resp.State.Set(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "Observability credential created") } @@ -166,9 +173,11 @@ func mapFields(r *observability.Credentials, model *Model) error { if r == nil { return fmt.Errorf("response input is nil") } + if model == nil { return fmt.Errorf("model input is nil") } + var userName string if model.Username.ValueString() != "" { userName = model.Username.ValueString() @@ -177,22 +186,26 @@ func mapFields(r *observability.Credentials, model *Model) error { } else { return fmt.Errorf("username id not present") } + model.Id = utils.BuildInternalTerraformId( model.ProjectId.ValueString(), model.InstanceId.ValueString(), userName, ) model.Username = types.StringPointerValue(r.Username) model.Password = types.StringPointerValue(r.Password) + return nil } // Read refreshes the Terraform state with the latest data. -func (r *credentialResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (r *credentialResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() instanceId := model.InstanceId.ValueString() userName := model.Username.ValueString() @@ -209,37 +222,45 @@ func (r *credentialResource) Read(ctx context.Context, req resource.ReadRequest, }, ) resp.State.RemoveResource(ctx) + return } + diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "Observability credential read") } -func (r *credentialResource) Update(ctx context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *credentialResource) Update(ctx context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { //nolint:gocritic // function signature required by Terraform // Update shouldn't be called core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating credential", "Credential can't be updated") } // Delete deletes the resource and removes the Terraform state on success. -func (r *credentialResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform +func (r *credentialResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { //nolint:gocritic // function signature required by Terraform // Retrieve values from state var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() instanceId := model.InstanceId.ValueString() userName := model.Username.ValueString() + _, err := r.client.DeleteCredentials(ctx, instanceId, projectId, userName).Execute() if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting credential", fmt.Sprintf("Calling API: %v", err)) return } + tflog.Info(ctx, "Observability credential deleted") } diff --git a/stackit/internal/services/observability/credential/resource_test.go b/stackit/internal/services/observability/credential/resource_test.go index 34ace309e..8c6d08162 100644 --- a/stackit/internal/services/observability/credential/resource_test.go +++ b/stackit/internal/services/observability/credential/resource_test.go @@ -59,13 +59,16 @@ func TestMapFields(t *testing.T) { ProjectId: tt.expected.ProjectId, InstanceId: tt.expected.InstanceId, } + err := mapFields(tt.input, state) if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(state, &tt.expected) if diff != "" { diff --git a/stackit/internal/services/observability/instance/datasource.go b/stackit/internal/services/observability/instance/datasource.go index 7352b6d10..3dfbe9478 100644 --- a/stackit/internal/services/observability/instance/datasource.go +++ b/stackit/internal/services/observability/instance/datasource.go @@ -5,9 +5,6 @@ import ( "fmt" "net/http" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - observabilityUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/observability/utils" - "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" @@ -15,7 +12,9 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/stackitcloud/stackit-sdk-go/services/observability" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + observabilityUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/observability/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" ) @@ -47,10 +46,13 @@ func (d *instanceDataSource) Configure(ctx context.Context, req datasource.Confi } apiClient := observabilityUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + d.client = apiClient + tflog.Info(ctx, "Observability instance client configured") } @@ -374,13 +376,15 @@ func (d *instanceDataSource) Schema(_ context.Context, _ datasource.SchemaReques } // Read refreshes the Terraform state with the latest data. -func (d *instanceDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (d *instanceDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.Config.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() instanceId := model.InstanceId.ValueString() instanceResp, err := d.client.GetInstance(ctx, instanceId, projectId).Execute() @@ -396,11 +400,14 @@ func (d *instanceDataSource) Read(ctx context.Context, req datasource.ReadReques }, ) resp.State.RemoveResource(ctx) + return } + if instanceResp != nil && instanceResp.Status != nil && *instanceResp.Status == observability.GETINSTANCERESPONSESTATUS_DELETE_SUCCEEDED { resp.State.RemoveResource(ctx) core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", "Instance was deleted successfully") + return } @@ -420,6 +427,7 @@ func (d *instanceDataSource) Read(ctx context.Context, req datasource.ReadReques // Set state to instance populated data diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -439,6 +447,7 @@ func (d *instanceDataSource) Read(ctx context.Context, req datasource.ReadReques // Set state to fully populated data diags = setACL(ctx, &resp.State, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -459,6 +468,7 @@ func (d *instanceDataSource) Read(ctx context.Context, req datasource.ReadReques // Set state to fully populated data diags := setMetricsRetentions(ctx, &resp.State, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -478,6 +488,7 @@ func (d *instanceDataSource) Read(ctx context.Context, req datasource.ReadReques diags = setLogsRetentions(ctx, &resp.State, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -497,6 +508,7 @@ func (d *instanceDataSource) Read(ctx context.Context, req datasource.ReadReques diags = setTracesRetentions(ctx, &resp.State, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -519,6 +531,7 @@ func (d *instanceDataSource) Read(ctx context.Context, req datasource.ReadReques // Set state to fully populated data diags = setAlertConfig(ctx, &resp.State, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } diff --git a/stackit/internal/services/observability/instance/resource.go b/stackit/internal/services/observability/instance/resource.go index 8cae47bf5..bff12c7dd 100644 --- a/stackit/internal/services/observability/instance/resource.go +++ b/stackit/internal/services/observability/instance/resource.go @@ -8,10 +8,6 @@ import ( "strings" "github.com/google/go-cmp/cmp" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - - observabilityUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/observability/utils" - "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" "github.com/hashicorp/terraform-plugin-framework-validators/setvalidator" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" @@ -36,6 +32,8 @@ import ( "github.com/stackitcloud/stackit-sdk-go/services/observability/wait" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + observabilityUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/observability/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" ) @@ -83,7 +81,7 @@ type Model struct { AlertConfig types.Object `tfsdk:"alert_config"` } -// Struct corresponding to Model.AlertConfig +// Struct corresponding to Model.AlertConfig. type alertConfigModel struct { GlobalConfiguration types.Object `tfsdk:"global"` Receivers types.List `tfsdk:"receivers"` @@ -96,7 +94,7 @@ var alertConfigTypes = map[string]attr.Type{ "global": types.ObjectType{AttrTypes: globalConfigurationTypes}, } -// Struct corresponding to Model.AlertConfig.global +// Struct corresponding to Model.AlertConfig.global. type globalConfigurationModel struct { OpsgenieApiKey types.String `tfsdk:"opsgenie_api_key"` OpsgenieApiUrl types.String `tfsdk:"opsgenie_api_url"` @@ -119,7 +117,7 @@ var globalConfigurationTypes = map[string]attr.Type{ "smtp_smart_host": types.StringType, } -// Struct corresponding to Model.AlertConfig.route +// Struct corresponding to Model.AlertConfig.route. type mainRouteModel struct { GroupBy types.List `tfsdk:"group_by"` GroupInterval types.String `tfsdk:"group_interval"` @@ -130,7 +128,7 @@ type mainRouteModel struct { } // Struct corresponding to Model.AlertConfig.route -// This is used to map the routes between the mainRouteModel and the last level of recursion of the routes field +// This is used to map the routes between the mainRouteModel and the last level of recursion of the routes field. type routeModelMiddle struct { Continue types.Bool `tfsdk:"continue"` GroupBy types.List `tfsdk:"group_by"` @@ -147,7 +145,7 @@ type routeModelMiddle struct { } // Struct corresponding to Model.AlertConfig.route but without the recursive routes field -// This is used to map the last level of recursion of the routes field +// This is used to map the last level of recursion of the routes field. type routeModelNoRoutes struct { Continue types.Bool `tfsdk:"continue"` GroupBy types.List `tfsdk:"group_by"` @@ -171,7 +169,7 @@ var mainRouteTypes = map[string]attr.Type{ "routes": types.ListType{ElemType: getRouteListType()}, } -// Struct corresponding to Model.AlertConfig.receivers +// Struct corresponding to Model.AlertConfig.receivers. type receiversModel struct { Name types.String `tfsdk:"name"` EmailConfigs types.List `tfsdk:"email_configs"` @@ -186,7 +184,7 @@ var receiversTypes = map[string]attr.Type{ "webhooks_configs": types.ListType{ElemType: types.ObjectType{AttrTypes: webHooksConfigsTypes}}, } -// Struct corresponding to Model.AlertConfig.receivers.emailConfigs +// Struct corresponding to Model.AlertConfig.receivers.emailConfigs. type emailConfigsModel struct { AuthIdentity types.String `tfsdk:"auth_identity"` AuthPassword types.String `tfsdk:"auth_password"` @@ -207,7 +205,7 @@ var emailConfigsTypes = map[string]attr.Type{ "to": types.StringType, } -// Struct corresponding to Model.AlertConfig.receivers.opsGenieConfigs +// Struct corresponding to Model.AlertConfig.receivers.opsGenieConfigs. type opsgenieConfigsModel struct { ApiKey types.String `tfsdk:"api_key"` ApiUrl types.String `tfsdk:"api_url"` @@ -224,7 +222,7 @@ var opsgenieConfigsTypes = map[string]attr.Type{ "send_resolved": types.BoolType, } -// Struct corresponding to Model.AlertConfig.receivers.webHooksConfigs +// Struct corresponding to Model.AlertConfig.receivers.webHooksConfigs. type webHooksConfigsModel struct { Url types.String `tfsdk:"url"` MsTeams types.Bool `tfsdk:"ms_teams"` @@ -398,10 +396,13 @@ func (r *instanceResource) Configure(ctx context.Context, req resource.Configure } apiClient := observabilityUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + r.client = apiClient + tflog.Info(ctx, "Observability instance client configured") } @@ -835,7 +836,7 @@ func (r *instanceResource) Schema(_ context.Context, _ resource.SchemaRequest, r // ModifyPlan will be called in the Plan phase. // It will check if the plan contains a change that requires replacement. If yes, it will show an error to the user. // Since there are observabiltiy plans which do not support specific configurations the request needs to be aborted with an error. -func (r *instanceResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform +func (r *instanceResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { //nolint:gocritic // function signature required by Terraform // If the plan is empty we are deleting the resource if req.Plan.Raw.IsNull() { return @@ -846,7 +847,9 @@ func (r *instanceResource) ModifyPlan(ctx context.Context, req resource.ModifyPl if req.Config.Raw.IsNull() { return } + resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...) + if resp.Diagnostics.HasError() { return } @@ -888,28 +891,31 @@ func (r *instanceResource) ModifyPlan(ctx context.Context, req resource.ModifyPl } // Create creates the resource and sets the initial Terraform state. -func (r *instanceResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *instanceResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { //nolint:gocritic // function signature required by Terraform // Retrieve values from plan var model Model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } acl := []string{} - if !(model.ACL.IsNull() || model.ACL.IsUnknown()) { + if !model.ACL.IsNull() && !model.ACL.IsUnknown() { diags = model.ACL.ElementsAs(ctx, &acl, false) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } } alertConfig := alertConfigModel{} - if !(model.AlertConfig.IsNull() || model.AlertConfig.IsUnknown()) { + if !model.AlertConfig.IsNull() && !model.AlertConfig.IsUnknown() { diags = model.AlertConfig.As(ctx, &alertConfig, basetypes.ObjectAsOptions{}) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -929,11 +935,13 @@ func (r *instanceResource) Create(ctx context.Context, req resource.CreateReques core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Creating API payload: %v", err)) return } + createResp, err := r.client.CreateInstance(ctx, projectId).CreateInstancePayload(*createPayload).Execute() if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Calling API: %v", err)) return } + instanceId := createResp.InstanceId ctx = tflog.SetField(ctx, "instance_id", instanceId) waitResp, err := wait.CreateInstanceWaitHandler(ctx, r.client, *instanceId, projectId).WaitWithContext(ctx) @@ -952,6 +960,7 @@ func (r *instanceResource) Create(ctx context.Context, req resource.CreateReques // Set state to instance populated data diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -962,6 +971,7 @@ func (r *instanceResource) Create(ctx context.Context, req resource.CreateReques core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Creating ACL: %v", err)) return } + aclList, err := r.client.ListACL(ctx, *instanceId, projectId).Execute() if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Calling API to list ACL data: %v", err)) @@ -978,6 +988,7 @@ func (r *instanceResource) Create(ctx context.Context, req resource.CreateReques // Set state to fully populated data diags = setACL(ctx, &resp.State, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -992,6 +1003,7 @@ func (r *instanceResource) Create(ctx context.Context, req resource.CreateReques // Set state to fully populated data diags = setMetricsRetentions(ctx, &resp.State, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -1003,6 +1015,7 @@ func (r *instanceResource) Create(ctx context.Context, req resource.CreateReques diags = setLogsRetentions(ctx, &resp.State, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -1014,6 +1027,7 @@ func (r *instanceResource) Create(ctx context.Context, req resource.CreateReques diags = setTracesRetentions(ctx, &resp.State, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -1021,18 +1035,21 @@ func (r *instanceResource) Create(ctx context.Context, req resource.CreateReques // Set metric retention days to zero diags = setMetricsRetentionsZero(ctx, &resp.State) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } // Set logs retention days to zero diags = setLogsRetentionsZero(ctx, &resp.State) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } // Set traces retention days to zero diags = setTracesRetentionsZero(ctx, &resp.State) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -1048,6 +1065,7 @@ func (r *instanceResource) Create(ctx context.Context, req resource.CreateReques // Set state to fully populated data diags = setAlertConfig(ctx, &resp.State, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -1057,13 +1075,15 @@ func (r *instanceResource) Create(ctx context.Context, req resource.CreateReques } // Read refreshes the Terraform state with the latest data. -func (r *instanceResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (r *instanceResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() instanceId := model.InstanceId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) @@ -1076,9 +1096,12 @@ func (r *instanceResource) Read(ctx context.Context, req resource.ReadRequest, r resp.State.RemoveResource(ctx) return } + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Calling API: %v", err)) + return } + if instanceResp != nil && instanceResp.Status != nil && *instanceResp.Status == observability.GETINSTANCERESPONSESTATUS_DELETE_SUCCEEDED { resp.State.RemoveResource(ctx) return @@ -1106,6 +1129,7 @@ func (r *instanceResource) Read(ctx context.Context, req resource.ReadRequest, r // Set state to fully populated data diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -1120,6 +1144,7 @@ func (r *instanceResource) Read(ctx context.Context, req resource.ReadRequest, r // Set state to fully populated data diags = setACL(ctx, &resp.State, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -1140,6 +1165,7 @@ func (r *instanceResource) Read(ctx context.Context, req resource.ReadRequest, r // Set state to fully populated data diags = setMetricsRetentions(ctx, &resp.State, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -1158,6 +1184,7 @@ func (r *instanceResource) Read(ctx context.Context, req resource.ReadRequest, r // Set state to fully populated data diags = setLogsRetentions(ctx, &resp.State, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -1176,6 +1203,7 @@ func (r *instanceResource) Read(ctx context.Context, req resource.ReadRequest, r // Set state to fully populated data diags = setTracesRetentions(ctx, &resp.State, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -1198,6 +1226,7 @@ func (r *instanceResource) Read(ctx context.Context, req resource.ReadRequest, r // Set state to fully populated data diags = setAlertConfig(ctx, &resp.State, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -1207,30 +1236,34 @@ func (r *instanceResource) Read(ctx context.Context, req resource.ReadRequest, r } // Update updates the resource and sets the updated Terraform state on success. -func (r *instanceResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *instanceResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { //nolint:gocritic // function signature required by Terraform // Retrieve values from plan var model Model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() instanceId := model.InstanceId.ValueString() acl := []string{} - if !(model.ACL.IsNull() || model.ACL.IsUnknown()) { + if !model.ACL.IsNull() && !model.ACL.IsUnknown() { diags = model.ACL.ElementsAs(ctx, &acl, false) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } } alertConfig := alertConfigModel{} - if !(model.AlertConfig.IsNull() || model.AlertConfig.IsUnknown()) { + if !model.AlertConfig.IsNull() && !model.AlertConfig.IsUnknown() { diags = model.AlertConfig.As(ctx, &alertConfig, basetypes.ObjectAsOptions{}) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -1248,17 +1281,21 @@ func (r *instanceResource) Update(ctx context.Context, req resource.UpdateReques core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Creating API payload: %v", err)) return } + var previousState Model diags = req.State.Get(ctx, &previousState) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + previousStatePayload, err := toUpdatePayload(&previousState) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Creating previous state payload: %v", err)) return } + var instance *observability.GetInstanceResponse // This check is required, because when values should be updated, that needs to be updated via a different endpoint, the waiter will run into a timeout if !cmp.Equal(previousStatePayload, payload) { @@ -1268,6 +1305,7 @@ func (r *instanceResource) Update(ctx context.Context, req resource.UpdateReques core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Calling API: %v", err)) return } + instance, err = wait.UpdateInstanceWaitHandler(ctx, r.client, instanceId, projectId).WaitWithContext(ctx) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Instance update waiting: %v", err)) @@ -1286,8 +1324,10 @@ func (r *instanceResource) Update(ctx context.Context, req resource.UpdateReques core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Processing API payload: %v", err)) return } + diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -1298,6 +1338,7 @@ func (r *instanceResource) Update(ctx context.Context, req resource.UpdateReques core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Updating ACL: %v", err)) return } + aclList, err := r.client.ListACL(ctx, instanceId, projectId).Execute() if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Calling API to list ACL data: %v", err)) @@ -1313,6 +1354,7 @@ func (r *instanceResource) Update(ctx context.Context, req resource.UpdateReques // Set state to ACL populated data resp.Diagnostics.Append(setACL(ctx, &resp.State, &model)...) + if resp.Diagnostics.HasError() { return } @@ -1327,6 +1369,7 @@ func (r *instanceResource) Update(ctx context.Context, req resource.UpdateReques // Set state to fully populated data diags = setMetricsRetentions(ctx, &resp.State, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -1338,6 +1381,7 @@ func (r *instanceResource) Update(ctx context.Context, req resource.UpdateReques diags = setLogsRetentions(ctx, &resp.State, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -1349,6 +1393,7 @@ func (r *instanceResource) Update(ctx context.Context, req resource.UpdateReques diags = setTracesRetentions(ctx, &resp.State, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -1356,18 +1401,21 @@ func (r *instanceResource) Update(ctx context.Context, req resource.UpdateReques // Set metric retention days to zero diags = setMetricsRetentionsZero(ctx, &resp.State) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } diags = setLogsRetentionsZero(ctx, &resp.State) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } diags = setTracesRetentionsZero(ctx, &resp.State) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -1383,6 +1431,7 @@ func (r *instanceResource) Update(ctx context.Context, req resource.UpdateReques // Set state to fully populated data diags = setAlertConfig(ctx, &resp.State, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -1392,11 +1441,12 @@ func (r *instanceResource) Update(ctx context.Context, req resource.UpdateReques } // Delete deletes the resource and removes the Terraform state on success. -func (r *instanceResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform +func (r *instanceResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { //nolint:gocritic // function signature required by Terraform // Retrieve values from state var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -1410,6 +1460,7 @@ func (r *instanceResource) Delete(ctx context.Context, req resource.DeleteReques core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting instance", fmt.Sprintf("Calling API: %v", err)) return } + _, err = wait.DeleteInstanceWaitHandler(ctx, r.client, instanceId, projectId).WaitWithContext(ctx) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting instance", fmt.Sprintf("Instance deletion waiting: %v", err)) @@ -1420,7 +1471,7 @@ func (r *instanceResource) Delete(ctx context.Context, req resource.DeleteReques } // ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: project_id,instance_id +// The expected format of the resource import identifier is: project_id,instance_id. func (r *instanceResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { idParts := strings.Split(req.ID, core.Separator) @@ -1429,6 +1480,7 @@ func (r *instanceResource) ImportState(ctx context.Context, req resource.ImportS "Error importing instance", fmt.Sprintf("Expected import identifier with format: [project_id],[instance_id] Got: %q", req.ID), ) + return } @@ -1441,9 +1493,11 @@ func mapFields(ctx context.Context, r *observability.GetInstanceResponse, model if r == nil { return fmt.Errorf("response input is nil") } + if model == nil { return fmt.Errorf("model input is nil") } + var instanceId string if model.InstanceId.ValueString() != "" { instanceId = model.InstanceId.ValueString() @@ -1467,15 +1521,18 @@ func mapFields(ctx context.Context, r *observability.GetInstanceResponse, model for k, v := range *ps { params[k] = types.StringValue(v) } + res, diags := types.MapValueFrom(ctx, types.StringType, params) if diags.HasError() { return fmt.Errorf("parameter mapping %s", diags.Errors()) } + model.Parameters = res } model.IsUpdatable = types.BoolPointerValue(r.IsUpdatable) model.DashboardURL = types.StringPointerValue(r.DashboardUrl) + if r.Instance != nil { i := *r.Instance model.GrafanaURL = types.StringPointerValue(i.GrafanaUrl) @@ -1503,9 +1560,10 @@ func mapACLField(aclList *observability.ListACLResponse, model *Model) error { } if aclList.Acl == nil || len(*aclList.Acl) == 0 { - if !(model.ACL.IsNull() || model.ACL.IsUnknown() || model.ACL.Equal(types.SetValueMust(types.StringType, []attr.Value{}))) { + if !model.ACL.IsNull() && !model.ACL.IsUnknown() && !model.ACL.Equal(types.SetValueMust(types.StringType, []attr.Value{})) { model.ACL = types.SetNull(types.StringType) } + return nil } @@ -1513,11 +1571,14 @@ func mapACLField(aclList *observability.ListACLResponse, model *Model) error { for _, cidr := range *aclList.Acl { acl = append(acl, types.StringValue(cidr)) } + aclTF, diags := types.SetValue(types.StringType, acl) if diags.HasError() { return fmt.Errorf("mapping ACL: %w", core.DiagsToError(diags)) } + model.ACL = aclTF + return nil } @@ -1525,6 +1586,7 @@ func mapLogsRetentionField(r *observability.LogsConfigResponse, model *Model) er if r == nil { return fmt.Errorf("response input is nil") } + if model == nil { return fmt.Errorf("model input is nil") } @@ -1538,11 +1600,14 @@ func mapLogsRetentionField(r *observability.LogsConfigResponse, model *Model) er } stripedLogsRetentionHours := strings.TrimSuffix(*r.Config.Retention, "h") + logsRetentionHours, err := strconv.ParseInt(stripedLogsRetentionHours, 10, 64) if err != nil { return fmt.Errorf("parsing logs retention hours: %w", err) } + model.LogsRetentionDays = types.Int64Value(logsRetentionHours / 24) + return nil } @@ -1550,6 +1615,7 @@ func mapTracesRetentionField(r *observability.TracesConfigResponse, model *Model if r == nil { return fmt.Errorf("response input is nil") } + if model == nil { return fmt.Errorf("model input is nil") } @@ -1563,11 +1629,14 @@ func mapTracesRetentionField(r *observability.TracesConfigResponse, model *Model } stripedTracesRetentionHours := strings.TrimSuffix(*r.Config.Retention, "h") + tracesRetentionHours, err := strconv.ParseInt(stripedTracesRetentionHours, 10, 64) if err != nil { return fmt.Errorf("parsing traces retention hours: %w", err) } + model.TracesRetentionDays = types.Int64Value(tracesRetentionHours / 24) + return nil } @@ -1575,6 +1644,7 @@ func mapMetricsRetentionField(r *observability.GetMetricsStorageRetentionRespons if r == nil { return fmt.Errorf("response input is nil") } + if model == nil { return fmt.Errorf("model input is nil") } @@ -1584,24 +1654,30 @@ func mapMetricsRetentionField(r *observability.GetMetricsStorageRetentionRespons } stripedMetricsRetentionDays := strings.TrimSuffix(*r.MetricsRetentionTimeRaw, "d") + metricsRetentionDays, err := strconv.ParseInt(stripedMetricsRetentionDays, 10, 64) if err != nil { return fmt.Errorf("parsing metrics retention days: %w", err) } + model.MetricsRetentionDays = types.Int64Value(metricsRetentionDays) stripedMetricsRetentionDays5m := strings.TrimSuffix(*r.MetricsRetentionTime5m, "d") + metricsRetentionDays5m, err := strconv.ParseInt(stripedMetricsRetentionDays5m, 10, 64) if err != nil { return fmt.Errorf("parsing metrics retention days 5m: %w", err) } + model.MetricsRetentionDays5mDownsampling = types.Int64Value(metricsRetentionDays5m) stripedMetricsRetentionDays1h := strings.TrimSuffix(*r.MetricsRetentionTime1h, "d") + metricsRetentionDays1h, err := strconv.ParseInt(stripedMetricsRetentionDays1h, 10, 64) if err != nil { return fmt.Errorf("parsing metrics retention days 1h: %w", err) } + model.MetricsRetentionDays1hDownsampling = types.Int64Value(metricsRetentionDays1h) return nil @@ -1618,8 +1694,9 @@ func mapAlertConfigField(ctx context.Context, resp *observability.GetAlertConfig } var alertConfigTF *alertConfigModel - if !(model.AlertConfig.IsNull() || model.AlertConfig.IsUnknown()) { + if !model.AlertConfig.IsNull() && !model.AlertConfig.IsUnknown() { alertConfigTF = &alertConfigModel{} + diags := model.AlertConfig.As(ctx, &alertConfigTF, basetypes.ObjectAsOptions{}) if diags.HasError() { return fmt.Errorf("mapping alert config: %w", core.DiagsToError(diags)) @@ -1643,6 +1720,7 @@ func mapAlertConfigField(ctx context.Context, resp *observability.GetAlertConfig var globalConfigModel *globalConfigurationModel if alertConfigTF != nil && !alertConfigTF.GlobalConfiguration.IsNull() && !alertConfigTF.GlobalConfiguration.IsUnknown() { globalConfigModel = &globalConfigurationModel{} + diags := alertConfigTF.GlobalConfiguration.As(ctx, globalConfigModel, basetypes.ObjectAsOptions{}) if diags.HasError() { return fmt.Errorf("mapping alert config: %w", core.DiagsToError(diags)) @@ -1671,15 +1749,18 @@ func mapAlertConfigField(ctx context.Context, resp *observability.GetAlertConfig if err != nil { return fmt.Errorf("getting mock alert config: %w", err) } + modelMockAlertConfig, diags := types.ObjectValueFrom(ctx, alertConfigTypes, mockAlertConfig) if diags.HasError() { return fmt.Errorf("converting mock alert config to TF type: %w", core.DiagsToError(diags)) } + if alertConfig.Equal(modelMockAlertConfig) { alertConfig = types.ObjectNull(alertConfigTypes) } model.AlertConfig = alertConfig + return nil } @@ -1687,7 +1768,7 @@ func mapAlertConfigField(ctx context.Context, resp *observability.GetAlertConfig // // This is done because the Alert Config cannot be removed from the instance, but can be unset by the user in the Terraform configuration. // So, we set the Alert Config in the instance to our mock configuration and -// map the Alert Config to an empty object in the Terraform state if it matches the mock alert config +// map the Alert Config to an empty object in the Terraform state if it matches the mock alert config. func getMockAlertConfig(ctx context.Context) (alertConfigModel, error) { mockEmailConfig, diags := types.ObjectValue(emailConfigsTypes, map[string]attr.Value{ "to": types.StringValue("123@gmail.com"), @@ -1776,19 +1857,23 @@ func mapGlobalConfigToAttributes(respGlobalConfigs *observability.Global, global smtpAuthIdentity := respGlobalConfigs.SmtpAuthIdentity smtpAuthPassword := respGlobalConfigs.SmtpAuthPassword smtpAuthUsername := respGlobalConfigs.SmtpAuthUsername + if globalConfigsTF != nil { if respGlobalConfigs.SmtpSmarthost == nil && !globalConfigsTF.SmtpSmartHost.IsNull() && !globalConfigsTF.SmtpSmartHost.IsUnknown() { smtpSmartHost = sdkUtils.Ptr(globalConfigsTF.SmtpSmartHost.ValueString()) } + if respGlobalConfigs.SmtpAuthIdentity == nil && !globalConfigsTF.SmtpAuthIdentity.IsNull() && !globalConfigsTF.SmtpAuthIdentity.IsUnknown() { smtpAuthIdentity = sdkUtils.Ptr(globalConfigsTF.SmtpAuthIdentity.ValueString()) } + if respGlobalConfigs.SmtpAuthPassword == nil && !globalConfigsTF.SmtpAuthPassword.IsNull() && !globalConfigsTF.SmtpAuthPassword.IsUnknown() { smtpAuthPassword = sdkUtils.Ptr(globalConfigsTF.SmtpAuthPassword.ValueString()) } + if respGlobalConfigs.SmtpAuthUsername == nil && !globalConfigsTF.SmtpAuthUsername.IsNull() && !globalConfigsTF.SmtpAuthUsername.IsUnknown() { smtpAuthUsername = sdkUtils.Ptr(globalConfigsTF.SmtpAuthUsername.ValueString()) @@ -1816,7 +1901,9 @@ func mapReceiversToAttributes(ctx context.Context, respReceivers *[]observabilit if respReceivers == nil { return types.ListNull(types.ObjectType{AttrTypes: receiversTypes}), nil } + receiversList := []attr.Value{} + emptyList, diags := types.ListValueFrom(ctx, types.ObjectType{AttrTypes: receiversTypes}, []attr.Value{}) if diags.HasError() { // Should not happen @@ -1831,6 +1918,7 @@ func mapReceiversToAttributes(ctx context.Context, respReceivers *[]observabilit receiver := (*respReceivers)[i] emailConfigList := []attr.Value{} + if receiver.EmailConfigs != nil { for _, emailConfig := range *receiver.EmailConfigs { emailConfigMap := map[string]attr.Value{ @@ -1842,15 +1930,18 @@ func mapReceiversToAttributes(ctx context.Context, respReceivers *[]observabilit "smart_host": types.StringPointerValue(emailConfig.Smarthost), "to": types.StringPointerValue(emailConfig.To), } + emailConfigModel, diags := types.ObjectValue(emailConfigsTypes, emailConfigMap) if diags.HasError() { return emptyList, fmt.Errorf("mapping email config: %w", core.DiagsToError(diags)) } + emailConfigList = append(emailConfigList, emailConfigModel) } } opsgenieConfigList := []attr.Value{} + if receiver.OpsgenieConfigs != nil { for _, opsgenieConfig := range *receiver.OpsgenieConfigs { opsGenieConfigMap := map[string]attr.Value{ @@ -1860,15 +1951,18 @@ func mapReceiversToAttributes(ctx context.Context, respReceivers *[]observabilit "priority": types.StringPointerValue(opsgenieConfig.Priority), "send_resolved": types.BoolPointerValue(opsgenieConfig.SendResolved), } + opsGenieConfigModel, diags := types.ObjectValue(opsgenieConfigsTypes, opsGenieConfigMap) if diags.HasError() { return emptyList, fmt.Errorf("mapping opsgenie config: %w", core.DiagsToError(diags)) } + opsgenieConfigList = append(opsgenieConfigList, opsGenieConfigModel) } } webhooksConfigList := []attr.Value{} + if receiver.WebHookConfigs != nil { for _, webhookConfig := range *receiver.WebHookConfigs { webHookConfigsMap := map[string]attr.Value{ @@ -1877,10 +1971,12 @@ func mapReceiversToAttributes(ctx context.Context, respReceivers *[]observabilit "google_chat": types.BoolPointerValue(webhookConfig.GoogleChat), "send_resolved": types.BoolPointerValue(webhookConfig.SendResolved), } + webHookConfigsModel, diags := types.ObjectValue(webHooksConfigsTypes, webHookConfigsMap) if diags.HasError() { return emptyList, fmt.Errorf("mapping webhooks config: %w", core.DiagsToError(diags)) } + webhooksConfigList = append(webhooksConfigList, webHookConfigsModel) } } @@ -1938,6 +2034,7 @@ func mapReceiversToAttributes(ctx context.Context, respReceivers *[]observabilit if diags.HasError() { return emptyList, fmt.Errorf("mapping receivers list: %w", core.DiagsToError(diags)) } + return returnReceiversList, nil } @@ -1976,7 +2073,7 @@ func mapRouteToAttributes(ctx context.Context, route *observability.Route) (attr // mapChildRoutesToAttributes maps the child routes to the Terraform attributes // This should be a recursive function to handle nested child routes // However, the API does not currently have the correct type for the child routes -// In the future, the current implementation should be the final case of the recursive function +// In the future, the current implementation should be the final case of the recursive function. func mapChildRoutesToAttributes(ctx context.Context, routes *[]observability.RouteSerializer) (basetypes.ListValue, error) { nullList := types.ListNull(getRouteListType()) if routes == nil { @@ -1984,6 +2081,7 @@ func mapChildRoutesToAttributes(ctx context.Context, routes *[]observability.Rou } routesList := []attr.Value{} + for _, route := range *routes { groupByModel, diags := types.ListValueFrom(ctx, types.StringType, route.GroupBy) if diags.HasError() { @@ -2029,6 +2127,7 @@ func mapChildRoutesToAttributes(ctx context.Context, routes *[]observability.Rou if diags.HasError() { return nullList, fmt.Errorf("mapping child routes list: %w", core.DiagsToError(diags)) } + return returnRoutesList, nil } @@ -2036,11 +2135,14 @@ func toCreatePayload(model *Model) (*observability.CreateInstancePayload, error) if model == nil { return nil, fmt.Errorf("nil model") } + elements := model.Parameters.Elements() pa := make(map[string]interface{}, len(elements)) + for k := range elements { pa[k] = elements[k].String() } + return &observability.CreateInstancePayload{ Name: conversion.StringValueToPointer(model.Name), PlanId: conversion.StringValueToPointer(model.PlanId), @@ -2050,7 +2152,9 @@ func toCreatePayload(model *Model) (*observability.CreateInstancePayload, error) func toUpdateMetricsStorageRetentionPayload(retentionDaysRaw, retentionDays5m, retentionDays1h *int64, resp *observability.GetMetricsStorageRetentionResponse) (*observability.UpdateMetricsStorageRetentionPayload, error) { var retentionTimeRaw string + var retentionTime5m string + var retentionTime1h string if resp == nil || resp.MetricsRetentionTimeRaw == nil || resp.MetricsRetentionTime5m == nil || resp.MetricsRetentionTime1h == nil { @@ -2099,11 +2203,14 @@ func toUpdatePayload(model *Model) (*observability.UpdateInstancePayload, error) if model == nil { return nil, fmt.Errorf("nil model") } + elements := model.Parameters.Elements() pa := make(map[string]interface{}, len(elements)) + for k, v := range elements { pa[k] = v.String() } + return &observability.UpdateInstancePayload{ Name: conversion.StringValueToPointer(model.Name), PlanId: conversion.StringValueToPointer(model.PlanId), @@ -2115,6 +2222,7 @@ func toUpdateAlertConfigPayload(ctx context.Context, model *alertConfigModel) (* if model == nil { return nil, fmt.Errorf("nil model") } + if model.Receivers.IsNull() || model.Receivers.IsUnknown() { return nil, fmt.Errorf("receivers in the model are null or unknown") } @@ -2133,6 +2241,7 @@ func toUpdateAlertConfigPayload(ctx context.Context, model *alertConfigModel) (* } routeTF := mainRouteModel{} + diags := model.Route.As(ctx, &routeTF, basetypes.ObjectAsOptions{}) if diags.HasError() { return nil, fmt.Errorf("mapping route: %w", core.DiagsToError(diags)) @@ -2157,7 +2266,9 @@ func toReceiverPayload(ctx context.Context, model *alertConfigModel) (*[]observa if model == nil { return nil, fmt.Errorf("nil model") } + receiversModel := []receiversModel{} + diags := model.Receivers.ElementsAs(ctx, &receiversModel, false) if diags.HasError() { return nil, fmt.Errorf("mapping receivers: %w", core.DiagsToError(diags)) @@ -2173,11 +2284,14 @@ func toReceiverPayload(ctx context.Context, model *alertConfigModel) (*[]observa if !receiver.EmailConfigs.IsNull() && !receiver.EmailConfigs.IsUnknown() { emailConfigs := []emailConfigsModel{} + diags := receiver.EmailConfigs.ElementsAs(ctx, &emailConfigs, false) if diags.HasError() { return nil, fmt.Errorf("mapping email configs: %w", core.DiagsToError(diags)) } + payloadEmailConfigs := []observability.CreateAlertConfigReceiverPayloadEmailConfigsInner{} + for i := range emailConfigs { emailConfig := emailConfigs[i] payloadEmailConfigs = append(payloadEmailConfigs, observability.CreateAlertConfigReceiverPayloadEmailConfigsInner{ @@ -2190,16 +2304,20 @@ func toReceiverPayload(ctx context.Context, model *alertConfigModel) (*[]observa To: conversion.StringValueToPointer(emailConfig.To), }) } + receiverPayload.EmailConfigs = &payloadEmailConfigs } if !receiver.OpsGenieConfigs.IsNull() && !receiver.OpsGenieConfigs.IsUnknown() { opsgenieConfigs := []opsgenieConfigsModel{} + diags := receiver.OpsGenieConfigs.ElementsAs(ctx, &opsgenieConfigs, false) if diags.HasError() { return nil, fmt.Errorf("mapping opsgenie configs: %w", core.DiagsToError(diags)) } + payloadOpsGenieConfigs := []observability.CreateAlertConfigReceiverPayloadOpsgenieConfigsInner{} + for i := range opsgenieConfigs { opsgenieConfig := opsgenieConfigs[i] payloadOpsGenieConfigs = append(payloadOpsGenieConfigs, observability.CreateAlertConfigReceiverPayloadOpsgenieConfigsInner{ @@ -2210,16 +2328,20 @@ func toReceiverPayload(ctx context.Context, model *alertConfigModel) (*[]observa SendResolved: conversion.BoolValueToPointer(opsgenieConfig.SendResolved), }) } + receiverPayload.OpsgenieConfigs = &payloadOpsGenieConfigs } if !receiver.WebHooksConfigs.IsNull() && !receiver.WebHooksConfigs.IsUnknown() { receiverWebHooksConfigs := []webHooksConfigsModel{} + diags := receiver.WebHooksConfigs.ElementsAs(ctx, &receiverWebHooksConfigs, false) if diags.HasError() { return nil, fmt.Errorf("mapping webhooks configs: %w", core.DiagsToError(diags)) } + payloadWebHooksConfigs := []observability.CreateAlertConfigReceiverPayloadWebHookConfigsInner{} + for i := range receiverWebHooksConfigs { webHooksConfig := receiverWebHooksConfigs[i] payloadWebHooksConfigs = append(payloadWebHooksConfigs, observability.CreateAlertConfigReceiverPayloadWebHookConfigsInner{ @@ -2229,11 +2351,13 @@ func toReceiverPayload(ctx context.Context, model *alertConfigModel) (*[]observa SendResolved: conversion.BoolValueToPointer(webHooksConfig.SendResolved), }) } + receiverPayload.WebHookConfigs = &payloadWebHooksConfigs } receivers = append(receivers, receiverPayload) } + return &receivers, nil } @@ -2243,10 +2367,12 @@ func toRoutePayload(ctx context.Context, routeTF *mainRouteModel) (*observabilit } var groupByPayload *[]string + var childRoutesPayload *[]observability.UpdateAlertConfigsPayloadRouteRoutesInner if !routeTF.GroupBy.IsNull() && !routeTF.GroupBy.IsUnknown() { groupByPayload = &[]string{} + diags := routeTF.GroupBy.ElementsAs(ctx, groupByPayload, false) if diags.HasError() { return nil, fmt.Errorf("mapping group by: %w", core.DiagsToError(diags)) @@ -2255,16 +2381,19 @@ func toRoutePayload(ctx context.Context, routeTF *mainRouteModel) (*observabilit if !routeTF.Routes.IsNull() && !routeTF.Routes.IsUnknown() { childRoutes := []routeModelMiddle{} + diags := routeTF.Routes.ElementsAs(ctx, &childRoutes, false) if diags.HasError() { // If there is an error, we will try to map the child routes as if they are the last child routes // This is done because the last child routes in the recursion have a different structure (don't have the `routes` fields) // and need to be unpacked to a different struct (routeModelNoRoutes) lastChildRoutes := []routeModelNoRoutes{} + diags = routeTF.Routes.ElementsAs(ctx, &lastChildRoutes, true) if diags.HasError() { return nil, fmt.Errorf("mapping child routes: %w", core.DiagsToError(diags)) } + for i := range lastChildRoutes { childRoute := routeModelMiddle{ Continue: lastChildRoutes[i].Continue, @@ -2283,12 +2412,15 @@ func toRoutePayload(ctx context.Context, routeTF *mainRouteModel) (*observabilit } childRoutesList := []observability.UpdateAlertConfigsPayloadRouteRoutesInner{} + for i := range childRoutes { childRoute := childRoutes[i] + childRoutePayload, err := toChildRoutePayload(ctx, &childRoute) if err != nil { return nil, fmt.Errorf("mapping child route: %w", err) } + childRoutesList = append(childRoutesList, *childRoutePayload) } @@ -2311,10 +2443,12 @@ func toChildRoutePayload(ctx context.Context, routeTF *routeModelMiddle) (*obser } var groupByPayload, matchersPayload *[]string + var matchPayload, matchRegexPayload *map[string]interface{} if !utils.IsUndefined(routeTF.GroupBy) { groupByPayload = &[]string{} + diags := routeTF.GroupBy.ElementsAs(ctx, groupByPayload, false) if diags.HasError() { return nil, fmt.Errorf("mapping group by: %w", core.DiagsToError(diags)) @@ -2326,6 +2460,7 @@ func toChildRoutePayload(ctx context.Context, routeTF *routeModelMiddle) (*obser if err != nil { return nil, fmt.Errorf("mapping match: %w", err) } + matchPayload = &matchMap } @@ -2334,6 +2469,7 @@ func toChildRoutePayload(ctx context.Context, routeTF *routeModelMiddle) (*obser if err != nil { return nil, fmt.Errorf("mapping match regex: %w", err) } + matchRegexPayload = &matchRegexMap } @@ -2342,6 +2478,7 @@ func toChildRoutePayload(ctx context.Context, routeTF *routeModelMiddle) (*obser if err != nil { return nil, fmt.Errorf("mapping match regex: %w", err) } + matchersPayload = matchersList } @@ -2361,6 +2498,7 @@ func toChildRoutePayload(ctx context.Context, routeTF *routeModelMiddle) (*obser func toGlobalConfigPayload(ctx context.Context, model *alertConfigModel) (*observability.UpdateAlertConfigsPayloadGlobal, error) { globalConfigModel := globalConfigurationModel{} + diags := model.GlobalConfiguration.As(ctx, &globalConfigModel, basetypes.ObjectAsOptions{}) if diags.HasError() { return nil, fmt.Errorf("mapping global configuration: %w", core.DiagsToError(diags)) @@ -2380,6 +2518,7 @@ func toGlobalConfigPayload(ctx context.Context, model *alertConfigModel) (*obser func loadPlanId(ctx context.Context, client observability.APIClient, model *Model) (observability.Plan, error) { projectId := model.ProjectId.ValueString() + res, err := client.ListPlans(ctx, projectId).Execute() if err != nil { return observability.Plan{}, err @@ -2387,57 +2526,64 @@ func loadPlanId(ctx context.Context, client observability.APIClient, model *Mode planName := model.PlanName.ValueString() avl := "" + plans := *res.Plans for i := range plans { p := plans[i] if p.Name == nil { continue } + if strings.EqualFold(*p.Name, planName) && p.PlanId != nil { model.PlanId = types.StringPointerValue(p.PlanId) return p, nil } + avl = fmt.Sprintf("%s\n- %s", avl, *p.Name) } + if model.PlanId.ValueString() == "" { return observability.Plan{}, fmt.Errorf("couldn't find plan_name '%s', available names are: %s", planName, avl) } + return observability.Plan{}, nil } func (r *instanceResource) getAlertConfigs(ctx context.Context, alertConfig *alertConfigModel, model *Model) error { projectId := model.ProjectId.ValueString() instanceId := model.InstanceId.ValueString() + var err error // Alert Config if utils.IsUndefined(model.AlertConfig) { *alertConfig, err = getMockAlertConfig(ctx) if err != nil { - return fmt.Errorf("Getting mock alert config: %w", err) + return fmt.Errorf("getting mock alert config: %w", err) } } alertConfigPayload, err := toUpdateAlertConfigPayload(ctx, alertConfig) if err != nil { - return fmt.Errorf("Building alert config payload: %w", err) + return fmt.Errorf("building alert config payload: %w", err) } if alertConfigPayload != nil { _, err = r.client.UpdateAlertConfigs(ctx, instanceId, projectId).UpdateAlertConfigsPayload(*alertConfigPayload).Execute() if err != nil { - return fmt.Errorf("Setting alert config: %w", err) + return fmt.Errorf("setting alert config: %w", err) } } alertConfigResp, err := r.client.GetAlertConfigs(ctx, instanceId, projectId).Execute() if err != nil { - return fmt.Errorf("Calling API to get alert config: %w", err) + return fmt.Errorf("calling API to get alert config: %w", err) } // Map response body to schema err = mapAlertConfigField(ctx, alertConfigResp, model) if err != nil { - return fmt.Errorf("Processing API response for the alert config: %w", err) + return fmt.Errorf("processing API response for the alert config: %w", err) } + return nil } @@ -2449,27 +2595,29 @@ func (r *instanceResource) getTracesRetention(ctx context.Context, model *Model) if tracesRetentionDays != nil { tracesResp, err := r.client.GetTracesConfigs(ctx, instanceId, projectId).Execute() if err != nil { - return fmt.Errorf("Getting traces retention policy: %w", err) + return fmt.Errorf("getting traces retention policy: %w", err) } + if tracesResp == nil { return fmt.Errorf("nil response") } retentionDays := fmt.Sprintf("%dh", *tracesRetentionDays*24) + _, err = r.client.UpdateTracesConfigs(ctx, instanceId, projectId).UpdateTracesConfigsPayload(observability.UpdateTracesConfigsPayload{Retention: &retentionDays}).Execute() if err != nil { - return fmt.Errorf("Setting traces retention policy: %w", err) + return fmt.Errorf("setting traces retention policy: %w", err) } } tracesResp, err := r.client.GetTracesConfigsExecute(ctx, instanceId, projectId) if err != nil { - return fmt.Errorf("Getting traces retention policy: %w", err) + return fmt.Errorf("getting traces retention policy: %w", err) } err = mapTracesRetentionField(tracesResp, model) if err != nil { - return fmt.Errorf("Processing API response for the traces retention %w", err) + return fmt.Errorf("processing API response for the traces retention %w", err) } return nil @@ -2483,27 +2631,29 @@ func (r *instanceResource) getLogsRetention(ctx context.Context, model *Model) e if logsRetentionDays != nil { logsResp, err := r.client.GetLogsConfigs(ctx, instanceId, projectId).Execute() if err != nil { - return fmt.Errorf("Getting logs retention policy: %w", err) + return fmt.Errorf("getting logs retention policy: %w", err) } + if logsResp == nil { return fmt.Errorf("nil response") } retentionDays := fmt.Sprintf("%dh", *logsRetentionDays*24) + _, err = r.client.UpdateLogsConfigs(ctx, instanceId, projectId).UpdateLogsConfigsPayload(observability.UpdateLogsConfigsPayload{Retention: &retentionDays}).Execute() if err != nil { - return fmt.Errorf("Setting logs retention policy: %w", err) + return fmt.Errorf("setting logs retention policy: %w", err) } } logsResp, err := r.client.GetLogsConfigsExecute(ctx, instanceId, projectId) if err != nil { - return fmt.Errorf("Getting logs retention policy: %w", err) + return fmt.Errorf("getting logs retention policy: %w", err) } err = mapLogsRetentionField(logsResp, model) if err != nil { - return fmt.Errorf("Processing API response for the logs retention %w", err) + return fmt.Errorf("processing API response for the logs retention %w", err) } return nil @@ -2521,30 +2671,32 @@ func (r *instanceResource) getMetricsRetention(ctx context.Context, model *Model // Need to get the metrics retention policy because update endpoint is a PUT and we need to send all fields metricsResp, err := r.client.GetMetricsStorageRetentionExecute(ctx, instanceId, projectId) if err != nil { - return fmt.Errorf("Getting metrics retention policy: %w", err) + return fmt.Errorf("getting metrics retention policy: %w", err) } metricsRetentionPayload, err := toUpdateMetricsStorageRetentionPayload(metricsRetentionDays, metricsRetentionDays5mDownsampling, metricsRetentionDays1hDownsampling, metricsResp) if err != nil { - return fmt.Errorf("Building metrics retention policy payload: %w", err) + return fmt.Errorf("building metrics retention policy payload: %w", err) } + _, err = r.client.UpdateMetricsStorageRetention(ctx, instanceId, projectId).UpdateMetricsStorageRetentionPayload(*metricsRetentionPayload).Execute() if err != nil { - return fmt.Errorf("Setting metrics retention policy: %w", err) + return fmt.Errorf("setting metrics retention policy: %w", err) } } // Get metrics retention policy after update metricsResp, err := r.client.GetMetricsStorageRetentionExecute(ctx, instanceId, projectId) if err != nil { - return fmt.Errorf("Getting metrics retention policy: %w", err) + return fmt.Errorf("getting metrics retention policy: %w", err) } // Map response body to schema err = mapMetricsRetentionField(metricsResp, model) if err != nil { - return fmt.Errorf("Processing API response for the metrics retention %w", err) + return fmt.Errorf("processing API response for the metrics retention %w", err) } + return nil } @@ -2558,6 +2710,7 @@ func setMetricsRetentionsZero(ctx context.Context, state *tfsdk.State) (diags di diags = append(diags, state.SetAttribute(ctx, path.Root("metrics_retention_days"), 0)...) diags = append(diags, state.SetAttribute(ctx, path.Root("metrics_retention_days_5m_downsampling"), 0)...) diags = append(diags, state.SetAttribute(ctx, path.Root("metrics_retention_days_1h_downsampling"), 0)...) + return diags } @@ -2565,6 +2718,7 @@ func setMetricsRetentions(ctx context.Context, state *tfsdk.State, model *Model) diags = append(diags, state.SetAttribute(ctx, path.Root("metrics_retention_days"), model.MetricsRetentionDays)...) diags = append(diags, state.SetAttribute(ctx, path.Root("metrics_retention_days_5m_downsampling"), model.MetricsRetentionDays5mDownsampling)...) diags = append(diags, state.SetAttribute(ctx, path.Root("metrics_retention_days_1h_downsampling"), model.MetricsRetentionDays1hDownsampling)...) + return diags } diff --git a/stackit/internal/services/observability/instance/resource_test.go b/stackit/internal/services/observability/instance/resource_test.go index de1450889..2cf4b8005 100644 --- a/stackit/internal/services/observability/instance/resource_test.go +++ b/stackit/internal/services/observability/instance/resource_test.go @@ -356,6 +356,7 @@ func fixtureRouteAttributeSchema(route *schema.ListNestedAttribute, isDatasource if route != nil { attributeMap["routes"] = *route } + return attributeMap } @@ -633,9 +634,11 @@ func TestMapFields(t *testing.T) { metricsErr := mapMetricsRetentionField(tt.getMetricsRetentionResp, state) logsErr := mapLogsRetentionField(tt.getLogsRetentionResp, state) tracesErr := mapTracesRetentionField(tt.getTracesRetentionResp, state) + if !tt.isValid && err == nil && aclErr == nil && metricsErr == nil && logsErr == nil && tracesErr == nil { t.Fatalf("Should have failed") } + if tt.isValid && (err != nil || aclErr != nil || metricsErr != nil || logsErr != nil || tracesErr != nil) { t.Fatalf("Should not have failed: %v", err) } @@ -982,10 +985,12 @@ func TestMapAlertConfigField(t *testing.T) { ACL: types.SetNull(types.StringType), Parameters: types.MapNull(types.StringType), } + err := mapAlertConfigField(context.Background(), tt.alertConfigResp, state) if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } @@ -1046,9 +1051,11 @@ func TestToCreatePayload(t *testing.T) { if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(output, tt.expected) if diff != "" { @@ -1105,9 +1112,11 @@ func TestToPayloadUpdate(t *testing.T) { if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(output, tt.expected) if diff != "" { @@ -1238,9 +1247,11 @@ func TestToUpdateMetricsStorageRetentionPayload(t *testing.T) { if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(output, tt.expected) if diff != "" { @@ -1404,9 +1415,11 @@ func TestToUpdateAlertConfigPayload(t *testing.T) { if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(output, tt.expected) if diff != "" { @@ -1518,6 +1531,7 @@ func TestGetRouteNestedObjectAux(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { output := getRouteNestedObjectAux(tt.isDatasource, tt.startingLevel, tt.recursionLimit) + diff := cmp.Diff(output, tt.expected) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -1603,6 +1617,7 @@ func TestGetRouteListTypeAux(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { output := getRouteListTypeAux(tt.startingLevel, tt.recursionLimit) + diff := cmp.Diff(output, tt.expected) if diff != "" { t.Fatalf("Data does not match: %s", diff) @@ -1614,23 +1629,28 @@ func TestGetRouteListTypeAux(t *testing.T) { func makeTestMap(t *testing.T) basetypes.MapValue { p := make(map[string]attr.Value, 1) p["key"] = types.StringValue("value") + params, diag := types.MapValueFrom(context.Background(), types.StringType, p) if diag.HasError() { t.Fail() } + return params } -// ToTerraformStringMapMust Silently ignores the error +// ToTerraformStringMapMust Silently ignores the error. func toTerraformStringMapMust(ctx context.Context, m map[string]string) basetypes.MapValue { labels := make(map[string]attr.Value, len(m)) + for l, v := range m { stringValue := types.StringValue(v) labels[l] = stringValue } + res, diags := types.MapValueFrom(ctx, types.StringType, m) if diags.HasError() { return types.MapNull(types.StringType) } + return res } diff --git a/stackit/internal/services/observability/log-alertgroup/datasource.go b/stackit/internal/services/observability/log-alertgroup/datasource.go index 040899bcb..f5bdc58b9 100644 --- a/stackit/internal/services/observability/log-alertgroup/datasource.go +++ b/stackit/internal/services/observability/log-alertgroup/datasource.go @@ -6,9 +6,6 @@ import ( "fmt" "net/http" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - observabilityUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/observability/utils" - "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" @@ -17,7 +14,9 @@ import ( "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/stackitcloud/stackit-sdk-go/core/oapierror" "github.com/stackitcloud/stackit-sdk-go/services/observability" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + observabilityUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/observability/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" ) @@ -44,10 +43,13 @@ func (l *logAlertGroupDataSource) Configure(ctx context.Context, req datasource. } apiClient := observabilityUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + l.client = apiClient + tflog.Info(ctx, "Observability log alert group client configured") } @@ -130,10 +132,11 @@ func (l *logAlertGroupDataSource) Schema(_ context.Context, _ datasource.SchemaR } } -func (l *logAlertGroupDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (l *logAlertGroupDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.Config.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -148,12 +151,15 @@ func (l *logAlertGroupDataSource) Read(ctx context.Context, req datasource.ReadR readAlertGroupResp, err := l.client.GetLogsAlertgroup(ctx, alertGroupName, instanceId, projectId).Execute() if err != nil { var oapiErr *oapierror.GenericOpenAPIError + ok := errors.As(err, &oapiErr) if ok && oapiErr.StatusCode == http.StatusNotFound { resp.State.RemoveResource(ctx) return } + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading log alert group", fmt.Sprintf("Calling API: %v", err)) + return } diff --git a/stackit/internal/services/observability/log-alertgroup/resource.go b/stackit/internal/services/observability/log-alertgroup/resource.go index c8427718f..821dca579 100644 --- a/stackit/internal/services/observability/log-alertgroup/resource.go +++ b/stackit/internal/services/observability/log-alertgroup/resource.go @@ -8,8 +8,6 @@ import ( "regexp" "strings" - observabilityUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/observability/utils" - "github.com/hashicorp/terraform-plugin-framework-validators/mapvalidator" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/attr" @@ -27,6 +25,7 @@ import ( "github.com/stackitcloud/stackit-sdk-go/services/observability" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + observabilityUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/observability/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" ) @@ -100,10 +99,13 @@ func (l *logAlertGroupResource) Configure(ctx context.Context, req resource.Conf } apiClient := observabilityUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + l.client = apiClient + tflog.Info(ctx, "Observability log alert group client configured") } @@ -230,11 +232,12 @@ func (l *logAlertGroupResource) Schema(_ context.Context, _ resource.SchemaReque } // Create creates the resource and sets the initial Terraform state. -func (l *logAlertGroupResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform +func (l *logAlertGroupResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { //nolint:gocritic // function signature required by Terraform // Retrieve values from plan var model Model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -274,17 +277,20 @@ func (l *logAlertGroupResource) Create(ctx context.Context, req resource.CreateR // Set the state with fully populated data. diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "log alert group created") } // Read refreshes the Terraform state with the latest data. -func (l *logAlertGroupResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (l *logAlertGroupResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -299,12 +305,15 @@ func (l *logAlertGroupResource) Read(ctx context.Context, req resource.ReadReque readAlertGroupResp, err := l.client.GetLogsAlertgroup(ctx, alertGroupName, instanceId, projectId).Execute() if err != nil { var oapiErr *oapierror.GenericOpenAPIError + ok := errors.As(err, &oapiErr) if ok && oapiErr.StatusCode == http.StatusNotFound { resp.State.RemoveResource(ctx) return } + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading log alert group", fmt.Sprintf("Calling API: %v", err)) + return } @@ -323,16 +332,17 @@ func (l *logAlertGroupResource) Read(ctx context.Context, req resource.ReadReque // The Update function is redundant since any modifications will // automatically trigger a resource recreation through Terraform's built-in // lifecycle management. -func (l *logAlertGroupResource) Update(ctx context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform +func (l *logAlertGroupResource) Update(ctx context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { //nolint:gocritic // function signature required by Terraform core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating log alert group", "Observability log alert groups can't be updated") } // Delete deletes the resource and removes the Terraform state on success. -func (l *logAlertGroupResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform +func (l *logAlertGroupResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { //nolint:gocritic // function signature required by Terraform // Retrieve values from state var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -354,7 +364,7 @@ func (l *logAlertGroupResource) Delete(ctx context.Context, req resource.DeleteR } // ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: project_id,instance_id,name +// The expected format of the resource import identifier is: project_id,instance_id,name. func (l *logAlertGroupResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { idParts := strings.Split(req.ID, core.Separator) @@ -363,6 +373,7 @@ func (l *logAlertGroupResource) ImportState(ctx context.Context, req resource.Im "Error importing scrape config", fmt.Sprintf("Expected import identifier with format: [project_id],[instance_id],[name] Got: %q", req.ID), ) + return } @@ -393,6 +404,7 @@ func toCreatePayload(ctx context.Context, model *Model) (*observability.CreateLo if err != nil { return nil, err } + payload.Rules = &rules } @@ -406,12 +418,14 @@ func toRulesPayload(ctx context.Context, model *Model) ([]observability.UpdateAl } var rules []rule + diags := model.Rules.ElementsAs(ctx, &rules, false) if diags.HasError() { return nil, core.DiagsToError(diags) } var oarrs []observability.UpdateAlertgroupsRequestInnerRulesInner + for i := range rules { rule := &rules[i] oarr := observability.UpdateAlertgroupsRequestInnerRulesInner{} @@ -421,6 +435,7 @@ func toRulesPayload(ctx context.Context, model *Model) ([]observability.UpdateAl if alert == nil { return nil, fmt.Errorf("found nil alert for rule[%d]", i) } + oarr.Alert = alert } @@ -429,6 +444,7 @@ func toRulesPayload(ctx context.Context, model *Model) ([]observability.UpdateAl if expression == nil { return nil, fmt.Errorf("found nil expression for rule[%d]", i) } + oarr.Expr = expression } @@ -437,6 +453,7 @@ func toRulesPayload(ctx context.Context, model *Model) ([]observability.UpdateAl if for_ == nil { return nil, fmt.Errorf("found nil expression for for_[%d]", i) } + oarr.For = for_ } @@ -445,6 +462,7 @@ func toRulesPayload(ctx context.Context, model *Model) ([]observability.UpdateAl if err != nil { return nil, fmt.Errorf("converting to Go map: %w", err) } + oarr.Labels = &labels } @@ -453,6 +471,7 @@ func toRulesPayload(ctx context.Context, model *Model) ([]observability.UpdateAl if err != nil { return nil, fmt.Errorf("converting to Go map: %w", err) } + oarr.Annotations = &annotations } @@ -504,6 +523,7 @@ func mapFields(ctx context.Context, alertGroup *observability.AlertGroup, model } else { return fmt.Errorf("found empty interval") } + model.Interval = types.StringValue(interval) if alertGroup.Rules != nil { @@ -534,6 +554,7 @@ func mapRules(_ context.Context, alertGroup *observability.AlertGroup, model *Mo for k, v := range *r.Labels { labelElems[k] = types.StringValue(v) } + ruleMap["labels"] = types.MapValueMust(types.StringType, labelElems) } @@ -542,6 +563,7 @@ func mapRules(_ context.Context, alertGroup *observability.AlertGroup, model *Mo for k, v := range *r.Annotations { annoElems[k] = types.StringValue(v) } + ruleMap["annotations"] = types.MapValueMust(types.StringType, annoElems) } @@ -549,6 +571,7 @@ func mapRules(_ context.Context, alertGroup *observability.AlertGroup, model *Mo if diags.HasError() { return fmt.Errorf("mapping index %d: %w", i, core.DiagsToError(diags)) } + newRules = append(newRules, ruleTf) } @@ -558,5 +581,6 @@ func mapRules(_ context.Context, alertGroup *observability.AlertGroup, model *Mo } model.Rules = rulesTf + return nil } diff --git a/stackit/internal/services/observability/log-alertgroup/resource_test.go b/stackit/internal/services/observability/log-alertgroup/resource_test.go index 4f3bd60bb..09f98c50f 100644 --- a/stackit/internal/services/observability/log-alertgroup/resource_test.go +++ b/stackit/internal/services/observability/log-alertgroup/resource_test.go @@ -311,6 +311,7 @@ func TestMapFields(t *testing.T) { if diff := cmp.Diff(tt.model.Name.ValueString(), tt.expectedName); diff != "" { t.Errorf("unexpected name (-got +want):\n%s", diff) } + if diff := cmp.Diff(tt.model.Id.ValueString(), tt.expectedID); diff != "" { t.Errorf("unexpected ID (-got +want):\n%s", diff) } diff --git a/stackit/internal/services/observability/observability_acc_test.go b/stackit/internal/services/observability/observability_acc_test.go index 9e5393f74..6c92ca68e 100644 --- a/stackit/internal/services/observability/observability_acc_test.go +++ b/stackit/internal/services/observability/observability_acc_test.go @@ -9,17 +9,15 @@ import ( "testing" "github.com/hashicorp/terraform-plugin-testing/config" - "github.com/hashicorp/terraform-plugin-testing/helper/resource" - "github.com/hashicorp/terraform-plugin-testing/helper/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/terraform" + stackitSdkConfig "github.com/stackitcloud/stackit-sdk-go/core/config" "github.com/stackitcloud/stackit-sdk-go/core/utils" "github.com/stackitcloud/stackit-sdk-go/services/observability" "github.com/stackitcloud/stackit-sdk-go/services/observability/wait" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/testutil" - - stackitSdkConfig "github.com/stackitcloud/stackit-sdk-go/core/config" ) //go:embed testdata/resource-min.tf @@ -28,11 +26,13 @@ var resourceMinConfig string //go:embed testdata/resource-max.tf var resourceMaxConfig string -// To prevent conversion issues -var alert_rule_expression = "sum(kube_pod_status_phase{phase=\"Running\"}) > 0" -var logalertgroup_expression = "sum(rate({namespace=\"example\"} |= \"Simulated error message\" [1m])) > 0" -var alert_rule_expression_updated = "sum(kube_pod_status_phase{phase=\"Error\"}) > 0" -var logalertgroup_expression_updated = "sum(rate({namespace=\"example\"} |= \"Another error message\" [1m])) > 0" +// To prevent conversion issues. +var ( + alert_rule_expression = "sum(kube_pod_status_phase{phase=\"Running\"}) > 0" + logalertgroup_expression = "sum(rate({namespace=\"example\"} |= \"Simulated error message\" [1m])) > 0" + alert_rule_expression_updated = "sum(kube_pod_status_phase{phase=\"Error\"}) > 0" + logalertgroup_expression_updated = "sum(rate({namespace=\"example\"} |= \"Another error message\" [1m])) > 0" +) var testConfigVarsMin = config.Variables{ "project_id": config.StringVariable(testutil.ProjectId), @@ -129,6 +129,7 @@ func configVarsMinUpdated() config.Variables { tempConfig := make(config.Variables, len(testConfigVarsMin)) maps.Copy(tempConfig, testConfigVarsMin) tempConfig["alert_rule_name"] = config.StringVariable("alert1-updated") + return tempConfig } @@ -144,6 +145,7 @@ func configVarsMaxUpdated() config.Variables { tempConfig["ms_teams"] = config.StringVariable("false") tempConfig["google_chat"] = config.StringVariable("true") tempConfig["matchers"] = config.StringVariable("instance =~ \"my.*\"") + return tempConfig } @@ -346,6 +348,7 @@ func TestAccResourceMin(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute name") } + return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, instanceId, name), nil }, ImportState: true, @@ -368,6 +371,7 @@ func TestAccResourceMin(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute name") } + return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, instanceId, name), nil }, ImportState: true, @@ -390,6 +394,7 @@ func TestAccResourceMin(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute name") } + return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, instanceId, name), nil }, ImportState: true, @@ -822,6 +827,7 @@ func TestAccResourceMax(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute name") } + return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, instanceId, name), nil }, ImportState: true, @@ -845,6 +851,7 @@ func TestAccResourceMax(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute name") } + return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, instanceId, name), nil }, ImportState: true, @@ -868,6 +875,7 @@ func TestAccResourceMax(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute name") } + return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, instanceId, name), nil }, ImportState: true, @@ -1024,7 +1032,9 @@ func TestAccResourceMax(t *testing.T) { func testAccCheckObservabilityDestroy(s *terraform.State) error { ctx := context.Background() + var client *observability.APIClient + var err error if testutil.ObservabilityCustomEndpoint == "" { client, err = observability.NewAPIClient( @@ -1035,11 +1045,13 @@ func testAccCheckObservabilityDestroy(s *terraform.State) error { stackitSdkConfig.WithEndpoint(testutil.ObservabilityCustomEndpoint), ) } + if err != nil { return fmt.Errorf("creating client: %w", err) } instancesToDestroy := []string{} + for _, rs := range s.RootModule().Resources { if rs.Type != "stackit_observability_instance" { continue @@ -1062,6 +1074,7 @@ func testAccCheckObservabilityDestroy(s *terraform.State) error { if err != nil { return fmt.Errorf("destroying instance %s during CheckDestroy: %w", *instances[i].Id, err) } + _, err = wait.DeleteInstanceWaitHandler(ctx, client, testutil.ProjectId, *instances[i].Id).WaitWithContext(ctx) if err != nil { return fmt.Errorf("destroying instance %s during CheckDestroy: waiting for deletion %w", *instances[i].Id, err) @@ -1069,5 +1082,6 @@ func testAccCheckObservabilityDestroy(s *terraform.State) error { } } } + return nil } diff --git a/stackit/internal/services/observability/scrapeconfig/datasource.go b/stackit/internal/services/observability/scrapeconfig/datasource.go index 552d320b3..770bfb07f 100644 --- a/stackit/internal/services/observability/scrapeconfig/datasource.go +++ b/stackit/internal/services/observability/scrapeconfig/datasource.go @@ -5,9 +5,6 @@ import ( "fmt" "net/http" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - observabilityUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/observability/utils" - "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" "github.com/hashicorp/terraform-plugin-framework-validators/mapvalidator" @@ -18,7 +15,9 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/stackitcloud/stackit-sdk-go/services/observability" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + observabilityUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/observability/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" ) @@ -50,9 +49,11 @@ func (d *scrapeConfigDataSource) Configure(ctx context.Context, req datasource.C } apiClient := observabilityUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + d.client = apiClient } @@ -183,13 +184,15 @@ func (d *scrapeConfigDataSource) Schema(_ context.Context, _ datasource.SchemaRe } // Read refreshes the Terraform state with the latest data. -func (d *scrapeConfigDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (d *scrapeConfigDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.Config.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() instanceId := model.InstanceId.ValueString() scName := model.Name.ValueString() @@ -207,6 +210,7 @@ func (d *scrapeConfigDataSource) Read(ctx context.Context, req datasource.ReadRe }, ) resp.State.RemoveResource(ctx) + return } @@ -215,10 +219,13 @@ func (d *scrapeConfigDataSource) Read(ctx context.Context, req datasource.ReadRe core.LogAndAddError(ctx, &resp.Diagnostics, "Mapping fields", err.Error()) return } + diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "Observability scrape config read") } diff --git a/stackit/internal/services/observability/scrapeconfig/resource.go b/stackit/internal/services/observability/scrapeconfig/resource.go index c8c3629ea..c0dd02bc4 100644 --- a/stackit/internal/services/observability/scrapeconfig/resource.go +++ b/stackit/internal/services/observability/scrapeconfig/resource.go @@ -7,10 +7,6 @@ import ( "strings" "time" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - - observabilityUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/observability/utils" - "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" "github.com/hashicorp/terraform-plugin-framework-validators/mapvalidator" @@ -35,6 +31,8 @@ import ( "github.com/stackitcloud/stackit-sdk-go/services/observability/wait" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + observabilityUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/observability/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" ) @@ -68,35 +66,35 @@ type Model struct { Targets types.List `tfsdk:"targets"` } -// Struct corresponding to Model.SAML2 +// Struct corresponding to Model.SAML2. type saml2Model struct { EnableURLParameters types.Bool `tfsdk:"enable_url_parameters"` } -// Types corresponding to saml2Model +// Types corresponding to saml2Model. var saml2Types = map[string]attr.Type{ "enable_url_parameters": types.BoolType, } -// Struct corresponding to Model.BasicAuth +// Struct corresponding to Model.BasicAuth. type basicAuthModel struct { Username types.String `tfsdk:"username"` Password types.String `tfsdk:"password"` } -// Types corresponding to basicAuthModel +// Types corresponding to basicAuthModel. var basicAuthTypes = map[string]attr.Type{ "username": types.StringType, "password": types.StringType, } -// Struct corresponding to Model.Targets[i] +// Struct corresponding to Model.Targets[i]. type targetModel struct { URLs types.List `tfsdk:"urls"` Labels types.Map `tfsdk:"labels"` } -// Types corresponding to targetModel +// Types corresponding to targetModel. var targetTypes = map[string]attr.Type{ "urls": types.ListType{ElemType: types.StringType}, "labels": types.MapType{ElemType: types.StringType}, @@ -125,10 +123,13 @@ func (r *scrapeConfigResource) Configure(ctx context.Context, req resource.Confi } apiClient := observabilityUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + r.client = apiClient + tflog.Info(ctx, "Observability scrape config client configured") } @@ -296,11 +297,12 @@ func (r *scrapeConfigResource) Schema(_ context.Context, _ resource.SchemaReques } // Create creates the resource and sets the initial Terraform state. -func (r *scrapeConfigResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *scrapeConfigResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { //nolint:gocritic // function signature required by Terraform // Retrieve values from plan var model Model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -313,6 +315,7 @@ func (r *scrapeConfigResource) Create(ctx context.Context, req resource.CreateRe if !model.SAML2.IsNull() && !model.SAML2.IsUnknown() { diags = model.SAML2.As(ctx, &saml2Model, basetypes.ObjectAsOptions{}) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -322,6 +325,7 @@ func (r *scrapeConfigResource) Create(ctx context.Context, req resource.CreateRe if !model.BasicAuth.IsNull() && !model.BasicAuth.IsUnknown() { diags = model.BasicAuth.As(ctx, &basicAuthModel, basetypes.ObjectAsOptions{}) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -331,6 +335,7 @@ func (r *scrapeConfigResource) Create(ctx context.Context, req resource.CreateRe if !model.Targets.IsNull() && !model.Targets.IsUnknown() { diags = model.Targets.ElementsAs(ctx, &targetsModel, false) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -342,21 +347,25 @@ func (r *scrapeConfigResource) Create(ctx context.Context, req resource.CreateRe core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating scrape config", fmt.Sprintf("Creating API payload: %v", err)) return } + _, err = r.client.CreateScrapeConfig(ctx, instanceId, projectId).CreateScrapeConfigPayload(*payload).Execute() if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating scrape config", fmt.Sprintf("Calling API: %v", err)) return } + _, err = wait.CreateScrapeConfigWaitHandler(ctx, r.client, instanceId, scName, projectId).WaitWithContext(ctx) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating scrape config", fmt.Sprintf("Scrape config creation waiting: %v", err)) return } + got, err := r.client.GetScrapeConfig(ctx, instanceId, scName, projectId).Execute() if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating scrape config", fmt.Sprintf("Calling API for updated data: %v", err)) return } + err = mapFields(ctx, got.Data, &model) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating scrape config", fmt.Sprintf("Processing API payload: %v", err)) @@ -365,20 +374,24 @@ func (r *scrapeConfigResource) Create(ctx context.Context, req resource.CreateRe // Set state to fully populated data diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "Observability scrape config created") } // Read refreshes the Terraform state with the latest data. -func (r *scrapeConfigResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (r *scrapeConfigResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() instanceId := model.InstanceId.ValueString() scName := model.Name.ValueString() @@ -390,7 +403,9 @@ func (r *scrapeConfigResource) Read(ctx context.Context, req resource.ReadReques resp.State.RemoveResource(ctx) return } + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading scrape config", fmt.Sprintf("Calling API: %v", err)) + return } @@ -403,21 +418,25 @@ func (r *scrapeConfigResource) Read(ctx context.Context, req resource.ReadReques // Set refreshed model diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "Observability scrape config read") } // Update updates the resource and sets the updated Terraform state on success. -func (r *scrapeConfigResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *scrapeConfigResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { //nolint:gocritic // function signature required by Terraform // Retrieve values from plan var model Model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() instanceId := model.InstanceId.ValueString() scName := model.Name.ValueString() @@ -426,6 +445,7 @@ func (r *scrapeConfigResource) Update(ctx context.Context, req resource.UpdateRe if !model.SAML2.IsNull() && !model.SAML2.IsUnknown() { diags = model.SAML2.As(ctx, &saml2Model, basetypes.ObjectAsOptions{}) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -435,6 +455,7 @@ func (r *scrapeConfigResource) Update(ctx context.Context, req resource.UpdateRe if !model.BasicAuth.IsNull() && !model.BasicAuth.IsUnknown() { diags = model.BasicAuth.As(ctx, &basicAuthModel, basetypes.ObjectAsOptions{}) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -444,6 +465,7 @@ func (r *scrapeConfigResource) Update(ctx context.Context, req resource.UpdateRe if !model.Targets.IsNull() && !model.Targets.IsUnknown() { diags = model.Targets.ElementsAs(ctx, &targetsModel, false) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -455,6 +477,7 @@ func (r *scrapeConfigResource) Update(ctx context.Context, req resource.UpdateRe core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating scrape config", fmt.Sprintf("Creating API payload: %v", err)) return } + _, err = r.client.UpdateScrapeConfig(ctx, instanceId, scName, projectId).UpdateScrapeConfigPayload(*payload).Execute() if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating scrape config", fmt.Sprintf("Calling API: %v", err)) @@ -469,25 +492,30 @@ func (r *scrapeConfigResource) Update(ctx context.Context, req resource.UpdateRe core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating scrape config", fmt.Sprintf("Calling API for updated data: %v", err)) return } + err = mapFields(ctx, scResp.Data, &model) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating scrape config", fmt.Sprintf("Processing API payload: %v", err)) return } + diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "Observability scrape config updated") } // Delete deletes the resource and removes the Terraform state on success. -func (r *scrapeConfigResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform +func (r *scrapeConfigResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { //nolint:gocritic // function signature required by Terraform // Retrieve values from state var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -502,6 +530,7 @@ func (r *scrapeConfigResource) Delete(ctx context.Context, req resource.DeleteRe core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting scrape config", fmt.Sprintf("Calling API: %v", err)) return } + _, err = wait.DeleteScrapeConfigWaitHandler(ctx, r.client, instanceId, scName, projectId).WaitWithContext(ctx) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting scrape config", fmt.Sprintf("Scrape config deletion waiting: %v", err)) @@ -512,7 +541,7 @@ func (r *scrapeConfigResource) Delete(ctx context.Context, req resource.DeleteRe } // ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: project_id,instance_id,name +// The expected format of the resource import identifier is: project_id,instance_id,name. func (r *scrapeConfigResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { idParts := strings.Split(req.ID, core.Separator) @@ -521,6 +550,7 @@ func (r *scrapeConfigResource) ImportState(ctx context.Context, req resource.Imp "Error importing scrape config", fmt.Sprintf("Expected import identifier with format: [project_id],[instance_id],[name] Got: %q", req.ID), ) + return } @@ -534,6 +564,7 @@ func mapFields(ctx context.Context, sc *observability.Job, model *Model) error { if sc == nil { return fmt.Errorf("response input is nil") } + if model == nil { return fmt.Errorf("model input is nil") } @@ -554,18 +585,22 @@ func mapFields(ctx context.Context, sc *observability.Job, model *Model) error { model.ScrapeInterval = types.StringPointerValue(sc.ScrapeInterval) model.ScrapeTimeout = types.StringPointerValue(sc.ScrapeTimeout) model.SampleLimit = types.Int64PointerValue(sc.SampleLimit) + err := mapSAML2(sc, model) if err != nil { return fmt.Errorf("map saml2: %w", err) } + err = mapBasicAuth(sc, model) if err != nil { return fmt.Errorf("map basic auth: %w", err) } + err = mapTargets(ctx, sc, model) if err != nil { return fmt.Errorf("map targets: %w", err) } + return nil } @@ -574,15 +609,19 @@ func mapBasicAuth(sc *observability.Job, model *Model) error { model.BasicAuth = types.ObjectNull(basicAuthTypes) return nil } + basicAuthMap := map[string]attr.Value{ "username": types.StringValue(*sc.BasicAuth.Username), "password": types.StringValue(*sc.BasicAuth.Password), } + basicAuthTF, diags := types.ObjectValue(basicAuthTypes, basicAuthMap) if diags.HasError() { return core.DiagsToError(diags) } + model.BasicAuth = basicAuthTF + return nil } @@ -596,9 +635,11 @@ func mapSAML2(sc *observability.Job, model *Model) error { } flag := true + if sc.Params == nil || *sc.Params == nil { return nil } + p := *sc.Params if v, ok := p["saml2"]; ok { if len(v) == 1 && v[0] == "disabled" { @@ -609,11 +650,14 @@ func mapSAML2(sc *observability.Job, model *Model) error { saml2Map := map[string]attr.Value{ "enable_url_parameters": types.BoolValue(flag), } + saml2TF, diags := types.ObjectValue(saml2Types, saml2Map) if diags.HasError() { return core.DiagsToError(diags) } + model.SAML2 = saml2TF + return nil } @@ -632,16 +676,19 @@ func mapTargets(ctx context.Context, sc *observability.Job, model *Model) error } newTargets := []attr.Value{} + for i, sc := range *sc.StaticConfigs { nt := targetModel{} // Map URLs urls := []attr.Value{} + if sc.Targets != nil { for _, v := range *sc.Targets { urls = append(urls, types.StringValue(v)) } } + nt.URLs = types.ListValueMust(types.StringType, urls) // Map Labels @@ -652,6 +699,7 @@ func mapTargets(ctx context.Context, sc *observability.Job, model *Model) error for k, v := range *sc.Labels { newl[k] = types.StringValue(v) } + nt.Labels = types.MapValueMust(types.StringType, newl) } @@ -660,6 +708,7 @@ func mapTargets(ctx context.Context, sc *observability.Job, model *Model) error "urls": nt.URLs, "labels": nt.Labels, } + targetTF, diags := types.ObjectValue(targetTypes, targetMap) if diags.HasError() { return core.DiagsToError(diags) @@ -674,6 +723,7 @@ func mapTargets(ctx context.Context, sc *observability.Job, model *Model) error } model.Targets = targetsTF + return nil } @@ -698,11 +748,13 @@ func toCreatePayload(ctx context.Context, model *Model, saml2Model *saml2Model, if sc.Params != nil { m = *sc.Params } + if saml2Model.EnableURLParameters.ValueBool() { m["saml2"] = []string{"enabled"} } else { m["saml2"] = []string{"disabled"} } + sc.Params = &m } @@ -714,23 +766,28 @@ func toCreatePayload(ctx context.Context, model *Model, saml2Model *saml2Model, } t := make([]observability.CreateScrapeConfigPayloadStaticConfigsInner, len(targetsModel)) + for i, target := range targetsModel { ti := observability.CreateScrapeConfigPayloadStaticConfigsInner{} urls := []string{} + diags := target.URLs.ElementsAs(ctx, &urls, false) if diags.HasError() { return nil, core.DiagsToError(diags) } + ti.Targets = &urls labels := map[string]interface{}{} for k, v := range target.Labels.Elements() { labels[k], _ = conversion.ToString(ctx, v) } + ti.Labels = &labels t[i] = ti } + sc.StaticConfigs = &t return &sc, nil @@ -740,15 +797,19 @@ func setDefaultsCreateScrapeConfig(sc *observability.CreateScrapeConfigPayload, if sc == nil { return } + if model.Scheme.IsNull() || model.Scheme.IsUnknown() { sc.Scheme = DefaultScheme.Ptr() } + if model.ScrapeInterval.IsNull() || model.ScrapeInterval.IsUnknown() { sc.ScrapeInterval = sdkUtils.Ptr(DefaultScrapeInterval) } + if model.ScrapeTimeout.IsNull() || model.ScrapeTimeout.IsUnknown() { sc.ScrapeTimeout = sdkUtils.Ptr(DefaultScrapeTimeout) } + if model.SampleLimit.IsNull() || model.SampleLimit.IsUnknown() { sc.SampleLimit = sdkUtils.Ptr(float64(DefaultSampleLimit)) } @@ -758,11 +819,13 @@ func setDefaultsCreateScrapeConfig(sc *observability.CreateScrapeConfigPayload, if sc.Params != nil { m = *sc.Params } + if DefaultSAML2EnableURLParameters { m["saml2"] = []string{"enabled"} } else { m["saml2"] = []string{"disabled"} } + sc.Params = &m } } @@ -787,11 +850,13 @@ func toUpdatePayload(ctx context.Context, model *Model, saml2Model *saml2Model, if sc.Params != nil { m = *sc.Params } + if saml2Model.EnableURLParameters.ValueBool() { m["saml2"] = []string{"enabled"} } else { m["saml2"] = []string{"disabled"} } + sc.Params = &m } @@ -803,23 +868,28 @@ func toUpdatePayload(ctx context.Context, model *Model, saml2Model *saml2Model, } t := make([]observability.UpdateScrapeConfigPayloadStaticConfigsInner, len(targetsModel)) + for i, target := range targetsModel { ti := observability.UpdateScrapeConfigPayloadStaticConfigsInner{} urls := []string{} + diags := target.URLs.ElementsAs(ctx, &urls, false) if diags.HasError() { return nil, core.DiagsToError(diags) } + ti.Targets = &urls ls := map[string]interface{}{} for k, v := range target.Labels.Elements() { ls[k], _ = conversion.ToString(ctx, v) } + ti.Labels = &ls t[i] = ti } + sc.StaticConfigs = &t return &sc, nil @@ -829,15 +899,19 @@ func setDefaultsUpdateScrapeConfig(sc *observability.UpdateScrapeConfigPayload, if sc == nil { return } + if model.Scheme.IsNull() || model.Scheme.IsUnknown() { sc.Scheme = observability.UpdateScrapeConfigPayloadGetSchemeAttributeType(DefaultScheme.Ptr()) } + if model.ScrapeInterval.IsNull() || model.ScrapeInterval.IsUnknown() { sc.ScrapeInterval = sdkUtils.Ptr(DefaultScrapeInterval) } + if model.ScrapeTimeout.IsNull() || model.ScrapeTimeout.IsUnknown() { sc.ScrapeTimeout = sdkUtils.Ptr(DefaultScrapeTimeout) } + if model.SampleLimit.IsNull() || model.SampleLimit.IsUnknown() { sc.SampleLimit = sdkUtils.Ptr(float64(DefaultSampleLimit)) } diff --git a/stackit/internal/services/observability/scrapeconfig/resource_test.go b/stackit/internal/services/observability/scrapeconfig/resource_test.go index 3ef068c18..25f980588 100644 --- a/stackit/internal/services/observability/scrapeconfig/resource_test.go +++ b/stackit/internal/services/observability/scrapeconfig/resource_test.go @@ -125,13 +125,16 @@ func TestMapFields(t *testing.T) { ProjectId: tt.expected.ProjectId, InstanceId: tt.expected.InstanceId, } + err := mapFields(context.Background(), tt.input, state) if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(state, &tt.expected) if diff != "" { @@ -313,9 +316,11 @@ func TestToCreatePayload(t *testing.T) { if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(output, tt.expected) if diff != "" { @@ -490,9 +495,11 @@ func TestToUpdatePayload(t *testing.T) { if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(output, tt.expected) if diff != "" { diff --git a/stackit/internal/services/observability/utils/util.go b/stackit/internal/services/observability/utils/util.go index 3e549ce70..8dde95867 100644 --- a/stackit/internal/services/observability/utils/util.go +++ b/stackit/internal/services/observability/utils/util.go @@ -4,10 +4,9 @@ import ( "context" "fmt" - "github.com/stackitcloud/stackit-sdk-go/services/observability" - "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/stackitcloud/stackit-sdk-go/core/config" + "github.com/stackitcloud/stackit-sdk-go/services/observability" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" ) @@ -22,6 +21,7 @@ func ConfigureClient(ctx context.Context, providerData *core.ProviderData, diags } else { apiClientConfigOptions = append(apiClientConfigOptions, config.WithRegion(providerData.GetRegion())) } + apiClient, err := observability.NewAPIClient(apiClientConfigOptions...) if err != nil { core.LogAndAddError(ctx, diags, "Error configuring API client", fmt.Sprintf("Configuring client: %v. This is an error related to the provider configuration, not to the resource configuration", err)) diff --git a/stackit/internal/services/observability/utils/util_test.go b/stackit/internal/services/observability/utils/util_test.go index 88386fb88..9bf88ff92 100644 --- a/stackit/internal/services/observability/utils/util_test.go +++ b/stackit/internal/services/observability/utils/util_test.go @@ -22,6 +22,7 @@ const ( func TestConfigureClient(t *testing.T) { /* mock authentication by setting service account token env variable */ os.Clearenv() + err := os.Setenv(sdkClients.ServiceAccountToken, "mock-val") if err != nil { t.Errorf("error setting env variable: %v", err) @@ -30,6 +31,7 @@ func TestConfigureClient(t *testing.T) { type args struct { providerData *core.ProviderData } + tests := []struct { name string args args @@ -51,6 +53,7 @@ func TestConfigureClient(t *testing.T) { if err != nil { t.Errorf("error configuring client: %v", err) } + return apiClient }(), wantErr: false, @@ -71,6 +74,7 @@ func TestConfigureClient(t *testing.T) { if err != nil { t.Errorf("error configuring client: %v", err) } + return apiClient }(), wantErr: false, @@ -82,6 +86,7 @@ func TestConfigureClient(t *testing.T) { diags := diag.Diagnostics{} actual := ConfigureClient(ctx, tt.args.providerData, &diags) + if diags.HasError() != tt.wantErr { t.Errorf("ConfigureClient() error = %v, want %v", diags.HasError(), tt.wantErr) } diff --git a/stackit/internal/services/opensearch/credential/datasource.go b/stackit/internal/services/opensearch/credential/datasource.go index c38dbd538..92daeca43 100644 --- a/stackit/internal/services/opensearch/credential/datasource.go +++ b/stackit/internal/services/opensearch/credential/datasource.go @@ -5,19 +5,17 @@ import ( "fmt" "net/http" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - opensearchUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/opensearch/utils" - "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/stackitcloud/stackit-sdk-go/services/opensearch" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + opensearchUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/opensearch/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - - "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/services/opensearch" ) // Ensure the implementation satisfies the expected interfaces. @@ -48,10 +46,13 @@ func (r *credentialDataSource) Configure(ctx context.Context, req datasource.Con } apiClient := opensearchUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + r.client = apiClient + tflog.Info(ctx, "OpenSearch credential client configured") } @@ -125,13 +126,15 @@ func (r *credentialDataSource) Schema(_ context.Context, _ datasource.SchemaRequ } // Read refreshes the Terraform state with the latest data. -func (r *credentialDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (r *credentialDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.Config.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() instanceId := model.InstanceId.ValueString() credentialId := model.CredentialId.ValueString() @@ -152,6 +155,7 @@ func (r *credentialDataSource) Read(ctx context.Context, req datasource.ReadRequ }, ) resp.State.RemoveResource(ctx) + return } @@ -165,8 +169,10 @@ func (r *credentialDataSource) Read(ctx context.Context, req datasource.ReadRequ // Set refreshed state diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "OpenSearch credential read") } diff --git a/stackit/internal/services/opensearch/credential/resource.go b/stackit/internal/services/opensearch/credential/resource.go index 5c4d27832..654e1b5f7 100644 --- a/stackit/internal/services/opensearch/credential/resource.go +++ b/stackit/internal/services/opensearch/credential/resource.go @@ -6,24 +6,22 @@ import ( "net/http" "strings" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - opensearchUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/opensearch/utils" - - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/stackitcloud/stackit-sdk-go/core/oapierror" "github.com/stackitcloud/stackit-sdk-go/services/opensearch" "github.com/stackitcloud/stackit-sdk-go/services/opensearch/wait" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + opensearchUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/opensearch/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" ) // Ensure the implementation satisfies the expected interfaces. @@ -70,10 +68,13 @@ func (r *credentialResource) Configure(ctx context.Context, req resource.Configu } apiClient := opensearchUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + r.client = apiClient + tflog.Info(ctx, "OpenSearch credential client configured") } @@ -161,13 +162,15 @@ func (r *credentialResource) Schema(_ context.Context, _ resource.SchemaRequest, } // Create creates the resource and sets the initial Terraform state. -func (r *credentialResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *credentialResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() instanceId := model.InstanceId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) @@ -179,10 +182,12 @@ func (r *credentialResource) Create(ctx context.Context, req resource.CreateRequ core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credential", fmt.Sprintf("Calling API: %v", err)) return } + if credentialsResp.Id == nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credential", "Got empty credential id") return } + credentialId := *credentialsResp.Id ctx = tflog.SetField(ctx, "credential_id", credentialId) @@ -198,22 +203,27 @@ func (r *credentialResource) Create(ctx context.Context, req resource.CreateRequ core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credential", fmt.Sprintf("Processing API payload: %v", err)) return } + diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "OpenSearch credential created") } // Read refreshes the Terraform state with the latest data. -func (r *credentialResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (r *credentialResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() instanceId := model.InstanceId.ValueString() credentialId := model.CredentialId.ValueString() @@ -228,7 +238,9 @@ func (r *credentialResource) Read(ctx context.Context, req resource.ReadRequest, resp.State.RemoveResource(ctx) return } + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading credential", fmt.Sprintf("Calling API: %v", err)) + return } @@ -242,23 +254,26 @@ func (r *credentialResource) Read(ctx context.Context, req resource.ReadRequest, // Set refreshed state diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "OpenSearch credential read") } // Update updates the resource and sets the updated Terraform state on success. -func (r *credentialResource) Update(ctx context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *credentialResource) Update(ctx context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { //nolint:gocritic // function signature required by Terraform // Update shouldn't be called core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating credential", "Credential can't be updated") } // Delete deletes the resource and removes the Terraform state on success. -func (r *credentialResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform +func (r *credentialResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -275,16 +290,18 @@ func (r *credentialResource) Delete(ctx context.Context, req resource.DeleteRequ if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting credential", fmt.Sprintf("Calling API: %v", err)) } + _, err = wait.DeleteCredentialsWaitHandler(ctx, r.client, projectId, instanceId, credentialId).WaitWithContext(ctx) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting credential", fmt.Sprintf("Instance deletion waiting: %v", err)) return } + tflog.Info(ctx, "OpenSearch credential deleted") } // ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: project_id,instance_id,credential_id +// The expected format of the resource import identifier is: project_id,instance_id,credential_id. func (r *credentialResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { idParts := strings.Split(req.ID, core.Separator) if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" { @@ -292,6 +309,7 @@ func (r *credentialResource) ImportState(ctx context.Context, req resource.Impor "Error importing credential", fmt.Sprintf("Expected import identifier with format [project_id],[instance_id],[credential_id], got %q", req.ID), ) + return } @@ -305,12 +323,15 @@ func mapFields(ctx context.Context, credentialsResp *opensearch.CredentialsRespo if credentialsResp == nil { return fmt.Errorf("response input is nil") } + if credentialsResp.Raw == nil { return fmt.Errorf("response credentials raw is nil") } + if model == nil { return fmt.Errorf("model input is nil") } + credentials := credentialsResp.Raw.Credentials var credentialId string @@ -333,6 +354,7 @@ func mapFields(ctx context.Context, credentialsResp *opensearch.CredentialsRespo model.CredentialId = types.StringValue(credentialId) model.Hosts = types.ListNull(types.StringType) + if credentials != nil { if credentials.Hosts != nil { respHosts := *credentials.Hosts @@ -345,6 +367,7 @@ func mapFields(ctx context.Context, credentialsResp *opensearch.CredentialsRespo model.Hosts = hostsTF } + model.Host = types.StringPointerValue(credentials.Host) model.Password = types.StringPointerValue(credentials.Password) model.Port = types.Int64PointerValue(credentials.Port) @@ -352,5 +375,6 @@ func mapFields(ctx context.Context, credentialsResp *opensearch.CredentialsRespo model.Uri = types.StringPointerValue(credentials.Uri) model.Username = types.StringPointerValue(credentials.Username) } + return nil } diff --git a/stackit/internal/services/opensearch/credential/resource_test.go b/stackit/internal/services/opensearch/credential/resource_test.go index baab02521..61df6a74a 100644 --- a/stackit/internal/services/opensearch/credential/resource_test.go +++ b/stackit/internal/services/opensearch/credential/resource_test.go @@ -207,9 +207,11 @@ func TestMapFields(t *testing.T) { if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(tt.state, tt.expected) if diff != "" { diff --git a/stackit/internal/services/opensearch/instance/datasource.go b/stackit/internal/services/opensearch/instance/datasource.go index e2b43b47a..4c8214fa6 100644 --- a/stackit/internal/services/opensearch/instance/datasource.go +++ b/stackit/internal/services/opensearch/instance/datasource.go @@ -5,19 +5,17 @@ import ( "fmt" "net/http" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - opensearchUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/opensearch/utils" - "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/stackitcloud/stackit-sdk-go/services/opensearch" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + opensearchUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/opensearch/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - - "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/stackitcloud/stackit-sdk-go/services/opensearch" ) // Ensure the implementation satisfies the expected interfaces. @@ -48,10 +46,13 @@ func (r *instanceDataSource) Configure(ctx context.Context, req datasource.Confi } apiClient := opensearchUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + r.client = apiClient + tflog.Info(ctx, "OpenSearch instance client configured") } @@ -208,13 +209,15 @@ func (r *instanceDataSource) Schema(_ context.Context, _ datasource.SchemaReques } // Read refreshes the Terraform state with the latest data. -func (r *instanceDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (r *instanceDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.Config.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() instanceId := model.InstanceId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) @@ -234,6 +237,7 @@ func (r *instanceDataSource) Read(ctx context.Context, req datasource.ReadReques }, ) resp.State.RemoveResource(ctx) + return } @@ -253,8 +257,10 @@ func (r *instanceDataSource) Read(ctx context.Context, req datasource.ReadReques // Set refreshed state diags = resp.State.Set(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "OpenSearch instance read") } diff --git a/stackit/internal/services/opensearch/instance/resource.go b/stackit/internal/services/opensearch/instance/resource.go index 68999e682..97c07c2f6 100644 --- a/stackit/internal/services/opensearch/instance/resource.go +++ b/stackit/internal/services/opensearch/instance/resource.go @@ -7,28 +7,25 @@ import ( "slices" "strings" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - - opensearchUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/opensearch/utils" - "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" "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/services/opensearch" "github.com/stackitcloud/stackit-sdk-go/services/opensearch/wait" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + opensearchUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/opensearch/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" ) // Ensure the implementation satisfies the expected interfaces. @@ -54,7 +51,7 @@ type Model struct { PlanId types.String `tfsdk:"plan_id"` } -// Struct corresponding to DataSourceModel.Parameters +// Struct corresponding to DataSourceModel.Parameters. type parametersModel struct { SgwAcl types.String `tfsdk:"sgw_acl"` EnableMonitoring types.Bool `tfsdk:"enable_monitoring"` @@ -72,7 +69,7 @@ type parametersModel struct { TlsProtocols types.List `tfsdk:"tls_protocols"` } -// Types corresponding to parametersModel +// Types corresponding to parametersModel. var parametersTypes = map[string]attr.Type{ "sgw_acl": basetypes.StringType{}, "enable_monitoring": basetypes.BoolType{}, @@ -113,10 +110,13 @@ func (r *instanceResource) Configure(ctx context.Context, req resource.Configure } apiClient := opensearchUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + r.client = apiClient + tflog.Info(ctx, "OpenSearch instance client configured") } @@ -323,21 +323,24 @@ func (r *instanceResource) Schema(_ context.Context, _ resource.SchemaRequest, r } // Create creates the resource and sets the initial Terraform state. -func (r *instanceResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *instanceResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) var parameters *parametersModel - if !(model.Parameters.IsNull() || model.Parameters.IsUnknown()) { + if !model.Parameters.IsNull() && !model.Parameters.IsUnknown() { parameters = ¶metersModel{} diags = model.Parameters.As(ctx, parameters, basetypes.ObjectAsOptions{}) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -361,6 +364,7 @@ func (r *instanceResource) Create(ctx context.Context, req resource.CreateReques core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Calling API: %v", err)) return } + instanceId := *createResp.InstanceId ctx = tflog.SetField(ctx, "instance_id", instanceId) waitResp, err := wait.CreateInstanceWaitHandler(ctx, r.client, projectId, instanceId).WaitWithContext(ctx) @@ -379,20 +383,24 @@ func (r *instanceResource) Create(ctx context.Context, req resource.CreateReques // Set state to fully populated data diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "OpenSearch instance created") } // Read refreshes the Terraform state with the latest data. -func (r *instanceResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (r *instanceResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() instanceId := model.InstanceId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) @@ -405,7 +413,9 @@ func (r *instanceResource) Read(ctx context.Context, req resource.ReadRequest, r resp.State.RemoveResource(ctx) return } + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Calling API: %v", err)) + return } @@ -426,30 +436,35 @@ func (r *instanceResource) Read(ctx context.Context, req resource.ReadRequest, r // Set refreshed state diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "OpenSearch instance read") } // Update updates the resource and sets the updated Terraform state on success. -func (r *instanceResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *instanceResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() instanceId := model.InstanceId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) ctx = tflog.SetField(ctx, "instance_id", instanceId) var parameters *parametersModel - if !(model.Parameters.IsNull() || model.Parameters.IsUnknown()) { + if !model.Parameters.IsNull() && !model.Parameters.IsUnknown() { parameters = ¶metersModel{} diags = model.Parameters.As(ctx, parameters, basetypes.ObjectAsOptions{}) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -473,6 +488,7 @@ func (r *instanceResource) Update(ctx context.Context, req resource.UpdateReques core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Calling API: %v", err)) return } + waitResp, err := wait.PartialUpdateInstanceWaitHandler(ctx, r.client, projectId, instanceId).WaitWithContext(ctx) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Instance update waiting: %v", err)) @@ -488,21 +504,25 @@ func (r *instanceResource) Update(ctx context.Context, req resource.UpdateReques diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "OpenSearch instance updated") } // Delete deletes the resource and removes the Terraform state on success. -func (r *instanceResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform +func (r *instanceResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { //nolint:gocritic // function signature required by Terraform // Retrieve values from state var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() instanceId := model.InstanceId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) @@ -514,16 +534,18 @@ func (r *instanceResource) Delete(ctx context.Context, req resource.DeleteReques core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting instance", fmt.Sprintf("Calling API: %v", err)) return } + _, err = wait.DeleteInstanceWaitHandler(ctx, r.client, projectId, instanceId).WaitWithContext(ctx) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting instance", fmt.Sprintf("Instance deletion waiting: %v", err)) return } + tflog.Info(ctx, "OpenSearch instance deleted") } // ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: project_id,instance_id +// The expected format of the resource import identifier is: project_id,instance_id. func (r *instanceResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { idParts := strings.Split(req.ID, core.Separator) @@ -532,6 +554,7 @@ func (r *instanceResource) ImportState(ctx context.Context, req resource.ImportS "Error importing instance", fmt.Sprintf("Expected import identifier with format: [project_id],[instance_id] Got: %q", req.ID), ) + return } @@ -544,6 +567,7 @@ func mapFields(instance *opensearch.Instance, model *Model) error { if instance == nil { return fmt.Errorf("response input is nil") } + if model == nil { return fmt.Errorf("model input is nil") } @@ -574,15 +598,19 @@ func mapFields(instance *opensearch.Instance, model *Model) error { if err != nil { return fmt.Errorf("mapping parameters: %w", err) } + model.Parameters = parameters } + return nil } func mapParameters(params map[string]interface{}) (types.Object, error) { attributes := map[string]attr.Value{} + for attribute := range parametersTypes { var valueInterface interface{} + var ok bool // This replacement is necessary because Terraform does not allow hyphens in attribute names @@ -598,6 +626,7 @@ func mapParameters(params map[string]interface{}) (types.Object, error) { } else { valueInterface, ok = params[attribute] } + if !ok { // All fields are optional, so this is ok // Set the value as nil, will be handled accordingly @@ -605,6 +634,7 @@ func mapParameters(params map[string]interface{}) (types.Object, error) { } var value attr.Value + switch parametersTypes[attribute].(type) { default: return types.ObjectNull(parametersTypes), fmt.Errorf("found unexpected attribute type '%T'", parametersTypes[attribute]) @@ -616,6 +646,7 @@ func mapParameters(params map[string]interface{}) (types.Object, error) { if !ok { return types.ObjectNull(parametersTypes), fmt.Errorf("found attribute '%s' of type %T, failed to assert as string", attribute, valueInterface) } + value = types.StringValue(valueString) } case basetypes.BoolType: @@ -626,6 +657,7 @@ func mapParameters(params map[string]interface{}) (types.Object, error) { if !ok { return types.ObjectNull(parametersTypes), fmt.Errorf("found attribute '%s' of type %T, failed to assert as bool", attribute, valueInterface) } + value = types.BoolValue(valueBool) } case basetypes.Int64Type: @@ -635,6 +667,7 @@ func mapParameters(params map[string]interface{}) (types.Object, error) { // This may be int64, int32, int or float64 // We try to assert all 4 var valueInt64 int64 + switch temp := valueInterface.(type) { default: return types.ObjectNull(parametersTypes), fmt.Errorf("found attribute '%s' of type %T, failed to assert as int", attribute, valueInterface) @@ -647,6 +680,7 @@ func mapParameters(params map[string]interface{}) (types.Object, error) { case float64: valueInt64 = int64(temp) } + value = types.Int64Value(valueInt64) } case basetypes.ListType: // Assumed to be a list of strings @@ -656,6 +690,7 @@ func mapParameters(params map[string]interface{}) (types.Object, error) { // This may be []string{} or []interface{} // We try to assert all 2 var valueList []attr.Value + switch temp := valueInterface.(type) { default: return types.ObjectNull(parametersTypes), fmt.Errorf("found attribute '%s' of type %T, failed to assert as array of interface", attribute, valueInterface) @@ -669,16 +704,20 @@ func mapParameters(params map[string]interface{}) (types.Object, error) { if !ok { return types.ObjectNull(parametersTypes), fmt.Errorf("found attribute '%s' with element '%s' of type %T, failed to assert as string", attribute, x, x) } + valueList = append(valueList, types.StringValue(xString)) } } + temp2, diags := types.ListValue(types.StringType, valueList) if diags.HasError() { return types.ObjectNull(parametersTypes), fmt.Errorf("failed to map %s: %w", attribute, core.DiagsToError(diags)) } + value = temp2 } } + attributes[attribute] = value } @@ -686,6 +725,7 @@ func mapParameters(params map[string]interface{}) (types.Object, error) { if diags.HasError() { return types.ObjectNull(parametersTypes), fmt.Errorf("failed to create object: %w", core.DiagsToError(diags)) } + return output, nil } @@ -693,10 +733,12 @@ func toCreatePayload(model *Model, parameters *parametersModel) (*opensearch.Cre if model == nil { return nil, fmt.Errorf("nil model") } + payloadParams, err := toInstanceParams(parameters) if err != nil { return nil, fmt.Errorf("convert parameters: %w", err) } + return &opensearch.CreateInstancePayload{ InstanceName: conversion.StringValueToPointer(model.Name), Parameters: payloadParams, @@ -708,10 +750,12 @@ func toUpdatePayload(model *Model, parameters *parametersModel) (*opensearch.Par if model == nil { return nil, fmt.Errorf("nil model") } + payloadParams, err := toInstanceParams(parameters) if err != nil { return nil, fmt.Errorf("convert parameters: %w", err) } + return &opensearch.PartialUpdateInstancePayload{ Parameters: payloadParams, PlanId: conversion.StringValueToPointer(model.PlanId), @@ -722,6 +766,7 @@ func toInstanceParams(parameters *parametersModel) (*opensearch.InstanceParamete if parameters == nil { return nil, nil } + payloadParams := &opensearch.InstanceParameters{} payloadParams.SgwAcl = conversion.StringValueToPointer(parameters.SgwAcl) @@ -736,6 +781,7 @@ func toInstanceParams(parameters *parametersModel) (*opensearch.InstanceParamete payloadParams.MonitoringInstanceId = conversion.StringValueToPointer(parameters.MonitoringInstanceId) var err error + payloadParams.Plugins, err = conversion.StringListToPointer(parameters.Plugins) if err != nil { return nil, fmt.Errorf("convert plugins: %w", err) @@ -761,6 +807,7 @@ func toInstanceParams(parameters *parametersModel) (*opensearch.InstanceParamete func (r *instanceResource) loadPlanId(ctx context.Context, model *Model) error { projectId := model.ProjectId.ValueString() + res, err := r.client.ListOfferings(ctx, projectId).Execute() if err != nil { return fmt.Errorf("getting OpenSearch offerings: %w", err) @@ -771,21 +818,25 @@ func (r *instanceResource) loadPlanId(ctx context.Context, model *Model) error { availableVersions := "" availablePlanNames := "" isValidVersion := false + for _, offer := range *res.Offerings { if !strings.EqualFold(*offer.Version, version) { availableVersions = fmt.Sprintf("%s\n- %s", availableVersions, *offer.Version) continue } + isValidVersion = true for _, plan := range *offer.Plans { if plan.Name == nil { continue } + if strings.EqualFold(*plan.Name, planName) && plan.Id != nil { model.PlanId = types.StringPointerValue(plan.Id) return nil } + availablePlanNames = fmt.Sprintf("%s\n- %s", availablePlanNames, *plan.Name) } } @@ -793,12 +844,14 @@ func (r *instanceResource) loadPlanId(ctx context.Context, model *Model) error { if !isValidVersion { return fmt.Errorf("couldn't find version '%s', available versions are: %s", version, availableVersions) } + return fmt.Errorf("couldn't find plan_name '%s' for version %s, available names are: %s", planName, version, availablePlanNames) } func loadPlanNameAndVersion(ctx context.Context, client *opensearch.APIClient, model *Model) error { projectId := model.ProjectId.ValueString() planId := model.PlanId.ValueString() + res, err := client.ListOfferings(ctx, projectId).Execute() if err != nil { return fmt.Errorf("getting OpenSearch offerings: %w", err) @@ -809,6 +862,7 @@ func loadPlanNameAndVersion(ctx context.Context, client *opensearch.APIClient, m if strings.EqualFold(*plan.Id, planId) && plan.Id != nil { model.PlanName = types.StringPointerValue(plan.Name) model.Version = types.StringPointerValue(offer.Version) + return nil } } diff --git a/stackit/internal/services/opensearch/instance/resource_test.go b/stackit/internal/services/opensearch/instance/resource_test.go index 82dd9b4d9..ca85a79b9 100644 --- a/stackit/internal/services/opensearch/instance/resource_test.go +++ b/stackit/internal/services/opensearch/instance/resource_test.go @@ -183,13 +183,16 @@ func TestMapFields(t *testing.T) { ProjectId: tt.expected.ProjectId, InstanceId: tt.expected.InstanceId, } + err := mapFields(tt.input, state) if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(state, &tt.expected) if diff != "" { @@ -263,22 +266,27 @@ func TestToCreatePayload(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { var parameters *parametersModel + if tt.input != nil { - if !(tt.input.Parameters.IsNull() || tt.input.Parameters.IsUnknown()) { + if !tt.input.Parameters.IsNull() && !tt.input.Parameters.IsUnknown() { parameters = ¶metersModel{} + diags := tt.input.Parameters.As(context.Background(), parameters, basetypes.ObjectAsOptions{}) if diags.HasError() { t.Fatalf("Error converting parameters: %v", diags.Errors()) } } } + output, err := toCreatePayload(tt.input, parameters) if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(output, tt.expected) if diff != "" { @@ -346,22 +354,27 @@ func TestToUpdatePayload(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { var parameters *parametersModel + if tt.input != nil { - if !(tt.input.Parameters.IsNull() || tt.input.Parameters.IsUnknown()) { + if !tt.input.Parameters.IsNull() && !tt.input.Parameters.IsUnknown() { parameters = ¶metersModel{} + diags := tt.input.Parameters.As(context.Background(), parameters, basetypes.ObjectAsOptions{}) if diags.HasError() { t.Fatalf("Error converting parameters: %v", diags.Errors()) } } } + output, err := toUpdatePayload(tt.input, parameters) if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(output, tt.expected) if diff != "" { diff --git a/stackit/internal/services/opensearch/opensearch_acc_test.go b/stackit/internal/services/opensearch/opensearch_acc_test.go index 223b12cee..864c8b815 100644 --- a/stackit/internal/services/opensearch/opensearch_acc_test.go +++ b/stackit/internal/services/opensearch/opensearch_acc_test.go @@ -16,7 +16,7 @@ import ( "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/testutil" ) -// Instance resource data +// Instance resource data. var instanceResource = map[string]string{ "project_id": testutil.ProjectId, "name": testutil.ResourceNameWithDateTime("opensearch"), @@ -42,6 +42,7 @@ func parametersConfig(params map[string]string) string { "tls_ciphers", } parameters := "parameters = {" + for k, v := range params { if utils.Contains(nonStringParams, k) { parameters += fmt.Sprintf("%s = %s\n", k, v) @@ -49,7 +50,9 @@ func parametersConfig(params map[string]string) string { parameters += fmt.Sprintf("%s = %q\n", k, v) } } + parameters += "\n}" + return parameters } @@ -84,7 +87,6 @@ func TestAccOpenSearchResource(t *testing.T) { ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, CheckDestroy: testAccCheckOpenSearchDestroy, Steps: []resource.TestStep{ - // Creation { Config: resourceConfig( @@ -201,6 +203,7 @@ func TestAccOpenSearchResource(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute credential_id") } + return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, instanceId, credentialId), nil }, ImportState: true, @@ -237,7 +240,9 @@ func TestAccOpenSearchResource(t *testing.T) { func testAccCheckOpenSearchDestroy(s *terraform.State) error { ctx := context.Background() + var client *opensearch.APIClient + var err error if testutil.OpenSearchCustomEndpoint == "" { client, err = opensearch.NewAPIClient( @@ -248,11 +253,13 @@ func testAccCheckOpenSearchDestroy(s *terraform.State) error { config.WithEndpoint(testutil.OpenSearchCustomEndpoint), ) } + if err != nil { return fmt.Errorf("creating client: %w", err) } instancesToDestroy := []string{} + for _, rs := range s.RootModule().Resources { if rs.Type != "stackit_opensearch_instance" { continue @@ -272,12 +279,14 @@ func testAccCheckOpenSearchDestroy(s *terraform.State) error { if instances[i].InstanceId == nil { continue } + if utils.Contains(instancesToDestroy, *instances[i].InstanceId) { if !checkInstanceDeleteSuccess(&instances[i]) { err := client.DeleteInstanceExecute(ctx, testutil.ProjectId, *instances[i].InstanceId) if err != nil { return fmt.Errorf("destroying instance %s during CheckDestroy: %w", *instances[i].InstanceId, err) } + _, err = wait.DeleteInstanceWaitHandler(ctx, client, testutil.ProjectId, *instances[i].InstanceId).WaitWithContext(ctx) if err != nil { return fmt.Errorf("destroying instance %s during CheckDestroy: waiting for deletion %w", *instances[i].InstanceId, err) @@ -285,6 +294,7 @@ func testAccCheckOpenSearchDestroy(s *terraform.State) error { } } } + return nil } @@ -300,5 +310,6 @@ func checkInstanceDeleteSuccess(i *opensearch.Instance) bool { return false } } + return true } diff --git a/stackit/internal/services/opensearch/utils/util.go b/stackit/internal/services/opensearch/utils/util.go index e630a860e..495f2260b 100644 --- a/stackit/internal/services/opensearch/utils/util.go +++ b/stackit/internal/services/opensearch/utils/util.go @@ -21,6 +21,7 @@ func ConfigureClient(ctx context.Context, providerData *core.ProviderData, diags } else { apiClientConfigOptions = append(apiClientConfigOptions, config.WithRegion(providerData.GetRegion())) } + apiClient, err := opensearch.NewAPIClient(apiClientConfigOptions...) if err != nil { core.LogAndAddError(ctx, diags, "Error configuring API client", fmt.Sprintf("Configuring client: %v. This is an error related to the provider configuration, not to the resource configuration", err)) diff --git a/stackit/internal/services/opensearch/utils/util_test.go b/stackit/internal/services/opensearch/utils/util_test.go index fa46355bf..34fb34209 100644 --- a/stackit/internal/services/opensearch/utils/util_test.go +++ b/stackit/internal/services/opensearch/utils/util_test.go @@ -22,6 +22,7 @@ const ( func TestConfigureClient(t *testing.T) { /* mock authentication by setting service account token env variable */ os.Clearenv() + err := os.Setenv(sdkClients.ServiceAccountToken, "mock-val") if err != nil { t.Errorf("error setting env variable: %v", err) @@ -30,6 +31,7 @@ func TestConfigureClient(t *testing.T) { type args struct { providerData *core.ProviderData } + tests := []struct { name string args args @@ -51,6 +53,7 @@ func TestConfigureClient(t *testing.T) { if err != nil { t.Errorf("error configuring client: %v", err) } + return apiClient }(), wantErr: false, @@ -71,6 +74,7 @@ func TestConfigureClient(t *testing.T) { if err != nil { t.Errorf("error configuring client: %v", err) } + return apiClient }(), wantErr: false, @@ -82,6 +86,7 @@ func TestConfigureClient(t *testing.T) { diags := diag.Diagnostics{} actual := ConfigureClient(ctx, tt.args.providerData, &diags) + if diags.HasError() != tt.wantErr { t.Errorf("ConfigureClient() error = %v, want %v", diags.HasError(), tt.wantErr) } diff --git a/stackit/internal/services/postgresflex/database/datasource.go b/stackit/internal/services/postgresflex/database/datasource.go index f1d8b1e96..2a2370c98 100644 --- a/stackit/internal/services/postgresflex/database/datasource.go +++ b/stackit/internal/services/postgresflex/database/datasource.go @@ -5,18 +5,16 @@ import ( "fmt" "net/http" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - postgresflexUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/postgresflex/utils" - "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/stackitcloud/stackit-sdk-go/services/postgresflex" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + postgresflexUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/postgresflex/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - - "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/stackitcloud/stackit-sdk-go/services/postgresflex" ) // Ensure the implementation satisfies the expected interfaces. @@ -43,16 +41,20 @@ func (r *databaseDataSource) Metadata(_ context.Context, req datasource.Metadata // Configure adds the provider configured client to the data source. func (r *databaseDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { var ok bool + r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) if !ok { return } apiClient := postgresflexUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + r.client = apiClient + tflog.Info(ctx, "Postgres Flex database client configured") } @@ -117,13 +119,15 @@ func (r *databaseDataSource) Schema(_ context.Context, _ datasource.SchemaReques } // Read refreshes the Terraform state with the latest data. -func (r *databaseDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (r *databaseDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.Config.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() instanceId := model.InstanceId.ValueString() databaseId := model.DatabaseId.ValueString() @@ -146,6 +150,7 @@ func (r *databaseDataSource) Read(ctx context.Context, req datasource.ReadReques }, ) resp.State.RemoveResource(ctx) + return } @@ -159,8 +164,10 @@ func (r *databaseDataSource) Read(ctx context.Context, req datasource.ReadReques // Set refreshed state diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "Postgres Flex database read") } diff --git a/stackit/internal/services/postgresflex/database/resource.go b/stackit/internal/services/postgresflex/database/resource.go index 024e18e96..2e9cea840 100644 --- a/stackit/internal/services/postgresflex/database/resource.go +++ b/stackit/internal/services/postgresflex/database/resource.go @@ -7,23 +7,21 @@ import ( "net/http" "strings" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - postgresflexUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/postgresflex/utils" - - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/stackitcloud/stackit-sdk-go/core/oapierror" "github.com/stackitcloud/stackit-sdk-go/services/postgresflex" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + postgresflexUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/postgresflex/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" ) // Ensure the implementation satisfies the expected interfaces. @@ -57,29 +55,35 @@ type databaseResource struct { // ModifyPlan implements resource.ResourceWithModifyPlan. // Use the modifier to set the effective region in the current plan. -func (r *databaseResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform +func (r *databaseResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { //nolint:gocritic // function signature required by Terraform var configModel Model // skip initial empty configuration to avoid follow-up errors if req.Config.Raw.IsNull() { return } + resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...) + if resp.Diagnostics.HasError() { return } var planModel Model + resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) + if resp.Diagnostics.HasError() { return } utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp) + if resp.Diagnostics.HasError() { return } resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...) + if resp.Diagnostics.HasError() { return } @@ -93,16 +97,20 @@ func (r *databaseResource) Metadata(_ context.Context, req resource.MetadataRequ // Configure adds the provider configured client to the resource. func (r *databaseResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { var ok bool + r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) if !ok { return } apiClient := postgresflexUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + r.client = apiClient + tflog.Info(ctx, "Postgres Flex database client configured") } @@ -191,13 +199,15 @@ func (r *databaseResource) Schema(_ context.Context, _ resource.SchemaRequest, r } // Create creates the resource and sets the initial Terraform state. -func (r *databaseResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *databaseResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() region := model.Region.ValueString() instanceId := model.InstanceId.ValueString() @@ -217,10 +227,12 @@ func (r *databaseResource) Create(ctx context.Context, req resource.CreateReques core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating database", fmt.Sprintf("Calling API: %v", err)) return } + if databaseResp == nil || databaseResp.Id == nil || *databaseResp.Id == "" { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating database", "API didn't return database Id. A database might have been created") return } + databaseId := *databaseResp.Id ctx = tflog.SetField(ctx, "database_id", databaseId) @@ -239,20 +251,24 @@ func (r *databaseResource) Create(ctx context.Context, req resource.CreateReques // Set state to fully populated data diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "Postgres Flex database created") } // Read refreshes the Terraform state with the latest data. -func (r *databaseResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (r *databaseResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() instanceId := model.InstanceId.ValueString() databaseId := model.DatabaseId.ValueString() @@ -265,11 +281,13 @@ func (r *databaseResource) Read(ctx context.Context, req resource.ReadRequest, r databaseResp, err := getDatabase(ctx, r.client, projectId, region, instanceId, databaseId) if err != nil { oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped - if (ok && oapiErr.StatusCode == http.StatusNotFound) || errors.Is(err, databaseNotFoundErr) { + if (ok && oapiErr.StatusCode == http.StatusNotFound) || errors.Is(err, errDatabaseNotFound) { resp.State.RemoveResource(ctx) return } + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading database", fmt.Sprintf("Calling API: %v", err)) + return } @@ -283,24 +301,27 @@ func (r *databaseResource) Read(ctx context.Context, req resource.ReadRequest, r // Set refreshed state diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "Postgres Flex database read") } // Update updates the resource and sets the updated Terraform state on success. -func (r *databaseResource) Update(ctx context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *databaseResource) Update(ctx context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { //nolint:gocritic // function signature required by Terraform // Update shouldn't be called core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating database", "Database can't be updated") } // Delete deletes the resource and removes the Terraform state on success. -func (r *databaseResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform +func (r *databaseResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { //nolint:gocritic // function signature required by Terraform // Retrieve values from plan var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -319,11 +340,12 @@ func (r *databaseResource) Delete(ctx context.Context, req resource.DeleteReques if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting database", fmt.Sprintf("Calling API: %v", err)) } + tflog.Info(ctx, "Postgres Flex database deleted") } // ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: project_id,zone_id,record_set_id +// The expected format of the resource import identifier is: project_id,zone_id,record_set_id. func (r *databaseResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { idParts := strings.Split(req.ID, core.Separator) if len(idParts) != 4 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" || idParts[3] == "" { @@ -331,6 +353,7 @@ func (r *databaseResource) ImportState(ctx context.Context, req resource.ImportS "Error importing database", fmt.Sprintf("Expected import identifier with format [project_id],[region],[instance_id],[database_id], got %q", req.ID), ) + return } @@ -349,9 +372,11 @@ func mapFields(databaseResp *postgresflex.InstanceDatabase, model *Model, region if databaseResp == nil { return fmt.Errorf("response is nil") } + if databaseResp.Id == nil || *databaseResp.Id == "" { return fmt.Errorf("id not present") } + if model == nil { return fmt.Errorf("model input is nil") } @@ -364,6 +389,7 @@ func mapFields(databaseResp *postgresflex.InstanceDatabase, model *Model, region } else { return fmt.Errorf("database id not present") } + model.Id = utils.BuildInternalTerraformId( model.ProjectId.ValueString(), region, model.InstanceId.ValueString(), databaseId, ) @@ -401,21 +427,24 @@ func toCreatePayload(model *Model) (*postgresflex.CreateDatabasePayload, error) }, nil } -var databaseNotFoundErr = errors.New("database not found") +var errDatabaseNotFound = errors.New("database not found") -// The API does not have a GetDatabase endpoint, only ListDatabases +// The API does not have a GetDatabase endpoint, only ListDatabases. func getDatabase(ctx context.Context, client *postgresflex.APIClient, projectId, region, instanceId, databaseId string) (*postgresflex.InstanceDatabase, error) { resp, err := client.ListDatabases(ctx, projectId, region, instanceId).Execute() if err != nil { return nil, err } + if resp == nil || resp.Databases == nil { return nil, fmt.Errorf("response is nil") } + for _, database := range *resp.Databases { if database.Id != nil && *database.Id == databaseId { return &database, nil } } - return nil, databaseNotFoundErr + + return nil, errDatabaseNotFound } diff --git a/stackit/internal/services/postgresflex/database/resource_test.go b/stackit/internal/services/postgresflex/database/resource_test.go index 1770801b6..05ef681fe 100644 --- a/stackit/internal/services/postgresflex/database/resource_test.go +++ b/stackit/internal/services/postgresflex/database/resource_test.go @@ -11,6 +11,7 @@ import ( func TestMapFields(t *testing.T) { const testRegion = "region" + tests := []struct { description string input *postgresflex.InstanceDatabase @@ -111,13 +112,16 @@ func TestMapFields(t *testing.T) { ProjectId: tt.expected.ProjectId, InstanceId: tt.expected.InstanceId, } + err := mapFields(tt.input, state, tt.region) if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(state, &tt.expected) if diff != "" { @@ -176,9 +180,11 @@ func TestToCreatePayload(t *testing.T) { if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(output, tt.expected) if diff != "" { diff --git a/stackit/internal/services/postgresflex/instance/datasource.go b/stackit/internal/services/postgresflex/instance/datasource.go index b411713be..b60ae1659 100644 --- a/stackit/internal/services/postgresflex/instance/datasource.go +++ b/stackit/internal/services/postgresflex/instance/datasource.go @@ -5,21 +5,19 @@ import ( "fmt" "net/http" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - postgresflexUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/postgresflex/utils" - "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "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/services/postgresflex" + "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/wait" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + postgresflexUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/postgresflex/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - - "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/services/postgresflex" - "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/wait" ) // Ensure the implementation satisfies the expected interfaces. @@ -46,16 +44,20 @@ func (r *instanceDataSource) Metadata(_ context.Context, req datasource.Metadata // Configure adds the provider configured client to the data source. func (r *instanceDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { var ok bool + r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) if !ok { return } apiClient := postgresflexUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + r.client = apiClient + tflog.Info(ctx, "Postgres Flex instance client configured") } @@ -150,10 +152,11 @@ func (r *instanceDataSource) Schema(_ context.Context, _ datasource.SchemaReques } // Read refreshes the Terraform state with the latest data. -func (r *instanceDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (r *instanceDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.Config.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -177,26 +180,32 @@ func (r *instanceDataSource) Read(ctx context.Context, req datasource.ReadReques }, ) resp.State.RemoveResource(ctx) + return } + if instanceResp != nil && instanceResp.Item != nil && instanceResp.Item.Status != nil && *instanceResp.Item.Status == wait.InstanceStateDeleted { resp.State.RemoveResource(ctx) core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", "Instance was deleted successfully") + return } - var flavor = &flavorModel{} - if !(model.Flavor.IsNull() || model.Flavor.IsUnknown()) { + flavor := &flavorModel{} + if !model.Flavor.IsNull() && !model.Flavor.IsUnknown() { diags = model.Flavor.As(ctx, flavor, basetypes.ObjectAsOptions{}) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } } - var storage = &storageModel{} - if !(model.Storage.IsNull() || model.Storage.IsUnknown()) { + + storage := &storageModel{} + if !model.Storage.IsNull() && !model.Storage.IsUnknown() { diags = model.Storage.As(ctx, storage, basetypes.ObjectAsOptions{}) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -210,8 +219,10 @@ func (r *instanceDataSource) Read(ctx context.Context, req datasource.ReadReques // Set refreshed state diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "Postgres Flex instance read") } diff --git a/stackit/internal/services/postgresflex/instance/resource.go b/stackit/internal/services/postgresflex/instance/resource.go index 4bb653afb..f48bced93 100644 --- a/stackit/internal/services/postgresflex/instance/resource.go +++ b/stackit/internal/services/postgresflex/instance/resource.go @@ -8,28 +8,26 @@ import ( "strings" "time" - postgresflexUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/postgresflex/utils" - "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" "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/services/postgresflex" "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/wait" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + postgresflexUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/postgresflex/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" ) // Ensure the implementation satisfies the expected interfaces. @@ -54,7 +52,7 @@ type Model struct { Region types.String `tfsdk:"region"` } -// Struct corresponding to Model.Flavor +// Struct corresponding to Model.Flavor. type flavorModel struct { Id types.String `tfsdk:"id"` Description types.String `tfsdk:"description"` @@ -62,7 +60,7 @@ type flavorModel struct { RAM types.Int64 `tfsdk:"ram"` } -// Types corresponding to flavorModel +// Types corresponding to flavorModel. var flavorTypes = map[string]attr.Type{ "id": basetypes.StringType{}, "description": basetypes.StringType{}, @@ -70,13 +68,13 @@ var flavorTypes = map[string]attr.Type{ "ram": basetypes.Int64Type{}, } -// Struct corresponding to Model.Storage +// Struct corresponding to Model.Storage. type storageModel struct { Class types.String `tfsdk:"class"` Size types.Int64 `tfsdk:"size"` } -// Types corresponding to storageModel +// Types corresponding to storageModel. var storageTypes = map[string]attr.Type{ "class": basetypes.StringType{}, "size": basetypes.Int64Type{}, @@ -95,29 +93,35 @@ type instanceResource struct { // ModifyPlan implements resource.ResourceWithModifyPlan. // Use the modifier to set the effective region in the current plan. -func (r *instanceResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform +func (r *instanceResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { //nolint:gocritic // function signature required by Terraform var configModel Model // skip initial empty configuration to avoid follow-up errors if req.Config.Raw.IsNull() { return } + resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...) + if resp.Diagnostics.HasError() { return } var planModel Model + resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) + if resp.Diagnostics.HasError() { return } utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp) + if resp.Diagnostics.HasError() { return } resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...) + if resp.Diagnostics.HasError() { return } @@ -131,16 +135,20 @@ func (r *instanceResource) Metadata(_ context.Context, req resource.MetadataRequ // Configure adds the provider configured client to the resource. func (r *instanceResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { var ok bool + r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) if !ok { return } apiClient := postgresflexUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + r.client = apiClient + tflog.Info(ctx, "Postgres Flex instance client configured") } @@ -265,44 +273,52 @@ func (r *instanceResource) Schema(_ context.Context, req resource.SchemaRequest, } // Create creates the resource and sets the initial Terraform state. -func (r *instanceResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *instanceResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { //nolint:gocritic // function signature required by Terraform // Retrieve values from plan var model Model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() region := model.Region.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) ctx = tflog.SetField(ctx, "region", region) var acl []string - if !(model.ACL.IsNull() || model.ACL.IsUnknown()) { + if !model.ACL.IsNull() && !model.ACL.IsUnknown() { diags = model.ACL.ElementsAs(ctx, &acl, false) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } } - var flavor = &flavorModel{} - if !(model.Flavor.IsNull() || model.Flavor.IsUnknown()) { + + flavor := &flavorModel{} + if !model.Flavor.IsNull() && !model.Flavor.IsUnknown() { diags = model.Flavor.As(ctx, flavor, basetypes.ObjectAsOptions{}) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + err := loadFlavorId(ctx, r.client, &model, flavor) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Loading flavor ID: %v", err)) return } } - var storage = &storageModel{} - if !(model.Storage.IsNull() || model.Storage.IsUnknown()) { + + storage := &storageModel{} + if !model.Storage.IsNull() && !model.Storage.IsUnknown() { diags = model.Storage.As(ctx, storage, basetypes.ObjectAsOptions{}) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -320,6 +336,7 @@ func (r *instanceResource) Create(ctx context.Context, req resource.CreateReques core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Calling API: %v", err)) return } + instanceId := *createResp.Id ctx = tflog.SetField(ctx, "instance_id", instanceId) waitResp, err := wait.CreateInstanceWaitHandler(ctx, r.client, projectId, region, instanceId).WaitWithContext(ctx) @@ -337,20 +354,24 @@ func (r *instanceResource) Create(ctx context.Context, req resource.CreateReques // Set state to fully populated data diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "Postgres Flex instance created") } // Read refreshes the Terraform state with the latest data. -func (r *instanceResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (r *instanceResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() instanceId := model.InstanceId.ValueString() region := r.providerData.GetRegionWithOverride(model.Region) @@ -358,18 +379,21 @@ func (r *instanceResource) Read(ctx context.Context, req resource.ReadRequest, r ctx = tflog.SetField(ctx, "instance_id", instanceId) ctx = tflog.SetField(ctx, "region", region) - var flavor = &flavorModel{} - if !(model.Flavor.IsNull() || model.Flavor.IsUnknown()) { + flavor := &flavorModel{} + if !model.Flavor.IsNull() && !model.Flavor.IsUnknown() { diags = model.Flavor.As(ctx, flavor, basetypes.ObjectAsOptions{}) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } } - var storage = &storageModel{} - if !(model.Storage.IsNull() || model.Storage.IsUnknown()) { + + storage := &storageModel{} + if !model.Storage.IsNull() && !model.Storage.IsUnknown() { diags = model.Storage.As(ctx, storage, basetypes.ObjectAsOptions{}) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -382,9 +406,12 @@ func (r *instanceResource) Read(ctx context.Context, req resource.ReadRequest, r resp.State.RemoveResource(ctx) return } + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", err.Error()) + return } + if instanceResp != nil && instanceResp.Item != nil && instanceResp.Item.Status != nil && *instanceResp.Item.Status == wait.InstanceStateDeleted { resp.State.RemoveResource(ctx) return @@ -399,21 +426,25 @@ func (r *instanceResource) Read(ctx context.Context, req resource.ReadRequest, r // Set refreshed state diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "Postgres Flex instance read") } // Update updates the resource and sets the updated Terraform state on success. -func (r *instanceResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *instanceResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { //nolint:gocritic // function signature required by Terraform // Retrieve values from plan var model Model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() instanceId := model.InstanceId.ValueString() region := model.Region.ValueString() @@ -422,30 +453,36 @@ func (r *instanceResource) Update(ctx context.Context, req resource.UpdateReques ctx = tflog.SetField(ctx, "region", region) var acl []string - if !(model.ACL.IsNull() || model.ACL.IsUnknown()) { + if !model.ACL.IsNull() && !model.ACL.IsUnknown() { diags = model.ACL.ElementsAs(ctx, &acl, false) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } } - var flavor = &flavorModel{} - if !(model.Flavor.IsNull() || model.Flavor.IsUnknown()) { + + flavor := &flavorModel{} + if !model.Flavor.IsNull() && !model.Flavor.IsUnknown() { diags = model.Flavor.As(ctx, flavor, basetypes.ObjectAsOptions{}) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + err := loadFlavorId(ctx, r.client, &model, flavor) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Loading flavor ID: %v", err)) return } } - var storage = &storageModel{} - if !(model.Storage.IsNull() || model.Storage.IsUnknown()) { + + storage := &storageModel{} + if !model.Storage.IsNull() && !model.Storage.IsUnknown() { diags = model.Storage.As(ctx, storage, basetypes.ObjectAsOptions{}) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -463,6 +500,7 @@ func (r *instanceResource) Update(ctx context.Context, req resource.UpdateReques core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", err.Error()) return } + waitResp, err := wait.PartialUpdateInstanceWaitHandler(ctx, r.client, projectId, region, instanceId).WaitWithContext(ctx) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Instance update waiting: %v", err)) @@ -475,23 +513,28 @@ func (r *instanceResource) Update(ctx context.Context, req resource.UpdateReques core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Processing API payload: %v", err)) return } + diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "Postgresflex instance updated") } // Delete deletes the resource and removes the Terraform state on success. -func (r *instanceResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform +func (r *instanceResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { //nolint:gocritic // function signature required by Terraform // Retrieve values from state var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() instanceId := model.InstanceId.ValueString() region := model.Region.ValueString() @@ -505,16 +548,18 @@ func (r *instanceResource) Delete(ctx context.Context, req resource.DeleteReques core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting instance", fmt.Sprintf("Calling API: %v", err)) return } + _, err = wait.DeleteInstanceWaitHandler(ctx, r.client, projectId, region, instanceId).SetTimeout(45 * time.Minute).WaitWithContext(ctx) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting instance", fmt.Sprintf("Instance deletion waiting: %v", err)) return } + tflog.Info(ctx, "Postgres Flex instance deleted") } // ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: project_id,instance_id +// The expected format of the resource import identifier is: project_id,instance_id. func (r *instanceResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { idParts := strings.Split(req.ID, core.Separator) @@ -523,6 +568,7 @@ func (r *instanceResource) ImportState(ctx context.Context, req resource.ImportS "Error importing instance", fmt.Sprintf("Expected import identifier with format: [project_id],[region],[instance_id] Got: %q", req.ID), ) + return } @@ -536,12 +582,15 @@ func mapFields(ctx context.Context, resp *postgresflex.InstanceResponse, model * if resp == nil { return fmt.Errorf("response input is nil") } + if resp.Item == nil { return fmt.Errorf("no instance provided") } + if model == nil { return fmt.Errorf("model input is nil") } + instance := resp.Item var instanceId string @@ -554,11 +603,14 @@ func mapFields(ctx context.Context, resp *postgresflex.InstanceResponse, model * } var aclList basetypes.ListValue + var diags diag.Diagnostics + if instance.Acl == nil || instance.Acl.Items == nil { aclList = types.ListNull(types.StringType) } else { respACL := *instance.Acl.Items + modelACL, err := utils.ListValuetoStringSlice(model.ACL) if err != nil { return err @@ -588,6 +640,7 @@ func mapFields(ctx context.Context, resp *postgresflex.InstanceResponse, model * "ram": types.Int64PointerValue(instance.Flavor.Memory), } } + flavorObject, diags := types.ObjectValue(flavorTypes, flavorValues) if diags.HasError() { return fmt.Errorf("creating flavor: %w", core.DiagsToError(diags)) @@ -605,6 +658,7 @@ func mapFields(ctx context.Context, resp *postgresflex.InstanceResponse, model * "size": types.Int64PointerValue(instance.Storage.Size), } } + storageObject, diags := types.ObjectValue(storageTypes, storageValues) if diags.HasError() { return fmt.Errorf("creating storage: %w", core.DiagsToError(diags)) @@ -620,6 +674,7 @@ func mapFields(ctx context.Context, resp *postgresflex.InstanceResponse, model * model.Storage = storageObject model.Version = types.StringPointerValue(instance.Version) model.Region = types.StringValue(region) + return nil } @@ -627,12 +682,15 @@ func toCreatePayload(model *Model, acl []string, flavor *flavorModel, storage *s if model == nil { return nil, fmt.Errorf("nil model") } + if acl == nil { return nil, fmt.Errorf("nil acl") } + if flavor == nil { return nil, fmt.Errorf("nil flavor") } + if storage == nil { return nil, fmt.Errorf("nil storage") } @@ -657,12 +715,15 @@ func toUpdatePayload(model *Model, acl []string, flavor *flavorModel, storage *s if model == nil { return nil, fmt.Errorf("nil model") } + if acl == nil { return nil, fmt.Errorf("nil acl") } + if flavor == nil { return nil, fmt.Errorf("nil flavor") } + if storage == nil { return nil, fmt.Errorf("nil storage") } @@ -691,13 +752,16 @@ func loadFlavorId(ctx context.Context, client postgresFlexClient, model *Model, if model == nil { return fmt.Errorf("nil model") } + if flavor == nil { return fmt.Errorf("nil flavor") } + cpu := conversion.Int64ValueToPointer(flavor.CPU) if cpu == nil { return fmt.Errorf("nil CPU") } + ram := conversion.Int64ValueToPointer(flavor.RAM) if ram == nil { return fmt.Errorf("nil RAM") @@ -705,26 +769,33 @@ func loadFlavorId(ctx context.Context, client postgresFlexClient, model *Model, projectId := model.ProjectId.ValueString() region := model.Region.ValueString() + res, err := client.ListFlavorsExecute(ctx, projectId, region) if err != nil { return fmt.Errorf("listing postgresflex flavors: %w", err) } avl := "" + if res.Flavors == nil { return fmt.Errorf("finding flavors for project %s", projectId) } + for _, f := range *res.Flavors { if f.Id == nil || f.Cpu == nil || f.Memory == nil { continue } + if *f.Cpu == *cpu && *f.Memory == *ram { flavor.Id = types.StringValue(*f.Id) flavor.Description = types.StringValue(*f.Description) + break } + avl = fmt.Sprintf("%s\n- %d CPU, %d GB RAM", avl, *f.Cpu, *f.Memory) } + if flavor.Id.ValueString() == "" { return fmt.Errorf("couldn't find flavor, available specs are:%s", avl) } diff --git a/stackit/internal/services/postgresflex/instance/resource_test.go b/stackit/internal/services/postgresflex/instance/resource_test.go index 4b3c58073..1d400e4e9 100644 --- a/stackit/internal/services/postgresflex/instance/resource_test.go +++ b/stackit/internal/services/postgresflex/instance/resource_test.go @@ -27,6 +27,7 @@ func (c *postgresFlexClientMocked) ListFlavorsExecute(_ context.Context, _, _ st func TestMapFields(t *testing.T) { const testRegion = "region" + tests := []struct { description string state Model @@ -295,9 +296,11 @@ func TestMapFields(t *testing.T) { if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(tt.state, tt.expected) if diff != "" { @@ -449,9 +452,11 @@ func TestToCreatePayload(t *testing.T) { if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(output, tt.expected) if diff != "" { @@ -603,9 +608,11 @@ func TestToUpdatePayload(t *testing.T) { if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(output, tt.expected) if diff != "" { @@ -752,13 +759,16 @@ func TestLoadFlavorId(t *testing.T) { CPU: tt.inputFlavor.CPU, RAM: tt.inputFlavor.RAM, } + err := loadFlavorId(context.Background(), client, model, flavorModel) if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(flavorModel, tt.expected) if diff != "" { diff --git a/stackit/internal/services/postgresflex/instance/use_state_for_unknown_if_flavor_unchanged_modifier.go b/stackit/internal/services/postgresflex/instance/use_state_for_unknown_if_flavor_unchanged_modifier.go index 38c924ba2..7b913089d 100644 --- a/stackit/internal/services/postgresflex/instance/use_state_for_unknown_if_flavor_unchanged_modifier.go +++ b/stackit/internal/services/postgresflex/instance/use_state_for_unknown_if_flavor_unchanged_modifier.go @@ -28,7 +28,7 @@ func (m useStateForUnknownIfFlavorUnchangedModifier) MarkdownDescription(ctx con return m.Description(ctx) } -func (m useStateForUnknownIfFlavorUnchangedModifier) PlanModifyString(ctx context.Context, req planmodifier.StringRequest, resp *planmodifier.StringResponse) { // nolint:gocritic // function signature required by Terraform +func (m useStateForUnknownIfFlavorUnchangedModifier) 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 @@ -50,14 +50,16 @@ func (m useStateForUnknownIfFlavorUnchangedModifier) PlanModifyString(ctx contex var stateModel Model diags := req.State.Get(ctx, &stateModel) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } - var stateFlavor = &flavorModel{} - if !(stateModel.Flavor.IsNull() || stateModel.Flavor.IsUnknown()) { + stateFlavor := &flavorModel{} + if !stateModel.Flavor.IsNull() && !stateModel.Flavor.IsUnknown() { diags = stateModel.Flavor.As(ctx, stateFlavor, basetypes.ObjectAsOptions{}) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -66,14 +68,16 @@ func (m useStateForUnknownIfFlavorUnchangedModifier) PlanModifyString(ctx contex var planModel Model diags = req.Plan.Get(ctx, &planModel) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } - var planFlavor = &flavorModel{} - if !(planModel.Flavor.IsNull() || planModel.Flavor.IsUnknown()) { + planFlavor := &flavorModel{} + if !planModel.Flavor.IsNull() && !planModel.Flavor.IsUnknown() { diags = planModel.Flavor.As(ctx, planFlavor, basetypes.ObjectAsOptions{}) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } diff --git a/stackit/internal/services/postgresflex/postgresflex_acc_test.go b/stackit/internal/services/postgresflex/postgresflex_acc_test.go index 122633b36..f2412d327 100644 --- a/stackit/internal/services/postgresflex/postgresflex_acc_test.go +++ b/stackit/internal/services/postgresflex/postgresflex_acc_test.go @@ -9,16 +9,15 @@ import ( "github.com/hashicorp/terraform-plugin-testing/helper/acctest" "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/terraform" - "github.com/stackitcloud/stackit-sdk-go/core/utils" - "github.com/stackitcloud/stackit-sdk-go/core/config" + "github.com/stackitcloud/stackit-sdk-go/core/utils" "github.com/stackitcloud/stackit-sdk-go/services/postgresflex" "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/wait" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/testutil" ) -// Instance resource data +// Instance resource data. var instanceResource = map[string]string{ "project_id": testutil.ProjectId, "name": fmt.Sprintf("tf-acc-%s", acctest.RandStringFromCharSet(7, acctest.CharSetAlphaNum)), @@ -35,14 +34,14 @@ var instanceResource = map[string]string{ "flavor_id": "2.4", } -// User resource data +// User resource data. var userResource = map[string]string{ "username": fmt.Sprintf("tfaccuser%s", acctest.RandStringFromCharSet(4, acctest.CharSetAlpha)), "role": "createdb", "project_id": instanceResource["project_id"], } -// Database resource data +// Database resource data. var databaseResource = map[string]string{ "name": fmt.Sprintf("tfaccdb%s", acctest.RandStringFromCharSet(4, acctest.CharSetAlphaNum)), } @@ -52,6 +51,7 @@ func configResources(backupSchedule string, region *string) string { if region != nil { regionConfig = fmt.Sprintf(`region = %q`, *region) } + return fmt.Sprintf(` %s @@ -321,7 +321,9 @@ func TestAccPostgresFlexFlexResource(t *testing.T) { func testAccCheckPostgresFlexDestroy(s *terraform.State) error { ctx := context.Background() + var client *postgresflex.APIClient + var err error if testutil.PostgresFlexCustomEndpoint == "" { client, err = postgresflex.NewAPIClient() @@ -330,11 +332,13 @@ func testAccCheckPostgresFlexDestroy(s *terraform.State) error { config.WithEndpoint(testutil.PostgresFlexCustomEndpoint), ) } + if err != nil { return fmt.Errorf("creating client: %w", err) } instancesToDestroy := []string{} + for _, rs := range s.RootModule().Resources { if rs.Type != "stackit_postgresflex_instance" { continue @@ -354,16 +358,19 @@ func testAccCheckPostgresFlexDestroy(s *terraform.State) error { if items[i].Id == nil { continue } + if utils.Contains(instancesToDestroy, *items[i].Id) { err := client.ForceDeleteInstanceExecute(ctx, testutil.ProjectId, testutil.Region, *items[i].Id) if err != nil { return fmt.Errorf("deleting instance %s during CheckDestroy: %w", *items[i].Id, err) } + _, err = wait.DeleteInstanceWaitHandler(ctx, client, testutil.ProjectId, testutil.Region, *items[i].Id).WaitWithContext(ctx) if err != nil { return fmt.Errorf("deleting instance %s during CheckDestroy: waiting for deletion %w", *items[i].Id, err) } } } + return nil } diff --git a/stackit/internal/services/postgresflex/user/datasource.go b/stackit/internal/services/postgresflex/user/datasource.go index a0fb8c3bc..229642077 100644 --- a/stackit/internal/services/postgresflex/user/datasource.go +++ b/stackit/internal/services/postgresflex/user/datasource.go @@ -5,20 +5,18 @@ import ( "fmt" "net/http" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - postgresflexUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/postgresflex/utils" - "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/stackitcloud/stackit-sdk-go/services/postgresflex" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + postgresflexUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/postgresflex/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - - "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/services/postgresflex" ) // Ensure the implementation satisfies the expected interfaces. @@ -57,16 +55,20 @@ func (r *userDataSource) Metadata(_ context.Context, req datasource.MetadataRequ // Configure adds the provider configured client to the data source. func (r *userDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { var ok bool + r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) if !ok { return } apiClient := postgresflexUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + r.client = apiClient + tflog.Info(ctx, "Postgres Flex user client configured") } @@ -134,13 +136,15 @@ func (r *userDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, r } // Read refreshes the Terraform state with the latest data. -func (r *userDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (r *userDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model DataSourceModel diags := req.Config.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() instanceId := model.InstanceId.ValueString() userId := model.UserId.ValueString() @@ -163,6 +167,7 @@ func (r *userDataSource) Read(ctx context.Context, req datasource.ReadRequest, r }, ) resp.State.RemoveResource(ctx) + return } @@ -176,9 +181,11 @@ func (r *userDataSource) Read(ctx context.Context, req datasource.ReadRequest, r // Set refreshed state diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "Postgres Flex user read") } @@ -186,9 +193,11 @@ func mapDataSourceFields(userResp *postgresflex.GetUserResponse, model *DataSour if userResp == nil || userResp.Item == nil { return fmt.Errorf("response is nil") } + if model == nil { return fmt.Errorf("model input is nil") } + user := userResp.Item var userId string @@ -199,6 +208,7 @@ func mapDataSourceFields(userResp *postgresflex.GetUserResponse, model *DataSour } else { return fmt.Errorf("user id not present") } + model.Id = utils.BuildInternalTerraformId( model.ProjectId.ValueString(), region, model.InstanceId.ValueString(), userId, ) @@ -212,14 +222,18 @@ func mapDataSourceFields(userResp *postgresflex.GetUserResponse, model *DataSour for _, role := range *user.Roles { roles = append(roles, types.StringValue(role)) } + rolesSet, diags := types.SetValue(types.StringType, roles) if diags.HasError() { return fmt.Errorf("failed to map roles: %w", core.DiagsToError(diags)) } + model.Roles = rolesSet } + model.Host = types.StringPointerValue(user.Host) model.Port = types.Int64PointerValue(user.Port) model.Region = types.StringValue(region) + return nil } diff --git a/stackit/internal/services/postgresflex/user/datasource_test.go b/stackit/internal/services/postgresflex/user/datasource_test.go index ac824ccbd..903dbc52c 100644 --- a/stackit/internal/services/postgresflex/user/datasource_test.go +++ b/stackit/internal/services/postgresflex/user/datasource_test.go @@ -12,6 +12,7 @@ import ( func TestMapDataSourceFields(t *testing.T) { const testRegion = "region" + tests := []struct { description string input *postgresflex.GetUserResponse @@ -126,13 +127,16 @@ func TestMapDataSourceFields(t *testing.T) { InstanceId: tt.expected.InstanceId, UserId: tt.expected.UserId, } + err := mapDataSourceFields(tt.input, state, tt.region) if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(state, &tt.expected) if diff != "" { diff --git a/stackit/internal/services/postgresflex/user/resource.go b/stackit/internal/services/postgresflex/user/resource.go index be364871c..204129a3e 100644 --- a/stackit/internal/services/postgresflex/user/resource.go +++ b/stackit/internal/services/postgresflex/user/resource.go @@ -6,27 +6,25 @@ import ( "net/http" "strings" - postgresflexUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/postgresflex/utils" - "github.com/hashicorp/terraform-plugin-framework-validators/setvalidator" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/stackitcloud/stackit-sdk-go/core/oapierror" "github.com/stackitcloud/stackit-sdk-go/services/postgresflex" "github.com/stackitcloud/stackit-sdk-go/services/postgresflex/wait" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + postgresflexUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/postgresflex/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" ) // Ensure the implementation satisfies the expected interfaces. @@ -64,29 +62,35 @@ type userResource struct { // ModifyPlan implements resource.ResourceWithModifyPlan. // Use the modifier to set the effective region in the current plan. -func (r *userResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform +func (r *userResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { //nolint:gocritic // function signature required by Terraform var configModel Model // skip initial empty configuration to avoid follow-up errors if req.Config.Raw.IsNull() { return } + resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...) + if resp.Diagnostics.HasError() { return } var planModel Model + resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) + if resp.Diagnostics.HasError() { return } utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp) + if resp.Diagnostics.HasError() { return } resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...) + if resp.Diagnostics.HasError() { return } @@ -100,16 +104,20 @@ func (r *userResource) Metadata(_ context.Context, req resource.MetadataRequest, // Configure adds the provider configured client to the resource. func (r *userResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { var ok bool + r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) if !ok { return } apiClient := postgresflexUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + r.client = apiClient + tflog.Info(ctx, "Postgres Flex user client configured") } @@ -215,13 +223,15 @@ func (r *userResource) Schema(_ context.Context, _ resource.SchemaRequest, resp } // Create creates the resource and sets the initial Terraform state. -func (r *userResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *userResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() instanceId := model.InstanceId.ValueString() region := model.Region.ValueString() @@ -230,9 +240,10 @@ func (r *userResource) Create(ctx context.Context, req resource.CreateRequest, r ctx = tflog.SetField(ctx, "region", region) var roles []string - if !(model.Roles.IsNull() || model.Roles.IsUnknown()) { + if !model.Roles.IsNull() && !model.Roles.IsUnknown() { diags = model.Roles.ElementsAs(ctx, &roles, false) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -250,10 +261,12 @@ func (r *userResource) Create(ctx context.Context, req resource.CreateRequest, r core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating user", fmt.Sprintf("Calling API: %v", err)) return } + if userResp == nil || userResp.Item == nil || userResp.Item.Id == nil || *userResp.Item.Id == "" { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating user", "API didn't return user Id. A user might have been created") return } + userId := *userResp.Item.Id ctx = tflog.SetField(ctx, "user_id", userId) @@ -266,20 +279,24 @@ func (r *userResource) Create(ctx context.Context, req resource.CreateRequest, r // Set state to fully populated data diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "Postgres Flex user created") } // Read refreshes the Terraform state with the latest data. -func (r *userResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (r *userResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() instanceId := model.InstanceId.ValueString() userId := model.UserId.ValueString() @@ -296,7 +313,9 @@ func (r *userResource) Read(ctx context.Context, req resource.ReadRequest, resp resp.State.RemoveResource(ctx) return } + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading user", fmt.Sprintf("Calling API: %v", err)) + return } @@ -310,21 +329,25 @@ func (r *userResource) Read(ctx context.Context, req resource.ReadRequest, resp // Set refreshed state diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "Postgres Flex user read") } // Update updates the resource and sets the updated Terraform state on success. -func (r *userResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *userResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { //nolint:gocritic // function signature required by Terraform // Retrieve values from plan var model Model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() instanceId := model.InstanceId.ValueString() userId := model.UserId.ValueString() @@ -338,14 +361,16 @@ func (r *userResource) Update(ctx context.Context, req resource.UpdateRequest, r var stateModel Model diags = req.State.Get(ctx, &stateModel) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } var roles []string - if !(model.Roles.IsNull() || model.Roles.IsUnknown()) { + if !model.Roles.IsNull() && !model.Roles.IsUnknown() { diags = model.Roles.ElementsAs(ctx, &roles, false) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -381,18 +406,21 @@ func (r *userResource) Update(ctx context.Context, req resource.UpdateRequest, r // Set state to fully populated data diags = resp.State.Set(ctx, stateModel) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "Postgres Flex user updated") } // Delete deletes the resource and removes the Terraform state on success. -func (r *userResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform +func (r *userResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { //nolint:gocritic // function signature required by Terraform // Retrieve values from plan var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -411,16 +439,18 @@ func (r *userResource) Delete(ctx context.Context, req resource.DeleteRequest, r if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting user", fmt.Sprintf("Calling API: %v", err)) } + _, err = wait.DeleteUserWaitHandler(ctx, r.client, projectId, region, instanceId, userId).WaitWithContext(ctx) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting user", fmt.Sprintf("Instance deletion waiting: %v", err)) return } + tflog.Info(ctx, "Postgres Flex user deleted") } // ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: project_id,zone_id,record_set_id +// The expected format of the resource import identifier is: project_id,zone_id,record_set_id. func (r *userResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { idParts := strings.Split(req.ID, core.Separator) if len(idParts) != 4 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" || idParts[3] == "" { @@ -428,6 +458,7 @@ func (r *userResource) ImportState(ctx context.Context, req resource.ImportState "Error importing user", fmt.Sprintf("Expected import identifier with format [project_id],[region],[instance_id],[user_id], got %q", req.ID), ) + return } @@ -446,14 +477,17 @@ func mapFieldsCreate(userResp *postgresflex.CreateUserResponse, model *Model, re if userResp == nil || userResp.Item == nil { return fmt.Errorf("response is nil") } + if model == nil { return fmt.Errorf("model input is nil") } + user := userResp.Item if user.Id == nil { return fmt.Errorf("user id not present") } + userId := *user.Id model.Id = utils.BuildInternalTerraformId( model.ProjectId.ValueString(), region, model.InstanceId.ValueString(), userId, @@ -464,6 +498,7 @@ func mapFieldsCreate(userResp *postgresflex.CreateUserResponse, model *Model, re if user.Password == nil { return fmt.Errorf("user password not present") } + model.Password = types.StringValue(*user.Password) if user.Roles == nil { @@ -473,16 +508,20 @@ func mapFieldsCreate(userResp *postgresflex.CreateUserResponse, model *Model, re for _, role := range *user.Roles { roles = append(roles, types.StringValue(role)) } + rolesSet, diags := types.SetValue(types.StringType, roles) if diags.HasError() { return fmt.Errorf("failed to map roles: %w", core.DiagsToError(diags)) } + model.Roles = rolesSet } + model.Host = types.StringPointerValue(user.Host) model.Port = types.Int64PointerValue(user.Port) model.Uri = types.StringPointerValue(user.Uri) model.Region = types.StringValue(region) + return nil } @@ -490,9 +529,11 @@ func mapFields(userResp *postgresflex.GetUserResponse, model *Model, region stri if userResp == nil || userResp.Item == nil { return fmt.Errorf("response is nil") } + if model == nil { return fmt.Errorf("model input is nil") } + user := userResp.Item var userId string @@ -503,6 +544,7 @@ func mapFields(userResp *postgresflex.GetUserResponse, model *Model, region stri } else { return fmt.Errorf("user id not present") } + model.Id = utils.BuildInternalTerraformId( model.ProjectId.ValueString(), region, model.InstanceId.ValueString(), userId, ) @@ -516,15 +558,19 @@ func mapFields(userResp *postgresflex.GetUserResponse, model *Model, region stri for _, role := range *user.Roles { roles = append(roles, types.StringValue(role)) } + rolesSet, diags := types.SetValue(types.StringType, roles) if diags.HasError() { return fmt.Errorf("failed to map roles: %w", core.DiagsToError(diags)) } + model.Roles = rolesSet } + model.Host = types.StringPointerValue(user.Host) model.Port = types.Int64PointerValue(user.Port) model.Region = types.StringValue(region) + return nil } @@ -532,6 +578,7 @@ func toCreatePayload(model *Model, roles []string) (*postgresflex.CreateUserPayl if model == nil { return nil, fmt.Errorf("nil model") } + if roles == nil { return nil, fmt.Errorf("nil roles") } @@ -546,6 +593,7 @@ func toUpdatePayload(model *Model, roles []string) (*postgresflex.UpdateUserPayl if model == nil { return nil, fmt.Errorf("nil model") } + if roles == nil { return nil, fmt.Errorf("nil roles") } diff --git a/stackit/internal/services/postgresflex/user/resource_test.go b/stackit/internal/services/postgresflex/user/resource_test.go index b5c137162..a80ba6af0 100644 --- a/stackit/internal/services/postgresflex/user/resource_test.go +++ b/stackit/internal/services/postgresflex/user/resource_test.go @@ -12,6 +12,7 @@ import ( func TestMapFieldsCreate(t *testing.T) { const testRegion = "region" + tests := []struct { description string input *postgresflex.CreateUserResponse @@ -150,13 +151,16 @@ func TestMapFieldsCreate(t *testing.T) { ProjectId: tt.expected.ProjectId, InstanceId: tt.expected.InstanceId, } + err := mapFieldsCreate(tt.input, state, tt.region) if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(state, &tt.expected) if diff != "" { @@ -169,6 +173,7 @@ func TestMapFieldsCreate(t *testing.T) { func TestMapFields(t *testing.T) { const testRegion = "region" + tests := []struct { description string input *postgresflex.GetUserResponse @@ -283,13 +288,16 @@ func TestMapFields(t *testing.T) { InstanceId: tt.expected.InstanceId, UserId: tt.expected.UserId, } + err := mapFields(tt.input, state, tt.region) if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(state, &tt.expected) if diff != "" { @@ -373,9 +381,11 @@ func TestToCreatePayload(t *testing.T) { if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(output, tt.expected) if diff != "" { @@ -456,9 +466,11 @@ func TestToUpdatePayload(t *testing.T) { if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(output, tt.expected) if diff != "" { diff --git a/stackit/internal/services/postgresflex/utils/util.go b/stackit/internal/services/postgresflex/utils/util.go index 478bb1426..29fd8e353 100644 --- a/stackit/internal/services/postgresflex/utils/util.go +++ b/stackit/internal/services/postgresflex/utils/util.go @@ -21,6 +21,7 @@ func ConfigureClient(ctx context.Context, providerData *core.ProviderData, diags } else { apiClientConfigOptions = append(apiClientConfigOptions, config.WithRegion(providerData.GetRegion())) } + apiClient, err := postgresflex.NewAPIClient(apiClientConfigOptions...) if err != nil { core.LogAndAddError(ctx, diags, "Error configuring API client", fmt.Sprintf("Configuring client: %v. This is an error related to the provider configuration, not to the resource configuration", err)) diff --git a/stackit/internal/services/postgresflex/utils/util_test.go b/stackit/internal/services/postgresflex/utils/util_test.go index 4af08da6a..24ddac915 100644 --- a/stackit/internal/services/postgresflex/utils/util_test.go +++ b/stackit/internal/services/postgresflex/utils/util_test.go @@ -22,6 +22,7 @@ const ( func TestConfigureClient(t *testing.T) { /* mock authentication by setting service account token env variable */ os.Clearenv() + err := os.Setenv(sdkClients.ServiceAccountToken, "mock-val") if err != nil { t.Errorf("error setting env variable: %v", err) @@ -30,6 +31,7 @@ func TestConfigureClient(t *testing.T) { type args struct { providerData *core.ProviderData } + tests := []struct { name string args args @@ -51,6 +53,7 @@ func TestConfigureClient(t *testing.T) { if err != nil { t.Errorf("error configuring client: %v", err) } + return apiClient }(), wantErr: false, @@ -71,6 +74,7 @@ func TestConfigureClient(t *testing.T) { if err != nil { t.Errorf("error configuring client: %v", err) } + return apiClient }(), wantErr: false, @@ -82,6 +86,7 @@ func TestConfigureClient(t *testing.T) { diags := diag.Diagnostics{} actual := ConfigureClient(ctx, tt.args.providerData, &diags) + if diags.HasError() != tt.wantErr { t.Errorf("ConfigureClient() error = %v, want %v", diags.HasError(), tt.wantErr) } diff --git a/stackit/internal/services/rabbitmq/credential/datasource.go b/stackit/internal/services/rabbitmq/credential/datasource.go index 428a93067..1379b2784 100644 --- a/stackit/internal/services/rabbitmq/credential/datasource.go +++ b/stackit/internal/services/rabbitmq/credential/datasource.go @@ -5,19 +5,17 @@ import ( "fmt" "net/http" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - rabbitmqUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/rabbitmq/utils" - "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/stackitcloud/stackit-sdk-go/services/rabbitmq" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + rabbitmqUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/rabbitmq/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - - "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/services/rabbitmq" ) // Ensure the implementation satisfies the expected interfaces. @@ -48,10 +46,13 @@ func (r *credentialDataSource) Configure(ctx context.Context, req datasource.Con } apiClient := rabbitmqUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + r.client = apiClient + tflog.Info(ctx, "RabbitMQ credential client configured") } @@ -136,13 +137,15 @@ func (r *credentialDataSource) Schema(_ context.Context, _ datasource.SchemaRequ } // Read refreshes the Terraform state with the latest data. -func (r *credentialDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (r *credentialDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.Config.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() instanceId := model.InstanceId.ValueString() credentialId := model.CredentialId.ValueString() @@ -163,6 +166,7 @@ func (r *credentialDataSource) Read(ctx context.Context, req datasource.ReadRequ }, ) resp.State.RemoveResource(ctx) + return } @@ -176,8 +180,10 @@ func (r *credentialDataSource) Read(ctx context.Context, req datasource.ReadRequ // Set refreshed state diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "RabbitMQ credential read") } diff --git a/stackit/internal/services/rabbitmq/credential/resource.go b/stackit/internal/services/rabbitmq/credential/resource.go index b16e47df9..28b76c662 100644 --- a/stackit/internal/services/rabbitmq/credential/resource.go +++ b/stackit/internal/services/rabbitmq/credential/resource.go @@ -6,24 +6,22 @@ import ( "net/http" "strings" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - rabbitmqUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/rabbitmq/utils" - - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/stackitcloud/stackit-sdk-go/core/oapierror" "github.com/stackitcloud/stackit-sdk-go/services/rabbitmq" "github.com/stackitcloud/stackit-sdk-go/services/rabbitmq/wait" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + rabbitmqUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/rabbitmq/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" ) // Ensure the implementation satisfies the expected interfaces. @@ -73,10 +71,13 @@ func (r *credentialResource) Configure(ctx context.Context, req resource.Configu } apiClient := rabbitmqUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + r.client = apiClient + tflog.Info(ctx, "RabbitMQ credential client configured") } @@ -175,13 +176,15 @@ func (r *credentialResource) Schema(_ context.Context, _ resource.SchemaRequest, } // Create creates the resource and sets the initial Terraform state. -func (r *credentialResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *credentialResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() instanceId := model.InstanceId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) @@ -193,10 +196,12 @@ func (r *credentialResource) Create(ctx context.Context, req resource.CreateRequ core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credential", fmt.Sprintf("Calling API: %v", err)) return } + if credentialsResp.Id == nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credential", "Got empty credential id") return } + credentialId := *credentialsResp.Id ctx = tflog.SetField(ctx, "credential_id", credentialId) @@ -212,22 +217,27 @@ func (r *credentialResource) Create(ctx context.Context, req resource.CreateRequ core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credential", fmt.Sprintf("Processing API payload: %v", err)) return } + diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "RabbitMQ credential created") } // Read refreshes the Terraform state with the latest data. -func (r *credentialResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (r *credentialResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() instanceId := model.InstanceId.ValueString() credentialId := model.CredentialId.ValueString() @@ -242,7 +252,9 @@ func (r *credentialResource) Read(ctx context.Context, req resource.ReadRequest, resp.State.RemoveResource(ctx) return } + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading credential", fmt.Sprintf("Calling API: %v", err)) + return } @@ -256,23 +268,26 @@ func (r *credentialResource) Read(ctx context.Context, req resource.ReadRequest, // Set refreshed state diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "RabbitMQ credential read") } // Update updates the resource and sets the updated Terraform state on success. -func (r *credentialResource) Update(ctx context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *credentialResource) Update(ctx context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { //nolint:gocritic // function signature required by Terraform // Update shouldn't be called core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating credential", "Credential can't be updated") } // Delete deletes the resource and removes the Terraform state on success. -func (r *credentialResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform +func (r *credentialResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -289,16 +304,18 @@ func (r *credentialResource) Delete(ctx context.Context, req resource.DeleteRequ if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting credential", fmt.Sprintf("Calling API: %v", err)) } + _, err = wait.DeleteCredentialsWaitHandler(ctx, r.client, projectId, instanceId, credentialId).WaitWithContext(ctx) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting credential", fmt.Sprintf("Instance deletion waiting: %v", err)) return } + tflog.Info(ctx, "RabbitMQ credential deleted") } // ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: project_id,instance_id,credential_id +// The expected format of the resource import identifier is: project_id,instance_id,credential_id. func (r *credentialResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { idParts := strings.Split(req.ID, core.Separator) if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" { @@ -306,6 +323,7 @@ func (r *credentialResource) ImportState(ctx context.Context, req resource.Impor "Error importing credential", fmt.Sprintf("Expected import identifier with format [project_id],[instance_id],[credential_id], got %q", req.ID), ) + return } @@ -319,12 +337,15 @@ func mapFields(ctx context.Context, credentialsResp *rabbitmq.CredentialsRespons if credentialsResp == nil { return fmt.Errorf("response input is nil") } + if credentialsResp.Raw == nil { return fmt.Errorf("response credentials raw is nil") } + if model == nil { return fmt.Errorf("model input is nil") } + credentials := credentialsResp.Raw.Credentials var credentialId string @@ -345,10 +366,12 @@ func mapFields(ctx context.Context, credentialsResp *rabbitmq.CredentialsRespons if err != nil { return err } + modelHttpApiUris, err := utils.ListValuetoStringSlice(model.HttpAPIURIs) if err != nil { return err } + modelUris, err := utils.ListValuetoStringSlice(model.Uris) if err != nil { return err @@ -357,6 +380,7 @@ func mapFields(ctx context.Context, credentialsResp *rabbitmq.CredentialsRespons model.Hosts = types.ListNull(types.StringType) model.Uris = types.ListNull(types.StringType) model.HttpAPIURIs = types.ListNull(types.StringType) + if credentials != nil { if credentials.Hosts != nil { respHosts := *credentials.Hosts @@ -369,7 +393,9 @@ func mapFields(ctx context.Context, credentialsResp *rabbitmq.CredentialsRespons model.Hosts = hostsTF } + model.Host = types.StringPointerValue(credentials.Host) + if credentials.HttpApiUris != nil { respHttpApiUris := *credentials.HttpApiUris @@ -403,5 +429,6 @@ func mapFields(ctx context.Context, credentialsResp *rabbitmq.CredentialsRespons model.Uri = types.StringPointerValue(credentials.Uri) model.Username = types.StringPointerValue(credentials.Username) } + return nil } diff --git a/stackit/internal/services/rabbitmq/credential/resource_test.go b/stackit/internal/services/rabbitmq/credential/resource_test.go index 5492d2fef..dfe465012 100644 --- a/stackit/internal/services/rabbitmq/credential/resource_test.go +++ b/stackit/internal/services/rabbitmq/credential/resource_test.go @@ -266,9 +266,11 @@ func TestMapFields(t *testing.T) { if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(tt.state, tt.expected) if diff != "" { diff --git a/stackit/internal/services/rabbitmq/instance/datasource.go b/stackit/internal/services/rabbitmq/instance/datasource.go index 1f7439a31..907a553bf 100644 --- a/stackit/internal/services/rabbitmq/instance/datasource.go +++ b/stackit/internal/services/rabbitmq/instance/datasource.go @@ -5,19 +5,17 @@ import ( "fmt" "net/http" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - rabbitmqUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/rabbitmq/utils" - "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/stackitcloud/stackit-sdk-go/services/rabbitmq" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + rabbitmqUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/rabbitmq/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - - "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/stackitcloud/stackit-sdk-go/services/rabbitmq" ) // Ensure the implementation satisfies the expected interfaces. @@ -48,10 +46,13 @@ func (r *instanceDataSource) Configure(ctx context.Context, req datasource.Confi } apiClient := rabbitmqUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + r.client = apiClient + tflog.Info(ctx, "RabbitMQ instance client configured") } @@ -204,13 +205,15 @@ func (r *instanceDataSource) Schema(_ context.Context, _ datasource.SchemaReques } // Read refreshes the Terraform state with the latest data. -func (r *instanceDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (r *instanceDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.Config.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() instanceId := model.InstanceId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) @@ -229,6 +232,7 @@ func (r *instanceDataSource) Read(ctx context.Context, req datasource.ReadReques }, ) resp.State.RemoveResource(ctx) + return } @@ -248,8 +252,10 @@ func (r *instanceDataSource) Read(ctx context.Context, req datasource.ReadReques // Set refreshed state diags = resp.State.Set(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "RabbitMQ instance read") } diff --git a/stackit/internal/services/rabbitmq/instance/resource.go b/stackit/internal/services/rabbitmq/instance/resource.go index d012858c9..0e8f0ad35 100644 --- a/stackit/internal/services/rabbitmq/instance/resource.go +++ b/stackit/internal/services/rabbitmq/instance/resource.go @@ -6,28 +6,25 @@ import ( "net/http" "strings" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - - rabbitmqUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/rabbitmq/utils" - "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" "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/services/rabbitmq" "github.com/stackitcloud/stackit-sdk-go/services/rabbitmq/wait" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + rabbitmqUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/rabbitmq/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" ) // Ensure the implementation satisfies the expected interfaces. @@ -53,7 +50,7 @@ type Model struct { PlanId types.String `tfsdk:"plan_id"` } -// Struct corresponding to DataSourceModel.Parameters +// Struct corresponding to DataSourceModel.Parameters. type parametersModel struct { SgwAcl types.String `tfsdk:"sgw_acl"` ConsumerTimeout types.Int64 `tfsdk:"consumer_timeout"` @@ -70,7 +67,7 @@ type parametersModel struct { TlsProtocols types.String `tfsdk:"tls_protocols"` } -// Types corresponding to parametersModel +// Types corresponding to parametersModel. var parametersTypes = map[string]attr.Type{ "sgw_acl": basetypes.StringType{}, "consumer_timeout": basetypes.Int64Type{}, @@ -110,10 +107,13 @@ func (r *instanceResource) Configure(ctx context.Context, req resource.Configure } apiClient := rabbitmqUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + r.client = apiClient + tflog.Info(ctx, "RabbitMQ instance client configured") } @@ -318,21 +318,24 @@ func (r *instanceResource) Schema(_ context.Context, _ resource.SchemaRequest, r } // Create creates the resource and sets the initial Terraform state. -func (r *instanceResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *instanceResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) var parameters *parametersModel - if !(model.Parameters.IsNull() || model.Parameters.IsUnknown()) { + if !model.Parameters.IsNull() && !model.Parameters.IsUnknown() { parameters = ¶metersModel{} diags = model.Parameters.As(ctx, parameters, basetypes.ObjectAsOptions{}) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -356,6 +359,7 @@ func (r *instanceResource) Create(ctx context.Context, req resource.CreateReques core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Calling API: %v", err)) return } + instanceId := *createResp.InstanceId ctx = tflog.SetField(ctx, "instance_id", instanceId) waitResp, err := wait.CreateInstanceWaitHandler(ctx, r.client, projectId, instanceId).WaitWithContext(ctx) @@ -374,20 +378,24 @@ func (r *instanceResource) Create(ctx context.Context, req resource.CreateReques // Set state to fully populated data diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "RabbitMQ instance created") } // Read refreshes the Terraform state with the latest data. -func (r *instanceResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (r *instanceResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() instanceId := model.InstanceId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) @@ -400,7 +408,9 @@ func (r *instanceResource) Read(ctx context.Context, req resource.ReadRequest, r resp.State.RemoveResource(ctx) return } + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Calling API: %v", err)) + return } @@ -421,30 +431,35 @@ func (r *instanceResource) Read(ctx context.Context, req resource.ReadRequest, r // Set refreshed state diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "RabbitMQ instance read") } // Update updates the resource and sets the updated Terraform state on success. -func (r *instanceResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *instanceResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() instanceId := model.InstanceId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) ctx = tflog.SetField(ctx, "instance_id", instanceId) var parameters *parametersModel - if !(model.Parameters.IsNull() || model.Parameters.IsUnknown()) { + if !model.Parameters.IsNull() && !model.Parameters.IsUnknown() { parameters = ¶metersModel{} diags = model.Parameters.As(ctx, parameters, basetypes.ObjectAsOptions{}) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -468,6 +483,7 @@ func (r *instanceResource) Update(ctx context.Context, req resource.UpdateReques core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Calling API: %v", err)) return } + waitResp, err := wait.PartialUpdateInstanceWaitHandler(ctx, r.client, projectId, instanceId).WaitWithContext(ctx) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Instance update waiting: %v", err)) @@ -483,21 +499,25 @@ func (r *instanceResource) Update(ctx context.Context, req resource.UpdateReques diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "RabbitMQ instance updated") } // Delete deletes the resource and removes the Terraform state on success. -func (r *instanceResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform +func (r *instanceResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { //nolint:gocritic // function signature required by Terraform // Retrieve values from state var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() instanceId := model.InstanceId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) @@ -509,16 +529,18 @@ func (r *instanceResource) Delete(ctx context.Context, req resource.DeleteReques core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting instance", fmt.Sprintf("Calling API: %v", err)) return } + _, err = wait.DeleteInstanceWaitHandler(ctx, r.client, projectId, instanceId).WaitWithContext(ctx) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting instance", fmt.Sprintf("Instance deletion waiting: %v", err)) return } + tflog.Info(ctx, "RabbitMQ instance deleted") } // ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: project_id,instance_id +// The expected format of the resource import identifier is: project_id,instance_id. func (r *instanceResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { idParts := strings.Split(req.ID, core.Separator) @@ -527,6 +549,7 @@ func (r *instanceResource) ImportState(ctx context.Context, req resource.ImportS "Error importing instance", fmt.Sprintf("Expected import identifier with format: [project_id],[instance_id] Got: %q", req.ID), ) + return } @@ -539,6 +562,7 @@ func mapFields(instance *rabbitmq.Instance, model *Model) error { if instance == nil { return fmt.Errorf("response input is nil") } + if model == nil { return fmt.Errorf("model input is nil") } @@ -569,15 +593,19 @@ func mapFields(instance *rabbitmq.Instance, model *Model) error { if err != nil { return fmt.Errorf("mapping parameters: %w", err) } + model.Parameters = parameters } + return nil } func mapParameters(params map[string]interface{}) (types.Object, error) { attributes := map[string]attr.Value{} + for attribute := range parametersTypes { var valueInterface interface{} + var ok bool // This replacement is necessary because Terraform does not allow hyphens in attribute names @@ -588,6 +616,7 @@ func mapParameters(params map[string]interface{}) (types.Object, error) { } else { valueInterface, ok = params[attribute] } + if !ok { // All fields are optional, so this is ok // Set the value as nil, will be handled accordingly @@ -595,6 +624,7 @@ func mapParameters(params map[string]interface{}) (types.Object, error) { } var value attr.Value + switch parametersTypes[attribute].(type) { default: return types.ObjectNull(parametersTypes), fmt.Errorf("found unexpected attribute type '%T'", parametersTypes[attribute]) @@ -606,6 +636,7 @@ func mapParameters(params map[string]interface{}) (types.Object, error) { if !ok { return types.ObjectNull(parametersTypes), fmt.Errorf("found attribute '%s' of type %T, failed to assert as string", attribute, valueInterface) } + value = types.StringValue(valueString) } case basetypes.BoolType: @@ -616,6 +647,7 @@ func mapParameters(params map[string]interface{}) (types.Object, error) { if !ok { return types.ObjectNull(parametersTypes), fmt.Errorf("found attribute '%s' of type %T, failed to assert as bool", attribute, valueInterface) } + value = types.BoolValue(valueBool) } case basetypes.Int64Type: @@ -625,6 +657,7 @@ func mapParameters(params map[string]interface{}) (types.Object, error) { // This may be int64, int32, int or float64 // We try to assert all 4 var valueInt64 int64 + switch temp := valueInterface.(type) { default: return types.ObjectNull(parametersTypes), fmt.Errorf("found attribute '%s' of type %T, failed to assert as int", attribute, valueInterface) @@ -637,6 +670,7 @@ func mapParameters(params map[string]interface{}) (types.Object, error) { case float64: valueInt64 = int64(temp) } + value = types.Int64Value(valueInt64) } case basetypes.ListType: // Assumed to be a list of strings @@ -646,6 +680,7 @@ func mapParameters(params map[string]interface{}) (types.Object, error) { // This may be []string{} or []interface{} // We try to assert all 2 var valueList []attr.Value + switch temp := valueInterface.(type) { default: return types.ObjectNull(parametersTypes), fmt.Errorf("found attribute '%s' of type %T, failed to assert as array of interface", attribute, valueInterface) @@ -659,16 +694,20 @@ func mapParameters(params map[string]interface{}) (types.Object, error) { if !ok { return types.ObjectNull(parametersTypes), fmt.Errorf("found attribute '%s' with element '%s' of type %T, failed to assert as string", attribute, x, x) } + valueList = append(valueList, types.StringValue(xString)) } } + temp2, diags := types.ListValue(types.StringType, valueList) if diags.HasError() { return types.ObjectNull(parametersTypes), fmt.Errorf("failed to map %s: %w", attribute, core.DiagsToError(diags)) } + value = temp2 } } + attributes[attribute] = value } @@ -676,6 +715,7 @@ func mapParameters(params map[string]interface{}) (types.Object, error) { if diags.HasError() { return types.ObjectNull(parametersTypes), fmt.Errorf("failed to create object: %w", core.DiagsToError(diags)) } + return output, nil } @@ -716,6 +756,7 @@ func toInstanceParams(parameters *parametersModel) (*rabbitmq.InstanceParameters if parameters == nil { return nil, nil } + payloadParams := &rabbitmq.InstanceParameters{} payloadParams.SgwAcl = conversion.StringValueToPointer(parameters.SgwAcl) @@ -729,6 +770,7 @@ func toInstanceParams(parameters *parametersModel) (*rabbitmq.InstanceParameters payloadParams.TlsProtocols = rabbitmq.InstanceParametersGetTlsProtocolsAttributeType(conversion.StringValueToPointer(parameters.TlsProtocols)) var err error + payloadParams.Plugins, err = conversion.StringListToPointer(parameters.Plugins) if err != nil { return nil, fmt.Errorf("converting plugins: %w", err) @@ -754,6 +796,7 @@ func toInstanceParams(parameters *parametersModel) (*rabbitmq.InstanceParameters func (r *instanceResource) loadPlanId(ctx context.Context, model *Model) error { projectId := model.ProjectId.ValueString() + res, err := r.client.ListOfferings(ctx, projectId).Execute() if err != nil { return fmt.Errorf("getting RabbitMQ offerings: %w", err) @@ -764,21 +807,25 @@ func (r *instanceResource) loadPlanId(ctx context.Context, model *Model) error { availableVersions := "" availablePlanNames := "" isValidVersion := false + for _, offer := range *res.Offerings { if !strings.EqualFold(*offer.Version, version) { availableVersions = fmt.Sprintf("%s\n- %s", availableVersions, *offer.Version) continue } + isValidVersion = true for _, plan := range *offer.Plans { if plan.Name == nil { continue } + if strings.EqualFold(*plan.Name, planName) && plan.Id != nil { model.PlanId = types.StringPointerValue(plan.Id) return nil } + availablePlanNames = fmt.Sprintf("%s\n- %s", availablePlanNames, *plan.Name) } } @@ -786,12 +833,14 @@ func (r *instanceResource) loadPlanId(ctx context.Context, model *Model) error { if !isValidVersion { return fmt.Errorf("couldn't find version '%s', available versions are: %s", version, availableVersions) } + return fmt.Errorf("couldn't find plan_name '%s' for version %s, available names are: %s", planName, version, availablePlanNames) } func loadPlanNameAndVersion(ctx context.Context, client *rabbitmq.APIClient, model *Model) error { projectId := model.ProjectId.ValueString() planId := model.PlanId.ValueString() + res, err := client.ListOfferings(ctx, projectId).Execute() if err != nil { return fmt.Errorf("getting RabbitMQ offerings: %w", err) @@ -802,6 +851,7 @@ func loadPlanNameAndVersion(ctx context.Context, client *rabbitmq.APIClient, mod if strings.EqualFold(*plan.Id, planId) && plan.Id != nil { model.PlanName = types.StringPointerValue(plan.Name) model.Version = types.StringPointerValue(offer.Version) + return nil } } diff --git a/stackit/internal/services/rabbitmq/instance/resource_test.go b/stackit/internal/services/rabbitmq/instance/resource_test.go index 1b42d27c1..4d263d943 100644 --- a/stackit/internal/services/rabbitmq/instance/resource_test.go +++ b/stackit/internal/services/rabbitmq/instance/resource_test.go @@ -163,13 +163,16 @@ func TestMapFields(t *testing.T) { ProjectId: tt.expected.ProjectId, InstanceId: tt.expected.InstanceId, } + err := mapFields(tt.input, state) if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(state, &tt.expected) if diff != "" { @@ -243,22 +246,27 @@ func TestToCreatePayload(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { var parameters *parametersModel + if tt.input != nil { - if !(tt.input.Parameters.IsNull() || tt.input.Parameters.IsUnknown()) { + if !tt.input.Parameters.IsNull() && !tt.input.Parameters.IsUnknown() { parameters = ¶metersModel{} + diags := tt.input.Parameters.As(context.Background(), parameters, basetypes.ObjectAsOptions{}) if diags.HasError() { t.Fatalf("Error converting parameters: %v", diags.Errors()) } } } + output, err := toCreatePayload(tt.input, parameters) if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(output, tt.expected) if diff != "" { @@ -327,22 +335,27 @@ func TestToUpdatePayload(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { var parameters *parametersModel + if tt.input != nil { - if !(tt.input.Parameters.IsNull() || tt.input.Parameters.IsUnknown()) { + if !tt.input.Parameters.IsNull() && !tt.input.Parameters.IsUnknown() { parameters = ¶metersModel{} + diags := tt.input.Parameters.As(context.Background(), parameters, basetypes.ObjectAsOptions{}) if diags.HasError() { t.Fatalf("Error converting parameters: %v", diags.Errors()) } } } + output, err := toUpdatePayload(tt.input, parameters) if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(output, tt.expected) if diff != "" { diff --git a/stackit/internal/services/rabbitmq/rabbitmq_acc_test.go b/stackit/internal/services/rabbitmq/rabbitmq_acc_test.go index ff9d3f417..85bf61858 100644 --- a/stackit/internal/services/rabbitmq/rabbitmq_acc_test.go +++ b/stackit/internal/services/rabbitmq/rabbitmq_acc_test.go @@ -17,7 +17,7 @@ import ( "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/testutil" ) -// Instance resource data +// Instance resource data. var instanceResource = map[string]string{ "project_id": testutil.ProjectId, "name": testutil.ResourceNameWithDateTime("rabbitmq"), @@ -40,6 +40,7 @@ func parametersConfig(params map[string]string) string { "tls_ciphers", } parameters := "parameters = {" + for k, v := range params { if utils.Contains(nonStringParams, k) { parameters += fmt.Sprintf("%s = %s\n", k, v) @@ -47,7 +48,9 @@ func parametersConfig(params map[string]string) string { parameters += fmt.Sprintf("%s = %q\n", k, v) } } + parameters += "\n}" + return parameters } @@ -199,6 +202,7 @@ func TestAccRabbitMQResource(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute instance_id") } + return fmt.Sprintf("%s,%s", testutil.ProjectId, instanceId), nil }, ImportState: true, @@ -219,6 +223,7 @@ func TestAccRabbitMQResource(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute credential_id") } + return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, instanceId, credentialId), nil }, ImportState: true, @@ -245,7 +250,9 @@ func TestAccRabbitMQResource(t *testing.T) { func testAccCheckRabbitMQDestroy(s *terraform.State) error { ctx := context.Background() + var client *rabbitmq.APIClient + var err error if testutil.RabbitMQCustomEndpoint == "" { client, err = rabbitmq.NewAPIClient( @@ -256,11 +263,13 @@ func testAccCheckRabbitMQDestroy(s *terraform.State) error { config.WithEndpoint(testutil.RabbitMQCustomEndpoint), ) } + if err != nil { return fmt.Errorf("creating client: %w", err) } instancesToDestroy := []string{} + for _, rs := range s.RootModule().Resources { if rs.Type != "stackit_rabbitmq_instance" { continue @@ -280,12 +289,14 @@ func testAccCheckRabbitMQDestroy(s *terraform.State) error { if instances[i].InstanceId == nil { continue } + if utils.Contains(instancesToDestroy, *instances[i].InstanceId) { if !checkInstanceDeleteSuccess(&instances[i]) { err := client.DeleteInstanceExecute(ctx, testutil.ProjectId, *instances[i].InstanceId) if err != nil { return fmt.Errorf("destroying instance %s during CheckDestroy: %w", *instances[i].InstanceId, err) } + _, err = wait.DeleteInstanceWaitHandler(ctx, client, testutil.ProjectId, *instances[i].InstanceId).WaitWithContext(ctx) if err != nil { return fmt.Errorf("destroying instance %s during CheckDestroy: waiting for deletion %w", *instances[i].InstanceId, err) @@ -293,6 +304,7 @@ func testAccCheckRabbitMQDestroy(s *terraform.State) error { } } } + return nil } @@ -308,5 +320,6 @@ func checkInstanceDeleteSuccess(i *rabbitmq.Instance) bool { return false } } + return true } diff --git a/stackit/internal/services/rabbitmq/utils/util.go b/stackit/internal/services/rabbitmq/utils/util.go index 1f7a1c099..fcf725664 100644 --- a/stackit/internal/services/rabbitmq/utils/util.go +++ b/stackit/internal/services/rabbitmq/utils/util.go @@ -21,6 +21,7 @@ func ConfigureClient(ctx context.Context, providerData *core.ProviderData, diags } else { apiClientConfigOptions = append(apiClientConfigOptions, config.WithRegion(providerData.GetRegion())) } + apiClient, err := rabbitmq.NewAPIClient(apiClientConfigOptions...) if err != nil { core.LogAndAddError(ctx, diags, "Error configuring API client", fmt.Sprintf("Configuring client: %v. This is an error related to the provider configuration, not to the resource configuration", err)) diff --git a/stackit/internal/services/rabbitmq/utils/util_test.go b/stackit/internal/services/rabbitmq/utils/util_test.go index 105f71d92..7ea8a3111 100644 --- a/stackit/internal/services/rabbitmq/utils/util_test.go +++ b/stackit/internal/services/rabbitmq/utils/util_test.go @@ -22,6 +22,7 @@ const ( func TestConfigureClient(t *testing.T) { /* mock authentication by setting service account token env variable */ os.Clearenv() + err := os.Setenv(sdkClients.ServiceAccountToken, "mock-val") if err != nil { t.Errorf("error setting env variable: %v", err) @@ -30,6 +31,7 @@ func TestConfigureClient(t *testing.T) { type args struct { providerData *core.ProviderData } + tests := []struct { name string args args @@ -51,6 +53,7 @@ func TestConfigureClient(t *testing.T) { if err != nil { t.Errorf("error configuring client: %v", err) } + return apiClient }(), wantErr: false, @@ -71,6 +74,7 @@ func TestConfigureClient(t *testing.T) { if err != nil { t.Errorf("error configuring client: %v", err) } + return apiClient }(), wantErr: false, @@ -82,6 +86,7 @@ func TestConfigureClient(t *testing.T) { diags := diag.Diagnostics{} actual := ConfigureClient(ctx, tt.args.providerData, &diags) + if diags.HasError() != tt.wantErr { t.Errorf("ConfigureClient() error = %v, want %v", diags.HasError(), tt.wantErr) } diff --git a/stackit/internal/services/redis/credential/datasource.go b/stackit/internal/services/redis/credential/datasource.go index 17f257755..b60c292f6 100644 --- a/stackit/internal/services/redis/credential/datasource.go +++ b/stackit/internal/services/redis/credential/datasource.go @@ -5,19 +5,17 @@ import ( "fmt" "net/http" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - redisUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/redis/utils" - "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/stackitcloud/stackit-sdk-go/services/redis" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + redisUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/redis/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - - "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/services/redis" ) // Ensure the implementation satisfies the expected interfaces. @@ -48,10 +46,13 @@ func (r *credentialDataSource) Configure(ctx context.Context, req datasource.Con } apiClient := redisUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + r.client = apiClient + tflog.Info(ctx, "Redis credential client configured") } @@ -127,13 +128,15 @@ func (r *credentialDataSource) Schema(_ context.Context, _ datasource.SchemaRequ } // Read refreshes the Terraform state with the latest data. -func (r *credentialDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (r *credentialDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.Config.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() instanceId := model.InstanceId.ValueString() credentialId := model.CredentialId.ValueString() @@ -154,6 +157,7 @@ func (r *credentialDataSource) Read(ctx context.Context, req datasource.ReadRequ }, ) resp.State.RemoveResource(ctx) + return } @@ -167,8 +171,10 @@ func (r *credentialDataSource) Read(ctx context.Context, req datasource.ReadRequ // Set refreshed state diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "Redis credential read") } diff --git a/stackit/internal/services/redis/credential/resource.go b/stackit/internal/services/redis/credential/resource.go index bc5f2b31c..f47f50dc2 100644 --- a/stackit/internal/services/redis/credential/resource.go +++ b/stackit/internal/services/redis/credential/resource.go @@ -6,24 +6,22 @@ import ( "net/http" "strings" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - redisUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/redis/utils" - - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/stackitcloud/stackit-sdk-go/core/oapierror" "github.com/stackitcloud/stackit-sdk-go/services/redis" "github.com/stackitcloud/stackit-sdk-go/services/redis/wait" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + redisUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/redis/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" ) // Ensure the implementation satisfies the expected interfaces. @@ -70,10 +68,13 @@ func (r *credentialResource) Configure(ctx context.Context, req resource.Configu } apiClient := redisUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + r.client = apiClient + tflog.Info(ctx, "Redis credential client configured") } @@ -163,13 +164,15 @@ func (r *credentialResource) Schema(_ context.Context, _ resource.SchemaRequest, } // Create creates the resource and sets the initial Terraform state. -func (r *credentialResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *credentialResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() instanceId := model.InstanceId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) @@ -181,10 +184,12 @@ func (r *credentialResource) Create(ctx context.Context, req resource.CreateRequ core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credential", fmt.Sprintf("Calling API: %v", err)) return } + if credentialsResp.Id == nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credential", "Got empty credential id") return } + credentialId := *credentialsResp.Id ctx = tflog.SetField(ctx, "credential_id", credentialId) @@ -200,22 +205,27 @@ func (r *credentialResource) Create(ctx context.Context, req resource.CreateRequ core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating credential", fmt.Sprintf("Processing API payload: %v", err)) return } + diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "Redis credential created") } // Read refreshes the Terraform state with the latest data. -func (r *credentialResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (r *credentialResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() instanceId := model.InstanceId.ValueString() credentialId := model.CredentialId.ValueString() @@ -230,7 +240,9 @@ func (r *credentialResource) Read(ctx context.Context, req resource.ReadRequest, resp.State.RemoveResource(ctx) return } + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading credential", fmt.Sprintf("Calling API: %v", err)) + return } @@ -244,23 +256,26 @@ func (r *credentialResource) Read(ctx context.Context, req resource.ReadRequest, // Set refreshed state diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "Redis credential read") } // Update updates the resource and sets the updated Terraform state on success. -func (r *credentialResource) Update(ctx context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *credentialResource) Update(ctx context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { //nolint:gocritic // function signature required by Terraform // Update shouldn't be called core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating credential", "Credential can't be updated") } // Delete deletes the resource and removes the Terraform state on success. -func (r *credentialResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform +func (r *credentialResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -277,16 +292,18 @@ func (r *credentialResource) Delete(ctx context.Context, req resource.DeleteRequ if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting credential", fmt.Sprintf("Calling API: %v", err)) } + _, err = wait.DeleteCredentialsWaitHandler(ctx, r.client, projectId, instanceId, credentialId).WaitWithContext(ctx) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting credential", fmt.Sprintf("Instance deletion waiting: %v", err)) return } + tflog.Info(ctx, "Redis credential deleted") } // ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: project_id,instance_id,credential_id +// The expected format of the resource import identifier is: project_id,instance_id,credential_id. func (r *credentialResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { idParts := strings.Split(req.ID, core.Separator) if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" { @@ -294,6 +311,7 @@ func (r *credentialResource) ImportState(ctx context.Context, req resource.Impor "Error importing credential", fmt.Sprintf("Expected import identifier with format [project_id],[instance_id],[credential_id], got %q", req.ID), ) + return } @@ -307,12 +325,15 @@ func mapFields(ctx context.Context, credentialsResp *redis.CredentialsResponse, if credentialsResp == nil { return fmt.Errorf("response input is nil") } + if credentialsResp.Raw == nil { return fmt.Errorf("response credentials raw is nil") } + if model == nil { return fmt.Errorf("model input is nil") } + credentials := credentialsResp.Raw.Credentials var credentialId string @@ -333,6 +354,7 @@ func mapFields(ctx context.Context, credentialsResp *redis.CredentialsResponse, model.CredentialId = types.StringValue(credentialId) model.Hosts = types.ListNull(types.StringType) + if credentials != nil { if credentials.Hosts != nil { respHosts := *credentials.Hosts @@ -346,6 +368,7 @@ func mapFields(ctx context.Context, credentialsResp *redis.CredentialsResponse, model.Hosts = hostsTF } + model.Host = types.StringPointerValue(credentials.Host) model.LoadBalancedHost = types.StringPointerValue(credentials.LoadBalancedHost) model.Password = types.StringPointerValue(credentials.Password) @@ -353,5 +376,6 @@ func mapFields(ctx context.Context, credentialsResp *redis.CredentialsResponse, model.Uri = types.StringPointerValue(credentials.Uri) model.Username = types.StringPointerValue(credentials.Username) } + return nil } diff --git a/stackit/internal/services/redis/credential/resource_test.go b/stackit/internal/services/redis/credential/resource_test.go index d4d1c6419..53f7265eb 100644 --- a/stackit/internal/services/redis/credential/resource_test.go +++ b/stackit/internal/services/redis/credential/resource_test.go @@ -207,9 +207,11 @@ func TestMapFields(t *testing.T) { if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(tt.state, tt.expected) if diff != "" { diff --git a/stackit/internal/services/redis/instance/datasource.go b/stackit/internal/services/redis/instance/datasource.go index baae95a20..f1d6a94fe 100644 --- a/stackit/internal/services/redis/instance/datasource.go +++ b/stackit/internal/services/redis/instance/datasource.go @@ -5,19 +5,17 @@ import ( "fmt" "net/http" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - redisUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/redis/utils" - "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/stackitcloud/stackit-sdk-go/services/redis" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + redisUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/redis/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - - "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/stackitcloud/stackit-sdk-go/services/redis" ) // Ensure the implementation satisfies the expected interfaces. @@ -48,10 +46,13 @@ func (r *instanceDataSource) Configure(ctx context.Context, req datasource.Confi } apiClient := redisUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + r.client = apiClient + tflog.Info(ctx, "Redis instance client configured") } @@ -252,13 +253,15 @@ func (r *instanceDataSource) Schema(_ context.Context, _ datasource.SchemaReques } // Read refreshes the Terraform state with the latest data. -func (r *instanceDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (r *instanceDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.Config.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() instanceId := model.InstanceId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) @@ -278,6 +281,7 @@ func (r *instanceDataSource) Read(ctx context.Context, req datasource.ReadReques }, ) resp.State.RemoveResource(ctx) + return } @@ -297,8 +301,10 @@ func (r *instanceDataSource) Read(ctx context.Context, req datasource.ReadReques // Set refreshed state diags = resp.State.Set(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "Redis instance read") } diff --git a/stackit/internal/services/redis/instance/resource.go b/stackit/internal/services/redis/instance/resource.go index 589d54979..efa8ed8fe 100644 --- a/stackit/internal/services/redis/instance/resource.go +++ b/stackit/internal/services/redis/instance/resource.go @@ -7,28 +7,25 @@ import ( "slices" "strings" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - - redisUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/redis/utils" - "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" "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/services/redis" "github.com/stackitcloud/stackit-sdk-go/services/redis/wait" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + redisUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/redis/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" ) // Ensure the implementation satisfies the expected interfaces. @@ -54,7 +51,7 @@ type Model struct { PlanId types.String `tfsdk:"plan_id"` } -// Struct corresponding to DataSourceModel.Parameters +// Struct corresponding to DataSourceModel.Parameters. type parametersModel struct { SgwAcl types.String `tfsdk:"sgw_acl"` DownAfterMilliseconds types.Int64 `tfsdk:"down_after_milliseconds"` @@ -80,7 +77,7 @@ type parametersModel struct { TlsProtocols types.String `tfsdk:"tls_protocols"` } -// Types corresponding to parametersModel +// Types corresponding to parametersModel. var parametersTypes = map[string]attr.Type{ "sgw_acl": basetypes.StringType{}, "down_after_milliseconds": basetypes.Int64Type{}, @@ -129,10 +126,13 @@ func (r *instanceResource) Configure(ctx context.Context, req resource.Configure } apiClient := redisUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + r.client = apiClient + tflog.Info(ctx, "Redis instance client configured") } @@ -389,21 +389,24 @@ func (r *instanceResource) Schema(_ context.Context, _ resource.SchemaRequest, r } // Create creates the resource and sets the initial Terraform state. -func (r *instanceResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *instanceResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) var parameters *parametersModel - if !(model.Parameters.IsNull() || model.Parameters.IsUnknown()) { + if !model.Parameters.IsNull() && !model.Parameters.IsUnknown() { parameters = ¶metersModel{} diags = model.Parameters.As(ctx, parameters, basetypes.ObjectAsOptions{}) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -427,6 +430,7 @@ func (r *instanceResource) Create(ctx context.Context, req resource.CreateReques core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Calling API: %v", err)) return } + instanceId := *createResp.InstanceId ctx = tflog.SetField(ctx, "instance_id", instanceId) waitResp, err := wait.CreateInstanceWaitHandler(ctx, r.client, projectId, instanceId).WaitWithContext(ctx) @@ -445,20 +449,24 @@ func (r *instanceResource) Create(ctx context.Context, req resource.CreateReques // Set state to fully populated data diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "Redis instance created") } // Read refreshes the Terraform state with the latest data. -func (r *instanceResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (r *instanceResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() instanceId := model.InstanceId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) @@ -471,7 +479,9 @@ func (r *instanceResource) Read(ctx context.Context, req resource.ReadRequest, r resp.State.RemoveResource(ctx) return } + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Calling API: %v", err)) + return } @@ -492,30 +502,35 @@ func (r *instanceResource) Read(ctx context.Context, req resource.ReadRequest, r // Set refreshed state diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "Redis instance read") } // Update updates the resource and sets the updated Terraform state on success. -func (r *instanceResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *instanceResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() instanceId := model.InstanceId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) ctx = tflog.SetField(ctx, "instance_id", instanceId) var parameters *parametersModel - if !(model.Parameters.IsNull() || model.Parameters.IsUnknown()) { + if !model.Parameters.IsNull() && !model.Parameters.IsUnknown() { parameters = ¶metersModel{} diags = model.Parameters.As(ctx, parameters, basetypes.ObjectAsOptions{}) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -539,6 +554,7 @@ func (r *instanceResource) Update(ctx context.Context, req resource.UpdateReques core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Calling API: %v", err)) return } + waitResp, err := wait.PartialUpdateInstanceWaitHandler(ctx, r.client, projectId, instanceId).WaitWithContext(ctx) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Instance update waiting: %v", err)) @@ -554,21 +570,25 @@ func (r *instanceResource) Update(ctx context.Context, req resource.UpdateReques diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "Redis instance updated") } // Delete deletes the resource and removes the Terraform state on success. -func (r *instanceResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform +func (r *instanceResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { //nolint:gocritic // function signature required by Terraform // Retrieve values from state var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() instanceId := model.InstanceId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) @@ -580,16 +600,18 @@ func (r *instanceResource) Delete(ctx context.Context, req resource.DeleteReques core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting instance", fmt.Sprintf("Calling API: %v", err)) return } + _, err = wait.DeleteInstanceWaitHandler(ctx, r.client, projectId, instanceId).WaitWithContext(ctx) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting instance", fmt.Sprintf("Instance deletion waiting: %v", err)) return } + tflog.Info(ctx, "Redis instance deleted") } // ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: project_id,instance_id +// The expected format of the resource import identifier is: project_id,instance_id. func (r *instanceResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { idParts := strings.Split(req.ID, core.Separator) @@ -598,6 +620,7 @@ func (r *instanceResource) ImportState(ctx context.Context, req resource.ImportS "Error importing instance", fmt.Sprintf("Expected import identifier with format: [project_id],[instance_id] Got: %q", req.ID), ) + return } @@ -610,6 +633,7 @@ func mapFields(instance *redis.Instance, model *Model) error { if instance == nil { return fmt.Errorf("response input is nil") } + if model == nil { return fmt.Errorf("model input is nil") } @@ -640,15 +664,19 @@ func mapFields(instance *redis.Instance, model *Model) error { if err != nil { return fmt.Errorf("mapping parameters: %w", err) } + model.Parameters = parameters } + return nil } func mapParameters(params map[string]interface{}) (types.Object, error) { attributes := map[string]attr.Value{} + for attribute := range parametersTypes { var valueInterface interface{} + var ok bool // This replacement is necessary because Terraform does not allow hyphens in attribute names @@ -673,6 +701,7 @@ func mapParameters(params map[string]interface{}) (types.Object, error) { } else { valueInterface, ok = params[attribute] } + if !ok { // All fields are optional, so this is ok // Set the value as nil, will be handled accordingly @@ -680,6 +709,7 @@ func mapParameters(params map[string]interface{}) (types.Object, error) { } var value attr.Value + switch parametersTypes[attribute].(type) { default: return types.ObjectNull(parametersTypes), fmt.Errorf("found unexpected attribute type '%T'", parametersTypes[attribute]) @@ -691,6 +721,7 @@ func mapParameters(params map[string]interface{}) (types.Object, error) { if !ok { return types.ObjectNull(parametersTypes), fmt.Errorf("found attribute '%s' of type %T, failed to assert as string", attribute, valueInterface) } + value = types.StringValue(valueString) } case basetypes.BoolType: @@ -701,6 +732,7 @@ func mapParameters(params map[string]interface{}) (types.Object, error) { if !ok { return types.ObjectNull(parametersTypes), fmt.Errorf("found attribute '%s' of type %T, failed to assert as bool", attribute, valueInterface) } + value = types.BoolValue(valueBool) } case basetypes.Int64Type: @@ -710,6 +742,7 @@ func mapParameters(params map[string]interface{}) (types.Object, error) { // This may be int64, int32, int or float64 // We try to assert all 4 var valueInt64 int64 + switch temp := valueInterface.(type) { default: return types.ObjectNull(parametersTypes), fmt.Errorf("found attribute '%s' of type %T, failed to assert as int", attribute, valueInterface) @@ -722,6 +755,7 @@ func mapParameters(params map[string]interface{}) (types.Object, error) { case float64: valueInt64 = int64(temp) } + value = types.Int64Value(valueInt64) } case basetypes.ListType: // Assumed to be a list of strings @@ -731,6 +765,7 @@ func mapParameters(params map[string]interface{}) (types.Object, error) { // This may be []string{} or []interface{} // We try to assert all 2 var valueList []attr.Value + switch temp := valueInterface.(type) { default: return types.ObjectNull(parametersTypes), fmt.Errorf("found attribute '%s' of type %T, failed to assert as array of interface", attribute, valueInterface) @@ -744,16 +779,20 @@ func mapParameters(params map[string]interface{}) (types.Object, error) { if !ok { return types.ObjectNull(parametersTypes), fmt.Errorf("found attribute '%s' with element '%s' of type %T, failed to assert as string", attribute, x, x) } + valueList = append(valueList, types.StringValue(xString)) } } + temp2, diags := types.ListValue(types.StringType, valueList) if diags.HasError() { return types.ObjectNull(parametersTypes), fmt.Errorf("failed to map %s: %w", attribute, core.DiagsToError(diags)) } + value = temp2 } } + attributes[attribute] = value } @@ -761,6 +800,7 @@ func mapParameters(params map[string]interface{}) (types.Object, error) { if diags.HasError() { return types.ObjectNull(parametersTypes), fmt.Errorf("failed to create object: %w", core.DiagsToError(diags)) } + return output, nil } @@ -801,6 +841,7 @@ func toInstanceParams(parameters *parametersModel) (*redis.InstanceParameters, e if parameters == nil { return nil, nil } + payloadParams := &redis.InstanceParameters{} payloadParams.SgwAcl = conversion.StringValueToPointer(parameters.SgwAcl) @@ -825,6 +866,7 @@ func toInstanceParams(parameters *parametersModel) (*redis.InstanceParameters, e payloadParams.TlsProtocols = redis.InstanceParametersGetTlsProtocolsAttributeType(conversion.StringValueToPointer(parameters.TlsProtocols)) var err error + payloadParams.Syslog, err = conversion.StringListToPointer(parameters.Syslog) if err != nil { return nil, fmt.Errorf("converting syslog: %w", err) @@ -840,6 +882,7 @@ func toInstanceParams(parameters *parametersModel) (*redis.InstanceParameters, e func (r *instanceResource) loadPlanId(ctx context.Context, model *Model) error { projectId := model.ProjectId.ValueString() + res, err := r.client.ListOfferings(ctx, projectId).Execute() if err != nil { return fmt.Errorf("getting Redis offerings: %w", err) @@ -850,21 +893,25 @@ func (r *instanceResource) loadPlanId(ctx context.Context, model *Model) error { availableVersions := "" availablePlanNames := "" isValidVersion := false + for _, offer := range *res.Offerings { if !strings.EqualFold(*offer.Version, version) { availableVersions = fmt.Sprintf("%s\n- %s", availableVersions, *offer.Version) continue } + isValidVersion = true for _, plan := range *offer.Plans { if plan.Name == nil { continue } + if strings.EqualFold(*plan.Name, planName) && plan.Id != nil { model.PlanId = types.StringPointerValue(plan.Id) return nil } + availablePlanNames = fmt.Sprintf("%s\n- %s", availablePlanNames, *plan.Name) } } @@ -872,12 +919,14 @@ func (r *instanceResource) loadPlanId(ctx context.Context, model *Model) error { if !isValidVersion { return fmt.Errorf("couldn't find version '%s', available versions are: %s", version, availableVersions) } + return fmt.Errorf("couldn't find plan_name '%s' for version %s, available names are: %s", planName, version, availablePlanNames) } func loadPlanNameAndVersion(ctx context.Context, client *redis.APIClient, model *Model) error { projectId := model.ProjectId.ValueString() planId := model.PlanId.ValueString() + res, err := client.ListOfferings(ctx, projectId).Execute() if err != nil { return fmt.Errorf("getting Redis offerings: %w", err) @@ -888,6 +937,7 @@ func loadPlanNameAndVersion(ctx context.Context, client *redis.APIClient, model if strings.EqualFold(*plan.Id, planId) && plan.Id != nil { model.PlanName = types.StringPointerValue(plan.Name) model.Version = types.StringPointerValue(offer.Version) + return nil } } diff --git a/stackit/internal/services/redis/instance/resource_test.go b/stackit/internal/services/redis/instance/resource_test.go index 9221878a6..4c615d297 100644 --- a/stackit/internal/services/redis/instance/resource_test.go +++ b/stackit/internal/services/redis/instance/resource_test.go @@ -183,13 +183,16 @@ func TestMapFields(t *testing.T) { ProjectId: tt.expected.ProjectId, InstanceId: tt.expected.InstanceId, } + err := mapFields(tt.input, state) if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(state, &tt.expected) if diff != "" { @@ -263,22 +266,27 @@ func TestToCreatePayload(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { var parameters *parametersModel + if tt.input != nil { - if !(tt.input.Parameters.IsNull() || tt.input.Parameters.IsUnknown()) { + if !tt.input.Parameters.IsNull() && !tt.input.Parameters.IsUnknown() { parameters = ¶metersModel{} + diags := tt.input.Parameters.As(context.Background(), parameters, basetypes.ObjectAsOptions{}) if diags.HasError() { t.Fatalf("Error converting parameters: %v", diags.Errors()) } } } + output, err := toCreatePayload(tt.input, parameters) if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(output, tt.expected) if diff != "" { @@ -346,22 +354,27 @@ func TestToUpdatePayload(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { var parameters *parametersModel + if tt.input != nil { - if !(tt.input.Parameters.IsNull() || tt.input.Parameters.IsUnknown()) { + if !tt.input.Parameters.IsNull() && !tt.input.Parameters.IsUnknown() { parameters = ¶metersModel{} + diags := tt.input.Parameters.As(context.Background(), parameters, basetypes.ObjectAsOptions{}) if diags.HasError() { t.Fatalf("Error converting parameters: %v", diags.Errors()) } } } + output, err := toUpdatePayload(tt.input, parameters) if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(output, tt.expected) if diff != "" { diff --git a/stackit/internal/services/redis/redis_acc_test.go b/stackit/internal/services/redis/redis_acc_test.go index e86fe3cab..6d8968ef3 100644 --- a/stackit/internal/services/redis/redis_acc_test.go +++ b/stackit/internal/services/redis/redis_acc_test.go @@ -17,7 +17,7 @@ import ( "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/testutil" ) -// Instance resource data +// Instance resource data. var instanceResource = map[string]string{ "project_id": testutil.ProjectId, "name": testutil.ResourceNameWithDateTime("redis"), @@ -44,6 +44,7 @@ func parametersConfig(params map[string]string) string { "tls_ciphers", } parameters := "parameters = {" + for k, v := range params { if utils.Contains(nonStringParams, k) { parameters += fmt.Sprintf("%s = %s\n", k, v) @@ -51,7 +52,9 @@ func parametersConfig(params map[string]string) string { parameters += fmt.Sprintf("%s = %q\n", k, v) } } + parameters += "\n}" + return parameters } @@ -212,6 +215,7 @@ func TestAccRedisResource(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute instance_id") } + return fmt.Sprintf("%s,%s", testutil.ProjectId, instanceId), nil }, ImportState: true, @@ -232,6 +236,7 @@ func TestAccRedisResource(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute credential_id") } + return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, instanceId, credentialId), nil }, ImportState: true, @@ -268,12 +273,15 @@ func checkInstanceDeleteSuccess(i *redis.Instance) bool { return false } } + return true } func testAccCheckRedisDestroy(s *terraform.State) error { ctx := context.Background() + var client *redis.APIClient + var err error if testutil.RedisCustomEndpoint == "" { client, err = redis.NewAPIClient( @@ -284,11 +292,13 @@ func testAccCheckRedisDestroy(s *terraform.State) error { config.WithEndpoint(testutil.RedisCustomEndpoint), ) } + if err != nil { return fmt.Errorf("creating client: %w", err) } instancesToDestroy := []string{} + for _, rs := range s.RootModule().Resources { if rs.Type != "stackit_redis_instance" { continue @@ -308,12 +318,14 @@ func testAccCheckRedisDestroy(s *terraform.State) error { if instances[i].InstanceId == nil { continue } + if utils.Contains(instancesToDestroy, *instances[i].InstanceId) { if !checkInstanceDeleteSuccess(&instances[i]) { err := client.DeleteInstanceExecute(ctx, testutil.ProjectId, *instances[i].InstanceId) if err != nil { return fmt.Errorf("destroying instance %s during CheckDestroy: %w", *instances[i].InstanceId, err) } + _, err = wait.DeleteInstanceWaitHandler(ctx, client, testutil.ProjectId, *instances[i].InstanceId).WaitWithContext(ctx) if err != nil { return fmt.Errorf("destroying instance %s during CheckDestroy: waiting for deletion %w", *instances[i].InstanceId, err) @@ -321,5 +333,6 @@ func testAccCheckRedisDestroy(s *terraform.State) error { } } } + return nil } diff --git a/stackit/internal/services/redis/utils/util.go b/stackit/internal/services/redis/utils/util.go index 0b9963ee2..22c683c2d 100644 --- a/stackit/internal/services/redis/utils/util.go +++ b/stackit/internal/services/redis/utils/util.go @@ -21,6 +21,7 @@ func ConfigureClient(ctx context.Context, providerData *core.ProviderData, diags } else { apiClientConfigOptions = append(apiClientConfigOptions, config.WithRegion(providerData.GetRegion())) } + apiClient, err := redis.NewAPIClient(apiClientConfigOptions...) if err != nil { core.LogAndAddError(ctx, diags, "Error configuring API client", fmt.Sprintf("Configuring client: %v. This is an error related to the provider configuration, not to the resource configuration", err)) diff --git a/stackit/internal/services/redis/utils/util_test.go b/stackit/internal/services/redis/utils/util_test.go index 51918317c..9f3818b54 100644 --- a/stackit/internal/services/redis/utils/util_test.go +++ b/stackit/internal/services/redis/utils/util_test.go @@ -22,6 +22,7 @@ const ( func TestConfigureClient(t *testing.T) { /* mock authentication by setting service account token env variable */ os.Clearenv() + err := os.Setenv(sdkClients.ServiceAccountToken, "mock-val") if err != nil { t.Errorf("error setting env variable: %v", err) @@ -30,6 +31,7 @@ func TestConfigureClient(t *testing.T) { type args struct { providerData *core.ProviderData } + tests := []struct { name string args args @@ -51,6 +53,7 @@ func TestConfigureClient(t *testing.T) { if err != nil { t.Errorf("error configuring client: %v", err) } + return apiClient }(), wantErr: false, @@ -71,6 +74,7 @@ func TestConfigureClient(t *testing.T) { if err != nil { t.Errorf("error configuring client: %v", err) } + return apiClient }(), wantErr: false, @@ -82,6 +86,7 @@ func TestConfigureClient(t *testing.T) { diags := diag.Diagnostics{} actual := ConfigureClient(ctx, tt.args.providerData, &diags) + if diags.HasError() != tt.wantErr { t.Errorf("ConfigureClient() error = %v, want %v", diags.HasError(), tt.wantErr) } diff --git a/stackit/internal/services/resourcemanager/folder/datasource.go b/stackit/internal/services/resourcemanager/folder/datasource.go index bbcce9240..be86c36b0 100644 --- a/stackit/internal/services/resourcemanager/folder/datasource.go +++ b/stackit/internal/services/resourcemanager/folder/datasource.go @@ -50,15 +50,19 @@ func (d *folderDataSource) Configure(ctx context.Context, req datasource.Configu } features.CheckBetaResourcesEnabled(ctx, &providerData, &resp.Diagnostics, "stackit_resourcemanager_folder", "datasource") + if resp.Diagnostics.HasError() { return } apiClient := resourcemanagerUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + d.client = apiClient + tflog.Info(ctx, "Resource Manager client configured") } @@ -143,10 +147,11 @@ func (d *folderDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, } // Read refreshes the Terraform state with the latest data. -func (d *folderDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (d *folderDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.Config.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -167,6 +172,7 @@ func (d *folderDataSource) Read(ctx context.Context, req datasource.ReadRequest, }, ) resp.State.RemoveResource(ctx) + return } @@ -178,8 +184,10 @@ func (d *folderDataSource) Read(ctx context.Context, req datasource.ReadRequest, diags = resp.State.Set(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "Resource Manager folder read") } diff --git a/stackit/internal/services/resourcemanager/folder/resource.go b/stackit/internal/services/resourcemanager/folder/resource.go index 6154ab478..4b663a645 100644 --- a/stackit/internal/services/resourcemanager/folder/resource.go +++ b/stackit/internal/services/resourcemanager/folder/resource.go @@ -82,15 +82,19 @@ func (r *folderResource) Configure(ctx context.Context, req resource.ConfigureRe } features.CheckBetaResourcesEnabled(ctx, &providerData, &resp.Diagnostics, "stackit_resourcemanager_folder", "resource") + if resp.Diagnostics.HasError() { return } apiClient := resourcemanagerUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + r.client = apiClient + tflog.Info(ctx, "Resource Manager client configured") } @@ -188,11 +192,13 @@ func (r *folderResource) Schema(_ context.Context, _ resource.SchemaRequest, res } // Create creates the resource and sets the initial Terraform state. -func (r *folderResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *folderResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { //nolint:gocritic // function signature required by Terraform tflog.Info(ctx, "creating folder") + var model ResourceModel diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -240,10 +246,11 @@ func (r *folderResource) Create(ctx context.Context, req resource.CreateRequest, } // Read refreshes the Terraform state with the latest data. -func (r *folderResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (r *folderResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model ResourceModel diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -260,7 +267,9 @@ func (r *folderResource) Read(ctx context.Context, req resource.ReadRequest, res resp.State.RemoveResource(ctx) return } + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading folder", fmt.Sprintf("Calling API: %v", err)) + return } @@ -273,21 +282,25 @@ func (r *folderResource) Read(ctx context.Context, req resource.ReadRequest, res // Set refreshed model diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "Resource Manager folder read") } // Update updates the resource and sets the updated Terraform state on success. -func (r *folderResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *folderResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { //nolint:gocritic // function signature required by Terraform // Retrieve values from plan var model ResourceModel diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + containerId := model.ContainerId.ValueString() ctx = tflog.SetField(ctx, "container_id", containerId) @@ -319,18 +332,21 @@ func (r *folderResource) Update(ctx context.Context, req resource.UpdateRequest, diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "Resource Manager folder updated") } // Delete deletes the resource and removes the Terraform state on success. -func (r *folderResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform +func (r *folderResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { //nolint:gocritic // function signature required by Terraform // Retrieve values from state var model ResourceModel diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -347,6 +363,7 @@ func (r *folderResource) Delete(ctx context.Context, req resource.DeleteRequest, "Error deleting folder. Deletion may fail because associated projects remain hidden for up to 7 days after user deletion due to technical requirements.", fmt.Sprintf("Calling API: %v", err), ) + return } @@ -354,7 +371,7 @@ func (r *folderResource) Delete(ctx context.Context, req resource.DeleteRequest, } // ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: container_id +// The expected format of the resource import identifier is: container_id. func (r *folderResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { idParts := strings.Split(req.ID, core.Separator) if len(idParts) != 1 || idParts[0] == "" { @@ -362,6 +379,7 @@ func (r *folderResource) ImportState(ctx context.Context, req resource.ImportSta "Error importing folder", fmt.Sprintf("Expected import identifier with format: [container_id] Got: %q", req.ID), ) + return } @@ -401,6 +419,7 @@ func mapFolderFields( } var err error + var tfLabels basetypes.MapValue if folderGetResponse.Labels != nil && len(*folderGetResponse.Labels) > 0 { tfLabels, err = conversion.ToTerraformStringMap(ctx, *folderGetResponse.Labels) @@ -412,6 +431,7 @@ func mapFolderFields( } var containerParentIdTF basetypes.StringValue + if folderGetResponse.Parent != nil { if _, err := uuid.Parse(model.ContainerParentId.ValueString()); err == nil { // the provided containerParent is the UUID identifier @@ -443,6 +463,7 @@ func mapFolderFields( diags.Append(state.SetAttribute(ctx, path.Root("labels"), model.Labels)...) diags.Append(state.SetAttribute(ctx, path.Root("creation_time"), model.CreationTime)...) diags.Append(state.SetAttribute(ctx, path.Root("update_time"), model.UpdateTime)...) + if diags.HasError() { return fmt.Errorf("update terraform state: %w", core.DiagsToError(diags)) } @@ -455,6 +476,7 @@ func toMembersPayload(model *ResourceModel) (*[]resourcemanager.Member, error) { if model == nil { return nil, fmt.Errorf("nil model") } + if model.OwnerEmail.IsNull() { return nil, fmt.Errorf("owner_email is null") } @@ -478,6 +500,7 @@ func toCreatePayload(model *ResourceModel) (*resourcemanager.CreateFolderPayload } modelLabels := model.Labels.Elements() + labels, err := conversion.ToOptStringMap(modelLabels) if err != nil { return nil, fmt.Errorf("converting to Go map: %w", err) @@ -497,6 +520,7 @@ func toUpdatePayload(model *ResourceModel) (*resourcemanager.PartialUpdateFolder } modelLabels := model.Labels.Elements() + labels, err := conversion.ToOptStringMap(modelLabels) if err != nil { return nil, fmt.Errorf("converting to GO map: %w", err) diff --git a/stackit/internal/services/resourcemanager/folder/resource_test.go b/stackit/internal/services/resourcemanager/folder/resource_test.go index 500f66f19..6dce3c35e 100644 --- a/stackit/internal/services/resourcemanager/folder/resource_test.go +++ b/stackit/internal/services/resourcemanager/folder/resource_test.go @@ -142,9 +142,11 @@ func TestMapFolderFields(t *testing.T) { if err != nil { t.Fatalf("Error converting to terraform string map: %v", err) } + tt.expected.Labels = convertedLabels } - var containerParentId = types.StringNull() + + containerParentId := types.StringNull() if tt.uuidContainerParentId { containerParentId = types.StringValue(testUUID) } else if tt.projectResp != nil && tt.projectResp.Parent != nil && tt.projectResp.Parent.ContainerId != nil { @@ -161,9 +163,11 @@ func TestMapFolderFields(t *testing.T) { if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(model, &tt.expected) if diff != "" { @@ -241,16 +245,20 @@ func TestToCreatePayload(t *testing.T) { if err != nil { t.Fatalf("Error converting to terraform string map: %v", err) } + tt.input.Labels = convertedLabels } } + output, err := toCreatePayload(tt.input) if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(output, tt.expected) if diff != "" { @@ -321,16 +329,20 @@ func TestToUpdatePayload(t *testing.T) { if err != nil { t.Fatalf("Error converting to terraform string map: %v", err) } + tt.input.Labels = convertedLabels } } + output, err := toUpdatePayload(tt.input) if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(output, tt.expected) if diff != "" { @@ -345,6 +357,7 @@ func TestToMembersPayload(t *testing.T) { type args struct { model *ResourceModel } + tests := []struct { name string args args @@ -388,6 +401,7 @@ func TestToMembersPayload(t *testing.T) { t.Errorf("toMembersPayload() error = %v, wantErr %v", err, tt.wantErr) return } + if !reflect.DeepEqual(got, tt.want) { t.Errorf("toMembersPayload() got = %v, want %v", got, tt.want) } diff --git a/stackit/internal/services/resourcemanager/project/datasource.go b/stackit/internal/services/resourcemanager/project/datasource.go index 0af3f8168..62db1f186 100644 --- a/stackit/internal/services/resourcemanager/project/datasource.go +++ b/stackit/internal/services/resourcemanager/project/datasource.go @@ -6,21 +6,19 @@ import ( "net/http" "regexp" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - resourcemanagerUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/resourcemanager/utils" - "github.com/hashicorp/terraform-plugin-framework-validators/mapvalidator" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/stackitcloud/stackit-sdk-go/services/resourcemanager" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + resourcemanagerUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/resourcemanager/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - - "github.com/hashicorp/terraform-plugin-framework/datasource" - "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/stackit-sdk-go/services/resourcemanager" ) // Ensure the implementation satisfies the expected interfaces. @@ -51,10 +49,13 @@ func (d *projectDataSource) Configure(ctx context.Context, req datasource.Config } apiClient := resourcemanagerUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + d.client = apiClient + tflog.Info(ctx, "Resource Manager project client configured") } @@ -138,10 +139,11 @@ func (d *projectDataSource) Schema(_ context.Context, _ datasource.SchemaRequest } // Read refreshes the Terraform state with the latest data. -func (d *projectDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (d *projectDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.Config.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -158,8 +160,10 @@ func (d *projectDataSource) Read(ctx context.Context, req datasource.ReadRequest } // set project identifier. If projectId is provided, it takes precedence over containerId - var identifier = containerId + identifier := containerId + identifierType := "Container" + if projectId != "" { identifier = projectId identifierType = "Project" @@ -178,6 +182,7 @@ func (d *projectDataSource) Read(ctx context.Context, req datasource.ReadRequest }, ) resp.State.RemoveResource(ctx) + return } @@ -189,8 +194,10 @@ func (d *projectDataSource) Read(ctx context.Context, req datasource.ReadRequest diags = resp.State.Set(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "Resource Manager project read") } diff --git a/stackit/internal/services/resourcemanager/project/resource.go b/stackit/internal/services/resourcemanager/project/resource.go index 228c437d2..71742b166 100644 --- a/stackit/internal/services/resourcemanager/project/resource.go +++ b/stackit/internal/services/resourcemanager/project/resource.go @@ -8,30 +8,28 @@ import ( "strings" "time" - resourcemanagerUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/resourcemanager/utils" - "github.com/google/uuid" "github.com/hashicorp/terraform-plugin-framework-validators/mapvalidator" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/diag" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/tfsdk" - "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" "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" sdkUtils "github.com/stackitcloud/stackit-sdk-go/core/utils" "github.com/stackitcloud/stackit-sdk-go/services/resourcemanager" "github.com/stackitcloud/stackit-sdk-go/services/resourcemanager/wait" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + resourcemanagerUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/resourcemanager/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" ) // Ensure the implementation satisfies the expected interfaces. @@ -84,10 +82,13 @@ func (r *projectResource) Configure(ctx context.Context, req resource.ConfigureR } apiClient := resourcemanagerUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + r.client = apiClient + tflog.Info(ctx, "Resource Manager project client configured") } @@ -188,10 +189,11 @@ func (r *projectResource) Schema(_ context.Context, _ resource.SchemaRequest, re } // Create creates the resource and sets the initial Terraform state. -func (r *projectResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *projectResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { //nolint:gocritic // function signature required by Terraform var model ResourceModel diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -211,6 +213,7 @@ func (r *projectResource) Create(ctx context.Context, req resource.CreateRequest core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating project", fmt.Sprintf("Calling API: %v", err)) return } + respContainerId := *createResp.ContainerId // If the request has not been processed yet and the containerId doesn't exist, @@ -230,20 +233,24 @@ func (r *projectResource) Create(ctx context.Context, req resource.CreateRequest // Set state to fully populated data diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "Resource Manager project created") } // Read refreshes the Terraform state with the latest data. -func (r *projectResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (r *projectResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model ResourceModel diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + containerId := model.ContainerId.ValueString() ctx = tflog.SetField(ctx, "container_id", containerId) @@ -254,7 +261,9 @@ func (r *projectResource) Read(ctx context.Context, req resource.ReadRequest, re resp.State.RemoveResource(ctx) return } + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading project", fmt.Sprintf("Calling API: %v", err)) + return } @@ -267,21 +276,25 @@ func (r *projectResource) Read(ctx context.Context, req resource.ReadRequest, re // Set refreshed model diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "Resource Manager project read") } // Update updates the resource and sets the updated Terraform state on success. -func (r *projectResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *projectResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { //nolint:gocritic // function signature required by Terraform // Retrieve values from plan var model ResourceModel diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + containerId := model.ContainerId.ValueString() ctx = tflog.SetField(ctx, "container_id", containerId) @@ -313,18 +326,21 @@ func (r *projectResource) Update(ctx context.Context, req resource.UpdateRequest diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "Resource Manager project updated") } // Delete deletes the resource and removes the Terraform state on success. -func (r *projectResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform +func (r *projectResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { //nolint:gocritic // function signature required by Terraform // Retrieve values from state var model ResourceModel diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -349,7 +365,7 @@ func (r *projectResource) Delete(ctx context.Context, req resource.DeleteRequest } // ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: container_id +// The expected format of the resource import identifier is: container_id. func (r *projectResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { idParts := strings.Split(req.ID, core.Separator) if len(idParts) != 1 || idParts[0] == "" { @@ -357,6 +373,7 @@ func (r *projectResource) ImportState(ctx context.Context, req resource.ImportSt "Error importing project", fmt.Sprintf("Expected import identifier with format: [container_id] Got: %q", req.ID), ) + return } @@ -366,11 +383,12 @@ func (r *projectResource) ImportState(ctx context.Context, req resource.ImportSt tflog.Info(ctx, "Resource Manager Project state imported") } -// mapProjectFields maps the API response to the Terraform model and update the Terraform state +// mapProjectFields maps the API response to the Terraform model and update the Terraform state. func mapProjectFields(ctx context.Context, projectResp *resourcemanager.GetProjectResponse, model *Model, state *tfsdk.State) (err error) { if projectResp == nil { return fmt.Errorf("response input is nil") } + if model == nil { return fmt.Errorf("model input is nil") } @@ -404,6 +422,7 @@ func mapProjectFields(ctx context.Context, projectResp *resourcemanager.GetProje } var containerParentIdTF basetypes.StringValue + if projectResp.Parent != nil { if _, err := uuid.Parse(model.ContainerParentId.ValueString()); err == nil { // the provided containerParentId is the UUID identifier @@ -435,6 +454,7 @@ func mapProjectFields(ctx context.Context, projectResp *resourcemanager.GetProje diags.Append(state.SetAttribute(ctx, path.Root("labels"), model.Labels)...) diags.Append(state.SetAttribute(ctx, path.Root("creation_time"), model.CreationTime)...) diags.Append(state.SetAttribute(ctx, path.Root("update_time"), model.UpdateTime)...) + if diags.HasError() { return fmt.Errorf("update terraform state: %w", core.DiagsToError(diags)) } @@ -447,6 +467,7 @@ func toMembersPayload(model *ResourceModel) (*[]resourcemanager.Member, error) { if model == nil { return nil, fmt.Errorf("nil model") } + if model.OwnerEmail.IsNull() { return nil, fmt.Errorf("owner_email is null") } @@ -470,6 +491,7 @@ func toCreatePayload(model *ResourceModel) (*resourcemanager.CreateProjectPayloa } modelLabels := model.Labels.Elements() + labels, err := conversion.ToOptStringMap(modelLabels) if err != nil { return nil, fmt.Errorf("converting to Go map: %w", err) @@ -489,6 +511,7 @@ func toUpdatePayload(model *ResourceModel) (*resourcemanager.PartialUpdateProjec } modelLabels := model.Labels.Elements() + labels, err := conversion.ToOptStringMap(modelLabels) if err != nil { return nil, fmt.Errorf("converting to GO map: %w", err) diff --git a/stackit/internal/services/resourcemanager/project/resource_test.go b/stackit/internal/services/resourcemanager/project/resource_test.go index 28aaded68..472b95bb3 100644 --- a/stackit/internal/services/resourcemanager/project/resource_test.go +++ b/stackit/internal/services/resourcemanager/project/resource_test.go @@ -142,9 +142,11 @@ func TestMapProjectFields(t *testing.T) { if err != nil { t.Fatalf("Error converting to terraform string map: %v", err) } + tt.expected.Labels = convertedLabels } - var containerParentId = types.StringNull() + + containerParentId := types.StringNull() if tt.uuidContainerParentId { containerParentId = types.StringValue(testUUID) } else if tt.projectResp != nil && tt.projectResp.Parent != nil && tt.projectResp.Parent.ContainerId != nil { @@ -161,9 +163,11 @@ func TestMapProjectFields(t *testing.T) { if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(model, &tt.expected) if diff != "" { @@ -241,16 +245,20 @@ func TestToCreatePayload(t *testing.T) { if err != nil { t.Fatalf("Error converting to terraform string map: %v", err) } + tt.input.Labels = convertedLabels } } + output, err := toCreatePayload(tt.input) if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(output, tt.expected) if diff != "" { @@ -321,16 +329,20 @@ func TestToUpdatePayload(t *testing.T) { if err != nil { t.Fatalf("Error converting to terraform string map: %v", err) } + tt.input.Labels = convertedLabels } } + output, err := toUpdatePayload(tt.input) if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(output, tt.expected) if diff != "" { @@ -345,6 +357,7 @@ func TestToMembersPayload(t *testing.T) { type args struct { model *ResourceModel } + tests := []struct { name string args args @@ -388,6 +401,7 @@ func TestToMembersPayload(t *testing.T) { t.Errorf("toMembersPayload() error = %v, wantErr %v", err, tt.wantErr) return } + if !reflect.DeepEqual(got, tt.want) { t.Errorf("toMembersPayload() got = %v, want %v", got, tt.want) } diff --git a/stackit/internal/services/resourcemanager/resourcemanager_acc_test.go b/stackit/internal/services/resourcemanager/resourcemanager_acc_test.go index 5490aefb1..500c9cb1c 100644 --- a/stackit/internal/services/resourcemanager/resourcemanager_acc_test.go +++ b/stackit/internal/services/resourcemanager/resourcemanager_acc_test.go @@ -32,17 +32,25 @@ var defaultLabels = config.ObjectVariable( }, ) -var projectNameParentContainerId = fmt.Sprintf("tfe2e-project-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum)) -var projectNameParentContainerIdUpdated = fmt.Sprintf("%s-updated", projectNameParentContainerId) +var ( + projectNameParentContainerId = fmt.Sprintf("tfe2e-project-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum)) + projectNameParentContainerIdUpdated = fmt.Sprintf("%s-updated", projectNameParentContainerId) +) -var projectNameParentUUID = fmt.Sprintf("tfe2e-project-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum)) -var projectNameParentUUIDUpdated = fmt.Sprintf("%s-updated", projectNameParentUUID) +var ( + projectNameParentUUID = fmt.Sprintf("tfe2e-project-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum)) + projectNameParentUUIDUpdated = fmt.Sprintf("%s-updated", projectNameParentUUID) +) -var folderNameParentContainerId = fmt.Sprintf("tfe2e-folder-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum)) -var folderNameParentContainerIdUpdated = fmt.Sprintf("%s-updated", folderNameParentContainerId) +var ( + folderNameParentContainerId = fmt.Sprintf("tfe2e-folder-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum)) + folderNameParentContainerIdUpdated = fmt.Sprintf("%s-updated", folderNameParentContainerId) +) -var folderNameParentUUID = fmt.Sprintf("tfe2e-folder-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum)) -var folderNameParentUUIDUpdated = fmt.Sprintf("%s-updated", folderNameParentUUID) +var ( + folderNameParentUUID = fmt.Sprintf("tfe2e-folder-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum)) + folderNameParentUUIDUpdated = fmt.Sprintf("%s-updated", folderNameParentUUID) +) var testConfigResourceProjectParentContainerId = config.Variables{ "name": config.StringVariable(projectNameParentContainerId), @@ -80,6 +88,7 @@ func testConfigProjectNameParentContainerIdUpdated() config.Variables { tempConfig := make(config.Variables, len(testConfigResourceProjectParentContainerId)) maps.Copy(tempConfig, testConfigResourceProjectParentContainerId) tempConfig["name"] = config.StringVariable(projectNameParentContainerIdUpdated) + return tempConfig } @@ -87,6 +96,7 @@ func testConfigProjectNameParentUUIDUpdated() config.Variables { tempConfig := make(config.Variables, len(testConfigResourceProjectParentUUID)) maps.Copy(tempConfig, testConfigResourceProjectParentUUID) tempConfig["name"] = config.StringVariable(projectNameParentUUIDUpdated) + return tempConfig } @@ -94,6 +104,7 @@ func testConfigFolderNameParentContainerIdUpdated() config.Variables { tempConfig := make(config.Variables, len(testConfigResourceFolderParentContainerId)) maps.Copy(tempConfig, testConfigResourceFolderParentContainerId) tempConfig["name"] = config.StringVariable(folderNameParentContainerIdUpdated) + return tempConfig } @@ -101,6 +112,7 @@ func testConfigFolderNameParentUUIDUpdated() config.Variables { tempConfig := make(config.Variables, len(testConfigResourceFolderParentUUID)) maps.Copy(tempConfig, testConfigResourceFolderParentUUID) tempConfig["name"] = config.StringVariable(folderNameParentUUIDUpdated) + return tempConfig } @@ -427,6 +439,7 @@ func testAccCheckDestroy(s *terraform.State) error { testAccCheckResourceManagerProjectsDestroy, testAccCheckResourceManagerFoldersDestroy, } + var errs []error wg := sync.WaitGroup{} @@ -438,16 +451,21 @@ func testAccCheckDestroy(s *terraform.State) error { if err != nil { errs = append(errs, err) } + wg.Done() }() } + wg.Wait() + return errors.Join(errs...) } func testAccCheckResourceManagerProjectsDestroy(s *terraform.State) error { ctx := context.Background() + var client *resourcemanager.APIClient + var err error if testutil.ResourceManagerCustomEndpoint == "" { client, err = resourcemanager.NewAPIClient() @@ -456,11 +474,13 @@ func testAccCheckResourceManagerProjectsDestroy(s *terraform.State) error { sdkConfig.WithEndpoint(testutil.ResourceManagerCustomEndpoint), ) } + if err != nil { return fmt.Errorf("creating client: %w", err) } projectsToDestroy := []string{} + for _, rs := range s.RootModule().Resources { if rs.Type != "stackit_resourcemanager_project" { continue @@ -471,6 +491,7 @@ func testAccCheckResourceManagerProjectsDestroy(s *terraform.State) error { } var containerParentId string + switch { case testutil.TestProjectParentContainerID != "": containerParentId = testutil.TestProjectParentContainerID @@ -490,6 +511,7 @@ func testAccCheckResourceManagerProjectsDestroy(s *terraform.State) error { if *items[i].LifecycleState == resourcemanager.LIFECYCLESTATE_DELETING { continue } + if !utils.Contains(projectsToDestroy, *items[i].ContainerId) { continue } @@ -498,17 +520,21 @@ func testAccCheckResourceManagerProjectsDestroy(s *terraform.State) error { if err != nil { return fmt.Errorf("destroying project %s during CheckDestroy: %w", *items[i].ContainerId, err) } + _, err = wait.DeleteProjectWaitHandler(ctx, client, *items[i].ContainerId).WaitWithContext(ctx) if err != nil { return fmt.Errorf("destroying project %s during CheckDestroy: waiting for deletion %w", *items[i].ContainerId, err) } } + return nil } func testAccCheckResourceManagerFoldersDestroy(s *terraform.State) error { ctx := context.Background() + var client *resourcemanager.APIClient + var err error if testutil.ResourceManagerCustomEndpoint == "" { client, err = resourcemanager.NewAPIClient() @@ -517,11 +543,13 @@ func testAccCheckResourceManagerFoldersDestroy(s *terraform.State) error { sdkConfig.WithEndpoint(testutil.ResourceManagerCustomEndpoint), ) } + if err != nil { return fmt.Errorf("creating client: %w", err) } foldersToDestroy := []string{} + for _, rs := range s.RootModule().Resources { if rs.Type != "stackit_resourcemanager_folder" { continue @@ -532,6 +560,7 @@ func testAccCheckResourceManagerFoldersDestroy(s *terraform.State) error { } var containerParentId string + switch { case testutil.TestProjectParentContainerID != "": containerParentId = testutil.TestProjectParentContainerID @@ -557,6 +586,7 @@ func testAccCheckResourceManagerFoldersDestroy(s *terraform.State) error { return fmt.Errorf("destroying folder %s during CheckDestroy: %w", *items[i].ContainerId, err) } } + return nil } @@ -565,9 +595,11 @@ func getImportIdFromID(s *terraform.State, resourceName, keyName string) (string if !ok { return "", fmt.Errorf("couldn't find resource %s", resourceName) } + id, ok := r.Primary.Attributes[keyName] if !ok { return "", fmt.Errorf("couldn't find attribute %s", keyName) } + return id, nil } diff --git a/stackit/internal/services/resourcemanager/utils/util.go b/stackit/internal/services/resourcemanager/utils/util.go index 134543940..f1aff2d8e 100644 --- a/stackit/internal/services/resourcemanager/utils/util.go +++ b/stackit/internal/services/resourcemanager/utils/util.go @@ -19,6 +19,7 @@ func ConfigureClient(ctx context.Context, providerData *core.ProviderData, diags if providerData.ResourceManagerCustomEndpoint != "" { apiClientConfigOptions = append(apiClientConfigOptions, config.WithEndpoint(providerData.ResourceManagerCustomEndpoint)) } + apiClient, err := resourcemanager.NewAPIClient(apiClientConfigOptions...) if err != nil { core.LogAndAddError(ctx, diags, "Error configuring API client", fmt.Sprintf("Configuring client: %v. This is an error related to the provider configuration, not to the resource configuration", err)) diff --git a/stackit/internal/services/resourcemanager/utils/util_test.go b/stackit/internal/services/resourcemanager/utils/util_test.go index 352c8fb9e..6adab5d65 100644 --- a/stackit/internal/services/resourcemanager/utils/util_test.go +++ b/stackit/internal/services/resourcemanager/utils/util_test.go @@ -22,6 +22,7 @@ const ( func TestConfigureClient(t *testing.T) { /* mock authentication by setting service account token env variable */ os.Clearenv() + err := os.Setenv(sdkClients.ServiceAccountToken, "mock-val") if err != nil { t.Errorf("error setting env variable: %v", err) @@ -30,6 +31,7 @@ func TestConfigureClient(t *testing.T) { type args struct { providerData *core.ProviderData } + tests := []struct { name string args args @@ -50,6 +52,7 @@ func TestConfigureClient(t *testing.T) { if err != nil { t.Errorf("error configuring client: %v", err) } + return apiClient }(), wantErr: false, @@ -70,6 +73,7 @@ func TestConfigureClient(t *testing.T) { if err != nil { t.Errorf("error configuring client: %v", err) } + return apiClient }(), wantErr: false, @@ -81,6 +85,7 @@ func TestConfigureClient(t *testing.T) { diags := diag.Diagnostics{} actual := ConfigureClient(ctx, tt.args.providerData, &diags) + if diags.HasError() != tt.wantErr { t.Errorf("ConfigureClient() error = %v, want %v", diags.HasError(), tt.wantErr) } diff --git a/stackit/internal/services/scf/organization/datasource.go b/stackit/internal/services/scf/organization/datasource.go index 528e46da9..218702933 100644 --- a/stackit/internal/services/scf/organization/datasource.go +++ b/stackit/internal/services/scf/organization/datasource.go @@ -11,7 +11,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/stackitcloud/stackit-sdk-go/services/scf" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" scfUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/scf/utils" @@ -38,16 +37,20 @@ type scfOrganizationDataSource struct { func (s *scfOrganizationDataSource) Configure(ctx context.Context, request datasource.ConfigureRequest, response *datasource.ConfigureResponse) { var ok bool + s.providerData, ok = conversion.ParseProviderData(ctx, request.ProviderData, &response.Diagnostics) if !ok { return } apiClient := scfUtils.ConfigureClient(ctx, &s.providerData, &response.Diagnostics) + if response.Diagnostics.HasError() { return } + s.client = apiClient + tflog.Info(ctx, "scf client configured") } @@ -132,6 +135,7 @@ func (s *scfOrganizationDataSource) Read(ctx context.Context, request datasource var model Model diags := request.Config.Get(ctx, &model) response.Diagnostics.Append(diags...) + if response.Diagnostics.HasError() { return } @@ -160,6 +164,7 @@ func (s *scfOrganizationDataSource) Read(ctx context.Context, request datasource }, ) response.State.RemoveResource(ctx) + return } diff --git a/stackit/internal/services/scf/organization/resource.go b/stackit/internal/services/scf/organization/resource.go index bdec91405..1309942f2 100644 --- a/stackit/internal/services/scf/organization/resource.go +++ b/stackit/internal/services/scf/organization/resource.go @@ -20,7 +20,6 @@ import ( "github.com/stackitcloud/stackit-sdk-go/core/oapierror" "github.com/stackitcloud/stackit-sdk-go/services/scf" "github.com/stackitcloud/stackit-sdk-go/services/scf/wait" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" scfUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/scf/utils" @@ -61,7 +60,7 @@ type scfOrganizationResource struct { providerData core.ProviderData } -// descriptions for the attributes in the Schema +// descriptions for the attributes in the Schema. var descriptions = map[string]string{ "id": "Terraform's internal resource ID, structured as \"`project_id`,`region`,`org_id`\".", "created_at": "The time when the organization was created", @@ -78,16 +77,20 @@ var descriptions = map[string]string{ func (s *scfOrganizationResource) Configure(ctx context.Context, request resource.ConfigureRequest, response *resource.ConfigureResponse) { var ok bool + s.providerData, ok = conversion.ParseProviderData(ctx, request.ProviderData, &response.Diagnostics) if !ok { return } apiClient := scfUtils.ConfigureClient(ctx, &s.providerData, &response.Diagnostics) + if response.Diagnostics.HasError() { return } + s.client = apiClient + tflog.Info(ctx, "scf client configured") } @@ -97,29 +100,35 @@ func (s *scfOrganizationResource) Metadata(_ context.Context, request resource.M // ModifyPlan implements resource.ResourceWithModifyPlan. // Use the modifier to set the effective region in the current plan. -func (r *scfOrganizationResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform +func (r *scfOrganizationResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { //nolint:gocritic // function signature required by Terraform var configModel Model // skip initial empty configuration to avoid follow-up errors if req.Config.Raw.IsNull() { return } + resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...) + if resp.Diagnostics.HasError() { return } var planModel Model + resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) + if resp.Diagnostics.HasError() { return } utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp) + if resp.Diagnostics.HasError() { return } resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...) + if resp.Diagnostics.HasError() { return } @@ -223,11 +232,12 @@ func (s *scfOrganizationResource) Schema(_ context.Context, _ resource.SchemaReq } } -func (s *scfOrganizationResource) Create(ctx context.Context, request resource.CreateRequest, response *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform +func (s *scfOrganizationResource) Create(ctx context.Context, request resource.CreateRequest, response *resource.CreateResponse) { //nolint:gocritic // function signature required by Terraform // Retrieve the planned values for the resource. var model Model diags := request.Plan.Get(ctx, &model) response.Diagnostics.Append(diags...) + if response.Diagnostics.HasError() { return } @@ -255,6 +265,7 @@ func (s *scfOrganizationResource) Create(ctx context.Context, request resource.C core.LogAndAddError(ctx, &response.Diagnostics, "Error creating scf organization", fmt.Sprintf("Calling API to create org: %v", err)) return } + orgId := *scfOrgCreateResponse.Guid // Apply the org quota if provided @@ -267,6 +278,7 @@ func (s *scfOrganizationResource) Create(ctx context.Context, request resource.C core.LogAndAddError(ctx, &response.Diagnostics, "Error creating scf organization", fmt.Sprintf("Calling API to apply quota: %v", err)) return } + model.QuotaId = types.StringPointerValue(applyOrgQuota.QuotaId) } @@ -298,18 +310,21 @@ func (s *scfOrganizationResource) Create(ctx context.Context, request resource.C // Set the state with fully populated data. diags = response.State.Set(ctx, model) response.Diagnostics.Append(diags...) + if response.Diagnostics.HasError() { return } + tflog.Info(ctx, "Scf organization created") } // Read refreshes the Terraform state with the latest scf organization data. -func (s *scfOrganizationResource) Read(ctx context.Context, request resource.ReadRequest, response *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (s *scfOrganizationResource) Read(ctx context.Context, request resource.ReadRequest, response *resource.ReadResponse) { //nolint:gocritic // function signature required by Terraform // Retrieve the current state of the resource. var model Model diags := request.State.Get(ctx, &model) response.Diagnostics.Append(diags...) + if response.Diagnostics.HasError() { return } @@ -326,12 +341,15 @@ func (s *scfOrganizationResource) Read(ctx context.Context, request resource.Rea scfOrgResponse, err := s.client.GetOrganization(ctx, projectId, region, orgId).Execute() if err != nil { var oapiErr *oapierror.GenericOpenAPIError + ok := errors.As(err, &oapiErr) if ok && oapiErr.StatusCode == http.StatusNotFound { response.State.RemoveResource(ctx) return } + core.LogAndAddError(ctx, &response.Diagnostics, "Error reading scf organization", fmt.Sprintf("Calling API: %v", err)) + return } @@ -348,14 +366,16 @@ func (s *scfOrganizationResource) Read(ctx context.Context, request resource.Rea } // Update attempts to update the resource. -func (s *scfOrganizationResource) Update(ctx context.Context, request resource.UpdateRequest, response *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform +func (s *scfOrganizationResource) Update(ctx context.Context, request resource.UpdateRequest, response *resource.UpdateResponse) { //nolint:gocritic // function signature required by Terraform // Retrieve values from plan var model Model diags := request.Plan.Get(ctx, &model) response.Diagnostics.Append(diags...) + if response.Diagnostics.HasError() { return } + region := model.Region.ValueString() projectId := model.ProjectId.ValueString() orgId := model.OrgId.ValueString() @@ -384,6 +404,7 @@ func (s *scfOrganizationResource) Update(ctx context.Context, request resource.U core.LogAndAddError(ctx, &response.Diagnostics, "Error updating organization", fmt.Sprintf("Processing API payload: %v", err)) return } + org = updatedOrg } @@ -397,6 +418,7 @@ func (s *scfOrganizationResource) Update(ctx context.Context, request resource.U core.LogAndAddError(ctx, &response.Diagnostics, "Error applying organization quota", fmt.Sprintf("Processing API payload: %v", err)) return } + org.QuotaId = applyOrgQuota.QuotaId } @@ -408,18 +430,21 @@ func (s *scfOrganizationResource) Update(ctx context.Context, request resource.U diags = response.State.Set(ctx, model) response.Diagnostics.Append(diags...) + if response.Diagnostics.HasError() { return } + tflog.Info(ctx, "organization updated") } // Delete deletes the git instance and removes it from the Terraform state on success. -func (s *scfOrganizationResource) Delete(ctx context.Context, request resource.DeleteRequest, response *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform +func (s *scfOrganizationResource) Delete(ctx context.Context, request resource.DeleteRequest, response *resource.DeleteResponse) { //nolint:gocritic // function signature required by Terraform // Retrieve current state of the resource. var model Model diags := request.State.Get(ctx, &model) response.Diagnostics.Append(diags...) + if response.Diagnostics.HasError() { return } @@ -460,6 +485,7 @@ func (s *scfOrganizationResource) ImportState(ctx context.Context, request resou "Error importing scf organization", fmt.Sprintf("Expected import identifier with format: [project_id],[region],[org_id] Got: %q", request.ID), ) + return } @@ -478,6 +504,7 @@ func mapFields(response *scf.Organization, model *Model) error { if response == nil { return fmt.Errorf("response input is nil") } + if model == nil { return fmt.Errorf("model input is nil") } @@ -521,10 +548,11 @@ func mapFields(response *scf.Organization, model *Model) error { model.QuotaId = types.StringPointerValue(response.QuotaId) model.CreateAt = types.StringValue(response.CreatedAt.String()) model.UpdatedAt = types.StringValue(response.UpdatedAt.String()) + return nil } -// toCreatePayload creates the payload to create a scf organization instance +// toCreatePayload creates the payload to create a scf organization instance. func toCreatePayload(model *Model) (scf.CreateOrganizationPayload, error) { if model == nil { return scf.CreateOrganizationPayload{}, fmt.Errorf("nil model") @@ -536,5 +564,6 @@ func toCreatePayload(model *Model) (scf.CreateOrganizationPayload, error) { if !model.PlatformId.IsNull() && !model.PlatformId.IsUnknown() { payload.PlatformId = model.PlatformId.ValueStringPointer() } + return payload, nil } diff --git a/stackit/internal/services/scf/organization/resource_test.go b/stackit/internal/services/scf/organization/resource_test.go index 956933ff6..a76160b6c 100644 --- a/stackit/internal/services/scf/organization/resource_test.go +++ b/stackit/internal/services/scf/organization/resource_test.go @@ -113,14 +113,17 @@ func TestMapFields(t *testing.T) { if tt.expected != nil { state.ProjectId = tt.expected.ProjectId } + err := mapFields(tt.input, state) if tt.isValid && err != nil { t.Fatalf("expected success, got error: %v", err) } + if !tt.isValid && err == nil { t.Fatalf("expected error, got nil") } + if tt.isValid { if diff := cmp.Diff(tt.expected, state); diff != "" { t.Errorf("unexpected diff (-want +got):\n%s", diff) diff --git a/stackit/internal/services/scf/organizationmanager/datasource.go b/stackit/internal/services/scf/organizationmanager/datasource.go index bf64b31e7..f950e2862 100644 --- a/stackit/internal/services/scf/organizationmanager/datasource.go +++ b/stackit/internal/services/scf/organizationmanager/datasource.go @@ -12,7 +12,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/stackitcloud/stackit-sdk-go/services/scf" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" scfUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/scf/utils" @@ -51,16 +50,20 @@ type scfOrganizationManagerDataSource struct { func (s *scfOrganizationManagerDataSource) Configure(ctx context.Context, request datasource.ConfigureRequest, response *datasource.ConfigureResponse) { var ok bool + s.providerData, ok = conversion.ParseProviderData(ctx, request.ProviderData, &response.Diagnostics) if !ok { return } apiClient := scfUtils.ConfigureClient(ctx, &s.providerData, &response.Diagnostics) + if response.Diagnostics.HasError() { return } + s.client = apiClient + tflog.Info(ctx, "scf client configured for scfOrganizationManagerDataSource") } @@ -137,6 +140,7 @@ func (s *scfOrganizationManagerDataSource) Read(ctx context.Context, request dat var model DataSourceModel diags := request.Config.Get(ctx, &model) response.Diagnostics.Append(diags...) + if response.Diagnostics.HasError() { return } @@ -163,6 +167,7 @@ func (s *scfOrganizationManagerDataSource) Read(ctx context.Context, request dat }, ) response.State.RemoveResource(ctx) + return } @@ -182,6 +187,7 @@ func mapFieldsDataSource(response *scf.OrgManager, model *DataSourceModel) error if response == nil { return fmt.Errorf("response input is nil") } + if model == nil { return fmt.Errorf("model input is nil") } @@ -234,5 +240,6 @@ func mapFieldsDataSource(response *scf.OrgManager, model *DataSourceModel) error model.UserName = types.StringPointerValue(response.Username) model.CreateAt = types.StringValue(response.CreatedAt.String()) model.UpdatedAt = types.StringValue(response.UpdatedAt.String()) + return nil } diff --git a/stackit/internal/services/scf/organizationmanager/datasource_test.go b/stackit/internal/services/scf/organizationmanager/datasource_test.go index 4ed5e0040..c3fda264e 100644 --- a/stackit/internal/services/scf/organizationmanager/datasource_test.go +++ b/stackit/internal/services/scf/organizationmanager/datasource_test.go @@ -98,14 +98,17 @@ func TestMapFieldsDataSource(t *testing.T) { if tt.expected != nil { state.ProjectId = tt.expected.ProjectId } + err := mapFieldsDataSource(tt.input, state) if tt.isValid && err != nil { t.Fatalf("expected success, got error: %v", err) } + if !tt.isValid && err == nil { t.Fatalf("expected error, got nil") } + if tt.isValid { if diff := cmp.Diff(tt.expected, state); diff != "" { t.Errorf("unexpected diff (-want +got):\n%s", diff) diff --git a/stackit/internal/services/scf/organizationmanager/resource.go b/stackit/internal/services/scf/organizationmanager/resource.go index d57894a82..d85b5d1f5 100644 --- a/stackit/internal/services/scf/organizationmanager/resource.go +++ b/stackit/internal/services/scf/organizationmanager/resource.go @@ -18,7 +18,6 @@ import ( "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/stackitcloud/stackit-sdk-go/core/oapierror" "github.com/stackitcloud/stackit-sdk-go/services/scf" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" scfUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/scf/utils" @@ -58,7 +57,7 @@ type scfOrganizationManagerResource struct { providerData core.ProviderData } -// descriptions for the attributes in the Schema +// descriptions for the attributes in the Schema. var descriptions = map[string]string{ "id": "Terraform's internal resource ID, structured as \"`project_id`,`region`,`org_id`,`user_id`\".", "region": "The region where the organization of the organization manager is located. If not defined, the provider region is used", @@ -74,16 +73,20 @@ var descriptions = map[string]string{ func (s *scfOrganizationManagerResource) Configure(ctx context.Context, request resource.ConfigureRequest, response *resource.ConfigureResponse) { // nolint:gocritic // function signature required by Terraform var ok bool + s.providerData, ok = conversion.ParseProviderData(ctx, request.ProviderData, &response.Diagnostics) if !ok { return } apiClient := scfUtils.ConfigureClient(ctx, &s.providerData, &response.Diagnostics) + if response.Diagnostics.HasError() { return } + s.client = apiClient + tflog.Info(ctx, "scf client configured") } @@ -99,23 +102,29 @@ func (r *scfOrganizationManagerResource) ModifyPlan(ctx context.Context, req res if req.Config.Raw.IsNull() { return } + resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...) + if resp.Diagnostics.HasError() { return } var planModel Model + resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) + if resp.Diagnostics.HasError() { return } utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp) + if resp.Diagnostics.HasError() { return } resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...) + if resp.Diagnostics.HasError() { return } @@ -210,6 +219,7 @@ func (s *scfOrganizationManagerResource) Create(ctx context.Context, request res var model Model diags := request.Plan.Get(ctx, &model) response.Diagnostics.Append(diags...) + if response.Diagnostics.HasError() { return } @@ -224,6 +234,7 @@ func (s *scfOrganizationManagerResource) Create(ctx context.Context, request res ctx = tflog.SetField(ctx, "region", region) response.Diagnostics.Append(diags...) + if response.Diagnostics.HasError() { return } @@ -244,9 +255,11 @@ func (s *scfOrganizationManagerResource) Create(ctx context.Context, request res // Set the state with fully populated data. diags = response.State.Set(ctx, model) response.Diagnostics.Append(diags...) + if response.Diagnostics.HasError() { return } + tflog.Info(ctx, "Scf organization manager created") } @@ -255,6 +268,7 @@ func (s *scfOrganizationManagerResource) Read(ctx context.Context, request resou var model Model diags := request.State.Get(ctx, &model) response.Diagnostics.Append(diags...) + if response.Diagnostics.HasError() { return } @@ -271,13 +285,17 @@ func (s *scfOrganizationManagerResource) Read(ctx context.Context, request resou scfOrgManager, err := s.client.GetOrgManagerExecute(ctx, projectId, region, orgId) if err != nil { var oapiErr *oapierror.GenericOpenAPIError + ok := errors.As(err, &oapiErr) if ok && oapiErr.StatusCode == http.StatusNotFound { core.LogAndAddWarning(ctx, &response.Diagnostics, "SCF Organization manager not found", "SCF Organization manager not found, remove from state") response.State.RemoveResource(ctx) + return } + core.LogAndAddError(ctx, &response.Diagnostics, "Error reading scf organization manager", fmt.Sprintf("Calling API: %v", err)) + return } @@ -303,6 +321,7 @@ func (s *scfOrganizationManagerResource) Delete(ctx context.Context, request res var model Model diags := request.State.Get(ctx, &model) response.Diagnostics.Append(diags...) + if response.Diagnostics.HasError() { return } @@ -318,14 +337,18 @@ func (s *scfOrganizationManagerResource) Delete(ctx context.Context, request res _, err := s.client.DeleteOrgManagerExecute(ctx, projectId, region, orgId) if err != nil { var oapiErr *oapierror.GenericOpenAPIError + ok := errors.As(err, &oapiErr) if ok && oapiErr.StatusCode == http.StatusGone { tflog.Info(ctx, "Scf organization manager was already deleted") return } + core.LogAndAddError(ctx, &response.Diagnostics, "Error deleting scf organization manager", fmt.Sprintf("Calling API: %v", err)) + return } + tflog.Info(ctx, "Scf organization manager deleted") } @@ -339,6 +362,7 @@ func (s *scfOrganizationManagerResource) ImportState(ctx context.Context, reques "Error importing scf organization manager", fmt.Sprintf("Expected import identifier with format: [project_id],[region],[org_id],[user_id] Got: %q", request.ID), ) + return } @@ -358,6 +382,7 @@ func mapFieldsCreate(response *scf.OrgManagerResponse, model *Model) error { if response == nil { return fmt.Errorf("response input is nil") } + if model == nil { return fmt.Errorf("model input is nil") } @@ -408,6 +433,7 @@ func mapFieldsCreate(response *scf.OrgManagerResponse, model *Model) error { model.Password = types.StringPointerValue(response.Password) model.CreateAt = types.StringValue(response.CreatedAt.String()) model.UpdatedAt = types.StringValue(response.UpdatedAt.String()) + return nil } @@ -415,6 +441,7 @@ func mapFieldsRead(response *scf.OrgManager, model *Model) error { if response == nil { return fmt.Errorf("response input is nil") } + if model == nil { return fmt.Errorf("model input is nil") } @@ -467,5 +494,6 @@ func mapFieldsRead(response *scf.OrgManager, model *Model) error { model.UserName = types.StringPointerValue(response.Username) model.CreateAt = types.StringValue(response.CreatedAt.String()) model.UpdatedAt = types.StringValue(response.UpdatedAt.String()) + return nil } diff --git a/stackit/internal/services/scf/organizationmanager/resource_test.go b/stackit/internal/services/scf/organizationmanager/resource_test.go index 1ca257596..b0deaf5f1 100644 --- a/stackit/internal/services/scf/organizationmanager/resource_test.go +++ b/stackit/internal/services/scf/organizationmanager/resource_test.go @@ -108,14 +108,17 @@ func TestMapFields(t *testing.T) { if tt.expected != nil { state.ProjectId = tt.expected.ProjectId } + err := mapFieldsRead(tt.input, state) if tt.isValid && err != nil { t.Fatalf("expected success, got error: %v", err) } + if !tt.isValid && err == nil { t.Fatalf("expected error, got nil") } + if tt.isValid { if diff := cmp.Diff(tt.expected, state); diff != "" { t.Errorf("unexpected diff (-want +got):\n%s", diff) @@ -215,14 +218,17 @@ func TestMapFieldsCreate(t *testing.T) { if tt.expected != nil { state.ProjectId = tt.expected.ProjectId } + err := mapFieldsCreate(tt.input, state) if tt.isValid && err != nil { t.Fatalf("expected success, got error: %v", err) } + if !tt.isValid && err == nil { t.Fatalf("expected error, got nil") } + if tt.isValid { if diff := cmp.Diff(tt.expected, state); diff != "" { t.Errorf("unexpected diff (-want +got):\n%s", diff) diff --git a/stackit/internal/services/scf/platform/datasource.go b/stackit/internal/services/scf/platform/datasource.go index a4bfacc1e..a0de1469d 100644 --- a/stackit/internal/services/scf/platform/datasource.go +++ b/stackit/internal/services/scf/platform/datasource.go @@ -11,7 +11,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/stackitcloud/stackit-sdk-go/services/scf" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" scfUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/scf/utils" @@ -38,16 +37,20 @@ type scfPlatformDataSource struct { func (s *scfPlatformDataSource) Configure(ctx context.Context, request datasource.ConfigureRequest, response *datasource.ConfigureResponse) { var ok bool + s.providerData, ok = conversion.ParseProviderData(ctx, request.ProviderData, &response.Diagnostics) if !ok { return } apiClient := scfUtils.ConfigureClient(ctx, &s.providerData, &response.Diagnostics) + if response.Diagnostics.HasError() { return } + s.client = apiClient + tflog.Info(ctx, "scf client configured for platform") } @@ -66,7 +69,7 @@ type Model struct { ConsoleUrl types.String `tfsdk:"console_url"` } -// descriptions for the attributes in the Schema +// descriptions for the attributes in the Schema. var descriptions = map[string]string{ "id": "Terraform's internal resource ID, structured as \"`project_id`,`region`,`platform_id`\".", "platform_id": "The unique id of the platform", @@ -132,6 +135,7 @@ func (s *scfPlatformDataSource) Read(ctx context.Context, request datasource.Rea var model Model diags := request.Config.Get(ctx, &model) response.Diagnostics.Append(diags...) + if response.Diagnostics.HasError() { return } @@ -158,6 +162,7 @@ func (s *scfPlatformDataSource) Read(ctx context.Context, request datasource.Rea }, ) response.State.RemoveResource(ctx) + return } @@ -178,14 +183,17 @@ func mapFields(response *scf.Platforms, model *Model) error { if response == nil { return fmt.Errorf("response input is nil") } + if model == nil { return fmt.Errorf("model input is nil") } var projectId string + if model.ProjectId.ValueString() == "" { return fmt.Errorf("project id is not present") } + projectId = model.ProjectId.ValueString() var region string @@ -215,5 +223,6 @@ func mapFields(response *scf.Platforms, model *Model) error { model.Region = types.StringValue(region) model.ApiUrl = types.StringPointerValue(response.ApiUrl) model.ConsoleUrl = types.StringPointerValue(response.ConsoleUrl) + return nil } diff --git a/stackit/internal/services/scf/platform/datasource_test.go b/stackit/internal/services/scf/platform/datasource_test.go index b15ee231c..7aa502bd4 100644 --- a/stackit/internal/services/scf/platform/datasource_test.go +++ b/stackit/internal/services/scf/platform/datasource_test.go @@ -91,14 +91,17 @@ func TestMapFields(t *testing.T) { if tt.expected != nil { state.ProjectId = tt.expected.ProjectId } + err := mapFields(tt.input, state) if tt.isValid && err != nil { t.Fatalf("expected success, got error: %v", err) } + if !tt.isValid && err == nil { t.Fatalf("expected error, got nil") } + if tt.isValid { if diff := cmp.Diff(tt.expected, state); diff != "" { t.Errorf("unexpected diff (-want +got):\n%s", diff) diff --git a/stackit/internal/services/scf/scf_acc_test.go b/stackit/internal/services/scf/scf_acc_test.go index 3003ba6d6..9498d504d 100644 --- a/stackit/internal/services/scf/scf_acc_test.go +++ b/stackit/internal/services/scf/scf_acc_test.go @@ -8,15 +8,13 @@ import ( "strings" "testing" - "github.com/stackitcloud/stackit-sdk-go/services/scf" - "github.com/hashicorp/terraform-plugin-testing/config" "github.com/hashicorp/terraform-plugin-testing/helper/acctest" "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/terraform" stackitSdkConfig "github.com/stackitcloud/stackit-sdk-go/core/config" "github.com/stackitcloud/stackit-sdk-go/core/utils" - + "github.com/stackitcloud/stackit-sdk-go/services/scf" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/testutil" ) @@ -27,11 +25,13 @@ var resourceMin string //go:embed testdata/resource-max.tf var resourceMax string -var randName = acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) -var nameMin = fmt.Sprintf("scf-min-%s-org", randName) -var nameMinUpdated = fmt.Sprintf("scf-min-%s-upd-org", randName) -var nameMax = fmt.Sprintf("scf-max-%s-org", randName) -var nameMaxUpdated = fmt.Sprintf("scf-max-%s-upd-org", randName) +var ( + randName = acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum) + nameMin = fmt.Sprintf("scf-min-%s-org", randName) + nameMinUpdated = fmt.Sprintf("scf-min-%s-upd-org", randName) + nameMax = fmt.Sprintf("scf-max-%s-org", randName) + nameMaxUpdated = fmt.Sprintf("scf-max-%s-upd-org", randName) +) const ( platformName = "Shared Cloud Foundry (public)" @@ -64,6 +64,7 @@ func testScfOrgConfigVarsMinUpdated() config.Variables { maps.Copy(tempConfig, testConfigVarsMin) // update scf organization to a new name tempConfig["name"] = config.StringVariable(nameMinUpdated) + return tempConfig } @@ -74,6 +75,7 @@ func testScfOrgConfigVarsMaxUpdated() config.Variables { tempConfig["name"] = config.StringVariable(nameMaxUpdated) tempConfig["quota_id"] = config.StringVariable(quotaIdMaxUpdated) tempConfig["suspended"] = config.BoolVariable(!suspendedMax) + return tempConfig } @@ -205,6 +207,7 @@ func TestAccScfOrganizationMin(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute region") } + return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, regionInAttributes, orgId), nil }, ImportState: true, @@ -383,6 +386,7 @@ func TestAccScfOrgMax(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute region") } + return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, regionInAttributes, orgId), nil }, ImportState: true, @@ -411,7 +415,9 @@ func TestAccScfOrgMax(t *testing.T) { func testAccCheckScfOrganizationDestroy(s *terraform.State) error { ctx := context.Background() + var client *scf.APIClient + var err error if testutil.ScfCustomEndpoint == "" { @@ -427,10 +433,12 @@ func testAccCheckScfOrganizationDestroy(s *terraform.State) error { } var orgsToDestroy []string + for _, rs := range s.RootModule().Resources { if rs.Type != "stackit_scf_organization" { continue } + orgId := strings.Split(rs.Primary.ID, core.Separator)[1] orgsToDestroy = append(orgsToDestroy, orgId) } @@ -445,6 +453,7 @@ func testAccCheckScfOrganizationDestroy(s *terraform.State) error { if scfOrgs[i].Guid == nil { continue } + if utils.Contains(orgsToDestroy, *scfOrgs[i].Guid) { _, err := client.DeleteOrganizationExecute(ctx, testutil.ProjectId, testutil.Region, *scfOrgs[i].Guid) if err != nil { @@ -452,5 +461,6 @@ func testAccCheckScfOrganizationDestroy(s *terraform.State) error { } } } + return nil } diff --git a/stackit/internal/services/scf/utils/utils.go b/stackit/internal/services/scf/utils/utils.go index 347e109bb..ce2987345 100644 --- a/stackit/internal/services/scf/utils/utils.go +++ b/stackit/internal/services/scf/utils/utils.go @@ -7,7 +7,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/stackitcloud/stackit-sdk-go/core/config" "github.com/stackitcloud/stackit-sdk-go/services/scf" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" ) @@ -20,6 +19,7 @@ func ConfigureClient(ctx context.Context, providerData *core.ProviderData, diags if providerData.ScfCustomEndpoint != "" { apiClientConfigOptions = append(apiClientConfigOptions, config.WithEndpoint(providerData.ScfCustomEndpoint)) } + apiClient, err := scf.NewAPIClient(apiClientConfigOptions...) if err != nil { core.LogAndAddError(ctx, diags, "Error configuring API client", fmt.Sprintf("Configuring client: %v. This is an error related to the provider configuration, not to the resource configuration", err)) diff --git a/stackit/internal/services/scf/utils/utils_test.go b/stackit/internal/services/scf/utils/utils_test.go index 1a77a0ae3..26bb0b50c 100644 --- a/stackit/internal/services/scf/utils/utils_test.go +++ b/stackit/internal/services/scf/utils/utils_test.go @@ -10,7 +10,6 @@ import ( sdkClients "github.com/stackitcloud/stackit-sdk-go/core/clients" "github.com/stackitcloud/stackit-sdk-go/core/config" "github.com/stackitcloud/stackit-sdk-go/services/scf" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" ) @@ -23,6 +22,7 @@ const ( func TestConfigureClient(t *testing.T) { /* mock authentication by setting service account token env variable */ os.Clearenv() + err := os.Setenv(sdkClients.ServiceAccountToken, "mock-val") if err != nil { t.Errorf("error setting env variable: %v", err) @@ -31,6 +31,7 @@ func TestConfigureClient(t *testing.T) { type args struct { providerData *core.ProviderData } + tests := []struct { name string args args @@ -51,6 +52,7 @@ func TestConfigureClient(t *testing.T) { if err != nil { t.Errorf("error configuring client: %v", err) } + return apiClient }(), wantErr: false, @@ -71,6 +73,7 @@ func TestConfigureClient(t *testing.T) { if err != nil { t.Errorf("error configuring client: %v", err) } + return apiClient }(), wantErr: false, @@ -82,6 +85,7 @@ func TestConfigureClient(t *testing.T) { diags := diag.Diagnostics{} actual := ConfigureClient(ctx, tt.args.providerData, &diags) + if diags.HasError() != tt.wantErr { t.Errorf("ConfigureClient() error = %v, want %v", diags.HasError(), tt.wantErr) } diff --git a/stackit/internal/services/secretsmanager/instance/datasource.go b/stackit/internal/services/secretsmanager/instance/datasource.go index 6cb409aca..a0de1e9a6 100644 --- a/stackit/internal/services/secretsmanager/instance/datasource.go +++ b/stackit/internal/services/secretsmanager/instance/datasource.go @@ -5,19 +5,17 @@ import ( "fmt" "net/http" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - secretsmanagerUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/secretsmanager/utils" - "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/stackitcloud/stackit-sdk-go/services/secretsmanager" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + secretsmanagerUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/secretsmanager/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - - "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/stackitcloud/stackit-sdk-go/services/secretsmanager" ) // Ensure the implementation satisfies the expected interfaces. @@ -48,10 +46,13 @@ func (r *instanceDataSource) Configure(ctx context.Context, req datasource.Confi } apiClient := secretsmanagerUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + r.client = apiClient + tflog.Info(ctx, "Secrets Manager instance client configured") } @@ -103,13 +104,15 @@ func (r *instanceDataSource) Schema(_ context.Context, _ datasource.SchemaReques } // Read refreshes the Terraform state with the latest data. -func (r *instanceDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (r *instanceDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.Config.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() instanceId := model.InstanceId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) @@ -128,8 +131,10 @@ func (r *instanceDataSource) Read(ctx context.Context, req datasource.ReadReques }, ) resp.State.RemoveResource(ctx) + return } + aclList, err := r.client.ListACLs(ctx, projectId, instanceId).Execute() if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Calling API for ACLs data: %v", err)) @@ -145,8 +150,10 @@ func (r *instanceDataSource) Read(ctx context.Context, req datasource.ReadReques // Set refreshed state diags = resp.State.Set(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "Secrets Manager instance read") } diff --git a/stackit/internal/services/secretsmanager/instance/resource.go b/stackit/internal/services/secretsmanager/instance/resource.go index 5f7bbc2f9..cd363cab1 100644 --- a/stackit/internal/services/secretsmanager/instance/resource.go +++ b/stackit/internal/services/secretsmanager/instance/resource.go @@ -6,28 +6,25 @@ import ( "net/http" "strings" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - - secretsmanagerUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/secretsmanager/utils" - "github.com/hashicorp/terraform-plugin-framework-validators/setvalidator" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/attr" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/stackitcloud/stackit-sdk-go/core/oapierror" sdkUtils "github.com/stackitcloud/stackit-sdk-go/core/utils" "github.com/stackitcloud/stackit-sdk-go/services/secretsmanager" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + secretsmanagerUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/secretsmanager/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" ) // Ensure the implementation satisfies the expected interfaces. @@ -68,10 +65,13 @@ func (r *instanceResource) Configure(ctx context.Context, req resource.Configure } apiClient := secretsmanagerUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + r.client = apiClient + tflog.Info(ctx, "Secrets Manager instance client configured") } @@ -143,20 +143,23 @@ func (r *instanceResource) Schema(_ context.Context, _ resource.SchemaRequest, r } // Create creates the resource and sets the initial Terraform state. -func (r *instanceResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *instanceResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) var acls []string - if !(model.ACLs.IsNull() || model.ACLs.IsUnknown()) { + if !model.ACLs.IsNull() && !model.ACLs.IsUnknown() { diags = model.ACLs.ElementsAs(ctx, &acls, false) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -174,6 +177,7 @@ func (r *instanceResource) Create(ctx context.Context, req resource.CreateReques core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Calling API: %v", err)) return } + instanceId := *createResp.Id ctx = tflog.SetField(ctx, "instance_id", instanceId) @@ -183,6 +187,7 @@ func (r *instanceResource) Create(ctx context.Context, req resource.CreateReques core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Creating ACLs: %v", err)) return } + aclList, err := r.client.ListACLs(ctx, projectId, instanceId).Execute() if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Calling API for ACLs data: %v", err)) @@ -199,20 +204,24 @@ func (r *instanceResource) Create(ctx context.Context, req resource.CreateReques // Set state to fully populated data diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "Secrets Manager instance created") } // Read refreshes the Terraform state with the latest data. -func (r *instanceResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (r *instanceResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() instanceId := model.InstanceId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) @@ -225,9 +234,12 @@ func (r *instanceResource) Read(ctx context.Context, req resource.ReadRequest, r resp.State.RemoveResource(ctx) return } + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Calling API: %v", err)) + return } + aclList, err := r.client.ListACLs(ctx, projectId, instanceId).Execute() if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", fmt.Sprintf("Calling API for ACLs data: %v", err)) @@ -244,29 +256,34 @@ func (r *instanceResource) Read(ctx context.Context, req resource.ReadRequest, r // Set refreshed state diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "Secrets Manager instance read") } // Update updates the resource and sets the updated Terraform state on success. -func (r *instanceResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *instanceResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() instanceId := model.InstanceId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) ctx = tflog.SetField(ctx, "instance_id", instanceId) var acls []string - if !(model.ACLs.IsNull() || model.ACLs.IsUnknown()) { + if !model.ACLs.IsNull() && !model.ACLs.IsUnknown() { diags = model.ACLs.ElementsAs(ctx, &acls, false) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -284,6 +301,7 @@ func (r *instanceResource) Update(ctx context.Context, req resource.UpdateReques core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Calling API: %v", err)) return } + aclList, err := r.client.ListACLs(ctx, projectId, instanceId).Execute() if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Calling API for ACLs data: %v", err)) @@ -299,21 +317,25 @@ func (r *instanceResource) Update(ctx context.Context, req resource.UpdateReques diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "Secrets Manager instance updated") } // Delete deletes the resource and removes the Terraform state on success. -func (r *instanceResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform +func (r *instanceResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { //nolint:gocritic // function signature required by Terraform // Retrieve values from state var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() instanceId := model.InstanceId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) @@ -325,11 +347,12 @@ func (r *instanceResource) Delete(ctx context.Context, req resource.DeleteReques core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting instance", fmt.Sprintf("Calling API: %v", err)) return } + tflog.Info(ctx, "Secrets Manager instance deleted") } // ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: project_id,instance_id +// The expected format of the resource import identifier is: project_id,instance_id. func (r *instanceResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { idParts := strings.Split(req.ID, core.Separator) @@ -338,6 +361,7 @@ func (r *instanceResource) ImportState(ctx context.Context, req resource.ImportS "Error importing instance", fmt.Sprintf("Expected import identifier with format: [project_id],[instance_id] Got: %q", req.ID), ) + return } @@ -350,6 +374,7 @@ func mapFields(instance *secretsmanager.Instance, aclList *secretsmanager.ListAC if instance == nil { return fmt.Errorf("response input is nil") } + if model == nil { return fmt.Errorf("model input is nil") } @@ -379,6 +404,7 @@ func mapACLs(aclList *secretsmanager.ListACLsResponse, model *Model) error { if aclList == nil { return fmt.Errorf("nil ACL list") } + if aclList.Acls == nil || len(*aclList.Acls) == 0 { model.ACLs = types.SetNull(types.StringType) return nil @@ -388,11 +414,14 @@ func mapACLs(aclList *secretsmanager.ListACLsResponse, model *Model) error { for _, acl := range *aclList.Acls { acls = append(acls, types.StringValue(*acl.Cidr)) } + aclsList, diags := types.SetValue(types.StringType, acls) if diags.HasError() { return fmt.Errorf("mapping ACLs: %w", core.DiagsToError(diags)) } + model.ACLs = aclsList + return nil } @@ -400,12 +429,13 @@ func toCreatePayload(model *Model) (*secretsmanager.CreateInstancePayload, error if model == nil { return nil, fmt.Errorf("nil model") } + return &secretsmanager.CreateInstancePayload{ Name: conversion.StringValueToPointer(model.Name), }, nil } -// updateACLs creates and deletes ACLs so that the instance's ACLs are the ones in the model +// updateACLs creates and deletes ACLs so that the instance's ACLs are the ones in the model. func updateACLs(ctx context.Context, projectId, instanceId string, acls []string, client *secretsmanager.APIClient) error { // Get ACLs current state currentACLsResp, err := client.ListACLs(ctx, projectId, instanceId).Execute() @@ -418,17 +448,20 @@ func updateACLs(ctx context.Context, projectId, instanceId string, acls []string isCreated bool id string } + aclsState := make(map[string]*aclState) for _, cidr := range acls { aclsState[cidr] = &aclState{ isInModel: true, } } + for _, acl := range *currentACLsResp.Acls { cidr := *acl.Cidr if _, ok := aclsState[cidr]; !ok { aclsState[cidr] = &aclState{} } + aclsState[cidr].isCreated = true aclsState[cidr].id = *acl.Id } @@ -439,6 +472,7 @@ func updateACLs(ctx context.Context, projectId, instanceId string, acls []string payload := secretsmanager.CreateACLPayload{ Cidr: sdkUtils.Ptr(cidr), } + _, err := client.CreateACL(ctx, projectId, instanceId).CreateACLPayload(payload).Execute() if err != nil { return fmt.Errorf("creating ACL '%v': %w", cidr, err) diff --git a/stackit/internal/services/secretsmanager/instance/resource_test.go b/stackit/internal/services/secretsmanager/instance/resource_test.go index 39d2df834..c8e3c4c17 100644 --- a/stackit/internal/services/secretsmanager/instance/resource_test.go +++ b/stackit/internal/services/secretsmanager/instance/resource_test.go @@ -101,13 +101,16 @@ func TestMapFields(t *testing.T) { ProjectId: tt.expected.ProjectId, InstanceId: tt.expected.InstanceId, } + err := mapFields(tt.input, tt.ListACLsResponse, state) if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(state, &tt.expected) if diff != "" { @@ -164,9 +167,11 @@ func TestToCreatePayload(t *testing.T) { if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(output, tt.expected) if diff != "" { @@ -199,6 +204,7 @@ func TestUpdateACLs(t *testing.T) { }, }, } + getAllACLsRespBytes, err := json.Marshal(getAllACLsResp) if err != nil { t.Fatalf("Failed to marshal get all ACLs response: %v", err) @@ -347,12 +353,15 @@ func TestUpdateACLs(t *testing.T) { // Handler for getting all ACLs getAllACLsHandler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "application/json") + if tt.getAllACLsFails { w.WriteHeader(http.StatusInternalServerError) + _, err := w.Write(failureRespBytes) if err != nil { t.Errorf("Get all ACLs handler: failed to write bad response: %v", err) } + return } @@ -365,16 +374,20 @@ func TestUpdateACLs(t *testing.T) { // Handler for creating ACL createACLHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { decoder := json.NewDecoder(r.Body) + var payload secretsmanager.CreateACLPayload + err := decoder.Decode(&payload) if err != nil { t.Errorf("Create ACL handler: failed to parse payload") return } + if payload.Cidr == nil { t.Errorf("Create ACL handler: nil CIDR") return } + cidr := *payload.Cidr if cidrExists, cidrWasCreated := aclsStates[cidr]; cidrWasCreated && cidrExists { t.Errorf("Create ACL handler: attempted to create CIDR '%v' that already exists", *payload.Cidr) @@ -382,12 +395,15 @@ func TestUpdateACLs(t *testing.T) { } w.Header().Set("Content-Type", "application/json") + if tt.createACLFails { w.WriteHeader(http.StatusInternalServerError) + _, err := w.Write(failureRespBytes) if err != nil { t.Errorf("Create ACL handler: failed to write bad response: %v", err) } + return } @@ -395,49 +411,60 @@ func TestUpdateACLs(t *testing.T) { Cidr: utils.Ptr(cidr), Id: utils.Ptr(fmt.Sprintf("id-%s", cidr)), } + respBytes, err := json.Marshal(resp) if err != nil { t.Errorf("Create ACL handler: failed to marshal response: %v", err) return } + _, err = w.Write(respBytes) if err != nil { t.Errorf("Create ACL handler: failed to write response: %v", err) } + aclsStates[cidr] = true }) // Handler for deleting ACL deleteACLHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) + aclId, ok := vars["aclId"] if !ok { t.Errorf("Delete ACL handler: no ACL ID") return } + cidr, ok := strings.CutPrefix(aclId, "id-") if !ok { t.Errorf("Delete ACL handler: got unexpected ACL ID '%v'", aclId) return } + cidr, _ = strings.CutSuffix(cidr, "-repeated") cidrExists, cidrWasCreated := aclsStates[cidr] + if !cidrWasCreated { t.Errorf("Delete ACL handler: attempted to delete CIDR '%v' that wasn't created", cidr) return } + if cidrWasCreated && !cidrExists { t.Errorf("Delete ACL handler: attempted to delete CIDR '%v' that was already deleted", cidr) return } w.Header().Set("Content-Type", "application/json") + if tt.deleteACLFails { w.WriteHeader(http.StatusInternalServerError) + _, err := w.Write(failureRespBytes) if err != nil { t.Errorf("Delete ACL handler: failed to write bad response: %v", err) } + return } @@ -445,21 +472,25 @@ func TestUpdateACLs(t *testing.T) { if err != nil { t.Errorf("Delete ACL handler: failed to write response: %v", err) } + aclsStates[cidr] = false }) // Setup server and client router := mux.NewRouter() router.HandleFunc("/v1/projects/{projectId}/instances/{instanceId}/acls", func(w http.ResponseWriter, r *http.Request) { - if r.Method == "GET" { + switch r.Method { + case "GET": getAllACLsHandler(w, r) - } else if r.Method == "POST" { + case "POST": createACLHandler(w, r) } }) router.HandleFunc("/v1/projects/{projectId}/instances/{instanceId}/acls/{aclId}", deleteACLHandler) + mockedServer := httptest.NewServer(router) defer mockedServer.Close() + client, err := secretsmanager.NewAPIClient( config.WithEndpoint(mockedServer.URL), config.WithoutAuthentication(), @@ -473,9 +504,11 @@ func TestUpdateACLs(t *testing.T) { if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(aclsStates, tt.expectedACLsStates) if diff != "" { diff --git a/stackit/internal/services/secretsmanager/secretsmanager_acc_test.go b/stackit/internal/services/secretsmanager/secretsmanager_acc_test.go index 16e6c1101..9b8231e3c 100644 --- a/stackit/internal/services/secretsmanager/secretsmanager_acc_test.go +++ b/stackit/internal/services/secretsmanager/secretsmanager_acc_test.go @@ -47,12 +47,14 @@ var testConfigVarsMax = config.Variables{ func configVarsInvalid(vars config.Variables) config.Variables { tempConfig := maps.Clone(vars) delete(tempConfig, "instance_name") + return tempConfig } func configVarsMinUpdated() config.Variables { tempConfig := maps.Clone(testConfigVarsMin) tempConfig["write_enabled"] = config.BoolVariable(false) + return tempConfig } @@ -60,6 +62,7 @@ func configVarsMaxUpdated() config.Variables { tempConfig := maps.Clone(testConfigVarsMax) tempConfig["write_enabled"] = config.BoolVariable(false) tempConfig["acl2"] = config.StringVariable("10.100.2.0/24") + return tempConfig } @@ -149,6 +152,7 @@ func TestAccSecretsManagerMin(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute instance_id") } + return fmt.Sprintf("%s,%s", testutil.ProjectId, instanceId), nil }, ImportState: true, @@ -302,6 +306,7 @@ func TestAccSecretsManagerMax(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute instance_id") } + return fmt.Sprintf("%s,%s", testutil.ProjectId, instanceId), nil }, ImportState: true, @@ -368,7 +373,9 @@ func TestAccSecretsManagerMax(t *testing.T) { func testAccCheckSecretsManagerDestroy(s *terraform.State) error { ctx := context.Background() + var client *secretsmanager.APIClient + var err error if testutil.SecretsManagerCustomEndpoint == "" { client, err = secretsmanager.NewAPIClient( @@ -379,11 +386,13 @@ func testAccCheckSecretsManagerDestroy(s *terraform.State) error { core_config.WithEndpoint(testutil.SecretsManagerCustomEndpoint), ) } + if err != nil { return fmt.Errorf("creating client: %w", err) } instancesToDestroy := []string{} + for _, rs := range s.RootModule().Resources { if rs.Type != "stackit_secretsmanager_instance" { continue @@ -403,6 +412,7 @@ func testAccCheckSecretsManagerDestroy(s *terraform.State) error { if instances[i].Id == nil { continue } + if utils.Contains(instancesToDestroy, *instances[i].Id) { err := client.DeleteInstanceExecute(ctx, testutil.ProjectId, *instances[i].Id) if err != nil { @@ -410,5 +420,6 @@ func testAccCheckSecretsManagerDestroy(s *terraform.State) error { } } } + return nil } diff --git a/stackit/internal/services/secretsmanager/user/datasource.go b/stackit/internal/services/secretsmanager/user/datasource.go index 9fa1e5000..0f79364e6 100644 --- a/stackit/internal/services/secretsmanager/user/datasource.go +++ b/stackit/internal/services/secretsmanager/user/datasource.go @@ -5,19 +5,17 @@ import ( "fmt" "net/http" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - secretsmanagerUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/secretsmanager/utils" - "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/stackitcloud/stackit-sdk-go/services/secretsmanager" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + secretsmanagerUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/secretsmanager/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - - "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/services/secretsmanager" ) // Ensure the implementation satisfies the expected interfaces. @@ -58,10 +56,13 @@ func (r *userDataSource) Configure(ctx context.Context, req datasource.Configure } apiClient := secretsmanagerUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + r.client = apiClient + tflog.Info(ctx, "Secrets Manager user client configured") } @@ -126,13 +127,15 @@ func (r *userDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, r } // Read refreshes the Terraform state with the latest data. -func (r *userDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (r *userDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model DataSourceModel diags := req.Config.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() instanceId := model.InstanceId.ValueString() userId := model.UserId.ValueString() @@ -153,6 +156,7 @@ func (r *userDataSource) Read(ctx context.Context, req datasource.ReadRequest, r }, ) resp.State.RemoveResource(ctx) + return } @@ -166,9 +170,11 @@ func (r *userDataSource) Read(ctx context.Context, req datasource.ReadRequest, r // Set refreshed state diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "Secrets Manager user read") } @@ -176,6 +182,7 @@ func mapDataSourceFields(user *secretsmanager.User, model *DataSourceModel) erro if user == nil { return fmt.Errorf("response input is nil") } + if model == nil { return fmt.Errorf("model input is nil") } @@ -194,5 +201,6 @@ func mapDataSourceFields(user *secretsmanager.User, model *DataSourceModel) erro model.Description = types.StringPointerValue(user.Description) model.WriteEnabled = types.BoolPointerValue(user.Write) model.Username = types.StringPointerValue(user.Username) + return nil } diff --git a/stackit/internal/services/secretsmanager/user/datasource_test.go b/stackit/internal/services/secretsmanager/user/datasource_test.go index 7ecfaba4b..aef836782 100644 --- a/stackit/internal/services/secretsmanager/user/datasource_test.go +++ b/stackit/internal/services/secretsmanager/user/datasource_test.go @@ -70,13 +70,16 @@ func TestMapDataSourceFields(t *testing.T) { ProjectId: tt.expected.ProjectId, InstanceId: tt.expected.InstanceId, } + err := mapDataSourceFields(tt.input, state) if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(state, &tt.expected) if diff != "" { diff --git a/stackit/internal/services/secretsmanager/user/resource.go b/stackit/internal/services/secretsmanager/user/resource.go index 12c4a6add..abbad9a73 100644 --- a/stackit/internal/services/secretsmanager/user/resource.go +++ b/stackit/internal/services/secretsmanager/user/resource.go @@ -6,24 +6,21 @@ import ( "net/http" "strings" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - - secretsmanagerUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/secretsmanager/utils" - - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/stackitcloud/stackit-sdk-go/core/oapierror" "github.com/stackitcloud/stackit-sdk-go/services/secretsmanager" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + secretsmanagerUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/secretsmanager/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" ) // Ensure the implementation satisfies the expected interfaces. @@ -67,10 +64,13 @@ func (r *userResource) Configure(ctx context.Context, req resource.ConfigureRequ } apiClient := secretsmanagerUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + r.client = apiClient + tflog.Info(ctx, "Secrets Manager user client configured") } @@ -156,13 +156,15 @@ func (r *userResource) Schema(_ context.Context, _ resource.SchemaRequest, resp } // Create creates the resource and sets the initial Terraform state. -func (r *userResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *userResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() instanceId := model.InstanceId.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) @@ -180,10 +182,12 @@ func (r *userResource) Create(ctx context.Context, req resource.CreateRequest, r core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating user", fmt.Sprintf("Calling API: %v", err)) return } + if userResp.Id == nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating user", "Got empty user id") return } + userId := *userResp.Id ctx = tflog.SetField(ctx, "user_id", userId) @@ -193,22 +197,27 @@ func (r *userResource) Create(ctx context.Context, req resource.CreateRequest, r core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating user", fmt.Sprintf("Processing API payload: %v", err)) return } + diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "Secrets Manager user created") } // Read refreshes the Terraform state with the latest data. -func (r *userResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (r *userResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() instanceId := model.InstanceId.ValueString() userId := model.UserId.ValueString() @@ -223,7 +232,9 @@ func (r *userResource) Read(ctx context.Context, req resource.ReadRequest, resp resp.State.RemoveResource(ctx) return } + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading user", fmt.Sprintf("Calling API: %v", err)) + return } @@ -237,21 +248,25 @@ func (r *userResource) Read(ctx context.Context, req resource.ReadRequest, resp // Set refreshed state diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "Secrets Manager user read") } // Update updates the resource and sets the updated Terraform state on success. -func (r *userResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *userResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { //nolint:gocritic // function signature required by Terraform // Retrieve values from plan var model Model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() instanceId := model.InstanceId.ValueString() userId := model.UserId.ValueString() @@ -271,6 +286,7 @@ func (r *userResource) Update(ctx context.Context, req resource.UpdateRequest, r core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating user", err.Error()) return } + user, err := r.client.GetUser(ctx, projectId, instanceId, userId).Execute() if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating user", fmt.Sprintf("Calling API to get user's current state: %v", err)) @@ -280,6 +296,7 @@ func (r *userResource) Update(ctx context.Context, req resource.UpdateRequest, r // Get existing state diags = req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -289,19 +306,23 @@ func (r *userResource) Update(ctx context.Context, req resource.UpdateRequest, r core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating user", fmt.Sprintf("Processing API payload: %v", err)) return } + diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "Secrets Manager user updated") } // Delete deletes the resource and removes the Terraform state on success. -func (r *userResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform +func (r *userResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -323,7 +344,7 @@ func (r *userResource) Delete(ctx context.Context, req resource.DeleteRequest, r } // ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: project_id,instance_id,user_id +// The expected format of the resource import identifier is: project_id,instance_id,user_id. func (r *userResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { idParts := strings.Split(req.ID, core.Separator) if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" { @@ -331,6 +352,7 @@ func (r *userResource) ImportState(ctx context.Context, req resource.ImportState "Error importing credential", fmt.Sprintf("Expected import identifier with format [project_id],[instance_id],[user_id], got %q", req.ID), ) + return } @@ -348,6 +370,7 @@ func toCreatePayload(model *Model) (*secretsmanager.CreateUserPayload, error) { if model == nil { return nil, fmt.Errorf("nil model") } + return &secretsmanager.CreateUserPayload{ Description: conversion.StringValueToPointer(model.Description), Write: conversion.BoolValueToPointer(model.WriteEnabled), @@ -358,6 +381,7 @@ func toUpdatePayload(model *Model) (*secretsmanager.UpdateUserPayload, error) { if model == nil { return nil, fmt.Errorf("nil model") } + return &secretsmanager.UpdateUserPayload{ Write: conversion.BoolValueToPointer(model.WriteEnabled), }, nil @@ -367,6 +391,7 @@ func mapFields(user *secretsmanager.User, model *Model) error { if user == nil { return fmt.Errorf("response input is nil") } + if model == nil { return fmt.Errorf("model input is nil") } @@ -389,5 +414,6 @@ func mapFields(user *secretsmanager.User, model *Model) error { if user.Password != nil && *user.Password != "" { model.Password = types.StringPointerValue(user.Password) } + return nil } diff --git a/stackit/internal/services/secretsmanager/user/resource_test.go b/stackit/internal/services/secretsmanager/user/resource_test.go index 2555685c4..059c4e319 100644 --- a/stackit/internal/services/secretsmanager/user/resource_test.go +++ b/stackit/internal/services/secretsmanager/user/resource_test.go @@ -124,13 +124,16 @@ func TestMapFields(t *testing.T) { if tt.modelPassword != nil { state.Password = types.StringPointerValue(tt.modelPassword) } + err := mapFields(tt.input, state) if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(state, &tt.expected) if diff != "" { @@ -206,9 +209,11 @@ func TestToCreatePayload(t *testing.T) { if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(output, tt.expected) if diff != "" { @@ -257,9 +262,11 @@ func TestToUpdatePayload(t *testing.T) { if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(output, tt.expected) if diff != "" { diff --git a/stackit/internal/services/secretsmanager/utils/util.go b/stackit/internal/services/secretsmanager/utils/util.go index 8829b844c..195d6b4a9 100644 --- a/stackit/internal/services/secretsmanager/utils/util.go +++ b/stackit/internal/services/secretsmanager/utils/util.go @@ -21,6 +21,7 @@ func ConfigureClient(ctx context.Context, providerData *core.ProviderData, diags } else { apiClientConfigOptions = append(apiClientConfigOptions, config.WithRegion(providerData.GetRegion())) } + apiClient, err := secretsmanager.NewAPIClient(apiClientConfigOptions...) if err != nil { core.LogAndAddError(ctx, diags, "Error configuring API client", fmt.Sprintf("Configuring client: %v. This is an error related to the provider configuration, not to the resource configuration", err)) diff --git a/stackit/internal/services/secretsmanager/utils/util_test.go b/stackit/internal/services/secretsmanager/utils/util_test.go index f2562a2de..6d673c969 100644 --- a/stackit/internal/services/secretsmanager/utils/util_test.go +++ b/stackit/internal/services/secretsmanager/utils/util_test.go @@ -22,6 +22,7 @@ const ( func TestConfigureClient(t *testing.T) { /* mock authentication by setting service account token env variable */ os.Clearenv() + err := os.Setenv(sdkClients.ServiceAccountToken, "mock-val") if err != nil { t.Errorf("error setting env variable: %v", err) @@ -30,6 +31,7 @@ func TestConfigureClient(t *testing.T) { type args struct { providerData *core.ProviderData } + tests := []struct { name string args args @@ -51,6 +53,7 @@ func TestConfigureClient(t *testing.T) { if err != nil { t.Errorf("error configuring client: %v", err) } + return apiClient }(), wantErr: false, @@ -71,6 +74,7 @@ func TestConfigureClient(t *testing.T) { if err != nil { t.Errorf("error configuring client: %v", err) } + return apiClient }(), wantErr: false, @@ -82,6 +86,7 @@ func TestConfigureClient(t *testing.T) { diags := diag.Diagnostics{} actual := ConfigureClient(ctx, tt.args.providerData, &diags) + if diags.HasError() != tt.wantErr { t.Errorf("ConfigureClient() error = %v, want %v", diags.HasError(), tt.wantErr) } diff --git a/stackit/internal/services/serverbackup/schedule/resource.go b/stackit/internal/services/serverbackup/schedule/resource.go index 5ba73f481..8a1a53a7f 100644 --- a/stackit/internal/services/serverbackup/schedule/resource.go +++ b/stackit/internal/services/serverbackup/schedule/resource.go @@ -7,12 +7,10 @@ import ( "strconv" "strings" - "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" - "github.com/hashicorp/terraform-plugin-framework/diag" - serverbackupUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/serverbackup/utils" - "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" + "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "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/resource/schema" @@ -22,15 +20,14 @@ import ( "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" - + "github.com/stackitcloud/stackit-sdk-go/core/oapierror" + "github.com/stackitcloud/stackit-sdk-go/services/serverbackup" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/features" + serverbackupUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/serverbackup/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - - "github.com/stackitcloud/stackit-sdk-go/core/oapierror" - "github.com/stackitcloud/stackit-sdk-go/services/serverbackup" ) // Ensure the implementation satisfies the expected interfaces. @@ -53,7 +50,7 @@ type Model struct { Region types.String `tfsdk:"region"` } -// scheduleBackupPropertiesModel maps schedule backup_properties data +// scheduleBackupPropertiesModel maps schedule backup_properties data. type scheduleBackupPropertiesModel struct { BackupName types.String `tfsdk:"name"` RetentionPeriod types.Int64 `tfsdk:"retention_period"` @@ -73,29 +70,35 @@ type scheduleResource struct { // ModifyPlan implements resource.ResourceWithModifyPlan. // Use the modifier to set the effective region in the current plan. -func (r *scheduleResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform +func (r *scheduleResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { //nolint:gocritic // function signature required by Terraform var configModel Model // skip initial empty configuration to avoid follow-up errors if req.Config.Raw.IsNull() { return } + resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...) + if resp.Diagnostics.HasError() { return } var planModel Model + resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) + if resp.Diagnostics.HasError() { return } utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp) + if resp.Diagnostics.HasError() { return } resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...) + if resp.Diagnostics.HasError() { return } @@ -109,21 +112,26 @@ func (r *scheduleResource) Metadata(_ context.Context, req resource.MetadataRequ // Configure adds the provider configured client to the resource. func (r *scheduleResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { var ok bool + r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) if !ok { return } features.CheckBetaResourcesEnabled(ctx, &r.providerData, &resp.Diagnostics, "stackit_server_backup_schedule", "resource") + if resp.Diagnostics.HasError() { return } apiClient := serverbackupUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + r.client = apiClient + tflog.Info(ctx, "Server backup client configured.") } @@ -237,13 +245,15 @@ func (r *scheduleResource) Schema(_ context.Context, _ resource.SchemaRequest, r } // Create creates the resource and sets the initial Terraform state. -func (r *scheduleResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *scheduleResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() serverId := model.ServerId.ValueString() region := r.providerData.GetRegionWithOverride(model.Region) @@ -265,11 +275,13 @@ func (r *scheduleResource) Create(ctx context.Context, req resource.CreateReques core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating server backup schedule", fmt.Sprintf("Creating API payload: %v", err)) return } + scheduleResp, err := r.client.CreateBackupSchedule(ctx, projectId, serverId, region).CreateBackupSchedulePayload(*payload).Execute() if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating server backup schedule", fmt.Sprintf("Calling API: %v", err)) return } + ctx = tflog.SetField(ctx, "backup_schedule_id", *scheduleResp.Id) // Map response body to schema @@ -278,22 +290,27 @@ func (r *scheduleResource) Create(ctx context.Context, req resource.CreateReques core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating server backup schedule", fmt.Sprintf("Processing API payload: %v", err)) return } + diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "Server backup schedule created.") } // Read refreshes the Terraform state with the latest data. -func (r *scheduleResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (r *scheduleResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() serverId := model.ServerId.ValueString() backupScheduleId := model.BackupScheduleId.ValueInt64() @@ -311,7 +328,9 @@ func (r *scheduleResource) Read(ctx context.Context, req resource.ReadRequest, r resp.State.RemoveResource(ctx) return } + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading backup schedule", fmt.Sprintf("Calling API: %v", err)) + return } @@ -325,20 +344,24 @@ func (r *scheduleResource) Read(ctx context.Context, req resource.ReadRequest, r // Set refreshed state diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "Server backup schedule read.") } // Update updates the resource and sets the updated Terraform state on success. -func (r *scheduleResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *scheduleResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() serverId := model.ServerId.ValueString() backupScheduleId := model.BackupScheduleId.ValueInt64() @@ -368,22 +391,27 @@ func (r *scheduleResource) Update(ctx context.Context, req resource.UpdateReques core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating server backup schedule", fmt.Sprintf("Processing API payload: %v", err)) return } + diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "Server backup schedule updated.") } // Delete deletes the resource and removes the Terraform state on success. -func (r *scheduleResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform +func (r *scheduleResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() serverId := model.ServerId.ValueString() backupScheduleId := model.BackupScheduleId.ValueInt64() @@ -399,6 +427,7 @@ func (r *scheduleResource) Delete(ctx context.Context, req resource.DeleteReques core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting server backup schedule", fmt.Sprintf("Calling API: %v", err)) return } + tflog.Info(ctx, "Server backup schedule deleted.") // Disable backups service in case there are no backups and no backup schedules. @@ -410,7 +439,7 @@ func (r *scheduleResource) Delete(ctx context.Context, req resource.DeleteReques } // ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: // project_id,server_id,schedule_id +// The expected format of the resource import identifier is: // project_id,server_id,schedule_id. func (r *scheduleResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { idParts := strings.Split(req.ID, core.Separator) if len(idParts) != 4 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" || idParts[3] == "" { @@ -418,6 +447,7 @@ func (r *scheduleResource) ImportState(ctx context.Context, req resource.ImportS "Error importing server backup schedule", fmt.Sprintf("Expected import identifier with format [project_id],[region],[server_id],[backup_schedule_id], got %q", req.ID), ) + return } @@ -427,6 +457,7 @@ func (r *scheduleResource) ImportState(ctx context.Context, req resource.ImportS "Error importing server backup schedule", fmt.Sprintf("Expected backup_schedule_id to be int64, got %q", idParts[2]), ) + return } @@ -441,9 +472,11 @@ func mapFields(ctx context.Context, schedule *serverbackup.BackupSchedule, model if schedule == nil { return fmt.Errorf("response input is nil") } + if model == nil { return fmt.Errorf("model input is nil") } + if schedule.Id == nil { return fmt.Errorf("response id is nil") } @@ -456,16 +489,20 @@ func mapFields(ctx context.Context, schedule *serverbackup.BackupSchedule, model model.Name = types.StringPointerValue(schedule.Name) model.Rrule = types.StringPointerValue(schedule.Rrule) model.Enabled = types.BoolPointerValue(schedule.Enabled) + if schedule.BackupProperties == nil { model.BackupProperties = nil return nil } volIds := types.ListNull(types.StringType) + if schedule.BackupProperties.VolumeIds != nil { var modelVolIds []string + if model.BackupProperties != nil { var err error + modelVolIds, err = utils.ListValuetoStringSlice(model.BackupProperties.VolumeIds) if err != nil { return err @@ -476,21 +513,24 @@ func mapFields(ctx context.Context, schedule *serverbackup.BackupSchedule, model reconciledVolIds := utils.ReconcileStringSlices(modelVolIds, respVolIds) var diags diag.Diagnostics + volIds, diags = types.ListValueFrom(ctx, types.StringType, reconciledVolIds) if diags.HasError() { return fmt.Errorf("failed to map volumeIds: %w", core.DiagsToError(diags)) } } + model.BackupProperties = &scheduleBackupPropertiesModel{ BackupName: types.StringValue(*schedule.BackupProperties.Name), RetentionPeriod: types.Int64Value(*schedule.BackupProperties.RetentionPeriod), VolumeIds: volIds, } model.Region = types.StringValue(region) + return nil } -// If already enabled, just continues +// If already enabled, just continues. func (r *scheduleResource) enableBackupsService(ctx context.Context, model *Model) error { projectId := model.ProjectId.ValueString() serverId := model.ServerId.ValueString() @@ -505,13 +545,16 @@ func (r *scheduleResource) enableBackupsService(ctx context.Context, model *Mode tflog.Debug(ctx, "Service for server backup already enabled") return nil } + return fmt.Errorf("enable server backup service: %w", err) } + tflog.Info(ctx, "Enabled server backup service") + return nil } -// Disables only if no backup schedules are present and no backups are present +// Disables only if no backup schedules are present and no backups are present. func (r *scheduleResource) disableBackupsService(ctx context.Context, model *Model) error { tflog.Debug(ctx, "Disabling server backup service (in case there are no backups and no backup schedules)") @@ -520,10 +563,12 @@ func (r *scheduleResource) disableBackupsService(ctx context.Context, model *Mod region := r.providerData.GetRegionWithOverride(model.Region) tflog.Debug(ctx, "Checking for existing backups") + backups, err := r.client.ListBackups(ctx, projectId, serverId, region).Execute() if err != nil { return fmt.Errorf("list backups: %w", err) } + if *backups.Items != nil && len(*backups.Items) > 0 { tflog.Debug(ctx, "Backups found - will not disable server backup service") return nil @@ -533,7 +578,9 @@ func (r *scheduleResource) disableBackupsService(ctx context.Context, model *Mod if err != nil { return fmt.Errorf("disable server backup service: %w", err) } + tflog.Info(ctx, "Disabled server backup service") + return nil } @@ -543,10 +590,13 @@ func toCreatePayload(model *Model) (*serverbackup.CreateBackupSchedulePayload, e } backupProperties := serverbackup.BackupProperties{} + if model.BackupProperties != nil { ids := []string{} + var err error - if !(model.BackupProperties.VolumeIds.IsNull() || model.BackupProperties.VolumeIds.IsUnknown()) { + + if !model.BackupProperties.VolumeIds.IsNull() && !model.BackupProperties.VolumeIds.IsUnknown() { ids, err = utils.ListValuetoStringSlice(model.BackupProperties.VolumeIds) if err != nil { return nil, fmt.Errorf("convert volume id: %w", err) @@ -556,12 +606,14 @@ func toCreatePayload(model *Model) (*serverbackup.CreateBackupSchedulePayload, e if len(ids) == 0 { ids = nil } + backupProperties = serverbackup.BackupProperties{ Name: conversion.StringValueToPointer(model.BackupProperties.BackupName), RetentionPeriod: conversion.Int64ValueToPointer(model.BackupProperties.RetentionPeriod), VolumeIds: &ids, } } + return &serverbackup.CreateBackupSchedulePayload{ Enabled: conversion.BoolValueToPointer(model.Enabled), Name: conversion.StringValueToPointer(model.Name), @@ -576,10 +628,13 @@ func toUpdatePayload(model *Model) (*serverbackup.UpdateBackupSchedulePayload, e } backupProperties := serverbackup.BackupProperties{} + if model.BackupProperties != nil { ids := []string{} + var err error - if !(model.BackupProperties.VolumeIds.IsNull() || model.BackupProperties.VolumeIds.IsUnknown()) { + + if !model.BackupProperties.VolumeIds.IsNull() && !model.BackupProperties.VolumeIds.IsUnknown() { ids, err = utils.ListValuetoStringSlice(model.BackupProperties.VolumeIds) if err != nil { return nil, fmt.Errorf("convert volume id: %w", err) @@ -589,6 +644,7 @@ func toUpdatePayload(model *Model) (*serverbackup.UpdateBackupSchedulePayload, e if len(ids) == 0 { ids = nil } + backupProperties = serverbackup.BackupProperties{ Name: conversion.StringValueToPointer(model.BackupProperties.BackupName), RetentionPeriod: conversion.Int64ValueToPointer(model.BackupProperties.RetentionPeriod), diff --git a/stackit/internal/services/serverbackup/schedule/resource_test.go b/stackit/internal/services/serverbackup/schedule/resource_test.go index c3ecd45fd..aad7e120e 100644 --- a/stackit/internal/services/serverbackup/schedule/resource_test.go +++ b/stackit/internal/services/serverbackup/schedule/resource_test.go @@ -80,13 +80,16 @@ func TestMapFields(t *testing.T) { ServerId: tt.expected.ServerId, } ctx := context.TODO() + err := mapFields(ctx, tt.input, state, "eu01") if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(state, &tt.expected) if diff != "" { @@ -154,9 +157,11 @@ func TestToCreatePayload(t *testing.T) { if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(output, tt.expected) if diff != "" { @@ -224,9 +229,11 @@ func TestToUpdatePayload(t *testing.T) { if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(output, tt.expected) if diff != "" { diff --git a/stackit/internal/services/serverbackup/schedule/schedule_datasource.go b/stackit/internal/services/serverbackup/schedule/schedule_datasource.go index 2d3f82cf8..034e796b0 100644 --- a/stackit/internal/services/serverbackup/schedule/schedule_datasource.go +++ b/stackit/internal/services/serverbackup/schedule/schedule_datasource.go @@ -6,21 +6,18 @@ import ( "net/http" "strconv" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - serverbackupUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/serverbackup/utils" - "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" - + "github.com/stackitcloud/stackit-sdk-go/services/serverbackup" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/features" + serverbackupUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/serverbackup/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - - "github.com/stackitcloud/stackit-sdk-go/services/serverbackup" ) // scheduleDataSourceBetaCheckDone is used to prevent multiple checks for beta resources. @@ -52,6 +49,7 @@ func (r *scheduleDataSource) Metadata(_ context.Context, req datasource.Metadata // Configure adds the provider configured client to the data source. func (r *scheduleDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { var ok bool + r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) if !ok { return @@ -59,17 +57,22 @@ func (r *scheduleDataSource) Configure(ctx context.Context, req datasource.Confi if !scheduleDataSourceBetaCheckDone { features.CheckBetaResourcesEnabled(ctx, &r.providerData, &resp.Diagnostics, "stackit_server_backup_schedule", "data source") + if resp.Diagnostics.HasError() { return } + scheduleDataSourceBetaCheckDone = true } apiClient := serverbackupUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + r.client = apiClient + tflog.Info(ctx, "Server backup client configured") } @@ -141,13 +144,15 @@ func (r *scheduleDataSource) Schema(_ context.Context, _ datasource.SchemaReques } // Read refreshes the Terraform state with the latest data. -func (r *scheduleDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (r *scheduleDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.Config.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() serverId := model.ServerId.ValueString() backupScheduleId := model.BackupScheduleId.ValueInt64() @@ -171,6 +176,7 @@ func (r *scheduleDataSource) Read(ctx context.Context, req datasource.ReadReques }, ) resp.State.RemoveResource(ctx) + return } @@ -184,8 +190,10 @@ func (r *scheduleDataSource) Read(ctx context.Context, req datasource.ReadReques // Set refreshed state diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "Server backup schedule read") } diff --git a/stackit/internal/services/serverbackup/schedule/schedules_datasource.go b/stackit/internal/services/serverbackup/schedule/schedules_datasource.go index 81cd5ade3..e79c12e55 100644 --- a/stackit/internal/services/serverbackup/schedule/schedules_datasource.go +++ b/stackit/internal/services/serverbackup/schedule/schedules_datasource.go @@ -5,20 +5,18 @@ import ( "fmt" "net/http" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - serverbackupUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/serverbackup/utils" - "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/stackitcloud/stackit-sdk-go/services/serverbackup" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/features" + serverbackupUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/serverbackup/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - - "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/stackitcloud/stackit-sdk-go/services/serverbackup" ) // scheduleDataSourceBetaCheckDone is used to prevent multiple checks for beta resources. @@ -50,6 +48,7 @@ func (r *schedulesDataSource) Metadata(_ context.Context, req datasource.Metadat // Configure adds the provider configured client to the data source. func (r *schedulesDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { var ok bool + r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) if !ok { return @@ -57,18 +56,22 @@ func (r *schedulesDataSource) Configure(ctx context.Context, req datasource.Conf if !schedulesDataSourceBetaCheckDone { features.CheckBetaResourcesEnabled(ctx, &r.providerData, &resp.Diagnostics, "stackit_server_backup_schedules", "data source") + if resp.Diagnostics.HasError() { return } + schedulesDataSourceBetaCheckDone = true } apiClient := serverbackupUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } r.client = apiClient + tflog.Info(ctx, "Server backup client configured") } @@ -164,13 +167,15 @@ type schedulesDatasourceItemModel struct { } // Read refreshes the Terraform state with the latest data. -func (r *schedulesDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (r *schedulesDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model schedulesDataSourceModel diags := req.Config.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() serverId := model.ServerId.ValueString() region := r.providerData.GetRegionWithOverride(model.Region) @@ -191,6 +196,7 @@ func (r *schedulesDataSource) Read(ctx context.Context, req datasource.ReadReque }, ) resp.State.RemoveResource(ctx) + return } @@ -204,9 +210,11 @@ func (r *schedulesDataSource) Read(ctx context.Context, req datasource.ReadReque // Set refreshed state diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "Server backup schedules read") } @@ -214,11 +222,13 @@ func mapSchedulesDatasourceFields(ctx context.Context, schedules *serverbackup.G if schedules == nil { return fmt.Errorf("response input is nil") } + if model == nil { return fmt.Errorf("model input is nil") } tflog.Debug(ctx, "response", map[string]any{"schedules": schedules}) + projectId := model.ProjectId.ValueString() serverId := model.ServerId.ValueString() @@ -232,10 +242,12 @@ func mapSchedulesDatasourceFields(ctx context.Context, schedules *serverbackup.G Rrule: types.StringValue(*schedule.Rrule), Enabled: types.BoolValue(*schedule.Enabled), } + ids, diags := types.ListValueFrom(ctx, types.StringType, schedule.BackupProperties.VolumeIds) if diags.HasError() { return fmt.Errorf("failed to map hosts: %w", core.DiagsToError(diags)) } + scheduleState.BackupProperties = &scheduleBackupPropertiesModel{ BackupName: types.StringValue(*schedule.BackupProperties.Name), RetentionPeriod: types.Int64Value(*schedule.BackupProperties.RetentionPeriod), @@ -243,5 +255,6 @@ func mapSchedulesDatasourceFields(ctx context.Context, schedules *serverbackup.G } model.Items = append(model.Items, scheduleState) } + return nil } diff --git a/stackit/internal/services/serverbackup/schedule/schedules_datasource_test.go b/stackit/internal/services/serverbackup/schedule/schedules_datasource_test.go index 70bb3d26b..2f8a471f6 100644 --- a/stackit/internal/services/serverbackup/schedule/schedules_datasource_test.go +++ b/stackit/internal/services/serverbackup/schedule/schedules_datasource_test.go @@ -89,13 +89,16 @@ func TestMapSchedulesDataSourceFields(t *testing.T) { ServerId: tt.expected.ServerId, } ctx := context.TODO() + err := mapSchedulesDatasourceFields(ctx, tt.input, state, "eu01") if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(state, &tt.expected) if diff != "" { diff --git a/stackit/internal/services/serverbackup/serverbackup_acc_test.go b/stackit/internal/services/serverbackup/serverbackup_acc_test.go index 9793291b3..e0664a877 100644 --- a/stackit/internal/services/serverbackup/serverbackup_acc_test.go +++ b/stackit/internal/services/serverbackup/serverbackup_acc_test.go @@ -53,6 +53,7 @@ var testConfigVarsMax = config.Variables{ func configVarsInvalid(vars config.Variables) config.Variables { tempConfig := maps.Clone(vars) tempConfig["retention_period"] = config.IntegerVariable(0) + return tempConfig } @@ -68,6 +69,7 @@ func configVarsMaxUpdated() config.Variables { tempConfig := maps.Clone(testConfigVarsMax) tempConfig["retention_period"] = config.IntegerVariable(12) tempConfig["rrule"] = config.StringVariable("DTSTART;TZID=Europe/Berlin:20250430T010000 RRULE:FREQ=DAILY;INTERVAL=3") + return tempConfig } @@ -76,6 +78,7 @@ func TestAccServerBackupScheduleMinResource(t *testing.T) { fmt.Println("TF_ACC_SERVER_ID not set, skipping test") return } + resource.Test(t, resource.TestCase{ ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, CheckDestroy: testAccCheckServerBackupScheduleDestroy, @@ -136,6 +139,7 @@ func TestAccServerBackupScheduleMinResource(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute backup_schedule_id") } + return fmt.Sprintf("%s,%s,%s,%s", testutil.ProjectId, testutil.Region, testutil.ServerId, scheduleId), nil }, ImportState: true, @@ -168,6 +172,7 @@ func TestAccServerBackupScheduleMaxResource(t *testing.T) { fmt.Println("TF_ACC_SERVER_ID not set, skipping test") return } + resource.Test(t, resource.TestCase{ ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, CheckDestroy: testAccCheckServerBackupScheduleDestroy, @@ -228,6 +233,7 @@ func TestAccServerBackupScheduleMaxResource(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute backup_schedule_id") } + return fmt.Sprintf("%s,%s,%s,%s", testutil.ProjectId, testutil.Region, testutil.ServerId, scheduleId), nil }, ImportState: true, @@ -257,7 +263,9 @@ func TestAccServerBackupScheduleMaxResource(t *testing.T) { func testAccCheckServerBackupScheduleDestroy(s *terraform.State) error { ctx := context.Background() + var client *serverbackup.APIClient + var err error if testutil.ServerBackupCustomEndpoint == "" { client, err = serverbackup.NewAPIClient() @@ -266,11 +274,13 @@ func testAccCheckServerBackupScheduleDestroy(s *terraform.State) error { core_config.WithEndpoint(testutil.ServerBackupCustomEndpoint), ) } + if err != nil { return fmt.Errorf("creating client: %w", err) } schedulesToDestroy := []string{} + for _, rs := range s.RootModule().Resources { if rs.Type != "stackit_server_backup_schedule" { continue @@ -290,6 +300,7 @@ func testAccCheckServerBackupScheduleDestroy(s *terraform.State) error { if schedules[i].Id == nil { continue } + scheduleId := strconv.FormatInt(*schedules[i].Id, 10) if utils.Contains(schedulesToDestroy, scheduleId) { err := client.DeleteBackupScheduleExecute(ctx, testutil.ProjectId, testutil.ServerId, scheduleId, testutil.Region) @@ -298,5 +309,6 @@ func testAccCheckServerBackupScheduleDestroy(s *terraform.State) error { } } } + return nil } diff --git a/stackit/internal/services/serverbackup/utils/util.go b/stackit/internal/services/serverbackup/utils/util.go index 1869bd5ee..32b6d07b4 100644 --- a/stackit/internal/services/serverbackup/utils/util.go +++ b/stackit/internal/services/serverbackup/utils/util.go @@ -4,10 +4,9 @@ import ( "context" "fmt" - "github.com/stackitcloud/stackit-sdk-go/services/serverbackup" - "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/stackitcloud/stackit-sdk-go/core/config" + "github.com/stackitcloud/stackit-sdk-go/services/serverbackup" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" ) @@ -20,6 +19,7 @@ func ConfigureClient(ctx context.Context, providerData *core.ProviderData, diags if providerData.ServerBackupCustomEndpoint != "" { apiClientConfigOptions = append(apiClientConfigOptions, config.WithEndpoint(providerData.ServerBackupCustomEndpoint)) } + apiClient, err := serverbackup.NewAPIClient(apiClientConfigOptions...) if err != nil { core.LogAndAddError(ctx, diags, "Error configuring API client", fmt.Sprintf("Configuring client: %v. This is an error related to the provider configuration, not to the resource configuration", err)) diff --git a/stackit/internal/services/serverbackup/utils/util_test.go b/stackit/internal/services/serverbackup/utils/util_test.go index e282c954d..3549139e4 100644 --- a/stackit/internal/services/serverbackup/utils/util_test.go +++ b/stackit/internal/services/serverbackup/utils/util_test.go @@ -22,6 +22,7 @@ const ( func TestConfigureClient(t *testing.T) { /* mock authentication by setting service account token env variable */ os.Clearenv() + err := os.Setenv(sdkClients.ServiceAccountToken, "mock-val") if err != nil { t.Errorf("error setting env variable: %v", err) @@ -30,6 +31,7 @@ func TestConfigureClient(t *testing.T) { type args struct { providerData *core.ProviderData } + tests := []struct { name string args args @@ -50,6 +52,7 @@ func TestConfigureClient(t *testing.T) { if err != nil { t.Errorf("error configuring client: %v", err) } + return apiClient }(), wantErr: false, @@ -70,6 +73,7 @@ func TestConfigureClient(t *testing.T) { if err != nil { t.Errorf("error configuring client: %v", err) } + return apiClient }(), wantErr: false, @@ -81,6 +85,7 @@ func TestConfigureClient(t *testing.T) { diags := diag.Diagnostics{} actual := ConfigureClient(ctx, tt.args.providerData, &diags) + if diags.HasError() != tt.wantErr { t.Errorf("ConfigureClient() error = %v, want %v", diags.HasError(), tt.wantErr) } diff --git a/stackit/internal/services/serverupdate/schedule/resource.go b/stackit/internal/services/serverupdate/schedule/resource.go index 2ef4e705b..0a265e465 100644 --- a/stackit/internal/services/serverupdate/schedule/resource.go +++ b/stackit/internal/services/serverupdate/schedule/resource.go @@ -7,8 +7,6 @@ import ( "strconv" "strings" - serverupdateUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/serverupdate/utils" - "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/path" @@ -20,15 +18,14 @@ import ( "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - + "github.com/stackitcloud/stackit-sdk-go/core/oapierror" + "github.com/stackitcloud/stackit-sdk-go/services/serverupdate" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/features" + serverupdateUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/serverupdate/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - - "github.com/stackitcloud/stackit-sdk-go/core/oapierror" - "github.com/stackitcloud/stackit-sdk-go/services/serverupdate" ) // Ensure the implementation satisfies the expected interfaces. @@ -64,29 +61,35 @@ type scheduleResource struct { // ModifyPlan implements resource.ResourceWithModifyPlan. // Use the modifier to set the effective region in the current plan. -func (r *scheduleResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform +func (r *scheduleResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { //nolint:gocritic // function signature required by Terraform var configModel Model // skip initial empty configuration to avoid follow-up errors if req.Config.Raw.IsNull() { return } + resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...) + if resp.Diagnostics.HasError() { return } var planModel Model + resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) + if resp.Diagnostics.HasError() { return } utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp) + if resp.Diagnostics.HasError() { return } resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...) + if resp.Diagnostics.HasError() { return } @@ -100,21 +103,26 @@ func (r *scheduleResource) Metadata(_ context.Context, req resource.MetadataRequ // Configure adds the provider configured client to the resource. func (r *scheduleResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { var ok bool + r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) if !ok { return } features.CheckBetaResourcesEnabled(ctx, &r.providerData, &resp.Diagnostics, "stackit_server_update_schedule", "resource") + if resp.Diagnostics.HasError() { return } apiClient := serverupdateUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + r.client = apiClient + tflog.Info(ctx, "Server update client configured.") } @@ -214,13 +222,15 @@ func (r *scheduleResource) Schema(_ context.Context, _ resource.SchemaRequest, r } // Create creates the resource and sets the initial Terraform state. -func (r *scheduleResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *scheduleResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() serverId := model.ServerId.ValueString() region := model.Region.ValueString() @@ -241,11 +251,13 @@ func (r *scheduleResource) Create(ctx context.Context, req resource.CreateReques core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating server update schedule", fmt.Sprintf("Creating API payload: %v", err)) return } + scheduleResp, err := r.client.CreateUpdateSchedule(ctx, projectId, serverId, region).CreateUpdateSchedulePayload(*payload).Execute() if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating server update schedule", fmt.Sprintf("Calling API: %v", err)) return } + ctx = tflog.SetField(ctx, "update_schedule_id", *scheduleResp.Id) // Map response body to schema @@ -254,22 +266,27 @@ func (r *scheduleResource) Create(ctx context.Context, req resource.CreateReques core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating server update schedule", fmt.Sprintf("Processing API payload: %v", err)) return } + diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "Server update schedule created.") } // Read refreshes the Terraform state with the latest data. -func (r *scheduleResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (r *scheduleResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() serverId := model.ServerId.ValueString() updateScheduleId := model.UpdateScheduleId.ValueInt64() @@ -286,7 +303,9 @@ func (r *scheduleResource) Read(ctx context.Context, req resource.ReadRequest, r resp.State.RemoveResource(ctx) return } + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading update schedule", fmt.Sprintf("Calling API: %v", err)) + return } @@ -300,20 +319,24 @@ func (r *scheduleResource) Read(ctx context.Context, req resource.ReadRequest, r // Set refreshed state diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "Server update schedule read.") } // Update updates the resource and sets the updated Terraform state on success. -func (r *scheduleResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *scheduleResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() serverId := model.ServerId.ValueString() updateScheduleId := model.UpdateScheduleId.ValueInt64() @@ -342,22 +365,27 @@ func (r *scheduleResource) Update(ctx context.Context, req resource.UpdateReques core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating server update schedule", fmt.Sprintf("Processing API payload: %v", err)) return } + diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "Server update schedule updated.") } // Delete deletes the resource and removes the Terraform state on success. -func (r *scheduleResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform +func (r *scheduleResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() serverId := model.ServerId.ValueString() updateScheduleId := model.UpdateScheduleId.ValueInt64() @@ -372,11 +400,12 @@ func (r *scheduleResource) Delete(ctx context.Context, req resource.DeleteReques core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting server update schedule", fmt.Sprintf("Calling API: %v", err)) return } + tflog.Info(ctx, "Server update schedule deleted.") } // ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: // project_id,server_id,schedule_id +// The expected format of the resource import identifier is: // project_id,server_id,schedule_id. func (r *scheduleResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { idParts := strings.Split(req.ID, core.Separator) if len(idParts) != 4 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" || idParts[3] == "" { @@ -384,6 +413,7 @@ func (r *scheduleResource) ImportState(ctx context.Context, req resource.ImportS "Error importing server update schedule", fmt.Sprintf("Expected import identifier with format [project_id],[region],[server_id],[update_schedule_id], got %q", req.ID), ) + return } @@ -393,6 +423,7 @@ func (r *scheduleResource) ImportState(ctx context.Context, req resource.ImportS "Error importing server update schedule", fmt.Sprintf("Expected update_schedule_id to be int64, got %q", idParts[2]), ) + return } @@ -407,9 +438,11 @@ func mapFields(schedule *serverupdate.UpdateSchedule, model *Model, region strin if schedule == nil { return fmt.Errorf("response input is nil") } + if model == nil { return fmt.Errorf("model input is nil") } + if schedule.Id == nil { return fmt.Errorf("response id is nil") } @@ -424,25 +457,30 @@ func mapFields(schedule *serverupdate.UpdateSchedule, model *Model, region strin model.Enabled = types.BoolPointerValue(schedule.Enabled) model.MaintenanceWindow = types.Int64PointerValue(schedule.MaintenanceWindow) model.Region = types.StringValue(region) + return nil } -// If already enabled, just continues +// If already enabled, just continues. func enableUpdatesService(ctx context.Context, model *Model, client *serverupdate.APIClient, region string) error { projectId := model.ProjectId.ValueString() serverId := model.ServerId.ValueString() payload := serverupdate.EnableServiceResourcePayload{} tflog.Debug(ctx, "Enabling server update service") + err := client.EnableServiceResource(ctx, projectId, serverId, region).EnableServiceResourcePayload(payload).Execute() if err != nil { if strings.Contains(err.Error(), "Tried to activate already active service") { tflog.Debug(ctx, "Service for server update already enabled") return nil } + return fmt.Errorf("enable server update service: %w", err) } + tflog.Info(ctx, "Enabled server update service") + return nil } diff --git a/stackit/internal/services/serverupdate/schedule/resource_test.go b/stackit/internal/services/serverupdate/schedule/resource_test.go index 43cb28951..ec385ea0a 100644 --- a/stackit/internal/services/serverupdate/schedule/resource_test.go +++ b/stackit/internal/services/serverupdate/schedule/resource_test.go @@ -11,6 +11,7 @@ import ( func TestMapFields(t *testing.T) { const testRegion = "region" + tests := []struct { description string input *sdk.UpdateSchedule @@ -77,13 +78,16 @@ func TestMapFields(t *testing.T) { ProjectId: tt.expected.ProjectId, ServerId: tt.expected.ServerId, } + err := mapFields(tt.input, state, tt.region) if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(state, &tt.expected) if diff != "" { @@ -148,9 +152,11 @@ func TestToCreatePayload(t *testing.T) { if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(output, tt.expected) if diff != "" { @@ -215,9 +221,11 @@ func TestToUpdatePayload(t *testing.T) { if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(output, tt.expected) if diff != "" { diff --git a/stackit/internal/services/serverupdate/schedule/schedule_datasource.go b/stackit/internal/services/serverupdate/schedule/schedule_datasource.go index b0b17e65b..ca3ad2f7b 100644 --- a/stackit/internal/services/serverupdate/schedule/schedule_datasource.go +++ b/stackit/internal/services/serverupdate/schedule/schedule_datasource.go @@ -6,20 +6,17 @@ import ( "net/http" "strconv" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - serverupdateUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/serverupdate/utils" - "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - + "github.com/stackitcloud/stackit-sdk-go/services/serverupdate" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/features" + serverupdateUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/serverupdate/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - - "github.com/stackitcloud/stackit-sdk-go/services/serverupdate" ) // scheduleDataSourceBetaCheckDone is used to prevent multiple checks for beta resources. @@ -51,6 +48,7 @@ func (r *scheduleDataSource) Metadata(_ context.Context, req datasource.Metadata // Configure adds the provider configured client to the data source. func (r *scheduleDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { var ok bool + r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) if !ok { return @@ -58,17 +56,22 @@ func (r *scheduleDataSource) Configure(ctx context.Context, req datasource.Confi if !scheduleDataSourceBetaCheckDone { features.CheckBetaResourcesEnabled(ctx, &r.providerData, &resp.Diagnostics, "stackit_server_update_schedule", "data source") + if resp.Diagnostics.HasError() { return } + scheduleDataSourceBetaCheckDone = true } apiClient := serverupdateUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + r.client = apiClient + tflog.Info(ctx, "Server update client configured") } @@ -128,13 +131,15 @@ func (r *scheduleDataSource) Schema(_ context.Context, _ datasource.SchemaReques } // Read refreshes the Terraform state with the latest data. -func (r *scheduleDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (r *scheduleDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.Config.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() serverId := model.ServerId.ValueString() updateScheduleId := model.UpdateScheduleId.ValueInt64() @@ -157,6 +162,7 @@ func (r *scheduleDataSource) Read(ctx context.Context, req datasource.ReadReques }, ) resp.State.RemoveResource(ctx) + return } @@ -170,8 +176,10 @@ func (r *scheduleDataSource) Read(ctx context.Context, req datasource.ReadReques // Set refreshed state diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "Server update schedule read") } diff --git a/stackit/internal/services/serverupdate/schedule/schedules_datasource.go b/stackit/internal/services/serverupdate/schedule/schedules_datasource.go index 4ea9a469e..407d787be 100644 --- a/stackit/internal/services/serverupdate/schedule/schedules_datasource.go +++ b/stackit/internal/services/serverupdate/schedule/schedules_datasource.go @@ -5,20 +5,18 @@ import ( "fmt" "net/http" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - serverupdateUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/serverupdate/utils" - "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/stackitcloud/stackit-sdk-go/services/serverupdate" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/features" + serverupdateUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/serverupdate/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - - "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/stackitcloud/stackit-sdk-go/services/serverupdate" ) // scheduleDataSourceBetaCheckDone is used to prevent multiple checks for beta resources. @@ -50,6 +48,7 @@ func (r *schedulesDataSource) Metadata(_ context.Context, req datasource.Metadat // Configure adds the provider configured client to the data source. func (r *schedulesDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { var ok bool + r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) if !ok { return @@ -57,17 +56,22 @@ func (r *schedulesDataSource) Configure(ctx context.Context, req datasource.Conf if !schedulesDataSourceBetaCheckDone { features.CheckBetaResourcesEnabled(ctx, &r.providerData, &resp.Diagnostics, "stackit_server_update_schedules", "data source") + if resp.Diagnostics.HasError() { return } + schedulesDataSourceBetaCheckDone = true } apiClient := serverupdateUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + r.client = apiClient + tflog.Info(ctx, "Server update client configured") } @@ -151,13 +155,15 @@ type schedulesDatasourceItemModel struct { } // Read refreshes the Terraform state with the latest data. -func (r *schedulesDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (r *schedulesDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model schedulesDataSourceModel diags := req.Config.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() serverId := model.ServerId.ValueString() region := r.providerData.GetRegionWithOverride(model.Region) @@ -177,6 +183,7 @@ func (r *schedulesDataSource) Read(ctx context.Context, req datasource.ReadReque }, ) resp.State.RemoveResource(ctx) + return } @@ -190,9 +197,11 @@ func (r *schedulesDataSource) Read(ctx context.Context, req datasource.ReadReque // Set refreshed state diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "Server update schedules read") } @@ -200,11 +209,13 @@ func mapSchedulesDatasourceFields(ctx context.Context, schedules *serverupdate.G if schedules == nil { return fmt.Errorf("response input is nil") } + if model == nil { return fmt.Errorf("model input is nil") } tflog.Debug(ctx, "response", map[string]any{"schedules": schedules}) + projectId := model.ProjectId.ValueString() serverId := model.ServerId.ValueString() @@ -221,5 +232,6 @@ func mapSchedulesDatasourceFields(ctx context.Context, schedules *serverupdate.G } model.Items = append(model.Items, scheduleState) } + return nil } diff --git a/stackit/internal/services/serverupdate/schedule/schedules_datasource_test.go b/stackit/internal/services/serverupdate/schedule/schedules_datasource_test.go index 2619daf09..fb6599e6a 100644 --- a/stackit/internal/services/serverupdate/schedule/schedules_datasource_test.go +++ b/stackit/internal/services/serverupdate/schedule/schedules_datasource_test.go @@ -12,6 +12,7 @@ import ( func TestMapSchedulesDataSourceFields(t *testing.T) { const testRegion = "region" + tests := []struct { description string input *sdk.GetUpdateSchedulesResponse @@ -80,13 +81,16 @@ func TestMapSchedulesDataSourceFields(t *testing.T) { ServerId: tt.expected.ServerId, } ctx := context.TODO() + err := mapSchedulesDatasourceFields(ctx, tt.input, state, tt.region) if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(state, &tt.expected) if diff != "" { diff --git a/stackit/internal/services/serverupdate/serverupdate_acc_test.go b/stackit/internal/services/serverupdate/serverupdate_acc_test.go index 33a0253a1..bf0aa4537 100644 --- a/stackit/internal/services/serverupdate/serverupdate_acc_test.go +++ b/stackit/internal/services/serverupdate/serverupdate_acc_test.go @@ -54,6 +54,7 @@ var testConfigVarsMax = config.Variables{ func configVarsInvalid(vars config.Variables) config.Variables { tempConfig := maps.Clone(vars) tempConfig["maintenance_window"] = config.IntegerVariable(0) + return tempConfig } @@ -69,6 +70,7 @@ func configVarsMaxUpdated() config.Variables { tempConfig := maps.Clone(testConfigVarsMax) tempConfig["maintenance_window"] = config.IntegerVariable(12) tempConfig["rrule"] = config.StringVariable("DTSTART;TZID=Europe/Berlin:20250430T010000 RRULE:FREQ=DAILY;INTERVAL=3") + return tempConfig } @@ -77,6 +79,7 @@ func TestAccServerUpdateScheduleMinResource(t *testing.T) { fmt.Println("TF_ACC_SERVER_ID not set, skipping test") return } + resource.Test(t, resource.TestCase{ ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, CheckDestroy: testAccCheckServerUpdateScheduleDestroy, @@ -134,6 +137,7 @@ func TestAccServerUpdateScheduleMinResource(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute update_schedule_id") } + return fmt.Sprintf("%s,%s,%s,%s", testutil.ProjectId, testutil.Region, testutil.ServerId, scheduleId), nil }, ImportState: true, @@ -164,6 +168,7 @@ func TestAccServerUpdateScheduleMaxResource(t *testing.T) { fmt.Println("TF_ACC_SERVER_ID not set, skipping test") return } + resource.Test(t, resource.TestCase{ ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, CheckDestroy: testAccCheckServerUpdateScheduleDestroy, @@ -222,6 +227,7 @@ func TestAccServerUpdateScheduleMaxResource(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute update_schedule_id") } + return fmt.Sprintf("%s,%s,%s,%s", testutil.ProjectId, testutil.Region, testutil.ServerId, scheduleId), nil }, ImportState: true, @@ -258,6 +264,7 @@ func testAccCheckServerUpdateScheduleDestroy(s *terraform.State) error { func deleteSchedule(ctx context.Context, s *terraform.State) error { var client *serverupdate.APIClient + var err error if testutil.ServerUpdateCustomEndpoint == "" { client, err = serverupdate.NewAPIClient() @@ -266,11 +273,13 @@ func deleteSchedule(ctx context.Context, s *terraform.State) error { core_config.WithEndpoint(testutil.ServerUpdateCustomEndpoint), ) } + if err != nil { return fmt.Errorf("creating client: %w", err) } schedulesToDestroy := []string{} + for _, rs := range s.RootModule().Resources { if rs.Type != "stackit_server_update_schedule" { continue @@ -290,6 +299,7 @@ func deleteSchedule(ctx context.Context, s *terraform.State) error { if schedules[i].Id == nil { continue } + scheduleId := strconv.FormatInt(*schedules[i].Id, 10) if utils.Contains(schedulesToDestroy, scheduleId) { err := client.DeleteUpdateScheduleExecute(ctx, testutil.ProjectId, testutil.ServerId, scheduleId, testutil.Region) @@ -298,5 +308,6 @@ func deleteSchedule(ctx context.Context, s *terraform.State) error { } } } + return nil } diff --git a/stackit/internal/services/serverupdate/utils/util.go b/stackit/internal/services/serverupdate/utils/util.go index cfd39299d..143429c28 100644 --- a/stackit/internal/services/serverupdate/utils/util.go +++ b/stackit/internal/services/serverupdate/utils/util.go @@ -4,10 +4,9 @@ import ( "context" "fmt" - "github.com/stackitcloud/stackit-sdk-go/services/serverupdate" - "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/stackitcloud/stackit-sdk-go/core/config" + "github.com/stackitcloud/stackit-sdk-go/services/serverupdate" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" ) @@ -20,6 +19,7 @@ func ConfigureClient(ctx context.Context, providerData *core.ProviderData, diags if providerData.ServerUpdateCustomEndpoint != "" { apiClientConfigOptions = append(apiClientConfigOptions, config.WithEndpoint(providerData.ServerUpdateCustomEndpoint)) } + apiClient, err := serverupdate.NewAPIClient(apiClientConfigOptions...) if err != nil { core.LogAndAddError(ctx, diags, "Error configuring API client", fmt.Sprintf("Configuring client: %v. This is an error related to the provider configuration, not to the resource configuration", err)) diff --git a/stackit/internal/services/serverupdate/utils/util_test.go b/stackit/internal/services/serverupdate/utils/util_test.go index 1b687588f..2cb6781f9 100644 --- a/stackit/internal/services/serverupdate/utils/util_test.go +++ b/stackit/internal/services/serverupdate/utils/util_test.go @@ -22,6 +22,7 @@ const ( func TestConfigureClient(t *testing.T) { /* mock authentication by setting service account token env variable */ os.Clearenv() + err := os.Setenv(sdkClients.ServiceAccountToken, "mock-val") if err != nil { t.Errorf("error setting env variable: %v", err) @@ -30,6 +31,7 @@ func TestConfigureClient(t *testing.T) { type args struct { providerData *core.ProviderData } + tests := []struct { name string args args @@ -50,6 +52,7 @@ func TestConfigureClient(t *testing.T) { if err != nil { t.Errorf("error configuring client: %v", err) } + return apiClient }(), wantErr: false, @@ -70,6 +73,7 @@ func TestConfigureClient(t *testing.T) { if err != nil { t.Errorf("error configuring client: %v", err) } + return apiClient }(), wantErr: false, @@ -81,6 +85,7 @@ func TestConfigureClient(t *testing.T) { diags := diag.Diagnostics{} actual := ConfigureClient(ctx, tt.args.providerData, &diags) + if diags.HasError() != tt.wantErr { t.Errorf("ConfigureClient() error = %v, want %v", diags.HasError(), tt.wantErr) } diff --git a/stackit/internal/services/serviceaccount/account/datasource.go b/stackit/internal/services/serviceaccount/account/datasource.go index 9f3430110..bc3071b20 100644 --- a/stackit/internal/services/serviceaccount/account/datasource.go +++ b/stackit/internal/services/serviceaccount/account/datasource.go @@ -4,16 +4,15 @@ import ( "context" "fmt" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - serviceaccountUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/serviceaccount/utils" - "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/stackitcloud/stackit-sdk-go/services/serviceaccount" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + serviceaccountUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/serviceaccount/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" ) @@ -41,10 +40,13 @@ func (r *serviceAccountDataSource) Configure(ctx context.Context, req datasource } apiClient := serviceaccountUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + r.client = apiClient + tflog.Info(ctx, "Service Account client configured") } @@ -94,10 +96,11 @@ func (r *serviceAccountDataSource) Schema(_ context.Context, _ datasource.Schema } // Read reads all service accounts from the API and updates the state with the latest information. -func (r *serviceAccountDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (r *serviceAccountDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.Config.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -117,6 +120,7 @@ func (r *serviceAccountDataSource) Read(ctx context.Context, req datasource.Read map[int]string{}, ) resp.State.RemoveResource(ctx) + return } @@ -144,6 +148,7 @@ func (r *serviceAccountDataSource) Read(ctx context.Context, req datasource.Read // Update the state with the service account model diags = resp.State.Set(ctx, &model) resp.Diagnostics.Append(diags...) + return } diff --git a/stackit/internal/services/serviceaccount/account/resource.go b/stackit/internal/services/serviceaccount/account/resource.go index 1a779a816..2c9ccd087 100644 --- a/stackit/internal/services/serviceaccount/account/resource.go +++ b/stackit/internal/services/serviceaccount/account/resource.go @@ -7,10 +7,6 @@ import ( "strings" "time" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - - serviceaccountUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/serviceaccount/utils" - "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" @@ -23,6 +19,8 @@ import ( "github.com/stackitcloud/stackit-sdk-go/services/serviceaccount" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + serviceaccountUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/serviceaccount/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" ) @@ -59,10 +57,13 @@ func (r *serviceAccountResource) Configure(ctx context.Context, req resource.Con } apiClient := serviceaccountUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + r.client = apiClient + tflog.Info(ctx, "Service Account client configured") } @@ -119,11 +120,12 @@ func (r *serviceAccountResource) Schema(_ context.Context, _ resource.SchemaRequ } // Create creates the resource and sets the initial Terraform state for service accounts. -func (r *serviceAccountResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *serviceAccountResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { //nolint:gocritic // function signature required by Terraform // Retrieve the planned values for the resource. var model Model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -150,6 +152,7 @@ func (r *serviceAccountResource) Create(ctx context.Context, req resource.Create // Set the service account name and map the response to the resource schema. model.Name = types.StringValue(serviceAccountName) + err = mapFields(serviceAccountResp, &model) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating service account", fmt.Sprintf("Processing API response: %v", err)) @@ -162,18 +165,21 @@ func (r *serviceAccountResource) Create(ctx context.Context, req resource.Create // Set the state with fully populated data. diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "Service account created") } // Read refreshes the Terraform state with the latest service account data. -func (r *serviceAccountResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (r *serviceAccountResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { //nolint:gocritic // function signature required by Terraform // Retrieve the current state of the resource. var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -205,6 +211,7 @@ func (r *serviceAccountResource) Read(ctx context.Context, req resource.ReadRequ // Set the updated state. diags = resp.State.Set(ctx, &model) resp.Diagnostics.Append(diags...) + return } @@ -218,17 +225,18 @@ func (r *serviceAccountResource) Read(ctx context.Context, req resource.ReadRequ // As a result, the Update function is redundant since any modifications will // automatically trigger a resource recreation through Terraform's built-in // lifecycle management. -func (r *serviceAccountResource) Update(ctx context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *serviceAccountResource) Update(ctx context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { //nolint:gocritic // function signature required by Terraform // Service accounts cannot be updated, so we log an error. core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating service account", "Service accounts can't be updated") } // Delete deletes the service account and removes it from the Terraform state on success. -func (r *serviceAccountResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform +func (r *serviceAccountResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { //nolint:gocritic // function signature required by Terraform // Retrieve current state of the resource. var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -245,11 +253,12 @@ func (r *serviceAccountResource) Delete(ctx context.Context, req resource.Delete core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting service account", fmt.Sprintf("Calling API: %v", err)) return } + tflog.Info(ctx, "Service account deleted") } // ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: project_id,email +// The expected format of the resource import identifier is: project_id,email. func (r *serviceAccountResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { // Split the import identifier to extract project ID and email. idParts := strings.Split(req.ID, core.Separator) @@ -260,6 +269,7 @@ func (r *serviceAccountResource) ImportState(ctx context.Context, req resource.I "Error importing service account", fmt.Sprintf("Expected import identifier with format: [project_id],[email] Got: %q", req.ID), ) + return } @@ -294,6 +304,7 @@ func mapFields(resp *serviceaccount.ServiceAccount, model *Model) error { if resp == nil { return fmt.Errorf("response input is nil") } + if model == nil { return fmt.Errorf("model input is nil") } diff --git a/stackit/internal/services/serviceaccount/account/resource_test.go b/stackit/internal/services/serviceaccount/account/resource_test.go index 14cbb992c..e06f9d687 100644 --- a/stackit/internal/services/serviceaccount/account/resource_test.go +++ b/stackit/internal/services/serviceaccount/account/resource_test.go @@ -47,9 +47,11 @@ func TestToCreatePayload(t *testing.T) { if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(output, tt.expected) if diff != "" { @@ -107,13 +109,16 @@ func TestMapFields(t *testing.T) { state := &Model{ ProjectId: tt.expected.ProjectId, } + err := mapFields(tt.input, state) if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(state, &tt.expected) if diff != "" { @@ -152,6 +157,7 @@ func TestParseNameFromEmail(t *testing.T) { if err != nil { t.Errorf("did not expect an error for email: %s, but got: %v", tc.email, err) } + if name != tc.expected { t.Errorf("expected name: %s, got: %s for email: %s", tc.expected, name, tc.email) } diff --git a/stackit/internal/services/serviceaccount/key/resource.go b/stackit/internal/services/serviceaccount/key/resource.go index 4e8565335..29bb3c4e1 100644 --- a/stackit/internal/services/serviceaccount/key/resource.go +++ b/stackit/internal/services/serviceaccount/key/resource.go @@ -8,8 +8,6 @@ import ( "net/http" "time" - serviceaccountUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/serviceaccount/utils" - "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" @@ -24,6 +22,7 @@ import ( "github.com/stackitcloud/stackit-sdk-go/services/serviceaccount" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + serviceaccountUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/serviceaccount/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" ) @@ -64,10 +63,13 @@ func (r *serviceAccountKeyResource) Configure(ctx context.Context, req resource. } apiClient := serviceaccountUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + r.client = apiClient + tflog.Info(ctx, "Service Account client configured") } @@ -153,11 +155,12 @@ func (r *serviceAccountKeyResource) Schema(_ context.Context, _ resource.SchemaR } // Create creates the resource and sets the initial Terraform state for service accounts. -func (r *serviceAccountKeyResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *serviceAccountKeyResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { //nolint:gocritic // function signature required by Terraform // Retrieve the planned values for the resource. var model Model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -181,7 +184,6 @@ func (r *serviceAccountKeyResource) Create(ctx context.Context, req resource.Cre // Initialize the API request with the required parameters. saAccountKeyResp, err := r.client.CreateServiceAccountKey(ctx, projectId, serviceAccountEmail).CreateServiceAccountKeyPayload(*payload).Execute() - if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Failed to create service account key", fmt.Sprintf("API call error: %v", err)) return @@ -197,18 +199,21 @@ func (r *serviceAccountKeyResource) Create(ctx context.Context, req resource.Cre // Set the state with fully populated data. diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "Service account key created") } // Read refreshes the Terraform state with the latest service account data. -func (r *serviceAccountKeyResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (r *serviceAccountKeyResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { //nolint:gocritic // function signature required by Terraform // Retrieve the current state of the resource. var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -226,16 +231,20 @@ func (r *serviceAccountKeyResource) Read(ctx context.Context, req resource.ReadR resp.State.RemoveResource(ctx) return } + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading service account key", fmt.Sprintf("Calling API: %v", err)) + return } // No mapping needed for read response, as private_key is excluded and ID remains unchanged. diags = resp.State.Set(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "key read") } @@ -245,17 +254,18 @@ func (r *serviceAccountKeyResource) Read(ctx context.Context, req resource.ReadR // As a result, the Update function is redundant since any modifications will // automatically trigger a resource recreation through Terraform's built-in // lifecycle management. -func (r *serviceAccountKeyResource) Update(ctx context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *serviceAccountKeyResource) Update(ctx context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { //nolint:gocritic // function signature required by Terraform // Service accounts cannot be updated, so we log an error. core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating service account key", "Service account key can't be updated") } // Delete deletes the service account key and removes it from the Terraform state on success. -func (r *serviceAccountKeyResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform +func (r *serviceAccountKeyResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { //nolint:gocritic // function signature required by Terraform // Retrieve current state of the resource. var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -291,6 +301,7 @@ func toCreatePayload(model *Model) (*serviceaccount.CreateServiceAccountKeyPaylo if err != nil { return nil, err } + payload.ValidUntil = &validUntil } @@ -307,6 +318,7 @@ func computeValidUntil(ttlDays *int64) (time.Time, error) { if ttlDays == nil { return time.Time{}, fmt.Errorf("ttlDays is nil") } + return time.Now().UTC().Add(time.Duration(*ttlDays) * 24 * time.Hour), nil } diff --git a/stackit/internal/services/serviceaccount/key/resource_test.go b/stackit/internal/services/serviceaccount/key/resource_test.go index 15f90a1ba..eed3ac525 100644 --- a/stackit/internal/services/serviceaccount/key/resource_test.go +++ b/stackit/internal/services/serviceaccount/key/resource_test.go @@ -35,13 +35,16 @@ func TestComputeValidUntil(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { int64TTlDays := int64(*tt.ttlDays) + validUntil, err := computeValidUntil(&int64TTlDays) if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { tolerance := 1 * time.Second if validUntil.Sub(tt.expected) > tolerance && tt.expected.Sub(validUntil) > tolerance { @@ -105,15 +108,19 @@ func TestMapResponse(t *testing.T) { Json: types.StringValue("{}"), RotateWhenChanged: types.MapValueMust(types.StringType, map[string]attr.Value{}), } + err := mapCreateResponse(tt.input, model) if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { model.Json = types.StringValue("{}") + diff := cmp.Diff(*model, tt.expected) if diff != "" { t.Fatalf("Data does not match: %s", diff) diff --git a/stackit/internal/services/serviceaccount/serviceaccount_acc_test.go b/stackit/internal/services/serviceaccount/serviceaccount_acc_test.go index 032dae785..e7f52a1a9 100644 --- a/stackit/internal/services/serviceaccount/serviceaccount_acc_test.go +++ b/stackit/internal/services/serviceaccount/serviceaccount_acc_test.go @@ -15,7 +15,7 @@ import ( "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/testutil" ) -// Service Account resource data +// Service Account resource data. var serviceAccountResource = map[string]string{ "project_id": testutil.ProjectId, "name01": "sa-test-01", @@ -134,6 +134,7 @@ func TestServiceAccount(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute email") } + return fmt.Sprintf("%s,%s", testutil.ProjectId, email), nil }, ImportState: true, @@ -146,7 +147,9 @@ func TestServiceAccount(t *testing.T) { func testAccCheckServiceAccountDestroy(s *terraform.State) error { ctx := context.Background() + var client *serviceaccount.APIClient + var err error if testutil.ServiceAccountCustomEndpoint == "" { @@ -162,10 +165,12 @@ func testAccCheckServiceAccountDestroy(s *terraform.State) error { } var instancesToDestroy []string + for _, rs := range s.RootModule().Resources { if rs.Type != "stackit_service_account" { continue } + serviceAccountEmail := strings.Split(rs.Primary.ID, core.Separator)[1] instancesToDestroy = append(instancesToDestroy, serviceAccountEmail) } @@ -180,6 +185,7 @@ func testAccCheckServiceAccountDestroy(s *terraform.State) error { if serviceAccounts[i].Email == nil { continue } + if utils.Contains(instancesToDestroy, *serviceAccounts[i].Email) { err := client.DeleteServiceAccount(ctx, testutil.ProjectId, *serviceAccounts[i].Email).Execute() if err != nil { @@ -187,5 +193,6 @@ func testAccCheckServiceAccountDestroy(s *terraform.State) error { } } } + return nil } diff --git a/stackit/internal/services/serviceaccount/token/resource.go b/stackit/internal/services/serviceaccount/token/resource.go index 86a7135cb..63bc8c5ee 100644 --- a/stackit/internal/services/serviceaccount/token/resource.go +++ b/stackit/internal/services/serviceaccount/token/resource.go @@ -7,10 +7,6 @@ import ( "net/http" "time" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - - serviceaccountUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/serviceaccount/utils" - "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" @@ -27,6 +23,8 @@ import ( "github.com/stackitcloud/stackit-sdk-go/services/serviceaccount" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + serviceaccountUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/serviceaccount/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" ) @@ -68,10 +66,13 @@ func (r *serviceAccountTokenResource) Configure(ctx context.Context, req resourc } apiClient := serviceaccountUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + r.client = apiClient + tflog.Info(ctx, "Service Account client configured") } @@ -169,12 +170,13 @@ func (r *serviceAccountTokenResource) Schema(_ context.Context, _ resource.Schem } // Create creates the resource and sets the initial Terraform state for service accounts. -func (r *serviceAccountTokenResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *serviceAccountTokenResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { //nolint:gocritic // function signature required by Terraform core.LogAndAddWarning(ctx, &resp.Diagnostics, "stackit_service_account_access_token resource deprecated", "use stackit_service_account_key resource instead") // Retrieve the planned values for the resource. var model Model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -194,7 +196,6 @@ func (r *serviceAccountTokenResource) Create(ctx context.Context, req resource.C // Initialize the API request with the required parameters. serviceAccountAccessTokenResp, err := r.client.CreateAccessToken(ctx, projectId, serviceAccountEmail).CreateAccessTokenPayload(*payload).Execute() - if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Failed to create service account access token", fmt.Sprintf("API call error: %v", err)) return @@ -210,19 +211,22 @@ func (r *serviceAccountTokenResource) Create(ctx context.Context, req resource.C // Set the state with fully populated data. diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "Service account access token created") } // Read refreshes the Terraform state with the latest service account data. -func (r *serviceAccountTokenResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (r *serviceAccountTokenResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { //nolint:gocritic // function signature required by Terraform core.LogAndAddWarning(ctx, &resp.Diagnostics, "stackit_service_account_access_token resource deprecated", "use stackit_service_account_key resource instead") // Retrieve the current state of the resource. var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -235,13 +239,15 @@ func (r *serviceAccountTokenResource) Read(ctx context.Context, req resource.Rea listSaTokensResp, err := r.client.ListAccessTokens(ctx, projectId, serviceAccountEmail).Execute() if err != nil { var oapiErr *oapierror.GenericOpenAPIError - ok := errors.As(err, &oapiErr) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped + ok := errors.As(err, &oapiErr) // due to security purposes, attempting to list access tokens for a non-existent Service Account will return 403. if ok && oapiErr.StatusCode == http.StatusNotFound || oapiErr.StatusCode == http.StatusForbidden { resp.State.RemoveResource(ctx) return } + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading service account tokens", fmt.Sprintf("Error calling API: %v", err)) + return } @@ -255,6 +261,7 @@ func (r *serviceAccountTokenResource) Read(ctx context.Context, req resource.Rea if !*saTokens[i].Active { tflog.Info(ctx, fmt.Sprintf("Service account access token with id %s is not active", model.AccessTokenId.ValueString())) resp.State.RemoveResource(ctx) + return } @@ -267,6 +274,7 @@ func (r *serviceAccountTokenResource) Read(ctx context.Context, req resource.Rea // Set the updated state. diags = resp.State.Set(ctx, &model) resp.Diagnostics.Append(diags...) + return } // If no matching service account access token is found, remove the resource from the state. @@ -280,18 +288,19 @@ func (r *serviceAccountTokenResource) Read(ctx context.Context, req resource.Rea // As a result, the Update function is redundant since any modifications will // automatically trigger a resource recreation through Terraform's built-in // lifecycle management. -func (r *serviceAccountTokenResource) Update(ctx context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *serviceAccountTokenResource) Update(ctx context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { //nolint:gocritic // function signature required by Terraform // Service accounts cannot be updated, so we log an error. core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating service account access token", "Service accounts can't be updated") } // Delete deletes the service account and removes it from the Terraform state on success. -func (r *serviceAccountTokenResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform +func (r *serviceAccountTokenResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { //nolint:gocritic // function signature required by Terraform core.LogAndAddWarning(ctx, &resp.Diagnostics, "stackit_service_account_access_token resource deprecated", "use stackit_service_account_key resource instead") // Retrieve current state of the resource. var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -309,6 +318,7 @@ func (r *serviceAccountTokenResource) Delete(ctx context.Context, req resource.D core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting service account token", fmt.Sprintf("Calling API: %v", err)) return } + tflog.Info(ctx, "Service account token deleted") } @@ -326,6 +336,7 @@ func mapCreateResponse(resp *serviceaccount.AccessToken, model *Model) error { if resp == nil { return fmt.Errorf("response input is nil") } + if model == nil { return fmt.Errorf("model input is nil") } @@ -339,12 +350,14 @@ func mapCreateResponse(resp *serviceaccount.AccessToken, model *Model) error { } var createdAt basetypes.StringValue + if resp.CreatedAt != nil { createdAtValue := *resp.CreatedAt createdAt = types.StringValue(createdAtValue.Format(time.RFC3339)) } var validUntil basetypes.StringValue + if resp.ValidUntil != nil { validUntilValue := *resp.ValidUntil validUntil = types.StringValue(validUntilValue.Format(time.RFC3339)) @@ -364,6 +377,7 @@ func mapListResponse(resp *serviceaccount.AccessTokenMetadata, model *Model) err if resp == nil { return fmt.Errorf("response input is nil") } + if model == nil { return fmt.Errorf("model input is nil") } @@ -373,12 +387,14 @@ func mapListResponse(resp *serviceaccount.AccessTokenMetadata, model *Model) err } var createdAt basetypes.StringValue + if resp.CreatedAt != nil { createdAtValue := *resp.CreatedAt createdAt = types.StringValue(createdAtValue.Format(time.RFC3339)) } var validUntil basetypes.StringValue + if resp.ValidUntil != nil { validUntilValue := *resp.ValidUntil validUntil = types.StringValue(validUntilValue.Format(time.RFC3339)) diff --git a/stackit/internal/services/serviceaccount/token/resource_test.go b/stackit/internal/services/serviceaccount/token/resource_test.go index 08f7d26a8..5cebe5d28 100644 --- a/stackit/internal/services/serviceaccount/token/resource_test.go +++ b/stackit/internal/services/serviceaccount/token/resource_test.go @@ -44,9 +44,11 @@ func TestToCreatePayload(t *testing.T) { if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(output, tt.expected) if diff != "" { @@ -138,13 +140,16 @@ func TestMapCreateResponse(t *testing.T) { ServiceAccountEmail: tt.expected.ServiceAccountEmail, RotateWhenChanged: types.MapValueMust(types.StringType, map[string]attr.Value{}), } + err := mapCreateResponse(tt.input, model) if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(*model, tt.expected) if diff != "" { @@ -212,13 +217,16 @@ func TestMapListResponse(t *testing.T) { ServiceAccountEmail: tt.expected.ServiceAccountEmail, RotateWhenChanged: types.MapValueMust(types.StringType, map[string]attr.Value{}), } + err := mapListResponse(tt.input, model) if !tt.isValid && err == nil { t.Fatalf("Expected an error but did not get one") } + if tt.isValid && err != nil { t.Fatalf("Did not expect an error but got one: %v", err) } + if tt.isValid { diff := cmp.Diff(*model, tt.expected) if diff != "" { diff --git a/stackit/internal/services/serviceaccount/utils/util.go b/stackit/internal/services/serviceaccount/utils/util.go index 5fd45eb0b..e7a824f78 100644 --- a/stackit/internal/services/serviceaccount/utils/util.go +++ b/stackit/internal/services/serviceaccount/utils/util.go @@ -19,6 +19,7 @@ func ConfigureClient(ctx context.Context, providerData *core.ProviderData, diags if providerData.ServiceAccountCustomEndpoint != "" { apiClientConfigOptions = append(apiClientConfigOptions, config.WithEndpoint(providerData.ServiceAccountCustomEndpoint)) } + apiClient, err := serviceaccount.NewAPIClient(apiClientConfigOptions...) if err != nil { core.LogAndAddError(ctx, diags, "Error configuring API client", fmt.Sprintf("Configuring client: %v. This is an error related to the provider configuration, not to the resource configuration", err)) diff --git a/stackit/internal/services/serviceaccount/utils/util_test.go b/stackit/internal/services/serviceaccount/utils/util_test.go index e18942f7c..3d70cfd52 100644 --- a/stackit/internal/services/serviceaccount/utils/util_test.go +++ b/stackit/internal/services/serviceaccount/utils/util_test.go @@ -22,6 +22,7 @@ const ( func TestConfigureClient(t *testing.T) { /* mock authentication by setting service account token env variable */ os.Clearenv() + err := os.Setenv(sdkClients.ServiceAccountToken, "mock-val") if err != nil { t.Errorf("error setting env variable: %v", err) @@ -30,6 +31,7 @@ func TestConfigureClient(t *testing.T) { type args struct { providerData *core.ProviderData } + tests := []struct { name string args args @@ -50,6 +52,7 @@ func TestConfigureClient(t *testing.T) { if err != nil { t.Errorf("error configuring client: %v", err) } + return apiClient }(), wantErr: false, @@ -70,6 +73,7 @@ func TestConfigureClient(t *testing.T) { if err != nil { t.Errorf("error configuring client: %v", err) } + return apiClient }(), wantErr: false, @@ -81,6 +85,7 @@ func TestConfigureClient(t *testing.T) { diags := diag.Diagnostics{} actual := ConfigureClient(ctx, tt.args.providerData, &diags) + if diags.HasError() != tt.wantErr { t.Errorf("ConfigureClient() error = %v, want %v", diags.HasError(), tt.wantErr) } diff --git a/stackit/internal/services/serviceenablement/utils/util.go b/stackit/internal/services/serviceenablement/utils/util.go index 77cdd6899..682874fc7 100644 --- a/stackit/internal/services/serviceenablement/utils/util.go +++ b/stackit/internal/services/serviceenablement/utils/util.go @@ -21,6 +21,7 @@ func ConfigureClient(ctx context.Context, providerData *core.ProviderData, diags } else { apiClientConfigOptions = append(apiClientConfigOptions, config.WithRegion(providerData.GetRegion())) } + apiClient, err := serviceenablement.NewAPIClient(apiClientConfigOptions...) if err != nil { core.LogAndAddError(ctx, diags, "Error configuring API client", fmt.Sprintf("Configuring client: %v. This is an error related to the provider configuration, not to the resource configuration", err)) diff --git a/stackit/internal/services/serviceenablement/utils/util_test.go b/stackit/internal/services/serviceenablement/utils/util_test.go index 5825ed6f9..74705fb8a 100644 --- a/stackit/internal/services/serviceenablement/utils/util_test.go +++ b/stackit/internal/services/serviceenablement/utils/util_test.go @@ -22,6 +22,7 @@ const ( func TestConfigureClient(t *testing.T) { /* mock authentication by setting service account token env variable */ os.Clearenv() + err := os.Setenv(sdkClients.ServiceAccountToken, "mock-val") if err != nil { t.Errorf("error setting env variable: %v", err) @@ -30,6 +31,7 @@ func TestConfigureClient(t *testing.T) { type args struct { providerData *core.ProviderData } + tests := []struct { name string args args @@ -51,6 +53,7 @@ func TestConfigureClient(t *testing.T) { if err != nil { t.Errorf("error configuring client: %v", err) } + return apiClient }(), wantErr: false, @@ -71,6 +74,7 @@ func TestConfigureClient(t *testing.T) { if err != nil { t.Errorf("error configuring client: %v", err) } + return apiClient }(), wantErr: false, @@ -82,6 +86,7 @@ func TestConfigureClient(t *testing.T) { diags := diag.Diagnostics{} actual := ConfigureClient(ctx, tt.args.providerData, &diags) + if diags.HasError() != tt.wantErr { t.Errorf("ConfigureClient() error = %v, want %v", diags.HasError(), tt.wantErr) } diff --git a/stackit/internal/services/ske/cluster/datasource.go b/stackit/internal/services/ske/cluster/datasource.go index 655011613..a14fc9a03 100644 --- a/stackit/internal/services/ske/cluster/datasource.go +++ b/stackit/internal/services/ske/cluster/datasource.go @@ -5,16 +5,15 @@ import ( "fmt" "net/http" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - skeUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/ske/utils" - "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/stackitcloud/stackit-sdk-go/services/ske" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + skeUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/ske/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" ) @@ -43,18 +42,23 @@ func (r *clusterDataSource) Metadata(_ context.Context, req datasource.MetadataR // Configure adds the provider configured client to the data source. func (r *clusterDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { var ok bool + r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) if !ok { return } apiClient := skeUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + r.client = apiClient + tflog.Info(ctx, "SKE client configured") } + func (r *clusterDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { resp.Schema = schema.Schema{ Description: "SKE Cluster data source schema. Must have a `region` specified in the provider configuration.", @@ -322,10 +326,11 @@ func (r *clusterDataSource) Schema(_ context.Context, _ datasource.SchemaRequest } // Read refreshes the Terraform state with the latest data. -func (r *clusterDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (r *clusterDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var state Model diags := req.Config.Get(ctx, &state) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -349,6 +354,7 @@ func (r *clusterDataSource) Read(ctx context.Context, req datasource.ReadRequest }, ) resp.State.RemoveResource(ctx) + return } @@ -361,8 +367,10 @@ func (r *clusterDataSource) Read(ctx context.Context, req datasource.ReadRequest // Set refreshed state diags = resp.State.Set(ctx, state) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "SKE cluster read") } diff --git a/stackit/internal/services/ske/cluster/resource.go b/stackit/internal/services/ske/cluster/resource.go index a944161c0..6549ae3a2 100644 --- a/stackit/internal/services/ske/cluster/resource.go +++ b/stackit/internal/services/ske/cluster/resource.go @@ -9,9 +9,6 @@ import ( "strings" "time" - serviceenablementUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/serviceenablement/utils" - skeUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/ske/utils" - "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/attr" @@ -40,6 +37,8 @@ import ( skeWait "github.com/stackitcloud/stackit-sdk-go/services/ske/wait" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + serviceenablementUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/serviceenablement/utils" + skeUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/ske/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" "golang.org/x/mod/semver" @@ -85,7 +84,7 @@ type Model struct { Region types.String `tfsdk:"region"` } -// Struct corresponding to Model.NodePools[i] +// Struct corresponding to Model.NodePools[i]. type nodePool struct { Name types.String `tfsdk:"name"` MachineType types.String `tfsdk:"machine_type"` @@ -106,7 +105,7 @@ type nodePool struct { AllowSystemComponents types.Bool `tfsdk:"allow_system_components"` } -// Types corresponding to nodePool +// Types corresponding to nodePool. var nodePoolTypes = map[string]attr.Type{ "name": basetypes.StringType{}, "machine_type": basetypes.StringType{}, @@ -127,21 +126,21 @@ var nodePoolTypes = map[string]attr.Type{ "allow_system_components": basetypes.BoolType{}, } -// Struct corresponding to nodePool.Taints[i] +// Struct corresponding to nodePool.Taints[i]. type taint struct { Effect types.String `tfsdk:"effect"` Key types.String `tfsdk:"key"` Value types.String `tfsdk:"value"` } -// Types corresponding to taint +// Types corresponding to taint. var taintTypes = map[string]attr.Type{ "effect": basetypes.StringType{}, "key": basetypes.StringType{}, "value": basetypes.StringType{}, } -// Struct corresponding to Model.maintenance +// Struct corresponding to Model.maintenance. type maintenance struct { EnableKubernetesVersionUpdates types.Bool `tfsdk:"enable_kubernetes_version_updates"` EnableMachineImageVersionUpdates types.Bool `tfsdk:"enable_machine_image_version_updates"` @@ -149,7 +148,7 @@ type maintenance struct { End types.String `tfsdk:"end"` } -// Types corresponding to maintenance +// Types corresponding to maintenance. var maintenanceTypes = map[string]attr.Type{ "enable_kubernetes_version_updates": basetypes.BoolType{}, "enable_machine_image_version_updates": basetypes.BoolType{}, @@ -157,31 +156,31 @@ var maintenanceTypes = map[string]attr.Type{ "end": basetypes.StringType{}, } -// Struct corresponding to Model.Network +// Struct corresponding to Model.Network. type network struct { ID types.String `tfsdk:"id"` } -// Types corresponding to network +// Types corresponding to network. var networkTypes = map[string]attr.Type{ "id": basetypes.StringType{}, } -// Struct corresponding to Model.Hibernations[i] +// Struct corresponding to Model.Hibernations[i]. type hibernation struct { Start types.String `tfsdk:"start"` End types.String `tfsdk:"end"` Timezone types.String `tfsdk:"timezone"` } -// Types corresponding to hibernation +// Types corresponding to hibernation. var hibernationTypes = map[string]attr.Type{ "start": basetypes.StringType{}, "end": basetypes.StringType{}, "timezone": basetypes.StringType{}, } -// Struct corresponding to Model.Extensions +// Struct corresponding to Model.Extensions. type extensions struct { Argus types.Object `tfsdk:"argus"` Observability types.Object `tfsdk:"observability"` @@ -189,7 +188,7 @@ type extensions struct { DNS types.Object `tfsdk:"dns"` } -// Types corresponding to extensions +// Types corresponding to extensions. var extensionsTypes = map[string]attr.Type{ "argus": basetypes.ObjectType{AttrTypes: argusTypes}, "observability": basetypes.ObjectType{AttrTypes: observabilityTypes}, @@ -197,49 +196,49 @@ var extensionsTypes = map[string]attr.Type{ "dns": basetypes.ObjectType{AttrTypes: dnsTypes}, } -// Struct corresponding to extensions.ACL +// Struct corresponding to extensions.ACL. type acl struct { Enabled types.Bool `tfsdk:"enabled"` AllowedCIDRs types.List `tfsdk:"allowed_cidrs"` } -// Types corresponding to acl +// Types corresponding to acl. var aclTypes = map[string]attr.Type{ "enabled": basetypes.BoolType{}, "allowed_cidrs": basetypes.ListType{ElemType: types.StringType}, } -// Struct corresponding to extensions.Argus +// Struct corresponding to extensions.Argus. type argus struct { Enabled types.Bool `tfsdk:"enabled"` ArgusInstanceId types.String `tfsdk:"argus_instance_id"` } -// Types corresponding to argus +// Types corresponding to argus. var argusTypes = map[string]attr.Type{ "enabled": basetypes.BoolType{}, "argus_instance_id": basetypes.StringType{}, } -// Struct corresponding to extensions.Observability +// Struct corresponding to extensions.Observability. type observability struct { Enabled types.Bool `tfsdk:"enabled"` InstanceId types.String `tfsdk:"instance_id"` } -// Types corresponding to observability +// Types corresponding to observability. var observabilityTypes = map[string]attr.Type{ "enabled": basetypes.BoolType{}, "instance_id": basetypes.StringType{}, } -// Struct corresponding to extensions.DNS +// Struct corresponding to extensions.DNS. type dns struct { Enabled types.Bool `tfsdk:"enabled"` Zones types.List `tfsdk:"zones"` } -// Types corresponding to DNS +// Types corresponding to DNS. var dnsTypes = map[string]attr.Type{ "enabled": basetypes.BoolType{}, "zones": basetypes.ListType{ElemType: types.StringType}, @@ -259,29 +258,35 @@ type clusterResource struct { // ModifyPlan implements resource.ResourceWithModifyPlan. // Use the modifier to set the effective region in the current plan. -func (r *clusterResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform +func (r *clusterResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { //nolint:gocritic // function signature required by Terraform var configModel Model // skip initial empty configuration to avoid follow-up errors if req.Config.Raw.IsNull() { return } + resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...) + if resp.Diagnostics.HasError() { return } var planModel Model + resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) + if resp.Diagnostics.HasError() { return } utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp) + if resp.Diagnostics.HasError() { return } resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...) + if resp.Diagnostics.HasError() { return } @@ -295,21 +300,27 @@ func (r *clusterResource) Metadata(_ context.Context, req resource.MetadataReque // Configure adds the provider configured client to the resource. func (r *clusterResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { var ok bool + r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) if !ok { return } skeClient := skeUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + serviceEnablementClient := serviceenablementUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + r.skeClient = skeClient r.enablementClient = serviceEnablementClient + tflog.Info(ctx, "SKE cluster clients configured") } @@ -685,7 +696,9 @@ func (r *clusterResource) Schema(_ context.Context, _ resource.SchemaRequest, re // The argus extension is deprecated but can still be used until it is removed on 06 January 2026. func (r *clusterResource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) { var resourceModel Model + resp.Diagnostics.Append(req.Config.Get(ctx, &resourceModel)...) + if resp.Diagnostics.HasError() { return } @@ -703,6 +716,7 @@ func validateConfig(ctx context.Context, respDiags *diag.Diagnostics, model *Mod extensions := &extensions{} diags := model.Extensions.As(ctx, extensions, basetypes.ObjectAsOptions{}) respDiags.Append(diags...) + if respDiags.HasError() { return } @@ -713,10 +727,11 @@ func validateConfig(ctx context.Context, respDiags *diag.Diagnostics, model *Mod } // Create creates the resource and sets the initial Terraform state. -func (r *clusterResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *clusterResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -748,12 +763,14 @@ func (r *clusterResource) Create(ctx context.Context, req resource.CreateRequest } r.createOrUpdateCluster(ctx, &resp.Diagnostics, &model, availableKubernetesVersions, availableMachines, nil, nil) + if resp.Diagnostics.HasError() { return } diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -767,6 +784,7 @@ func sortK8sVersions(versions []ske.KubernetesVersion) { if v1 == nil { return false } + if v2 == nil { return true } @@ -778,18 +796,21 @@ func sortK8sVersions(versions []ske.KubernetesVersion) { if !strings.HasPrefix(t1, "v") { t1 = "v" + t1 } + if !strings.HasPrefix(t2, "v") { t2 = "v" + t2 } + return semver.Compare(t1, t2) > 0 }) } // loadAvailableVersions loads the available k8s and machine versions from the API. // The k8s versions are sorted descending order, i.e. the latest versions (including previews) -// are listed first +// are listed first. func (r *clusterResource) loadAvailableVersions(ctx context.Context, region string) ([]ske.KubernetesVersion, []ske.MachineImage, error) { c := r.skeClient + res, err := c.ListProviderOptions(ctx, region).Execute() if err != nil { return nil, nil, fmt.Errorf("calling API: %w", err) @@ -808,7 +829,7 @@ func (r *clusterResource) loadAvailableVersions(ctx context.Context, region stri // getCurrentVersions makes a call to get the details of a cluster and returns the current kubernetes version and a // a map with the machine image for each nodepool, which can be used to check the current machine image versions. -// if the cluster doesn't exist or some error occurs, returns nil for both +// if the cluster doesn't exist or some error occurs, returns nil for both. func getCurrentVersions(ctx context.Context, c skeClient, m *Model) (kubernetesVersion *string, nodePoolMachineImages map[string]*ske.Image) { res, err := c.GetClusterExecute(ctx, m.ProjectId.ValueString(), m.Region.ValueString(), m.Name.ValueString()) if err != nil || res == nil { @@ -824,10 +845,12 @@ func getCurrentVersions(ctx context.Context, c skeClient, m *Model) (kubernetesV } nodePoolMachineImages = map[string]*ske.Image{} + for _, nodePool := range *res.Nodepools { if nodePool.Name == nil || nodePool.Machine == nil || nodePool.Machine.Image == nil || nodePool.Machine.Image.Name == nil { continue } + nodePoolMachineImages[*nodePool.Name] = nodePool.Machine.Image } @@ -839,37 +862,45 @@ func (r *clusterResource) createOrUpdateCluster(ctx context.Context, diags *diag projectId := model.ProjectId.ValueString() name := model.Name.ValueString() region := model.Region.ValueString() + kubernetes, hasDeprecatedVersion, err := toKubernetesPayload(model, availableKubernetesVersions, currentKubernetesVersion, diags) if err != nil { core.LogAndAddError(ctx, diags, "Error creating/updating cluster", fmt.Sprintf("Creating cluster config API payload: %v", err)) return } + if hasDeprecatedVersion { diags.AddWarning("Deprecated Kubernetes version", fmt.Sprintf("Version %s of Kubernetes is deprecated, please update it", *kubernetes.Version)) } + nodePools, deprecatedVersionsUsed, err := toNodepoolsPayload(ctx, model, availableMachineVersions, currentMachineImages) if err != nil { core.LogAndAddError(ctx, diags, "Error creating/updating cluster", fmt.Sprintf("Creating node pools API payload: %v", err)) return } + if len(deprecatedVersionsUsed) != 0 { diags.AddWarning("Deprecated node pools OS versions used", fmt.Sprintf("The following versions of machines are deprecated, please update them: [%s]", strings.Join(deprecatedVersionsUsed, ","))) } + maintenance, err := toMaintenancePayload(ctx, model) if err != nil { core.LogAndAddError(ctx, diags, "Error creating/updating cluster", fmt.Sprintf("Creating maintenance API payload: %v", err)) return } + network, err := toNetworkPayload(ctx, model) if err != nil { core.LogAndAddError(ctx, diags, "Error creating/updating cluster", fmt.Sprintf("Creating network API payload: %v", err)) return } + hibernations, err := toHibernationsPayload(ctx, model) if err != nil { core.LogAndAddError(ctx, diags, "Error creating/updating cluster", fmt.Sprintf("Creating hibernations API payload: %v", err)) return } + extensions, err := toExtensionsPayload(ctx, model) if err != nil { core.LogAndAddError(ctx, diags, "Error creating/updating cluster", fmt.Sprintf("Creating extension API payload: %v", err)) @@ -884,6 +915,7 @@ func (r *clusterResource) createOrUpdateCluster(ctx context.Context, diags *diag Network: network, Nodepools: &nodePools, } + _, err = r.skeClient.CreateOrUpdateCluster(ctx, projectId, region, name).CreateOrUpdateClusterPayload(payload).Execute() if err != nil { core.LogAndAddError(ctx, diags, "Error creating/updating cluster", fmt.Sprintf("Calling API: %v", err)) @@ -895,6 +927,7 @@ func (r *clusterResource) createOrUpdateCluster(ctx context.Context, diags *diag core.LogAndAddError(ctx, diags, "Error creating/updating cluster", fmt.Sprintf("Cluster creation waiting: %v", err)) return } + if waitResp.Status.Error != nil && waitResp.Status.Error.Message != nil && *waitResp.Status.Error.Code == ske.RUNTIMEERRORCODE_OBSERVABILITY_INSTANCE_NOT_FOUND { core.LogAndAddWarning(ctx, diags, "Warning during creating/updating cluster", fmt.Sprintf("Cluster is in Impaired state due to an invalid observability instance id, the cluster is usable but metrics won't be forwarded: %s", *waitResp.Status.Error.Message)) } @@ -908,6 +941,7 @@ func (r *clusterResource) createOrUpdateCluster(ctx context.Context, diags *diag func toNodepoolsPayload(ctx context.Context, m *Model, availableMachineVersions []ske.MachineImage, currentMachineImages map[string]*ske.Image) ([]ske.Nodepool, []string, error) { nodePools := []nodePool{} + diags := m.NodePools.ElementsAs(ctx, &nodePools, false) if diags.HasError() { return nil, nil, core.DiagsToError(diags) @@ -915,6 +949,7 @@ func toNodepoolsPayload(ctx context.Context, m *Model, availableMachineVersions cnps := []ske.Nodepool{} deprecatedVersionsUsed := []string{} + for i := range nodePools { nodePool := nodePools[i] @@ -925,12 +960,14 @@ func toNodepoolsPayload(ctx context.Context, m *Model, availableMachineVersions // taints taintsModel := []taint{} + diags := nodePool.Taints.ElementsAs(ctx, &taintsModel, false) if diags.HasError() { return nil, nil, core.DiagsToError(diags) } ts := []ske.Taint{} + for _, v := range taintsModel { t := ske.Taint{ Effect: ske.TaintGetEffectAttributeType(conversion.StringValueToPointer(v.Effect)), @@ -946,27 +983,33 @@ func toNodepoolsPayload(ctx context.Context, m *Model, availableMachineVersions ls = nil } else { lsm := map[string]string{} + for k, v := range nodePool.Labels.Elements() { nv, err := conversion.ToString(ctx, v) if err != nil { lsm[k] = "" continue } + lsm[k] = nv } + ls = &lsm } // zones zs := []string{} + for _, v := range nodePool.AvailabilityZones.Elements() { if v.IsNull() || v.IsUnknown() { continue } + s, err := conversion.ToString(ctx, v) if err != nil { continue } + zs = append(zs, s) } @@ -996,6 +1039,7 @@ func toNodepoolsPayload(ctx context.Context, m *Model, availableMachineVersions if err != nil { return nil, nil, fmt.Errorf("getting latest matching machine image version: %w", err) } + if hasDeprecatedVersion && machineVersion != nil { deprecatedVersionsUsed = append(deprecatedVersionsUsed, *machineVersion) } @@ -1040,6 +1084,7 @@ func verifySystemComponentsInNodePools(nodePools []ske.Nodepool) error { return nil // A node pool allowing system components was found } } + return fmt.Errorf("at least one node_pool must allow system components") } @@ -1064,6 +1109,7 @@ func latestMatchingMachineVersion(availableImages []ske.MachineImage, versionMin } var availableMachineVersions []ske.MachineImageVersion + for _, machine := range availableImages { if machine.Name != nil && *machine.Name == osName && machine.Versions != nil { availableMachineVersions = *machine.Versions @@ -1083,8 +1129,10 @@ func latestMatchingMachineVersion(availableImages []ske.MachineImage, versionMin if err != nil { return nil, false, fmt.Errorf("get latest supported machine image version: %w", err) } + return latestVersion, false, nil } + versionMin = currentImage.Version } else if currentImage != nil && currentImage.Name != nil && *currentImage.Name == osName { // If the os_version_min is set but is lower than the current version used in the cluster, @@ -1098,7 +1146,9 @@ func latestMatchingMachineVersion(availableImages []ske.MachineImage, versionMin } var fullVersion bool + versionExp := validate.FullVersionRegex + versionRegex := regexp.MustCompile(versionExp) if versionRegex.MatchString(*versionMin) { fullVersion = true @@ -1111,13 +1161,16 @@ func latestMatchingMachineVersion(availableImages []ske.MachineImage, versionMin } var versionUsed *string + var state *string + var availableVersionsArray []string // Get the higher available version that matches the major, minor and patch version provided by the user for _, v := range availableMachineVersions { if v.State == nil || v.Version == nil { continue } + availableVersionsArray = append(availableVersionsArray, *v.Version) vPreffixed := "v" + *v.Version @@ -1126,6 +1179,7 @@ func latestMatchingMachineVersion(availableImages []ske.MachineImage, versionMin if semver.Compare(vPreffixed, providedVersionPrefixed) == 0 { versionUsed = v.Version state = v.State + break } } else { @@ -1153,15 +1207,19 @@ func latestMatchingMachineVersion(availableImages []ske.MachineImage, versionMin func getLatestSupportedMachineVersion(versions []ske.MachineImageVersion) (*string, error) { foundMachineVersion := false + var latestVersion *string + for i := range versions { version := versions[i] if *version.State != VersionStateSupported { continue } + if latestVersion != nil { oldSemVer := fmt.Sprintf("v%s", *latestVersion) newSemVer := fmt.Sprintf("v%s", *version.Version) + if semver.Compare(newSemVer, oldSemVer) != 1 { continue } @@ -1170,14 +1228,17 @@ func getLatestSupportedMachineVersion(versions []ske.MachineImageVersion) (*stri foundMachineVersion = true latestVersion = version.Version } + if !foundMachineVersion { return nil, fmt.Errorf("no supported machine version found") } + return latestVersion, nil } func toHibernationsPayload(ctx context.Context, m *Model) (*ske.Hibernation, error) { hibernation := []hibernation{} + diags := m.Hibernations.ElementsAs(ctx, &hibernation, false) if diags.HasError() { return nil, core.DiagsToError(diags) @@ -1188,15 +1249,18 @@ func toHibernationsPayload(ctx context.Context, m *Model) (*ske.Hibernation, err } scs := []ske.HibernationSchedule{} + for _, h := range hibernation { sc := ske.HibernationSchedule{ Start: conversion.StringValueToPointer(h.Start), End: conversion.StringValueToPointer(h.End), } + if !h.Timezone.IsNull() && !h.Timezone.IsUnknown() { tz := h.Timezone.ValueString() sc.Timezone = &tz } + scs = append(scs, sc) } @@ -1209,26 +1273,33 @@ func toExtensionsPayload(ctx context.Context, m *Model) (*ske.Extension, error) if m.Extensions.IsNull() || m.Extensions.IsUnknown() { return nil, nil } + ex := extensions{} + diags := m.Extensions.As(ctx, &ex, basetypes.ObjectAsOptions{}) if diags.HasError() { return nil, fmt.Errorf("converting extensions object: %v", diags.Errors()) } var skeAcl *ske.ACL - if !(ex.ACL.IsNull() || ex.ACL.IsUnknown()) { + + if !ex.ACL.IsNull() && !ex.ACL.IsUnknown() { acl := acl{} + diags = ex.ACL.As(ctx, &acl, basetypes.ObjectAsOptions{}) if diags.HasError() { return nil, fmt.Errorf("converting extensions.acl object: %v", diags.Errors()) } + aclEnabled := conversion.BoolValueToPointer(acl.Enabled) cidrs := []string{} + diags = 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, @@ -1236,12 +1307,15 @@ func toExtensionsPayload(ctx context.Context, m *Model) (*ske.Extension, error) } var skeObservability *ske.Observability + if !utils.IsUndefined(ex.Observability) { observability := observability{} + diags = ex.Observability.As(ctx, &observability, basetypes.ObjectAsOptions{}) if diags.HasError() { return nil, fmt.Errorf("converting extensions.observability object: %v", diags.Errors()) } + observabilityEnabled := conversion.BoolValueToPointer(observability.Enabled) observabilityInstanceId := conversion.StringValueToPointer(observability.InstanceId) skeObservability = &ske.Observability{ @@ -1250,10 +1324,12 @@ func toExtensionsPayload(ctx context.Context, m *Model) (*ske.Extension, error) } } else if !utils.IsUndefined(ex.Argus) { // Fallback to deprecated argus argus := argus{} + diags = ex.Argus.As(ctx, &argus, basetypes.ObjectAsOptions{}) if diags.HasError() { return nil, fmt.Errorf("converting extensions.argus object: %v", diags.Errors()) } + argusEnabled := conversion.BoolValueToPointer(argus.Enabled) argusInstanceId := conversion.StringValueToPointer(argus.ArgusInstanceId) skeObservability = &ske.Observability{ @@ -1263,19 +1339,24 @@ func toExtensionsPayload(ctx context.Context, m *Model) (*ske.Extension, error) } var skeDNS *ske.DNS - if !(ex.DNS.IsNull() || ex.DNS.IsUnknown()) { + + if !ex.DNS.IsNull() && !ex.DNS.IsUnknown() { dns := dns{} + diags = ex.DNS.As(ctx, &dns, basetypes.ObjectAsOptions{}) if diags.HasError() { return nil, fmt.Errorf("converting extensions.dns object: %v", diags.Errors()) } + dnsEnabled := conversion.BoolValueToPointer(dns.Enabled) zones := []string{} + diags = dns.Zones.ElementsAs(ctx, &zones, true) if diags.HasError() { return nil, fmt.Errorf("converting extensions.dns.zones object: %v", diags.Errors()) } + skeDNS = &ske.DNS{ Enabled: dnsEnabled, Zones: &zones, @@ -1294,6 +1375,7 @@ func parseMaintenanceWindowTime(t string) (time.Time, error) { if err != nil { v, err = time.Parse("15:04:05Z", t) } + return v, err } @@ -1303,26 +1385,31 @@ func toMaintenancePayload(ctx context.Context, m *Model) (*ske.Maintenance, erro } maintenance := maintenance{} + diags := m.Maintenance.As(ctx, &maintenance, basetypes.ObjectAsOptions{}) if diags.HasError() { return nil, fmt.Errorf("converting maintenance object: %v", diags.Errors()) } var timeWindowStart *time.Time - if !(maintenance.Start.IsNull() || maintenance.Start.IsUnknown()) { + + if !maintenance.Start.IsNull() && !maintenance.Start.IsUnknown() { tempTime, err := parseMaintenanceWindowTime(maintenance.Start.ValueString()) if err != nil { return nil, fmt.Errorf("converting maintenance object: %w", err) } + timeWindowStart = sdkUtils.Ptr(tempTime) } var timeWindowEnd *time.Time - if !(maintenance.End.IsNull() || maintenance.End.IsUnknown()) { + + if !maintenance.End.IsNull() && !maintenance.End.IsUnknown() { tempTime, err := parseMaintenanceWindowTime(maintenance.End.ValueString()) if err != nil { return nil, fmt.Errorf("converting maintenance object: %w", err) } + timeWindowEnd = sdkUtils.Ptr(tempTime) } @@ -1344,6 +1431,7 @@ func toNetworkPayload(ctx context.Context, m *Model) (*ske.Network, error) { } network := network{} + diags := m.Network.As(ctx, &network, basetypes.ObjectAsOptions{}) if diags.HasError() { return nil, fmt.Errorf("converting network object: %v", diags.Errors()) @@ -1358,6 +1446,7 @@ func mapFields(ctx context.Context, cl *ske.Cluster, m *Model, region string) er if cl == nil { return fmt.Errorf("response input is nil") } + if m == nil { return fmt.Errorf("model input is nil") } @@ -1370,6 +1459,7 @@ func mapFields(ctx context.Context, cl *ske.Cluster, m *Model, region string) er } else { return fmt.Errorf("name not present") } + m.Name = types.StringValue(name) m.Id = utils.BuildInternalTerraformId(m.ProjectId.ValueString(), region, name) @@ -1380,8 +1470,10 @@ func mapFields(ctx context.Context, cl *ske.Cluster, m *Model, region string) er } m.EgressAddressRanges = types.ListNull(types.StringType) + if cl.Status != nil { var diags diag.Diagnostics + m.EgressAddressRanges, diags = types.ListValueFrom(ctx, types.StringType, cl.Status.EgressAddressRanges) if diags.HasError() { return fmt.Errorf("map egressAddressRanges: %w", core.DiagsToError(diags)) @@ -1389,8 +1481,10 @@ func mapFields(ctx context.Context, cl *ske.Cluster, m *Model, region string) er } m.PodAddressRanges = types.ListNull(types.StringType) + if cl.Status != nil { var diags diag.Diagnostics + m.PodAddressRanges, diags = types.ListValueFrom(ctx, types.StringType, cl.Status.PodAddressRanges) if diags.HasError() { return fmt.Errorf("map podAddressRanges: %w", core.DiagsToError(diags)) @@ -1401,22 +1495,27 @@ func mapFields(ctx context.Context, cl *ske.Cluster, m *Model, region string) er if err != nil { return fmt.Errorf("map node_pools: %w", err) } + err = mapMaintenance(ctx, cl, m) if err != nil { return fmt.Errorf("map maintenance: %w", err) } + err = mapNetwork(cl, m) if err != nil { return fmt.Errorf("map network: %w", err) } + err = mapHibernations(cl, m) if err != nil { return fmt.Errorf("map hibernations: %w", err) } + err = mapExtensions(ctx, cl, m) if err != nil { return fmt.Errorf("map extensions: %w", err) } + return nil } @@ -1446,6 +1545,7 @@ func mapNodePools(ctx context.Context, cl *ske.Cluster, model *Model) error { } nodePools := []attr.Value{} + for i, nodePoolResp := range *cl.Nodepools { nodePool := map[string]attr.Value{ "name": types.StringPointerValue(nodePoolResp.Name), @@ -1482,6 +1582,7 @@ func mapNodePools(ctx context.Context, cl *ske.Cluster, model *Model) error { if i < len(modelNodePools) && !modelNodePools[i].Taints.IsNull() && !modelNodePools[i].Taints.IsUnknown() { taintsInModel = true } + err := mapTaints(nodePoolResp.Taints, nodePool, taintsInModel) if err != nil { return fmt.Errorf("mapping index %d, field taints: %w", i, err) @@ -1492,10 +1593,12 @@ func mapNodePools(ctx context.Context, cl *ske.Cluster, model *Model) error { for k, v := range *nodePoolResp.Labels { elems[k] = types.StringValue(v) } + elemsTF, diags := types.MapValue(types.StringType, elems) if diags.HasError() { return fmt.Errorf("mapping index %d, field labels: %w", i, core.DiagsToError(diags)) } + nodePool["labels"] = elemsTF } @@ -1504,6 +1607,7 @@ func mapNodePools(ctx context.Context, cl *ske.Cluster, model *Model) error { if diags.HasError() { return fmt.Errorf("mapping index %d, field availability_zones: %w", i, core.DiagsToError(diags)) } + nodePool["availability_zones"] = elemsTF } @@ -1511,13 +1615,17 @@ func mapNodePools(ctx context.Context, cl *ske.Cluster, model *Model) error { if diags.HasError() { return fmt.Errorf("mapping index %d: %w", i, core.DiagsToError(diags)) } + nodePools = append(nodePools, nodePoolTF) } + nodePoolsTF, diags := basetypes.NewListValue(types.ObjectType{AttrTypes: nodePoolTypes}, nodePools) if diags.HasError() { return core.DiagsToError(diags) } + model.NodePools = nodePoolsTF + return nil } @@ -1528,10 +1636,14 @@ func mapTaints(t *[]ske.Taint, nodePool map[string]attr.Value, existInModel bool if diags.HasError() { return fmt.Errorf("create empty taints list: %w", core.DiagsToError(diags)) } + nodePool["taints"] = taintsTF + return nil } + nodePool["taints"] = types.ListNull(types.ObjectType{AttrTypes: taintTypes}) + return nil } @@ -1543,10 +1655,12 @@ func mapTaints(t *[]ske.Taint, nodePool map[string]attr.Value, existInModel bool "key": types.StringPointerValue(taintResp.Key), "value": types.StringPointerValue(taintResp.Value), } + taintTF, diags := basetypes.NewObjectValue(taintTypes, taint) if diags.HasError() { return fmt.Errorf("mapping index %d: %w", i, core.DiagsToError(diags)) } + taints = append(taints, taintTF) } @@ -1556,6 +1670,7 @@ func mapTaints(t *[]ske.Taint, nodePool map[string]attr.Value, existInModel bool } nodePool["taints"] = taintsTF + return nil } @@ -1566,10 +1681,14 @@ func mapHibernations(cl *ske.Cluster, m *Model) error { if diags.HasError() { return fmt.Errorf("hibernations is an empty list, converting to terraform empty list: %w", core.DiagsToError(diags)) } + m.Hibernations = emptyHibernations + return nil } + m.Hibernations = basetypes.NewListNull(basetypes.ObjectType{AttrTypes: hibernationTypes}) + return nil } @@ -1578,21 +1697,26 @@ func mapHibernations(cl *ske.Cluster, m *Model) error { if diags.HasError() { return fmt.Errorf("hibernations is an empty list, converting to terraform empty list: %w", core.DiagsToError(diags)) } + m.Hibernations = emptyHibernations + return nil } hibernations := []attr.Value{} + for i, hibernationResp := range *cl.Hibernation.Schedules { hibernation := map[string]attr.Value{ "start": types.StringPointerValue(hibernationResp.Start), "end": types.StringPointerValue(hibernationResp.End), "timezone": types.StringPointerValue(hibernationResp.Timezone), } + hibernationTF, diags := basetypes.NewObjectValue(hibernationTypes, hibernation) if diags.HasError() { return fmt.Errorf("mapping index %d: %w", i, core.DiagsToError(diags)) } + hibernations = append(hibernations, hibernationTF) } @@ -1602,6 +1726,7 @@ func mapHibernations(cl *ske.Cluster, m *Model) error { } m.Hibernations = hibernationsTF + return nil } @@ -1611,29 +1736,36 @@ func mapMaintenance(ctx context.Context, cl *ske.Cluster, m *Model) error { m.Maintenance = types.ObjectNull(maintenanceTypes) return nil } + ekvu := types.BoolNull() if cl.Maintenance.AutoUpdate.KubernetesVersion != nil { ekvu = types.BoolValue(*cl.Maintenance.AutoUpdate.KubernetesVersion) } + emvu := types.BoolNull() if cl.Maintenance.AutoUpdate.KubernetesVersion != nil { emvu = types.BoolValue(*cl.Maintenance.AutoUpdate.MachineImageVersion) } + startTime, endTime, err := getMaintenanceTimes(ctx, cl, m) if err != nil { return fmt.Errorf("getting maintenance times: %w", err) } + maintenanceValues := map[string]attr.Value{ "enable_kubernetes_version_updates": ekvu, "enable_machine_image_version_updates": emvu, "start": types.StringValue(startTime), "end": types.StringValue(endTime), } + maintenanceObject, diags := types.ObjectValue(maintenanceTypes, maintenanceValues) if diags.HasError() { return fmt.Errorf("create maintenance object: %w", core.DiagsToError(diags)) } + m.Maintenance = maintenanceObject + return nil } @@ -1651,6 +1783,7 @@ func mapNetwork(cl *ske.Cluster, m *Model) error { if m.Network.Attributes() == nil { m.Network = types.ObjectNull(networkTypes) } + return nil } @@ -1658,14 +1791,18 @@ func mapNetwork(cl *ske.Cluster, m *Model) error { if cl.Network.Id != nil { id = types.StringValue(*cl.Network.Id) } + networkValues := map[string]attr.Value{ "id": id, } + networkObject, diags := types.ObjectValue(networkTypes, networkValues) if diags.HasError() { return fmt.Errorf("create network object: %w", core.DiagsToError(diags)) } + m.Network = networkObject + return nil } @@ -1678,13 +1815,15 @@ func getMaintenanceTimes(ctx context.Context, cl *ske.Cluster, m *Model) (startT } maintenance := &maintenance{} + diags := m.Maintenance.As(ctx, maintenance, basetypes.ObjectAsOptions{}) if diags.HasError() { return "", "", fmt.Errorf("converting maintenance object %w", core.DiagsToError(diags.Errors())) } startTime = startTimeAPI.Format("15:04:05Z07:00") - if !(maintenance.Start.IsNull() || maintenance.Start.IsUnknown()) { + + if !maintenance.Start.IsNull() && !maintenance.Start.IsUnknown() { startTimeTF, err := time.Parse("15:04:05Z07:00", maintenance.Start.ValueString()) if err != nil { return "", "", fmt.Errorf("parsing start time '%s' from TF config as RFC time: %w", maintenance.Start.ValueString(), err) @@ -1696,7 +1835,8 @@ func getMaintenanceTimes(ctx context.Context, cl *ske.Cluster, m *Model) (startT } endTime = endTimeAPI.Format("15:04:05Z07:00") - if !(maintenance.End.IsNull() || maintenance.End.IsUnknown()) { + + if !maintenance.End.IsNull() && !maintenance.End.IsUnknown() { endTimeTF, err := time.Parse("15:04:05Z07:00", maintenance.End.ValueString()) if err != nil { return "", "", fmt.Errorf("parsing end time '%s' from TF config as RFC time: %w", maintenance.End.ValueString(), err) @@ -1712,6 +1852,7 @@ func getMaintenanceTimes(ctx context.Context, cl *ske.Cluster, m *Model) (startT func checkDisabledExtensions(ctx context.Context, ex *extensions) (aclDisabled, observabilityDisabled, dnsDisabled bool, err error) { var diags diag.Diagnostics + acl := acl{} if ex.ACL.IsNull() { acl.Enabled = types.BoolValue(false) @@ -1727,10 +1868,12 @@ func checkDisabledExtensions(ctx context.Context, ex *extensions) (aclDisabled, observability.Enabled = types.BoolValue(false) } else if !ex.Argus.IsNull() { argus := argus{} + diags = ex.Argus.As(ctx, &argus, basetypes.ObjectAsOptions{}) if diags.HasError() { return false, false, false, fmt.Errorf("converting extensions.argus object: %v", diags.Errors()) } + observability.Enabled = argus.Enabled observability.InstanceId = argus.ArgusInstanceId } else { @@ -1760,6 +1903,7 @@ func mapExtensions(ctx context.Context, cl *ske.Cluster, m *Model) error { } var diags diag.Diagnostics + ex := extensions{} if !m.Extensions.IsNull() { diags := m.Extensions.As(ctx, &ex, basetypes.ObjectAsOptions{}) @@ -1781,20 +1925,20 @@ func mapExtensions(ctx context.Context, cl *ske.Cluster, m *Model) error { if err != nil { return fmt.Errorf("checking if extensions are disabled: %w", err) } - disabledExtensions := false - if aclDisabled && observabilityDisabled && dnsDisabled { - disabledExtensions = true - } + + disabledExtensions := aclDisabled && observabilityDisabled && dnsDisabled emptyExtensions := &ske.Extension{} if *cl.Extensions == *emptyExtensions && (disabledExtensions || m.Extensions.IsNull()) { if m.Extensions.Attributes() == nil { m.Extensions = types.ObjectNull(extensionsTypes) } + return nil } aclExtension := types.ObjectNull(aclTypes) + if cl.Extensions.Acl != nil { enabled := types.BoolNull() if cl.Extensions.Acl.Enabled != nil { @@ -1822,6 +1966,7 @@ func mapExtensions(ctx context.Context, cl *ske.Cluster, m *Model) error { // Deprecated: argus won't be received from backend. Use observabilty instead. argusExtension := types.ObjectNull(argusTypes) observabilityExtension := types.ObjectNull(observabilityTypes) + if cl.Extensions.Observability != nil { enabled := types.BoolNull() if cl.Extensions.Observability.Enabled != nil { @@ -1847,6 +1992,7 @@ func mapExtensions(ctx context.Context, cl *ske.Cluster, m *Model) error { if diags.HasError() { return fmt.Errorf("creating observability extension: %w", core.DiagsToError(diags)) } + argusExtension, diags = types.ObjectValue(argusTypes, argusExtensionValues) if diags.HasError() { return fmt.Errorf("creating argus extension: %w", core.DiagsToError(diags)) @@ -1856,14 +2002,17 @@ func mapExtensions(ctx context.Context, cl *ske.Cluster, m *Model) error { // set deprecated argus extension observability := observability{} + diags = ex.Observability.As(ctx, &observability, basetypes.ObjectAsOptions{}) if diags.HasError() { return fmt.Errorf("converting extensions.observability object: %v", diags.Errors()) } + argusExtensionValues := map[string]attr.Value{ "enabled": observability.Enabled, "argus_instance_id": observability.InstanceId, } + argusExtension, diags = types.ObjectValue(argusTypes, argusExtensionValues) if diags.HasError() { return fmt.Errorf("creating argus extension: %w", core.DiagsToError(diags)) @@ -1871,6 +2020,7 @@ func mapExtensions(ctx context.Context, cl *ske.Cluster, m *Model) error { } dnsExtension := types.ObjectNull(dnsTypes) + if cl.Extensions.Dns != nil { enabled := types.BoolNull() if cl.Extensions.Dns.Enabled != nil { @@ -1918,12 +2068,15 @@ func mapExtensions(ctx context.Context, cl *ske.Cluster, m *Model) error { if diags.HasError() { return fmt.Errorf("creating extensions: %w", core.DiagsToError(diags)) } + m.Extensions = extensions + return nil } func toKubernetesPayload(m *Model, availableVersions []ske.KubernetesVersion, currentKubernetesVersion *string, diags *diag.Diagnostics) (kubernetesPayload *ske.Kubernetes, hasDeprecatedVersion bool, err error) { providedVersionMin := m.KubernetesVersionMin.ValueStringPointer() + versionUsed, hasDeprecatedVersion, err := latestMatchingKubernetesVersion(availableVersions, providedVersionMin, currentKubernetesVersion, diags) if err != nil { return nil, false, fmt.Errorf("getting latest matching kubernetes version: %w", err) @@ -1932,6 +2085,7 @@ func toKubernetesPayload(m *Model, availableVersions []ske.KubernetesVersion, cu k := &ske.Kubernetes{ Version: versionUsed, } + return k, hasDeprecatedVersion, nil } @@ -1946,8 +2100,10 @@ func latestMatchingKubernetesVersion(availableVersions []ske.KubernetesVersion, if err != nil { return nil, false, fmt.Errorf("get latest supported kubernetes version: %w", err) } + return latestVersion, false, nil } + kubernetesVersionMin = currentKubernetesVersion } else if currentKubernetesVersion != nil { // For an already existing cluster, if kubernetes_version_min is set to a lower version than what is being used in the cluster @@ -1975,6 +2131,7 @@ func latestMatchingKubernetesVersion(availableVersions []ske.KubernetesVersion, selectedVersion *ske.KubernetesVersion availableVersionsArray []string ) + if fullVersion { availableVersionsArray, selectedVersion = selectFullVersion(availableVersions, providedVersionPrefixed) } else { @@ -2000,6 +2157,7 @@ func selectFullVersion(availableVersions []ske.KubernetesVersion, kubernetesVers if versionCandidate.State == nil || versionCandidate.Version == nil { continue } + availableVersionsArray = append(availableVersionsArray, *versionCandidate.Version) vPrefixed := "v" + *versionCandidate.Version @@ -2009,15 +2167,18 @@ func selectFullVersion(availableVersions []ske.KubernetesVersion, kubernetesVers break } } + return availableVersionsArray, selectedVersion } func selectMatchingVersion(availableVersions []ske.KubernetesVersion, kubernetesVersionMin string) (availableVersionsArray []string, selectedVersion *ske.KubernetesVersion) { sortK8sVersions(availableVersions) + for _, candidateVersion := range availableVersions { if candidateVersion.State == nil || candidateVersion.Version == nil { continue } + availableVersionsArray = append(availableVersionsArray, *candidateVersion.Version) vPreffixed := "v" + *candidateVersion.Version @@ -2033,6 +2194,7 @@ func selectMatchingVersion(availableVersions []ske.KubernetesVersion, kubernetes // all other cases are ignored } } + return availableVersionsArray, selectedVersion } @@ -2074,15 +2236,19 @@ func isSupported(v *ske.KubernetesVersion) bool { func getLatestSupportedKubernetesVersion(versions []ske.KubernetesVersion) (*string, error) { foundKubernetesVersion := false + var latestVersion *string + for i := range versions { version := versions[i] if *version.State != VersionStateSupported { continue } + if latestVersion != nil { oldSemVer := fmt.Sprintf("v%s", *latestVersion) newSemVer := fmt.Sprintf("v%s", *version.Version) + if semver.Compare(newSemVer, oldSemVer) != 1 { continue } @@ -2091,19 +2257,23 @@ func getLatestSupportedKubernetesVersion(versions []ske.KubernetesVersion) (*str foundKubernetesVersion = true latestVersion = version.Version } + if !foundKubernetesVersion { return nil, fmt.Errorf("no supported Kubernetes version found") } + return latestVersion, nil } -func (r *clusterResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (r *clusterResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var state Model diags := req.State.Get(ctx, &state) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := state.ProjectId.ValueString() name := state.Name.ValueString() region := r.providerData.GetRegionWithOverride(state.Region) @@ -2118,7 +2288,9 @@ func (r *clusterResource) Read(ctx context.Context, req resource.ReadRequest, re resp.State.RemoveResource(ctx) return } + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading cluster", fmt.Sprintf("Calling API: %v", err)) + return } @@ -2130,16 +2302,19 @@ func (r *clusterResource) Read(ctx context.Context, req resource.ReadRequest, re diags = resp.State.Set(ctx, state) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "SKE cluster read") } -func (r *clusterResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *clusterResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -2160,24 +2335,30 @@ func (r *clusterResource) Update(ctx context.Context, req resource.UpdateRequest currentKubernetesVersion, currentMachineImages := getCurrentVersions(ctx, r.skeClient, &model) r.createOrUpdateCluster(ctx, &resp.Diagnostics, &model, availableKubernetesVersions, availableMachines, currentKubernetesVersion, currentMachineImages) + if resp.Diagnostics.HasError() { return } diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "SKE cluster updated") } -func (r *clusterResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform +func (r *clusterResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { //nolint:gocritic // function signature required by Terraform var model Model + resp.Diagnostics.Append(req.State.Get(ctx, &model)...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() name := model.Name.ValueString() region := model.Region.ValueString() @@ -2186,21 +2367,24 @@ func (r *clusterResource) Delete(ctx context.Context, req resource.DeleteRequest ctx = tflog.SetField(ctx, "region", region) c := r.skeClient + _, err := c.DeleteCluster(ctx, projectId, region, name).Execute() if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting cluster", fmt.Sprintf("Calling API: %v", err)) return } + _, err = skeWait.DeleteClusterWaitHandler(ctx, r.skeClient, projectId, region, name).WaitWithContext(ctx) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting cluster", fmt.Sprintf("Cluster deletion waiting: %v", err)) return } + tflog.Info(ctx, "SKE cluster deleted") } // ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: project_id,name +// The expected format of the resource import identifier is: project_id,name. func (r *clusterResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { idParts := strings.Split(req.ID, core.Separator) @@ -2209,6 +2393,7 @@ func (r *clusterResource) ImportState(ctx context.Context, req resource.ImportSt "Error importing cluster", fmt.Sprintf("Expected import identifier with format: [project_id],[region],[name] Got: %q", req.ID), ) + return } diff --git a/stackit/internal/services/ske/cluster/resource_test.go b/stackit/internal/services/ske/cluster/resource_test.go index e29c15cc8..dc647fd2a 100644 --- a/stackit/internal/services/ske/cluster/resource_test.go +++ b/stackit/internal/services/ske/cluster/resource_test.go @@ -35,6 +35,7 @@ func (c *skeClientMocked) GetClusterExecute(_ context.Context, _, _, _ string) ( func TestMapFields(t *testing.T) { cs := ske.ClusterStatusState("OK") + tests := []struct { description string stateExtensions types.Object @@ -711,13 +712,16 @@ func TestMapFields(t *testing.T) { Extensions: tt.stateExtensions, NodePools: tt.stateNodePools, } + err := mapFields(context.Background(), tt.input, state, tt.region) if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(state, &tt.expected) if diff != "" { @@ -1208,21 +1212,26 @@ func TestLatestMatchingKubernetesVersion(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { var diags diag.Diagnostics + versionUsed, hasDeprecatedVersion, err := latestMatchingKubernetesVersion(tt.availableVersions, tt.kubernetesVersionMin, tt.currentKubernetesVersion, &diags) if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { if *versionUsed != *tt.expectedVersionUsed { t.Fatalf("Used version does not match: expecting %s, got %s", *tt.expectedVersionUsed, *versionUsed) } + if tt.expectedHasDeprecatedVersion != hasDeprecatedVersion { t.Fatalf("hasDeprecatedVersion flag is wrong: expecting %t, got %t", tt.expectedHasDeprecatedVersion, hasDeprecatedVersion) } } + if hasWarnings := len(diags.Warnings()) > 0; tt.expectedWarning != hasWarnings { t.Fatalf("Emitted warnings do not match. Expected %t but got %t", tt.expectedWarning, hasWarnings) } @@ -1725,13 +1734,16 @@ func TestLatestMatchingMachineVersion(t *testing.T) { if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { if *versionUsed != *tt.expectedVersionUsed { t.Fatalf("Used version does not match: expecting %s, got %s", *tt.expectedVersionUsed, *versionUsed) } + if tt.expectedHasDeprecatedVersion != hasDeprecatedVersion { t.Fatalf("hasDeprecatedVersion flag is wrong: expecting %t, got %t", tt.expectedHasDeprecatedVersion, hasDeprecatedVersion) } @@ -1855,28 +1867,33 @@ func TestGetMaintenanceTimes(t *testing.T) { "start": types.StringPointerValue(tt.startTF), "end": types.StringPointerValue(tt.endTF), } + maintenanceObject, diags := types.ObjectValue(maintenanceTypes, maintenanceValues) if diags.HasError() { t.Fatalf("failed to create flavor: %v", core.DiagsToError(diags)) } + tfState := &Model{ Maintenance: maintenanceObject, } start, end, err := getMaintenanceTimes(context.Background(), apiResponse, tfState) - if err != nil { if tt.isValid { t.Errorf("getMaintenanceTimes failed on valid input: %v", err) } + return } + if !tt.isValid { t.Fatalf("getMaintenanceTimes didn't fail on invalid input") } + if tt.startExpected != start { t.Errorf("expected start '%s', got '%s'", tt.startExpected, start) } + if tt.endExpected != end { t.Errorf("expected end '%s', got '%s'", tt.endExpected, end) } @@ -2074,6 +2091,7 @@ func TestGetCurrentVersion(t *testing.T) { Name: types.StringValue("name"), } kubernetesVersion, machineImageVersions := getCurrentVersions(context.Background(), client, model) + diff := cmp.Diff(kubernetesVersion, tt.expectedKubernetesVersion) if diff != "" { t.Errorf("Kubernetes version does not match: %s", diff) @@ -2142,12 +2160,15 @@ func TestGetLatestSupportedKubernetesVersion(t *testing.T) { if tt.isValid && err != nil { t.Errorf("failed on valid input") } + if !tt.isValid && err == nil { t.Errorf("did not fail on invalid input") } + if !tt.isValid { return } + diff := cmp.Diff(version, tt.expectedVersion) if diff != "" { t.Fatalf("Output is not as expected: %s", diff) @@ -2211,12 +2232,15 @@ func TestGetLatestSupportedMachineVersion(t *testing.T) { if tt.isValid && err != nil { t.Errorf("failed on valid input") } + if !tt.isValid && err == nil { t.Errorf("did not fail on invalid input") } + if !tt.isValid { return } + diff := cmp.Diff(version, tt.expectedVersion) if diff != "" { t.Fatalf("Output is not as expected: %s", diff) @@ -2275,6 +2299,7 @@ func TestToNetworkPayload(t *testing.T) { if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid { diff := cmp.Diff(payload, tt.expected) if diff != "" { @@ -2371,9 +2396,11 @@ func TestMaintenanceWindow(t *testing.T) { if diags.HasError() { t.Fatalf("cannot create object value: %v", diags) } + model := Model{ Maintenance: val, } + maintenance, err := toMaintenancePayload(context.Background(), &model) if err != nil { t.Fatalf("cannot create payload: %v", err) @@ -2381,10 +2408,12 @@ func TestMaintenanceWindow(t *testing.T) { startLocation := maintenance.TimeWindow.Start.Location() endLocation := maintenance.TimeWindow.End.Location() + wantStart, err := time.ParseInLocation(time.TimeOnly, tt.wantStart, startLocation) if err != nil { t.Fatalf("cannot parse start date %q: %v", tt.wantStart, err) } + wantEnd, err := time.ParseInLocation(time.TimeOnly, tt.wantEnd, endLocation) if err != nil { t.Fatalf("cannot parse end date %q: %v", tt.wantEnd, err) @@ -2393,6 +2422,7 @@ func TestMaintenanceWindow(t *testing.T) { if expected, actual := wantStart.In(startLocation), *maintenance.TimeWindow.Start; expected != actual { t.Errorf("invalid start date. expected %s but got %s", expected, actual) } + if expected, actual := wantEnd.In(endLocation), (*maintenance.TimeWindow.End); expected != actual { t.Errorf("invalid End date. expected %s but got %s", expected, actual) } @@ -2469,16 +2499,19 @@ func TestSortK8sVersion(t *testing.T) { joinK8sVersions := func(in []ske.KubernetesVersion, sep string) string { var builder strings.Builder + for i, l := 0, len(in); i < l; i++ { if i > 0 { builder.WriteString(sep) } + if v := in[i].Version; v != nil { builder.WriteString(*v) } else { builder.WriteString("undef") } } + return builder.String() } @@ -2631,6 +2664,7 @@ func TestValidateConfig(t *testing.T) { diags := diag.Diagnostics{} validateConfig(ctx, &diags, tt.model) + if diags.HasError() != tt.wantErr { t.Errorf("validateConfig() = %v, want %v", diags.HasError(), tt.wantErr) } diff --git a/stackit/internal/services/ske/kubeconfig/resource.go b/stackit/internal/services/ske/kubeconfig/resource.go index c4637b76d..cf2637182 100644 --- a/stackit/internal/services/ske/kubeconfig/resource.go +++ b/stackit/internal/services/ske/kubeconfig/resource.go @@ -7,18 +7,9 @@ import ( "strconv" "time" - skeUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/ske/utils" - "github.com/google/uuid" "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" "github.com/hashicorp/terraform-plugin-framework/path" - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier" @@ -26,9 +17,16 @@ import ( "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" sdkUtils "github.com/stackitcloud/stackit-sdk-go/core/utils" "github.com/stackitcloud/stackit-sdk-go/services/ske" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + skeUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/ske/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" ) // Ensure the implementation satisfies the expected interfaces. @@ -71,16 +69,20 @@ func (r *kubeconfigResource) Metadata(_ context.Context, req resource.MetadataRe // Configure adds the provider configured client to the resource. func (r *kubeconfigResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { var ok bool + r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) if !ok { return } apiClient := skeUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + r.client = apiClient + tflog.Info(ctx, "SKE kubeconfig client configured") } @@ -204,47 +206,56 @@ func (r *kubeconfigResource) Schema(_ context.Context, _ resource.SchemaRequest, } // ModifyPlan will be called in the Plan phase and will check if the plan is a creation of the resource -// If so, show warning related to deprecated credentials endpoints -func (r *kubeconfigResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform +// If so, show warning related to deprecated credentials endpoints. +func (r *kubeconfigResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { //nolint:gocritic // function signature required by Terraform if req.State.Raw.IsNull() { // Planned to create a kubeconfig core.LogAndAddWarning(ctx, &resp.Diagnostics, "Planned to create kubeconfig", "Once this resource is created, you will no longer be able to use the deprecated credentials endpoints and the kube_config field on the cluster resource will be empty for this cluster. For more info check How to Rotate SKE Credentials (https://docs.stackit.cloud/stackit/en/how-to-rotate-ske-credentials-200016334.html)") } + var configModel Model // skip initial empty configuration to avoid follow-up errors if req.Config.Raw.IsNull() { return } + resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...) + if resp.Diagnostics.HasError() { return } var planModel Model + resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) + if resp.Diagnostics.HasError() { return } utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp) + if resp.Diagnostics.HasError() { return } resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...) + if resp.Diagnostics.HasError() { return } } // Create creates the resource and sets the initial Terraform state. -func (r *kubeconfigResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *kubeconfigResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() clusterName := model.ClusterName.ValueString() kubeconfigUUID := uuid.New().String() @@ -265,9 +276,11 @@ func (r *kubeconfigResource) Create(ctx context.Context, req resource.CreateRequ // Set state to fully populated data diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "SKE kubeconfig created") } @@ -276,11 +289,12 @@ func (r *kubeconfigResource) Create(ctx context.Context, req resource.CreateRequ // If the refresh field is set, Read will check the expiration date and will get a new valid kubeconfig if it has expired // If kubeconfig creation time is before lastCompletionTime of the credentials rotation or // before cluster creation time a new kubeconfig is created. -func (r *kubeconfigResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (r *kubeconfigResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { //nolint:gocritic // function signature required by Terraform // Retrieve values from plan var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -294,9 +308,11 @@ func (r *kubeconfigResource) Read(ctx context.Context, req resource.ReadRequest, // Prevent recreation of kubeconfig when updating to v2 api version diags = resp.State.SetAttribute(ctx, path.Root("region"), region) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + ctx = tflog.SetField(ctx, "project_id", projectId) ctx = tflog.SetField(ctx, "cluster_name", clusterName) ctx = tflog.SetField(ctx, "kube_config_id", kubeconfigUUID) @@ -315,6 +331,7 @@ func (r *kubeconfigResource) Read(ctx context.Context, req resource.ReadRequest, }, ) resp.State.RemoveResource(ctx) + return } @@ -346,6 +363,7 @@ func (r *kubeconfigResource) Read(ctx context.Context, req resource.ReadRequest, // Set state to fully populated data diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -371,22 +389,24 @@ func (r *kubeconfigResource) createKubeconfig(ctx context.Context, model *Model) if err != nil { return fmt.Errorf("processing API payload: %w", err) } + return nil } -func (r *kubeconfigResource) Update(ctx context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *kubeconfigResource) Update(ctx context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { //nolint:gocritic // function signature required by Terraform // Update shouldn't be called core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating kubeconfig", "Kubeconfig can't be updated") } // Delete deletes the resource and removes the Terraform state on success. -func (r *kubeconfigResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform +func (r *kubeconfigResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { //nolint:gocritic // function signature required by Terraform core.LogAndAddWarning(ctx, &resp.Diagnostics, "Deleting kubeconfig", "Deleted this resource will only remove the values from the terraform state, it will not trigger a deletion or revoke of the actual kubeconfig as this is not supported by the SKE API. The kubeconfig will still be valid until it expires.") // Retrieve values from plan var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -408,6 +428,7 @@ func mapFields(kubeconfigResp *ske.Kubeconfig, model *Model, creationTime time.T if kubeconfigResp == nil { return fmt.Errorf("response is nil") } + if model == nil { return fmt.Errorf("model input is nil") } @@ -425,6 +446,7 @@ func mapFields(kubeconfigResp *ske.Kubeconfig, model *Model, creationTime time.T // set creation time model.CreationTime = types.StringValue(creationTime.Format(time.RFC3339)) model.Region = types.StringValue(region) + return nil } @@ -434,7 +456,9 @@ func toCreatePayload(model *Model) (*ske.CreateKubeconfigPayload, error) { } expiration := conversion.Int64ValueToPointer(model.Expiration) + var expirationStringPtr *string + if expiration != nil { expirationStringPtr = sdkUtils.Ptr(strconv.FormatInt(*expiration, 10)) } @@ -444,59 +468,69 @@ func toCreatePayload(model *Model) (*ske.CreateKubeconfigPayload, error) { }, nil } -// helper function to check if kubecondig has expired +// helper function to check if kubecondig has expired. func checkHasExpired(model *Model, currentTime time.Time) (bool, error) { expiresAt := model.ExpiresAt if model.Refresh.ValueBool() && !expiresAt.IsNull() { if expiresAt.IsUnknown() { return true, nil } + expiresAt, err := time.Parse(time.RFC3339, expiresAt.ValueString()) if err != nil { return false, fmt.Errorf("converting expiresAt field to timestamp: %w", err) } + if !model.RefreshBefore.IsNull() { expiresAt = expiresAt.Add(-time.Duration(model.RefreshBefore.ValueInt64()) * time.Second) } + if expiresAt.Before(currentTime) { return true, nil } } + return false, nil } -// helper function to check if a credentials rotation was done +// helper function to check if a credentials rotation was done. func checkCredentialsRotation(cluster *ske.Cluster, model *Model) (bool, error) { creationTimeValue := model.CreationTime if creationTimeValue.IsNull() || creationTimeValue.IsUnknown() { return false, nil } + creationTime, err := time.Parse(time.RFC3339, creationTimeValue.ValueString()) if err != nil { return false, fmt.Errorf("converting creationTime field to timestamp: %w", err) } + if cluster.Status.CredentialsRotation.LastCompletionTime != nil { if creationTime.Before(*cluster.Status.CredentialsRotation.LastCompletionTime) { return true, nil } } + return false, nil } -// helper function to check if a cluster recreation was done +// helper function to check if a cluster recreation was done. func checkClusterRecreation(cluster *ske.Cluster, model *Model) (bool, error) { creationTimeValue := model.CreationTime if creationTimeValue.IsNull() || creationTimeValue.IsUnknown() { return false, nil } + creationTime, err := time.Parse(time.RFC3339, creationTimeValue.ValueString()) if err != nil { return false, fmt.Errorf("converting creationTime field to timestamp: %w", err) } + if cluster.Status.CreationTime != nil { if creationTime.Before(*cluster.Status.CreationTime) { return true, nil } } + return false, nil } diff --git a/stackit/internal/services/ske/kubeconfig/resource_test.go b/stackit/internal/services/ske/kubeconfig/resource_test.go index f2a7fac8a..c7e53fcc9 100644 --- a/stackit/internal/services/ske/kubeconfig/resource_test.go +++ b/stackit/internal/services/ske/kubeconfig/resource_test.go @@ -13,6 +13,7 @@ import ( func TestMapFields(t *testing.T) { const testRegion = "eu01" + tests := []struct { description string input *ske.Kubeconfig @@ -66,13 +67,16 @@ func TestMapFields(t *testing.T) { ClusterName: tt.expected.ClusterName, } creationTime, _ := time.Parse(time.RFC3339, tt.expected.CreationTime.ValueString()) + err := mapFields(tt.input, state, creationTime, testRegion) if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(state, &tt.expected, cmpopts.IgnoreFields(Model{}, "Id")) // Id includes a random uuid if diff != "" { @@ -119,9 +123,11 @@ func TestToCreatePayload(t *testing.T) { if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(output, tt.expected) if diff != "" { @@ -199,6 +205,7 @@ func TestCheckHasExpired(t *testing.T) { t.Errorf("checkHasExpired() error = %v, expectedError %v", err, tt.expectedError) return } + if got != tt.expected { t.Errorf("checkHasExpired() = %v, expected %v", got, tt.expected) } @@ -267,6 +274,7 @@ func TestCheckCredentialsRotation(t *testing.T) { t.Errorf("checkCredentialsRotation() error = %v, expectedError %v", err, tt.expectedError) return } + if got != tt.expected { t.Errorf("checkCredentialsRotation() = %v, expected %v", got, tt.expected) } @@ -329,6 +337,7 @@ func TestCheckClusterRecreation(t *testing.T) { t.Errorf("checkClusterRecreation() error = %v, expectedError %v", err, tt.expectedError) return } + if got != tt.expected { t.Errorf("checkClusterRecreation() = %v, expected %v", got, tt.expected) } diff --git a/stackit/internal/services/ske/ske_acc_test.go b/stackit/internal/services/ske/ske_acc_test.go index b5a2d1796..87cc0bdf8 100644 --- a/stackit/internal/services/ske/ske_acc_test.go +++ b/stackit/internal/services/ske/ske_acc_test.go @@ -112,7 +112,6 @@ func TestAccSKEMin(t *testing.T) { ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, CheckDestroy: testAccCheckSKEDestroy, Steps: []resource.TestStep{ - // 1) Creation { Config: testutil.SKEProviderConfig() + "\n" + resourceMin, @@ -194,6 +193,7 @@ func TestAccSKEMin(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute name") } + return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, testutil.Region, name), nil }, ImportState: true, @@ -234,7 +234,6 @@ func TestAccSKEMax(t *testing.T) { ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, CheckDestroy: testAccCheckSKEDestroy, Steps: []resource.TestStep{ - // 1) Creation { Config: testutil.SKEProviderConfig() + "\n" + resourceMax, @@ -385,6 +384,7 @@ func TestAccSKEMax(t *testing.T) { if !ok { return "", fmt.Errorf("couldn't find attribute name") } + return fmt.Sprintf("%s,%s,%s", testutil.ProjectId, testutil.Region, name), nil }, ImportState: true, @@ -457,7 +457,9 @@ func TestAccSKEMax(t *testing.T) { func testAccCheckSKEDestroy(s *terraform.State) error { ctx := context.Background() + var client *ske.APIClient + var err error if testutil.SKECustomEndpoint == "" { client, err = ske.NewAPIClient() @@ -466,11 +468,13 @@ func testAccCheckSKEDestroy(s *terraform.State) error { coreConfig.WithEndpoint(testutil.SKECustomEndpoint), ) } + if err != nil { return fmt.Errorf("creating client: %w", err) } clustersToDestroy := []string{} + for _, rs := range s.RootModule().Resources { if rs.Type != "stackit_ske_cluster" { continue @@ -490,17 +494,20 @@ func testAccCheckSKEDestroy(s *terraform.State) error { if items[i].Name == nil { continue } + if utils.Contains(clustersToDestroy, *items[i].Name) { _, err := client.DeleteClusterExecute(ctx, testutil.ProjectId, testutil.Region, *items[i].Name) if err != nil { return fmt.Errorf("destroying cluster %s during CheckDestroy: %w", *items[i].Name, err) } + _, err = wait.DeleteClusterWaitHandler(ctx, client, testutil.ProjectId, testutil.Region, *items[i].Name).WaitWithContext(ctx) if err != nil { return fmt.Errorf("destroying cluster %s during CheckDestroy: waiting for deletion %w", *items[i].Name, err) } } } + return nil } @@ -522,6 +529,7 @@ func NewSkeProviderOptions(nodePoolOs string) *SkeProviderOptions { ctx := context.Background() var client *ske.APIClient + var err error if testutil.SKECustomEndpoint == "" { @@ -559,11 +567,13 @@ func (s *SkeProviderOptions) getMachineVersionAt(position int) string { for _, mi := range *s.options.MachineImages { if mi.Name != nil && *mi.Name == s.nodePoolOsName && mi.Versions != nil { count := 0 + for _, v := range *mi.Versions { if v.State != nil && v.Version != nil { if count == position { return *v.Version } + count++ } } @@ -585,11 +595,13 @@ func (s *SkeProviderOptions) getK8sVersionAt(position int) string { } count := 0 + for _, v := range *s.options.KubernetesVersions { if v.State != nil && *v.State == "supported" && v.Version != nil { if count == position { return *v.Version } + count++ } } diff --git a/stackit/internal/services/ske/utils/util.go b/stackit/internal/services/ske/utils/util.go index 91e89b175..157ccc68e 100644 --- a/stackit/internal/services/ske/utils/util.go +++ b/stackit/internal/services/ske/utils/util.go @@ -19,6 +19,7 @@ func ConfigureClient(ctx context.Context, providerData *core.ProviderData, diags if providerData.SKECustomEndpoint != "" { apiClientConfigOptions = append(apiClientConfigOptions, config.WithEndpoint(providerData.SKECustomEndpoint)) } + apiClient, err := ske.NewAPIClient(apiClientConfigOptions...) if err != nil { core.LogAndAddError(ctx, diags, "Error configuring API client", fmt.Sprintf("Configuring client: %v. This is an error related to the provider configuration, not to the resource configuration", err)) diff --git a/stackit/internal/services/ske/utils/util_test.go b/stackit/internal/services/ske/utils/util_test.go index 501406aac..5f7934b00 100644 --- a/stackit/internal/services/ske/utils/util_test.go +++ b/stackit/internal/services/ske/utils/util_test.go @@ -22,6 +22,7 @@ const ( func TestConfigureClient(t *testing.T) { /* mock authentication by setting service account token env variable */ os.Clearenv() + err := os.Setenv(sdkClients.ServiceAccountToken, "mock-val") if err != nil { t.Errorf("error setting env variable: %v", err) @@ -30,6 +31,7 @@ func TestConfigureClient(t *testing.T) { type args struct { providerData *core.ProviderData } + tests := []struct { name string args args @@ -50,6 +52,7 @@ func TestConfigureClient(t *testing.T) { if err != nil { t.Errorf("error configuring client: %v", err) } + return apiClient }(), wantErr: false, @@ -70,6 +73,7 @@ func TestConfigureClient(t *testing.T) { if err != nil { t.Errorf("error configuring client: %v", err) } + return apiClient }(), wantErr: false, @@ -81,6 +85,7 @@ func TestConfigureClient(t *testing.T) { diags := diag.Diagnostics{} actual := ConfigureClient(ctx, tt.args.providerData, &diags) + if diags.HasError() != tt.wantErr { t.Errorf("ConfigureClient() error = %v, want %v", diags.HasError(), tt.wantErr) } diff --git a/stackit/internal/services/sqlserverflex/instance/datasource.go b/stackit/internal/services/sqlserverflex/instance/datasource.go index f88c0800d..7c2780c4b 100644 --- a/stackit/internal/services/sqlserverflex/instance/datasource.go +++ b/stackit/internal/services/sqlserverflex/instance/datasource.go @@ -5,20 +5,18 @@ import ( "fmt" "net/http" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - sqlserverflexUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/sqlserverflex/utils" - "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "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/services/sqlserverflex" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + sqlserverflexUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/sqlserverflex/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - - "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex" ) // Ensure the implementation satisfies the expected interfaces. @@ -45,16 +43,20 @@ func (r *instanceDataSource) Metadata(_ context.Context, req datasource.Metadata // Configure adds the provider configured client to the data source. func (r *instanceDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { var ok bool + r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) if !ok { return } apiClient := sqlserverflexUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + r.client = apiClient + tflog.Info(ctx, "SQLServer Flex instance client configured") } @@ -164,10 +166,11 @@ func (r *instanceDataSource) Schema(_ context.Context, _ datasource.SchemaReques } // Read refreshes the Terraform state with the latest data. -func (r *instanceDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (r *instanceDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.Config.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -191,29 +194,35 @@ func (r *instanceDataSource) Read(ctx context.Context, req datasource.ReadReques }, ) resp.State.RemoveResource(ctx) + return } - var flavor = &flavorModel{} - if !(model.Flavor.IsNull() || model.Flavor.IsUnknown()) { + flavor := &flavorModel{} + if !model.Flavor.IsNull() && !model.Flavor.IsUnknown() { diags = model.Flavor.As(ctx, flavor, basetypes.ObjectAsOptions{}) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } } - var storage = &storageModel{} - if !(model.Storage.IsNull() || model.Storage.IsUnknown()) { + + storage := &storageModel{} + if !model.Storage.IsNull() && !model.Storage.IsUnknown() { diags = model.Storage.As(ctx, storage, basetypes.ObjectAsOptions{}) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } } - var options = &optionsModel{} - if !(model.Options.IsNull() || model.Options.IsUnknown()) { + + options := &optionsModel{} + if !model.Options.IsNull() && !model.Options.IsUnknown() { diags = model.Options.As(ctx, options, basetypes.ObjectAsOptions{}) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -227,8 +236,10 @@ func (r *instanceDataSource) Read(ctx context.Context, req datasource.ReadReques // Set refreshed state diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "SQLServer Flex instance read") } diff --git a/stackit/internal/services/sqlserverflex/instance/resource.go b/stackit/internal/services/sqlserverflex/instance/resource.go index 9bc2069c0..7adb21bc7 100644 --- a/stackit/internal/services/sqlserverflex/instance/resource.go +++ b/stackit/internal/services/sqlserverflex/instance/resource.go @@ -9,20 +9,10 @@ import ( "strings" "time" - sqlserverflexUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/sqlserverflex/utils" - "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "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/schema/validator" - "github.com/hashicorp/terraform-plugin-framework/types/basetypes" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier" @@ -30,11 +20,19 @@ import ( "github.com/hashicorp/terraform-plugin-framework/resource/schema/objectplanmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" "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" coreUtils "github.com/stackitcloud/stackit-sdk-go/core/utils" "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex" "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex/wait" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + sqlserverflexUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/sqlserverflex/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" ) // Ensure the implementation satisfies the expected interfaces. @@ -60,7 +58,7 @@ type Model struct { Region types.String `tfsdk:"region"` } -// Struct corresponding to Model.Flavor +// Struct corresponding to Model.Flavor. type flavorModel struct { Id types.String `tfsdk:"id"` Description types.String `tfsdk:"description"` @@ -68,7 +66,7 @@ type flavorModel struct { RAM types.Int64 `tfsdk:"ram"` } -// Types corresponding to flavorModel +// Types corresponding to flavorModel. var flavorTypes = map[string]attr.Type{ "id": basetypes.StringType{}, "description": basetypes.StringType{}, @@ -76,25 +74,25 @@ var flavorTypes = map[string]attr.Type{ "ram": basetypes.Int64Type{}, } -// Struct corresponding to Model.Storage +// Struct corresponding to Model.Storage. type storageModel struct { Class types.String `tfsdk:"class"` Size types.Int64 `tfsdk:"size"` } -// Types corresponding to storageModel +// Types corresponding to storageModel. var storageTypes = map[string]attr.Type{ "class": basetypes.StringType{}, "size": basetypes.Int64Type{}, } -// Struct corresponding to Model.Options +// Struct corresponding to Model.Options. type optionsModel struct { Edition types.String `tfsdk:"edition"` RetentionDays types.Int64 `tfsdk:"retention_days"` } -// Types corresponding to optionsModel +// Types corresponding to optionsModel. var optionsTypes = map[string]attr.Type{ "edition": basetypes.StringType{}, "retention_days": basetypes.Int64Type{}, @@ -119,44 +117,54 @@ func (r *instanceResource) Metadata(_ context.Context, req resource.MetadataRequ // Configure adds the provider configured client to the resource. func (r *instanceResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { var ok bool + r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) if !ok { return } apiClient := sqlserverflexUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + r.client = apiClient + tflog.Info(ctx, "SQLServer Flex instance client configured") } // ModifyPlan implements resource.ResourceWithModifyPlan. // Use the modifier to set the effective region in the current plan. -func (r *instanceResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform +func (r *instanceResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { //nolint:gocritic // function signature required by Terraform var configModel Model // skip initial empty configuration to avoid follow-up errors if req.Config.Raw.IsNull() { return } + resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...) + if resp.Diagnostics.HasError() { return } var planModel Model + resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) + if resp.Diagnostics.HasError() { return } utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp) + if resp.Diagnostics.HasError() { return } resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...) + if resp.Diagnostics.HasError() { return } @@ -337,53 +345,62 @@ func (r *instanceResource) Schema(_ context.Context, _ resource.SchemaRequest, r } // Create creates the resource and sets the initial Terraform state. -func (r *instanceResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *instanceResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { //nolint:gocritic // function signature required by Terraform // Retrieve values from plan var model Model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() region := model.Region.ValueString() ctx = tflog.SetField(ctx, "project_id", projectId) ctx = tflog.SetField(ctx, "region", region) var acl []string - if !(model.ACL.IsNull() || model.ACL.IsUnknown()) { + if !model.ACL.IsNull() && !model.ACL.IsUnknown() { diags = model.ACL.ElementsAs(ctx, &acl, false) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } } - var flavor = &flavorModel{} - if !(model.Flavor.IsNull() || model.Flavor.IsUnknown()) { + + flavor := &flavorModel{} + if !model.Flavor.IsNull() && !model.Flavor.IsUnknown() { diags = model.Flavor.As(ctx, flavor, basetypes.ObjectAsOptions{}) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + err := loadFlavorId(ctx, r.client, &model, flavor) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Loading flavor ID: %v", err)) return } } - var storage = &storageModel{} - if !(model.Storage.IsNull() || model.Storage.IsUnknown()) { + + storage := &storageModel{} + if !model.Storage.IsNull() && !model.Storage.IsUnknown() { diags = model.Storage.As(ctx, storage, basetypes.ObjectAsOptions{}) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } } - var options = &optionsModel{} - if !(model.Options.IsNull() || model.Options.IsUnknown()) { + options := &optionsModel{} + if !model.Options.IsNull() && !model.Options.IsUnknown() { diags = model.Options.As(ctx, options, basetypes.ObjectAsOptions{}) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -401,6 +418,7 @@ func (r *instanceResource) Create(ctx context.Context, req resource.CreateReques core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating instance", fmt.Sprintf("Calling API: %v", err)) return } + instanceId := *createResp.Id ctx = tflog.SetField(ctx, "instance_id", instanceId) // The creation waiter sometimes returns an error from the API: "instance with id xxx has unexpected status Failure" @@ -420,6 +438,7 @@ func (r *instanceResource) Create(ctx context.Context, req resource.CreateReques // Set state to fully populated data diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -432,13 +451,15 @@ func (r *instanceResource) Create(ctx context.Context, req resource.CreateReques } // Read refreshes the Terraform state with the latest data. -func (r *instanceResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (r *instanceResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() instanceId := model.InstanceId.ValueString() region := r.providerData.GetRegionWithOverride(model.Region) @@ -447,27 +468,31 @@ func (r *instanceResource) Read(ctx context.Context, req resource.ReadRequest, r ctx = tflog.SetField(ctx, "instance_id", instanceId) ctx = tflog.SetField(ctx, "region", region) - var flavor = &flavorModel{} - if !(model.Flavor.IsNull() || model.Flavor.IsUnknown()) { + flavor := &flavorModel{} + if !model.Flavor.IsNull() && !model.Flavor.IsUnknown() { diags = model.Flavor.As(ctx, flavor, basetypes.ObjectAsOptions{}) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } } - var storage = &storageModel{} - if !(model.Storage.IsNull() || model.Storage.IsUnknown()) { + + storage := &storageModel{} + if !model.Storage.IsNull() && !model.Storage.IsUnknown() { diags = model.Storage.As(ctx, storage, basetypes.ObjectAsOptions{}) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } } - var options = &optionsModel{} - if !(model.Options.IsNull() || model.Options.IsUnknown()) { + options := &optionsModel{} + if !model.Options.IsNull() && !model.Options.IsUnknown() { diags = model.Options.As(ctx, options, basetypes.ObjectAsOptions{}) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -480,7 +505,9 @@ func (r *instanceResource) Read(ctx context.Context, req resource.ReadRequest, r resp.State.RemoveResource(ctx) return } + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading instance", err.Error()) + return } @@ -493,21 +520,25 @@ func (r *instanceResource) Read(ctx context.Context, req resource.ReadRequest, r // Set refreshed state diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "SQLServer Flex instance read") } // Update updates the resource and sets the updated Terraform state on success. -func (r *instanceResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *instanceResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { //nolint:gocritic // function signature required by Terraform // Retrieve values from plan var model Model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() instanceId := model.InstanceId.ValueString() region := model.Region.ValueString() @@ -517,39 +548,46 @@ func (r *instanceResource) Update(ctx context.Context, req resource.UpdateReques ctx = tflog.SetField(ctx, "region", region) var acl []string - if !(model.ACL.IsNull() || model.ACL.IsUnknown()) { + if !model.ACL.IsNull() && !model.ACL.IsUnknown() { diags = model.ACL.ElementsAs(ctx, &acl, false) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } } - var flavor = &flavorModel{} - if !(model.Flavor.IsNull() || model.Flavor.IsUnknown()) { + + flavor := &flavorModel{} + if !model.Flavor.IsNull() && !model.Flavor.IsUnknown() { diags = model.Flavor.As(ctx, flavor, basetypes.ObjectAsOptions{}) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + err := loadFlavorId(ctx, r.client, &model, flavor) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Loading flavor ID: %v", err)) return } } - var storage = &storageModel{} - if !(model.Storage.IsNull() || model.Storage.IsUnknown()) { + + storage := &storageModel{} + if !model.Storage.IsNull() && !model.Storage.IsUnknown() { diags = model.Storage.As(ctx, storage, basetypes.ObjectAsOptions{}) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } } - var options = &optionsModel{} - if !(model.Options.IsNull() || model.Options.IsUnknown()) { + options := &optionsModel{} + if !model.Options.IsNull() && !model.Options.IsUnknown() { diags = model.Options.As(ctx, options, basetypes.ObjectAsOptions{}) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -567,6 +605,7 @@ func (r *instanceResource) Update(ctx context.Context, req resource.UpdateReques core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", err.Error()) return } + waitResp, err := wait.UpdateInstanceWaitHandler(ctx, r.client, projectId, instanceId, region).WaitWithContext(ctx) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Instance update waiting: %v", err)) @@ -579,23 +618,28 @@ func (r *instanceResource) Update(ctx context.Context, req resource.UpdateReques core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating instance", fmt.Sprintf("Processing API payload: %v", err)) return } + diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "SQLServer Flex instance updated") } // Delete deletes the resource and removes the Terraform state on success. -func (r *instanceResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform +func (r *instanceResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { //nolint:gocritic // function signature required by Terraform // Retrieve values from state var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() instanceId := model.InstanceId.ValueString() region := model.Region.ValueString() @@ -609,16 +653,18 @@ func (r *instanceResource) Delete(ctx context.Context, req resource.DeleteReques core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting instance", fmt.Sprintf("Calling API: %v", err)) return } + _, err = wait.DeleteInstanceWaitHandler(ctx, r.client, projectId, instanceId, region).WaitWithContext(ctx) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting instance", fmt.Sprintf("Instance deletion waiting: %v", err)) return } + tflog.Info(ctx, "SQLServer Flex instance deleted") } // ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: project_id,instance_id +// The expected format of the resource import identifier is: project_id,instance_id. func (r *instanceResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { idParts := strings.Split(req.ID, core.Separator) @@ -627,6 +673,7 @@ func (r *instanceResource) ImportState(ctx context.Context, req resource.ImportS "Error importing instance", fmt.Sprintf("Expected import identifier with format: [project_id],[region],[instance_id] Got: %q", req.ID), ) + return } @@ -640,12 +687,15 @@ func mapFields(ctx context.Context, resp *sqlserverflex.GetInstanceResponse, mod if resp == nil { return fmt.Errorf("response input is nil") } + if resp.Item == nil { return fmt.Errorf("no instance provided") } + if model == nil { return fmt.Errorf("model input is nil") } + instance := resp.Item var instanceId string @@ -658,11 +708,14 @@ func mapFields(ctx context.Context, resp *sqlserverflex.GetInstanceResponse, mod } var aclList basetypes.ListValue + var diags diag.Diagnostics + if instance.Acl == nil || instance.Acl.Items == nil { aclList = types.ListNull(types.StringType) } else { respACL := *instance.Acl.Items + modelACL, err := utils.ListValuetoStringSlice(model.ACL) if err != nil { return err @@ -692,6 +745,7 @@ func mapFields(ctx context.Context, resp *sqlserverflex.GetInstanceResponse, mod "ram": types.Int64PointerValue(instance.Flavor.Memory), } } + flavorObject, diags := types.ObjectValue(flavorTypes, flavorValues) if diags.HasError() { return fmt.Errorf("creating flavor: %w", core.DiagsToError(diags)) @@ -709,6 +763,7 @@ func mapFields(ctx context.Context, resp *sqlserverflex.GetInstanceResponse, mod "size": types.Int64PointerValue(instance.Storage.Size), } } + storageObject, diags := types.ObjectValue(storageTypes, storageValues) if diags.HasError() { return fmt.Errorf("creating storage: %w", core.DiagsToError(diags)) @@ -722,17 +777,20 @@ func mapFields(ctx context.Context, resp *sqlserverflex.GetInstanceResponse, mod } } else { retentionDays := options.RetentionDays + retentionDaysString, ok := (*instance.Options)["retentionDays"] if ok { retentionDaysValue, err := strconv.ParseInt(retentionDaysString, 10, 64) if err != nil { return fmt.Errorf("parse retentionDays to int64: %w", err) } + retentionDays = types.Int64Value(retentionDaysValue) } edition := options.Edition editionValue, ok := (*instance.Options)["edition"] + if ok { edition = types.StringValue(editionValue) } @@ -742,6 +800,7 @@ func mapFields(ctx context.Context, resp *sqlserverflex.GetInstanceResponse, mod "retention_days": retentionDays, } } + optionsObject, diags := types.ObjectValue(optionsTypes, optionsValues) if diags.HasError() { return fmt.Errorf("creating options: %w", core.DiagsToError(diags)) @@ -764,6 +823,7 @@ func mapFields(ctx context.Context, resp *sqlserverflex.GetInstanceResponse, mod model.Version = types.StringPointerValue(instance.Version) model.Options = optionsObject model.Region = types.StringValue(region) + return nil } @@ -771,26 +831,33 @@ func toCreatePayload(model *Model, acl []string, flavor *flavorModel, storage *s if model == nil { return nil, fmt.Errorf("nil model") } + aclPayload := &sqlserverflex.CreateInstancePayloadAcl{} if acl != nil { aclPayload.Items = &acl } + if flavor == nil { return nil, fmt.Errorf("nil flavor") } + storagePayload := &sqlserverflex.CreateInstancePayloadStorage{} if storage != nil { storagePayload.Class = conversion.StringValueToPointer(storage.Class) storagePayload.Size = conversion.Int64ValueToPointer(storage.Size) } + optionsPayload := &sqlserverflex.CreateInstancePayloadOptions{} if options != nil { optionsPayload.Edition = conversion.StringValueToPointer(options.Edition) retentionDaysInt := conversion.Int64ValueToPointer(options.RetentionDays) + var retentionDays *string + if retentionDaysInt != nil { retentionDays = coreUtils.Ptr(strconv.FormatInt(*retentionDaysInt, 10)) } + optionsPayload.RetentionDays = retentionDays } @@ -809,10 +876,12 @@ func toUpdatePayload(model *Model, acl []string, flavor *flavorModel) (*sqlserve if model == nil { return nil, fmt.Errorf("nil model") } + aclPayload := &sqlserverflex.CreateInstancePayloadAcl{} if acl != nil { aclPayload.Items = &acl } + if flavor == nil { return nil, fmt.Errorf("nil flavor") } @@ -834,13 +903,16 @@ func loadFlavorId(ctx context.Context, client sqlserverflexClient, model *Model, if model == nil { return fmt.Errorf("nil model") } + if flavor == nil { return fmt.Errorf("nil flavor") } + cpu := conversion.Int64ValueToPointer(flavor.CPU) if cpu == nil { return fmt.Errorf("nil CPU") } + ram := conversion.Int64ValueToPointer(flavor.RAM) if ram == nil { return fmt.Errorf("nil RAM") @@ -848,26 +920,33 @@ func loadFlavorId(ctx context.Context, client sqlserverflexClient, model *Model, projectId := model.ProjectId.ValueString() region := model.Region.ValueString() + res, err := client.ListFlavorsExecute(ctx, projectId, region) if err != nil { return fmt.Errorf("listing sqlserverflex flavors: %w", err) } avl := "" + if res.Flavors == nil { return fmt.Errorf("finding flavors for project %s", projectId) } + for _, f := range *res.Flavors { if f.Id == nil || f.Cpu == nil || f.Memory == nil { continue } + if *f.Cpu == *cpu && *f.Memory == *ram { flavor.Id = types.StringValue(*f.Id) flavor.Description = types.StringValue(*f.Description) + break } + avl = fmt.Sprintf("%s\n- %d CPU, %d GB RAM", avl, *f.Cpu, *f.Memory) } + if flavor.Id.ValueString() == "" { return fmt.Errorf("couldn't find flavor, available specs are:%s", avl) } diff --git a/stackit/internal/services/sqlserverflex/instance/resource_test.go b/stackit/internal/services/sqlserverflex/instance/resource_test.go index 66021845b..6567dfa88 100644 --- a/stackit/internal/services/sqlserverflex/instance/resource_test.go +++ b/stackit/internal/services/sqlserverflex/instance/resource_test.go @@ -27,6 +27,7 @@ func (c *sqlserverflexClientMocked) ListFlavorsExecute(_ context.Context, _, _ s func TestMapFields(t *testing.T) { const testRegion = "region" + tests := []struct { description string state Model @@ -333,9 +334,11 @@ func TestMapFields(t *testing.T) { if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(tt.state, tt.expected) if diff != "" { @@ -531,9 +534,11 @@ func TestToCreatePayload(t *testing.T) { if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(output, tt.expected) if diff != "" { @@ -654,9 +659,11 @@ func TestToUpdatePayload(t *testing.T) { if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(output, tt.expected) if diff != "" { @@ -803,13 +810,16 @@ func TestLoadFlavorId(t *testing.T) { CPU: tt.inputFlavor.CPU, RAM: tt.inputFlavor.RAM, } + err := loadFlavorId(context.Background(), client, model, flavorModel) if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(flavorModel, tt.expected) if diff != "" { diff --git a/stackit/internal/services/sqlserverflex/sqlserverflex_acc_test.go b/stackit/internal/services/sqlserverflex/sqlserverflex_acc_test.go index e88ac5994..151b07dd5 100644 --- a/stackit/internal/services/sqlserverflex/sqlserverflex_acc_test.go +++ b/stackit/internal/services/sqlserverflex/sqlserverflex_acc_test.go @@ -26,6 +26,7 @@ var ( //go:embed testdata/resource-min.tf resourceMinConfig string ) + var testConfigVarsMin = config.Variables{ "project_id": config.StringVariable(testutil.ProjectId), "name": config.StringVariable(fmt.Sprintf("tf-acc-%s", acctest.RandStringFromCharSet(7, acctest.CharSetAlphaNum))), @@ -60,12 +61,14 @@ var testConfigVarsMax = config.Variables{ func configVarsMinUpdated() config.Variables { temp := maps.Clone(testConfigVarsMax) temp["name"] = config.StringVariable(testutil.ConvertConfigVariable(temp["name"]) + "changed") + return temp } func configVarsMaxUpdated() config.Variables { temp := maps.Clone(testConfigVarsMax) temp["backup_schedule"] = config.StringVariable("00 12 * * *") + return temp } @@ -376,6 +379,7 @@ func TestAccSQLServerFlexMaxResource(t *testing.T) { if s[0].Attributes["backup_schedule"] != testutil.ConvertConfigVariable(testConfigVarsMax["backup_schedule"]) { return fmt.Errorf("expected backup_schedule %s, got %s", testConfigVarsMax["backup_schedule"], s[0].Attributes["backup_schedule"]) } + return nil }, }, @@ -432,7 +436,9 @@ func TestAccSQLServerFlexMaxResource(t *testing.T) { func testAccChecksqlserverflexDestroy(s *terraform.State) error { ctx := context.Background() + var client *sqlserverflex.APIClient + var err error if testutil.SQLServerFlexCustomEndpoint == "" { client, err = sqlserverflex.NewAPIClient() @@ -441,11 +447,13 @@ func testAccChecksqlserverflexDestroy(s *terraform.State) error { core_config.WithEndpoint(testutil.SQLServerFlexCustomEndpoint), ) } + if err != nil { return fmt.Errorf("creating client: %w", err) } instancesToDestroy := []string{} + for _, rs := range s.RootModule().Resources { if rs.Type != "stackit_sqlserverflex_instance" { continue @@ -465,16 +473,19 @@ func testAccChecksqlserverflexDestroy(s *terraform.State) error { if items[i].Id == nil { continue } + if utils.Contains(instancesToDestroy, *items[i].Id) { err := client.DeleteInstanceExecute(ctx, testutil.ProjectId, *items[i].Id, testutil.Region) if err != nil { return fmt.Errorf("destroying instance %s during CheckDestroy: %w", *items[i].Id, err) } + _, err = wait.DeleteInstanceWaitHandler(ctx, client, testutil.ProjectId, *items[i].Id, testutil.Region).WaitWithContext(ctx) if err != nil { return fmt.Errorf("destroying instance %s during CheckDestroy: waiting for deletion %w", *items[i].Id, err) } } } + return nil } diff --git a/stackit/internal/services/sqlserverflex/user/datasource.go b/stackit/internal/services/sqlserverflex/user/datasource.go index ffb94c4b0..f0aee121b 100644 --- a/stackit/internal/services/sqlserverflex/user/datasource.go +++ b/stackit/internal/services/sqlserverflex/user/datasource.go @@ -5,20 +5,18 @@ import ( "fmt" "net/http" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - sqlserverflexUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/sqlserverflex/utils" - "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + sqlserverflexUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/sqlserverflex/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - - "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex" ) // Ensure the implementation satisfies the expected interfaces. @@ -57,16 +55,20 @@ func (r *userDataSource) Metadata(_ context.Context, req datasource.MetadataRequ // Configure adds the provider configured client to the data source. func (r *userDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { var ok bool + r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) if !ok { return } apiClient := sqlserverflexUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + r.client = apiClient + tflog.Info(ctx, "SQLServer Flex user client configured") } @@ -139,13 +141,15 @@ func (r *userDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, r } // Read refreshes the Terraform state with the latest data. -func (r *userDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (r *userDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model DataSourceModel diags := req.Config.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() instanceId := model.InstanceId.ValueString() userId := model.UserId.ValueString() @@ -168,6 +172,7 @@ func (r *userDataSource) Read(ctx context.Context, req datasource.ReadRequest, r }, ) resp.State.RemoveResource(ctx) + return } @@ -181,9 +186,11 @@ func (r *userDataSource) Read(ctx context.Context, req datasource.ReadRequest, r // Set refreshed state diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "SQLServer Flex user read") } @@ -191,9 +198,11 @@ func mapDataSourceFields(userResp *sqlserverflex.GetUserResponse, model *DataSou if userResp == nil || userResp.Item == nil { return fmt.Errorf("response is nil") } + if model == nil { return fmt.Errorf("model input is nil") } + user := userResp.Item var userId string @@ -204,6 +213,7 @@ func mapDataSourceFields(userResp *sqlserverflex.GetUserResponse, model *DataSou } else { return fmt.Errorf("user id not present") } + model.Id = utils.BuildInternalTerraformId( model.ProjectId.ValueString(), region, model.InstanceId.ValueString(), userId, ) @@ -217,14 +227,18 @@ func mapDataSourceFields(userResp *sqlserverflex.GetUserResponse, model *DataSou for _, role := range *user.Roles { roles = append(roles, types.StringValue(role)) } + rolesSet, diags := types.SetValue(types.StringType, roles) if diags.HasError() { return fmt.Errorf("failed to map roles: %w", core.DiagsToError(diags)) } + model.Roles = rolesSet } + model.Host = types.StringPointerValue(user.Host) model.Port = types.Int64PointerValue(user.Port) model.Region = types.StringValue(region) + return nil } diff --git a/stackit/internal/services/sqlserverflex/user/datasource_test.go b/stackit/internal/services/sqlserverflex/user/datasource_test.go index b5179c44b..25e8eccba 100644 --- a/stackit/internal/services/sqlserverflex/user/datasource_test.go +++ b/stackit/internal/services/sqlserverflex/user/datasource_test.go @@ -12,6 +12,7 @@ import ( func TestMapDataSourceFields(t *testing.T) { const testRegion = "region" + tests := []struct { description string input *sqlserverflex.GetUserResponse @@ -126,13 +127,16 @@ func TestMapDataSourceFields(t *testing.T) { InstanceId: tt.expected.InstanceId, UserId: tt.expected.UserId, } + err := mapDataSourceFields(tt.input, state, tt.region) if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(state, &tt.expected) if diff != "" { diff --git a/stackit/internal/services/sqlserverflex/user/resource.go b/stackit/internal/services/sqlserverflex/user/resource.go index 153750c78..9a6509c7e 100644 --- a/stackit/internal/services/sqlserverflex/user/resource.go +++ b/stackit/internal/services/sqlserverflex/user/resource.go @@ -6,15 +6,6 @@ import ( "net/http" "strings" - sqlserverflexUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/sqlserverflex/utils" - - "github.com/hashicorp/terraform-plugin-framework/schema/validator" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" - "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" - "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" @@ -22,9 +13,16 @@ import ( "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/setplanmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/stackitcloud/stackit-sdk-go/core/oapierror" "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + sqlserverflexUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/sqlserverflex/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" ) // Ensure the implementation satisfies the expected interfaces. @@ -67,44 +65,54 @@ func (r *userResource) Metadata(_ context.Context, req resource.MetadataRequest, // Configure adds the provider configured client to the resource. func (r *userResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { var ok bool + r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) if !ok { return } apiClient := sqlserverflexUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { return } + r.client = apiClient + tflog.Info(ctx, "SQLServer Flex user client configured") } // ModifyPlan implements resource.ResourceWithModifyPlan. // Use the modifier to set the effective region in the current plan. -func (r *userResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { // nolint:gocritic // function signature required by Terraform +func (r *userResource) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { //nolint:gocritic // function signature required by Terraform var configModel Model // skip initial empty configuration to avoid follow-up errors if req.Config.Raw.IsNull() { return } + resp.Diagnostics.Append(req.Config.Get(ctx, &configModel)...) + if resp.Diagnostics.HasError() { return } var planModel Model + resp.Diagnostics.Append(req.Plan.Get(ctx, &planModel)...) + if resp.Diagnostics.HasError() { return } utils.AdaptRegion(ctx, configModel.Region, &planModel.Region, r.providerData.GetRegion(), resp) + if resp.Diagnostics.HasError() { return } resp.Diagnostics.Append(resp.Plan.Set(ctx, planModel)...) + if resp.Diagnostics.HasError() { return } @@ -208,13 +216,15 @@ func (r *userResource) Schema(_ context.Context, _ resource.SchemaRequest, resp } // Create creates the resource and sets the initial Terraform state. -func (r *userResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *userResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.Plan.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() instanceId := model.InstanceId.ValueString() region := model.Region.ValueString() @@ -224,9 +234,10 @@ func (r *userResource) Create(ctx context.Context, req resource.CreateRequest, r ctx = tflog.SetField(ctx, "region", region) var roles []string - if !(model.Roles.IsNull() || model.Roles.IsUnknown()) { + if !model.Roles.IsNull() && !model.Roles.IsUnknown() { diags = model.Roles.ElementsAs(ctx, &roles, false) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -244,10 +255,12 @@ func (r *userResource) Create(ctx context.Context, req resource.CreateRequest, r core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating user", fmt.Sprintf("Calling API: %v", err)) return } + if userResp == nil || userResp.Item == nil || userResp.Item.Id == nil || *userResp.Item.Id == "" { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating user", "API didn't return user Id. A user might have been created") return } + userId := *userResp.Item.Id ctx = tflog.SetField(ctx, "user_id", userId) @@ -260,20 +273,24 @@ func (r *userResource) Create(ctx context.Context, req resource.CreateRequest, r // Set state to fully populated data diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "SQLServer Flex user created") } // Read refreshes the Terraform state with the latest data. -func (r *userResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +func (r *userResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { //nolint:gocritic // function signature required by Terraform var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + projectId := model.ProjectId.ValueString() instanceId := model.InstanceId.ValueString() userId := model.UserId.ValueString() @@ -290,7 +307,9 @@ func (r *userResource) Read(ctx context.Context, req resource.ReadRequest, resp resp.State.RemoveResource(ctx) return } + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading user", fmt.Sprintf("Calling API: %v", err)) + return } @@ -304,24 +323,27 @@ func (r *userResource) Read(ctx context.Context, req resource.ReadRequest, resp // Set refreshed state diags = resp.State.Set(ctx, model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } + tflog.Info(ctx, "SQLServer Flex user read") } // Update updates the resource and sets the updated Terraform state on success. -func (r *userResource) Update(ctx context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform +func (r *userResource) Update(ctx context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { //nolint:gocritic // function signature required by Terraform // Update shouldn't be called core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating user", "User can't be updated") } // Delete deletes the resource and removes the Terraform state on success. -func (r *userResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform +func (r *userResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { //nolint:gocritic // function signature required by Terraform // Retrieve values from plan var model Model diags := req.State.Get(ctx, &model) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } @@ -341,11 +363,12 @@ func (r *userResource) Delete(ctx context.Context, req resource.DeleteRequest, r core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting user", fmt.Sprintf("Calling API: %v", err)) return } + tflog.Info(ctx, "SQLServer Flex user deleted") } // ImportState imports a resource into the Terraform state on success. -// The expected format of the resource import identifier is: project_id,zone_id,record_set_id +// The expected format of the resource import identifier is: project_id,zone_id,record_set_id. func (r *userResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { idParts := strings.Split(req.ID, core.Separator) if len(idParts) != 4 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" || idParts[3] == "" { @@ -353,6 +376,7 @@ func (r *userResource) ImportState(ctx context.Context, req resource.ImportState "Error importing user", fmt.Sprintf("Expected import identifier with format [project_id],[region],[instance_id],[user_id], got %q", req.ID), ) + return } @@ -371,14 +395,17 @@ func mapFieldsCreate(userResp *sqlserverflex.CreateUserResponse, model *Model, r if userResp == nil || userResp.Item == nil { return fmt.Errorf("response is nil") } + if model == nil { return fmt.Errorf("model input is nil") } + user := userResp.Item if user.Id == nil { return fmt.Errorf("user id not present") } + userId := *user.Id model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), region, model.InstanceId.ValueString(), userId) model.UserId = types.StringValue(userId) @@ -387,6 +414,7 @@ func mapFieldsCreate(userResp *sqlserverflex.CreateUserResponse, model *Model, r if user.Password == nil { return fmt.Errorf("user password not present") } + model.Password = types.StringValue(*user.Password) if user.Roles != nil { @@ -394,10 +422,12 @@ func mapFieldsCreate(userResp *sqlserverflex.CreateUserResponse, model *Model, r for _, role := range *user.Roles { roles = append(roles, types.StringValue(role)) } + rolesSet, diags := types.SetValue(types.StringType, roles) if diags.HasError() { return fmt.Errorf("failed to map roles: %w", core.DiagsToError(diags)) } + model.Roles = rolesSet } @@ -408,6 +438,7 @@ func mapFieldsCreate(userResp *sqlserverflex.CreateUserResponse, model *Model, r model.Host = types.StringPointerValue(user.Host) model.Port = types.Int64PointerValue(user.Port) model.Region = types.StringValue(region) + return nil } @@ -415,9 +446,11 @@ func mapFields(userResp *sqlserverflex.GetUserResponse, model *Model, region str if userResp == nil || userResp.Item == nil { return fmt.Errorf("response is nil") } + if model == nil { return fmt.Errorf("model input is nil") } + user := userResp.Item var userId string @@ -428,6 +461,7 @@ func mapFields(userResp *sqlserverflex.GetUserResponse, model *Model, region str } else { return fmt.Errorf("user id not present") } + model.Id = utils.BuildInternalTerraformId( model.ProjectId.ValueString(), region, @@ -442,10 +476,12 @@ func mapFields(userResp *sqlserverflex.GetUserResponse, model *Model, region str for _, role := range *user.Roles { roles = append(roles, types.StringValue(role)) } + rolesSet, diags := types.SetValue(types.StringType, roles) if diags.HasError() { return fmt.Errorf("failed to map roles: %w", core.DiagsToError(diags)) } + model.Roles = rolesSet } @@ -456,6 +492,7 @@ func mapFields(userResp *sqlserverflex.GetUserResponse, model *Model, region str model.Host = types.StringPointerValue(user.Host) model.Port = types.Int64PointerValue(user.Port) model.Region = types.StringValue(region) + return nil } diff --git a/stackit/internal/services/sqlserverflex/user/resource_test.go b/stackit/internal/services/sqlserverflex/user/resource_test.go index 058b213d4..2d344c348 100644 --- a/stackit/internal/services/sqlserverflex/user/resource_test.go +++ b/stackit/internal/services/sqlserverflex/user/resource_test.go @@ -12,6 +12,7 @@ import ( func TestMapFieldsCreate(t *testing.T) { const testRegion = "region" + tests := []struct { description string input *sqlserverflex.CreateUserResponse @@ -145,13 +146,16 @@ func TestMapFieldsCreate(t *testing.T) { ProjectId: tt.expected.ProjectId, InstanceId: tt.expected.InstanceId, } + err := mapFieldsCreate(tt.input, state, tt.region) if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(state, &tt.expected) if diff != "" { @@ -164,6 +168,7 @@ func TestMapFieldsCreate(t *testing.T) { func TestMapFields(t *testing.T) { const testRegion = "region" + tests := []struct { description string input *sqlserverflex.GetUserResponse @@ -278,13 +283,16 @@ func TestMapFields(t *testing.T) { InstanceId: tt.expected.InstanceId, UserId: tt.expected.UserId, } + err := mapFields(tt.input, state, tt.region) if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(state, &tt.expected) if diff != "" { @@ -373,9 +381,11 @@ func TestToCreatePayload(t *testing.T) { if !tt.isValid && err == nil { t.Fatalf("Should have failed") } + if tt.isValid && err != nil { t.Fatalf("Should not have failed: %v", err) } + if tt.isValid { diff := cmp.Diff(output, tt.expected) if diff != "" { diff --git a/stackit/internal/services/sqlserverflex/utils/util.go b/stackit/internal/services/sqlserverflex/utils/util.go index 5c14c0856..ace7a2b5e 100644 --- a/stackit/internal/services/sqlserverflex/utils/util.go +++ b/stackit/internal/services/sqlserverflex/utils/util.go @@ -4,10 +4,9 @@ import ( "context" "fmt" - "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex" - "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/stackitcloud/stackit-sdk-go/core/config" + "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" ) @@ -22,6 +21,7 @@ func ConfigureClient(ctx context.Context, providerData *core.ProviderData, diags } else { apiClientConfigOptions = append(apiClientConfigOptions, config.WithRegion(providerData.GetRegion())) } + apiClient, err := sqlserverflex.NewAPIClient(apiClientConfigOptions...) if err != nil { core.LogAndAddError(ctx, diags, "Error configuring API client", fmt.Sprintf("Configuring client: %v. This is an error related to the provider configuration, not to the resource configuration", err)) diff --git a/stackit/internal/services/sqlserverflex/utils/util_test.go b/stackit/internal/services/sqlserverflex/utils/util_test.go index 5ee93949b..cf41aee9d 100644 --- a/stackit/internal/services/sqlserverflex/utils/util_test.go +++ b/stackit/internal/services/sqlserverflex/utils/util_test.go @@ -22,6 +22,7 @@ const ( func TestConfigureClient(t *testing.T) { /* mock authentication by setting service account token env variable */ os.Clearenv() + err := os.Setenv(sdkClients.ServiceAccountToken, "mock-val") if err != nil { t.Errorf("error setting env variable: %v", err) @@ -30,6 +31,7 @@ func TestConfigureClient(t *testing.T) { type args struct { providerData *core.ProviderData } + tests := []struct { name string args args @@ -51,6 +53,7 @@ func TestConfigureClient(t *testing.T) { if err != nil { t.Errorf("error configuring client: %v", err) } + return apiClient }(), wantErr: false, @@ -71,6 +74,7 @@ func TestConfigureClient(t *testing.T) { if err != nil { t.Errorf("error configuring client: %v", err) } + return apiClient }(), wantErr: false, @@ -82,6 +86,7 @@ func TestConfigureClient(t *testing.T) { diags := diag.Diagnostics{} actual := ConfigureClient(ctx, tt.args.providerData, &diags) + if diags.HasError() != tt.wantErr { t.Errorf("ConfigureClient() error = %v, want %v", diags.HasError(), tt.wantErr) } diff --git a/stackit/internal/testutil/testutil.go b/stackit/internal/testutil/testutil.go index a2651db28..75050a23a 100644 --- a/stackit/internal/testutil/testutil.go +++ b/stackit/internal/testutil/testutil.go @@ -11,12 +11,11 @@ import ( "github.com/hashicorp/terraform-plugin-framework/providerserver" "github.com/hashicorp/terraform-plugin-go/tfprotov6" "github.com/hashicorp/terraform-plugin-testing/config" - "github.com/stackitcloud/terraform-provider-stackit/stackit" ) const ( - // Default location of credentials JSON + // Default location of credentials JSON. credentialsFilePath = ".stackit/credentials.json" //nolint:gosec // linter false positive ) @@ -32,23 +31,23 @@ var ( // E2ETestsEnabled checks if end-to-end tests should be run. // It is enabled when the TF_ACC environment variable is set to "1". E2ETestsEnabled = os.Getenv("TF_ACC") == "1" - // OrganizationId is the id of organization used for tests + // OrganizationId is the id of organization used for tests. OrganizationId = os.Getenv("TF_ACC_ORGANIZATION_ID") - // ProjectId is the id of project used for tests + // ProjectId is the id of project used for tests. ProjectId = os.Getenv("TF_ACC_PROJECT_ID") Region = os.Getenv("TF_ACC_REGION") - // ServerId is the id of a server used for some tests + // ServerId is the id of a server used for some tests. ServerId = getenv("TF_ACC_SERVER_ID", "") - // TestProjectParentContainerID is the container id of the parent resource under which projects are created as part of the resource-manager acceptance tests + // TestProjectParentContainerID is the container id of the parent resource under which projects are created as part of the resource-manager acceptance tests. TestProjectParentContainerID = os.Getenv("TF_ACC_TEST_PROJECT_PARENT_CONTAINER_ID") - // TestProjectParentUUID is the uuid of the parent resource under which projects are created as part of the resource-manager acceptance tests + // TestProjectParentUUID is the uuid of the parent resource under which projects are created as part of the resource-manager acceptance tests. TestProjectParentUUID = os.Getenv("TF_ACC_TEST_PROJECT_PARENT_UUID") - // TestProjectServiceAccountEmail is the e-mail of a service account with admin permissions on the organization under which projects are created as part of the resource-manager acceptance tests + // TestProjectServiceAccountEmail is the e-mail of a service account with admin permissions on the organization under which projects are created as part of the resource-manager acceptance tests. TestProjectServiceAccountEmail = os.Getenv("TF_ACC_TEST_PROJECT_SERVICE_ACCOUNT_EMAIL") // TestProjectUserEmail is the e-mail of a user for the project created as part of the resource-manager acceptance tests - // Default email: acc-test@sa.stackit.cloud + // Default email: acc-test@sa.stackit.cloud. TestProjectUserEmail = getenv("TF_ACC_TEST_PROJECT_USER_EMAIL", "acc-test@sa.stackit.cloud") - // TestImageLocalFilePath is the local path to an image file used for image acceptance tests + // TestImageLocalFilePath is the local path to an image file used for image acceptance tests. TestImageLocalFilePath = getenv("TF_ACC_TEST_IMAGE_LOCAL_FILE_PATH", "default") CdnCustomEndpoint = os.Getenv("TF_ACC_CDN_CUSTOM_ENDPOINT") @@ -85,6 +84,7 @@ func ObservabilityProviderConfig() string { default_region = "eu01" }` } + return fmt.Sprintf(` provider "stackit" { observability_custom_endpoint = "%s" @@ -92,6 +92,7 @@ func ObservabilityProviderConfig() string { ObservabilityCustomEndpoint, ) } + func CdnProviderConfig() string { if CdnCustomEndpoint == "" { return ` @@ -99,6 +100,7 @@ func CdnProviderConfig() string { enable_beta_resources = true }` } + return fmt.Sprintf(` provider "stackit" { cdn_custom_endpoint = "%s" @@ -112,6 +114,7 @@ func DnsProviderConfig() string { if DnsCustomEndpoint == "" { return `provider "stackit" {}` } + return fmt.Sprintf(` provider "stackit" { dns_custom_endpoint = "%s" @@ -127,6 +130,7 @@ func IaaSProviderConfig() string { default_region = "eu01" }` } + return fmt.Sprintf(` provider "stackit" { iaas_custom_endpoint = "%s" @@ -143,6 +147,7 @@ func IaaSProviderConfigWithBetaResourcesEnabled() string { default_region = "eu01" }` } + return fmt.Sprintf(` provider "stackit" { enable_beta_resources = true @@ -160,6 +165,7 @@ func IaaSProviderConfigWithExperiments() string { experiments = [ "routing-tables", "network" ] }` } + return fmt.Sprintf(` provider "stackit" { iaas_custom_endpoint = "%s" @@ -176,6 +182,7 @@ func LoadBalancerProviderConfig() string { default_region = "eu01" }` } + return fmt.Sprintf(` provider "stackit" { loadbalancer_custom_endpoint = "%s" @@ -191,6 +198,7 @@ func LogMeProviderConfig() string { default_region = "eu01" }` } + return fmt.Sprintf(` provider "stackit" { logme_custom_endpoint = "%s" @@ -206,6 +214,7 @@ func MariaDBProviderConfig() string { default_region = "eu01" }` } + return fmt.Sprintf(` provider "stackit" { mariadb_custom_endpoint = "%s" @@ -222,6 +231,7 @@ func ModelServingProviderConfig() string { } ` } + return fmt.Sprintf(` provider "stackit" { modelserving_custom_endpoint = "%s" @@ -237,6 +247,7 @@ func MongoDBFlexProviderConfig() string { default_region = "eu01" }` } + return fmt.Sprintf(` provider "stackit" { mongodbflex_custom_endpoint = "%s" @@ -252,6 +263,7 @@ func ObjectStorageProviderConfig() string { default_region = "eu01" }` } + return fmt.Sprintf(` provider "stackit" { objectstorage_custom_endpoint = "%s" @@ -267,6 +279,7 @@ func OpenSearchProviderConfig() string { default_region = "eu01" }` } + return fmt.Sprintf(` provider "stackit" { opensearch_custom_endpoint = "%s" @@ -282,6 +295,7 @@ func PostgresFlexProviderConfig() string { default_region = "eu01" }` } + return fmt.Sprintf(` provider "stackit" { postgresflex_custom_endpoint = "%s" @@ -297,6 +311,7 @@ func RabbitMQProviderConfig() string { default_region = "eu01" }` } + return fmt.Sprintf(` provider "stackit" { rabbitmq_custom_endpoint = "%s" @@ -312,6 +327,7 @@ func RedisProviderConfig() string { default_region = "eu01" }` } + return fmt.Sprintf(` provider "stackit" { redis_custom_endpoint = "%s" @@ -330,6 +346,7 @@ func ResourceManagerProviderConfig() string { token, ) } + return fmt.Sprintf(` provider "stackit" { resourcemanager_custom_endpoint = "%s" @@ -354,6 +371,7 @@ func ResourceManagerProviderConfigBetaEnabled() string { token, ) } + return fmt.Sprintf(` provider "stackit" { resourcemanager_custom_endpoint = "%s" @@ -374,6 +392,7 @@ func SecretsManagerProviderConfig() string { default_region = "eu01" }` } + return fmt.Sprintf(` provider "stackit" { secretsmanager_custom_endpoint = "%s" @@ -389,6 +408,7 @@ func SQLServerFlexProviderConfig() string { default_region = "eu01" }` } + return fmt.Sprintf(` provider "stackit" { sqlserverflex_custom_endpoint = "%s" @@ -405,6 +425,7 @@ func ServerBackupProviderConfig() string { enable_beta_resources = true }` } + return fmt.Sprintf(` provider "stackit" { server_backup_custom_endpoint = "%s" @@ -422,6 +443,7 @@ func ServerUpdateProviderConfig() string { enable_beta_resources = true }` } + return fmt.Sprintf(` provider "stackit" { server_update_custom_endpoint = "%s" @@ -438,6 +460,7 @@ func SKEProviderConfig() string { default_region = "eu01" }` } + return fmt.Sprintf(` provider "stackit" { ske_custom_endpoint = "%s" @@ -454,6 +477,7 @@ func AuthorizationProviderConfig() string { experiments = ["iam"] }` } + return fmt.Sprintf(` provider "stackit" { authorization_custom_endpoint = "%s" @@ -471,6 +495,7 @@ func ServiceAccountProviderConfig() string { enable_beta_resources = true }` } + return fmt.Sprintf(` provider "stackit" { service_account_custom_endpoint = "%s" @@ -488,6 +513,7 @@ func GitProviderConfig() string { enable_beta_resources = true }` } + return fmt.Sprintf(` provider "stackit" { git_custom_endpoint = "%s" @@ -504,6 +530,7 @@ func ScfProviderConfig() string { default_region = "eu01" }` } + return fmt.Sprintf(` provider "stackit" { default_region = "eu01" @@ -517,11 +544,13 @@ func ResourceNameWithDateTime(name string) string { dateTime := time.Now().Format(time.RFC3339) // Remove timezone to have a smaller datetime dateTimeTrimmed, _, _ := strings.Cut(dateTime, "+") + return fmt.Sprintf("tf-acc-%s-%s", name, dateTimeTrimmed) } func GetTestProjectServiceAccountToken(path string) string { var err error + token, tokenSet := os.LookupEnv("TF_ACC_TEST_PROJECT_SERVICE_ACCOUNT_TOKEN") if !tokenSet || token == "" { token, err = readTestTokenFromCredentialsFile(path) @@ -529,6 +558,7 @@ func GetTestProjectServiceAccountToken(path string) string { return "" } } + return token } @@ -537,10 +567,12 @@ func readTestTokenFromCredentialsFile(path string) (string, error) { customPath, customPathSet := os.LookupEnv("STACKIT_CREDENTIALS_PATH") if !customPathSet || customPath == "" { path = credentialsFilePath + home, err := os.UserHomeDir() if err != nil { return "", fmt.Errorf("getting home directory: %w", err) } + path = filepath.Join(home, path) } else { path = customPath @@ -555,10 +587,12 @@ func readTestTokenFromCredentialsFile(path string) (string, error) { var credentials struct { TF_ACC_TEST_PROJECT_SERVICE_ACCOUNT_TOKEN string `json:"TF_ACC_TEST_PROJECT_SERVICE_ACCOUNT_TOKEN"` } + err = json.Unmarshal(credentialsRaw, &credentials) if err != nil { return "", fmt.Errorf("unmarshalling credentials: %w", err) } + return credentials.TF_ACC_TEST_PROJECT_SERVICE_ACCOUNT_TOKEN, nil } @@ -567,10 +601,11 @@ func getenv(key, defaultValue string) string { if val == "" { return defaultValue } + return val } -// CreateDefaultLocalFile is a helper for local_file_path. No real data is created +// CreateDefaultLocalFile is a helper for local_file_path. No real data is created. func CreateDefaultLocalFile() os.File { // Define the file name and size fileName := "test-512k.img" @@ -598,7 +633,9 @@ func ConvertConfigVariable(variable config.Variable) string { result := string(tmpByteArray[1 : len(tmpByteArray)-1]) // Replace escaped quotes which where added MarshalJSON rawString := strings.ReplaceAll(result, `\"`, `"`) + return rawString } + return string(tmpByteArray) } diff --git a/stackit/internal/utils/attributes.go b/stackit/internal/utils/attributes.go index bddc30baa..ace2203c7 100644 --- a/stackit/internal/utils/attributes.go +++ b/stackit/internal/utils/attributes.go @@ -17,12 +17,14 @@ type attributeGetter interface { 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 } @@ -30,14 +32,19 @@ func ToTime(ctx context.Context, format string, val types.String, target *time.T // 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 } diff --git a/stackit/internal/utils/attributes_test.go b/stackit/internal/utils/attributes_test.go index b7b3c8a12..85c089ff1 100644 --- a/stackit/internal/utils/attributes_test.go +++ b/stackit/internal/utils/attributes_test.go @@ -22,6 +22,7 @@ func mustLocation(name string) *time.Location { if err != nil { log.Panicf("cannot load location %s: %v", name, err) } + return loc } @@ -31,6 +32,7 @@ func TestGetTimeFromString(t *testing.T) { source attributeGetterFunc dateFormat string } + tests := []struct { name string args args @@ -47,6 +49,7 @@ func TestGetTimeFromString(t *testing.T) { log.Panicf("wrong type %T", target) } *t = types.StringValue("2025-02-06T09:41:00+01:00") + return nil }, dateFormat: time.RFC3339, @@ -69,6 +72,7 @@ func TestGetTimeFromString(t *testing.T) { 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() { diff --git a/stackit/internal/utils/headers_test.go b/stackit/internal/utils/headers_test.go index f7f0c1758..2b6291990 100644 --- a/stackit/internal/utils/headers_test.go +++ b/stackit/internal/utils/headers_test.go @@ -11,6 +11,7 @@ func TestUserAgentConfigOption(t *testing.T) { type args struct { providerVersion string } + tests := []struct { name string args args @@ -27,12 +28,14 @@ func TestUserAgentConfigOption(t *testing.T) { 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) diff --git a/stackit/internal/utils/regions.go b/stackit/internal/utils/regions.go index 89dbdae9b..727b4d731 100644 --- a/stackit/internal/utils/regions.go +++ b/stackit/internal/utils/regions.go @@ -9,16 +9,18 @@ import ( "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" ) -// AdaptRegion rewrites the region of a terraform plan +// 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 @@ -30,7 +32,9 @@ func AdaptRegion(ctx context.Context, configRegion types.String, planRegion *typ p := path.Root("region") if !intendedRegion.Equal(*planRegion) { resp.RequiresReplace.Append(p) + *planRegion = intendedRegion } + resp.Diagnostics.Append(resp.Plan.SetAttribute(ctx, p, *planRegion)...) } diff --git a/stackit/internal/utils/regions_test.go b/stackit/internal/utils/regions_test.go index 78ca8db65..74c826bca 100644 --- a/stackit/internal/utils/regions_test.go +++ b/stackit/internal/utils/regions_test.go @@ -14,10 +14,12 @@ 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 @@ -67,6 +69,7 @@ func TestAdaptRegion(t *testing.T) { 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, } @@ -76,9 +79,11 @@ func TestAdaptRegion(t *testing.T) { } 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) } diff --git a/stackit/internal/utils/utils.go b/stackit/internal/utils/utils.go index 5190660f6..7c1bc1e57 100644 --- a/stackit/internal/utils/utils.go +++ b/stackit/internal/utils/utils.go @@ -7,12 +7,11 @@ import ( "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/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" "github.com/hashicorp/terraform-plugin-log/tflog" @@ -26,9 +25,7 @@ const ( ModelServingServiceId = "cloud.stackit.model-serving" ) -var ( - LegacyProjectRoles = []string{"project.admin", "project.auditor", "project.member", "project.owner"} -) +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 @@ -48,12 +45,14 @@ func ReconcileStringSlices(list1, list2 []string) []string { // 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 @@ -61,6 +60,7 @@ func ReconcileStringSlices(list1, list2 []string) []string { for _, elem := range list1Copy { inList1[elem] = true } + for _, elem := range list2 { if !inList1[elem] { list1Copy = append(list1Copy, elem) @@ -72,11 +72,13 @@ func ReconcileStringSlices(list1, list2 []string) []string { 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()) } @@ -84,7 +86,7 @@ func ListValuetoStringSlice(list basetypes.ListValue) ([]string, error) { } // 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 +// 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 { @@ -92,8 +94,10 @@ func SimplifyBackupSchedule(schedule string) string { if simplified == "" { simplified = "0" } + return simplified }) + return simplifiedSchedule } @@ -102,18 +106,23 @@ 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 SupportedValuesDocumentation(values []string) string { if len(values) == 0 { return "" } + return "Supported values are: " + strings.Join(QuoteValues(values), ", ") + "." } @@ -122,6 +131,7 @@ func QuoteValues(values []string) []string { for i, value := range values { quotedValues[i] = fmt.Sprintf("`%s`", value) } + return quotedValues } @@ -134,19 +144,21 @@ type value interface { IsNull() bool } -// IsUndefined checks if a passed value is unknown or null +// 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 +// 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)) @@ -157,18 +169,21 @@ func LogError(ctx context.Context, inputDiags *diag.Diagnostics, err error, summ 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 +// 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, ", ")) } @@ -189,7 +204,7 @@ func CheckListRemoval(ctx context.Context, configModelList, planModelList types. } } -// SetAndLogStateFields writes the given map of key-value pairs to the state +// 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) diff --git a/stackit/internal/utils/utils_test.go b/stackit/internal/utils/utils_test.go index 8d6851aae..8424932f4 100644 --- a/stackit/internal/utils/utils_test.go +++ b/stackit/internal/utils/utils_test.go @@ -6,17 +6,16 @@ import ( "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/diag" "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/hashicorp/terraform-plugin-go/tftypes" "github.com/stackitcloud/stackit-sdk-go/core/utils" ) @@ -73,6 +72,7 @@ func TestReconcileStrLists(t *testing.T) { 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) @@ -119,11 +119,14 @@ func TestListValuetoStrSlice(t *testing.T) { 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) @@ -173,6 +176,7 @@ func TestConvertPointerSliceToStringSlice(t *testing.T) { 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) @@ -335,6 +339,7 @@ func TestFormatPossibleValues(t *testing.T) { type args struct { values []string } + tests := []struct { name string args args @@ -368,6 +373,7 @@ func TestIsUndefined(t *testing.T) { type args struct { val value } + tests := []struct { name string args args @@ -408,6 +414,7 @@ func TestBuildInternalTerraformId(t *testing.T) { type args struct { idParts []string } + tests := []struct { name string args args @@ -441,6 +448,7 @@ func TestCheckListRemoval(t *testing.T) { type model struct { AllowedAddresses types.List `tfsdk:"allowed_addresses"` } + tests := []struct { description string configModelList types.List @@ -523,6 +531,7 @@ func TestCheckListRemoval(t *testing.T) { if diags := plan.Set(context.Background(), model{tt.planModelList}); diags.HasError() { t.Fatalf("cannot create test model: %v", diags) } + resp := resource.ModifyPlanResponse{ Plan: plan, } @@ -530,10 +539,12 @@ func TestCheckListRemoval(t *testing.T) { 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) @@ -568,10 +579,12 @@ func TestSetAndLogStateFields(t *testing.T) { state *tfsdk.State values map[string]interface{} } + type want struct { hasError bool state *tfsdk.State } + tests := []struct { name string args args @@ -602,6 +615,7 @@ func TestSetAndLogStateFields(t *testing.T) { }), Schema: testSchema, } + return &state }(), values: map[string]interface{}{ @@ -622,6 +636,7 @@ func TestSetAndLogStateFields(t *testing.T) { } 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 }(), }, diff --git a/stackit/internal/validate/validate.go b/stackit/internal/validate/validate.go index 0af0f3c64..e60c5edf7 100644 --- a/stackit/internal/validate/validate.go +++ b/stackit/internal/validate/validate.go @@ -43,10 +43,11 @@ func (v *Validator) MarkdownDescription(_ context.Context) string { return v.markdownDescription } -func (v *Validator) ValidateString(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) { // nolint:gocritic // function signature required by Terraform +func (v *Validator) ValidateString(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) { //nolint:gocritic // function signature required by Terraform if req.ConfigValue.IsUnknown() || req.ConfigValue.IsNull() { return } + v.validate(ctx, req, resp) } @@ -107,6 +108,7 @@ func IP(allowZeroAddress bool) *Validator { func RecordSet() *Validator { const typePath = "type" + return &Validator{ description: "value must be a valid record set", validate: func(ctx context.Context, req validator.StringRequest, resp *validator.StringResponse) { @@ -243,6 +245,7 @@ func RFC3339SecondsOnly() *Validator { description, req.ConfigValue.ValueString(), )) + return } @@ -357,6 +360,7 @@ func ValidNoTrailingNewline() *Validator { description, val, )) + return } if val[len(val)-1] == '\n' { diff --git a/stackit/internal/validate/validate_test.go b/stackit/internal/validate/validate_test.go index 3436a7a1f..af3d01793 100644 --- a/stackit/internal/validate/validate_test.go +++ b/stackit/internal/validate/validate_test.go @@ -48,6 +48,7 @@ func TestUUID(t *testing.T) { if !tt.isValid && !r.Diagnostics.HasError() { t.Fatalf("Should have failed") } + if tt.isValid && r.Diagnostics.HasError() { t.Fatalf("Should not have failed: %v", r.Diagnostics.Errors()) } @@ -92,6 +93,7 @@ func TestNoUUID(t *testing.T) { if !tt.isValid && !r.Diagnostics.HasError() { t.Fatalf("Should have failed") } + if tt.isValid && r.Diagnostics.HasError() { t.Fatalf("Should not have failed: %v", r.Diagnostics.Errors()) } @@ -189,6 +191,7 @@ func TestIP(t *testing.T) { if !tt.isValid && !r.Diagnostics.HasError() { t.Fatalf("Should have failed") } + if tt.isValid && r.Diagnostics.HasError() { t.Fatalf("Should not have failed: %v", r.Diagnostics.Errors()) } @@ -346,6 +349,7 @@ func TestRecordSet(t *testing.T) { if !tt.isValid && !r.Diagnostics.HasError() { t.Fatalf("Should have failed") } + if tt.isValid && r.Diagnostics.HasError() { t.Fatalf("Should not have failed: %v", r.Diagnostics.Errors()) } @@ -390,6 +394,7 @@ func TestNoSeparator(t *testing.T) { if !tt.isValid && !r.Diagnostics.HasError() { t.Fatalf("Should have failed") } + if tt.isValid && r.Diagnostics.HasError() { t.Fatalf("Should not have failed: %v", r.Diagnostics.Errors()) } @@ -444,6 +449,7 @@ func TestNonLegacyProjectRole(t *testing.T) { if !tt.isValid && !r.Diagnostics.HasError() { t.Fatalf("Should have failed") } + if tt.isValid && r.Diagnostics.HasError() { t.Fatalf("Should not have failed: %v", r.Diagnostics.Errors()) } @@ -508,6 +514,7 @@ func TestMinorVersionNumber(t *testing.T) { if !tt.isValid && !r.Diagnostics.HasError() { t.Fatalf("Should have failed") } + if tt.isValid && r.Diagnostics.HasError() { t.Fatalf("Should not have failed: %v", r.Diagnostics.Errors()) } @@ -582,6 +589,7 @@ func TestVersionNumber(t *testing.T) { if !tt.isValid && !r.Diagnostics.HasError() { t.Fatalf("Should have failed") } + if tt.isValid && r.Diagnostics.HasError() { t.Fatalf("Should not have failed: %v", r.Diagnostics.Errors()) } @@ -636,6 +644,7 @@ func TestRFC3339SecondsOnly(t *testing.T) { if !tt.isValid && !r.Diagnostics.HasError() { t.Fatalf("Should have failed") } + if tt.isValid && r.Diagnostics.HasError() { t.Fatalf("Should not have failed: %v", r.Diagnostics.Errors()) } @@ -720,6 +729,7 @@ func TestCIDR(t *testing.T) { if !tt.isValid && !r.Diagnostics.HasError() { t.Fatalf("Should have failed") } + if tt.isValid && r.Diagnostics.HasError() { t.Fatalf("Should not have failed: %v", r.Diagnostics.Errors()) } @@ -774,6 +784,7 @@ func TestRrule(t *testing.T) { if !tt.isValid && !r.Diagnostics.HasError() { t.Fatalf("Should have failed") } + if tt.isValid && r.Diagnostics.HasError() { t.Fatalf("Should not have failed: %v", r.Diagnostics.Errors()) } @@ -813,6 +824,7 @@ func TestFileExists(t *testing.T) { if !tt.isValid && !r.Diagnostics.HasError() { t.Fatalf("Should have failed") } + if tt.isValid && r.Diagnostics.HasError() { t.Fatalf("Should not have failed: %v", r.Diagnostics.Errors()) } @@ -889,6 +901,7 @@ func TestValidTtlDuration(t *testing.T) { if !tt.isValid && !r.Diagnostics.HasError() { t.Fatalf("Expected validation to fail for input: %v", tt.input) } + if tt.isValid && r.Diagnostics.HasError() { t.Fatalf("Expected validation to succeed for input: %v, but got errors: %v", tt.input, r.Diagnostics.Errors()) } @@ -960,6 +973,7 @@ func TestValidNoTrailingNewline(t *testing.T) { if !tt.isValid && !r.Diagnostics.HasError() { t.Fatalf("Expected validation to fail for input: %q", tt.input) } + if tt.isValid && r.Diagnostics.HasError() { t.Fatalf("Expected validation to succeed for input: %q, but got errors: %v", tt.input, r.Diagnostics.Errors()) } diff --git a/stackit/provider.go b/stackit/provider.go index 5f51fc33c..0386881c4 100644 --- a/stackit/provider.go +++ b/stackit/provider.go @@ -92,7 +92,7 @@ import ( sqlServerFlexUser "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/sqlserverflex/user" ) -// Ensure the implementation satisfies the expected interfaces +// Ensure the implementation satisfies the expected interfaces. var ( _ provider.Provider = &Provider{} ) @@ -371,12 +371,14 @@ func (p *Provider) Configure(ctx context.Context, req provider.ConfigureRequest, var providerConfig providerModel diags := req.Config.Get(ctx, &providerConfig) resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { return } // Configure SDK client sdkConfig := &config.Configuration{} + var providerData core.ProviderData // Helper function to set a string field if it's known and not null @@ -393,6 +395,7 @@ func (p *Provider) Configure(ctx context.Context, req provider.ConfigureRequest, if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring provider", fmt.Sprintf("Setting up bool value: %v", diags.Errors())) } + setter(val.ValueBool()) } } @@ -408,7 +411,7 @@ func (p *Provider) Configure(ctx context.Context, req provider.ConfigureRequest, // Provider Data Configuration setStringField(providerConfig.DefaultRegion, func(v string) { providerData.DefaultRegion = v }) - setStringField(providerConfig.Region, func(v string) { providerData.Region = v }) // nolint:staticcheck // preliminary handling of deprecated attribute + setStringField(providerConfig.Region, func(v string) { providerData.Region = v }) //nolint:staticcheck // preliminary handling of deprecated attribute setStringField(providerConfig.CdnCustomEndpoint, func(v string) { providerData.CdnCustomEndpoint = v }) setStringField(providerConfig.DNSCustomEndpoint, func(v string) { providerData.DnsCustomEndpoint = v }) setStringField(providerConfig.GitCustomEndpoint, func(v string) { providerData.GitCustomEndpoint = v }) @@ -434,12 +437,14 @@ func (p *Provider) Configure(ctx context.Context, req provider.ConfigureRequest, setStringField(providerConfig.ServiceEnablementCustomEndpoint, func(v string) { providerData.ServiceEnablementCustomEndpoint = v }) setBoolField(providerConfig.EnableBetaResources, func(v bool) { providerData.EnableBetaResources = v }) - if !(providerConfig.Experiments.IsUnknown() || providerConfig.Experiments.IsNull()) { + if !providerConfig.Experiments.IsUnknown() && !providerConfig.Experiments.IsNull() { var experimentValues []string + diags := providerConfig.Experiments.ElementsAs(ctx, &experimentValues, false) if diags.HasError() { core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring provider", fmt.Sprintf("Setting up experiments: %v", diags.Errors())) } + providerData.Experiments = experimentValues } diff --git a/stackit/provider_acc_test.go b/stackit/provider_acc_test.go index 24eb81f86..c3f68dc86 100644 --- a/stackit/provider_acc_test.go +++ b/stackit/provider_acc_test.go @@ -33,16 +33,18 @@ var testConfigProviderCredentials = config.Variables{ // Based on os.UserHomeDir(). func getHomeEnvVariableName() string { env := "HOME" + switch runtime.GOOS { case "windows": env = "USERPROFILE" case "plan9": env = "home" } + return env } -// create temporary home and initialize the credentials file as well +// create temporary home and initialize the credentials file as well. func createTemporaryHome(createValidCredentialsFile bool, t *testing.T) string { // create a temporary file tempHome, err := os.MkdirTemp("", "tempHome") @@ -57,10 +59,12 @@ func createTemporaryHome(createValidCredentialsFile bool, t *testing.T) string { } filePath := path.Join(stackitFolder, "credentials.json") + file, err := os.Create(filePath) if err != nil { t.Fatalf("Failed to create credentials file: %v", err) } + defer func() { if err := file.Close(); err != nil { t.Fatalf("Error while closing the file: %v", err) @@ -72,6 +76,7 @@ func createTemporaryHome(createValidCredentialsFile bool, t *testing.T) string { if createValidCredentialsFile { token = testutil.GetTestProjectServiceAccountToken("") } + content := fmt.Sprintf(` { "STACKIT_SERVICE_ACCOUNT_TOKEN": "%s" @@ -84,7 +89,7 @@ func createTemporaryHome(createValidCredentialsFile bool, t *testing.T) string { return tempHome } -// Function to overwrite the home folder +// Function to overwrite the home folder. func setTemporaryHome(tempHomePath string) { env := getHomeEnvVariableName() if err := os.Setenv(env, tempHomePath); err != nil { @@ -92,11 +97,12 @@ func setTemporaryHome(tempHomePath string) { } } -// cleanup the temporary home and reset the environment variable +// cleanup the temporary home and reset the environment variable. func cleanupTemporaryHome(tempHomePath string, t *testing.T) { if err := os.RemoveAll(tempHomePath); err != nil { t.Fatalf("Error cleaning up temporary folder: %v", err) } + originalHomeDir, err := os.UserHomeDir() if err != nil { t.Fatalf("Failed to restore home directory back to normal: %v", err) @@ -113,6 +119,7 @@ func getServiceAccountToken() (string, error) { if !set || token == "" { return "", fmt.Errorf("Token not set, please set TF_ACC_TEST_PROJECT_SERVICE_ACCOUNT_TOKEN to a valid token to perform tests") } + return token, nil } @@ -122,6 +129,7 @@ func TestAccEnvVarTokenValid(t *testing.T) { t.Skipf( "Acceptance tests skipped unless env '%s' set", resource.EnvTfAcc) + return } @@ -132,6 +140,7 @@ func TestAccEnvVarTokenValid(t *testing.T) { t.Setenv("STACKIT_CREDENTIALS_PATH", "") t.Setenv("STACKIT_SERVICE_ACCOUNT_TOKEN", token) + tempHomeFolder := createTemporaryHome(false, t) defer cleanupTemporaryHome(tempHomeFolder, t) resource.Test(t, resource.TestCase{ @@ -149,6 +158,7 @@ func TestAccEnvVarTokenValid(t *testing.T) { func TestAccEnvVarTokenInvalid(t *testing.T) { t.Setenv("STACKIT_CREDENTIALS_PATH", "") t.Setenv("STACKIT_SERVICE_ACCOUNT_TOKEN", "foo") + tempHomeFolder := createTemporaryHome(false, t) defer cleanupTemporaryHome(tempHomeFolder, t) resource.Test(t, resource.TestCase{ @@ -167,6 +177,7 @@ func TestAccEnvVarTokenInvalid(t *testing.T) { func TestAccCredentialsFileValid(t *testing.T) { t.Setenv("STACKIT_CREDENTIALS_PATH", "") t.Setenv("STACKIT_SERVICE_ACCOUNT_TOKEN", "") + tempHomeFolder := createTemporaryHome(true, t) defer cleanupTemporaryHome(tempHomeFolder, t) resource.Test(t, resource.TestCase{ @@ -184,6 +195,7 @@ func TestAccCredentialsFileValid(t *testing.T) { func TestAccCredentialsFileInvalid(t *testing.T) { t.Setenv("STACKIT_CREDENTIALS_PATH", "") t.Setenv("STACKIT_SERVICE_ACCOUNT_TOKEN", "") + tempHomeFolder := createTemporaryHome(false, t) defer cleanupTemporaryHome(tempHomeFolder, t) resource.Test(t, resource.TestCase{ @@ -205,6 +217,7 @@ func TestAccProviderConfigureValidValues(t *testing.T) { t.Skipf( "Acceptance tests skipped unless env '%s' set", resource.EnvTfAcc) + return } // use service account token for these tests @@ -215,6 +228,7 @@ func TestAccProviderConfigureValidValues(t *testing.T) { t.Setenv("STACKIT_CREDENTIALS_PATH", "") t.Setenv("STACKIT_SERVICE_ACCOUNT_TOKEN", token) + tempHomeFolder := createTemporaryHome(true, t) defer cleanupTemporaryHome(tempHomeFolder, t) resource.Test(t, resource.TestCase{ @@ -234,6 +248,7 @@ func TestAccProviderConfigureAnInvalidValue(t *testing.T) { t.Skipf( "Acceptance tests skipped unless env '%s' set", resource.EnvTfAcc) + return } // use service account token for these tests @@ -244,6 +259,7 @@ func TestAccProviderConfigureAnInvalidValue(t *testing.T) { t.Setenv("STACKIT_CREDENTIALS_PATH", "") t.Setenv("STACKIT_SERVICE_ACCOUNT_TOKEN", token) + tempHomeFolder := createTemporaryHome(true, t) defer cleanupTemporaryHome(tempHomeFolder, t) resource.Test(t, resource.TestCase{