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  }