github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/caas/kubernetes/provider/cloud.go (about)

     1  // Copyright 2019 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package provider
     5  
     6  import (
     7  	jujuclock "github.com/juju/clock"
     8  	"github.com/juju/errors"
     9  	"github.com/juju/utils/v3"
    10  	k8slabels "k8s.io/apimachinery/pkg/labels"
    11  
    12  	k8s "github.com/juju/juju/caas/kubernetes"
    13  	"github.com/juju/juju/caas/kubernetes/clientconfig"
    14  	k8scloud "github.com/juju/juju/caas/kubernetes/cloud"
    15  	k8sconstants "github.com/juju/juju/caas/kubernetes/provider/constants"
    16  	k8sutils "github.com/juju/juju/caas/kubernetes/provider/utils"
    17  	"github.com/juju/juju/cloud"
    18  	"github.com/juju/juju/environs"
    19  	environscloudspec "github.com/juju/juju/environs/cloudspec"
    20  	"github.com/juju/juju/environs/config"
    21  )
    22  
    23  // ClientConfigFuncGetter returns a function returning az reader that will read a k8s cluster config for a given cluster type
    24  type ClientConfigFuncGetter func(string) (clientconfig.ClientConfigFunc, error)
    25  
    26  // GetClusterMetadataFunc returns the ClusterMetadata using the provided ClusterMetadataChecker
    27  type GetClusterMetadataFunc func(KubeCloudStorageParams) (*k8s.ClusterMetadata, error)
    28  
    29  // KubeCloudParams defines the parameters used to extract a k8s cluster definition from kubeconfig data.
    30  type KubeCloudParams struct {
    31  	ClusterName string
    32  	ContextName string
    33  	CloudName   string
    34  	// CredentialUID ensures RBAC resources are unique.
    35  	CredentialUID      string
    36  	HostCloudRegion    string
    37  	CaasType           string
    38  	ClientConfigGetter ClientConfigFuncGetter
    39  	Clock              jujuclock.Clock
    40  }
    41  
    42  // KubeCloudStorageParams defines the parameters used to determine storage details for a k8s cluster.
    43  type KubeCloudStorageParams struct {
    44  	WorkloadStorage        string
    45  	HostCloudRegion        string
    46  	MetadataChecker        k8s.ClusterMetadataChecker
    47  	GetClusterMetadataFunc GetClusterMetadataFunc
    48  }
    49  
    50  // UpdateKubeCloudWithStorage updates the passed Cloud with storage details retrieved from the cloud's cluster.
    51  func UpdateKubeCloudWithStorage(k8sCloud cloud.Cloud, storageParams KubeCloudStorageParams) (cloud.Cloud, error) {
    52  	// Get the cluster metadata and see what storage comes back based on the
    53  	// preffered rules for metadata.
    54  	clusterMetadata, err := storageParams.GetClusterMetadataFunc(storageParams)
    55  	if err != nil {
    56  		return cloud.Cloud{}, ClusterQueryError{Message: err.Error()}
    57  	}
    58  	if clusterMetadata == nil {
    59  		return cloud.Cloud{}, ClusterQueryError{Message: "cannot get cluster metadata"}
    60  	}
    61  
    62  	if storageParams.HostCloudRegion == "" && clusterMetadata.Cloud != "" {
    63  		var region string
    64  		if clusterMetadata.Regions != nil && clusterMetadata.Regions.Size() > 0 {
    65  			region = clusterMetadata.Regions.SortedValues()[0]
    66  		}
    67  		storageParams.HostCloudRegion = cloud.BuildHostCloudRegion(clusterMetadata.Cloud, region)
    68  	}
    69  	k8sCloud.HostCloudRegion = storageParams.HostCloudRegion
    70  
    71  	if k8sCloud.HostCloudRegion != "" {
    72  		_, region, err := cloud.SplitHostCloudRegion(k8sCloud.HostCloudRegion)
    73  		if err != nil {
    74  			// Shouldn't happen as HostCloudRegion is validated earlier.
    75  			return cloud.Cloud{}, errors.Trace(err)
    76  		}
    77  		if region != "" {
    78  			k8sCloud.Regions = []cloud.Region{{
    79  				Name:     region,
    80  				Endpoint: k8sCloud.Endpoint,
    81  			}}
    82  		}
    83  	}
    84  
    85  	// We at least expected operator storage to be available to successfully use
    86  	// this cloud.
    87  	if clusterMetadata.OperatorStorageClass == nil {
    88  		return cloud.Cloud{}, &environs.PreferredStorageNotFound{
    89  			Message: "no preferred operator storage found in Kubernetes cluster",
    90  		}
    91  	}
    92  
    93  	if k8sCloud.Config == nil {
    94  		k8sCloud.Config = make(map[string]interface{})
    95  	}
    96  
    97  	k8sCloud.Config[k8sconstants.OperatorStorageKey] = clusterMetadata.OperatorStorageClass.Name
    98  	k8sCloud.Config[k8sconstants.WorkloadStorageKey] = ""
    99  	if clusterMetadata.WorkloadStorageClass != nil {
   100  		k8sCloud.Config[k8sconstants.WorkloadStorageKey] = clusterMetadata.WorkloadStorageClass.Name
   101  	}
   102  	return k8sCloud, nil
   103  }
   104  
   105  // BaseKubeCloudOpenParams provides a basic OpenParams for a cluster
   106  func BaseKubeCloudOpenParams(cloud cloud.Cloud, credential cloud.Credential) (environs.OpenParams, error) {
   107  	// To get a k8s client, we need a config with minimal information.
   108  	// It's not used unless operating on a real model but we need to supply it.
   109  	uuid, err := utils.NewUUID()
   110  	if err != nil {
   111  		return environs.OpenParams{}, errors.Trace(err)
   112  	}
   113  	attrs := map[string]interface{}{
   114  		config.NameKey: "add-cloud",
   115  		config.TypeKey: "kubernetes",
   116  		config.UUIDKey: uuid.String(),
   117  	}
   118  	cfg, err := config.New(config.UseDefaults, attrs)
   119  	if err != nil {
   120  		return environs.OpenParams{}, errors.Trace(err)
   121  	}
   122  
   123  	cloudSpec, err := environscloudspec.MakeCloudSpec(cloud, "", &credential)
   124  	if err != nil {
   125  		return environs.OpenParams{}, errors.Trace(err)
   126  	}
   127  	openParams := environs.OpenParams{
   128  		Cloud: cloudSpec, Config: cfg,
   129  	}
   130  	return openParams, nil
   131  }
   132  
   133  // FinalizeCloud is part of the environs.CloudFinalizer interface.
   134  func (p kubernetesEnvironProvider) FinalizeCloud(ctx environs.FinalizeCloudContext, cld cloud.Cloud) (cloud.Cloud, error) {
   135  	// We set the clouds auth types to all kubernetes supported auth types here
   136  	// so that finalize credentials is free to change the credentials of the
   137  	// bootstrap. See lp-1918486
   138  	cld.AuthTypes = k8scloud.SupportedAuthTypes()
   139  
   140  	// if storage is already defined there is no need to query the cluster
   141  	if opStorage, ok := cld.Config[k8sconstants.OperatorStorageKey]; ok && opStorage != "" {
   142  		return cld, nil
   143  	}
   144  
   145  	var credentials cloud.Credential
   146  	if cld.Name != k8s.K8sCloudMicrok8s {
   147  		creds, err := p.RegisterCredentials(cld)
   148  		if err != nil {
   149  			return cld, err
   150  		}
   151  
   152  		cloudCred, exists := creds[cld.Name]
   153  		if !exists {
   154  			return cld, nil
   155  		}
   156  
   157  		credentials = cloudCred.AuthCredentials[creds[cld.Name].DefaultCredential]
   158  	} else {
   159  		// Need the credentials, need to query for those details
   160  		mk8sCloud, err := p.builtinCloudGetter(p.cmdRunner)
   161  		if err != nil {
   162  			return cloud.Cloud{}, errors.Trace(err)
   163  		}
   164  		cld = mk8sCloud
   165  
   166  		creds, err := p.RegisterCredentials(cld)
   167  		if err != nil {
   168  			return cld, err
   169  		}
   170  
   171  		credentials = creds[cld.Name].AuthCredentials[creds[cld.Name].DefaultCredential]
   172  	}
   173  
   174  	if cld.SkipTLSVerify {
   175  		logger.Warningf("k8s cloud %v is configured to skip server certificate validity checks", cld.Name)
   176  	}
   177  
   178  	openParams, err := BaseKubeCloudOpenParams(cld, credentials)
   179  	if err != nil {
   180  		return cloud.Cloud{}, errors.Trace(err)
   181  	}
   182  	broker, err := p.brokerGetter(openParams)
   183  	if err != nil {
   184  		return cloud.Cloud{}, errors.Trace(err)
   185  	}
   186  	if cld.Name == k8s.K8sCloudMicrok8s {
   187  		if err := ensureMicroK8sSuitable(broker); err != nil {
   188  			return cld, errors.Trace(err)
   189  		}
   190  	}
   191  	storageUpdateParams := KubeCloudStorageParams{
   192  		MetadataChecker: broker,
   193  		GetClusterMetadataFunc: func(storageParams KubeCloudStorageParams) (*k8s.ClusterMetadata, error) {
   194  			clusterMetadata, err := storageParams.MetadataChecker.GetClusterMetadata("")
   195  			if err != nil {
   196  				return nil, errors.Trace(err)
   197  			}
   198  			return clusterMetadata, nil
   199  		},
   200  	}
   201  
   202  	cld, err = UpdateKubeCloudWithStorage(cld, storageUpdateParams)
   203  	if err != nil {
   204  		return cld, errors.Trace(err)
   205  	}
   206  
   207  	if cld.HostCloudRegion == "" {
   208  		cld.HostCloudRegion = k8s.K8sCloudOther
   209  	}
   210  
   211  	return cld, nil
   212  }
   213  
   214  func checkDefaultStorageExist(broker ClusterMetadataStorageChecker) error {
   215  	storageClasses, err := broker.ListStorageClasses(k8slabels.NewSelector())
   216  	if err != nil && !errors.IsNotFound(err) {
   217  		return errors.Annotate(err, "cannot list storage classes")
   218  	}
   219  	for _, sc := range storageClasses {
   220  		if sc.Annotations["storageclass.kubernetes.io/is-default-class"] == "true" {
   221  			return nil
   222  		}
   223  	}
   224  	return errors.NotFoundf("default storage")
   225  }
   226  
   227  func checkDNSAddonEnabled(broker ClusterMetadataStorageChecker) error {
   228  	pods, err := broker.ListPods("kube-system", k8sutils.LabelsToSelector(map[string]string{"k8s-app": "kube-dns"}))
   229  	if err != nil && !errors.IsNotFound(err) {
   230  		return errors.Annotate(err, "cannot list kube-dns pods")
   231  	}
   232  	if len(pods) > 0 {
   233  		return nil
   234  	}
   235  	return errors.NotFoundf("dns pod")
   236  }
   237  
   238  func ensureMicroK8sSuitable(broker ClusterMetadataStorageChecker) error {
   239  	err := checkDefaultStorageExist(broker)
   240  	if errors.IsNotFound(err) {
   241  		return errors.New("required storage addon is not enabled")
   242  	}
   243  	if err != nil {
   244  		return errors.Trace(err)
   245  	}
   246  
   247  	err = checkDNSAddonEnabled(broker)
   248  	if errors.IsNotFound(err) {
   249  		return errors.New("required dns addon is not enabled")
   250  	}
   251  	return errors.Trace(err)
   252  }