From 5f5d685b187159e87cdb6aacbe29327495225dfd Mon Sep 17 00:00:00 2001 From: Jennifer Ubah Date: Wed, 16 Sep 2026 16:02:08 -0400 Subject: [PATCH 1/8] Bump control plane version + update test Assisted-By: Cursor AI Signed-off-by: Jennifer Ubah --- go.mod | 14 +++++------ go.sum | 28 +++++++++++----------- internal/commands/catalog_instance_test.go | 15 ++++++++++-- internal/commands/catalog_item_test.go | 9 +++++++ 4 files changed, 43 insertions(+), 23 deletions(-) diff --git a/go.mod b/go.mod index c2d0b33..9427129 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.25.5 require ( github.com/coreos/go-oidc/v3 v3.20.0 - github.com/dcm-project/control-plane v0.0.0-20260617094433-e4374fc25292 + github.com/dcm-project/control-plane v0.0.0-20260805143143-76ca1d7a6639 github.com/onsi/ginkgo/v2 v2.29.0 github.com/onsi/gomega v1.41.0 github.com/spf13/cobra v1.10.2 @@ -48,11 +48,11 @@ require ( github.com/spf13/pflag v1.0.10 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/woodsbury/decimal128 v1.4.0 // indirect - golang.org/x/mod v0.35.0 // indirect - golang.org/x/net v0.53.0 // indirect - golang.org/x/sync v0.20.0 // indirect - golang.org/x/sys v0.44.0 // indirect - golang.org/x/text v0.37.0 // indirect - golang.org/x/tools v0.44.0 // indirect + golang.org/x/mod v0.37.0 // indirect + golang.org/x/net v0.56.0 // indirect + golang.org/x/sync v0.21.0 // indirect + golang.org/x/sys v0.46.0 // indirect + golang.org/x/text v0.39.0 // indirect + golang.org/x/tools v0.47.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect ) diff --git a/go.sum b/go.sum index f9639f9..5434119 100644 --- a/go.sum +++ b/go.sum @@ -14,8 +14,8 @@ github.com/danieljoos/wincred v1.2.2/go.mod h1:w7w4Utbrz8lqeMbDAK0lkNJUv5sAOkFi7 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dcm-project/control-plane v0.0.0-20260617094433-e4374fc25292 h1:VnUyqtBqLrXTC1ClDSogbvVGDWvikhvmyxP5Znw1yX4= -github.com/dcm-project/control-plane v0.0.0-20260617094433-e4374fc25292/go.mod h1:dYV62wU2z+H1XUBdGoTMdh0V5j8AyJc4Lk55TLgPSBw= +github.com/dcm-project/control-plane v0.0.0-20260805143143-76ca1d7a6639 h1:xA7kdlg1NZOiLJkCEyAf11I9StZfNlsksv8cStTUsII= +github.com/dcm-project/control-plane v0.0.0-20260805143143-76ca1d7a6639/go.mod h1:oLQKa1vW81Dmpit5iGjU0EQaRARadjrJ/BmCadwqir4= github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ= github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= @@ -144,20 +144,20 @@ github.com/zalando/go-keyring v0.2.6 h1:r7Yc3+H+Ux0+M72zacZoItR3UDxeWfKTcabvkI8u github.com/zalando/go-keyring v0.2.6/go.mod h1:2TCrxYrbUNYfNS/Kgy/LSrkSQzZ5UPVH85RwfczwvcI= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= -golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= -golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= -golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= +golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= +golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= +golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= +golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= -golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= -golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= -golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ= -golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= -golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= -golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= -golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= +golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= +golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= +golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.39.0 h1:UbZz4pLOvn600D6Oh6GGEI6VAmndrEBLv8/6BEXzyus= +golang.org/x/text v0.39.0/go.mod h1:3UwRclnC2g0TU9x8PZiyfOajCd1zaUNHF9cvqcQZ+ZM= +golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q= +golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA= google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A= google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/internal/commands/catalog_instance_test.go b/internal/commands/catalog_instance_test.go index b0059cc..8400f0a 100644 --- a/internal/commands/catalog_instance_test.go +++ b/internal/commands/catalog_instance_test.go @@ -82,11 +82,22 @@ var _ = Describe("Catalog Instance Commands", func() { var body map[string]any Expect(json.NewDecoder(r.Body).Decode(&body)).To(Succeed()) Expect(body["display_name"]).To(Equal("My App Instance")) + spec, ok := body["spec"].(map[string]any) + Expect(ok).To(BeTrue(), "request body must include spec") + Expect(spec["catalog_item_id"]).To(Equal("my-catalog-item")) + userValues, ok := spec["user_values"].([]any) + Expect(ok).To(BeTrue(), "request body must include spec.user_values") + Expect(userValues).To(HaveLen(1)) + userValue, ok := userValues[0].(map[string]any) + Expect(ok).To(BeTrue()) + Expect(userValue["resource"]).To(Equal("app")) + Expect(userValue["path"]).To(Equal("vcpu.count")) + Expect(userValue["value"]).To(Equal("2")) writeJSONResponse(w, http.StatusCreated, sampleInstanceResponse()) })) - yamlFile := writeTempFile("display_name: My App Instance\napi_version: v1alpha1\nspec:\n catalog_item_id: my-catalog-item\n user_values: []\n", ".yaml") + yamlFile := writeTempFile("display_name: My App Instance\napi_version: v1alpha1\nspec:\n catalog_item_id: my-catalog-item\n user_values:\n - resource: app\n path: vcpu.count\n value: \"2\"\n", ".yaml") err := executeCommand("catalog", "instance", "create", "--from-file", yamlFile) Expect(err).NotTo(HaveOccurred()) @@ -106,7 +117,7 @@ var _ = Describe("Catalog Instance Commands", func() { writeJSONResponse(w, http.StatusCreated, sampleInstanceResponse()) })) - yamlFile := writeTempFile("display_name: My App Instance\napi_version: v1alpha1\nspec:\n catalog_item_id: my-catalog-item\n user_values: []\n", ".yaml") + yamlFile := writeTempFile("display_name: My App Instance\napi_version: v1alpha1\nspec:\n catalog_item_id: my-catalog-item\n user_values:\n - resource: app\n path: vcpu.count\n value: \"2\"\n", ".yaml") err := executeCommand("catalog", "instance", "create", "--from-file", yamlFile, "--id", "my-instance") Expect(err).NotTo(HaveOccurred()) diff --git a/internal/commands/catalog_item_test.go b/internal/commands/catalog_item_test.go index c730b3b..ad28e7f 100644 --- a/internal/commands/catalog_item_test.go +++ b/internal/commands/catalog_item_test.go @@ -86,6 +86,15 @@ var _ = Describe("Catalog Item Commands", func() { var body map[string]any Expect(json.NewDecoder(r.Body).Decode(&body)).To(Succeed()) Expect(body["display_name"]).To(Equal("Small Container")) + spec, ok := body["spec"].(map[string]any) + Expect(ok).To(BeTrue(), "request body must include spec") + resources, ok := spec["resources"].([]any) + Expect(ok).To(BeTrue(), "request body must include spec.resources") + Expect(resources).To(HaveLen(1)) + resource, ok := resources[0].(map[string]any) + Expect(ok).To(BeTrue()) + Expect(resource["name"]).To(Equal("main")) + Expect(resource["service_type"]).To(Equal("container")) writeJSONResponse(w, http.StatusCreated, sampleCatalogItemResponse()) })) From ecb05490f88233af03dc28cc9a73c0453594aa4e Mon Sep 17 00:00:00 2001 From: Jennifer Ubah Date: Wed, 16 Sep 2026 16:39:58 -0400 Subject: [PATCH 2/8] Update doc + remove top level service type in catalog item Assisted-By: Cursor AI Signed-off-by: Jennifer Ubah --- README.md | 53 +++++++++++++++++--------- internal/commands/catalog_item.go | 13 +------ internal/commands/catalog_item_test.go | 2 - 3 files changed, 36 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index 77671e6..ba6a6cc 100644 --- a/README.md +++ b/README.md @@ -426,28 +426,41 @@ Catalog item file format: ```yaml api_version: v1alpha1 -display_name: "Small Container" +display_name: "App with Database" spec: - service_type: container - fields: - - path: spec.replicas - display_name: "Replica Count" - editable: true - default: "1" - validation_schema: - type: integer - minimum: 1 - maximum: 10 - - path: spec.container.image - display_name: "Container Image" - editable: true + resources: + - name: app + service_type: container + fields: + - path: replicas + display_name: "Replica Count" + editable: true + default: "1" + validation_schema: + type: integer + minimum: 1 + maximum: 10 + - path: image + display_name: "Container Image" + editable: true + - name: db + service_type: database + requires_resources: + - app + fields: + - path: engine + editable: false + default: "postgres" + - path: version + editable: false + default: "16" ``` Example output (table): ``` -ID UID DISPLAY NAME CREATED -my-catalog-item b2c3d4e5-f6a7-8901-bcde-f12345678901 Small Container 2026-03-09T10:00:00Z +ID UID DISPLAY NAME CREATED +my-catalog-item b2c3d4e5-f6a7-8901-bcde-f12345678901 App with Database 2026-03-09T10:00:00Z ``` #### `dcm catalog item list` @@ -513,9 +526,11 @@ display_name: "My App Instance" spec: catalog_item_id: my-catalog-item user_values: - - path: spec.replicas + - resource: spec.replicas + path: replicas value: "3" - - path: spec.container.image + - resource: app + path: spec.container.image value: "nginx:latest" ``` @@ -1019,7 +1034,7 @@ dcm policy list dcm catalog instance create --from-file instance.yaml │ ├─▶ Read and parse instance.yaml - │ Contains: catalog_item_id, user_values + │ Contains: catalog_item_id, user_values (each with resource, path, value) ├─▶ POST /api/v1alpha1/catalog-item-instances ├─▶ Display created instance └─▶ Exit 0 diff --git a/internal/commands/catalog_item.go b/internal/commands/catalog_item.go index 77f59bf..0e81284 100644 --- a/internal/commands/catalog_item.go +++ b/internal/commands/catalog_item.go @@ -13,24 +13,15 @@ import ( ) var catalogItemTableDef = &output.TableDef{ - Headers: []string{"UID", "DISPLAY NAME", "SERVICE TYPE", "CREATED"}, + Headers: []string{"UID", "DISPLAY NAME", "CREATED"}, RowFunc: func(resource any) []string { m, ok := resource.(map[string]any) if !ok { - return []string{"", "", "", ""} - } - var serviceType string - if spec, ok := m["spec"].(map[string]any); ok { - if resources, ok := spec["resources"].([]any); ok && len(resources) > 0 { - if res, ok := resources[0].(map[string]any); ok { - serviceType = stringifyValue(res, "service_type") - } - } + return []string{"", "", ""} } return []string{ stringifyValue(m, "uid"), stringifyValue(m, "display_name"), - serviceType, stringifyValue(m, "create_time"), } }, diff --git a/internal/commands/catalog_item_test.go b/internal/commands/catalog_item_test.go index ad28e7f..9e4a74d 100644 --- a/internal/commands/catalog_item_test.go +++ b/internal/commands/catalog_item_test.go @@ -299,11 +299,9 @@ var _ = Describe("Catalog Item Commands", func() { out := outBuf.String() Expect(out).To(ContainSubstring("UID")) Expect(out).To(ContainSubstring("DISPLAY NAME")) - Expect(out).To(ContainSubstring("SERVICE TYPE")) Expect(out).To(ContainSubstring("CREATED")) Expect(out).To(ContainSubstring("b2c3d4e5-f6a7-8901-bcde-f12345678901")) Expect(out).To(ContainSubstring("Small Container")) - Expect(out).To(ContainSubstring("container")) Expect(out).To(ContainSubstring("2026-03-09T10:00:00Z")) }) }) From 3d850ede645ff650dcbc1fd055d85d598bfc45e6 Mon Sep 17 00:00:00 2001 From: Jennifer Ubah Date: Thu, 17 Sep 2026 10:26:16 -0400 Subject: [PATCH 3/8] Update examples in readme Assisted-By: Cursor AI Signed-off-by: Jennifer Ubah --- README.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index ba6a6cc..39ad316 100644 --- a/README.md +++ b/README.md @@ -432,17 +432,17 @@ spec: - name: app service_type: container fields: - - path: replicas - display_name: "Replica Count" + - path: image.reference + display_name: "Container Image" + editable: true + - path: resources.cpu.min + display_name: "CPU Min" editable: true default: "1" validation_schema: type: integer minimum: 1 - maximum: 10 - - path: image - display_name: "Container Image" - editable: true + maximum: 10 - name: db service_type: database requires_resources: @@ -526,12 +526,12 @@ display_name: "My App Instance" spec: catalog_item_id: my-catalog-item user_values: - - resource: spec.replicas - path: replicas - value: "3" - resource: app - path: spec.container.image + path: image.reference value: "nginx:latest" + - resource: app + path: resources.cpu.min + value: "2" ``` Example output (table): From ef5cf909abd14fe4240508fcb3ac393c2251c774 Mon Sep 17 00:00:00 2001 From: Jennifer Ubah Date: Thu, 17 Sep 2026 12:06:03 -0400 Subject: [PATCH 4/8] Update control plane version Assisted-By: Cursor AI Signed-off-by: Jennifer Ubah --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 9427129..722de93 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.25.5 require ( github.com/coreos/go-oidc/v3 v3.20.0 - github.com/dcm-project/control-plane v0.0.0-20260805143143-76ca1d7a6639 + github.com/dcm-project/control-plane v0.0.0-20260817201929-c04802d05ecc github.com/onsi/ginkgo/v2 v2.29.0 github.com/onsi/gomega v1.41.0 github.com/spf13/cobra v1.10.2 diff --git a/go.sum b/go.sum index 5434119..e3abe33 100644 --- a/go.sum +++ b/go.sum @@ -14,8 +14,8 @@ github.com/danieljoos/wincred v1.2.2/go.mod h1:w7w4Utbrz8lqeMbDAK0lkNJUv5sAOkFi7 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dcm-project/control-plane v0.0.0-20260805143143-76ca1d7a6639 h1:xA7kdlg1NZOiLJkCEyAf11I9StZfNlsksv8cStTUsII= -github.com/dcm-project/control-plane v0.0.0-20260805143143-76ca1d7a6639/go.mod h1:oLQKa1vW81Dmpit5iGjU0EQaRARadjrJ/BmCadwqir4= +github.com/dcm-project/control-plane v0.0.0-20260817201929-c04802d05ecc h1:akJkh+ASNmDij3GV3O2PvW+FW9VvGdiiGf6/8hYwywQ= +github.com/dcm-project/control-plane v0.0.0-20260817201929-c04802d05ecc/go.mod h1:oLQKa1vW81Dmpit5iGjU0EQaRARadjrJ/BmCadwqir4= github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ= github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= From 5d9c332f60f6af5cd0372b91f6945b49edc740cc Mon Sep 17 00:00:00 2001 From: Jennifer Ubah Date: Thu, 17 Sep 2026 12:19:00 -0400 Subject: [PATCH 5/8] Update readme Assisted-By: Cursor AI Signed-off-by: Jennifer Ubah --- README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 995d051..a45e790 100644 --- a/README.md +++ b/README.md @@ -440,9 +440,8 @@ spec: editable: true default: "1" validation_schema: - type: integer - minimum: 1 - maximum: 10 + type: string + pattern: '^[1-9][0-9]*m?$' - name: db service_type: database requires_resources: From 654d515235896cd5e639d6d2766e4800a9042291 Mon Sep 17 00:00:00 2001 From: Jennifer Ubah Date: Fri, 18 Sep 2026 11:06:08 -0400 Subject: [PATCH 6/8] removed trailing space Assisted-By: Cursor AI Signed-off-by: Jennifer Ubah --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index a45e790..edf2af0 100644 --- a/README.md +++ b/README.md @@ -441,7 +441,7 @@ spec: default: "1" validation_schema: type: string - pattern: '^[1-9][0-9]*m?$' + pattern: '^[1-9][0-9]*m?$' - name: db service_type: database requires_resources: @@ -458,8 +458,8 @@ spec: Example output (table): ``` -ID UID DISPLAY NAME CREATED -my-catalog-item b2c3d4e5-f6a7-8901-bcde-f12345678901 App with Database 2026-03-09T10:00:00Z +UID DISPLAY NAME CREATED +b2c3d4e5-f6a7-8901-bcde-f12345678901 App with Database 2026-03-09T10:00:00Z ``` #### `dcm catalog item list` From 0ad249471404e605d4ce577365f1dfebb4d7e672 Mon Sep 17 00:00:00 2001 From: Jennifer Ubah Date: Fri, 18 Sep 2026 11:39:42 -0400 Subject: [PATCH 7/8] Updated test to cover composite resource Assisted-By: Cursor AI Signed-off-by: Jennifer Ubah --- internal/commands/catalog_instance_test.go | 50 ++++++++++++++++ internal/commands/catalog_item_test.go | 66 ++++++++++++++++++++++ internal/commands/contract_test.go | 15 +++++ 3 files changed, 131 insertions(+) diff --git a/internal/commands/catalog_instance_test.go b/internal/commands/catalog_instance_test.go index 8400f0a..ac028fd 100644 --- a/internal/commands/catalog_instance_test.go +++ b/internal/commands/catalog_instance_test.go @@ -27,6 +27,32 @@ func sampleInstanceResponse() map[string]any { } } +// sampleTwoResourceInstanceResponse returns an instance with user_values for two resources. +func sampleTwoResourceInstanceResponse() map[string]any { + return map[string]any{ + "path": "catalog-item-instances/my-instance", + "uid": "c3d4e5f6-a7b8-9012-cdef-123456789012", + "display_name": "My App Instance", + "create_time": "2026-03-09T10:00:00Z", + "resource_id": "res-abc123", + "spec": map[string]any{ + "catalog_item_id": "my-catalog-item", + "user_values": []any{ + map[string]any{ + "resource": "app", + "path": "image.reference", + "value": "nginx:latest", + }, + map[string]any{ + "resource": "db", + "path": "engine", + "value": "postgres", + }, + }, + }, + } +} + // emptyInstanceListResponse returns a standard empty instance list response body. func emptyInstanceListResponse() map[string]any { return map[string]any{ @@ -412,4 +438,28 @@ var _ = Describe("Catalog Instance Commands", func() { Expect(errBuf.String()).To(ContainSubstring("NOT_FOUND")) }) }) + + It("should create an instance with user_values for multiple resources", func() { + expected := sampleTwoResourceInstanceResponse() + + server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + Expect(r.Method).To(Equal(http.MethodPost)) + Expect(r.URL.Path).To(Equal("/api/v1alpha1/catalog-item-instances")) + + var body map[string]any + Expect(json.NewDecoder(r.Body).Decode(&body)).To(Succeed()) + Expect(body["display_name"]).To(Equal(expected["display_name"])) + Expect(body["spec"]).To(Equal(expected["spec"])) + + writeJSONResponse(w, http.StatusCreated, expected) + })) + + inputFile := writeTempJSON(map[string]any{ + "display_name": expected["display_name"], + "spec": expected["spec"], + }) + + err := executeCommand("catalog", "instance", "create", "--from-file", inputFile) + Expect(err).NotTo(HaveOccurred()) + }) }) diff --git a/internal/commands/catalog_item_test.go b/internal/commands/catalog_item_test.go index 9e4a74d..4703ddd 100644 --- a/internal/commands/catalog_item_test.go +++ b/internal/commands/catalog_item_test.go @@ -31,6 +31,48 @@ func sampleCatalogItemResponse() map[string]any { } } +// sampleTwoResourceCatalogItemResponse returns a catalog item with two distinct resources. +func sampleTwoResourceCatalogItemResponse() map[string]any { + return map[string]any{ + "path": "catalog-items/my-catalog-item", + "uid": "b2c3d4e5-f6a7-8901-bcde-f12345678901", + "display_name": "App with Database", + "create_time": "2026-03-09T10:00:00Z", + "spec": map[string]any{ + "resources": []any{ + map[string]any{ + "name": "app", + "service_type": "container", + "fields": []any{ + map[string]any{ + "path": "image.reference", + "default": "nginx:latest", + }, + }, + }, + map[string]any{ + "name": "db", + "service_type": "database", + "requires_resources": []any{"app"}, + "fields": []any{ + map[string]any{ + "path": "engine", + "default": "postgres", + }, + }, + }, + }, + }, + } +} + +// writeTempJSON marshals v to JSON and writes it to a temporary file. +func writeTempJSON(v any) string { + data, err := json.Marshal(v) + Expect(err).NotTo(HaveOccurred()) + return writeTempFile(string(data), ".json") +} + // emptyCatalogItemListResponse returns a standard empty catalog item list response body. func emptyCatalogItemListResponse() map[string]any { return map[string]any{ @@ -348,4 +390,28 @@ var _ = Describe("Catalog Item Commands", func() { Expect(errBuf.String()).To(ContainSubstring("NOT_FOUND")) }) }) + + It("should create a catalog item with multiple resources", func() { + expected := sampleTwoResourceCatalogItemResponse() + + server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + Expect(r.Method).To(Equal(http.MethodPost)) + Expect(r.URL.Path).To(Equal("/api/v1alpha1/catalog-items")) + + var body map[string]any + Expect(json.NewDecoder(r.Body).Decode(&body)).To(Succeed()) + Expect(body["display_name"]).To(Equal(expected["display_name"])) + Expect(body["spec"]).To(Equal(expected["spec"])) + + writeJSONResponse(w, http.StatusCreated, expected) + })) + + inputFile := writeTempJSON(map[string]any{ + "display_name": expected["display_name"], + "spec": expected["spec"], + }) + + err := executeCommand("catalog", "item", "create", "--from-file", inputFile) + Expect(err).NotTo(HaveOccurred()) + }) }) diff --git a/internal/commands/contract_test.go b/internal/commands/contract_test.go index 8a92999..8fc3876 100644 --- a/internal/commands/contract_test.go +++ b/internal/commands/contract_test.go @@ -6,6 +6,7 @@ import ( "net/http" "net/http/httptest" "path/filepath" + "strings" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -109,4 +110,18 @@ var _ = Describe("Documentation Contract", func() { } }) }) + + Describe("Catalog Item table output", func() { + It("should emit UID, DISPLAY NAME, CREATED without an ID column", func() { + server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + writeJSONResponse(w, http.StatusOK, sampleCatalogItemResponse()) + })) + + err := executeCommand("catalog", "item", "get", "my-catalog-item") + Expect(err).NotTo(HaveOccurred()) + + header := strings.Split(strings.TrimSpace(outBuf.String()), "\n")[0] + Expect(header).To(MatchRegexp(`^UID\s+DISPLAY NAME\s+CREATED\s*$`)) + }) + }) }) From 1fca8d9e0137199b743ffe184c4c4de44ea1d427 Mon Sep 17 00:00:00 2001 From: Jennifer Ubah Date: Tue, 22 Sep 2026 10:23:48 -0400 Subject: [PATCH 8/8] resolve nits Assisted-By: Cursor AI Signed-off-by: Jennifer Ubah --- internal/commands/catalog_instance_test.go | 48 +++++++++++----------- internal/commands/catalog_item_test.go | 48 +++++++++++----------- 2 files changed, 48 insertions(+), 48 deletions(-) diff --git a/internal/commands/catalog_instance_test.go b/internal/commands/catalog_instance_test.go index ac028fd..bf1fbc1 100644 --- a/internal/commands/catalog_instance_test.go +++ b/internal/commands/catalog_instance_test.go @@ -173,6 +173,30 @@ var _ = Describe("Catalog Instance Commands", func() { Expect(errors.As(err, &fmtErr)).To(BeTrue()) Expect(errBuf.String()).To(ContainSubstring("INTERNAL")) }) + + It("should create an instance with user_values for multiple resources", func() { + expected := sampleTwoResourceInstanceResponse() + + server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + Expect(r.Method).To(Equal(http.MethodPost)) + Expect(r.URL.Path).To(Equal("/api/v1alpha1/catalog-item-instances")) + + var body map[string]any + Expect(json.NewDecoder(r.Body).Decode(&body)).To(Succeed()) + Expect(body["display_name"]).To(Equal(expected["display_name"])) + Expect(body["spec"]).To(Equal(expected["spec"])) + + writeJSONResponse(w, http.StatusCreated, expected) + })) + + inputFile := writeTempJSON(map[string]any{ + "display_name": expected["display_name"], + "spec": expected["spec"], + }) + + err := executeCommand("catalog", "instance", "create", "--from-file", inputFile) + Expect(err).NotTo(HaveOccurred()) + }) }) Describe("list", func() { @@ -438,28 +462,4 @@ var _ = Describe("Catalog Instance Commands", func() { Expect(errBuf.String()).To(ContainSubstring("NOT_FOUND")) }) }) - - It("should create an instance with user_values for multiple resources", func() { - expected := sampleTwoResourceInstanceResponse() - - server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - Expect(r.Method).To(Equal(http.MethodPost)) - Expect(r.URL.Path).To(Equal("/api/v1alpha1/catalog-item-instances")) - - var body map[string]any - Expect(json.NewDecoder(r.Body).Decode(&body)).To(Succeed()) - Expect(body["display_name"]).To(Equal(expected["display_name"])) - Expect(body["spec"]).To(Equal(expected["spec"])) - - writeJSONResponse(w, http.StatusCreated, expected) - })) - - inputFile := writeTempJSON(map[string]any{ - "display_name": expected["display_name"], - "spec": expected["spec"], - }) - - err := executeCommand("catalog", "instance", "create", "--from-file", inputFile) - Expect(err).NotTo(HaveOccurred()) - }) }) diff --git a/internal/commands/catalog_item_test.go b/internal/commands/catalog_item_test.go index 4703ddd..6b80229 100644 --- a/internal/commands/catalog_item_test.go +++ b/internal/commands/catalog_item_test.go @@ -191,6 +191,30 @@ var _ = Describe("Catalog Item Commands", func() { Expect(errors.As(err, &fmtErr)).To(BeTrue()) Expect(errBuf.String()).To(ContainSubstring("INTERNAL")) }) + + It("should create a catalog item with multiple resources", func() { + expected := sampleTwoResourceCatalogItemResponse() + + server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + Expect(r.Method).To(Equal(http.MethodPost)) + Expect(r.URL.Path).To(Equal("/api/v1alpha1/catalog-items")) + + var body map[string]any + Expect(json.NewDecoder(r.Body).Decode(&body)).To(Succeed()) + Expect(body["display_name"]).To(Equal(expected["display_name"])) + Expect(body["spec"]).To(Equal(expected["spec"])) + + writeJSONResponse(w, http.StatusCreated, expected) + })) + + inputFile := writeTempJSON(map[string]any{ + "display_name": expected["display_name"], + "spec": expected["spec"], + }) + + err := executeCommand("catalog", "item", "create", "--from-file", inputFile) + Expect(err).NotTo(HaveOccurred()) + }) }) Describe("list", func() { @@ -390,28 +414,4 @@ var _ = Describe("Catalog Item Commands", func() { Expect(errBuf.String()).To(ContainSubstring("NOT_FOUND")) }) }) - - It("should create a catalog item with multiple resources", func() { - expected := sampleTwoResourceCatalogItemResponse() - - server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - Expect(r.Method).To(Equal(http.MethodPost)) - Expect(r.URL.Path).To(Equal("/api/v1alpha1/catalog-items")) - - var body map[string]any - Expect(json.NewDecoder(r.Body).Decode(&body)).To(Succeed()) - Expect(body["display_name"]).To(Equal(expected["display_name"])) - Expect(body["spec"]).To(Equal(expected["spec"])) - - writeJSONResponse(w, http.StatusCreated, expected) - })) - - inputFile := writeTempJSON(map[string]any{ - "display_name": expected["display_name"], - "spec": expected["spec"], - }) - - err := executeCommand("catalog", "item", "create", "--from-file", inputFile) - Expect(err).NotTo(HaveOccurred()) - }) })