sigs.k8s.io/cluster-api-provider-azure@v1.17.0/azure/services/managedclusters/managedclusters.go (about) 1 /* 2 Copyright 2020 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package managedclusters 18 19 import ( 20 "context" 21 "fmt" 22 23 "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy" 24 asocontainerservicev1hub "github.com/Azure/azure-service-operator/v2/api/containerservice/v1api20231001/storage" 25 "github.com/Azure/azure-service-operator/v2/pkg/genruntime" 26 "github.com/pkg/errors" 27 corev1 "k8s.io/api/core/v1" 28 "k8s.io/client-go/tools/clientcmd" 29 "k8s.io/utils/ptr" 30 infrav1 "sigs.k8s.io/cluster-api-provider-azure/api/v1beta1" 31 "sigs.k8s.io/cluster-api-provider-azure/azure" 32 "sigs.k8s.io/cluster-api-provider-azure/azure/services/aso" 33 clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" 34 "sigs.k8s.io/cluster-api/util/secret" 35 "sigs.k8s.io/controller-runtime/pkg/client" 36 "sigs.k8s.io/controller-runtime/pkg/conversion" 37 ) 38 39 const ( 40 serviceName = "managedcluster" 41 kubeletIdentityKey = "kubeletidentity" 42 43 // The aadResourceID is the application-id used by the server side. The access token accessing AKS clusters need to be issued for this app. 44 // Refer: https://azure.github.io/kubelogin/concepts/aks.html?highlight=6dae42f8-4368-4678-94ff-3960e28e3630#azure-kubernetes-service-aad-server 45 aadResourceID = "6dae42f8-4368-4678-94ff-3960e28e3630" 46 47 // oidcIssuerProfileUrl is a constant representing the key name for the oidc-issuer-profile-url config map. 48 oidcIssuerProfileURL = "oidc-issuer-profile-url" 49 ) 50 51 // ManagedClusterScope defines the scope interface for a managed cluster. 52 type ManagedClusterScope interface { 53 aso.Scope 54 azure.Authorizer 55 ManagedClusterSpec() azure.ASOResourceSpecGetter[genruntime.MetaObject] 56 SetControlPlaneEndpoint(clusterv1.APIEndpoint) 57 MakeEmptyKubeConfigSecret() corev1.Secret 58 GetAdminKubeconfigData() []byte 59 SetAdminKubeconfigData([]byte) 60 GetUserKubeconfigData() []byte 61 SetUserKubeconfigData([]byte) 62 IsAADEnabled() bool 63 AreLocalAccountsDisabled() bool 64 SetOIDCIssuerProfileStatus(*infrav1.OIDCIssuerProfileStatus) 65 MakeClusterCA() *corev1.Secret 66 StoreClusterInfo(context.Context, []byte) error 67 SetAutoUpgradeVersionStatus(version string) 68 SetVersionStatus(version string) 69 IsManagedVersionUpgrade() bool 70 } 71 72 // New creates a new service. 73 func New(scope ManagedClusterScope) *aso.Service[genruntime.MetaObject, ManagedClusterScope] { 74 // genruntime.MetaObject is used here instead of an *asocontainerservicev1.ManagedCluster to better 75 // facilitate returning different API versions. 76 svc := aso.NewService[genruntime.MetaObject](serviceName, scope) 77 svc.Specs = []azure.ASOResourceSpecGetter[genruntime.MetaObject]{scope.ManagedClusterSpec()} 78 svc.ConditionType = infrav1.ManagedClusterRunningCondition 79 svc.PostCreateOrUpdateResourceHook = postCreateOrUpdateResourceHook 80 return svc 81 } 82 83 func postCreateOrUpdateResourceHook(ctx context.Context, scope ManagedClusterScope, obj genruntime.MetaObject, err error) error { 84 if err != nil { 85 return err 86 } 87 88 managedCluster := &asocontainerservicev1hub.ManagedCluster{} 89 if err := obj.(conversion.Convertible).ConvertTo(managedCluster); err != nil { 90 return err 91 } 92 93 // Update control plane endpoint. 94 endpoint := clusterv1.APIEndpoint{ 95 Host: ptr.Deref(managedCluster.Status.Fqdn, ""), 96 Port: 443, 97 } 98 if managedCluster.Status.ApiServerAccessProfile != nil && 99 ptr.Deref(managedCluster.Status.ApiServerAccessProfile.EnablePrivateCluster, false) && 100 !ptr.Deref(managedCluster.Status.ApiServerAccessProfile.EnablePrivateClusterPublicFQDN, false) { 101 endpoint = clusterv1.APIEndpoint{ 102 Host: ptr.Deref(managedCluster.Status.PrivateFQDN, ""), 103 Port: 443, 104 } 105 } 106 scope.SetControlPlaneEndpoint(endpoint) 107 108 // Update kubeconfig data 109 // Always fetch credentials in case of rotation 110 adminKubeConfigData, userKubeConfigData, err := reconcileKubeconfig(ctx, scope, managedCluster.Namespace) 111 if err != nil { 112 return errors.Wrap(err, "error while reconciling kubeconfigs") 113 } 114 scope.SetAdminKubeconfigData(adminKubeConfigData) 115 scope.SetUserKubeconfigData(userKubeConfigData) 116 117 scope.SetOIDCIssuerProfileStatus(nil) 118 if managedCluster.Status.OidcIssuerProfile != nil && managedCluster.Status.OidcIssuerProfile.IssuerURL != nil { 119 scope.SetOIDCIssuerProfileStatus(&infrav1.OIDCIssuerProfileStatus{ 120 IssuerURL: managedCluster.Status.OidcIssuerProfile.IssuerURL, 121 }) 122 } 123 if managedCluster.Status.CurrentKubernetesVersion != nil { 124 currentKubernetesVersion := fmt.Sprintf("v%s", *managedCluster.Status.CurrentKubernetesVersion) 125 scope.SetVersionStatus(currentKubernetesVersion) 126 if scope.IsManagedVersionUpgrade() { 127 scope.SetAutoUpgradeVersionStatus(currentKubernetesVersion) 128 } 129 } 130 131 return nil 132 } 133 134 // reconcileKubeconfig will reconcile admin kubeconfig and user kubeconfig. 135 /* 136 Returns the admin kubeconfig and user kubeconfig 137 If AAD is enabled a user kubeconfig will also get generated and stored in the secret <cluster-name>-kubeconfig-user 138 If we disable local accounts for AAD clusters we do not have access to admin kubeconfig, hence we need to create 139 the admin kubeconfig by authenticating with the user credentials and retrieving the token for kubeconfig. 140 The token is used to create the admin kubeconfig. 141 The user needs to ensure to provide service principal with admin AAD privileges. 142 */ 143 func reconcileKubeconfig(ctx context.Context, scope ManagedClusterScope, namespace string) (adminKubeConfigData []byte, userKubeConfigData []byte, err error) { 144 if scope.IsAADEnabled() { 145 if userKubeConfigData, err = getUserKubeconfigData(ctx, scope, namespace); err != nil { 146 return nil, nil, errors.Wrap(err, "error while trying to get user kubeconfig") 147 } 148 } 149 150 if scope.AreLocalAccountsDisabled() { 151 userKubeconfigWithToken, err := getUserKubeConfigWithToken(ctx, userKubeConfigData, scope) 152 if err != nil { 153 return nil, nil, errors.Wrap(err, "error while trying to get user kubeconfig with token") 154 } 155 return userKubeconfigWithToken, userKubeConfigData, nil 156 } 157 158 asoSecret := &corev1.Secret{} 159 err = scope.GetClient().Get( 160 ctx, 161 client.ObjectKey{ 162 Namespace: namespace, 163 Name: adminKubeconfigSecretName(scope.ClusterName()), 164 }, 165 asoSecret, 166 ) 167 if err != nil { 168 return nil, nil, errors.Wrap(err, "failed to get ASO admin kubeconfig secret") 169 } 170 adminKubeConfigData = asoSecret.Data[secret.KubeconfigDataName] 171 return adminKubeConfigData, userKubeConfigData, nil 172 } 173 174 // getUserKubeconfigData gets user kubeconfig when aad is enabled for the aad clusters. 175 func getUserKubeconfigData(ctx context.Context, scope ManagedClusterScope, namespace string) ([]byte, error) { 176 asoSecret := &corev1.Secret{} 177 err := scope.GetClient().Get( 178 ctx, 179 client.ObjectKey{ 180 Namespace: namespace, 181 Name: userKubeconfigSecretName(scope.ClusterName()), 182 }, 183 asoSecret, 184 ) 185 if err != nil { 186 return nil, errors.Wrap(err, "failed to get ASO user kubeconfig secret") 187 } 188 kubeConfigData := asoSecret.Data[secret.KubeconfigDataName] 189 return kubeConfigData, nil 190 } 191 192 // getUserKubeConfigWithToken returns the kubeconfig with user token, for capz to create the target cluster. 193 func getUserKubeConfigWithToken(ctx context.Context, userKubeConfigData []byte, auth azure.Authorizer) ([]byte, error) { 194 token, err := auth.Token().GetToken(ctx, policy.TokenRequestOptions{Scopes: []string{aadResourceID + "/.default"}}) 195 if err != nil { 196 return nil, errors.Wrap(err, "error while getting aad token for user kubeconfig") 197 } 198 config, err := clientcmd.Load(userKubeConfigData) 199 if err != nil { 200 return nil, errors.Wrap(err, "error while trying to unmarshal new user kubeconfig with token") 201 } 202 for _, auth := range config.AuthInfos { 203 auth.Token = token.Token 204 auth.Exec = nil 205 } 206 kubeconfig, err := clientcmd.Write(*config) 207 if err != nil { 208 return nil, errors.Wrap(err, "error while trying to marshal new user kubeconfig with token") 209 } 210 return kubeconfig, nil 211 }