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  }