diff --git a/apps/api/openapi/openapi.json b/apps/api/openapi/openapi.json index 11ee7d8b5..44c19e9f6 100644 --- a/apps/api/openapi/openapi.json +++ b/apps/api/openapi/openapi.json @@ -24,17 +24,6 @@ "BooleanValue": { "type": "boolean" }, - "CelSelector": { - "properties": { - "cel": { - "type": "string" - } - }, - "required": [ - "cel" - ], - "type": "object" - }, "CreateDeploymentRequest": { "properties": { "description": { @@ -812,7 +801,8 @@ "EnvironmentProgressionRule": { "properties": { "dependsOnEnvironmentSelector": { - "$ref": "#/components/schemas/Selector" + "description": "CEL expression to match the environment(s) that must have a successful release before this environment can proceed.", + "type": "string" }, "maximumAgeHours": { "description": "Maximum age of dependency deployment before blocking progression (prevents stale promotions)", @@ -1237,18 +1227,6 @@ ], "type": "object" }, - "JsonSelector": { - "properties": { - "json": { - "additionalProperties": true, - "type": "object" - } - }, - "required": [ - "json" - ], - "type": "object" - }, "LiteralValue": { "oneOf": [ { @@ -2005,16 +1983,6 @@ ], "type": "object" }, - "Selector": { - "oneOf": [ - { - "$ref": "#/components/schemas/JsonSelector" - }, - { - "$ref": "#/components/schemas/CelSelector" - } - ] - }, "SensitiveValue": { "properties": { "valueHash": { @@ -2836,7 +2804,8 @@ "type": "string" }, "selector": { - "$ref": "#/components/schemas/Selector" + "description": "CEL expression to select which versions are eligible for deployment.", + "type": "string" } }, "required": [ @@ -3115,7 +3084,8 @@ "selector": { "properties": { "default": { - "$ref": "#/components/schemas/Selector" + "description": "CEL expression for the default selector.", + "type": "string" }, "entityType": { "enum": [ diff --git a/apps/api/openapi/schemas/core.jsonnet b/apps/api/openapi/schemas/core.jsonnet index 779c7b7b9..510c7f388 100644 --- a/apps/api/openapi/schemas/core.jsonnet +++ b/apps/api/openapi/schemas/core.jsonnet @@ -1,30 +1,6 @@ local openapi = import '../lib/openapi.libsonnet'; { - // Selector types - JsonSelector: { - type: 'object', - required: ['json'], - properties: { - json: { type: 'object', additionalProperties: true }, - }, - }, - - CelSelector: { - type: 'object', - required: ['cel'], - properties: { - cel: { type: 'string' }, - }, - }, - - Selector: { - oneOf: [ - openapi.schemaRef('JsonSelector'), - openapi.schemaRef('CelSelector'), - ], - }, - // Property matcher PropertyMatcher: { type: 'object', diff --git a/apps/api/openapi/schemas/policies.jsonnet b/apps/api/openapi/schemas/policies.jsonnet index d1132ad9f..5b9be8ca3 100644 --- a/apps/api/openapi/schemas/policies.jsonnet +++ b/apps/api/openapi/schemas/policies.jsonnet @@ -145,7 +145,10 @@ local openapi = import '../lib/openapi.libsonnet'; type: 'object', required: ['selector'], properties: { - selector: openapi.schemaRef('Selector'), + selector: { + type: 'string', + description: 'CEL expression to select which versions are eligible for deployment.', + }, description: { type: 'string', description: 'Human-readable description of what this version selector does. Example: "Only deploy v2.x versions to staging environments"', @@ -157,7 +160,10 @@ local openapi = import '../lib/openapi.libsonnet'; type: 'object', required: ['dependsOnEnvironmentSelector'], properties: { - dependsOnEnvironmentSelector: openapi.schemaRef('Selector'), + dependsOnEnvironmentSelector: { + type: 'string', + description: 'CEL expression to match the environment(s) that must have a successful release before this environment can proceed.', + }, minimumSuccessPercentage: { type: 'number', format: 'float', minimum: 0, maximum: 100, default: 100 }, successStatuses: { type: 'array', items: openapi.schemaRef('JobStatus') }, diff --git a/apps/api/openapi/schemas/workflows.jsonnet b/apps/api/openapi/schemas/workflows.jsonnet index 3d1ef6d16..2afe74457 100644 --- a/apps/api/openapi/schemas/workflows.jsonnet +++ b/apps/api/openapi/schemas/workflows.jsonnet @@ -96,7 +96,10 @@ local openapi = import '../lib/openapi.libsonnet'; required: ['entityType'], properties: { entityType: { type: 'string', enum: ['resource', 'environment', 'deployment'] }, - default: openapi.schemaRef('Selector'), + default: { + type: 'string', + description: 'CEL expression for the default selector.', + }, }, }, }, diff --git a/apps/api/src/routes/v1/workspaces/policies.ts b/apps/api/src/routes/v1/workspaces/policies.ts index 2b9e88b8c..ced0539e4 100644 --- a/apps/api/src/routes/v1/workspaces/policies.ts +++ b/apps/api/src/routes/v1/workspaces/policies.ts @@ -189,9 +189,7 @@ const formatPolicy = (p: PolicyRow) => { ...p.environmentProgressionRules.map((r) => formatPolicyRule(r.id, r.policyId, r.createdAt, { environmentProgression: { - dependsOnEnvironmentSelector: JSON.parse( - r.dependsOnEnvironmentSelector, - ), + dependsOnEnvironmentSelector: r.dependsOnEnvironmentSelector, ...(r.maximumAgeHours != null && { maximumAgeHours: r.maximumAgeHours, }), @@ -258,7 +256,7 @@ const formatPolicy = (p: PolicyRow) => { ...p.versionSelectorRules.map((r) => formatPolicyRule(r.id, r.policyId, r.createdAt, { versionSelector: { - selector: JSON.parse(r.selector), + selector: r.selector, ...(r.description != null && { description: r.description }), }, }), diff --git a/apps/api/src/routes/v1/workspaces/resources.ts b/apps/api/src/routes/v1/workspaces/resources.ts index cd0b348b7..ae3a7303d 100644 --- a/apps/api/src/routes/v1/workspaces/resources.ts +++ b/apps/api/src/routes/v1/workspaces/resources.ts @@ -178,13 +178,11 @@ const updateVariablesForResource: AsyncTypedHandler< }); }; -const parseSelector = (raw: string | null | undefined) => { +const parseSelector = ( + raw: string | null | undefined, +): string | undefined => { if (raw == null || raw === "false") return undefined; - try { - return JSON.parse(raw) as Record; - } catch { - return { cel: raw }; - } + return raw; }; const getDeploymentsForResource: AsyncTypedHandler< diff --git a/apps/api/src/types/openapi.ts b/apps/api/src/types/openapi.ts index 29859e711..093d820d2 100644 --- a/apps/api/src/types/openapi.ts +++ b/apps/api/src/types/openapi.ts @@ -915,9 +915,6 @@ export interface components { /** @enum {string} */ ApprovalStatus: "approved" | "rejected"; BooleanValue: boolean; - CelSelector: { - cel: string; - }; CreateDeploymentRequest: { description?: string; jobAgentConfig?: { @@ -1198,7 +1195,8 @@ export interface components { resourceSelector?: string; }; EnvironmentProgressionRule: { - dependsOnEnvironmentSelector: components["schemas"]["Selector"]; + /** @description CEL expression to match the environment(s) that must have a successful release before this environment can proceed. */ + dependsOnEnvironmentSelector: string; /** * Format: int32 * @description Maximum age of dependency deployment before blocking progression (prevents stale promotions) @@ -1351,11 +1349,6 @@ export interface components { release: components["schemas"]["Release"]; resource?: components["schemas"]["Resource"]; }; - JsonSelector: { - json: { - [key: string]: unknown; - }; - }; LiteralValue: components["schemas"]["BooleanValue"] | components["schemas"]["NumberValue"] | components["schemas"]["IntegerValue"] | components["schemas"]["StringValue"] | components["schemas"]["ObjectValue"] | components["schemas"]["NullValue"]; MetricProvider: components["schemas"]["HTTPMetricProvider"] | components["schemas"]["SleepMetricProvider"] | components["schemas"]["DatadogMetricProvider"] | components["schemas"]["PrometheusMetricProvider"] | components["schemas"]["TerraformCloudRunMetricProvider"]; /** @enum {boolean} */ @@ -1622,7 +1615,6 @@ export interface components { /** @description Job statuses that count toward the retry limit. If null or empty, defaults to ["failure", "invalidIntegration", "invalidJobAgent"] for maxRetries > 0, or ["failure", "invalidIntegration", "invalidJobAgent", "successful"] for maxRetries = 0. Cancelled and skipped jobs never count by default (allows redeployment after cancellation). Example: ["failure", "cancelled"] will only count failed/cancelled jobs. */ retryOnStatuses?: components["schemas"]["JobStatus"][]; }; - Selector: components["schemas"]["JsonSelector"] | components["schemas"]["CelSelector"]; SensitiveValue: { valueHash: string; }; @@ -1926,7 +1918,8 @@ export interface components { VersionSelectorRule: { /** @description Human-readable description of what this version selector does. Example: "Only deploy v2.x versions to staging environments" */ description?: string; - selector: components["schemas"]["Selector"]; + /** @description CEL expression to select which versions are eligible for deployment. */ + selector: string; }; Workflow: { id: string; @@ -2009,7 +2002,8 @@ export interface components { WorkflowSelectorArrayInput: { key: string; selector: { - default?: components["schemas"]["Selector"]; + /** @description CEL expression for the default selector. */ + default?: string; /** @enum {string} */ entityType: "resource" | "environment" | "deployment"; }; diff --git a/apps/web/app/api/openapi.ts b/apps/web/app/api/openapi.ts index 4d4dc43af..093d820d2 100644 --- a/apps/web/app/api/openapi.ts +++ b/apps/web/app/api/openapi.ts @@ -628,6 +628,24 @@ export interface paths { get: operations["getResourceProviderByName"]; put?: never; post?: never; + /** Delete a resource provider by name */ + delete: operations["deleteResourceProviderByName"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/workspaces/{workspaceId}/resource-providers/name/{name}/resources": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get the resources for a resource provider */ + get: operations["getResourceProviderResources"]; + put?: never; + post?: never; delete?: never; options?: never; head?: never; @@ -897,12 +915,6 @@ export interface components { /** @enum {string} */ ApprovalStatus: "approved" | "rejected"; BooleanValue: boolean; - CelMatcher: { - cel: string; - }; - CelSelector: { - cel: string; - }; CreateDeploymentRequest: { description?: string; jobAgentConfig?: { @@ -914,7 +926,8 @@ export interface components { [key: string]: string; }; name: string; - resourceSelector?: components["schemas"]["Selector"]; + /** @description CEL expression to determine if the deployment should be used */ + resourceSelector?: string; slug: string; }; CreateDeploymentVersionRequest: { @@ -939,7 +952,8 @@ export interface components { [key: string]: string; }; name: string; - resourceSelector?: components["schemas"]["Selector"]; + /** @description CEL expression to determine if the environment should be used */ + resourceSelector?: string; }; CreatePolicyRequest: { description?: string; @@ -966,18 +980,13 @@ export interface components { versionSelector?: components["schemas"]["VersionSelectorRule"]; }; CreateRelationshipRuleRequest: { + cel: string; description?: string; - fromSelector?: components["schemas"]["Selector"]; - fromType: components["schemas"]["RelatableEntityType"]; - matcher: components["schemas"]["CelMatcher"]; metadata: { [key: string]: string; }; name: string; reference: string; - relationshipType: string; - toSelector?: components["schemas"]["Selector"]; - toType: components["schemas"]["RelatableEntityType"]; }; CreateSystemRequest: { description?: string; @@ -1066,7 +1075,8 @@ export interface components { [key: string]: string; }; name: string; - resourceSelector?: components["schemas"]["Selector"]; + /** @description CEL expression to determine if the deployment should be used */ + resourceSelector?: string; slug: string; }; DeploymentAndSystems: { @@ -1103,7 +1113,8 @@ export interface components { id: string; /** Format: int64 */ priority: number; - resourceSelector?: components["schemas"]["Selector"]; + /** @description A CEL expression to select which resources this value applies to */ + resourceSelector?: string; value: components["schemas"]["Value"]; }; DeploymentVariableValueRequestAccepted: { @@ -1180,10 +1191,12 @@ export interface components { [key: string]: string; }; name: string; - resourceSelector?: components["schemas"]["Selector"]; + /** @description CEL expression to determine if the environment should be used */ + resourceSelector?: string; }; EnvironmentProgressionRule: { - dependsOnEnvironmentSelector: components["schemas"]["Selector"]; + /** @description CEL expression to match the environment(s) that must have a successful release before this environment can proceed. */ + dependsOnEnvironmentSelector: string; /** * Format: int32 * @description Maximum age of dependency deployment before blocking progression (prevents stale promotions) @@ -1336,11 +1349,6 @@ export interface components { release: components["schemas"]["Release"]; resource?: components["schemas"]["Resource"]; }; - JsonSelector: { - json: { - [key: string]: unknown; - }; - }; LiteralValue: components["schemas"]["BooleanValue"] | components["schemas"]["NumberValue"] | components["schemas"]["IntegerValue"] | components["schemas"]["StringValue"] | components["schemas"]["ObjectValue"] | components["schemas"]["NullValue"]; MetricProvider: components["schemas"]["HTTPMetricProvider"] | components["schemas"]["SleepMetricProvider"] | components["schemas"]["DatadogMetricProvider"] | components["schemas"]["PrometheusMetricProvider"] | components["schemas"]["TerraformCloudRunMetricProvider"]; /** @enum {boolean} */ @@ -1463,22 +1471,15 @@ export interface components { path: string[]; reference: string; }; - /** @enum {string} */ - RelatableEntityType: "deployment" | "environment" | "resource"; RelationshipRule: { + cel: string; description?: string; - fromSelector?: components["schemas"]["Selector"]; - fromType: components["schemas"]["RelatableEntityType"]; id: string; - matcher: components["schemas"]["CelMatcher"]; metadata: { [key: string]: string; }; name: string; reference: string; - relationshipType: string; - toSelector?: components["schemas"]["Selector"]; - toType: components["schemas"]["RelatableEntityType"]; workspaceId: string; }; Release: { @@ -1614,7 +1615,6 @@ export interface components { /** @description Job statuses that count toward the retry limit. If null or empty, defaults to ["failure", "invalidIntegration", "invalidJobAgent"] for maxRetries > 0, or ["failure", "invalidIntegration", "invalidJobAgent", "successful"] for maxRetries = 0. Cancelled and skipped jobs never count by default (allows redeployment after cancellation). Example: ["failure", "cancelled"] will only count failed/cancelled jobs. */ retryOnStatuses?: components["schemas"]["JobStatus"][]; }; - Selector: components["schemas"]["JsonSelector"] | components["schemas"]["CelSelector"]; SensitiveValue: { valueHash: string; }; @@ -1717,7 +1717,8 @@ export interface components { [key: string]: string; }; name: string; - resourceSelector?: components["schemas"]["Selector"]; + /** @description CEL expression to determine if the deployment should be used */ + resourceSelector?: string; slug: string; }; UpsertDeploymentVariableRequest: { @@ -1730,7 +1731,8 @@ export interface components { deploymentVariableId: string; /** Format: int64 */ priority: number; - resourceSelector?: components["schemas"]["Selector"]; + /** @description A CEL expression to select which resources this value applies to */ + resourceSelector?: string; value: components["schemas"]["Value"]; }; UpsertDeploymentVersionRequest: { @@ -1756,7 +1758,8 @@ export interface components { [key: string]: string; }; name: string; - resourceSelector?: components["schemas"]["Selector"]; + /** @description CEL expression to determine if the environment should be used */ + resourceSelector?: string; }; UpsertJobAgentRequest: { config: { @@ -1796,18 +1799,13 @@ export interface components { versionSelector?: components["schemas"]["VersionSelectorRule"]; }; UpsertRelationshipRuleRequest: { + cel: string; description?: string; - fromSelector?: components["schemas"]["Selector"]; - fromType: components["schemas"]["RelatableEntityType"]; - matcher: components["schemas"]["CelMatcher"]; metadata: { [key: string]: string; }; name: string; reference: string; - relationshipType: string; - toSelector?: components["schemas"]["Selector"]; - toType: components["schemas"]["RelatableEntityType"]; }; UpsertResourceProviderRequest: { id: string; @@ -1920,7 +1918,8 @@ export interface components { VersionSelectorRule: { /** @description Human-readable description of what this version selector does. Example: "Only deploy v2.x versions to staging environments" */ description?: string; - selector: components["schemas"]["Selector"]; + /** @description CEL expression to select which versions are eligible for deployment. */ + selector: string; }; Workflow: { id: string; @@ -2003,7 +2002,8 @@ export interface components { WorkflowSelectorArrayInput: { key: string; selector: { - default?: components["schemas"]["Selector"]; + /** @description CEL expression for the default selector. */ + default?: string; /** @enum {string} */ entityType: "resource" | "environment" | "deployment"; }; @@ -4348,6 +4348,100 @@ export interface operations { }; }; }; + deleteResourceProviderByName: { + parameters: { + query?: never; + header?: never; + path: { + /** @description ID of the workspace */ + workspaceId: string; + /** @description Name of the resource provider */ + name: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Accepted response */ + 202: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ResourceProvider"]; + }; + }; + /** @description Invalid request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Resource not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; + getResourceProviderResources: { + parameters: { + query?: never; + header?: never; + path: { + /** @description ID of the workspace */ + workspaceId: string; + /** @description Name of the resource provider */ + name: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Paginated list of items */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + items: components["schemas"]["Resource"][]; + /** @description Maximum number of items returned */ + limit: number; + /** @description Number of items skipped */ + offset: number; + /** @description Total number of items available */ + total: number; + }; + }; + }; + /** @description Invalid request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + /** @description Resource not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ErrorResponse"]; + }; + }; + }; + }; setResourceProviderResources: { parameters: { query?: never; diff --git a/apps/web/app/routes/ws/deployments/page.$deploymentId.resources.tsx b/apps/web/app/routes/ws/deployments/page.$deploymentId.resources.tsx index e698e8df0..93caaebb0 100644 --- a/apps/web/app/routes/ws/deployments/page.$deploymentId.resources.tsx +++ b/apps/web/app/routes/ws/deployments/page.$deploymentId.resources.tsx @@ -22,7 +22,7 @@ const useResources = (selector: string) => { const resourcesQuery = trpc.resource.list.useQuery({ workspaceId: workspace.id, - selector: { cel: selector }, + selector, limit: 200, offset: 0, }); diff --git a/apps/web/app/routes/ws/environments/page.$environmentId.resources.tsx b/apps/web/app/routes/ws/environments/page.$environmentId.resources.tsx index 7bba13630..d07f78e71 100644 --- a/apps/web/app/routes/ws/environments/page.$environmentId.resources.tsx +++ b/apps/web/app/routes/ws/environments/page.$environmentId.resources.tsx @@ -35,7 +35,7 @@ export default function EnvironmentResources() { ); const resourcesQuery = trpc.resource.list.useQuery({ workspaceId: workspace.id, - selector: { cel: filterDebounced }, + selector: filterDebounced, limit: 200, offset: 0, }); diff --git a/apps/web/app/routes/ws/jobs/_components/ResourceFilter.tsx b/apps/web/app/routes/ws/jobs/_components/ResourceFilter.tsx index 8c742abaf..f8f58d14d 100644 --- a/apps/web/app/routes/ws/jobs/_components/ResourceFilter.tsx +++ b/apps/web/app/routes/ws/jobs/_components/ResourceFilter.tsx @@ -25,11 +25,10 @@ function useResourcesSearch() { const [celDebounced, setCelDebounced] = useState(cel); useDebounce(() => setCelDebounced(cel), 1000, [cel]); + const escaped = celDebounced.replace(/\\/g, "\\\\").replace(/'/g, "\\'"); const { data, isLoading } = trpc.resource.list.useQuery({ workspaceId: workspace.id, - selector: { - cel: `resource.name.contains('${celDebounced}') || resource.identifier.contains('${celDebounced}')`, - }, + selector: `resource.name.contains('${escaped}') || resource.identifier.contains('${escaped}')`, limit: 20, offset: 0, }); diff --git a/apps/web/app/routes/ws/resources.tsx b/apps/web/app/routes/ws/resources.tsx index 1d7921e6d..dd6316a75 100644 --- a/apps/web/app/routes/ws/resources.tsx +++ b/apps/web/app/routes/ws/resources.tsx @@ -67,7 +67,7 @@ export default function Resources() { const { data: resources } = trpc.resource.list.useQuery( { workspaceId: workspace.id, - selector: { cel: cleanedCel }, + selector: cleanedCel, kind, limit: 200, offset: 0, diff --git a/apps/workspace-engine/oapi/openapi.json b/apps/workspace-engine/oapi/openapi.json index 5112e908b..efe7f1d4e 100644 --- a/apps/workspace-engine/oapi/openapi.json +++ b/apps/workspace-engine/oapi/openapi.json @@ -98,17 +98,6 @@ ], "type": "object" }, - "CelSelector": { - "properties": { - "cel": { - "type": "string" - } - }, - "required": [ - "cel" - ], - "type": "object" - }, "DatadogMetricProvider": { "properties": { "aggregator": { @@ -223,7 +212,8 @@ "type": "string" }, "resourceSelector": { - "$ref": "#/components/schemas/Selector" + "description": "CEL expression to determine if the deployment should be used", + "type": "string" }, "slug": { "type": "string" @@ -326,7 +316,8 @@ "type": "integer" }, "resourceSelector": { - "$ref": "#/components/schemas/Selector" + "description": "CEL expression to determine if the deployment variable value should be used", + "type": "string" }, "value": { "$ref": "#/components/schemas/Value" @@ -568,7 +559,8 @@ "type": "string" }, "resourceSelector": { - "$ref": "#/components/schemas/Selector" + "description": "CEL expression to determine if the environment should be used", + "type": "string" }, "workspaceId": { "type": "string" @@ -586,7 +578,8 @@ "EnvironmentProgressionRule": { "properties": { "dependsOnEnvironmentSelector": { - "$ref": "#/components/schemas/Selector" + "description": "CEL expression to determine if the environment progression rule should be used", + "type": "string" }, "maximumAgeHours": { "description": "Maximum age of dependency deployment before blocking progression (prevents stale promotions)", @@ -1108,18 +1101,6 @@ ], "type": "object" }, - "JsonSelector": { - "properties": { - "json": { - "additionalProperties": true, - "type": "object" - } - }, - "required": [ - "json" - ], - "type": "object" - }, "LiteralValue": { "oneOf": [ { @@ -1595,7 +1576,8 @@ "type": "string" }, "fromSelector": { - "$ref": "#/components/schemas/Selector" + "description": "CEL expression to determine if the relationship rule should be used", + "type": "string" }, "fromType": { "$ref": "#/components/schemas/RelatableEntityType" @@ -1629,7 +1611,8 @@ "type": "string" }, "toSelector": { - "$ref": "#/components/schemas/Selector" + "description": "CEL expression to determine if the relationship rule should be used", + "type": "string" }, "toType": { "$ref": "#/components/schemas/RelatableEntityType" @@ -2138,16 +2121,6 @@ ], "type": "object" }, - "Selector": { - "oneOf": [ - { - "$ref": "#/components/schemas/JsonSelector" - }, - { - "$ref": "#/components/schemas/CelSelector" - } - ] - }, "SensitiveValue": { "properties": { "valueHash": { @@ -2520,7 +2493,8 @@ "type": "string" }, "selector": { - "$ref": "#/components/schemas/Selector" + "description": "CEL expression to determine if the version selector should be used", + "type": "string" } }, "required": [ @@ -2864,7 +2838,8 @@ "selector": { "properties": { "default": { - "$ref": "#/components/schemas/Selector" + "description": "CEL expression to determine if the selector array input should be used", + "type": "string" }, "entityType": { "enum": [ @@ -2933,9 +2908,13 @@ "schema": { "properties": { "resourceSelector": { - "$ref": "#/components/schemas/Selector" + "description": "CEL expression to validate.", + "type": "string" } }, + "required": [ + "resourceSelector" + ], "type": "object" } } @@ -3124,7 +3103,8 @@ "schema": { "properties": { "filter": { - "$ref": "#/components/schemas/Selector" + "description": "CEL expression to filter resources. Defaults to \"true\" (all resources).", + "type": "string" } }, "type": "object" diff --git a/apps/workspace-engine/oapi/spec/paths/resource.jsonnet b/apps/workspace-engine/oapi/spec/paths/resource.jsonnet index c5665cb99..ce3d0c8b9 100644 --- a/apps/workspace-engine/oapi/spec/paths/resource.jsonnet +++ b/apps/workspace-engine/oapi/spec/paths/resource.jsonnet @@ -74,7 +74,15 @@ local openapi = import '../lib/openapi.libsonnet'; required: true, content: { 'application/json': { - schema: { type: 'object', properties: { filter: openapi.schemaRef('Selector') } }, + schema: { + type: 'object', + properties: { + filter: { + type: 'string', + description: 'CEL expression to filter resources. Defaults to "true" (all resources).', + }, + }, + }, }, }, }, diff --git a/apps/workspace-engine/oapi/spec/paths/validate.jsonnet b/apps/workspace-engine/oapi/spec/paths/validate.jsonnet index 7ce04c9c2..c945584ec 100644 --- a/apps/workspace-engine/oapi/spec/paths/validate.jsonnet +++ b/apps/workspace-engine/oapi/spec/paths/validate.jsonnet @@ -10,8 +10,12 @@ local openapi = import '../lib/openapi.libsonnet'; 'application/json': { schema: { type: 'object', + required: ['resourceSelector'], properties: { - resourceSelector: openapi.schemaRef('Selector'), + resourceSelector: { + type: 'string', + description: 'CEL expression to validate.', + }, }, }, }, diff --git a/apps/workspace-engine/oapi/spec/schemas/core.jsonnet b/apps/workspace-engine/oapi/spec/schemas/core.jsonnet index 779c7b7b9..510c7f388 100644 --- a/apps/workspace-engine/oapi/spec/schemas/core.jsonnet +++ b/apps/workspace-engine/oapi/spec/schemas/core.jsonnet @@ -1,30 +1,6 @@ local openapi = import '../lib/openapi.libsonnet'; { - // Selector types - JsonSelector: { - type: 'object', - required: ['json'], - properties: { - json: { type: 'object', additionalProperties: true }, - }, - }, - - CelSelector: { - type: 'object', - required: ['cel'], - properties: { - cel: { type: 'string' }, - }, - }, - - Selector: { - oneOf: [ - openapi.schemaRef('JsonSelector'), - openapi.schemaRef('CelSelector'), - ], - }, - // Property matcher PropertyMatcher: { type: 'object', diff --git a/apps/workspace-engine/oapi/spec/schemas/deployments.jsonnet b/apps/workspace-engine/oapi/spec/schemas/deployments.jsonnet index d877b1baa..f3e136626 100644 --- a/apps/workspace-engine/oapi/spec/schemas/deployments.jsonnet +++ b/apps/workspace-engine/oapi/spec/schemas/deployments.jsonnet @@ -12,7 +12,7 @@ local openapi = import '../lib/openapi.libsonnet'; jobAgentId: { type: 'string' }, jobAgentConfig: openapi.schemaRef('JobAgentConfig'), jobAgents: { type: 'array', items: openapi.schemaRef('DeploymentJobAgent') }, - resourceSelector: openapi.schemaRef('Selector'), + resourceSelector: { type: 'string', description: 'CEL expression to determine if the deployment should be used' }, metadata: { type: 'object', additionalProperties: { type: 'string' } }, }, }, @@ -74,7 +74,7 @@ local openapi = import '../lib/openapi.libsonnet'; id: { type: 'string' }, deploymentVariableId: { type: 'string' }, priority: { type: 'integer', format: 'int64' }, - resourceSelector: openapi.schemaRef('Selector'), + resourceSelector: { type: 'string', description: 'CEL expression to determine if the deployment variable value should be used' }, value: openapi.schemaRef('Value'), }, }, diff --git a/apps/workspace-engine/oapi/spec/schemas/environments.jsonnet b/apps/workspace-engine/oapi/spec/schemas/environments.jsonnet index 25b730d3e..cb7e0a195 100644 --- a/apps/workspace-engine/oapi/spec/schemas/environments.jsonnet +++ b/apps/workspace-engine/oapi/spec/schemas/environments.jsonnet @@ -8,7 +8,7 @@ local openapi = import '../lib/openapi.libsonnet'; id: { type: 'string' }, name: { type: 'string' }, description: { type: 'string' }, - resourceSelector: openapi.schemaRef('Selector'), + resourceSelector: { type: 'string', description: 'CEL expression to determine if the environment should be used' }, createdAt: { type: 'string', format: 'date-time' }, metadata: { type: 'object', additionalProperties: { type: 'string' } }, workspaceId: { type: 'string' }, diff --git a/apps/workspace-engine/oapi/spec/schemas/policy.jsonnet b/apps/workspace-engine/oapi/spec/schemas/policy.jsonnet index 1e0ff99b8..439e718f7 100644 --- a/apps/workspace-engine/oapi/spec/schemas/policy.jsonnet +++ b/apps/workspace-engine/oapi/spec/schemas/policy.jsonnet @@ -75,7 +75,7 @@ local openapi = import '../lib/openapi.libsonnet'; type: 'object', required: ['selector'], properties: { - selector: openapi.schemaRef('Selector'), + selector: { type: 'string', description: 'CEL expression to determine if the version selector should be used' }, description: { type: 'string', description: 'Human-readable description of what this version selector does. Example: "Only deploy v2.x versions to staging environments"', @@ -144,7 +144,7 @@ local openapi = import '../lib/openapi.libsonnet'; type: 'object', required: ['dependsOnEnvironmentSelector'], properties: { - dependsOnEnvironmentSelector: openapi.schemaRef('Selector'), + dependsOnEnvironmentSelector: { type: 'string', description: 'CEL expression to determine if the environment progression rule should be used' }, minimumSuccessPercentage: { type: 'number', format: 'float', minimum: 0, maximum: 100, default: 100 }, successStatuses: { type: 'array', items: openapi.schemaRef('JobStatus') }, diff --git a/apps/workspace-engine/oapi/spec/schemas/relationship.jsonnet b/apps/workspace-engine/oapi/spec/schemas/relationship.jsonnet index 6e2d111ea..24bd00f06 100644 --- a/apps/workspace-engine/oapi/spec/schemas/relationship.jsonnet +++ b/apps/workspace-engine/oapi/spec/schemas/relationship.jsonnet @@ -39,9 +39,9 @@ local openapi = import '../lib/openapi.libsonnet'; description: { type: 'string' }, reference: { type: 'string' }, fromType: openapi.schemaRef('RelatableEntityType'), - fromSelector: openapi.schemaRef('Selector'), + fromSelector: { type: 'string', description: 'CEL expression to determine if the relationship rule should be used' }, toType: openapi.schemaRef('RelatableEntityType'), - toSelector: openapi.schemaRef('Selector'), + toSelector: { type: 'string', description: 'CEL expression to determine if the relationship rule should be used' }, matcher: { oneOf: [ openapi.schemaRef('CelMatcher'), diff --git a/apps/workspace-engine/oapi/spec/schemas/workflows.jsonnet b/apps/workspace-engine/oapi/spec/schemas/workflows.jsonnet index 8f7dc6931..ee29717c2 100644 --- a/apps/workspace-engine/oapi/spec/schemas/workflows.jsonnet +++ b/apps/workspace-engine/oapi/spec/schemas/workflows.jsonnet @@ -95,7 +95,7 @@ local openapi = import '../lib/openapi.libsonnet'; required: ['entityType'], properties: { entityType: { type: 'string', enum: ['resource', 'environment', 'deployment'] }, - default: openapi.schemaRef('Selector'), + default: { type: 'string', description: 'CEL expression to determine if the selector array input should be used' }, }, }, }, diff --git a/apps/workspace-engine/pkg/db/convert.go b/apps/workspace-engine/pkg/db/convert.go index 95ddd455d..fa94d3a64 100644 --- a/apps/workspace-engine/pkg/db/convert.go +++ b/apps/workspace-engine/pkg/db/convert.go @@ -3,34 +3,12 @@ package db import ( "encoding/json" + "workspace-engine/pkg/oapi" + "github.com/charmbracelet/log" "github.com/google/uuid" - "workspace-engine/pkg/oapi" ) -// celToSelector converts a raw selector string (as stored in the DB) into an -// oapi.Selector union. It first tries JSON unmarshal for forward-compatibility -// with the {"cel":"..."} format, then falls back to wrapping the string as a -// CelSelector for plain CEL expressions. Unrecognized JSON objects (e.g. legacy -// JSON selectors) produce a "false" CelSelector. -func celToSelector(raw string) oapi.Selector { - if len(raw) > 0 && raw[0] == '{' { - var sel oapi.Selector - if err := json.Unmarshal([]byte(raw), &sel); err == nil { - if cs, e := sel.AsCelSelector(); e == nil && cs.Cel != "" { - return sel - } - } - log.Warn("unrecognized JSON selector format, treating as false", "selector", raw) - var s oapi.Selector - _ = s.FromCelSelector(oapi.CelSelector{Cel: "false"}) - return s - } - var s oapi.Selector - _ = s.FromCelSelector(oapi.CelSelector{Cel: raw}) - return s -} - func ToOapiDeployment(row Deployment) *oapi.Deployment { d := &oapi.Deployment{ Id: row.ID.String(), @@ -177,9 +155,8 @@ func ToOapiPolicyWithRules(row ListPoliciesWithRulesByWorkspaceIDRow) *oapi.Poli var progs []progressionJSON _ = json.Unmarshal(row.EnvironmentProgressionRules, &progs) for _, pr := range progs { - depSelector := celToSelector(pr.DependsOnEnvironmentSelector) rule := oapi.EnvironmentProgressionRule{ - DependsOnEnvironmentSelector: depSelector, + DependsOnEnvironmentSelector: pr.DependsOnEnvironmentSelector, MaximumAgeHours: pr.MaximumAgeHours, MinimumSockTimeMinutes: pr.MinimumSoakTimeMinutes, MinimumSuccessPercentage: pr.MinimumSuccessPercentage, @@ -240,13 +217,12 @@ func ToOapiPolicyWithRules(row ListPoliciesWithRulesByWorkspaceIDRow) *oapi.Poli var selectors []selectorJSON _ = json.Unmarshal(row.VersionSelectorRules, &selectors) for _, s := range selectors { - vSelector := celToSelector(s.Selector) p.Rules = append(p.Rules, oapi.PolicyRule{ Id: s.Id, PolicyId: p.Id, VersionSelector: &oapi.VersionSelectorRule{ Description: s.Description, - Selector: vSelector, + Selector: s.Selector, }, }) } @@ -343,8 +319,7 @@ func ToOapiDeploymentVariableValue(row DeploymentVariableValue) oapi.DeploymentV _ = json.Unmarshal(row.Value, &v.Value) } if row.ResourceSelector.Valid && row.ResourceSelector.String != "" { - sel := celToSelector(row.ResourceSelector.String) - v.ResourceSelector = &sel + v.ResourceSelector = &row.ResourceSelector.String } return v } diff --git a/apps/workspace-engine/pkg/db/convert_test.go b/apps/workspace-engine/pkg/db/convert_test.go index 205501e18..ebdbf80d5 100644 --- a/apps/workspace-engine/pkg/db/convert_test.go +++ b/apps/workspace-engine/pkg/db/convert_test.go @@ -9,73 +9,8 @@ import ( "github.com/jackc/pgx/v5/pgtype" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "workspace-engine/pkg/oapi" ) -// --------------------------------------------------------------------------- -// celToSelector -// --------------------------------------------------------------------------- - -func TestCelToSelector_PlainCEL(t *testing.T) { - raw := `version.tag == 'abc123'` - sel := celToSelector(raw) - - cs, err := sel.AsCelSelector() - require.NoError(t, err) - assert.Equal(t, raw, cs.Cel) -} - -func TestCelToSelector_JSONCelFormat(t *testing.T) { - raw := `{"cel":"version.tag == 'abc123'"}` - sel := celToSelector(raw) - - cs, err := sel.AsCelSelector() - require.NoError(t, err) - assert.Equal(t, "version.tag == 'abc123'", cs.Cel) -} - -func TestCelToSelector_JSONSelectorFormat(t *testing.T) { - raw := `{"json":{"type":"comparison","operator":"equals","key":"version.tag","value":"abc123"}}` - sel := celToSelector(raw) - - cs, err := sel.AsCelSelector() - require.NoError(t, err) - assert.Equal(t, "false", cs.Cel, "legacy JSON selectors should fall back to false") -} - -func TestCelToSelector_EmptyString(t *testing.T) { - sel := celToSelector("") - - cs, err := sel.AsCelSelector() - require.NoError(t, err) - assert.Empty(t, cs.Cel) -} - -func TestCelToSelector_LiteralTrue(t *testing.T) { - sel := celToSelector("true") - - cs, err := sel.AsCelSelector() - require.NoError(t, err) - assert.Equal(t, "true", cs.Cel) -} - -func TestCelToSelector_LiteralFalse(t *testing.T) { - sel := celToSelector("false") - - cs, err := sel.AsCelSelector() - require.NoError(t, err) - assert.Equal(t, "false", cs.Cel) -} - -func TestCelToSelector_ComplexCELExpression(t *testing.T) { - raw := `resource.metadata["env"] == "prod" && version.tag != "latest"` - sel := celToSelector(raw) - - cs, err := sel.AsCelSelector() - require.NoError(t, err) - assert.Equal(t, raw, cs.Cel) -} - // --------------------------------------------------------------------------- // ToOapiEnvironment // --------------------------------------------------------------------------- @@ -210,9 +145,8 @@ func TestToOapiPolicyWithRules_VersionSelectorPlainCEL(t *testing.T) { require.Len(t, p.Rules, 1) require.NotNil(t, p.Rules[0].VersionSelector) - cs, err := p.Rules[0].VersionSelector.Selector.AsCelSelector() - require.NoError(t, err) - assert.Equal(t, celExpr, cs.Cel) + cs := p.Rules[0].VersionSelector.Selector + assert.Equal(t, celExpr, cs) } func TestToOapiPolicyWithRules_VersionSelectorJSONCelFormat(t *testing.T) { @@ -221,10 +155,6 @@ func TestToOapiPolicyWithRules_VersionSelectorJSONCelFormat(t *testing.T) { ruleID := uuid.New().String() celExpr := "version.tag == 'abc'" - var selectorJSON oapi.Selector - _ = selectorJSON.FromCelSelector(oapi.CelSelector{Cel: celExpr}) - selectorBytes, _ := selectorJSON.MarshalJSON() - row := ListPoliciesWithRulesByWorkspaceIDRow{ ID: policyID, Name: "test-policy", @@ -235,51 +165,7 @@ func TestToOapiPolicyWithRules_VersionSelectorJSONCelFormat(t *testing.T) { WorkspaceID: wsID, CreatedAt: pgtype.Timestamptz{Time: time.Now(), Valid: true}, VersionSelectorRules: mustMarshal(t, []map[string]any{ - {"id": ruleID, "description": "test", "selector": string(selectorBytes)}, - }), - ApprovalRules: []byte("[]"), - DeploymentWindowRules: []byte("[]"), - DeploymentDependencyRules: []byte("[]"), - EnvironmentProgressionRules: []byte("[]"), - GradualRolloutRules: []byte("[]"), - VersionCooldownRules: []byte("[]"), - } - - p := ToOapiPolicyWithRules(row) - require.Len(t, p.Rules, 1) - require.NotNil(t, p.Rules[0].VersionSelector) - - cs, err := p.Rules[0].VersionSelector.Selector.AsCelSelector() - require.NoError(t, err) - assert.Equal(t, celExpr, cs.Cel) -} - -func TestToOapiPolicyWithRules_VersionSelectorJSONFormat(t *testing.T) { - policyID := uuid.New() - wsID := uuid.New() - ruleID := uuid.New().String() - - jsonMap := map[string]any{ - "type": "comparison", - "operator": "equals", - "key": "tag", - "value": "v1", - } - var selectorJSON oapi.Selector - _ = selectorJSON.FromJsonSelector(oapi.JsonSelector{Json: jsonMap}) - selectorBytes, _ := selectorJSON.MarshalJSON() - - row := ListPoliciesWithRulesByWorkspaceIDRow{ - ID: policyID, - Name: "test-policy", - Selector: "true", - Metadata: map[string]string{}, - Priority: 1, - Enabled: true, - WorkspaceID: wsID, - CreatedAt: pgtype.Timestamptz{Time: time.Now(), Valid: true}, - VersionSelectorRules: mustMarshal(t, []map[string]any{ - {"id": ruleID, "description": "test", "selector": string(selectorBytes)}, + {"id": ruleID, "description": "test", "selector": celExpr}, }), ApprovalRules: []byte("[]"), DeploymentWindowRules: []byte("[]"), @@ -293,9 +179,8 @@ func TestToOapiPolicyWithRules_VersionSelectorJSONFormat(t *testing.T) { require.Len(t, p.Rules, 1) require.NotNil(t, p.Rules[0].VersionSelector) - cs, err := p.Rules[0].VersionSelector.Selector.AsCelSelector() - require.NoError(t, err) - assert.Equal(t, "false", cs.Cel, "legacy JSON selectors should fall back to false") + cs := p.Rules[0].VersionSelector.Selector + assert.Equal(t, celExpr, cs) } // --------------------------------------------------------------------------- @@ -332,48 +217,8 @@ func TestToOapiPolicyWithRules_EnvironmentProgressionPlainCEL(t *testing.T) { require.Len(t, p.Rules, 1) require.NotNil(t, p.Rules[0].EnvironmentProgression) - cs, err := p.Rules[0].EnvironmentProgression.DependsOnEnvironmentSelector.AsCelSelector() - require.NoError(t, err) - assert.Equal(t, celExpr, cs.Cel) -} - -func TestToOapiPolicyWithRules_EnvironmentProgressionJSONCelFormat(t *testing.T) { - policyID := uuid.New() - wsID := uuid.New() - ruleID := uuid.New().String() - celExpr := `environment.name == "staging"` - - var selectorJSON oapi.Selector - _ = selectorJSON.FromCelSelector(oapi.CelSelector{Cel: celExpr}) - selectorBytes, _ := selectorJSON.MarshalJSON() - - row := ListPoliciesWithRulesByWorkspaceIDRow{ - ID: policyID, - Name: "test-policy", - Selector: "true", - Metadata: map[string]string{}, - Priority: 1, - Enabled: true, - WorkspaceID: wsID, - CreatedAt: pgtype.Timestamptz{Time: time.Now(), Valid: true}, - EnvironmentProgressionRules: mustMarshal(t, []map[string]any{ - {"id": ruleID, "dependsOnEnvironmentSelector": string(selectorBytes)}, - }), - ApprovalRules: []byte("[]"), - DeploymentWindowRules: []byte("[]"), - DeploymentDependencyRules: []byte("[]"), - GradualRolloutRules: []byte("[]"), - VersionCooldownRules: []byte("[]"), - VersionSelectorRules: []byte("[]"), - } - - p := ToOapiPolicyWithRules(row) - require.Len(t, p.Rules, 1) - require.NotNil(t, p.Rules[0].EnvironmentProgression) - - cs, err := p.Rules[0].EnvironmentProgression.DependsOnEnvironmentSelector.AsCelSelector() - require.NoError(t, err) - assert.Equal(t, celExpr, cs.Cel) + cs := p.Rules[0].EnvironmentProgression.DependsOnEnvironmentSelector + assert.Equal(t, celExpr, cs) } // --------------------------------------------------------------------------- @@ -395,65 +240,7 @@ func TestToOapiDeploymentVariableValue_PlainCELSelector(t *testing.T) { v := ToOapiDeploymentVariableValue(row) require.NotNil(t, v.ResourceSelector) - - cs, err := v.ResourceSelector.AsCelSelector() - require.NoError(t, err) - assert.Equal(t, celExpr, cs.Cel) -} - -func TestToOapiDeploymentVariableValue_JSONCelSelector(t *testing.T) { - id := uuid.New() - dvID := uuid.New() - celExpr := `resource.kind == "Cluster"` - - var sel oapi.Selector - _ = sel.FromCelSelector(oapi.CelSelector{Cel: celExpr}) - selectorBytes, _ := sel.MarshalJSON() - - row := DeploymentVariableValue{ - ID: id, - DeploymentVariableID: dvID, - Value: []byte(`{"string":"hello"}`), - ResourceSelector: pgtype.Text{String: string(selectorBytes), Valid: true}, - Priority: 1, - } - - v := ToOapiDeploymentVariableValue(row) - require.NotNil(t, v.ResourceSelector) - - cs, err := v.ResourceSelector.AsCelSelector() - require.NoError(t, err) - assert.Equal(t, celExpr, cs.Cel) -} - -func TestToOapiDeploymentVariableValue_JSONSelector(t *testing.T) { - id := uuid.New() - dvID := uuid.New() - - jsonMap := map[string]any{ - "type": "comparison", - "operator": "equals", - "key": "kind", - "value": "Cluster", - } - var sel oapi.Selector - _ = sel.FromJsonSelector(oapi.JsonSelector{Json: jsonMap}) - selectorBytes, _ := sel.MarshalJSON() - - row := DeploymentVariableValue{ - ID: id, - DeploymentVariableID: dvID, - Value: []byte(`{"string":"hello"}`), - ResourceSelector: pgtype.Text{String: string(selectorBytes), Valid: true}, - Priority: 1, - } - - v := ToOapiDeploymentVariableValue(row) - require.NotNil(t, v.ResourceSelector) - - cs, err := v.ResourceSelector.AsCelSelector() - require.NoError(t, err) - assert.Equal(t, "false", cs.Cel, "legacy JSON selectors should fall back to false") + assert.Equal(t, celExpr, *v.ResourceSelector) } func TestToOapiDeploymentVariableValue_EmptySelector(t *testing.T) { diff --git a/apps/workspace-engine/pkg/oapi/hash.go b/apps/workspace-engine/pkg/oapi/hash.go index 83af54c9d..c531a5f0a 100644 --- a/apps/workspace-engine/pkg/oapi/hash.go +++ b/apps/workspace-engine/pkg/oapi/hash.go @@ -18,19 +18,8 @@ func fnv64a(data []byte) uint64 { return hash } -// Hash returns a fast, unique identifier for the selector based on its JSON content. -// Uses FNV-1a hashing for speed - suitable for cache keys. -func (s *Selector) Hash() string { - if s == nil { - return "" - } - // MarshalJSON returns the union field directly - data, err := s.MarshalJSON() - if err != nil { - return "" - } - hash := fnv64a(data) - // Encode as hex string (16 chars) - faster than fmt.Sprintf +func SelectorHash(selector string) string { + hash := fnv64a([]byte(selector)) var buf [16]byte binary.BigEndian.PutUint64(buf[:8], hash) const hextable = "0123456789abcdef" diff --git a/apps/workspace-engine/pkg/oapi/hash_test.go b/apps/workspace-engine/pkg/oapi/hash_test.go index 2e654a535..f90c4820d 100644 --- a/apps/workspace-engine/pkg/oapi/hash_test.go +++ b/apps/workspace-engine/pkg/oapi/hash_test.go @@ -4,87 +4,49 @@ import ( "testing" ) -func TestSelector_Hash(t *testing.T) { - t.Run("nil selector returns empty string", func(t *testing.T) { - var s *Selector - if got := s.Hash(); got != "" { - t.Errorf("Hash() = %q, want empty string", got) +func TestSelectorHash(t *testing.T) { + t.Run("empty string returns consistent hash", func(t *testing.T) { + hash := SelectorHash("") + if len(hash) != 16 { + t.Errorf("SelectorHash() length = %d, want 16", len(hash)) } }) - t.Run("json selector produces consistent hash", func(t *testing.T) { - s := &Selector{} - _ = s.FromJsonSelector(JsonSelector{ - Json: map[string]any{ - "type": "Resource", - "key": "metadata.name", - }, - }) - - hash1 := s.Hash() - hash2 := s.Hash() - - if hash1 != hash2 { - t.Errorf("Hash() not deterministic: %q != %q", hash1, hash2) - } - if len(hash1) != 16 { - t.Errorf("Hash() length = %d, want 16", len(hash1)) - } - }) - - t.Run("cel selector produces consistent hash", func(t *testing.T) { - s := &Selector{} - _ = s.FromCelSelector(CelSelector{ - Cel: "resource.metadata.name == 'test'", - }) - - hash1 := s.Hash() - hash2 := s.Hash() + t.Run("produces consistent hash", func(t *testing.T) { + hash1 := SelectorHash("resource.metadata.name == 'test'") + hash2 := SelectorHash("resource.metadata.name == 'test'") if hash1 != hash2 { - t.Errorf("Hash() not deterministic: %q != %q", hash1, hash2) + t.Errorf("SelectorHash() not deterministic: %q != %q", hash1, hash2) } if len(hash1) != 16 { - t.Errorf("Hash() length = %d, want 16", len(hash1)) + t.Errorf("SelectorHash() length = %d, want 16", len(hash1)) } }) t.Run("different selectors produce different hashes", func(t *testing.T) { - s1 := &Selector{} - _ = s1.FromJsonSelector(JsonSelector{ - Json: map[string]any{"key": "value1"}, - }) + h1 := SelectorHash("resource.kind == 'Pod'") + h2 := SelectorHash("resource.kind == 'Service'") - s2 := &Selector{} - _ = s2.FromJsonSelector(JsonSelector{ - Json: map[string]any{"key": "value2"}, - }) - - if s1.Hash() == s2.Hash() { + if h1 == h2 { t.Error("Different selectors should produce different hashes") } }) t.Run("same content produces same hash", func(t *testing.T) { - s1 := &Selector{} - _ = s1.FromCelSelector(CelSelector{Cel: "true"}) - - s2 := &Selector{} - _ = s2.FromCelSelector(CelSelector{Cel: "true"}) + h1 := SelectorHash("true") + h2 := SelectorHash("true") - if s1.Hash() != s2.Hash() { - t.Errorf("Same content should produce same hash: %q != %q", s1.Hash(), s2.Hash()) + if h1 != h2 { + t.Errorf("Same content should produce same hash: %q != %q", h1, h2) } }) t.Run("hash contains only hex characters", func(t *testing.T) { - s := &Selector{} - _ = s.FromCelSelector(CelSelector{Cel: "resource.kind == 'Pod'"}) - - hash := s.Hash() + hash := SelectorHash("resource.kind == 'Pod'") for i, c := range hash { if (c < '0' || c > '9') && (c < 'a' || c > 'f') { - t.Errorf("Hash() contains non-hex char %q at position %d", c, i) + t.Errorf("SelectorHash() contains non-hex char %q at position %d", c, i) } } }) @@ -116,31 +78,16 @@ func TestFnv64a(t *testing.T) { }) } -func BenchmarkSelector_Hash(b *testing.B) { - s := &Selector{} - _ = s.FromJsonSelector(JsonSelector{ - Json: map[string]any{ - "type": "Resource", - "conditions": []any{ - map[string]any{"key": "metadata.name", "operator": "equals", "value": "test"}, - map[string]any{ - "key": "metadata.namespace", - "operator": "equals", - "value": "default", - }, - }, - }, - }) - +func BenchmarkSelectorHash(b *testing.B) { b.ReportAllocs() for b.Loop() { - _ = s.Hash() + _ = SelectorHash("resource.kind == 'Pod' && resource.metadata.namespace == 'default'") } } func BenchmarkFnv64a(b *testing.B) { data := []byte( - `{"type":"Resource","conditions":[{"key":"metadata.name","operator":"equals","value":"test"}]}`, + `resource.kind == 'Pod' && resource.metadata.namespace == 'default'`, ) b.ReportAllocs() diff --git a/apps/workspace-engine/pkg/oapi/merge.go b/apps/workspace-engine/pkg/oapi/merge.go index ff844c769..fbf3ab3f7 100644 --- a/apps/workspace-engine/pkg/oapi/merge.go +++ b/apps/workspace-engine/pkg/oapi/merge.go @@ -12,7 +12,7 @@ func DeepMergeConfigs(configs ...JobAgentConfig) JobAgentConfig { return out } -func deepMergeInto(dst, src map[string]interface{}) { +func deepMergeInto(dst, src map[string]any) { for k, srcVal := range src { dstVal, exists := dst[k] if !exists { @@ -20,8 +20,8 @@ func deepMergeInto(dst, src map[string]interface{}) { continue } - dstMap, dstOK := dstVal.(map[string]interface{}) - srcMap, srcOK := srcVal.(map[string]interface{}) + dstMap, dstOK := dstVal.(map[string]any) + srcMap, srcOK := srcVal.(map[string]any) if dstOK && srcOK { deepMergeInto(dstMap, srcMap) continue diff --git a/apps/workspace-engine/pkg/oapi/oapi.gen.go b/apps/workspace-engine/pkg/oapi/oapi.gen.go index 5e254ffc2..5a50bf0e9 100644 --- a/apps/workspace-engine/pkg/oapi/oapi.gen.go +++ b/apps/workspace-engine/pkg/oapi/oapi.gen.go @@ -257,11 +257,6 @@ type CelMatcher struct { Cel string `json:"cel"` } -// CelSelector defines model for CelSelector. -type CelSelector struct { - Cel string `json:"cel"` -} - // DatadogMetricProvider defines model for DatadogMetricProvider. type DatadogMetricProvider struct { // Aggregator Datadog aggregator @@ -300,15 +295,17 @@ type DeployDecision struct { // Deployment defines model for Deployment. type Deployment struct { - Description *string `json:"description,omitempty"` - Id string `json:"id"` - JobAgentConfig JobAgentConfig `json:"jobAgentConfig"` - JobAgentId *string `json:"jobAgentId,omitempty"` - JobAgents *[]DeploymentJobAgent `json:"jobAgents,omitempty"` - Metadata map[string]string `json:"metadata"` - Name string `json:"name"` - ResourceSelector *Selector `json:"resourceSelector,omitempty"` - Slug string `json:"slug"` + Description *string `json:"description,omitempty"` + Id string `json:"id"` + JobAgentConfig JobAgentConfig `json:"jobAgentConfig"` + JobAgentId *string `json:"jobAgentId,omitempty"` + JobAgents *[]DeploymentJobAgent `json:"jobAgents,omitempty"` + Metadata map[string]string `json:"metadata"` + Name string `json:"name"` + + // ResourceSelector CEL expression to determine if the deployment should be used + ResourceSelector *string `json:"resourceSelector,omitempty"` + Slug string `json:"slug"` } // DeploymentAndSystems defines model for DeploymentAndSystems. @@ -343,11 +340,13 @@ type DeploymentVariable struct { // DeploymentVariableValue defines model for DeploymentVariableValue. type DeploymentVariableValue struct { - DeploymentVariableId string `json:"deploymentVariableId"` - Id string `json:"id"` - Priority int64 `json:"priority"` - ResourceSelector *Selector `json:"resourceSelector,omitempty"` - Value Value `json:"value"` + DeploymentVariableId string `json:"deploymentVariableId"` + Id string `json:"id"` + Priority int64 `json:"priority"` + + // ResourceSelector CEL expression to determine if the deployment variable value should be used + ResourceSelector *string `json:"resourceSelector,omitempty"` + Value Value `json:"value"` } // DeploymentVariableWithValues defines model for DeploymentVariableWithValues. @@ -423,18 +422,21 @@ type EntityRelation struct { // Environment defines model for Environment. type Environment struct { - CreatedAt time.Time `json:"createdAt"` - Description *string `json:"description,omitempty"` - Id string `json:"id"` - Metadata map[string]string `json:"metadata"` - Name string `json:"name"` - ResourceSelector *Selector `json:"resourceSelector,omitempty"` - WorkspaceId string `json:"workspaceId"` + CreatedAt time.Time `json:"createdAt"` + Description *string `json:"description,omitempty"` + Id string `json:"id"` + Metadata map[string]string `json:"metadata"` + Name string `json:"name"` + + // ResourceSelector CEL expression to determine if the environment should be used + ResourceSelector *string `json:"resourceSelector,omitempty"` + WorkspaceId string `json:"workspaceId"` } // EnvironmentProgressionRule defines model for EnvironmentProgressionRule. type EnvironmentProgressionRule struct { - DependsOnEnvironmentSelector Selector `json:"dependsOnEnvironmentSelector"` + // DependsOnEnvironmentSelector CEL expression to determine if the environment progression rule should be used + DependsOnEnvironmentSelector string `json:"dependsOnEnvironmentSelector"` // MaximumAgeHours Maximum age of dependency deployment before blocking progression (prevents stale promotions) MaximumAgeHours *int32 `json:"maximumAgeHours,omitempty"` @@ -453,14 +455,16 @@ type EnvironmentSummary struct { // EnvironmentWithSystems defines model for EnvironmentWithSystems. type EnvironmentWithSystems struct { - CreatedAt time.Time `json:"createdAt"` - Description *string `json:"description,omitempty"` - Id string `json:"id"` - Metadata map[string]string `json:"metadata"` - Name string `json:"name"` - ResourceSelector *Selector `json:"resourceSelector,omitempty"` - Systems []System `json:"systems"` - WorkspaceId string `json:"workspaceId"` + CreatedAt time.Time `json:"createdAt"` + Description *string `json:"description,omitempty"` + Id string `json:"id"` + Metadata map[string]string `json:"metadata"` + Name string `json:"name"` + + // ResourceSelector CEL expression to determine if the environment should be used + ResourceSelector *string `json:"resourceSelector,omitempty"` + Systems []System `json:"systems"` + WorkspaceId string `json:"workspaceId"` } // ErrorResponse defines model for ErrorResponse. @@ -643,11 +647,6 @@ type JobWithVerifications struct { Verifications []JobVerification `json:"verifications"` } -// JsonSelector defines model for JsonSelector. -type JsonSelector struct { - Json map[string]interface{} `json:"json"` -} - // LiteralValue defines model for LiteralValue. type LiteralValue struct { union json.RawMessage @@ -838,8 +837,10 @@ type RelationDirection string // RelationshipRule defines model for RelationshipRule. type RelationshipRule struct { - Description *string `json:"description,omitempty"` - FromSelector *Selector `json:"fromSelector,omitempty"` + Description *string `json:"description,omitempty"` + + // FromSelector CEL expression to determine if the relationship rule should be used + FromSelector *string `json:"fromSelector,omitempty"` FromType RelatableEntityType `json:"fromType"` Id string `json:"id"` Matcher RelationshipRule_Matcher `json:"matcher"` @@ -847,9 +848,11 @@ type RelationshipRule struct { Name string `json:"name"` Reference string `json:"reference"` RelationshipType string `json:"relationshipType"` - ToSelector *Selector `json:"toSelector,omitempty"` - ToType RelatableEntityType `json:"toType"` - WorkspaceId string `json:"workspaceId"` + + // ToSelector CEL expression to determine if the relationship rule should be used + ToSelector *string `json:"toSelector,omitempty"` + ToType RelatableEntityType `json:"toType"` + WorkspaceId string `json:"workspaceId"` } // RelationshipRule_Matcher defines model for RelationshipRule.Matcher. @@ -1038,11 +1041,6 @@ type RuleEvaluation struct { // RuleEvaluationActionType Type of action required type RuleEvaluationActionType string -// Selector defines model for Selector. -type Selector struct { - union json.RawMessage -} - // SensitiveValue defines model for SensitiveValue. type SensitiveValue struct { ValueHash string `json:"valueHash"` @@ -1235,8 +1233,10 @@ type VersionCooldownRule struct { // VersionSelectorRule defines model for VersionSelectorRule. type VersionSelectorRule struct { // Description Human-readable description of what this version selector does. Example: "Only deploy v2.x versions to staging environments" - Description *string `json:"description,omitempty"` - Selector Selector `json:"selector"` + Description *string `json:"description,omitempty"` + + // Selector CEL expression to determine if the version selector should be used + Selector string `json:"selector"` } // VersionSummary defines model for VersionSummary. @@ -1384,7 +1384,8 @@ type WorkflowRunWithJobs struct { type WorkflowSelectorArrayInput struct { Key string `json:"key"` Selector struct { - Default *Selector `json:"default,omitempty"` + // Default CEL expression to determine if the selector array input should be used + Default *string `json:"default,omitempty"` EntityType WorkflowSelectorArrayInputSelectorEntityType `json:"entityType"` } `json:"selector"` Type WorkflowSelectorArrayInputType `json:"type"` @@ -1408,7 +1409,8 @@ type WorkflowStringInputType string // ValidateResourceSelectorJSONBody defines parameters for ValidateResourceSelector. type ValidateResourceSelectorJSONBody struct { - ResourceSelector *Selector `json:"resourceSelector,omitempty"` + // ResourceSelector CEL expression to validate. + ResourceSelector string `json:"resourceSelector"` } // ComputeAggergateJSONBody defines parameters for ComputeAggergate. @@ -1426,7 +1428,8 @@ type ComputeAggergateJSONBody struct { // QueryResourcesJSONBody defines parameters for QueryResources. type QueryResourcesJSONBody struct { - Filter *Selector `json:"filter,omitempty"` + // Filter CEL expression to filter resources. Defaults to "true" (all resources). + Filter *string `json:"filter,omitempty"` } // QueryResourcesParams defines parameters for QueryResources. @@ -2093,68 +2096,6 @@ func (t *RelationshipRule_Matcher) UnmarshalJSON(b []byte) error { return err } -// AsJsonSelector returns the union data inside the Selector as a JsonSelector -func (t Selector) AsJsonSelector() (JsonSelector, error) { - var body JsonSelector - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromJsonSelector overwrites any union data inside the Selector as the provided JsonSelector -func (t *Selector) FromJsonSelector(v JsonSelector) error { - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeJsonSelector performs a merge with any union data inside the Selector, using the provided JsonSelector -func (t *Selector) MergeJsonSelector(v JsonSelector) error { - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -// AsCelSelector returns the union data inside the Selector as a CelSelector -func (t Selector) AsCelSelector() (CelSelector, error) { - var body CelSelector - err := json.Unmarshal(t.union, &body) - return body, err -} - -// FromCelSelector overwrites any union data inside the Selector as the provided CelSelector -func (t *Selector) FromCelSelector(v CelSelector) error { - b, err := json.Marshal(v) - t.union = b - return err -} - -// MergeCelSelector performs a merge with any union data inside the Selector, using the provided CelSelector -func (t *Selector) MergeCelSelector(v CelSelector) error { - b, err := json.Marshal(v) - if err != nil { - return err - } - - merged, err := runtime.JSONMerge(t.union, b) - t.union = merged - return err -} - -func (t Selector) MarshalJSON() ([]byte, error) { - b, err := t.union.MarshalJSON() - return b, err -} - -func (t *Selector) UnmarshalJSON(b []byte) error { - err := t.union.UnmarshalJSON(b) - return err -} - // AsLiteralValue returns the union data inside the Value as a LiteralValue func (t Value) AsLiteralValue() (LiteralValue, error) { var body LiteralValue diff --git a/apps/workspace-engine/pkg/selector/langs/jsonselector/unknown/unknown.go b/apps/workspace-engine/pkg/selector/langs/jsonselector/unknown/unknown.go index 7a82f4463..da47b0d72 100644 --- a/apps/workspace-engine/pkg/selector/langs/jsonselector/unknown/unknown.go +++ b/apps/workspace-engine/pkg/selector/langs/jsonselector/unknown/unknown.go @@ -4,7 +4,6 @@ import ( "fmt" "github.com/goccy/go-json" - "workspace-engine/pkg/oapi" ) var propertyAliases = map[string]string{ @@ -63,17 +62,3 @@ func ParseFromMap(selectorMap map[string]any) (UnknownCondition, error) { return condition, nil } - -// SelectorToUnknownCondition converts a protobuf Selector to an UnknownCondition. -func SelectorToUnknownCondition(selector *oapi.Selector) (UnknownCondition, error) { - if selector == nil { - return UnknownCondition{}, fmt.Errorf("selector is nil") - } - - jsonSelector, err := selector.AsJsonSelector() - if err != nil { - return UnknownCondition{}, err - } - - return ParseFromMap(jsonSelector.Json) -} diff --git a/apps/workspace-engine/pkg/selector/match.go b/apps/workspace-engine/pkg/selector/match.go index d63da4987..9510531bc 100644 --- a/apps/workspace-engine/pkg/selector/match.go +++ b/apps/workspace-engine/pkg/selector/match.go @@ -2,15 +2,13 @@ package selector import ( "context" - "fmt" "time" - "github.com/dgraph-io/ristretto/v2" "workspace-engine/pkg/oapi" "workspace-engine/pkg/selector/langs/cel" - "workspace-engine/pkg/selector/langs/jsonselector" - "workspace-engine/pkg/selector/langs/jsonselector/unknown" "workspace-engine/pkg/selector/langs/util" + + "github.com/dgraph-io/ristretto/v2" ) var matchCache, _ = ristretto.NewCache(&ristretto.Config[string, bool]{ @@ -33,32 +31,8 @@ func (y *YesMatchableCondition) Matches(entity any) (bool, error) { return true, nil } -func Matchable(ctx context.Context, selector *oapi.Selector) (util.MatchableCondition, error) { - jsonSelector, err := selector.AsJsonSelector() - if err == nil && len(jsonSelector.Json) != 0 { - unknownCondition, err := unknown.ParseFromMap(jsonSelector.Json) - if err != nil { - return &NoMatchableCondition{}, err - } - - condition, err := jsonselector.ConvertToSelector(ctx, unknownCondition) - if err != nil { - return &NoMatchableCondition{}, err - } - - return condition, nil - } - - cselSelector, err := selector.AsCelSelector() - if err != nil { - return &NoMatchableCondition{}, fmt.Errorf("selector is not a cel or json selector") - } - - if cselSelector.Cel == "" { - return &NoMatchableCondition{}, fmt.Errorf("cel selector is empty") - } - - condition, err := cel.Compile(cselSelector.Cel) +func Matchable(ctx context.Context, selector string) (util.MatchableCondition, error) { + condition, err := cel.Compile(selector) if err != nil { return &NoMatchableCondition{}, err } @@ -101,13 +75,13 @@ func entityCacheKey(item any) string { return "" } -func Match(ctx context.Context, selector *oapi.Selector, item any) (bool, error) { - if selector == nil { +func Match(ctx context.Context, selector string, item any) (bool, error) { + if selector == "" || selector == "false" { return false, nil } // Try cache lookup - selectorHash := selector.Hash() + selectorHash := oapi.SelectorHash(selector) entityKey := entityCacheKey(item) if selectorHash != "" && entityKey != "" { cacheKey := selectorHash + ":" + entityKey diff --git a/apps/workspace-engine/pkg/selector/match_test.go b/apps/workspace-engine/pkg/selector/match_test.go index 86e6ca6b4..84854082c 100644 --- a/apps/workspace-engine/pkg/selector/match_test.go +++ b/apps/workspace-engine/pkg/selector/match_test.go @@ -2,72 +2,23 @@ package selector import ( "context" - "encoding/json" "testing" "time" "workspace-engine/pkg/oapi" - "workspace-engine/pkg/selector/langs/jsonselector/unknown" ) -// Helper function to create a JSON selector from an unknown condition. -func createJsonSelector(t *testing.T, condition unknown.UnknownCondition) *oapi.Selector { - t.Helper() - - jsonBytes, err := json.Marshal(condition) - if err != nil { - t.Fatalf("Failed to marshal condition: %v", err) - } - - var conditionMap map[string]any - if err := json.Unmarshal(jsonBytes, &conditionMap); err != nil { - t.Fatalf("Failed to unmarshal condition: %v", err) - } - - selector := &oapi.Selector{} - if err := selector.FromJsonSelector(oapi.JsonSelector{Json: conditionMap}); err != nil { - t.Fatalf("Failed to create JSON selector: %v", err) - } - return selector -} - -// Helper function to create a CEL selector. -func createCelSelector(t *testing.T, expression string) *oapi.Selector { - t.Helper() - - selector := &oapi.Selector{} - if err := selector.FromCelSelector(oapi.CelSelector{Cel: expression}); err != nil { - t.Fatalf("Failed to create CEL selector: %v", err) - } - return selector -} - -// Helper function to create an empty JSON selector. -func createEmptyJsonSelector(t *testing.T) *oapi.Selector { - t.Helper() - - selector := &oapi.Selector{} - if err := selector.FromJsonSelector(oapi.JsonSelector{Json: map[string]any{}}); err != nil { - t.Fatalf("Failed to create empty JSON selector: %v", err) - } - return selector -} - -func TestMatch_JsonSelector_Resource(t *testing.T) { +func TestMatch_Resource_Properties(t *testing.T) { tests := []struct { name string - condition unknown.UnknownCondition + selector string resource oapi.Resource wantMatch bool wantErr bool }{ { - name: "name contains match", - condition: unknown.UnknownCondition{ - Property: "Name", - Operator: "contains", - Value: "production", - }, + name: "name contains match", + selector: "resource.name.contains('production')", resource: oapi.Resource{ Id: "1", Name: "production-server", @@ -77,12 +28,8 @@ func TestMatch_JsonSelector_Resource(t *testing.T) { wantErr: false, }, { - name: "name contains no match", - condition: unknown.UnknownCondition{ - Property: "Name", - Operator: "contains", - Value: "production", - }, + name: "name contains no match", + selector: "resource.name.contains('production')", resource: oapi.Resource{ Id: "2", Name: "staging-server", @@ -92,12 +39,8 @@ func TestMatch_JsonSelector_Resource(t *testing.T) { wantErr: false, }, { - name: "kind equals match", - condition: unknown.UnknownCondition{ - Property: "Kind", - Operator: "equals", - Value: "database", - }, + name: "kind equals match", + selector: "resource.kind == 'database'", resource: oapi.Resource{ Id: "3", Name: "postgres", @@ -107,12 +50,8 @@ func TestMatch_JsonSelector_Resource(t *testing.T) { wantErr: false, }, { - name: "identifier starts-with match", - condition: unknown.UnknownCondition{ - Property: "Identifier", - Operator: "starts-with", - Value: "k8s-", - }, + name: "identifier starts-with match", + selector: "resource.identifier.startsWith('k8s-')", resource: oapi.Resource{ Id: "4", Identifier: "k8s-cluster-prod", @@ -123,13 +62,8 @@ func TestMatch_JsonSelector_Resource(t *testing.T) { wantErr: false, }, { - name: "metadata equals match", - condition: unknown.UnknownCondition{ - Property: "metadata", - Operator: "equals", - Value: "production", - MetadataKey: "env", - }, + name: "metadata equals match", + selector: "resource.metadata['env'] == 'production'", resource: oapi.Resource{ Id: "5", Name: "api-server", @@ -141,13 +75,8 @@ func TestMatch_JsonSelector_Resource(t *testing.T) { wantErr: false, }, { - name: "metadata equals no match", - condition: unknown.UnknownCondition{ - Property: "metadata", - Operator: "equals", - Value: "production", - MetadataKey: "env", - }, + name: "metadata equals no match", + selector: "resource.metadata['env'] == 'production'", resource: oapi.Resource{ Id: "6", Name: "api-server", @@ -159,22 +88,8 @@ func TestMatch_JsonSelector_Resource(t *testing.T) { wantErr: false, }, { - name: "AND condition all match", - condition: unknown.UnknownCondition{ - Operator: "and", - Conditions: []unknown.UnknownCondition{ - { - Property: "Name", - Operator: "contains", - Value: "prod", - }, - { - Property: "Kind", - Operator: "equals", - Value: "service", - }, - }, - }, + name: "AND condition all match", + selector: "resource.name.contains('prod') && resource.kind == 'service'", resource: oapi.Resource{ Id: "7", Name: "prod-api", @@ -184,22 +99,8 @@ func TestMatch_JsonSelector_Resource(t *testing.T) { wantErr: false, }, { - name: "AND condition one does not match", - condition: unknown.UnknownCondition{ - Operator: "and", - Conditions: []unknown.UnknownCondition{ - { - Property: "Name", - Operator: "contains", - Value: "prod", - }, - { - Property: "Kind", - Operator: "equals", - Value: "service", - }, - }, - }, + name: "AND condition one does not match", + selector: "resource.name.contains('prod') && resource.kind == 'service'", resource: oapi.Resource{ Id: "8", Name: "prod-api", @@ -209,22 +110,8 @@ func TestMatch_JsonSelector_Resource(t *testing.T) { wantErr: false, }, { - name: "OR condition one matches", - condition: unknown.UnknownCondition{ - Operator: "or", - Conditions: []unknown.UnknownCondition{ - { - Property: "Name", - Operator: "contains", - Value: "staging", - }, - { - Property: "Kind", - Operator: "equals", - Value: "service", - }, - }, - }, + name: "OR condition one matches", + selector: "resource.name.contains('staging') || resource.kind == 'service'", resource: oapi.Resource{ Id: "9", Name: "prod-api", @@ -234,22 +121,8 @@ func TestMatch_JsonSelector_Resource(t *testing.T) { wantErr: false, }, { - name: "OR condition none match", - condition: unknown.UnknownCondition{ - Operator: "or", - Conditions: []unknown.UnknownCondition{ - { - Property: "Name", - Operator: "contains", - Value: "staging", - }, - { - Property: "Kind", - Operator: "equals", - Value: "database", - }, - }, - }, + name: "OR condition none match", + selector: "resource.name.contains('staging') || resource.kind == 'database'", resource: oapi.Resource{ Id: "10", Name: "prod-api", @@ -263,9 +136,8 @@ func TestMatch_JsonSelector_Resource(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ctx := context.Background() - selector := createJsonSelector(t, tt.condition) - match, err := Match(ctx, selector, tt.resource) + match, err := Match(ctx, tt.selector, tt.resource) if (err != nil) != tt.wantErr { t.Errorf("Match() error = %v, wantErr %v", err, tt.wantErr) @@ -279,21 +151,17 @@ func TestMatch_JsonSelector_Resource(t *testing.T) { } } -func TestMatch_JsonSelector_Deployment(t *testing.T) { +func TestMatch_Deployment_Properties(t *testing.T) { tests := []struct { name string - condition unknown.UnknownCondition + selector string deployment oapi.Deployment wantMatch bool wantErr bool }{ { - name: "deployment name contains match", - condition: unknown.UnknownCondition{ - Property: "Name", - Operator: "contains", - Value: "api", - }, + name: "deployment name contains match", + selector: "deployment.name.contains('api')", deployment: oapi.Deployment{ Id: "1", Name: "api-deployment", @@ -306,12 +174,8 @@ func TestMatch_JsonSelector_Deployment(t *testing.T) { wantErr: false, }, { - name: "deployment slug starts-with match", - condition: unknown.UnknownCondition{ - Property: "Slug", - Operator: "starts-with", - Value: "prod-", - }, + name: "deployment slug starts-with match", + selector: "deployment.slug.startsWith('prod-')", deployment: oapi.Deployment{ Id: "2", Name: "Production API", @@ -322,12 +186,8 @@ func TestMatch_JsonSelector_Deployment(t *testing.T) { wantErr: false, }, { - name: "deployment slug starts-with no match", - condition: unknown.UnknownCondition{ - Property: "Slug", - Operator: "starts-with", - Value: "prod-", - }, + name: "deployment slug starts-with no match", + selector: "deployment.slug.startsWith('prod-')", deployment: oapi.Deployment{ Id: "3", Name: "Staging API", @@ -342,9 +202,8 @@ func TestMatch_JsonSelector_Deployment(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ctx := context.Background() - selector := createJsonSelector(t, tt.condition) - match, err := Match(ctx, selector, tt.deployment) + match, err := Match(ctx, tt.selector, tt.deployment) if (err != nil) != tt.wantErr { t.Errorf("Match() error = %v, wantErr %v", err, tt.wantErr) @@ -358,21 +217,17 @@ func TestMatch_JsonSelector_Deployment(t *testing.T) { } } -func TestMatch_JsonSelector_Environment(t *testing.T) { +func TestMatch_Environment_Properties(t *testing.T) { tests := []struct { name string - condition unknown.UnknownCondition + selector string environment oapi.Environment wantMatch bool wantErr bool }{ { - name: "environment name equals match", - condition: unknown.UnknownCondition{ - Property: "Name", - Operator: "equals", - Value: "production", - }, + name: "environment name equals match", + selector: "environment.name == 'production'", environment: oapi.Environment{ Id: "1", Name: "production", @@ -382,12 +237,8 @@ func TestMatch_JsonSelector_Environment(t *testing.T) { wantErr: false, }, { - name: "environment name equals no match", - condition: unknown.UnknownCondition{ - Property: "Name", - Operator: "equals", - Value: "production", - }, + name: "environment name equals no match", + selector: "environment.name == 'production'", environment: oapi.Environment{ Id: "2", Name: "staging", @@ -397,12 +248,8 @@ func TestMatch_JsonSelector_Environment(t *testing.T) { wantErr: false, }, { - name: "environment name contains match", - condition: unknown.UnknownCondition{ - Property: "Name", - Operator: "contains", - Value: "prod", - }, + name: "environment name contains match", + selector: "environment.name.contains('prod')", environment: oapi.Environment{ Id: "3", Name: "production-us-east", @@ -416,9 +263,8 @@ func TestMatch_JsonSelector_Environment(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ctx := context.Background() - selector := createJsonSelector(t, tt.condition) - match, err := Match(ctx, selector, tt.environment) + match, err := Match(ctx, tt.selector, tt.environment) if (err != nil) != tt.wantErr { t.Errorf("Match() error = %v, wantErr %v", err, tt.wantErr) @@ -432,9 +278,6 @@ func TestMatch_JsonSelector_Environment(t *testing.T) { } } -// NOTE: These tests document the current behavior of the Match function -// with CEL selectors. There appears to be a bug in match.go lines 37-39 -// where non-empty CEL expressions return false without evaluation. func TestMatch_CelSelector_Resource(t *testing.T) { tests := []struct { name string @@ -766,9 +609,8 @@ func TestMatch_CelSelector_Resource(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ctx := context.Background() - selector := createCelSelector(t, tt.expression) - match, err := Match(ctx, selector, tt.resource) + match, err := Match(ctx, tt.expression, tt.resource) if (err != nil) != tt.wantErr { t.Errorf("Match() error = %v, wantErr %v", err, tt.wantErr) @@ -831,9 +673,8 @@ func TestMatch_CelSelector_Deployment(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ctx := context.Background() - selector := createCelSelector(t, tt.expression) - match, err := Match(ctx, selector, tt.deployment) + match, err := Match(ctx, tt.expression, tt.deployment) if (err != nil) != tt.wantErr { t.Errorf("Match() error = %v, wantErr %v", err, tt.wantErr) @@ -893,9 +734,8 @@ func TestMatch_CelSelector_Environment(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ctx := context.Background() - selector := createCelSelector(t, tt.expression) - match, err := Match(ctx, selector, tt.environment) + match, err := Match(ctx, tt.expression, tt.environment) if (err != nil) != tt.wantErr { t.Errorf("Match() error = %v, wantErr %v", err, tt.wantErr) @@ -909,9 +749,8 @@ func TestMatch_CelSelector_Environment(t *testing.T) { } } -func TestMatch_EmptyJsonSelector(t *testing.T) { +func TestMatch_EmptySelector(t *testing.T) { ctx := context.Background() - selector := createEmptyJsonSelector(t) resource := oapi.Resource{ Id: "1", @@ -919,30 +758,28 @@ func TestMatch_EmptyJsonSelector(t *testing.T) { Kind: "service", } - // Empty JSON selector falls through to CEL selector logic - // Empty CEL string causes compilation error - _, err := Match(ctx, selector, resource) + match, err := Match(ctx, "", resource) - if err == nil { - t.Error("Match() expected error for empty selector (CEL compilation fails), got nil") + if err != nil { + t.Errorf("Match() unexpected error for empty selector: %v", err) + return + } + + if match { + t.Error("Match() expected false for empty selector, got true") } } func TestMatch_InvalidCelExpression(t *testing.T) { ctx := context.Background() - // Create a CEL selector with invalid syntax - selector := &oapi.Selector{} - _ = selector.FromCelSelector(oapi.CelSelector{Cel: "invalid syntax =="}) - resource := oapi.Resource{ Id: "1", Name: "test-resource", Kind: "service", } - // Invalid CEL expressions should return an error during compilation - _, err := Match(ctx, selector, resource) + _, err := Match(ctx, "invalid syntax ==", resource) if err == nil { t.Errorf("Match() expected error for invalid CEL syntax, got nil") @@ -950,22 +787,17 @@ func TestMatch_InvalidCelExpression(t *testing.T) { } } -func TestMatch_JsonSelector_EdgeCases(t *testing.T) { +func TestMatch_EdgeCases(t *testing.T) { tests := []struct { name string - condition unknown.UnknownCondition + selector string item any wantMatch bool wantErr bool }{ { - name: "empty metadata key", - condition: unknown.UnknownCondition{ - Property: "metadata", - Operator: "equals", - Value: "value", - MetadataKey: "nonexistent", - }, + name: "metadata key does not exist", + selector: "resource.metadata['nonexistent'] == 'value'", item: oapi.Resource{ Id: "1", Name: "test", @@ -975,11 +807,8 @@ func TestMatch_JsonSelector_EdgeCases(t *testing.T) { wantErr: false, }, { - name: "empty AND conditions", - condition: unknown.UnknownCondition{ - Operator: "and", - Conditions: []unknown.UnknownCondition{}, - }, + name: "true selector matches everything", + selector: "true", item: oapi.Resource{ Id: "2", Name: "test", @@ -988,11 +817,8 @@ func TestMatch_JsonSelector_EdgeCases(t *testing.T) { wantErr: false, }, { - name: "empty OR conditions", - condition: unknown.UnknownCondition{ - Operator: "or", - Conditions: []unknown.UnknownCondition{}, - }, + name: "false selector matches nothing", + selector: "false", item: oapi.Resource{ Id: "3", Name: "test", @@ -1005,9 +831,8 @@ func TestMatch_JsonSelector_EdgeCases(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ctx := context.Background() - selector := createJsonSelector(t, tt.condition) - match, err := Match(ctx, selector, tt.item) + match, err := Match(ctx, tt.selector, tt.item) if (err != nil) != tt.wantErr { t.Errorf("Match() error = %v, wantErr %v", err, tt.wantErr) @@ -1092,9 +917,8 @@ func TestMatch_CelSelector_ComplexExpressions(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ctx := context.Background() - selector := createCelSelector(t, tt.expression) - match, err := Match(ctx, selector, tt.resource) + match, err := Match(ctx, tt.expression, tt.resource) if (err != nil) != tt.wantErr { t.Errorf("Match() error = %v, wantErr %v", err, tt.wantErr) @@ -1110,12 +934,10 @@ func TestMatch_CelSelector_ComplexExpressions(t *testing.T) { func TestMatch_Cache_ResourceUpdatedAtInvalidation(t *testing.T) { ctx := context.Background() - selector := createCelSelector(t, "resource.name == 'server-v1'") now := time.Now() - later := now.Add(time.Hour) // Use a significantly different time + later := now.Add(time.Hour) - // Resource v1 - matches selector resourceV1 := &oapi.Resource{ Id: "cache-invalidation-test-1", Name: "server-v1", @@ -1124,7 +946,7 @@ func TestMatch_Cache_ResourceUpdatedAtInvalidation(t *testing.T) { UpdatedAt: &now, } - match1, err := Match(ctx, selector, resourceV1) + match1, err := Match(ctx, "resource.name == 'server-v1'", resourceV1) if err != nil { t.Fatalf("Match() error = %v", err) } @@ -1132,17 +954,15 @@ func TestMatch_Cache_ResourceUpdatedAtInvalidation(t *testing.T) { t.Error("Match() for v1 resource = false, want true") } - // Resource v2 - same ID, different UpdatedAt, different name (doesn't match selector) - // This simulates an entity being updated resourceV2 := &oapi.Resource{ Id: "cache-invalidation-test-1", - Name: "server-v2", // Changed - no longer matches selector + Name: "server-v2", Kind: "server", CreatedAt: now, - UpdatedAt: &later, // Different UpdatedAt = different cache key + UpdatedAt: &later, } - match2, err := Match(ctx, selector, resourceV2) + match2, err := Match(ctx, "resource.name == 'server-v1'", resourceV2) if err != nil { t.Fatalf("Match() error = %v", err) } @@ -1153,20 +973,18 @@ func TestMatch_Cache_ResourceUpdatedAtInvalidation(t *testing.T) { func TestMatch_Cache_ResourceCreatedAtFallback(t *testing.T) { ctx := context.Background() - selector := createCelSelector(t, "resource.kind == 'database'") now := time.Now() - // Resource without UpdatedAt should use CreatedAt for cache key resource := &oapi.Resource{ Id: "cache-test-resource-2", Name: "postgres", Kind: "database", CreatedAt: now, - UpdatedAt: nil, // No UpdatedAt + UpdatedAt: nil, } - match1, err := Match(ctx, selector, resource) + match1, err := Match(ctx, "resource.kind == 'database'", resource) if err != nil { t.Fatalf("Match() error = %v", err) } @@ -1174,8 +992,7 @@ func TestMatch_Cache_ResourceCreatedAtFallback(t *testing.T) { t.Error("Match() = false, want true") } - // Same resource should produce same result - match2, err := Match(ctx, selector, resource) + match2, err := Match(ctx, "resource.kind == 'database'", resource) if err != nil { t.Fatalf("Match() error = %v", err) } @@ -1186,12 +1003,10 @@ func TestMatch_Cache_ResourceCreatedAtFallback(t *testing.T) { func TestMatch_Cache_DifferentUpdatedAtProducesDifferentKeys(t *testing.T) { ctx := context.Background() - selector := createCelSelector(t, "resource.name == 'test'") now := time.Now() later := now.Add(time.Hour) - // Two resources with same ID but different UpdatedAt should have different cache keys resource1 := &oapi.Resource{ Id: "same-id-different-time", Name: "test", @@ -1208,12 +1023,12 @@ func TestMatch_Cache_DifferentUpdatedAtProducesDifferentKeys(t *testing.T) { UpdatedAt: &later, } - match1, _ := Match(ctx, selector, resource1) + match1, _ := Match(ctx, "resource.name == 'test'", resource1) if !match1 { t.Error("First resource should match") } - match2, _ := Match(ctx, selector, resource2) + match2, _ := Match(ctx, "resource.name == 'test'", resource2) if match2 { t.Error("Second resource should not match (different UpdatedAt = different cache key)") } @@ -1242,7 +1057,7 @@ func TestEntityCacheKey(t *testing.T) { }) t.Run("resource with zero CreatedAt returns empty (not cached)", func(t *testing.T) { - r := &oapi.Resource{Id: "r3", UpdatedAt: nil} // CreatedAt is zero + r := &oapi.Resource{Id: "r3", UpdatedAt: nil} key := entityCacheKey(r) if key != "" { t.Errorf( @@ -1262,7 +1077,7 @@ func TestEntityCacheKey(t *testing.T) { }) t.Run("job with zero UpdatedAt returns empty (not cached)", func(t *testing.T) { - j := &oapi.Job{Id: "j2", CreatedAt: now} // UpdatedAt is zero + j := &oapi.Job{Id: "j2", CreatedAt: now} key := entityCacheKey(j) if key != "" { t.Errorf("entityCacheKey() for Job with zero UpdatedAt = %q, want empty string", key) diff --git a/apps/workspace-engine/pkg/selector/resources.go b/apps/workspace-engine/pkg/selector/resources.go index 5c5c67600..8b7c011a2 100644 --- a/apps/workspace-engine/pkg/selector/resources.go +++ b/apps/workspace-engine/pkg/selector/resources.go @@ -10,7 +10,7 @@ type Selector any func FilterResources( ctx context.Context, - sel *oapi.Selector, + sel string, resources []*oapi.Resource, _opts ...FilterOption, ) (map[string]*oapi.Resource, error) { @@ -54,12 +54,12 @@ func WithChunking(chunkSize, maxConcurrency int) FilterOption { // This is a simplified version of Filter that just does the basic computation. func Filter[T any]( ctx context.Context, - sel *oapi.Selector, + sel string, resources []T, _opts ...FilterOption, ) ([]T, error) { // If no selector is provided, return empty slice - if sel == nil { + if sel == "" || sel == "false" { return []T{}, nil } diff --git a/apps/workspace-engine/pkg/selector/resources_test.go b/apps/workspace-engine/pkg/selector/resources_test.go index 8888d540c..140f12f2a 100644 --- a/apps/workspace-engine/pkg/selector/resources_test.go +++ b/apps/workspace-engine/pkg/selector/resources_test.go @@ -2,37 +2,14 @@ package selector import ( "context" - "encoding/json" + "fmt" "slices" "testing" "time" "workspace-engine/pkg/oapi" - "workspace-engine/pkg/selector/langs/jsonselector/unknown" ) -// Helper function to convert UnknownCondition to *oapi.Selector. -func conditionToSelector(t *testing.T, condition unknown.UnknownCondition) *oapi.Selector { - t.Helper() - - // Convert condition to map - jsonBytes, err := json.Marshal(condition) - if err != nil { - t.Fatalf("Failed to marshal condition: %v", err) - } - - var conditionMap map[string]any - if err := json.Unmarshal(jsonBytes, &conditionMap); err != nil { - t.Fatalf("Failed to unmarshal condition: %v", err) - } - - v := &oapi.Selector{} - if err := v.FromJsonSelector(oapi.JsonSelector{Json: conditionMap}); err != nil { - t.Fatalf("Failed to create JSON selector: %v", err) - } - return v -} - // Helper function to validate filtered results. func validateFilteredResources( t *testing.T, @@ -53,7 +30,6 @@ func validateFilteredResources( } } - // Check for unexpected IDs for id := range result { if !expectedIDs[id] { t.Errorf("FilterResources() unexpected resource ID %s in results", id) @@ -64,19 +40,15 @@ func validateFilteredResources( func TestFilterResources_StringConditions(t *testing.T) { tests := []struct { name string - condition unknown.UnknownCondition + selector string resources []*oapi.Resource expectedCount int expectedIDs map[string]bool wantErr bool }{ { - name: "contains operator matches exact name", - condition: unknown.UnknownCondition{ - Property: "Name", - Operator: "contains", - Value: "production", - }, + name: "contains operator matches exact name", + selector: "resource.name.contains('production')", resources: []*oapi.Resource{ { Id: "1", @@ -94,12 +66,8 @@ func TestFilterResources_StringConditions(t *testing.T) { wantErr: false, }, { - name: "starts-with operator filters resources", - condition: unknown.UnknownCondition{ - Property: "Name", - Operator: "starts-with", - Value: "prod", - }, + name: "starts-with operator filters resources", + selector: "resource.name.startsWith('prod')", resources: []*oapi.Resource{ { Id: "1", @@ -119,12 +87,8 @@ func TestFilterResources_StringConditions(t *testing.T) { wantErr: false, }, { - name: "ends-with operator filters resources", - condition: unknown.UnknownCondition{ - Property: "Kind", - Operator: "ends-with", - Value: "service", - }, + name: "ends-with operator filters resources", + selector: "resource.kind.endsWith('service')", resources: []*oapi.Resource{ { Id: "1", @@ -144,12 +108,8 @@ func TestFilterResources_StringConditions(t *testing.T) { wantErr: false, }, { - name: "contains operator filters resources", - condition: unknown.UnknownCondition{ - Property: "Identifier", - Operator: "contains", - Value: "k8s", - }, + name: "contains operator filters resources", + selector: "resource.identifier.contains('k8s')", resources: []*oapi.Resource{ { Id: "1", @@ -173,8 +133,7 @@ func TestFilterResources_StringConditions(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ctx := context.Background() - selector := conditionToSelector(t, tt.condition) - result, err := FilterResources(ctx, selector, tt.resources) + result, err := FilterResources(ctx, tt.selector, tt.resources) if (err != nil) != tt.wantErr { t.Errorf("FilterResources() error = %v, wantErr %v", err, tt.wantErr) @@ -189,20 +148,15 @@ func TestFilterResources_StringConditions(t *testing.T) { func TestFilterResources_MetadataConditions(t *testing.T) { tests := []struct { name string - condition unknown.UnknownCondition + selector string resources []*oapi.Resource expectedCount int expectedIDs map[string]bool wantErr bool }{ { - name: "metadata equals filter", - condition: unknown.UnknownCondition{ - Property: "metadata", - Operator: "equals", - Value: "production", - MetadataKey: "env", - }, + name: "metadata equals filter", + selector: "resource.metadata['env'] == 'production'", resources: []*oapi.Resource{ { Id: "1", @@ -231,13 +185,8 @@ func TestFilterResources_MetadataConditions(t *testing.T) { wantErr: false, }, { - name: "metadata contains filter", - condition: unknown.UnknownCondition{ - Property: "metadata", - Operator: "contains", - Value: "critical", - MetadataKey: "tags", - }, + name: "metadata contains filter", + selector: "resource.metadata['tags'].contains('critical')", resources: []*oapi.Resource{ { Id: "1", @@ -257,13 +206,8 @@ func TestFilterResources_MetadataConditions(t *testing.T) { wantErr: false, }, { - name: "metadata starts-with filter", - condition: unknown.UnknownCondition{ - Property: "metadata", - Operator: "starts-with", - Value: "us-", - MetadataKey: "region", - }, + name: "metadata starts-with filter", + selector: "resource.metadata['region'].startsWith('us-')", resources: []*oapi.Resource{ { Id: "1", @@ -289,13 +233,8 @@ func TestFilterResources_MetadataConditions(t *testing.T) { wantErr: false, }, { - name: "metadata missing key returns no matches", - condition: unknown.UnknownCondition{ - Property: "metadata", - Operator: "equals", - Value: "value", - MetadataKey: "nonexistent", - }, + name: "metadata missing key returns no matches", + selector: "resource.metadata['nonexistent'] == 'value'", resources: []*oapi.Resource{ { Id: "1", @@ -313,8 +252,7 @@ func TestFilterResources_MetadataConditions(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ctx := context.Background() - selector := conditionToSelector(t, tt.condition) - result, err := FilterResources(ctx, selector, tt.resources) + result, err := FilterResources(ctx, tt.selector, tt.resources) if (err != nil) != tt.wantErr { t.Errorf("FilterResources() error = %v, wantErr %v", err, tt.wantErr) @@ -333,19 +271,15 @@ func TestFilterResources_DateConditions(t *testing.T) { tests := []struct { name string - condition unknown.UnknownCondition + selector string resources []*oapi.Resource expectedCount int expectedIDs []string wantErr bool }{ { - name: "after operator filters resources created after date", - condition: unknown.UnknownCondition{ - Property: "created-at", - Operator: "after", - Value: baseTime.Format(time.RFC3339), - }, + name: "after operator filters resources created after date", + selector: fmt.Sprintf("resource.createdAt > timestamp('%s')", baseTime.Format(time.RFC3339)), resources: []*oapi.Resource{ { Id: "1", @@ -365,12 +299,8 @@ func TestFilterResources_DateConditions(t *testing.T) { wantErr: false, }, { - name: "before operator filters resources created before date", - condition: unknown.UnknownCondition{ - Property: "created-at", - Operator: "before", - Value: baseTime.Format(time.RFC3339), - }, + name: "before operator filters resources created before date", + selector: fmt.Sprintf("resource.createdAt < timestamp('%s')", baseTime.Format(time.RFC3339)), resources: []*oapi.Resource{ { Id: "1", @@ -390,12 +320,8 @@ func TestFilterResources_DateConditions(t *testing.T) { wantErr: false, }, { - name: "after-or-on operator includes exact match", - condition: unknown.UnknownCondition{ - Property: "created-at", - Operator: "after-or-on", - Value: baseTime.Format(time.RFC3339), - }, + name: "after-or-on operator includes exact match", + selector: fmt.Sprintf("resource.createdAt >= timestamp('%s')", baseTime.Format(time.RFC3339)), resources: []*oapi.Resource{ { Id: "1", @@ -415,12 +341,8 @@ func TestFilterResources_DateConditions(t *testing.T) { wantErr: false, }, { - name: "before-or-on operator includes exact match", - condition: unknown.UnknownCondition{ - Property: "created-at", - Operator: "before-or-on", - Value: baseTime.Format(time.RFC3339), - }, + name: "before-or-on operator includes exact match", + selector: fmt.Sprintf("resource.createdAt <= timestamp('%s')", baseTime.Format(time.RFC3339)), resources: []*oapi.Resource{ { Id: "1", @@ -444,8 +366,7 @@ func TestFilterResources_DateConditions(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ctx := context.Background() - selector := conditionToSelector(t, tt.condition) - result, err := FilterResources(ctx, selector, tt.resources) + result, err := FilterResources(ctx, tt.selector, tt.resources) if (err != nil) != tt.wantErr { t.Errorf("FilterResources() error = %v, wantErr %v", err, tt.wantErr) @@ -485,29 +406,15 @@ func TestFilterResources_DeeplyNestedConditions(t *testing.T) { tests := []struct { name string - condition unknown.UnknownCondition + selector string resources []*oapi.Resource expectedCount int expectedIDs []string wantErr bool }{ { - name: "AND with multiple string conditions", - condition: unknown.UnknownCondition{ - Operator: "and", - Conditions: []unknown.UnknownCondition{ - { - Property: "name", - Operator: "starts-with", - Value: "prod", - }, - { - Property: "kind", - Operator: "contains", - Value: "service", - }, - }, - }, + name: "AND with multiple string conditions", + selector: "resource.name.startsWith('prod') && resource.kind.contains('service')", resources: []*oapi.Resource{ { Id: "1", @@ -530,24 +437,8 @@ func TestFilterResources_DeeplyNestedConditions(t *testing.T) { wantErr: false, }, { - name: "OR with multiple metadata conditions", - condition: unknown.UnknownCondition{ - Operator: "or", - Conditions: []unknown.UnknownCondition{ - { - Property: "metadata", - Operator: "equals", - Value: "production", - MetadataKey: "env", - }, - { - Property: "metadata", - Operator: "equals", - Value: "staging", - MetadataKey: "env", - }, - }, - }, + name: "OR with multiple metadata conditions", + selector: "resource.metadata['env'] == 'production' || resource.metadata['env'] == 'staging'", resources: []*oapi.Resource{ { Id: "1", @@ -574,31 +465,10 @@ func TestFilterResources_DeeplyNestedConditions(t *testing.T) { }, { name: "nested AND/OR with string and date conditions", - condition: unknown.UnknownCondition{ - Operator: "and", - Conditions: []unknown.UnknownCondition{ - { - Operator: "or", - Conditions: []unknown.UnknownCondition{ - { - Property: "name", - Operator: "contains", - Value: "api", - }, - { - Property: "name", - Operator: "contains", - Value: "service", - }, - }, - }, - { - Property: "created-at", - Operator: "after", - Value: recentTime.Format(time.RFC3339), - }, - }, - }, + selector: fmt.Sprintf( + "(resource.name.contains('api') || resource.name.contains('service')) && resource.createdAt > timestamp('%s')", + recentTime.Format(time.RFC3339), + ), resources: []*oapi.Resource{ { Id: "1", @@ -626,45 +496,8 @@ func TestFilterResources_DeeplyNestedConditions(t *testing.T) { wantErr: false, }, { - name: "deeply nested AND/OR/AND with metadata and strings", - condition: unknown.UnknownCondition{ - Operator: "and", - Conditions: []unknown.UnknownCondition{ - { - Operator: "or", - Conditions: []unknown.UnknownCondition{ - { - Property: "metadata", - Operator: "starts-with", - Value: "us-", - MetadataKey: "region", - }, - { - Property: "metadata", - Operator: "starts-with", - Value: "eu-", - MetadataKey: "region", - }, - }, - }, - { - Operator: "and", - Conditions: []unknown.UnknownCondition{ - { - Property: "kind", - Operator: "contains", - Value: "cluster", - }, - { - Property: "metadata", - Operator: "equals", - Value: "premium", - MetadataKey: "tier", - }, - }, - }, - }, - }, + name: "deeply nested AND/OR/AND with metadata and strings", + selector: "(resource.metadata['region'].startsWith('us-') || resource.metadata['region'].startsWith('eu-')) && (resource.kind.contains('cluster') && resource.metadata['tier'] == 'premium')", resources: []*oapi.Resource{ { Id: "1", @@ -705,43 +538,10 @@ func TestFilterResources_DeeplyNestedConditions(t *testing.T) { }, { name: "complex nested condition with all types", - condition: unknown.UnknownCondition{ - Operator: "and", - Conditions: []unknown.UnknownCondition{ - { - Property: "name", - Operator: "starts-with", - Value: "prod", - }, - { - Operator: "or", - Conditions: []unknown.UnknownCondition{ - { - Property: "metadata", - Operator: "equals", - Value: "production", - MetadataKey: "env", - }, - { - Property: "metadata", - Operator: "equals", - Value: "prod", - MetadataKey: "env", - }, - }, - }, - { - Property: "created-at", - Operator: "after", - Value: recentTime.Format(time.RFC3339), - }, - { - Property: "kind", - Operator: "contains", - Value: "service", - }, - }, - }, + selector: fmt.Sprintf( + "resource.name.startsWith('prod') && (resource.metadata['env'] == 'production' || resource.metadata['env'] == 'prod') && resource.createdAt > timestamp('%s') && resource.kind.contains('service')", + recentTime.Format(time.RFC3339), + ), resources: []*oapi.Resource{ { Id: "1", @@ -789,8 +589,7 @@ func TestFilterResources_DeeplyNestedConditions(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ctx := context.Background() - selector := conditionToSelector(t, tt.condition) - result, err := FilterResources(ctx, selector, tt.resources) + result, err := FilterResources(ctx, tt.selector, tt.resources) if (err != nil) != tt.wantErr { t.Errorf("FilterResources() error = %v, wantErr %v", err, tt.wantErr) @@ -837,19 +636,15 @@ func TestFilterResources_ConfigFieldConditions(t *testing.T) { tests := []struct { name string - condition unknown.UnknownCondition + selector string resources []*oapi.Resource expectedCount int expectedIDs []string wantErr bool }{ { - name: "filter by version field", - condition: unknown.UnknownCondition{ - Property: "Version", - Operator: "starts-with", - Value: "1.", - }, + name: "filter by version field", + selector: "resource.version.startsWith('1.')", resources: []*oapi.Resource{ { Id: "1", @@ -867,12 +662,8 @@ func TestFilterResources_ConfigFieldConditions(t *testing.T) { wantErr: false, }, { - name: "filter by identifier with contains", - condition: unknown.UnknownCondition{ - Property: "Identifier", - Operator: "contains", - Value: "cluster", - }, + name: "filter by identifier with contains", + selector: "resource.identifier.contains('cluster')", resources: []*oapi.Resource{ { Id: "1", @@ -896,8 +687,7 @@ func TestFilterResources_ConfigFieldConditions(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ctx := context.Background() - selector := conditionToSelector(t, tt.condition) - result, err := FilterResources(ctx, selector, tt.resources) + result, err := FilterResources(ctx, tt.selector, tt.resources) if (err != nil) != tt.wantErr { t.Errorf("FilterResources() error = %v, wantErr %v", err, tt.wantErr) @@ -933,29 +723,21 @@ func TestFilterResources_ConfigFieldConditions(t *testing.T) { func TestFilterResources_EmptyAndEdgeCases(t *testing.T) { tests := []struct { name string - condition unknown.UnknownCondition + selector string resources []*oapi.Resource expectedCount int wantErr bool }{ { - name: "empty resource list", - condition: unknown.UnknownCondition{ - Property: "Name", - Operator: "contains", - Value: "test", - }, + name: "empty resource list", + selector: "resource.name.contains('test')", resources: []*oapi.Resource{}, expectedCount: 0, wantErr: false, }, { - name: "no resources match condition", - condition: unknown.UnknownCondition{ - Property: "Name", - Operator: "contains", - Value: "nonexistent", - }, + name: "no resources match condition", + selector: "resource.name.contains('nonexistent')", resources: []*oapi.Resource{ { Id: "1", @@ -966,12 +748,8 @@ func TestFilterResources_EmptyAndEdgeCases(t *testing.T) { wantErr: false, }, { - name: "all resources match condition", - condition: unknown.UnknownCondition{ - Property: "Kind", - Operator: "contains", - Value: "service", - }, + name: "all resources match condition", + selector: "resource.kind.contains('service')", resources: []*oapi.Resource{ { Id: "1", @@ -990,11 +768,8 @@ func TestFilterResources_EmptyAndEdgeCases(t *testing.T) { wantErr: false, }, { - name: "empty AND condition matches all", - condition: unknown.UnknownCondition{ - Operator: "and", - Conditions: []unknown.UnknownCondition{}, - }, + name: "true selector matches all", + selector: "true", resources: []*oapi.Resource{ { Id: "1", @@ -1009,11 +784,8 @@ func TestFilterResources_EmptyAndEdgeCases(t *testing.T) { wantErr: false, }, { - name: "empty OR condition matches none", - condition: unknown.UnknownCondition{ - Operator: "or", - Conditions: []unknown.UnknownCondition{}, - }, + name: "false selector matches none", + selector: "false", resources: []*oapi.Resource{ { Id: "1", @@ -1028,8 +800,7 @@ func TestFilterResources_EmptyAndEdgeCases(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ctx := context.Background() - selector := conditionToSelector(t, tt.condition) - result, err := FilterResources(ctx, selector, tt.resources) + result, err := FilterResources(ctx, tt.selector, tt.resources) if (err != nil) != tt.wantErr { t.Errorf("FilterResources() error = %v, wantErr %v", err, tt.wantErr) @@ -1055,33 +826,10 @@ func TestFilterResources_ComplexRealWorldScenarios(t *testing.T) { t.Run( "filter production kubernetes services in US regions created recently", func(t *testing.T) { - condition := unknown.UnknownCondition{ - Operator: "and", - Conditions: []unknown.UnknownCondition{ - { - Property: "kind", - Operator: "contains", - Value: "kubernetes-service", - }, - { - Property: "metadata", - Operator: "equals", - Value: "production", - MetadataKey: "env", - }, - { - Property: "metadata", - Operator: "starts-with", - Value: "us-", - MetadataKey: "region", - }, - { - Property: "CreatedAt", - Operator: "after", - Value: recentTime.Format(time.RFC3339), - }, - }, - } + sel := fmt.Sprintf( + "resource.kind.contains('kubernetes-service') && resource.metadata['env'] == 'production' && resource.metadata['region'].startsWith('us-') && resource.createdAt > timestamp('%s')", + recentTime.Format(time.RFC3339), + ) resources := []*oapi.Resource{ { @@ -1127,8 +875,7 @@ func TestFilterResources_ComplexRealWorldScenarios(t *testing.T) { } ctx := context.Background() - selector := conditionToSelector(t, condition) - result, err := FilterResources(ctx, selector, resources) + result, err := FilterResources(ctx, sel, resources) if err != nil { t.Fatalf("FilterResources() unexpected error: %v", err) @@ -1147,54 +894,7 @@ func TestFilterResources_ComplexRealWorldScenarios(t *testing.T) { ) t.Run("filter critical services in any production environment", func(t *testing.T) { - condition := unknown.UnknownCondition{ - Operator: "and", - Conditions: []unknown.UnknownCondition{ - { - Property: "metadata", - Operator: "equals", - Value: "critical", - MetadataKey: "priority", - }, - { - Operator: "or", - Conditions: []unknown.UnknownCondition{ - { - Property: "metadata", - Operator: "equals", - Value: "production", - MetadataKey: "env", - }, - { - Property: "metadata", - Operator: "equals", - Value: "prod", - MetadataKey: "env", - }, - }, - }, - { - Operator: "or", - Conditions: []unknown.UnknownCondition{ - { - Property: "Name", - Operator: "contains", - Value: "payment", - }, - { - Property: "Name", - Operator: "contains", - Value: "auth", - }, - { - Property: "Name", - Operator: "contains", - Value: "billing", - }, - }, - }, - }, - } + sel := "resource.metadata['priority'] == 'critical' && (resource.metadata['env'] == 'production' || resource.metadata['env'] == 'prod') && (resource.name.contains('payment') || resource.name.contains('auth') || resource.name.contains('billing'))" resources := []*oapi.Resource{ { @@ -1232,8 +932,7 @@ func TestFilterResources_ComplexRealWorldScenarios(t *testing.T) { } ctx := context.Background() - selector := conditionToSelector(t, condition) - result, err := FilterResources(ctx, selector, resources) + result, err := FilterResources(ctx, sel, resources) if err != nil { t.Fatalf("FilterResources() unexpected error: %v", err) diff --git a/apps/workspace-engine/pkg/workspace/jobs/factory_test.go b/apps/workspace-engine/pkg/workspace/jobs/factory_test.go index ffd324a0d..827baee12 100644 --- a/apps/workspace-engine/pkg/workspace/jobs/factory_test.go +++ b/apps/workspace-engine/pkg/workspace/jobs/factory_test.go @@ -7,11 +7,12 @@ import ( "testing" "time" - "github.com/google/uuid" - "github.com/stretchr/testify/require" "workspace-engine/pkg/oapi" "workspace-engine/pkg/statechange" "workspace-engine/pkg/workspace/store" + + "github.com/google/uuid" + "github.com/stretchr/testify/require" ) func newID() string { return uuid.New().String() } @@ -24,14 +25,6 @@ func mustCreateJobAgentConfig(t *testing.T, configJSON string) oapi.JobAgentConf return config } -func mustCreateResourceSelector(t *testing.T) *oapi.Selector { - t.Helper() - selector := &oapi.Selector{} - err := selector.UnmarshalJSON([]byte(`{"type": "all"}`)) - require.NoError(t, err) - return selector -} - // Test helpers for setting up store with test data func setupTestStore() *store.Store { @@ -46,13 +39,14 @@ func createTestDeployment( jobAgentConfig oapi.JobAgentConfig, ) *oapi.Deployment { t.Helper() + resourceSelector := "true" return &oapi.Deployment{ Id: id, Name: "test-deployment", Slug: "test-deployment", JobAgentId: jobAgentId, JobAgentConfig: jobAgentConfig, - ResourceSelector: mustCreateResourceSelector(t), + ResourceSelector: &resourceSelector, } } @@ -63,12 +57,13 @@ func createTestEnvironment( name string, ) *oapi.Environment { t.Helper() + resourceSelector := "true" return &oapi.Environment{ Id: id, Name: name, Metadata: map[string]string{}, CreatedAt: time.Now(), - ResourceSelector: mustCreateResourceSelector(t), + ResourceSelector: &resourceSelector, } } diff --git a/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/deploymentdependency/deployment_dependency.go b/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/deploymentdependency/deployment_dependency.go index 0faa64190..0ae4ebcf0 100644 --- a/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/deploymentdependency/deployment_dependency.go +++ b/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/deploymentdependency/deployment_dependency.go @@ -4,19 +4,14 @@ import ( "context" "fmt" - "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/attribute" "workspace-engine/pkg/oapi" "workspace-engine/pkg/selector" "workspace-engine/pkg/workspace/releasemanager/policy/evaluator" "workspace-engine/pkg/workspace/releasemanager/policy/results" -) -func celToSelector(cel string) *oapi.Selector { - s := &oapi.Selector{} - _ = s.FromCelSelector(oapi.CelSelector{Cel: cel}) - return s -} + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" +) var tracer = otel.Tracer("DeploymentDependencyEvaluator") @@ -58,7 +53,7 @@ func (e *DeploymentDependencyEvaluator) findMatchingDeployments( ctx context.Context, scope evaluator.EvaluatorScope, ) ([]*oapi.Deployment, error) { - deploymentSelector := celToSelector(e.rule.DependsOn) + deploymentSelector := e.rule.DependsOn deployments, err := e.getters.GetAllDeployments(ctx, scope.Environment.WorkspaceId) if err != nil { return nil, fmt.Errorf("failed to get deployments: %w", err) diff --git a/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/environmentprogression/environmentprogression.go b/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/environmentprogression/environmentprogression.go index f8fca3e1c..e65dae035 100644 --- a/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/environmentprogression/environmentprogression.go +++ b/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/environmentprogression/environmentprogression.go @@ -5,14 +5,15 @@ import ( "fmt" "time" - "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/codes" - "go.opentelemetry.io/otel/trace" "workspace-engine/pkg/oapi" "workspace-engine/pkg/selector" "workspace-engine/pkg/workspace/releasemanager/policy/evaluator" "workspace-engine/pkg/workspace/releasemanager/policy/results" + + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/trace" ) var tracer = otel.Tracer("workspace/releasemanager/policy/evaluator/environmentprogression") @@ -161,6 +162,10 @@ func (e *EnvironmentProgressionEvaluator) findDependencyEnvironments( if err != nil { return nil, fmt.Errorf("failed to get all environments: %w", err) } + if e.rule.DependsOnEnvironmentSelector == "" { + return nil, fmt.Errorf("invalid rule: DependsOnEnvironmentSelector must be non-empty") + } + envSystemIDs := e.getters.GetSystemIDsForEnvironment(environment.Id) for _, env := range envItems { candidateSystemIDs := e.getters.GetSystemIDsForEnvironment(env.Id) @@ -174,7 +179,7 @@ func (e *EnvironmentProgressionEvaluator) findDependencyEnvironments( } // Apply the selector - matched, err := selector.Match(ctx, &e.rule.DependsOnEnvironmentSelector, *env) + matched, err := selector.Match(ctx, e.rule.DependsOnEnvironmentSelector, *env) if err != nil { return nil, fmt.Errorf("failed to match environment selector: %w", err) } diff --git a/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/environmentprogression/environmentprogression_test.go b/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/environmentprogression/environmentprogression_test.go index 926e64f5b..1f6eded51 100644 --- a/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/environmentprogression/environmentprogression_test.go +++ b/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/environmentprogression/environmentprogression_test.go @@ -5,10 +5,11 @@ import ( "testing" "time" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" "workspace-engine/pkg/oapi" "workspace-engine/pkg/workspace/releasemanager/policy/evaluator" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) // TestEnvironmentProgressionEvaluator_VersionNotInDependency tests that when a version has not been deployed @@ -19,11 +20,7 @@ func TestEnvironmentProgressionEvaluator_VersionNotInDependency(t *testing.T) { ctx := context.Background() // Create a CEL selector that matches staging - selector := oapi.Selector{} - err := selector.FromCelSelector(oapi.CelSelector{ - Cel: "environment.name == 'staging'", - }) - require.NoError(t, err, "failed to create selector") + selector := "environment.name == 'staging'" // Create rule: prod depends on staging rule := &oapi.PolicyRule{ @@ -107,11 +104,7 @@ func TestEnvironmentProgressionEvaluator_VersionSuccessfulInDependency(t *testin mock.addJob(stagingReleaseTarget, job, stagingRelease) // Create a CEL selector that matches staging - selector := oapi.Selector{} - err := selector.FromCelSelector(oapi.CelSelector{ - Cel: "environment.name == 'staging'", - }) - require.NoError(t, err, "failed to create selector") + selector := "environment.name == 'staging'" // Create rule: prod depends on staging rule := &oapi.PolicyRule{ @@ -184,11 +177,7 @@ func TestEnvironmentProgressionEvaluator_SoakTimeNotMet(t *testing.T) { mock.addJob(stagingReleaseTarget, job, stagingRelease) // Create a CEL selector that matches staging - selector := oapi.Selector{} - err := selector.FromCelSelector(oapi.CelSelector{ - Cel: "environment.name == 'staging'", - }) - require.NoError(t, err, "failed to create selector") + selector := "environment.name == 'staging'" // Create rule: prod depends on staging with 30 minute soak time soakTime := int32(30) @@ -226,11 +215,7 @@ func TestEnvironmentProgressionEvaluator_NoMatchingEnvironments(t *testing.T) { ctx := context.Background() // Create a CEL selector that matches nothing - selector := oapi.Selector{} - err := selector.FromCelSelector(oapi.CelSelector{ - Cel: "environment.name == 'non-existent-env'", - }) - require.NoError(t, err, "failed to create selector") + selector := "environment.name == 'non-existent-env'" // Create rule with selector that matches no environments rule := &oapi.PolicyRule{ @@ -387,11 +372,7 @@ func TestEnvironmentProgressionEvaluator_SatisfiedAt_PassRateOnly(t *testing.T) mock.addJob(stagingReleaseTarget3, job3, release3) // Create selector and rule with 50% pass rate requirement (no soak time) - selector := oapi.Selector{} - err := selector.FromCelSelector(oapi.CelSelector{ - Cel: "environment.name == 'staging'", - }) - require.NoError(t, err) + selector := "environment.name == 'staging'" minSuccessPercentage := float32(50.0) rule := &oapi.PolicyRule{ @@ -476,11 +457,7 @@ func TestEnvironmentProgressionEvaluator_SatisfiedAt_SoakTimeOnly(t *testing.T) } mock.addJob(stagingReleaseTarget, job, stagingRelease) - selector := oapi.Selector{} - err := selector.FromCelSelector(oapi.CelSelector{ - Cel: "environment.name == 'staging'", - }) - require.NoError(t, err) + selector := "environment.name == 'staging'" rule := &oapi.PolicyRule{ Id: "rule-1", @@ -602,11 +579,7 @@ func TestEnvironmentProgressionEvaluator_SatisfiedAt_BothPassRateAndSoakTime(t * // Soak time satisfied at: 10:50 // Expected satisfiedAt: 10:50 (the later of the two) - selector := oapi.Selector{} - err := selector.FromCelSelector(oapi.CelSelector{ - Cel: "environment.name == 'staging'", - }) - require.NoError(t, err) + selector := "environment.name == 'staging'" minSuccessPercentage := float32(50.0) rule := &oapi.PolicyRule{ @@ -763,11 +736,7 @@ func TestEnvironmentProgressionEvaluator_SatisfiedAt_PassRateBeforeSoakTime(t *t soakMinutes := int32(15) - selector := oapi.Selector{} - err := selector.FromCelSelector(oapi.CelSelector{ - Cel: "environment.name == 'staging'", - }) - require.NoError(t, err) + selector := "environment.name == 'staging'" minSuccessPercentage := float32(67.0) rule := &oapi.PolicyRule{ @@ -848,11 +817,7 @@ func TestEnvironmentProgressionEvaluator_SatisfiedAt_NotSatisfied(t *testing.T) } mock.addJob(stagingReleaseTarget, job, stagingRelease) - selector := oapi.Selector{} - err := selector.FromCelSelector(oapi.CelSelector{ - Cel: "environment.name == 'staging'", - }) - require.NoError(t, err) + selector := "environment.name == 'staging'" soakMinutes := int32(30) rule := &oapi.PolicyRule{ @@ -907,11 +872,7 @@ func TestEnvironmentProgressionEvaluator_NoReleaseTargets_Allowed(t *testing.T) CreatedAt: versionCreatedAt, } - selector := oapi.Selector{} - err := selector.FromCelSelector(oapi.CelSelector{ - Cel: "environment.name == 'staging'", - }) - require.NoError(t, err) + selector := "environment.name == 'staging'" soakMinutes := int32(30) rule := &oapi.PolicyRule{ diff --git a/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/environmentprogression/mock_test.go b/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/environmentprogression/mock_test.go index 5f730e8bf..3257c6a01 100644 --- a/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/environmentprogression/mock_test.go +++ b/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/environmentprogression/mock_test.go @@ -144,23 +144,22 @@ func (m *mockGetters) addJob(rt *oapi.ReleaseTarget, job *oapi.Job, release *oap func setupMock() *mockGetters { m := newMockGetters() - resourceSelector := &oapi.Selector{} - _ = resourceSelector.FromCelSelector(oapi.CelSelector{Cel: "true"}) + resourceSelector := "true" m.environments["env-dev"] = &oapi.Environment{ Id: "env-dev", Name: "dev", - ResourceSelector: resourceSelector, + ResourceSelector: &resourceSelector, } m.environments["env-staging"] = &oapi.Environment{ Id: "env-staging", Name: "staging", - ResourceSelector: resourceSelector, + ResourceSelector: &resourceSelector, } m.environments["env-prod"] = &oapi.Environment{ Id: "env-prod", Name: "prod", - ResourceSelector: resourceSelector, + ResourceSelector: &resourceSelector, } m.systemEnvs["env-dev"] = []string{"system-1"} @@ -214,13 +213,12 @@ func setupMock() *mockGetters { func setupMockForSoakTime() *mockGetters { m := newMockGetters() - resourceSelector := &oapi.Selector{} - _ = resourceSelector.FromCelSelector(oapi.CelSelector{Cel: "true"}) + resourceSelector := "true" m.environments["env-staging"] = &oapi.Environment{ Id: "env-staging", Name: "staging", - ResourceSelector: resourceSelector, + ResourceSelector: &resourceSelector, } m.systemEnvs["env-staging"] = []string{"system-1"} diff --git a/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/gradualrollout/gradualrollout_test.go b/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/gradualrollout/gradualrollout_test.go index c366a01ac..5dfbb496b 100644 --- a/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/gradualrollout/gradualrollout_test.go +++ b/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/gradualrollout/gradualrollout_test.go @@ -7,11 +7,12 @@ import ( "testing" "time" + "workspace-engine/pkg/oapi" + "workspace-engine/pkg/workspace/releasemanager/policy/evaluator" + "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "workspace-engine/pkg/oapi" - "workspace-engine/pkg/workspace/releasemanager/policy/evaluator" ) // mockGetters implements the full Getters interface for testing. @@ -580,11 +581,7 @@ func TestGradualRolloutEvaluator_IfEnvironmentProgressionPolicySkipped_RolloutSt twoHoursLater := baseTime.Add(2 * time.Hour) ts := newTestSetup(3, baseTime) - selector := oapi.Selector{} - require.NoError( - t, - selector.FromCelSelector(oapi.CelSelector{Cel: "environment.name == 'staging'"}), - ) + selector := "environment.name == 'staging'" minSuccess := float32(100.0) ts.mock.policies = []*oapi.Policy{{ @@ -640,12 +637,6 @@ func TestGradualRolloutEvaluator_EnvironmentProgressionSkip_SatisfiedAt(t *testi skipTime := baseTime.Add(1 * time.Hour) ts := newTestSetup(3, baseTime) - selector := oapi.Selector{} - require.NoError( - t, - selector.FromCelSelector(oapi.CelSelector{Cel: "environment.name == 'staging'"}), - ) - minSuccess := float32(100.0) ts.mock.policies = []*oapi.Policy{{ Enabled: true, @@ -653,7 +644,7 @@ func TestGradualRolloutEvaluator_EnvironmentProgressionSkip_SatisfiedAt(t *testi Rules: []oapi.PolicyRule{{ Id: "env-prog-rule", EnvironmentProgression: &oapi.EnvironmentProgressionRule{ - DependsOnEnvironmentSelector: selector, + DependsOnEnvironmentSelector: "environment.name == 'staging'", MinimumSuccessPercentage: &minSuccess, }, }}, @@ -680,19 +671,13 @@ func TestGradualRolloutEvaluator_EnvironmentProgressionOnly_Unsatisfied(t *testi baseTime := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC) ts := newTestSetup(3, baseTime) - selector := oapi.Selector{} - require.NoError( - t, - selector.FromCelSelector(oapi.CelSelector{Cel: "environment.name == 'staging'"}), - ) - minSuccess := float32(100.0) ts.mock.policies = []*oapi.Policy{{ Enabled: true, Selector: "true", Rules: []oapi.PolicyRule{{ EnvironmentProgression: &oapi.EnvironmentProgressionRule{ - DependsOnEnvironmentSelector: selector, + DependsOnEnvironmentSelector: "environment.name == 'staging'", MinimumSuccessPercentage: &minSuccess, }, }}, @@ -721,12 +706,6 @@ func TestGradualRolloutEvaluator_BothPolicies_BothSatisfied(t *testing.T) { envProgTime := baseTime.Add(1 * time.Hour) ts := newTestSetup(1, baseTime) - selector := oapi.Selector{} - require.NoError( - t, - selector.FromCelSelector(oapi.CelSelector{Cel: "environment.name == 'staging'"}), - ) - minSuccess := float32(100.0) ts.mock.policies = []*oapi.Policy{{ Enabled: true, @@ -734,7 +713,7 @@ func TestGradualRolloutEvaluator_BothPolicies_BothSatisfied(t *testing.T) { Rules: []oapi.PolicyRule{ {Id: "approval-rule", AnyApproval: &oapi.AnyApprovalRule{MinApprovals: 2}}, {Id: "env-prog-rule", EnvironmentProgression: &oapi.EnvironmentProgressionRule{ - DependsOnEnvironmentSelector: selector, + DependsOnEnvironmentSelector: "environment.name == 'staging'", MinimumSuccessPercentage: &minSuccess, }}, }, @@ -773,11 +752,7 @@ func TestGradualRolloutEvaluator_BothPolicies_ApprovalLater(t *testing.T) { approvalTime := baseTime.Add(1 * time.Hour) ts := newTestSetup(1, baseTime) - selector := oapi.Selector{} - require.NoError( - t, - selector.FromCelSelector(oapi.CelSelector{Cel: "environment.name == 'staging'"}), - ) + selector := "environment.name == 'staging'" minSuccess := float32(100.0) ts.mock.policies = []*oapi.Policy{{ @@ -822,12 +797,6 @@ func TestGradualRolloutEvaluator_BothPolicies_ApprovalUnsatisfied(t *testing.T) baseTime := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC) ts := newTestSetup(1, baseTime) - selector := oapi.Selector{} - require.NoError( - t, - selector.FromCelSelector(oapi.CelSelector{Cel: "environment.name == 'staging'"}), - ) - minSuccess := float32(100.0) ts.mock.policies = []*oapi.Policy{{ Enabled: true, @@ -836,7 +805,7 @@ func TestGradualRolloutEvaluator_BothPolicies_ApprovalUnsatisfied(t *testing.T) Rules: []oapi.PolicyRule{ {AnyApproval: &oapi.AnyApprovalRule{MinApprovals: 2}}, {Id: "env-prog-rule", EnvironmentProgression: &oapi.EnvironmentProgressionRule{ - DependsOnEnvironmentSelector: selector, + DependsOnEnvironmentSelector: "environment.name == 'staging'", MinimumSuccessPercentage: &minSuccess, }}, }, @@ -869,11 +838,7 @@ func TestGradualRolloutEvaluator_BothPolicies_EnvProgUnsatisfied(t *testing.T) { baseTime := time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC) ts := newTestSetup(1, baseTime) - selector := oapi.Selector{} - require.NoError( - t, - selector.FromCelSelector(oapi.CelSelector{Cel: "environment.name == 'staging'"}), - ) + selector := "environment.name == 'staging'" minSuccess := float32(100.0) approvalTime := baseTime.Add(30 * time.Minute) @@ -1036,12 +1001,6 @@ func TestGradualRolloutEvaluator_EnvProgressionJustSatisfied_OnlyPosition0Allowe skipTime := baseTime.Add(1 * time.Hour) ts := newTestSetup(5, baseTime) - selector := oapi.Selector{} - require.NoError( - t, - selector.FromCelSelector(oapi.CelSelector{Cel: "environment.name == 'staging'"}), - ) - minSuccess := float32(100.0) ts.mock.policies = []*oapi.Policy{{ Enabled: true, @@ -1049,7 +1008,7 @@ func TestGradualRolloutEvaluator_EnvProgressionJustSatisfied_OnlyPosition0Allowe Rules: []oapi.PolicyRule{{ Id: "env-prog-rule", EnvironmentProgression: &oapi.EnvironmentProgressionRule{ - DependsOnEnvironmentSelector: selector, + DependsOnEnvironmentSelector: "environment.name == 'staging'", MinimumSuccessPercentage: &minSuccess, }, }}, diff --git a/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/versionselector/versionselector.go b/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/versionselector/versionselector.go index b647e23bd..41703b435 100644 --- a/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/versionselector/versionselector.go +++ b/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/versionselector/versionselector.go @@ -4,14 +4,14 @@ import ( "context" "fmt" - "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/trace" "workspace-engine/pkg/celutil" "workspace-engine/pkg/oapi" - "workspace-engine/pkg/selector" "workspace-engine/pkg/workspace/releasemanager/policy/evaluator" "workspace-engine/pkg/workspace/releasemanager/policy/results" + + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" ) var tracer = otel.Tracer("workspace/releasemanager/policy/evaluator/versionselector") @@ -60,7 +60,7 @@ func (e *Evaluator) Evaluate( ctx context.Context, scope evaluator.EvaluatorScope, ) *oapi.RuleEvaluation { - ctx, span := tracer.Start(ctx, "VersionSelectorEvaluator.Evaluate", + _, span := tracer.Start(ctx, "VersionSelectorEvaluator.Evaluate", trace.WithAttributes( attribute.String("version.id", scope.Version.Id), attribute.String("version.tag", scope.Version.Tag), @@ -74,51 +74,37 @@ func (e *Evaluator) Evaluate( resource := scope.Resource // Try to extract CEL selector first - celSelector, celErr := e.rule.Selector.AsCelSelector() - if celErr == nil { - return e.evaluateCEL(ctx, scope, deployment, resource, celSelector, span) + celSelector := e.rule.Selector + if celSelector != "" { + return e.evaluateCEL(scope, deployment, resource, celSelector, span) } - // Try to extract JSON selector - jsonSelector, jsonErr := e.rule.Selector.AsJsonSelector() - if jsonErr == nil { - return e.evaluateJSON(ctx, scope, deployment, resource, jsonSelector, span) - } - - // Failed to parse selector return results.NewDeniedResult( - fmt.Sprintf( - "Version selector: failed to parse selector: cel error: %v, json error: %v", - celErr, - jsonErr, - ), + "Version selector: selector is required but was empty", ). - WithDetail("celError", celErr.Error()). - WithDetail("jsonError", jsonErr.Error()) + WithDetail("selector", celSelector) } // evaluateCEL evaluates a CEL-based selector. func (e *Evaluator) evaluateCEL( - ctx context.Context, scope evaluator.EvaluatorScope, deployment *oapi.Deployment, resource *oapi.Resource, - celSelector oapi.CelSelector, + celSelector string, span trace.Span, ) *oapi.RuleEvaluation { - celExpression := celSelector.Cel span.SetAttributes(attribute.String("selector.type", "cel")) - span.SetAttributes(attribute.String("selector.expression", celExpression)) + span.SetAttributes(attribute.String("selector.expression", celSelector)) // Compile CEL expression - program, err := compile(celExpression) + program, err := compile(celSelector) if err != nil { span.RecordError(err) return results.NewDeniedResult( fmt.Sprintf("Version selector: failed to compile CEL expression: %v", err), ). WithDetail("error", err.Error()). - WithDetail("expression", celExpression) + WithDetail("expression", celSelector) } // Build CEL context @@ -169,7 +155,7 @@ func (e *Evaluator) evaluateCEL( fmt.Sprintf("Version selector: CEL evaluation failed: %v", err), ). WithDetail("error", err.Error()). - WithDetail("expression", celExpression) + WithDetail("expression", celSelector) } if !result { @@ -186,56 +172,7 @@ func (e *Evaluator) evaluateCEL( return results.NewDeniedResult( fmt.Sprintf("Version selector: %s", description), ). - WithDetail("expression", celExpression). - WithDetail("version_id", scope.Version.Id). - WithDetail("version_tag", scope.Version.Tag) - } - - span.AddEvent("Version allowed by selector", - trace.WithAttributes( - attribute.Bool("selector.result", true), - )) - - return results.NewAllowedResult("Version selector: version matches selector"). - WithDetail("expression", celExpression). - WithDetail("version_id", scope.Version.Id). - WithDetail("version_tag", scope.Version.Tag) -} - -// evaluateJSON evaluates a JSON-based selector. -func (e *Evaluator) evaluateJSON( - ctx context.Context, - scope evaluator.EvaluatorScope, - deployment *oapi.Deployment, - resource *oapi.Resource, - jsonSelector oapi.JsonSelector, - span trace.Span, -) *oapi.RuleEvaluation { - span.SetAttributes(attribute.String("selector.type", "json")) - - // Use the existing selector matching logic - matched, err := selector.Match(ctx, &e.rule.Selector, scope.Version) - if err != nil { - span.RecordError(err) - return results.NewDeniedResult( - fmt.Sprintf("Version selector: failed to evaluate JSON selector: %v", err), - ).WithDetail("error", err.Error()) - } - - if !matched { - description := "Version does not match selector" - if e.rule.Description != nil && *e.rule.Description != "" { - description = *e.rule.Description - } - - span.AddEvent("Version blocked by selector", - trace.WithAttributes( - attribute.Bool("selector.result", false), - )) - - return results.NewDeniedResult( - fmt.Sprintf("Version selector: %s", description), - ). + WithDetail("expression", celSelector). WithDetail("version_id", scope.Version.Id). WithDetail("version_tag", scope.Version.Tag) } @@ -246,6 +183,7 @@ func (e *Evaluator) evaluateJSON( )) return results.NewAllowedResult("Version selector: version matches selector"). + WithDetail("expression", celSelector). WithDetail("version_id", scope.Version.Id). WithDetail("version_tag", scope.Version.Tag) } diff --git a/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/versionselector/versionselector_test.go b/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/versionselector/versionselector_test.go index 9a66b40c1..f6d64840e 100644 --- a/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/versionselector/versionselector_test.go +++ b/apps/workspace-engine/pkg/workspace/releasemanager/policy/evaluator/versionselector/versionselector_test.go @@ -5,10 +5,11 @@ import ( "testing" "time" - "github.com/google/uuid" - "github.com/stretchr/testify/assert" "workspace-engine/pkg/oapi" "workspace-engine/pkg/workspace/releasemanager/policy/evaluator" + + "github.com/google/uuid" + "github.com/stretchr/testify/assert" ) func createTestDeployment() (*oapi.Deployment, string) { @@ -57,13 +58,10 @@ func TestNewEvaluator(t *testing.T) { }) t.Run("returns evaluator when rule has version selector", func(t *testing.T) { - selector := &oapi.Selector{} - _ = selector.FromCelSelector(oapi.CelSelector{Cel: "true"}) - rule := &oapi.PolicyRule{ Id: "versionSelector", VersionSelector: &oapi.VersionSelectorRule{ - Selector: *selector, + Selector: "true", }, } eval := NewEvaluator(rule) @@ -72,13 +70,10 @@ func TestNewEvaluator(t *testing.T) { } func TestScopeFields(t *testing.T) { - selector := &oapi.Selector{} - _ = selector.FromCelSelector(oapi.CelSelector{Cel: "true"}) - rule := &oapi.PolicyRule{ Id: "versionSelector", VersionSelector: &oapi.VersionSelectorRule{ - Selector: *selector, + Selector: "true", }, } @@ -107,15 +102,10 @@ func TestEvaluateCEL_VersionTagMatching(t *testing.T) { t.Run("allows version when CEL expression matches", func(t *testing.T) { version := createTestVersion(deployment.Id, "v2.1.0", nil) - selector := &oapi.Selector{} - _ = selector.FromCelSelector(oapi.CelSelector{ - Cel: `version.tag.startsWith("v2.")`, - }) - rule := &oapi.PolicyRule{ Id: "versionSelector", VersionSelector: &oapi.VersionSelectorRule{ - Selector: *selector, + Selector: "version.tag.startsWith('v2.')", }, } @@ -138,15 +128,10 @@ func TestEvaluateCEL_VersionTagMatching(t *testing.T) { t.Run("blocks version when CEL expression does not match", func(t *testing.T) { version := createTestVersion(deployment.Id, "v1.5.0", nil) - selector := &oapi.Selector{} - _ = selector.FromCelSelector(oapi.CelSelector{ - Cel: `version.tag.startsWith("v2.")`, - }) - rule := &oapi.PolicyRule{ Id: "versionSelector", VersionSelector: &oapi.VersionSelectorRule{ - Selector: *selector, + Selector: "version.tag.startsWith('v2.')", }, } @@ -175,15 +160,10 @@ func TestEvaluateCEL_EnvironmentMatching(t *testing.T) { version := createTestVersion(deployment.Id, "v2.0.0", nil) t.Run("allows version for matching environment", func(t *testing.T) { - selector := &oapi.Selector{} - _ = selector.FromCelSelector(oapi.CelSelector{ - Cel: `environment.name == "staging"`, - }) - rule := &oapi.PolicyRule{ Id: "versionSelector", VersionSelector: &oapi.VersionSelectorRule{ - Selector: *selector, + Selector: "environment.name == 'staging'", }, } @@ -202,15 +182,10 @@ func TestEvaluateCEL_EnvironmentMatching(t *testing.T) { }) t.Run("blocks version for non-matching environment", func(t *testing.T) { - selector := &oapi.Selector{} - _ = selector.FromCelSelector(oapi.CelSelector{ - Cel: `environment.name == "production"`, - }) - rule := &oapi.PolicyRule{ Id: "versionSelector", VersionSelector: &oapi.VersionSelectorRule{ - Selector: *selector, + Selector: "environment.name == 'production'", }, } @@ -240,15 +215,10 @@ func TestEvaluateCEL_ResourceMetadataMatching(t *testing.T) { version := createTestVersion(deployment.Id, "v1.0.0", nil) t.Run("allows version when resource metadata matches", func(t *testing.T) { - selector := &oapi.Selector{} - _ = selector.FromCelSelector(oapi.CelSelector{ - Cel: `resource.metadata["tier"] == "production"`, - }) - rule := &oapi.PolicyRule{ Id: "versionSelector", VersionSelector: &oapi.VersionSelectorRule{ - Selector: *selector, + Selector: "resource.metadata['tier'] == 'production'", }, } @@ -267,15 +237,10 @@ func TestEvaluateCEL_ResourceMetadataMatching(t *testing.T) { }) t.Run("blocks version when resource metadata does not match", func(t *testing.T) { - selector := &oapi.Selector{} - _ = selector.FromCelSelector(oapi.CelSelector{ - Cel: `resource.metadata["tier"] == "staging"`, - }) - rule := &oapi.PolicyRule{ Id: "versionSelector", VersionSelector: &oapi.VersionSelectorRule{ - Selector: *selector, + Selector: "resource.metadata['tier'] == 'staging'", }, } @@ -307,15 +272,10 @@ func TestEvaluateCEL_CombinedConditions(t *testing.T) { ) t.Run("allows version when all conditions match", func(t *testing.T) { - selector := &oapi.Selector{} - _ = selector.FromCelSelector(oapi.CelSelector{ - Cel: `version.tag.startsWith("v2.") && environment.name == "staging" && resource.metadata["canary"] == "true"`, - }) - rule := &oapi.PolicyRule{ Id: "versionSelector", VersionSelector: &oapi.VersionSelectorRule{ - Selector: *selector, + Selector: "version.tag.startsWith('v2.') && environment.name == 'staging' && resource.metadata['canary'] == 'true'", }, } @@ -334,15 +294,10 @@ func TestEvaluateCEL_CombinedConditions(t *testing.T) { }) t.Run("blocks version when one condition fails", func(t *testing.T) { - selector := &oapi.Selector{} - _ = selector.FromCelSelector(oapi.CelSelector{ - Cel: `version.tag.startsWith("v3.") && environment.name == "staging"`, - }) - rule := &oapi.PolicyRule{ Id: "versionSelector", VersionSelector: &oapi.VersionSelectorRule{ - Selector: *selector, + Selector: "version.tag.startsWith('v3.') && environment.name == 'staging'", }, } @@ -369,15 +324,10 @@ func TestEvaluate_InvalidCEL(t *testing.T) { resource := createTestResource(nil) version := createTestVersion(deployment.Id, "v1.0.0", nil) - selector := &oapi.Selector{} - _ = selector.FromCelSelector(oapi.CelSelector{ - Cel: `invalid CEL expression!!!`, - }) - rule := &oapi.PolicyRule{ Id: "versionSelector", VersionSelector: &oapi.VersionSelectorRule{ - Selector: *selector, + Selector: "invalid cel expression!!!", }, } @@ -404,16 +354,11 @@ func TestEvaluate_WithDescription(t *testing.T) { resource := createTestResource(nil) version := createTestVersion(deployment.Id, "v1.0.0", nil) - selector := &oapi.Selector{} - _ = selector.FromCelSelector(oapi.CelSelector{ - Cel: `version.tag.startsWith("v2.")`, - }) - description := "Only deploy v2.x versions to staging" rule := &oapi.PolicyRule{ Id: "versionSelector", VersionSelector: &oapi.VersionSelectorRule{ - Selector: *selector, + Selector: "version.tag.startsWith('v2.')", Description: &description, }, } diff --git a/apps/workspace-engine/pkg/workspace/releasemanager/variables/variablemanager.go b/apps/workspace-engine/pkg/workspace/releasemanager/variables/variablemanager.go index 4e7f338e6..eeb3c110a 100644 --- a/apps/workspace-engine/pkg/workspace/releasemanager/variables/variablemanager.go +++ b/apps/workspace-engine/pkg/workspace/releasemanager/variables/variablemanager.go @@ -7,13 +7,14 @@ import ( "sort" "strings" - "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/trace" "workspace-engine/pkg/oapi" "workspace-engine/pkg/selector" "workspace-engine/pkg/workspace/relationships" "workspace-engine/pkg/workspace/store" + + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" ) var tracer = otel.Tracer("workspace/releasemanager/variablemanager") @@ -195,7 +196,10 @@ func (m *Manager) tryResolveDeploymentVariableValue( // Sort values by priority (higher priority first) sortedValues := make([]*oapi.DeploymentVariableValue, 0, len(values)) for _, value := range values { - matches, _ := selector.Match(ctx, value.ResourceSelector, resource) + if value.ResourceSelector == nil { + continue + } + matches, _ := selector.Match(ctx, *value.ResourceSelector, resource) if !matches { continue } diff --git a/apps/workspace-engine/pkg/workspace/releasemanager/variables/variablemanager_bench_test.go b/apps/workspace-engine/pkg/workspace/releasemanager/variables/variablemanager_bench_test.go index ea1a6a00f..83acd70bd 100644 --- a/apps/workspace-engine/pkg/workspace/releasemanager/variables/variablemanager_bench_test.go +++ b/apps/workspace-engine/pkg/workspace/releasemanager/variables/variablemanager_bench_test.go @@ -6,11 +6,12 @@ import ( "testing" "time" - "github.com/google/uuid" "workspace-engine/pkg/oapi" "workspace-engine/pkg/statechange" "workspace-engine/pkg/workspace/relationships" "workspace-engine/pkg/workspace/store" + + "github.com/google/uuid" ) // ===== Benchmark Helper Functions ===== @@ -34,8 +35,7 @@ func createBenchResource(workspaceID, id, name string) *oapi.Resource { } func createBenchDeployment(systemID, id, name string) *oapi.Deployment { - selector := &oapi.Selector{} - _ = selector.FromCelSelector(oapi.CelSelector{Cel: "true"}) + selector := "true" description := fmt.Sprintf("Benchmark deployment %s", name) jobAgentID := uuid.New().String() @@ -44,7 +44,7 @@ func createBenchDeployment(systemID, id, name string) *oapi.Deployment { Name: name, Slug: name, Description: &description, - ResourceSelector: selector, + ResourceSelector: &selector, JobAgentId: &jobAgentID, JobAgentConfig: oapi.JobAgentConfig{}, } @@ -114,26 +114,17 @@ func setupVariableBenchmark( valueID := uuid.New().String() // Create selector that matches based on tier and region - var selector *oapi.Selector + var resourceSelector string switch j % 3 { case 0: // Matches production tier - selector = &oapi.Selector{} - _ = selector.FromCelSelector(oapi.CelSelector{ - Cel: "resource.metadata.tier == 'production'", - }) + resourceSelector = "resource.metadata.tier == 'production'" case 1: // Matches specific region - selector = &oapi.Selector{} - _ = selector.FromCelSelector(oapi.CelSelector{ - Cel: "resource.metadata.region == 'us-west-1'", - }) + resourceSelector = "resource.metadata.region == 'us-west-1'" default: // Matches both - selector = &oapi.Selector{} - _ = selector.FromCelSelector(oapi.CelSelector{ - Cel: "resource.metadata.tier == 'production' && resource.metadata.region == 'us-west-1'", - }) + resourceSelector = "resource.metadata.tier == 'production' && resource.metadata.region == 'us-west-1'" } value := &oapi.Value{} @@ -149,7 +140,7 @@ func setupVariableBenchmark( numDeploymentVariableValues - j, ), // Higher priority for earlier values Value: *value, - ResourceSelector: selector, + ResourceSelector: &resourceSelector, } st.DeploymentVariableValues.Upsert(ctx, valueID, deploymentVarValue) } @@ -186,22 +177,8 @@ func setupVariableBenchmark( // Create relationship rule relRuleID := uuid.New().String() - fromSelector := &oapi.Selector{} - _ = fromSelector.FromJsonSelector(oapi.JsonSelector{ - Json: map[string]any{ - "type": "kind", - "operator": "equals", - "value": "service", - }, - }) - toSelector := &oapi.Selector{} - _ = toSelector.FromJsonSelector(oapi.JsonSelector{ - Json: map[string]any{ - "type": "kind", - "operator": "equals", - "value": "database", - }, - }) + fromSelector := "kind == 'service'" + toSelector := "kind == 'database'" matcher := oapi.RelationshipRule_Matcher{} _ = matcher.FromPropertiesMatcher(oapi.PropertiesMatcher{ @@ -222,8 +199,8 @@ func setupVariableBenchmark( FromType: "resource", ToType: "resource", RelationshipType: "depends-on", - FromSelector: fromSelector, - ToSelector: toSelector, + FromSelector: &fromSelector, + ToSelector: &toSelector, Matcher: matcher, Metadata: map[string]string{}, } @@ -641,22 +618,8 @@ func setupLargeRelationshipBenchmark( // Create relationship rule 1: Service to Database dbRelRuleID := uuid.New().String() - dbFromSelector := &oapi.Selector{} - _ = dbFromSelector.FromJsonSelector(oapi.JsonSelector{ - Json: map[string]any{ - "type": "kind", - "operator": "equals", - "value": "service", - }, - }) - dbToSelector := &oapi.Selector{} - _ = dbToSelector.FromJsonSelector(oapi.JsonSelector{ - Json: map[string]any{ - "type": "kind", - "operator": "equals", - "value": "database", - }, - }) + dbFromSelector := "kind == 'service'" + dbToSelector := "kind == 'database'" dbMatcher := oapi.RelationshipRule_Matcher{} _ = dbMatcher.FromPropertiesMatcher(oapi.PropertiesMatcher{ @@ -677,8 +640,8 @@ func setupLargeRelationshipBenchmark( FromType: "resource", ToType: "resource", RelationshipType: "depends-on", - FromSelector: dbFromSelector, - ToSelector: dbToSelector, + FromSelector: &dbFromSelector, + ToSelector: &dbToSelector, Matcher: dbMatcher, Metadata: map[string]string{}, } @@ -686,22 +649,8 @@ func setupLargeRelationshipBenchmark( // Create relationship rule 2: Service to VPC vpcRelRuleID := uuid.New().String() - vpcFromSelector := &oapi.Selector{} - _ = vpcFromSelector.FromJsonSelector(oapi.JsonSelector{ - Json: map[string]any{ - "type": "kind", - "operator": "equals", - "value": "service", - }, - }) - vpcToSelector := &oapi.Selector{} - _ = vpcToSelector.FromJsonSelector(oapi.JsonSelector{ - Json: map[string]any{ - "type": "kind", - "operator": "equals", - "value": "vpc", - }, - }) + vpcFromSelector := "kind == 'service'" + vpcToSelector := "kind == 'vpc'" vpcMatcher := oapi.RelationshipRule_Matcher{} _ = vpcMatcher.FromPropertiesMatcher(oapi.PropertiesMatcher{ @@ -722,8 +671,8 @@ func setupLargeRelationshipBenchmark( FromType: "resource", ToType: "resource", RelationshipType: "depends-on", - FromSelector: vpcFromSelector, - ToSelector: vpcToSelector, + FromSelector: &vpcFromSelector, + ToSelector: &vpcToSelector, Matcher: vpcMatcher, Metadata: map[string]string{}, } @@ -938,22 +887,8 @@ func BenchmarkEvaluate_NxM_AmplifiedScaling(b *testing.B) { // Create relationship rules - service -> database dbRelRuleID := uuid.New().String() - dbFromSelector := &oapi.Selector{} - _ = dbFromSelector.FromJsonSelector(oapi.JsonSelector{ - Json: map[string]any{ - "type": "kind", - "operator": "equals", - "value": "service", - }, - }) - dbToSelector := &oapi.Selector{} - _ = dbToSelector.FromJsonSelector(oapi.JsonSelector{ - Json: map[string]any{ - "type": "kind", - "operator": "equals", - "value": "database", - }, - }) + dbFromSelector := "kind == 'service'" + dbToSelector := "kind == 'database'" dbMatcher := oapi.RelationshipRule_Matcher{} _ = dbMatcher.FromPropertiesMatcher(oapi.PropertiesMatcher{ Properties: []oapi.PropertyMatcher{ @@ -972,8 +907,8 @@ func BenchmarkEvaluate_NxM_AmplifiedScaling(b *testing.B) { FromType: "resource", ToType: "resource", RelationshipType: "depends-on", - FromSelector: dbFromSelector, - ToSelector: dbToSelector, + FromSelector: &dbFromSelector, + ToSelector: &dbToSelector, Matcher: dbMatcher, Metadata: map[string]string{}, } @@ -981,22 +916,8 @@ func BenchmarkEvaluate_NxM_AmplifiedScaling(b *testing.B) { // Create relationship rules - service -> cache cacheRelRuleID := uuid.New().String() - cacheFromSelector := &oapi.Selector{} - _ = cacheFromSelector.FromJsonSelector(oapi.JsonSelector{ - Json: map[string]any{ - "type": "kind", - "operator": "equals", - "value": "service", - }, - }) - cacheToSelector := &oapi.Selector{} - _ = cacheToSelector.FromJsonSelector(oapi.JsonSelector{ - Json: map[string]any{ - "type": "kind", - "operator": "equals", - "value": "cache", - }, - }) + cacheFromSelector := "kind == 'service'" + cacheToSelector := "kind == 'cache'" cacheMatcher := oapi.RelationshipRule_Matcher{} _ = cacheMatcher.FromPropertiesMatcher(oapi.PropertiesMatcher{ Properties: []oapi.PropertyMatcher{ @@ -1015,8 +936,8 @@ func BenchmarkEvaluate_NxM_AmplifiedScaling(b *testing.B) { FromType: "resource", ToType: "resource", RelationshipType: "depends-on", - FromSelector: cacheFromSelector, - ToSelector: cacheToSelector, + FromSelector: &cacheFromSelector, + ToSelector: &cacheToSelector, Matcher: cacheMatcher, Metadata: map[string]string{}, } @@ -1024,22 +945,8 @@ func BenchmarkEvaluate_NxM_AmplifiedScaling(b *testing.B) { // Create relationship rules - service -> queue queueRelRuleID := uuid.New().String() - queueFromSelector := &oapi.Selector{} - _ = queueFromSelector.FromJsonSelector(oapi.JsonSelector{ - Json: map[string]any{ - "type": "kind", - "operator": "equals", - "value": "service", - }, - }) - queueToSelector := &oapi.Selector{} - _ = queueToSelector.FromJsonSelector(oapi.JsonSelector{ - Json: map[string]any{ - "type": "kind", - "operator": "equals", - "value": "queue", - }, - }) + queueFromSelector := "kind == 'service'" + queueToSelector := "kind == 'queue'" queueMatcher := oapi.RelationshipRule_Matcher{} _ = queueMatcher.FromPropertiesMatcher(oapi.PropertiesMatcher{ Properties: []oapi.PropertyMatcher{ @@ -1058,8 +965,8 @@ func BenchmarkEvaluate_NxM_AmplifiedScaling(b *testing.B) { FromType: "resource", ToType: "resource", RelationshipType: "depends-on", - FromSelector: queueFromSelector, - ToSelector: queueToSelector, + FromSelector: &queueFromSelector, + ToSelector: &queueToSelector, Matcher: queueMatcher, Metadata: map[string]string{}, } @@ -1283,22 +1190,8 @@ func BenchmarkEvaluate_MultipleReleaseTargets_ProductionScenario(b *testing.B) { // Create relationship rules - service -> database dbRelRuleID := uuid.New().String() - dbFromSelector := &oapi.Selector{} - _ = dbFromSelector.FromJsonSelector(oapi.JsonSelector{ - Json: map[string]any{ - "type": "kind", - "operator": "equals", - "value": "service", - }, - }) - dbToSelector := &oapi.Selector{} - _ = dbToSelector.FromJsonSelector(oapi.JsonSelector{ - Json: map[string]any{ - "type": "kind", - "operator": "equals", - "value": "database", - }, - }) + dbFromSelector := "kind == 'service'" + dbToSelector := "kind == 'database'" dbMatcher := oapi.RelationshipRule_Matcher{} _ = dbMatcher.FromPropertiesMatcher(oapi.PropertiesMatcher{ Properties: []oapi.PropertyMatcher{ @@ -1317,8 +1210,8 @@ func BenchmarkEvaluate_MultipleReleaseTargets_ProductionScenario(b *testing.B) { FromType: "resource", ToType: "resource", RelationshipType: "depends-on", - FromSelector: dbFromSelector, - ToSelector: dbToSelector, + FromSelector: &dbFromSelector, + ToSelector: &dbToSelector, Matcher: dbMatcher, Metadata: map[string]string{}, } @@ -1326,22 +1219,8 @@ func BenchmarkEvaluate_MultipleReleaseTargets_ProductionScenario(b *testing.B) { // Create relationship rules - service -> vpc vpcRelRuleID := uuid.New().String() - vpcFromSelector := &oapi.Selector{} - _ = vpcFromSelector.FromJsonSelector(oapi.JsonSelector{ - Json: map[string]any{ - "type": "kind", - "operator": "equals", - "value": "service", - }, - }) - vpcToSelector := &oapi.Selector{} - _ = vpcToSelector.FromJsonSelector(oapi.JsonSelector{ - Json: map[string]any{ - "type": "kind", - "operator": "equals", - "value": "vpc", - }, - }) + vpcFromSelector := "kind == 'service'" + vpcToSelector := "kind == 'vpc'" vpcMatcher := oapi.RelationshipRule_Matcher{} _ = vpcMatcher.FromPropertiesMatcher(oapi.PropertiesMatcher{ Properties: []oapi.PropertyMatcher{ @@ -1360,8 +1239,8 @@ func BenchmarkEvaluate_MultipleReleaseTargets_ProductionScenario(b *testing.B) { FromType: "resource", ToType: "resource", RelationshipType: "depends-on", - FromSelector: vpcFromSelector, - ToSelector: vpcToSelector, + FromSelector: &vpcFromSelector, + ToSelector: &vpcToSelector, Matcher: vpcMatcher, Metadata: map[string]string{}, } diff --git a/apps/workspace-engine/pkg/workspace/releasemanager/variables/variablemanager_test.go b/apps/workspace-engine/pkg/workspace/releasemanager/variables/variablemanager_test.go index 22642346b..b93f1d59f 100644 --- a/apps/workspace-engine/pkg/workspace/releasemanager/variables/variablemanager_test.go +++ b/apps/workspace-engine/pkg/workspace/releasemanager/variables/variablemanager_test.go @@ -6,11 +6,12 @@ import ( "testing" "time" - "github.com/google/uuid" "workspace-engine/pkg/oapi" "workspace-engine/pkg/statechange" "workspace-engine/pkg/workspace/relationships" "workspace-engine/pkg/workspace/store" + + "github.com/google/uuid" ) // Helper function to create a test store with a resource. @@ -80,7 +81,7 @@ func addDeploymentVariableValue( st *store.Store, deploymentVariableID string, priority int64, - selector *oapi.Selector, + selector string, value *oapi.Value, ) { ctx := context.Background() @@ -90,7 +91,7 @@ func addDeploymentVariableValue( Id: valueID, DeploymentVariableId: deploymentVariableID, Priority: priority, - ResourceSelector: selector, + ResourceSelector: &selector, Value: *value, } @@ -110,15 +111,6 @@ func addResourceVariable(st *store.Store, resourceID, key string, value *oapi.Va st.ResourceVariables.Upsert(ctx, rv) } -// Helper function to create a CEL selector. -func mustCreateSelector(celExpression string) *oapi.Selector { - selector := &oapi.Selector{} - if err := selector.FromCelSelector(oapi.CelSelector{Cel: celExpression}); err != nil { - panic(err) - } - return selector -} - // Helper function to create a literal value from a Go value. func mustCreateLiteralValue(value any) *oapi.LiteralValue { lv := &oapi.LiteralValue{} @@ -265,7 +257,7 @@ func TestVariableManager_ResourceVariableTakesPrecedence(t *testing.T) { // Deployment has replicas with value = 3 varID := addDeploymentVariable(st, deploymentID, "replicas", nil) - selector := mustCreateSelector("true") // matches all resources + selector := "true" // matches all resources addDeploymentVariableValue(st, varID, 10, selector, mustCreateValueFromLiteral(3)) // Evaluate @@ -325,12 +317,11 @@ func TestVariableManager_DeploymentVariablePriority(t *testing.T) { // Deployment variable with multiple values varID := addDeploymentVariable(st, deploymentID, "env", nil) - selector := mustCreateSelector("true") // both match // Add high priority value - addDeploymentVariableValue(st, varID, 10, selector, mustCreateValueFromLiteral("high-priority")) + addDeploymentVariableValue(st, varID, 10, "true", mustCreateValueFromLiteral("high-priority")) // Add low priority value - addDeploymentVariableValue(st, varID, 5, selector, mustCreateValueFromLiteral("low-priority")) + addDeploymentVariableValue(st, varID, 5, "true", mustCreateValueFromLiteral("low-priority")) // Evaluate mgr := New(st) @@ -391,8 +382,7 @@ func TestVariableManager_FallbackToDefault(t *testing.T) { varID := addDeploymentVariable(st, deploymentID, "config", defaultValue) // Selector only matches prod resources - selector := mustCreateSelector("resource.metadata.env == 'prod'") - addDeploymentVariableValue(st, varID, 10, selector, mustCreateValueFromLiteral("prod-config")) + addDeploymentVariableValue(st, varID, 10, "resource.metadata.env == 'prod'", mustCreateValueFromLiteral("prod-config")) // Evaluate (resource has env=dev, so selector won't match) mgr := New(st) @@ -452,8 +442,7 @@ func TestVariableManager_NoDefaultNotIncluded(t *testing.T) { varID := addDeploymentVariable(st, deploymentID, "config", nil) // Selector only matches prod resources - selector := mustCreateSelector("resource.metadata.env == 'prod'") - addDeploymentVariableValue(st, varID, 10, selector, mustCreateValueFromLiteral("prod-config")) + addDeploymentVariableValue(st, varID, 10, "resource.metadata.env == 'prod'", mustCreateValueFromLiteral("prod-config")) // Evaluate (resource has env=dev, so selector won't match) mgr := New(st) @@ -504,22 +493,20 @@ func TestVariableManager_SelectorFiltering(t *testing.T) { varID := addDeploymentVariable(st, deploymentID, "endpoint", nil) // East coast value - eastSelector := mustCreateSelector("resource.metadata.region == 'us-east-1'") addDeploymentVariableValue( st, varID, 10, - eastSelector, + "resource.metadata.region == 'us-east-1'", mustCreateValueFromLiteral("east.example.com"), ) // West coast value - westSelector := mustCreateSelector("resource.metadata.region == 'us-west-1'") addDeploymentVariableValue( st, varID, 10, - westSelector, + "resource.metadata.region == 'us-west-1'", mustCreateValueFromLiteral("west.example.com"), ) @@ -582,21 +569,19 @@ func TestVariableManager_NoSelectorMatches(t *testing.T) { varID := addDeploymentVariable(st, deploymentID, "endpoint", defaultValue) // Only US selectors - eastSelector := mustCreateSelector("resource.metadata.region == 'us-east-1'") addDeploymentVariableValue( st, varID, 10, - eastSelector, + "resource.metadata.region == 'us-east-1'", mustCreateValueFromLiteral("east.example.com"), ) - westSelector := mustCreateSelector("resource.metadata.region == 'us-west-1'") addDeploymentVariableValue( st, varID, 10, - westSelector, + "resource.metadata.region == 'us-west-1'", mustCreateValueFromLiteral("west.example.com"), ) @@ -807,29 +792,27 @@ func TestVariableManager_MixedPriorities(t *testing.T) { st, varID1, 10, - mustCreateSelector("true"), + "true", mustCreateValueFromLiteral("from-deployment"), ) // var2: resolved from deployment variable value (no resource var) varID2 := addDeploymentVariable(st, deploymentID, "var2", mustCreateLiteralValue("default2")) - premiumSelector := mustCreateSelector("resource.metadata.tier == 'premium'") addDeploymentVariableValue( st, varID2, 10, - premiumSelector, + "resource.metadata.tier == 'premium'", mustCreateValueFromLiteral("from-deployment-value"), ) // var3: resolved from default (selector doesn't match) varID3 := addDeploymentVariable(st, deploymentID, "var3", mustCreateLiteralValue("default3")) - basicSelector := mustCreateSelector("resource.metadata.tier == 'basic'") addDeploymentVariableValue( st, varID3, 10, - basicSelector, + "resource.metadata.tier == 'basic'", mustCreateValueFromLiteral("from-deployment-value-basic"), ) @@ -839,7 +822,7 @@ func TestVariableManager_MixedPriorities(t *testing.T) { st, varID4, 10, - basicSelector, + "resource.metadata.tier == 'basic'", mustCreateValueFromLiteral("basic-only"), ) @@ -938,11 +921,9 @@ func TestVariableManager_MultipleResources(t *testing.T) { // Deployment variable with env-specific values varID := addDeploymentVariable(st, deploymentID, "replicas", mustCreateLiteralValue(1)) - prodSelector := mustCreateSelector("resource.metadata.env == 'production'") - addDeploymentVariableValue(st, varID, 10, prodSelector, mustCreateValueFromLiteral(10)) + addDeploymentVariableValue(st, varID, 10, "resource.metadata.env == 'production'", mustCreateValueFromLiteral(10)) - stagingSelector := mustCreateSelector("resource.metadata.env == 'staging'") - addDeploymentVariableValue(st, varID, 10, stagingSelector, mustCreateValueFromLiteral(3)) + addDeploymentVariableValue(st, varID, 10, "resource.metadata.env == 'staging'", mustCreateValueFromLiteral(3)) mgr := New(st) diff --git a/apps/workspace-engine/pkg/workspace/releasemanager/verification/scheduler_test.go b/apps/workspace-engine/pkg/workspace/releasemanager/verification/scheduler_test.go index b1bfff0c3..08da9603b 100644 --- a/apps/workspace-engine/pkg/workspace/releasemanager/verification/scheduler_test.go +++ b/apps/workspace-engine/pkg/workspace/releasemanager/verification/scheduler_test.go @@ -9,12 +9,13 @@ import ( "testing" "time" - "github.com/google/uuid" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" "workspace-engine/pkg/oapi" "workspace-engine/pkg/statechange" "workspace-engine/pkg/workspace/store" + + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) // Helper functions. @@ -105,9 +106,8 @@ func createTestRelease(s *store.Store, ctx context.Context) *oapi.Release { Name: "test-env", Description: ptr("Test environment"), } - selector := &oapi.Selector{} - _ = selector.FromCelSelector(oapi.CelSelector{Cel: "true"}) - environment.ResourceSelector = selector + selector := "true" + environment.ResourceSelector = &selector _ = s.Environments.Upsert(ctx, environment) // Create deployment @@ -118,9 +118,8 @@ func createTestRelease(s *store.Store, ctx context.Context) *oapi.Release { Slug: "test-deployment", Description: ptr("Test deployment"), } - deploymentSelector := &oapi.Selector{} - _ = deploymentSelector.FromCelSelector(oapi.CelSelector{Cel: "true"}) - deployment.ResourceSelector = deploymentSelector + deploymentSelector := "true" + deployment.ResourceSelector = &deploymentSelector _ = s.Deployments.Upsert(ctx, deployment) // Create version diff --git a/apps/workspace-engine/pkg/workspace/store/deployments.go b/apps/workspace-engine/pkg/workspace/store/deployments.go index 5273bb5fb..afe26c95e 100644 --- a/apps/workspace-engine/pkg/workspace/store/deployments.go +++ b/apps/workspace-engine/pkg/workspace/store/deployments.go @@ -91,7 +91,11 @@ func (e *Deployments) Resources( allResourcesSlice = append(allResourcesSlice, resource) } - resources, err := selector.FilterResources(ctx, deployment.ResourceSelector, allResourcesSlice) + sel := "" + if deployment.ResourceSelector != nil { + sel = *deployment.ResourceSelector + } + resources, err := selector.FilterResources(ctx, sel, allResourcesSlice) if err != nil { return nil, err } @@ -110,7 +114,11 @@ func (e *Deployments) ForResource( ) ([]*oapi.Deployment, error) { deployments := make([]*oapi.Deployment, 0) for _, deployment := range e.Items() { - matched, err := selector.Match(ctx, deployment.ResourceSelector, resource) + sel := "" + if deployment.ResourceSelector != nil { + sel = *deployment.ResourceSelector + } + matched, err := selector.Match(ctx, sel, resource) if err != nil { return nil, err } diff --git a/apps/workspace-engine/pkg/workspace/store/diffcheck/deployment_test.go b/apps/workspace-engine/pkg/workspace/store/diffcheck/deployment_test.go index dfde90abe..fae4ed1f9 100644 --- a/apps/workspace-engine/pkg/workspace/store/diffcheck/deployment_test.go +++ b/apps/workspace-engine/pkg/workspace/store/diffcheck/deployment_test.go @@ -3,8 +3,9 @@ package diffcheck import ( "testing" - "github.com/stretchr/testify/assert" "workspace-engine/pkg/oapi" + + "github.com/stretchr/testify/assert" ) func TestHasDeploymentChanges_NoChanges(t *testing.T) { @@ -71,16 +72,8 @@ func TestHasDeploymentChangesBasic_DetectsChanges(t *testing.T) { oldAgent := "agent-old" newAgent := "agent-new" - oldSelector := &oapi.Selector{} - assert.NoError(t, oldSelector.FromCelSelector(oapi.CelSelector{Cel: "system == 'legacy'"})) - - newSelector := &oapi.Selector{} - assert.NoError( - t, - newSelector.FromCelSelector( - oapi.CelSelector{Cel: "system == 'modern' && team == 'platform'"}, - ), - ) + oldSelector := "system == 'legacy'" + newSelector := "system == 'modern' && team == 'platform'" old := &oapi.Deployment{ Name: "api-old", @@ -94,7 +87,7 @@ func TestHasDeploymentChangesBasic_DetectsChanges(t *testing.T) { "key": "value", }, }, - ResourceSelector: oldSelector, + ResourceSelector: &oldSelector, } updated := &oapi.Deployment{ @@ -107,7 +100,7 @@ func TestHasDeploymentChangesBasic_DetectsChanges(t *testing.T) { "replicas": 2, "extra": true, }, - ResourceSelector: newSelector, + ResourceSelector: &newSelector, } changes := hasDeploymentChangesBasic(old, updated) @@ -326,24 +319,13 @@ func TestHasDeploymentChanges_JobAgentConfigNestedChange(t *testing.T) { } func TestHasDeploymentChanges_ResourceSelectorChanged(t *testing.T) { - oldSelector := &oapi.Selector{} - _ = oldSelector.FromJsonSelector(oapi.JsonSelector{ - Json: map[string]any{ - "app": "api", - }, - }) - - newSelector := &oapi.Selector{} - _ = newSelector.FromJsonSelector(oapi.JsonSelector{ - Json: map[string]any{ - "app": "web", - }, - }) + oldSelector := "app == 'api'" + newSelector := "app == 'web'" old := &oapi.Deployment{ Name: "api-deployment", Slug: "api-deployment", - ResourceSelector: oldSelector, + ResourceSelector: &oldSelector, JobAgentConfig: oapi.JobAgentConfig{}, Id: "deploy-123", } @@ -351,7 +333,7 @@ func TestHasDeploymentChanges_ResourceSelectorChanged(t *testing.T) { updated := &oapi.Deployment{ Name: "api-deployment", Slug: "api-deployment", - ResourceSelector: newSelector, + ResourceSelector: &newSelector, JobAgentConfig: oapi.JobAgentConfig{}, Id: "deploy-123", } diff --git a/apps/workspace-engine/pkg/workspace/store/diffcheck/environment_test.go b/apps/workspace-engine/pkg/workspace/store/diffcheck/environment_test.go index c99eb7d69..f422b5b9a 100644 --- a/apps/workspace-engine/pkg/workspace/store/diffcheck/environment_test.go +++ b/apps/workspace-engine/pkg/workspace/store/diffcheck/environment_test.go @@ -4,8 +4,9 @@ import ( "testing" "time" - "github.com/stretchr/testify/assert" "workspace-engine/pkg/oapi" + + "github.com/stretchr/testify/assert" ) func TestHasEnvironmentChanges_NoChanges(t *testing.T) { @@ -56,31 +57,19 @@ func TestHasEnvironmentChangesBasic_DetectsChanges(t *testing.T) { oldDesc := "old description" newDesc := "new description" - oldSelector := &oapi.Selector{} - assert.NoError(t, oldSelector.FromJsonSelector(oapi.JsonSelector{ - Json: map[string]any{ - "env": "prod", - }, - })) - - newSelector := &oapi.Selector{} - assert.NoError(t, newSelector.FromJsonSelector(oapi.JsonSelector{ - Json: map[string]any{ - "env": "staging", - "region": "us-east-1", - }, - })) + oldSelector := "env == 'prod'" + newSelector := "env == 'staging' && region == 'us-east-1'" old := &oapi.Environment{ Name: "staging", Description: &oldDesc, - ResourceSelector: oldSelector, + ResourceSelector: &oldSelector, } updated := &oapi.Environment{ Name: "production", Description: &newDesc, - ResourceSelector: newSelector, + ResourceSelector: &newSelector, } changes := hasEnvironmentChangesBasic(old, updated) @@ -168,29 +157,18 @@ func TestHasEnvironmentChanges_DescriptionSetToNil(t *testing.T) { func TestHasEnvironmentChanges_ResourceSelectorChanged(t *testing.T) { // Create selectors with JsonSelector - oldSelector := &oapi.Selector{} - _ = oldSelector.FromJsonSelector(oapi.JsonSelector{ - Json: map[string]any{ - "env": "prod", - }, - }) - - newSelector := &oapi.Selector{} - _ = newSelector.FromJsonSelector(oapi.JsonSelector{ - Json: map[string]any{ - "env": "staging", - }, - }) + oldSelector := "env == 'prod'" + newSelector := "env == 'staging'" old := &oapi.Environment{ Name: "production", - ResourceSelector: oldSelector, + ResourceSelector: &oldSelector, Id: "env-123", } updated := &oapi.Environment{ Name: "production", - ResourceSelector: newSelector, + ResourceSelector: &newSelector, Id: "env-123", } @@ -210,13 +188,7 @@ func TestHasEnvironmentChanges_ResourceSelectorChanged(t *testing.T) { } func TestHasEnvironmentChanges_ResourceSelectorNilToSet(t *testing.T) { - newSelector := &oapi.Selector{} - _ = newSelector.FromJsonSelector(oapi.JsonSelector{ - Json: map[string]any{ - "env": "prod", - }, - }) - + newSelector := "env == 'prod'" old := &oapi.Environment{ Name: "production", ResourceSelector: nil, @@ -225,7 +197,7 @@ func TestHasEnvironmentChanges_ResourceSelectorNilToSet(t *testing.T) { updated := &oapi.Environment{ Name: "production", - ResourceSelector: newSelector, + ResourceSelector: &newSelector, Id: "env-123", } diff --git a/apps/workspace-engine/pkg/workspace/store/environments.go b/apps/workspace-engine/pkg/workspace/store/environments.go index 31f2105dc..876255b20 100644 --- a/apps/workspace-engine/pkg/workspace/store/environments.go +++ b/apps/workspace-engine/pkg/workspace/store/environments.go @@ -77,7 +77,11 @@ func (e *Environments) Resources( allResourcesSlice = append(allResourcesSlice, resource) } - resources, err := selector.FilterResources(ctx, environment.ResourceSelector, allResourcesSlice) + sel := "" + if environment.ResourceSelector != nil { + sel = *environment.ResourceSelector + } + resources, err := selector.FilterResources(ctx, sel, allResourcesSlice) if err != nil { return nil, err } @@ -96,7 +100,11 @@ func (e *Environments) ForResource( ) ([]*oapi.Environment, error) { environments := make([]*oapi.Environment, 0) for _, environment := range e.Items() { - matched, err := selector.Match(ctx, environment.ResourceSelector, resource) + sel := "" + if environment.ResourceSelector != nil { + sel = *environment.ResourceSelector + } + matched, err := selector.Match(ctx, sel, resource) if err != nil { return nil, err } diff --git a/apps/workspace-engine/pkg/workspace/store/relationship_indexes.go b/apps/workspace-engine/pkg/workspace/store/relationship_indexes.go index ebd7b08e7..b7eedb240 100644 --- a/apps/workspace-engine/pkg/workspace/store/relationship_indexes.go +++ b/apps/workspace-engine/pkg/workspace/store/relationship_indexes.go @@ -173,17 +173,11 @@ func NewRelationshipIndexes(store *Store) *RelationshipIndexes { // selectorToCel converts a Selector into a CEL fragment by extracting its CEL // expression and replacing the entity-type variable (e.g. "resource", "deployment", // "environment") with the given prefix ("from" or "to"). -func selectorToCel(sel *oapi.Selector, entityType oapi.RelatableEntityType, prefix string) string { - if sel == nil { +func selectorToCel(sel *string, entityType oapi.RelatableEntityType, prefix string) string { + if sel == nil || *sel == "" { return "" } - cs, err := sel.AsCelSelector() - if err != nil || cs.Cel == "" { - return "" - } - // Replace the entity type variable name with the from/to prefix. - // e.g. "resource.kind == 'vpc'" → "from.kind == 'vpc'" - replaced := strings.ReplaceAll(cs.Cel, string(entityType)+".", prefix+".") + replaced := strings.ReplaceAll(*sel, string(entityType)+".", prefix+".") return replaced } diff --git a/apps/workspace-engine/pkg/workspace/store/release_targets_test.go b/apps/workspace-engine/pkg/workspace/store/release_targets_test.go index ea66bf1df..704a3ece4 100644 --- a/apps/workspace-engine/pkg/workspace/store/release_targets_test.go +++ b/apps/workspace-engine/pkg/workspace/store/release_targets_test.go @@ -50,9 +50,8 @@ func createTestReleaseAndJob( Name: "test-env", Description: ptr("Test environment"), } - selector := &oapi.Selector{} - _ = selector.FromCelSelector(oapi.CelSelector{Cel: "true"}) - environment.ResourceSelector = selector + selector := "true" + environment.ResourceSelector = &selector _ = s.Environments.Upsert(ctx, environment) // Create deployment @@ -63,9 +62,8 @@ func createTestReleaseAndJob( Slug: "test-deployment", Description: ptr("Test deployment"), } - deploymentSelector := &oapi.Selector{} - _ = deploymentSelector.FromCelSelector(oapi.CelSelector{Cel: "true"}) - deployment.ResourceSelector = deploymentSelector + deploymentSelector := "true" + deployment.ResourceSelector = &deploymentSelector _ = s.Deployments.Upsert(ctx, deployment) // Create version @@ -278,9 +276,8 @@ func TestGetCurrentRelease_FailedVerification_FallbackToPrevious(t *testing.T) { environmentId := uuid.New().String() environment := &oapi.Environment{Id: environmentId, Name: "test-env"} - selector := &oapi.Selector{} - _ = selector.FromCelSelector(oapi.CelSelector{Cel: "true"}) - environment.ResourceSelector = selector + selector := "true" + environment.ResourceSelector = &selector _ = s.Environments.Upsert(ctx, environment) deploymentId := uuid.New().String() @@ -289,9 +286,8 @@ func TestGetCurrentRelease_FailedVerification_FallbackToPrevious(t *testing.T) { Name: "test-deployment", Slug: "test-deployment", } - deploymentSelector := &oapi.Selector{} - _ = deploymentSelector.FromCelSelector(oapi.CelSelector{Cel: "true"}) - deployment.ResourceSelector = deploymentSelector + deploymentSelector := "true" + deployment.ResourceSelector = &deploymentSelector _ = s.Deployments.Upsert(ctx, deployment) // Create release target @@ -410,9 +406,8 @@ func TestGetCurrentRelease_RunningVerification_FallbackToPrevious(t *testing.T) environmentId := uuid.New().String() environment := &oapi.Environment{Id: environmentId, Name: "test-env"} - selector := &oapi.Selector{} - _ = selector.FromCelSelector(oapi.CelSelector{Cel: "true"}) - environment.ResourceSelector = selector + selector := "true" + environment.ResourceSelector = &selector _ = s.Environments.Upsert(ctx, environment) deploymentId := uuid.New().String() @@ -421,9 +416,8 @@ func TestGetCurrentRelease_RunningVerification_FallbackToPrevious(t *testing.T) Name: "test-deployment", Slug: "test-deployment", } - deploymentSelector := &oapi.Selector{} - _ = deploymentSelector.FromCelSelector(oapi.CelSelector{Cel: "true"}) - deployment.ResourceSelector = deploymentSelector + deploymentSelector := "true" + deployment.ResourceSelector = &deploymentSelector _ = s.Deployments.Upsert(ctx, deployment) // Create release target @@ -584,9 +578,8 @@ func TestGetCurrentRelease_CancelledVerification_FallbackToPrevious(t *testing.T environmentId := uuid.New().String() environment := &oapi.Environment{Id: environmentId, Name: "test-env"} - selector := &oapi.Selector{} - _ = selector.FromCelSelector(oapi.CelSelector{Cel: "true"}) - environment.ResourceSelector = selector + selector := "true" + environment.ResourceSelector = &selector _ = s.Environments.Upsert(ctx, environment) deploymentId := uuid.New().String() @@ -595,9 +588,8 @@ func TestGetCurrentRelease_CancelledVerification_FallbackToPrevious(t *testing.T Name: "test-deployment", Slug: "test-deployment", } - deploymentSelector := &oapi.Selector{} - _ = deploymentSelector.FromCelSelector(oapi.CelSelector{Cel: "true"}) - deployment.ResourceSelector = deploymentSelector + deploymentSelector := "true" + deployment.ResourceSelector = &deploymentSelector _ = s.Deployments.Upsert(ctx, deployment) // Create release target diff --git a/apps/workspace-engine/pkg/workspace/store/repository/db/deployments/mapper.go b/apps/workspace-engine/pkg/workspace/store/repository/db/deployments/mapper.go index 537865a6f..fb66e14ed 100644 --- a/apps/workspace-engine/pkg/workspace/store/repository/db/deployments/mapper.go +++ b/apps/workspace-engine/pkg/workspace/store/repository/db/deployments/mapper.go @@ -10,33 +10,6 @@ import ( "workspace-engine/pkg/oapi" ) -func selectorFromString(s string) *oapi.Selector { - if s == "" { - return nil - } - var sel oapi.Selector - if err := json.Unmarshal([]byte(s), &sel); err == nil { - if cs, e := sel.AsCelSelector(); e == nil && cs.Cel != "" { - return &sel - } - } - sel = oapi.Selector{} - celJSON, _ := json.Marshal(oapi.CelSelector{Cel: s}) - _ = sel.UnmarshalJSON(celJSON) - return &sel -} - -func selectorToString(sel *oapi.Selector) string { - if sel == nil { - return "false" - } - cel, err := sel.AsCelSelector() - if err == nil && cel.Cel != "" { - return cel.Cel - } - return "false" -} - // ToOapi converts a db.Deployment into an oapi.Deployment. func ToOapi(row db.Deployment) *oapi.Deployment { description := row.Description @@ -69,9 +42,9 @@ func ToOapi(row db.Deployment) *oapi.Deployment { } } - var resourceSelector *oapi.Selector + var resourceSelector *string if row.ResourceSelector.Valid { - resourceSelector = selectorFromString(row.ResourceSelector.String) + resourceSelector = &row.ResourceSelector.String } return &oapi.Deployment{ @@ -124,7 +97,12 @@ func ToUpsertParams(d *oapi.Deployment) (db.UpsertDeploymentParams, error) { } } - selStr := selectorToString(d.ResourceSelector) + var selStr string + if d.ResourceSelector == nil || *d.ResourceSelector == "" { + selStr = "false" + } else { + selStr = *d.ResourceSelector + } resourceSelector := pgtype.Text{String: selStr, Valid: true} return db.UpsertDeploymentParams{ diff --git a/apps/workspace-engine/pkg/workspace/store/repository/db/deploymentvariables/mapper.go b/apps/workspace-engine/pkg/workspace/store/repository/db/deploymentvariables/mapper.go index 7ca7f15c8..69eee89ea 100644 --- a/apps/workspace-engine/pkg/workspace/store/repository/db/deploymentvariables/mapper.go +++ b/apps/workspace-engine/pkg/workspace/store/repository/db/deploymentvariables/mapper.go @@ -66,26 +66,10 @@ func ToVariableUpsertParams(e *oapi.DeploymentVariable) (db.UpsertDeploymentVari }, nil } -func selectorFromString(s string) *oapi.Selector { - if s == "" { - return nil - } - var sel oapi.Selector - if err := json.Unmarshal([]byte(s), &sel); err == nil { - if cs, e := sel.AsCelSelector(); e == nil && cs.Cel != "" { - return &sel - } - } - sel = oapi.Selector{} - celJSON, _ := json.Marshal(oapi.CelSelector{Cel: s}) - _ = sel.UnmarshalJSON(celJSON) - return &sel -} - func ValueToOapi(row db.DeploymentVariableValue) *oapi.DeploymentVariableValue { - var resourceSelector *oapi.Selector + var resourceSelector *string if row.ResourceSelector.Valid && row.ResourceSelector.String != "" { - resourceSelector = selectorFromString(row.ResourceSelector.String) + resourceSelector = &row.ResourceSelector.String } value := oapi.Value{} @@ -117,11 +101,8 @@ func ToValueUpsertParams( } var resourceSelector pgtype.Text - if e.ResourceSelector != nil { - cel, err := e.ResourceSelector.AsCelSelector() - if err == nil && cel.Cel != "" { - resourceSelector = pgtype.Text{String: cel.Cel, Valid: true} - } + if e.ResourceSelector != nil && *e.ResourceSelector != "" { + resourceSelector = pgtype.Text{String: *e.ResourceSelector, Valid: true} } valueBytes, err := e.Value.MarshalJSON() diff --git a/apps/workspace-engine/pkg/workspace/store/repository/db/environments/mapper.go b/apps/workspace-engine/pkg/workspace/store/repository/db/environments/mapper.go index a17d7fef6..a4b1acdeb 100644 --- a/apps/workspace-engine/pkg/workspace/store/repository/db/environments/mapper.go +++ b/apps/workspace-engine/pkg/workspace/store/repository/db/environments/mapper.go @@ -1,7 +1,6 @@ package environments import ( - "encoding/json" "fmt" "github.com/google/uuid" @@ -10,33 +9,6 @@ import ( "workspace-engine/pkg/oapi" ) -func selectorFromString(s string) *oapi.Selector { - if s == "" { - return nil - } - var sel oapi.Selector - if err := json.Unmarshal([]byte(s), &sel); err == nil { - if cs, e := sel.AsCelSelector(); e == nil && cs.Cel != "" { - return &sel - } - } - sel = oapi.Selector{} - celJSON, _ := json.Marshal(oapi.CelSelector{Cel: s}) - _ = sel.UnmarshalJSON(celJSON) - return &sel -} - -func selectorToString(sel *oapi.Selector) string { - if sel == nil { - return "false" - } - cel, err := sel.AsCelSelector() - if err == nil && cel.Cel != "" { - return cel.Cel - } - return "false" -} - // ToOapi converts a db.Environment into an oapi.Environment. func ToOapi(row db.Environment) *oapi.Environment { var description *string @@ -53,7 +25,7 @@ func ToOapi(row db.Environment) *oapi.Environment { Id: row.ID.String(), Name: row.Name, Description: description, - ResourceSelector: selectorFromString(row.ResourceSelector), + ResourceSelector: &row.ResourceSelector, Metadata: metadata, CreatedAt: row.CreatedAt.Time, } @@ -81,11 +53,16 @@ func ToUpsertParams(e *oapi.Environment) (db.UpsertEnvironmentParams, error) { createdAt = pgtype.Timestamptz{Time: e.CreatedAt, Valid: true} } + resourceSelector := "false" + if e.ResourceSelector != nil && *e.ResourceSelector != "" { + resourceSelector = *e.ResourceSelector + } + return db.UpsertEnvironmentParams{ ID: id, Name: e.Name, Description: description, - ResourceSelector: selectorToString(e.ResourceSelector), + ResourceSelector: resourceSelector, Metadata: metadata, WorkspaceID: uuid.Nil, // set by caller CreatedAt: createdAt, diff --git a/apps/workspace-engine/pkg/workspace/store/repository/db/policies/mapper.go b/apps/workspace-engine/pkg/workspace/store/repository/db/policies/mapper.go index 38495713e..ccf06c17c 100644 --- a/apps/workspace-engine/pkg/workspace/store/repository/db/policies/mapper.go +++ b/apps/workspace-engine/pkg/workspace/store/repository/db/policies/mapper.go @@ -5,34 +5,14 @@ import ( "fmt" "time" + "workspace-engine/pkg/db" + "workspace-engine/pkg/oapi" + "github.com/charmbracelet/log" "github.com/google/uuid" "github.com/jackc/pgx/v5/pgtype" - "workspace-engine/pkg/db" - "workspace-engine/pkg/oapi" ) -func selectorFromString(s string) oapi.Selector { - var sel oapi.Selector - if err := json.Unmarshal([]byte(s), &sel); err == nil { - if cs, e := sel.AsCelSelector(); e == nil && cs.Cel != "" { - return sel - } - } - sel = oapi.Selector{} - celJSON, _ := json.Marshal(oapi.CelSelector{Cel: s}) - _ = sel.UnmarshalJSON(celJSON) - return sel -} - -func selectorToString(sel oapi.Selector) string { - cel, err := sel.AsCelSelector() - if err == nil && cel.Cel != "" { - return cel.Cel - } - return "false" -} - func pgtypeTextToPtr(t pgtype.Text) *string { if t.Valid { return &t.String @@ -122,13 +102,12 @@ func PolicyToOapi(row db.Policy, rules RuleRows) *oapi.Policy { } for _, r := range rules.EnvironmentProgression { - sel := selectorFromString(r.DependsOnEnvironmentSelector) policyRules = append(policyRules, oapi.PolicyRule{ Id: r.ID.String(), PolicyId: r.PolicyID.String(), CreatedAt: r.CreatedAt.Time.Format(time.RFC3339), EnvironmentProgression: &oapi.EnvironmentProgressionRule{ - DependsOnEnvironmentSelector: sel, + DependsOnEnvironmentSelector: r.DependsOnEnvironmentSelector, MaximumAgeHours: pgtypeInt4ToPtr(r.MaximumAgeHours), MinimumSockTimeMinutes: pgtypeInt4ToPtr(r.MinimumSoakTimeMinutes), MinimumSuccessPercentage: pgtypeFloat4ToPtr(r.MinimumSuccessPercentage), @@ -220,14 +199,13 @@ func PolicyToOapi(row db.Policy, rules RuleRows) *oapi.Policy { } for _, r := range rules.VersionSelector { - sel := selectorFromString(r.Selector) policyRules = append(policyRules, oapi.PolicyRule{ Id: r.ID.String(), PolicyId: r.PolicyID.String(), CreatedAt: r.CreatedAt.Time.Format(time.RFC3339), VersionSelector: &oapi.VersionSelectorRule{ Description: pgtypeTextToPtr(r.Description), - Selector: sel, + Selector: r.Selector, }, }) } diff --git a/apps/workspace-engine/pkg/workspace/store/repository/db/policies/repo.go b/apps/workspace-engine/pkg/workspace/store/repository/db/policies/repo.go index 7bc6b1598..a48f48059 100644 --- a/apps/workspace-engine/pkg/workspace/store/repository/db/policies/repo.go +++ b/apps/workspace-engine/pkg/workspace/store/repository/db/policies/repo.go @@ -6,13 +6,14 @@ import ( "fmt" "time" + "workspace-engine/pkg/db" + "workspace-engine/pkg/oapi" + "github.com/charmbracelet/log" "github.com/google/uuid" "github.com/jackc/pgx/v5/pgtype" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" - "workspace-engine/pkg/db" - "workspace-engine/pkg/oapi" ) var policyRepoTracer = otel.Tracer("workspace/store/repository/db/policies") @@ -302,7 +303,7 @@ func (r *Repo) insertRulesWithQueries( db.UpsertEnvironmentProgressionRuleParams{ ID: ruleID, PolicyID: policyID, - DependsOnEnvironmentSelector: selectorToString(ep.DependsOnEnvironmentSelector), + DependsOnEnvironmentSelector: ep.DependsOnEnvironmentSelector, MaximumAgeHours: optInt32ToPgint4(ep.MaximumAgeHours), MinimumSoakTimeMinutes: optInt32ToPgint4(ep.MinimumSockTimeMinutes), MinimumSuccessPercentage: optFloat32ToPgfloat4(ep.MinimumSuccessPercentage), @@ -409,7 +410,7 @@ func (r *Repo) insertRulesWithQueries( ID: ruleID, PolicyID: policyID, Description: optStringToPgtext(vs.Description), - Selector: selectorToString(vs.Selector), + Selector: vs.Selector, CreatedAt: createdAt, }); err != nil { return fmt.Errorf("upsert version_selector rule: %w", err) diff --git a/apps/workspace-engine/pkg/workspace/store/resources.go b/apps/workspace-engine/pkg/workspace/store/resources.go index 6ea664ff9..30949019d 100644 --- a/apps/workspace-engine/pkg/workspace/store/resources.go +++ b/apps/workspace-engine/pkg/workspace/store/resources.go @@ -157,7 +157,7 @@ func (r *Resources) Variables(resourceId string) map[string]*oapi.ResourceVariab return variables } -func (r *Resources) ForSelector(ctx context.Context, sel *oapi.Selector) map[string]*oapi.Resource { +func (r *Resources) ForSelector(ctx context.Context, sel string) map[string]*oapi.Resource { resources := make(map[string]*oapi.Resource) for _, resource := range r.Items() { matched, err := selector.Match(ctx, sel, resource) @@ -175,14 +175,20 @@ func (r *Resources) ForEnvironment( ctx context.Context, environment *oapi.Environment, ) map[string]*oapi.Resource { - return r.ForSelector(ctx, environment.ResourceSelector) + if environment.ResourceSelector == nil { + return make(map[string]*oapi.Resource) + } + return r.ForSelector(ctx, *environment.ResourceSelector) } func (r *Resources) ForDeployment( ctx context.Context, deployment *oapi.Deployment, ) map[string]*oapi.Resource { - return r.ForSelector(ctx, deployment.ResourceSelector) + if deployment.ResourceSelector == nil { + return make(map[string]*oapi.Resource) + } + return r.ForSelector(ctx, *deployment.ResourceSelector) } func (r *Resources) ForProvider(ctx context.Context, providerId string) map[string]*oapi.Resource { diff --git a/apps/workspace-engine/pkg/workspace/store/restore_test.go b/apps/workspace-engine/pkg/workspace/store/restore_test.go index 4ec1d1c5d..9d6a0830c 100644 --- a/apps/workspace-engine/pkg/workspace/store/restore_test.go +++ b/apps/workspace-engine/pkg/workspace/store/restore_test.go @@ -79,11 +79,8 @@ func TestStore_Restore_MaterializedViewsInitialized(t *testing.T) { } // Set up resource selector to match resources with env=production - selector := &oapi.Selector{} - _ = selector.FromCelSelector(oapi.CelSelector{ - Cel: "resource.metadata['env'] == 'production'", - }) - environment.ResourceSelector = selector + selector := "resource.metadata['env'] == 'production'" + environment.ResourceSelector = &selector // Create deployment with resource selector deploymentId := uuid.New().String() @@ -94,11 +91,8 @@ func TestStore_Restore_MaterializedViewsInitialized(t *testing.T) { Description: ptr("Web application"), } - deploymentSelector := &oapi.Selector{} - _ = deploymentSelector.FromCelSelector(oapi.CelSelector{ - Cel: "resource.metadata['env'] == 'production'", - }) - deployment.ResourceSelector = deploymentSelector + deploymentSelector := "resource.metadata['env'] == 'production'" + deployment.ResourceSelector = &deploymentSelector // Build changes and save to persistence changes := persistence.NewChangesBuilder(namespace). @@ -262,33 +256,24 @@ func TestStore_Restore_MultipleEnvironments(t *testing.T) { Id: prodEnvId, Name: "production", } - prodSelector := &oapi.Selector{} - _ = prodSelector.FromCelSelector( - oapi.CelSelector{Cel: "resource.metadata['env'] == 'production'"}, - ) - prodEnv.ResourceSelector = prodSelector + prodSelector := "resource.metadata['env'] == 'production'" + prodEnv.ResourceSelector = &prodSelector stagingEnvId := uuid.New().String() stagingEnv := &oapi.Environment{ Id: stagingEnvId, Name: "staging", } - stagingSelector := &oapi.Selector{} - _ = stagingSelector.FromCelSelector( - oapi.CelSelector{Cel: "resource.metadata['env'] == 'staging'"}, - ) - stagingEnv.ResourceSelector = stagingSelector + stagingSelector := "resource.metadata['env'] == 'staging'" + stagingEnv.ResourceSelector = &stagingSelector devEnvId := uuid.New().String() devEnv := &oapi.Environment{ Id: devEnvId, Name: "development", } - devSelector := &oapi.Selector{} - _ = devSelector.FromCelSelector( - oapi.CelSelector{Cel: "resource.metadata['env'] == 'development'"}, - ) - devEnv.ResourceSelector = devSelector + devSelector := "resource.metadata['env'] == 'development'" + devEnv.ResourceSelector = &devSelector // Save all entities changes := persistence.NewChangesBuilder(namespace). @@ -387,11 +372,8 @@ func TestStore_Restore_AllMaterializedViewsInitialized(t *testing.T) { Name: "production", Description: ptr("Production environment"), } - env1Selector := &oapi.Selector{} - _ = env1Selector.FromCelSelector( - oapi.CelSelector{Cel: "resource.metadata['env'] == 'production'"}, - ) - env1.ResourceSelector = env1Selector + env1Selector := "resource.metadata['env'] == 'production'" + env1.ResourceSelector = &env1Selector env2Id := uuid.New().String() env2 := &oapi.Environment{ @@ -399,11 +381,8 @@ func TestStore_Restore_AllMaterializedViewsInitialized(t *testing.T) { Name: "production-frontend", Description: ptr("Production frontend environment"), } - env2Selector := &oapi.Selector{} - _ = env2Selector.FromCelSelector( - oapi.CelSelector{Cel: "resource.metadata['tier'] == 'frontend'"}, - ) - env2.ResourceSelector = env2Selector + env2Selector := "resource.metadata['tier'] == 'frontend'" + env2.ResourceSelector = &env2Selector // Create deployments with selectors deploy1Id := uuid.New().String() @@ -413,11 +392,8 @@ func TestStore_Restore_AllMaterializedViewsInitialized(t *testing.T) { Slug: "api-deploy", Description: ptr("API deployment"), } - deploy1Selector := &oapi.Selector{} - _ = deploy1Selector.FromCelSelector( - oapi.CelSelector{Cel: "resource.metadata['tier'] == 'backend'"}, - ) - deploy1.ResourceSelector = deploy1Selector + deploy1Selector := "resource.metadata['tier'] == 'backend'" + deploy1.ResourceSelector = &deploy1Selector deploy2Id := uuid.New().String() deploy2 := &oapi.Deployment{ @@ -426,11 +402,8 @@ func TestStore_Restore_AllMaterializedViewsInitialized(t *testing.T) { Slug: "frontend-deploy", Description: ptr("Frontend deployment"), } - deploy2Selector := &oapi.Selector{} - _ = deploy2Selector.FromCelSelector( - oapi.CelSelector{Cel: "resource.metadata['tier'] == 'frontend'"}, - ) - deploy2.ResourceSelector = deploy2Selector + deploy2Selector := "resource.metadata['tier'] == 'frontend'" + deploy2.ResourceSelector = &deploy2Selector // Create deployment versions version1Id := uuid.New().String() @@ -555,9 +528,8 @@ func TestStore_Restore_DetectsMissingMaterializedViewInitialization(t *testing.T Id: environmentId, Name: "production", } - selector := &oapi.Selector{} - _ = selector.FromCelSelector(oapi.CelSelector{Cel: "resource.metadata['env'] == 'production'"}) - environment.ResourceSelector = selector + selector := "resource.metadata['env'] == 'production'" + environment.ResourceSelector = &selector changes := persistence.NewChangesBuilder(namespace). Set(system). @@ -699,13 +671,11 @@ func TestStore_Restore_RelationshipRules(t *testing.T) { } // Set up selectors - fromSelector := &oapi.Selector{} - _ = fromSelector.FromCelSelector(oapi.CelSelector{Cel: "resource.kind == 'vpc'"}) - rule1.FromSelector = fromSelector + fromSelector := "resource.kind == 'vpc'" + rule1.FromSelector = &fromSelector - toSelector := &oapi.Selector{} - _ = toSelector.FromCelSelector(oapi.CelSelector{Cel: "resource.kind == 'kubernetes-cluster'"}) - rule1.ToSelector = toSelector + toSelector := "resource.kind == 'kubernetes-cluster'" + rule1.ToSelector = &toSelector // Set up matcher matcher := &oapi.RelationshipRule_Matcher{} diff --git a/apps/workspace-engine/pkg/workspace/store/store.go b/apps/workspace-engine/pkg/workspace/store/store.go index 1a9b1623e..10e44c686 100644 --- a/apps/workspace-engine/pkg/workspace/store/store.go +++ b/apps/workspace-engine/pkg/workspace/store/store.go @@ -508,14 +508,21 @@ func (s *Store) Restore( // Check environment selector once per resource for _, resource := range s.Resources.Items() { - isInEnv, err := selector.Match(ctx, environment.ResourceSelector, resource) + envSel := "" + if environment.ResourceSelector != nil { + envSel = *environment.ResourceSelector + } + isInEnv, err := selector.Match(ctx, envSel, resource) if err != nil || !isInEnv { continue } - // Only check deployment selectors for matching deployments for _, deployment := range matchingDeployments { - isInDeployment, err := selector.Match(ctx, deployment.ResourceSelector, resource) + depSel := "" + if deployment.ResourceSelector != nil { + depSel = *deployment.ResourceSelector + } + isInDeployment, err := selector.Match(ctx, depSel, resource) if err != nil || !isInDeployment { continue } diff --git a/apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/resolve.go b/apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/resolve.go index afae50f74..10d58fc06 100644 --- a/apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/resolve.go +++ b/apps/workspace-engine/svc/controllers/desiredrelease/variableresolver/resolve.go @@ -250,7 +250,7 @@ func resolveFromValues( matched = append(matched, v) continue } - ok, _ := selector.Match(ctx, v.ResourceSelector, resource) + ok, _ := selector.Match(ctx, *v.ResourceSelector, resource) if ok { matched = append(matched, v) } diff --git a/apps/workspace-engine/svc/controllers/jobeligibility/reconcile_test.go b/apps/workspace-engine/svc/controllers/jobeligibility/reconcile_test.go index 97800b7c8..94d12c731 100644 --- a/apps/workspace-engine/svc/controllers/jobeligibility/reconcile_test.go +++ b/apps/workspace-engine/svc/controllers/jobeligibility/reconcile_test.go @@ -1509,10 +1509,10 @@ func TestReconcile_JobAgentConfig_DeepMergesThreeLevels(t *testing.T) { cfg := setter.createdJobs[0].JobAgentConfig - assert.Equal(t, float64(123), cfg["installationId"], "agent-level key should survive") + assert.InEpsilon(t, float64(123), cfg["installationId"], 0, "agent-level key should survive") assert.Equal(t, "myorg", cfg["owner"], "agent-level key should survive") assert.Equal(t, "myrepo", cfg["repo"], "agent-level key should survive") - assert.Equal(t, float64(456), cfg["workflowId"], "deployment-level key should be merged in") + assert.InEpsilon(t, float64(456), cfg["workflowId"], 0, "deployment-level key should be merged in") assert.Equal(t, "v1.2.3", cfg["image"], "version-level key should be merged in") - assert.Equal(t, float64(90), cfg["timeout"], "version-level should win over deployment and agent") + assert.InEpsilon(t, float64(90), cfg["timeout"], 0, "version-level should win over deployment and agent") } diff --git a/apps/workspace-engine/svc/http/server/openapi/resources/server.go b/apps/workspace-engine/svc/http/server/openapi/resources/server.go index 853748388..29b8ff929 100644 --- a/apps/workspace-engine/svc/http/server/openapi/resources/server.go +++ b/apps/workspace-engine/svc/http/server/openapi/resources/server.go @@ -3,9 +3,12 @@ package resources import ( "net/http" "sort" + "time" "github.com/gin-gonic/gin" "go.opentelemetry.io/otel" + + "workspace-engine/pkg/celutil" "workspace-engine/pkg/oapi" "workspace-engine/pkg/store/resources" ) @@ -29,15 +32,25 @@ func (r *Resources) QueryResources( resourcesGetter := resources.PostgresGetResources{} cel := "true" - if body.Filter != nil { - sel, err := body.Filter.AsCelSelector() - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{ - "error": "Invalid filter: " + err.Error(), - }) - return - } - cel = sel.Cel + if body.Filter != nil && *body.Filter != "" { + cel = *body.Filter + } + celValidator, err := celutil.NewEnvBuilder(). + WithMapVariables("resource"). + WithStandardExtensions(). + BuildCached(12 * time.Hour) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "error": "Failed to create cel validator: " + err.Error(), + }) + return + } + + if err := celValidator.Validate(cel); err != nil { + c.JSON(http.StatusBadRequest, gin.H{ + "error": "Invalid filter expression: " + err.Error(), + }) + return } resources, err := resourcesGetter.GetResources( c.Request.Context(), diff --git a/apps/workspace-engine/svc/http/server/openapi/validators/validators.go b/apps/workspace-engine/svc/http/server/openapi/validators/validators.go index 24f310e05..b4cc7d8a9 100644 --- a/apps/workspace-engine/svc/http/server/openapi/validators/validators.go +++ b/apps/workspace-engine/svc/http/server/openapi/validators/validators.go @@ -23,18 +23,12 @@ func (v *Validator) ValidateResourceSelector(c *gin.Context) { return } - cel, err := req.ResourceSelector.AsCelSelector() - if err != nil { - c.JSON(http.StatusOK, gin.H{"valid": false, "errors": []string{err.Error()}}) - return - } - - if cel.Cel == "" { + if req.ResourceSelector == "" { c.JSON(http.StatusOK, gin.H{"valid": false, "errors": []string{"CEL is required"}}) return } - if err := selectorEnv.Validate(cel.Cel); err != nil { + if err := selectorEnv.Validate(req.ResourceSelector); err != nil { c.JSON(http.StatusOK, gin.H{"valid": false, "errors": []string{err.Error()}}) return } diff --git a/apps/workspace-engine/test/controllers/harness/pipeline_opts.go b/apps/workspace-engine/test/controllers/harness/pipeline_opts.go index e4ae9a8e1..cbc87afde 100644 --- a/apps/workspace-engine/test/controllers/harness/pipeline_opts.go +++ b/apps/workspace-engine/test/controllers/harness/pipeline_opts.go @@ -396,9 +396,7 @@ func WithDeploymentWindowRule( // WithVersionSelectorRule configures a version selector rule with CEL. func WithVersionSelectorRule(cel string) PolicyRuleOption { return func(r *oapi.PolicyRule) { - s := &oapi.Selector{} - _ = s.FromCelSelector(oapi.CelSelector{Cel: cel}) - r.VersionSelector = &oapi.VersionSelectorRule{Selector: *s} + r.VersionSelector = &oapi.VersionSelectorRule{Selector: cel} } } @@ -477,10 +475,8 @@ func WithEnvironmentProgressionRule( opts ...EnvironmentProgressionOption, ) PolicyRuleOption { return func(r *oapi.PolicyRule) { - sel := &oapi.Selector{} - _ = sel.FromCelSelector(oapi.CelSelector{Cel: dependsOnSelector}) rule := &oapi.EnvironmentProgressionRule{ - DependsOnEnvironmentSelector: *sel, + DependsOnEnvironmentSelector: dependsOnSelector, } for _, o := range opts { o(rule) @@ -548,9 +544,7 @@ func ValuePriority(p int64) VariableValueOption { // so it only applies to resources matching the selector. func ValueSelector(cel string) VariableValueOption { return func(dvv *oapi.DeploymentVariableValue) { - s := &oapi.Selector{} - _ = s.FromCelSelector(oapi.CelSelector{Cel: cel}) - dvv.ResourceSelector = s + dvv.ResourceSelector = &cel } } diff --git a/packages/trpc/src/routes/resources.ts b/packages/trpc/src/routes/resources.ts index ad2d628ef..c28469567 100644 --- a/packages/trpc/src/routes/resources.ts +++ b/packages/trpc/src/routes/resources.ts @@ -164,9 +164,7 @@ export const resourcesRouter = router({ .input( z.object({ workspaceId: z.uuid(), - selector: z - .object({ json: z.record(z.string(), z.unknown()) }) - .or(z.object({ cel: z.string() })), + selector: z.string(), kind: z.string().optional(), limit: z.number().min(1).max(1000).default(50), offset: z.number().min(0).default(0), @@ -178,9 +176,9 @@ export const resourcesRouter = router({ const filter = (() => { if (kind == null) return selector; const kindFilter = `resource.kind == "${kind}"`; - if ("cel" in selector) - return { cel: `(${selector.cel}) && ${kindFilter}` }; - return { cel: kindFilter }; + if (selector && selector !== "true") + return `(${selector}) && ${kindFilter}`; + return kindFilter; })(); const result = await getClientFor().POST( diff --git a/packages/workspace-engine-sdk/src/schema.ts b/packages/workspace-engine-sdk/src/schema.ts index 5bc81b63c..042b11a7a 100644 --- a/packages/workspace-engine-sdk/src/schema.ts +++ b/packages/workspace-engine-sdk/src/schema.ts @@ -91,9 +91,6 @@ export interface components { CelMatcher: { cel: string; }; - CelSelector: { - cel: string; - }; DatadogMetricProvider: { /** * @description Datadog aggregator @@ -151,7 +148,8 @@ export interface components { [key: string]: string; }; name: string; - resourceSelector?: components["schemas"]["Selector"]; + /** @description CEL expression to determine if the deployment should be used */ + resourceSelector?: string; slug: string; }; DeploymentAndSystems: { @@ -180,7 +178,8 @@ export interface components { id: string; /** Format: int64 */ priority: number; - resourceSelector?: components["schemas"]["Selector"]; + /** @description CEL expression to determine if the deployment variable value should be used */ + resourceSelector?: string; value: components["schemas"]["Value"]; }; DeploymentVariableWithValues: { @@ -259,11 +258,13 @@ export interface components { [key: string]: string; }; name: string; - resourceSelector?: components["schemas"]["Selector"]; + /** @description CEL expression to determine if the environment should be used */ + resourceSelector?: string; workspaceId: string; }; EnvironmentProgressionRule: { - dependsOnEnvironmentSelector: components["schemas"]["Selector"]; + /** @description CEL expression to determine if the environment progression rule should be used */ + dependsOnEnvironmentSelector: string; /** * Format: int32 * @description Maximum age of dependency deployment before blocking progression (prevents stale promotions) @@ -446,11 +447,6 @@ export interface components { job: components["schemas"]["Job"]; verifications: components["schemas"]["JobVerification"][]; }; - JsonSelector: { - json: { - [key: string]: unknown; - }; - }; LiteralValue: components["schemas"]["BooleanValue"] | components["schemas"]["NumberValue"] | components["schemas"]["IntegerValue"] | components["schemas"]["StringValue"] | components["schemas"]["ObjectValue"] | components["schemas"]["NullValue"]; MetricProvider: components["schemas"]["HTTPMetricProvider"] | components["schemas"]["SleepMetricProvider"] | components["schemas"]["DatadogMetricProvider"] | components["schemas"]["PrometheusMetricProvider"] | components["schemas"]["TerraformCloudRunMetricProvider"]; /** @enum {boolean} */ @@ -617,7 +613,8 @@ export interface components { RelationDirection: "from" | "to"; RelationshipRule: { description?: string; - fromSelector?: components["schemas"]["Selector"]; + /** @description CEL expression to determine if the relationship rule should be used */ + fromSelector?: string; fromType: components["schemas"]["RelatableEntityType"]; id: string; matcher: components["schemas"]["CelMatcher"] | components["schemas"]["PropertiesMatcher"]; @@ -627,7 +624,8 @@ export interface components { name: string; reference: string; relationshipType: string; - toSelector?: components["schemas"]["Selector"]; + /** @description CEL expression to determine if the relationship rule should be used */ + toSelector?: string; toType: components["schemas"]["RelatableEntityType"]; workspaceId: string; }; @@ -808,7 +806,6 @@ export interface components { */ satisfiedAt?: string; }; - Selector: components["schemas"]["JsonSelector"] | components["schemas"]["CelSelector"]; SensitiveValue: { valueHash: string; }; @@ -970,7 +967,8 @@ export interface components { VersionSelectorRule: { /** @description Human-readable description of what this version selector does. Example: "Only deploy v2.x versions to staging environments" */ description?: string; - selector: components["schemas"]["Selector"]; + /** @description CEL expression to determine if the version selector should be used */ + selector: string; }; VersionSummary: { id: string; @@ -1065,7 +1063,8 @@ export interface components { WorkflowSelectorArrayInput: { key: string; selector: { - default?: components["schemas"]["Selector"]; + /** @description CEL expression to determine if the selector array input should be used */ + default?: string; /** @enum {string} */ entityType: "resource" | "environment" | "deployment"; }; @@ -1100,7 +1099,8 @@ export interface operations { requestBody?: { content: { "application/json": { - resourceSelector?: components["schemas"]["Selector"]; + /** @description CEL expression to validate. */ + resourceSelector: string; }; }; }; @@ -1193,7 +1193,8 @@ export interface operations { requestBody: { content: { "application/json": { - filter?: components["schemas"]["Selector"]; + /** @description CEL expression to filter resources. Defaults to "true" (all resources). */ + filter?: string; }; }; };