Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions internal/flink/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ func New(cfg *config.Config, prerunner pcmd.PreRunner) *cobra.Command {
cmd.AddCommand(c.newDetachedSavepointCommand())
cmd.AddCommand(c.newEnvironmentCommand())
cmd.AddCommand(c.newSavepointCommand())
cmd.AddCommand(c.newSystemInfoCommand())

// On-Prem and Cloud Commands
cmd.AddCommand(c.newComputePoolCommand(cfg))
Expand Down
89 changes: 89 additions & 0 deletions internal/flink/command_system_info.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package flink

import (
"github.com/spf13/cobra"

pcmd "github.com/confluentinc/cli/v4/pkg/cmd"
"github.com/confluentinc/cli/v4/pkg/output"
)

type systemInfoOut struct {
Version string `human:"Version" serialized:"version"`
Revision string `human:"Revision" serialized:"revision"`
}

type localSystemInformation struct {
Status *localSystemInformationStatus `json:"status,omitempty" yaml:"status,omitempty"`
}

type localSystemInformationStatus struct {
Version *string `json:"version,omitempty" yaml:"version,omitempty"`
Revision *string `json:"revision,omitempty" yaml:"revision,omitempty"`
}

func (c *command) newSystemInfoCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "system-info",
Short: "Display CMF system information.",
Args: cobra.NoArgs,
RunE: c.systemInfo,
Annotations: map[string]string{pcmd.RunRequirement: pcmd.RequireCloudLogout},
}

addCmfFlagSet(cmd)
pcmd.AddOutputFlag(cmd)

return cmd
}
Comment on lines +24 to +37
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

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

The recursive help golden tests expect a fixture for the leaf command help output (e.g., test/fixtures/output/flink/system-info-help-onprem.golden). Adding this command without the corresponding help fixture will cause TestHelp to fail for the on-prem configuration.

Copilot uses AI. Check for mistakes.

func (c *command) systemInfo(cmd *cobra.Command, _ []string) error {
client, err := c.GetCmfClient(cmd)
if err != nil {
return err
}

result, err := client.GetSystemInformation(c.createContext())
if err != nil {
return err
}

sysInfo := parseSystemInformation(result)

if output.GetFormat(cmd) == output.Human {
table := output.NewTable(cmd)
table.Add(&systemInfoOut{
Version: derefString(sysInfo.Status.Version),
Revision: derefString(sysInfo.Status.Revision),
Comment on lines +53 to +56
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

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

sysInfo.Status can be nil when the CMF response is missing/invalid status, but the human-output branch unconditionally dereferences sysInfo.Status.Version/Revision, which will panic. Guard against sysInfo.Status == nil (or initialize a non-nil status before dereferencing) so the command fails gracefully instead of crashing.

Suggested change
table := output.NewTable(cmd)
table.Add(&systemInfoOut{
Version: derefString(sysInfo.Status.Version),
Revision: derefString(sysInfo.Status.Revision),
status := &localSystemInformationStatus{}
if sysInfo.Status != nil {
status = sysInfo.Status
}
table := output.NewTable(cmd)
table.Add(&systemInfoOut{
Version: derefString(status.Version),
Revision: derefString(status.Revision),

Copilot uses AI. Check for mistakes.
})
return table.Print()
}

return output.SerializedOutput(cmd, sysInfo)
}

func parseSystemInformation(raw map[string]interface{}) localSystemInformation {
sysInfo := localSystemInformation{}

statusMap, ok := raw["status"].(map[string]interface{})
if !ok {
return sysInfo
}

status := &localSystemInformationStatus{}
if v, ok := statusMap["version"].(string); ok {
status.Version = &v
}
if v, ok := statusMap["revision"].(string); ok {
status.Revision = &v
}
sysInfo.Status = status

return sysInfo
}

func derefString(s *string) string {
if s == nil {
return ""
}
return *s
}
42 changes: 42 additions & 0 deletions pkg/flink/cmf_rest_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package flink

import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
Expand Down Expand Up @@ -35,6 +36,7 @@ type CmfClientInterface interface {
DeleteStatement(ctx context.Context, environment, statement string) error
UpdateStatement(ctx context.Context, environment, statementName string, statement cmfsdk.Statement) error
GetStatementResults(ctx context.Context, environment, statementName, pageToken string) (cmfsdk.StatementResult, error)
GetSystemInformation(ctx context.Context) (map[string]interface{}, error)
CmfApiContext() context.Context
}

Expand Down Expand Up @@ -529,6 +531,46 @@ func (cmfClient *CmfRestClient) GetStatementResults(ctx context.Context, environ
return resp, nil
}

func (cmfClient *CmfRestClient) GetSystemInformation(ctx context.Context) (map[string]interface{}, error) {
baseURL := cmfClient.GetConfig().Servers[0].URL
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

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

Building the request URL via string concatenation can produce an invalid path when the configured base URL ends with / (resulting in //cmf/api/...). Prefer joining/normalizing the base URL (e.g., trimming the trailing slash or using url.JoinPath) to ensure the request is well-formed.

Suggested change
baseURL := cmfClient.GetConfig().Servers[0].URL
baseURL := strings.TrimRight(cmfClient.GetConfig().Servers[0].URL, "/")

Copilot uses AI. Check for mistakes.
url := baseURL + "/cmf/api/v1/system-information"

req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, fmt.Errorf("failed to create system information request: %s", err)
}

if token, ok := ctx.Value(cmfsdk.ContextAccessToken).(string); ok && token != "" {
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
}

resp, err := cmfClient.GetConfig().HTTPClient.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to get system information: %s", err)
}
defer resp.Body.Close()

body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read system information response: %s", err)
}

if resp.StatusCode != http.StatusOK {
trimmed := strings.TrimSpace(string(body))
if trimmed != "" {
return nil, errors.New(trimmed)
}
return nil, errors.New(resp.Status)
}

var result map[string]interface{}
if err := json.Unmarshal(body, &result); err != nil {
return nil, fmt.Errorf("failed to parse system information response: %s", err)
}

return result, nil
}

func (cmfClient *CmfRestClient) CreateCatalog(ctx context.Context, kafkaCatalog cmfsdk.KafkaCatalog) (cmfsdk.KafkaCatalog, error) {
catalogName := kafkaCatalog.Metadata.Name
outputCatalog, httpResponse, err := cmfClient.SQLApi.CreateKafkaCatalog(ctx).KafkaCatalog(kafkaCatalog).Execute()
Expand Down
15 changes: 15 additions & 0 deletions pkg/flink/test/mock/cmf_client_mock.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions test/fixtures/output/flink/help-onprem.golden
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Available Commands:
environment Manage Flink environments.
savepoint Manage Flink savepoints.
statement Manage Flink SQL statements.
system-info Display CMF system information.

Global Flags:
-h, --help Show help for this command.
Expand Down
16 changes: 16 additions & 0 deletions test/fixtures/output/flink/system-info-help-onprem.golden
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
Display CMF system information.

Usage:
confluent flink system-info [flags]

Flags:
--url string Base URL of the Confluent Manager for Apache Flink (CMF). Environment variable "CONFLUENT_CMF_URL" may be set in place of this flag.
--client-key-path string Path to client private key for mTLS authentication. Environment variable "CONFLUENT_CMF_CLIENT_KEY_PATH" may be set in place of this flag.
--client-cert-path string Path to client cert to be verified by Confluent Manager for Apache Flink. Include for mTLS authentication. Environment variable "CONFLUENT_CMF_CLIENT_CERT_PATH" may be set in place of this flag.
--certificate-authority-path string Path to a PEM-encoded Certificate Authority to verify the Confluent Manager for Apache Flink connection. Environment variable "CONFLUENT_CMF_CERTIFICATE_AUTHORITY_PATH" may be set in place of this flag.
-o, --output string Specify the output format as "human", "json", or "yaml". (default "human")

Global Flags:
-h, --help Show help for this command.
--unsafe-trace Equivalent to -vvvv, but also log HTTP requests and responses which might contain plaintext secrets.
-v, --verbose count Increase verbosity (-v for warn, -vv for info, -vvv for debug, -vvvv for trace).
6 changes: 6 additions & 0 deletions test/fixtures/output/flink/system-info-json.golden
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"status": {
"version": "1.0.0",
"revision": "abc1234def5678"
}
}
3 changes: 3 additions & 0 deletions test/fixtures/output/flink/system-info-yaml.golden
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
status:
version: 1.0.0
revision: abc1234def5678
4 changes: 4 additions & 0 deletions test/fixtures/output/flink/system-info.golden
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
+----------+----------------+
| Version | 1.0.0 |
| Revision | abc1234def5678 |
+----------+----------------+
10 changes: 10 additions & 0 deletions test/flink_onprem_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -511,6 +511,16 @@ func (s *CLITestSuite) TestFlinkStatementExceptionListOnPrem() {
runIntegrationTestsWithMultipleAuth(s, tests)
}

func (s *CLITestSuite) TestFlinkSystemInfo() {
tests := []CLITest{
{args: "flink system-info", fixture: "flink/system-info.golden"},
{args: "flink system-info --output json", fixture: "flink/system-info-json.golden"},
{args: "flink system-info --output yaml", fixture: "flink/system-info-yaml.golden"},
}

runIntegrationTestsWithMultipleAuth(s, tests)
}

func (s *CLITestSuite) TestFlinkOnPremWithCloudLogin() {
test := CLITest{args: "flink environment list --output json", fixture: "flink/environment/list-cloud.golden", login: "cloud", exitCode: 1}
s.runIntegrationTest(test)
Expand Down
20 changes: 20 additions & 0 deletions test/test-server/flink_onprem_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -390,7 +390,7 @@
require.NoError(t, err)
return
default:
require.Fail(t, fmt.Sprintf("Unexpected method %s", r.Method))

Check failure on line 393 in test/test-server/flink_onprem_handler.go

View check run for this annotation

SonarQube-Confluent / SonarQube Code Analysis

Define a constant instead of duplicating this literal "Unexpected method %s" 16 times.

[S1192] String literals should not be duplicated See more on https://sonarqube.confluent.io/project/issues?id=cli&pullRequest=3316&issues=cf03e087-32a2-4884-9f71-04686f9fbca5&open=cf03e087-32a2-4884-9f71-04686f9fbca5
}
}
}
Expand Down Expand Up @@ -1259,3 +1259,23 @@
}
}
}

func handleCmfSystemInformation(t *testing.T) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
handleLoginType(t, r)

switch r.Method {
case http.MethodGet:
sysInfo := map[string]interface{}{
"status": map[string]interface{}{
"version": "1.0.0",
"revision": "abc1234def5678",
},
}
err := json.NewEncoder(w).Encode(sysInfo)
require.NoError(t, err)
default:
require.Fail(t, fmt.Sprintf("Unexpected method %s", r.Method))
}
}
}
1 change: 1 addition & 0 deletions test/test-server/flink_onprem_router.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ var flinkRoutes = []route{
{"/cmf/api/v1/environments/{envName}/statements/{stmtName}/savepoints/{savepointName}", handleCmfSavepoint},
{"/cmf/api/v1/detached-savepoints", handleCmfDetachedSavepoints},
{"/cmf/api/v1/detached-savepoints/{detachedSavepointName}", handleCmfDetachedSavepoint},
{"/cmf/api/v1/system-information", handleCmfSystemInformation},
}

func NewFlinkOnPremRouter(t *testing.T) *mux.Router {
Expand Down