diff --git a/.golangci.yml b/.golangci.yml index 9abd8e53..7fa97b08 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -27,6 +27,7 @@ linters: - G104 # Duplicates errcheck - G304 # File inclusion from variable - expected for config/dynamic loading - G704 # SSRF via taint analysis - URLs from admin config are intentional + - G710 # Open redirect via taint - OAuth2 redirect targets are provider-issued URLs from admin-configured provider endpoints - G118 # Context cancel/goroutine patterns - cancel is returned to caller or used in signal handler; background goroutines are intentional - G120 # Form data size - ParseMultipartForm already sets explicit limits; FormValue after that is already bounded - G122 # WalkDir/Walk TOCTOU - OS-provided walk paths; acceptable for CI/CD pipeline tooling @@ -53,6 +54,14 @@ linters: linters: - staticcheck text: "SA5011" + # Third-party vendored Go code in ui/node_modules is not ours to fix + - path: ui/node_modules/ + linters: + - govet + - staticcheck + - unused + - gocritic + - gosec presets: - std-error-handling diff --git a/cmd/iac-codemod/add_validate_plan.go b/cmd/iac-codemod/add_validate_plan.go index 3626d9c5..f2e74366 100644 --- a/cmd/iac-codemod/add_validate_plan.go +++ b/cmd/iac-codemod/add_validate_plan.go @@ -12,7 +12,6 @@ import ( "go/token" "io" "io/fs" - "os" "path/filepath" "sort" "strings" @@ -643,50 +642,6 @@ func qualifierFromProviderMethods(methods []*ast.FuncDecl) string { return "" } -// siblingUsesInterfacesImport returns true if any non-test .go file -// in dir (other than excludePath) imports -// github.com/GoCodeAlone/workflow/interfaces. Used to decide whether -// to inject an interfaces import into a file that doesn't have one -// when emitting a qualified ValidatePlan stub (review round-4 #1). -func siblingUsesInterfacesImport(dir, excludePath string) bool { - const wantPath = "github.com/GoCodeAlone/workflow/interfaces" - entries, err := os.ReadDir(dir) - if err != nil { - return false - } - for _, e := range entries { - if e.IsDir() { - continue - } - name := e.Name() - if !strings.HasSuffix(name, ".go") || strings.HasSuffix(name, "_test.go") { - continue - } - fpath := filepath.Join(dir, name) - if fpath == excludePath { - continue - } - src, err := readFile(fpath) - if err != nil { - continue - } - fs := token.NewFileSet() - sib, err := parser.ParseFile(fs, fpath, src, parser.ImportsOnly) - if err != nil { - continue - } - for _, imp := range sib.Imports { - if imp.Path == nil { - continue - } - if strings.Trim(imp.Path.Value, `"`) == wantPath { - return true - } - } - } - return false -} - // interfacesQualifier returns the package alias `file` uses for // github.com/GoCodeAlone/workflow/interfaces. If the import is // renamed (`alias "github.com/.../interfaces"`), the alias name is diff --git a/cmd/iac-codemod/lint.go b/cmd/iac-codemod/lint.go index 9ffb6d88..46e2b4f7 100644 --- a/cmd/iac-codemod/lint.go +++ b/cmd/iac-codemod/lint.go @@ -1039,39 +1039,6 @@ func receiverTypeName(fn *ast.FuncDecl) string { return id.Name } -// bodyCallsSelector reports whether the function body contains a -// CallExpr whose callee is a SelectorExpr with the given X.Name and -// Sel.Name, e.g. `wfctlhelpers.Plan(...)`. -func bodyCallsSelector(body *ast.BlockStmt, pkgIdent, selName string) bool { - if body == nil { - return false - } - found := false - ast.Inspect(body, func(n ast.Node) bool { - if found { - return false - } - call, ok := n.(*ast.CallExpr) - if !ok { - return true - } - sel, ok := call.Fun.(*ast.SelectorExpr) - if !ok { - return true - } - x, ok := sel.X.(*ast.Ident) - if !ok { - return true - } - if x.Name == pkgIdent && sel.Sel.Name == selName { - found = true - return false - } - return true - }) - return found -} - // bodyReferencesField reports whether the function body references any // SelectorExpr with the given Sel.Name, e.g. any `.ForceNew`. func bodyReferencesField(body *ast.BlockStmt, fieldName string) bool { diff --git a/cmd/iac-codemod/refactor_apply.go b/cmd/iac-codemod/refactor_apply.go index 3a1d9301..ad23d38b 100644 --- a/cmd/iac-codemod/refactor_apply.go +++ b/cmd/iac-codemod/refactor_apply.go @@ -22,10 +22,6 @@ func init() { modes["refactor-apply"] = runRefactorApply } -// applyCanonicalCallExpr is the canonical replacement-body expression -// emitted by refactor-apply. -const applyCanonicalCallExpr = "wfctlhelpers.ApplyPlan(ctx, p, plan)" - // applyClassification labels the disposition of a single Apply() // method site. The non-canonical idioms are surfaced as distinct // classes so the report can suggest the right hand-port handling. @@ -123,7 +119,7 @@ func runRefactorApply(args []string, opts *Options, stdout, stderr io.Writer) in if *reportFile != "" { var buf bytes.Buffer report.print(&buf, opts) - if err := os.WriteFile(*reportFile, buf.Bytes(), 0o644); err != nil { + if err := os.WriteFile(*reportFile, buf.Bytes(), 0o600); err != nil { fmt.Fprintf(stderr, "iac-codemod refactor-apply: write report-file %s: %v\n", *reportFile, err) return 1 } @@ -476,7 +472,7 @@ func isCanonicalApplyLoopAssign(a *ast.AssignStmt, recvName string) bool { if !ok { return false } - if !((recvName != "" && x.Name == recvName) || (recvName == "" && x.Name == "p")) { + if (recvName == "" || x.Name != recvName) && (recvName != "" || x.Name != "p") { return false } // Round-12 #7: also verify the lookup KEY is `action.Resource.Type`. @@ -1162,7 +1158,7 @@ func isProviderIDGuard(ifs *ast.IfStmt) bool { if id, ok := be.Y.(*ast.Ident); ok && id.Name == "nil" { yIsNil = true } - if !(xIsCurrent && yIsNil) { + if !xIsCurrent || !yIsNil { // Allow the reverse order too (`nil != action.Current`), // though it's not idiomatic Go. yIsCurrent := false @@ -1173,7 +1169,7 @@ func isProviderIDGuard(ifs *ast.IfStmt) bool { if id, ok := be.X.(*ast.Ident); ok && id.Name == "nil" { xIsNil = true } - if !(yIsCurrent && xIsNil) { + if !yIsCurrent || !xIsNil { return false } } diff --git a/cmd/iac-codemod/refactor_plan.go b/cmd/iac-codemod/refactor_plan.go index 2640ee64..c5ba4284 100644 --- a/cmd/iac-codemod/refactor_plan.go +++ b/cmd/iac-codemod/refactor_plan.go @@ -39,11 +39,6 @@ const helperImportPath = "github.com/GoCodeAlone/workflow/iac/wfctlhelpers" // files would fail to compile". const planHelperImportPath = "github.com/GoCodeAlone/workflow/platform" -// planCanonicalCallExpr is the canonical replacement-body expression -// emitted by refactor-plan. Calls platform.ComputePlan (the real helper); -// see planHelperImportPath above for the review-correction rationale. -const planCanonicalCallExpr = "platform.ComputePlan(ctx, p, desired, current)" - // planClassification labels the disposition of a single Plan() method // site. Each report entry carries one classification; the rewriter // honors only `planCanonical`. @@ -1108,13 +1103,13 @@ func rewritePlanBody(fn *ast.FuncDecl, file *ast.File) { ast.NewIdent(currentName), }, } - // plan, err := platform.ComputePlan(ctx, p, desired, current) + // generates: plan, err := platform.ComputePlan(ctx, p, desired, current) planAssign := &ast.AssignStmt{ Lhs: []ast.Expr{ast.NewIdent("plan"), ast.NewIdent("err")}, Tok: token.DEFINE, Rhs: []ast.Expr{call}, } - // return &plan, err + // generates: return &plan, err returnStmt := &ast.ReturnStmt{ Results: []ast.Expr{ &ast.UnaryExpr{Op: token.AND, X: ast.NewIdent("plan")}, diff --git a/cmd/wfctl/infra.go b/cmd/wfctl/infra.go index cc0ac103..9f9625c7 100644 --- a/cmd/wfctl/infra.go +++ b/cmd/wfctl/infra.go @@ -295,7 +295,7 @@ func runInfraPlan(args []string) error { // cmd/wfctl/main.go's top-level wrapper. errors.New (rather than // fmt.Errorf) avoids govet's no-verbs noise and is canonical for // fixed-string error literals per Go convention. - return errors.New("this plan requires JIT resolution; persisted plan.json is not supported. Run 'wfctl infra apply' directly without -o/--plan.") + return errors.New("this plan requires JIT resolution; persisted plan.json is not supported. Run 'wfctl infra apply' directly without -o/--plan") } // Embed a hash of the desired-state inputs so wfctl infra apply --plan // can detect stale plans when the config changes after plan generation. diff --git a/cmd/wfctl/infra_bootstrap.go b/cmd/wfctl/infra_bootstrap.go index 49a11acd..5524b2c6 100644 --- a/cmd/wfctl/infra_bootstrap.go +++ b/cmd/wfctl/infra_bootstrap.go @@ -429,13 +429,9 @@ func bootstrapSecrets(ctx context.Context, provider secrets.Provider, cfg *Secre if forceRotate[gen.Key] { deleteKey := gen.Key if delErr := provider.Delete(ctx, deleteKey); delErr != nil { - if errors.Is(delErr, secrets.ErrNotFound) || errors.Is(delErr, secrets.ErrUnsupported) { - // Secret was already absent or provider doesn't support Delete. - // Log a warning and continue — the create path handles this. - fmt.Fprintf(os.Stderr, "warn: rotate-pre-delete %s: %v (continuing)\n", deleteKey, delErr) - } else { - fmt.Fprintf(os.Stderr, "warn: rotate-pre-delete %s: %v (continuing)\n", deleteKey, delErr) - } + // Log and continue regardless of the error kind — both absent/unsupported + // secrets and unexpected errors should not block the rotation flow. + fmt.Fprintf(os.Stderr, "warn: rotate-pre-delete %s: %v (continuing)\n", deleteKey, delErr) } // Invalidate the List cache so the existence check below reflects the // deletion. A fresh List call after Delete is the safe path for diff --git a/cmd/wfctl/infra_plan_jit_reject_test.go b/cmd/wfctl/infra_plan_jit_reject_test.go index b6dc21e7..d43117e1 100644 --- a/cmd/wfctl/infra_plan_jit_reject_test.go +++ b/cmd/wfctl/infra_plan_jit_reject_test.go @@ -20,7 +20,7 @@ import ( // prefix from cmd/wfctl/main.go's top-level error wrapper; the value // returned by runInfraPlan (and asserted here) does NOT include that // prefix. -const expectedJITRejectError = "this plan requires JIT resolution; persisted plan.json is not supported. Run 'wfctl infra apply' directly without -o/--plan." +const expectedJITRejectError = "this plan requires JIT resolution; persisted plan.json is not supported. Run 'wfctl infra apply' directly without -o/--plan" // TestInfraPlan_RejectsPersistedJITPlan_WithExactErrorString is the // canonical T5.5 scenario: a config whose env_vars carry a diff --git a/cmd/wfctl/migrations_baseline.go b/cmd/wfctl/migrations_baseline.go index e4433181..cf61f9e8 100644 --- a/cmd/wfctl/migrations_baseline.go +++ b/cmd/wfctl/migrations_baseline.go @@ -230,6 +230,13 @@ func parseMigrationStatus(stdout string) (migrationBaselineCandidateResult, erro pendingSeen = true if line == "No migrations applied." { currentSeen = true + // A fresh database with no migrations applied cannot be dirty. + // Set dirtySeen so callers don't require a redundant "Dirty: false" line + // in output from drivers (e.g. atlas) that omit it on a clean first run. + if !dirtySeen { + dirtySeen = true + // status.Dirty is already false (zero value) + } } } } diff --git a/cmd/wfctl/migrations_baseline_test.go b/cmd/wfctl/migrations_baseline_test.go index 65f51330..c0747941 100644 --- a/cmd/wfctl/migrations_baseline_test.go +++ b/cmd/wfctl/migrations_baseline_test.go @@ -249,6 +249,62 @@ func TestParseMigrationStatusRejectsUnknownOutput(t *testing.T) { } } +func TestParseMigrationStatusFreshDatabaseNoMigrationsApplied(t *testing.T) { + // A fresh database with no migrations applied cannot be dirty. Drivers such as + // atlas omit the "Dirty:" line in this case; parseMigrationStatus must not + // require it when "No migrations applied." is the reported state. + status, err := parseMigrationStatus("No migrations applied.\n") + if err != nil { + t.Fatalf("fresh-database status without Dirty line: %v", err) + } + if status.Dirty { + t.Fatal("fresh-database status: expected dirty=false") + } + if status.Current != "" { + t.Fatalf("fresh-database status: expected empty current, got %q", status.Current) + } + if len(status.Pending) != 0 { + t.Fatalf("fresh-database status: expected no pending, got %v", status.Pending) + } + + // Explicit "Dirty: false" alongside "No migrations applied." must also work. + status, err = parseMigrationStatus("No migrations applied.\nDirty: false\n") + if err != nil { + t.Fatalf("fresh-database status with explicit Dirty: false: %v", err) + } + if status.Dirty { + t.Fatal("expected dirty=false") + } + + // "Dirty: true" with "No migrations applied." is unusual but must be respected + // when the driver explicitly signals it (edge case: corrupted revisions table). + status, err = parseMigrationStatus("No migrations applied.\nDirty: true\n") + if err != nil { + t.Fatalf("fresh-database status with explicit Dirty: true: %v", err) + } + if !status.Dirty { + t.Fatal("expected dirty=true when driver explicitly reports it") + } + + // JSON format: fresh database with empty current and no pending. + status, err = parseMigrationStatus(`{"current":"","dirty":false,"pending":[]}`) + if err != nil { + t.Fatalf("fresh-database JSON status: %v", err) + } + if status.Dirty || status.Current != "" || len(status.Pending) != 0 { + t.Fatalf("unexpected fresh-database JSON status: %+v", status) + } + + // JSON format: null pending (some drivers emit null instead of []). + status, err = parseMigrationStatus(`{"current":"","dirty":false,"pending":null}`) + if err != nil { + t.Fatalf("fresh-database JSON status with null pending: %v", err) + } + if status.Dirty || status.Current != "" || len(status.Pending) != 0 { + t.Fatalf("unexpected fresh-database JSON status (null pending): %+v", status) + } +} + func TestMigrationSourceChangedNormalizesDotSlashAndRoot(t *testing.T) { if !migrationSourceChanged("./migrations", []string{"migrations/202604270001_add_users.up.sql"}) { t.Fatal("expected ./migrations to match git diff path") @@ -454,6 +510,66 @@ func TestExtractTarRejectsCleanedTargetOutsideDestination(t *testing.T) { } } +func TestRunMigrationsValidateAtlasFreshDatabaseStatusWithoutDirtyLine(t *testing.T) { + // Simulates the atlas driver returning "No migrations applied." without a + // "Dirty:" line after running `up` on a fresh (empty) ephemeral database. + // This is the scenario surfaced by the atlas executor panic fix: once the + // binary no longer panics, wfctl must parse the status output correctly. + cfgPath := writeMigrationBaselineConfigData(t, ` +version: 1 +ci: + migrations: + - name: app + driver: atlas + source_dir: migrations + database: + env: DATABASE_URL + validation: + baseline_candidate: true +`) + resultPath := filepath.Join(t.TempDir(), "result.json") + t.Setenv("DATABASE_URL", "postgres://secret@example/db") + + var calls []string + restore := stubMigrationBaselineHooks(t, &calls, []string{"migrations/202604270001_add_users.up.sql"}, "abc123") + defer restore() + oldRunner := newMigrationPluginRunner + newMigrationPluginRunner = func() migrationPluginRunner { + return migrationPluginRunner{ + exec: func(_ context.Context, _ string, args []string, _ map[string]string) (migrationCommandResult, error) { + command := migrationCommandFromArgs(args) + if command == "status" { + // Atlas driver output for a fresh database: no "Dirty:" line. + return migrationCommandResult{Stdout: "No migrations applied.\n"}, nil + } + return migrationCommandResult{}, nil + }, + } + } + defer func() { newMigrationPluginRunner = oldRunner }() + + if err := runMigrations([]string{"validate", "--config", cfgPath, "--env", "ci", "--result-file", resultPath}); err != nil { + t.Fatalf("validation failed for fresh-database atlas status: %v", err) + } + data, err := os.ReadFile(resultPath) + if err != nil { + t.Fatal(err) + } + var got migrationValidationResult + if err := json.Unmarshal(data, &got); err != nil { + t.Fatalf("decode result file: %v\n%s", err, data) + } + if got.Decision != "pass" || len(got.Migrations) != 1 { + t.Fatalf("unexpected validation result: %+v", got) + } + if got.Migrations[0].BaselineCandidate != "pass" { + t.Fatalf("baseline_candidate check should pass: %+v", got.Migrations[0]) + } + if got.Migrations[0].Dirty { + t.Fatal("fresh database should not be dirty") + } +} + func TestRunMigrationsValidateUsesEphemeralDSNForBaselineCandidate(t *testing.T) { cfgPath := writeMigrationBaselineConfig(t, true) t.Setenv("DATABASE_URL", "postgres://real-db.example/app") diff --git a/iac/diffcache/cache_filesystem.go b/iac/diffcache/cache_filesystem.go index 52818b71..0035cf6e 100644 --- a/iac/diffcache/cache_filesystem.go +++ b/iac/diffcache/cache_filesystem.go @@ -123,7 +123,7 @@ func (c *filesystemCache) Get(k Key) (interfaces.DiffResult, bool) { // working" signal from elsewhere. The cache-as-amortization framing // in the package godoc sets the expectation. func (c *filesystemCache) Put(k Key, result interfaces.DiffResult) { - if err := os.MkdirAll(c.dir, 0o755); err != nil { + if err := os.MkdirAll(c.dir, 0o750); err != nil { return } env := envelope{SchemaVersion: cacheSchemaVersion, Result: result} diff --git a/iac/wfctlhelpers/apply.go b/iac/wfctlhelpers/apply.go index 7f824b3f..1d876bb2 100644 --- a/iac/wfctlhelpers/apply.go +++ b/iac/wfctlhelpers/apply.go @@ -136,7 +136,8 @@ func applyPlanWithEnvProvider( // to later actions in the same plan). syncedOutputs := buildInitialSyncedOutputs(plan.Actions) - for _, action := range plan.Actions { + for i := range plan.Actions { + action := plan.Actions[i] // Honor cancellation at the loop boundary. Drivers should also // check ctx internally for in-flight work, but the loop check // guarantees apply stops between actions even if a driver @@ -205,7 +206,8 @@ func applyPlanWithEnvProvider( // ProviderID). func buildInitialSyncedOutputs(actions []interfaces.PlanAction) map[string]map[string]any { out := make(map[string]map[string]any) - for _, a := range actions { + for i := range actions { + a := actions[i] if a.Current == nil { continue } diff --git a/iac/wfctlhelpers/apply_create_test.go b/iac/wfctlhelpers/apply_create_test.go index cbb44042..bb03f14f 100644 --- a/iac/wfctlhelpers/apply_create_test.go +++ b/iac/wfctlhelpers/apply_create_test.go @@ -28,7 +28,7 @@ type fakeDriverWithUpsert struct { } func (d *fakeDriverWithUpsert) Create(_ context.Context, _ interfaces.ResourceSpec) (*interfaces.ResourceOutput, error) { - d.fakeDriver.createCount++ + d.createCount++ if d.createErr != nil { return nil, d.createErr } @@ -44,7 +44,7 @@ func (d *fakeDriverWithUpsert) Read(_ context.Context, _ interfaces.ResourceRef) func (d *fakeDriverWithUpsert) Update(_ context.Context, ref interfaces.ResourceRef, spec interfaces.ResourceSpec) (*interfaces.ResourceOutput, error) { d.updateCalled = true - d.fakeDriver.updateCount++ + d.updateCount++ if d.updateOut != nil { return d.updateOut, nil } @@ -133,7 +133,7 @@ type alreadyExistsBareDriver struct { } func (d *alreadyExistsBareDriver) Create(_ context.Context, _ interfaces.ResourceSpec) (*interfaces.ResourceOutput, error) { - d.fakeDriver.createCount++ + d.createCount++ return nil, interfaces.ErrResourceAlreadyExists } diff --git a/iac/wfctlhelpers/apply_jit_test.go b/iac/wfctlhelpers/apply_jit_test.go index a81c69c3..8938e0aa 100644 --- a/iac/wfctlhelpers/apply_jit_test.go +++ b/iac/wfctlhelpers/apply_jit_test.go @@ -26,7 +26,7 @@ type jitRecordingDriver struct { } func (d *jitRecordingDriver) Create(_ context.Context, spec interfaces.ResourceSpec) (*interfaces.ResourceOutput, error) { - d.fakeDriver.createCount++ + d.createCount++ d.mu.Lock() if d.seenConfigs == nil { d.seenConfigs = make(map[string]map[string]any) @@ -40,7 +40,7 @@ func (d *jitRecordingDriver) Create(_ context.Context, spec interfaces.ResourceS } func (d *jitRecordingDriver) Update(_ context.Context, ref interfaces.ResourceRef, spec interfaces.ResourceSpec) (*interfaces.ResourceOutput, error) { - d.fakeDriver.updateCount++ + d.updateCount++ d.mu.Lock() if d.seenUpdateCfgs == nil { d.seenUpdateCfgs = make(map[string]map[string]any) diff --git a/iac/wfctlhelpers/apply_postcondition_test.go b/iac/wfctlhelpers/apply_postcondition_test.go index b7ef765b..e45e3dad 100644 --- a/iac/wfctlhelpers/apply_postcondition_test.go +++ b/iac/wfctlhelpers/apply_postcondition_test.go @@ -232,6 +232,6 @@ type erroringFakeDriver struct { } func (d *erroringFakeDriver) Create(_ context.Context, _ interfaces.ResourceSpec) (*interfaces.ResourceOutput, error) { - d.fakeDriver.createCount++ + d.createCount++ return nil, errFromDispatch } diff --git a/iac/wfctlhelpers/apply_replace_test.go b/iac/wfctlhelpers/apply_replace_test.go index 5e215bb8..aaf12d91 100644 --- a/iac/wfctlhelpers/apply_replace_test.go +++ b/iac/wfctlhelpers/apply_replace_test.go @@ -25,14 +25,14 @@ type orderRecordingDriver struct { } func (d *orderRecordingDriver) Delete(_ context.Context, _ interfaces.ResourceRef) error { - d.fakeDriver.deleteCount++ + d.deleteCount++ d.step++ d.deleteAt = d.step return d.deleteErr } func (d *orderRecordingDriver) Create(_ context.Context, spec interfaces.ResourceSpec) (*interfaces.ResourceOutput, error) { - d.fakeDriver.createCount++ + d.createCount++ d.step++ d.createAt = d.step if d.createErr != nil { @@ -169,12 +169,12 @@ type perResourceReplaceDriver struct { } func (d *perResourceReplaceDriver) Delete(_ context.Context, _ interfaces.ResourceRef) error { - d.fakeDriver.deleteCount++ + d.deleteCount++ return nil } func (d *perResourceReplaceDriver) Create(_ context.Context, spec interfaces.ResourceSpec) (*interfaces.ResourceOutput, error) { - d.fakeDriver.createCount++ + d.createCount++ id := d.newIDs[spec.Name] if id == "" { id = "fallback-id" @@ -279,7 +279,7 @@ type cancelOnDeleteDriver struct { } func (d *cancelOnDeleteDriver) Delete(_ context.Context, _ interfaces.ResourceRef) error { - d.fakeDriver.deleteCount++ + d.deleteCount++ d.cancel() // ctx is now canceled; Create must not run. return nil } diff --git a/iac/wfctlhelpers/apply_update_delete_test.go b/iac/wfctlhelpers/apply_update_delete_test.go index 44614653..b9c38b50 100644 --- a/iac/wfctlhelpers/apply_update_delete_test.go +++ b/iac/wfctlhelpers/apply_update_delete_test.go @@ -21,7 +21,7 @@ type providerIDCapturingDriver struct { } func (d *providerIDCapturingDriver) Update(_ context.Context, ref interfaces.ResourceRef, spec interfaces.ResourceSpec) (*interfaces.ResourceOutput, error) { - d.fakeDriver.updateCount++ + d.updateCount++ d.updateRef = ref d.updateSpec = spec if d.updateErr != nil { @@ -31,7 +31,7 @@ func (d *providerIDCapturingDriver) Update(_ context.Context, ref interfaces.Res } func (d *providerIDCapturingDriver) Delete(_ context.Context, ref interfaces.ResourceRef) error { - d.fakeDriver.deleteCount++ + d.deleteCount++ d.deleteRef = ref return d.deleteErr } diff --git a/module/scan_provider_test.go b/module/scan_provider_test.go index 187e6d86..2536ae17 100644 --- a/module/scan_provider_test.go +++ b/module/scan_provider_test.go @@ -51,7 +51,7 @@ func (a *scanMockApp) GetService(name string, target any) error { return fmt.Errorf("service %q not found", name) } rv := reflect.ValueOf(target) - if rv.Kind() != reflect.Ptr || rv.IsNil() { + if rv.Kind() != reflect.Pointer || rv.IsNil() { return fmt.Errorf("target must be a non-nil pointer") } rv.Elem().Set(reflect.ValueOf(svc)) diff --git a/schema/reflect.go b/schema/reflect.go index 1bf58b81..f82e6dd7 100644 --- a/schema/reflect.go +++ b/schema/reflect.go @@ -15,7 +15,7 @@ func GenerateConfigFields(configStruct interface{}) []ConfigFieldDef { if t == nil { return nil } - if t.Kind() == reflect.Ptr { + if t.Kind() == reflect.Pointer { t = t.Elem() } if t.Kind() != reflect.Struct {