sigs.k8s.io/cluster-api-provider-azure@v1.14.3/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 asocontainerservicev1preview "github.com/Azure/azure-service-operator/v2/api/containerservice/v1api20230202preview" 24 asocontainerservicev1 "github.com/Azure/azure-service-operator/v2/api/containerservice/v1api20231001" 25 asocontainerservicev1hub "github.com/Azure/azure-service-operator/v2/api/containerservice/v1api20231001/storage" 26 "github.com/Azure/azure-service-operator/v2/pkg/genruntime" 27 "github.com/pkg/errors" 28 corev1 "k8s.io/api/core/v1" 29 "k8s.io/client-go/tools/clientcmd" 30 "k8s.io/utils/ptr" 31 infrav1 "sigs.k8s.io/cluster-api-provider-azure/api/v1beta1" 32 "sigs.k8s.io/cluster-api-provider-azure/azure" 33 "sigs.k8s.io/cluster-api-provider-azure/azure/services/aso" 34 "sigs.k8s.io/cluster-api-provider-azure/azure/services/token" 35 clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1" 36 "sigs.k8s.io/cluster-api/util/secret" 37 "sigs.k8s.io/controller-runtime/pkg/client" 38 ) 39 40 const ( 41 serviceName = "managedcluster" 42 kubeletIdentityKey = "kubeletidentity" 43 44 // The aadResourceID is the application-id used by the server side. The access token accessing AKS clusters need to be issued for this app. 45 // Refer: https://azure.github.io/kubelogin/concepts/aks.html?highlight=6dae42f8-4368-4678-94ff-3960e28e3630#azure-kubernetes-service-aad-server 46 aadResourceID = "6dae42f8-4368-4678-94ff-3960e28e3630" 47 48 // oidcIssuerProfileUrl is a constant representing the key name for the oidc-issuer-profile-url config map. 49 oidcIssuerProfileURL = "oidc-issuer-profile-url" 50 ) 51 52 // ManagedClusterScope defines the scope interface for a managed cluster. 53 type ManagedClusterScope interface { 54 aso.Scope 55 azure.Authorizer 56 ManagedClusterSpec() azure.ASOResourceSpecGetter[genruntime.MetaObject] 57 SetControlPlaneEndpoint(clusterv1.APIEndpoint) 58 MakeEmptyKubeConfigSecret() corev1.Secret 59 GetAdminKubeconfigData() []byte 60 SetAdminKubeconfigData([]byte) 61 GetUserKubeconfigData() []byte 62 SetUserKubeconfigData([]byte) 63 IsAADEnabled() bool 64 AreLocalAccountsDisabled() bool 65 SetOIDCIssuerProfileStatus(*infrav1.OIDCIssuerProfileStatus) 66 MakeClusterCA() *corev1.Secret 67 StoreClusterInfo(context.Context, []byte) error 68 SetAutoUpgradeVersionStatus(version string) 69 SetVersionStatus(version string) 70 IsManagedVersionUpgrade() bool 71 IsPreviewEnabled() bool 72 } 73 74 // New creates a new service. 75 func New(scope ManagedClusterScope) *aso.Service[genruntime.MetaObject, ManagedClusterScope] { 76 // genruntime.MetaObject is used here instead of an *asocontainerservicev1.ManagedCluster to better 77 // facilitate returning different API versions. 78 svc := aso.NewService[genruntime.MetaObject](serviceName, scope) 79 svc.Specs = []azure.ASOResourceSpecGetter[genruntime.MetaObject]{scope.ManagedClusterSpec()} 80 svc.ConditionType = infrav1.ManagedClusterRunningCondition 81 svc.PostCreateOrUpdateResourceHook = postCreateOrUpdateResourceHook 82 return svc 83 } 84 85 func postCreateOrUpdateResourceHook(ctx context.Context, scope ManagedClusterScope, obj genruntime.MetaObject, err error) error { 86 if err != nil { 87 return err 88 } 89 90 // If existing is preview, convert to stable for this function. 91 var existing *asocontainerservicev1.ManagedCluster 92 if scope.IsPreviewEnabled() { 93 existingPreview := obj.(*asocontainerservicev1preview.ManagedCluster) 94 hub := &asocontainerservicev1hub.ManagedCluster{} 95 if err := existingPreview.ConvertTo(hub); err != nil { 96 return err 97 } 98 prev := &asocontainerservicev1.ManagedCluster{} 99 if err := prev.ConvertFrom(hub); err != nil { 100 return err 101 } 102 existing = prev 103 } else { 104 existing = obj.(*asocontainerservicev1.ManagedCluster) 105 } 106 managedCluster := existing 107 108 // Update control plane endpoint. 109 endpoint := clusterv1.APIEndpoint{ 110 Host: ptr.Deref(managedCluster.Status.Fqdn, ""), 111 Port: 443, 112 } 113 if managedCluster.Status.ApiServerAccessProfile != nil && 114 ptr.Deref(managedCluster.Status.ApiServerAccessProfile.EnablePrivateCluster, false) && 115 !ptr.Deref(managedCluster.Status.ApiServerAccessProfile.EnablePrivateClusterPublicFQDN, false) { 116 endpoint = clusterv1.APIEndpoint{ 117 Host: ptr.Deref(managedCluster.Status.PrivateFQDN, ""), 118 Port: 443, 119 } 120 } 121 scope.SetControlPlaneEndpoint(endpoint) 122 123 // Update kubeconfig data 124 // Always fetch credentials in case of rotation 125 adminKubeConfigData, userKubeConfigData, err := reconcileKubeconfig(ctx, scope, managedCluster.Namespace) 126 if err != nil { 127 return errors.Wrap(err, "error while reconciling kubeconfigs") 128 } 129 scope.SetAdminKubeconfigData(adminKubeConfigData) 130 scope.SetUserKubeconfigData(userKubeConfigData) 131 132 scope.SetOIDCIssuerProfileStatus(nil) 133 if managedCluster.Status.OidcIssuerProfile != nil && managedCluster.Status.OidcIssuerProfile.IssuerURL != nil { 134 scope.SetOIDCIssuerProfileStatus(&infrav1.OIDCIssuerProfileStatus{ 135 IssuerURL: managedCluster.Status.OidcIssuerProfile.IssuerURL, 136 }) 137 } 138 if managedCluster.Status.CurrentKubernetesVersion != nil { 139 currentKubernetesVersion := fmt.Sprintf("v%s", *managedCluster.Status.CurrentKubernetesVersion) 140 scope.SetVersionStatus(currentKubernetesVersion) 141 if scope.IsManagedVersionUpgrade() { 142 scope.SetAutoUpgradeVersionStatus(currentKubernetesVersion) 143 } 144 } 145 146 return nil 147 } 148 149 // reconcileKubeconfig will reconcile admin kubeconfig and user kubeconfig. 150 /* 151 Returns the admin kubeconfig and user kubeconfig 152 If AAD is enabled a user kubeconfig will also get generated and stored in the secret <cluster-name>-kubeconfig-user 153 If we disable local accounts for AAD clusters we do not have access to admin kubeconfig, hence we need to create 154 the admin kubeconfig by authenticating with the user credentials and retrieving the token for kubeconfig. 155 The token is used to create the admin kubeconfig. 156 The user needs to ensure to provide service principal with admin AAD privileges. 157 */ 158 func reconcileKubeconfig(ctx context.Context, scope ManagedClusterScope, namespace string) (adminKubeConfigData []byte, userKubeConfigData []byte, err error) { 159 if scope.IsAADEnabled() { 160 if userKubeConfigData, err = getUserKubeconfigData(ctx, scope, namespace); err != nil { 161 return nil, nil, errors.Wrap(err, "error while trying to get user kubeconfig") 162 } 163 } 164 165 if scope.AreLocalAccountsDisabled() { 166 userKubeconfigWithToken, err := getUserKubeConfigWithToken(userKubeConfigData, ctx, scope) 167 if err != nil { 168 return nil, nil, errors.Wrap(err, "error while trying to get user kubeconfig with token") 169 } 170 return userKubeconfigWithToken, userKubeConfigData, nil 171 } 172 173 asoSecret := &corev1.Secret{} 174 err = scope.GetClient().Get( 175 ctx, 176 client.ObjectKey{ 177 Namespace: namespace, 178 Name: adminKubeconfigSecretName(scope.ClusterName()), 179 }, 180 asoSecret, 181 ) 182 if err != nil { 183 return nil, nil, errors.Wrap(err, "failed to get ASO admin kubeconfig secret") 184 } 185 adminKubeConfigData = asoSecret.Data[secret.KubeconfigDataName] 186 return adminKubeConfigData, userKubeConfigData, nil 187 } 188 189 // getUserKubeconfigData gets user kubeconfig when aad is enabled for the aad clusters. 190 func getUserKubeconfigData(ctx context.Context, scope ManagedClusterScope, namespace string) ([]byte, error) { 191 asoSecret := &corev1.Secret{} 192 err := scope.GetClient().Get( 193 ctx, 194 client.ObjectKey{ 195 Namespace: namespace, 196 Name: userKubeconfigSecretName(scope.ClusterName()), 197 }, 198 asoSecret, 199 ) 200 if err != nil { 201 return nil, errors.Wrap(err, "failed to get ASO user kubeconfig secret") 202 } 203 kubeConfigData := asoSecret.Data[secret.KubeconfigDataName] 204 return kubeConfigData, nil 205 } 206 207 // getUserKubeConfigWithToken returns the kubeconfig with user token, for capz to create the target cluster. 208 func getUserKubeConfigWithToken(userKubeConfigData []byte, ctx context.Context, scope azure.Authorizer) ([]byte, error) { 209 tokenClient, err := token.NewClient(scope) 210 if err != nil { 211 return nil, errors.Wrap(err, "error while getting aad token client") 212 } 213 214 token, err := tokenClient.GetAzureActiveDirectoryToken(ctx, aadResourceID) 215 if err != nil { 216 return nil, errors.Wrap(err, "error while getting aad token for user kubeconfig") 217 } 218 219 return createUserKubeconfigWithToken(token, userKubeConfigData) 220 } 221 222 // createUserKubeconfigWithToken gets the kubeconfig data for authenticating with target cluster. 223 func createUserKubeconfigWithToken(token string, userKubeConfigData []byte) ([]byte, error) { 224 config, err := clientcmd.Load(userKubeConfigData) 225 if err != nil { 226 return nil, errors.Wrap(err, "error while trying to unmarshal new user kubeconfig with token") 227 } 228 for _, auth := range config.AuthInfos { 229 auth.Token = token 230 auth.Exec = nil 231 } 232 kubeconfig, err := clientcmd.Write(*config) 233 if err != nil { 234 return nil, errors.Wrap(err, "error while trying to marshal new user kubeconfig with token") 235 } 236 return kubeconfig, nil 237 }