Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
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 @@ -10,6 +10,7 @@ Available Commands:
detached-savepoint Manage Flink detached savepoints.
environment Manage Flink environments.
savepoint Manage Flink savepoints.
system-info Display CMF system information.
statement Manage Flink SQL statements.

Global Flags:
Expand Down
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 @@ -1259,3 +1259,23 @@ func handleCmfStatementExceptions(t *testing.T) http.HandlerFunc {
}
}
}

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