From 074f989703303f6e1d5c3bda2e5b2e9c14b4e87c Mon Sep 17 00:00:00 2001 From: Bharath Nallapeta Date: Fri, 27 Dec 2024 16:28:01 +0530 Subject: [PATCH] Add OpenStack credential propagation support (#697) * Add OpenStack credential propagation support Signed-off-by: Bharath Nallapeta * Fix a golangci-lint issue: unparam namespace Signed-off-by: Bharath Nallapeta * PR comment - rebase issue fixed Signed-off-by: Bharath Nallapeta --------- Signed-off-by: Bharath Nallapeta --- Makefile | 4 + cmd/main.go | 2 + config/dev/openstack-credentials.yaml | 31 +++ go.mod | 1 + go.sum | 2 + .../clusterdeployment_controller.go | 26 ++- internal/credspropagation/azure.go | 3 +- internal/credspropagation/common.go | 11 +- internal/credspropagation/openstack.go | 221 ++++++++++++++++++ internal/credspropagation/vsphere.go | 6 +- internal/webhook/clusterdeployment_webhook.go | 2 +- 11 files changed, 295 insertions(+), 14 deletions(-) create mode 100644 config/dev/openstack-credentials.yaml create mode 100644 internal/credspropagation/openstack.go diff --git a/Makefile b/Makefile index c74ae3682..230fbd881 100644 --- a/Makefile +++ b/Makefile @@ -356,6 +356,10 @@ dev-eks-creds: dev-aws-creds dev-aks-creds: envsubst @NAMESPACE=$(NAMESPACE) $(ENVSUBST) -no-unset -i config/dev/aks-credentials.yaml | $(KUBECTL) apply -f - +.PHONY: dev-openstack-creds +dev-openstack-creds: envsubst + @NAMESPACE=$(NAMESPACE) $(ENVSUBST) -no-unset -i config/dev/openstack-credentials.yaml | $(KUBECTL) apply -f - + .PHONY: dev-apply ## Apply the development environment by deploying the kind cluster, local registry and the HMC helm chart. dev-apply: kind-deploy registry-deploy dev-push dev-deploy dev-templates dev-release diff --git a/cmd/main.go b/cmd/main.go index 03ee5f50e..84c9944cb 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 new file mode 100644 index 000000000..ebeb70297 --- /dev/null +++ b/config/dev/openstack-credentials.yaml @@ -0,0 +1,31 @@ +--- +apiVersion: v1 +kind: Secret +metadata: + name: openstack-cloud-config + namespace: ${NAMESPACE} +stringData: + clouds.yaml: | + clouds: + openstack: + auth: + auth_url: ${OS_AUTH_URL} + 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 +metadata: + name: openstack-cluster-identity-cred + namespace: ${NAMESPACE} +spec: + description: OpenStack credentials + identityRef: + apiVersion: v1 + kind: Secret + name: openstack-cloud-config + namespace: ${NAMESPACE} diff --git a/go.mod b/go.mod index a087df646..bfb20676f 100644 --- a/go.mod +++ b/go.mod @@ -30,6 +30,7 @@ require ( sigs.k8s.io/cluster-api v1.9.3 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 92fa1ef32..22fab400b 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/clusterdeployment_controller.go b/internal/controller/clusterdeployment_controller.go index b972eaa84..c6f02d4f8 100644 --- a/internal/controller/clusterdeployment_controller.go +++ b/internal/controller/clusterdeployment_controller.go @@ -355,7 +355,7 @@ func (r *ClusterDeploymentReconciler) updateCluster(ctx context.Context, mc *hmc } if mc.Spec.PropagateCredentials { - 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 } @@ -696,7 +696,7 @@ func (r *ClusterDeploymentReconciler) objectsAvailable(ctx context.Context, name return len(itemsList.Items) != 0, nil } -func (r *ClusterDeploymentReconciler) reconcileCredentialPropagation(ctx context.Context, clusterDeployment *hmc.ClusterDeployment) error { +func (r *ClusterDeploymentReconciler) reconcileCredentialPropagation(ctx context.Context, clusterDeployment *hmc.ClusterDeployment, credential *hmc.Credential) error { l := ctrl.LoggerFrom(ctx) l.Info("Reconciling CCM credentials propagation") @@ -715,8 +715,9 @@ func (r *ClusterDeploymentReconciler) reconcileCredentialPropagation(ctx context propnCfg := &credspropagation.PropagationCfg{ Client: r.Client, - ClusterDeployment: clusterDeployment, + IdentityRef: credential.Spec.IdentityRef, KubeconfSecret: kubeconfSecret, + ClusterDeployment: clusterDeployment, SystemNamespace: r.SystemNamespace, } @@ -763,6 +764,25 @@ func (r *ClusterDeploymentReconciler) reconcileCredentialPropagation(ctx context Reason: hmc.SucceededReason, Message: "vSphere CCM credentials created", }) + case "openstack": + l.Info("OpenStack creds propagation start") + if err := credspropagation.PropagateOpenStackSecrets(ctx, propnCfg); err != nil { + errMsg := fmt.Sprintf("failed to create OpenStack CCM credentials: %s", err) + apimeta.SetStatusCondition(clusterDeployment.GetConditions(), metav1.Condition{ + Type: hmc.CredentialsPropagatedCondition, + Status: metav1.ConditionFalse, + Reason: hmc.FailedReason, + Message: errMsg, + }) + return errors.New(errMsg) + } + + apimeta.SetStatusCondition(clusterDeployment.GetConditions(), metav1.Condition{ + Type: hmc.CredentialsPropagatedCondition, + Status: metav1.ConditionTrue, + Reason: hmc.SucceededReason, + Message: "OpenStack CCM credentials created", + }) default: apimeta.SetStatusCondition(clusterDeployment.GetConditions(), metav1.Condition{ Type: hmc.CredentialsPropagatedCondition, diff --git a/internal/credspropagation/azure.go b/internal/credspropagation/azure.go index 53abb4951..c81291517 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" ) @@ -94,7 +93,7 @@ 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 } func getAzureSubnetData(azureCluster *capz.AzureCluster) (subnetName, secGroup, routeTable string) { diff --git a/internal/credspropagation/common.go b/internal/credspropagation/common.go index ed33d11cd..02bbf21eb 100644 --- a/internal/credspropagation/common.go +++ b/internal/credspropagation/common.go @@ -30,8 +30,9 @@ import ( type PropagationCfg struct { Client client.Client - ClusterDeployment *hmc.ClusterDeployment KubeconfSecret *corev1.Secret + IdentityRef *corev1.ObjectReference + ClusterDeployment *hmc.ClusterDeployment SystemNamespace string } @@ -53,11 +54,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 +66,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 new file mode 100644 index 000000000..c39acef9b --- /dev/null +++ b/internal/credspropagation/openstack.go @@ -0,0 +1,221 @@ +// 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" + + "gopkg.in/yaml.v3" + corev1 "k8s.io/api/core/v1" + capo "sigs.k8s.io/cluster-api-provider-openstack/api/v1beta1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +type ( + cloudConfFields struct { + AuthURL string + ApplicationCredentialID string + ApplicationCredentialName string + ApplicationCredentialSecret string + Username string + Password string + RegionName string + FloatingNetworkID string + PublicNetworkName string + } + + cloudsYaml struct { + Clouds map[string]cloud `yaml:"clouds"` + } + + cloud struct { + Auth auth `yaml:"auth"` + RegionName string `yaml:"region_name"` + } + + 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"` + ProjectDomainName string `yaml:"project_domain_name"` + } +) + +// PropagateOpenStackSecrets propagates OpenStack secrets +func PropagateOpenStackSecrets(ctx context.Context, cfg *PropagationCfg) error { + if cfg == nil { + return errors.New("PropagationCfg is nil") + } + // Fetch the OpenStackCluster resource + openstackCluster := &capo.OpenStackCluster{} + if err := cfg.Client.Get(ctx, client.ObjectKey{ + Name: cfg.ClusterDeployment.Name, + Namespace: cfg.ClusterDeployment.Namespace, + }, openstackCluster); err != nil { + return fmt.Errorf("unable to get OpenStackCluster %s/%s: %w", + cfg.ClusterDeployment.Namespace, cfg.ClusterDeployment.Name, err) + } + + // Fetch the OpenStack secret + openstackSecret, err := fetchOpenStackSecret(ctx, cfg) + if err != nil { + return fmt.Errorf("failed to fetch OpenStack secret: %w", err) + } + + // Generate the CCM secret using the extracted cloudName + ccmSecret, err := generateOpenStackCCMSecret(openstackCluster, openstackSecret) + 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) (*corev1.Secret, error) { + openstackSecret := &corev1.Secret{} + if err := cfg.Client.Get(ctx, client.ObjectKey{ + Name: cfg.IdentityRef.Name, + Namespace: cfg.IdentityRef.Namespace, + }, openstackSecret); err != nil { + return nil, fmt.Errorf("failed to get OpenStack secret %s/%s: %w", + cfg.IdentityRef.Namespace, cfg.IdentityRef.Name, err) + } + return openstackSecret, nil +} + +// Generate the CCM secret from the OpenStack secret +func generateOpenStackCCMSecret(openstackCluster *capo.OpenStackCluster, openstackSecret *corev1.Secret) (*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 }}" + +[LoadBalancer] +{{- if .FloatingNetworkID }} +floating-network-id="{{ .FloatingNetworkID }}" +{{- end }} + +[Network] +{{- if .PublicNetworkName }} +public-network-name="{{ .PublicNetworkName }}" +{{- end }} +` + + // Parse the clouds.yaml content + cloudsYamlData, ok := openstackSecret.Data["clouds.yaml"] + if !ok { + return nil, errors.New("missing clouds.yaml in OpenStack secret") + } + + parsedCloudsYaml, err := parseCloudsYaml(cloudsYamlData) + if err != nil { + return nil, fmt.Errorf("failed to parse clouds.yaml: %w", err) + } + + // Extract cloudConfFields using the provided cloudName + fields, err := extractCloudConfFields(parsedCloudsYaml, openstackCluster.Spec.IdentityRef.CloudName) + if err != nil { + return nil, fmt.Errorf("failed to extract cloud.conf fields: %w", err) + } + + // Fetch external network details from OpenStackCluster + // externalNetwork, err := fetchExternalNetwork(ctx, cfg) + externalNetwork := openstackCluster.Status.ExternalNetwork + if externalNetwork == nil || externalNetwork.ID == "" || externalNetwork.Name == "" { + return nil, errors.New("external network details are incomplete") + } + 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 parsed cloudsYaml + if err := yaml.Unmarshal(data, &parsed); err != nil { + return nil, fmt.Errorf("failed to parse clouds.yaml: %w", err) + } + return &parsed, nil +} + +// Extract fields required for the cloud.conf file +func extractCloudConfFields(cy *cloudsYaml, cloudName string) (cloudConfFields, error) { + var fields cloudConfFields + + cloud, exists := cy.Clouds[cloudName] + if !exists { + return fields, fmt.Errorf("cloud '%s' not found in clouds.yaml", cloudName) + } + + auth := cloud.Auth + fields = cloudConfFields{ + AuthURL: auth.AuthURL, + ApplicationCredentialID: auth.ApplicationCredentialID, + ApplicationCredentialName: auth.ApplicationCredentialName, + ApplicationCredentialSecret: auth.ApplicationCredentialSecret, + Username: auth.Username, + Password: auth.Password, + RegionName: cloud.RegionName, + } + + return fields, 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{ + "cloud.conf": buf.Bytes(), + } + + return makeSecret("openstack-cloud-config", secretData), nil +} diff --git a/internal/credspropagation/vsphere.go b/internal/credspropagation/vsphere.go index f9f3e5ab5..a4cf3b2f5 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 } diff --git a/internal/webhook/clusterdeployment_webhook.go b/internal/webhook/clusterdeployment_webhook.go index 0ae339121..2ddff3c5c 100644 --- a/internal/webhook/clusterdeployment_webhook.go +++ b/internal/webhook/clusterdeployment_webhook.go @@ -285,7 +285,7 @@ func isCredMatchTemplate(cred *hmcv1alpha1.Credential, template *hmcv1alpha1.Clu if idtyKind != "VSphereClusterIdentity" { return errMsg(provider) } - case "infrastructure-internal": + case "infrastructure-openstack", "infrastructure-internal": if idtyKind != "Secret" { return errMsg(provider) }