diff --git a/apps/event-worker/src/workers/job-update/trigger-dependent-targets.ts b/apps/event-worker/src/workers/job-update/trigger-dependent-targets.ts index d4c107606..736c4ceea 100644 --- a/apps/event-worker/src/workers/job-update/trigger-dependent-targets.ts +++ b/apps/event-worker/src/workers/job-update/trigger-dependent-targets.ts @@ -9,6 +9,7 @@ import { takeFirstOrNull, } from "@ctrlplane/db"; import { db } from "@ctrlplane/db/client"; +import { getResourceChildren } from "@ctrlplane/db/queries"; import * as schema from "@ctrlplane/db/schema"; import { dispatchQueueJob } from "@ctrlplane/events"; import { logger } from "@ctrlplane/logger"; @@ -68,7 +69,7 @@ const getNewlySatisfiedDependencies = async ( }; const getReleaseTargetsToEvaluate = async ( - resourceId: string, + resourceIds: string[], newlySatisfiedDependencies: schema.VersionDependency[], ) => db @@ -88,7 +89,7 @@ const getReleaseTargetsToEvaluate = async ( schema.deploymentVersion.id, newlySatisfiedDependencies.map((d) => d.versionId), ), - eq(schema.releaseTarget.resourceId, resourceId), + inArray(schema.releaseTarget.resourceId, resourceIds), ), ) .then((rows) => rows.map((row) => row.release_target)); @@ -103,8 +104,10 @@ export const triggerDependentTargets = async (job: schema.Job) => { const version = await getVersion(versionId); const newlySatisfiedDependencies = await getNewlySatisfiedDependencies(version); + const childResources = await getResourceChildren(db, resourceId); + const childResourceIds = childResources.map(({ target }) => target.id); const releaseTargetsToEvaluate = await getReleaseTargetsToEvaluate( - resourceId, + [resourceId, ...childResourceIds], newlySatisfiedDependencies, ); diff --git a/apps/webservice/src/app/api/v1/resource-relationship-rules/[ruleId]/openapi.ts b/apps/webservice/src/app/api/v1/resource-relationship-rules/[ruleId]/openapi.ts index dc75a5189..3516df0ff 100644 --- a/apps/webservice/src/app/api/v1/resource-relationship-rules/[ruleId]/openapi.ts +++ b/apps/webservice/src/app/api/v1/resource-relationship-rules/[ruleId]/openapi.ts @@ -104,6 +104,55 @@ export const openapi: Swagger.SwaggerV3 = { }, }, }, + delete: { + summary: "Delete a resource relationship rule", + operationId: "deleteResourceRelationshipRule", + parameters: [ + { + name: "ruleId", + in: "path", + required: true, + schema: { type: "string", format: "uuid" }, + }, + ], + responses: { + 200: { + description: "The deleted resource relationship rule", + content: { + "application/json": { + schema: { + $ref: "#/components/schemas/ResourceRelationshipRule", + }, + }, + }, + }, + 404: { + description: "The resource relationship rule was not found", + content: { + "application/json": { + schema: { + type: "object", + properties: { error: { type: "string" } }, + required: ["error"], + }, + }, + }, + }, + 500: { + description: + "An error occurred while deleting the resource relationship rule", + content: { + "application/json": { + schema: { + type: "object", + properties: { error: { type: "string" } }, + required: ["error"], + }, + }, + }, + }, + }, + }, }, }, }; diff --git a/apps/webservice/src/app/api/v1/resource-relationship-rules/[ruleId]/route.ts b/apps/webservice/src/app/api/v1/resource-relationship-rules/[ruleId]/route.ts index 16db9a434..50f77e324 100644 --- a/apps/webservice/src/app/api/v1/resource-relationship-rules/[ruleId]/route.ts +++ b/apps/webservice/src/app/api/v1/resource-relationship-rules/[ruleId]/route.ts @@ -4,7 +4,7 @@ import { NextResponse } from "next/server"; import { INTERNAL_SERVER_ERROR, NOT_FOUND } from "http-status"; import _ from "lodash"; -import { eq, takeFirst } from "@ctrlplane/db"; +import { eq, takeFirst, takeFirstOrNull } from "@ctrlplane/db"; import * as schema from "@ctrlplane/db/schema"; import { logger } from "@ctrlplane/logger"; import { Permission } from "@ctrlplane/validators/auth"; @@ -172,3 +172,46 @@ export const PATCH = request() ); } }); + +export const DELETE = request() + .use(authn) + .use( + authz(({ can, params }) => + can.perform(Permission.ResourceRelationshipRuleDelete).on({ + type: "resourceRelationshipRule", + id: params.ruleId ?? "", + }), + ), + ) + .handle<{ db: Tx }, { params: Promise<{ ruleId: string }> }>( + async ({ db }, { params }) => { + try { + const { ruleId } = await params; + + const rule = await db + .select() + .from(schema.resourceRelationshipRule) + .where(eq(schema.resourceRelationshipRule.id, ruleId)) + .then(takeFirstOrNull); + if (rule == null) + return NextResponse.json( + { error: "Resource relationship rule not found" }, + { status: NOT_FOUND }, + ); + + const deletedRule = await db + .delete(schema.resourceRelationshipRule) + .where(eq(schema.resourceRelationshipRule.id, ruleId)) + .returning() + .then(takeFirst); + + return NextResponse.json(deletedRule); + } catch (error) { + log.error(error); + return NextResponse.json( + { error: "Failed to delete resource relationship rule" }, + { status: INTERNAL_SERVER_ERROR }, + ); + } + }, + ); diff --git a/apps/webservice/src/app/api/v1/resource-relationship-rules/openapi.ts b/apps/webservice/src/app/api/v1/resource-relationship-rules/openapi.ts index f498465af..b2bdb00a8 100644 --- a/apps/webservice/src/app/api/v1/resource-relationship-rules/openapi.ts +++ b/apps/webservice/src/app/api/v1/resource-relationship-rules/openapi.ts @@ -66,14 +66,6 @@ export const openapi: Swagger.SwaggerV3 = { schemas: { ResourceRelationshipRuleDependencyType: { type: "string", - enum: [ - "depends_on", - "depends_indirectly_on", - "uses_at_runtime", - "created_after", - "provisioned_in", - "inherits_from", - ], }, MetadataEqualsConstraint: { diff --git a/e2e/api/entities-builder.ts b/e2e/api/entities-builder.ts index dc2124a80..9dadc708e 100644 --- a/e2e/api/entities-builder.ts +++ b/e2e/api/entities-builder.ts @@ -166,6 +166,59 @@ export class EntitiesBuilder { return results; } + async upsertResourceRelationshipsFixtures(): Promise { + if ( + !this.fixtures.resourceRelationships || + this.fixtures.resourceRelationships.length === 0 + ) { + throw new Error("No resource relationships defined in YAML file"); + } + + const results: FetchResultInfo[] = []; + const workspaceId = this.workspace.id; + + for (const relationship of this.fixtures.resourceRelationships) { + console.log(`Upserting resource relationship: ${relationship.reference}`); + + const requestBody = { + ...relationship, + workspaceId, + sourceKind: relationship.source.kind, + sourceVersion: relationship.source.version, + targetKind: relationship.target?.kind, + targetVersion: relationship.target?.version, + name: relationship.reference, + }; + const fetchResponse = await this.api.POST( + "/v1/resource-relationship-rules", + { body: requestBody }, + ); + + results.push({ fetchResponse, requestBody }); + + this.refs.resourceRelationships.push({ + id: fetchResponse.data!.id, + reference: fetchResponse.data!.reference, + dependencyType: fetchResponse.data!.dependencyType, + source: { + kind: fetchResponse.data!.sourceKind, + version: fetchResponse.data!.sourceVersion, + }, + ...(fetchResponse.data?.targetKind != null || + fetchResponse.data?.targetVersion != null + ? { + target: { + kind: fetchResponse.data!.targetKind, + version: fetchResponse.data!.targetVersion, + }, + } + : {}), + }); + } + + return results; + } + async upsertEnvironmentFixtures(): Promise { if ( !this.fixtures.environments || @@ -651,5 +704,14 @@ export async function cleanupImportedEntities( ), }); } + + for (const resourceRelationship of refs.resourceRelationships ?? []) { + results.push({ + fetchResponse: await api.DELETE( + "/v1/resource-relationship-rules/{ruleId}", + { params: { path: { ruleId: resourceRelationship.id } } }, + ), + }); + } return results; } diff --git a/e2e/api/entity-fixtures.ts b/e2e/api/entity-fixtures.ts index dba0cbd97..0095dca2f 100644 --- a/e2e/api/entity-fixtures.ts +++ b/e2e/api/entity-fixtures.ts @@ -99,6 +99,23 @@ export const ResourceFixture = z.object({ metadata: z.record(z.string()).optional(), }); +export const ResourceRelationshipFixture = z.object({ + source: z.object({ + kind: z.string(), + version: z.string(), + }), + target: z + .object({ + kind: z.string().optional(), + version: z.string().optional(), + }) + .optional(), + dependencyType: z.string(), + dependencyDescription: z.string().optional(), + description: z.string().optional(), + reference: z.string(), +}); + export const PolicyFixture = z.object({ name: z.string(), targets: z.array( @@ -151,6 +168,7 @@ export const EntityFixtures = z.object({ system: SystemFixture, environments: z.array(EnvironmentFixture).optional(), resources: z.array(ResourceFixture).optional(), + resourceRelationships: z.array(ResourceRelationshipFixture).optional(), deployments: z.array(DeploymentFixture).optional(), policies: z.array(PolicyFixture).optional(), agents: z.array(AgentFixture).optional(), diff --git a/e2e/api/entity-refs.ts b/e2e/api/entity-refs.ts index 61938a661..c95d9109a 100644 --- a/e2e/api/entity-refs.ts +++ b/e2e/api/entity-refs.ts @@ -64,6 +64,20 @@ export interface ResourceRef { metadata?: Record; } +export interface ResourceRelationshipRef { + id: string; + reference: string; + dependencyType: string; + source: { + kind: string; + version: string; + }; + target?: { + kind?: string; + version?: string; + }; +} + export interface PolicyRef { id: string; name: string; @@ -80,6 +94,8 @@ export class EntityRefs { public resources: Array = []; + public resourceRelationships: Array = []; + public deployments: Array = []; public policies: Array = []; @@ -121,6 +137,24 @@ export class EntityRefs { ); } + public takeResourceRelationships( + count: number, + ): Array { + return takeRandom(this.resourceRelationships, count); + } + + public oneResourceRelationship(): ResourceRelationshipRef { + return takeRandom(this.resourceRelationships, 1)[0]; + } + + public getResourceRelationshipByReference( + reference: string, + ): ResourceRelationshipRef { + return exactlyOne( + this.resourceRelationships.filter((rel) => rel.reference === reference), + ); + } + public takeDeployments(count: number): Array { return takeRandom(this.deployments, count); } diff --git a/e2e/api/schema.ts b/e2e/api/schema.ts index 32acb9aaa..e3e8f73d2 100644 --- a/e2e/api/schema.ts +++ b/e2e/api/schema.ts @@ -656,7 +656,8 @@ export interface paths { get?: never; put?: never; post?: never; - delete?: never; + /** Delete a resource relationship rule */ + delete: operations["deleteResourceRelationshipRule"]; options?: never; head?: never; /** Update a resource relationship rule */ @@ -1553,14 +1554,7 @@ export interface components { targetMetadataEquals?: components["schemas"]["MetadataEqualsConstraint"][]; sourceMetadataEquals?: components["schemas"]["MetadataEqualsConstraint"][]; }; - /** @enum {string} */ - ResourceRelationshipRuleDependencyType: - | "depends_on" - | "depends_indirectly_on" - | "uses_at_runtime" - | "created_after" - | "provisioned_in" - | "inherits_from"; + ResourceRelationshipRuleDependencyType: string; MetadataEqualsConstraint: { key?: string; value?: string; @@ -3916,6 +3910,50 @@ export interface operations { }; }; }; + deleteResourceRelationshipRule: { + parameters: { + query?: never; + header?: never; + path: { + ruleId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The deleted resource relationship rule */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ResourceRelationshipRule"]; + }; + }; + /** @description The resource relationship rule was not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + error: string; + }; + }; + }; + /** @description An error occurred while deleting the resource relationship rule */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + error: string; + }; + }; + }; + }; + }; updateResourceRelationshipRule: { parameters: { query?: never; diff --git a/e2e/tests/api/version-dependency.spec.ts b/e2e/tests/api/version-dependency.spec.ts index 4243788ec..b4eee872f 100644 --- a/e2e/tests/api/version-dependency.spec.ts +++ b/e2e/tests/api/version-dependency.spec.ts @@ -65,8 +65,12 @@ const insertVersionForParent = async ( const getReleaseTarget = async ( api: Client, builder: EntitiesBuilder, - deploymentId: string, ) => { + const { prefix } = builder.refs; + const deploymentId = builder.refs.deployments.find( + (d) => d.slug === `${prefix}-child-deployment`, + )!.id; + const workspaceId = builder.workspace.id; const resourceRef = builder.refs.oneResource(); const { identifier } = resourceRef; @@ -106,12 +110,11 @@ const getRelease = async ( const markJobAsSuccessful = async ( api: Client, builder: EntitiesBuilder, + agentName: string, ) => { - const agentId = builder.refs.oneAgent().id; + const agentId = builder.refs.getAgentLike(agentName).id; const nextJobResponse = await api.GET("/v1/job-agents/{agentId}/queue/next", { - params: { - path: { agentId }, - }, + params: { path: { agentId } }, }); expect(nextJobResponse.response.status).toBe(200); const nextJobId = nextJobResponse.data?.jobs?.[0]?.id; @@ -136,7 +139,7 @@ test.describe("Version Dependency", () => { await builder.upsertEnvironmentFixtures(); await builder.upsertAgentFixtures(); - const agentId = builder.refs.oneAgent().id; + const agentId = builder.refs.getAgentLike("agent-1").id; await builder.upsertDeploymentFixtures(agentId); }); @@ -148,16 +151,8 @@ test.describe("Version Dependency", () => { api, page, }) => { - const { prefix } = builder.refs; - const childDeployment = builder.refs.deployments.find( - (d) => d.slug === `${prefix}-child-deployment`, - )!; const { versionTag } = await insertVersionForChild(api, builder); - const releaseTarget = await getReleaseTarget( - api, - builder, - childDeployment.id, - ); + const releaseTarget = await getReleaseTarget(api, builder); await page.waitForTimeout(5_000); const release = await getRelease(api, releaseTarget.id, versionTag); expect(release).toBeUndefined(); @@ -176,14 +171,264 @@ test.describe("Version Dependency", () => { builder, ); await insertVersionForParent(api, builder, selectorTag); - await markJobAsSuccessful(api, builder); + await markJobAsSuccessful(api, builder, "agent-1"); await page.waitForTimeout(5_000); - const releaseTarget = await getReleaseTarget( + const releaseTarget = await getReleaseTarget(api, builder); + const release = await getRelease(api, releaseTarget.id, versionTag); + expect(release).toBeDefined(); + }); + + const createExternalSystem = async ( + api: Client, + builder: EntitiesBuilder, + ) => { + const { prefix } = builder.refs; + const externalSystem = await api.POST("/v1/systems", { + body: { + name: `${prefix}-parent-system`, + slug: `${prefix}-parent-system`, + description: "External system for testing version dependencies", + workspaceId: builder.workspace.id, + }, + }); + expect(externalSystem.response.status).toBe(201); + return externalSystem.data?.id ?? ""; + }; + + const createExternalParentDeployment = async ( + api: Client, + builder: EntitiesBuilder, + externalSystemId: string, + ) => { + const agentId = builder.refs.getAgentLike("agent-2").id; + + const { prefix } = builder.refs; + const externalParentDeployment = await api.POST("/v1/deployments", { + body: { + name: `${prefix}-parent-deployment-2`, + slug: `${prefix}-parent-deployment-2`, + description: "Parent deployment for testing version dependencies", + systemId: externalSystemId, + jobAgentId: agentId, + }, + }); + expect(externalParentDeployment.response.status).toBe(201); + return externalParentDeployment.data?.id ?? ""; + }; + + const createExternalParentEnvironment = async ( + api: Client, + builder: EntitiesBuilder, + externalSystemId: string, + ) => { + const { prefix } = builder.refs; + const externalParentEnvironment = await api.POST("/v1/environments", { + body: { + name: `${prefix}-parent-environment`, + slug: `${prefix}-parent-environment`, + description: "Parent environment for testing version dependencies", + systemId: externalSystemId, + resourceSelector: { + type: "comparison", + operator: "and", + conditions: [ + { + type: "identifier", + operator: "contains", + value: prefix, + }, + { + type: "kind", + operator: "equals", + value: `${prefix}-parent-service`, + }, + ], + }, + }, + }); + expect(externalParentEnvironment.response.status).toBe(200); + }; + + const createExternalParentResource = async ( + api: Client, + builder: EntitiesBuilder, + ) => { + const { prefix } = builder.refs; + const externalParentResource = await api.POST("/v1/resources", { + body: { + name: `${prefix}-parent-resource`, + kind: `${prefix}-parent-service`, + identifier: `${prefix}-parent-resource`, + version: "1.0.0", + config: {}, + workspaceId: builder.workspace.id, + metadata: { e2e: "true" }, + }, + }); + expect(externalParentResource.response.status).toBe(200); + return externalParentResource.data?.id ?? ""; + }; + + const createExternalParentResourceRelationship = async ( + api: Client, + builder: EntitiesBuilder, + ) => { + const { prefix } = builder.refs; + const reference = faker.string.numeric(10); + + const resourceRelationshipRuleResponse = await api.POST( + "/v1/resource-relationship-rules", + { + body: { + workspaceId: builder.workspace.id, + name: `${prefix}-parent-resource-relationship`, + reference, + dependencyType: "depends on", + dependencyDescription: + "Parent resource for testing version dependencies", + sourceKind: `${prefix}-parent-service`, + sourceVersion: "1.0.0", + targetKind: "service", + targetVersion: "1.0.0", + metadataKeysMatches: [{ sourceKey: "e2e", targetKey: "e2e" }], + }, + }, + ); + expect(resourceRelationshipRuleResponse.response.status).toBe(200); + return resourceRelationshipRuleResponse.data?.id ?? ""; + }; + + const insertVersionForChildWithExternalDependency = async ( + api: Client, + builder: EntitiesBuilder, + externalDeploymentId: string, + ) => { + const { prefix } = builder.refs; + const childDeployment = builder.refs.deployments.find( + (d) => d.slug === `${prefix}-child-deployment`, + )!; + + const versionTag = faker.string.alphanumeric(10); + const selectorTag = faker.string.alphanumeric(10); + + const deploymentVersionResponse = await api.POST( + "/v1/deployment-versions", + { + body: { + tag: versionTag, + deploymentId: childDeployment.id, + dependencies: [ + { + deploymentId: externalDeploymentId, + versionSelector: { + type: "tag", + operator: "equals", + value: selectorTag, + }, + }, + ], + }, + }, + ); + + expect(deploymentVersionResponse.response.status).toBe(201); + + return { versionTag, selectorTag }; + }; + + const insertVersionForParentDeployment = async ( + api: Client, + externalDeploymentId: string, + versionTag: string, + ) => { + const deploymentVersionResponse = await api.POST( + "/v1/deployment-versions", + { + body: { + tag: versionTag, + deploymentId: externalDeploymentId, + }, + }, + ); + + expect(deploymentVersionResponse.response.status).toBe(201); + }; + + const cleanupExternalParentResource = async ( + api: Client, + externalParentResourceId: string, + ) => { + const deleteResponse = await api.DELETE(`/v1/resources/{resourceId}`, { + params: { path: { resourceId: externalParentResourceId } }, + }); + expect(deleteResponse.response.status).toBe(200); + }; + + const cleanupRelationshipRule = async ( + api: Client, + externalParentResourceRelationshipId: string, + ) => { + const deleteResponse = await api.DELETE( + `/v1/resource-relationship-rules/{ruleId}`, + { + params: { path: { ruleId: externalParentResourceRelationshipId } }, + }, + ); + expect(deleteResponse.response.status).toBe(200); + }; + + test("should handle a version dependency on an external parent resource", async ({ + api, + page, + }) => { + const externalSystem = await createExternalSystem(api, builder); + const externalParentDeploymentId = await createExternalParentDeployment( api, builder, - childDeployment.id, + externalSystem, ); - const release = await getRelease(api, releaseTarget.id, versionTag); - expect(release).toBeDefined(); + await createExternalParentEnvironment(api, builder, externalSystem); + + const externalParentResourceId = await createExternalParentResource( + api, + builder, + ); + const externalParentResourceRelationshipId = + await createExternalParentResourceRelationship(api, builder); + + const { versionTag, selectorTag } = + await insertVersionForChildWithExternalDependency( + api, + builder, + externalParentDeploymentId, + ); + + const releaseTarget = await getReleaseTarget(api, builder); + await page.waitForTimeout(5_000); + const initialReleaseResult = await getRelease( + api, + releaseTarget.id, + versionTag, + ); + + expect(initialReleaseResult).toBeUndefined(); + + await insertVersionForParentDeployment( + api, + externalParentDeploymentId, + selectorTag, + ); + await markJobAsSuccessful(api, builder, "agent-2"); + await page.waitForTimeout(5_000); + const finalReleaseResult = await getRelease( + api, + releaseTarget.id, + versionTag, + ); + + expect(finalReleaseResult).toBeDefined(); + + await cleanupExternalParentResource(api, externalParentResourceId); + await cleanupRelationshipRule(api, externalParentResourceRelationshipId); }); }); diff --git a/e2e/tests/api/version-dependency.spec.yaml b/e2e/tests/api/version-dependency.spec.yaml index 0ad0a9aae..686ff030f 100644 --- a/e2e/tests/api/version-dependency.spec.yaml +++ b/e2e/tests/api/version-dependency.spec.yaml @@ -10,6 +10,8 @@ resources: version: "1.0.0" config: enabled: true + metadata: + e2e: "true" deployments: - name: "{{ prefix }}-parent-deployment" @@ -32,5 +34,7 @@ environments: value: "{{ prefix }}-resource" agents: - - name: "{{ prefix }}-agent" + - name: "{{ prefix }}-agent-1" + type: "{{ prefix }}-agent-type" + - name: "{{ prefix }}-agent-2" type: "{{ prefix }}-agent-type" diff --git a/openapi.v1.json b/openapi.v1.json index eac083fa7..176d4bef3 100644 --- a/openapi.v1.json +++ b/openapi.v1.json @@ -3667,6 +3667,69 @@ } } } + }, + "delete": { + "summary": "Delete a resource relationship rule", + "operationId": "deleteResourceRelationshipRule", + "parameters": [ + { + "name": "ruleId", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + } + ], + "responses": { + "200": { + "description": "The deleted resource relationship rule", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResourceRelationshipRule" + } + } + } + }, + "404": { + "description": "The resource relationship rule was not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + }, + "required": [ + "error" + ] + } + } + } + }, + "500": { + "description": "An error occurred while deleting the resource relationship rule", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "string" + } + }, + "required": [ + "error" + ] + } + } + } + } + } } }, "/v1/resource-relationship-rules": { @@ -6779,15 +6842,7 @@ } }, "ResourceRelationshipRuleDependencyType": { - "type": "string", - "enum": [ - "depends_on", - "depends_indirectly_on", - "uses_at_runtime", - "created_after", - "provisioned_in", - "inherits_from" - ] + "type": "string" }, "MetadataEqualsConstraint": { "type": "object", diff --git a/packages/rule-engine/src/manager/version-manager-rules/version-dependency.ts b/packages/rule-engine/src/manager/version-manager-rules/version-dependency.ts index 16b243749..9703e1dbd 100644 --- a/packages/rule-engine/src/manager/version-manager-rules/version-dependency.ts +++ b/packages/rule-engine/src/manager/version-manager-rules/version-dependency.ts @@ -1,5 +1,13 @@ -import { and, eq, selector, takeFirst, takeFirstOrNull } from "@ctrlplane/db"; +import { + and, + eq, + inArray, + selector, + takeFirst, + takeFirstOrNull, +} from "@ctrlplane/db"; import { db } from "@ctrlplane/db/client"; +import { getResourceParents } from "@ctrlplane/db/queries"; import * as schema from "@ctrlplane/db/schema"; import { JobStatus } from "@ctrlplane/validators/jobs"; @@ -25,6 +33,11 @@ const getResourceFromReleaseTarget = async (releaseTargetId: string) => const getIsVersionDependencySatisfied = async (releaseTargetId: string) => { const resource = await getResourceFromReleaseTarget(releaseTargetId); + const parentRelationships = await getResourceParents(db, resource.id); + const parentResourceIds = Object.values( + parentRelationships.relationships, + ).map(({ source }) => source.id); + const resourceIdsToCheck = [resource.id, ...parentResourceIds]; return (dependency: schema.VersionDependency) => db @@ -49,7 +62,7 @@ const getIsVersionDependencySatisfied = async (releaseTargetId: string) => { .innerJoin(schema.job, eq(schema.releaseJob.jobId, schema.job.id)) .where( and( - eq(schema.releaseTarget.resourceId, resource.id), + inArray(schema.releaseTarget.resourceId, resourceIdsToCheck), eq(schema.releaseTarget.deploymentId, dependency.deploymentId), selector() .query()