Skip to content

CF-2067 : Add Flink artifact management commands for Confluent Platform#3406

Open
Paras Negi (paras-negi-flink) wants to merge 4 commits into
mainfrom
cf-2067-flink-artifact-cli
Open

CF-2067 : Add Flink artifact management commands for Confluent Platform#3406
Paras Negi (paras-negi-flink) wants to merge 4 commits into
mainfrom
cf-2067-flink-artifact-cli

Conversation

@paras-negi-flink

@paras-negi-flink Paras Negi (paras-negi-flink) commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Release Notes

New Features

  • Added the confluent flink artifact command group for Confluent Platform (CMF) to manage Flink UDF/job artifacts: create, list, describe, update, delete, plus an artifact version subgroup (create, list, describe, delete, download).

Checklist

  • I have successfully built and used a custom CLI binary, without linter issues from this PR.
  • I have clearly specified in the What section below whether this PR applies to Confluent Cloud, Confluent Platform, or both.
  • I have attached manual CLI verification results in the Test & Review section below.
  • I have added appropriate CLI integration or unit tests for any new or updated commands and functionality.
  • I confirm that this PR introduces no breaking changes or backward compatibility issues.
  • I have indicated the potential customer impact if something goes wrong in the Blast Radius section below.
  • I have put checkmarks below confirming that the feature associated with this PR is enabled in:
    • Confluent Cloud prod
    • Confluent Cloud stag
    • Confluent Platform
    • Check this box if the feature is enabled for certain organizations only

What

Implements CF-2067 — CLI: Flink artifact management for Confluent Platform (CMF on-prem).

  • Adds the on-prem confluent flink artifact group: create, list, describe, update, delete, plus an artifact version subgroup (create, list, describe, delete, download).
  • Branches on login mode (like compute-pool/statement): Confluent Cloud keeps its existing commands; on-prem talks to CMF (cmf-sdk-go v0.0.8 / CMF 2.4.0) via CmfRestClient.
  • create/update/version create hand-build the multipart request because the SDK serializes the object part as a Go struct instead of JSON, mirroring the existing GetSystemInformation convention; read paths use the SDK.
  • Human describe/create/update output includes labels and annotations; list stays lean.

Blast Radius

  • Scope is limited to the new on-prem confluent flink artifact commands; existing behavior (Cloud flink artifact, other flink/kafka commands) is unchanged.
  • Artifact management is opt-in server-side (cmf.artifacts.enabled=false by default in CMF); the commands only operate against a CMF that has it enabled.
  • No breaking changes; reverting is straightforward (command group + wrapper methods).

References

Test & Review

Environment

  • Repo: confluentinc/cli, branch cf-2067-flink-artifact-cli (on top of main); CLI built from this branch.
  • CMF: 2.4.0, built from source at the release-2.4.0 tag and run as the Spring Boot jar with --cmf.artifacts.enabled=true and a local file:// artifact store. Blob layout on disk: .../environments/<env>/<name>/<version>.

Automated: TestCLI/TestFlinkArtifact*OnPrem integration suites pass (each runs under both anonymous and on-prem login); make lint-go and lint-cli clean.

Manual — every command exercised end-to-end against CMF 2.4.0. Real captures below (verbose JSON collapsed).

create

$ confluent flink artifact create my-artifact --artifact-file udf-v1.jar --environment demo
+---------------+-----------------------------+
| Name          | my-artifact                 |
| Version       | 1                           |
| Phase         | READY                       |
| Size          | 55                          |
| Creation Time | 2026-07-20T11:59:48.461340Z |
+---------------+-----------------------------+

# invalid file extension is rejected client-side
$ confluent flink artifact create bad --artifact-file udf.txt --environment demo
Error: only extensions allowed for `--artifact-file` are "jar" and "zip"

# duplicate name -> CMF 409
$ confluent flink artifact create labeled --artifact-file udf-v1.jar --environment demo
Error: failed to create artifact "labeled" in the environment "demo": {"errors":[{"message":"Artifact 'labeled' already exists in Environment 'demo'."}]}
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.461Z
list -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)

$ confluent flink artifact describe labeled --environment demo
+---------------+--------------------------+
| Name          | labeled                  |
| Version       | 1                        |
| Phase         | READY                    |
| Size          | 55                       |
| Creation Time | 2026-07-20T11:59:48.529Z |
| Labels        | owner=team-a             |
|               | tier=gold                |
+---------------+--------------------------+

# non-existent artifact
$ confluent flink artifact describe nope --environment demo
Error: failed to describe artifact "nope" in the environment "demo": {"errors":[{"message":"Artifact 'nope' not found in Environment 'demo'."}]}
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)

# metadata-only update, NO --label -> labels PRESERVED, version stays 1
$ confluent flink artifact update labeled --environment demo -o json
# ... metadata.labels still {"owner":"team-a","tier":"gold"}, status.version = 1  (full JSON below)

# update WITH a new --label -> labels REPLACED (tier dropped)
$ confluent flink artifact update labeled --label owner=team-b --environment demo
+---------------+--------------------------+
| Name          | labeled                  |
| Version       | 1                        |
| Phase         | READY                    |
| Size          | 55                       |
| Creation Time | 2026-07-20T11:59:48.529Z |
| Labels        | owner=team-b             |
+---------------+--------------------------+

This contract is also guarded in tests: the test-server asserts the CLI omits the labels field 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)

$ confluent flink artifact version create my-artifact --artifact-file udf-v2.jar --environment demo
+---------------+-------------------------------------------------------------------------+
| Version       | 2                                                                       |
| Phase         | READY                                                                   |
| Size          | 58                                                                      |
| Checksum      | sha256:ffd4ec4384719091995407aca1f2a29ef5bdc6728de220e08bd4fca0ba364cd9 |
| Creation Time | 2026-07-20T11:59:49.120470Z                                             |
+---------------+-------------------------------------------------------------------------+

# re-uploading identical content is deduplicated -> stays at version 2 (no v3)
$ confluent flink artifact version create my-artifact --artifact-file udf-v2.jar --environment demo
# -> Version 2, same checksum

version list

$ confluent flink artifact version list my-artifact --environment demo
  Version | Phase | Size |                                Checksum                                 |      Creation Time
----------+-------+------+-------------------------------------------------------------------------+---------------------------
  2       | READY | 58   | sha256:ffd4ec4384719091995407aca1f2a29ef5bdc6728de220e08bd4fca0ba364cd9 | 2026-07-20T11:59:49.120Z
  1       | READY | 55   | sha256:768068ea2d6a6d2281c241c6ad02346180f1e81d08e681c08e908d6dfa8f38a5 | 2026-07-20T11:59:48.461Z
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

$ confluent flink artifact version describe my-artifact --version 1 --environment demo
+---------------+-------------------------------------------------------------------------+
| Version       | 1                                                                       |
| Phase         | READY                                                                   |
| Size          | 55                                                                      |
| Checksum      | sha256:768068ea2d6a6d2281c241c6ad02346180f1e81d08e681c08e908d6dfa8f38a5 |
| Creation Time | 2026-07-20T11:59:48.461Z                                                |
+---------------+-------------------------------------------------------------------------+
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

$ confluent flink artifact version download my-artifact --output-file latest.jar --environment demo
Downloaded Flink artifact "my-artifact" to "latest.jar".
# content check: latest.jar == udf-v2.jar  -> MATCH   (re-running overwrites the output file, like `asyncapi export`)

delete (version-scoped and whole-artifact, incl. multi-delete and not-found)

$ confluent flink artifact version delete my-artifact --version 1 --force --environment demo
Deleted version "1" of Flink artifact "my-artifact".

$ confluent flink artifact version list my-artifact --environment demo
  Version | Phase | Size |                                Checksum                                 |      Creation Time
----------+-------+------+-------------------------------------------------------------------------+---------------------------
  2       | READY | 58   | sha256:ffd4ec4384719091995407aca1f2a29ef5bdc6728de220e08bd4fca0ba364cd9 | 2026-07-20T11:59:49.120Z

$ confluent flink artifact delete my-artifact labeled --force --environment demo
Deleted Flink artifacts "my-artifact" and "labeled".

$ confluent flink artifact delete ghost --force --environment demo
Error: Flink artifact "ghost" not found

Suggestions:
    List available Flink artifacts with `confluent flink artifact list`.
    Check that CMF is running and accessible.

$ confluent flink artifact version delete ghost --version 1 --force --environment demo
Error: failed to describe artifact "ghost" in the environment "demo": {"errors":[{"message":"Artifact 'ghost' not found in Environment 'demo'."}]}

Suggestions:
    List available Flink artifacts with `confluent flink artifact list`.
    Check that CMF is running and accessible.

Blobs genuinely land on the backend at .../environments/<env>/<name>/<version> and deletes propagate. This confirms the hand-built multipart create/update/version create are accepted by CMF's real @RequestPart JSON 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):

--- describe ---   |   --- list ---   |   --- versions ---   |   --- vdescribe ---
17d16                  17d16               17d16                  17d16
< status.message       < status.message    < status.message       < status.message

The only field the API returns that the CLI doesn't is status.message, which is null here and dropped via omitempty (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-version creationTimestamp included). The list/versions pagination wrapper (pageable, metadata.size) is intentionally not surfaced. No silent field drops.

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.
Copilot AI review requested due to automatic review settings July 17, 2026 03:49
@confluent-cla-assistant

Copy link
Copy Markdown

🎉 All Contributor License Agreements have been signed. Ready to merge.
Please push an empty commit if you would like to re-run the checks to verify CLA status for all contributors.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 artifact CRUD + artifact version subcommands (create/list/describe/delete/download) and wires them into the Flink command tree.
  • Extends CmfRestClient with artifact endpoints, including a manual multipart uploader to ensure the artifact part 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.

Comment on lines +852 to +854
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>
@sonarqube-confluent

Copy link
Copy Markdown

@paras-negi-flink
Paras Negi (paras-negi-flink) marked this pull request as ready for review July 20, 2026 12:52
@paras-negi-flink
Paras Negi (paras-negi-flink) requested a review from a team as a code owner July 20, 2026 12:53

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

Suggested change
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)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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".

Comment thread test/flink_onprem_test.go
func (s *CLITestSuite) TestFlinkArtifactVersionListOnPrem() {
tests := []CLITest{
// success
{args: "flink artifact version list test-artifact --environment test-env", fixture: "flink/artifact/version/list-success.golden"},

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +116 to +121
if artifact.Metadata.Labels != nil {
out.Labels = artifact.Metadata.Labels
}
if artifact.Metadata.Annotations != nil {
out.Annotations = artifact.Metadata.Annotations
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

Suggested change
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)) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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."

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants