From bc47d4508465d5b633477204bf03fa701ad4d428 Mon Sep 17 00:00:00 2001 From: Matt Clark Date: Wed, 7 Jan 2026 12:51:38 -0800 Subject: [PATCH 1/5] OSD-28241: Add multi-environment OCM and backplane connection support --- cmd/common/helpers.go | 29 ++ cmd/common/helpers_test.go | 70 +++++ cmd/hive/cmd.go | 2 + cmd/hive/loginhivetests.go | 519 ++++++++++++++++++++++++++++++++ pkg/k8s/client.go | 41 +++ pkg/k8s/client_test.go | 117 +++++++ pkg/utils/ocm.go | 285 ++++++++++++++++++ pkg/utils/ocm_test.go | 602 +++++++++++++++++++++++++++++++++++++ 8 files changed, 1665 insertions(+) create mode 100644 cmd/common/helpers_test.go create mode 100644 cmd/hive/loginhivetests.go create mode 100644 pkg/k8s/client_test.go diff --git a/cmd/common/helpers.go b/cmd/common/helpers.go index 534f2abb1..fdab63133 100644 --- a/cmd/common/helpers.go +++ b/cmd/common/helpers.go @@ -5,6 +5,7 @@ import ( "fmt" "io" + ocmsdk "github.com/openshift-online/ocm-sdk-go" bplogin "github.com/openshift/backplane-cli/cmd/ocm-backplane/login" bpconfig "github.com/openshift/backplane-cli/pkg/cli/config" "github.com/openshift/osdctl/pkg/utils" @@ -76,3 +77,31 @@ func GetKubeConfigAndClient(clusterID string, elevationReasons ...string) (clien } return kubeCli, kubeconfig, clientset, err } + +// If some elevationReasons are provided, then the config will be elevated with user backplane-cluster-admin +// Using provided OCM sdk connection for config values. +func GetKubeConfigAndClientWithConn(clusterID string, ocm *ocmsdk.Connection, elevationReasons ...string) (client.Client, *rest.Config, *kubernetes.Clientset, error) { + bp, err := bpconfig.GetBackplaneConfigurationWithConn(ocm) + if err != nil { + return nil, nil, nil, fmt.Errorf("failed to load backplane-cli config: %v", err) + } + var kubeconfig *rest.Config + if len(elevationReasons) == 0 { + kubeconfig, err = bplogin.GetRestConfigWithConn(bp, ocm, clusterID) + } else { + kubeconfig, err = bplogin.GetRestConfigAsUserWithConn(bp, ocm, clusterID, "backplane-cluster-admin", elevationReasons...) + } + if err != nil { + return nil, nil, nil, err + } + // create the clientset + clientset, err := kubernetes.NewForConfig(kubeconfig) + if err != nil { + return nil, nil, nil, err + } + kubeCli, err := client.New(kubeconfig, client.Options{}) + if err != nil { + return nil, nil, nil, err + } + return kubeCli, kubeconfig, clientset, nil +} diff --git a/cmd/common/helpers_test.go b/cmd/common/helpers_test.go new file mode 100644 index 000000000..a9e1b1bad --- /dev/null +++ b/cmd/common/helpers_test.go @@ -0,0 +1,70 @@ +package common + +import ( + "testing" + + sdk "github.com/openshift-online/ocm-sdk-go" +) + +// TestGetKubeConfigAndClientWithConn tests the GetKubeConfigAndClientWithConn function +// which creates a Kubernetes client, REST config, and clientset using a provided OCM SDK +// connection. This function supports both regular and elevated (backplane-cluster-admin) +// access based on the presence of elevation reasons. +func TestGetKubeConfigAndClientWithConn(t *testing.T) { + tests := []struct { + name string + clusterID string + ocmConn *sdk.Connection + elevationReasons []string + wantErr bool + }{ + { + // Test that passing a nil OCM connection without elevation returns an error + name: "nil OCM connection", + clusterID: "test-cluster-id", + ocmConn: nil, + elevationReasons: nil, + wantErr: true, + }, + { + // Test that passing a nil OCM connection with elevation reasons also returns an error + name: "nil OCM connection with elevation reasons", + clusterID: "test-cluster-id", + ocmConn: nil, + elevationReasons: []string{"testing"}, + wantErr: true, + }, + { + // Test that passing an empty cluster ID with nil connection returns an error + name: "empty cluster ID with nil connection", + clusterID: "", + ocmConn: nil, + elevationReasons: nil, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + kubeCli, kubeconfig, clientset, err := GetKubeConfigAndClientWithConn(tt.clusterID, tt.ocmConn, tt.elevationReasons...) + if tt.wantErr { + if err == nil { + t.Errorf("GetKubeConfigAndClientWithConn() expected error but got none") + } + } else { + if err != nil { + t.Errorf("GetKubeConfigAndClientWithConn() unexpected error = %v", err) + } + if kubeCli == nil { + t.Errorf("GetKubeConfigAndClientWithConn() returned nil kubeCli") + } + if kubeconfig == nil { + t.Errorf("GetKubeConfigAndClientWithConn() returned nil kubeconfig") + } + if clientset == nil { + t.Errorf("GetKubeConfigAndClientWithConn() returned nil clientset") + } + } + }) + } +} diff --git a/cmd/hive/cmd.go b/cmd/hive/cmd.go index 6a8c9226e..0de8ae1be 100644 --- a/cmd/hive/cmd.go +++ b/cmd/hive/cmd.go @@ -2,6 +2,7 @@ package hive import ( "fmt" + cd "github.com/openshift/osdctl/cmd/hive/clusterdeployment" "github.com/spf13/cobra" "k8s.io/cli-runtime/pkg/genericclioptions" @@ -19,6 +20,7 @@ func NewCmdHive(streams genericclioptions.IOStreams, client client.Client) *cobr hiveCmd.AddCommand(NewCmdClusterSyncFailures(streams, client)) hiveCmd.AddCommand(cd.NewCmdClusterDeployment(streams, client)) + hiveCmd.AddCommand(newCmdTestHiveLogin()) return hiveCmd } diff --git a/cmd/hive/loginhivetests.go b/cmd/hive/loginhivetests.go new file mode 100644 index 000000000..e8654e46c --- /dev/null +++ b/cmd/hive/loginhivetests.go @@ -0,0 +1,519 @@ +package hive + +import ( + "context" + "fmt" + "os" + "strings" + + ocmConfig "github.com/openshift-online/ocm-common/pkg/ocm/config" + sdk "github.com/openshift-online/ocm-sdk-go" + v1 "github.com/openshift-online/ocm-sdk-go/clustersmgmt/v1" + configv1 "github.com/openshift/api/config/v1" + hivev1 "github.com/openshift/hive/apis/hive/v1" + common "github.com/openshift/osdctl/cmd/common" + k8s "github.com/openshift/osdctl/pkg/k8s" + "github.com/openshift/osdctl/pkg/printer" + "github.com/openshift/osdctl/pkg/utils" + "github.com/spf13/cobra" + "github.com/spf13/viper" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + cmdutil "k8s.io/kubectl/pkg/cmd/util" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// testHiveLoginOptions defines the struct for running ocm and backplane client tests + +type testHiveLoginOptions struct { + clusterID string + output string + verbose bool + hiveOcmConfigPath string + hiveOcmURL string + reason string +} + +const longDescription = ` +This test utility attempts to exercise and validate OSDCTL's functions related to +OCM and backplane client connections. + +This test utiltiy can be run against an OSD/Rosa Classic target cluster. This utility +will attempt to discover the Hive cluster, and create both +OCM and kube client connections, and perform basic requests for each to connection in +order to validate functionality of the related OSDCTL utility functions. + +This test utility allows for the target cluster to exist in a separate OCM +environment (ie integration, staging) from the hive cluster (ie production). + +The default OCM environment vars should be set for the target cluster. +If the target cluster exists outside of the OCM 'production' environment, the user +has the option to provide the production OCM config (with valid token set), +or provide the production OCM API url as a command arguement, or set the value in the osdctl +config yaml file (ie: "hive_ocm_url: https://api.openshift.com" or "hive_ocm_url: production" ). +For testing purposes comment out 'hive_ocm_url' from osdctl's config if testing an empty value. +` + +// Defines command to run through series of tests to validate existing and new and legacy +// ocm + backplane client functions +func newCmdTestHiveLogin() *cobra.Command { + ops := newtestHiveLoginOptions() + testHiveLoginCmd := &cobra.Command{ + Use: "login-tests", + Short: "Test utility to exercise OSDCTL client connections for both Target Cluster and it's Hive Cluster.", + Long: longDescription, + Args: cobra.NoArgs, + DisableAutoGenTag: true, + Run: func(cmd *cobra.Command, args []string) { + cmdutil.CheckErr(ops.run()) + }, + } + + testHiveLoginCmd.Flags().BoolVarP(&ops.verbose, "verbose", "", false, "Verbose output") + testHiveLoginCmd.Flags().StringVarP(&ops.clusterID, "cluster-id", "C", "", "Cluster ID") + testHiveLoginCmd.Flags().StringVar(&ops.hiveOcmConfigPath, "hive-ocm-config", "", "OCM config for hive if different than Cluster") + testHiveLoginCmd.Flags().StringVar(&ops.hiveOcmURL, "hive-ocm-url", "", "OCM URL for hive, this will fallback to reading from the osdctl config value: 'hive_ocm_url' if left empty") + + testHiveLoginCmd.MarkFlagRequired("cluster-id") + return testHiveLoginCmd +} + +func newtestHiveLoginOptions() *testHiveLoginOptions { + return &testHiveLoginOptions{} +} + +func printDiv() { + fmt.Printf("\n---------------------------------------------------------------\n\n") +} + +func dumpClusterOperators(kubeClient client.Client) error { + var cos configv1.ClusterOperatorList + if err := kubeClient.List(context.TODO(), &cos, &client.ListOptions{}); err != nil { + fmt.Fprintf(os.Stderr, "error fetching cluster operators, err:'%s'\n", err) + return err + } + table := printer.NewTablePrinter(os.Stderr, 20, 1, 1, ' ') + table.AddRow([]string{"NAME", "AVAILABLE", "PROGRESSING", "DEGRADED"}) + for _, co := range cos.Items { + var available configv1.ConditionStatus + var progressing configv1.ConditionStatus + var degraded configv1.ConditionStatus + for _, cond := range co.Status.Conditions { + switch cond.Type { + case configv1.OperatorAvailable: + available = cond.Status + case configv1.OperatorProgressing: + progressing = cond.Status + case configv1.OperatorDegraded: + degraded = cond.Status + } + } + table.AddRow([]string{co.Name, string(available), string(progressing), string(degraded)}) + } + table.Flush() + return nil +} + +func getClusterDeployment(hiveKubeClient client.Client, clusterID string) (cd *hivev1.ClusterDeployment, err error) { + var cds hivev1.ClusterDeploymentList + if err := hiveKubeClient.List(context.TODO(), &cds, &client.ListOptions{}); err != nil { + fmt.Printf("err fetching cluster deployments, err:'%v'", err) + return nil, err + } + for _, cdeploy := range cds.Items { + if strings.Contains(cdeploy.Namespace, clusterID) { + fmt.Printf("Got Hive ClusterDeployment for target cluster:'%s'\n", cdeploy.Name) + return &cdeploy, nil + } + } + return nil, fmt.Errorf("clusterDeployment for cluster:'%s' not found", clusterID) +} + +// setupOCMConnection creates an OCM client and fetches the target cluster +func setupOCMConnection(clusterID string) (*sdk.Connection, *v1.Cluster, string, error) { + printDiv() + fmt.Printf("Building ocm client using legacy functions and env vars...\n") + ocmClient, err := utils.CreateConnection() + if err != nil { + return nil, nil, "", err + } + + cluster, err := utils.GetClusterAnyStatus(ocmClient, clusterID) + if err != nil { + fmt.Printf("Failed to fetch cluster '%s' from OCM, err:'%v'", clusterID, err) + return nil, nil, "", err + } + + actualClusterID := cluster.ID() + if clusterID != actualClusterID { + fmt.Printf("Using internal ID:'%s' for provided cluster:'%s'\n", actualClusterID, clusterID) + } + + fmt.Printf("Fetched cluster from OCM:'%s'\n", actualClusterID) + printDiv() + + return ocmClient, cluster, actualClusterID, nil +} + +// setupHiveOCMConfig builds the Hive OCM configuration from provided options +func setupHiveOCMConfig(hiveOcmConfigPath, hiveOcmURL string) (*ocmConfig.Config, error) { + var hiveOCMCfg *ocmConfig.Config + var err error + + // Test building OCM config from a provided file path + if len(hiveOcmConfigPath) > 0 { + fmt.Printf("Attempting to build OCM config from provided file path...\n") + hiveOCMCfg, err = utils.GetOcmConfigFromFilePath(hiveOcmConfigPath) + if err != nil { + fmt.Printf("Failed to build Hive OCM config from file path:'%s'\n", hiveOcmConfigPath) + return nil, err + } + } + + // Test replacing just the OCM URL for an already built config + if len(hiveOcmURL) > 0 { + if hiveOCMCfg == nil { + fmt.Printf("Attempting to build OCM config...\n") + hiveOCMCfg, err = utils.GetOCMConfigFromEnv() + if err != nil { + fmt.Printf("Failed to build OCM config from legacy function\n") + return nil, err + } + } + hiveOCMCfg.URL = hiveOcmURL + } + + return hiveOCMCfg, nil +} + +// setupHiveOCMConnection creates an OCM connection for Hive using the provided config +func setupHiveOCMConnection(hiveOCMCfg *ocmConfig.Config, hiveOcmConfigPath string) (*sdk.Connection, error) { + if hiveOCMCfg == nil { + return nil, nil + } + + hiveBuilder, err := utils.GetOCMSdkConnBuilderFromConfig(hiveOCMCfg) + if err != nil { + fmt.Printf("Failed to create sdk connection builder from hive ocm cfg, err:'%s'\n", err) + return nil, err + } + + hiveOCM, err := hiveBuilder.Build() + if err != nil { + fmt.Printf("Error connecting to OCM env using config at: '%s'\nErr:%v", hiveOcmConfigPath, err) + return nil, err + } + + fmt.Printf("Built OCM config and connection from provided config inputs\n") + printDiv() + + return hiveOCM, nil +} + +// setupHiveCluster fetches the Hive cluster for the target cluster +func setupHiveCluster(clusterID string, ocmClient, hiveOCM *sdk.Connection) (*v1.Cluster, error) { + // No OCM related config provided, test the legacy path + if hiveOCM == nil { + fmt.Println("---- No hive config provided. Using same OCM connections for target cluster and hive ----") + hiveOCM = ocmClient + _, err := utils.GetHiveCluster(clusterID) + if err != nil { + fmt.Printf("Failed to fetch hive cluster from OCM with legacy function, err:'%v'", err) + return nil, err + } + } + + printDiv() + hiveCluster, err := utils.GetHiveClusterWithConn(clusterID, ocmClient, hiveOCM) + if err != nil { + fmt.Printf("Failed to fetch hive cluster with provided OCM conneciton, err:'%v'", err) + return nil, err + } + + fmt.Printf("Got Hive Cluster from OCM:'%s'\n", hiveCluster.ID()) + printDiv() + + return hiveCluster, nil +} + +// testK8sNew tests creating a Kube client using k8s.New() +func testK8sNew(clusterID string, cluster *v1.Cluster) error { + fmt.Println("Attempting to create and test Kube Client with k8s.New()...") + kubeClient, err := k8s.New(clusterID, client.Options{}) + if err != nil { + return fmt.Errorf("failed to login to cluster:'%s', err: %w", clusterID, err) + } + fmt.Printf("Created client connection to target cluster:'%s', '%s'\n", cluster.ID(), cluster.Name()) + + if err := dumpClusterOperators(kubeClient); err != nil { + return err + } + + fmt.Println("Create and test Kube Client with k8s.New() - PASS") + printDiv() + return nil +} + +// testK8sNewWithConn tests creating a Kube client using k8s.NewWithConn() +func testK8sNewWithConn(hiveCluster *v1.Cluster, hiveOCM *sdk.Connection) error { + fmt.Println("Attempting to create and test Kube Client with k8s.NewWithConn()...") + hiveClient, err := k8s.NewWithConn(hiveCluster.ID(), client.Options{}, hiveOCM) + if err != nil { + return fmt.Errorf("failed to login to hive cluster:'%s', err %w", hiveCluster.ID(), err) + } + fmt.Printf("Created client connection to HIVE cluster:'%s', '%s'\n", hiveCluster.ID(), hiveCluster.Name()) + + if err := dumpClusterOperators(hiveClient); err != nil { + return err + } + + fmt.Println("Create and test Kube Client with k8s.NewWithConn() - PASS") + printDiv() + return nil +} + +// testK8sNewAsBackplaneClusterAdmin tests creating an elevated Kube client using k8s.NewAsBackplaneClusterAdminWithConn() +func testK8sNewAsBackplaneClusterAdmin(hiveCluster *v1.Cluster, hiveOCM *sdk.Connection, clusterID, reason string) error { + fmt.Println("Attempting to create and test Kube Client with k8s.NewAsBackplaneClusterAdminWithConn()...") + hiveAdminClient, err := k8s.NewAsBackplaneClusterAdminWithConn(hiveCluster.ID(), client.Options{}, hiveOCM, reason) + if err != nil { + return fmt.Errorf("failed to login to hive cluster:'%s', err %w", hiveCluster.ID(), err) + } + fmt.Printf("Created 'ClusterAdmin' client connection to HIVE cluster:'%s', '%s'\n", hiveCluster.ID(), hiveCluster.Name()) + + clusterDep, err := getClusterDeployment(hiveAdminClient, clusterID) + if err != nil { + return err + } + + fmt.Printf("Fetched ClusterDeployment:'%s/%s' for cluster:'%s' from HIVE using elevated client\n", clusterDep.Namespace, clusterDep.Name, clusterID) + fmt.Println("Create and test Kube Client withk8s.NewAsBackplaneClusterAdminWithConn() - PASS") + printDiv() + return nil +} + +// testGetKubeConfigAndClient tests GetKubeConfigAndClient() without admin elevation +func testGetKubeConfigAndClient(clusterID string) error { + fmt.Printf("Testing non-backplane-admin client, clientSet GetKubeConfigAndClient() for cluster:'%s'\n", clusterID) + kubeCli, _, kubeClientSet, err := common.GetKubeConfigAndClient(clusterID) + if err != nil { + return err + } + + if err := dumpClusterOperators(kubeCli); err != nil { + return err + } + + nsList, err := kubeClientSet.CoreV1().Namespaces().List(context.TODO(), metav1.ListOptions{}) + if err != nil { + fmt.Printf("ClientSet list namespaces failed, err:'%v'\n", err) + return err + } + + fmt.Printf("Got '%d' namespaces\n", len(nsList.Items)) + fmt.Println("non-bpadmin Create and test Kube Client, Clientset with GetKubeConfigAndClient() - PASS") + printDiv() + return nil +} + +// testGetKubeConfigAndClientWithConn tests GetKubeConfigAndClientWithConn() without admin elevation +func testGetKubeConfigAndClientWithConn(clusterID string, ocmClient *sdk.Connection) error { + fmt.Printf("Testing non-backplane-admin client, clientset GetKubeConfigAndClientWithConn for cluster:'%s'\n", clusterID) + kubeCli, _, kubeClientSet, err := common.GetKubeConfigAndClientWithConn(clusterID, ocmClient) + if err != nil { + return err + } + + if err := dumpClusterOperators(kubeCli); err != nil { + return err + } + + nsList, err := kubeClientSet.CoreV1().Namespaces().List(context.TODO(), metav1.ListOptions{}) + if err != nil { + fmt.Printf("ClientSet list namespaces failed, err:'%v'\n", err) + return err + } + + fmt.Printf("Got '%d' namespaces\n", len(nsList.Items)) + fmt.Println("non-bpadmin Create and test Kube Client, Clientset with GetKubeConfigAndClientWithConn() - PASS") + printDiv() + return nil +} + +// testGetKubeConfigAndClientAdmin tests GetKubeConfigAndClient() with admin elevation +func testGetKubeConfigAndClientAdmin(clusterID, reason string) error { + fmt.Printf("Testing backplane-admin client, clientset GetKubeConfigAndClient() for cluster:'%s'\n", clusterID) + kubeCli, _, kubeClientSet, err := common.GetKubeConfigAndClient(clusterID, reason) + if err != nil { + return err + } + + if err := dumpClusterOperators(kubeCli); err != nil { + return err + } + + openshiftMonitoringNamespace := "openshift-monitoring" + podList, err := kubeClientSet.CoreV1().Pods(openshiftMonitoringNamespace).List(context.TODO(), metav1.ListOptions{}) + if err != nil { + fmt.Printf("ClientSet list 'openshift-monitoring' pods failed, err:'%v'\n", err) + return err + } + + fmt.Printf("Got %d pods in namespace:'%s' :\n", len(podList.Items), openshiftMonitoringNamespace) + for i, pod := range podList.Items { + fmt.Printf("Got pod (%d/%d): '%s/%s' \n", i, len(podList.Items), pod.Namespace, pod.Name) + } + + fmt.Println("bpadmin Create and test Kube Client, Clientset with GetKubeConfigAndClient() - PASS") + printDiv() + return nil +} + +// testGetKubeConfigAndClientWithConnAdmin tests GetKubeConfigAndClientWithConn() with admin elevation +func testGetKubeConfigAndClientWithConnAdmin(clusterID string, ocmClient *sdk.Connection, reason string) error { + fmt.Printf("Testing backplane-admin GetKubeConfigAndClientWithConn() for cluster:'%s'\n", clusterID) + kubeCli, _, kubeClientSet, err := common.GetKubeConfigAndClientWithConn(clusterID, ocmClient, reason) + if err != nil { + return err + } + + if err := dumpClusterOperators(kubeCli); err != nil { + return err + } + + podList, err := kubeClientSet.CoreV1().Pods("openshift-monitoring").List(context.TODO(), metav1.ListOptions{}) + if err != nil { + fmt.Printf("ClientSet list 'openshift-monitoring' pods failed, err:'%v'\n", err) + return err + } + + fmt.Printf("Got %d pods\n", len(podList.Items)) + for i, pod := range podList.Items { + fmt.Printf("Got pod (%d/%d): '%s/%s' \n", i, len(podList.Items), pod.Namespace, pod.Name) + } + + fmt.Println("bpadmin Create and test Kube Client, Clientset with GetKubeConfigAndClientWithConn() - PASS") + printDiv() + return nil +} + +// testGetHiveBPWithoutElevation tests GetHiveBPClientForCluster() without elevation +func testGetHiveBPWithoutElevation(clusterID, hiveOcmURL string) error { + fmt.Printf("Testing GetHiveBPClientForCluster() hive backplane connection w/o elevation\n") + hiveBP, err := utils.GetHiveBPClientForCluster(clusterID, client.Options{}, "", hiveOcmURL) + if err != nil { + return err + } + + if err := dumpClusterOperators(hiveBP); err != nil { + return err + } + + fmt.Println("Create and test GetHiveBPClientForCluster() without elevation reason - PASS") + printDiv() + return nil +} + +// testGetHiveBPWithElevation tests GetHiveBPClientForCluster() with elevation +func testGetHiveBPWithElevation(clusterID, reason, hiveOcmURL string) error { + fmt.Printf("Testing GetHiveBPClientForCluster() hive backplane connection with elevation\n") + hiveBP, err := utils.GetHiveBPClientForCluster(clusterID, client.Options{}, reason, hiveOcmURL) + if err != nil { + return err + } + + if err := dumpClusterOperators(hiveBP); err != nil { + return err + } + + clusterDep, err := getClusterDeployment(hiveBP, clusterID) + if err != nil { + return err + } + + fmt.Printf("Fetched ClusterDeployment:'%s/%s' for cluster:'%s' from HIVE using elevated client\n", clusterDep.Namespace, clusterDep.Name, clusterID) + fmt.Println("Create and test GetHiveBPClientForCluster() with elevation reason - PASS") + printDiv() + return nil +} + +func (o *testHiveLoginOptions) run() error { + // Initialize Hive OCM URL from args or config + if len(o.hiveOcmURL) > 0 { + fmt.Printf("Using Hive OCM URL set in args:'%s'\n", o.hiveOcmURL) + } else { + o.hiveOcmURL = viper.GetString("hive_ocm_url") + if len(o.hiveOcmURL) > 0 { + fmt.Printf("Got Hive OCM URL from viper vars:'%s'\n", o.hiveOcmURL) + } else { + fmt.Printf("No 'separate' Hive OCM URL set, using defaults set for target cluster.\n") + } + } + + o.reason = "Testing osdctl clients with cluster admin" + + // Setup: Create OCM connection and fetch target cluster + ocmClient, cluster, clusterID, err := setupOCMConnection(o.clusterID) + if err != nil { + return err + } + defer ocmClient.Close() + o.clusterID = clusterID + + // Setup: Build Hive OCM config if provided + hiveOCMCfg, err := setupHiveOCMConfig(o.hiveOcmConfigPath, o.hiveOcmURL) + if err != nil { + return err + } + + // Setup: Create Hive OCM connection if config was built + hiveOCM, err := setupHiveOCMConnection(hiveOCMCfg, o.hiveOcmConfigPath) + if err != nil { + return err + } + + // Setup: Fetch Hive cluster + hiveCluster, err := setupHiveCluster(clusterID, ocmClient, hiveOCM) + if err != nil { + return err + } + + // Run individual tests + if err := testK8sNew(clusterID, cluster); err != nil { + return err + } + + if err := testK8sNewWithConn(hiveCluster, hiveOCM); err != nil { + return err + } + + if err := testK8sNewAsBackplaneClusterAdmin(hiveCluster, hiveOCM, clusterID, o.reason); err != nil { + return err + } + + if err := testGetKubeConfigAndClient(clusterID); err != nil { + return err + } + + if err := testGetKubeConfigAndClientWithConn(clusterID, ocmClient); err != nil { + return err + } + + if err := testGetKubeConfigAndClientAdmin(clusterID, o.reason); err != nil { + return err + } + + if err := testGetKubeConfigAndClientWithConnAdmin(clusterID, ocmClient, o.reason); err != nil { + return err + } + + if err := testGetHiveBPWithoutElevation(clusterID, o.hiveOcmURL); err != nil { + return err + } + + if err := testGetHiveBPWithElevation(clusterID, "Testing hive client backplane connections", o.hiveOcmURL); err != nil { + return err + } + + fmt.Println("All tests Passed") + return nil +} diff --git a/pkg/k8s/client.go b/pkg/k8s/client.go index ee9640c29..32d8d4f5d 100644 --- a/pkg/k8s/client.go +++ b/pkg/k8s/client.go @@ -5,6 +5,7 @@ import ( "fmt" "io" + sdk "github.com/openshift-online/ocm-sdk-go" bplogin "github.com/openshift/backplane-cli/cmd/ocm-backplane/login" bpconfig "github.com/openshift/backplane-cli/pkg/cli/config" bputils "github.com/openshift/backplane-cli/pkg/utils" @@ -162,6 +163,26 @@ func NewRestConfig(clusterID string) (*rest.Config, error) { return cfg, nil } +// Create Backplane connection to a provided cluster, using a provided ocm sdk connection +// This is intended to allow backplane connections to multiple clusters which exist in different +// ocm environments by allowing the caller to provide an ocm connection to the function. +func NewWithConn(clusterID string, options client.Options, ocmConn *sdk.Connection) (client.Client, error) { + if ocmConn == nil { + return nil, fmt.Errorf("nil OCM sdk connection provided to NewWithConn()") + } + bp, err := bpconfig.GetBackplaneConfigurationWithConn(ocmConn) + if err != nil { + return nil, fmt.Errorf("failed to load backplane-cli config: %v", err) + } + + cfg, err := bplogin.GetRestConfigWithConn(bp, ocmConn, clusterID) + if err != nil { + return nil, err + } + setRuntimeLoggerDiscard() + return client.New(cfg, options) +} + func NewAsBackplaneClusterAdmin(clusterID string, options client.Options, elevationReasons ...string) (client.Client, error) { bp, err := bpconfig.GetBackplaneConfiguration() if err != nil { @@ -176,6 +197,26 @@ func NewAsBackplaneClusterAdmin(clusterID string, options client.Options, elevat return client.New(cfg, options) } +// Create Backplane connection as cluster admin to a provided cluster, using a provided ocm sdk connection +// This is intended to allow backplane connections to multiple clusters which exist in different +// ocm environments by allowing the caller to provide an ocm connection to the function. +func NewAsBackplaneClusterAdminWithConn(clusterID string, options client.Options, ocmConn *sdk.Connection, elevationReasons ...string) (client.Client, error) { + if ocmConn == nil { + return nil, fmt.Errorf("nil OCM sdk connection provided to NewAsBackplaneClusterAdminWithConn()") + } + bp, err := bpconfig.GetBackplaneConfigurationWithConn(ocmConn) + if err != nil { + return nil, fmt.Errorf("failed to load backplane-cli config: %v", err) + } + + cfg, err := bplogin.GetRestConfigAsUserWithConn(bp, ocmConn, clusterID, "backplane-cluster-admin", elevationReasons...) + if err != nil { + return nil, err + } + setRuntimeLoggerDiscard() + return client.New(cfg, options) +} + func setRuntimeLoggerDiscard() { // To avoid warnings/backtrace, if k8s controller-runtime logger has not already been set, do it now... if !log.Log.Enabled() { diff --git a/pkg/k8s/client_test.go b/pkg/k8s/client_test.go new file mode 100644 index 000000000..16816ffec --- /dev/null +++ b/pkg/k8s/client_test.go @@ -0,0 +1,117 @@ +package k8s + +import ( + "testing" + + sdk "github.com/openshift-online/ocm-sdk-go" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// TestNewWithConn tests the NewWithConn function which creates a Kubernetes client +// using backplane with a provided OCM SDK connection. This allows connecting to clusters +// in different OCM environments by providing a custom OCM connection. +func TestNewWithConn(t *testing.T) { + tests := []struct { + name string + clusterID string + options client.Options + ocmConn *sdk.Connection + wantErr bool + }{ + { + // Test that passing a nil OCM connection returns an error + name: "nil OCM connection", + clusterID: "test-cluster-id", + options: client.Options{}, + ocmConn: nil, + wantErr: true, + }, + { + // Test that passing an empty cluster ID with nil connection returns an error + name: "empty cluster ID with nil connection", + clusterID: "", + options: client.Options{}, + ocmConn: nil, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + client, err := NewWithConn(tt.clusterID, tt.options, tt.ocmConn) + if tt.wantErr { + if err == nil { + t.Errorf("NewWithConn() expected error but got none") + } + } else { + if err != nil { + t.Errorf("NewWithConn() unexpected error = %v", err) + } + if client == nil { + t.Errorf("NewWithConn() returned nil client") + } + } + }) + } +} + +// TestNewAsBackplaneClusterAdminWithConn tests the NewAsBackplaneClusterAdminWithConn function +// which creates an elevated Kubernetes client (backplane-cluster-admin) using a provided OCM +// SDK connection. This function allows connecting to clusters in different OCM environments +// with elevated permissions. +func TestNewAsBackplaneClusterAdminWithConn(t *testing.T) { + tests := []struct { + name string + clusterID string + options client.Options + ocmConn *sdk.Connection + elevationReasons []string + wantErr bool + }{ + { + // Test that passing a nil OCM connection with elevation reasons returns an error + name: "nil OCM connection", + clusterID: "test-cluster-id", + options: client.Options{}, + ocmConn: nil, + elevationReasons: []string{"testing"}, + wantErr: true, + }, + { + // Test that passing a nil OCM connection without elevation reasons also returns an error + name: "nil OCM connection with no elevation reasons", + clusterID: "test-cluster-id", + options: client.Options{}, + ocmConn: nil, + elevationReasons: nil, + wantErr: true, + }, + { + // Test that passing an empty cluster ID with nil connection returns an error + name: "empty cluster ID with nil connection", + clusterID: "", + options: client.Options{}, + ocmConn: nil, + elevationReasons: []string{"testing"}, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + client, err := NewAsBackplaneClusterAdminWithConn(tt.clusterID, tt.options, tt.ocmConn, tt.elevationReasons...) + if tt.wantErr { + if err == nil { + t.Errorf("NewAsBackplaneClusterAdminWithConn() expected error but got none") + } + } else { + if err != nil { + t.Errorf("NewAsBackplaneClusterAdminWithConn() unexpected error = %v", err) + } + if client == nil { + t.Errorf("NewAsBackplaneClusterAdminWithConn() returned nil client") + } + } + }) + } +} diff --git a/pkg/utils/ocm.go b/pkg/utils/ocm.go index 32cd9b091..b1d3b508a 100644 --- a/pkg/utils/ocm.go +++ b/pkg/utils/ocm.go @@ -5,6 +5,7 @@ import ( "fmt" "log" "os" + "path/filepath" "regexp" "strings" @@ -13,6 +14,9 @@ import ( sdk "github.com/openshift-online/ocm-sdk-go" amsv1 "github.com/openshift-online/ocm-sdk-go/accountsmgmt/v1" cmv1 "github.com/openshift-online/ocm-sdk-go/clustersmgmt/v1" + "github.com/openshift/osdctl/pkg/k8s" + "github.com/spf13/viper" + "sigs.k8s.io/controller-runtime/pkg/client" ocmConfig "github.com/openshift-online/ocm-common/pkg/ocm/config" ocmConnBuilder "github.com/openshift-online/ocm-common/pkg/ocm/connection-builder" @@ -171,6 +175,84 @@ func GenerateQuery(clusterIdentifier string) string { } } +// Finds the OCM Configuration file and returns the path to it. +// ( Taken wholesale from openshift-online/ocm-cli ) +func getOCMConfigLocation() (string, error) { + if ocmconfig := os.Getenv("OCM_CONFIG"); ocmconfig != "" { + return ocmconfig, nil + } + + // Determine home directory to use for the legacy file path + home, err := os.UserHomeDir() + if err != nil { + return "", err + } + + path := filepath.Join(home, ".ocm.json") + + _, err = os.Stat(path) + if os.IsNotExist(err) { + // Determine standard config directory + configDir, err := os.UserConfigDir() + if err != nil { + return path, err + } + + // Use standard config directory + path = filepath.Join(configDir, "/ocm/ocm.json") + } + + return path, nil +} + +// Exported function fetch and return OCM config +func GetOCMConfigFromEnv() (*ocmConfig.Config, error) { + return loadOCMConfig() +} + +// Loads the OCM Configuration file +// Taken wholesale from openshift-online/ocm-cli +func loadOCMConfig() (*ocmConfig.Config, error) { + var err error + + file, err := getOCMConfigLocation() + if err != nil { + return nil, err + } + + _, err = os.Stat(file) + if os.IsNotExist(err) { + cfg := &ocmConfig.Config{} + err = nil + return cfg, err + } + + if err != nil { + err = fmt.Errorf("can't check if config file '%s' exists: %v", file, err) + return nil, err + } + + data, err := os.ReadFile(file) + if err != nil { + err = fmt.Errorf("can't read config file '%s': %v", file, err) + return nil, err + } + + if len(data) == 0 { + return nil, nil + } + + cfg := &ocmConfig.Config{} + err = json.Unmarshal(data, cfg) + + if err != nil { + err = fmt.Errorf("can't parse config file '%s': %v", file, err) + return cfg, err + } + + return cfg, nil +} + // Creates a connection to OCM func CreateConnection() (*sdk.Connection, error) { urlEnv := os.Getenv("OCM_URL") @@ -202,6 +284,34 @@ func CreateConnection() (*sdk.Connection, error) { return connBuilder.Build() } +// Creates a connection to OCM +func CreateConnectionWithUrl(OcmUrl string) (*sdk.Connection, error) { + var ocmApiUrl string = OcmUrl + if len(OcmUrl) <= 0 { + return nil, fmt.Errorf("CreateConnectionWithUrl provided empty OCM URL") + } + // First we need to validate URL in the case where it may be an alias + ocmApiUrl, ok := urlAliases[OcmUrl] + if !ok { + return nil, fmt.Errorf("invalid OCM_URL found: %s\nValid URL aliases are: 'production', 'staging', 'integration'", OcmUrl) + } + config, err := ocmConfig.Load() + if err != nil { + return nil, fmt.Errorf("unable to load OCM config. %w", err) + } + + agentString := fmt.Sprintf("osdctl-%s", Version) + + connBuilder := ocmConnBuilder.NewConnection().Config(config).AsAgent(agentString) + + if connBuilder == nil { + return nil, fmt.Errorf("CreateConnectionWithUrl, ocm connection builder returned nil builder") + } + connBuilder.WithApiUrl(ocmApiUrl) + + return connBuilder.Build() +} + func GetSupportRoleArnForCluster(ocmClient *sdk.Connection, clusterID string) (string, error) { clusterResponse, err := ocmClient.ClustersMgmt().V1().Clusters().Cluster(clusterID).Get().Send() @@ -300,6 +410,36 @@ func GetHiveShard(clusterID string) (string, error) { return shard, nil } +// Returns the hive shard corresponding to a cluster using provided OCM connection +// e.g. https://api..byo5.p1.openshiftapps.com:6443 +func GetHiveShardWithConn(clusterID string, conn *sdk.Connection) (string, error) { + if conn == nil { + return "", fmt.Errorf("nil OCM sdk connection provided to GetHiveShardWithConn()") + } + + shardPath, err := conn.ClustersMgmt().V1().Clusters(). + Cluster(clusterID). + ProvisionShard(). + Get(). + Send() + + if err != nil { + return "", err + } + + var shard string + + if shardPath != nil { + shard = shardPath.Body().HiveConfig().Server() + } + + if shard == "" { + return "", fmt.Errorf("Unable to retrieve shard for cluster %s", clusterID) + } + + return shard, nil +} + func GetHiveCluster(clusterId string) (*cmv1.Cluster, error) { conn, err := CreateConnection() if err != nil { @@ -335,6 +475,91 @@ func GetHiveCluster(clusterId string) (*cmv1.Cluster, error) { return resp.Items().Get(0), nil } +func GetHiveBPClientForCluster(clusterID string, options client.Options, elevationReason string, hiveOCMURL string) (client.Client, error) { + var hiveOCMConn *sdk.Connection + var err error + if len(clusterID) <= 0 { + return nil, fmt.Errorf("GetHiveBPClientForCluster provided empty target cluster ID") + } + if len(hiveOCMURL) <= 0 { + hiveOCMURL = viper.GetString("hive_ocm_url") + } + if len(hiveOCMURL) > 0 { + hiveOCMConn, err = CreateConnectionWithUrl(hiveOCMURL) + if err != nil { + return nil, fmt.Errorf("unable to create hive OCM connection with URL:'%s'. Err: %w", hiveOCMURL, err) + } + defer hiveOCMConn.Close() + hiveCluster, err := GetHiveClusterWithConn(clusterID, nil, hiveOCMConn) + if err != nil { + return nil, fmt.Errorf("failed to fetch hive cluster for cluster:'%s', ocmURL:'%s', Err:'%v'", clusterID, hiveOCMURL, err) + } + if len(elevationReason) > 0 { + return k8s.NewAsBackplaneClusterAdminWithConn(hiveCluster.ID(), options, hiveOCMConn, elevationReason) + } + return k8s.NewWithConn(hiveCluster.ID(), options, hiveOCMConn) + } else { + hiveCluster, err := GetHiveCluster(clusterID) + if err != nil { + return nil, fmt.Errorf("failed to fetch hive cluster for cluster:'%s', err:'%v'", clusterID, err) + } + if len(elevationReason) > 0 { + return k8s.NewAsBackplaneClusterAdmin(hiveCluster.ID(), options, elevationReason) + } + return k8s.New(hiveCluster.ID(), options) + } +} + +// Fetch Hive Cluster with provided OCM connections. +// In the case that the target cluster(stage, integration, etc does not reside +// in the same OCM env as Hive (prod), separate OCM SDK connections +// can be provided for accessing each. If nil is provided a temporary connection using +// the default OCM env vars will be made. +func GetHiveClusterWithConn(clusterId string, clusterOCM *sdk.Connection, hiveOCM *sdk.Connection) (*cmv1.Cluster, error) { + var err error + if clusterOCM == nil { + clusterOCM, err = CreateConnection() + if err != nil { + return nil, err + } + // If provided by caller do not close, only close if connection created here. + defer clusterOCM.Close() + } + if hiveOCM == nil { + hiveOCM = clusterOCM + } + provisionShard, err := clusterOCM.ClustersMgmt().V1().Clusters(). + Cluster(clusterId). + ProvisionShard(). + Get(). + Send() + if err != nil { + fmt.Printf("Failed to get provisionShard for cluster:'%s', err:'%v'", clusterId, err) + return nil, err + } + + hiveApiUrl, ok := provisionShard.Body().HiveConfig().GetServer() + if !ok { + fmt.Printf("No provisionShard found for cluster:'%s'", clusterId) + return nil, fmt.Errorf("no provision shard url found for %s", clusterId) + } + resp, err := hiveOCM.ClustersMgmt().V1().Clusters().List(). + Parameter("search", fmt.Sprintf("api.url='%s'", hiveApiUrl)). + Send() + if err != nil { + fmt.Printf("Error listing clusters with hiveApiUrl:'%s'", hiveApiUrl) + return nil, err + } + + if resp.Items().Empty() { + fmt.Printf("Failed to find hive cluster from hiveApiURL:'%s'", hiveApiUrl) + return nil, fmt.Errorf("failed to find cluster with api.url=%s", hiveApiUrl) + } + + return resp.Items().Get(0), nil + +} + // GetManagementCluster returns the OCM Cluster object for a provided clusterId func GetManagementCluster(clusterId string) (*cmv1.Cluster, error) { conn, err := CreateConnection() @@ -515,3 +740,63 @@ func SendRequest(request *sdk.Request) (*sdk.Response, error) { } return response, nil } + +// Creates an OCM Config object from values read at provided filePath +// utils has a local 'copy' of the config struct +// rather than vendor from "github.com/openshift-online/ocm-cli/pkg/config" +func GetOcmConfigFromFilePath(filePath string) (*ocmConfig.Config, error) { + data, err := os.ReadFile(filePath) + if err != nil { + err = fmt.Errorf("can't read config file '%s': %v", filePath, err) + return nil, err + } + + if len(data) == 0 { + return nil, fmt.Errorf("empty config file:'%s'", filePath) + } + cfg := &ocmConfig.Config{} + err = json.Unmarshal(data, cfg) + if err != nil { + err = fmt.Errorf("can't parse config file '%s': %v", filePath, err) + return nil, err + } + return cfg, nil +} + +func GetOCMSdkConnBuilderFromConfig(ocmCfg *ocmConfig.Config) (*sdk.ConnectionBuilder, error) { + if ocmCfg == nil { + return nil, fmt.Errorf("nil OCM config provided to OCMSdkConnBuilderFromConfig()") + } + // Can use the sdk.connection builder or alternatively omc cli's connection builder wrappers here. + // Each returns an ocm-sdk connection builder. + ocmSdkConnBuilder := sdk.NewConnectionBuilder() + ocmSdkConnBuilder.URL(ocmCfg.URL) + ocmSdkConnBuilder.Tokens(ocmCfg.AccessToken, ocmCfg.RefreshToken) + ocmSdkConnBuilder.Client(ocmCfg.ClientID, ocmCfg.ClientSecret) + return ocmSdkConnBuilder, nil +} + +// Returns an ocmSdkConnBuilder with initial values read from provided configFilePath. +func GetOCMSdkConnBuilderFromFilePath(configFilePath string) (*sdk.ConnectionBuilder, error) { + // Now get the backplane url and access token from OCM... + ocmConfig, err := GetOcmConfigFromFilePath(configFilePath) + if err != nil { + return nil, err + } + return GetOCMSdkConnBuilderFromConfig(ocmConfig) + +} + +// Returns an OCM SDK connection using values read from provided configFilePath. +func GetOCMSdkConnFromFilePath(configFilePath string) (*sdk.Connection, error) { + ocmSdkConnBuilder, err := GetOCMSdkConnBuilderFromFilePath(configFilePath) + if err != nil { + return nil, err + } + ocmSdkConn, err := ocmSdkConnBuilder.Build() + + if err != nil { + return nil, err + } + return ocmSdkConn, nil +} diff --git a/pkg/utils/ocm_test.go b/pkg/utils/ocm_test.go index 32c27a730..0b848a9d9 100644 --- a/pkg/utils/ocm_test.go +++ b/pkg/utils/ocm_test.go @@ -1,8 +1,14 @@ package utils import ( + "encoding/json" "os" + "path/filepath" "testing" + + ocmConfig "github.com/openshift-online/ocm-common/pkg/ocm/config" + sdk "github.com/openshift-online/ocm-sdk-go" + "sigs.k8s.io/controller-runtime/pkg/client" ) func resetEnvVars(t *testing.T) { @@ -57,3 +63,599 @@ func TestGenerateQuery(t *testing.T) { }) } } + +// TestGetOcmConfigFromFilePath tests the GetOcmConfigFromFilePath function which loads +// OCM configuration from a JSON file at the provided path. It validates that the function +// correctly handles valid config files, non-existent files, empty files, and malformed JSON. +func TestGetOcmConfigFromFilePath(t *testing.T) { + tests := []struct { + name string + setupFunc func(t *testing.T) string + wantErr bool + errContains string + checkConfig func(*testing.T, *ocmConfig.Config) + }{ + { + // Test that a valid OCM config file is successfully parsed and loaded with correct values + name: "valid config file", + setupFunc: func(t *testing.T) string { + tmpDir := t.TempDir() + configFile := filepath.Join(tmpDir, "ocm.json") + config := ocmConfig.Config{ + AccessToken: "test-access-token", + RefreshToken: "test-refresh-token", + URL: "https://api.openshift.com", + ClientID: "test-client-id", + ClientSecret: "test-client-secret", + } + data, err := json.Marshal(config) + if err != nil { + t.Fatalf("failed to marshal config: %v", err) + } + if err := os.WriteFile(configFile, data, 0644); err != nil { + t.Fatalf("failed to write config file: %v", err) + } + return configFile + }, + wantErr: false, + checkConfig: func(t *testing.T, cfg *ocmConfig.Config) { + if cfg == nil { + t.Error("expected non-nil config") + return + } + if cfg.AccessToken != "test-access-token" { + t.Errorf("expected AccessToken 'test-access-token', got '%s'", cfg.AccessToken) + } + if cfg.RefreshToken != "test-refresh-token" { + t.Errorf("expected RefreshToken 'test-refresh-token', got '%s'", cfg.RefreshToken) + } + if cfg.URL != "https://api.openshift.com" { + t.Errorf("expected URL 'https://api.openshift.com', got '%s'", cfg.URL) + } + if cfg.ClientID != "test-client-id" { + t.Errorf("expected ClientID 'test-client-id', got '%s'", cfg.ClientID) + } + if cfg.ClientSecret != "test-client-secret" { + t.Errorf("expected ClientSecret 'test-client-secret', got '%s'", cfg.ClientSecret) + } + }, + }, + { + // Test that attempting to load a non-existent file returns an appropriate error + name: "non-existent file", + setupFunc: func(t *testing.T) string { + return "/nonexistent/path/ocm.json" + }, + wantErr: true, + errContains: "can't read config file", + }, + { + // Test that an empty config file returns an error + name: "empty config file", + setupFunc: func(t *testing.T) string { + tmpDir := t.TempDir() + configFile := filepath.Join(tmpDir, "ocm.json") + if err := os.WriteFile(configFile, []byte(""), 0644); err != nil { + t.Fatalf("failed to write empty file: %v", err) + } + return configFile + }, + wantErr: true, + errContains: "empty config file", + }, + { + // Test that a file with invalid JSON syntax returns a parse error + name: "invalid json", + setupFunc: func(t *testing.T) string { + tmpDir := t.TempDir() + configFile := filepath.Join(tmpDir, "ocm.json") + if err := os.WriteFile(configFile, []byte("{invalid json}"), 0644); err != nil { + t.Fatalf("failed to write invalid json: %v", err) + } + return configFile + }, + wantErr: true, + errContains: "can't parse config file", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + filePath := tt.setupFunc(t) + cfg, err := GetOcmConfigFromFilePath(filePath) + if tt.wantErr { + if err == nil { + t.Errorf("GetOcmConfigFromFilePath() expected error but got none") + } else if tt.errContains != "" && !contains(err.Error(), tt.errContains) { + t.Errorf("GetOcmConfigFromFilePath() error = %v, want error containing %v", err, tt.errContains) + } + } else { + if err != nil { + t.Errorf("GetOcmConfigFromFilePath() unexpected error = %v", err) + } + if cfg == nil { + t.Errorf("GetOcmConfigFromFilePath() returned nil config") + } + if tt.checkConfig != nil { + tt.checkConfig(t, cfg) + } + } + }) + } +} + +// TestGetOCMSdkConnBuilderFromConfig tests the GetOCMSdkConnBuilderFromConfig function +// which creates an OCM SDK connection builder from a provided OCM config object. +// It validates nil config handling and successful builder creation with valid config. +func TestGetOCMSdkConnBuilderFromConfig(t *testing.T) { + tests := []struct { + name string + config *ocmConfig.Config + wantErr bool + }{ + { + // Test that a valid OCM config successfully creates a connection builder + name: "valid config", + config: &ocmConfig.Config{ + AccessToken: "test-access-token", + RefreshToken: "test-refresh-token", + URL: "https://api.openshift.com", + ClientID: "test-client-id", + ClientSecret: "test-client-secret", + }, + wantErr: false, + }, + { + // Test that passing a nil config returns an error + name: "nil config", + config: nil, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + builder, err := GetOCMSdkConnBuilderFromConfig(tt.config) + if tt.wantErr { + if err == nil { + t.Errorf("GetOCMSdkConnBuilderFromConfig() expected error but got none") + } + } else { + if err != nil { + t.Errorf("GetOCMSdkConnBuilderFromConfig() unexpected error = %v", err) + } + if builder == nil { + t.Errorf("GetOCMSdkConnBuilderFromConfig() returned nil builder") + } + } + }) + } +} + +// TestGetOCMSdkConnBuilderFromFilePath tests the GetOCMSdkConnBuilderFromFilePath function +// which reads an OCM config file and creates an SDK connection builder from it. +// It validates both successful builder creation and error handling for invalid file paths. +func TestGetOCMSdkConnBuilderFromFilePath(t *testing.T) { + tests := []struct { + name string + setupFunc func(t *testing.T) string + wantErr bool + errContains string + }{ + { + // Test that a valid config file successfully creates a connection builder + name: "valid config file", + setupFunc: func(t *testing.T) string { + tmpDir := t.TempDir() + configFile := filepath.Join(tmpDir, "ocm.json") + config := ocmConfig.Config{ + AccessToken: "test-access-token", + RefreshToken: "test-refresh-token", + URL: "https://api.openshift.com", + ClientID: "test-client-id", + ClientSecret: "test-client-secret", + } + data, err := json.Marshal(config) + if err != nil { + t.Fatalf("failed to marshal config: %v", err) + } + if err := os.WriteFile(configFile, data, 0644); err != nil { + t.Fatalf("failed to write config file: %v", err) + } + return configFile + }, + wantErr: false, + }, + { + // Test that attempting to load from a non-existent file returns an error + name: "non-existent file", + setupFunc: func(t *testing.T) string { + return "/nonexistent/path/ocm.json" + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + filePath := tt.setupFunc(t) + builder, err := GetOCMSdkConnBuilderFromFilePath(filePath) + if tt.wantErr { + if err == nil { + t.Errorf("GetOCMSdkConnBuilderFromFilePath() expected error but got none") + } + } else { + if err != nil { + t.Errorf("GetOCMSdkConnBuilderFromFilePath() unexpected error = %v", err) + } + if builder == nil { + t.Errorf("GetOCMSdkConnBuilderFromFilePath() returned nil builder") + } + } + }) + } +} + +// TestGetOCMSdkConnFromFilePath tests the GetOCMSdkConnFromFilePath function which +// reads an OCM config file and creates a fully initialized OCM SDK connection from it. +// It validates error handling for non-existent files and empty config files. +func TestGetOCMSdkConnFromFilePath(t *testing.T) { + tests := []struct { + name string + setupFunc func(t *testing.T) string + wantErr bool + }{ + { + // Test that attempting to create a connection from a non-existent file returns an error + name: "non-existent file", + setupFunc: func(t *testing.T) string { + return "/nonexistent/path/ocm.json" + }, + wantErr: true, + }, + { + // Test that an empty config file returns an error when trying to build a connection + name: "empty config file", + setupFunc: func(t *testing.T) string { + tmpDir := t.TempDir() + configFile := filepath.Join(tmpDir, "ocm.json") + if err := os.WriteFile(configFile, []byte(""), 0644); err != nil { + t.Fatalf("failed to write empty file: %v", err) + } + return configFile + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + filePath := tt.setupFunc(t) + conn, err := GetOCMSdkConnFromFilePath(filePath) + if tt.wantErr { + if err == nil { + t.Errorf("GetOCMSdkConnFromFilePath() expected error but got none") + } + } else { + if err != nil { + t.Errorf("GetOCMSdkConnFromFilePath() unexpected error = %v", err) + } + if conn != nil { + defer conn.Close() + } + } + }) + } +} + +// TestGetHiveShardWithConn tests the GetHiveShardWithConn function which retrieves +// the hive shard URL for a cluster using a provided OCM SDK connection. +// It validates that the function properly handles nil connection inputs. +func TestGetHiveShardWithConn(t *testing.T) { + tests := []struct { + name string + clusterID string + conn *sdk.Connection + wantErr bool + }{ + { + // Test that passing a nil OCM connection returns an error + name: "nil connection", + clusterID: "test-cluster-id", + conn: nil, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := GetHiveShardWithConn(tt.clusterID, tt.conn) + if tt.wantErr { + if err == nil { + t.Errorf("GetHiveShardWithConn() expected error but got none") + } + } else { + if err != nil { + t.Errorf("GetHiveShardWithConn() unexpected error = %v", err) + } + } + }) + } +} + +// TestGetHiveClusterWithConn tests the GetHiveClusterWithConn function which fetches +// the hive cluster information using separate OCM connections for the target cluster +// and hive cluster. It validates the function's ability to create temporary connections +// when nil connections are provided. +func TestGetHiveClusterWithConn(t *testing.T) { + tests := []struct { + name string + clusterID string + clusterOCM *sdk.Connection + hiveOCM *sdk.Connection + wantErr bool + }{ + { + // Test that when both connections are nil, the function attempts to create a temporary connection + // This will fail without proper OCM environment variables set + name: "both connections nil - should create temporary connection", + clusterID: "test-cluster-id", + clusterOCM: nil, + hiveOCM: nil, + wantErr: true, // will fail when trying to create connection without proper env vars + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := GetHiveClusterWithConn(tt.clusterID, tt.clusterOCM, tt.hiveOCM) + if tt.wantErr { + if err == nil { + t.Errorf("GetHiveClusterWithConn() expected error but got none") + } + } else { + if err != nil { + t.Errorf("GetHiveClusterWithConn() unexpected error = %v", err) + } + } + }) + } +} + +// TestGetOCMConfigFromEnv tests the GetOCMConfigFromEnv function which loads +// OCM configuration from environment variables and default file locations. +// It validates the function's ability to handle different config file locations +// including OCM_CONFIG env var, ~/.ocm.json, and $XDG_CONFIG_HOME/ocm/ocm.json. +func TestGetOCMConfigFromEnv(t *testing.T) { + tests := []struct { + name string + setupFunc func(t *testing.T) func() + wantErr bool + errContains string + checkConfig func(*testing.T, *ocmConfig.Config) + }{ + { + // Test loading config from a custom OCM_CONFIG environment variable path + name: "with OCM_CONFIG env var set to valid file", + setupFunc: func(t *testing.T) func() { + tmpDir := t.TempDir() + configFile := filepath.Join(tmpDir, "custom-ocm.json") + config := ocmConfig.Config{ + AccessToken: "test-token-from-env", + RefreshToken: "test-refresh", + URL: "https://api.openshift.com", + } + data, err := json.Marshal(config) + if err != nil { + t.Fatalf("failed to marshal config: %v", err) + } + if err := os.WriteFile(configFile, data, 0644); err != nil { + t.Fatalf("failed to write config file: %v", err) + } + oldVal := os.Getenv("OCM_CONFIG") + os.Setenv("OCM_CONFIG", configFile) + return func() { + os.Setenv("OCM_CONFIG", oldVal) + } + }, + wantErr: false, + checkConfig: func(t *testing.T, cfg *ocmConfig.Config) { + if cfg == nil { + t.Error("expected non-nil config") + return + } + if cfg.AccessToken != "test-token-from-env" { + t.Errorf("expected AccessToken 'test-token-from-env', got '%s'", cfg.AccessToken) + } + }, + }, + { + // Test that when no config file exists, an empty config is returned without error + name: "no config file exists - returns empty config", + setupFunc: func(t *testing.T) func() { + // Set OCM_CONFIG to a non-existent path + oldVal := os.Getenv("OCM_CONFIG") + os.Setenv("OCM_CONFIG", "/nonexistent/path/ocm.json") + return func() { + os.Setenv("OCM_CONFIG", oldVal) + } + }, + wantErr: false, + checkConfig: func(t *testing.T, cfg *ocmConfig.Config) { + if cfg == nil { + t.Error("expected non-nil empty config") + } + }, + }, + { + // Test that a malformed config file returns a parse error + name: "invalid json in config file", + setupFunc: func(t *testing.T) func() { + tmpDir := t.TempDir() + configFile := filepath.Join(tmpDir, "invalid-ocm.json") + if err := os.WriteFile(configFile, []byte("{invalid json}"), 0644); err != nil { + t.Fatalf("failed to write invalid json: %v", err) + } + oldVal := os.Getenv("OCM_CONFIG") + os.Setenv("OCM_CONFIG", configFile) + return func() { + os.Setenv("OCM_CONFIG", oldVal) + } + }, + wantErr: true, + errContains: "can't parse config file", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cleanup := tt.setupFunc(t) + defer cleanup() + + cfg, err := GetOCMConfigFromEnv() + if tt.wantErr { + if err == nil { + t.Errorf("GetOCMConfigFromEnv() expected error but got none") + } else if tt.errContains != "" && !contains(err.Error(), tt.errContains) { + t.Errorf("GetOCMConfigFromEnv() error = %v, want error containing %v", err, tt.errContains) + } + } else { + if err != nil { + t.Errorf("GetOCMConfigFromEnv() unexpected error = %v", err) + } + if tt.checkConfig != nil { + tt.checkConfig(t, cfg) + } + } + }) + } +} + +// TestCreateConnectionWithUrl tests the CreateConnectionWithUrl function which creates +// an OCM SDK connection with a specified URL. It validates URL alias handling for +// 'production', 'staging', and 'integration' environments. +// Note: Successful connection creation requires valid OCM credentials and is tested +// in integration tests or the hive-login test command. +func TestCreateConnectionWithUrl(t *testing.T) { + tests := []struct { + name string + ocmUrl string + wantErr bool + errContains string + }{ + { + // Test that an empty URL returns an error + name: "empty URL", + ocmUrl: "", + wantErr: true, + errContains: "empty OCM URL", + }, + { + // Test that an invalid alias returns an error with valid aliases listed + name: "invalid URL alias", + ocmUrl: "invalid-alias", + wantErr: true, + errContains: "invalid OCM_URL found", + }, + { + // Test that 'production' alias doesn't fail with "invalid alias" error + // Will fail with credentials error if not logged in, which is expected + name: "production alias recognized", + ocmUrl: "production", + wantErr: true, // Will fail without credentials, but not with "invalid alias" error + }, + { + // Test that 'staging' alias doesn't fail with "invalid alias" error + // Will fail with credentials error if not logged in, which is expected + name: "staging alias recognized", + ocmUrl: "staging", + wantErr: true, // Will fail without credentials, but not with "invalid alias" error + }, + { + // Test that 'integration' alias doesn't fail with "invalid alias" error + // Will fail with credentials error if not logged in, which is expected + name: "integration alias recognized", + ocmUrl: "integration", + wantErr: true, // Will fail without credentials, but not with "invalid alias" error + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + conn, err := CreateConnectionWithUrl(tt.ocmUrl) + if tt.wantErr { + if err == nil { + t.Errorf("CreateConnectionWithUrl() expected error but got none") + } else if tt.errContains != "" && !contains(err.Error(), tt.errContains) { + t.Errorf("CreateConnectionWithUrl() error = %v, want error containing %v", err, tt.errContains) + } + } else { + if err != nil { + t.Errorf("CreateConnectionWithUrl() unexpected error = %v", err) + } + if conn != nil { + defer conn.Close() + } + } + }) + } +} + +// TestGetHiveBPClientForCluster tests the GetHiveBPClientForCluster function which creates a +// backplane client connection to a hive cluster. It validates input validation and +// error handling for empty cluster IDs. +// Note: Successful connection creation requires valid cluster ID, OCM credentials, +// and accessible hive cluster. These scenarios are tested in integration tests or +// the hive-login test command. +func TestGetHiveBPClientForCluster(t *testing.T) { + tests := []struct { + name string + clusterID string + elevationReason string + hiveOCMURL string + wantErr bool + errContains string + }{ + { + // Test that an empty cluster ID returns an error + name: "empty cluster ID", + clusterID: "", + elevationReason: "", + hiveOCMURL: "", + wantErr: true, + errContains: "empty target cluster ID", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := GetHiveBPClientForCluster(tt.clusterID, client.Options{}, tt.elevationReason, tt.hiveOCMURL) + if tt.wantErr { + if err == nil { + t.Errorf("GetHiveBPClientForCluster() expected error but got none") + } else if tt.errContains != "" && !contains(err.Error(), tt.errContains) { + t.Errorf("GetHiveBPClientForCluster() error = %v, want error containing %v", err, tt.errContains) + } + } else { + if err != nil { + t.Errorf("GetHiveBPClientForCluster() unexpected error = %v", err) + } + } + }) + } +} + +// Helper function to check if a string contains a substring +func contains(s, substr string) bool { + return len(s) >= len(substr) && (s == substr || len(substr) == 0 || + (len(s) > 0 && len(substr) > 0 && stringContains(s, substr))) +} + +func stringContains(s, substr string) bool { + for i := 0; i <= len(s)-len(substr); i++ { + if s[i:i+len(substr)] == substr { + return true + } + } + return false +} From d2a0173b74611f02dfb2fe641a144169a6d263eb Mon Sep 17 00:00:00 2001 From: Matt Clark Date: Wed, 7 Jan 2026 13:36:16 -0800 Subject: [PATCH 2/5] OSD-28241: Lint updates. Test updates. --- cmd/hive/loginhivetests.go | 9 ++++----- pkg/utils/ocm.go | 3 +-- pkg/utils/ocm_test.go | 40 +++++++++++++++++++------------------- 3 files changed, 25 insertions(+), 27 deletions(-) diff --git a/cmd/hive/loginhivetests.go b/cmd/hive/loginhivetests.go index e8654e46c..a50ee8725 100644 --- a/cmd/hive/loginhivetests.go +++ b/cmd/hive/loginhivetests.go @@ -26,7 +26,6 @@ import ( type testHiveLoginOptions struct { clusterID string - output string verbose bool hiveOcmConfigPath string hiveOcmURL string @@ -37,7 +36,7 @@ const longDescription = ` This test utility attempts to exercise and validate OSDCTL's functions related to OCM and backplane client connections. -This test utiltiy can be run against an OSD/Rosa Classic target cluster. This utility +This test utility can be run against an OSD/Rosa Classic target cluster. This utility will attempt to discover the Hive cluster, and create both OCM and kube client connections, and perform basic requests for each to connection in order to validate functionality of the related OSDCTL utility functions. @@ -48,7 +47,7 @@ environment (ie integration, staging) from the hive cluster (ie production). The default OCM environment vars should be set for the target cluster. If the target cluster exists outside of the OCM 'production' environment, the user has the option to provide the production OCM config (with valid token set), -or provide the production OCM API url as a command arguement, or set the value in the osdctl +or provide the production OCM API url as a command argument, or set the value in the osdctl config yaml file (ie: "hive_ocm_url: https://api.openshift.com" or "hive_ocm_url: production" ). For testing purposes comment out 'hive_ocm_url' from osdctl's config if testing an empty value. ` @@ -73,7 +72,7 @@ func newCmdTestHiveLogin() *cobra.Command { testHiveLoginCmd.Flags().StringVar(&ops.hiveOcmConfigPath, "hive-ocm-config", "", "OCM config for hive if different than Cluster") testHiveLoginCmd.Flags().StringVar(&ops.hiveOcmURL, "hive-ocm-url", "", "OCM URL for hive, this will fallback to reading from the osdctl config value: 'hive_ocm_url' if left empty") - testHiveLoginCmd.MarkFlagRequired("cluster-id") + _ = testHiveLoginCmd.MarkFlagRequired("cluster-id") return testHiveLoginCmd } @@ -225,7 +224,7 @@ func setupHiveCluster(clusterID string, ocmClient, hiveOCM *sdk.Connection) (*v1 printDiv() hiveCluster, err := utils.GetHiveClusterWithConn(clusterID, ocmClient, hiveOCM) if err != nil { - fmt.Printf("Failed to fetch hive cluster with provided OCM conneciton, err:'%v'", err) + fmt.Printf("Failed to fetch hive cluster with provided OCM connection, err:'%v'", err) return nil, err } diff --git a/pkg/utils/ocm.go b/pkg/utils/ocm.go index b1d3b508a..1ce008477 100644 --- a/pkg/utils/ocm.go +++ b/pkg/utils/ocm.go @@ -286,7 +286,6 @@ func CreateConnection() (*sdk.Connection, error) { // Creates a connection to OCM func CreateConnectionWithUrl(OcmUrl string) (*sdk.Connection, error) { - var ocmApiUrl string = OcmUrl if len(OcmUrl) <= 0 { return nil, fmt.Errorf("CreateConnectionWithUrl provided empty OCM URL") } @@ -434,7 +433,7 @@ func GetHiveShardWithConn(clusterID string, conn *sdk.Connection) (string, error } if shard == "" { - return "", fmt.Errorf("Unable to retrieve shard for cluster %s", clusterID) + return "", fmt.Errorf("unable to retrieve shard for cluster %s", clusterID) } return shard, nil diff --git a/pkg/utils/ocm_test.go b/pkg/utils/ocm_test.go index 0b848a9d9..0bd221552 100644 --- a/pkg/utils/ocm_test.go +++ b/pkg/utils/ocm_test.go @@ -92,8 +92,8 @@ func TestGetOcmConfigFromFilePath(t *testing.T) { if err != nil { t.Fatalf("failed to marshal config: %v", err) } - if err := os.WriteFile(configFile, data, 0644); err != nil { - t.Fatalf("failed to write config file: %v", err) + if err := os.WriteFile(configFile, data, 0600); err != nil { + t.Skipf("failed to write config file (insufficient permissions?): %v", err) } return configFile }, @@ -135,8 +135,8 @@ func TestGetOcmConfigFromFilePath(t *testing.T) { setupFunc: func(t *testing.T) string { tmpDir := t.TempDir() configFile := filepath.Join(tmpDir, "ocm.json") - if err := os.WriteFile(configFile, []byte(""), 0644); err != nil { - t.Fatalf("failed to write empty file: %v", err) + if err := os.WriteFile(configFile, []byte(""), 0600); err != nil { + t.Skipf("failed to write empty file (insufficient permissions?): %v", err) } return configFile }, @@ -149,8 +149,8 @@ func TestGetOcmConfigFromFilePath(t *testing.T) { setupFunc: func(t *testing.T) string { tmpDir := t.TempDir() configFile := filepath.Join(tmpDir, "ocm.json") - if err := os.WriteFile(configFile, []byte("{invalid json}"), 0644); err != nil { - t.Fatalf("failed to write invalid json: %v", err) + if err := os.WriteFile(configFile, []byte("{invalid json}"), 0600); err != nil { + t.Skipf("failed to write invalid json (insufficient permissions?): %v", err) } return configFile }, @@ -259,8 +259,8 @@ func TestGetOCMSdkConnBuilderFromFilePath(t *testing.T) { if err != nil { t.Fatalf("failed to marshal config: %v", err) } - if err := os.WriteFile(configFile, data, 0644); err != nil { - t.Fatalf("failed to write config file: %v", err) + if err := os.WriteFile(configFile, data, 0600); err != nil { + t.Skipf("failed to write config file (insufficient permissions?): %v", err) } return configFile }, @@ -319,8 +319,8 @@ func TestGetOCMSdkConnFromFilePath(t *testing.T) { setupFunc: func(t *testing.T) string { tmpDir := t.TempDir() configFile := filepath.Join(tmpDir, "ocm.json") - if err := os.WriteFile(configFile, []byte(""), 0644); err != nil { - t.Fatalf("failed to write empty file: %v", err) + if err := os.WriteFile(configFile, []byte(""), 0600); err != nil { + t.Skipf("failed to write empty file (insufficient permissions?): %v", err) } return configFile }, @@ -449,8 +449,8 @@ func TestGetOCMConfigFromEnv(t *testing.T) { if err != nil { t.Fatalf("failed to marshal config: %v", err) } - if err := os.WriteFile(configFile, data, 0644); err != nil { - t.Fatalf("failed to write config file: %v", err) + if err := os.WriteFile(configFile, data, 0600); err != nil { + t.Skipf("failed to write config file (insufficient permissions?): %v", err) } oldVal := os.Getenv("OCM_CONFIG") os.Setenv("OCM_CONFIG", configFile) @@ -493,8 +493,8 @@ func TestGetOCMConfigFromEnv(t *testing.T) { setupFunc: func(t *testing.T) func() { tmpDir := t.TempDir() configFile := filepath.Join(tmpDir, "invalid-ocm.json") - if err := os.WriteFile(configFile, []byte("{invalid json}"), 0644); err != nil { - t.Fatalf("failed to write invalid json: %v", err) + if err := os.WriteFile(configFile, []byte("{invalid json}"), 0600); err != nil { + t.Skipf("failed to write invalid json (insufficient permissions?): %v", err) } oldVal := os.Getenv("OCM_CONFIG") os.Setenv("OCM_CONFIG", configFile) @@ -609,12 +609,12 @@ func TestCreateConnectionWithUrl(t *testing.T) { // the hive-login test command. func TestGetHiveBPClientForCluster(t *testing.T) { tests := []struct { - name string - clusterID string - elevationReason string - hiveOCMURL string - wantErr bool - errContains string + name string + clusterID string + elevationReason string + hiveOCMURL string + wantErr bool + errContains string }{ { // Test that an empty cluster ID returns an error From c89cea02c12e1babe83d588105a561d2775de632 Mon Sep 17 00:00:00 2001 From: Matt Clark Date: Wed, 7 Jan 2026 13:57:37 -0800 Subject: [PATCH 3/5] OSD-28241: Make generate-docs --- docs/README.md | 47 ++++++++++++++++++++++++++ docs/osdctl_hive.md | 1 + docs/osdctl_hive_login-tests.md | 59 +++++++++++++++++++++++++++++++++ 3 files changed, 107 insertions(+) create mode 100644 docs/osdctl_hive_login-tests.md diff --git a/docs/README.md b/docs/README.md index d441b3f3a..2070b5d44 100644 --- a/docs/README.md +++ b/docs/README.md @@ -94,6 +94,7 @@ - `list` - List cluster deployment crs - `listresources` - List all resources on a hive cluster related to a given cluster - `clustersync-failures [flags]` - List clustersync failures + - `login-tests` - Test utility to exercise OSDCTL client connections for both Target Cluster and it's Hive Cluster. - `iampermissions` - STS/WIF utilities - `diff` - Diff IAM permissions for cluster operators between two versions - `get` - Get OCP CredentialsRequests @@ -2825,6 +2826,52 @@ osdctl hive clustersync-failures [flags] --syncsets Include failing syncsets. (default true) ``` +### osdctl hive login-tests + + +This test utility attempts to exercise and validate OSDCTL's functions related to +OCM and backplane client connections. + +This test utility can be run against an OSD/Rosa Classic target cluster. This utility +will attempt to discover the Hive cluster, and create both +OCM and kube client connections, and perform basic requests for each to connection in +order to validate functionality of the related OSDCTL utility functions. + +This test utility allows for the target cluster to exist in a separate OCM +environment (ie integration, staging) from the hive cluster (ie production). + +The default OCM environment vars should be set for the target cluster. +If the target cluster exists outside of the OCM 'production' environment, the user +has the option to provide the production OCM config (with valid token set), +or provide the production OCM API url as a command argument, or set the value in the osdctl +config yaml file (ie: "hive_ocm_url: https://api.openshift.com" or "hive_ocm_url: production" ). +For testing purposes comment out 'hive_ocm_url' from osdctl's config if testing an empty value. + + +``` +osdctl hive login-tests [flags] +``` + +#### Flags + +``` + --as string Username to impersonate for the operation. User could be a regular user or a service account in a namespace. + --cluster string The name of the kubeconfig cluster to use + -C, --cluster-id string Cluster ID + --context string The name of the kubeconfig context to use + -h, --help help for login-tests + --hive-ocm-config string OCM config for hive if different than Cluster + --hive-ocm-url string OCM URL for hive, this will fallback to reading from the osdctl config value: 'hive_ocm_url' if left empty + --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'] + --request-timeout string The length of time to wait before giving up on a single server request. Non-zero values should contain a corresponding time unit (e.g. 1s, 2m, 3h). A value of zero means don't timeout requests. (default "0") + -s, --server string The address and port of the Kubernetes API server + --skip-aws-proxy-check aws_proxy Don't use the configured aws_proxy value + -S, --skip-version-check skip checking to see if this is the most recent release + --verbose Verbose output +``` + ### osdctl iampermissions STS/WIF utilities diff --git a/docs/osdctl_hive.md b/docs/osdctl_hive.md index 4c5eed0ce..0151d88af 100644 --- a/docs/osdctl_hive.md +++ b/docs/osdctl_hive.md @@ -28,4 +28,5 @@ hive related utilities * [osdctl](osdctl.md) - OSD CLI * [osdctl hive clusterdeployment](osdctl_hive_clusterdeployment.md) - cluster deployment related utilities * [osdctl hive clustersync-failures](osdctl_hive_clustersync-failures.md) - List clustersync failures +* [osdctl hive login-tests](osdctl_hive_login-tests.md) - Test utility to exercise OSDCTL client connections for both Target Cluster and it's Hive Cluster. diff --git a/docs/osdctl_hive_login-tests.md b/docs/osdctl_hive_login-tests.md new file mode 100644 index 000000000..c736f65ac --- /dev/null +++ b/docs/osdctl_hive_login-tests.md @@ -0,0 +1,59 @@ +## osdctl hive login-tests + +Test utility to exercise OSDCTL client connections for both Target Cluster and it's Hive Cluster. + +### Synopsis + + +This test utility attempts to exercise and validate OSDCTL's functions related to +OCM and backplane client connections. + +This test utility can be run against an OSD/Rosa Classic target cluster. This utility +will attempt to discover the Hive cluster, and create both +OCM and kube client connections, and perform basic requests for each to connection in +order to validate functionality of the related OSDCTL utility functions. + +This test utility allows for the target cluster to exist in a separate OCM +environment (ie integration, staging) from the hive cluster (ie production). + +The default OCM environment vars should be set for the target cluster. +If the target cluster exists outside of the OCM 'production' environment, the user +has the option to provide the production OCM config (with valid token set), +or provide the production OCM API url as a command argument, or set the value in the osdctl +config yaml file (ie: "hive_ocm_url: https://api.openshift.com" or "hive_ocm_url: production" ). +For testing purposes comment out 'hive_ocm_url' from osdctl's config if testing an empty value. + + +``` +osdctl hive login-tests [flags] +``` + +### Options + +``` + -C, --cluster-id string Cluster ID + -h, --help help for login-tests + --hive-ocm-config string OCM config for hive if different than Cluster + --hive-ocm-url string OCM URL for hive, this will fallback to reading from the osdctl config value: 'hive_ocm_url' if left empty + --verbose Verbose output +``` + +### Options inherited from parent commands + +``` + --as string Username to impersonate for the operation. User could be a regular user or a service account in a namespace. + --cluster string The name of the kubeconfig cluster to use + --context string The name of the kubeconfig context to use + --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'] + --request-timeout string The length of time to wait before giving up on a single server request. Non-zero values should contain a corresponding time unit (e.g. 1s, 2m, 3h). A value of zero means don't timeout requests. (default "0") + -s, --server string The address and port of the Kubernetes API server + --skip-aws-proxy-check aws_proxy Don't use the configured aws_proxy value + -S, --skip-version-check skip checking to see if this is the most recent release +``` + +### SEE ALSO + +* [osdctl hive](osdctl_hive.md) - hive related utilities + From 7e5296fc30e2f72fa9c4cc1c12d35f8ab5359299 Mon Sep 17 00:00:00 2001 From: Matt Clark Date: Thu, 8 Jan 2026 17:11:46 -0800 Subject: [PATCH 4/5] OSD-28241: Remove manual integration test command --- cmd/hive/cmd.go | 1 - cmd/hive/loginhivetests.go | 518 ------------------------------------- 2 files changed, 519 deletions(-) delete mode 100644 cmd/hive/loginhivetests.go diff --git a/cmd/hive/cmd.go b/cmd/hive/cmd.go index 0de8ae1be..e1a758fb2 100644 --- a/cmd/hive/cmd.go +++ b/cmd/hive/cmd.go @@ -20,7 +20,6 @@ func NewCmdHive(streams genericclioptions.IOStreams, client client.Client) *cobr hiveCmd.AddCommand(NewCmdClusterSyncFailures(streams, client)) hiveCmd.AddCommand(cd.NewCmdClusterDeployment(streams, client)) - hiveCmd.AddCommand(newCmdTestHiveLogin()) return hiveCmd } diff --git a/cmd/hive/loginhivetests.go b/cmd/hive/loginhivetests.go deleted file mode 100644 index a50ee8725..000000000 --- a/cmd/hive/loginhivetests.go +++ /dev/null @@ -1,518 +0,0 @@ -package hive - -import ( - "context" - "fmt" - "os" - "strings" - - ocmConfig "github.com/openshift-online/ocm-common/pkg/ocm/config" - sdk "github.com/openshift-online/ocm-sdk-go" - v1 "github.com/openshift-online/ocm-sdk-go/clustersmgmt/v1" - configv1 "github.com/openshift/api/config/v1" - hivev1 "github.com/openshift/hive/apis/hive/v1" - common "github.com/openshift/osdctl/cmd/common" - k8s "github.com/openshift/osdctl/pkg/k8s" - "github.com/openshift/osdctl/pkg/printer" - "github.com/openshift/osdctl/pkg/utils" - "github.com/spf13/cobra" - "github.com/spf13/viper" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - cmdutil "k8s.io/kubectl/pkg/cmd/util" - "sigs.k8s.io/controller-runtime/pkg/client" -) - -// testHiveLoginOptions defines the struct for running ocm and backplane client tests - -type testHiveLoginOptions struct { - clusterID string - verbose bool - hiveOcmConfigPath string - hiveOcmURL string - reason string -} - -const longDescription = ` -This test utility attempts to exercise and validate OSDCTL's functions related to -OCM and backplane client connections. - -This test utility can be run against an OSD/Rosa Classic target cluster. This utility -will attempt to discover the Hive cluster, and create both -OCM and kube client connections, and perform basic requests for each to connection in -order to validate functionality of the related OSDCTL utility functions. - -This test utility allows for the target cluster to exist in a separate OCM -environment (ie integration, staging) from the hive cluster (ie production). - -The default OCM environment vars should be set for the target cluster. -If the target cluster exists outside of the OCM 'production' environment, the user -has the option to provide the production OCM config (with valid token set), -or provide the production OCM API url as a command argument, or set the value in the osdctl -config yaml file (ie: "hive_ocm_url: https://api.openshift.com" or "hive_ocm_url: production" ). -For testing purposes comment out 'hive_ocm_url' from osdctl's config if testing an empty value. -` - -// Defines command to run through series of tests to validate existing and new and legacy -// ocm + backplane client functions -func newCmdTestHiveLogin() *cobra.Command { - ops := newtestHiveLoginOptions() - testHiveLoginCmd := &cobra.Command{ - Use: "login-tests", - Short: "Test utility to exercise OSDCTL client connections for both Target Cluster and it's Hive Cluster.", - Long: longDescription, - Args: cobra.NoArgs, - DisableAutoGenTag: true, - Run: func(cmd *cobra.Command, args []string) { - cmdutil.CheckErr(ops.run()) - }, - } - - testHiveLoginCmd.Flags().BoolVarP(&ops.verbose, "verbose", "", false, "Verbose output") - testHiveLoginCmd.Flags().StringVarP(&ops.clusterID, "cluster-id", "C", "", "Cluster ID") - testHiveLoginCmd.Flags().StringVar(&ops.hiveOcmConfigPath, "hive-ocm-config", "", "OCM config for hive if different than Cluster") - testHiveLoginCmd.Flags().StringVar(&ops.hiveOcmURL, "hive-ocm-url", "", "OCM URL for hive, this will fallback to reading from the osdctl config value: 'hive_ocm_url' if left empty") - - _ = testHiveLoginCmd.MarkFlagRequired("cluster-id") - return testHiveLoginCmd -} - -func newtestHiveLoginOptions() *testHiveLoginOptions { - return &testHiveLoginOptions{} -} - -func printDiv() { - fmt.Printf("\n---------------------------------------------------------------\n\n") -} - -func dumpClusterOperators(kubeClient client.Client) error { - var cos configv1.ClusterOperatorList - if err := kubeClient.List(context.TODO(), &cos, &client.ListOptions{}); err != nil { - fmt.Fprintf(os.Stderr, "error fetching cluster operators, err:'%s'\n", err) - return err - } - table := printer.NewTablePrinter(os.Stderr, 20, 1, 1, ' ') - table.AddRow([]string{"NAME", "AVAILABLE", "PROGRESSING", "DEGRADED"}) - for _, co := range cos.Items { - var available configv1.ConditionStatus - var progressing configv1.ConditionStatus - var degraded configv1.ConditionStatus - for _, cond := range co.Status.Conditions { - switch cond.Type { - case configv1.OperatorAvailable: - available = cond.Status - case configv1.OperatorProgressing: - progressing = cond.Status - case configv1.OperatorDegraded: - degraded = cond.Status - } - } - table.AddRow([]string{co.Name, string(available), string(progressing), string(degraded)}) - } - table.Flush() - return nil -} - -func getClusterDeployment(hiveKubeClient client.Client, clusterID string) (cd *hivev1.ClusterDeployment, err error) { - var cds hivev1.ClusterDeploymentList - if err := hiveKubeClient.List(context.TODO(), &cds, &client.ListOptions{}); err != nil { - fmt.Printf("err fetching cluster deployments, err:'%v'", err) - return nil, err - } - for _, cdeploy := range cds.Items { - if strings.Contains(cdeploy.Namespace, clusterID) { - fmt.Printf("Got Hive ClusterDeployment for target cluster:'%s'\n", cdeploy.Name) - return &cdeploy, nil - } - } - return nil, fmt.Errorf("clusterDeployment for cluster:'%s' not found", clusterID) -} - -// setupOCMConnection creates an OCM client and fetches the target cluster -func setupOCMConnection(clusterID string) (*sdk.Connection, *v1.Cluster, string, error) { - printDiv() - fmt.Printf("Building ocm client using legacy functions and env vars...\n") - ocmClient, err := utils.CreateConnection() - if err != nil { - return nil, nil, "", err - } - - cluster, err := utils.GetClusterAnyStatus(ocmClient, clusterID) - if err != nil { - fmt.Printf("Failed to fetch cluster '%s' from OCM, err:'%v'", clusterID, err) - return nil, nil, "", err - } - - actualClusterID := cluster.ID() - if clusterID != actualClusterID { - fmt.Printf("Using internal ID:'%s' for provided cluster:'%s'\n", actualClusterID, clusterID) - } - - fmt.Printf("Fetched cluster from OCM:'%s'\n", actualClusterID) - printDiv() - - return ocmClient, cluster, actualClusterID, nil -} - -// setupHiveOCMConfig builds the Hive OCM configuration from provided options -func setupHiveOCMConfig(hiveOcmConfigPath, hiveOcmURL string) (*ocmConfig.Config, error) { - var hiveOCMCfg *ocmConfig.Config - var err error - - // Test building OCM config from a provided file path - if len(hiveOcmConfigPath) > 0 { - fmt.Printf("Attempting to build OCM config from provided file path...\n") - hiveOCMCfg, err = utils.GetOcmConfigFromFilePath(hiveOcmConfigPath) - if err != nil { - fmt.Printf("Failed to build Hive OCM config from file path:'%s'\n", hiveOcmConfigPath) - return nil, err - } - } - - // Test replacing just the OCM URL for an already built config - if len(hiveOcmURL) > 0 { - if hiveOCMCfg == nil { - fmt.Printf("Attempting to build OCM config...\n") - hiveOCMCfg, err = utils.GetOCMConfigFromEnv() - if err != nil { - fmt.Printf("Failed to build OCM config from legacy function\n") - return nil, err - } - } - hiveOCMCfg.URL = hiveOcmURL - } - - return hiveOCMCfg, nil -} - -// setupHiveOCMConnection creates an OCM connection for Hive using the provided config -func setupHiveOCMConnection(hiveOCMCfg *ocmConfig.Config, hiveOcmConfigPath string) (*sdk.Connection, error) { - if hiveOCMCfg == nil { - return nil, nil - } - - hiveBuilder, err := utils.GetOCMSdkConnBuilderFromConfig(hiveOCMCfg) - if err != nil { - fmt.Printf("Failed to create sdk connection builder from hive ocm cfg, err:'%s'\n", err) - return nil, err - } - - hiveOCM, err := hiveBuilder.Build() - if err != nil { - fmt.Printf("Error connecting to OCM env using config at: '%s'\nErr:%v", hiveOcmConfigPath, err) - return nil, err - } - - fmt.Printf("Built OCM config and connection from provided config inputs\n") - printDiv() - - return hiveOCM, nil -} - -// setupHiveCluster fetches the Hive cluster for the target cluster -func setupHiveCluster(clusterID string, ocmClient, hiveOCM *sdk.Connection) (*v1.Cluster, error) { - // No OCM related config provided, test the legacy path - if hiveOCM == nil { - fmt.Println("---- No hive config provided. Using same OCM connections for target cluster and hive ----") - hiveOCM = ocmClient - _, err := utils.GetHiveCluster(clusterID) - if err != nil { - fmt.Printf("Failed to fetch hive cluster from OCM with legacy function, err:'%v'", err) - return nil, err - } - } - - printDiv() - hiveCluster, err := utils.GetHiveClusterWithConn(clusterID, ocmClient, hiveOCM) - if err != nil { - fmt.Printf("Failed to fetch hive cluster with provided OCM connection, err:'%v'", err) - return nil, err - } - - fmt.Printf("Got Hive Cluster from OCM:'%s'\n", hiveCluster.ID()) - printDiv() - - return hiveCluster, nil -} - -// testK8sNew tests creating a Kube client using k8s.New() -func testK8sNew(clusterID string, cluster *v1.Cluster) error { - fmt.Println("Attempting to create and test Kube Client with k8s.New()...") - kubeClient, err := k8s.New(clusterID, client.Options{}) - if err != nil { - return fmt.Errorf("failed to login to cluster:'%s', err: %w", clusterID, err) - } - fmt.Printf("Created client connection to target cluster:'%s', '%s'\n", cluster.ID(), cluster.Name()) - - if err := dumpClusterOperators(kubeClient); err != nil { - return err - } - - fmt.Println("Create and test Kube Client with k8s.New() - PASS") - printDiv() - return nil -} - -// testK8sNewWithConn tests creating a Kube client using k8s.NewWithConn() -func testK8sNewWithConn(hiveCluster *v1.Cluster, hiveOCM *sdk.Connection) error { - fmt.Println("Attempting to create and test Kube Client with k8s.NewWithConn()...") - hiveClient, err := k8s.NewWithConn(hiveCluster.ID(), client.Options{}, hiveOCM) - if err != nil { - return fmt.Errorf("failed to login to hive cluster:'%s', err %w", hiveCluster.ID(), err) - } - fmt.Printf("Created client connection to HIVE cluster:'%s', '%s'\n", hiveCluster.ID(), hiveCluster.Name()) - - if err := dumpClusterOperators(hiveClient); err != nil { - return err - } - - fmt.Println("Create and test Kube Client with k8s.NewWithConn() - PASS") - printDiv() - return nil -} - -// testK8sNewAsBackplaneClusterAdmin tests creating an elevated Kube client using k8s.NewAsBackplaneClusterAdminWithConn() -func testK8sNewAsBackplaneClusterAdmin(hiveCluster *v1.Cluster, hiveOCM *sdk.Connection, clusterID, reason string) error { - fmt.Println("Attempting to create and test Kube Client with k8s.NewAsBackplaneClusterAdminWithConn()...") - hiveAdminClient, err := k8s.NewAsBackplaneClusterAdminWithConn(hiveCluster.ID(), client.Options{}, hiveOCM, reason) - if err != nil { - return fmt.Errorf("failed to login to hive cluster:'%s', err %w", hiveCluster.ID(), err) - } - fmt.Printf("Created 'ClusterAdmin' client connection to HIVE cluster:'%s', '%s'\n", hiveCluster.ID(), hiveCluster.Name()) - - clusterDep, err := getClusterDeployment(hiveAdminClient, clusterID) - if err != nil { - return err - } - - fmt.Printf("Fetched ClusterDeployment:'%s/%s' for cluster:'%s' from HIVE using elevated client\n", clusterDep.Namespace, clusterDep.Name, clusterID) - fmt.Println("Create and test Kube Client withk8s.NewAsBackplaneClusterAdminWithConn() - PASS") - printDiv() - return nil -} - -// testGetKubeConfigAndClient tests GetKubeConfigAndClient() without admin elevation -func testGetKubeConfigAndClient(clusterID string) error { - fmt.Printf("Testing non-backplane-admin client, clientSet GetKubeConfigAndClient() for cluster:'%s'\n", clusterID) - kubeCli, _, kubeClientSet, err := common.GetKubeConfigAndClient(clusterID) - if err != nil { - return err - } - - if err := dumpClusterOperators(kubeCli); err != nil { - return err - } - - nsList, err := kubeClientSet.CoreV1().Namespaces().List(context.TODO(), metav1.ListOptions{}) - if err != nil { - fmt.Printf("ClientSet list namespaces failed, err:'%v'\n", err) - return err - } - - fmt.Printf("Got '%d' namespaces\n", len(nsList.Items)) - fmt.Println("non-bpadmin Create and test Kube Client, Clientset with GetKubeConfigAndClient() - PASS") - printDiv() - return nil -} - -// testGetKubeConfigAndClientWithConn tests GetKubeConfigAndClientWithConn() without admin elevation -func testGetKubeConfigAndClientWithConn(clusterID string, ocmClient *sdk.Connection) error { - fmt.Printf("Testing non-backplane-admin client, clientset GetKubeConfigAndClientWithConn for cluster:'%s'\n", clusterID) - kubeCli, _, kubeClientSet, err := common.GetKubeConfigAndClientWithConn(clusterID, ocmClient) - if err != nil { - return err - } - - if err := dumpClusterOperators(kubeCli); err != nil { - return err - } - - nsList, err := kubeClientSet.CoreV1().Namespaces().List(context.TODO(), metav1.ListOptions{}) - if err != nil { - fmt.Printf("ClientSet list namespaces failed, err:'%v'\n", err) - return err - } - - fmt.Printf("Got '%d' namespaces\n", len(nsList.Items)) - fmt.Println("non-bpadmin Create and test Kube Client, Clientset with GetKubeConfigAndClientWithConn() - PASS") - printDiv() - return nil -} - -// testGetKubeConfigAndClientAdmin tests GetKubeConfigAndClient() with admin elevation -func testGetKubeConfigAndClientAdmin(clusterID, reason string) error { - fmt.Printf("Testing backplane-admin client, clientset GetKubeConfigAndClient() for cluster:'%s'\n", clusterID) - kubeCli, _, kubeClientSet, err := common.GetKubeConfigAndClient(clusterID, reason) - if err != nil { - return err - } - - if err := dumpClusterOperators(kubeCli); err != nil { - return err - } - - openshiftMonitoringNamespace := "openshift-monitoring" - podList, err := kubeClientSet.CoreV1().Pods(openshiftMonitoringNamespace).List(context.TODO(), metav1.ListOptions{}) - if err != nil { - fmt.Printf("ClientSet list 'openshift-monitoring' pods failed, err:'%v'\n", err) - return err - } - - fmt.Printf("Got %d pods in namespace:'%s' :\n", len(podList.Items), openshiftMonitoringNamespace) - for i, pod := range podList.Items { - fmt.Printf("Got pod (%d/%d): '%s/%s' \n", i, len(podList.Items), pod.Namespace, pod.Name) - } - - fmt.Println("bpadmin Create and test Kube Client, Clientset with GetKubeConfigAndClient() - PASS") - printDiv() - return nil -} - -// testGetKubeConfigAndClientWithConnAdmin tests GetKubeConfigAndClientWithConn() with admin elevation -func testGetKubeConfigAndClientWithConnAdmin(clusterID string, ocmClient *sdk.Connection, reason string) error { - fmt.Printf("Testing backplane-admin GetKubeConfigAndClientWithConn() for cluster:'%s'\n", clusterID) - kubeCli, _, kubeClientSet, err := common.GetKubeConfigAndClientWithConn(clusterID, ocmClient, reason) - if err != nil { - return err - } - - if err := dumpClusterOperators(kubeCli); err != nil { - return err - } - - podList, err := kubeClientSet.CoreV1().Pods("openshift-monitoring").List(context.TODO(), metav1.ListOptions{}) - if err != nil { - fmt.Printf("ClientSet list 'openshift-monitoring' pods failed, err:'%v'\n", err) - return err - } - - fmt.Printf("Got %d pods\n", len(podList.Items)) - for i, pod := range podList.Items { - fmt.Printf("Got pod (%d/%d): '%s/%s' \n", i, len(podList.Items), pod.Namespace, pod.Name) - } - - fmt.Println("bpadmin Create and test Kube Client, Clientset with GetKubeConfigAndClientWithConn() - PASS") - printDiv() - return nil -} - -// testGetHiveBPWithoutElevation tests GetHiveBPClientForCluster() without elevation -func testGetHiveBPWithoutElevation(clusterID, hiveOcmURL string) error { - fmt.Printf("Testing GetHiveBPClientForCluster() hive backplane connection w/o elevation\n") - hiveBP, err := utils.GetHiveBPClientForCluster(clusterID, client.Options{}, "", hiveOcmURL) - if err != nil { - return err - } - - if err := dumpClusterOperators(hiveBP); err != nil { - return err - } - - fmt.Println("Create and test GetHiveBPClientForCluster() without elevation reason - PASS") - printDiv() - return nil -} - -// testGetHiveBPWithElevation tests GetHiveBPClientForCluster() with elevation -func testGetHiveBPWithElevation(clusterID, reason, hiveOcmURL string) error { - fmt.Printf("Testing GetHiveBPClientForCluster() hive backplane connection with elevation\n") - hiveBP, err := utils.GetHiveBPClientForCluster(clusterID, client.Options{}, reason, hiveOcmURL) - if err != nil { - return err - } - - if err := dumpClusterOperators(hiveBP); err != nil { - return err - } - - clusterDep, err := getClusterDeployment(hiveBP, clusterID) - if err != nil { - return err - } - - fmt.Printf("Fetched ClusterDeployment:'%s/%s' for cluster:'%s' from HIVE using elevated client\n", clusterDep.Namespace, clusterDep.Name, clusterID) - fmt.Println("Create and test GetHiveBPClientForCluster() with elevation reason - PASS") - printDiv() - return nil -} - -func (o *testHiveLoginOptions) run() error { - // Initialize Hive OCM URL from args or config - if len(o.hiveOcmURL) > 0 { - fmt.Printf("Using Hive OCM URL set in args:'%s'\n", o.hiveOcmURL) - } else { - o.hiveOcmURL = viper.GetString("hive_ocm_url") - if len(o.hiveOcmURL) > 0 { - fmt.Printf("Got Hive OCM URL from viper vars:'%s'\n", o.hiveOcmURL) - } else { - fmt.Printf("No 'separate' Hive OCM URL set, using defaults set for target cluster.\n") - } - } - - o.reason = "Testing osdctl clients with cluster admin" - - // Setup: Create OCM connection and fetch target cluster - ocmClient, cluster, clusterID, err := setupOCMConnection(o.clusterID) - if err != nil { - return err - } - defer ocmClient.Close() - o.clusterID = clusterID - - // Setup: Build Hive OCM config if provided - hiveOCMCfg, err := setupHiveOCMConfig(o.hiveOcmConfigPath, o.hiveOcmURL) - if err != nil { - return err - } - - // Setup: Create Hive OCM connection if config was built - hiveOCM, err := setupHiveOCMConnection(hiveOCMCfg, o.hiveOcmConfigPath) - if err != nil { - return err - } - - // Setup: Fetch Hive cluster - hiveCluster, err := setupHiveCluster(clusterID, ocmClient, hiveOCM) - if err != nil { - return err - } - - // Run individual tests - if err := testK8sNew(clusterID, cluster); err != nil { - return err - } - - if err := testK8sNewWithConn(hiveCluster, hiveOCM); err != nil { - return err - } - - if err := testK8sNewAsBackplaneClusterAdmin(hiveCluster, hiveOCM, clusterID, o.reason); err != nil { - return err - } - - if err := testGetKubeConfigAndClient(clusterID); err != nil { - return err - } - - if err := testGetKubeConfigAndClientWithConn(clusterID, ocmClient); err != nil { - return err - } - - if err := testGetKubeConfigAndClientAdmin(clusterID, o.reason); err != nil { - return err - } - - if err := testGetKubeConfigAndClientWithConnAdmin(clusterID, ocmClient, o.reason); err != nil { - return err - } - - if err := testGetHiveBPWithoutElevation(clusterID, o.hiveOcmURL); err != nil { - return err - } - - if err := testGetHiveBPWithElevation(clusterID, "Testing hive client backplane connections", o.hiveOcmURL); err != nil { - return err - } - - fmt.Println("All tests Passed") - return nil -} From cd5c619099338d71bfc54d3034d421903c6f3f0a Mon Sep 17 00:00:00 2001 From: Matt Clark Date: Thu, 8 Jan 2026 17:42:23 -0800 Subject: [PATCH 5/5] OSD-28241: Regenerate docs with removal of test command --- docs/README.md | 47 --------------------------------------------- docs/osdctl_hive.md | 1 - 2 files changed, 48 deletions(-) diff --git a/docs/README.md b/docs/README.md index 2070b5d44..d441b3f3a 100644 --- a/docs/README.md +++ b/docs/README.md @@ -94,7 +94,6 @@ - `list` - List cluster deployment crs - `listresources` - List all resources on a hive cluster related to a given cluster - `clustersync-failures [flags]` - List clustersync failures - - `login-tests` - Test utility to exercise OSDCTL client connections for both Target Cluster and it's Hive Cluster. - `iampermissions` - STS/WIF utilities - `diff` - Diff IAM permissions for cluster operators between two versions - `get` - Get OCP CredentialsRequests @@ -2826,52 +2825,6 @@ osdctl hive clustersync-failures [flags] --syncsets Include failing syncsets. (default true) ``` -### osdctl hive login-tests - - -This test utility attempts to exercise and validate OSDCTL's functions related to -OCM and backplane client connections. - -This test utility can be run against an OSD/Rosa Classic target cluster. This utility -will attempt to discover the Hive cluster, and create both -OCM and kube client connections, and perform basic requests for each to connection in -order to validate functionality of the related OSDCTL utility functions. - -This test utility allows for the target cluster to exist in a separate OCM -environment (ie integration, staging) from the hive cluster (ie production). - -The default OCM environment vars should be set for the target cluster. -If the target cluster exists outside of the OCM 'production' environment, the user -has the option to provide the production OCM config (with valid token set), -or provide the production OCM API url as a command argument, or set the value in the osdctl -config yaml file (ie: "hive_ocm_url: https://api.openshift.com" or "hive_ocm_url: production" ). -For testing purposes comment out 'hive_ocm_url' from osdctl's config if testing an empty value. - - -``` -osdctl hive login-tests [flags] -``` - -#### Flags - -``` - --as string Username to impersonate for the operation. User could be a regular user or a service account in a namespace. - --cluster string The name of the kubeconfig cluster to use - -C, --cluster-id string Cluster ID - --context string The name of the kubeconfig context to use - -h, --help help for login-tests - --hive-ocm-config string OCM config for hive if different than Cluster - --hive-ocm-url string OCM URL for hive, this will fallback to reading from the osdctl config value: 'hive_ocm_url' if left empty - --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'] - --request-timeout string The length of time to wait before giving up on a single server request. Non-zero values should contain a corresponding time unit (e.g. 1s, 2m, 3h). A value of zero means don't timeout requests. (default "0") - -s, --server string The address and port of the Kubernetes API server - --skip-aws-proxy-check aws_proxy Don't use the configured aws_proxy value - -S, --skip-version-check skip checking to see if this is the most recent release - --verbose Verbose output -``` - ### osdctl iampermissions STS/WIF utilities diff --git a/docs/osdctl_hive.md b/docs/osdctl_hive.md index 0151d88af..4c5eed0ce 100644 --- a/docs/osdctl_hive.md +++ b/docs/osdctl_hive.md @@ -28,5 +28,4 @@ hive related utilities * [osdctl](osdctl.md) - OSD CLI * [osdctl hive clusterdeployment](osdctl_hive_clusterdeployment.md) - cluster deployment related utilities * [osdctl hive clustersync-failures](osdctl_hive_clustersync-failures.md) - List clustersync failures -* [osdctl hive login-tests](osdctl_hive_login-tests.md) - Test utility to exercise OSDCTL client connections for both Target Cluster and it's Hive Cluster.