pkg/util/dynamichelper/dynamichelper.go (145 lines of code) (raw):
package dynamichelper
// Copyright (c) Microsoft Corporation.
// Licensed under the Apache License 2.0.
import (
"context"
"strings"
"github.com/sirupsen/logrus"
kerrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
kruntime "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest"
"k8s.io/client-go/util/retry"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/Azure/ARO-RP/pkg/util/clienthelper"
_ "github.com/Azure/ARO-RP/pkg/util/scheme"
)
type Interface interface {
Refresh() error
EnsureDeleted(ctx context.Context, groupKind, namespace, name string) error
EnsureDeletedGVR(ctx context.Context, groupKind, namespace, name, optionalVersion string) error
Ensure(ctx context.Context, objs ...kruntime.Object) error
IsConstraintTemplateReady(ctx context.Context, name string) (bool, error)
}
type dynamicHelper struct {
GVRResolver
log *logrus.Entry
restcli rest.Interface
dynamicClient dynamic.Interface
}
func New(log *logrus.Entry, restconfig *rest.Config) (Interface, error) {
dh := &dynamicHelper{
log: log,
}
var err error
dh.GVRResolver, err = NewGVRResolver(log, restconfig)
if err != nil {
return nil, err
}
dh.dynamicClient, err = dynamic.NewForConfig(restconfig)
if err != nil {
return nil, err
}
restconfig = rest.CopyConfig(restconfig)
restconfig.NegotiatedSerializer = scheme.Codecs.WithoutConversion()
restconfig.GroupVersion = &schema.GroupVersion{}
dh.restcli, err = rest.RESTClientFor(restconfig)
if err != nil {
return nil, err
}
return dh, nil
}
func (dh *dynamicHelper) EnsureDeleted(ctx context.Context, groupKind, namespace, name string) error {
return dh.EnsureDeletedGVR(ctx, groupKind, namespace, name, "")
}
func (dh *dynamicHelper) EnsureDeletedGVR(ctx context.Context, groupKind, namespace, name, optionalVersion string) error {
gvr, err := dh.Resolve(groupKind, optionalVersion)
if err != nil {
return err
}
// gatekeeper policies are unstructured and should be deleted differently
if isKindUnstructured(groupKind) {
dh.log.Infof("Delete unstructured obj kind %s ns %s name %s version %s", groupKind, namespace, name, optionalVersion)
return dh.deleteUnstructuredObj(ctx, groupKind, namespace, name)
}
dh.log.Infof("Delete kind %s ns %s name %s", groupKind, namespace, name)
err = dh.restcli.Delete().AbsPath(makeURLSegments(gvr, namespace, name)...).Do(ctx).Error()
if kerrors.IsNotFound(err) {
err = nil
}
return err
}
// Ensure that one or more objects match their desired state. Only update
// objects that need to be updated.
func (dh *dynamicHelper) Ensure(ctx context.Context, objs ...kruntime.Object) error {
for _, o := range objs {
if un, ok := o.(*unstructured.Unstructured); ok {
err := dh.ensureUnstructuredObj(ctx, un)
if err != nil {
return err
}
continue
}
err := dh.ensureOne(ctx, o)
if err != nil {
return err
}
}
return nil
}
func (dh *dynamicHelper) ensureOne(ctx context.Context, new kruntime.Object) error {
gvks, _, err := scheme.Scheme.ObjectKinds(new)
if err != nil {
return err
}
gvk := gvks[0]
gvr, err := dh.Resolve(gvk.GroupKind().String(), gvk.Version)
if err != nil {
return err
}
acc, err := meta.Accessor(new)
if err != nil {
return err
}
return retry.RetryOnConflict(retry.DefaultRetry, func() error {
old, err := dh.restcli.Get().AbsPath(makeURLSegments(gvr, acc.GetNamespace(), acc.GetName())...).Do(ctx).Get()
if kerrors.IsNotFound(err) {
dh.log.Infof("Create %s", keyFunc(gvk.GroupKind(), acc.GetNamespace(), acc.GetName()))
return dh.restcli.Post().AbsPath(makeURLSegments(gvr, acc.GetNamespace(), "")...).Body(new).Do(ctx).Error()
}
if err != nil {
return err
}
candidate, changed, diff, err := dh.mergeWithLogic(acc.GetName(), gvk.GroupKind().String(), old, new)
if err != nil || !changed {
return err
}
dh.log.Infof("Update %s: %s", keyFunc(gvk.GroupKind(), acc.GetNamespace(), acc.GetName()), diff)
return dh.restcli.Put().AbsPath(makeURLSegments(gvr, acc.GetNamespace(), acc.GetName())...).Body(candidate).Do(ctx).Error()
})
}
func (dh *dynamicHelper) mergeWithLogic(name, groupKind string, old, new kruntime.Object) (kruntime.Object, bool, string, error) {
if strings.HasPrefix(name, "gatekeeper") {
dh.log.Debugf("Skip updating %s: %s", name, groupKind)
return nil, false, "", nil
}
if strings.HasPrefix(groupKind, "ConstraintTemplate.templates.gatekeeper") {
return mergeGK(old, new)
}
return clienthelper.Merge(old.(client.Object), new.(client.Object))
}
func makeURLSegments(gvr *schema.GroupVersionResource, namespace, name string) (url []string) {
if gvr.Group == "" {
url = append(url, "api")
} else {
url = append(url, "apis", gvr.Group)
}
url = append(url, gvr.Version)
if namespace != "" {
url = append(url, "namespaces", namespace)
}
url = append(url, gvr.Resource)
if len(name) > 0 {
url = append(url, name)
}
return url
}