github.com/oam-dev/kubevela@v1.9.11/pkg/component/ref_objects.go (about)

     1  /*
     2  Copyright 2021 The KubeVela Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8  	http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package component
    18  
    19  import (
    20  	"context"
    21  	"encoding/json"
    22  	"fmt"
    23  	"strings"
    24  
    25  	"github.com/kubevela/pkg/util/slices"
    26  	"github.com/pkg/errors"
    27  	corev1 "k8s.io/api/core/v1"
    28  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    29  	"k8s.io/apimachinery/pkg/api/meta"
    30  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    31  	"k8s.io/apimachinery/pkg/labels"
    32  	"k8s.io/apimachinery/pkg/runtime"
    33  	"k8s.io/apimachinery/pkg/runtime/schema"
    34  	utilfeature "k8s.io/apiserver/pkg/util/feature"
    35  	"sigs.k8s.io/controller-runtime/pkg/client"
    36  
    37  	velaclient "github.com/kubevela/pkg/controller/client"
    38  
    39  	"github.com/oam-dev/kubevela/apis/core.oam.dev/common"
    40  	"github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha1"
    41  	"github.com/oam-dev/kubevela/pkg/features"
    42  	"github.com/oam-dev/kubevela/pkg/multicluster"
    43  	"github.com/oam-dev/kubevela/pkg/oam"
    44  )
    45  
    46  const (
    47  	// RefObjectsAvailableScopeGlobal ref-objects component can refer to arbitrary objects in any cluster
    48  	RefObjectsAvailableScopeGlobal = "global"
    49  	// RefObjectsAvailableScopeCluster ref-objects component can only refer to objects inside the hub cluster
    50  	RefObjectsAvailableScopeCluster = "cluster"
    51  	// RefObjectsAvailableScopeNamespace ref-objects component can only refer to objects inside the application namespace
    52  	RefObjectsAvailableScopeNamespace = "namespace"
    53  )
    54  
    55  // RefObjectsAvailableScope indicates the available scope for objects to refer
    56  var RefObjectsAvailableScope = RefObjectsAvailableScopeGlobal
    57  
    58  // GetLabelSelectorFromRefObjectSelector extract labelSelector from `labelSelector` first. If empty, extract from `selector`
    59  func GetLabelSelectorFromRefObjectSelector(selector v1alpha1.ObjectReferrer) map[string]string {
    60  	if selector.LabelSelector != nil {
    61  		return selector.LabelSelector
    62  	}
    63  	if utilfeature.DefaultMutableFeatureGate.Enabled(features.DeprecatedObjectLabelSelector) {
    64  		return selector.DeprecatedLabelSelector
    65  	}
    66  	return nil
    67  }
    68  
    69  // GetGroupVersionKindFromRefObjectSelector extract GroupVersionKind by Resource if provided, otherwise, extract from APIVersion and Kind directly
    70  func GetGroupVersionKindFromRefObjectSelector(mapper meta.RESTMapper, selector v1alpha1.ObjectReferrer) (schema.GroupVersionKind, error) {
    71  	if selector.Resource != "" {
    72  		gvks, err := mapper.KindsFor(schema.GroupVersionResource{Group: selector.Group, Resource: selector.Resource})
    73  		if err != nil {
    74  			return schema.GroupVersionKind{}, err
    75  		}
    76  		if len(gvks) == 0 {
    77  			return schema.GroupVersionKind{}, errors.Errorf("no kind found for resource %s", selector.Resource)
    78  		}
    79  		return gvks[0], nil
    80  	}
    81  	if utilfeature.DefaultMutableFeatureGate.Enabled(features.LegacyObjectTypeIdentifier) {
    82  		if selector.APIVersion != "" && selector.Kind != "" {
    83  			gv, err := schema.ParseGroupVersion(selector.APIVersion)
    84  			if err != nil {
    85  				return schema.GroupVersionKind{}, errors.Wrapf(err, "invalid APIVersion")
    86  			}
    87  			return gv.WithKind(selector.Kind), nil
    88  		}
    89  		return schema.GroupVersionKind{}, errors.Errorf("neither resource or apiVersion/kind is set for referring objects")
    90  	}
    91  	return schema.GroupVersionKind{}, errors.Errorf("resource is not set and legacy object type identifier is disabled for referring objects")
    92  }
    93  
    94  // ValidateRefObjectSelector validate if exclusive fields are set for the selector
    95  func ValidateRefObjectSelector(selector v1alpha1.ObjectReferrer) error {
    96  	labelSelector := GetLabelSelectorFromRefObjectSelector(selector)
    97  	if labelSelector != nil && selector.Name != "" {
    98  		return errors.Errorf("invalid object selector for ref-objects, name and labelSelector cannot be both set")
    99  	}
   100  	return nil
   101  }
   102  
   103  // ClearRefObjectForDispatch reset the objects for dispatch
   104  func ClearRefObjectForDispatch(un *unstructured.Unstructured) {
   105  	un.SetResourceVersion("")
   106  	un.SetGeneration(0)
   107  	un.SetOwnerReferences(nil)
   108  	un.SetDeletionTimestamp(nil)
   109  	un.SetManagedFields(nil)
   110  	un.SetUID("")
   111  	unstructured.RemoveNestedField(un.Object, "metadata", "creationTimestamp")
   112  	unstructured.RemoveNestedField(un.Object, "status")
   113  	// TODO(somefive): make the following logic more generalizable
   114  	if un.GetKind() == "Service" && un.GetAPIVersion() == "v1" {
   115  		if clusterIP, exist, _ := unstructured.NestedString(un.Object, "spec", "clusterIP"); exist && clusterIP != corev1.ClusterIPNone {
   116  			unstructured.RemoveNestedField(un.Object, "spec", "clusterIP")
   117  			unstructured.RemoveNestedField(un.Object, "spec", "clusterIPs")
   118  		}
   119  	}
   120  }
   121  
   122  // SelectRefObjectsForDispatch select objects by selector from kubernetes
   123  func SelectRefObjectsForDispatch(ctx context.Context, cli client.Client, appNs string, compName string, selector v1alpha1.ObjectReferrer) (objs []*unstructured.Unstructured, err error) {
   124  	if err = ValidateRefObjectSelector(selector); err != nil {
   125  		return nil, err
   126  	}
   127  	labelSelector := GetLabelSelectorFromRefObjectSelector(selector)
   128  	ns := appNs
   129  	if selector.Namespace != "" {
   130  		if RefObjectsAvailableScope == RefObjectsAvailableScopeNamespace {
   131  			return nil, errors.Errorf("cannot refer to objects outside the application's namespace")
   132  		}
   133  		ns = selector.Namespace
   134  	}
   135  	if selector.Cluster != "" && selector.Cluster != multicluster.ClusterLocalName {
   136  		if RefObjectsAvailableScope != RefObjectsAvailableScopeGlobal {
   137  			return nil, errors.Errorf("cannot refer to objects outside control plane")
   138  		}
   139  		ctx = multicluster.ContextWithClusterName(ctx, selector.Cluster)
   140  	}
   141  	gvk, err := GetGroupVersionKindFromRefObjectSelector(cli.RESTMapper(), selector)
   142  	if err != nil {
   143  		return nil, err
   144  	}
   145  	isNamespaced, err := IsGroupVersionKindNamespaceScoped(cli.RESTMapper(), gvk)
   146  	if err != nil {
   147  		return nil, err
   148  	}
   149  	if selector.Name == "" && labelSelector != nil {
   150  		uns := &unstructured.UnstructuredList{}
   151  		uns.SetGroupVersionKind(gvk)
   152  		opts := []client.ListOption{client.MatchingLabels(labelSelector)}
   153  		if isNamespaced {
   154  			opts = append(opts, client.InNamespace(ns))
   155  		}
   156  		if err = cli.List(ctx, uns, opts...); err != nil {
   157  			return nil, errors.Wrapf(err, "failed to load ref object %s with selector", gvk.Kind)
   158  		}
   159  		for _, _un := range uns.Items {
   160  			objs = append(objs, _un.DeepCopy())
   161  		}
   162  	} else {
   163  		un := &unstructured.Unstructured{}
   164  		un.SetGroupVersionKind(gvk)
   165  		un.SetName(selector.Name)
   166  		if isNamespaced {
   167  			un.SetNamespace(ns)
   168  		}
   169  		if selector.Name == "" {
   170  			un.SetName(compName)
   171  		}
   172  		if err := cli.Get(ctx, client.ObjectKeyFromObject(un), un); err != nil {
   173  			return nil, errors.Wrapf(err, "failed to load ref object %s %s/%s", un.GetKind(), un.GetNamespace(), un.GetName())
   174  		}
   175  		objs = append(objs, un)
   176  	}
   177  	for _, obj := range objs {
   178  		ClearRefObjectForDispatch(obj)
   179  	}
   180  	return objs, nil
   181  }
   182  
   183  // ReferredObjectsDelegatingClient delegate client get/list function by retrieving ref-objects from existing objects
   184  func ReferredObjectsDelegatingClient(cli client.Client, objs []*unstructured.Unstructured) client.Client {
   185  	objs = slices.Filter(objs, func(obj *unstructured.Unstructured) bool {
   186  		return obj.GetAnnotations() == nil || obj.GetAnnotations()[oam.AnnotationResourceURL] == ""
   187  	})
   188  	return velaclient.DelegatingHandlerClient{
   189  		Client: cli,
   190  		Getter: func(ctx context.Context, key client.ObjectKey, obj client.Object, _ ...client.GetOption) error {
   191  			un, ok := obj.(*unstructured.Unstructured)
   192  			if !ok {
   193  				return errors.Errorf("ReferredObjectsDelegatingClient does not support non-unstructured type")
   194  			}
   195  			gvk := un.GroupVersionKind()
   196  			for _, _un := range objs {
   197  				if gvk == _un.GroupVersionKind() && key == client.ObjectKeyFromObject(_un) {
   198  					_un.DeepCopyInto(un)
   199  					return nil
   200  				}
   201  			}
   202  			return apierrors.NewNotFound(schema.GroupResource{Group: gvk.Group, Resource: gvk.Kind}, un.GetName())
   203  		},
   204  		Lister: func(ctx context.Context, list client.ObjectList, opts ...client.ListOption) error {
   205  			uns, ok := list.(*unstructured.UnstructuredList)
   206  			if !ok {
   207  				return errors.Errorf("ReferredObjectsDelegatingClient does not support non-unstructured type")
   208  			}
   209  			gvk := uns.GroupVersionKind()
   210  			gvk.Kind = strings.TrimSuffix(gvk.Kind, "List")
   211  			listOpts := &client.ListOptions{}
   212  			for _, opt := range opts {
   213  				opt.ApplyToList(listOpts)
   214  			}
   215  			for _, _un := range objs {
   216  				if gvk != _un.GroupVersionKind() {
   217  					continue
   218  				}
   219  				if listOpts.Namespace != "" && listOpts.Namespace != _un.GetNamespace() {
   220  					continue
   221  				}
   222  				if listOpts.LabelSelector != nil && !listOpts.LabelSelector.Matches(labels.Set(_un.GetLabels())) {
   223  					continue
   224  				}
   225  				uns.Items = append(uns.Items, *_un)
   226  			}
   227  			return nil
   228  		},
   229  	}
   230  }
   231  
   232  // AppendUnstructuredObjects add new objects into object list if not exists
   233  func AppendUnstructuredObjects(objs []*unstructured.Unstructured, newObjs ...*unstructured.Unstructured) []*unstructured.Unstructured {
   234  	for _, newObj := range newObjs {
   235  		idx := -1
   236  		for i, oldObj := range objs {
   237  			if oldObj.GroupVersionKind() == newObj.GroupVersionKind() && client.ObjectKeyFromObject(oldObj) == client.ObjectKeyFromObject(newObj) {
   238  				idx = i
   239  				break
   240  			}
   241  		}
   242  		if idx >= 0 {
   243  			objs[idx] = newObj
   244  		} else {
   245  			objs = append(objs, newObj)
   246  		}
   247  	}
   248  	return objs
   249  }
   250  
   251  // ConvertUnstructuredsToReferredObjects convert unstructured objects into ReferredObjects
   252  func ConvertUnstructuredsToReferredObjects(uns []*unstructured.Unstructured) (refObjs []common.ReferredObject, err error) {
   253  	for _, obj := range uns {
   254  		bs, err := json.Marshal(obj)
   255  		if err != nil {
   256  			return nil, err
   257  		}
   258  		refObjs = append(refObjs, common.ReferredObject{RawExtension: runtime.RawExtension{Raw: bs}})
   259  	}
   260  	return refObjs, nil
   261  }
   262  
   263  // IsGroupVersionKindNamespaceScoped check if the target GroupVersionKind is namespace scoped resource
   264  func IsGroupVersionKindNamespaceScoped(mapper meta.RESTMapper, gvk schema.GroupVersionKind) (bool, error) {
   265  	mappings, err := mapper.RESTMappings(gvk.GroupKind(), gvk.Version)
   266  	if err != nil {
   267  		return false, err
   268  	}
   269  	if len(mappings) == 0 {
   270  		return false, fmt.Errorf("unable to fund the mappings for gvk %s", gvk)
   271  	}
   272  	return mappings[0].Scope.Name() == meta.RESTScopeNameNamespace, nil
   273  }