diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md new file mode 100644 index 00000000..2ee0b05e --- /dev/null +++ b/docs/troubleshooting.md @@ -0,0 +1,29 @@ +# Troubleshooting + +This guide explains general info on how to debug issues if a cluster creation fails. + +## providerClient authentication err + +If you are using https, and when you encounter issues like: + +``` +kubectl logs -n cspo-system -l control-plane=controller-manager +... +[manager] 2024-04-15T15:20:07Z DEBUG events Post "https://10.0.3.15/identity/v3/auth/tokens": tls: failed to verify certificate: x509: certificate signed by unknown authority {"type": "Warning", "object": {"kind":"OpenStackNodeImageRelease","namespace":"cluster","name":"openstack-ferrol-1-27-ubuntu-capi-image-v1.27.8-v2","uid":"93d2c1c8-5a19-45f8-9f93-8e8bd5227ebf","apiVersion":"infrastructure.clusterstack.x-k8s.io/v1alpha1","resourceVersion":"3773"}, "reason": "OpenStackProviderClientNotSet"} +... +``` + +you must specify the CA certificate in your secret, which contains the access data to the OpenStack instance, then secret should look similar to this example: + + ```bash +apiVersion: v1 +data: + cacert: + clouds.yaml: +kind: Secret +metadata: + labels: + clusterctl.cluster.x-k8s.io/move: "true" + name: "openstack" + namespace: cluster + ``` diff --git a/internal/controller/openstacknodeimagerelease_controller.go b/internal/controller/openstacknodeimagerelease_controller.go index 9ac398d7..359f172c 100644 --- a/internal/controller/openstacknodeimagerelease_controller.go +++ b/internal/controller/openstacknodeimagerelease_controller.go @@ -19,7 +19,10 @@ package controller import ( "context" + "crypto/tls" + "crypto/x509" "fmt" + "net/http" "time" "github.com/gophercloud/gophercloud/v2" @@ -53,6 +56,7 @@ const ( defaultCloudName = "openstack" cloudNameSecretKey = "cloudName" cloudsSecretKey = "clouds.yaml" + caSecretKey = "cacert" waitForImageBecomeActive = 30 * time.Second ) @@ -97,7 +101,7 @@ func (r *OpenStackNodeImageReleaseReconciler) Reconcile(ctx context.Context, req }() // Get OpenStack cloud config from sercet - cloud, err := r.getCloudFromSecret(ctx, openstacknodeimagerelease.Namespace, openstacknodeimagerelease.Spec.IdentityRef.Name) + cloud, caCert, err := r.getCloudFromSecret(ctx, openstacknodeimagerelease.Namespace, openstacknodeimagerelease.Spec.IdentityRef.Name) if err != nil { if apierrors.IsNotFound(err) { conditions.MarkFalse(openstacknodeimagerelease, @@ -122,9 +126,39 @@ func (r *OpenStackNodeImageReleaseReconciler) Reconcile(ctx context.Context, req conditions.MarkTrue(openstacknodeimagerelease, apiv1alpha1.CloudAvailableCondition) + clientOpts := new(clientconfig.ClientOpts) + + if cloud.AuthInfo != nil { + clientOpts.AuthInfo = cloud.AuthInfo + clientOpts.AuthType = cloud.AuthType + clientOpts.RegionName = cloud.RegionName + clientOpts.EndpointType = cloud.EndpointType + } + opts, _ := clientconfig.AuthOptions(clientOpts) + opts.AllowReauth = true + // Create an OpenStack provider client - opts := &clientconfig.ClientOpts{AuthInfo: cloud.AuthInfo} - providerClient, err := clientconfig.AuthenticatedClient(ctx, opts) + providerClient, _ := openstack.NewClient(opts.IdentityEndpoint) + + config := &tls.Config{ + MinVersion: tls.VersionTLS12, + } + + if cloud.Verify != nil { + config.InsecureSkipVerify = !*cloud.Verify + } + + if caCert != nil { + config.RootCAs = x509.NewCertPool() + ok := config.RootCAs.AppendCertsFromPEM(caCert) + if !ok { + // If no certificates were successfully parsed, set RootCAs to nil + config.RootCAs = nil + } + } + + providerClient.HTTPClient.Transport = &http.Transport{Proxy: http.ProxyFromEnvironment, TLSClientConfig: config} + err = openstack.Authenticate(ctx, providerClient, *opts) if err != nil { record.Warnf(openstacknodeimagerelease, "OpenStackProviderClientNotSet", err.Error()) logger.Error(err, "failed to create a provider client") @@ -293,7 +327,7 @@ func (r *OpenStackNodeImageReleaseReconciler) Reconcile(ctx context.Context, req return ctrl.Result{}, nil } -func (r *OpenStackNodeImageReleaseReconciler) getCloudFromSecret(ctx context.Context, secretNamespace, secretName string) (clientconfig.Cloud, error) { +func (r *OpenStackNodeImageReleaseReconciler) getCloudFromSecret(ctx context.Context, secretNamespace, secretName string) (clientconfig.Cloud, []byte, error) { var clouds clientconfig.Clouds emptyCloud := clientconfig.Cloud{} var cloudName string @@ -304,7 +338,7 @@ func (r *OpenStackNodeImageReleaseReconciler) getCloudFromSecret(ctx context.Con Name: secretName, }, secret) if err != nil { - return emptyCloud, fmt.Errorf("failed to get secret %s in namespace %s: %w", secretName, secretNamespace, err) + return emptyCloud, nil, fmt.Errorf("failed to get secret %s in namespace %s: %w", secretName, secretNamespace, err) } content, ok := secret.Data[cloudNameSecretKey] @@ -312,23 +346,30 @@ func (r *OpenStackNodeImageReleaseReconciler) getCloudFromSecret(ctx context.Con cloudName = defaultCloudName } else { if err := yaml.Unmarshal(content, &cloudName); err != nil { - return emptyCloud, fmt.Errorf("failed to unmarshal cloudName stored in secret %s: %w", secretName, err) + return emptyCloud, nil, fmt.Errorf("failed to unmarshal cloudName stored in secret %s: %w", secretName, err) } } content, ok = secret.Data[cloudsSecretKey] if !ok { - return emptyCloud, fmt.Errorf("OpenStack credentials secret %s did not contain key %s", secretName, cloudsSecretKey) + return emptyCloud, nil, fmt.Errorf("OpenStack credentials secret %s did not contain key %s", secretName, cloudsSecretKey) } if err = yaml.Unmarshal(content, &clouds); err != nil { - return emptyCloud, fmt.Errorf("failed to unmarshal clouds credentials stored in secret %s: %w", secretName, err) + return emptyCloud, nil, fmt.Errorf("failed to unmarshal clouds credentials stored in secret %s: %w", secretName, err) } cloud, ok := clouds.Clouds[cloudName] if !ok { - return emptyCloud, fmt.Errorf("failed to find cloud %s in %s", cloudName, cloudsSecretKey) + return emptyCloud, nil, fmt.Errorf("failed to find cloud %s in %s", cloudName, cloudsSecretKey) } - return cloud, nil + + // get caCert + caCert, ok := secret.Data[caSecretKey] + if !ok { + return cloud, nil, nil + } + + return cloud, caCert, nil } func getImageID(ctx context.Context, imagesClient *gophercloud.ServiceClient, imageCreateOps *apiv1alpha1.CreateOpts) (string, error) { diff --git a/internal/controller/openstacknodeimagerelease_controller_test.go b/internal/controller/openstacknodeimagerelease_controller_test.go index a2022669..8695fca8 100644 --- a/internal/controller/openstacknodeimagerelease_controller_test.go +++ b/internal/controller/openstacknodeimagerelease_controller_test.go @@ -68,7 +68,7 @@ clouds: Client: client, } - cloud, err := r.getCloudFromSecret(context.TODO(), secretNamespace, secretName) + cloud, caCert, err := r.getCloudFromSecret(context.TODO(), secretNamespace, secretName) expectedCloud := clientconfig.Cloud{ AuthInfo: &clientconfig.AuthInfo{ @@ -83,6 +83,65 @@ clouds: } assert.NoError(t, err) assert.Equal(t, expectedCloud, cloud) + assert.Equal(t, []byte(nil), caCert) + + err = client.Delete(context.TODO(), secret) + assert.NoError(t, err) +} + +func TestGetCloudFromSecretWithCaCert(t *testing.T) { + client := fake.NewClientBuilder().Build() + + secretName := "test-secret" + secretNamespace := "test-namespace" + cloudsYAML := ` +clouds: + openstack: + auth: + username: test_user + password: test_password + project_name: test_project + project_id: test_project_id + auth_url: test_auth_url + domain_name: test_domain + region_name: test_region +` + expectedcaCert := []byte("test-ca-cert") + + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: secretName, + Namespace: secretNamespace, + }, + Data: map[string][]byte{ + cloudsSecretKey: []byte(cloudsYAML), + caSecretKey: expectedcaCert, + }, + Type: corev1.SecretTypeOpaque, + } + err := client.Create(context.TODO(), secret) + assert.NoError(t, err) + + r := &OpenStackNodeImageReleaseReconciler{ + Client: client, + } + + cloud, caCert, err := r.getCloudFromSecret(context.TODO(), secretNamespace, secretName) + + expectedCloud := clientconfig.Cloud{ + AuthInfo: &clientconfig.AuthInfo{ + Username: "test_user", + Password: "test_password", + ProjectName: "test_project", + ProjectID: "test_project_id", + AuthURL: "test_auth_url", + DomainName: "test_domain", + }, + RegionName: "test_region", + } + assert.NoError(t, err) + assert.Equal(t, expectedCloud, cloud) + assert.Equal(t, caCert, expectedcaCert) err = client.Delete(context.TODO(), secret) assert.NoError(t, err) @@ -99,7 +158,7 @@ func TestGetCloudFromSecretNotFound(t *testing.T) { secretNamespace := "nonexistent-namespace" expectedError := "secrets \"nonexistent-secret\" not found" - cloud, err := r.getCloudFromSecret(context.TODO(), secretNamespace, secretName) + cloud, caCert, err := r.getCloudFromSecret(context.TODO(), secretNamespace, secretName) expectedErrorMessage := fmt.Sprintf("failed to get secret %s in namespace %s: %v", secretName, secretNamespace, expectedError) @@ -107,6 +166,7 @@ func TestGetCloudFromSecretNotFound(t *testing.T) { assert.True(t, apierrors.IsNotFound(err)) assert.Equal(t, clientconfig.Cloud{}, cloud) assert.EqualError(t, err, expectedErrorMessage) + assert.Equal(t, []byte(nil), caCert) } func TestGetCloudFromSecretMissingCloudsSecretKey(t *testing.T) { @@ -131,11 +191,12 @@ func TestGetCloudFromSecretMissingCloudsSecretKey(t *testing.T) { err := client.Create(context.TODO(), secret) assert.NoError(t, err) - cloud, err := r.getCloudFromSecret(context.TODO(), secretNamespace, secretName) + cloud, caCert, err := r.getCloudFromSecret(context.TODO(), secretNamespace, secretName) assert.Error(t, err) assert.EqualError(t, err, fmt.Sprintf("OpenStack credentials secret %s did not contain key %s", secretName, cloudsSecretKey)) assert.Equal(t, clientconfig.Cloud{}, cloud) + assert.Equal(t, []byte(nil), caCert) err = client.Delete(context.TODO(), secret) assert.NoError(t, err) @@ -178,11 +239,12 @@ clouds: err := client.Create(context.TODO(), secret) assert.NoError(t, err) - cloud, err := r.getCloudFromSecret(context.TODO(), secretNamespace, secretName) + cloud, caCert, err := r.getCloudFromSecret(context.TODO(), secretNamespace, secretName) assert.Error(t, err) assert.EqualError(t, err, fmt.Sprintf("failed to find cloud %s in %s", cloudName, cloudsSecretKey)) assert.Equal(t, clientconfig.Cloud{}, cloud) + assert.Equal(t, []byte(nil), caCert) err = client.Delete(context.TODO(), secret) assert.NoError(t, err)