From e904e61031406bcaad4d7c6f3b73977e6e8cfe55 Mon Sep 17 00:00:00 2001 From: Ygal Blum Date: Tue, 31 Mar 2026 11:48:24 -0400 Subject: [PATCH] Add catalog instance rehydrate command Add dcm catalog instance rehydrate INSTANCE_ID command that triggers rehydration of an existing catalog item instance via POST to the catalog-manager new :rehydrate custom method endpoint. Co-Authored-By: Claude Opus 4.6 Signed-off-by: Ygal Blum --- .ai/checkpoints/catalog-instance-rehydrate.md | 62 +++++++++++++++++++ .ai/specs/dcm-cli.spec.md | 35 +++++++++-- .ai/test-plans/dcm-cli-unit.test-plan.md | 42 ++++++++++++- go.mod | 12 ++-- go.sum | 27 ++++---- internal/commands/catalog_instance.go | 41 ++++++++++++ internal/commands/catalog_instance_test.go | 62 +++++++++++++++++++ 7 files changed, 257 insertions(+), 24 deletions(-) create mode 100644 .ai/checkpoints/catalog-instance-rehydrate.md diff --git a/.ai/checkpoints/catalog-instance-rehydrate.md b/.ai/checkpoints/catalog-instance-rehydrate.md new file mode 100644 index 0000000..e47fcdf --- /dev/null +++ b/.ai/checkpoints/catalog-instance-rehydrate.md @@ -0,0 +1,62 @@ +# Checkpoint: Catalog Instance Rehydrate Command + +- **Branch:** `catalog-instance-rehydrate` +- **Base:** `main` (commit `b0d7ba8`) +- **Date:** 2026-03-31 +- **Status:** Complete + +--- + +## Scope + +Adds the `dcm catalog instance rehydrate` command, which triggers rehydration of +an existing catalog item instance. The command sends a POST request to +`/api/v1alpha1/catalog-item-instances/{id}:rehydrate` and displays the +rehydrated instance in the configured output format. + +This feature depends on the rehydration-flow branch of the Catalog Manager fork +(`github.com/ygalblum/dcm-catalog-manager`), integrated via a `replace` +directive in `go.mod`. + +### Requirements Addressed + +| ID | Description | Status | +|----|-------------|--------| +| REQ-CIN-120 | `dcm catalog instance rehydrate INSTANCE_ID` triggers rehydration | Done | +| REQ-CIN-130 | Display rehydrated instance in configured output format | Done | +| REQ-CIN-110 (updated) | Missing positional arg for `rehydrate` → usage error (exit code 2) | Done | + +### Tests Implemented (4 specs) + +| TC ID | Description | Status | +|-------|-------------|--------| +| TC-U150 | Rehydrate instance — POST `/api/v1alpha1/catalog-item-instances/my-instance:rehydrate`, displays result | Pass | +| TC-U151 | Rehydrate without INSTANCE_ID → UsageError (exit code 2) | Pass | +| TC-U152 | Rehydrate non-existent instance — 404 RFC 7807 error formatted to stderr | Pass | +| TC-U153 | Rehydrate server error — 500 RFC 7807 error formatted to stderr | Pass | + +--- + +## Files Created / Modified + +| File | Change | Purpose | +|------|--------|---------| +| `go.mod` | Modified | Added `replace` directive to use `github.com/ygalblum/dcm-catalog-manager` fork with rehydration API | +| `go.sum` | Modified | Updated checksums for fork dependency | +| `internal/commands/catalog_instance.go` | Modified | Added `newCatalogInstanceRehydrateCommand()` and registered it in `newCatalogInstanceCommand()` | +| `internal/commands/catalog_instance_test.go` | Modified | Added 4 Ginkgo test specs for rehydrate (success, missing arg, not found, server error) | +| `.ai/specs/dcm-cli.spec.md` | Modified | Added REQ-CIN-120, REQ-CIN-130, AC-CIN-130/140/150 for rehydrate | +| `.ai/test-plans/dcm-cli-unit.test-plan.md` | Modified | Added TC-U150–TC-U153, updated coverage matrix | +| `.ai/checkpoints/catalog-instance-rehydrate.md` | Created | This checkpoint | + +--- + +## Key Design Decisions + +1. **Fork via `replace` directive** — The upstream `catalog-manager` module does not yet have the rehydrate API. A Go module `replace` directive points to the `ygalblum/dcm-catalog-manager` fork's `rehydration-flow` branch, keeping the import paths unchanged throughout the codebase. + +2. **Same patterns as existing instance commands** — The rehydrate command follows the identical structure used by `get`: accepts a positional `INSTANCE_ID`, creates the catalog client, sends the request, and formats the response or error. + +3. **POST with no request body** — The rehydrate API takes no request body; only the instance ID in the URL path is required. The generated client's `RehydrateCatalogItemInstance` method handles this. + +4. **200 OK response** — Unlike `create` (201) or `delete` (204), the rehydrate endpoint returns 200 with the updated instance body, which is decoded and displayed via `FormatOne`. diff --git a/.ai/specs/dcm-cli.spec.md b/.ai/specs/dcm-cli.spec.md index 6c57ff9..620fd3e 100644 --- a/.ai/specs/dcm-cli.spec.md +++ b/.ai/specs/dcm-cli.spec.md @@ -15,7 +15,7 @@ dependencies. - Policy CRUD operations (create, list, get, update, delete) - Service type read operations (list, get) - Catalog item operations (create, list, get, delete) -- Catalog item instance operations (create, list, get, delete) +- Catalog item instance operations (create, list, get, delete, rehydrate) - SP resource read operations (list, get) via Service Provider Resource Manager - SP provider read operations (list, get) via Service Provider Manager - Version display @@ -795,8 +795,10 @@ Formatting). #### Overview Implement the `dcm catalog instance` command group with subcommands: `create`, -`list`, `get`, `delete`. Instances represent deployed catalog items. No update -operation is supported for instances in v1alpha1. +`list`, `get`, `delete`, `rehydrate`. Instances represent deployed catalog items. +No update operation is supported for instances in v1alpha1. The `rehydrate` +command triggers rehydration of an existing instance, generating a new resource +ID and delegating to the Placement Manager. Out of scope: instance update/day-2 operations, instance status watching, instance logs. @@ -815,7 +817,9 @@ instance logs. | REQ-CIN-080 | `dcm catalog instance delete` MUST display a success message in the format `Catalog item instance "" deleted successfully.` | MUST | | | REQ-CIN-090 | All catalog instance commands MUST use the generated Catalog Manager client | MUST | | | REQ-CIN-100 | `--from-file` MUST be required for `create` | MUST | | -| REQ-CIN-110 | Missing positional arguments for `get`, `delete` MUST result in a usage error (exit code 2) | MUST | | +| REQ-CIN-110 | Missing positional arguments for `get`, `delete`, `rehydrate` MUST result in a usage error (exit code 2) | MUST | | +| REQ-CIN-120 | `dcm catalog instance rehydrate` MUST accept an `INSTANCE_ID` positional argument and trigger rehydration of the instance | MUST | | +| REQ-CIN-130 | `dcm catalog instance rehydrate` MUST display the rehydrated instance in the configured output format | MUST | | #### Table Output Columns @@ -918,6 +922,29 @@ my-instance c3d4e5f6-a7b8-9012-cdef-123456789012 My App Instance my-catalog - **And** the API returns a server-side error (e.g., 500 Internal Server Error or 409 Conflict) with RFC 7807 body - **Then** the CLI MUST display the error in the configured output format and exit with code 1 +##### AC-CIN-130: Rehydrate instance + +- **Validates:** REQ-CIN-120, REQ-CIN-130 +- **Given** an instance with ID `my-instance` exists +- **When** `dcm catalog instance rehydrate my-instance` is invoked +- **Then** a POST request MUST be sent to `/api/v1alpha1/catalog-item-instances/my-instance:rehydrate` +- **And** the rehydrated instance MUST be displayed in the configured output format + +##### AC-CIN-140: Rehydrate without INSTANCE_ID + +- **Validates:** REQ-CIN-110 +- **Given** no positional argument is provided +- **When** `dcm catalog instance rehydrate` is invoked +- **Then** the CLI MUST exit with code 2 and display a usage error + +##### AC-CIN-150: Rehydrate non-existent instance + +- **Validates:** REQ-CIN-120, REQ-XC-ERR-010 +- **Given** no instance with ID `nonexistent` exists +- **When** `dcm catalog instance rehydrate nonexistent` is invoked +- **Then** the API returns a 404 with RFC 7807 body +- **And** the CLI MUST display the error in the configured output format and exit with code 1 + #### Dependencies Depends on Topic 1 (CLI Framework), Topic 2 (Configuration), Topic 3 (Output diff --git a/.ai/test-plans/dcm-cli-unit.test-plan.md b/.ai/test-plans/dcm-cli-unit.test-plan.md index dc173cf..fbdaf67 100644 --- a/.ai/test-plans/dcm-cli-unit.test-plan.md +++ b/.ai/test-plans/dcm-cli-unit.test-plan.md @@ -870,6 +870,42 @@ test classes. Instead: - **When:** `dcm catalog instance get my-instance` is executed with `--output table` - **Then:** The table output includes columns: ID, UID, DISPLAY NAME, CATALOG ITEM, RESOURCE ID, CREATED +### TC-U150: Rehydrate instance + +- **Requirement:** REQ-CIN-120, REQ-CIN-130 +- **Acceptance Criteria:** AC-CIN-130 +- **Type:** Unit +- **Given:** A mock server returning 200 with a rehydrated instance +- **When:** `dcm catalog instance rehydrate my-instance` is executed +- **Then:** A POST request is sent to `/api/v1alpha1/catalog-item-instances/my-instance:rehydrate` AND the rehydrated instance is displayed in the configured output format + +### TC-U151: Rehydrate instance without INSTANCE_ID fails + +- **Requirement:** REQ-CIN-110 +- **Acceptance Criteria:** AC-CIN-140 +- **Type:** Unit +- **Given:** No positional argument is provided +- **When:** `dcm catalog instance rehydrate` is executed +- **Then:** The CLI exits with code 2 and displays a usage error + +### TC-U152: Rehydrate non-existent instance + +- **Requirement:** REQ-CIN-120, REQ-XC-ERR-010 +- **Acceptance Criteria:** AC-CIN-150, AC-XC-ERR-010 +- **Type:** Unit +- **Given:** A mock server returning 404 with RFC 7807 body for instance ID `nonexistent` +- **When:** `dcm catalog instance rehydrate nonexistent` is executed +- **Then:** The CLI displays the error in the configured output format AND exits with code 1 + +### TC-U153: Rehydrate instance server error + +- **Requirement:** REQ-CIN-120, REQ-XC-ERR-010 +- **Acceptance Criteria:** AC-XC-ERR-010 +- **Type:** Unit +- **Given:** A mock server returning 500 with RFC 7807 body +- **When:** `dcm catalog instance rehydrate my-instance` is executed +- **Then:** The CLI displays the error in the configured output format AND exits with code 1 + --- ## 9 · SP Resource Commands @@ -1575,7 +1611,9 @@ dedicated test class or `Describe` block. | REQ-CIN-080 | TC-U077 | Covered | | REQ-CIN-090 | TC-U067 (via TC-U058, TC-U073, TC-U075, TC-U077) | Covered | | REQ-CIN-100 | TC-U072 | Covered | -| REQ-CIN-110 | TC-U076, TC-U078 | Covered | +| REQ-CIN-110 | TC-U076, TC-U078, TC-U151 | Covered | +| REQ-CIN-120 | TC-U150, TC-U152, TC-U153 | Covered | +| REQ-CIN-130 | TC-U150 | Covered | | REQ-SPR-010 | TC-U121, TC-U122, TC-U123 | Covered | | REQ-SPR-020 | TC-U121 | Covered | | REQ-SPR-030 | TC-U124 | Covered | @@ -1624,7 +1662,7 @@ dedicated test class or `Describe` block. | REQ-XC-TLS-070 | TC-U095, TC-U096 | Covered | | REQ-XC-TLS-080 | TC-U090, TC-U097 | Covered | -**Total:** 113 test case IDs — 87 in behavioural test classes, 26 in the utility +**Total:** 117 test case IDs — 91 in behavioural test classes, 26 in the utility index (tested transitively through higher-level behavioural tests). --- diff --git a/go.mod b/go.mod index b5b9c33..7f69546 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/dcm-project/cli go 1.25.5 require ( - github.com/dcm-project/catalog-manager v0.0.0-20260313160905-1ff110850088 + github.com/dcm-project/catalog-manager v0.0.0-20260408124701-1315c44e39b9 github.com/dcm-project/policy-manager v0.0.0-20260310132113-15bd45617e87 github.com/dcm-project/service-provider-manager v0.0.0-20260324094657-8aad860d86d2 github.com/onsi/ginkgo/v2 v2.28.1 @@ -16,6 +16,7 @@ require ( require ( github.com/Masterminds/semver/v3 v3.4.0 // indirect github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/getkin/kin-openapi v0.134.0 // indirect github.com/go-logr/logr v1.4.3 // indirect @@ -30,11 +31,12 @@ require ( github.com/josharian/intern v1.0.0 // indirect github.com/mailru/easyjson v0.9.1 // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect - github.com/oapi-codegen/runtime v1.3.0 // indirect + github.com/oapi-codegen/runtime v1.3.1 // indirect github.com/oasdiff/yaml v0.0.0-20260313112342-a3ea61cb4d4c // indirect github.com/oasdiff/yaml3 v0.0.0-20260224194419-61cd415a242b // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/perimeterx/marshmallow v1.1.5 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/sagikazarmark/locafero v0.11.0 // indirect github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect github.com/spf13/afero v1.15.0 // indirect @@ -42,11 +44,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.32.0 // indirect - golang.org/x/net v0.49.0 // indirect + golang.org/x/mod v0.33.0 // indirect + golang.org/x/net v0.50.0 // indirect golang.org/x/sync v0.19.0 // indirect golang.org/x/sys v0.42.0 // indirect golang.org/x/text v0.34.0 // indirect - golang.org/x/tools v0.41.0 // indirect + golang.org/x/tools v0.42.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect ) diff --git a/go.sum b/go.sum index 29261f4..f31451f 100644 --- a/go.sum +++ b/go.sum @@ -6,10 +6,10 @@ github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dcm-project/catalog-manager v0.0.0-20260313160905-1ff110850088 h1:Vdz6ZiT7XkFp3mZOjJEDCIfwrkCuWaDM9ronB6o1SmM= -github.com/dcm-project/catalog-manager v0.0.0-20260313160905-1ff110850088/go.mod h1:NJNzrVwaR0c5YmNLgDZ9ve0ueWqs/GAsLOXeQriIR7g= +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/catalog-manager v0.0.0-20260408124701-1315c44e39b9 h1:+J3PhsbirFK7GB/RTXE11d9LTXQYHhrQVSERestD45Y= +github.com/dcm-project/catalog-manager v0.0.0-20260408124701-1315c44e39b9/go.mod h1:Xx+nYzmOFKpo0nlSHe3/jHVz3t2dIhN3ov3w+8P2jjU= github.com/dcm-project/policy-manager v0.0.0-20260310132113-15bd45617e87 h1:IgIFK8eWeNHLloVuwbGZLzun8LHA6d5nqVrct7nB+S8= github.com/dcm-project/policy-manager v0.0.0-20260310132113-15bd45617e87/go.mod h1:a9eT8Ws0Gy/6FJGp+dWmrB4s/hyfVE0PQPats/aQW0E= github.com/dcm-project/service-provider-manager v0.0.0-20260324094657-8aad860d86d2 h1:A0AJ0Yog5w34xA2JSjeEKkehQn7kqObala8IVX6OjkM= @@ -70,8 +70,8 @@ github.com/mfridman/tparse v0.18.0 h1:wh6dzOKaIwkUGyKgOntDW4liXSo37qg5AXbIhkMV3v github.com/mfridman/tparse v0.18.0/go.mod h1:gEvqZTuCgEhPbYk/2lS3Kcxg1GmTxxU7kTC8DvP0i/A= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw= github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8= -github.com/oapi-codegen/runtime v1.3.0 h1:vyK1zc0gDWWXgk2xoQa4+X4RNNc5SL2RbTpJS/4vMYA= -github.com/oapi-codegen/runtime v1.3.0/go.mod h1:kOdeacKy7t40Rclb1je37ZLFboFxh+YLy0zaPCMibPY= +github.com/oapi-codegen/runtime v1.3.1 h1:RgDY6J4OGQLbRXhG/Xpt3vSVqYpHQS7hN4m85+5xB9g= +github.com/oapi-codegen/runtime v1.3.1/go.mod h1:kOdeacKy7t40Rclb1je37ZLFboFxh+YLy0zaPCMibPY= github.com/oasdiff/yaml v0.0.0-20260313112342-a3ea61cb4d4c h1:7ACFcSaQsrWtrH4WHHfUqE1C+f8r2uv8KGaW0jTNjus= github.com/oasdiff/yaml v0.0.0-20260313112342-a3ea61cb4d4c/go.mod h1:JKox4Gszkxt57kj27u7rvi7IFoIULvCZHUsBTUmQM/s= github.com/oasdiff/yaml3 v0.0.0-20260224194419-61cd415a242b h1:vivRhVUAa9t1q0Db4ZmezBP8pWQWnXHFokZj0AOea2g= @@ -84,8 +84,9 @@ github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0 github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= @@ -125,18 +126,18 @@ github.com/woodsbury/decimal128 v1.4.0 h1:xJATj7lLu4f2oObouMt2tgGiElE5gO6mSWUjQs github.com/woodsbury/decimal128 v1.4.0/go.mod h1:BP46FUrVjVhdTbKT+XuQh2xfQaGki9LMIRJSFuh6THU= 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.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= -golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= -golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= -golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= +golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= +golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= +golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= +golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= -golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= -golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= +golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= +golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= 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.go b/internal/commands/catalog_instance.go index ad926bc..496707b 100644 --- a/internal/commands/catalog_instance.go +++ b/internal/commands/catalog_instance.go @@ -43,6 +43,7 @@ func newCatalogInstanceCommand() *cobra.Command { cmd.AddCommand(newCatalogInstanceListCommand()) cmd.AddCommand(newCatalogInstanceGetCommand()) cmd.AddCommand(newCatalogInstanceDeleteCommand()) + cmd.AddCommand(newCatalogInstanceRehydrateCommand()) return cmd } @@ -215,6 +216,46 @@ func newCatalogInstanceGetCommand() *cobra.Command { } } +func newCatalogInstanceRehydrateCommand() *cobra.Command { + return &cobra.Command{ + Use: "rehydrate INSTANCE_ID", + Short: "Rehydrate a catalog item instance", + Args: ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + cfg := config.FromCommand(cmd) + formatter, err := newFormatter(cmd, catalogInstanceTableDef, "catalog instance rehydrate") + if err != nil { + return err + } + + client, err := newCatalogClient(cfg) + if err != nil { + return fmt.Errorf("creating catalog client: %w", err) + } + + ctx, cancel := requestContext(cmd) + defer cancel() + + resp, err := client.RehydrateCatalogItemInstance(ctx, args[0]) + if err != nil { + return connectionError(err, cfg) + } + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode != http.StatusOK { + return handleErrorResponse(resp, formatter) + } + + var result map[string]any + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return fmt.Errorf("parsing response: %w", err) + } + + return formatter.FormatOne(result) + }, + } +} + func newCatalogInstanceDeleteCommand() *cobra.Command { return &cobra.Command{ Use: "delete INSTANCE_ID", diff --git a/internal/commands/catalog_instance_test.go b/internal/commands/catalog_instance_test.go index 188b1e3..c79e50d 100644 --- a/internal/commands/catalog_instance_test.go +++ b/internal/commands/catalog_instance_test.go @@ -297,6 +297,68 @@ var _ = Describe("Catalog Instance Commands", func() { }) }) + Describe("rehydrate", func() { + // TC-U150: Rehydrate instance + It("TC-U150: should rehydrate an instance and display the result", func() { + 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/my-instance:rehydrate")) + + writeJSONResponse(w, http.StatusOK, sampleInstanceResponse()) + })) + + err := executeCommand("catalog", "instance", "rehydrate", "my-instance") + Expect(err).NotTo(HaveOccurred()) + + out := outBuf.String() + Expect(out).To(ContainSubstring("c3d4e5f6-a7b8-9012-cdef-123456789012")) + Expect(out).To(ContainSubstring("My App Instance")) + }) + + // TC-U151: Rehydrate instance without INSTANCE_ID fails + It("TC-U151: should return a UsageError when INSTANCE_ID is missing", func() { + err := executeCommand("catalog", "instance", "rehydrate") + Expect(err).To(HaveOccurred()) + + var usageErr *commands.UsageError + Expect(errors.As(err, &usageErr)).To(BeTrue()) + }) + + // TC-U152: Rehydrate non-existent instance + It("TC-U152: should display error for non-existent instance", func() { + server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + writeRFC7807(w, http.StatusNotFound, "NOT_FOUND", + `Catalog item instance "nonexistent" not found.`, + "The requested catalog item instance resource does not exist.") + })) + + err := executeCommand("catalog", "instance", "rehydrate", "nonexistent") + Expect(err).To(HaveOccurred()) + + var fmtErr *commands.FormattedError + Expect(errors.As(err, &fmtErr)).To(BeTrue()) + + errOut := errBuf.String() + Expect(errOut).To(ContainSubstring("NOT_FOUND")) + Expect(errOut).To(ContainSubstring("not found")) + Expect(outBuf.String()).To(BeEmpty()) + }) + + // TC-U153: Rehydrate instance server error + It("TC-U153: should display error and exit code 1 on server error", func() { + server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + writeRFC7807(w, http.StatusInternalServerError, "INTERNAL", "Internal server error", "Something went wrong") + })) + + err := executeCommand("catalog", "instance", "rehydrate", "my-instance") + Expect(err).To(HaveOccurred()) + + var fmtErr *commands.FormattedError + Expect(errors.As(err, &fmtErr)).To(BeTrue()) + Expect(errBuf.String()).To(ContainSubstring("INTERNAL")) + }) + }) + Describe("delete", func() { // TC-U077: Delete instance It("TC-U077: should delete an instance and display success message", func() {