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 }