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

     1  // Copyright 2018 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package provider
     5  
     6  import (
     7  	"context"
     8  
     9  	"github.com/juju/collections/set"
    10  	"github.com/juju/errors"
    11  	corev1 "k8s.io/api/core/v1"
    12  	storagev1 "k8s.io/api/storage/v1"
    13  	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    14  	k8slabels "k8s.io/apimachinery/pkg/labels"
    15  	"k8s.io/apimachinery/pkg/selection"
    16  	core "k8s.io/client-go/kubernetes/typed/core/v1"
    17  	storage "k8s.io/client-go/kubernetes/typed/storage/v1"
    18  
    19  	"github.com/juju/juju/caas/kubernetes"
    20  	providerstorage "github.com/juju/juju/caas/kubernetes/provider/storage"
    21  	"github.com/juju/juju/environs"
    22  	environscontext "github.com/juju/juju/environs/context"
    23  )
    24  
    25  // newLabelRequirements creates a list of k8s node label requirements.
    26  // This should be called inside package init function to panic earlier
    27  // if there is a invalid requirement definition.
    28  func newLabelRequirements(rs ...requirementParams) k8slabels.Selector {
    29  	s := k8slabels.NewSelector()
    30  	for _, r := range rs {
    31  		l, err := k8slabels.NewRequirement(r.key, r.operator, r.strValues)
    32  		if err != nil {
    33  			// this panic only happens if the compiled code is wrong.
    34  			panic(errors.Annotatef(err, "incorrect requirement config %v", r))
    35  		}
    36  		s = s.Add(*l)
    37  	}
    38  	return s
    39  }
    40  
    41  func labelSetToRequirements(labels k8slabels.Set) []k8slabels.Requirement {
    42  	out, _ := k8slabels.SelectorFromValidatedSet(labels).Requirements()
    43  	return out
    44  }
    45  
    46  func mergeSelectors(selectors ...k8slabels.Selector) k8slabels.Selector {
    47  	s := k8slabels.NewSelector()
    48  	for _, v := range selectors {
    49  		if v.Empty() {
    50  			continue
    51  		}
    52  		rs, selectable := v.Requirements()
    53  		if selectable {
    54  			s = s.Add(rs...)
    55  		} else {
    56  			logger.Warningf("%v is not selectable", v)
    57  		}
    58  	}
    59  	return s
    60  }
    61  
    62  // requirementParams defines parameters used to create a k8s label requirement.
    63  type requirementParams struct {
    64  	key       string
    65  	operator  selection.Operator
    66  	strValues []string
    67  }
    68  
    69  const regionLabelName = "failure-domain.beta.kubernetes.io/region"
    70  
    71  func getCloudRegionFromNodeMeta(node corev1.Node) (string, string) {
    72  	for cloudType, checkers := range k8sCloudCheckers {
    73  		for _, checker := range checkers {
    74  			if checker.Matches(k8slabels.Set(node.GetLabels())) {
    75  				region := node.Labels[regionLabelName]
    76  				if region == "" && cloudType == kubernetes.K8sCloudMicrok8s {
    77  					region = kubernetes.Microk8sRegion
    78  				}
    79  				return cloudType, region
    80  			}
    81  		}
    82  	}
    83  	return "", ""
    84  }
    85  
    86  func toCaaSStorageProvisioner(sc *storagev1.StorageClass) *kubernetes.StorageProvisioner {
    87  	caasSc := &kubernetes.StorageProvisioner{
    88  		Name:        sc.Name,
    89  		Provisioner: sc.Provisioner,
    90  		Parameters:  sc.Parameters,
    91  	}
    92  	if sc.VolumeBindingMode != nil {
    93  		caasSc.VolumeBindingMode = string(*sc.VolumeBindingMode)
    94  	}
    95  	if sc.ReclaimPolicy != nil {
    96  		caasSc.ReclaimPolicy = string(*sc.ReclaimPolicy)
    97  	}
    98  	return caasSc
    99  }
   100  
   101  // ValidateCloudEndpoint returns nil if the current model can talk to the kubernetes
   102  // endpoint.  Used as validation during model upgrades.
   103  // Implements environs.CloudEndpointChecker
   104  func (k *kubernetesClient) ValidateCloudEndpoint(_ environscontext.ProviderCallContext) error {
   105  	_, err := k.GetClusterMetadata("")
   106  	return errors.Trace(err)
   107  }
   108  
   109  // GetClusterMetadata implements ClusterMetadataChecker. If a nominated storage
   110  // class is provided
   111  func (k *kubernetesClient) GetClusterMetadata(nominatedStorageClass string) (*kubernetes.ClusterMetadata, error) {
   112  	return GetClusterMetadata(
   113  		context.TODO(),
   114  		nominatedStorageClass,
   115  		k.client().CoreV1().Nodes(),
   116  		k.client().StorageV1().StorageClasses(),
   117  	)
   118  }
   119  
   120  // GetClusterMetadata is responsible for gather a Kubernetes cluster metadata
   121  // for Juju to make decisions. This relates to the cloud the cluster may or may
   122  // not be running in + storage available. Split out from the main
   123  // kubernetesClient struct so that it can be tested correctly.
   124  func GetClusterMetadata(
   125  	ctx context.Context,
   126  	nominatedStorageClass string,
   127  	nodeI core.NodeInterface,
   128  	storageClassI storage.StorageClassInterface,
   129  ) (*kubernetes.ClusterMetadata, error) {
   130  	var result kubernetes.ClusterMetadata
   131  	var err error
   132  	result.Cloud, result.Regions, err = listHostCloudRegions(ctx, nodeI)
   133  	if err != nil {
   134  		return nil, errors.Annotate(err, "cannot determine cluster region")
   135  	}
   136  
   137  	// We may have the workload storage but still need to look for operator storage.
   138  	storageClasses, err := storageClassI.List(context.TODO(), v1.ListOptions{})
   139  	if err != nil {
   140  		return nil, errors.Annotate(err, "listing storage classes")
   141  	}
   142  
   143  	preferredOperatorStorage := providerstorage.PreferredOperatorStorageForCloud(result.Cloud).Prepend(
   144  		&providerstorage.PreferredStorageNominated{
   145  			StorageClassName: nominatedStorageClass,
   146  		},
   147  	)
   148  
   149  	preferredWorkloadStorage := providerstorage.PreferredWorkloadStorageForCloud(result.Cloud).Prepend(
   150  		&providerstorage.PreferredStorageNominated{
   151  			StorageClassName: nominatedStorageClass,
   152  		},
   153  	)
   154  
   155  	var (
   156  		selectedOperatorSC *storagev1.StorageClass
   157  		selectedWorkloadSC *storagev1.StorageClass
   158  		operatorPriority   int
   159  		workloadPriority   int
   160  	)
   161  	for i, sc := range storageClasses.Items {
   162  		priority, matches := preferredOperatorStorage.Matches(&sc)
   163  		if matches && (priority < operatorPriority || selectedOperatorSC == nil) {
   164  			selectedOperatorSC = &storageClasses.Items[i]
   165  			operatorPriority = priority
   166  		}
   167  
   168  		priority, matches = preferredWorkloadStorage.Matches(&sc)
   169  		if matches && (priority < workloadPriority || selectedWorkloadSC == nil) {
   170  			selectedWorkloadSC = &storageClasses.Items[i]
   171  			workloadPriority = priority
   172  		}
   173  	}
   174  
   175  	if nominatedStorageClass != "" {
   176  		if selectedOperatorSC == nil || selectedOperatorSC.Name != nominatedStorageClass {
   177  			return nil, &environs.NominatedStorageNotFound{
   178  				StorageName: nominatedStorageClass,
   179  			}
   180  		}
   181  
   182  		if selectedWorkloadSC == nil || selectedWorkloadSC.Name != nominatedStorageClass {
   183  			return nil, &environs.NominatedStorageNotFound{
   184  				StorageName: nominatedStorageClass,
   185  			}
   186  		}
   187  	}
   188  
   189  	result.OperatorStorageClass = selectedOperatorSC
   190  	result.WorkloadStorageClass = selectedWorkloadSC
   191  	return &result, nil
   192  }
   193  
   194  // listHostCloudRegions lists all the cloud regions that this cluster has worker nodes/instances running in.
   195  func listHostCloudRegions(
   196  	ctx context.Context,
   197  	nodeI core.NodeInterface,
   198  ) (string, set.Strings, error) {
   199  	// we only check 5 worker nodes as of now just run in the one region and
   200  	// we are just looking for a running worker to sniff its region.
   201  	nodes, err := nodeI.List(ctx, v1.ListOptions{Limit: 5})
   202  	if err != nil {
   203  		return "", nil, errors.Annotate(err, "listing nodes")
   204  	}
   205  	result := set.NewStrings()
   206  	var cloudResult string
   207  	for _, n := range nodes.Items {
   208  		var nodeCloud, region string
   209  		if nodeCloud, region = getCloudRegionFromNodeMeta(n); nodeCloud == "" {
   210  			continue
   211  		}
   212  		cloudResult = nodeCloud
   213  		result.Add(region)
   214  	}
   215  	return cloudResult, result, nil
   216  }