Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ Support specifying the CA cert or adding verify=false into clouds.yaml #148

Merged
merged 6 commits into from
Apr 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions docs/troubleshooting.md
Original file line number Diff line number Diff line change
@@ -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: <PEM_ENCODED_CA_CERT>
clouds.yaml: <ENCODED_CLOUDS_YAML>
kind: Secret
metadata:
labels:
clusterctl.cluster.x-k8s.io/move: "true"
name: "openstack"
namespace: cluster
```
61 changes: 51 additions & 10 deletions internal/controller/openstacknodeimagerelease_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@ package controller

import (
"context"
"crypto/tls"
"crypto/x509"
"fmt"
"net/http"
"time"

"github.com/gophercloud/gophercloud/v2"
Expand Down Expand Up @@ -53,6 +56,7 @@ const (
defaultCloudName = "openstack"
cloudNameSecretKey = "cloudName"
cloudsSecretKey = "clouds.yaml"
caSecretKey = "cacert"
waitForImageBecomeActive = 30 * time.Second
)

Expand Down Expand Up @@ -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,
Expand All @@ -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")
Expand Down Expand Up @@ -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
Expand All @@ -304,31 +338,38 @@ 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]
if !ok {
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) {
Expand Down
70 changes: 66 additions & 4 deletions internal/controller/openstacknodeimagerelease_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand All @@ -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)
Expand All @@ -99,14 +158,15 @@ 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)

assert.Error(t, err)
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) {
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
Loading