diff --git a/app/controlplane/pkg/biz/workflowrun_integration_test.go b/app/controlplane/pkg/biz/workflowrun_integration_test.go index 030cf1d74..cf415c5e0 100644 --- a/app/controlplane/pkg/biz/workflowrun_integration_test.go +++ b/app/controlplane/pkg/biz/workflowrun_integration_test.go @@ -130,6 +130,89 @@ func (s *workflowRunIntegrationTestSuite) TestList() { } } +func (s *workflowRunIntegrationTestSuite) TestListExcludesSoftDeletedWorkflows() { + ctx := context.Background() + + // Create a fresh workflow + run, soft-delete the workflow, and make sure + // the run is excluded from List. Regression guard for the case where + // org-scoped reads use the denormalized organization_id column on + // workflow_runs and could otherwise leak runs from deleted workflows. + wf, err := s.Workflow.Create(ctx, &biz.WorkflowCreateOpts{ + Name: "to-be-deleted", OrgID: s.org2.ID, Project: "test-project", + }) + s.Require().NoError(err) + + _, err = s.WorkflowRun.Create(ctx, &biz.WorkflowRunCreateOpts{ + WorkflowID: wf.ID.String(), ContractRevision: s.contractVersion, CASBackendID: s.casBackend.ID, + }) + s.Require().NoError(err) + + s.Require().NoError(s.Workflow.Delete(ctx, s.org2.ID, wf.ID.String())) + + got, _, err := s.WorkflowRun.List(ctx, s.org2.ID, &biz.RunListFilters{}, &pagination.CursorOptions{Limit: 10}) + s.Require().NoError(err) + for _, r := range got { + s.NotEqual(wf.ID, r.Workflow.ID, "run from soft-deleted workflow leaked into List") + } +} + +func (s *workflowRunIntegrationTestSuite) TestListIsolatedByOrg() { + ctx := context.Background() + + // org1 has runOrg1; org2 has runOrg2 + runOrg2Public (see setupWorkflowRunTestData). + // Regression guard for the org-scoping switch from the workflows edge to + // the denormalized organization_id column on workflow_runs. + got, _, err := s.WorkflowRun.List(ctx, s.org.ID, &biz.RunListFilters{}, &pagination.CursorOptions{Limit: 10}) + s.Require().NoError(err) + gotIDs := make([]uuid.UUID, 0, len(got)) + for _, r := range got { + gotIDs = append(gotIDs, r.ID) + } + s.ElementsMatch([]uuid.UUID{s.runOrg1.ID}, gotIDs, "org1 List leaked runs from another org") + + got, _, err = s.WorkflowRun.List(ctx, s.org2.ID, &biz.RunListFilters{}, &pagination.CursorOptions{Limit: 10}) + s.Require().NoError(err) + gotIDs = gotIDs[:0] + for _, r := range got { + gotIDs = append(gotIDs, r.ID) + } + s.ElementsMatch([]uuid.UUID{s.runOrg2.ID, s.runOrg2Public.ID}, gotIDs, "org2 List did not return its own runs") +} + +func (s *workflowRunIntegrationTestSuite) TestListFilterByProjectIDs() { + ctx := context.Background() + + // Create a second workflow in a different project in org2 and a run for it. + // Filtering by the original project's ID should exclude this run. + otherProjectWF, err := s.Workflow.Create(ctx, &biz.WorkflowCreateOpts{ + Name: "wf-other-project", OrgID: s.org2.ID, Project: "other-project", + }) + s.Require().NoError(err) + + otherProjectRun, err := s.WorkflowRun.Create(ctx, &biz.WorkflowRunCreateOpts{ + WorkflowID: otherProjectWF.ID.String(), ContractRevision: s.contractVersion, CASBackendID: s.casBackend.ID, + }) + s.Require().NoError(err) + + // With no project filter, all org2 runs (including the new one) are returned. + allRuns, _, err := s.WorkflowRun.List(ctx, s.org2.ID, &biz.RunListFilters{}, &pagination.CursorOptions{Limit: 10}) + s.Require().NoError(err) + allIDs := make([]uuid.UUID, 0, len(allRuns)) + for _, r := range allRuns { + allIDs = append(allIDs, r.ID) + } + s.Contains(allIDs, otherProjectRun.ID) + + // Filtering by the original project's ID excludes runs from other projects. + filtered, _, err := s.WorkflowRun.List(ctx, s.org2.ID, + &biz.RunListFilters{ProjectIDs: []uuid.UUID{s.workflowOrg2.ProjectID}}, + &pagination.CursorOptions{Limit: 10}) + s.Require().NoError(err) + for _, r := range filtered { + s.NotEqual(otherProjectRun.ID, r.ID, "run from non-selected project leaked into List") + } +} + func (s *workflowRunIntegrationTestSuite) TestSaveAttestation() { assert := assert.New(s.T()) ctx := context.Background() diff --git a/app/controlplane/pkg/data/ent/client.go b/app/controlplane/pkg/data/ent/client.go index 8db658d29..5e8db4503 100644 --- a/app/controlplane/pkg/data/ent/client.go +++ b/app/controlplane/pkg/data/ent/client.go @@ -2220,6 +2220,22 @@ func (c *OrganizationClient) QueryWorkflows(_m *Organization) *WorkflowQuery { return query } +// QueryWorkflowruns queries the workflowruns edge of a Organization. +func (c *OrganizationClient) QueryWorkflowruns(_m *Organization) *WorkflowRunQuery { + query := (&WorkflowRunClient{config: c.config}).Query() + query.path = func(context.Context) (fromV *sql.Selector, _ error) { + id := _m.ID + step := sqlgraph.NewStep( + sqlgraph.From(organization.Table, organization.FieldID, id), + sqlgraph.To(workflowrun.Table, workflowrun.FieldID), + sqlgraph.Edge(sqlgraph.O2M, false, organization.WorkflowrunsTable, organization.WorkflowrunsColumn), + ) + fromV = sqlgraph.Neighbors(_m.driver.Dialect(), step) + return fromV, nil + } + return query +} + // QueryCasBackends queries the cas_backends edge of a Organization. func (c *OrganizationClient) QueryCasBackends(_m *Organization) *CASBackendQuery { query := (&CASBackendClient{config: c.config}).Query() @@ -3881,6 +3897,22 @@ func (c *WorkflowRunClient) QueryWorkflow(_m *WorkflowRun) *WorkflowQuery { return query } +// QueryOrganization queries the organization edge of a WorkflowRun. +func (c *WorkflowRunClient) QueryOrganization(_m *WorkflowRun) *OrganizationQuery { + query := (&OrganizationClient{config: c.config}).Query() + query.path = func(context.Context) (fromV *sql.Selector, _ error) { + id := _m.ID + step := sqlgraph.NewStep( + sqlgraph.From(workflowrun.Table, workflowrun.FieldID, id), + sqlgraph.To(organization.Table, organization.FieldID), + sqlgraph.Edge(sqlgraph.M2O, true, workflowrun.OrganizationTable, workflowrun.OrganizationColumn), + ) + fromV = sqlgraph.Neighbors(_m.driver.Dialect(), step) + return fromV, nil + } + return query +} + // QueryContractVersion queries the contract_version edge of a WorkflowRun. func (c *WorkflowRunClient) QueryContractVersion(_m *WorkflowRun) *WorkflowContractVersionQuery { query := (&WorkflowContractVersionClient{config: c.config}).Query() diff --git a/app/controlplane/pkg/data/ent/migrate/migrations/20260516210119.sql b/app/controlplane/pkg/data/ent/migrate/migrations/20260516210119.sql new file mode 100644 index 000000000..575831a23 --- /dev/null +++ b/app/controlplane/pkg/data/ent/migrate/migrations/20260516210119.sql @@ -0,0 +1,86 @@ +-- atlas:txmode none + +-- Denormalize organization_id onto workflow_runs so org-scoped list/aggregate +-- queries become sargable without joining workflows. +-- +-- Why a trigger? +-- +-- The control plane deploys as a multi-replica Deployment with rolling +-- updates. When this migration runs (in the initContainer of a new pod), +-- old pods are still serving traffic with code that does NOT set +-- organization_id on INSERT. The moment step 6 below enforces NOT NULL, +-- every INSERT from those old pods would fail with a constraint violation +-- until the rolling update replaces them — a window of seconds to minutes +-- in which workflow run creation is broken org-wide. +-- +-- The BEFORE INSERT trigger below bridges that window: whenever a writer +-- doesn't set organization_id, the trigger fills it from the parent +-- workflow via a single PK lookup (~0.1ms). New code paths set the column +-- explicitly so the trigger's IF check short-circuits; the trigger only +-- does real work for inserts coming from the old replicas. Once every +-- replica runs the new code, the trigger is dead weight — a follow-up +-- release will drop both the trigger and its function. + +-- 1. Nullable add (catalog-only, instant). +ALTER TABLE "workflow_runs" ADD COLUMN "organization_id" uuid; + +-- 2. FK NOT VALID (no scan, brief AccessExclusive lock). +ALTER TABLE "workflow_runs" + ADD CONSTRAINT "workflow_runs_organizations_workflowruns" + FOREIGN KEY ("organization_id") REFERENCES "organizations" ("id") + ON UPDATE NO ACTION ON DELETE CASCADE NOT VALID; + +-- 3. Trigger function: fills organization_id from the parent workflow when +-- the caller didn't set it. Removed by a follow-up migration in the next +-- release once all replicas set the column explicitly. +CREATE OR REPLACE FUNCTION fill_workflow_run_organization_id() RETURNS trigger AS $$ +BEGIN + IF NEW.organization_id IS NULL THEN + SELECT organization_id INTO NEW.organization_id + FROM "workflows" WHERE id = NEW.workflow_id; + END IF; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER workflow_runs_fill_organization_id + BEFORE INSERT ON "workflow_runs" + FOR EACH ROW EXECUTE FUNCTION fill_workflow_run_organization_id(); + +-- 4. Batched backfill. Concurrent inserts from old replicas are protected by +-- the trigger above, so they can't introduce new NULL rows mid-loop. +-- One COMMIT per batch keeps the longest row-lock window in the millisecond +-- range and avoids one giant WAL entry. +DO $$ +DECLARE + rows_done INT; +BEGIN + LOOP + WITH batch AS ( + SELECT wr.id, w.organization_id + FROM "workflow_runs" wr + JOIN "workflows" w ON wr.workflow_id = w.id + WHERE wr.organization_id IS NULL + LIMIT 5000 + ) + UPDATE "workflow_runs" wr + SET organization_id = b.organization_id + FROM batch b + WHERE wr.id = b.id; + GET DIAGNOSTICS rows_done = ROW_COUNT; + COMMIT; + EXIT WHEN rows_done = 0; + END LOOP; +END $$; + +-- 5. Validate the FK now that data is consistent. SHARE UPDATE EXCLUSIVE +-- permits concurrent DML. +ALTER TABLE "workflow_runs" VALIDATE CONSTRAINT "workflow_runs_organizations_workflowruns"; + +-- 6. Enforce NOT NULL. In PG 12+ this is a verify-only scan (no rewrite). +-- Safe because the trigger guarantees no concurrent NULL inserts. +ALTER TABLE "workflow_runs" ALTER COLUMN "organization_id" SET NOT NULL; + +-- 7. Create the org-scoped list index without blocking writes. +CREATE INDEX CONCURRENTLY "workflowrun_organization_id_created_at" + ON "workflow_runs" ("organization_id", "created_at" DESC); diff --git a/app/controlplane/pkg/data/ent/migrate/migrations/atlas.sum b/app/controlplane/pkg/data/ent/migrate/migrations/atlas.sum index 5c7b9cd15..c0985084b 100644 --- a/app/controlplane/pkg/data/ent/migrate/migrations/atlas.sum +++ b/app/controlplane/pkg/data/ent/migrate/migrations/atlas.sum @@ -1,4 +1,4 @@ -h1:Ob1pZMVVtMju/FJnsU/Li8Fg2DjcXUxzYDuaBWk9vp0= +h1:OSH+lqOh2mE49KklM6mUMDQjrL1N2nGHdz9aERNstTM= 20230706165452_init-schema.sql h1:VvqbNFEQnCvUVyj2iDYVQQxDM0+sSXqocpt/5H64k8M= 20230710111950-cas-backend.sql h1:A8iBuSzZIEbdsv9ipBtscZQuaBp3V5/VMw7eZH6GX+g= 20230712094107-cas-backends-workflow-runs.sql h1:a5rzxpVGyd56nLRSsKrmCFc9sebg65RWzLghKHh5xvI= @@ -134,3 +134,4 @@ h1:Ob1pZMVVtMju/FJnsU/Li8Fg2DjcXUxzYDuaBWk9vp0= 20260504100323.sql h1:FP8y59ZXFUsyskbIfl/1nE7vo4OJcOPuALy3pAJaStQ= 20260511202105.sql h1:Tw9OkiWm7cT4p2pNklSUGM9DzKS38uUuYjXl8BdIwnQ= 20260514150303.sql h1:0bGVXYN5rBP9Hn9x/ou8JgKotKiFbSKWGHX2dBH/wCA= +20260516210119.sql h1:rfBnXQwPnrhVYAp/OIiFPGcS+Tx1x9CAMSDPGs8HIT8= diff --git a/app/controlplane/pkg/data/ent/migrate/schema.go b/app/controlplane/pkg/data/ent/migrate/schema.go index ada9db2c8..99c493e72 100644 --- a/app/controlplane/pkg/data/ent/migrate/schema.go +++ b/app/controlplane/pkg/data/ent/migrate/schema.go @@ -782,6 +782,7 @@ var ( {Name: "policy_violations_count", Type: field.TypeInt32, Nullable: true}, {Name: "policy_violations_suppressed", Type: field.TypeInt32, Nullable: true}, {Name: "policy_has_gates", Type: field.TypeBool, Nullable: true}, + {Name: "organization_id", Type: field.TypeUUID}, {Name: "version_id", Type: field.TypeUUID}, {Name: "workflow_id", Type: field.TypeUUID}, {Name: "workflow_run_contract_version", Type: field.TypeUUID, Nullable: true}, @@ -793,20 +794,26 @@ var ( PrimaryKey: []*schema.Column{WorkflowRunsColumns[0]}, ForeignKeys: []*schema.ForeignKey{ { - Symbol: "workflow_runs_project_versions_runs", + Symbol: "workflow_runs_organizations_workflowruns", Columns: []*schema.Column{WorkflowRunsColumns[20]}, + RefColumns: []*schema.Column{OrganizationsColumns[0]}, + OnDelete: schema.Cascade, + }, + { + Symbol: "workflow_runs_project_versions_runs", + Columns: []*schema.Column{WorkflowRunsColumns[21]}, RefColumns: []*schema.Column{ProjectVersionsColumns[0]}, OnDelete: schema.NoAction, }, { Symbol: "workflow_runs_workflows_workflowruns", - Columns: []*schema.Column{WorkflowRunsColumns[21]}, + Columns: []*schema.Column{WorkflowRunsColumns[22]}, RefColumns: []*schema.Column{WorkflowsColumns[0]}, OnDelete: schema.Cascade, }, { Symbol: "workflow_runs_workflow_contract_versions_contract_version", - Columns: []*schema.Column{WorkflowRunsColumns[22]}, + Columns: []*schema.Column{WorkflowRunsColumns[23]}, RefColumns: []*schema.Column{WorkflowContractVersionsColumns[0]}, OnDelete: schema.Cascade, }, @@ -825,7 +832,7 @@ var ( { Name: "workflowrun_workflow_id_created_at", Unique: false, - Columns: []*schema.Column{WorkflowRunsColumns[21], WorkflowRunsColumns[1]}, + Columns: []*schema.Column{WorkflowRunsColumns[22], WorkflowRunsColumns[1]}, Annotation: &entsql.IndexAnnotation{ DescColumns: map[string]bool{ WorkflowRunsColumns[1].Name: true, @@ -835,7 +842,17 @@ var ( { Name: "workflowrun_workflow_id_state_created_at", Unique: false, - Columns: []*schema.Column{WorkflowRunsColumns[21], WorkflowRunsColumns[3], WorkflowRunsColumns[1]}, + Columns: []*schema.Column{WorkflowRunsColumns[22], WorkflowRunsColumns[3], WorkflowRunsColumns[1]}, + Annotation: &entsql.IndexAnnotation{ + DescColumns: map[string]bool{ + WorkflowRunsColumns[1].Name: true, + }, + }, + }, + { + Name: "workflowrun_organization_id_created_at", + Unique: false, + Columns: []*schema.Column{WorkflowRunsColumns[20], WorkflowRunsColumns[1]}, Annotation: &entsql.IndexAnnotation{ DescColumns: map[string]bool{ WorkflowRunsColumns[1].Name: true, @@ -860,12 +877,12 @@ var ( { Name: "workflowrun_workflow_id", Unique: false, - Columns: []*schema.Column{WorkflowRunsColumns[21]}, + Columns: []*schema.Column{WorkflowRunsColumns[22]}, }, { Name: "workflowrun_version_id_workflow_id", Unique: false, - Columns: []*schema.Column{WorkflowRunsColumns[20], WorkflowRunsColumns[21]}, + Columns: []*schema.Column{WorkflowRunsColumns[21], WorkflowRunsColumns[22]}, }, { Name: "workflowrun_policy_status", @@ -1014,9 +1031,10 @@ func init() { WorkflowsTable.ForeignKeys[3].RefTable = WorkflowRunsTable WorkflowContractsTable.ForeignKeys[0].RefTable = OrganizationsTable WorkflowContractVersionsTable.ForeignKeys[0].RefTable = WorkflowContractsTable - WorkflowRunsTable.ForeignKeys[0].RefTable = ProjectVersionsTable - WorkflowRunsTable.ForeignKeys[1].RefTable = WorkflowsTable - WorkflowRunsTable.ForeignKeys[2].RefTable = WorkflowContractVersionsTable + WorkflowRunsTable.ForeignKeys[0].RefTable = OrganizationsTable + WorkflowRunsTable.ForeignKeys[1].RefTable = ProjectVersionsTable + WorkflowRunsTable.ForeignKeys[2].RefTable = WorkflowsTable + WorkflowRunsTable.ForeignKeys[3].RefTable = WorkflowContractVersionsTable ReferrerReferencesTable.ForeignKeys[0].RefTable = ReferrersTable ReferrerReferencesTable.ForeignKeys[1].RefTable = ReferrersTable ReferrerWorkflowsTable.ForeignKeys[0].RefTable = ReferrersTable diff --git a/app/controlplane/pkg/data/ent/mutation.go b/app/controlplane/pkg/data/ent/mutation.go index 9d9ed1506..2b3b99d41 100644 --- a/app/controlplane/pkg/data/ent/mutation.go +++ b/app/controlplane/pkg/data/ent/mutation.go @@ -8987,6 +8987,9 @@ type OrganizationMutation struct { workflows map[uuid.UUID]struct{} removedworkflows map[uuid.UUID]struct{} clearedworkflows bool + workflowruns map[uuid.UUID]struct{} + removedworkflowruns map[uuid.UUID]struct{} + clearedworkflowruns bool cas_backends map[uuid.UUID]struct{} removedcas_backends map[uuid.UUID]struct{} clearedcas_backends bool @@ -9745,6 +9748,60 @@ func (m *OrganizationMutation) ResetWorkflows() { m.removedworkflows = nil } +// AddWorkflowrunIDs adds the "workflowruns" edge to the WorkflowRun entity by ids. +func (m *OrganizationMutation) AddWorkflowrunIDs(ids ...uuid.UUID) { + if m.workflowruns == nil { + m.workflowruns = make(map[uuid.UUID]struct{}) + } + for i := range ids { + m.workflowruns[ids[i]] = struct{}{} + } +} + +// ClearWorkflowruns clears the "workflowruns" edge to the WorkflowRun entity. +func (m *OrganizationMutation) ClearWorkflowruns() { + m.clearedworkflowruns = true +} + +// WorkflowrunsCleared reports if the "workflowruns" edge to the WorkflowRun entity was cleared. +func (m *OrganizationMutation) WorkflowrunsCleared() bool { + return m.clearedworkflowruns +} + +// RemoveWorkflowrunIDs removes the "workflowruns" edge to the WorkflowRun entity by IDs. +func (m *OrganizationMutation) RemoveWorkflowrunIDs(ids ...uuid.UUID) { + if m.removedworkflowruns == nil { + m.removedworkflowruns = make(map[uuid.UUID]struct{}) + } + for i := range ids { + delete(m.workflowruns, ids[i]) + m.removedworkflowruns[ids[i]] = struct{}{} + } +} + +// RemovedWorkflowruns returns the removed IDs of the "workflowruns" edge to the WorkflowRun entity. +func (m *OrganizationMutation) RemovedWorkflowrunsIDs() (ids []uuid.UUID) { + for id := range m.removedworkflowruns { + ids = append(ids, id) + } + return +} + +// WorkflowrunsIDs returns the "workflowruns" edge IDs in the mutation. +func (m *OrganizationMutation) WorkflowrunsIDs() (ids []uuid.UUID) { + for id := range m.workflowruns { + ids = append(ids, id) + } + return +} + +// ResetWorkflowruns resets all changes to the "workflowruns" edge. +func (m *OrganizationMutation) ResetWorkflowruns() { + m.workflowruns = nil + m.clearedworkflowruns = false + m.removedworkflowruns = nil +} + // AddCasBackendIDs adds the "cas_backends" edge to the CASBackend entity by ids. func (m *OrganizationMutation) AddCasBackendIDs(ids ...uuid.UUID) { if m.cas_backends == nil { @@ -10354,7 +10411,7 @@ func (m *OrganizationMutation) ResetField(name string) error { // AddedEdges returns all edge names that were set/added in this mutation. func (m *OrganizationMutation) AddedEdges() []string { - edges := make([]string, 0, 8) + edges := make([]string, 0, 9) if m.memberships != nil { edges = append(edges, organization.EdgeMemberships) } @@ -10364,6 +10421,9 @@ func (m *OrganizationMutation) AddedEdges() []string { if m.workflows != nil { edges = append(edges, organization.EdgeWorkflows) } + if m.workflowruns != nil { + edges = append(edges, organization.EdgeWorkflowruns) + } if m.cas_backends != nil { edges = append(edges, organization.EdgeCasBackends) } @@ -10404,6 +10464,12 @@ func (m *OrganizationMutation) AddedIDs(name string) []ent.Value { ids = append(ids, id) } return ids + case organization.EdgeWorkflowruns: + ids := make([]ent.Value, 0, len(m.workflowruns)) + for id := range m.workflowruns { + ids = append(ids, id) + } + return ids case organization.EdgeCasBackends: ids := make([]ent.Value, 0, len(m.cas_backends)) for id := range m.cas_backends { @@ -10440,7 +10506,7 @@ func (m *OrganizationMutation) AddedIDs(name string) []ent.Value { // RemovedEdges returns all edge names that were removed in this mutation. func (m *OrganizationMutation) RemovedEdges() []string { - edges := make([]string, 0, 8) + edges := make([]string, 0, 9) if m.removedmemberships != nil { edges = append(edges, organization.EdgeMemberships) } @@ -10450,6 +10516,9 @@ func (m *OrganizationMutation) RemovedEdges() []string { if m.removedworkflows != nil { edges = append(edges, organization.EdgeWorkflows) } + if m.removedworkflowruns != nil { + edges = append(edges, organization.EdgeWorkflowruns) + } if m.removedcas_backends != nil { edges = append(edges, organization.EdgeCasBackends) } @@ -10490,6 +10559,12 @@ func (m *OrganizationMutation) RemovedIDs(name string) []ent.Value { ids = append(ids, id) } return ids + case organization.EdgeWorkflowruns: + ids := make([]ent.Value, 0, len(m.removedworkflowruns)) + for id := range m.removedworkflowruns { + ids = append(ids, id) + } + return ids case organization.EdgeCasBackends: ids := make([]ent.Value, 0, len(m.removedcas_backends)) for id := range m.removedcas_backends { @@ -10526,7 +10601,7 @@ func (m *OrganizationMutation) RemovedIDs(name string) []ent.Value { // ClearedEdges returns all edge names that were cleared in this mutation. func (m *OrganizationMutation) ClearedEdges() []string { - edges := make([]string, 0, 8) + edges := make([]string, 0, 9) if m.clearedmemberships { edges = append(edges, organization.EdgeMemberships) } @@ -10536,6 +10611,9 @@ func (m *OrganizationMutation) ClearedEdges() []string { if m.clearedworkflows { edges = append(edges, organization.EdgeWorkflows) } + if m.clearedworkflowruns { + edges = append(edges, organization.EdgeWorkflowruns) + } if m.clearedcas_backends { edges = append(edges, organization.EdgeCasBackends) } @@ -10564,6 +10642,8 @@ func (m *OrganizationMutation) EdgeCleared(name string) bool { return m.clearedworkflow_contracts case organization.EdgeWorkflows: return m.clearedworkflows + case organization.EdgeWorkflowruns: + return m.clearedworkflowruns case organization.EdgeCasBackends: return m.clearedcas_backends case organization.EdgeIntegrations: @@ -10599,6 +10679,9 @@ func (m *OrganizationMutation) ResetEdge(name string) error { case organization.EdgeWorkflows: m.ResetWorkflows() return nil + case organization.EdgeWorkflowruns: + m.ResetWorkflowruns() + return nil case organization.EdgeCasBackends: m.ResetCasBackends() return nil @@ -18168,6 +18251,8 @@ type WorkflowRunMutation struct { clearedFields map[string]struct{} workflow *uuid.UUID clearedworkflow bool + organization *uuid.UUID + clearedorganization bool contract_version *uuid.UUID clearedcontract_version bool cas_backends map[uuid.UUID]struct{} @@ -18885,6 +18970,42 @@ func (m *WorkflowRunMutation) ResetWorkflowID() { m.workflow = nil } +// SetOrganizationID sets the "organization_id" field. +func (m *WorkflowRunMutation) SetOrganizationID(u uuid.UUID) { + m.organization = &u +} + +// OrganizationID returns the value of the "organization_id" field in the mutation. +func (m *WorkflowRunMutation) OrganizationID() (r uuid.UUID, exists bool) { + v := m.organization + if v == nil { + return + } + return *v, true +} + +// OldOrganizationID returns the old "organization_id" field's value of the WorkflowRun entity. +// If the WorkflowRun object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *WorkflowRunMutation) OldOrganizationID(ctx context.Context) (v uuid.UUID, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldOrganizationID is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldOrganizationID requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldOrganizationID: %w", err) + } + return oldValue.OrganizationID, nil +} + +// ResetOrganizationID resets all changes to the "organization_id" field. +func (m *WorkflowRunMutation) ResetOrganizationID() { + m.organization = nil +} + // SetHasPolicyViolations sets the "has_policy_violations" field. func (m *WorkflowRunMutation) SetHasPolicyViolations(b bool) { m.has_policy_violations = &b @@ -19409,6 +19530,33 @@ func (m *WorkflowRunMutation) ResetWorkflow() { m.clearedworkflow = false } +// ClearOrganization clears the "organization" edge to the Organization entity. +func (m *WorkflowRunMutation) ClearOrganization() { + m.clearedorganization = true + m.clearedFields[workflowrun.FieldOrganizationID] = struct{}{} +} + +// OrganizationCleared reports if the "organization" edge to the Organization entity was cleared. +func (m *WorkflowRunMutation) OrganizationCleared() bool { + return m.clearedorganization +} + +// OrganizationIDs returns the "organization" edge IDs in the mutation. +// Note that IDs always returns len(IDs) <= 1 for unique edges, and you should use +// OrganizationID instead. It exists only for internal usage by the builders. +func (m *WorkflowRunMutation) OrganizationIDs() (ids []uuid.UUID) { + if id := m.organization; id != nil { + ids = append(ids, *id) + } + return +} + +// ResetOrganization resets all changes to the "organization" edge. +func (m *WorkflowRunMutation) ResetOrganization() { + m.organization = nil + m.clearedorganization = false +} + // SetContractVersionID sets the "contract_version" edge to the WorkflowContractVersion entity by id. func (m *WorkflowRunMutation) SetContractVersionID(id uuid.UUID) { m.contract_version = &id @@ -19602,7 +19750,7 @@ func (m *WorkflowRunMutation) Type() string { // order to get all numeric fields that were incremented/decremented, call // AddedFields(). func (m *WorkflowRunMutation) Fields() []string { - fields := make([]string, 0, 21) + fields := make([]string, 0, 22) if m.created_at != nil { fields = append(fields, workflowrun.FieldCreatedAt) } @@ -19642,6 +19790,9 @@ func (m *WorkflowRunMutation) Fields() []string { if m.workflow != nil { fields = append(fields, workflowrun.FieldWorkflowID) } + if m.organization != nil { + fields = append(fields, workflowrun.FieldOrganizationID) + } if m.has_policy_violations != nil { fields = append(fields, workflowrun.FieldHasPolicyViolations) } @@ -19700,6 +19851,8 @@ func (m *WorkflowRunMutation) Field(name string) (ent.Value, bool) { return m.VersionID() case workflowrun.FieldWorkflowID: return m.WorkflowID() + case workflowrun.FieldOrganizationID: + return m.OrganizationID() case workflowrun.FieldHasPolicyViolations: return m.HasPolicyViolations() case workflowrun.FieldPolicyStatus: @@ -19751,6 +19904,8 @@ func (m *WorkflowRunMutation) OldField(ctx context.Context, name string) (ent.Va return m.OldVersionID(ctx) case workflowrun.FieldWorkflowID: return m.OldWorkflowID(ctx) + case workflowrun.FieldOrganizationID: + return m.OldOrganizationID(ctx) case workflowrun.FieldHasPolicyViolations: return m.OldHasPolicyViolations(ctx) case workflowrun.FieldPolicyStatus: @@ -19867,6 +20022,13 @@ func (m *WorkflowRunMutation) SetField(name string, value ent.Value) error { } m.SetWorkflowID(v) return nil + case workflowrun.FieldOrganizationID: + v, ok := value.(uuid.UUID) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetOrganizationID(v) + return nil case workflowrun.FieldHasPolicyViolations: v, ok := value.(bool) if !ok { @@ -20191,6 +20353,9 @@ func (m *WorkflowRunMutation) ResetField(name string) error { case workflowrun.FieldWorkflowID: m.ResetWorkflowID() return nil + case workflowrun.FieldOrganizationID: + m.ResetOrganizationID() + return nil case workflowrun.FieldHasPolicyViolations: m.ResetHasPolicyViolations() return nil @@ -20221,10 +20386,13 @@ func (m *WorkflowRunMutation) ResetField(name string) error { // AddedEdges returns all edge names that were set/added in this mutation. func (m *WorkflowRunMutation) AddedEdges() []string { - edges := make([]string, 0, 5) + edges := make([]string, 0, 6) if m.workflow != nil { edges = append(edges, workflowrun.EdgeWorkflow) } + if m.organization != nil { + edges = append(edges, workflowrun.EdgeOrganization) + } if m.contract_version != nil { edges = append(edges, workflowrun.EdgeContractVersion) } @@ -20248,6 +20416,10 @@ func (m *WorkflowRunMutation) AddedIDs(name string) []ent.Value { if id := m.workflow; id != nil { return []ent.Value{*id} } + case workflowrun.EdgeOrganization: + if id := m.organization; id != nil { + return []ent.Value{*id} + } case workflowrun.EdgeContractVersion: if id := m.contract_version; id != nil { return []ent.Value{*id} @@ -20272,7 +20444,7 @@ func (m *WorkflowRunMutation) AddedIDs(name string) []ent.Value { // RemovedEdges returns all edge names that were removed in this mutation. func (m *WorkflowRunMutation) RemovedEdges() []string { - edges := make([]string, 0, 5) + edges := make([]string, 0, 6) if m.removedcas_backends != nil { edges = append(edges, workflowrun.EdgeCasBackends) } @@ -20295,10 +20467,13 @@ func (m *WorkflowRunMutation) RemovedIDs(name string) []ent.Value { // ClearedEdges returns all edge names that were cleared in this mutation. func (m *WorkflowRunMutation) ClearedEdges() []string { - edges := make([]string, 0, 5) + edges := make([]string, 0, 6) if m.clearedworkflow { edges = append(edges, workflowrun.EdgeWorkflow) } + if m.clearedorganization { + edges = append(edges, workflowrun.EdgeOrganization) + } if m.clearedcontract_version { edges = append(edges, workflowrun.EdgeContractVersion) } @@ -20320,6 +20495,8 @@ func (m *WorkflowRunMutation) EdgeCleared(name string) bool { switch name { case workflowrun.EdgeWorkflow: return m.clearedworkflow + case workflowrun.EdgeOrganization: + return m.clearedorganization case workflowrun.EdgeContractVersion: return m.clearedcontract_version case workflowrun.EdgeCasBackends: @@ -20339,6 +20516,9 @@ func (m *WorkflowRunMutation) ClearEdge(name string) error { case workflowrun.EdgeWorkflow: m.ClearWorkflow() return nil + case workflowrun.EdgeOrganization: + m.ClearOrganization() + return nil case workflowrun.EdgeContractVersion: m.ClearContractVersion() return nil @@ -20359,6 +20539,9 @@ func (m *WorkflowRunMutation) ResetEdge(name string) error { case workflowrun.EdgeWorkflow: m.ResetWorkflow() return nil + case workflowrun.EdgeOrganization: + m.ResetOrganization() + return nil case workflowrun.EdgeContractVersion: m.ResetContractVersion() return nil diff --git a/app/controlplane/pkg/data/ent/organization.go b/app/controlplane/pkg/data/ent/organization.go index d7007a95c..55eb4c642 100644 --- a/app/controlplane/pkg/data/ent/organization.go +++ b/app/controlplane/pkg/data/ent/organization.go @@ -55,6 +55,8 @@ type OrganizationEdges struct { WorkflowContracts []*WorkflowContract `json:"workflow_contracts,omitempty"` // Workflows holds the value of the workflows edge. Workflows []*Workflow `json:"workflows,omitempty"` + // Workflowruns holds the value of the workflowruns edge. + Workflowruns []*WorkflowRun `json:"workflowruns,omitempty"` // CasBackends holds the value of the cas_backends edge. CasBackends []*CASBackend `json:"cas_backends,omitempty"` // Integrations holds the value of the integrations edge. @@ -67,7 +69,7 @@ type OrganizationEdges struct { Groups []*Group `json:"groups,omitempty"` // loadedTypes holds the information for reporting if a // type was loaded (or requested) in eager-loading or not. - loadedTypes [8]bool + loadedTypes [9]bool } // MembershipsOrErr returns the Memberships value or an error if the edge @@ -97,10 +99,19 @@ func (e OrganizationEdges) WorkflowsOrErr() ([]*Workflow, error) { return nil, &NotLoadedError{edge: "workflows"} } +// WorkflowrunsOrErr returns the Workflowruns value or an error if the edge +// was not loaded in eager-loading. +func (e OrganizationEdges) WorkflowrunsOrErr() ([]*WorkflowRun, error) { + if e.loadedTypes[3] { + return e.Workflowruns, nil + } + return nil, &NotLoadedError{edge: "workflowruns"} +} + // CasBackendsOrErr returns the CasBackends value or an error if the edge // was not loaded in eager-loading. func (e OrganizationEdges) CasBackendsOrErr() ([]*CASBackend, error) { - if e.loadedTypes[3] { + if e.loadedTypes[4] { return e.CasBackends, nil } return nil, &NotLoadedError{edge: "cas_backends"} @@ -109,7 +120,7 @@ func (e OrganizationEdges) CasBackendsOrErr() ([]*CASBackend, error) { // IntegrationsOrErr returns the Integrations value or an error if the edge // was not loaded in eager-loading. func (e OrganizationEdges) IntegrationsOrErr() ([]*Integration, error) { - if e.loadedTypes[4] { + if e.loadedTypes[5] { return e.Integrations, nil } return nil, &NotLoadedError{edge: "integrations"} @@ -118,7 +129,7 @@ func (e OrganizationEdges) IntegrationsOrErr() ([]*Integration, error) { // APITokensOrErr returns the APITokens value or an error if the edge // was not loaded in eager-loading. func (e OrganizationEdges) APITokensOrErr() ([]*APIToken, error) { - if e.loadedTypes[5] { + if e.loadedTypes[6] { return e.APITokens, nil } return nil, &NotLoadedError{edge: "api_tokens"} @@ -127,7 +138,7 @@ func (e OrganizationEdges) APITokensOrErr() ([]*APIToken, error) { // ProjectsOrErr returns the Projects value or an error if the edge // was not loaded in eager-loading. func (e OrganizationEdges) ProjectsOrErr() ([]*Project, error) { - if e.loadedTypes[6] { + if e.loadedTypes[7] { return e.Projects, nil } return nil, &NotLoadedError{edge: "projects"} @@ -136,7 +147,7 @@ func (e OrganizationEdges) ProjectsOrErr() ([]*Project, error) { // GroupsOrErr returns the Groups value or an error if the edge // was not loaded in eager-loading. func (e OrganizationEdges) GroupsOrErr() ([]*Group, error) { - if e.loadedTypes[7] { + if e.loadedTypes[8] { return e.Groups, nil } return nil, &NotLoadedError{edge: "groups"} @@ -277,6 +288,11 @@ func (_m *Organization) QueryWorkflows() *WorkflowQuery { return NewOrganizationClient(_m.config).QueryWorkflows(_m) } +// QueryWorkflowruns queries the "workflowruns" edge of the Organization entity. +func (_m *Organization) QueryWorkflowruns() *WorkflowRunQuery { + return NewOrganizationClient(_m.config).QueryWorkflowruns(_m) +} + // QueryCasBackends queries the "cas_backends" edge of the Organization entity. func (_m *Organization) QueryCasBackends() *CASBackendQuery { return NewOrganizationClient(_m.config).QueryCasBackends(_m) diff --git a/app/controlplane/pkg/data/ent/organization/organization.go b/app/controlplane/pkg/data/ent/organization/organization.go index 310bf1a49..386d48179 100644 --- a/app/controlplane/pkg/data/ent/organization/organization.go +++ b/app/controlplane/pkg/data/ent/organization/organization.go @@ -43,6 +43,8 @@ const ( EdgeWorkflowContracts = "workflow_contracts" // EdgeWorkflows holds the string denoting the workflows edge name in mutations. EdgeWorkflows = "workflows" + // EdgeWorkflowruns holds the string denoting the workflowruns edge name in mutations. + EdgeWorkflowruns = "workflowruns" // EdgeCasBackends holds the string denoting the cas_backends edge name in mutations. EdgeCasBackends = "cas_backends" // EdgeIntegrations holds the string denoting the integrations edge name in mutations. @@ -76,6 +78,13 @@ const ( WorkflowsInverseTable = "workflows" // WorkflowsColumn is the table column denoting the workflows relation/edge. WorkflowsColumn = "organization_id" + // WorkflowrunsTable is the table that holds the workflowruns relation/edge. + WorkflowrunsTable = "workflow_runs" + // WorkflowrunsInverseTable is the table name for the WorkflowRun entity. + // It exists in this package in order to avoid circular dependency with the "workflowrun" package. + WorkflowrunsInverseTable = "workflow_runs" + // WorkflowrunsColumn is the table column denoting the workflowruns relation/edge. + WorkflowrunsColumn = "organization_id" // CasBackendsTable is the table that holds the cas_backends relation/edge. CasBackendsTable = "cas_backends" // CasBackendsInverseTable is the table name for the CASBackend entity. @@ -258,6 +267,20 @@ func ByWorkflows(term sql.OrderTerm, terms ...sql.OrderTerm) OrderOption { } } +// ByWorkflowrunsCount orders the results by workflowruns count. +func ByWorkflowrunsCount(opts ...sql.OrderTermOption) OrderOption { + return func(s *sql.Selector) { + sqlgraph.OrderByNeighborsCount(s, newWorkflowrunsStep(), opts...) + } +} + +// ByWorkflowruns orders the results by workflowruns terms. +func ByWorkflowruns(term sql.OrderTerm, terms ...sql.OrderTerm) OrderOption { + return func(s *sql.Selector) { + sqlgraph.OrderByNeighborTerms(s, newWorkflowrunsStep(), append([]sql.OrderTerm{term}, terms...)...) + } +} + // ByCasBackendsCount orders the results by cas_backends count. func ByCasBackendsCount(opts ...sql.OrderTermOption) OrderOption { return func(s *sql.Selector) { @@ -348,6 +371,13 @@ func newWorkflowsStep() *sqlgraph.Step { sqlgraph.Edge(sqlgraph.O2M, false, WorkflowsTable, WorkflowsColumn), ) } +func newWorkflowrunsStep() *sqlgraph.Step { + return sqlgraph.NewStep( + sqlgraph.From(Table, FieldID), + sqlgraph.To(WorkflowrunsInverseTable, FieldID), + sqlgraph.Edge(sqlgraph.O2M, false, WorkflowrunsTable, WorkflowrunsColumn), + ) +} func newCasBackendsStep() *sqlgraph.Step { return sqlgraph.NewStep( sqlgraph.From(Table, FieldID), diff --git a/app/controlplane/pkg/data/ent/organization/where.go b/app/controlplane/pkg/data/ent/organization/where.go index c61ac829f..92d139d1e 100644 --- a/app/controlplane/pkg/data/ent/organization/where.go +++ b/app/controlplane/pkg/data/ent/organization/where.go @@ -480,6 +480,29 @@ func HasWorkflowsWith(preds ...predicate.Workflow) predicate.Organization { }) } +// HasWorkflowruns applies the HasEdge predicate on the "workflowruns" edge. +func HasWorkflowruns() predicate.Organization { + return predicate.Organization(func(s *sql.Selector) { + step := sqlgraph.NewStep( + sqlgraph.From(Table, FieldID), + sqlgraph.Edge(sqlgraph.O2M, false, WorkflowrunsTable, WorkflowrunsColumn), + ) + sqlgraph.HasNeighbors(s, step) + }) +} + +// HasWorkflowrunsWith applies the HasEdge predicate on the "workflowruns" edge with a given conditions (other predicates). +func HasWorkflowrunsWith(preds ...predicate.WorkflowRun) predicate.Organization { + return predicate.Organization(func(s *sql.Selector) { + step := newWorkflowrunsStep() + sqlgraph.HasNeighborsWith(s, step, func(s *sql.Selector) { + for _, p := range preds { + p(s) + } + }) + }) +} + // HasCasBackends applies the HasEdge predicate on the "cas_backends" edge. func HasCasBackends() predicate.Organization { return predicate.Organization(func(s *sql.Selector) { diff --git a/app/controlplane/pkg/data/ent/organization_create.go b/app/controlplane/pkg/data/ent/organization_create.go index b6770887b..492d312e3 100644 --- a/app/controlplane/pkg/data/ent/organization_create.go +++ b/app/controlplane/pkg/data/ent/organization_create.go @@ -21,6 +21,7 @@ import ( "github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent/project" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent/workflow" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent/workflowcontract" + "github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent/workflowrun" "github.com/google/uuid" ) @@ -229,6 +230,21 @@ func (_c *OrganizationCreate) AddWorkflows(v ...*Workflow) *OrganizationCreate { return _c.AddWorkflowIDs(ids...) } +// AddWorkflowrunIDs adds the "workflowruns" edge to the WorkflowRun entity by IDs. +func (_c *OrganizationCreate) AddWorkflowrunIDs(ids ...uuid.UUID) *OrganizationCreate { + _c.mutation.AddWorkflowrunIDs(ids...) + return _c +} + +// AddWorkflowruns adds the "workflowruns" edges to the WorkflowRun entity. +func (_c *OrganizationCreate) AddWorkflowruns(v ...*WorkflowRun) *OrganizationCreate { + ids := make([]uuid.UUID, len(v)) + for i := range v { + ids[i] = v[i].ID + } + return _c.AddWorkflowrunIDs(ids...) +} + // AddCasBackendIDs adds the "cas_backends" edge to the CASBackend entity by IDs. func (_c *OrganizationCreate) AddCasBackendIDs(ids ...uuid.UUID) *OrganizationCreate { _c.mutation.AddCasBackendIDs(ids...) @@ -527,6 +543,22 @@ func (_c *OrganizationCreate) createSpec() (*Organization, *sqlgraph.CreateSpec) } _spec.Edges = append(_spec.Edges, edge) } + if nodes := _c.mutation.WorkflowrunsIDs(); len(nodes) > 0 { + edge := &sqlgraph.EdgeSpec{ + Rel: sqlgraph.O2M, + Inverse: false, + Table: organization.WorkflowrunsTable, + Columns: []string{organization.WorkflowrunsColumn}, + Bidi: false, + Target: &sqlgraph.EdgeTarget{ + IDSpec: sqlgraph.NewFieldSpec(workflowrun.FieldID, field.TypeUUID), + }, + } + for _, k := range nodes { + edge.Target.Nodes = append(edge.Target.Nodes, k) + } + _spec.Edges = append(_spec.Edges, edge) + } if nodes := _c.mutation.CasBackendsIDs(); len(nodes) > 0 { edge := &sqlgraph.EdgeSpec{ Rel: sqlgraph.O2M, diff --git a/app/controlplane/pkg/data/ent/organization_query.go b/app/controlplane/pkg/data/ent/organization_query.go index b879ca750..93b3479fc 100644 --- a/app/controlplane/pkg/data/ent/organization_query.go +++ b/app/controlplane/pkg/data/ent/organization_query.go @@ -23,6 +23,7 @@ import ( "github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent/project" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent/workflow" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent/workflowcontract" + "github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent/workflowrun" "github.com/google/uuid" ) @@ -36,6 +37,7 @@ type OrganizationQuery struct { withMemberships *MembershipQuery withWorkflowContracts *WorkflowContractQuery withWorkflows *WorkflowQuery + withWorkflowruns *WorkflowRunQuery withCasBackends *CASBackendQuery withIntegrations *IntegrationQuery withAPITokens *APITokenQuery @@ -144,6 +146,28 @@ func (_q *OrganizationQuery) QueryWorkflows() *WorkflowQuery { return query } +// QueryWorkflowruns chains the current query on the "workflowruns" edge. +func (_q *OrganizationQuery) QueryWorkflowruns() *WorkflowRunQuery { + query := (&WorkflowRunClient{config: _q.config}).Query() + query.path = func(ctx context.Context) (fromU *sql.Selector, err error) { + if err := _q.prepareQuery(ctx); err != nil { + return nil, err + } + selector := _q.sqlQuery(ctx) + if err := selector.Err(); err != nil { + return nil, err + } + step := sqlgraph.NewStep( + sqlgraph.From(organization.Table, organization.FieldID, selector), + sqlgraph.To(workflowrun.Table, workflowrun.FieldID), + sqlgraph.Edge(sqlgraph.O2M, false, organization.WorkflowrunsTable, organization.WorkflowrunsColumn), + ) + fromU = sqlgraph.SetNeighbors(_q.driver.Dialect(), step) + return fromU, nil + } + return query +} + // QueryCasBackends chains the current query on the "cas_backends" edge. func (_q *OrganizationQuery) QueryCasBackends() *CASBackendQuery { query := (&CASBackendClient{config: _q.config}).Query() @@ -449,6 +473,7 @@ func (_q *OrganizationQuery) Clone() *OrganizationQuery { withMemberships: _q.withMemberships.Clone(), withWorkflowContracts: _q.withWorkflowContracts.Clone(), withWorkflows: _q.withWorkflows.Clone(), + withWorkflowruns: _q.withWorkflowruns.Clone(), withCasBackends: _q.withCasBackends.Clone(), withIntegrations: _q.withIntegrations.Clone(), withAPITokens: _q.withAPITokens.Clone(), @@ -494,6 +519,17 @@ func (_q *OrganizationQuery) WithWorkflows(opts ...func(*WorkflowQuery)) *Organi return _q } +// WithWorkflowruns tells the query-builder to eager-load the nodes that are connected to +// the "workflowruns" edge. The optional arguments are used to configure the query builder of the edge. +func (_q *OrganizationQuery) WithWorkflowruns(opts ...func(*WorkflowRunQuery)) *OrganizationQuery { + query := (&WorkflowRunClient{config: _q.config}).Query() + for _, opt := range opts { + opt(query) + } + _q.withWorkflowruns = query + return _q +} + // WithCasBackends tells the query-builder to eager-load the nodes that are connected to // the "cas_backends" edge. The optional arguments are used to configure the query builder of the edge. func (_q *OrganizationQuery) WithCasBackends(opts ...func(*CASBackendQuery)) *OrganizationQuery { @@ -627,10 +663,11 @@ func (_q *OrganizationQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([] var ( nodes = []*Organization{} _spec = _q.querySpec() - loadedTypes = [8]bool{ + loadedTypes = [9]bool{ _q.withMemberships != nil, _q.withWorkflowContracts != nil, _q.withWorkflows != nil, + _q.withWorkflowruns != nil, _q.withCasBackends != nil, _q.withIntegrations != nil, _q.withAPITokens != nil, @@ -682,6 +719,13 @@ func (_q *OrganizationQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([] return nil, err } } + if query := _q.withWorkflowruns; query != nil { + if err := _q.loadWorkflowruns(ctx, query, nodes, + func(n *Organization) { n.Edges.Workflowruns = []*WorkflowRun{} }, + func(n *Organization, e *WorkflowRun) { n.Edges.Workflowruns = append(n.Edges.Workflowruns, e) }); err != nil { + return nil, err + } + } if query := _q.withCasBackends; query != nil { if err := _q.loadCasBackends(ctx, query, nodes, func(n *Organization) { n.Edges.CasBackends = []*CASBackend{} }, @@ -813,6 +857,37 @@ func (_q *OrganizationQuery) loadWorkflows(ctx context.Context, query *WorkflowQ } return nil } +func (_q *OrganizationQuery) loadWorkflowruns(ctx context.Context, query *WorkflowRunQuery, nodes []*Organization, init func(*Organization), assign func(*Organization, *WorkflowRun)) error { + fks := make([]driver.Value, 0, len(nodes)) + nodeids := make(map[uuid.UUID]*Organization) + for i := range nodes { + fks = append(fks, nodes[i].ID) + nodeids[nodes[i].ID] = nodes[i] + if init != nil { + init(nodes[i]) + } + } + query.withFKs = true + if len(query.ctx.Fields) > 0 { + query.ctx.AppendFieldOnce(workflowrun.FieldOrganizationID) + } + query.Where(predicate.WorkflowRun(func(s *sql.Selector) { + s.Where(sql.InValues(s.C(organization.WorkflowrunsColumn), fks...)) + })) + neighbors, err := query.All(ctx) + if err != nil { + return err + } + for _, n := range neighbors { + fk := n.OrganizationID + node, ok := nodeids[fk] + if !ok { + return fmt.Errorf(`unexpected referenced foreign-key "organization_id" returned %v for node %v`, fk, n.ID) + } + assign(node, n) + } + return nil +} func (_q *OrganizationQuery) loadCasBackends(ctx context.Context, query *CASBackendQuery, nodes []*Organization, init func(*Organization), assign func(*Organization, *CASBackend)) error { fks := make([]driver.Value, 0, len(nodes)) nodeids := make(map[uuid.UUID]*Organization) diff --git a/app/controlplane/pkg/data/ent/organization_update.go b/app/controlplane/pkg/data/ent/organization_update.go index 44fc3b02b..d17b05860 100644 --- a/app/controlplane/pkg/data/ent/organization_update.go +++ b/app/controlplane/pkg/data/ent/organization_update.go @@ -22,6 +22,7 @@ import ( "github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent/project" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent/workflow" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent/workflowcontract" + "github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent/workflowrun" "github.com/google/uuid" ) @@ -247,6 +248,21 @@ func (_u *OrganizationUpdate) AddWorkflows(v ...*Workflow) *OrganizationUpdate { return _u.AddWorkflowIDs(ids...) } +// AddWorkflowrunIDs adds the "workflowruns" edge to the WorkflowRun entity by IDs. +func (_u *OrganizationUpdate) AddWorkflowrunIDs(ids ...uuid.UUID) *OrganizationUpdate { + _u.mutation.AddWorkflowrunIDs(ids...) + return _u +} + +// AddWorkflowruns adds the "workflowruns" edges to the WorkflowRun entity. +func (_u *OrganizationUpdate) AddWorkflowruns(v ...*WorkflowRun) *OrganizationUpdate { + ids := make([]uuid.UUID, len(v)) + for i := range v { + ids[i] = v[i].ID + } + return _u.AddWorkflowrunIDs(ids...) +} + // AddCasBackendIDs adds the "cas_backends" edge to the CASBackend entity by IDs. func (_u *OrganizationUpdate) AddCasBackendIDs(ids ...uuid.UUID) *OrganizationUpdate { _u.mutation.AddCasBackendIDs(ids...) @@ -390,6 +406,27 @@ func (_u *OrganizationUpdate) RemoveWorkflows(v ...*Workflow) *OrganizationUpdat return _u.RemoveWorkflowIDs(ids...) } +// ClearWorkflowruns clears all "workflowruns" edges to the WorkflowRun entity. +func (_u *OrganizationUpdate) ClearWorkflowruns() *OrganizationUpdate { + _u.mutation.ClearWorkflowruns() + return _u +} + +// RemoveWorkflowrunIDs removes the "workflowruns" edge to WorkflowRun entities by IDs. +func (_u *OrganizationUpdate) RemoveWorkflowrunIDs(ids ...uuid.UUID) *OrganizationUpdate { + _u.mutation.RemoveWorkflowrunIDs(ids...) + return _u +} + +// RemoveWorkflowruns removes "workflowruns" edges to WorkflowRun entities. +func (_u *OrganizationUpdate) RemoveWorkflowruns(v ...*WorkflowRun) *OrganizationUpdate { + ids := make([]uuid.UUID, len(v)) + for i := range v { + ids[i] = v[i].ID + } + return _u.RemoveWorkflowrunIDs(ids...) +} + // ClearCasBackends clears all "cas_backends" edges to the CASBackend entity. func (_u *OrganizationUpdate) ClearCasBackends() *OrganizationUpdate { _u.mutation.ClearCasBackends() @@ -719,6 +756,51 @@ func (_u *OrganizationUpdate) sqlSave(ctx context.Context) (_node int, err error } _spec.Edges.Add = append(_spec.Edges.Add, edge) } + if _u.mutation.WorkflowrunsCleared() { + edge := &sqlgraph.EdgeSpec{ + Rel: sqlgraph.O2M, + Inverse: false, + Table: organization.WorkflowrunsTable, + Columns: []string{organization.WorkflowrunsColumn}, + Bidi: false, + Target: &sqlgraph.EdgeTarget{ + IDSpec: sqlgraph.NewFieldSpec(workflowrun.FieldID, field.TypeUUID), + }, + } + _spec.Edges.Clear = append(_spec.Edges.Clear, edge) + } + if nodes := _u.mutation.RemovedWorkflowrunsIDs(); len(nodes) > 0 && !_u.mutation.WorkflowrunsCleared() { + edge := &sqlgraph.EdgeSpec{ + Rel: sqlgraph.O2M, + Inverse: false, + Table: organization.WorkflowrunsTable, + Columns: []string{organization.WorkflowrunsColumn}, + Bidi: false, + Target: &sqlgraph.EdgeTarget{ + IDSpec: sqlgraph.NewFieldSpec(workflowrun.FieldID, field.TypeUUID), + }, + } + for _, k := range nodes { + edge.Target.Nodes = append(edge.Target.Nodes, k) + } + _spec.Edges.Clear = append(_spec.Edges.Clear, edge) + } + if nodes := _u.mutation.WorkflowrunsIDs(); len(nodes) > 0 { + edge := &sqlgraph.EdgeSpec{ + Rel: sqlgraph.O2M, + Inverse: false, + Table: organization.WorkflowrunsTable, + Columns: []string{organization.WorkflowrunsColumn}, + Bidi: false, + Target: &sqlgraph.EdgeTarget{ + IDSpec: sqlgraph.NewFieldSpec(workflowrun.FieldID, field.TypeUUID), + }, + } + for _, k := range nodes { + edge.Target.Nodes = append(edge.Target.Nodes, k) + } + _spec.Edges.Add = append(_spec.Edges.Add, edge) + } if _u.mutation.CasBackendsCleared() { edge := &sqlgraph.EdgeSpec{ Rel: sqlgraph.O2M, @@ -1174,6 +1256,21 @@ func (_u *OrganizationUpdateOne) AddWorkflows(v ...*Workflow) *OrganizationUpdat return _u.AddWorkflowIDs(ids...) } +// AddWorkflowrunIDs adds the "workflowruns" edge to the WorkflowRun entity by IDs. +func (_u *OrganizationUpdateOne) AddWorkflowrunIDs(ids ...uuid.UUID) *OrganizationUpdateOne { + _u.mutation.AddWorkflowrunIDs(ids...) + return _u +} + +// AddWorkflowruns adds the "workflowruns" edges to the WorkflowRun entity. +func (_u *OrganizationUpdateOne) AddWorkflowruns(v ...*WorkflowRun) *OrganizationUpdateOne { + ids := make([]uuid.UUID, len(v)) + for i := range v { + ids[i] = v[i].ID + } + return _u.AddWorkflowrunIDs(ids...) +} + // AddCasBackendIDs adds the "cas_backends" edge to the CASBackend entity by IDs. func (_u *OrganizationUpdateOne) AddCasBackendIDs(ids ...uuid.UUID) *OrganizationUpdateOne { _u.mutation.AddCasBackendIDs(ids...) @@ -1317,6 +1414,27 @@ func (_u *OrganizationUpdateOne) RemoveWorkflows(v ...*Workflow) *OrganizationUp return _u.RemoveWorkflowIDs(ids...) } +// ClearWorkflowruns clears all "workflowruns" edges to the WorkflowRun entity. +func (_u *OrganizationUpdateOne) ClearWorkflowruns() *OrganizationUpdateOne { + _u.mutation.ClearWorkflowruns() + return _u +} + +// RemoveWorkflowrunIDs removes the "workflowruns" edge to WorkflowRun entities by IDs. +func (_u *OrganizationUpdateOne) RemoveWorkflowrunIDs(ids ...uuid.UUID) *OrganizationUpdateOne { + _u.mutation.RemoveWorkflowrunIDs(ids...) + return _u +} + +// RemoveWorkflowruns removes "workflowruns" edges to WorkflowRun entities. +func (_u *OrganizationUpdateOne) RemoveWorkflowruns(v ...*WorkflowRun) *OrganizationUpdateOne { + ids := make([]uuid.UUID, len(v)) + for i := range v { + ids[i] = v[i].ID + } + return _u.RemoveWorkflowrunIDs(ids...) +} + // ClearCasBackends clears all "cas_backends" edges to the CASBackend entity. func (_u *OrganizationUpdateOne) ClearCasBackends() *OrganizationUpdateOne { _u.mutation.ClearCasBackends() @@ -1676,6 +1794,51 @@ func (_u *OrganizationUpdateOne) sqlSave(ctx context.Context) (_node *Organizati } _spec.Edges.Add = append(_spec.Edges.Add, edge) } + if _u.mutation.WorkflowrunsCleared() { + edge := &sqlgraph.EdgeSpec{ + Rel: sqlgraph.O2M, + Inverse: false, + Table: organization.WorkflowrunsTable, + Columns: []string{organization.WorkflowrunsColumn}, + Bidi: false, + Target: &sqlgraph.EdgeTarget{ + IDSpec: sqlgraph.NewFieldSpec(workflowrun.FieldID, field.TypeUUID), + }, + } + _spec.Edges.Clear = append(_spec.Edges.Clear, edge) + } + if nodes := _u.mutation.RemovedWorkflowrunsIDs(); len(nodes) > 0 && !_u.mutation.WorkflowrunsCleared() { + edge := &sqlgraph.EdgeSpec{ + Rel: sqlgraph.O2M, + Inverse: false, + Table: organization.WorkflowrunsTable, + Columns: []string{organization.WorkflowrunsColumn}, + Bidi: false, + Target: &sqlgraph.EdgeTarget{ + IDSpec: sqlgraph.NewFieldSpec(workflowrun.FieldID, field.TypeUUID), + }, + } + for _, k := range nodes { + edge.Target.Nodes = append(edge.Target.Nodes, k) + } + _spec.Edges.Clear = append(_spec.Edges.Clear, edge) + } + if nodes := _u.mutation.WorkflowrunsIDs(); len(nodes) > 0 { + edge := &sqlgraph.EdgeSpec{ + Rel: sqlgraph.O2M, + Inverse: false, + Table: organization.WorkflowrunsTable, + Columns: []string{organization.WorkflowrunsColumn}, + Bidi: false, + Target: &sqlgraph.EdgeTarget{ + IDSpec: sqlgraph.NewFieldSpec(workflowrun.FieldID, field.TypeUUID), + }, + } + for _, k := range nodes { + edge.Target.Nodes = append(edge.Target.Nodes, k) + } + _spec.Edges.Add = append(_spec.Edges.Add, edge) + } if _u.mutation.CasBackendsCleared() { edge := &sqlgraph.EdgeSpec{ Rel: sqlgraph.O2M, diff --git a/app/controlplane/pkg/data/ent/schema/organization.go b/app/controlplane/pkg/data/ent/schema/organization.go index 78ff0a6d9..b91b13d50 100644 --- a/app/controlplane/pkg/data/ent/schema/organization.go +++ b/app/controlplane/pkg/data/ent/schema/organization.go @@ -72,6 +72,7 @@ func (Organization) Edges() []ent.Edge { edge.To("memberships", Membership.Type).Annotations(entsql.Annotation{OnDelete: entsql.Cascade}), edge.To("workflow_contracts", WorkflowContract.Type).Annotations(entsql.Annotation{OnDelete: entsql.Cascade}), edge.To("workflows", Workflow.Type).Annotations(entsql.Annotation{OnDelete: entsql.Cascade}), + edge.To("workflowruns", WorkflowRun.Type).Annotations(entsql.Annotation{OnDelete: entsql.Cascade}), edge.To("cas_backends", CASBackend.Type).Annotations(entsql.Annotation{OnDelete: entsql.Cascade}), edge.To("integrations", Integration.Type).Annotations(entsql.Annotation{OnDelete: entsql.Cascade}), edge.To("api_tokens", APIToken.Type).Annotations(entsql.Annotation{OnDelete: entsql.Cascade}), diff --git a/app/controlplane/pkg/data/ent/schema/workflowrun.go b/app/controlplane/pkg/data/ent/schema/workflowrun.go index 65ce8a477..98bbac6ea 100644 --- a/app/controlplane/pkg/data/ent/schema/workflowrun.go +++ b/app/controlplane/pkg/data/ent/schema/workflowrun.go @@ -59,6 +59,13 @@ func (WorkflowRun) Fields() []ent.Field { // We have runs without data field.UUID("version_id", uuid.UUID{}), field.UUID("workflow_id", uuid.UUID{}).Immutable(), + // Denormalized organization scope to make org-level list/aggregate queries + // sargable without joining workflows. Immutable: a workflow run never + // changes orgs (the parent workflow doesn't either). The migration + // installs a BEFORE INSERT trigger that fills this from workflow_id so + // the rolling-deploy window is safe; a second migration in the same + // release drops the trigger once schema is enforced. + field.UUID("organization_id", uuid.UUID{}).Immutable(), // Whether the run has policy violations (nullable for backward compatibility) field.Bool("has_policy_violations").Optional().Nillable(), // Canonical policy status summary — all fields nullable so pre-existing @@ -82,6 +89,9 @@ func (WorkflowRun) Edges() []ent.Edge { edge.From("workflow", Workflow.Type).Field("workflow_id"). Ref("workflowruns").Required().Immutable(). Unique(), + edge.From("organization", Organization.Type).Field("organization_id"). + Ref("workflowruns").Required().Immutable(). + Unique(), edge.To("contract_version", WorkflowContractVersion.Type).Unique().Annotations(entsql.Annotation{OnDelete: entsql.Cascade}), // A WorkflowRun can have multiple CASBackends associated to it edge.To("cas_backends", CASBackend.Type), @@ -97,6 +107,9 @@ func (WorkflowRun) Indexes() []ent.Index { index.Fields("created_at").Annotations(entsql.DescColumns("created_at")), index.Fields("workflow_id", "created_at").Annotations(entsql.DescColumns("created_at")), index.Fields("workflow_id", "state", "created_at").Annotations(entsql.DescColumns("created_at")), + // Org-scoped list (used by WorkflowRunRepo.List). Replaces the planner's + // EXISTS-vs-JOIN ambiguity with a single sargable range scan. + index.Fields("organization_id", "created_at").Annotations(entsql.DescColumns("created_at")), // Expiration job index.Fields("state", "created_at"), // search and order by finish date diff --git a/app/controlplane/pkg/data/ent/workflowrun.go b/app/controlplane/pkg/data/ent/workflowrun.go index 8d64bc649..e67a2a273 100644 --- a/app/controlplane/pkg/data/ent/workflowrun.go +++ b/app/controlplane/pkg/data/ent/workflowrun.go @@ -12,6 +12,7 @@ import ( "entgo.io/ent/dialect/sql" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/biz" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent/attestation" + "github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent/organization" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent/projectversion" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent/workflow" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent/workflowcontractversion" @@ -51,6 +52,8 @@ type WorkflowRun struct { VersionID uuid.UUID `json:"version_id,omitempty"` // WorkflowID holds the value of the "workflow_id" field. WorkflowID uuid.UUID `json:"workflow_id,omitempty"` + // OrganizationID holds the value of the "organization_id" field. + OrganizationID uuid.UUID `json:"organization_id,omitempty"` // HasPolicyViolations holds the value of the "has_policy_violations" field. HasPolicyViolations *bool `json:"has_policy_violations,omitempty"` // PolicyStatus holds the value of the "policy_status" field. @@ -78,6 +81,8 @@ type WorkflowRun struct { type WorkflowRunEdges struct { // Workflow holds the value of the workflow edge. Workflow *Workflow `json:"workflow,omitempty"` + // Organization holds the value of the organization edge. + Organization *Organization `json:"organization,omitempty"` // ContractVersion holds the value of the contract_version edge. ContractVersion *WorkflowContractVersion `json:"contract_version,omitempty"` // CasBackends holds the value of the cas_backends edge. @@ -88,7 +93,7 @@ type WorkflowRunEdges struct { AttestationBundle *Attestation `json:"attestation_bundle,omitempty"` // loadedTypes holds the information for reporting if a // type was loaded (or requested) in eager-loading or not. - loadedTypes [5]bool + loadedTypes [6]bool } // WorkflowOrErr returns the Workflow value or an error if the edge @@ -102,12 +107,23 @@ func (e WorkflowRunEdges) WorkflowOrErr() (*Workflow, error) { return nil, &NotLoadedError{edge: "workflow"} } +// OrganizationOrErr returns the Organization value or an error if the edge +// was not loaded in eager-loading, or loaded but was not found. +func (e WorkflowRunEdges) OrganizationOrErr() (*Organization, error) { + if e.Organization != nil { + return e.Organization, nil + } else if e.loadedTypes[1] { + return nil, &NotFoundError{label: organization.Label} + } + return nil, &NotLoadedError{edge: "organization"} +} + // ContractVersionOrErr returns the ContractVersion value or an error if the edge // was not loaded in eager-loading, or loaded but was not found. func (e WorkflowRunEdges) ContractVersionOrErr() (*WorkflowContractVersion, error) { if e.ContractVersion != nil { return e.ContractVersion, nil - } else if e.loadedTypes[1] { + } else if e.loadedTypes[2] { return nil, &NotFoundError{label: workflowcontractversion.Label} } return nil, &NotLoadedError{edge: "contract_version"} @@ -116,7 +132,7 @@ func (e WorkflowRunEdges) ContractVersionOrErr() (*WorkflowContractVersion, erro // CasBackendsOrErr returns the CasBackends value or an error if the edge // was not loaded in eager-loading. func (e WorkflowRunEdges) CasBackendsOrErr() ([]*CASBackend, error) { - if e.loadedTypes[2] { + if e.loadedTypes[3] { return e.CasBackends, nil } return nil, &NotLoadedError{edge: "cas_backends"} @@ -127,7 +143,7 @@ func (e WorkflowRunEdges) CasBackendsOrErr() ([]*CASBackend, error) { func (e WorkflowRunEdges) VersionOrErr() (*ProjectVersion, error) { if e.Version != nil { return e.Version, nil - } else if e.loadedTypes[3] { + } else if e.loadedTypes[4] { return nil, &NotFoundError{label: projectversion.Label} } return nil, &NotLoadedError{edge: "version"} @@ -138,7 +154,7 @@ func (e WorkflowRunEdges) VersionOrErr() (*ProjectVersion, error) { func (e WorkflowRunEdges) AttestationBundleOrErr() (*Attestation, error) { if e.AttestationBundle != nil { return e.AttestationBundle, nil - } else if e.loadedTypes[4] { + } else if e.loadedTypes[5] { return nil, &NotFoundError{label: attestation.Label} } return nil, &NotLoadedError{edge: "attestation_bundle"} @@ -159,7 +175,7 @@ func (*WorkflowRun) scanValues(columns []string) ([]any, error) { values[i] = new(sql.NullString) case workflowrun.FieldCreatedAt, workflowrun.FieldFinishedAt: values[i] = new(sql.NullTime) - case workflowrun.FieldID, workflowrun.FieldVersionID, workflowrun.FieldWorkflowID: + case workflowrun.FieldID, workflowrun.FieldVersionID, workflowrun.FieldWorkflowID, workflowrun.FieldOrganizationID: values[i] = new(uuid.UUID) case workflowrun.ForeignKeys[0]: // workflow_run_contract_version values[i] = &sql.NullScanner{S: new(uuid.UUID)} @@ -264,6 +280,12 @@ func (_m *WorkflowRun) assignValues(columns []string, values []any) error { } else if value != nil { _m.WorkflowID = *value } + case workflowrun.FieldOrganizationID: + if value, ok := values[i].(*uuid.UUID); !ok { + return fmt.Errorf("unexpected type %T for field organization_id", values[i]) + } else if value != nil { + _m.OrganizationID = *value + } case workflowrun.FieldHasPolicyViolations: if value, ok := values[i].(*sql.NullBool); !ok { return fmt.Errorf("unexpected type %T for field has_policy_violations", values[i]) @@ -345,6 +367,11 @@ func (_m *WorkflowRun) QueryWorkflow() *WorkflowQuery { return NewWorkflowRunClient(_m.config).QueryWorkflow(_m) } +// QueryOrganization queries the "organization" edge of the WorkflowRun entity. +func (_m *WorkflowRun) QueryOrganization() *OrganizationQuery { + return NewWorkflowRunClient(_m.config).QueryOrganization(_m) +} + // QueryContractVersion queries the "contract_version" edge of the WorkflowRun entity. func (_m *WorkflowRun) QueryContractVersion() *WorkflowContractVersionQuery { return NewWorkflowRunClient(_m.config).QueryContractVersion(_m) @@ -427,6 +454,9 @@ func (_m *WorkflowRun) String() string { builder.WriteString("workflow_id=") builder.WriteString(fmt.Sprintf("%v", _m.WorkflowID)) builder.WriteString(", ") + builder.WriteString("organization_id=") + builder.WriteString(fmt.Sprintf("%v", _m.OrganizationID)) + builder.WriteString(", ") if v := _m.HasPolicyViolations; v != nil { builder.WriteString("has_policy_violations=") builder.WriteString(fmt.Sprintf("%v", *v)) diff --git a/app/controlplane/pkg/data/ent/workflowrun/where.go b/app/controlplane/pkg/data/ent/workflowrun/where.go index 52ae8cd2d..0fc4add6c 100644 --- a/app/controlplane/pkg/data/ent/workflowrun/where.go +++ b/app/controlplane/pkg/data/ent/workflowrun/where.go @@ -112,6 +112,11 @@ func WorkflowID(v uuid.UUID) predicate.WorkflowRun { return predicate.WorkflowRun(sql.FieldEQ(FieldWorkflowID, v)) } +// OrganizationID applies equality check predicate on the "organization_id" field. It's identical to OrganizationIDEQ. +func OrganizationID(v uuid.UUID) predicate.WorkflowRun { + return predicate.WorkflowRun(sql.FieldEQ(FieldOrganizationID, v)) +} + // HasPolicyViolations applies equality check predicate on the "has_policy_violations" field. It's identical to HasPolicyViolationsEQ. func HasPolicyViolations(v bool) predicate.WorkflowRun { return predicate.WorkflowRun(sql.FieldEQ(FieldHasPolicyViolations, v)) @@ -747,6 +752,26 @@ func WorkflowIDNotIn(vs ...uuid.UUID) predicate.WorkflowRun { return predicate.WorkflowRun(sql.FieldNotIn(FieldWorkflowID, vs...)) } +// OrganizationIDEQ applies the EQ predicate on the "organization_id" field. +func OrganizationIDEQ(v uuid.UUID) predicate.WorkflowRun { + return predicate.WorkflowRun(sql.FieldEQ(FieldOrganizationID, v)) +} + +// OrganizationIDNEQ applies the NEQ predicate on the "organization_id" field. +func OrganizationIDNEQ(v uuid.UUID) predicate.WorkflowRun { + return predicate.WorkflowRun(sql.FieldNEQ(FieldOrganizationID, v)) +} + +// OrganizationIDIn applies the In predicate on the "organization_id" field. +func OrganizationIDIn(vs ...uuid.UUID) predicate.WorkflowRun { + return predicate.WorkflowRun(sql.FieldIn(FieldOrganizationID, vs...)) +} + +// OrganizationIDNotIn applies the NotIn predicate on the "organization_id" field. +func OrganizationIDNotIn(vs ...uuid.UUID) predicate.WorkflowRun { + return predicate.WorkflowRun(sql.FieldNotIn(FieldOrganizationID, vs...)) +} + // HasPolicyViolationsEQ applies the EQ predicate on the "has_policy_violations" field. func HasPolicyViolationsEQ(v bool) predicate.WorkflowRun { return predicate.WorkflowRun(sql.FieldEQ(FieldHasPolicyViolations, v)) @@ -1090,6 +1115,29 @@ func HasWorkflowWith(preds ...predicate.Workflow) predicate.WorkflowRun { }) } +// HasOrganization applies the HasEdge predicate on the "organization" edge. +func HasOrganization() predicate.WorkflowRun { + return predicate.WorkflowRun(func(s *sql.Selector) { + step := sqlgraph.NewStep( + sqlgraph.From(Table, FieldID), + sqlgraph.Edge(sqlgraph.M2O, true, OrganizationTable, OrganizationColumn), + ) + sqlgraph.HasNeighbors(s, step) + }) +} + +// HasOrganizationWith applies the HasEdge predicate on the "organization" edge with a given conditions (other predicates). +func HasOrganizationWith(preds ...predicate.Organization) predicate.WorkflowRun { + return predicate.WorkflowRun(func(s *sql.Selector) { + step := newOrganizationStep() + sqlgraph.HasNeighborsWith(s, step, func(s *sql.Selector) { + for _, p := range preds { + p(s) + } + }) + }) +} + // HasContractVersion applies the HasEdge predicate on the "contract_version" edge. func HasContractVersion() predicate.WorkflowRun { return predicate.WorkflowRun(func(s *sql.Selector) { diff --git a/app/controlplane/pkg/data/ent/workflowrun/workflowrun.go b/app/controlplane/pkg/data/ent/workflowrun/workflowrun.go index 828477f26..7fa381dda 100644 --- a/app/controlplane/pkg/data/ent/workflowrun/workflowrun.go +++ b/app/controlplane/pkg/data/ent/workflowrun/workflowrun.go @@ -43,6 +43,8 @@ const ( FieldVersionID = "version_id" // FieldWorkflowID holds the string denoting the workflow_id field in the database. FieldWorkflowID = "workflow_id" + // FieldOrganizationID holds the string denoting the organization_id field in the database. + FieldOrganizationID = "organization_id" // FieldHasPolicyViolations holds the string denoting the has_policy_violations field in the database. FieldHasPolicyViolations = "has_policy_violations" // FieldPolicyStatus holds the string denoting the policy_status field in the database. @@ -61,6 +63,8 @@ const ( FieldPolicyHasGates = "policy_has_gates" // EdgeWorkflow holds the string denoting the workflow edge name in mutations. EdgeWorkflow = "workflow" + // EdgeOrganization holds the string denoting the organization edge name in mutations. + EdgeOrganization = "organization" // EdgeContractVersion holds the string denoting the contract_version edge name in mutations. EdgeContractVersion = "contract_version" // EdgeCasBackends holds the string denoting the cas_backends edge name in mutations. @@ -78,6 +82,13 @@ const ( WorkflowInverseTable = "workflows" // WorkflowColumn is the table column denoting the workflow relation/edge. WorkflowColumn = "workflow_id" + // OrganizationTable is the table that holds the organization relation/edge. + OrganizationTable = "workflow_runs" + // OrganizationInverseTable is the table name for the Organization entity. + // It exists in this package in order to avoid circular dependency with the "organization" package. + OrganizationInverseTable = "organizations" + // OrganizationColumn is the table column denoting the organization relation/edge. + OrganizationColumn = "organization_id" // ContractVersionTable is the table that holds the contract_version relation/edge. ContractVersionTable = "workflow_runs" // ContractVersionInverseTable is the table name for the WorkflowContractVersion entity. @@ -122,6 +133,7 @@ var Columns = []string{ FieldContractRevisionLatest, FieldVersionID, FieldWorkflowID, + FieldOrganizationID, FieldHasPolicyViolations, FieldPolicyStatus, FieldPolicyEvaluationsTotal, @@ -268,6 +280,11 @@ func ByWorkflowID(opts ...sql.OrderTermOption) OrderOption { return sql.OrderByField(FieldWorkflowID, opts...).ToFunc() } +// ByOrganizationID orders the results by the organization_id field. +func ByOrganizationID(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldOrganizationID, opts...).ToFunc() +} + // ByHasPolicyViolations orders the results by the has_policy_violations field. func ByHasPolicyViolations(opts ...sql.OrderTermOption) OrderOption { return sql.OrderByField(FieldHasPolicyViolations, opts...).ToFunc() @@ -315,6 +332,13 @@ func ByWorkflowField(field string, opts ...sql.OrderTermOption) OrderOption { } } +// ByOrganizationField orders the results by organization field. +func ByOrganizationField(field string, opts ...sql.OrderTermOption) OrderOption { + return func(s *sql.Selector) { + sqlgraph.OrderByNeighborTerms(s, newOrganizationStep(), sql.OrderByField(field, opts...)) + } +} + // ByContractVersionField orders the results by contract_version field. func ByContractVersionField(field string, opts ...sql.OrderTermOption) OrderOption { return func(s *sql.Selector) { @@ -356,6 +380,13 @@ func newWorkflowStep() *sqlgraph.Step { sqlgraph.Edge(sqlgraph.M2O, true, WorkflowTable, WorkflowColumn), ) } +func newOrganizationStep() *sqlgraph.Step { + return sqlgraph.NewStep( + sqlgraph.From(Table, FieldID), + sqlgraph.To(OrganizationInverseTable, FieldID), + sqlgraph.Edge(sqlgraph.M2O, true, OrganizationTable, OrganizationColumn), + ) +} func newContractVersionStep() *sqlgraph.Step { return sqlgraph.NewStep( sqlgraph.From(Table, FieldID), diff --git a/app/controlplane/pkg/data/ent/workflowrun_create.go b/app/controlplane/pkg/data/ent/workflowrun_create.go index beb4f402e..d2b1f353f 100644 --- a/app/controlplane/pkg/data/ent/workflowrun_create.go +++ b/app/controlplane/pkg/data/ent/workflowrun_create.go @@ -15,6 +15,7 @@ import ( "github.com/chainloop-dev/chainloop/app/controlplane/pkg/biz" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent/attestation" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent/casbackend" + "github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent/organization" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent/projectversion" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent/workflow" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent/workflowcontractversion" @@ -165,6 +166,12 @@ func (_c *WorkflowRunCreate) SetWorkflowID(v uuid.UUID) *WorkflowRunCreate { return _c } +// SetOrganizationID sets the "organization_id" field. +func (_c *WorkflowRunCreate) SetOrganizationID(v uuid.UUID) *WorkflowRunCreate { + _c.mutation.SetOrganizationID(v) + return _c +} + // SetHasPolicyViolations sets the "has_policy_violations" field. func (_c *WorkflowRunCreate) SetHasPolicyViolations(v bool) *WorkflowRunCreate { _c.mutation.SetHasPolicyViolations(v) @@ -296,6 +303,11 @@ func (_c *WorkflowRunCreate) SetWorkflow(v *Workflow) *WorkflowRunCreate { return _c.SetWorkflowID(v.ID) } +// SetOrganization sets the "organization" edge to the Organization entity. +func (_c *WorkflowRunCreate) SetOrganization(v *Organization) *WorkflowRunCreate { + return _c.SetOrganizationID(v.ID) +} + // SetContractVersionID sets the "contract_version" edge to the WorkflowContractVersion entity by ID. func (_c *WorkflowRunCreate) SetContractVersionID(id uuid.UUID) *WorkflowRunCreate { _c.mutation.SetContractVersionID(id) @@ -428,6 +440,9 @@ func (_c *WorkflowRunCreate) check() error { if _, ok := _c.mutation.WorkflowID(); !ok { return &ValidationError{Name: "workflow_id", err: errors.New(`ent: missing required field "WorkflowRun.workflow_id"`)} } + if _, ok := _c.mutation.OrganizationID(); !ok { + return &ValidationError{Name: "organization_id", err: errors.New(`ent: missing required field "WorkflowRun.organization_id"`)} + } if v, ok := _c.mutation.PolicyStatus(); ok { if err := workflowrun.PolicyStatusValidator(v); err != nil { return &ValidationError{Name: "policy_status", err: fmt.Errorf(`ent: validator failed for field "WorkflowRun.policy_status": %w`, err)} @@ -436,6 +451,9 @@ func (_c *WorkflowRunCreate) check() error { if len(_c.mutation.WorkflowIDs()) == 0 { return &ValidationError{Name: "workflow", err: errors.New(`ent: missing required edge "WorkflowRun.workflow"`)} } + if len(_c.mutation.OrganizationIDs()) == 0 { + return &ValidationError{Name: "organization", err: errors.New(`ent: missing required edge "WorkflowRun.organization"`)} + } if len(_c.mutation.VersionIDs()) == 0 { return &ValidationError{Name: "version", err: errors.New(`ent: missing required edge "WorkflowRun.version"`)} } @@ -568,6 +586,23 @@ func (_c *WorkflowRunCreate) createSpec() (*WorkflowRun, *sqlgraph.CreateSpec) { _node.WorkflowID = nodes[0] _spec.Edges = append(_spec.Edges, edge) } + if nodes := _c.mutation.OrganizationIDs(); len(nodes) > 0 { + edge := &sqlgraph.EdgeSpec{ + Rel: sqlgraph.M2O, + Inverse: true, + Table: workflowrun.OrganizationTable, + Columns: []string{workflowrun.OrganizationColumn}, + Bidi: false, + Target: &sqlgraph.EdgeTarget{ + IDSpec: sqlgraph.NewFieldSpec(organization.FieldID, field.TypeUUID), + }, + } + for _, k := range nodes { + edge.Target.Nodes = append(edge.Target.Nodes, k) + } + _node.OrganizationID = nodes[0] + _spec.Edges = append(_spec.Edges, edge) + } if nodes := _c.mutation.ContractVersionIDs(); len(nodes) > 0 { edge := &sqlgraph.EdgeSpec{ Rel: sqlgraph.M2O, @@ -1069,6 +1104,9 @@ func (u *WorkflowRunUpsertOne) UpdateNewValues() *WorkflowRunUpsertOne { if _, exists := u.create.mutation.WorkflowID(); exists { s.SetIgnore(workflowrun.FieldWorkflowID) } + if _, exists := u.create.mutation.OrganizationID(); exists { + s.SetIgnore(workflowrun.FieldOrganizationID) + } })) return u } @@ -1709,6 +1747,9 @@ func (u *WorkflowRunUpsertBulk) UpdateNewValues() *WorkflowRunUpsertBulk { if _, exists := b.mutation.WorkflowID(); exists { s.SetIgnore(workflowrun.FieldWorkflowID) } + if _, exists := b.mutation.OrganizationID(); exists { + s.SetIgnore(workflowrun.FieldOrganizationID) + } } })) return u diff --git a/app/controlplane/pkg/data/ent/workflowrun_query.go b/app/controlplane/pkg/data/ent/workflowrun_query.go index bc78efe49..702e6c368 100644 --- a/app/controlplane/pkg/data/ent/workflowrun_query.go +++ b/app/controlplane/pkg/data/ent/workflowrun_query.go @@ -15,6 +15,7 @@ import ( "entgo.io/ent/schema/field" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent/attestation" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent/casbackend" + "github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent/organization" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent/predicate" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent/projectversion" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/data/ent/workflow" @@ -31,6 +32,7 @@ type WorkflowRunQuery struct { inters []Interceptor predicates []predicate.WorkflowRun withWorkflow *WorkflowQuery + withOrganization *OrganizationQuery withContractVersion *WorkflowContractVersionQuery withCasBackends *CASBackendQuery withVersion *ProjectVersionQuery @@ -95,6 +97,28 @@ func (_q *WorkflowRunQuery) QueryWorkflow() *WorkflowQuery { return query } +// QueryOrganization chains the current query on the "organization" edge. +func (_q *WorkflowRunQuery) QueryOrganization() *OrganizationQuery { + query := (&OrganizationClient{config: _q.config}).Query() + query.path = func(ctx context.Context) (fromU *sql.Selector, err error) { + if err := _q.prepareQuery(ctx); err != nil { + return nil, err + } + selector := _q.sqlQuery(ctx) + if err := selector.Err(); err != nil { + return nil, err + } + step := sqlgraph.NewStep( + sqlgraph.From(workflowrun.Table, workflowrun.FieldID, selector), + sqlgraph.To(organization.Table, organization.FieldID), + sqlgraph.Edge(sqlgraph.M2O, true, workflowrun.OrganizationTable, workflowrun.OrganizationColumn), + ) + fromU = sqlgraph.SetNeighbors(_q.driver.Dialect(), step) + return fromU, nil + } + return query +} + // QueryContractVersion chains the current query on the "contract_version" edge. func (_q *WorkflowRunQuery) QueryContractVersion() *WorkflowContractVersionQuery { query := (&WorkflowContractVersionClient{config: _q.config}).Query() @@ -376,6 +400,7 @@ func (_q *WorkflowRunQuery) Clone() *WorkflowRunQuery { inters: append([]Interceptor{}, _q.inters...), predicates: append([]predicate.WorkflowRun{}, _q.predicates...), withWorkflow: _q.withWorkflow.Clone(), + withOrganization: _q.withOrganization.Clone(), withContractVersion: _q.withContractVersion.Clone(), withCasBackends: _q.withCasBackends.Clone(), withVersion: _q.withVersion.Clone(), @@ -398,6 +423,17 @@ func (_q *WorkflowRunQuery) WithWorkflow(opts ...func(*WorkflowQuery)) *Workflow return _q } +// WithOrganization tells the query-builder to eager-load the nodes that are connected to +// the "organization" edge. The optional arguments are used to configure the query builder of the edge. +func (_q *WorkflowRunQuery) WithOrganization(opts ...func(*OrganizationQuery)) *WorkflowRunQuery { + query := (&OrganizationClient{config: _q.config}).Query() + for _, opt := range opts { + opt(query) + } + _q.withOrganization = query + return _q +} + // WithContractVersion tells the query-builder to eager-load the nodes that are connected to // the "contract_version" edge. The optional arguments are used to configure the query builder of the edge. func (_q *WorkflowRunQuery) WithContractVersion(opts ...func(*WorkflowContractVersionQuery)) *WorkflowRunQuery { @@ -521,8 +557,9 @@ func (_q *WorkflowRunQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]* nodes = []*WorkflowRun{} withFKs = _q.withFKs _spec = _q.querySpec() - loadedTypes = [5]bool{ + loadedTypes = [6]bool{ _q.withWorkflow != nil, + _q.withOrganization != nil, _q.withContractVersion != nil, _q.withCasBackends != nil, _q.withVersion != nil, @@ -562,6 +599,12 @@ func (_q *WorkflowRunQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]* return nil, err } } + if query := _q.withOrganization; query != nil { + if err := _q.loadOrganization(ctx, query, nodes, nil, + func(n *WorkflowRun, e *Organization) { n.Edges.Organization = e }); err != nil { + return nil, err + } + } if query := _q.withContractVersion; query != nil { if err := _q.loadContractVersion(ctx, query, nodes, nil, func(n *WorkflowRun, e *WorkflowContractVersion) { n.Edges.ContractVersion = e }); err != nil { @@ -619,6 +662,35 @@ func (_q *WorkflowRunQuery) loadWorkflow(ctx context.Context, query *WorkflowQue } return nil } +func (_q *WorkflowRunQuery) loadOrganization(ctx context.Context, query *OrganizationQuery, nodes []*WorkflowRun, init func(*WorkflowRun), assign func(*WorkflowRun, *Organization)) error { + ids := make([]uuid.UUID, 0, len(nodes)) + nodeids := make(map[uuid.UUID][]*WorkflowRun) + for i := range nodes { + fk := nodes[i].OrganizationID + if _, ok := nodeids[fk]; !ok { + ids = append(ids, fk) + } + nodeids[fk] = append(nodeids[fk], nodes[i]) + } + if len(ids) == 0 { + return nil + } + query.Where(organization.IDIn(ids...)) + neighbors, err := query.All(ctx) + if err != nil { + return err + } + for _, n := range neighbors { + nodes, ok := nodeids[n.ID] + if !ok { + return fmt.Errorf(`unexpected foreign-key "organization_id" returned %v`, n.ID) + } + for i := range nodes { + assign(nodes[i], n) + } + } + return nil +} func (_q *WorkflowRunQuery) loadContractVersion(ctx context.Context, query *WorkflowContractVersionQuery, nodes []*WorkflowRun, init func(*WorkflowRun), assign func(*WorkflowRun, *WorkflowContractVersion)) error { ids := make([]uuid.UUID, 0, len(nodes)) nodeids := make(map[uuid.UUID][]*WorkflowRun) @@ -800,6 +872,9 @@ func (_q *WorkflowRunQuery) querySpec() *sqlgraph.QuerySpec { if _q.withWorkflow != nil { _spec.Node.AddColumnOnce(workflowrun.FieldWorkflowID) } + if _q.withOrganization != nil { + _spec.Node.AddColumnOnce(workflowrun.FieldOrganizationID) + } if _q.withVersion != nil { _spec.Node.AddColumnOnce(workflowrun.FieldVersionID) } diff --git a/app/controlplane/pkg/data/ent/workflowrun_update.go b/app/controlplane/pkg/data/ent/workflowrun_update.go index 340df0035..cfa86cf40 100644 --- a/app/controlplane/pkg/data/ent/workflowrun_update.go +++ b/app/controlplane/pkg/data/ent/workflowrun_update.go @@ -569,6 +569,9 @@ func (_u *WorkflowRunUpdate) check() error { if _u.mutation.WorkflowCleared() && len(_u.mutation.WorkflowIDs()) > 0 { return errors.New(`ent: clearing a required unique edge "WorkflowRun.workflow"`) } + if _u.mutation.OrganizationCleared() && len(_u.mutation.OrganizationIDs()) > 0 { + return errors.New(`ent: clearing a required unique edge "WorkflowRun.organization"`) + } if _u.mutation.VersionCleared() && len(_u.mutation.VersionIDs()) > 0 { return errors.New(`ent: clearing a required unique edge "WorkflowRun.version"`) } @@ -1413,6 +1416,9 @@ func (_u *WorkflowRunUpdateOne) check() error { if _u.mutation.WorkflowCleared() && len(_u.mutation.WorkflowIDs()) > 0 { return errors.New(`ent: clearing a required unique edge "WorkflowRun.workflow"`) } + if _u.mutation.OrganizationCleared() && len(_u.mutation.OrganizationIDs()) > 0 { + return errors.New(`ent: clearing a required unique edge "WorkflowRun.organization"`) + } if _u.mutation.VersionCleared() && len(_u.mutation.VersionIDs()) > 0 { return errors.New(`ent: clearing a required unique edge "WorkflowRun.version"`) } diff --git a/app/controlplane/pkg/data/workflowrun.go b/app/controlplane/pkg/data/workflowrun.go index 0dea348d3..964522b0f 100644 --- a/app/controlplane/pkg/data/workflowrun.go +++ b/app/controlplane/pkg/data/workflowrun.go @@ -102,6 +102,7 @@ func (r *WorkflowRunRepo) Create(ctx context.Context, opts *biz.WorkflowRunRepoC // Create workflow run p, err = tx.WorkflowRun.Create(). SetWorkflowID(opts.WorkflowID). + SetOrganizationID(wf.OrganizationID). SetVersionID(version.ID). SetContractVersionID(opts.SchemaVersionID). SetRunURL(opts.RunURL). @@ -367,28 +368,23 @@ func (r *WorkflowRunRepo) List(ctx context.Context, orgID uuid.UUID, filters *bi return nil, "", errors.New("pagination options is required") } - // workflow filters - wfPredicates := []predicate.Workflow{ - workflow.DeletedAtIsNil(), - workflow.OrganizationID(orgID), - } - if filters.ProjectIDs != nil { + // Org-scoped query uses the denormalized organization_id column for a + // sargable range scan via the (organization_id, created_at DESC) index. + // The prior HasWorkflowWith form compiled to a correlated EXISTS that + // let the planner pick a bad ORDER BY created_at DESC plan. The + // deleted_at filter still goes via the workflows edge: it's a per-row + // PK lookup applied after the index narrows the candidate set, so it + // doesn't reintroduce the planner ambiguity. + wfPredicates := []predicate.Workflow{workflow.DeletedAtIsNil()} + if filters != nil && filters.ProjectIDs != nil { wfPredicates = append(wfPredicates, workflow.ProjectIDIn(filters.ProjectIDs...)) } - // query first for workflows to avoid joining the workflow_runs table - wfExist, err := r.data.DB.Workflow.Query().Where(wfPredicates...).Exist(ctx) - if err != nil { - return nil, "", fmt.Errorf("getting workflows: %w", err) - } - if !wfExist { - // No workflows in the org, no runs - return nil, "", nil - } - - // Query workflow runs by joining with workflows - q := r.data.DB.WorkflowRun.Query().Where( - workflowrun.HasWorkflowWith(wfPredicates...)). + q := r.data.DB.WorkflowRun.Query(). + Where( + workflowrun.OrganizationID(orgID), + workflowrun.HasWorkflowWith(wfPredicates...), + ). Order(ent.Desc(workflowrun.FieldCreatedAt)). WithWorkflowAndProject().WithVersion(). Limit(p.Limit + 1)