From 1b61b9681c15e8cc8b7f3f993bb3c4cf1d1d266c Mon Sep 17 00:00:00 2001 From: Matt Clark Date: Mon, 9 Mar 2026 23:05:45 -0700 Subject: [PATCH] SREP-3938: Add --hive-ocm-url flag to network verify-egress for multi-env OCM support --- cmd/network/verification.go | 75 +++++++++++++++++-- cmd/network/verification_test.go | 104 +++++++++++++++++++++++++++ docs/README.md | 1 + docs/osdctl_network_verify-egress.md | 8 +++ 4 files changed, 181 insertions(+), 7 deletions(-) diff --git a/cmd/network/verification.go b/cmd/network/verification.go index f2f005db6..d0d8074f8 100644 --- a/cmd/network/verification.go +++ b/cmd/network/verification.go @@ -105,6 +105,8 @@ type EgressVerification struct { Namespace string // SkipServiceLog disables automatic service log prompting on verification failures SkipServiceLog bool + // hiveOcmUrl is the OCM environment URL for Hive operations (Classic clusters only) + hiveOcmUrl string } func NewCmdValidateEgress() *cobra.Command { @@ -165,6 +167,13 @@ func NewCmdValidateEgress() *cobra.Command { # Run network verification without sending service logs on failure osdctl network verify-egress --cluster-id my-rosa-cluster --skip-service-log + # For a classic cluster that needs automatic proxy CA-bundle retrieval, + # target staging OCM while querying Hive from production + # (Note: --hive-ocm-url only applies to Hive-backed CA-bundle lookup) + export OCM_URL=staging + ocm login + osdctl network verify-egress --cluster-id my-staging-cluster --hive-ocm-url production + # (Not recommended) Run against a specific VPC, without specifying cluster-id osdctl network verify-egress --subnet-id subnet-abcdefg123 --security-group sg-abcdefgh123 --region us-east-1`, @@ -195,6 +204,7 @@ func NewCmdValidateEgress() *cobra.Command { validateEgressCmd.Flags().StringVar(&e.KubeConfig, "kubeconfig", "", "(optional) path to kubeconfig file for pod mode (uses default kubeconfig if not specified)") validateEgressCmd.Flags().StringVar(&e.Namespace, "namespace", "openshift-network-diagnostics", "(optional) Kubernetes namespace to run verification pods in") validateEgressCmd.Flags().BoolVar(&e.SkipServiceLog, "skip-service-log", false, "(optional) disable automatic service log sending when verification fails") + validateEgressCmd.Flags().StringVar(&e.hiveOcmUrl, "hive-ocm-url", "", "(optional) OCM environment URL for hive operations. Aliases: 'production', 'staging', 'integration'. If not specified, uses the same OCM environment as the target cluster.") return validateEgressCmd } @@ -434,6 +444,54 @@ func selectMostRecentCaBundleConfigMap(configMaps []corev1.ConfigMap) (string, e return "", fmt.Errorf("%s data not found in the ConfigMap %s", caBundleConfigMapKey, foundCM.Name) } +// getHiveClient resolves the Hive cluster and builds a Kubernetes client for it. +// It handles both single-environment and multi-environment OCM modes. +func (e *EgressVerification) getHiveClient(ctx context.Context, scheme *runtime.Scheme) (*cmv1.Cluster, client.Client, error) { + if e.hiveOcmUrl != "" { + // Multi-environment path - enables staging/integration testing + targetOCM, err := utils.CreateConnection() + if err != nil { + return nil, nil, fmt.Errorf("failed to create target OCM connection: %w", err) + } + defer targetOCM.Close() + + hiveOCM, err := utils.CreateConnectionWithUrl(e.hiveOcmUrl) + if err != nil { + return nil, nil, fmt.Errorf("failed to create hive OCM connection with URL '%s': %w", e.hiveOcmUrl, err) + } + defer hiveOCM.Close() + + e.log.Debug(ctx, "using multi-environment OCM: target cluster OCM and hive OCM URL '%s'", e.hiveOcmUrl) + + hive, err := utils.GetHiveClusterWithConn(e.cluster.ID(), targetOCM, hiveOCM) + if err != nil { + return nil, nil, fmt.Errorf("failed to get hive cluster (OCM URL:'%s'): %w", e.hiveOcmUrl, err) + } + + e.log.Debug(ctx, "assembling K8s client for %s (%s)", hive.ID(), hive.Name()) + hc, err := k8s.NewWithConn(hive.ID(), client.Options{Scheme: scheme}, hiveOCM) + if err != nil { + return nil, nil, fmt.Errorf("failed to create hive k8s client (OCM URL:'%s'): %w", e.hiveOcmUrl, err) + } + + return hive, hc, nil + } + + // Single-environment path - backward compatible + hive, err := utils.GetHiveCluster(e.cluster.ID()) + if err != nil { + return nil, nil, err + } + + e.log.Debug(ctx, "assembling K8s client for %s (%s)", hive.ID(), hive.Name()) + hc, err := k8s.New(hive.ID(), client.Options{Scheme: scheme}) + if err != nil { + return nil, nil, err + } + + return hive, hc, nil +} + func (e *EgressVerification) getCaBundleFromHive(ctx context.Context) (string, error) { scheme := runtime.NewScheme() if err := corev1.AddToScheme(scheme); err != nil { @@ -444,13 +502,7 @@ func (e *EgressVerification) getCaBundleFromHive(ctx context.Context) (string, e return "", err } - hive, err := utils.GetHiveCluster(e.cluster.ID()) - if err != nil { - return "", err - } - - e.log.Debug(ctx, "assembling K8s client for %s (%s)", hive.ID(), hive.Name()) - hc, err := k8s.New(hive.ID(), client.Options{Scheme: scheme}) + _, hc, err := e.getHiveClient(ctx, scheme) if err != nil { return "", err } @@ -624,6 +676,15 @@ func (e *EgressVerification) validateInput() error { } } + // Validate and resolve --hive-ocm-url if provided + if e.hiveOcmUrl != "" { + resolvedUrl, err := utils.ValidateAndResolveOcmUrl(e.hiveOcmUrl) + if err != nil { + return fmt.Errorf("invalid --hive-ocm-url: %w", err) + } + e.hiveOcmUrl = resolvedUrl + } + return nil } diff --git a/cmd/network/verification_test.go b/cmd/network/verification_test.go index a9a72dcf3..da4ba3768 100644 --- a/cmd/network/verification_test.go +++ b/cmd/network/verification_test.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "reflect" + "strings" "testing" "time" @@ -19,6 +20,7 @@ import ( "github.com/openshift/osd-network-verifier/pkg/probes/curl" onv "github.com/openshift/osd-network-verifier/pkg/verifier" "github.com/openshift/osdctl/cmd/servicelog" + "github.com/openshift/osdctl/pkg/utils" "github.com/stretchr/testify/assert" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -73,6 +75,46 @@ func TestEgressVerification_ValidateInput(t *testing.T) { }, wantError: false, }, + { + name: "valid_hive_ocm_url_production", + ev: &EgressVerification{ + SubnetIds: []string{"subnet-123"}, + hiveOcmUrl: "production", + }, + wantError: false, + }, + { + name: "valid_hive_ocm_url_staging", + ev: &EgressVerification{ + SubnetIds: []string{"subnet-123"}, + hiveOcmUrl: "staging", + }, + wantError: false, + }, + { + name: "valid_hive_ocm_url_integration", + ev: &EgressVerification{ + SubnetIds: []string{"subnet-123"}, + hiveOcmUrl: "integration", + }, + wantError: false, + }, + { + name: "valid_hive_ocm_url_full_url", + ev: &EgressVerification{ + SubnetIds: []string{"subnet-123"}, + hiveOcmUrl: "https://api.openshift.com", + }, + wantError: false, + }, + { + name: "invalid_hive_ocm_url", + ev: &EgressVerification{ + SubnetIds: []string{"subnet-123"}, + hiveOcmUrl: "invalid-environment", + }, + wantError: true, + }, } for _, tt := range tests { @@ -87,6 +129,68 @@ func TestEgressVerification_ValidateInput(t *testing.T) { } } +// TestHiveOcmUrlValidation tests the validation of --hive-ocm-url flag +func TestHiveOcmUrlValidation(t *testing.T) { + tests := []struct { + name string + hiveOcmUrl string + expectErr bool + errContains string + }{ + { + name: "Valid hive-ocm-url (production)", + hiveOcmUrl: "production", + expectErr: false, + }, + { + name: "Valid hive-ocm-url (staging)", + hiveOcmUrl: "staging", + expectErr: false, + }, + { + name: "Valid hive-ocm-url (integration)", + hiveOcmUrl: "integration", + expectErr: false, + }, + { + name: "Valid hive-ocm-url (full URL)", + hiveOcmUrl: "https://api.openshift.com", + expectErr: false, + }, + { + name: "Invalid hive-ocm-url", + hiveOcmUrl: "invalid-environment", + expectErr: true, + errContains: "invalid OCM_URL", + }, + { + name: "Empty hive-ocm-url", + hiveOcmUrl: "", + expectErr: true, + errContains: "empty OCM URL", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // This simulates the validation that occurs in the validateInput() method + _, err := utils.ValidateAndResolveOcmUrl(tt.hiveOcmUrl) + + if tt.expectErr { + if err == nil { + t.Errorf("Expected error containing '%s', but got nil", tt.errContains) + } else if !strings.Contains(err.Error(), tt.errContains) { + t.Errorf("Expected error containing '%s', but got: %v", tt.errContains, err) + } + } else { + if err != nil { + t.Errorf("Expected no error, but got: %v", err) + } + } + }) + } +} + func TestEgressVerification_GetPlatform(t *testing.T) { tests := []struct { name string diff --git a/docs/README.md b/docs/README.md index 458cac5e2..c23e39b14 100644 --- a/docs/README.md +++ b/docs/README.md @@ -3589,6 +3589,7 @@ osdctl network verify-egress [flags] --egress-timeout duration (optional) timeout for individual egress verification requests (default 5s) --gcp-project-id string (optional) the GCP project ID to run verification for -h, --help help for verify-egress + --hive-ocm-url string (optional) OCM environment URL for hive operations. Aliases: 'production', 'staging', 'integration'. If not specified, uses the same OCM environment as the target cluster. --insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure --kubeconfig string (optional) path to kubeconfig file for pod mode (uses default kubeconfig if not specified) --namespace string (optional) Kubernetes namespace to run verification pods in (default "openshift-network-diagnostics") diff --git a/docs/osdctl_network_verify-egress.md b/docs/osdctl_network_verify-egress.md index 23dd92254..50f5f97fb 100644 --- a/docs/osdctl_network_verify-egress.md +++ b/docs/osdctl_network_verify-egress.md @@ -64,6 +64,13 @@ osdctl network verify-egress [flags] # Run network verification without sending service logs on failure osdctl network verify-egress --cluster-id my-rosa-cluster --skip-service-log + # For a classic cluster that needs automatic proxy CA-bundle retrieval, + # target staging OCM while querying Hive from production + # (Note: --hive-ocm-url only applies to Hive-backed CA-bundle lookup) + export OCM_URL=staging + ocm login + osdctl network verify-egress --cluster-id my-staging-cluster --hive-ocm-url production + # (Not recommended) Run against a specific VPC, without specifying cluster-id osdctl network verify-egress --subnet-id subnet-abcdefg123 --security-group sg-abcdefgh123 --region us-east-1 @@ -80,6 +87,7 @@ osdctl network verify-egress [flags] --egress-timeout duration (optional) timeout for individual egress verification requests (default 5s) --gcp-project-id string (optional) the GCP project ID to run verification for -h, --help help for verify-egress + --hive-ocm-url string (optional) OCM environment URL for hive operations. Aliases: 'production', 'staging', 'integration'. If not specified, uses the same OCM environment as the target cluster. --kubeconfig string (optional) path to kubeconfig file for pod mode (uses default kubeconfig if not specified) --namespace string (optional) Kubernetes namespace to run verification pods in (default "openshift-network-diagnostics") --no-tls (optional) if provided, ignore all ssl certificate validations on client-side.