Copyright 2021 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package apply
import (
"context"
"fmt"
"strconv"
hashstructure "github.com/mitchellh/hashstructure/v2"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
"github.com/oam-dev/kubevela/pkg/oam"
"github.com/pkg/errors"
corev1 "k8s.io/api/core/v1"
kerrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/types"
"k8s.io/klog/v2"
"sigs.k8s.io/controller-runtime/pkg/client"
)
const (
LabelRenderHash = "oam.dev/render-hash"
)
type Applicator interface {
Apply(context.Context, client.Object, ...ApplyOption) error
}
type applyAction struct {
skipUpdate bool
updateAnnotation bool
}
type ApplyOption func(act *applyAction, existing, desired client.Object) error
func NewAPIApplicator(c client.Client) *APIApplicator {
return &APIApplicator{
creator: creatorFn(createOrGetExisting),
patcher: patcherFn(threeWayMergePatch),
c: c,
}
}
type creator interface {
createOrGetExisting(context.Context, *applyAction, client.Client, client.Object, ...ApplyOption) (client.Object, error)
}
type creatorFn func(context.Context, *applyAction, client.Client, client.Object, ...ApplyOption) (client.Object, error)
func (fn creatorFn) createOrGetExisting(ctx context.Context, act *applyAction, c client.Client, o client.Object, ao ...ApplyOption) (client.Object, error) {
return fn(ctx, act, c, o, ao...)
}
type patcher interface {
patch(c, m client.Object, a *applyAction) (client.Patch, error)
}
type patcherFn func(c, m client.Object, a *applyAction) (client.Patch, error)
func (fn patcherFn) patch(c, m client.Object, a *applyAction) (client.Patch, error) {
return fn(c, m, a)
}
type APIApplicator struct {
creator
patcher
c client.Client
}
func loggingApply(msg string, desired client.Object) {
d, ok := desired.(metav1.Object)
if !ok {
klog.InfoS(msg, "resource", desired.GetObjectKind().GroupVersionKind().String())
return
}
klog.InfoS(msg, "name", d.GetName(), "resource", desired.GetObjectKind().GroupVersionKind().String())
}
func filterRecordForSpecial(desired client.Object) bool {
if desired == nil {
return false
}
gvk := desired.GetObjectKind().GroupVersionKind()
gp, kd := gvk.Group, gvk.Kind
if gp == "" {
if kd == "Secret" || kd == "ConfigMap" {
return false
}
if _, ok := desired.(*corev1.ConfigMap); ok {
return false
}
if _, ok := desired.(*corev1.Secret); ok {
return false
}
}
ann := desired.GetAnnotations()
if ann != nil {
lac := ann[oam.AnnotationLastAppliedConfig]
if lac == "-" || lac == "skip" {
return false
}
}
return true
}
func (a *APIApplicator) Apply(ctx context.Context, desired client.Object, ao ...ApplyOption) error {
_, err := generateRenderHash(desired)
if err != nil {
return err
}
applyAct := &applyAction{updateAnnotation: filterRecordForSpecial(desired)}
existing, err := a.createOrGetExisting(ctx, applyAct, a.c, desired, ao...)
if err != nil {
return err
}
if existing == nil {
return nil
}
if err := executeApplyOptions(applyAct, existing, desired, ao); err != nil {
return err
}
if applyAct.skipUpdate {
loggingApply("skip update", desired)
return nil
}
loggingApply("patching object", desired)
patch, err := a.patcher.patch(existing, desired, applyAct)
if err != nil {
return errors.Wrap(err, "cannot calculate patch by computing a three way diff")
}
return errors.Wrapf(a.c.Patch(ctx, desired, patch), "cannot patch object")
}
func ComputeSpecHash(spec interface{}) (string, error) {
specHash, err := hashstructure.Hash(spec, hashstructure.FormatV2, nil)
if err != nil {
return "", err
}
specHashLabel := strconv.FormatUint(specHash, 16)
return specHashLabel, nil
}
func generateRenderHash(desired client.Object) (string, error) {
if desired == nil {
return "", nil
}
desiredHash, err := ComputeSpecHash(desired)
if err != nil {
return "", errors.Wrap(err, "compute desired hash")
}
AddLabels(desired, map[string]string{
LabelRenderHash: desiredHash,
})
return desiredHash, nil
}
func getRenderHash(existing client.Object) string {
labels := existing.GetLabels()
if labels == nil {
return ""
}
return labels[LabelRenderHash]
}
func createOrGetExisting(ctx context.Context, act *applyAction, c client.Client, desired client.Object, ao ...ApplyOption) (client.Object, error) {
var create = func() (client.Object, error) {
if err := executeApplyOptions(act, nil, desired, ao); err != nil {
return nil, err
}
if act.updateAnnotation {
if err := addLastAppliedConfigAnnotation(desired); err != nil {
return nil, err
}
}
loggingApply("creating object", desired)
return nil, errors.Wrap(c.Create(ctx, desired), "cannot create object")
}
if desired.GetName() == "" && desired.GetGenerateName() != "" {
return create()
}
existing := &unstructured.Unstructured{}
existing.GetObjectKind().SetGroupVersionKind(desired.GetObjectKind().GroupVersionKind())
err := c.Get(ctx, types.NamespacedName{Name: desired.GetName(), Namespace: desired.GetNamespace()}, existing)
if kerrors.IsNotFound(err) {
return create()
}
if err != nil {
return nil, errors.Wrap(err, "cannot get object")
}
return existing, nil
}
func executeApplyOptions(act *applyAction, existing, desired client.Object, aos []ApplyOption) error {
for _, fn := range aos {
if err := fn(act, existing, desired); err != nil {
return errors.Wrap(err, "cannot apply ApplyOption")
}
}
return nil
}
func NotUpdateRenderHashEqual() ApplyOption {
return func(act *applyAction, existing, desired client.Object) error {
if existing == nil || desired == nil {
return nil
}
newSt, ok := desired.(*unstructured.Unstructured)
if !ok {
return nil
}
oldSt := existing.(*unstructured.Unstructured)
if !ok {
return nil
}
if getRenderHash(existing) == getRenderHash(desired) {
*newSt = *oldSt
act.skipUpdate = true
}
return nil
}
}
func MustBeControllableBy(u types.UID) ApplyOption {
return func(_ *applyAction, existing, _ client.Object) error {
if existing == nil {
return nil
}
c := metav1.GetControllerOf(existing.(metav1.Object))
if c == nil {
return nil
}
if c.UID != u {
return errors.Errorf("existing object is not controlled by UID %q", u)
}
return nil
}
}
func MustBeControlledByApp(app *v1beta1.Application) ApplyOption {
return func(_ *applyAction, existing, _ client.Object) error {
if existing == nil {
return nil
}
labels := existing.GetLabels()
if labels == nil {
return nil
}
if appName, exists := labels[oam.LabelAppName]; exists && appName != app.Name {
return fmt.Errorf("existing object is managed by other application %s", appName)
}
ns := app.Namespace
if ns == "" {
ns = metav1.NamespaceDefault
}
if appNs, exists := labels[oam.LabelAppNamespace]; exists && appNs != ns {
return fmt.Errorf("existing object is managed by other application %s/%s", appNs, labels[oam.LabelAppName])
}
return nil
}
}
func MakeCustomApplyOption(f func(existing, desired client.Object) error) ApplyOption {
return func(act *applyAction, existing, desired client.Object) error {
return f(existing, desired)
}
}
func DisableUpdateAnnotation() ApplyOption {
return func(a *applyAction, existing, _ client.Object) error {
a.updateAnnotation = false
return nil
}
}
func MergeMapOverrideWithDst(src, dst map[string]string) map[string]string {
if src == nil && dst == nil {
return nil
}
r := make(map[string]string)
for k, v := range src {
r[k] = v
}
for k, v := range dst {
r[k] = v
}
return r
}
type labelAnnotationObject interface {
GetLabels() map[string]string
SetLabels(labels map[string]string)
GetAnnotations() map[string]string
SetAnnotations(annotations map[string]string)
}
func AddLabels(o labelAnnotationObject, labels map[string]string) {
o.SetLabels(MergeMapOverrideWithDst(o.GetLabels(), labels))
}