github.com/cilium/cilium@v1.16.2/pkg/k8s/apis/crdhelpers/register.go (about) 1 // SPDX-License-Identifier: Apache-2.0 2 // Copyright Authors of Cilium 3 4 package crdhelpers 5 6 import ( 7 "context" 8 goerrors "errors" 9 "fmt" 10 11 "github.com/blang/semver/v4" 12 "github.com/sirupsen/logrus" 13 apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" 14 apiextensionsclient "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" 15 v1client "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1" 16 "k8s.io/apimachinery/pkg/api/errors" 17 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 18 "k8s.io/apimachinery/pkg/util/wait" 19 20 "github.com/cilium/cilium/pkg/logging" 21 "github.com/cilium/cilium/pkg/logging/logfields" 22 "github.com/cilium/cilium/pkg/time" 23 "github.com/cilium/cilium/pkg/versioncheck" 24 ) 25 26 // subsysK8s is the value for logfields.LogSubsys 27 const subsysK8s = "k8s" 28 29 // log is the k8s package logger object. 30 var log = logging.DefaultLogger.WithField(logfields.LogSubsys, subsysK8s) 31 32 // CreateUpdateCRD ensures the CRD object is installed into the K8s cluster. It 33 // will create or update the CRD and its validation schema as necessary. This 34 // function only accepts v1 CRD objects. 35 func CreateUpdateCRD( 36 clientset apiextensionsclient.Interface, 37 crd *apiextensionsv1.CustomResourceDefinition, 38 poller poller, 39 crdSchemaVersionLabelKey string, 40 minCRDSchemaVersion semver.Version, 41 ) error { 42 scopedLog := log.WithField("name", crd.Name) 43 44 v1CRDClient := clientset.ApiextensionsV1() 45 clusterCRD, err := v1CRDClient.CustomResourceDefinitions().Get( 46 context.TODO(), 47 crd.ObjectMeta.Name, 48 metav1.GetOptions{}) 49 if errors.IsNotFound(err) { 50 scopedLog.Info("Creating CRD (CustomResourceDefinition)...") 51 52 clusterCRD, err = v1CRDClient.CustomResourceDefinitions().Create( 53 context.TODO(), 54 crd, 55 metav1.CreateOptions{}) 56 // This occurs when multiple agents race to create the CRD. Since another has 57 // created it, it will also update it, hence the non-error return. 58 if errors.IsAlreadyExists(err) { 59 return nil 60 } 61 } 62 if err != nil { 63 return err 64 } 65 66 if err := updateV1CRD(scopedLog, crd, clusterCRD, v1CRDClient, poller, crdSchemaVersionLabelKey, minCRDSchemaVersion); err != nil { 67 return err 68 } 69 if err := waitForV1CRD(scopedLog, clusterCRD, v1CRDClient, poller); err != nil { 70 return err 71 } 72 73 scopedLog.Info("CRD (CustomResourceDefinition) is installed and up-to-date") 74 75 return nil 76 } 77 78 func needsUpdateV1( 79 clusterCRD *apiextensionsv1.CustomResourceDefinition, 80 crdSchemaVersionLabelKey string, 81 minCRDSchemaVersion semver.Version, 82 ) bool { 83 if clusterCRD.Spec.Versions[0].Schema == nil { 84 // no validation detected 85 return true 86 } 87 v, ok := clusterCRD.Labels[crdSchemaVersionLabelKey] 88 if !ok { 89 // no schema version detected 90 return true 91 } 92 93 clusterVersion, err := versioncheck.Version(v) 94 if err != nil || clusterVersion.LT(minCRDSchemaVersion) { 95 // version in cluster is either unparsable or smaller than current version 96 return true 97 } 98 99 return false 100 } 101 102 func updateV1CRD( 103 scopedLog *logrus.Entry, 104 crd, clusterCRD *apiextensionsv1.CustomResourceDefinition, 105 client v1client.CustomResourceDefinitionsGetter, 106 poller poller, 107 crdSchemaVersionLabelKey string, 108 minCRDSchemaVersion semver.Version, 109 ) error { 110 scopedLog.Debug("Checking if CRD (CustomResourceDefinition) needs update...") 111 112 if crd.Spec.Versions[0].Schema != nil && needsUpdateV1(clusterCRD, crdSchemaVersionLabelKey, minCRDSchemaVersion) { 113 scopedLog.Info("Updating CRD (CustomResourceDefinition)...") 114 115 // Update the CRD with the validation schema. 116 err := poller.Poll(500*time.Millisecond, 60*time.Second, func() (bool, error) { 117 var err error 118 clusterCRD, err = client.CustomResourceDefinitions().Get( 119 context.TODO(), 120 crd.ObjectMeta.Name, 121 metav1.GetOptions{}) 122 if err != nil { 123 return false, err 124 } 125 126 // This seems too permissive but we only get here if the version is 127 // different per needsUpdate above. If so, we want to update on any 128 // validation change including adding or removing validation. 129 if needsUpdateV1(clusterCRD, crdSchemaVersionLabelKey, minCRDSchemaVersion) { 130 scopedLog.Debug("CRD validation is different, updating it...") 131 132 clusterCRD.ObjectMeta.Labels = crd.ObjectMeta.Labels 133 clusterCRD.Spec = crd.Spec 134 135 // Even though v1 CRDs omit this field by default (which also 136 // means it's false) it is still carried over from the previous 137 // CRD. Therefore, we must set this to false explicitly because 138 // the apiserver will carry over the old value (true). 139 clusterCRD.Spec.PreserveUnknownFields = false 140 141 _, err := client.CustomResourceDefinitions().Update( 142 context.TODO(), 143 clusterCRD, 144 metav1.UpdateOptions{}) 145 switch { 146 case errors.IsConflict(err): // Occurs as Operators race to update CRDs. 147 scopedLog.WithError(err). 148 Debug("The CRD update was based on an older version, retrying...") 149 return false, nil 150 case err == nil: 151 return true, nil 152 } 153 154 scopedLog.WithError(err).Debug("Unable to update CRD validation") 155 156 return false, err 157 } 158 159 return true, nil 160 }) 161 if err != nil { 162 scopedLog.WithError(err).Error("Unable to update CRD") 163 return err 164 } 165 } 166 167 return nil 168 } 169 170 func waitForV1CRD( 171 scopedLog *logrus.Entry, 172 crd *apiextensionsv1.CustomResourceDefinition, 173 client v1client.CustomResourceDefinitionsGetter, 174 poller poller, 175 ) error { 176 scopedLog.Debug("Waiting for CRD (CustomResourceDefinition) to be available...") 177 178 err := poller.Poll(500*time.Millisecond, 60*time.Second, func() (bool, error) { 179 for _, cond := range crd.Status.Conditions { 180 switch cond.Type { 181 case apiextensionsv1.Established: 182 if cond.Status == apiextensionsv1.ConditionTrue { 183 return true, nil 184 } 185 case apiextensionsv1.NamesAccepted: 186 if cond.Status == apiextensionsv1.ConditionFalse { 187 err := goerrors.New(cond.Reason) 188 scopedLog.WithError(err).Error("Name conflict for CRD") 189 return false, err 190 } 191 } 192 } 193 194 var err error 195 if crd, err = client.CustomResourceDefinitions().Get( 196 context.TODO(), 197 crd.ObjectMeta.Name, 198 metav1.GetOptions{}); err != nil { 199 return false, err 200 } 201 return false, err 202 }) 203 if err != nil { 204 return fmt.Errorf("error occurred waiting for CRD: %w", err) 205 } 206 207 return nil 208 } 209 210 // poller is an interface that abstracts the polling logic when dealing with 211 // CRD changes / updates to the apiserver. The reason this exists is mainly for 212 // unit-testing. 213 type poller interface { 214 Poll(interval, duration time.Duration, conditionFn func() (bool, error)) error 215 } 216 217 func NewDefaultPoller() defaultPoll { 218 return defaultPoll{} 219 } 220 221 type defaultPoll struct{} 222 223 func (p defaultPoll) Poll( 224 interval, duration time.Duration, 225 conditionFn func() (bool, error), 226 ) error { 227 return wait.Poll(interval, duration, conditionFn) 228 }