CF-2067 : Add Flink artifact management commands for Confluent Platform#3406
CF-2067 : Add Flink artifact management commands for Confluent Platform#3406Paras Negi (paras-negi-flink) wants to merge 4 commits into
Conversation
Add the on-prem command group backed by the CMF
artifact API (cmf-sdk-go v0.0.8 / CMF 2.4.0):
artifact {create, list, describe, update, delete}
artifact version {create, list, describe, delete, download}
The group branches on login mode like compute-pool/statement: cloud keeps the
existing presigned-URL commands; on-prem talks to CMF through CmfRestClient.
CreateArtifact and UpdateArtifact build the multipart request by hand because
the generated SDK serializes the object part with fmt %v rather than JSON;
the part is sent as application/json, matching CMF's @RequestPart
binding. Verified end-to-end against live CMF with MinIO-backed S3 storage.
Includes the CmfRestClient wrapper methods, LocalArtifact output types,
test-server handlers, integration tests, and golden fixtures.
|
🎉 All Contributor License Agreements have been signed. Ready to merge. |
There was a problem hiding this comment.
Pull request overview
Adds a new on-prem (cmf / Confluent Platform) implementation of confluent flink artifact and confluent flink artifact version commands, backed by CMF’s artifact REST API via CmfRestClient, along with test-server handlers, integration tests, and golden fixtures.
Changes:
- Introduces on-prem
flink artifactCRUD +artifact versionsubcommands (create/list/describe/delete/download) and wires them into the Flink command tree. - Extends
CmfRestClientwith artifact endpoints, including a manual multipart uploader to ensure theartifactpart is sent as JSON. - Adds on-prem test-server routes/handlers plus integration tests and fixtures for the new command surface.
Reviewed changes
Copilot reviewed 73 out of 73 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| test/test-server/flink_onprem_router.go | Registers CMF artifact REST routes for the on-prem test server. |
| test/test-server/flink_onprem_handler.go | Implements artifact/versions/content handlers used by integration tests. |
| test/flink_onprem_test.go | Adds integration test suites for on-prem artifact and artifact-version commands. |
| test/fixtures/output/flink/help-onprem.golden | Updates on-prem Flink help output to include the new artifact command group. |
| test/fixtures/output/flink/artifact/version/list-success.golden | Golden output for human artifact version list success. |
| test/fixtures/output/flink/artifact/version/list-success-yaml.golden | Golden output for YAML artifact version list. |
| test/fixtures/output/flink/artifact/version/list-success-json.golden | Golden output for JSON artifact version list. |
| test/fixtures/output/flink/artifact/version/list-help-onprem.golden | Golden help text for artifact version list (on-prem). |
| test/fixtures/output/flink/artifact/version/help-onprem.golden | Golden help text for artifact version group (on-prem). |
| test/fixtures/output/flink/artifact/version/download-version-force-success.golden | Golden output for versioned download with --force. |
| test/fixtures/output/flink/artifact/version/download-success.golden | Golden output for downloading latest version. |
| test/fixtures/output/flink/artifact/version/download-help-onprem.golden | Golden help text for artifact version download. |
| test/fixtures/output/flink/artifact/version/describe-success.golden | Golden output for human artifact version describe success. |
| test/fixtures/output/flink/artifact/version/describe-success-yaml.golden | Golden output for YAML artifact version describe. |
| test/fixtures/output/flink/artifact/version/describe-success-json.golden | Golden output for JSON artifact version describe. |
| test/fixtures/output/flink/artifact/version/describe-non-exist-failure.golden | Golden output for artifact version describe error. |
| test/fixtures/output/flink/artifact/version/describe-help-onprem.golden | Golden help text for artifact version describe. |
| test/fixtures/output/flink/artifact/version/delete-successful.golden | Golden output for interactive artifact version delete. |
| test/fixtures/output/flink/artifact/version/delete-non-exist-failure.golden | Golden output for artifact version delete error. |
| test/fixtures/output/flink/artifact/version/delete-help-onprem.golden | Golden help text for artifact version delete. |
| test/fixtures/output/flink/artifact/version/delete-force.golden | Golden output for artifact version delete --force. |
| test/fixtures/output/flink/artifact/version/delete-all-force.golden | Golden output for deleting all versions with --force. |
| test/fixtures/output/flink/artifact/version/create-success.golden | Golden output for human artifact version create success. |
| test/fixtures/output/flink/artifact/version/create-success-yaml.golden | Golden output for YAML artifact version create. |
| test/fixtures/output/flink/artifact/version/create-success-json.golden | Golden output for JSON artifact version create. |
| test/fixtures/output/flink/artifact/version/create-help-onprem.golden | Golden help text for artifact version create. |
| test/fixtures/output/flink/artifact/update-success-yaml.golden | Golden output for YAML artifact update. |
| test/fixtures/output/flink/artifact/update-success-json.golden | Golden output for JSON artifact update. |
| test/fixtures/output/flink/artifact/update-non-exist-failure.golden | Golden output for artifact update error. |
| test/fixtures/output/flink/artifact/update-help-onprem.golden | Golden help text for artifact update. |
| test/fixtures/output/flink/artifact/list-success.golden | Golden output for human artifact list success. |
| test/fixtures/output/flink/artifact/list-success-yaml.golden | Golden output for YAML artifact list. |
| test/fixtures/output/flink/artifact/list-success-json.golden | Golden output for JSON artifact list. |
| test/fixtures/output/flink/artifact/list-non-exist-environment-failure.golden | Golden output for artifact list with missing env. |
| test/fixtures/output/flink/artifact/list-missing-env-flag-failure.golden | Golden output for artifact list without required --environment. |
| test/fixtures/output/flink/artifact/list-help-onprem.golden | Golden help text for artifact list. |
| test/fixtures/output/flink/artifact/help-onprem.golden | Golden help text for artifact group (on-prem). |
| test/fixtures/output/flink/artifact/describe-success.golden | Golden output for human artifact describe success. |
| test/fixtures/output/flink/artifact/describe-success-yaml.golden | Golden output for YAML artifact describe. |
| test/fixtures/output/flink/artifact/describe-success-json.golden | Golden output for JSON artifact describe. |
| test/fixtures/output/flink/artifact/describe-non-exist-failure.golden | Golden output for artifact describe error. |
| test/fixtures/output/flink/artifact/describe-help-onprem.golden | Golden help text for artifact describe. |
| test/fixtures/output/flink/artifact/delete-single-successful.golden | Golden output for interactive single delete. |
| test/fixtures/output/flink/artifact/delete-single-force.golden | Golden output for single delete with --force. |
| test/fixtures/output/flink/artifact/delete-non-exist-failure.golden | Golden output for delete-not-found with suggestions. |
| test/fixtures/output/flink/artifact/delete-multiple-successful.golden | Golden output for interactive multi-delete. |
| test/fixtures/output/flink/artifact/delete-help-onprem.golden | Golden help text for artifact delete. |
| test/fixtures/output/flink/artifact/create-with-labels-json.golden | Golden output for create with labels (JSON). |
| test/fixtures/output/flink/artifact/create-success.golden | Golden output for human artifact create success. |
| test/fixtures/output/flink/artifact/create-success-yaml.golden | Golden output for YAML artifact create. |
| test/fixtures/output/flink/artifact/create-success-json.golden | Golden output for JSON artifact create. |
| test/fixtures/output/flink/artifact/create-missing-file-flag-failure.golden | Golden output for missing --artifact-file. |
| test/fixtures/output/flink/artifact/create-invalid-extension-failure.golden | Golden output for invalid file extension validation. |
| test/fixtures/output/flink/artifact/create-help-onprem.golden | Golden help text for artifact create. |
| test/fixtures/output/flink/artifact/create-existing-failure.golden | Golden output for create conflict error. |
| test/fixtures/input/flink/artifact/artifact.jar | Adds dummy artifact file fixture for create tests. |
| test/fixtures/input/flink/artifact/artifact-v2.jar | Adds dummy artifact fixture for version-create tests. |
| pkg/flink/cmf_rest_client.go | Adds CMF artifact client methods plus manual multipart uploader for create/update. |
| internal/flink/local_types.go | Introduces LocalArtifact types for serialized JSON/YAML outputs. |
| internal/flink/command.go | Wires the artifact command group into Flink commands for both modes. |
| internal/flink/command_artifact.go | Splits artifact command group behavior by login mode (cloud vs on-prem). |
| internal/flink/command_artifact_version_onprem.go | Adds on-prem artifact version command group and subcommands. |
| internal/flink/command_artifact_version_list_onprem.go | Implements on-prem artifact version list. |
| internal/flink/command_artifact_version_download_onprem.go | Implements on-prem artifact version download. |
| internal/flink/command_artifact_version_describe_onprem.go | Implements on-prem artifact version describe. |
| internal/flink/command_artifact_version_delete_onprem.go | Implements on-prem artifact version delete. |
| internal/flink/command_artifact_version_create_onprem.go | Implements on-prem artifact version create (upload new version). |
| internal/flink/command_artifact_update_onprem.go | Implements on-prem artifact update (metadata-only labels). |
| internal/flink/command_artifact_onprem.go | Adds shared on-prem artifact helpers (output mapping, file open/validation, conversions). |
| internal/flink/command_artifact_list_onprem.go | Implements on-prem artifact list. |
| internal/flink/command_artifact_describe_onprem.go | Implements on-prem artifact describe. |
| internal/flink/command_artifact_delete_onprem.go | Implements on-prem artifact delete using the repo’s deletion helpers. |
| internal/flink/command_artifact_create_onprem.go | Implements on-prem artifact create (upload initial version). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| body := new(bytes.Buffer) | ||
| writer := multipart.NewWriter(body) | ||
|
|
Fix a nil-pointer panic in `flink artifact version download` when the server returns a 2xx with an empty body: the SDK leaves the *os.File nil, so the deferred cleanup dereferenced nil. Guard against it, and remove the partial output file if the copy fails so a failed download can't leave a truncated artifact behind. Also escape the environment and artifact names in the hand-rolled upload URLs, document the artifact- vs version-level Creation Time sources and the upload path's tracing/User-Agent gap, and align the version-delete not-found error with the top-level delete's suggestions. Add integration coverage for the metadata-only update path, version-create failures, and the download content, no-force happy path, and pre-existing file guard. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Extract the CMF manual-request plumbing that the artifact upload path had
copy-pasted from GetSystemInformation into shared helpers (cmfBaseURL,
setCmfAuthHeader, cmfErrorFromBody) and route both methods through them, so
base-URL, auth-header, and error-from-body logic live in one place.
Drop the redundant `Changed("label")` guard in the update handler, since
getLabelsFlag already returns a nil map when the flag is omitted (matching
the create handler), and hoist the duplicated artifact-not-found suggestion
string into a shared constant used by both delete handlers.
No behavior change; artifact and system-info integration suites pass with
unchanged golden files.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The human table for `describe`, `create`, and `update` only showed Name/Version/Phase/Size/Creation Time, so labels set via `--label` were invisible unless the user switched to `-o json`/`yaml`. Add a single-artifact human view that surfaces Labels and Annotations (omitted when empty); the list view stays lean to avoid wide rows, and serialized output is unchanged. Also strengthen label-contract test coverage: the test-server now asserts the CLI omits the labels field entirely (rather than sending an empty object, which CMF treats as "clear") on create and update, and a metadata-only update case verifies the response carries no labels. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Abhijeet Sharma (abhijeet2096-confluent)
left a comment
There was a problem hiding this comment.
Review of the new on-prem confluent flink artifact command group. The change compiles and go vets cleanly, closely mirrors the existing CMF on-prem sibling commands, and is well-tested and well-commented — overall in good shape.
No merge-blockers. The inline notes are a few hardening items (chiefly the uploadArtifact success-status gate and the delete-path coverage/error-masking) plus some optional polish. Details inline.
| return cmfsdk.Artifact{}, err | ||
| } | ||
|
|
||
| if response.StatusCode >= http.StatusBadRequest { |
There was a problem hiding this comment.
This gate treats any status < 400 as success, so a 3xx (e.g. a proxy/gateway redirect) would fall through and get json.Unmarshal'd into a zero-value Artifact, printing a "successful" create/update with a blank Name/Version/Phase. The sibling GetSystemInformation uses != http.StatusOK and the SDK's own download path rejects >= 300. Since create returns 201 (which is why != 200 isn't usable here), suggest an explicit 2xx check plus a note on the 201:
| if response.StatusCode >= http.StatusBadRequest { | |
| // Create returns 201 and update 200, so treat any non-2xx status as a failure. | |
| if response.StatusCode < http.StatusOK || response.StatusCode >= http.StatusMultipleChoices { |
| require.NoError(t, err) | ||
| return | ||
| case http.MethodDelete: | ||
| w.WriteHeader(http.StatusNoContent) |
There was a problem hiding this comment.
The DELETE branch returns 204 unconditionally and never inspects the version query param. Because the confirmation prompt and success message in command_artifact_version_delete_onprem.go are both derived from the local --version flag, delete --version 2, --version all, and artifact-level delete all pass their goldens even if the client sent the wrong version to the server — and this is a data-loss path. Could we assert r.URL.Query().Get("version") matches the value expected for each test case here?
|
|
||
| existenceFunc := func(name string) bool { | ||
| _, err := client.DescribeArtifact(c.createContext(), environment, name, "") | ||
| return err == nil |
There was a problem hiding this comment.
existenceFunc collapses every failure of DescribeArtifact (404, but also 401/expired token, 500, connection refused) to false, so those all surface as "Flink artifact \"x\" not found". This matches the entrenched repo pattern, so not asking to rework it — but note the version delete precheck you added in this same PR does it better (it preserves err.Error()). Worth making the inconsistency a conscious choice; ideally only a true 404 maps to "not found".
| func (s *CLITestSuite) TestFlinkArtifactVersionListOnPrem() { | ||
| tests := []CLITest{ | ||
| // success | ||
| {args: "flink artifact version list test-artifact --environment test-env", fixture: "flink/artifact/version/list-success.golden"}, |
There was a problem hiding this comment.
This is the only new command with no failure-path coverage — the handler's 404 branch and the required---environment path are never exercised. Suggest adding something like:
{args: "flink artifact version list non-exist-artifact --environment test-env", fixture: "flink/artifact/version/list-non-exist-failure.golden", exitCode: 1},
{args: "flink artifact version list test-artifact", fixture: "flink/artifact/version/list-missing-env-flag-failure.golden", exitCode: 1},| } | ||
| } | ||
|
|
||
| // createArtifactObject builds a fully-populated Artifact for the given name and version with deterministic field values. |
There was a problem hiding this comment.
This doc comment describes createArtifactObject, but it sits above assertArtifactLabelContract — so godoc for assertArtifactLabelContract leads with an unrelated sentence and createArtifactObject (defined below) ends up undocumented. Looks like two comments got merged; the createArtifactObject line should move down to its own function.
| if artifact.Metadata.Labels != nil { | ||
| out.Labels = artifact.Metadata.Labels | ||
| } | ||
| if artifact.Metadata.Annotations != nil { | ||
| out.Annotations = artifact.Metadata.Annotations | ||
| } |
There was a problem hiding this comment.
These fields are value maps (map[string]string) with human:"...,omitempty", so assigning a nil map is a no-op and the guards can collapse:
| if artifact.Metadata.Labels != nil { | |
| out.Labels = artifact.Metadata.Labels | |
| } | |
| if artifact.Metadata.Annotations != nil { | |
| out.Annotations = artifact.Metadata.Annotations | |
| } | |
| out.Labels = artifact.Metadata.Labels | |
| out.Annotations = artifact.Metadata.Annotations |
(Note: the analogous guard in convertSdkArtifactToLocalArtifact is load-bearing and should stay — those targets are *map[string]string, so unconditional assignment would emit labels: null.)
| // openArtifactFile validates the artifact file extension and opens it for upload. The caller is responsible for closing the returned file. | ||
| func openArtifactFile(path string) (*os.File, error) { | ||
| extension := strings.TrimPrefix(filepath.Ext(path), ".") | ||
| if !slices.Contains(allowedFileExtensions, strings.ToLower(extension)) { |
There was a problem hiding this comment.
This extension check (and its exact error string) is duplicated verbatim from the Cloud command in command_artifact_create.go. Consider a small shared helper (e.g. validateArtifactFileExtension) so the allowed-extensions rule lives in one place. Optional, since it touches the pre-existing Cloud file.
| return parseSdkError(httpResp, err) | ||
| } | ||
|
|
||
| // DownloadArtifactContent downloads the binary content of an artifact. When version is empty, the latest version is downloaded. |
There was a problem hiding this comment.
Could we add a line to this doc comment noting the returned *os.File is a temp file the caller must close and os.Remove? The consumer does this correctly today, but the ownership contract is currently tribal knowledge.
| Annotations map[string]string `human:"Annotations,omitempty"` | ||
| } | ||
|
|
||
| // artifactVersionOutOnPrem is the human-readable view of a single artifact version (version list, version describe). |
There was a problem hiding this comment.
Minor: this view is also used to render version create output (via printArtifactVersionOnPrem), so the "(version list, version describe)" list is incomplete — suggest "the single-version views" or adding "version create".
| // uploadArtifact sends a multipart/form-data request for the artifact create and update endpoints. | ||
| // The generated SDK serializes the "artifact" object part with fmt "%v" (Go struct representation) rather than JSON, | ||
| // so the request is built here (mirroring GetSystemInformation's manual CMF request handling) to send a proper JSON part. | ||
| // Like GetSystemInformation, this bypasses the SDK's request pipeline, so `--unsafe-trace` request logging and the |
There was a problem hiding this comment.
Small clarity nit: "these two calls" can be misread as create+update (both flow through uploadArtifact). Suggest naming them, e.g. "...do not apply to the manually built artifact-upload and system-information requests."




Release Notes
New Features
confluent flink artifactcommand group for Confluent Platform (CMF) to manage Flink UDF/job artifacts:create,list,describe,update,delete, plus anartifact versionsubgroup (create,list,describe,delete,download).Checklist
Whatsection below whether this PR applies to Confluent Cloud, Confluent Platform, or both.Test & Reviewsection below.Blast Radiussection below.What
Implements CF-2067 — CLI: Flink artifact management for Confluent Platform (CMF on-prem).
confluent flink artifactgroup:create,list,describe,update,delete, plus anartifact versionsubgroup (create,list,describe,delete,download).compute-pool/statement): Confluent Cloud keeps its existing commands; on-prem talks to CMF (cmf-sdk-gov0.0.8 / CMF 2.4.0) viaCmfRestClient.create/update/version createhand-build the multipart request because the SDK serializes the object part as a Go struct instead of JSON, mirroring the existingGetSystemInformationconvention; read paths use the SDK.describe/create/updateoutput includes labels and annotations;liststays lean.Blast Radius
confluent flink artifactcommands; existing behavior (Cloudflink artifact, otherflink/kafkacommands) is unchanged.cmf.artifacts.enabled=falseby default in CMF); the commands only operate against a CMF that has it enabled.References
Test & Review
Environment
confluentinc/cli, branchcf-2067-flink-artifact-cli(on top ofmain); CLI built from this branch.release-2.4.0tag and run as the Spring Boot jar with--cmf.artifacts.enabled=trueand a localfile://artifact store. Blob layout on disk:.../environments/<env>/<name>/<version>.Automated:
TestCLI/TestFlinkArtifact*OnPremintegration suites pass (each runs under both anonymous and on-prem login);make lint-goandlint-cliclean.Manual — every command exercised end-to-end against CMF 2.4.0. Real captures below (verbose JSON collapsed).
create
create labeled --label owner=team-a,tier=gold -o json{ "apiVersion": "cmf.confluent.io/v1", "kind": "Artifact", "metadata": { "name": "labeled", "creationTimestamp": "2026-07-20T11:59:48.529686Z", "updateTimestamp": "2026-07-20T11:59:48.529686Z", "uid": "c3a54496-7b55-46dd-9127-9c992792d164", "labels": { "owner": "team-a", "tier": "gold" } }, "spec": {}, "status": { "version": 1, "creationTimestamp": "2026-07-20T11:59:48.529686Z", "path": "file:/tmp/cmf-artifacts/environments/demo/labeled/1", "size": 55, "checksum": "sha256:768068ea2d6a6d2281c241c6ad02346180f1e81d08e681c08e908d6dfa8f38a5", "phase": "READY" } }list
$ confluent flink artifact list --environment demo Name | Version | Phase | Size | Creation Time --------------+---------+-------+------+--------------------------- labeled | 1 | READY | 55 | 2026-07-20T11:59:48.529Z my-artifact | 1 | READY | 55 | 2026-07-20T11:59:48.461Zlist -o json[ { "apiVersion": "cmf.confluent.io/v1", "kind": "Artifact", "metadata": { "name": "labeled", "creationTimestamp": "2026-07-20T11:59:48.529Z", "updateTimestamp": "2026-07-20T11:59:48.529Z", "uid": "c3a54496-7b55-46dd-9127-9c992792d164", "labels": { "owner": "team-a", "tier": "gold" } }, "spec": {}, "status": { "version": 1, "creationTimestamp": "2026-07-20T11:59:48.529Z", "path": "file:/tmp/cmf-artifacts/environments/demo/labeled/1", "size": 55, "checksum": "sha256:768068ea2d6a6d2281c241c6ad02346180f1e81d08e681c08e908d6dfa8f38a5", "phase": "READY" } }, { "apiVersion": "cmf.confluent.io/v1", "kind": "Artifact", "metadata": { "name": "my-artifact", "creationTimestamp": "2026-07-20T11:59:48.461Z", "updateTimestamp": "2026-07-20T11:59:48.461Z", "uid": "727f83e8-2a62-44fa-bd55-cb0e270ffc34" }, "spec": {}, "status": { "version": 1, "creationTimestamp": "2026-07-20T11:59:48.461Z", "path": "file:/tmp/cmf-artifacts/environments/demo/my-artifact/1", "size": 55, "checksum": "sha256:768068ea2d6a6d2281c241c6ad02346180f1e81d08e681c08e908d6dfa8f38a5", "phase": "READY" } } ]describe (labels/annotations shown in human output)
describe labeled -o json{ "apiVersion": "cmf.confluent.io/v1", "kind": "Artifact", "metadata": { "name": "labeled", "creationTimestamp": "2026-07-20T11:59:48.529Z", "updateTimestamp": "2026-07-20T11:59:48.529Z", "uid": "c3a54496-7b55-46dd-9127-9c992792d164", "labels": { "owner": "team-a", "tier": "gold" } }, "spec": {}, "status": { "version": 1, "creationTimestamp": "2026-07-20T11:59:48.529Z", "path": "file:/tmp/cmf-artifacts/environments/demo/labeled/1", "size": 55, "checksum": "sha256:768068ea2d6a6d2281c241c6ad02346180f1e81d08e681c08e908d6dfa8f38a5", "phase": "READY" } }update — label preserve vs replace (verified against a real, stateful CMF)
This contract is also guarded in tests: the test-server asserts the CLI omits the
labelsfield entirely (never an empty{}, which CMF treats as "clear") on create/update, and a metadata-only update case verifies the response carries no labels.update labeled -o json(no --label → labels preserved){ "apiVersion": "cmf.confluent.io/v1", "kind": "Artifact", "metadata": { "name": "labeled", "creationTimestamp": "2026-07-20T11:59:48.529Z", "updateTimestamp": "2026-07-20T11:59:48.529Z", "uid": "c3a54496-7b55-46dd-9127-9c992792d164", "labels": { "owner": "team-a", "tier": "gold" } }, "spec": {}, "status": { "version": 1, "creationTimestamp": "2026-07-20T11:59:48.529Z", "path": "file:/tmp/cmf-artifacts/environments/demo/labeled/1", "size": 55, "checksum": "sha256:768068ea2d6a6d2281c241c6ad02346180f1e81d08e681c08e908d6dfa8f38a5", "phase": "READY" } }version create (+ dedup)
version list
version list -o json[ { "apiVersion": "cmf.confluent.io/v1", "kind": "Artifact", "metadata": { "name": "my-artifact", "creationTimestamp": "2026-07-20T11:59:48.461Z", "updateTimestamp": "2026-07-20T11:59:49.120Z", "uid": "727f83e8-2a62-44fa-bd55-cb0e270ffc34" }, "spec": {}, "status": { "version": 2, "creationTimestamp": "2026-07-20T11:59:49.120Z", "path": "file:/tmp/cmf-artifacts/environments/demo/my-artifact/2", "size": 58, "checksum": "sha256:ffd4ec4384719091995407aca1f2a29ef5bdc6728de220e08bd4fca0ba364cd9", "phase": "READY" } }, { "apiVersion": "cmf.confluent.io/v1", "kind": "Artifact", "metadata": { "name": "my-artifact", "creationTimestamp": "2026-07-20T11:59:48.461Z", "updateTimestamp": "2026-07-20T11:59:49.120Z", "uid": "727f83e8-2a62-44fa-bd55-cb0e270ffc34" }, "spec": {}, "status": { "version": 1, "creationTimestamp": "2026-07-20T11:59:48.461Z", "path": "file:/tmp/cmf-artifacts/environments/demo/my-artifact/1", "size": 55, "checksum": "sha256:768068ea2d6a6d2281c241c6ad02346180f1e81d08e681c08e908d6dfa8f38a5", "phase": "READY" } } ]version describe
version describe --version 2 -o json{ "apiVersion": "cmf.confluent.io/v1", "kind": "Artifact", "metadata": { "name": "my-artifact", "creationTimestamp": "2026-07-20T11:59:48.461Z", "updateTimestamp": "2026-07-20T11:59:49.120Z", "uid": "727f83e8-2a62-44fa-bd55-cb0e270ffc34" }, "spec": {}, "status": { "version": 2, "creationTimestamp": "2026-07-20T11:59:49.120Z", "path": "file:/tmp/cmf-artifacts/environments/demo/my-artifact/2", "size": 58, "checksum": "sha256:ffd4ec4384719091995407aca1f2a29ef5bdc6728de220e08bd4fca0ba364cd9", "phase": "READY" } }version download
delete (version-scoped and whole-artifact, incl. multi-delete and not-found)
Blobs genuinely land on the backend at
.../environments/<env>/<name>/<version>and deletes propagate. This confirms the hand-built multipartcreate/update/version createare accepted by CMF's real@RequestPartJSON binding.API ↔ CLI parity check
To confirm the CLI drops nothing, I hit the raw CMF REST API directly for every read operation and diffed the response key set against the CLI
-o json(<= only in raw API,>= only in CLI):The only field the API returns that the CLI doesn't is
status.message, which isnullhere and dropped viaomitempty(it is surfaced when populated, e.g. an error phase). Everything else —apiVersion, kind, spec, metadata{name, uid, creationTimestamp, updateTimestamp, labels, annotations}, status{version, creationTimestamp, path, size, checksum, phase}— matches exactly (annotations and per-versioncreationTimestampincluded). The list/versions pagination wrapper (pageable,metadata.size) is intentionally not surfaced. No silent field drops.