From e99ff60c865323163aa0e8ebb5a06ad0440a7eb1 Mon Sep 17 00:00:00 2001 From: eshulman2 Date: Mon, 23 Feb 2026 11:28:50 +0200 Subject: [PATCH] openstack: Disable LoadBalancer in cloud config when Octavia is unavailable When Octavia (load-balancer) endpoint is not available in the OpenStack service catalog, explicitly set [LoadBalancer] enabled = false in the cloud provider config. This prevents the Cloud Controller Manager from crashing on startup due to an upstream change in cloud-provider-openstack that changed error handling from klog.Errorf to klog.Fatalf when the LoadBalancer client cannot be created. The fix uses the same pattern already established in the cluster destroy code (pkg/destroy/openstack/openstack.go) to detect Octavia availability by checking for gophercloud.ErrEndpointNotFound. Fixes: https://issues.redhat.com/browse/OCPBUGS-64842 Co-Authored-By: Claude Opus 4.5 --- .../openstack/cloudproviderconfig.go | 34 +++++++++++++++++-- .../openstack/cloudproviderconfig_test.go | 2 +- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/pkg/asset/manifests/openstack/cloudproviderconfig.go b/pkg/asset/manifests/openstack/cloudproviderconfig.go index 43c8a66391e..74cf765ebce 100644 --- a/pkg/asset/manifests/openstack/cloudproviderconfig.go +++ b/pkg/asset/manifests/openstack/cloudproviderconfig.go @@ -2,6 +2,7 @@ package openstack import ( "context" + "errors" "os" "strconv" "strings" @@ -25,6 +26,27 @@ type Error struct { func (e Error) Error() string { return e.msg + ": " + e.err.Error() } func (e Error) Unwrap() error { return e.err } +// isOctaviaAvailable checks if the Octavia (load-balancer) endpoint exists +// in the OpenStack service catalog. +func isOctaviaAvailable(ctx context.Context, clientOpts *clientconfig.ClientOpts) bool { + if clientOpts == nil { + // If no client options provided, assume Octavia is available + // to maintain backward compatibility + return true + } + _, err := openstackdefaults.NewServiceClient(ctx, "load-balancer", clientOpts) + if err != nil { + var gerr *gophercloud.ErrEndpointNotFound + if errors.As(err, &gerr) { + return false + } + // For other errors, assume Octavia might be available + // to avoid incorrectly disabling it + return true + } + return true +} + // CloudProviderConfigSecret generates the cloud provider config for the OpenStack // platform, that will be stored in the system secret. // TODO: I think this is crud for the legacy cloud-provider and is no longer needed. Burn it with fire? @@ -78,7 +100,7 @@ func CloudProviderConfigSecret(cloud *clientconfig.Cloud) ([]byte, error) { return []byte(res.String()), nil } -func generateCloudProviderConfig(ctx context.Context, networkClient *gophercloud.ServiceClient, cloudConfig *clientconfig.Cloud, installConfig types.InstallConfig) (cloudProviderConfigData, cloudProviderConfigCABundleData string, err error) { +func generateCloudProviderConfig(ctx context.Context, networkClient *gophercloud.ServiceClient, cloudConfig *clientconfig.Cloud, clientOpts *clientconfig.ClientOpts, installConfig types.InstallConfig) (cloudProviderConfigData, cloudProviderConfigCABundleData string, err error) { cloudProviderConfigData = `[Global] secret-name = openstack-credentials secret-namespace = kube-system @@ -96,7 +118,13 @@ secret-namespace = kube-system cloudProviderConfigCABundleData = string(caFile) } - if installConfig.OpenStack.ExternalNetwork != "" { + switch { + case !isOctaviaAvailable(ctx, clientOpts): + // Explicitly disable LoadBalancer when Octavia is not available + // to prevent CCM from crashing on startup. + // See: https://issues.redhat.com/browse/OCPBUGS-64842 + cloudProviderConfigData += "\n[LoadBalancer]\nenabled = false\n" + case installConfig.OpenStack.ExternalNetwork != "": networkName := installConfig.OpenStack.ExternalNetwork // Yes, we use a name in install-config.yaml :/ networkID, err := networkutils.IDFromName(ctx, networkClient, networkName) if err != nil { @@ -123,5 +151,5 @@ func GenerateCloudProviderConfig(ctx context.Context, installConfig types.Instal return "", "", Error{err, "failed to create a network client"} } - return generateCloudProviderConfig(ctx, networkClient, session.CloudConfig, installConfig) + return generateCloudProviderConfig(ctx, networkClient, session.CloudConfig, session.ClientOpts, installConfig) } diff --git a/pkg/asset/manifests/openstack/cloudproviderconfig_test.go b/pkg/asset/manifests/openstack/cloudproviderconfig_test.go index 39bca7fd8d1..ba2ee6ce550 100644 --- a/pkg/asset/manifests/openstack/cloudproviderconfig_test.go +++ b/pkg/asset/manifests/openstack/cloudproviderconfig_test.go @@ -130,7 +130,7 @@ region = my_region for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { - actualConfig, _, err := generateCloudProviderConfig(context.Background(), nil, &cloud, *tc.installConfig) + actualConfig, _, err := generateCloudProviderConfig(context.Background(), nil, &cloud, nil, *tc.installConfig) assert.NoError(t, err, "unexpected error when generating cloud provider config") assert.Equal(t, tc.expectedConfig, actualConfig, "unexpected cloud provider config") })