diff --git a/cmd/main.go b/cmd/main.go index 35c8e2c2a..7fad4930a 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -28,6 +28,7 @@ import ( clientgoscheme "k8s.io/client-go/kubernetes/scheme" _ "k8s.io/client-go/plugin/pkg/client/auth" capz "sigs.k8s.io/cluster-api-provider-azure/api/v1beta1" + capo "sigs.k8s.io/cluster-api-provider-openstack/api/v1beta1" capv "sigs.k8s.io/cluster-api-provider-vsphere/apis/v1beta1" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/healthz" @@ -57,6 +58,7 @@ func init() { utilruntime.Must(sveltosv1beta1.AddToScheme(scheme)) utilruntime.Must(capz.AddToScheme(scheme)) utilruntime.Must(capv.AddToScheme(scheme)) + utilruntime.Must(capo.AddToScheme(scheme)) // +kubebuilder:scaffold:scheme } diff --git a/config/dev/openstack-credentials.yaml b/config/dev/openstack-credentials.yaml index 5664e9539..ebeb70297 100644 --- a/config/dev/openstack-credentials.yaml +++ b/config/dev/openstack-credentials.yaml @@ -10,14 +10,12 @@ stringData: openstack: auth: auth_url: ${OS_AUTH_URL} - username: ${OS_USERNAME} - password: ${OS_PASSWORD} - project_id: ${OS_PROJECT_ID} - project_name: ${OS_PROJECT_NAME} - user_domain_name: ${OS_USER_DOMAIN_NAME} + application_credential_id: ${OS_APPLICATION_CREDENTIAL_ID} + application_credential_secret: ${OS_APPLICATION_CREDENTIAL_SECRET} region_name: ${OS_REGION_NAME} interface: ${OS_INTERFACE} identity_api_version: ${OS_IDENTITY_API_VERSION} + auth_type: ${OS_AUTH_TYPE} --- apiVersion: hmc.mirantis.com/v1alpha1 kind: Credential diff --git a/go.mod b/go.mod index 8f9768d16..1659074bb 100644 --- a/go.mod +++ b/go.mod @@ -30,6 +30,7 @@ require ( sigs.k8s.io/cluster-api v1.9.1 sigs.k8s.io/cluster-api-operator v0.14.0 sigs.k8s.io/cluster-api-provider-azure v1.17.2 + sigs.k8s.io/cluster-api-provider-openstack v0.11.3 sigs.k8s.io/cluster-api-provider-vsphere v1.12.0 sigs.k8s.io/controller-runtime v0.19.3 sigs.k8s.io/yaml v1.4.0 diff --git a/go.sum b/go.sum index a37a76c16..67ef8e1e3 100644 --- a/go.sum +++ b/go.sum @@ -684,6 +684,8 @@ sigs.k8s.io/cluster-api-operator v0.14.0 h1:0QgO6+XGrNNJnNHKBwvQD5v6w+EaH3Z0RL1n sigs.k8s.io/cluster-api-operator v0.14.0/go.mod h1:euShpVN6HyxXas28HkrYxhCPVDW1UV6ljbRBAeCxp8Y= sigs.k8s.io/cluster-api-provider-azure v1.17.2 h1:uS9ggE/bryI0hiOWHBa56nYHkWmsPZW3bzYeAddL4vM= sigs.k8s.io/cluster-api-provider-azure v1.17.2/go.mod h1:ohdf0TYutOn5vKsXpNVeZUVfUSNIwNhfF6wDjbiqPI0= +sigs.k8s.io/cluster-api-provider-openstack v0.11.3 h1:ZJ3G+m11bgaD227EuFjuFsFC95MRzJm9JbDIte0xwII= +sigs.k8s.io/cluster-api-provider-openstack v0.11.3/go.mod h1:0rH6yksLcuwWK/SoSoCOJi4A0kOSL3qrA+qvDVZ9NjU= sigs.k8s.io/cluster-api-provider-vsphere v1.12.0 h1:9ze+1JSdLAGiLklsnORvj/vs2XpR9jyVmkT0Dwo1nuc= sigs.k8s.io/cluster-api-provider-vsphere v1.12.0/go.mod h1:2y9fsZQ3qjT1kL6IXiOUVcyV0n8DLBQGvyPnId9xRzk= sigs.k8s.io/controller-runtime v0.19.3 h1:XO2GvC9OPftRst6xWCpTgBZO04S2cbp0Qqkj8bX1sPw= diff --git a/internal/controller/managedcluster_controller.go b/internal/controller/managedcluster_controller.go index 4f24d9240..8728f0dff 100644 --- a/internal/controller/managedcluster_controller.go +++ b/internal/controller/managedcluster_controller.go @@ -354,7 +354,7 @@ func (r *ManagedClusterReconciler) updateCluster(ctx context.Context, mc *hmc.Ma return ctrl.Result{RequeueAfter: DefaultRequeueInterval}, nil } - if err := r.reconcileCredentialPropagation(ctx, mc); err != nil { + if err := r.reconcileCredentialPropagation(ctx, mc, cred); err != nil { l.Error(err, "failed to reconcile credentials propagation") return ctrl.Result{}, err } @@ -694,7 +694,7 @@ func (r *ManagedClusterReconciler) objectsAvailable(ctx context.Context, namespa return len(itemsList.Items) != 0, nil } -func (r *ManagedClusterReconciler) reconcileCredentialPropagation(ctx context.Context, managedCluster *hmc.ManagedCluster) error { +func (r *ManagedClusterReconciler) reconcileCredentialPropagation(ctx context.Context, managedCluster *hmc.ManagedCluster, credential *hmc.Credential) error { l := ctrl.LoggerFrom(ctx) l.Info("Reconciling CCM credentials propagation") @@ -763,7 +763,7 @@ func (r *ManagedClusterReconciler) reconcileCredentialPropagation(ctx context.Co }) case "openstack": l.Info("OpenStack creds propagation start") - if err := credspropagation.PropagateOpenStackSecrets(ctx, propnCfg); err != nil { + if err := credspropagation.PropagateOpenStackSecrets(ctx, propnCfg, credential); err != nil { errMsg := fmt.Sprintf("failed to create OpenStack CCM credentials: %s", err) apimeta.SetStatusCondition(managedCluster.GetConditions(), metav1.Condition{ Type: hmc.CredentialsPropagatedCondition, diff --git a/internal/credspropagation/azure.go b/internal/credspropagation/azure.go index 05eb6973a..329b2c107 100644 --- a/internal/credspropagation/azure.go +++ b/internal/credspropagation/azure.go @@ -20,7 +20,6 @@ import ( "fmt" corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" capz "sigs.k8s.io/cluster-api-provider-azure/api/v1beta1" "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -92,5 +91,5 @@ func generateAzureCCMSecret(azureCluster *capz.AzureCluster, azureClIdty *capz.A "cloud-config": azureJSON, } - return makeSecret("azure-cloud-provider", metav1.NamespaceSystem, secretData), nil + return makeSecret("azure-cloud-provider", secretData), nil } diff --git a/internal/credspropagation/common.go b/internal/credspropagation/common.go index 01529f480..1f7d53680 100644 --- a/internal/credspropagation/common.go +++ b/internal/credspropagation/common.go @@ -53,11 +53,11 @@ func applyCCMConfigs(ctx context.Context, kubeconfSecret *corev1.Secret, objects return nil } -func makeSecret(name, namespace string, data map[string][]byte) *corev1.Secret { +func makeSecret(name string, data map[string][]byte) *corev1.Secret { s := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: name, - Namespace: namespace, + Namespace: metav1.NamespaceSystem, }, Data: data, } @@ -65,11 +65,11 @@ func makeSecret(name, namespace string, data map[string][]byte) *corev1.Secret { return s } -func makeConfigMap(name, namespace string, data map[string]string) *corev1.ConfigMap { +func makeConfigMap(name string, data map[string]string) *corev1.ConfigMap { c := &corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ Name: name, - Namespace: namespace, + Namespace: metav1.NamespaceSystem, }, Data: data, } diff --git a/internal/credspropagation/openstack.go b/internal/credspropagation/openstack.go index bdb0a2f95..589556717 100644 --- a/internal/credspropagation/openstack.go +++ b/internal/credspropagation/openstack.go @@ -1,76 +1,259 @@ -// Copyright 2024 -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - package credspropagation import ( + "bytes" "context" + "errors" "fmt" + texttemplate "text/template" - hmc "github.com/Mirantis/hmc/api/v1alpha1" + "gopkg.in/yaml.v3" corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + capo "sigs.k8s.io/cluster-api-provider-openstack/api/v1beta1" "sigs.k8s.io/controller-runtime/pkg/client" + + hmc "github.com/Mirantis/hmc/api/v1alpha1" ) -func PropagateOpenStackSecrets(ctx context.Context, cfg *PropagationCfg) error { - openstackManagedCluster := &hmc.ManagedCluster{} - if err := cfg.Client.Get(ctx, client.ObjectKey{ +// Fields needed to populate the cloud.conf template +type CloudConfFields struct { + AuthURL string + ApplicationCredentialID string + ApplicationCredentialName string + ApplicationCredentialSecret string + Username string + Password string + ProjectID string + ProjectName string + UserDomainName string + ProjectDomainName string + RegionName string + Interface string + FloatingNetworkID string + PublicNetworkName string +} + +type CloudsYaml struct { + Clouds map[string]Cloud `yaml:"clouds"` +} + +type Cloud struct { + Auth Auth `yaml:"auth"` + RegionName string `yaml:"region_name"` + Interface string `yaml:"interface"` +} + +type Auth struct { + AuthURL string `yaml:"auth_url"` + ApplicationCredentialID string `yaml:"application_credential_id"` + ApplicationCredentialName string `yaml:"application_credential_name"` + ApplicationCredentialSecret string `yaml:"application_credential_secret"` + Username string `yaml:"username"` + Password string `yaml:"password"` + ProjectID string `yaml:"project_id"` + ProjectName string `yaml:"project_name"` + UserDomainName string `yaml:"user_domain_name"` + ProjectDomainName string `yaml:"project_domain_name"` +} + +// PropagateOpenStackSecrets propagates OpenStack secrets +func PropagateOpenStackSecrets(ctx context.Context, cfg *PropagationCfg, credential *hmc.Credential) error { + // Fetch the OpenStackCluster resource + openstackCluster := &capo.OpenStackCluster{} + err := cfg.Client.Get(ctx, client.ObjectKey{ Name: cfg.ManagedCluster.Name, Namespace: cfg.ManagedCluster.Namespace, - }, openstackManagedCluster); err != nil { - return fmt.Errorf("failed to get ManagedCluster %s: %w", cfg.ManagedCluster.Name, err) + }, openstackCluster) + if err != nil { + if apierrors.IsNotFound(err) { + return fmt.Errorf("OpenStackCluster %s/%s not found: %w", + cfg.ManagedCluster.Namespace, cfg.ManagedCluster.Name, err) + } + return fmt.Errorf("unable to get OpenStackCluster %s/%s: %w", + cfg.ManagedCluster.Namespace, cfg.ManagedCluster.Name, err) } - openstackCredential := &hmc.Credential{} - if err := cfg.Client.Get(ctx, client.ObjectKey{ - Name: openstackManagedCluster.Spec.Credential, - Namespace: openstackManagedCluster.Namespace, - }, openstackCredential); err != nil { - return fmt.Errorf("failed to get OpenStackCredential %s: %w", cfg.ManagedCluster.Spec.Credential, err) + // Extract cloudName from the OpenStackCluster resource + cloudName := openstackCluster.Spec.IdentityRef.CloudName + if cloudName == "" { + return errors.New("cloudName is not specified in OpenStackCluster.spec.identityRef.cloudName") + } + + // Fetch the OpenStack secret + openstackSecret, err := fetchOpenStackSecret(ctx, cfg, credential) + if err != nil { + return fmt.Errorf("failed to fetch OpenStack secret: %w", err) + } + + // Generate the CCM secret using the extracted cloudName + ccmSecret, err := generateOpenStackCCMSecret(ctx, cfg, openstackSecret, cloudName) + if err != nil { + return fmt.Errorf("failed to generate CCM secret: %w", err) + } + + // Apply the CCM configuration + if err := applyCCMConfigs(ctx, cfg.KubeconfSecret, ccmSecret); err != nil { + return fmt.Errorf("failed to apply CCM configuration: %w", err) + } + + return nil +} + +// Fetch the OpenStack secret +func fetchOpenStackSecret(ctx context.Context, cfg *PropagationCfg, credential *hmc.Credential) (*corev1.Secret, error) { + if credential.Spec.IdentityRef == nil { + return nil, errors.New("credential.Spec.IdentityRef is nil") } - // Fetch the secret containing OpenStack credentials openstackSecret := &corev1.Secret{} - openstackSecretName := openstackCredential.Spec.IdentityRef.Name - openstackSecretNamespace := openstackCredential.Spec.IdentityRef.Namespace if err := cfg.Client.Get(ctx, client.ObjectKey{ - Name: openstackSecretName, - Namespace: openstackSecretNamespace, + Name: credential.Spec.IdentityRef.Name, + Namespace: credential.Spec.IdentityRef.Namespace, }, openstackSecret); err != nil { - return fmt.Errorf("failed to get OpenStack secret %s: %w", openstackSecretName, err) + return nil, fmt.Errorf("failed to get OpenStack secret %s/%s: %w", + credential.Spec.IdentityRef.Namespace, credential.Spec.IdentityRef.Name, err) } + return openstackSecret, nil +} + +// Generate the CCM secret from the OpenStack secret +func generateOpenStackCCMSecret(ctx context.Context, cfg *PropagationCfg, openstackSecret *corev1.Secret, cloudName string) (*corev1.Secret, error) { + const cloudConfTemplate = ` +[Global] +auth-url="{{ .AuthURL }}" +{{- if .ApplicationCredentialID }} +application-credential-id="{{ .ApplicationCredentialID }}" +{{- end }} +{{- if .ApplicationCredentialName }} +application-credential-name="{{ .ApplicationCredentialName }}" +{{- end }} +{{- if .ApplicationCredentialSecret }} +application-credential-secret="{{ .ApplicationCredentialSecret }}" +{{- end }} +{{- if and (not .ApplicationCredentialID) (not .ApplicationCredentialSecret) }} +username="{{ .Username }}" +password="{{ .Password }}" +{{- end }} +region="{{ .RegionName }}" +interface="{{ .Interface }}" + +[LoadBalancer] +{{- if .FloatingNetworkID }} +floating-network-id="{{ .FloatingNetworkID }}" +{{- end }} - // Generate CCM secret - ccmSecret, err := generateOpenStackCCMSecret(openstackSecret) +[Network] +{{- if .PublicNetworkName }} +public-network-name="{{ .PublicNetworkName }}" +{{- end }} +` + + // Parse the clouds.yaml content + cloudsYaml, ok := openstackSecret.Data["clouds.yaml"] + if !ok { + return nil, errors.New("missing clouds.yaml in OpenStack secret") + } + + parsedCloudsYaml, err := parseCloudsYaml(cloudsYaml) if err != nil { - return fmt.Errorf("failed to generate OpenStack CCM secret: %s", err) + return nil, fmt.Errorf("failed to parse clouds.yaml: %w", err) } - // Apply CCM config - if err := applyCCMConfigs(ctx, cfg.KubeconfSecret, ccmSecret); err != nil { - return fmt.Errorf("failed to apply OpenStack CCM secret: %s", err) + // Extract CloudConfFields using the provided cloudName + fields, err := extractCloudConfFields(parsedCloudsYaml, cloudName) + if err != nil { + return nil, fmt.Errorf("failed to extract cloud.conf fields: %w", err) } - return nil + // Fetch external network details from OpenStackCluster + externalNetwork, err := fetchExternalNetwork(ctx, cfg) + if err != nil { + return nil, fmt.Errorf("failed to fetch external network details: %w", err) + } + if externalNetwork == nil { + return nil, errors.New("external network is nil") + } + if externalNetwork.ID == "" || externalNetwork.Name == "" { + return nil, errors.New("external network details not found") + } + fields.FloatingNetworkID = externalNetwork.ID + fields.PublicNetworkName = externalNetwork.Name + + // Render the cloud.conf secret + return renderCloudConf(cloudConfTemplate, fields) +} + +// Parse the clouds.yaml content into structured types +func parseCloudsYaml(data []byte) (*CloudsYaml, error) { + var cloudsYaml CloudsYaml + if err := yaml.Unmarshal(data, &cloudsYaml); err != nil { + return nil, fmt.Errorf("failed to parse clouds.yaml: %w", err) + } + return &cloudsYaml, nil } -func generateOpenStackCCMSecret(openstackSecret *corev1.Secret) (*corev1.Secret, error) { - // Use the data from the fetched secret +// Extract fields required for the cloud.conf file +func extractCloudConfFields(cloudsYaml *CloudsYaml, cloudName string) (CloudConfFields, error) { + var fields CloudConfFields + + // Access the specified cloud's configuration using cloudName + cloud, exists := cloudsYaml.Clouds[cloudName] + if !exists { + return fields, fmt.Errorf("cloud '%s' not found in clouds.yaml", cloudName) + } + + // Access the 'auth' section and extract fields within the specified cloud + auth := cloud.Auth + fields.AuthURL = auth.AuthURL + fields.ApplicationCredentialID = auth.ApplicationCredentialID + fields.ApplicationCredentialName = auth.ApplicationCredentialName + fields.ApplicationCredentialSecret = auth.ApplicationCredentialSecret + fields.Username = auth.Username + fields.Password = auth.Password + fields.ProjectID = auth.ProjectID + fields.ProjectName = auth.ProjectName + fields.UserDomainName = auth.UserDomainName + fields.ProjectDomainName = auth.ProjectDomainName + fields.RegionName = cloud.RegionName + fields.Interface = cloud.Interface + + return fields, nil +} + +// Fetch external network details from OpenStackCluster +func fetchExternalNetwork(ctx context.Context, cfg *PropagationCfg) (*capo.NetworkStatus, error) { + openstackCluster := &capo.OpenStackCluster{} + err := cfg.Client.Get(ctx, client.ObjectKey{ + Name: cfg.ManagedCluster.Name, + Namespace: cfg.ManagedCluster.Namespace, + }, openstackCluster) + if err != nil { + if apierrors.IsNotFound(err) { + return nil, fmt.Errorf("OpenStackCluster %s/%s not found", + cfg.ManagedCluster.Namespace, cfg.ManagedCluster.Name) + } + return nil, fmt.Errorf("unable to get OpenStackCluster %s/%s: %w", + cfg.ManagedCluster.Namespace, cfg.ManagedCluster.Name, err) + } + return openstackCluster.Status.ExternalNetwork, nil +} + +// Render cloud.conf using the template and fields +func renderCloudConf(templateStr string, fields CloudConfFields) (*corev1.Secret, error) { + tmpl, err := texttemplate.New("cloudConf").Parse(templateStr) + if err != nil { + return nil, fmt.Errorf("failed to parse cloud.conf template: %w", err) + } + + var buf bytes.Buffer + if err := tmpl.Execute(&buf, fields); err != nil { + return nil, fmt.Errorf("failed to render cloud.conf template: %w", err) + } + secretData := map[string][]byte{ - "clouds.yaml": openstackSecret.Data["clouds.yaml"], + "cloud.conf": buf.Bytes(), } - return makeSecret("openstack-cloud-config", metav1.NamespaceSystem, secretData), nil + return makeSecret("openstack-cloud-config", secretData), nil } diff --git a/internal/credspropagation/vsphere.go b/internal/credspropagation/vsphere.go index 189a6bd32..df3da4782 100644 --- a/internal/credspropagation/vsphere.go +++ b/internal/credspropagation/vsphere.go @@ -119,8 +119,8 @@ func generateVSphereCCMConfigs(vCl *capv.VSphereCluster, vScrt *corev1.Secret, v cmData := map[string]string{ "vsphere.conf": string(ccmCfgYaml), } - return makeSecret(secretName, metav1.NamespaceSystem, secretData), - makeConfigMap("cloud-config", metav1.NamespaceSystem, cmData), + return makeSecret(secretName, secretData), + makeConfigMap("cloud-config", cmData), nil } @@ -161,5 +161,5 @@ datacenters = "{{ .Datacenter }}" "csi-vsphere.conf": buf.Bytes(), } - return makeSecret("vcenter-config-secret", metav1.NamespaceSystem, secretData), nil + return makeSecret("vcenter-config-secret", secretData), nil }