pkg/dubboctl/internal/kube/object.go (180 lines of code) (raw):
// Licensed to the Apache Software Foundation (ASF) under one or more
// contributor license agreements.  See the NOTICE file distributed with
// this work for additional information regarding copyright ownership.
// The ASF licenses this file to You 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 kube
import (
	"bytes"
	"fmt"
	"sort"
	"strings"
	"github.com/apache/dubbo-admin/pkg/dubboctl/internal/util"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	"k8s.io/apimachinery/pkg/util/yaml"
)
const (
	hashPrompt = "Namespace:Kind:Name=>"
)
// Object wraps k8s Unstructured and exposes the fields we need
type Object struct {
	internal *unstructured.Unstructured
	Namespace string
	Name      string
	Group     string
	Kind      string
	yamlStr string
}
func (obj *Object) IsValid() bool {
	return obj.Kind != ""
}
func (obj *Object) IsEqual(another *Object) bool {
	if obj == nil {
		return another == nil
	}
	if another == nil {
		return false
	}
	if obj.Namespace != another.Namespace ||
		obj.Name != another.Name || obj.Group != another.Group || obj.Kind != another.Kind {
		return false
	}
	if obj.Unstructured() == nil {
		return another.Unstructured() == nil
	}
	if another.Unstructured() == nil {
		return false
	}
	objJson, err := obj.Unstructured().MarshalJSON()
	if err != nil {
		return false
	}
	anotherJson, err := another.Unstructured().MarshalJSON()
	if err != nil {
		return false
	}
	return bytes.Equal(objJson, anotherJson)
}
func (obj *Object) Unstructured() *unstructured.Unstructured {
	return obj.internal
}
func (obj *Object) Hash() string {
	return strings.Join([]string{obj.Namespace, obj.Kind, obj.Name}, ":")
}
func (obj *Object) YAML() string {
	return obj.yamlStr
}
func (obj *Object) SetNamespace(ns string) {
	obj.Namespace = ns
	obj.internal.SetNamespace(ns)
}
type Objects []*Object
// SortMap generates corresponding map and sorted keys
func (objs Objects) SortMap() (map[string]*Object, []string) {
	var keys []string
	res := make(map[string]*Object)
	for _, obj := range objs {
		if obj.IsValid() {
			hash := obj.Hash()
			res[hash] = obj
			keys = append(keys, hash)
		}
	}
	sort.Strings(keys)
	return res, keys
}
func NewObject(obj *unstructured.Unstructured, yamlStr string) *Object {
	newObj := &Object{
		internal: obj,
		yamlStr:  yamlStr,
	}
	newObj.Namespace = obj.GetNamespace()
	newObj.Name = obj.GetName()
	gvk := obj.GroupVersionKind()
	newObj.Group = gvk.Group
	newObj.Kind = gvk.Kind
	return newObj
}
// ParseObjectsFromManifest parse Objects from manifest which divided by YAML separator "\n---\n"
func ParseObjectsFromManifest(manifest string, continueOnErr bool) (Objects, error) {
	segments, err := util.SplitYAML(manifest)
	if err != nil {
		return nil, err
	}
	var objects Objects
	for _, segment := range segments {
		newObj, err := ParseObjectFromManifest(segment)
		if err != nil {
			if !continueOnErr {
				return nil, err
			}
			continue
		}
		if newObj.IsValid() {
			objects = append(objects, newObj)
		}
	}
	return objects, nil
}
// ParseObjectFromManifest parse Object from manifest which represents a single K8s object
func ParseObjectFromManifest(manifest string) (*Object, error) {
	reader := strings.NewReader(manifest)
	decoder := yaml.NewYAMLOrJSONDecoder(reader, 1024)
	internal := &unstructured.Unstructured{}
	if err := decoder.Decode(internal); err != nil {
		return nil, err
	}
	newObj := NewObject(internal, manifest)
	return newObj, nil
}
// CompareObjects compares object lists and returns diff, add, err using util.DiffYAML.
// It compares objects with same hash value(Namespace:Kind:Name) and returns diff.
// For objects that only one list have, it returns add.
// For objects that could not be parsed successfully, it returns err.
// Refer to TestCompareObjects for examples.
func CompareObjects(objsA, objsB Objects) (string, string, string) {
	var diffRes strings.Builder
	var addRes strings.Builder
	var errRes strings.Builder
	sep := "\n------\n"
	mapA, keysA := objsA.SortMap()
	mapB, keysB := objsB.SortMap()
	for _, hashA := range keysA {
		objA := mapA[hashA]
		if objB, ok := mapB[hashA]; ok {
			diff, err := util.DiffYAML(objA.YAML(), objB.YAML())
			if err != nil {
				errRes.WriteString(fmt.Sprintf("%s%s parse failed, err:\n%s", hashPrompt, hashA, err))
				errRes.WriteString(sep)
				continue
			}
			if diff != "" {
				diffRes.WriteString(fmt.Sprintf("%s%s diff:\n", hashPrompt, hashA))
				diffRes.WriteString(diff)
				diffRes.WriteString(sep)
			}
		} else {
			add, err := util.DiffYAML(objA.YAML(), "")
			if err != nil {
				errRes.WriteString(fmt.Sprintf("%s%s in previous section parse failed, err:\n%s", hashPrompt, hashA, err))
				errRes.WriteString(sep)
				continue
			}
			if add != "" {
				addRes.WriteString(fmt.Sprintf("%s%s in previous addition:\n", hashPrompt, hashA))
				addRes.WriteString(add)
				addRes.WriteString(sep)
			}
		}
	}
	for _, hashB := range keysB {
		objB := mapB[hashB]
		if _, ok := mapA[hashB]; !ok {
			add, err := util.DiffYAML(objB.YAML(), "")
			if err != nil {
				errRes.WriteString(fmt.Sprintf("%s%s in next section parse failed, err:\n%s", hashPrompt, hashB, err))
				errRes.WriteString(sep)
				continue
			}
			if add != "" {
				addRes.WriteString(fmt.Sprintf("%s%s in next addition:\n", hashPrompt, hashB))
				addRes.WriteString(add)
				addRes.WriteString(sep)
			}
		}
	}
	return diffRes.String(), addRes.String(), errRes.String()
}
// CompareObject compares two objects and returns diff.
func CompareObject(objA, objB *Object) (string, error) {
	diff, err := util.DiffYAML(objA.YAML(), objB.YAML())
	if err != nil {
		return "", err
	}
	return diff, nil
}