diff --git a/scripts/project.sh b/scripts/project.sh index 8c2879cd7..b21b04f89 100755 --- a/scripts/project.sh +++ b/scripts/project.sh @@ -15,6 +15,8 @@ elif [ "$action" = "tools" ]; then cd ${ROOT_DIR} go mod download + (cd tools && go mod download) + (cd tools/testdata && go mod download) go install github.com/hashicorp/terraform-plugin-docs/cmd/tfplugindocs@v0.24.0 go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.12.2 diff --git a/stackit/internal/services/dns/recordset/resource.go b/stackit/internal/services/dns/recordset/resource.go index 6e1baa3d1..6ed634ca3 100644 --- a/stackit/internal/services/dns/recordset/resource.go +++ b/stackit/internal/services/dns/recordset/resource.go @@ -211,7 +211,7 @@ func (r *recordSetResource) Create(ctx context.Context, req resource.CreateReque return } - waiterTimeout := wait.CreateRecordSetWaitHandler(ctx, r.client.DefaultAPI, "", "", "").GetTimeout() //nolint:tfctxinit,tfwriteid // false positive - only called to get default wait handler timeout value + waiterTimeout := wait.CreateRecordSetWaitHandler(ctx, r.client.DefaultAPI, "", "", "").GetTimeout() createTimeout, diags := model.Timeouts.Create(ctx, waiterTimeout+core.DefaultTimeoutMargin) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { @@ -347,7 +347,7 @@ func (r *recordSetResource) Update(ctx context.Context, req resource.UpdateReque return } - waiterTimeout := wait.PartialUpdateRecordSetWaitHandler(ctx, r.client.DefaultAPI, "", "", "").GetTimeout() //nolint:tfctxinit,tfwriteid // false positive - only called to get default wait handler timeout value + waiterTimeout := wait.PartialUpdateRecordSetWaitHandler(ctx, r.client.DefaultAPI, "", "", "").GetTimeout() updateTimeout, diags := model.Timeouts.Update(ctx, waiterTimeout+core.DefaultTimeoutMargin) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { @@ -409,7 +409,7 @@ func (r *recordSetResource) Delete(ctx context.Context, req resource.DeleteReque return } - waiterTimeout := wait.DeleteRecordSetWaitHandler(ctx, r.client.DefaultAPI, "", "", "").GetTimeout() //nolint:tfctxinit,tfwriteid // false positive - only called to get default wait handler timeout value + waiterTimeout := wait.DeleteRecordSetWaitHandler(ctx, r.client.DefaultAPI, "", "", "").GetTimeout() deleteTimeout, diags := model.Timeouts.Delete(ctx, waiterTimeout+core.DefaultTimeoutMargin) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { diff --git a/stackit/internal/services/dns/zone/resource.go b/stackit/internal/services/dns/zone/resource.go index a31b660c2..114071e12 100644 --- a/stackit/internal/services/dns/zone/resource.go +++ b/stackit/internal/services/dns/zone/resource.go @@ -304,7 +304,7 @@ func (r *zoneResource) Create(ctx context.Context, req resource.CreateRequest, r return } - waiterTimeout := wait.CreateZoneWaitHandler(ctx, r.client.DefaultAPI, "", "").GetTimeout() //nolint:tfctxinit,tfwriteid // false positive - only called to get default wait handler timeout value + waiterTimeout := wait.CreateZoneWaitHandler(ctx, r.client.DefaultAPI, "", "").GetTimeout() createTimeout, diags := model.Timeouts.Create(ctx, waiterTimeout+core.DefaultTimeoutMargin) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { @@ -435,7 +435,7 @@ func (r *zoneResource) Update(ctx context.Context, req resource.UpdateRequest, r return } - waiterTimeout := wait.PartialUpdateZoneWaitHandler(ctx, r.client.DefaultAPI, "", "").GetTimeout() //nolint:tfctxinit,tfwriteid // false positive - only called to get default wait handler timeout value + waiterTimeout := wait.PartialUpdateZoneWaitHandler(ctx, r.client.DefaultAPI, "", "").GetTimeout() updateTimeout, diags := model.Timeouts.Update(ctx, waiterTimeout+core.DefaultTimeoutMargin) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { @@ -495,7 +495,7 @@ func (r *zoneResource) Delete(ctx context.Context, req resource.DeleteRequest, r return } - waiterTimeout := wait.DeleteZoneWaitHandler(ctx, r.client.DefaultAPI, "", "").GetTimeout() //nolint:tfctxinit,tfwriteid // false positive - only called to get default wait handler timeout value + waiterTimeout := wait.DeleteZoneWaitHandler(ctx, r.client.DefaultAPI, "", "").GetTimeout() deleteTimeout, diags := model.Timeouts.Delete(ctx, waiterTimeout+core.DefaultTimeoutMargin) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { diff --git a/stackit/internal/services/dremio/instance/resource.go b/stackit/internal/services/dremio/instance/resource.go index 425b1d435..842ab8ba5 100644 --- a/stackit/internal/services/dremio/instance/resource.go +++ b/stackit/internal/services/dremio/instance/resource.go @@ -396,7 +396,7 @@ func (r *instanceResource) Create(ctx context.Context, req resource.CreateReques return } - waiterTimeout := dremioWaiter.CreateDremioWaitHandler(ctx, r.client.DefaultAPI, "", "", "").GetTimeout() //nolint:tfctxinit,tfwriteid // false positive - only called to get default wait handler timeout value + waiterTimeout := dremioWaiter.CreateDremioWaitHandler(ctx, r.client.DefaultAPI, "", "", "").GetTimeout() createTimeout, diags := model.Timeouts.Create(ctx, waiterTimeout+core.DefaultTimeoutMargin) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { @@ -521,7 +521,7 @@ func (r *instanceResource) Update(ctx context.Context, req resource.UpdateReques return } - waiterTimeout := dremioWaiter.UpdateDremioWaitHandler(ctx, r.client.DefaultAPI, "", "", "").GetTimeout() //nolint:tfctxinit,tfwriteid // false positive - only called to get default wait handler timeout value + waiterTimeout := dremioWaiter.UpdateDremioWaitHandler(ctx, r.client.DefaultAPI, "", "", "").GetTimeout() updateTimeout, diags := model.Timeouts.Update(ctx, waiterTimeout+core.DefaultTimeoutMargin) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { @@ -592,7 +592,7 @@ func (r *instanceResource) Delete(ctx context.Context, req resource.DeleteReques return } - waiterTimeout := dremioWaiter.DeleteDremioWaitHandler(ctx, r.client.DefaultAPI, "", "", "").GetTimeout() //nolint:tfctxinit,tfwriteid // false positive - only called to get default wait handler timeout value + waiterTimeout := dremioWaiter.DeleteDremioWaitHandler(ctx, r.client.DefaultAPI, "", "", "").GetTimeout() deleteTimeout, diags := model.Timeouts.Delete(ctx, waiterTimeout+core.DefaultTimeoutMargin) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { diff --git a/stackit/internal/services/dremio/user/resource.go b/stackit/internal/services/dremio/user/resource.go index 8cd0bca0b..e22c3d62f 100644 --- a/stackit/internal/services/dremio/user/resource.go +++ b/stackit/internal/services/dremio/user/resource.go @@ -243,7 +243,7 @@ func (r *userResource) Create(ctx context.Context, req resource.CreateRequest, r return } - waiterTimeout := dremioWaiter.CreateDremioUserWaitHandler(ctx, r.client.DefaultAPI, "", "", "", "").GetTimeout() //nolint:tfctxinit,tfwriteid // false positive - only called to get default wait handler timeout value + waiterTimeout := dremioWaiter.CreateDremioUserWaitHandler(ctx, r.client.DefaultAPI, "", "", "", "").GetTimeout() createTimeout, diags := model.Timeouts.Create(ctx, waiterTimeout+core.DefaultTimeoutMargin) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { @@ -376,7 +376,7 @@ func (r *userResource) Delete(ctx context.Context, req resource.DeleteRequest, r return } - waiterTimeout := dremioWaiter.DeleteDremioUserWaitHandler(ctx, r.client.DefaultAPI, "", "", "", "").GetTimeout() //nolint:tfctxinit,tfwriteid // false positive - only called to get default wait handler timeout value + waiterTimeout := dremioWaiter.DeleteDremioUserWaitHandler(ctx, r.client.DefaultAPI, "", "", "", "").GetTimeout() deleteTimeout, diags := model.Timeouts.Delete(ctx, waiterTimeout+core.DefaultTimeoutMargin) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { diff --git a/stackit/internal/services/iaas/image/resource.go b/stackit/internal/services/iaas/image/resource.go index 2441818b4..1d8fbb28c 100644 --- a/stackit/internal/services/iaas/image/resource.go +++ b/stackit/internal/services/iaas/image/resource.go @@ -475,9 +475,10 @@ func (r *imageResource) Create(ctx context.Context, req resource.CreateRequest, } // Wait for image to become available - waiter := wait.UploadImageWaitHandler(ctx, r.client.DefaultAPI, projectId, region, imageCreateResp.Id) //nolint:tfwriteid // false positive - id fields are actually stored already using the mapFields() call above - waiter = waiter.SetTimeout(7 * 24 * time.Hour) // Set timeout to one week, to make the timeout useless - waitResp, err := waiter.WaitWithContext(ctx) + waiter := wait.UploadImageWaitHandler(ctx, r.client.DefaultAPI, projectId, region, imageCreateResp.Id) + // Set timeout to one week, to make the timeout useless + waiter = waiter.SetTimeout(7 * 24 * time.Hour) + waitResp, err := waiter.WaitWithContext(ctx) //nolint:tfwriteid // false positive - id fields are actually stored already using the mapFields() call above if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating image", fmt.Sprintf("Waiting for image to become available: %v", err)) return diff --git a/stackit/internal/services/iaasalpha/vpcnetworkrange/resource.go b/stackit/internal/services/iaasalpha/vpcnetworkrange/resource.go index 77d3f2c2a..69eda90e2 100644 --- a/stackit/internal/services/iaasalpha/vpcnetworkrange/resource.go +++ b/stackit/internal/services/iaasalpha/vpcnetworkrange/resource.go @@ -256,7 +256,7 @@ func (r *vpcNetworkRangeResource) Create(ctx context.Context, req resource.Creat return } - waiterTimeout := wait.CreateVPCNetworkRangeWaitHandler(ctx, r.client.DefaultAPI, "", "", "", "").GetTimeout() //nolint:tfctxinit,tfwriteid // false positive - only called to get default wait handler timeout value + waiterTimeout := wait.CreateVPCNetworkRangeWaitHandler(ctx, r.client.DefaultAPI, "", "", "", "").GetTimeout() createTimeout, diags := model.Timeouts.Create(ctx, waiterTimeout+core.DefaultTimeoutMargin) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { @@ -396,7 +396,7 @@ func (r *vpcNetworkRangeResource) Update(ctx context.Context, req resource.Updat return } - waiterTimeout := wait.UpdateVPCNetworkRangeWaitHandler(ctx, r.client.DefaultAPI, "", "", "", "").GetTimeout() //nolint:tfctxinit,tfwriteid // false positive - only called to get default wait handler timeout value + waiterTimeout := wait.UpdateVPCNetworkRangeWaitHandler(ctx, r.client.DefaultAPI, "", "", "", "").GetTimeout() updateTimeout, diags := model.Timeouts.Update(ctx, waiterTimeout+core.DefaultTimeoutMargin) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { @@ -468,7 +468,7 @@ func (r *vpcNetworkRangeResource) Delete(ctx context.Context, req resource.Delet return } - waiterTimeout := wait.DeleteVPCNetworkRangeWaitHandler(ctx, r.client.DefaultAPI, "", "", "", "").GetTimeout() //nolint:tfctxinit,tfwriteid // false positive - only called to get default wait handler timeout value + waiterTimeout := wait.DeleteVPCNetworkRangeWaitHandler(ctx, r.client.DefaultAPI, "", "", "", "").GetTimeout() updateTimeout, diags := model.Timeouts.Delete(ctx, waiterTimeout+core.DefaultTimeoutMargin) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { diff --git a/stackit/internal/services/iaasalpha/vpcregion/resource.go b/stackit/internal/services/iaasalpha/vpcregion/resource.go index 505256fa3..af0eb650b 100644 --- a/stackit/internal/services/iaasalpha/vpcregion/resource.go +++ b/stackit/internal/services/iaasalpha/vpcregion/resource.go @@ -166,7 +166,7 @@ func (v *vpcRegion) Create(ctx context.Context, request resource.CreateRequest, return } - waiterTimeout := wait.CreateVPCRegionWaitHandler(ctx, v.client.DefaultAPI, "", "", "").GetTimeout() //nolint:tfctxinit,tfwriteid // false positive - only called to get default wait handler timeout value + waiterTimeout := wait.CreateVPCRegionWaitHandler(ctx, v.client.DefaultAPI, "", "", "").GetTimeout() createTimeout, diags := model.Timeouts.Create(ctx, waiterTimeout+core.DefaultTimeoutMargin) response.Diagnostics.Append(diags...) if response.Diagnostics.HasError() { @@ -331,7 +331,7 @@ func (v *vpcRegion) Delete(ctx context.Context, request resource.DeleteRequest, return } - waiterTimeout := wait.DeleteVPCRegionWaitHandler(ctx, v.client.DefaultAPI, "", "", "").GetTimeout() //nolint:tfctxinit,tfwriteid // false positive - only called to get default wait handler timeout value + waiterTimeout := wait.DeleteVPCRegionWaitHandler(ctx, v.client.DefaultAPI, "", "", "").GetTimeout() deleteTimeout, diags := model.Timeouts.Delete(ctx, waiterTimeout+core.DefaultTimeoutMargin) response.Diagnostics.Append(diags...) if response.Diagnostics.HasError() { diff --git a/stackit/internal/services/objectstorage/credential/resource.go b/stackit/internal/services/objectstorage/credential/resource.go index 996e77fdc..36ad7b7cf 100644 --- a/stackit/internal/services/objectstorage/credential/resource.go +++ b/stackit/internal/services/objectstorage/credential/resource.go @@ -381,7 +381,7 @@ func (r *credentialResource) Read(ctx context.Context, req resource.ReadRequest, return } - ctx = core.LogResponse(ctx) //nolint:tflogresponse // false positive - SDK call is actually done inside readCredentials() + ctx = core.LogResponse(ctx) if !found { resp.State.RemoveResource(ctx) diff --git a/stackit/internal/services/objectstorage/credentialsgroup/datasource.go b/stackit/internal/services/objectstorage/credentialsgroup/datasource.go index 18c952544..cd090c745 100644 --- a/stackit/internal/services/objectstorage/credentialsgroup/datasource.go +++ b/stackit/internal/services/objectstorage/credentialsgroup/datasource.go @@ -129,7 +129,7 @@ func (r *credentialsGroupDataSource) Read(ctx context.Context, req datasource.Re return } - ctx = core.LogResponse(ctx) //nolint:tflogresponse // false positive - SDK call is actually done inside readCredentialsGroups() + ctx = core.LogResponse(ctx) if !found { resp.State.RemoveResource(ctx) diff --git a/stackit/internal/services/objectstorage/credentialsgroup/resource.go b/stackit/internal/services/objectstorage/credentialsgroup/resource.go index 3a46711af..e0c34f284 100644 --- a/stackit/internal/services/objectstorage/credentialsgroup/resource.go +++ b/stackit/internal/services/objectstorage/credentialsgroup/resource.go @@ -266,7 +266,7 @@ func (r *credentialsGroupResource) Read(ctx context.Context, req resource.ReadRe return } - ctx = core.LogResponse(ctx) //nolint:tflogresponse // false positive - SDK call is hidden in readCredentialsGroups() + ctx = core.LogResponse(ctx) if !found { resp.State.RemoveResource(ctx) diff --git a/stackit/internal/services/postgresflex/database/datasource.go b/stackit/internal/services/postgresflex/database/datasource.go index c7f7edb94..5d56048b9 100644 --- a/stackit/internal/services/postgresflex/database/datasource.go +++ b/stackit/internal/services/postgresflex/database/datasource.go @@ -161,7 +161,7 @@ func (r *databaseDataSource) Read(ctx context.Context, req datasource.ReadReques return } - ctx = core.LogResponse(ctx) //nolint:tflogresponse // false positive - sdk call is hidden in getDatabase() + ctx = core.LogResponse(ctx) // Map response body to schema and populate Computed attribute values err = mapFields(databaseResp, &model, region) diff --git a/stackit/internal/services/postgresflex/database/resource.go b/stackit/internal/services/postgresflex/database/resource.go index 4e5232186..c54063aef 100644 --- a/stackit/internal/services/postgresflex/database/resource.go +++ b/stackit/internal/services/postgresflex/database/resource.go @@ -301,7 +301,7 @@ func (r *databaseResource) Read(ctx context.Context, req resource.ReadRequest, r return } - ctx = core.LogResponse(ctx) //nolint:tflogresponse // false positive - sdk call is hidden in getDatabase() + ctx = core.LogResponse(ctx) // Map response body to schema err = mapFields(databaseResp, &model, region) diff --git a/stackit/internal/services/ske/kubeconfig/resource.go b/stackit/internal/services/ske/kubeconfig/resource.go index 348b45955..cb2d9ea65 100644 --- a/stackit/internal/services/ske/kubeconfig/resource.go +++ b/stackit/internal/services/ske/kubeconfig/resource.go @@ -266,7 +266,7 @@ func (r *kubeconfigResource) Create(ctx context.Context, req resource.CreateRequ err := r.createKubeconfig(ctx, &model) - ctx = core.LogResponse(ctx) //nolint:tflogresponse // SDK call is hidden in r.createKubeconfig() + ctx = core.LogResponse(ctx) if err != nil { core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating kubeconfig", fmt.Sprintf("Creating kubeconfig: %v", err)) diff --git a/tools/go.mod b/tools/go.mod index 22dd2b053..201808491 100644 --- a/tools/go.mod +++ b/tools/go.mod @@ -6,3 +6,8 @@ require ( github.com/golangci/plugin-module-register v0.1.2 golang.org/x/tools v0.45.0 ) + +require ( + golang.org/x/mod v0.36.0 // indirect + golang.org/x/sync v0.20.0 // indirect +) diff --git a/tools/go.sum b/tools/go.sum index bdfff8dcb..fd1fd4c39 100644 --- a/tools/go.sum +++ b/tools/go.sum @@ -1,4 +1,10 @@ github.com/golangci/plugin-module-register v0.1.2 h1:e5WM6PO6NIAEcij3B053CohVp3HIYbzSuP53UAYgOpg= github.com/golangci/plugin-module-register v0.1.2/go.mod h1:1+QGTsKBvAIvPvoY/os+G5eoqxWn70HYDm2uvUyGuVw= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4= +golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8= golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0= diff --git a/tools/internal/facts/servicecall/servicecall.go b/tools/internal/facts/servicecall/servicecall.go new file mode 100644 index 000000000..06e097a9f --- /dev/null +++ b/tools/internal/facts/servicecall/servicecall.go @@ -0,0 +1,235 @@ +// Package servicecall identifies functions that make STACKIT SDK service calls. +package servicecall + +import ( + "go/ast" + "go/types" + "reflect" + "strings" + + "golang.org/x/tools/go/analysis" + "golang.org/x/tools/go/analysis/passes/inspect" + "golang.org/x/tools/go/ast/inspector" + "golang.org/x/tools/go/types/typeutil" + "golang.org/x/tools/refactor/satisfy" + + "github.com/stackitcloud/terraform-provider-stackit/tools/internal/lintutils" +) + +var Analyzer = &analysis.Analyzer{ + Name: "servicecall", + Doc: "Publishes a fact for functions that call a STACKIT SDK service", + Requires: []*analysis.Analyzer{inspect.Analyzer}, + FactTypes: []analysis.Fact{ + new(IsServiceCall), + }, + ResultType: reflect.TypeOf((*Result)(nil)), + Run: run, +} + +// IsServiceCall is a fact indicating that a function calls a STACKIT SDK service. +type IsServiceCall struct{} + +func (*IsServiceCall) AFact() {} + +func (*IsServiceCall) String() string { return "serviceCall" } + +// Result provides the service-call functions identified while analyzing a package. +type Result struct { + functions map[*types.Func]struct{} +} + +// HasServiceCall reports whether fn is marked as making a STACKIT SDK service call. +func (r *Result) HasServiceCall(fn *types.Func) bool { + _, ok := r.functions[fn.Origin()] + return ok +} + +// HasServiceCall reports whether call invokes a marked service-call function +// directly or receives one as a function reference argument. +func HasServiceCall(call *ast.CallExpr, info *types.Info, result *Result) bool { + callee, _ := typeutil.Callee(info, call).(*types.Func) + if callee != nil && result.HasServiceCall(callee) { + return true + } + + for _, argument := range call.Args { + if referencedFunction := functionReference(argument, info); referencedFunction != nil && result.HasServiceCall(referencedFunction) { + return true + } + } + return false +} + +func run(pass *analysis.Pass) (any, error) { + inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) + + // callers maps a callee to the functions that call it. Once a callee is + // known to call a service, the fact is propagated to all of its callers. + callers := make(map[*types.Func]map[*types.Func]struct{}) + serviceCalls := make(map[*types.Func]struct{}) + var serviceCallers []*types.Func + + // A function that receives a known service-call function will invoke that + // function indirectly, so it is itself a service-call function. + markReferencedServiceCall := func(caller, referencedFunction *types.Func) { + if referencedFunction == nil || !isKnownServiceCall(pass, nil, referencedFunction) { + return + } + serviceCalls[referencedFunction.Origin()] = struct{}{} + serviceCallers = append(serviceCallers, caller) + } + + inspect.Preorder([]ast.Node{(*ast.FuncDecl)(nil)}, func(n ast.Node) { + decl := n.(*ast.FuncDecl) + if decl.Body == nil { + return + } + + caller, _ := pass.TypesInfo.Defs[decl.Name].(*types.Func) + if caller == nil { + return + } + caller = caller.Origin() + + ast.Inspect(decl.Body, func(n ast.Node) bool { + if _, ok := n.(*ast.FuncLit); ok { + // A call in a closure does not belong to the enclosing function. + return false + } + + call, ok := n.(*ast.CallExpr) + if !ok { + return true + } + for _, argument := range call.Args { + markReferencedServiceCall(caller, functionReference(argument, pass.TypesInfo)) + } + + callee, _ := typeutil.Callee(pass.TypesInfo, call).(*types.Func) + if callee == nil { + return true + } + callee = callee.Origin() + + if isKnownServiceCall(pass, call, callee) { + serviceCalls[callee] = struct{}{} + serviceCallers = append(serviceCallers, caller) + return true + } + if callee.Pkg() == pass.Pkg { + if callers[callee] == nil { + callers[callee] = make(map[*types.Func]struct{}) + } + callers[callee][caller] = struct{}{} + } + return true + }) + }) + + addInterfaceEdges(pass, callers) + + marked := make(map[*types.Func]struct{}) + var propagate func(*types.Func) + propagate = func(fn *types.Func) { + if _, ok := marked[fn]; ok { + return + } + marked[fn] = struct{}{} + pass.ExportObjectFact(fn, new(IsServiceCall)) + for caller := range callers[fn] { + propagate(caller) + } + } + for _, fn := range serviceCallers { + propagate(fn) + } + for fn := range marked { + serviceCalls[fn] = struct{}{} + } + + return &Result{functions: serviceCalls}, nil +} + +// addInterfaceEdges models calls through interface methods as calls to each +// implementation established by an assignment in the current package. This +// permits facts from concrete SDK service methods to propagate through the +// request's interface field to its Execute method. +func addInterfaceEdges(pass *analysis.Pass, callers map[*types.Func]map[*types.Func]struct{}) { + var finder satisfy.Finder + finder.Find(pass.TypesInfo, pass.Files) + + for assignment := range finder.Result { + iface := assignment.LHS.Underlying().(*types.Interface) + for method := range iface.Methods() { + // Facts can only be exported for objects in the current package. + if method.Pkg() != pass.Pkg { + continue + } + + implementation, _, _ := types.LookupFieldOrMethod(assignment.RHS, false, pass.Pkg, method.Name()) + implementationFunc, ok := implementation.(*types.Func) + if !ok { + continue + } + + implementationFunc = implementationFunc.Origin() + method = method.Origin() + if callers[implementationFunc] == nil { + callers[implementationFunc] = make(map[*types.Func]struct{}) + } + callers[implementationFunc][method] = struct{}{} + } + } +} + +// functionReference returns the function denoted by expr, if expr is a +// statically identifiable function value. +func functionReference(expr ast.Expr, info *types.Info) *types.Func { + switch expr := expr.(type) { + case *ast.Ident: + fn, _ := info.ObjectOf(expr).(*types.Func) + return fn + case *ast.SelectorExpr: + fn, _ := info.ObjectOf(expr.Sel).(*types.Func) + return fn + case *ast.ParenExpr: + return functionReference(expr.X, info) + case *ast.IndexExpr: + return functionReference(expr.X, info) + case *ast.IndexListExpr: + return functionReference(expr.X, info) + } + return nil +} + +func isKnownServiceCall(pass *analysis.Pass, call *ast.CallExpr, fn *types.Func) bool { + return isSDKCallAPI(fn) || + (call != nil && lintutils.IsWaitCall(pass.TypesInfo, call, fn.Name())) || + hasServiceCallFact(pass, fn) +} + +func hasServiceCallFact(pass *analysis.Pass, fn *types.Func) bool { + if fn.Pkg() == pass.Pkg { + return false + } + var fact IsServiceCall + return pass.ImportObjectFact(fn, &fact) +} + +func isSDKCallAPI(fn *types.Func) bool { + if fn.Name() != "callAPI" || fn.Pkg() == nil || !strings.HasPrefix(fn.Pkg().Path(), lintutils.StackitSdkModulePrefix) { + return false + } + + receiver := fn.Signature().Recv() + if receiver == nil { + return false + } + pointer, ok := receiver.Type().(*types.Pointer) + if !ok { + return false + } + named, ok := pointer.Elem().(*types.Named) + return ok && named.Obj().Name() == "APIClient" && named.Obj().Pkg() == fn.Pkg() +} diff --git a/tools/internal/facts/servicecall/servicecall_test.go b/tools/internal/facts/servicecall/servicecall_test.go new file mode 100644 index 000000000..c2a693cce --- /dev/null +++ b/tools/internal/facts/servicecall/servicecall_test.go @@ -0,0 +1,16 @@ +package servicecall + +import ( + "path/filepath" + "testing" + + "golang.org/x/tools/go/analysis/analysistest" +) + +func TestAnalyzer(t *testing.T) { + dir, err := filepath.Abs("../../../testdata") + if err != nil { + t.Fatal(err) + } + analysistest.Run(t, dir, Analyzer, "github.com/stackitcloud/terraform-provider-stackit/servicecall") +} diff --git a/tools/internal/lintutils/lintutil.go b/tools/internal/lintutils/lintutil.go index f4ee80564..ac2efa5e7 100644 --- a/tools/internal/lintutils/lintutil.go +++ b/tools/internal/lintutils/lintutil.go @@ -9,6 +9,46 @@ import ( const StackitSdkModulePrefix = "github.com/stackitcloud/stackit-sdk-go" +// IsWaitCall reports whether call executes an SDK async-action wait handler. +func IsWaitCall(info *types.Info, call *ast.CallExpr, calledFuncName string) bool { + // Must be a selector like handler.WaitWithContext(). + sel, ok := call.Fun.(*ast.SelectorExpr) + if !ok { + return false + } + // serviceenablement itself is used in other resources and does not have an ID itself. + if waiterCall, ok := sel.X.(*ast.CallExpr); ok { + _, waiterFuncName := GetCallInfo(waiterCall, info) + if waiterFuncName == "EnableServiceWaitHandler" { + return false + } + } + obj := info.Uses[sel.Sel] + if obj == nil { + return false + } + sig, ok := obj.Type().(*types.Signature) + if !ok { + return false + } + recv := sig.Recv() + if recv == nil { + return false + } + recvType := recv.Type() + if ptr, ok := recvType.(*types.Pointer); ok { + recvType = ptr.Elem() + } + named, ok := recvType.(*types.Named) + if !ok || named.Obj().Pkg() == nil { + return false + } + // Must be WaitWithContext on a wait.AsyncActionHandler receiver. + return named.Obj().Pkg().Path() == "github.com/stackitcloud/stackit-sdk-go/core/wait" && + named.Obj().Name() == "AsyncActionHandler" && + calledFuncName == "WaitWithContext" +} + // GetCallInfo resolves the package path and function name of a CallExpr. func GetCallInfo(call *ast.CallExpr, info *types.Info) (string, string) { var ident *ast.Ident diff --git a/tools/linters/tfctxinit/tfctxinit.go b/tools/linters/tfctxinit/tfctxinit.go index 7d0692f6d..055472e97 100644 --- a/tools/linters/tfctxinit/tfctxinit.go +++ b/tools/linters/tfctxinit/tfctxinit.go @@ -2,10 +2,10 @@ package tfctxinit import ( "go/ast" - "strings" "golang.org/x/tools/go/ast/inspector" + "github.com/stackitcloud/terraform-provider-stackit/tools/internal/facts/servicecall" "github.com/stackitcloud/terraform-provider-stackit/tools/internal/lintutils" "github.com/golangci/plugin-module-register/register" @@ -20,7 +20,7 @@ const ( var Analyzer = &analysis.Analyzer{ Name: analyzerName, Doc: "Ensures core.InitProviderContext is called before any SDK call in a Terraform resource lifecycle (CRUD) implementation", - Requires: []*analysis.Analyzer{inspect.Analyzer}, + Requires: []*analysis.Analyzer{inspect.Analyzer, servicecall.Analyzer}, Run: run, } @@ -32,6 +32,7 @@ func run(pass *analysis.Pass) (any, error) { ) inspectNode := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) + serviceCalls := pass.ResultOf[servicecall.Analyzer].(*servicecall.Result) // Filter only for function declarations (Terraform CRUD methods) nodeFilter := []ast.Node{ @@ -64,8 +65,8 @@ func run(pass *analysis.Pass) (any, error) { hasCalledUtil = true } - // Check if we've hit a STACKIT SDK call before the util function - if strings.HasPrefix(pkgPath, lintutils.StackitSdkModulePrefix) && !hasCalledUtil { + // Check if we've hit a service call before the util function + if !hasCalledUtil && servicecall.HasServiceCall(call, pass.TypesInfo, serviceCalls) { pass.Reportf( call.Pos(), "%s: call to %s must happen AFTER %s.%s is called in %s", diff --git a/tools/linters/tfctxinit/tfctxinit_test.go b/tools/linters/tfctxinit/tfctxinit_test.go new file mode 100644 index 000000000..b764a696d --- /dev/null +++ b/tools/linters/tfctxinit/tfctxinit_test.go @@ -0,0 +1,16 @@ +package tfctxinit + +import ( + "path/filepath" + "testing" + + "golang.org/x/tools/go/analysis/analysistest" +) + +func TestAnalyzer(t *testing.T) { + dir, err := filepath.Abs("../../testdata") + if err != nil { + t.Fatal(err) + } + analysistest.Run(t, dir, Analyzer, "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/tfctxinit") +} diff --git a/tools/linters/tflogresponse/tflogresponse.go b/tools/linters/tflogresponse/tflogresponse.go index e75fb8434..1b94f4b50 100644 --- a/tools/linters/tflogresponse/tflogresponse.go +++ b/tools/linters/tflogresponse/tflogresponse.go @@ -2,13 +2,13 @@ package tflogresponse import ( "go/ast" - "strings" "github.com/golangci/plugin-module-register/register" "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/analysis/passes/inspect" "golang.org/x/tools/go/ast/inspector" + "github.com/stackitcloud/terraform-provider-stackit/tools/internal/facts/servicecall" "github.com/stackitcloud/terraform-provider-stackit/tools/internal/lintutils" ) @@ -19,7 +19,7 @@ const ( var Analyzer = &analysis.Analyzer{ Name: analyzerName, Doc: "Ensures that core.LogResponse is called in every resource/datasource CRUD method after ctx.InitProviderContext was called and at least one STACKIT SDK call was made.", - Requires: []*analysis.Analyzer{inspect.Analyzer}, + Requires: []*analysis.Analyzer{inspect.Analyzer, servicecall.Analyzer}, Run: run, } @@ -31,6 +31,7 @@ func run(pass *analysis.Pass) (any, error) { ) inspectNode := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) + serviceCalls := pass.ResultOf[servicecall.Analyzer].(*servicecall.Result) // Filter only for function declarations (Terraform CRUD methods) nodeFilter := []ast.Node{ @@ -76,8 +77,7 @@ func run(pass *analysis.Pass) (any, error) { } case stateLookingForSdkOrLogResponseCall: - // Check if this call belongs to STACKIT SDK modules - if strings.HasPrefix(pkgPath, lintutils.StackitSdkModulePrefix) { + if servicecall.HasServiceCall(call, pass.TypesInfo, serviceCalls) { foundIntermediateSdkModuleCall = true } else if pkgPath == utilPkg && calledFuncName == funcLogResponse { if !foundIntermediateSdkModuleCall { diff --git a/tools/linters/tflogresponse/tflogresponse_test.go b/tools/linters/tflogresponse/tflogresponse_test.go new file mode 100644 index 000000000..ef1c7416d --- /dev/null +++ b/tools/linters/tflogresponse/tflogresponse_test.go @@ -0,0 +1,17 @@ +package tflogresponse + +import ( + "path/filepath" + "testing" + + "golang.org/x/tools/go/analysis/analysistest" +) + +func TestAnalyzer(t *testing.T) { + dir, err := filepath.Abs("../../testdata") + if err != nil { + t.Fatal(err) + } + + analysistest.Run(t, dir, Analyzer, "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/tflogresponse") +} diff --git a/tools/linters/tfwriteid/tfwriteid.go b/tools/linters/tfwriteid/tfwriteid.go index 5981217c5..77f97df55 100644 --- a/tools/linters/tfwriteid/tfwriteid.go +++ b/tools/linters/tfwriteid/tfwriteid.go @@ -2,7 +2,6 @@ package tfwriteid import ( "go/ast" - "strings" "github.com/golangci/plugin-module-register/register" "golang.org/x/tools/go/analysis" @@ -63,7 +62,8 @@ func run(pass *analysis.Pass) (interface{}, error) { } // Check if we've hit a STACKIT SDK wait handler call before the util function - if strings.HasPrefix(pkgPath, lintutils.StackitSdkModulePrefix) && strings.Contains(pkgPath, "wait") && !strings.HasPrefix(pkgPath, "github.com/stackitcloud/stackit-sdk-go/services/serviceenablement") && pkgPath != "github.com/stackitcloud/stackit-sdk-go/core/wait" && !hasCalledUtil { + callsWait := lintutils.IsWaitCall(pass.TypesInfo, call, calledFuncName) + if callsWait && !hasCalledUtil { pass.Reportf( call.Pos(), "%s: call to wait handler from %s must happen AFTER %s.%s is called in %s %s", diff --git a/tools/linters/tfwriteid/tfwriteid_test.go b/tools/linters/tfwriteid/tfwriteid_test.go new file mode 100644 index 000000000..c068deb6e --- /dev/null +++ b/tools/linters/tfwriteid/tfwriteid_test.go @@ -0,0 +1,16 @@ +package tfwriteid + +import ( + "path/filepath" + "testing" + + "golang.org/x/tools/go/analysis/analysistest" +) + +func TestAnalyzer(t *testing.T) { + dir, err := filepath.Abs("../../testdata") + if err != nil { + t.Fatal(err) + } + analysistest.Run(t, dir, Analyzer, "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/tfwriteid") +} diff --git a/tools/testdata/go.mod b/tools/testdata/go.mod new file mode 100644 index 000000000..1b4779fba --- /dev/null +++ b/tools/testdata/go.mod @@ -0,0 +1,15 @@ +module github.com/stackitcloud/terraform-provider-stackit + +go 1.25.9 + +require ( + github.com/stackitcloud/stackit-sdk-go/core v0.26.0 + github.com/stackitcloud/stackit-sdk-go/services/iaas v1.13.0 + github.com/stackitcloud/stackit-sdk-go/services/serviceenablement v1.7.0 +) + +require ( + github.com/golang-jwt/jwt/v5 v5.3.1 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/stackitcloud/stackit-sdk-go/services/resourcemanager v0.24.0 // indirect +) diff --git a/tools/testdata/go.sum b/tools/testdata/go.sum new file mode 100644 index 000000000..7c933a504 --- /dev/null +++ b/tools/testdata/go.sum @@ -0,0 +1,14 @@ +github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= +github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/stackitcloud/stackit-sdk-go/core v0.26.0 h1:jQEb9gkehfp6VCP6TcYk7BI10cz4l0KM2L6hqYBH2QA= +github.com/stackitcloud/stackit-sdk-go/core v0.26.0/go.mod h1:WU1hhxnjXw2EV7CYa1nlEvNpMiRY6CvmIOaHuL3pOaA= +github.com/stackitcloud/stackit-sdk-go/services/iaas v1.13.0 h1:PQZ6n71CMadLU3DjJxJXiPHdK9Bz4hMttBREQD6no34= +github.com/stackitcloud/stackit-sdk-go/services/iaas v1.13.0/go.mod h1:AbPN9BGkdjc+tVsXEX9Vr8BPDjdlDmG26K1FwCKZQVU= +github.com/stackitcloud/stackit-sdk-go/services/resourcemanager v0.24.0 h1:JPP6a0ME1tZXr4iB69d/LtJsCAr58ENBadFaK9f48/c= +github.com/stackitcloud/stackit-sdk-go/services/resourcemanager v0.24.0/go.mod h1:NEz3f+GV5G++BE9/MmZCsXJyCih7jtg0pZuSyG2sLEs= +github.com/stackitcloud/stackit-sdk-go/services/serviceenablement v1.7.0 h1:TNZHrunhsXRbuqZcucLs2Gqy1sEyvabufM7pB5Tscmo= +github.com/stackitcloud/stackit-sdk-go/services/serviceenablement v1.7.0/go.mod h1:fXq3TmVLb4JMSve989NFFViMFoYa83s7M3hJWgN6mdQ= diff --git a/tools/testdata/servicecall/servicecall.go b/tools/testdata/servicecall/servicecall.go new file mode 100644 index 000000000..11dc1b110 --- /dev/null +++ b/tools/testdata/servicecall/servicecall.go @@ -0,0 +1,38 @@ +package servicecall + +import ( + "context" + + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api/wait" +) + +func callsService(service *iaas.DefaultAPIService) { // want callsService:"serviceCall" + service.AddNetworkToServerExecute(iaas.ApiAddNetworkToServerRequest{}) +} + +func propagatesServiceCall(service *iaas.DefaultAPIService) { // want propagatesServiceCall:"serviceCall" + callsService(service) +} + +func propagatesAgain(service *iaas.DefaultAPIService) { // want propagatesAgain:"serviceCall" + propagatesServiceCall(service) +} + +func doesNotCallService(client iaas.DefaultAPI) { + client.AddNetworkToServer(context.Background(), "", "", "", "") +} + +func callsServiceInterface(client iaas.DefaultAPI) { // want callsServiceInterface:"serviceCall" + client.AddNetworkToServer(context.Background(), "", "", "", "").Execute() +} + +func waiterCallIsServiceCall(client iaas.DefaultAPI) { // want waiterCallIsServiceCall:"serviceCall" + ctx := context.Background() + wait.CreateSnapshotWaitHandler(ctx, client, "", "", "").WaitWithContext(ctx) +} + +func waiterCreationIsNotAServiceCall(client iaas.DefaultAPI) { + ctx := context.Background() + wait.CreateSnapshotWaitHandler(ctx, client, "", "", "") +} diff --git a/tools/testdata/stackit/internal/core/core.go b/tools/testdata/stackit/internal/core/core.go new file mode 100644 index 000000000..bb3fc9459 --- /dev/null +++ b/tools/testdata/stackit/internal/core/core.go @@ -0,0 +1,11 @@ +package core + +import "context" + +func InitProviderContext(ctx context.Context) context.Context { + return ctx +} + +func LogResponse(ctx context.Context) context.Context { + return ctx +} diff --git a/tools/testdata/stackit/internal/services/tfctxinit/tfctxinit.go b/tools/testdata/stackit/internal/services/tfctxinit/tfctxinit.go new file mode 100644 index 000000000..8c0c30f3e --- /dev/null +++ b/tools/testdata/stackit/internal/services/tfctxinit/tfctxinit.go @@ -0,0 +1,39 @@ +package tfctxinit + +import ( + "context" + + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + "github.com/stackitcloud/terraform-provider-stackit/testtypes" +) + +type resource struct{} + +func (r *resource) Create(ctx context.Context, req testtypes.CreateRequest, resp *testtypes.CreateResponse) { + // Creating an API client before calling InitProviderContext is fine. + iaas.NewAPIClient() + + var service *iaas.DefaultAPIService + service.AddNetworkToServerExecute(iaas.ApiAddNetworkToServerRequest{}) // want "tfctxinit: call to github.com/stackitcloud/stackit-sdk-go must happen AFTER github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core.InitProviderContext is called in Create" + core.InitProviderContext(ctx) +} + +func (r *resource) Read(ctx context.Context, req testtypes.ReadRequest, resp *testtypes.ReadResponse) { + core.InitProviderContext(ctx) + iaas.NewAPIClient() +} + +func sdkCallOutsideLifecycleMethod() { + iaas.NewAPIClient() +} + +func wrapper[T any](fn func() (*T, error)) (*T, error) { + return fn() +} + +func (r *resource) Delete(ctx context.Context, req testtypes.DeleteRequest, resp *testtypes.DeleteResponse) { + var service iaas.DefaultAPIService + wrapper(service.CreateAffinityGroup(ctx, "", "").Execute) // want "tfctxinit: call to github.com/stackitcloud/stackit-sdk-go must happen AFTER github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core.InitProviderContext is called in Delete" + core.InitProviderContext(ctx) +} diff --git a/tools/testdata/stackit/internal/services/tflogresponse/tflogresponse.go b/tools/testdata/stackit/internal/services/tflogresponse/tflogresponse.go new file mode 100644 index 000000000..b8472d150 --- /dev/null +++ b/tools/testdata/stackit/internal/services/tflogresponse/tflogresponse.go @@ -0,0 +1,80 @@ +package tflogresponse + +import ( + "context" + + iaas "github.com/stackitcloud/stackit-sdk-go/services/iaas/v2api" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + "github.com/stackitcloud/terraform-provider-stackit/testtypes" +) + +type resource struct{} + +func (r *resource) Create(ctx context.Context, req testtypes.CreateRequest, resp *testtypes.CreateResponse) { + c, _ := iaas.NewAPIClient() + ctx = core.InitProviderContext(ctx) + c.DefaultAPI.AddNetworkToServer(ctx, "", "", "", "").Execute() + ctx = core.LogResponse(ctx) +} + +func (r *resource) Read(ctx context.Context, req testtypes.ReadRequest, resp *testtypes.ReadResponse) { + ctx = core.InitProviderContext(ctx) + ctx = core.LogResponse(ctx) // want "tflogresponse: invalid sequence: LogResponse called without an intermediate call to github.com/stackitcloud/stackit-sdk-go after InitProviderContext" +} + +func (r *resource) Update(ctx context.Context, req testtypes.UpdateRequest, resp *testtypes.UpdateResponse) { + var service iaas.DefaultAPIService + ctx = core.InitProviderContext(ctx) // want "tflogresponse: invalid sequence: InitProviderContext was called, but LogResponse was never called afterwards" + service.AddNetworkToServerExecute(iaas.ApiAddNetworkToServerRequest{}) +} + +func (r *resource) Delete(ctx context.Context, req testtypes.DeleteRequest, resp *testtypes.DeleteResponse) { + var service iaas.DefaultAPIService + ctx = core.InitProviderContext(ctx) + service.AddNetworkToServerExecute(iaas.ApiAddNetworkToServerRequest{}) + ctx = core.LogResponse(ctx) +} + +func nonLifecycleMethod(ctx context.Context) { + var service iaas.DefaultAPIService + ctx = core.InitProviderContext(ctx) + service.AddNetworkToServerExecute(iaas.ApiAddNetworkToServerRequest{}) +} + +// SDK service call through helper func + +type resource2 struct{} + +func (f *resource2) Read(ctx context.Context, req testtypes.ReadRequest, resp *testtypes.ReadResponse) { + ctx = core.InitProviderContext(ctx) + indirection(nil) + ctx = core.LogResponse(ctx) +} + +func indirection(service *iaas.DefaultAPIService) { + service.AddNetworkToServerExecute(iaas.ApiAddNetworkToServerRequest{}) +} + +func wrapper[T any](fn func() (*T, error)) (*T, error) { + return fn() +} + +type resource3 struct{} + +func (r *resource3) Delete(ctx context.Context, req testtypes.DeleteRequest, resp *testtypes.DeleteResponse) { + c, _ := iaas.NewAPIClient() + ctx = core.InitProviderContext(ctx) + wrapper(c.DefaultAPI.CreateAffinityGroup(ctx, "", "").Execute) + ctx = core.LogResponse(ctx) +} + +type falseNegative struct{} + +func (f *falseNegative) Read(ctx context.Context, req testtypes.ReadRequest, resp *testtypes.ReadResponse) { + var service iaas.DefaultAPIService + ctx = core.InitProviderContext(ctx) + service.AddNetworkToServerExecute(iaas.ApiAddNetworkToServerRequest{}) + ctx = core.LogResponse(ctx) + service.AddNetworkToServerExecute(iaas.ApiAddNetworkToServerRequest{}) + // this should fail because the traceID of the 2nd AddNetworkToServerExecute will never be logged +} diff --git a/tools/testdata/stackit/internal/services/tfwriteid/tfwriteid.go b/tools/testdata/stackit/internal/services/tfwriteid/tfwriteid.go new file mode 100644 index 000000000..8007ef0a8 --- /dev/null +++ b/tools/testdata/stackit/internal/services/tfwriteid/tfwriteid.go @@ -0,0 +1,18 @@ +package tfwriteid + +import ( + "context" + + corewait "github.com/stackitcloud/stackit-sdk-go/core/wait" + serviceenablementwait "github.com/stackitcloud/stackit-sdk-go/services/serviceenablement/v2api/wait" + "github.com/stackitcloud/terraform-provider-stackit/testtypes" +) + +type resource struct{} + +func (r *resource) Create(ctx context.Context, req testtypes.CreateRequest, resp *testtypes.CreateResponse) { + _, _ = serviceenablementwait.EnableServiceWaitHandler(ctx, nil, "", "", "").WaitWithContext(ctx) + + waiter := corewait.New(func() (bool, *struct{}, error) { return true, nil, nil }) + _, _ = waiter.WaitWithContext(ctx) // want "tfwriteid: call to wait handler from github.com/stackitcloud/stackit-sdk-go must happen AFTER github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils.SetAndLogStateFields is called in Create github.com/stackitcloud/stackit-sdk-go/core/wait" +} diff --git a/tools/testdata/testtypes/testtypes.go b/tools/testdata/testtypes/testtypes.go new file mode 100644 index 000000000..1f7133c9e --- /dev/null +++ b/tools/testdata/testtypes/testtypes.go @@ -0,0 +1,10 @@ +package testtypes + +type CreateRequest struct{} +type CreateResponse struct{} +type ReadRequest struct{} +type ReadResponse struct{} +type UpdateRequest struct{} +type UpdateResponse struct{} +type DeleteRequest struct{} +type DeleteResponse struct{}