diff --git a/apps/api/src/types/openapi.ts b/apps/api/src/types/openapi.ts index 34cb5a71b..8deed3a5a 100644 --- a/apps/api/src/types/openapi.ts +++ b/apps/api/src/types/openapi.ts @@ -816,6 +816,9 @@ export interface components { [key: string]: unknown; }; jobAgentId?: string; + metadata?: { + [key: string]: string; + }; name: string; resourceSelector?: components["schemas"]["Selector"]; slug: string; @@ -839,6 +842,9 @@ export interface components { }; CreateEnvironmentRequest: { description?: string; + metadata?: { + [key: string]: string; + }; name: string; resourceSelector?: components["schemas"]["Selector"]; systemId: string; @@ -871,6 +877,9 @@ export interface components { }; CreateSystemRequest: { description?: string; + metadata?: { + [key: string]: string; + }; name: string; }; CreateWorkspaceRequest: { @@ -930,6 +939,9 @@ export interface components { [key: string]: unknown; }; jobAgentId?: string; + metadata: { + [key: string]: string; + }; name: string; resourceSelector?: components["schemas"]["Selector"]; slug: string; @@ -1007,6 +1019,9 @@ export interface components { createdAt: string; description?: string; id: string; + metadata: { + [key: string]: string; + }; name: string; resourceSelector?: components["schemas"]["Selector"]; systemId: string; @@ -1346,6 +1361,9 @@ export interface components { System: { description?: string; id: string; + metadata: { + [key: string]: string; + }; name: string; slug: string; workspaceId: string; @@ -1400,6 +1418,9 @@ export interface components { [key: string]: unknown; }; jobAgentId?: string; + metadata?: { + [key: string]: string; + }; name: string; resourceSelector?: components["schemas"]["Selector"]; slug: string; @@ -1435,6 +1456,9 @@ export interface components { }; UpsertEnvironmentRequest: { description?: string; + metadata?: { + [key: string]: string; + }; name: string; resourceSelector?: components["schemas"]["Selector"]; systemId: string; @@ -1485,6 +1509,9 @@ export interface components { }; UpsertSystemRequest: { description?: string; + metadata?: { + [key: string]: string; + }; name: string; }; UpsertUserApprovalRecordRequest: { diff --git a/apps/workspace-engine/oapi/openapi.json b/apps/workspace-engine/oapi/openapi.json index 9542daa37..966ceb28d 100644 --- a/apps/workspace-engine/oapi/openapi.json +++ b/apps/workspace-engine/oapi/openapi.json @@ -179,6 +179,12 @@ "name": { "type": "string" }, + "metadata": { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, "resourceSelector": { "$ref": "#/components/schemas/Selector" }, @@ -192,6 +198,7 @@ "required": [ "id", "name", + "metadata", "slug", "systemId", "jobAgentConfig" @@ -459,6 +466,7 @@ "required": [ "id", "name", + "metadata", "systemId", "createdAt" ], @@ -737,6 +745,12 @@ "name": { "type": "string" }, + "metadata": { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, "type": { "type": "string" }, @@ -749,7 +763,8 @@ "workspaceId", "name", "type", - "config" + "config", + "metadata" ], "type": "object" }, @@ -1764,6 +1779,12 @@ "name": { "type": "string" }, + "metadata": { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, "workspaceId": { "type": "string" } @@ -1771,7 +1792,8 @@ "required": [ "id", "workspaceId", - "name" + "name", + "metadata" ], "type": "object" }, diff --git a/apps/workspace-engine/oapi/spec/schemas/deployments.jsonnet b/apps/workspace-engine/oapi/spec/schemas/deployments.jsonnet index e609154c3..cbcbc5dd9 100644 --- a/apps/workspace-engine/oapi/spec/schemas/deployments.jsonnet +++ b/apps/workspace-engine/oapi/spec/schemas/deployments.jsonnet @@ -3,7 +3,7 @@ local openapi = import '../lib/openapi.libsonnet'; { Deployment: { type: 'object', - required: ['id', 'name', 'slug', 'systemId', 'jobAgentConfig'], + required: ['id', 'name', 'slug', 'systemId', 'jobAgentConfig', 'metadata'], properties: { id: { type: 'string' }, name: { type: 'string' }, @@ -13,6 +13,10 @@ local openapi = import '../lib/openapi.libsonnet'; jobAgentId: { type: 'string' }, jobAgentConfig: openapi.schemaRef('JobAgentConfig'), resourceSelector: openapi.schemaRef('Selector'), + metadata: { + type: 'object', + additionalProperties: { type: 'string' }, + }, }, }, diff --git a/apps/workspace-engine/oapi/spec/schemas/entities.jsonnet b/apps/workspace-engine/oapi/spec/schemas/entities.jsonnet index a3994ca3f..f4d824a93 100644 --- a/apps/workspace-engine/oapi/spec/schemas/entities.jsonnet +++ b/apps/workspace-engine/oapi/spec/schemas/entities.jsonnet @@ -64,7 +64,7 @@ local openapi = import '../lib/openapi.libsonnet'; Environment: { type: 'object', - required: ['id', 'name', 'systemId', 'createdAt'], + required: ['id', 'name', 'systemId', 'createdAt', 'metadata'], properties: { id: { type: 'string' }, name: { type: 'string' }, @@ -72,29 +72,41 @@ local openapi = import '../lib/openapi.libsonnet'; systemId: { type: 'string' }, resourceSelector: openapi.schemaRef('Selector'), createdAt: { type: 'string', format: 'date-time' }, + metadata: { + type: 'object', + additionalProperties: { type: 'string' }, + }, }, }, System: { type: 'object', - required: ['id', 'workspaceId', 'name'], + required: ['id', 'workspaceId', 'name', 'metadata'], properties: { id: { type: 'string' }, workspaceId: { type: 'string' }, name: { type: 'string' }, description: { type: 'string' }, + metadata: { + type: 'object', + additionalProperties: { type: 'string' }, + }, }, }, JobAgent: { type: 'object', - required: ['id', 'workspaceId', 'name', 'type', 'config'], + required: ['id', 'workspaceId', 'name', 'type', 'config', 'metadata'], properties: { id: { type: 'string' }, workspaceId: { type: 'string' }, name: { type: 'string' }, type: { type: 'string' }, config: openapi.schemaRef('JobAgentConfig'), + metadata: { + type: 'object', + additionalProperties: { type: 'string' }, + }, }, }, diff --git a/apps/workspace-engine/pkg/oapi/oapi.gen.go b/apps/workspace-engine/pkg/oapi/oapi.gen.go index 983163cf4..653747bc4 100644 --- a/apps/workspace-engine/pkg/oapi/oapi.gen.go +++ b/apps/workspace-engine/pkg/oapi/oapi.gen.go @@ -279,14 +279,15 @@ 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"` - Name string `json:"name"` - ResourceSelector *Selector `json:"resourceSelector,omitempty"` - Slug string `json:"slug"` - SystemId string `json:"systemId"` + Description *string `json:"description,omitempty"` + Id string `json:"id"` + JobAgentConfig JobAgentConfig `json:"jobAgentConfig"` + JobAgentId *string `json:"jobAgentId,omitempty"` + Metadata map[string]string `json:"metadata"` + Name string `json:"name"` + ResourceSelector *Selector `json:"resourceSelector,omitempty"` + Slug string `json:"slug"` + SystemId string `json:"systemId"` } // DeploymentAndSystem defines model for DeploymentAndSystem. @@ -378,12 +379,13 @@ 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"` - Name string `json:"name"` - ResourceSelector *Selector `json:"resourceSelector,omitempty"` - SystemId string `json:"systemId"` + 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"` + SystemId string `json:"systemId"` } // EnvironmentProgressionRule defines model for EnvironmentProgressionRule. @@ -502,11 +504,12 @@ type Job struct { // JobAgent defines model for JobAgent. type JobAgent struct { - Config JobAgentConfig `json:"config"` - Id string `json:"id"` - Name string `json:"name"` - Type string `json:"type"` - WorkspaceId string `json:"workspaceId"` + Config JobAgentConfig `json:"config"` + Id string `json:"id"` + Metadata map[string]string `json:"metadata"` + Name string `json:"name"` + Type string `json:"type"` + WorkspaceId string `json:"workspaceId"` } // JobAgentConfig defines model for JobAgentConfig. @@ -890,10 +893,11 @@ type StringValue = string // System defines model for System. type System struct { - Description *string `json:"description,omitempty"` - Id string `json:"id"` - Name string `json:"name"` - WorkspaceId string `json:"workspaceId"` + Description *string `json:"description,omitempty"` + Id string `json:"id"` + Metadata map[string]string `json:"metadata"` + Name string `json:"name"` + WorkspaceId string `json:"workspaceId"` } // TerraformCloudJobAgentConfig defines model for TerraformCloudJobAgentConfig. diff --git a/apps/workspace-engine/pkg/selector/langs/cel/cel.go b/apps/workspace-engine/pkg/selector/langs/cel/cel.go index 71194e4a6..d3db1205d 100644 --- a/apps/workspace-engine/pkg/selector/langs/cel/cel.go +++ b/apps/workspace-engine/pkg/selector/langs/cel/cel.go @@ -192,12 +192,13 @@ func resourceToMap(r *oapi.Resource) map[string]any { } func deploymentToMap(d *oapi.Deployment) map[string]any { - m := make(map[string]any, 8) + m := make(map[string]any, 9) m["id"] = d.Id m["name"] = d.Name m["slug"] = d.Slug m["systemId"] = d.SystemId m["jobAgentConfig"] = d.JobAgentConfig + m["metadata"] = d.Metadata if d.Description != nil { m["description"] = *d.Description } @@ -211,11 +212,12 @@ func deploymentToMap(d *oapi.Deployment) map[string]any { } func environmentToMap(e *oapi.Environment) map[string]any { - m := make(map[string]any, 6) + m := make(map[string]any, 7) m["id"] = e.Id m["name"] = e.Name m["systemId"] = e.SystemId m["createdAt"] = e.CreatedAt + m["metadata"] = e.Metadata if e.Description != nil { m["description"] = *e.Description } diff --git a/apps/workspace-engine/pkg/workspace/relationships/property.go b/apps/workspace-engine/pkg/workspace/relationships/property.go index 5c42e9708..9acfd39aa 100644 --- a/apps/workspace-engine/pkg/workspace/relationships/property.go +++ b/apps/workspace-engine/pkg/workspace/relationships/property.go @@ -125,6 +125,17 @@ func getDeploymentProperty(deployment *oapi.Deployment, propertyPath []string) ( return nil, err } return convertValue(value) + case "metadata": + if len(propertyPath) == 1 { + return convertValue(deployment.Metadata) + } + if len(propertyPath) == 2 { + if val, ok := deployment.Metadata[propertyPath[1]]; ok { + return convertValue(val) + } + return nil, fmt.Errorf("metadata key %s not found", propertyPath[1]) + } + return nil, fmt.Errorf("metadata path too deep: %v", propertyPath) default: return getPropertyReflection(deployment, propertyPath) } @@ -149,6 +160,17 @@ func getEnvironmentProperty(environment *oapi.Environment, propertyPath []string return nil, fmt.Errorf("description is nil") case "system_id", "systemid": return convertValue(environment.SystemId) + case "metadata": + if len(propertyPath) == 1 { + return convertValue(environment.Metadata) + } + if len(propertyPath) == 2 { + if val, ok := environment.Metadata[propertyPath[1]]; ok { + return convertValue(val) + } + return nil, fmt.Errorf("metadata key %s not found", propertyPath[1]) + } + return nil, fmt.Errorf("metadata path too deep: %v", propertyPath) default: return getPropertyReflection(environment, propertyPath) } diff --git a/apps/workspace-engine/pkg/workspace/store/deployments.go b/apps/workspace-engine/pkg/workspace/store/deployments.go index 457c5231c..f0a8628eb 100644 --- a/apps/workspace-engine/pkg/workspace/store/deployments.go +++ b/apps/workspace-engine/pkg/workspace/store/deployments.go @@ -34,6 +34,10 @@ func (e *Deployments) Upsert(ctx context.Context, deployment *oapi.Deployment) e _, span := deploymentsTracer.Start(ctx, "UpsertDeployment") defer span.End() + if deployment.Metadata == nil { + deployment.Metadata = map[string]string{} + } + e.repo.Deployments.Set(deployment.Id, deployment) e.store.changeset.RecordUpsert(deployment) diff --git a/apps/workspace-engine/pkg/workspace/store/diffcheck/deployment.go b/apps/workspace-engine/pkg/workspace/store/diffcheck/deployment.go index 083cf0abd..a0065ffbd 100644 --- a/apps/workspace-engine/pkg/workspace/store/diffcheck/deployment.go +++ b/apps/workspace-engine/pkg/workspace/store/diffcheck/deployment.go @@ -144,6 +144,18 @@ func hasDeploymentChangesBasic(old, new *oapi.Deployment) map[string]bool { } } + // Compare metadata + for key := range old.Metadata { + if newVal, exists := new.Metadata[key]; !exists || old.Metadata[key] != newVal { + changed["metadata."+key] = true + } + } + for key := range new.Metadata { + if _, exists := old.Metadata[key]; !exists { + changed["metadata."+key] = true + } + } + // Compare ResourceSelector using deep equality if !deepEqual(old.ResourceSelector, new.ResourceSelector) { changed["resourceselector"] = true 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 f3902acc8..093a39b03 100644 --- a/apps/workspace-engine/pkg/workspace/store/diffcheck/deployment_test.go +++ b/apps/workspace-engine/pkg/workspace/store/diffcheck/deployment_test.go @@ -413,6 +413,34 @@ func TestHasDeploymentChanges_ResourceSelectorChanged(t *testing.T) { assert.True(t, hasResourceSelectorChange, "Should detect resourceSelector change") } +func TestHasDeploymentChanges_MetadataChanged(t *testing.T) { + old := &oapi.Deployment{ + Name: "api-deployment", + Slug: "api-deployment", + SystemId: "sys-123", + JobAgentConfig: oapi.JobAgentConfig{}, + Metadata: map[string]string{ + "tier": "backend", + }, + Id: "deploy-123", + } + + new := &oapi.Deployment{ + Name: "api-deployment", + Slug: "api-deployment", + SystemId: "sys-123", + JobAgentConfig: oapi.JobAgentConfig{}, + Metadata: map[string]string{ + "tier": "frontend", + }, + Id: "deploy-123", + } + + changes := HasDeploymentChanges(old, new) + assert.Len(t, changes, 1, "Should detect metadata change") + assert.True(t, changes["metadata.tier"], "Should detect metadata.tier change") +} + func TestHasDeploymentChanges_MultipleChanges(t *testing.T) { oldDesc := "old description" newDesc := "new description" diff --git a/apps/workspace-engine/pkg/workspace/store/diffcheck/environment.go b/apps/workspace-engine/pkg/workspace/store/diffcheck/environment.go index 0e0a8c2ca..3be14105f 100644 --- a/apps/workspace-engine/pkg/workspace/store/diffcheck/environment.go +++ b/apps/workspace-engine/pkg/workspace/store/diffcheck/environment.go @@ -81,5 +81,17 @@ func hasEnvironmentChangesBasic(old, new *oapi.Environment) map[string]bool { changed["resourceselector"] = true } + // Compare metadata + for key := range old.Metadata { + if newVal, exists := new.Metadata[key]; !exists || old.Metadata[key] != newVal { + changed["metadata."+key] = true + } + } + for key := range new.Metadata { + if _, exists := old.Metadata[key]; !exists { + changed["metadata."+key] = true + } + } + return changed } 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 dd4e91947..20e4713d0 100644 --- a/apps/workspace-engine/pkg/workspace/store/diffcheck/environment_test.go +++ b/apps/workspace-engine/pkg/workspace/store/diffcheck/environment_test.go @@ -242,6 +242,30 @@ func TestHasEnvironmentChanges_ResourceSelectorChanged(t *testing.T) { assert.True(t, hasResourceSelectorChange, "Should detect resourceSelector change") } +func TestHasEnvironmentChanges_MetadataChanged(t *testing.T) { + old := &oapi.Environment{ + Name: "production", + SystemId: "sys-123", + Metadata: map[string]string{ + "region": "us-east-1", + }, + Id: "env-123", + } + + new := &oapi.Environment{ + Name: "production", + SystemId: "sys-123", + Metadata: map[string]string{ + "region": "us-west-2", + }, + Id: "env-123", + } + + changes := HasEnvironmentChanges(old, new) + assert.Len(t, changes, 1, "Should detect metadata change") + assert.True(t, changes["metadata.region"], "Should detect metadata.region change") +} + func TestHasEnvironmentChanges_ResourceSelectorNilToSet(t *testing.T) { newSelector := &oapi.Selector{} _ = newSelector.FromJsonSelector(oapi.JsonSelector{ diff --git a/apps/workspace-engine/pkg/workspace/store/environments.go b/apps/workspace-engine/pkg/workspace/store/environments.go index ecdf8075f..2127bcafb 100644 --- a/apps/workspace-engine/pkg/workspace/store/environments.go +++ b/apps/workspace-engine/pkg/workspace/store/environments.go @@ -29,6 +29,10 @@ func (e *Environments) Get(id string) (*oapi.Environment, bool) { } func (e *Environments) Upsert(ctx context.Context, environment *oapi.Environment) error { + if environment.Metadata == nil { + environment.Metadata = map[string]string{} + } + e.repo.Environments.Set(environment.Id, environment) e.store.changeset.RecordUpsert(environment) diff --git a/apps/workspace-engine/pkg/workspace/store/job_agents.go b/apps/workspace-engine/pkg/workspace/store/job_agents.go index b77559d75..653b95f3b 100644 --- a/apps/workspace-engine/pkg/workspace/store/job_agents.go +++ b/apps/workspace-engine/pkg/workspace/store/job_agents.go @@ -19,6 +19,10 @@ type JobAgents struct { } func (j *JobAgents) Upsert(ctx context.Context, jobAgent *oapi.JobAgent) { + if jobAgent.Metadata == nil { + jobAgent.Metadata = map[string]string{} + } + j.repo.JobAgents.Set(jobAgent.Id, jobAgent) j.store.changeset.RecordUpsert(jobAgent) } diff --git a/apps/workspace-engine/pkg/workspace/store/systems.go b/apps/workspace-engine/pkg/workspace/store/systems.go index 8c4c087f2..23d379a00 100644 --- a/apps/workspace-engine/pkg/workspace/store/systems.go +++ b/apps/workspace-engine/pkg/workspace/store/systems.go @@ -23,6 +23,10 @@ func (s *Systems) Get(id string) (*oapi.System, bool) { } func (s *Systems) Upsert(ctx context.Context, system *oapi.System) error { + if system.Metadata == nil { + system.Metadata = map[string]string{} + } + s.repo.Systems.Set(system.Id, system) s.store.changeset.RecordUpsert(system)