diff --git a/pkg/resourceinterpreter/customizedinterpreter/customized.go b/pkg/resourceinterpreter/customizedinterpreter/customized.go index 2a782d005..2b2422e95 100644 --- a/pkg/resourceinterpreter/customizedinterpreter/customized.go +++ b/pkg/resourceinterpreter/customizedinterpreter/customized.go @@ -275,3 +275,18 @@ func applyPatch(object *unstructured.Unstructured, patch []byte, patchType confi return nil, fmt.Errorf("return patch type %s is not support", patchType) } } + +// GetDependencies returns the dependencies of give object. +// return matched value to indicate whether there is a matching hook. +func (e *CustomizedInterpreter) GetDependencies(ctx context.Context, attributes *webhook.RequestAttributes) (dependencies []configv1alpha1.DependentObjectReference, matched bool, err error) { + var response *webhook.ResponseAttributes + response, matched, err = e.interpret(ctx, attributes) + if err != nil { + return + } + if !matched { + return + } + + return response.Dependencies, matched, nil +} diff --git a/pkg/resourceinterpreter/defaultinterpreter/default.go b/pkg/resourceinterpreter/defaultinterpreter/default.go index e72cda6d2..b90431210 100644 --- a/pkg/resourceinterpreter/defaultinterpreter/default.go +++ b/pkg/resourceinterpreter/defaultinterpreter/default.go @@ -18,6 +18,7 @@ type DefaultInterpreter struct { reviseReplicaHandlers map[schema.GroupVersionKind]reviseReplicaInterpreter retentionHandlers map[schema.GroupVersionKind]retentionInterpreter aggregateStatusHandlers map[schema.GroupVersionKind]aggregateStatusInterpreter + dependenciesHandlers map[schema.GroupVersionKind]dependenciesInterpreter } // NewDefaultInterpreter return a new DefaultInterpreter. @@ -27,6 +28,7 @@ func NewDefaultInterpreter() *DefaultInterpreter { reviseReplicaHandlers: getAllDefaultReviseReplicaInterpreter(), retentionHandlers: getAllDefaultRetentionInterpreter(), aggregateStatusHandlers: getAllDefaultAggregateStatusInterpreter(), + dependenciesHandlers: getAllDefaultDependenciesInterpreter(), } } @@ -49,7 +51,10 @@ func (e *DefaultInterpreter) HookEnabled(kind schema.GroupVersionKind, operation if _, exist := e.aggregateStatusHandlers[kind]; exist { return true } - + case configv1alpha1.InterpreterOperationInterpretDependency: + if _, exist := e.dependenciesHandlers[kind]; exist { + return true + } // TODO(RainbowMango): more cases should be added here } @@ -92,3 +97,12 @@ func (e *DefaultInterpreter) AggregateStatus(object *unstructured.Unstructured, } return handler(object, aggregatedStatusItems) } + +// GetDependencies returns the dependent resources of the given object. +func (e *DefaultInterpreter) GetDependencies(object *unstructured.Unstructured) (dependencies []configv1alpha1.DependentObjectReference, err error) { + handler, exist := e.dependenciesHandlers[object.GroupVersionKind()] + if !exist { + return dependencies, fmt.Errorf("defalut interpreter for operation %s not found", configv1alpha1.InterpreterOperationInterpretDependency) + } + return handler(object) +} diff --git a/pkg/resourceinterpreter/defaultinterpreter/dependencies.go b/pkg/resourceinterpreter/defaultinterpreter/dependencies.go new file mode 100644 index 000000000..baaccb400 --- /dev/null +++ b/pkg/resourceinterpreter/defaultinterpreter/dependencies.go @@ -0,0 +1,118 @@ +package defaultinterpreter + +import ( + "fmt" + + appsv1 "k8s.io/api/apps/v1" + batchv1 "k8s.io/api/batch/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + + configv1alpha1 "github.com/karmada-io/karmada/pkg/apis/config/v1alpha1" + "github.com/karmada-io/karmada/pkg/util" + "github.com/karmada-io/karmada/pkg/util/helper" +) + +type dependenciesInterpreter func(object *unstructured.Unstructured) ([]configv1alpha1.DependentObjectReference, error) + +func getAllDefaultDependenciesInterpreter() map[schema.GroupVersionKind]dependenciesInterpreter { + s := make(map[schema.GroupVersionKind]dependenciesInterpreter) + s[appsv1.SchemeGroupVersion.WithKind(util.DeploymentKind)] = getDeploymentDependencies + s[batchv1.SchemeGroupVersion.WithKind(util.JobKind)] = getJobDependencies + s[corev1.SchemeGroupVersion.WithKind(util.PodKind)] = getPodDependencies + s[appsv1.SchemeGroupVersion.WithKind(util.DaemonSetKind)] = getDaemonSetDependencies + s[appsv1.SchemeGroupVersion.WithKind(util.StatefulSetKind)] = getStatefulSetDependencies + return s +} + +func getDeploymentDependencies(object *unstructured.Unstructured) ([]configv1alpha1.DependentObjectReference, error) { + deploymentObj, err := helper.ConvertToDeployment(object) + if err != nil { + return nil, fmt.Errorf("failed to convert Deployment from unstructured object: %v", err) + } + + podObj, err := GetPodFromTemplate(&deploymentObj.Spec.Template, deploymentObj, nil) + if err != nil { + return nil, err + } + + return getDependenciesFromPodTemplate(podObj) +} + +func getJobDependencies(object *unstructured.Unstructured) ([]configv1alpha1.DependentObjectReference, error) { + jobObj, err := helper.ConvertToJob(object) + if err != nil { + return nil, fmt.Errorf("failed to convert Job from unstructured object: %v", err) + } + + podObj, err := GetPodFromTemplate(&jobObj.Spec.Template, jobObj, nil) + if err != nil { + return nil, err + } + + return getDependenciesFromPodTemplate(podObj) +} + +func getPodDependencies(object *unstructured.Unstructured) ([]configv1alpha1.DependentObjectReference, error) { + podObj, err := helper.ConvertToPod(object) + if err != nil { + return nil, fmt.Errorf("failed to convert Pod from unstructured object: %v", err) + } + + return getDependenciesFromPodTemplate(podObj) +} + +func getDaemonSetDependencies(object *unstructured.Unstructured) ([]configv1alpha1.DependentObjectReference, error) { + daemonSetObj, err := helper.ConvertToDaemonSet(object) + if err != nil { + return nil, fmt.Errorf("failed to convert DaemonSet from unstructured object: %v", err) + } + + podObj, err := GetPodFromTemplate(&daemonSetObj.Spec.Template, daemonSetObj, nil) + if err != nil { + return nil, err + } + + return getDependenciesFromPodTemplate(podObj) +} + +func getStatefulSetDependencies(object *unstructured.Unstructured) ([]configv1alpha1.DependentObjectReference, error) { + statefulSetObj, err := helper.ConvertToStatefulSet(object) + if err != nil { + return nil, fmt.Errorf("failed to convert StatefulSet from unstructured object: %v", err) + } + + podObj, err := GetPodFromTemplate(&statefulSetObj.Spec.Template, statefulSetObj, nil) + if err != nil { + return nil, err + } + + return getDependenciesFromPodTemplate(podObj) +} + +func getDependenciesFromPodTemplate(podObj *corev1.Pod) ([]configv1alpha1.DependentObjectReference, error) { + dependentConfigMaps := getConfigMapNames(podObj) + dependentSecrets := getSecretNames(podObj) + + var dependentObjectRefs []configv1alpha1.DependentObjectReference + for cm := range dependentConfigMaps { + dependentObjectRefs = append(dependentObjectRefs, configv1alpha1.DependentObjectReference{ + APIVersion: "v1", + Kind: "ConfigMap", + Namespace: podObj.Namespace, + Name: cm, + }) + } + + for secret := range dependentSecrets { + dependentObjectRefs = append(dependentObjectRefs, configv1alpha1.DependentObjectReference{ + APIVersion: "v1", + Kind: "Secret", + Namespace: podObj.Namespace, + Name: secret, + }) + } + + return dependentObjectRefs, nil +} diff --git a/pkg/resourceinterpreter/defaultinterpreter/dependencies_util.go b/pkg/resourceinterpreter/defaultinterpreter/dependencies_util.go new file mode 100644 index 000000000..1f5f109a4 --- /dev/null +++ b/pkg/resourceinterpreter/defaultinterpreter/dependencies_util.go @@ -0,0 +1,318 @@ +/* +Copyright 2014 The Kubernetes 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. +*/ + +// This code is directly lifted from the Kubernetes codebase in order to avoid relying on the k8s.io/kubernetes package. +// For reference: https://github.com/kubernetes/kubernetes/blob/release-1.22/pkg/api/v1/pod/util.go, +// https://github.com/kubernetes/kubernetes/blob/release-1.22/pkg/apis/core/validation/validation.go#L228, +// https://github.com/kubernetes/kubernetes/blob/release-1.22/pkg/kubelet/configmap/configmap_manager.go#L101-L108, +// https://github.com/kubernetes/kubernetes/blob/release-1.22/pkg/kubelet/secret/secret_manager.go#L102-L109 +// + +package defaultinterpreter + +import ( + "fmt" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/meta" + apimachineryvalidation "k8s.io/apimachinery/pkg/api/validation" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/sets" +) + +// ValidatePodName can be used to check whether the given pod name is valid. +// Prefix indicates this name will be used as part of generation, in which case +// trailing dashes are allowed. +var ValidatePodName = apimachineryvalidation.NameIsDNSSubdomain + +// ContainerType signifies container type +type ContainerType int + +const ( + // Containers is for normal containers + Containers ContainerType = 1 << iota + // InitContainers is for init containers + InitContainers + // EphemeralContainers is for ephemeral containers + EphemeralContainers +) + +// AllContainers specifies that all containers be visited +const AllContainers ContainerType = (InitContainers | Containers | EphemeralContainers) + +// Visitor is called with each object name, and returns true if visiting should continue +type Visitor func(name string) (shouldContinue bool) + +// ContainerVisitor is called with each container spec, and returns true +// if visiting should continue. +type ContainerVisitor func(container *corev1.Container, containerType ContainerType) (shouldContinue bool) + +// VisitContainers invokes the visitor function with a pointer to every container +// spec in the given pod spec with type set in mask. If visitor returns false, +// visiting is short-circuited. VisitContainers returns true if visiting completes, +// false if visiting was short-circuited. +func VisitContainers(podSpec *corev1.PodSpec, mask ContainerType, visitor ContainerVisitor) bool { + if mask&InitContainers != 0 { + for i := range podSpec.InitContainers { + if !visitor(&podSpec.InitContainers[i], InitContainers) { + return false + } + } + } + if mask&Containers != 0 { + for i := range podSpec.Containers { + if !visitor(&podSpec.Containers[i], Containers) { + return false + } + } + } + if mask&EphemeralContainers != 0 { + for i := range podSpec.EphemeralContainers { + if !visitor((*corev1.Container)(&podSpec.EphemeralContainers[i].EphemeralContainerCommon), EphemeralContainers) { + return false + } + } + } + return true +} + +func visitContainerSecretNames(container *corev1.Container, visitor Visitor) bool { + for _, env := range container.EnvFrom { + if env.SecretRef != nil { + if !visitor(env.SecretRef.Name) { + return false + } + } + } + for _, envVar := range container.Env { + if envVar.ValueFrom != nil && envVar.ValueFrom.SecretKeyRef != nil { + if !visitor(envVar.ValueFrom.SecretKeyRef.Name) { + return false + } + } + } + return true +} + +func visitContainerConfigmapNames(container *corev1.Container, visitor Visitor) bool { + for _, env := range container.EnvFrom { + if env.ConfigMapRef != nil { + if !visitor(env.ConfigMapRef.Name) { + return false + } + } + } + for _, envVar := range container.Env { + if envVar.ValueFrom != nil && envVar.ValueFrom.ConfigMapKeyRef != nil { + if !visitor(envVar.ValueFrom.ConfigMapKeyRef.Name) { + return false + } + } + } + return true +} + +// GetPodFromTemplate generates pod object from a template. +func GetPodFromTemplate(template *corev1.PodTemplateSpec, parentObject runtime.Object, controllerRef *metav1.OwnerReference) (*corev1.Pod, error) { + desiredLabels := getPodsLabelSet(template) + desiredFinalizers := getPodsFinalizers(template) + desiredAnnotations := getPodsAnnotationSet(template) + accessor, err := meta.Accessor(parentObject) + if err != nil { + return nil, fmt.Errorf("parentObject does not have ObjectMeta, %v", err) + } + prefix := getPodsPrefix(accessor.GetName()) + + pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Labels: desiredLabels, + Annotations: desiredAnnotations, + GenerateName: prefix, + Finalizers: desiredFinalizers, + Namespace: accessor.GetNamespace(), + }, + } + if controllerRef != nil { + pod.OwnerReferences = append(pod.OwnerReferences, *controllerRef) + } + pod.Spec = *template.Spec.DeepCopy() + return pod, nil +} + +func getPodsLabelSet(template *corev1.PodTemplateSpec) labels.Set { + desiredLabels := make(labels.Set) + for k, v := range template.Labels { + desiredLabels[k] = v + } + return desiredLabels +} + +func getPodsFinalizers(template *corev1.PodTemplateSpec) []string { + desiredFinalizers := make([]string, len(template.Finalizers)) + copy(desiredFinalizers, template.Finalizers) + return desiredFinalizers +} + +func getPodsAnnotationSet(template *corev1.PodTemplateSpec) labels.Set { + desiredAnnotations := make(labels.Set) + for k, v := range template.Annotations { + desiredAnnotations[k] = v + } + return desiredAnnotations +} + +func getPodsPrefix(controllerName string) string { + // use the dash (if the name isn't too long) to make the pod name a bit prettier + prefix := fmt.Sprintf("%s-", controllerName) + if len(ValidatePodName(prefix, true)) != 0 { + prefix = controllerName + } + return prefix +} + +func skipEmptyNames(visitor Visitor) Visitor { + return func(name string) bool { + if len(name) == 0 { + // continue visiting + return true + } + // delegate to visitor + return visitor(name) + } +} + +// VisitPodConfigmapNames invokes the visitor function with the name of every configmap +// referenced by the pod spec. If visitor returns false, visiting is short-circuited. +// Transitive references (e.g. pod -> pvc -> pv -> secret) are not visited. +// Returns true if visiting completed, false if visiting was short-circuited. +func VisitPodConfigmapNames(pod *corev1.Pod, visitor Visitor) bool { + visitor = skipEmptyNames(visitor) + VisitContainers(&pod.Spec, AllContainers, func(c *corev1.Container, containerType ContainerType) bool { + return visitContainerConfigmapNames(c, visitor) + }) + var source *corev1.VolumeSource + for i := range pod.Spec.Volumes { + source = &pod.Spec.Volumes[i].VolumeSource + switch { + case source.Projected != nil: + for j := range source.Projected.Sources { + if source.Projected.Sources[j].ConfigMap != nil { + if !visitor(source.Projected.Sources[j].ConfigMap.Name) { + return false + } + } + } + case source.ConfigMap != nil: + if !visitor(source.ConfigMap.Name) { + return false + } + } + } + return true +} + +// VisitPodSecretNames invokes the visitor function with the name of every secret +// referenced by the pod spec. If visitor returns false, visiting is short-circuited. +// Transitive references (e.g. pod -> pvc -> pv -> secret) are not visited. +// Returns true if visiting completed, false if visiting was short-circuited. +//nolint:gocyclo +func VisitPodSecretNames(pod *corev1.Pod, visitor Visitor) bool { + visitor = skipEmptyNames(visitor) + for _, reference := range pod.Spec.ImagePullSecrets { + if !visitor(reference.Name) { + return false + } + } + VisitContainers(&pod.Spec, AllContainers, func(c *corev1.Container, containerType ContainerType) bool { + return visitContainerSecretNames(c, visitor) + }) + var source *corev1.VolumeSource + + for i := range pod.Spec.Volumes { + source = &pod.Spec.Volumes[i].VolumeSource + switch { + case source.AzureFile != nil: + if len(source.AzureFile.SecretName) > 0 && !visitor(source.AzureFile.SecretName) { + return false + } + case source.CephFS != nil: + if source.CephFS.SecretRef != nil && !visitor(source.CephFS.SecretRef.Name) { + return false + } + case source.Cinder != nil: + if source.Cinder.SecretRef != nil && !visitor(source.Cinder.SecretRef.Name) { + return false + } + case source.FlexVolume != nil: + if source.FlexVolume.SecretRef != nil && !visitor(source.FlexVolume.SecretRef.Name) { + return false + } + case source.Projected != nil: + for j := range source.Projected.Sources { + if source.Projected.Sources[j].Secret != nil { + if !visitor(source.Projected.Sources[j].Secret.Name) { + return false + } + } + } + case source.RBD != nil: + if source.RBD.SecretRef != nil && !visitor(source.RBD.SecretRef.Name) { + return false + } + case source.Secret != nil: + if !visitor(source.Secret.SecretName) { + return false + } + case source.ScaleIO != nil: + if source.ScaleIO.SecretRef != nil && !visitor(source.ScaleIO.SecretRef.Name) { + return false + } + case source.ISCSI != nil: + if source.ISCSI.SecretRef != nil && !visitor(source.ISCSI.SecretRef.Name) { + return false + } + case source.StorageOS != nil: + if source.StorageOS.SecretRef != nil && !visitor(source.StorageOS.SecretRef.Name) { + return false + } + case source.CSI != nil: + if source.CSI.NodePublishSecretRef != nil && !visitor(source.CSI.NodePublishSecretRef.Name) { + return false + } + } + } + return true +} + +func getSecretNames(pod *corev1.Pod) sets.String { + result := sets.NewString() + VisitPodSecretNames(pod, func(name string) bool { + result.Insert(name) + return true + }) + return result +} + +func getConfigMapNames(pod *corev1.Pod) sets.String { + result := sets.NewString() + VisitPodConfigmapNames(pod, func(name string) bool { + result.Insert(name) + return true + }) + return result +} diff --git a/pkg/resourceinterpreter/interpreter.go b/pkg/resourceinterpreter/interpreter.go index 45ad34557..af62c5529 100644 --- a/pkg/resourceinterpreter/interpreter.go +++ b/pkg/resourceinterpreter/interpreter.go @@ -35,6 +35,8 @@ type ResourceInterpreter interface { // AggregateStatus returns the objects that based on the 'object' but with status aggregated. AggregateStatus(object *unstructured.Unstructured, aggregatedStatusItems []workv1alpha2.AggregatedStatusItem) (*unstructured.Unstructured, error) + // GetDependencies returns the dependent resources of the given object. + GetDependencies(object *unstructured.Unstructured) (dependencies []configv1alpha1.DependentObjectReference, err error) // other common method } @@ -153,3 +155,22 @@ func (i *customResourceInterpreterImpl) AggregateStatus(object *unstructured.Uns return i.defaultInterpreter.AggregateStatus(object, aggregatedStatusItems) } + +// GetDependencies returns the dependent resources of the given object. +func (i *customResourceInterpreterImpl) GetDependencies(object *unstructured.Unstructured) (dependencies []configv1alpha1.DependentObjectReference, err error) { + klog.V(4).Infof("Begin to get dependencies for object: %v %s/%s.", object.GroupVersionKind(), object.GetNamespace(), object.GetName()) + + dependencies, hookEnabled, err := i.customizedInterpreter.GetDependencies(context.TODO(), &webhook.RequestAttributes{ + Operation: configv1alpha1.InterpreterOperationInterpretDependency, + Object: object, + }) + if err != nil { + return + } + if hookEnabled { + return + } + + dependencies, err = i.defaultInterpreter.GetDependencies(object) + return +}