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  }