From cd93ad40e5292a6022a7b4493a1596d5c48d2538 Mon Sep 17 00:00:00 2001 From: Jon Langevin Date: Fri, 15 May 2026 18:35:29 -0400 Subject: [PATCH 1/8] =?UTF-8?q?refactor:=20delete=20in-core=20gcs=20IaC=20?= =?UTF-8?q?store=20+=20storage.gcs=20+=20platform=5Fkubernetes=5Fgke=20?= =?UTF-8?q?=E2=80=94=20now=20plugin-served=20via=20workflow-plugin-gcp=20v?= =?UTF-8?q?1.1.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DOCUMENTATION.md | 1 - module/iac_module.go | 25 +- module/iac_state_gcs.go | 269 -------------------- module/iac_state_gcs_test.go | 248 ------------------- module/platform_kubernetes_gke.go | 231 ------------------ module/storage_gcs.go | 251 ------------------- module/storage_gcs_test.go | 391 ------------------------------ plugins/storage/plugin.go | 31 +-- plugins/storage/plugin_test.go | 21 +- 9 files changed, 15 insertions(+), 1453 deletions(-) delete mode 100644 module/iac_state_gcs.go delete mode 100644 module/iac_state_gcs_test.go delete mode 100644 module/platform_kubernetes_gke.go delete mode 100644 module/storage_gcs.go delete mode 100644 module/storage_gcs_test.go diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index fb5cd924..9275749c 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -536,7 +536,6 @@ See [v0.53.0 migration guide](docs/migrations/v0.53.0-aws-iac-removal.md). ### Storage | Type | Description | Plugin | |------|-------------|--------| -| `storage.gcs` | Google Cloud Storage | storage | | `storage.local` | Local filesystem storage | storage | | `storage.sqlite` | SQLite storage | storage | | `storage.artifact` | Artifact store for build artifacts shared across pipeline steps | storage | diff --git a/module/iac_module.go b/module/iac_module.go index 0ee73c2d..8f2935a1 100644 --- a/module/iac_module.go +++ b/module/iac_module.go @@ -10,10 +10,10 @@ import ( ) // IaCModule registers an IaCStateStore in the service registry. -// Supported in-core backends: "memory" (default), "filesystem", "gcs", and -// "postgres" — plus any backend provided by a loaded plugin (e.g. "azure_blob" -// via workflow-plugin-azure, "spaces" via workflow-plugin-digitalocean, "s3" -// via workflow-plugin-aws). +// Supported in-core backends: "memory" (default), "filesystem", and "postgres" +// — plus any backend provided by a loaded plugin (e.g. "azure_blob" via +// workflow-plugin-azure, "spaces" via workflow-plugin-digitalocean, "s3" via +// workflow-plugin-aws, "gcs" via workflow-plugin-gcp). // // Config example: // @@ -54,17 +54,6 @@ func (m *IaCModule) Init(app modular.Application) error { dir = "/var/lib/workflow/iac-state" } m.store = NewFSIaCStateStore(dir) - case "gcs": - bucket, _ := m.config["bucket"].(string) - prefix, _ := m.config["prefix"].(string) - if bucket == "" { - return fmt.Errorf("iac.state %q: gcs backend requires 'bucket' config", m.name) - } - store, err := NewGCSIaCStateStore(context.Background(), bucket, prefix) - if err != nil { - return fmt.Errorf("iac.state %q: gcs backend: %w", m.name, err) - } - m.store = store case "postgres": dsn, _ := m.config["dsn"].(string) if dsn == "" { @@ -96,10 +85,10 @@ func (m *IaCModule) Init(app modular.Application) error { break } return fmt.Errorf("iac.state %q: backend %q is not built into workflow core "+ - "(in-core backends: 'memory', 'filesystem', 'gcs', 'postgres'). "+ + "(in-core backends: 'memory', 'filesystem', 'postgres'). "+ "If %q is a plugin-provided backend (e.g. 'azure_blob' via workflow-plugin-azure, "+ - "'spaces' via workflow-plugin-digitalocean, 's3' via workflow-plugin-aws), "+ - "install and load that plugin", m.name, m.backend, m.backend) + "'spaces' via workflow-plugin-digitalocean, 's3' via workflow-plugin-aws, "+ + "'gcs' via workflow-plugin-gcp), install and load that plugin", m.name, m.backend, m.backend) } return app.RegisterService(m.name, m.store) diff --git a/module/iac_state_gcs.go b/module/iac_state_gcs.go deleted file mode 100644 index f2d8ebaf..00000000 --- a/module/iac_state_gcs.go +++ /dev/null @@ -1,269 +0,0 @@ -package module - -import ( - "bytes" - "context" - "encoding/json" - "errors" - "fmt" - "strings" - - "cloud.google.com/go/storage" - "google.golang.org/api/iterator" - "google.golang.org/api/option" -) - -// ErrGCSNotFound is returned by GCSObjectClient when an object does not exist. -var ErrGCSNotFound = errors.New("gcs: object not found") - -// GCSObjectClient abstracts the GCS operations used by GCSIaCStateStore, -// allowing a mock to be injected for testing. -type GCSObjectClient interface { - ReadObject(ctx context.Context, key string) (data []byte, generation int64, err error) - WriteObject(ctx context.Context, key string, data []byte, contentType string) (generation int64, err error) - WriteObjectIfGenerationMatch(ctx context.Context, key string, data []byte, contentType string, generation int64) (newGeneration int64, err error) - DeleteObject(ctx context.Context, key string) error - ListObjects(ctx context.Context, prefix string) ([]string, error) -} - -// GCSIaCStateStore persists IaC state as JSON objects in Google Cloud Storage. -// Locking uses GCS generation-match preconditions for atomic, race-free lock acquisition. -type GCSIaCStateStore struct { - client GCSObjectClient - bucket string - prefix string - lockState map[string]int64 // resource -> lock object generation (in-memory tracking) -} - -// NewGCSIaCStateStore creates a GCS-backed state store using Application Default Credentials. -func NewGCSIaCStateStore(ctx context.Context, bucket, prefix string, opts ...option.ClientOption) (*GCSIaCStateStore, error) { - if bucket == "" { - return nil, fmt.Errorf("iac gcs state: bucket must not be empty") - } - if prefix == "" { - prefix = "iac-state/" - } - gcsClient, err := storage.NewClient(ctx, opts...) - if err != nil { - return nil, fmt.Errorf("iac gcs state: create client: %w", err) - } - return &GCSIaCStateStore{ - client: &gcsRealClient{client: gcsClient, bucket: bucket}, - bucket: bucket, - prefix: prefix, - lockState: make(map[string]int64), - }, nil -} - -// NewGCSIaCStateStoreWithClient creates a GCS state store with an injected client (for testing). -func NewGCSIaCStateStoreWithClient(client GCSObjectClient, bucket, prefix string) *GCSIaCStateStore { - if prefix == "" { - prefix = "iac-state/" - } - return &GCSIaCStateStore{ - client: client, - bucket: bucket, - prefix: prefix, - lockState: make(map[string]int64), - } -} - -func (s *GCSIaCStateStore) stateKey(resourceID string) string { - return s.prefix + sanitizeID(resourceID) + ".json" -} - -func (s *GCSIaCStateStore) lockKey(resourceID string) string { - return s.prefix + sanitizeID(resourceID) + ".lock" -} - -// GetState retrieves a state record by resource ID. Returns nil, nil when not found. -func (s *GCSIaCStateStore) GetState(ctx context.Context, resourceID string) (*IaCState, error) { - data, _, err := s.client.ReadObject(ctx, s.stateKey(resourceID)) - if err != nil { - if errors.Is(err, ErrGCSNotFound) { - return nil, nil - } - return nil, fmt.Errorf("iac gcs state: GetState %q: %w", resourceID, err) - } - var st IaCState - if err := json.Unmarshal(data, &st); err != nil { - return nil, fmt.Errorf("iac gcs state: GetState %q: unmarshal: %w", resourceID, err) - } - return &st, nil -} - -// SaveState writes the state record as a JSON object to GCS. -func (s *GCSIaCStateStore) SaveState(ctx context.Context, state *IaCState) error { - if state == nil { - return fmt.Errorf("iac gcs state: SaveState: state must not be nil") - } - if state.ResourceID == "" { - return fmt.Errorf("iac gcs state: SaveState: resource_id must not be empty") - } - data, err := json.MarshalIndent(state, "", " ") - if err != nil { - return fmt.Errorf("iac gcs state: SaveState %q: marshal: %w", state.ResourceID, err) - } - if _, err := s.client.WriteObject(ctx, s.stateKey(state.ResourceID), data, "application/json"); err != nil { - return fmt.Errorf("iac gcs state: SaveState %q: write: %w", state.ResourceID, err) - } - return nil -} - -// ListStates lists all state objects and returns those matching the filter. -func (s *GCSIaCStateStore) ListStates(ctx context.Context, filter map[string]string) ([]*IaCState, error) { - keys, err := s.client.ListObjects(ctx, s.prefix) - if err != nil { - return nil, fmt.Errorf("iac gcs state: ListStates: %w", err) - } - var results []*IaCState - for _, key := range keys { - if !strings.HasSuffix(key, ".json") { - continue - } - data, _, err := s.client.ReadObject(ctx, key) - if err != nil { - // A canceled / deadlined context must abort the listing rather - // than silently return partial results; only genuinely unreadable - // objects are skipped. - if ctx.Err() != nil || errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { - return nil, fmt.Errorf("iac gcs state: ListStates: %w", err) - } - continue - } - var st IaCState - if err := json.Unmarshal(data, &st); err != nil { - continue - } - if matchesFilter(&st, filter) { - results = append(results, &st) - } - } - return results, nil -} - -// DeleteState removes the state object for resourceID. -func (s *GCSIaCStateStore) DeleteState(ctx context.Context, resourceID string) error { - if err := s.client.DeleteObject(ctx, s.stateKey(resourceID)); err != nil { - if errors.Is(err, ErrGCSNotFound) { - return fmt.Errorf("iac gcs state: DeleteState %q: not found", resourceID) - } - return fmt.Errorf("iac gcs state: DeleteState %q: %w", resourceID, err) - } - return nil -} - -// Lock acquires an advisory lock using GCS generation-match preconditions. -// The lock object is written with If-None-Match (generation 0), which is atomic. -func (s *GCSIaCStateStore) Lock(ctx context.Context, resourceID string) error { - key := s.lockKey(resourceID) - body := []byte("locked") - gen, err := s.client.WriteObjectIfGenerationMatch(ctx, key, body, "text/plain", 0) - if err != nil { - if strings.Contains(err.Error(), "precondition failed") || strings.Contains(err.Error(), "exists") { - return fmt.Errorf("iac gcs state: Lock %q: resource is already locked", resourceID) - } - return fmt.Errorf("iac gcs state: Lock %q: %w", resourceID, err) - } - s.lockState[resourceID] = gen - return nil -} - -// Unlock removes the lock object for resourceID. -func (s *GCSIaCStateStore) Unlock(ctx context.Context, resourceID string) error { - key := s.lockKey(resourceID) - if err := s.client.DeleteObject(ctx, key); err != nil { - if errors.Is(err, ErrGCSNotFound) { - return fmt.Errorf("iac gcs state: Unlock %q: not locked", resourceID) - } - return fmt.Errorf("iac gcs state: Unlock %q: %w", resourceID, err) - } - delete(s.lockState, resourceID) - return nil -} - -// gcsRealClient wraps the real GCS client to satisfy GCSObjectClient. -type gcsRealClient struct { - client *storage.Client - bucket string -} - -func (c *gcsRealClient) ReadObject(ctx context.Context, key string) ([]byte, int64, error) { - obj := c.client.Bucket(c.bucket).Object(key) - attrs, err := obj.Attrs(ctx) - if err != nil { - if errors.Is(err, storage.ErrObjectNotExist) { - return nil, 0, ErrGCSNotFound - } - return nil, 0, err - } - r, err := obj.NewReader(ctx) - if err != nil { - return nil, 0, err - } - defer r.Close() - buf := new(bytes.Buffer) - if _, err := buf.ReadFrom(r); err != nil { - return nil, 0, err - } - return buf.Bytes(), attrs.Generation, nil -} - -func (c *gcsRealClient) WriteObject(ctx context.Context, key string, data []byte, contentType string) (int64, error) { - w := c.client.Bucket(c.bucket).Object(key).NewWriter(ctx) - w.ContentType = contentType - if _, err := w.Write(data); err != nil { - _ = w.Close() - return 0, err - } - if err := w.Close(); err != nil { - return 0, err - } - attrs, err := c.client.Bucket(c.bucket).Object(key).Attrs(ctx) - if err != nil { - return 0, err - } - return attrs.Generation, nil -} - -func (c *gcsRealClient) WriteObjectIfGenerationMatch(ctx context.Context, key string, data []byte, contentType string, generation int64) (int64, error) { - obj := c.client.Bucket(c.bucket).Object(key).If(storage.Conditions{GenerationMatch: generation}) - w := obj.NewWriter(ctx) - w.ContentType = contentType - if _, err := w.Write(data); err != nil { - _ = w.Close() - return 0, err - } - if err := w.Close(); err != nil { - return 0, fmt.Errorf("precondition failed: %w", err) - } - attrs, err := c.client.Bucket(c.bucket).Object(key).Attrs(ctx) - if err != nil { - return 0, err - } - return attrs.Generation, nil -} - -func (c *gcsRealClient) DeleteObject(ctx context.Context, key string) error { - err := c.client.Bucket(c.bucket).Object(key).Delete(ctx) - if errors.Is(err, storage.ErrObjectNotExist) { - return ErrGCSNotFound - } - return err -} - -func (c *gcsRealClient) ListObjects(ctx context.Context, prefix string) ([]string, error) { - it := c.client.Bucket(c.bucket).Objects(ctx, &storage.Query{Prefix: prefix}) - var keys []string - for { - attrs, err := it.Next() - if errors.Is(err, iterator.Done) { - break - } - if err != nil { - return nil, err - } - keys = append(keys, attrs.Name) - } - return keys, nil -} diff --git a/module/iac_state_gcs_test.go b/module/iac_state_gcs_test.go deleted file mode 100644 index b5b35504..00000000 --- a/module/iac_state_gcs_test.go +++ /dev/null @@ -1,248 +0,0 @@ -package module_test - -import ( - "context" - "encoding/json" - "fmt" - "strings" - "sync" - "testing" - - "github.com/GoCodeAlone/workflow/module" -) - -// mockGCSClient is an in-memory implementation of GCSObjectClient for testing. -type mockGCSClient struct { - mu sync.Mutex - objects map[string][]byte // key -> body - generation map[string]int64 // key -> current generation - errOnPut map[string]error // key -> error to return on conditional Put -} - -func newMockGCSClient() *mockGCSClient { - return &mockGCSClient{ - objects: make(map[string][]byte), - generation: make(map[string]int64), - errOnPut: make(map[string]error), - } -} - -func (m *mockGCSClient) ReadObject(_ context.Context, key string) ([]byte, int64, error) { - m.mu.Lock() - defer m.mu.Unlock() - data, ok := m.objects[key] - if !ok { - return nil, 0, module.ErrGCSNotFound - } - return data, m.generation[key], nil -} - -func (m *mockGCSClient) WriteObject(_ context.Context, key string, data []byte, _ string) (int64, error) { - m.mu.Lock() - defer m.mu.Unlock() - m.generation[key]++ - m.objects[key] = data - return m.generation[key], nil -} - -func (m *mockGCSClient) WriteObjectIfGenerationMatch(_ context.Context, key string, data []byte, _ string, generation int64) (int64, error) { - m.mu.Lock() - defer m.mu.Unlock() - if err, ok := m.errOnPut[key]; ok { - return 0, err - } - curr := m.generation[key] - if generation == 0 { - // Must not exist - if _, exists := m.objects[key]; exists { - return 0, fmt.Errorf("precondition failed: object exists") - } - } else if curr != generation { - return 0, fmt.Errorf("precondition failed: generation mismatch (want %d, have %d)", generation, curr) - } - m.generation[key]++ - m.objects[key] = data - return m.generation[key], nil -} - -func (m *mockGCSClient) DeleteObject(_ context.Context, key string) error { - m.mu.Lock() - defer m.mu.Unlock() - if _, ok := m.objects[key]; !ok { - return module.ErrGCSNotFound - } - delete(m.objects, key) - delete(m.generation, key) - return nil -} - -func (m *mockGCSClient) ListObjects(_ context.Context, prefix string) ([]string, error) { - m.mu.Lock() - defer m.mu.Unlock() - var keys []string - for k := range m.objects { - if strings.HasPrefix(k, prefix) { - keys = append(keys, k) - } - } - return keys, nil -} - -func newTestGCSStore(client module.GCSObjectClient) *module.GCSIaCStateStore { - return module.NewGCSIaCStateStoreWithClient(client, "test-bucket", "iac-state/") -} - -func TestGCSIaCStateStore_GetState_NotFound(t *testing.T) { - store := newTestGCSStore(newMockGCSClient()) - - st, err := store.GetState(context.Background(), "nonexistent") - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - if st != nil { - t.Fatalf("expected nil, got %+v", st) - } -} - -func TestGCSIaCStateStore_SaveAndGetState(t *testing.T) { - store := newTestGCSStore(newMockGCSClient()) - - state := &module.IaCState{ - ResourceID: "gcs-cluster", - ResourceType: "kubernetes", - Provider: "gcp", - Status: "active", - } - if err := store.SaveState(context.Background(), state); err != nil { - t.Fatalf("SaveState: %v", err) - } - - got, err := store.GetState(context.Background(), "gcs-cluster") - if err != nil { - t.Fatalf("GetState: %v", err) - } - if got == nil { - t.Fatal("expected state, got nil") - } - if got.Provider != "gcp" { - t.Errorf("Provider = %q, want %q", got.Provider, "gcp") - } -} - -func TestGCSIaCStateStore_SaveState_Nil(t *testing.T) { - store := newTestGCSStore(newMockGCSClient()) - if err := store.SaveState(context.Background(), nil); err == nil { - t.Fatal("expected error for nil state") - } -} - -func TestGCSIaCStateStore_SaveState_EmptyID(t *testing.T) { - store := newTestGCSStore(newMockGCSClient()) - if err := store.SaveState(context.Background(), &module.IaCState{}); err == nil { - t.Fatal("expected error for empty resource_id") - } -} - -func TestGCSIaCStateStore_ListStates(t *testing.T) { - store := newTestGCSStore(newMockGCSClient()) - - for _, st := range []*module.IaCState{ - {ResourceID: "r1", ResourceType: "k8s", Provider: "gcp", Status: "active"}, - {ResourceID: "r2", ResourceType: "db", Provider: "gcp", Status: "active"}, - {ResourceID: "r3", ResourceType: "k8s", Provider: "aws", Status: "destroyed"}, - } { - if err := store.SaveState(context.Background(), st); err != nil { - t.Fatalf("SaveState %q: %v", st.ResourceID, err) - } - } - - all, err := store.ListStates(context.Background(), nil) - if err != nil { - t.Fatalf("ListStates(nil): %v", err) - } - if len(all) != 3 { - t.Errorf("ListStates = %d, want 3", len(all)) - } - - filtered, err := store.ListStates(context.Background(), map[string]string{"provider": "gcp"}) - if err != nil { - t.Fatalf("ListStates(provider=gcp): %v", err) - } - if len(filtered) != 2 { - t.Errorf("ListStates(provider=gcp) = %d, want 2", len(filtered)) - } -} - -func TestGCSIaCStateStore_DeleteState(t *testing.T) { - store := newTestGCSStore(newMockGCSClient()) - - if err := store.SaveState(context.Background(), &module.IaCState{ResourceID: "del-me", Status: "active"}); err != nil { - t.Fatalf("SaveState: %v", err) - } - if err := store.DeleteState(context.Background(), "del-me"); err != nil { - t.Fatalf("DeleteState: %v", err) - } - st, err := store.GetState(context.Background(), "del-me") - if err != nil { - t.Fatalf("GetState after delete: %v", err) - } - if st != nil { - t.Fatal("expected nil after delete") - } -} - -func TestGCSIaCStateStore_DeleteState_NotFound(t *testing.T) { - store := newTestGCSStore(newMockGCSClient()) - if err := store.DeleteState(context.Background(), "nonexistent"); err == nil { - t.Fatal("expected error deleting nonexistent state") - } -} - -func TestGCSIaCStateStore_LockUnlock(t *testing.T) { - store := newTestGCSStore(newMockGCSClient()) - - if err := store.Lock(context.Background(), "res-1"); err != nil { - t.Fatalf("Lock: %v", err) - } - // Double lock must fail. - if err := store.Lock(context.Background(), "res-1"); err == nil { - t.Fatal("expected error on double lock") - } - if err := store.Unlock(context.Background(), "res-1"); err != nil { - t.Fatalf("Unlock: %v", err) - } - // Re-lock should succeed after unlock. - if err := store.Lock(context.Background(), "res-1"); err != nil { - t.Fatalf("Lock after unlock: %v", err) - } -} - -func TestGCSIaCStateStore_Unlock_NotLocked(t *testing.T) { - store := newTestGCSStore(newMockGCSClient()) - if err := store.Unlock(context.Background(), "not-locked"); err == nil { - t.Fatal("expected error unlocking non-locked resource") - } -} - -func TestGCSIaCStateStore_JSONRoundTrip(t *testing.T) { - store := newTestGCSStore(newMockGCSClient()) - - state := &module.IaCState{ - ResourceID: "rt-gcs", - Provider: "gcp", - Status: "active", - Outputs: map[string]any{"endpoint": "https://gcs.example.com"}, - } - if err := store.SaveState(context.Background(), state); err != nil { - t.Fatalf("SaveState: %v", err) - } - got, err := store.GetState(context.Background(), "rt-gcs") - if err != nil { - t.Fatalf("GetState: %v", err) - } - wantJSON, _ := json.Marshal(state) - gotJSON, _ := json.Marshal(got) - if string(wantJSON) != string(gotJSON) { - t.Errorf("round-trip mismatch:\n want: %s\n got: %s", wantJSON, gotJSON) - } -} diff --git a/module/platform_kubernetes_gke.go b/module/platform_kubernetes_gke.go deleted file mode 100644 index 4304ddf3..00000000 --- a/module/platform_kubernetes_gke.go +++ /dev/null @@ -1,231 +0,0 @@ -// platform_kubernetes_gke.go holds the GKE Kubernetes backend — the only -// platform.kubernetes backend that imports a cloud SDK (google.golang.org/api). -// Isolated here so the cloud-SDK extraction can delete it cleanly; the SDK-free -// backends stay in platform_kubernetes_core.go. -package module - -import ( - "context" - "fmt" - "strings" - "time" - - container "google.golang.org/api/container/v1" - "google.golang.org/api/option" -) - -// gkeBackend manages Google Kubernetes Engine clusters via the GCP Container API. -type gkeBackend struct{} - -func (b *gkeBackend) gkeLocation(k *PlatformKubernetes) string { - if z, ok := k.config["zone"].(string); ok && z != "" { - return z - } - if r, ok := k.config["location"].(string); ok && r != "" { - return r - } - if k.provider != nil { - return k.provider.Region() - } - return "us-central1" -} - -func (b *gkeBackend) gkeProject(k *PlatformKubernetes) string { - if p, ok := k.config["project_id"].(string); ok && p != "" { - return p - } - if k.provider != nil { - if creds, err := k.provider.GetCredentials(context.Background()); err == nil && creds.ProjectID != "" { - return creds.ProjectID - } - } - return "" -} - -func (b *gkeBackend) plan(k *PlatformKubernetes) (*PlatformPlan, error) { - project := b.gkeProject(k) - if project == "" { - return nil, fmt.Errorf("gke plan: 'project_id' is required in module config or cloud account") - } - location := b.gkeLocation(k) - - plan := &PlatformPlan{ - Provider: "gke", - Resource: k.clusterName(), - } - - action := PlatformAction{Type: "create", Resource: k.clusterName(), Detail: fmt.Sprintf("create GKE cluster %q in %s", k.clusterName(), location)} - - if svc, svcErr := b.containerService(k); svcErr == nil { - name := fmt.Sprintf("projects/%s/locations/%s/clusters/%s", project, location, k.clusterName()) - if cluster, getErr := svc.Projects.Locations.Clusters.Get(name).Context(context.Background()).Do(); getErr == nil { - action = PlatformAction{Type: "noop", Resource: k.clusterName(), Detail: fmt.Sprintf("GKE cluster %q exists (status: %s)", k.clusterName(), cluster.Status)} - } - } - - plan.Actions = []PlatformAction{action} - return plan, nil -} - -func (b *gkeBackend) apply(k *PlatformKubernetes) (*PlatformResult, error) { - project := b.gkeProject(k) - if project == "" { - return nil, fmt.Errorf("gke apply: 'project_id' is required in module config or cloud account") - } - location := b.gkeLocation(k) - - svc, err := b.containerService(k) - if err != nil { - return nil, fmt.Errorf("gke apply: GCP credentials: %w", err) - } - - version := k.state.Version - if version == "" { - version = "1.29" - } - - // Build node pools from nodeGroups config - var nodePools []*container.NodePool - for _, ng := range k.nodeGroups() { - machineType := ng.InstanceType - if machineType == "" { - machineType = "e2-medium" - } - nodePools = append(nodePools, &container.NodePool{ - Name: ng.Name, - InitialNodeCount: int64(ng.Min), - Config: &container.NodeConfig{ - MachineType: machineType, - }, - Autoscaling: &container.NodePoolAutoscaling{ - Enabled: true, - MinNodeCount: int64(ng.Min), - MaxNodeCount: int64(ng.Max), - }, - }) - } - if len(nodePools) == 0 { - nodePools = []*container.NodePool{{ - Name: "default-pool", - InitialNodeCount: 1, - Config: &container.NodeConfig{MachineType: "e2-medium"}, - }} - } - - parent := fmt.Sprintf("projects/%s/locations/%s", project, location) - req := &container.CreateClusterRequest{ - Cluster: &container.Cluster{ - Name: k.clusterName(), - InitialClusterVersion: version, - NodePools: nodePools, - }, - } - - _, err = svc.Projects.Locations.Clusters.Create(parent, req).Context(context.Background()).Do() - if err != nil { - if strings.Contains(err.Error(), "Already Exists") || strings.Contains(err.Error(), "ALREADY_EXISTS") { - return &PlatformResult{ - Success: true, - Message: fmt.Sprintf("GKE cluster %q already exists", k.clusterName()), - State: k.state, - }, nil - } - return nil, fmt.Errorf("gke apply: CreateCluster: %w", err) - } - - k.state.Status = "creating" - k.state.NodeGroups = k.nodeGroups() - k.state.CreatedAt = time.Now() - - return &PlatformResult{ - Success: true, - Message: fmt.Sprintf("GKE cluster %q creation initiated in %s", k.clusterName(), location), - State: k.state, - }, nil -} - -func (b *gkeBackend) status(k *PlatformKubernetes) (*KubernetesClusterState, error) { - project := b.gkeProject(k) - if project == "" { - k.state.Status = "unknown" - return k.state, nil - } - location := b.gkeLocation(k) - - if svc, svcErr := b.containerService(k); svcErr == nil { - name := fmt.Sprintf("projects/%s/locations/%s/clusters/%s", project, location, k.clusterName()) - if cluster, getErr := svc.Projects.Locations.Clusters.Get(name).Context(context.Background()).Do(); getErr == nil { - k.state.Status = strings.ToLower(cluster.Status) - k.state.Endpoint = cluster.Endpoint - if cluster.CurrentMasterVersion != "" { - k.state.Version = cluster.CurrentMasterVersion - } - var groups []NodeGroupState - for _, np := range cluster.NodePools { - groups = append(groups, NodeGroupState{ - Name: np.Name, - Current: int(np.InitialNodeCount), - }) - } - k.state.NodeGroups = groups - } else { - k.state.Status = "not-found" - } - } - - return k.state, nil -} - -func (b *gkeBackend) destroy(k *PlatformKubernetes) error { - project := b.gkeProject(k) - if project == "" { - return fmt.Errorf("gke destroy: 'project_id' is required") - } - location := b.gkeLocation(k) - - svc, err := b.containerService(k) - if err != nil { - return fmt.Errorf("gke destroy: GCP credentials: %w", err) - } - - name := fmt.Sprintf("projects/%s/locations/%s/clusters/%s", project, location, k.clusterName()) - _, err = svc.Projects.Locations.Clusters.Delete(name).Context(context.Background()).Do() - if err != nil { - if strings.Contains(err.Error(), "NOT_FOUND") || strings.Contains(err.Error(), "notFound") { - k.state.Status = "deleted" - return nil - } - return fmt.Errorf("gke destroy: DeleteCluster: %w", err) - } - - k.state.Status = "deleting" - return nil -} - -func (b *gkeBackend) containerService(k *PlatformKubernetes) (*container.Service, error) { - if k.provider == nil { - return nil, fmt.Errorf("no GCP cloud account configured") - } - - creds, err := k.provider.GetCredentials(context.Background()) - if err != nil { - return nil, fmt.Errorf("get GCP credentials: %w", err) - } - - var opts []option.ClientOption - if len(creds.ServiceAccountJSON) > 0 { - opts = append(opts, option.WithCredentialsJSON(creds.ServiceAccountJSON)) //nolint:staticcheck // SA1019: no alternative available without security advisory scope - } - - svc, err := container.NewService(context.Background(), opts...) - if err != nil { - return nil, fmt.Errorf("create container service: %w", err) - } - return svc, nil -} - -func init() { - RegisterKubernetesBackend("gke", func(_ map[string]any) (kubernetesBackend, error) { - return &gkeBackend{}, nil - }) -} diff --git a/module/storage_gcs.go b/module/storage_gcs.go deleted file mode 100644 index 5c15f49e..00000000 --- a/module/storage_gcs.go +++ /dev/null @@ -1,251 +0,0 @@ -package module - -import ( - "context" - "fmt" - "io" - - "cloud.google.com/go/storage" - "github.com/GoCodeAlone/modular" - "github.com/GoCodeAlone/workflow/store" - "google.golang.org/api/iterator" - "google.golang.org/api/option" -) - -// gcsBucketHandle abstracts a GCS bucket handle for testability. -type gcsBucketHandle interface { - Objects(ctx context.Context, q *storage.Query) objectIterator - Object(name string) objectHandle -} - -// objectIterator abstracts a GCS object iterator. -type objectIterator interface { - Next() (*storage.ObjectAttrs, error) -} - -// objectHandle abstracts a GCS object handle. -type objectHandle interface { - NewReader(ctx context.Context) (io.ReadCloser, error) - NewWriter(ctx context.Context) io.WriteCloser - Delete(ctx context.Context) error - Attrs(ctx context.Context) (*storage.ObjectAttrs, error) -} - -// realBucketHandle wraps *storage.BucketHandle to satisfy gcsBucketHandle. -type realBucketHandle struct{ bh *storage.BucketHandle } - -func (r *realBucketHandle) Objects(ctx context.Context, q *storage.Query) objectIterator { - return r.bh.Objects(ctx, q) -} - -func (r *realBucketHandle) Object(name string) objectHandle { - return &realObjectHandle{r.bh.Object(name)} -} - -// realObjectHandle wraps *storage.ObjectHandle to satisfy objectHandle. -type realObjectHandle struct{ oh *storage.ObjectHandle } - -func (r *realObjectHandle) NewReader(ctx context.Context) (io.ReadCloser, error) { - return r.oh.NewReader(ctx) -} - -func (r *realObjectHandle) NewWriter(ctx context.Context) io.WriteCloser { - return r.oh.NewWriter(ctx) -} - -func (r *realObjectHandle) Delete(ctx context.Context) error { return r.oh.Delete(ctx) } - -func (r *realObjectHandle) Attrs(ctx context.Context) (*storage.ObjectAttrs, error) { - return r.oh.Attrs(ctx) -} - -// GCSStorage provides object storage operations using Google Cloud Storage. -type GCSStorage struct { - name string - bucket string - project string - credentialsFile string - client *storage.Client - testBucket gcsBucketHandle // non-nil only in tests - logger modular.Logger -} - -// SetBucketHandle injects a gcsBucketHandle, used in tests to avoid real GCS calls. -func (g *GCSStorage) SetBucketHandle(bh gcsBucketHandle) { g.testBucket = bh } - -// getBucket returns the bucket handle, preferring the injected test handle. -func (g *GCSStorage) getBucket() gcsBucketHandle { - if g.testBucket != nil { - return g.testBucket - } - if g.client == nil { - return nil - } - return &realBucketHandle{g.client.Bucket(g.bucket)} -} - -// NewGCSStorage creates a new GCS storage module. -func NewGCSStorage(name string) *GCSStorage { - return &GCSStorage{ - name: name, - logger: &noopLogger{}, - } -} - -func (g *GCSStorage) Name() string { return g.name } - -func (g *GCSStorage) Init(app modular.Application) error { - g.logger = app.Logger() - return nil -} - -func (g *GCSStorage) ProvidesServices() []modular.ServiceProvider { - return []modular.ServiceProvider{ - { - Name: g.name, - Description: "Google Cloud Storage", - Instance: g, - }, - } -} - -func (g *GCSStorage) RequiresServices() []modular.ServiceDependency { - return nil -} - -// SetBucket sets the GCS bucket name. -func (g *GCSStorage) SetBucket(bucket string) { g.bucket = bucket } - -// SetProject sets the GCP project ID. -func (g *GCSStorage) SetProject(project string) { g.project = project } - -// SetCredentialsFile sets the path to a service account JSON key file. -func (g *GCSStorage) SetCredentialsFile(path string) { g.credentialsFile = path } - -// Start initializes the GCS client. -func (g *GCSStorage) Start(ctx context.Context) error { - opts := []option.ClientOption{} - if g.credentialsFile != "" { - opts = append(opts, option.WithAuthCredentialsFile(option.ServiceAccount, g.credentialsFile)) - } - if g.project != "" { - opts = append(opts, option.WithQuotaProject(g.project)) - } - - client, err := storage.NewClient(ctx, opts...) - if err != nil { - return fmt.Errorf("failed to create GCS client: %w", err) - } - - g.client = client - g.logger.Info("GCS storage started", "bucket", g.bucket, "project", g.project) - return nil -} - -// Stop closes the GCS client. -func (g *GCSStorage) Stop(_ context.Context) error { - if g.client != nil { - if err := g.client.Close(); err != nil { - return fmt.Errorf("failed to close GCS client: %w", err) - } - g.client = nil - } - g.logger.Info("GCS storage stopped") - return nil -} - -// List returns file entries under the given prefix. -func (g *GCSStorage) List(ctx context.Context, prefix string) ([]store.FileInfo, error) { - if g.client == nil && g.testBucket == nil { - return nil, fmt.Errorf("GCS client not initialized; call Start first") - } - - it := g.getBucket().Objects(ctx, &storage.Query{Prefix: prefix}) - var files []store.FileInfo - for { - attrs, err := it.Next() - if err == iterator.Done { - break - } - if err != nil { - return nil, fmt.Errorf("failed to list objects with prefix %q: %w", prefix, err) - } - files = append(files, store.FileInfo{ - Name: attrs.Name, - Path: attrs.Name, - Size: attrs.Size, - ModTime: attrs.Updated, - }) - } - return files, nil -} - -// Get retrieves an object from GCS. -func (g *GCSStorage) Get(ctx context.Context, key string) (io.ReadCloser, error) { - if g.client == nil && g.testBucket == nil { - return nil, fmt.Errorf("GCS client not initialized; call Start first") - } - - r, err := g.getBucket().Object(key).NewReader(ctx) - if err != nil { - return nil, fmt.Errorf("failed to get object %q: %w", key, err) - } - return r, nil -} - -// Put uploads an object to GCS. -func (g *GCSStorage) Put(ctx context.Context, key string, reader io.Reader) error { - if g.client == nil && g.testBucket == nil { - return fmt.Errorf("GCS client not initialized; call Start first") - } - - w := g.getBucket().Object(key).NewWriter(ctx) - if _, err := io.Copy(w, reader); err != nil { - _ = w.Close() - return fmt.Errorf("failed to write object %q: %w", key, err) - } - if err := w.Close(); err != nil { - return fmt.Errorf("failed to close writer for object %q: %w", key, err) - } - - g.logger.Info("Object uploaded", "key", key, "bucket", g.bucket) - return nil -} - -// Delete removes an object from GCS. -func (g *GCSStorage) Delete(ctx context.Context, key string) error { - if g.client == nil && g.testBucket == nil { - return fmt.Errorf("GCS client not initialized; call Start first") - } - - if err := g.getBucket().Object(key).Delete(ctx); err != nil { - return fmt.Errorf("failed to delete object %q: %w", key, err) - } - - g.logger.Info("Object deleted", "key", key, "bucket", g.bucket) - return nil -} - -// Stat returns metadata for an object. -func (g *GCSStorage) Stat(ctx context.Context, key string) (store.FileInfo, error) { - if g.client == nil && g.testBucket == nil { - return store.FileInfo{}, fmt.Errorf("GCS client not initialized; call Start first") - } - - attrs, err := g.getBucket().Object(key).Attrs(ctx) - if err != nil { - return store.FileInfo{}, fmt.Errorf("failed to stat object %q: %w", key, err) - } - - return store.FileInfo{ - Name: attrs.Name, - Path: attrs.Name, - Size: attrs.Size, - ModTime: attrs.Updated, - }, nil -} - -// MkdirAll is a no-op for object storage (GCS has no real directories). -func (g *GCSStorage) MkdirAll(_ context.Context, _ string) error { - return nil -} diff --git a/module/storage_gcs_test.go b/module/storage_gcs_test.go deleted file mode 100644 index 16bc9fe3..00000000 --- a/module/storage_gcs_test.go +++ /dev/null @@ -1,391 +0,0 @@ -package module - -import ( - "bytes" - "context" - "fmt" - "io" - "strings" - "testing" - "time" - - "cloud.google.com/go/storage" - "google.golang.org/api/iterator" -) - -// ---- mock helpers ---- - -// mockObjectIterator returns a fixed list of attrs, then iterator.Done. -type mockObjectIterator struct { - items []*storage.ObjectAttrs - pos int - err error // returned after all items -} - -func (m *mockObjectIterator) Next() (*storage.ObjectAttrs, error) { - if m.err != nil && m.pos >= len(m.items) { - return nil, m.err - } - if m.pos >= len(m.items) { - return nil, iterator.Done - } - a := m.items[m.pos] - m.pos++ - return a, nil -} - -// mockObjectHandle simulates a single GCS object. -type mockObjectHandle struct { - content []byte - attrs *storage.ObjectAttrs - readErr error - writeErr error - deleteErr error - attrsErr error - written *bytes.Buffer // captures Put data -} - -func (m *mockObjectHandle) NewReader(_ context.Context) (io.ReadCloser, error) { - if m.readErr != nil { - return nil, m.readErr - } - return io.NopCloser(bytes.NewReader(m.content)), nil -} - -func (m *mockObjectHandle) NewWriter(_ context.Context) io.WriteCloser { - m.written = &bytes.Buffer{} - return &mockWriteCloser{buf: m.written, closeErr: m.writeErr} -} - -func (m *mockObjectHandle) Delete(_ context.Context) error { return m.deleteErr } - -func (m *mockObjectHandle) Attrs(_ context.Context) (*storage.ObjectAttrs, error) { - if m.attrsErr != nil { - return nil, m.attrsErr - } - return m.attrs, nil -} - -// mockWriteCloser is an io.WriteCloser backed by a bytes.Buffer. -type mockWriteCloser struct { - buf *bytes.Buffer - closeErr error -} - -func (w *mockWriteCloser) Write(p []byte) (int, error) { return w.buf.Write(p) } -func (w *mockWriteCloser) Close() error { return w.closeErr } - -// mockBucketHandle routes Object() calls to per-key mockObjectHandles. -type mockBucketHandle struct { - objects map[string]*mockObjectHandle - listErr error - listItems []*storage.ObjectAttrs -} - -func (m *mockBucketHandle) Objects(_ context.Context, _ *storage.Query) objectIterator { - return &mockObjectIterator{items: m.listItems, err: m.listErr} -} - -func (m *mockBucketHandle) Object(name string) objectHandle { - if oh, ok := m.objects[name]; ok { - return oh - } - return &mockObjectHandle{attrsErr: fmt.Errorf("object %q not found", name), - readErr: fmt.Errorf("object %q not found", name), - deleteErr: fmt.Errorf("object %q not found", name), - } -} - -// newGCSWithMock wires up a GCSStorage backed by a mockBucketHandle. -func newGCSWithMock(bh gcsBucketHandle) *GCSStorage { - g := NewGCSStorage("gcs-test") - g.SetBucketHandle(bh) - return g -} - -// ---- existing basic tests ---- - -func TestGCSStorageName(t *testing.T) { - g := NewGCSStorage("gcs-test") - if g.Name() != "gcs-test" { - t.Errorf("expected name 'gcs-test', got %q", g.Name()) - } -} - -func TestGCSStorageModuleInterface(t *testing.T) { - g := NewGCSStorage("gcs-test") - - // Test Init - app, _ := NewTestApplication() - if err := g.Init(app); err != nil { - t.Fatalf("Init failed: %v", err) - } - - // Test ProvidesServices - services := g.ProvidesServices() - if len(services) != 1 { - t.Fatalf("expected 1 service, got %d", len(services)) - } - if services[0].Name != "gcs-test" { - t.Errorf("expected service name 'gcs-test', got %q", services[0].Name) - } - - // Test RequiresServices - deps := g.RequiresServices() - if len(deps) != 0 { - t.Errorf("expected no dependencies, got %d", len(deps)) - } -} - -func TestGCSStorageConfig(t *testing.T) { - g := NewGCSStorage("gcs-test") - - g.SetBucket("my-bucket") - if g.bucket != "my-bucket" { - t.Errorf("expected bucket 'my-bucket', got %q", g.bucket) - } - - g.SetProject("my-project") - if g.project != "my-project" { - t.Errorf("expected project 'my-project', got %q", g.project) - } - - g.SetCredentialsFile("/path/to/creds.json") - if g.credentialsFile != "/path/to/creds.json" { - t.Errorf("expected credentialsFile '/path/to/creds.json', got %q", g.credentialsFile) - } -} - -func TestGCSStorageOperationsWithoutClient(t *testing.T) { - g := NewGCSStorage("gcs-test") - - ctx := context.Background() - - // Operations should fail without Start - if _, err := g.List(ctx, ""); err == nil { - t.Error("List should fail without initialized client") - } - - if _, err := g.Get(ctx, "key"); err == nil { - t.Error("Get should fail without initialized client") - } - - if err := g.Put(ctx, "key", nil); err == nil { - t.Error("Put should fail without initialized client") - } - - if err := g.Delete(ctx, "key"); err == nil { - t.Error("Delete should fail without initialized client") - } - - if _, err := g.Stat(ctx, "key"); err == nil { - t.Error("Stat should fail without initialized client") - } -} - -func TestGCSStorageStop(t *testing.T) { - g := NewGCSStorage("gcs-test") - app, _ := NewTestApplication() - _ = g.Init(app) - - // Stop without Start should be safe (no-op when client is nil) - if err := g.Stop(context.Background()); err != nil { - t.Fatalf("Stop without Start failed: %v", err) - } -} - -func TestGCSStorageMkdirAll(t *testing.T) { - g := NewGCSStorage("gcs-test") - - // MkdirAll is a no-op for object storage - if err := g.MkdirAll(context.Background(), "some/path"); err != nil { - t.Fatalf("MkdirAll should be a no-op, got error: %v", err) - } -} - -// ---- mock-based tests ---- - -func TestGCSStorage_List(t *testing.T) { - now := time.Now() - bh := &mockBucketHandle{ - listItems: []*storage.ObjectAttrs{ - {Name: "prefix/a.txt", Size: 10, Updated: now}, - {Name: "prefix/b.txt", Size: 20, Updated: now}, - }, - } - g := newGCSWithMock(bh) - - files, err := g.List(context.Background(), "prefix/") - if err != nil { - t.Fatalf("List: %v", err) - } - if len(files) != 2 { - t.Fatalf("expected 2 files, got %d", len(files)) - } - if files[0].Name != "prefix/a.txt" || files[0].Size != 10 { - t.Errorf("unexpected first file: %+v", files[0]) - } - if files[1].Name != "prefix/b.txt" || files[1].Size != 20 { - t.Errorf("unexpected second file: %+v", files[1]) - } - if !files[0].ModTime.Equal(now) { - t.Errorf("ModTime mismatch: got %v, want %v", files[0].ModTime, now) - } -} - -func TestGCSStorage_ListEmpty(t *testing.T) { - bh := &mockBucketHandle{} - g := newGCSWithMock(bh) - - files, err := g.List(context.Background(), "none/") - if err != nil { - t.Fatalf("List empty: %v", err) - } - if len(files) != 0 { - t.Errorf("expected 0 files, got %d", len(files)) - } -} - -func TestGCSStorage_ListError(t *testing.T) { - bh := &mockBucketHandle{ - listItems: []*storage.ObjectAttrs{{Name: "a.txt", Size: 1}}, - listErr: fmt.Errorf("permission denied"), - } - g := newGCSWithMock(bh) - - _, err := g.List(context.Background(), "") - if err == nil { - t.Fatal("expected error from iterator, got nil") - } - if !strings.Contains(err.Error(), "permission denied") { - t.Errorf("unexpected error: %v", err) - } -} - -func TestGCSStorage_Get(t *testing.T) { - content := []byte("hello gcs") - bh := &mockBucketHandle{ - objects: map[string]*mockObjectHandle{ - "myfile.txt": {content: content}, - }, - } - g := newGCSWithMock(bh) - - rc, err := g.Get(context.Background(), "myfile.txt") - if err != nil { - t.Fatalf("Get: %v", err) - } - defer rc.Close() - - got, err := io.ReadAll(rc) - if err != nil { - t.Fatalf("ReadAll: %v", err) - } - if !bytes.Equal(got, content) { - t.Errorf("content mismatch: got %q, want %q", got, content) - } -} - -func TestGCSStorage_GetNotFound(t *testing.T) { - bh := &mockBucketHandle{objects: map[string]*mockObjectHandle{}} - g := newGCSWithMock(bh) - - _, err := g.Get(context.Background(), "missing.txt") - if err == nil { - t.Fatal("expected error for missing object, got nil") - } - if !strings.Contains(err.Error(), "not found") { - t.Errorf("unexpected error: %v", err) - } -} - -func TestGCSStorage_Put(t *testing.T) { - oh := &mockObjectHandle{} - bh := &mockBucketHandle{objects: map[string]*mockObjectHandle{"upload.txt": oh}} - g := newGCSWithMock(bh) - - data := []byte("data to upload") - if err := g.Put(context.Background(), "upload.txt", bytes.NewReader(data)); err != nil { - t.Fatalf("Put: %v", err) - } - - if oh.written == nil { - t.Fatal("expected data to be written, but writer was never used") - } - if !bytes.Equal(oh.written.Bytes(), data) { - t.Errorf("written data mismatch: got %q, want %q", oh.written.Bytes(), data) - } -} - -func TestGCSStorage_PutWriteError(t *testing.T) { - oh := &mockObjectHandle{writeErr: fmt.Errorf("write failed")} - bh := &mockBucketHandle{objects: map[string]*mockObjectHandle{"bad.txt": oh}} - g := newGCSWithMock(bh) - - err := g.Put(context.Background(), "bad.txt", bytes.NewReader([]byte("x"))) - if err == nil { - t.Fatal("expected write error, got nil") - } - if !strings.Contains(err.Error(), "write failed") { - t.Errorf("unexpected error: %v", err) - } -} - -func TestGCSStorage_Delete(t *testing.T) { - oh := &mockObjectHandle{} - bh := &mockBucketHandle{objects: map[string]*mockObjectHandle{"del.txt": oh}} - g := newGCSWithMock(bh) - - if err := g.Delete(context.Background(), "del.txt"); err != nil { - t.Fatalf("Delete: %v", err) - } -} - -func TestGCSStorage_DeleteNotFound(t *testing.T) { - bh := &mockBucketHandle{objects: map[string]*mockObjectHandle{}} - g := newGCSWithMock(bh) - - err := g.Delete(context.Background(), "ghost.txt") - if err == nil { - t.Fatal("expected error for missing object, got nil") - } - if !strings.Contains(err.Error(), "not found") { - t.Errorf("unexpected error: %v", err) - } -} - -func TestGCSStorage_Stat(t *testing.T) { - now := time.Now() - oh := &mockObjectHandle{ - attrs: &storage.ObjectAttrs{Name: "stat.txt", Size: 42, Updated: now}, - } - bh := &mockBucketHandle{objects: map[string]*mockObjectHandle{"stat.txt": oh}} - g := newGCSWithMock(bh) - - info, err := g.Stat(context.Background(), "stat.txt") - if err != nil { - t.Fatalf("Stat: %v", err) - } - if info.Name != "stat.txt" { - t.Errorf("Name: got %q, want 'stat.txt'", info.Name) - } - if info.Size != 42 { - t.Errorf("Size: got %d, want 42", info.Size) - } - if !info.ModTime.Equal(now) { - t.Errorf("ModTime mismatch: got %v, want %v", info.ModTime, now) - } -} - -func TestGCSStorage_StatNotFound(t *testing.T) { - bh := &mockBucketHandle{objects: map[string]*mockObjectHandle{}} - g := newGCSWithMock(bh) - - _, err := g.Stat(context.Background(), "noexist.txt") - if err == nil { - t.Fatal("expected error for missing object, got nil") - } - if !strings.Contains(err.Error(), "not found") { - t.Errorf("unexpected error: %v", err) - } -} diff --git a/plugins/storage/plugin.go b/plugins/storage/plugin.go index febbe33e..17d3dc97 100644 --- a/plugins/storage/plugin.go +++ b/plugins/storage/plugin.go @@ -12,8 +12,8 @@ import ( ) // Plugin provides storage and database capabilities: storage.local, -// storage.gcs, storage.sqlite, storage.artifact, database.workflow, -// persistence.store, cache.redis modules, and artifact pipeline step factories. +// storage.sqlite, storage.artifact, database.workflow, persistence.store, +// cache.redis modules, and artifact pipeline step factories. type Plugin struct { plugin.BaseEnginePlugin } @@ -35,7 +35,6 @@ func New() *Plugin { Tier: plugin.TierCore, ModuleTypes: []string{ "storage.local", - "storage.gcs", "storage.sqlite", "storage.artifact", "database.workflow", @@ -92,19 +91,6 @@ func (p *Plugin) ModuleFactories() map[string]plugin.ModuleFactory { } return module.NewLocalStorageModule(name, rootDir) }, - "storage.gcs": func(name string, cfg map[string]any) modular.Module { - gcsMod := module.NewGCSStorage(name) - if bucket, ok := cfg["bucket"].(string); ok { - gcsMod.SetBucket(bucket) - } - if project, ok := cfg["project"].(string); ok { - gcsMod.SetProject(project) - } - if creds, ok := cfg["credentialsFile"].(string); ok { - gcsMod.SetCredentialsFile(creds) - } - return gcsMod - }, "storage.sqlite": func(name string, cfg map[string]any) modular.Module { dbPath := "data/workflow.db" if p, ok := cfg["dbPath"].(string); ok && p != "" { @@ -320,19 +306,6 @@ func (p *Plugin) ModuleSchemas() []*schema.ModuleSchema { }, DefaultConfig: map[string]any{"rootDir": "./data/storage"}, }, - { - Type: "storage.gcs", - Label: "GCS Storage", - Category: "integration", - Description: "Google Cloud Storage integration", - Inputs: []schema.ServiceIODef{{Name: "object", Type: "[]byte", Description: "Object data to store or retrieve"}}, - Outputs: []schema.ServiceIODef{{Name: "storage", Type: "ObjectStore", Description: "GCS-compatible object storage service"}}, - ConfigFields: []schema.ConfigFieldDef{ - {Key: "bucket", Label: "Bucket", Type: schema.FieldTypeString, Required: true, Description: "GCS bucket name", Placeholder: "my-bucket"}, - {Key: "project", Label: "GCP Project", Type: schema.FieldTypeString, Description: "Google Cloud project ID", Placeholder: "my-project"}, - {Key: "credentialsFile", Label: "Credentials File", Type: schema.FieldTypeFilePath, Description: "Path to service account JSON key file", Placeholder: "credentials/gcs-key.json", Sensitive: true}, - }, - }, { Type: "storage.sqlite", Label: "SQLite Storage", diff --git a/plugins/storage/plugin_test.go b/plugins/storage/plugin_test.go index 80fc6ca2..31716fa6 100644 --- a/plugins/storage/plugin_test.go +++ b/plugins/storage/plugin_test.go @@ -21,8 +21,8 @@ func TestPluginManifest(t *testing.T) { if m.Name != "storage" { t.Errorf("expected name %q, got %q", "storage", m.Name) } - if len(m.ModuleTypes) != 8 { - t.Errorf("expected 8 module types, got %d", len(m.ModuleTypes)) + if len(m.ModuleTypes) != 7 { + t.Errorf("expected 7 module types, got %d", len(m.ModuleTypes)) } if len(m.StepTypes) != 4 { t.Errorf("expected 4 step types, got %d", len(m.StepTypes)) @@ -51,7 +51,7 @@ func TestModuleFactories(t *testing.T) { factories := p.ModuleFactories() expectedTypes := []string{ - "storage.local", "storage.gcs", + "storage.local", "storage.sqlite", "database.workflow", "persistence.store", "cache.redis", } @@ -101,15 +101,6 @@ func TestModuleFactoryWithConfig(t *testing.T) { t.Fatal("persistence.store factory returned nil with config") } - // storage.gcs with config - mod = factories["storage.gcs"]("gcs-test", map[string]any{ - "bucket": "test-bucket", - "project": "test-project", - "credentialsFile": "/tmp/creds.json", - }) - if mod == nil { - t.Fatal("storage.gcs factory returned nil with config") - } } func TestStepFactories(t *testing.T) { @@ -124,8 +115,8 @@ func TestStepFactories(t *testing.T) { func TestModuleSchemas(t *testing.T) { p := New() schemas := p.ModuleSchemas() - if len(schemas) != 8 { - t.Fatalf("expected 8 module schemas, got %d", len(schemas)) + if len(schemas) != 7 { + t.Fatalf("expected 7 module schemas, got %d", len(schemas)) } types := map[string]bool{} @@ -133,7 +124,7 @@ func TestModuleSchemas(t *testing.T) { types[s.Type] = true } expectedTypes := []string{ - "storage.local", "storage.gcs", + "storage.local", "storage.sqlite", "database.workflow", "database.partitioned", "persistence.store", "cache.redis", } From 4a80f71b363dbbef5f26e946320d1bc74b607c17 Mon Sep 17 00:00:00 2001 From: Jon Langevin Date: Fri, 15 May 2026 18:52:06 -0400 Subject: [PATCH 2/8] build: drop GCP storage/SDK from workflow core + arm asymmetric CI gate (allowlist OAuth2 ADC compute/metadata) + wfctl gcs actionable error + replace stylistic google.Endpoint in auth_oauth2.go --- .github/workflows/ci.yml | 36 ++++++++++++++++- .phase-c-complete | 0 cmd/wfctl/infra_state_store.go | 5 +-- go.mod | 24 ------------ go.sum | 60 ---------------------------- module/auth_oauth2.go | 17 +++++++- scripts/audit-cloud-symbols.sh | 72 ++++++++++++++++++++++++++++++++++ 7 files changed, 123 insertions(+), 91 deletions(-) create mode 100644 .phase-c-complete diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 90187b3a..c025afc0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -395,12 +395,44 @@ jobs: ! grep -qH "digitalocean/godo" go.mod example/go.mod cloud-sdk-audit: - name: Cloud-SDK inventory + k8s-backend init() partition audit + name: Cloud-SDK inventory + k8s-backend init() partition + asymmetric graph audit runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - name: Audit cloud-SDK imports + init() partition + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + cache: true + - name: Download module deps (needed for `go list -deps`) + env: + GOWORK: "off" + run: go mod download + - name: Audit cloud-SDK imports + init() partition + Phase C asymmetric gate + # The script enforces (once .phase-c-complete is armed): + # - module/ zero gcp/azure SDK real imports + # - build graph: zero Azure/azure-sdk-for-go, zero google.golang.org/api, + # zero cloud.google.com/go except compute/metadata (OAuth2 ADC helper + # legitimately pulled by provider/gcp's service-account auth). run: bash scripts/audit-cloud-symbols.sh --check + - name: Standalone asymmetric gate — `go list -deps` (mirrors audit script) + # Independent assertion of the same invariant for clarity in CI failure logs. + # Any cloud.google.com/go path other than compute/metadata fails the build. + env: + GOWORK: "off" + run: | + UNEXPECTED=$(go list -deps ./... \ + | grep -E '^(cloud\.google\.com/go|google\.golang\.org/api|github\.com/Azure/azure-sdk-for-go)' \ + | grep -v '^cloud\.google\.com/go/compute/metadata$' \ + || true) + if [ -n "$UNEXPECTED" ]; then + echo "FAIL: unexpected gcp/azure/api transitive deps in workflow-core build graph:" + printf ' %s\n' $UNEXPECTED + echo + echo "Only cloud.google.com/go/compute/metadata is allowlisted (OAuth2 ADC helper)." + echo "Other gcp/azure SDK packages belong in a plugin, not workflow core." + echo "See docs/migrations/2026-05-15-plugin-modules-on-iac.md (Phase C)." + exit 1 + fi aws-sdk-banned: name: Verify removed AWS SDK packages are not imported (issue #653) diff --git a/.phase-c-complete b/.phase-c-complete new file mode 100644 index 00000000..e69de29b diff --git a/cmd/wfctl/infra_state_store.go b/cmd/wfctl/infra_state_store.go index f4f5c757..7f1a68b6 100644 --- a/cmd/wfctl/infra_state_store.go +++ b/cmd/wfctl/infra_state_store.go @@ -94,9 +94,8 @@ func resolveStateStore(cfgFile, envName string) (infraStateStore, error) { "install and load the plugin to use the S3 backend (wfctl direct-path commands no longer support in-tree s3)", backend) case "gcs": - return nil, fmt.Errorf("gcs state store backend not yet supported by wfctl direct-path commands; " + - "create the bucket manually and reference it in iac.state.bucket. " + - "Contribute a resolveGCSStateStore helper to unblock this") + return nil, fmt.Errorf("iac.state backend %q is now plugin-served by workflow-plugin-gcp v1.1.0; "+ + "install and load the plugin to use the GCS backend (wfctl direct-path commands no longer support in-tree gcs)", backend) case "azure": return nil, fmt.Errorf("azure state store backend not yet supported by wfctl direct-path commands; " + diff --git a/go.mod b/go.mod index 96f1c4af..e477a720 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,6 @@ go 1.26.0 require ( charm.land/bubbletea/v2 v2.0.2 charm.land/lipgloss/v2 v2.0.2 - cloud.google.com/go/storage v1.61.3 github.com/GoCodeAlone/go-plugin v1.7.0 github.com/GoCodeAlone/modular v1.13.0 github.com/GoCodeAlone/modular/modules/auth v1.15.0 @@ -66,7 +65,6 @@ require ( golang.org/x/text v0.36.0 golang.org/x/time v0.15.0 golang.org/x/tools v0.44.0 - google.golang.org/api v0.272.0 google.golang.org/grpc v1.80.0 google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.6.1 google.golang.org/protobuf v1.36.11 @@ -78,18 +76,9 @@ require ( ) require ( - cel.dev/expr v0.25.1 // indirect - cloud.google.com/go v0.123.0 // indirect - cloud.google.com/go/auth v0.19.0 // indirect - cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect cloud.google.com/go/compute/metadata v0.9.0 // indirect - cloud.google.com/go/iam v1.5.3 // indirect - cloud.google.com/go/monitoring v1.24.3 // indirect github.com/BurntSushi/toml v1.6.0 // indirect github.com/DataDog/datadog-go/v5 v5.8.3 // indirect - github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.32.0 // indirect - github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 // indirect - github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/RoaringBitmap/roaring v1.9.4 // indirect github.com/Workiva/go-datastructures v1.1.7 // indirect @@ -124,7 +113,6 @@ require ( github.com/clipperhouse/displaywidth v0.11.0 // indirect github.com/clipperhouse/uax29/v2 v2.7.0 // indirect github.com/cloudevents/sdk-go/v2 v2.16.2 // indirect - github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 // indirect github.com/containerd/errdefs v1.0.0 // indirect github.com/containerd/errdefs/pkg v0.3.0 // indirect github.com/cucumber/gherkin/go/v26 v26.2.0 // indirect @@ -140,8 +128,6 @@ require ( github.com/eapache/go-resiliency v1.7.0 // indirect github.com/eapache/queue v1.1.0 // indirect github.com/emicklei/go-restful/v3 v3.13.0 // indirect - github.com/envoyproxy/go-control-plane/envoy v1.37.0 // indirect - github.com/envoyproxy/protoc-gen-validate v1.3.3 // indirect github.com/fatih/color v1.19.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/flowchartsman/retry v1.2.0 // indirect @@ -170,9 +156,6 @@ require ( github.com/google/btree v1.1.3 // indirect github.com/google/gnostic-models v0.7.1 // indirect github.com/google/jsonschema-go v0.4.2 // indirect - github.com/google/s2a-go v0.1.9 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.3.14 // indirect - github.com/googleapis/gax-go/v2 v2.19.0 // indirect github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect @@ -239,7 +222,6 @@ require ( github.com/petermattis/goid v0.0.0-20260226131333-17d1149c6ac6 // indirect github.com/pierrec/lz4/v4 v4.1.26 // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.67.5 // indirect @@ -256,7 +238,6 @@ require ( github.com/sourcegraph/jsonrpc2 v0.2.1 // indirect github.com/spf13/cast v1.10.0 // indirect github.com/spf13/pflag v1.0.10 // indirect - github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect github.com/tidwall/btree v1.8.1 // indirect github.com/tidwall/match v1.2.0 // indirect github.com/tidwall/redcon v1.6.2 // indirect @@ -275,12 +256,8 @@ require ( go.etcd.io/bbolt v1.4.3 // indirect go.mongodb.org/mongo-driver v1.17.9 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/contrib/detectors/gcp v1.43.0 // indirect - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 // indirect - go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.43.0 // indirect go.opentelemetry.io/otel/metric v1.43.0 // indirect - go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect go.opentelemetry.io/proto/otlp v1.10.0 // indirect go.uber.org/atomic v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect @@ -289,7 +266,6 @@ require ( go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/net v0.53.0 // indirect golang.org/x/sys v0.43.0 // indirect - google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260420184626-e10c466a9529 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260420184626-e10c466a9529 // indirect gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect diff --git a/go.sum b/go.sum index 284ba675..2ccaab77 100644 --- a/go.sum +++ b/go.sum @@ -1,30 +1,10 @@ -cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4= -cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= charm.land/bubbletea/v2 v2.0.2 h1:4CRtRnuZOdFDTWSff9r8QFt/9+z6Emubz3aDMnf/dx0= charm.land/bubbletea/v2 v2.0.2/go.mod h1:3LRff2U4WIYXy7MTxfbAQ+AdfM3D8Xuvz2wbsOD9OHQ= charm.land/lipgloss/v2 v2.0.2 h1:xFolbF8JdpNkM2cEPTfXEcW1p6NRzOWTSamRfYEw8cs= charm.land/lipgloss/v2 v2.0.2/go.mod h1:KjPle2Qd3YmvP1KL5OMHiHysGcNwq6u83MUjYkFvEkM= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE= -cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU= -cloud.google.com/go/auth v0.19.0 h1:DGYwtbcsGsT1ywuxsIoWi1u/vlks0moIblQHgSDgQkQ= -cloud.google.com/go/auth v0.19.0/go.mod h1:2Aph7BT2KnaSFOM0JDPyiYgNh6PL9vGMiP8CUIXZ+IY= -cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= -cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= -cloud.google.com/go/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc= -cloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU= -cloud.google.com/go/logging v1.13.2 h1:qqlHCBvieJT9Cdq4QqYx1KPadCQ2noD4FK02eNqHAjA= -cloud.google.com/go/logging v1.13.2/go.mod h1:zaybliM3yun1J8mU2dVQ1/qDzjbOqEijZCn6hSBtKak= -cloud.google.com/go/longrunning v0.8.0 h1:LiKK77J3bx5gDLi4SMViHixjD2ohlkwBi+mKA7EhfW8= -cloud.google.com/go/longrunning v0.8.0/go.mod h1:UmErU2Onzi+fKDg2gR7dusz11Pe26aknR4kHmJJqIfk= -cloud.google.com/go/monitoring v1.24.3 h1:dde+gMNc0UhPZD1Azu6at2e79bfdztVDS5lvhOdsgaE= -cloud.google.com/go/monitoring v1.24.3/go.mod h1:nYP6W0tm3N9H/bOw8am7t62YTzZY+zUeQ+Bi6+2eonI= -cloud.google.com/go/storage v1.61.3 h1:VS//ZfBuPGDvakfD9xyPW1RGF1Vy3BWUoVZXgW1KMOg= -cloud.google.com/go/storage v1.61.3/go.mod h1:JtqK8BBB7TWv0HVGHubtUdzYYrakOQIsMLffZ2Z/HWk= -cloud.google.com/go/trace v1.11.7 h1:kDNDX8JkaAG3R2nq1lIdkb7FCSi1rCmsEtKVsty7p+U= -cloud.google.com/go/trace v1.11.7/go.mod h1:TNn9d5V3fQVf6s4SCveVMIBS2LJUqo73GACmq/Tky0s= dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= @@ -50,14 +30,6 @@ github.com/GoCodeAlone/modular/modules/reverseproxy/v2 v2.8.0 h1:cvdLHbM/vzvygQT github.com/GoCodeAlone/modular/modules/reverseproxy/v2 v2.8.0/go.mod h1:/9ipMG4qM2CHQ14BfXKdVlYRJelef6M8MFI5TbZv67M= github.com/GoCodeAlone/yaegi v0.17.2 h1:WK6Y6e0t1a6U7r+S2dN3CGWW1PizYD3zO0zneToZPxM= github.com/GoCodeAlone/yaegi v0.17.2/go.mod h1:z5Pr6Wse6QJcQvpgxTxzMAevFarH0N37TG88Y9dprx0= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.32.0 h1:rIkQfkCOVKc1OiRCNcSDD8ml5RJlZbH/Xsq7lbpynwc= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.32.0/go.mod h1:RD2SsorTmYhF6HkTmDw7KmPYQk8OBYwTkuasChwv7R4= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 h1:UnDZ/zFfG1JhH/DqxIZYU/1CUAlTUScoXD/LcM2Ykk8= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0/go.mod h1:IA1C1U7jO/ENqm/vhi7V9YYpBsp+IMyqNrEN94N7tVc= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.55.0 h1:7t/qx5Ost0s0wbA/VDrByOooURhp+ikYwv20i9Y07TQ= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.55.0/go.mod h1:vB2GH9GAYYJTO3mEn8oYwzEdhlayZIdQz6zdzgUIRvA= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 h1:0s6TxfCu2KHkkZPnBfsQ2y5qia0jl3MMrmBhu3nCOYk= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0/go.mod h1:Mf6O40IAyB9zR/1J8nGDDPirZQQPbYJni8Yisy7NTMc= github.com/IBM/sarama v1.47.0 h1:GcQFEd12+KzfPYeLgN69Fh7vLCtYRhVIx0rO4TZO318= github.com/IBM/sarama v1.47.0/go.mod h1:7gLLIU97nznOmA6TX++Qds+DRxH89P2XICY2KAQUzAY= github.com/Microsoft/go-winio v0.5.0/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= @@ -172,8 +144,6 @@ github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJ github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= github.com/cloudevents/sdk-go/v2 v2.16.2 h1:ZYDFrYke4FD+jM8TZTJJO6JhKHzOQl2oqpFK1D+NnQM= github.com/cloudevents/sdk-go/v2 v2.16.2/go.mod h1:laOcGImm4nVJEU+PHnUrKL56CKmRL65RlQF0kRmW/kg= -github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 h1:aBangftG7EVZoUb69Os8IaYg++6uMOdKK83QtkkvJik= -github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2/go.mod h1:qwXFYgsP6T7XnJtbKlf1HP8AjxZZyzxMmc+Lq5GjlU4= github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= @@ -228,14 +198,6 @@ github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= -github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA= -github.com/envoyproxy/go-control-plane v0.14.0/go.mod h1:NcS5X47pLl/hfqxU70yPwL9ZMkUlwlKxtAohpi2wBEU= -github.com/envoyproxy/go-control-plane/envoy v1.37.0 h1:u3riX6BoYRfF4Dr7dwSOroNfdSbEPe9Yyl09/B6wBrQ= -github.com/envoyproxy/go-control-plane/envoy v1.37.0/go.mod h1:DReE9MMrmecPy+YvQOAOHNYMALuowAnbjjEMkkWOi6A= -github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI= -github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4= -github.com/envoyproxy/protoc-gen-validate v1.3.3 h1:MVQghNeW+LZcmXe7SY1V36Z+WFMDjpqGAGacLe2T0ds= -github.com/envoyproxy/protoc-gen-validate v1.3.3/go.mod h1:TsndJ/ngyIdQRhMcVVGDDHINPLWB7C82oDArY51KfB0= github.com/expr-lang/expr v1.17.8 h1:W1loDTT+0PQf5YteHSTpju2qfUfNoBt4yw9+wOEU9VM= github.com/expr-lang/expr v1.17.8/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= @@ -355,18 +317,10 @@ github.com/google/go-tpm v0.9.8/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8= github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= -github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc= -github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0= github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8= github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= -github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= -github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= 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/googleapis/enterprise-certificate-proxy v0.3.14 h1:yh8ncqsbUY4shRD5dA6RlzjJaT4hi3kII+zYw8wmLb8= -github.com/googleapis/enterprise-certificate-proxy v0.3.14/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg= -github.com/googleapis/gax-go/v2 v2.19.0 h1:fYQaUOiGwll0cGj7jmHT/0nPlcrZDFPrZRhTsoCr8hE= -github.com/googleapis/gax-go/v2 v2.19.0/go.mod h1:w2ROXVdfGEVFXzmlciUU4EdjHgWvB5h2n6x/8XSTTJA= github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= @@ -617,8 +571,6 @@ github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= -github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -692,8 +644,6 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo= -github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= @@ -792,10 +742,6 @@ go.mongodb.org/mongo-driver v1.17.9 h1:IexDdCuuNJ3BHrELgBlyaH9p60JXAvdzWR128q+U5 go.mongodb.org/mongo-driver v1.17.9/go.mod h1:LlOhpH5NUEfhxcAwG0UEkMqwYcc4JU18gtCdGudk/tQ= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/contrib/detectors/gcp v1.43.0 h1:62yY3dT7/ShwOxzA0RsKRgshBmfElKI4d/Myu2OxDFU= -go.opentelemetry.io/contrib/detectors/gcp v1.43.0/go.mod h1:RyaZMFY7yi1kAs45S6mbFGz8O8rqB0dTY14uzvG4LCs= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0 h1:0Qx7VGBacMm9ZENQ7TnNObTYI4ShC+lHI16seduaxZo= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0/go.mod h1:Sje3i3MjSPKTSPvVWCaL8ugBzJwik3u4smCjUeuupqg= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 h1:CqXxU8VOmDefoh0+ztfGaymYbhdB/tT3zs79QaZTNGY= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0/go.mod h1:BuhAPThV8PBHBvg8ZzZ/Ok3idOdhWIodywz2xEcRbJo= go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= @@ -804,8 +750,6 @@ go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bT go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 h1:3iZJKlCZufyRzPzlQhUIWVmfltrXuGyfjREgGP3UUjc= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0/go.mod h1:/G+nUPfhq2e+qiXMGxMwumDrP5jtzU+mWN7/sjT2rak= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.43.0 h1:TC+BewnDpeiAmcscXbGMfxkO+mwYUwE/VySwvw88PfA= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.43.0/go.mod h1:J/ZyF4vfPwsSr9xJSPyQ4LqtcTPULFR64KwTikGLe+A= go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= @@ -930,11 +874,7 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= -google.golang.org/api v0.272.0 h1:eLUQZGnAS3OHn31URRf9sAmRk3w2JjMx37d2k8AjJmA= -google.golang.org/api v0.272.0/go.mod h1:wKjowi5LNJc5qarNvDCvNQBn3rVK8nSy6jg2SwRwzIA= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 h1:XzmzkmB14QhVhgnawEVsOn6OFsnpyxNPRY9QV01dNB0= -google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:L43LFes82YgSonw6iTXTxXUX1OlULt4AQtkik4ULL/I= google.golang.org/genproto/googleapis/api v0.0.0-20260420184626-e10c466a9529 h1:zUWMZsvo/IJcD1t6MNCPO/azZTwz0TvwCBqr5aifoVY= google.golang.org/genproto/googleapis/api v0.0.0-20260420184626-e10c466a9529/go.mod h1:a5OGAgyRr4lqco7AG9hQM9Fwh0N2ZV4grR0eXFEsXQg= google.golang.org/genproto/googleapis/rpc v0.0.0-20260420184626-e10c466a9529 h1:XF8+t6QQiS0o9ArVan/HW8Q7cycNPGsJf6GA2nXxYAg= diff --git a/module/auth_oauth2.go b/module/auth_oauth2.go index f6c572b9..d7fd881a 100644 --- a/module/auth_oauth2.go +++ b/module/auth_oauth2.go @@ -15,9 +15,22 @@ import ( "github.com/GoCodeAlone/modular" "golang.org/x/oauth2" "golang.org/x/oauth2/github" - "golang.org/x/oauth2/google" ) +// googleOAuth2Endpoint hard-codes Google's static IdP OAuth2 endpoints (RFC 6749). +// We avoid importing `golang.org/x/oauth2/google` because its package init +// transitively pulls `cloud.google.com/go/compute/metadata` (an ADC helper +// for GCE/GKE workload identity) — and Phase C's permanent asymmetric CI +// gate (decisions/0034 + plan 2026-05-15-plugin-modules-on-iac.md Task 18) +// asserts zero `cloud.google.com/go/*` packages in core's build graph. +// These URLs are static published Google OAuth2 endpoints: +// +// https://developers.google.com/identity/protocols/oauth2/web-server +var googleOAuth2Endpoint = oauth2.Endpoint{ + AuthURL: "https://accounts.google.com/o/oauth2/auth", + TokenURL: "https://oauth2.googleapis.com/token", +} + // OAuth2ProviderConfig holds configuration for a single OAuth2 provider. type OAuth2ProviderConfig struct { Name string `json:"name" yaml:"name"` @@ -83,7 +96,7 @@ func buildProviderEntry(pc *OAuth2ProviderConfig) *oauth2ProviderEntry { switch pc.Name { case "google": - endpoint = google.Endpoint + endpoint = googleOAuth2Endpoint userInfoURL = "https://www.googleapis.com/oauth2/v2/userinfo" if len(pc.Scopes) == 0 { pc.Scopes = []string{"openid", "email", "profile"} diff --git a/scripts/audit-cloud-symbols.sh b/scripts/audit-cloud-symbols.sh index b45d6a6b..6c03022a 100755 --- a/scripts/audit-cloud-symbols.sh +++ b/scripts/audit-cloud-symbols.sh @@ -84,6 +84,78 @@ if [[ -f "$CREDS" ]]; then fi fi +echo +echo "== Invariant: module/ zero real imports of cloud.google.com/go / google.golang.org/api / Azure/azure-sdk-for-go ==" +# Phase C (plan 2026-05-15-plugin-modules-on-iac.md Task 18) asserts module/ +# stays free of gcp/azure SDK imports. Real-import (not comment) only. Only +# enforced once .phase-c-complete is armed. +MOD_FORBIDDEN=() +while IFS= read -r f; do + [[ -z "$f" ]] && continue + for sdk in 'cloud.google.com/go' 'google.golang.org/api' 'github.com/Azure/azure-sdk-for-go'; do + if real_import "$f" "$sdk"; then + MOD_FORBIDDEN+=("$f -> $sdk") + fi + done +done < <(grep -rl 'cloud\.google\.com/go\|google\.golang\.org/api\|github\.com/Azure/azure-sdk-for-go' module/ --include='*.go' 2>/dev/null | grep -v '_test\.go' | sort) +if [[ ${#MOD_FORBIDDEN[@]} -eq 0 ]]; then + echo " module/: 0 real imports — clean" +else + printf ' module/ REAL IMPORT: %s\n' "${MOD_FORBIDDEN[@]}" + if [[ $CHECK -eq 1 && -f .phase-c-complete ]]; then + echo " INVARIANT VIOLATED: module/ has gcp/azure SDK imports post-Phase-C" + FAIL=1 + fi +fi + +echo +echo "== Invariant: build graph has no unexpected gcp/azure/api transitive deps ==" +# Phase C permanent asymmetric gate (plan Task 18). Reads `go list -deps ./...` +# and asserts: +# - github.com/Azure/azure-sdk-for-go : zero (catch-all) +# - google.golang.org/api : zero (catch-all) +# - cloud.google.com/go : only `compute/metadata` allowed +# (OAuth2 ADC helper pulled by golang.org/x/oauth2/google in +# provider/gcp's service-account auth path — see decisions/0034 + +# 2026-05-15 migration doc; legitimate transitive, not a GCP SDK +# client). +# Asymmetric vs aws-sdk-go-v2 (Phase B): aws-sdk-go-v2 STAYS for out-of-scope +# provider/aws/ + plugin/rbac/aws.go + iam/aws.go + artifact/s3.go. +if [[ $CHECK -eq 1 && -f .phase-c-complete ]]; then + DEPS=$(GOWORK=off go list -deps ./... 2>/dev/null || true) + AZURE_UNEXPECTED=$(echo "$DEPS" | grep -F 'github.com/Azure/azure-sdk-for-go' || true) + API_UNEXPECTED=$(echo "$DEPS" | grep '^google\.golang\.org/api' || true) + GCP_UNEXPECTED=$(echo "$DEPS" \ + | grep '^cloud\.google\.com/go' \ + | grep -v '^cloud\.google\.com/go/compute/metadata$' \ + || true) + if [[ -n "$AZURE_UNEXPECTED" ]]; then + echo " FAIL: azure-sdk-for-go transitive deps in core build graph:" + printf ' %s\n' $AZURE_UNEXPECTED + FAIL=1 + fi + if [[ -n "$API_UNEXPECTED" ]]; then + echo " FAIL: google.golang.org/api transitive deps in core build graph:" + printf ' %s\n' $API_UNEXPECTED + FAIL=1 + fi + if [[ -n "$GCP_UNEXPECTED" ]]; then + echo " FAIL: unexpected cloud.google.com/go transitive deps in core build graph:" + printf ' %s\n' $GCP_UNEXPECTED + echo + echo " Only cloud.google.com/go/compute/metadata is allowlisted (OAuth2 ADC helper" + echo " pulled by provider/gcp's service-account auth). If you need a new GCP SDK" + echo " package, factor it into a plugin instead — workflow core is gcp-SDK-free" + echo " per Phase C (plan 2026-05-15-plugin-modules-on-iac.md Task 18)." + FAIL=1 + fi + if [[ -z "$AZURE_UNEXPECTED$API_UNEXPECTED$GCP_UNEXPECTED" ]]; then + echo " build graph: clean (compute/metadata allowlisted)" + fi +else + echo " build-graph gate skipped (.phase-c-complete not armed or --check absent)" +fi + echo echo "== Advisory: platform_kubernetes_kind.go backend split readiness ==" KIND=module/platform_kubernetes_kind.go From 470de84802c641b2b7df5a0d78a560c3a1ae1878 Mon Sep 17 00:00:00 2001 From: Jon Langevin Date: Fri, 15 May 2026 19:05:27 -0400 Subject: [PATCH 3/8] =?UTF-8?q?docs:=20Phase=20C=20migration=20doc=20+=20c?= =?UTF-8?q?ross-phase=20verification=20=E2=80=94=20cloud-SDK=20extraction?= =?UTF-8?q?=20complete?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DOCUMENTATION.md | 2 +- .../2026-05-15-plugin-modules-on-iac.md | 182 +++++++++++++++--- 2 files changed, 159 insertions(+), 25 deletions(-) diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index 9275749c..059feeaa 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -502,7 +502,7 @@ steps. See [v0.52.0 migration guide](docs/migrations/v0.52.0-godo-removal.md). Use the generic `infra.*` module types with `provider: aws` and `step.iac_*` pipeline steps. See [v0.53.0 migration guide](docs/migrations/v0.53.0-aws-iac-removal.md). | `iac.provider` | Cloud provider configuration (aws, gcp, azure, digitalocean) for IaC operations | platform | -| `iac.state` | IaC state persistence (memory, filesystem, spaces, gcs, azure_blob, postgres) | platform | +| `iac.state` | IaC state persistence (memory + filesystem + postgres in-core; spaces / s3 / gcs / azure_blob via plugins) | platform | | `infra.vpc` | Virtual Private Cloud and subnet management | platform | | `infra.database` | Managed database instance provisioning and configuration | platform | | `infra.cache` | In-memory cache cluster provisioning (Redis, Memcached) | platform | diff --git a/docs/migrations/2026-05-15-plugin-modules-on-iac.md b/docs/migrations/2026-05-15-plugin-modules-on-iac.md index b722e745..8dfe86d5 100644 --- a/docs/migrations/2026-05-15-plugin-modules-on-iac.md +++ b/docs/migrations/2026-05-15-plugin-modules-on-iac.md @@ -1,20 +1,25 @@ -# 2026-05-15 — Plugin-modules-on-IaC: Phase B clean break +# 2026-05-15 — Plugin-modules-on-IaC: Phase B + Phase C clean break -This migration covers **Phase B** of the -[plugin-modules-on-IaC plan](../plans/2026-05-15-plugin-modules-on-iac.md): -workflow-core sheds the remaining in-core AWS/DO storage + state surfaces and -the SDK-bearing AWS credential resolvers. Each surface is now plugin-native. +This migration covers **Phase B** (AWS / DigitalOcean) and **Phase C** (GCP) +of the +[plugin-modules-on-IaC plan](../plans/2026-05-15-plugin-modules-on-iac.md). +Workflow-core sheds the remaining in-core cloud-SDK-bearing surfaces: +S3/Spaces/GCS IaC state stores, `storage.s3` + `storage.gcs` modules, +`step.s3_upload`, the in-core `gkeBackend`, and the SDK-bearing AWS +profile/role_arn credential resolvers. Each surface is now plugin-native. -The companion **Phase C** migration (GCP) follows in a separate PR; this doc is -amended in-place when that ships. +Phase B shipped in PR `feat/phase-b-core-deletion`; Phase C in +`feat/phase-c-core-deletion`. Engine + plugin versioning is covered below. ## Engine floor -Phase B requires **workflow `>= v0.53.0`** in any deployment that uses the +Both phases require **workflow `>= v0.53.0`** in any deployment that uses the affected backends. The `>= v0.53.0` engine has the typed `IaCStateBackend` gRPC contract (Phase A, decisions/0036), the `Configure` RPC that delivers the `iac.state` module YAML to the plugin, and the plugin-backend registry that -`IaCModule.Init` consults in its `default:`-arm. +`IaCModule.Init` consults in its `default:`-arm. Phase C additionally relies on +the `grpcKubernetesBackend` adapter + plugin-backend registry shipped in PR +`#681` (ADR 0037) for the `platform.kubernetes type: gke` resolution path. ## What changed @@ -25,9 +30,17 @@ gRPC contract (Phase A, decisions/0036), the `Configure` RPC that delivers the | `storage.s3` module | in-core `module.S3Storage` (registered by `plugins/storage`) | plugin-native in `workflow-plugin-aws >= v1.1.0` | | `step.s3_upload` pipeline step | in-core `module.S3UploadStep` (registered by `plugins/pipelinesteps`) | plugin-native in `workflow-plugin-aws >= v1.1.0` | | `cloud.account` `provider: aws` + `credentials.type: profile` or `role_arn` | SDK-bearing resolver loaded the profile / called `sts:AssumeRole` in-core | core records a `credential_source` marker only; the aws plugin performs SDK resolution via `awscreds.BuildAWSConfig` (decisions/0036 + 0038) | +| `iac.state` `backend: gcs` | in-core `module.GCSIaCStateStore` (via `cloud.google.com/go/storage`) | plugin-served by [`workflow-plugin-gcp`](https://github.com/GoCodeAlone/workflow-plugin-gcp) `>= v1.1.0` | +| `storage.gcs` module | in-core `module.GCSStorage` (registered by `plugins/storage`) | plugin-native in `workflow-plugin-gcp >= v1.1.0` | +| `platform.kubernetes` `type: gke` | in-core `gkeBackend` (via `google.golang.org/api/container/v1`) | plugin-served by `workflow-plugin-gcp >= v1.1.0`; routed through the `grpcKubernetesBackend` adapter (ADR 0037) | -The YAML field names and `backend:` values are **unchanged**. The break is -strictly about *which binary* serves them. +The YAML field names and `backend:` / `type:` / `provider:` values are +**unchanged**. The break is strictly about *which binary* serves them. + +`platform.kubernetes type: kind`, `k3s`, `eks`, and `aks` stay in core +(kind/k3s are in-memory test backends; eks is an actionable-error stub +pointing at `workflow-plugin-aws`; aks uses Azure REST + OAuth2 with no +Azure-SDK import — see Phase A's `cloud_account_azure.go` rewrite). ## Why @@ -51,10 +64,11 @@ Without the plugin, `IaCModule.Init` fails fast: ``` iac.state "": backend "spaces" is not built into workflow core -(in-core backends: 'memory', 'filesystem', 'gcs', 'postgres'). +(in-core backends: 'memory', 'filesystem', 'postgres'). If "spaces" is a plugin-provided backend (e.g. 'azure_blob' via workflow-plugin-azure, 'spaces' via workflow-plugin-digitalocean, -'s3' via workflow-plugin-aws), install and load that plugin +'s3' via workflow-plugin-aws, 'gcs' via workflow-plugin-gcp), +install and load that plugin ``` ### `iac.state backend: s3` @@ -104,13 +118,84 @@ warning is what tells operators which side to upgrade. `credentials.type: static` and `credentials.type: env` are unaffected — those paths have always been SDK-free and resolve in-core. +### `iac.state backend: gcs` + +Load `workflow-plugin-gcp >= v1.1.0`. The YAML `backend: gcs` value and all +config keys (`bucket`, `prefix`, plus any GCP credential config) keep their +semantics. Application Default Credentials and service-account JSON resolution +still work — they just happen in the plugin process now. + +Without the plugin, `IaCModule.Init` returns the same actionable error as the +spaces/s3 cases (in-core backends list now `'memory', 'filesystem', +'postgres'`; plugin examples list includes `'gcs' via workflow-plugin-gcp`). +The wfctl direct-path commands (`wfctl infra ...`) return the same shape: + +``` +iac.state backend "gcs" is now plugin-served by workflow-plugin-gcp v1.1.0; +install and load the plugin to use the GCS backend (wfctl direct-path +commands no longer support in-tree gcs) +``` + +### `storage.gcs` module + +Moves into `workflow-plugin-gcp >= v1.1.0`. Same shape as `storage.s3`: +credentials inline or referenced via `credentials_ref:` pointing at a +`gcp.credentials` module loaded by the plugin. With no plugin loaded the +module type is unknown at engine boot — load the plugin in the deployment's +plugin manifest. + +### `platform.kubernetes type: gke` + +The in-core `gkeBackend` (which spoke directly to +`google.golang.org/api/container/v1`) is removed. The `type: gke` dispatch +now flows through the `kubernetesBackendClientRegistry` populated at +plugin-load time by `workflow-plugin-gcp >= v1.1.0`, routed via the +`grpcKubernetesBackend` adapter shipped in PR `#681` per +[ADR 0037](../../decisions/0037-gke-cross-process-contract.md). + +The YAML `type: gke` value is unchanged. All cluster-level config keys +(`project`, `location`/`zone`, `version`, `nodeGroups`, …) keep their +semantics; the plugin's `GKEDriver.Read` conforms its output to the same +status/endpoint keys the in-core `gkeBackend` produced. + +Without the plugin, `PlatformKubernetes.Init` fails fast and the error +message identifies `workflow-plugin-gcp` as the missing plugin (same shape +as the iac.state error above). + +## OAuth2 ADC allowlist disclosure + +Workflow core's `provider/gcp/` package retains +`golang.org/x/oauth2/google` for its service-account credential resolution +(`google.Credentials`, `FindDefaultCredentials`, +`CredentialsFromJSONWithTypeAndParams`). That import transitively pulls +**`cloud.google.com/go/compute/metadata`** — the OAuth2 Application Default +Credentials helper used to fetch tokens from the GCE/GKE metadata server. + +The Phase C asymmetric audit gate (in +[`scripts/audit-cloud-symbols.sh`](../../scripts/audit-cloud-symbols.sh)) and +the mirroring `.github/workflows/ci.yml` `cloud-sdk-audit` job **allowlist +this single transitive path** and **fail CI on any other** `cloud.google.com/go/*` +dep. Any new GCP SDK package (e.g. `cloud.google.com/go/storage`, +`google.golang.org/api/*`) belongs in `workflow-plugin-gcp`, not core. + +This is the GCP-side mirror of Phase B's `aws-sdk-go-v2`-retention paragraph: +`provider/aws/` legitimately uses the AWS SDK for its deploy pipeline, +`provider/gcp/` legitimately uses OAuth2 ADC for service-account auth, and +both arrangements are intentional — the audit gate just guards against +scope creep beyond those known seams. + ## Rollback -Phase B's clean-breaks roll back only as a **matched pair** with the plugin -releases that serve them — reverting PR `feat/phase-b-core-deletion` -restores the in-core paths, but the plugin v1.1.0 tags are immutable. A -patch-level defect in either plugin port is resolved with a `v1.1.1` -release, not by re-introducing the in-core implementation. +Both Phase B and Phase C clean-breaks roll back only as a **matched pair** +with the plugin releases that serve them — reverting PR +`feat/phase-b-core-deletion` or `feat/phase-c-core-deletion` restores the +in-core paths, but the corresponding plugin v1.1.0 tags are immutable. A +patch-level defect in any plugin port is resolved with a `v1.1.1` release, +not by re-introducing the in-core implementation. + +A running deployment that has already cut over to plugin-served `gcs` / +`gke` must coordinate engine + plugin versions on rollback — pinning both +sides to a pre-Phase-C state in the deploy manifest. The `cloud_account_aws.go` deletion (164 lines of dead code that #653 had already orphaned) is not part of the matched-pair rollback — it had zero @@ -118,18 +203,67 @@ non-test consumers. ## Verification -Once Phase B is merged: +### Phase B (post-merge) -- `go mod tidy` against the merged tree should make no net change to AWS SDK +- `go mod tidy` against the merged tree makes no net change to AWS SDK service modules — `aws-sdk-go-v2` stays in `go.mod` because `provider/aws/`, `plugin/rbac/aws.go`, `iam/aws.go`, and `artifact/s3.go` still import it. - The `.phase-b-complete` marker arms `scripts/audit-cloud-symbols.sh --check`'s zero-`aws-sdk-go-v2` invariant on - `module/cloud_account_aws_creds.go`. Running the audit script post-merge - must report `audit-cloud-symbols: OK`. + `module/cloud_account_aws_creds.go`. + +### Phase C (post-merge) + +- `go mod tidy` drops `cloud.google.com/go/storage`, `google.golang.org/api`, + `cloud.google.com/go/auth*`, `cloud.google.com/go/monitoring`, + `cloud.google.com/go/iam`, and `GoogleCloudPlatform/opentelemetry-operations-go/*` + (~24 lines). `cloud.google.com/go/compute/metadata` remains as the only + `cloud.google.com/go/*` entry (allowlisted, see disclosure above). +- The `.phase-c-complete` marker arms two additional `--check` invariants: + - `module/` has **zero real imports** of `cloud.google.com/go`, + `google.golang.org/api`, or `github.com/Azure/azure-sdk-for-go`. + - The whole-repo build graph (`go list -deps ./...`) has zero + `Azure/azure-sdk-for-go`, zero `google.golang.org/api`, and zero + `cloud.google.com/go/*` **except** `compute/metadata`. + +### Cross-phase invariant re-check + +Run from a checkout of the merged tree: + +```bash +bash scripts/audit-cloud-symbols.sh --check # → audit-cloud-symbols: OK +GOWORK=off go list -deps ./... \ + | grep '^cloud\.google\.com/go' \ + | grep -v '^cloud\.google\.com/go/compute/metadata$' # → empty +GOWORK=off go list -deps ./... \ + | grep -E '^(google\.golang\.org/api|github\.com/Azure/azure-sdk-for-go)' # → empty +GOWORK=off go build ./... && GOWORK=off go test ./... # → all green +``` + +Phase A's invariants (typed `IaCStateBackend` contract, `Configure` RPC) are +re-validated by the same audit run since `module/` is the scope they protect. + +## Phase recap + +| Phase | What | PRs | ADRs | +|---|---|---|---| +| **A** | Typed `IaCStateBackend` gRPC contract; `Configure` RPC; plugin-backend registry; `azure_blob` → workflow-plugin-azure v1.1.0 | plan-1 PRs 1–3; locked B/C/D plan PRs 1–2 | 0035, 0036 | +| **B** | In-core `iac_state_spaces`, `s3_storage`, `pipeline_step_s3_upload` deletion; SDK-free AWS profile/role_arn resolvers with `credential_source` markers; `cloud_account_aws.go` (dead) deletion; `aws-sdk-go-v2` *retained* in `go.mod` for `provider/aws/` et al. | `feat/phase-b-core-deletion` (PR `#687`) | 0034, 0038 | +| **C** | In-core `iac_state_gcs`, `storage_gcs`, `platform_kubernetes_gke` deletion; GCP SDKs dropped from `go.mod` (one allowlisted OAuth2 ADC transitive); permanent asymmetric audit + CI gate; wfctl gcs/s3/spaces actionable errors | `feat/phase-c-core-deletion` | 0037, 0039 (TBD — captures the gate-allowlist trade-off; follow-up) | + +Plan-1 and plan-2 manifests + per-task spec records live under +`docs/plans/` (`2026-05-14-cloud-sdk-extraction-bcd.md`, +`2026-05-15-plugin-modules-on-iac.md`). + +**Final invariant statement:** workflow-core now imports zero cloud-provider +SDK clients in `module/`; provider-specific surfaces (`provider/aws/`, +`provider/gcp/`'s OAuth2-only path) retain only what's needed for the +out-of-scope deploy-pipeline / credential-resolution work that #653 + +decisions/0034 explicitly carve out. Every other cloud-provider integration +crosses the engine ↔ plugin gRPC boundary. ## Related design + plans -- Plan: [docs/plans/2026-05-15-plugin-modules-on-iac.md](../plans/2026-05-15-plugin-modules-on-iac.md) -- Decisions: 0034 (autonomous plugin releases), 0035 (assumed-seam grep), 0036 (Configure RPC), 0038 (credential_source marker) +- Plans: [2026-05-14 cloud-SDK extraction (B/C/D)](../plans/2026-05-14-cloud-sdk-extraction-bcd.md), [2026-05-15 plugin-modules-on-iac](../plans/2026-05-15-plugin-modules-on-iac.md) +- Decisions: 0034 (autonomous plugin releases), 0035 (assumed-seam grep / real-import audit), 0036 (Configure RPC), 0037 (GKE cross-process contract — ResourceDriver fold), 0038 (credential_source marker), 0039 (TBD — asymmetric audit gate + compute/metadata allowlist trade-off; follow-up filing) - Predecessors: [v0.52.0 godo removal](v0.52.0-godo-removal.md), [v0.53.0 AWS IaC removal](v0.53.0-aws-iac-removal.md), [2026-05-14 azure plugin extraction](2026-05-14-cloud-sdk-extraction.md) From 00d9f72a314011ad74d69bf6b1a3788880b5dfbb Mon Sep 17 00:00:00 2001 From: Jon Langevin Date: Fri, 15 May 2026 19:30:05 -0400 Subject: [PATCH 4/8] =?UTF-8?q?fix:=20address=20Copilot=20PR-5=20findings?= =?UTF-8?q?=20=E2=80=94=20tidy=20example/go.mod=20+=20capture=20go=20list?= =?UTF-8?q?=20exit=20code=20in=20audit=20script=20+=20CI=20gate=20+=20corr?= =?UTF-8?q?ect=20stale=20auth=5Foauth2=20comment?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 12 ++++++- example/go.mod | 24 ------------- example/go.sum | 62 ---------------------------------- module/auth_oauth2.go | 11 ++++-- scripts/audit-cloud-symbols.sh | 15 +++++++- 5 files changed, 33 insertions(+), 91 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c025afc0..1b65493d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -420,7 +420,17 @@ jobs: env: GOWORK: "off" run: | - UNEXPECTED=$(go list -deps ./... \ + set +e + DEPS=$(go list -deps ./... 2>&1) + LIST_EXIT=$? + set -e + if [ $LIST_EXIT -ne 0 ]; then + echo "FAIL: \`go list -deps ./...\` exited $LIST_EXIT (gate cannot enforce):" + echo "$DEPS" | head -10 | sed 's/^/ /' + exit 1 + fi + # `|| true` on grep is fine: exit 1 from grep means "no matches" = success. + UNEXPECTED=$(echo "$DEPS" \ | grep -E '^(cloud\.google\.com/go|google\.golang\.org/api|github\.com/Azure/azure-sdk-for-go)' \ | grep -v '^cloud\.google\.com/go/compute/metadata$' \ || true) diff --git a/example/go.mod b/example/go.mod index e60336cb..e837e38b 100644 --- a/example/go.mod +++ b/example/go.mod @@ -10,14 +10,6 @@ require ( ) require ( - cel.dev/expr v0.25.1 // indirect - cloud.google.com/go v0.123.0 // indirect - cloud.google.com/go/auth v0.20.0 // indirect - cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect - cloud.google.com/go/compute/metadata v0.9.0 // indirect - cloud.google.com/go/iam v1.8.0 // indirect - cloud.google.com/go/monitoring v1.26.0 // indirect - cloud.google.com/go/storage v1.62.1 // indirect github.com/BurntSushi/toml v1.6.0 // indirect github.com/DataDog/datadog-go/v5 v5.8.3 // indirect github.com/GoCodeAlone/modular/modules/auth v1.15.0 // indirect @@ -26,9 +18,6 @@ require ( github.com/GoCodeAlone/modular/modules/jsonschema v1.15.0 // indirect github.com/GoCodeAlone/modular/modules/reverseproxy/v2 v2.8.0 // indirect github.com/GoCodeAlone/yaegi v0.17.2 // indirect - github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.32.0 // indirect - github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.56.0 // indirect - github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.56.0 // indirect github.com/IBM/sarama v1.47.0 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/RoaringBitmap/roaring v1.9.4 // indirect @@ -60,7 +49,6 @@ require ( github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cloudevents/sdk-go/v2 v2.16.2 // indirect - github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 // indirect github.com/containerd/errdefs v1.0.0 // indirect github.com/containerd/errdefs/pkg v0.3.0 // indirect github.com/danieljoos/wincred v1.2.3 // indirect @@ -75,8 +63,6 @@ require ( github.com/eapache/go-resiliency v1.7.0 // indirect github.com/eapache/queue v1.1.0 // indirect github.com/emicklei/go-restful/v3 v3.13.0 // indirect - github.com/envoyproxy/go-control-plane/envoy v1.37.0 // indirect - github.com/envoyproxy/protoc-gen-validate v1.3.3 // indirect github.com/expr-lang/expr v1.17.8 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/flowchartsman/retry v1.2.0 // indirect @@ -105,10 +91,7 @@ require ( github.com/golobby/cast v1.3.3 // indirect github.com/google/btree v1.1.3 // indirect github.com/google/gnostic-models v0.7.1 // indirect - github.com/google/s2a-go v0.1.9 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.3.14 // indirect - github.com/googleapis/gax-go/v2 v2.21.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect @@ -158,7 +141,6 @@ require ( github.com/opencontainers/image-spec v1.1.1 // indirect github.com/pierrec/lz4/v4 v4.1.26 // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/prometheus/client_golang v1.23.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.67.5 // indirect @@ -172,7 +154,6 @@ require ( github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 // indirect github.com/spf13/pflag v1.0.10 // indirect - github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect github.com/tidwall/btree v1.8.1 // indirect github.com/tidwall/match v1.2.0 // indirect github.com/tidwall/redcon v1.6.2 // indirect @@ -189,15 +170,12 @@ require ( go.etcd.io/bbolt v1.4.3 // indirect go.mongodb.org/mongo-driver v1.17.9 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/contrib/detectors/gcp v1.43.0 // indirect - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 // indirect go.opentelemetry.io/otel v1.43.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 // indirect go.opentelemetry.io/otel/metric v1.43.0 // indirect go.opentelemetry.io/otel/sdk v1.43.0 // indirect - go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect go.opentelemetry.io/otel/trace v1.43.0 // indirect go.opentelemetry.io/proto/otlp v1.10.0 // indirect go.uber.org/atomic v1.11.0 // indirect @@ -215,8 +193,6 @@ require ( golang.org/x/text v0.36.0 // indirect golang.org/x/time v0.15.0 // indirect golang.org/x/tools v0.44.0 // indirect - google.golang.org/api v0.275.0 // indirect - google.golang.org/genproto v0.0.0-20260406210006-6f92a3bedf2d // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260420184626-e10c466a9529 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260420184626-e10c466a9529 // indirect google.golang.org/grpc v1.80.0 // indirect diff --git a/example/go.sum b/example/go.sum index 95255ba6..32e11b53 100644 --- a/example/go.sum +++ b/example/go.sum @@ -1,26 +1,4 @@ -cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4= -cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE= -cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU= -cloud.google.com/go/auth v0.20.0 h1:kXTssoVb4azsVDoUiF8KvxAqrsQcQtB53DcSgta74CA= -cloud.google.com/go/auth v0.20.0/go.mod h1:942/yi/itH1SsmpyrbnTMDgGfdy2BUqIKyd0cyYLc5Q= -cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= -cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= -cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= -cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= -cloud.google.com/go/iam v1.8.0 h1:e5QOdN1zQ3MTWYtXIf2buX+jxqvo2sKqBCOLrteLd1M= -cloud.google.com/go/iam v1.8.0/go.mod h1:IkWUaEeLK91WQqTKa/fi5xdHJbL49kv2j/vlAZQSJ+k= -cloud.google.com/go/logging v1.14.0 h1:xpPpY8cVT6n9DgIRgrWyE+YEsGlO/994pWnbc7o5Eh4= -cloud.google.com/go/logging v1.14.0/go.mod h1:jmI+Try/fZeOTOAer3wVYOuPf9WX9PyzhlSDoBAi4HM= -cloud.google.com/go/longrunning v0.9.0 h1:0EzbDEGsAvOZNbqXopgniY0w0a1phvu5IdUFq8grmqY= -cloud.google.com/go/longrunning v0.9.0/go.mod h1:pkTz846W7bF4o2SzdWJ40Hu0Re+UoNT6Q5t+igIcb8E= -cloud.google.com/go/monitoring v1.26.0 h1:858kWP5akszJFeiWBSmQJIfS8vsCqkX9hjc7HPsv/tk= -cloud.google.com/go/monitoring v1.26.0/go.mod h1:72NOVjJXHY/HBfoLT0+qlCZBT059+9VXLeAnL2PeeVM= -cloud.google.com/go/storage v1.62.1 h1:Os0G3XbUbjZumkpDUf2Y0rLoXJTCF1kU2kWUujKYXD8= -cloud.google.com/go/storage v1.62.1/go.mod h1:cpYz/kRVZ+UQAF1uHeea10/9ewcRbxGoGNKsS9daSXA= -cloud.google.com/go/trace v1.12.0 h1:XvWHYfr9q88cX4pZyou6qCcSagnuASyUq2ej1dB6NzQ= -cloud.google.com/go/trace v1.12.0/go.mod h1:TOYfyeoyCGsSH0ifXD6Aius24uQI9xV3RyvOdljFIyg= dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= @@ -44,14 +22,6 @@ github.com/GoCodeAlone/modular/modules/reverseproxy/v2 v2.8.0 h1:cvdLHbM/vzvygQT github.com/GoCodeAlone/modular/modules/reverseproxy/v2 v2.8.0/go.mod h1:/9ipMG4qM2CHQ14BfXKdVlYRJelef6M8MFI5TbZv67M= github.com/GoCodeAlone/yaegi v0.17.2 h1:WK6Y6e0t1a6U7r+S2dN3CGWW1PizYD3zO0zneToZPxM= github.com/GoCodeAlone/yaegi v0.17.2/go.mod h1:z5Pr6Wse6QJcQvpgxTxzMAevFarH0N37TG88Y9dprx0= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.32.0 h1:rIkQfkCOVKc1OiRCNcSDD8ml5RJlZbH/Xsq7lbpynwc= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.32.0/go.mod h1:RD2SsorTmYhF6HkTmDw7KmPYQk8OBYwTkuasChwv7R4= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.56.0 h1:O2sXMyJh8b7devAGdE+163xtRurt0RVpB6DIzX5vGfg= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.56.0/go.mod h1:hEpiGU18xf70qb3jbTcIggWAiEfX/cOIVc2OTe4OegA= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.56.0 h1:ZIT85vKP7LBS84XJ0WdJ3dPOX3iz4j3c0+lpajGQMyo= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.56.0/go.mod h1:rqP9UEhOXv9WhQ7Gjz+G5y/pf8+BJZW5/Ts0AhE0PwE= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.56.0 h1:0YP0+/ixwu+Uqeu/FGiBZNQ19huiUxxiPXIc9WsLKuQ= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.56.0/go.mod h1:6ZZMQhZKDvUvkJw2rc+oDP90tMMzuU/J+5HG1ZmPOmE= github.com/IBM/sarama v1.47.0 h1:GcQFEd12+KzfPYeLgN69Fh7vLCtYRhVIx0rO4TZO318= github.com/IBM/sarama v1.47.0/go.mod h1:7gLLIU97nznOmA6TX++Qds+DRxH89P2XICY2KAQUzAY= github.com/Microsoft/go-winio v0.5.0/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= @@ -136,8 +106,6 @@ github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6D github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= github.com/cloudevents/sdk-go/v2 v2.16.2 h1:ZYDFrYke4FD+jM8TZTJJO6JhKHzOQl2oqpFK1D+NnQM= github.com/cloudevents/sdk-go/v2 v2.16.2/go.mod h1:laOcGImm4nVJEU+PHnUrKL56CKmRL65RlQF0kRmW/kg= -github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 h1:aBangftG7EVZoUb69Os8IaYg++6uMOdKK83QtkkvJik= -github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2/go.mod h1:qwXFYgsP6T7XnJtbKlf1HP8AjxZZyzxMmc+Lq5GjlU4= github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= @@ -189,14 +157,6 @@ github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= -github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA= -github.com/envoyproxy/go-control-plane v0.14.0/go.mod h1:NcS5X47pLl/hfqxU70yPwL9ZMkUlwlKxtAohpi2wBEU= -github.com/envoyproxy/go-control-plane/envoy v1.37.0 h1:u3riX6BoYRfF4Dr7dwSOroNfdSbEPe9Yyl09/B6wBrQ= -github.com/envoyproxy/go-control-plane/envoy v1.37.0/go.mod h1:DReE9MMrmecPy+YvQOAOHNYMALuowAnbjjEMkkWOi6A= -github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI= -github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4= -github.com/envoyproxy/protoc-gen-validate v1.3.3 h1:MVQghNeW+LZcmXe7SY1V36Z+WFMDjpqGAGacLe2T0ds= -github.com/envoyproxy/protoc-gen-validate v1.3.3/go.mod h1:TsndJ/ngyIdQRhMcVVGDDHINPLWB7C82oDArY51KfB0= github.com/expr-lang/expr v1.17.8 h1:W1loDTT+0PQf5YteHSTpju2qfUfNoBt4yw9+wOEU9VM= github.com/expr-lang/expr v1.17.8/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4= github.com/fatih/color v1.19.0 h1:Zp3PiM21/9Ld6FzSKyL5c/BULoe/ONr9KlbYVOfG8+w= @@ -307,18 +267,10 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= github.com/google/go-tpm v0.9.8/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc= -github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0= github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8= github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= -github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= -github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= 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/googleapis/enterprise-certificate-proxy v0.3.14 h1:yh8ncqsbUY4shRD5dA6RlzjJaT4hi3kII+zYw8wmLb8= -github.com/googleapis/enterprise-certificate-proxy v0.3.14/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg= -github.com/googleapis/gax-go/v2 v2.21.0 h1:h45NjjzEO3faG9Lg/cFrBh2PgegVVgzqKzuZl/wMbiI= -github.com/googleapis/gax-go/v2 v2.21.0/go.mod h1:But/NJU6TnZsrLai/xBAQLLz+Hc7fHZJt/hsCz3Fih4= github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 h1:5VipnvEpbqr2gA2VbM+nYVbkIF28c5ZQfqCBQ5g2xfk= @@ -505,8 +457,6 @@ github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= -github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -566,8 +516,6 @@ github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo= -github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= @@ -649,10 +597,6 @@ go.mongodb.org/mongo-driver v1.17.9 h1:IexDdCuuNJ3BHrELgBlyaH9p60JXAvdzWR128q+U5 go.mongodb.org/mongo-driver v1.17.9/go.mod h1:LlOhpH5NUEfhxcAwG0UEkMqwYcc4JU18gtCdGudk/tQ= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/contrib/detectors/gcp v1.43.0 h1:62yY3dT7/ShwOxzA0RsKRgshBmfElKI4d/Myu2OxDFU= -go.opentelemetry.io/contrib/detectors/gcp v1.43.0/go.mod h1:RyaZMFY7yi1kAs45S6mbFGz8O8rqB0dTY14uzvG4LCs= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0 h1:0Qx7VGBacMm9ZENQ7TnNObTYI4ShC+lHI16seduaxZo= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0/go.mod h1:Sje3i3MjSPKTSPvVWCaL8ugBzJwik3u4smCjUeuupqg= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 h1:CqXxU8VOmDefoh0+ztfGaymYbhdB/tT3zs79QaZTNGY= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0/go.mod h1:BuhAPThV8PBHBvg8ZzZ/Ok3idOdhWIodywz2xEcRbJo= go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= @@ -661,8 +605,6 @@ go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bT go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0/go.mod h1:Vl1/iaggsuRlrHf/hfPJPvVag77kKyvrLeD10kpMl+A= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 h1:3iZJKlCZufyRzPzlQhUIWVmfltrXuGyfjREgGP3UUjc= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0/go.mod h1:/G+nUPfhq2e+qiXMGxMwumDrP5jtzU+mWN7/sjT2rak= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.43.0 h1:TC+BewnDpeiAmcscXbGMfxkO+mwYUwE/VySwvw88PfA= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.43.0/go.mod h1:J/ZyF4vfPwsSr9xJSPyQ4LqtcTPULFR64KwTikGLe+A= go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= @@ -782,11 +724,7 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= -google.golang.org/api v0.275.0 h1:vfY5d9vFVJeWEZT65QDd9hbndr7FyZ2+6mIzGAh71NI= -google.golang.org/api v0.275.0/go.mod h1:Fnag/EWUPIcJXuIkP1pjoTgS5vdxlk3eeemL7Do6bvw= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/genproto v0.0.0-20260406210006-6f92a3bedf2d h1:N1Ec54vZnIPd7MnxRiYLW+oY4fDR4BOS/LrssdD9+ek= -google.golang.org/genproto v0.0.0-20260406210006-6f92a3bedf2d/go.mod h1:c2hJ1grtnH0xUiEKGDGkjGNTJ1Hy2LrblyKOHF0sqRM= google.golang.org/genproto/googleapis/api v0.0.0-20260420184626-e10c466a9529 h1:zUWMZsvo/IJcD1t6MNCPO/azZTwz0TvwCBqr5aifoVY= google.golang.org/genproto/googleapis/api v0.0.0-20260420184626-e10c466a9529/go.mod h1:a5OGAgyRr4lqco7AG9hQM9Fwh0N2ZV4grR0eXFEsXQg= google.golang.org/genproto/googleapis/rpc v0.0.0-20260420184626-e10c466a9529 h1:XF8+t6QQiS0o9ArVan/HW8Q7cycNPGsJf6GA2nXxYAg= diff --git a/module/auth_oauth2.go b/module/auth_oauth2.go index d7fd881a..d74c0071 100644 --- a/module/auth_oauth2.go +++ b/module/auth_oauth2.go @@ -20,9 +20,14 @@ import ( // googleOAuth2Endpoint hard-codes Google's static IdP OAuth2 endpoints (RFC 6749). // We avoid importing `golang.org/x/oauth2/google` because its package init // transitively pulls `cloud.google.com/go/compute/metadata` (an ADC helper -// for GCE/GKE workload identity) — and Phase C's permanent asymmetric CI -// gate (decisions/0034 + plan 2026-05-15-plugin-modules-on-iac.md Task 18) -// asserts zero `cloud.google.com/go/*` packages in core's build graph. +// for GCE/GKE workload identity). The Phase C asymmetric CI gate +// (decisions/0034 + plan 2026-05-15-plugin-modules-on-iac.md Task 18) asserts +// no UNEXPECTED `cloud.google.com/go/*` packages in core's build graph; +// `cloud.google.com/go/compute/metadata` is allowlisted as the OAuth2 ADC +// helper transitively pulled by provider/gcp's service-account auth. Keeping +// this auth handler off `oauth2/google` keeps that allowlist single-purpose +// (provider/gcp only) — adding a second importer would entrench the +// transitive dep in a surface that doesn't need it. // These URLs are static published Google OAuth2 endpoints: // // https://developers.google.com/identity/protocols/oauth2/web-server diff --git a/scripts/audit-cloud-symbols.sh b/scripts/audit-cloud-symbols.sh index 6c03022a..1e6ee800 100755 --- a/scripts/audit-cloud-symbols.sh +++ b/scripts/audit-cloud-symbols.sh @@ -122,7 +122,20 @@ echo "== Invariant: build graph has no unexpected gcp/azure/api transitive deps # Asymmetric vs aws-sdk-go-v2 (Phase B): aws-sdk-go-v2 STAYS for out-of-scope # provider/aws/ + plugin/rbac/aws.go + iam/aws.go + artifact/s3.go. if [[ $CHECK -eq 1 && -f .phase-c-complete ]]; then - DEPS=$(GOWORK=off go list -deps ./... 2>/dev/null || true) + # Capture exit code separately so a failed `go list` cannot be swallowed + # by `|| true` (which would also mask legitimate gate violations). + set +e + DEPS=$(GOWORK=off go list -deps ./... 2>&1) + LIST_EXIT=$? + set -e + if [[ $LIST_EXIT -ne 0 ]]; then + echo " FAIL: \`go list -deps ./...\` exited $LIST_EXIT (gate cannot enforce):" + echo "$DEPS" | head -10 | sed 's/^/ /' + FAIL=1 + DEPS="" + fi + # `|| true` on each grep is fine: grep returning 1 means "no matches" = + # success case. Only the outer `go list` exit code matters for gate sanity. AZURE_UNEXPECTED=$(echo "$DEPS" | grep -F 'github.com/Azure/azure-sdk-for-go' || true) API_UNEXPECTED=$(echo "$DEPS" | grep '^google\.golang\.org/api' || true) GCP_UNEXPECTED=$(echo "$DEPS" \ From 924173e87a73f2c82a9026b31586cff68ed4d45a Mon Sep 17 00:00:00 2001 From: Jon Langevin Date: Fri, 15 May 2026 19:44:44 -0400 Subject: [PATCH 5/8] fix: suppress gosec G101 false-positive on public OAuth2 token endpoint URL --- module/auth_oauth2.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/module/auth_oauth2.go b/module/auth_oauth2.go index d74c0071..70ba63c3 100644 --- a/module/auth_oauth2.go +++ b/module/auth_oauth2.go @@ -33,7 +33,7 @@ import ( // https://developers.google.com/identity/protocols/oauth2/web-server var googleOAuth2Endpoint = oauth2.Endpoint{ AuthURL: "https://accounts.google.com/o/oauth2/auth", - TokenURL: "https://oauth2.googleapis.com/token", + TokenURL: "https://oauth2.googleapis.com/token", //nolint:gosec // G101: public OAuth2 token endpoint URL, not a credential } // OAuth2ProviderConfig holds configuration for a single OAuth2 provider. From e472dac0a0a13cd7ef52091b27983a6ed5419e01 Mon Sep 17 00:00:00 2001 From: Jon Langevin Date: Fri, 15 May 2026 19:59:57 -0400 Subject: [PATCH 6/8] fix: G101 suppression on AuthURL + audit script regex fixes (real_import grep -F, anchor azure prefix to ^) --- module/auth_oauth2.go | 4 ++-- scripts/audit-cloud-symbols.sh | 13 +++++++++---- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/module/auth_oauth2.go b/module/auth_oauth2.go index 70ba63c3..1544418c 100644 --- a/module/auth_oauth2.go +++ b/module/auth_oauth2.go @@ -32,8 +32,8 @@ import ( // // https://developers.google.com/identity/protocols/oauth2/web-server var googleOAuth2Endpoint = oauth2.Endpoint{ - AuthURL: "https://accounts.google.com/o/oauth2/auth", - TokenURL: "https://oauth2.googleapis.com/token", //nolint:gosec // G101: public OAuth2 token endpoint URL, not a credential + AuthURL: "https://accounts.google.com/o/oauth2/auth", //nolint:gosec // G101: public OAuth2 auth endpoint URL, not a credential + TokenURL: "https://oauth2.googleapis.com/token", //nolint:gosec // G101: public OAuth2 token endpoint URL, not a credential } // OAuth2ProviderConfig holds configuration for a single OAuth2 provider. diff --git a/scripts/audit-cloud-symbols.sh b/scripts/audit-cloud-symbols.sh index 1e6ee800..090f937c 100755 --- a/scripts/audit-cloud-symbols.sh +++ b/scripts/audit-cloud-symbols.sh @@ -42,7 +42,9 @@ real_import() { # file, sdk → 0 if sdk appears in a real import (block OR sin # under `set -o pipefail`. # Single-line form matches plain, aliased, dot, and blank imports: # import "pkg" / import foo "pkg" / import . "pkg" / import _ "pkg" - { import_block "$1"; grep -E '^import +([A-Za-z_.][A-Za-z0-9_]* +)?"' "$1" 2>/dev/null || true; } | grep -q "$2" + # Match the SDK string with `-F` (fixed string): SDK prefixes contain `.` + # which would otherwise be regex metachars matching any character. + { import_block "$1"; grep -E '^import +([A-Za-z_.][A-Za-z0-9_]* +)?"' "$1" 2>/dev/null || true; } | grep -qF "$2" } CHECK=0 @@ -136,10 +138,13 @@ if [[ $CHECK -eq 1 && -f .phase-c-complete ]]; then fi # `|| true` on each grep is fine: grep returning 1 means "no matches" = # success case. Only the outer `go list` exit code matters for gate sanity. - AZURE_UNEXPECTED=$(echo "$DEPS" | grep -F 'github.com/Azure/azure-sdk-for-go' || true) - API_UNEXPECTED=$(echo "$DEPS" | grep '^google\.golang\.org/api' || true) + # Anchor every prefix to `^`: `go list` may emit `go: downloading …` lines + # on stderr that get captured into $DEPS during transient module fetches — + # matching unanchored would false-fail on those informational lines. + AZURE_UNEXPECTED=$(echo "$DEPS" | grep -E '^github\.com/Azure/azure-sdk-for-go' || true) + API_UNEXPECTED=$(echo "$DEPS" | grep -E '^google\.golang\.org/api' || true) GCP_UNEXPECTED=$(echo "$DEPS" \ - | grep '^cloud\.google\.com/go' \ + | grep -E '^cloud\.google\.com/go' \ | grep -v '^cloud\.google\.com/go/compute/metadata$' \ || true) if [[ -n "$AZURE_UNEXPECTED" ]]; then From 54a5b583245ff339260c68c701a48926ea46972b Mon Sep 17 00:00:00 2001 From: Jon Langevin Date: Fri, 15 May 2026 20:18:12 -0400 Subject: [PATCH 7/8] =?UTF-8?q?fix:=20golangci-lint=20G101=20suppression?= =?UTF-8?q?=20=E2=80=94=20leading=20comment=20on=20var=20decl=20for=20stru?= =?UTF-8?q?ct-literal=20fields?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- module/auth_oauth2.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/module/auth_oauth2.go b/module/auth_oauth2.go index 1544418c..ebc7b5b3 100644 --- a/module/auth_oauth2.go +++ b/module/auth_oauth2.go @@ -31,9 +31,11 @@ import ( // These URLs are static published Google OAuth2 endpoints: // // https://developers.google.com/identity/protocols/oauth2/web-server +// +//nolint:gosec // G101: public OAuth2 IdP endpoint URLs (auth + token), not credentials var googleOAuth2Endpoint = oauth2.Endpoint{ - AuthURL: "https://accounts.google.com/o/oauth2/auth", //nolint:gosec // G101: public OAuth2 auth endpoint URL, not a credential - TokenURL: "https://oauth2.googleapis.com/token", //nolint:gosec // G101: public OAuth2 token endpoint URL, not a credential + AuthURL: "https://accounts.google.com/o/oauth2/auth", + TokenURL: "https://oauth2.googleapis.com/token", } // OAuth2ProviderConfig holds configuration for a single OAuth2 provider. From 1c887a58373854321126cb66c9032ff64e1f5aad Mon Sep 17 00:00:00 2001 From: Jon Langevin Date: Fri, 15 May 2026 20:31:12 -0400 Subject: [PATCH 8/8] =?UTF-8?q?fix:=20audit=20script=20=E2=80=94=20strip?= =?UTF-8?q?=20comment=20lines=20in=20real=5Fimport=20+=20portable=20-E=20a?= =?UTF-8?q?lternation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/audit-cloud-symbols.sh | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/scripts/audit-cloud-symbols.sh b/scripts/audit-cloud-symbols.sh index 090f937c..042afb08 100755 --- a/scripts/audit-cloud-symbols.sh +++ b/scripts/audit-cloud-symbols.sh @@ -42,9 +42,15 @@ real_import() { # file, sdk → 0 if sdk appears in a real import (block OR sin # under `set -o pipefail`. # Single-line form matches plain, aliased, dot, and blank imports: # import "pkg" / import foo "pkg" / import . "pkg" / import _ "pkg" + # Strip `//` line comments before matching — `import_block` returns every + # line in `import (...)` including comments, so a comment that names the + # SDK (e.g. `// TODO: re-add cloud.google.com/go/storage`) would otherwise + # false-positive as a real import. # Match the SDK string with `-F` (fixed string): SDK prefixes contain `.` # which would otherwise be regex metachars matching any character. - { import_block "$1"; grep -E '^import +([A-Za-z_.][A-Za-z0-9_]* +)?"' "$1" 2>/dev/null || true; } | grep -qF "$2" + { import_block "$1"; grep -E '^import +([A-Za-z_.][A-Za-z0-9_]* +)?"' "$1" 2>/dev/null || true; } \ + | sed 's|//.*||' \ + | grep -qF "$2" } CHECK=0 @@ -99,7 +105,7 @@ while IFS= read -r f; do MOD_FORBIDDEN+=("$f -> $sdk") fi done -done < <(grep -rl 'cloud\.google\.com/go\|google\.golang\.org/api\|github\.com/Azure/azure-sdk-for-go' module/ --include='*.go' 2>/dev/null | grep -v '_test\.go' | sort) +done < <(grep -rlE 'cloud\.google\.com/go|google\.golang\.org/api|github\.com/Azure/azure-sdk-for-go' module/ --include='*.go' 2>/dev/null | grep -v '_test\.go' | sort) if [[ ${#MOD_FORBIDDEN[@]} -eq 0 ]]; then echo " module/: 0 real imports — clean" else