mirror of
https://github.com/open-cluster-management-io/ocm.git
synced 2026-02-14 18:09:57 +00:00
Some checks failed
Scorecard supply-chain security / Scorecard analysis (push) Failing after 1m25s
Post / coverage (push) Failing after 36m59s
Post / images (amd64, addon-manager) (push) Failing after 7m34s
Post / images (amd64, placement) (push) Failing after 7m4s
Post / images (amd64, registration) (push) Failing after 7m8s
Post / images (amd64, registration-operator) (push) Failing after 7m3s
Post / images (amd64, work) (push) Failing after 6m59s
Post / images (arm64, addon-manager) (push) Failing after 7m0s
Post / images (arm64, placement) (push) Failing after 6m54s
Post / images (arm64, registration) (push) Failing after 6m55s
Post / images (arm64, registration-operator) (push) Failing after 6m55s
Post / images (arm64, work) (push) Failing after 7m16s
Post / image manifest (addon-manager) (push) Has been skipped
Post / image manifest (placement) (push) Has been skipped
Post / image manifest (registration) (push) Has been skipped
Post / image manifest (registration-operator) (push) Has been skipped
Post / image manifest (work) (push) Has been skipped
Post / trigger clusteradm e2e (push) Has been skipped
Signed-off-by: Jian Qiu <jqiu@redhat.com>
287 lines
6.9 KiB
Go
287 lines
6.9 KiB
Go
package crdmanager
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/openshift/library-go/pkg/operator/resource/resourceapply"
|
|
"github.com/openshift/library-go/pkg/operator/resource/resourcemerge"
|
|
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
|
"k8s.io/apimachinery/pkg/api/meta"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/runtime"
|
|
"k8s.io/apimachinery/pkg/runtime/serializer"
|
|
utilerrors "k8s.io/apimachinery/pkg/util/errors"
|
|
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
|
versionutil "k8s.io/apimachinery/pkg/util/version"
|
|
"k8s.io/klog/v2"
|
|
"k8s.io/utils/pointer"
|
|
|
|
"open-cluster-management.io/ocm/pkg/version"
|
|
)
|
|
|
|
// versionAnnotationKey is an annotation key on crd resources to mark the ocm version of the crds.
|
|
const (
|
|
versionAnnotationKey = "operator.open-cluster-management.io/version"
|
|
// defaultVersion is set if gitVersion cannot be obtained. It is the lownest version so crd is updated as long
|
|
// as it has a higher version. It also ensures the crd spec is still compared
|
|
// for update when version is not obtained.
|
|
defaultVersion = "0.0.0"
|
|
)
|
|
|
|
var (
|
|
genericScheme = runtime.NewScheme()
|
|
genericCodecs = serializer.NewCodecFactory(genericScheme)
|
|
genericCodec = genericCodecs.UniversalDeserializer()
|
|
)
|
|
|
|
func init() {
|
|
utilruntime.Must(apiextensionsv1.AddToScheme(genericScheme))
|
|
}
|
|
|
|
type CRD interface {
|
|
*apiextensionsv1.CustomResourceDefinition
|
|
}
|
|
|
|
type Manager[T CRD] struct {
|
|
client crdClient[T]
|
|
equal func(old, new T) bool
|
|
version *versionutil.Version
|
|
}
|
|
|
|
type crdClient[T CRD] interface {
|
|
Get(ctx context.Context, name string, opt metav1.GetOptions) (T, error)
|
|
Create(ctx context.Context, obj T, opt metav1.CreateOptions) (T, error)
|
|
Update(ctx context.Context, obj T, opt metav1.UpdateOptions) (T, error)
|
|
Delete(ctx context.Context, name string, opt metav1.DeleteOptions) error
|
|
}
|
|
|
|
type RemainingCRDError struct {
|
|
RemainingCRDs []string
|
|
}
|
|
|
|
func (r *RemainingCRDError) Error() string {
|
|
return fmt.Sprintf("There are still remaining CRDs: %s", strings.Join(r.RemainingCRDs, ","))
|
|
}
|
|
|
|
func NewManager[T CRD](client crdClient[T], equalFunc func(old, new T) bool) *Manager[T] {
|
|
gitVersion := version.Get().GitVersion
|
|
if len(gitVersion) == 0 {
|
|
gitVersion = defaultVersion
|
|
}
|
|
v, err := versionutil.ParseGeneric(gitVersion)
|
|
if err != nil {
|
|
utilruntime.HandleError(err)
|
|
}
|
|
manager := &Manager[T]{
|
|
client: client,
|
|
equal: equalFunc,
|
|
version: v,
|
|
}
|
|
|
|
return manager
|
|
}
|
|
|
|
func (m *Manager[T]) CleanOne(ctx context.Context, name string, skip bool) error {
|
|
// remove version annotation if skip clean
|
|
if skip {
|
|
existing, err := m.client.Get(ctx, name, metav1.GetOptions{})
|
|
switch {
|
|
case apierrors.IsNotFound(err):
|
|
return nil
|
|
case err != nil:
|
|
return err
|
|
}
|
|
accessor, err := meta.Accessor(existing)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
annotations := accessor.GetAnnotations()
|
|
if annotations == nil {
|
|
return nil
|
|
}
|
|
v, ok := annotations[versionAnnotationKey]
|
|
if !ok {
|
|
return nil
|
|
}
|
|
cnt, err := m.version.Compare(v)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if cnt != 0 {
|
|
return nil
|
|
}
|
|
delete(annotations, versionAnnotationKey)
|
|
accessor.SetAnnotations(annotations)
|
|
_, err = m.client.Update(ctx, existing, metav1.UpdateOptions{})
|
|
return err
|
|
}
|
|
|
|
err := m.client.Delete(ctx, name, metav1.DeleteOptions{})
|
|
switch {
|
|
case apierrors.IsNotFound(err):
|
|
return nil
|
|
case err == nil:
|
|
return &RemainingCRDError{RemainingCRDs: []string{name}}
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
func (m *Manager[T]) Clean(ctx context.Context, skip bool, manifests resourceapply.AssetFunc, files ...string) error {
|
|
var errs []error
|
|
var remainingCRDs []string
|
|
|
|
for _, file := range files {
|
|
objBytes, err := manifests(file)
|
|
if err != nil {
|
|
errs = append(errs, err)
|
|
continue
|
|
}
|
|
|
|
requiredObj, _, err := genericCodec.Decode(objBytes, nil, nil)
|
|
if err != nil {
|
|
errs = append(errs, err)
|
|
continue
|
|
}
|
|
|
|
accessor, err := meta.Accessor(requiredObj)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = m.CleanOne(ctx, accessor.GetName(), skip)
|
|
var remainingErr *RemainingCRDError
|
|
switch {
|
|
case errors.As(err, &remainingErr):
|
|
remainingCRDs = append(remainingCRDs, accessor.GetName())
|
|
case err != nil:
|
|
errs = append(errs, err)
|
|
}
|
|
}
|
|
|
|
if len(errs) > 0 {
|
|
return utilerrors.NewAggregate(errs)
|
|
}
|
|
if len(remainingCRDs) > 0 {
|
|
return &RemainingCRDError{RemainingCRDs: remainingCRDs}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (m *Manager[T]) Apply(ctx context.Context, manifests resourceapply.AssetFunc, files ...string) error {
|
|
var errs []error
|
|
|
|
for _, file := range files {
|
|
objBytes, err := manifests(file)
|
|
if err != nil {
|
|
errs = append(errs, err)
|
|
continue
|
|
}
|
|
|
|
requiredObj, _, err := genericCodec.Decode(objBytes, nil, nil)
|
|
if err != nil {
|
|
errs = append(errs, err)
|
|
continue
|
|
}
|
|
|
|
err = m.applyOne(ctx, requiredObj.(T))
|
|
if err != nil {
|
|
errs = append(errs, err)
|
|
}
|
|
}
|
|
|
|
return utilerrors.NewAggregate(errs)
|
|
}
|
|
|
|
func (m *Manager[T]) applyOne(ctx context.Context, required T) error {
|
|
logger := klog.FromContext(ctx)
|
|
|
|
accessor, err := meta.Accessor(required)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
existing, err := m.client.Get(ctx, accessor.GetName(), metav1.GetOptions{})
|
|
if apierrors.IsNotFound(err) {
|
|
_, err := m.client.Create(ctx, required, metav1.CreateOptions{})
|
|
logger.Info("crd is created", "crdName", accessor.GetName())
|
|
return err
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ok, err := m.shouldUpdate(existing, required)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if !ok {
|
|
return nil
|
|
}
|
|
|
|
existingAccessor, err := meta.Accessor(existing)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
annotations := accessor.GetAnnotations()
|
|
if annotations == nil {
|
|
annotations = map[string]string{}
|
|
}
|
|
annotations[versionAnnotationKey] = m.version.String()
|
|
accessor.SetAnnotations(annotations)
|
|
accessor.SetResourceVersion(existingAccessor.GetResourceVersion())
|
|
|
|
_, err = m.client.Update(ctx, required, metav1.UpdateOptions{})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
logger.Info("crd is updated", "crdName", accessor.GetName(), "version", m.version.String())
|
|
|
|
return nil
|
|
}
|
|
|
|
func (m *Manager[T]) shouldUpdate(old, new T) (bool, error) {
|
|
// if existingVersion is higher than the required version, do not update crd.
|
|
accessor, err := meta.Accessor(old)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
var existingVersion string
|
|
if accessor.GetAnnotations() != nil {
|
|
existingVersion = accessor.GetAnnotations()[versionAnnotationKey]
|
|
}
|
|
|
|
// always update if existing doest not have version annotation
|
|
if len(existingVersion) == 0 {
|
|
return true, nil
|
|
}
|
|
|
|
cnt, err := m.version.Compare(existingVersion)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
// if the version are the same, compare the spec
|
|
if cnt == 0 {
|
|
return !m.equal(old, new), nil
|
|
}
|
|
|
|
// do not update when version is higher
|
|
return cnt > 0, nil
|
|
}
|
|
|
|
func EqualV1(old, new *apiextensionsv1.CustomResourceDefinition) bool {
|
|
modified := pointer.Bool(false)
|
|
|
|
resourcemerge.EnsureCustomResourceDefinitionV1(modified, old, *new)
|
|
return !*modified
|
|
}
|