diff --git a/controller/comparator.go b/controller/comparator.go index 6d7e5ed50f43e..fe9b0bd003499 100644 --- a/controller/comparator.go +++ b/controller/comparator.go @@ -82,16 +82,28 @@ func (ks *KsonnetAppComparator) CompareAppState( // Retrieve the live versions of the objects liveObjs, err := kubeutil.GetResourcesWithLabel(clst.RESTConfig(), namespace, common.LabelApplicationName, app.Name) - if err != nil { return nil, err } - objByFullName := ks.groupLiveObjects(liveObjs, targetObjs) + + liveObjByFullName := ks.groupLiveObjects(liveObjs, targetObjs) controlledLiveObj := make([]*unstructured.Unstructured, len(targetObjs)) + // Move live resources which have corresponding target object to controlledLiveObj for i, targetObj := range targetObjs { - controlledLiveObj[i] = objByFullName[getResourceFullName(targetObj)] + fullName := getResourceFullName(targetObj) + controlledLiveObj[i] = liveObjByFullName[fullName] + delete(liveObjByFullName, fullName) + } + + // Move root level live resources to controlledLiveObj and add nil to targetObjs to indicate that target object is missing + for fullName := range liveObjByFullName { + liveObj := liveObjByFullName[fullName] + if !hasParent(liveObj) { + targetObjs = append(targetObjs, nil) + controlledLiveObj = append(controlledLiveObj, liveObj) + } } // Do the actual comparison @@ -99,19 +111,40 @@ func (ks *KsonnetAppComparator) CompareAppState( if err != nil { return nil, err } + comparisonStatus := v1alpha1.ComparisonStatusSynced resources := make([]v1alpha1.ResourceState, len(targetObjs)) for i := 0; i < len(targetObjs); i++ { resState := v1alpha1.ResourceState{ ChildLiveResources: make([]v1alpha1.ResourceNode, 0), } - targetObjBytes, err := json.Marshal(targetObjs[i].Object) - if err != nil { - return nil, err + diffResult := diffResults.Diffs[i] + if diffResult.Modified { + // Set resource state to 'OutOfSync' since target and corresponding live resource are different + resState.Status = v1alpha1.ComparisonStatusOutOfSync + comparisonStatus = v1alpha1.ComparisonStatusOutOfSync + } else { + resState.Status = v1alpha1.ComparisonStatusSynced + } + + if targetObjs[i] == nil { + resState.TargetState = "null" + // Set resource state to 'OutOfSync' since target resource is missing and live resource is unexpected + resState.Status = v1alpha1.ComparisonStatusOutOfSync + comparisonStatus = v1alpha1.ComparisonStatusOutOfSync + } else { + targetObjBytes, err := json.Marshal(targetObjs[i].Object) + if err != nil { + return nil, err + } + resState.TargetState = string(targetObjBytes) } - resState.TargetState = string(targetObjBytes) + if controlledLiveObj[i] == nil { resState.LiveState = "null" + // Set resource state to 'OutOfSync' since target resource present but corresponding live resource is missing + resState.Status = v1alpha1.ComparisonStatusOutOfSync + comparisonStatus = v1alpha1.ComparisonStatusOutOfSync } else { liveObjBytes, err := json.Marshal(controlledLiveObj[i].Object) if err != nil { @@ -119,19 +152,14 @@ func (ks *KsonnetAppComparator) CompareAppState( } resState.LiveState = string(liveObjBytes) } - diffResult := diffResults.Diffs[i] - if diffResult.Modified { - resState.Status = v1alpha1.ComparisonStatusOutOfSync - } else { - resState.Status = v1alpha1.ComparisonStatusSynced - } + resources[i] = resState } for i, resource := range resources { liveResource := controlledLiveObj[i] if liveResource != nil { - childResources, err := getChildren(liveResource, objByFullName) + childResources, err := getChildren(liveResource, liveObjByFullName) if err != nil { return nil, err } @@ -145,26 +173,36 @@ func (ks *KsonnetAppComparator) CompareAppState( Server: clst.Server, Namespace: namespace, Resources: resources, - } - if diffResults.Modified { - compResult.Status = v1alpha1.ComparisonStatusOutOfSync - } else { - compResult.Status = v1alpha1.ComparisonStatusSynced + Status: comparisonStatus, } return &compResult, nil } -func getChildren(parent *unstructured.Unstructured, objByFullName map[string]*unstructured.Unstructured) ([]v1alpha1.ResourceNode, error) { +func hasParent(obj *unstructured.Unstructured) bool { + // TODO: remove special case after Service and Endpoint get explicit relationship ( https://github.com/kubernetes/kubernetes/issues/28483 ) + return obj.GetKind() == kubeutil.EndpointsKind || metav1.GetControllerOf(obj) != nil +} + +func isControlledBy(obj *unstructured.Unstructured, parent *unstructured.Unstructured) bool { + // TODO: remove special case after Service and Endpoint get explicit relationship ( https://github.com/kubernetes/kubernetes/issues/28483 ) + if obj.GetKind() == kubeutil.EndpointsKind && parent.GetKind() == kubeutil.ServiceKind { + return obj.GetName() == parent.GetName() + } + return metav1.IsControlledBy(obj, parent) +} + +func getChildren(parent *unstructured.Unstructured, liveObjByFullName map[string]*unstructured.Unstructured) ([]v1alpha1.ResourceNode, error) { children := make([]v1alpha1.ResourceNode, 0) - for _, obj := range objByFullName { - if metav1.IsControlledBy(obj, parent) { + for fullName, obj := range liveObjByFullName { + if isControlledBy(obj, parent) { + delete(liveObjByFullName, fullName) childResource := v1alpha1.ResourceNode{} json, err := json.Marshal(obj) if err != nil { return nil, err } childResource.State = string(json) - childResourceChildren, err := getChildren(obj, objByFullName) + childResourceChildren, err := getChildren(obj, liveObjByFullName) if err != nil { return nil, err } diff --git a/util/kube/kube.go b/util/kube/kube.go index 77c50388fdd24..ef9dd4fbd0e6b 100644 --- a/util/kube/kube.go +++ b/util/kube/kube.go @@ -34,6 +34,11 @@ const ( deleteCollectionVerb = "deletecollection" ) +const ( + ServiceKind = "Service" + EndpointsKind = "Endpoints" +) + var ( // location to use for generating temporary files, such as the ca.crt needed by kubectl kubectlTempDir string