From 06374a91bd009fede3f19bc1295b7d5837cb024c Mon Sep 17 00:00:00 2001 From: Matt Clark Date: Mon, 9 Mar 2026 23:11:02 -0700 Subject: [PATCH] SREP-3940: Add --hive-ocm-url flag to cluster break-glass for multi-env OCM support --- cmd/cluster/access/access.go | 66 +++++++++++++++++++------ cmd/cluster/access/access_test.go | 79 ++++++++++++++++++++++++++++++ docs/README.md | 1 + docs/osdctl_cluster_break-glass.md | 7 +-- 4 files changed, 135 insertions(+), 18 deletions(-) diff --git a/cmd/cluster/access/access.go b/cmd/cluster/access/access.go index 9fccf7dcc..52856ee78 100644 --- a/cmd/cluster/access/access.go +++ b/cmd/cluster/access/access.go @@ -66,6 +66,7 @@ func NewCmdAccess(streams genericclioptions.IOStreams, client *k8s.LazyClient) * accessCmd.AddCommand(newCmdCleanup(client, streams)) accessCmd.Flags().StringVar(&ops.reason, "reason", "", "The reason for this command, which requires elevation, to be run (usualy an OHSS or PD ticket)") accessCmd.Flags().StringVarP(&ops.clusterID, "cluster-id", "C", "", "Provide the internal ID of the cluster") + accessCmd.Flags().StringVar(&ops.hiveOcmUrl, "hive-ocm-url", "", "(optional) OCM environment URL for hive operations. Aliases: 'production', 'staging', 'integration'. If not specified, uses the same OCM environment as the target cluster.") _ = accessCmd.MarkFlagRequired("reason") _ = accessCmd.MarkFlagRequired("cluster-id") @@ -74,8 +75,9 @@ func NewCmdAccess(streams genericclioptions.IOStreams, client *k8s.LazyClient) * // clusterAccessOptions contains the objects and information required to access a cluster type clusterAccessOptions struct { - reason string - clusterID string + reason string + clusterID string + hiveOcmUrl string genericclioptions.IOStreams } @@ -112,23 +114,26 @@ func (c *clusterAccessOptions) Readln() (string, error) { // accessCmdComplete verifies the command's invocation, returning an error if the usage is invalid func (c *clusterAccessOptions) accessCmdComplete() error { - return osdctlutil.IsValidClusterKey(c.clusterID) -} - -// Run executes the 'break-glass' access subcommand -func (c *clusterAccessOptions) Run(ctx context.Context) error { - // Login to hive shard - hive, err := osdctlutil.GetHiveCluster(c.clusterID) - if err != nil { - return fmt.Errorf("failed to retrieve hive shard for %q: %w", c.clusterID, err) + if err := osdctlutil.IsValidClusterKey(c.clusterID); err != nil { + return err } - hiveClient, err := k8s.NewAsBackplaneClusterAdmin(hive.ID(), kclient.Options{Scheme: scheme.Scheme}, c.reason, fmt.Sprintf("Elevation required to break-glass on %q cluster", c.clusterID)) - if err != nil { - return fmt.Errorf("failed to login to hive shard %q: %w", hive.Name(), err) + // Validate --hive-ocm-url if provided + if c.hiveOcmUrl != "" { + _, err := osdctlutil.ValidateAndResolveOcmUrl(c.hiveOcmUrl) + if err != nil { + return fmt.Errorf("invalid --hive-ocm-url: %w", err) + } } - c.Println(fmt.Sprintf("Retrieving Kubeconfig for cluster '%s'", c.clusterID)) + return nil +} + +// Run executes the 'break-glass' access subcommand +func (c *clusterAccessOptions) Run(ctx context.Context) error { + var hive *clustersmgmtv1.Cluster + var hiveClient kclient.Client + var err error // Connect to ocm and grab cluster definition: user-provided cluster identifier could be any one of name, internal ID, or UUID // and we need to ensure we're only referring to cluster by internal-ID while interacting with hive @@ -145,6 +150,37 @@ func (c *clusterAccessOptions) Run(ctx context.Context) error { return err } c.Println(fmt.Sprintf("Internal Cluster ID: %s", cluster.ID())) + c.Println(fmt.Sprintf("Retrieving Kubeconfig for cluster '%s'", c.clusterID)) + + if c.hiveOcmUrl != "" { + // Multi-environment path - enables staging/integration testing + hiveOCM, err := osdctlutil.CreateConnectionWithUrl(c.hiveOcmUrl) + if err != nil { + return fmt.Errorf("failed to create hive OCM connection with URL '%s': %w", c.hiveOcmUrl, err) + } + defer hiveOCM.Close() + + hive, err = osdctlutil.GetHiveClusterWithConn(cluster.ID(), conn, hiveOCM) + if err != nil { + return fmt.Errorf("failed to retrieve hive shard for %q (OCM URL:'%s'): %w", c.clusterID, c.hiveOcmUrl, err) + } + + hiveClient, err = k8s.NewAsBackplaneClusterAdminWithConn(hive.ID(), kclient.Options{Scheme: scheme.Scheme}, hiveOCM, c.reason, fmt.Sprintf("Elevation required to break-glass on %q cluster", c.clusterID)) + if err != nil { + return fmt.Errorf("failed to login to hive shard %q (OCM URL:'%s'): %w", hive.Name(), c.hiveOcmUrl, err) + } + } else { + // Original path - backward compatible + hive, err = osdctlutil.GetHiveCluster(cluster.ID()) + if err != nil { + return fmt.Errorf("failed to retrieve hive shard for %q: %w", c.clusterID, err) + } + + hiveClient, err = k8s.NewAsBackplaneClusterAdmin(hive.ID(), kclient.Options{Scheme: scheme.Scheme}, c.reason, fmt.Sprintf("Elevation required to break-glass on %q cluster", c.clusterID)) + if err != nil { + return fmt.Errorf("failed to login to hive shard %q: %w", hive.Name(), err) + } + } // Retrieve the kubeconfig secret from the cluster's namespace on hive ns, err := getClusterNamespace(hiveClient, cluster.ID()) diff --git a/cmd/cluster/access/access_test.go b/cmd/cluster/access/access_test.go index e57cc0db8..c8355a1c6 100644 --- a/cmd/cluster/access/access_test.go +++ b/cmd/cluster/access/access_test.go @@ -438,3 +438,82 @@ func TestGetKubeConfigSecret(t *testing.T) { }) } } + +// TestClusterAccessOptions_accessCmdComplete tests the early validation of cluster-id and hive-ocm-url +func TestClusterAccessOptions_accessCmdComplete(t *testing.T) { + tests := []struct { + name string + clusterID string + hiveOcmUrl string + expectErr bool + errContains string + }{ + { + name: "Valid cluster ID, no hive-ocm-url", + clusterID: "test-cluster-123", + hiveOcmUrl: "", + expectErr: false, + }, + { + name: "Valid cluster ID, valid hive-ocm-url (production)", + clusterID: "test-cluster-123", + hiveOcmUrl: "production", + expectErr: false, + }, + { + name: "Valid cluster ID, valid hive-ocm-url (staging)", + clusterID: "test-cluster-123", + hiveOcmUrl: "staging", + expectErr: false, + }, + { + name: "Valid cluster ID, valid hive-ocm-url (integration)", + clusterID: "test-cluster-123", + hiveOcmUrl: "integration", + expectErr: false, + }, + { + name: "Valid cluster ID, valid hive-ocm-url (full URL)", + clusterID: "test-cluster-123", + hiveOcmUrl: "https://api.openshift.com", + expectErr: false, + }, + { + name: "Valid cluster ID, invalid hive-ocm-url", + clusterID: "test-cluster-123", + hiveOcmUrl: "invalid-environment", + expectErr: true, + errContains: "invalid --hive-ocm-url", + }, + { + name: "Empty cluster ID", + clusterID: "", + hiveOcmUrl: "", + expectErr: true, + errContains: "isn't valid", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &clusterAccessOptions{ + clusterID: tt.clusterID, + hiveOcmUrl: tt.hiveOcmUrl, + } + + err := c.accessCmdComplete() + + if tt.expectErr { + if err == nil { + t.Errorf("Expected error containing '%s', but got nil", tt.errContains) + } else if !strings.Contains(err.Error(), tt.errContains) { + t.Errorf("Expected error containing '%s', but got: %v", tt.errContains, err) + } + } else { + if err != nil { + t.Errorf("Expected no error, but got: %v", err) + } + } + }) + } +} diff --git a/docs/README.md b/docs/README.md index 458cac5e2..32c4e668d 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1224,6 +1224,7 @@ osdctl cluster break-glass --cluster-id [flags] -C, --cluster-id string Provide the internal ID of the cluster --context string The name of the kubeconfig context to use -h, --help help for break-glass + --hive-ocm-url string (optional) OCM environment URL for hive operations. Aliases: 'production', 'staging', 'integration'. If not specified, uses the same OCM environment as the target cluster. --insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure --kubeconfig string Path to the kubeconfig file to use for CLI requests. -o, --output string Valid formats are ['', 'json', 'yaml', 'env'] diff --git a/docs/osdctl_cluster_break-glass.md b/docs/osdctl_cluster_break-glass.md index 9ce2fb7c9..f3f969a2f 100644 --- a/docs/osdctl_cluster_break-glass.md +++ b/docs/osdctl_cluster_break-glass.md @@ -13,9 +13,10 @@ osdctl cluster break-glass --cluster-id [flags] ### Options ``` - -C, --cluster-id string Provide the internal ID of the cluster - -h, --help help for break-glass - --reason string The reason for this command, which requires elevation, to be run (usualy an OHSS or PD ticket) + -C, --cluster-id string Provide the internal ID of the cluster + -h, --help help for break-glass + --hive-ocm-url string (optional) OCM environment URL for hive operations. Aliases: 'production', 'staging', 'integration'. If not specified, uses the same OCM environment as the target cluster. + --reason string The reason for this command, which requires elevation, to be run (usualy an OHSS or PD ticket) ``` ### Options inherited from parent commands