operator/pkg/object/objects.go (434 lines of code) (raw):

// Copyright Istio 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 manifest provides functions for going between in-memory k8s objects (unstructured.Unstructured) and their JSON or YAML representations. */ package object import ( "bufio" "bytes" "fmt" "sort" "strings" ) import ( "istio.io/pkg/log" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/util/intstr" k8syaml "k8s.io/apimachinery/pkg/util/yaml" "sigs.k8s.io/yaml" ) import ( "github.com/apache/dubbo-go-pixiu/operator/pkg/apis/istio/v1alpha1" "github.com/apache/dubbo-go-pixiu/operator/pkg/helm" names "github.com/apache/dubbo-go-pixiu/operator/pkg/name" "github.com/apache/dubbo-go-pixiu/operator/pkg/tpath" "github.com/apache/dubbo-go-pixiu/operator/pkg/util" ) const ( // YAMLSeparator is a separator for multi-document YAML files. YAMLSeparator = "\n---\n" ) // K8sObject is an in-memory representation of a k8s object, used for moving between different representations // (Unstructured, JSON, YAML) with cached rendering. type K8sObject struct { object *unstructured.Unstructured Group string Kind string Name string Namespace string json []byte yaml []byte } // NewK8sObject creates a new K8sObject and returns a ptr to it. func NewK8sObject(u *unstructured.Unstructured, json, yaml []byte) *K8sObject { o := &K8sObject{ object: u, json: json, yaml: yaml, } gvk := u.GetObjectKind().GroupVersionKind() o.Group = gvk.Group o.Kind = gvk.Kind o.Name = u.GetName() o.Namespace = u.GetNamespace() return o } // Hash returns a unique, insecure hash based on kind, namespace and name. func Hash(kind, namespace, name string) string { switch kind { case names.ClusterRoleStr, names.ClusterRoleBindingStr, names.MeshPolicyStr: namespace = "" } return strings.Join([]string{kind, namespace, name}, ":") } // FromHash parses kind, namespace and name from a hash. func FromHash(hash string) (kind, namespace, name string) { hv := strings.Split(hash, ":") if len(hv) != 3 { return "Bad hash string: " + hash, "", "" } kind, namespace, name = hv[0], hv[1], hv[2] return } // HashNameKind returns a unique, insecure hash based on kind and name. func HashNameKind(kind, name string) string { return strings.Join([]string{kind, name}, ":") } // ParseJSONToK8sObject parses JSON to an K8sObject. func ParseJSONToK8sObject(json []byte) (*K8sObject, error) { o, _, err := unstructured.UnstructuredJSONScheme.Decode(json, nil, nil) if err != nil { return nil, fmt.Errorf("error parsing json into unstructured object: %v", err) } u, ok := o.(*unstructured.Unstructured) if !ok { return nil, fmt.Errorf("parsed unexpected type %T", o) } return NewK8sObject(u, json, nil), nil } // ParseYAMLToK8sObject parses YAML to an Object. func ParseYAMLToK8sObject(yaml []byte) (*K8sObject, error) { r := bytes.NewReader(yaml) decoder := k8syaml.NewYAMLOrJSONDecoder(r, 1024) out := &unstructured.Unstructured{} err := decoder.Decode(out) if err != nil { return nil, fmt.Errorf("error decoding object %v: %v", string(yaml), err) } return NewK8sObject(out, nil, yaml), nil } // UnstructuredObject exposes the raw object, primarily for testing func (o *K8sObject) UnstructuredObject() *unstructured.Unstructured { return o.object } // ResolveK8sConflict - This method resolves k8s object possible // conflicting settings. Which K8sObjects may need such method // depends on the type of the K8sObject. func (o *K8sObject) ResolveK8sConflict() *K8sObject { if o.Kind == names.PDBStr { return resolvePDBConflict(o) } return o } // Unstructured exposes the raw object content, primarily for testing func (o *K8sObject) Unstructured() map[string]interface{} { return o.UnstructuredObject().UnstructuredContent() } // Container returns a container subtree for Deployment objects if one is found, or nil otherwise. func (o *K8sObject) Container(name string) map[string]interface{} { u := o.Unstructured() path := fmt.Sprintf("spec.template.spec.containers.[name:%s]", name) node, f, err := tpath.GetPathContext(u, util.PathFromString(path), false) if err == nil && f { // Must be the type from the schema. return node.Node.(map[string]interface{}) } return nil } // GroupVersionKind returns the GroupVersionKind for the K8sObject func (o *K8sObject) GroupVersionKind() schema.GroupVersionKind { return o.object.GroupVersionKind() } // Version returns the APIVersion of the K8sObject func (o *K8sObject) Version() string { return o.object.GetAPIVersion() } // Hash returns a unique hash for the K8sObject func (o *K8sObject) Hash() string { return Hash(o.Kind, o.Namespace, o.Name) } // HashNameKind returns a hash for the K8sObject based on the name and kind only. func (o *K8sObject) HashNameKind() string { return HashNameKind(o.Kind, o.Name) } // JSON returns a JSON representation of the K8sObject, using an internal cache. func (o *K8sObject) JSON() ([]byte, error) { if o.json != nil { return o.json, nil } b, err := o.object.MarshalJSON() if err != nil { return nil, err } return b, nil } // YAML returns a YAML representation of the K8sObject, using an internal cache. func (o *K8sObject) YAML() ([]byte, error) { if o == nil { return nil, nil } if o.yaml != nil { return o.yaml, nil } oj, err := o.JSON() if err != nil { return nil, err } o.json = oj y, err := yaml.JSONToYAML(oj) if err != nil { return nil, err } o.yaml = y return y, nil } // YAMLDebugString returns a YAML representation of the K8sObject, or an error string if the K8sObject cannot be rendered to YAML. func (o *K8sObject) YAMLDebugString() string { y, err := o.YAML() if err != nil { return err.Error() } return string(y) } // K8sObjects holds a collection of k8s objects, so that we can filter / sequence them type K8sObjects []*K8sObject // String implements the Stringer interface. func (os K8sObjects) String() string { var out []string for _, oo := range os { out = append(out, oo.YAMLDebugString()) } return strings.Join(out, helm.YAMLSeparator) } // Keys returns a slice with the keys of os. func (os K8sObjects) Keys() []string { var out []string for _, oo := range os { out = append(out, oo.Hash()) } return out } // UnstructuredItems returns the list of items of unstructured.Unstructured. func (os K8sObjects) UnstructuredItems() []unstructured.Unstructured { var usList []unstructured.Unstructured for _, obj := range os { usList = append(usList, *obj.UnstructuredObject()) } return usList } // ParseK8sObjectsFromYAMLManifest returns a K8sObjects representation of manifest. func ParseK8sObjectsFromYAMLManifest(manifest string) (K8sObjects, error) { return ParseK8sObjectsFromYAMLManifestFailOption(manifest, true) } // ParseK8sObjectsFromYAMLManifestFailOption returns a K8sObjects representation of manifest. Continues parsing when a bad object // is found if failOnError is set to false. func ParseK8sObjectsFromYAMLManifestFailOption(manifest string, failOnError bool) (K8sObjects, error) { var b bytes.Buffer var yamls []string scanner := bufio.NewScanner(strings.NewReader(manifest)) for scanner.Scan() { line := scanner.Text() if strings.HasPrefix(line, "---") { // yaml separator yamls = append(yamls, b.String()) b.Reset() } else { if _, err := b.WriteString(line); err != nil { return nil, err } if _, err := b.WriteString("\n"); err != nil { return nil, err } } } yamls = append(yamls, b.String()) var objects K8sObjects for _, yaml := range yamls { yaml = removeNonYAMLLines(yaml) if yaml == "" { continue } o, err := ParseYAMLToK8sObject([]byte(yaml)) if err != nil { e := fmt.Errorf("failed to parse YAML to a k8s object: %s", err) if failOnError { return nil, e } log.Error(err.Error()) continue } if o.Valid() { objects = append(objects, o) } } return objects, nil } func removeNonYAMLLines(yms string) string { var b strings.Builder for _, s := range strings.Split(yms, "\n") { if strings.HasPrefix(s, "#") { continue } b.WriteString(s) b.WriteString("\n") } // helm charts sometimes emits blank objects with just a "disabled" comment. return strings.TrimSpace(b.String()) } // YAMLManifest returns a YAML representation of K8sObjects os. func (os K8sObjects) YAMLManifest() (string, error) { var b bytes.Buffer for i, item := range os { if i != 0 { if _, err := b.WriteString("\n\n"); err != nil { return "", err } } ym, err := item.YAML() if err != nil { return "", fmt.Errorf("error building yaml: %v", err) } if _, err := b.Write(ym); err != nil { return "", err } if _, err := b.Write([]byte(YAMLSeparator)); err != nil { return "", err } } return b.String(), nil } // Sort will order the items in K8sObjects in order of score, group, kind, name. The intent is to // have a deterministic ordering in which K8sObjects are applied. func (os K8sObjects) Sort(score func(o *K8sObject) int) { sort.Slice(os, func(i, j int) bool { iScore := score(os[i]) jScore := score(os[j]) return iScore < jScore || (iScore == jScore && os[i].Group < os[j].Group) || (iScore == jScore && os[i].Group == os[j].Group && os[i].Kind < os[j].Kind) || (iScore == jScore && os[i].Group == os[j].Group && os[i].Kind == os[j].Kind && os[i].Name < os[j].Name) }) } // ToMap returns a map of K8sObject hash to K8sObject. func (os K8sObjects) ToMap() map[string]*K8sObject { ret := make(map[string]*K8sObject) for _, oo := range os { if oo.Valid() { ret[oo.Hash()] = oo } } return ret } // ToNameKindMap returns a map of K8sObject name/kind hash to K8sObject. func (os K8sObjects) ToNameKindMap() map[string]*K8sObject { ret := make(map[string]*K8sObject) for _, oo := range os { if oo.Valid() { ret[oo.HashNameKind()] = oo } } return ret } // Valid checks returns true if Kind of K8sObject is not empty. func (o *K8sObject) Valid() bool { return o.Kind != "" } // FullName returns namespace/name of K8s object func (o *K8sObject) FullName() string { return fmt.Sprintf("%s/%s", o.Namespace, o.Name) } // Equal returns true if o and other are both valid and equal to each other. func (o *K8sObject) Equal(other *K8sObject) bool { if o == nil { return other == nil } if other == nil { return o == nil } ay, err := o.YAML() if err != nil { return false } by, err := other.YAML() if err != nil { return false } return util.IsYAMLEqual(string(ay), string(by)) } func istioCustomResources(group string) bool { switch group { case names.ConfigAPIGroupName, names.SecurityAPIGroupName, names.AuthenticationAPIGroupName, names.NetworkingAPIGroupName: return true } return false } // DefaultObjectOrder is default sorting function used to sort k8s objects. func DefaultObjectOrder() func(o *K8sObject) int { return func(o *K8sObject) int { gk := o.Group + "/" + o.Kind switch { // Create CRDs asap - both because they are slow and because we will likely create instances of them soon case gk == "apiextensions.k8s.io/CustomResourceDefinition": return -1000 // We need to create ServiceAccounts, Roles before we bind them with a RoleBinding case gk == "/ServiceAccount" || gk == "rbac.authorization.k8s.io/ClusterRole": return 1 case gk == "rbac.authorization.k8s.io/ClusterRoleBinding": return 2 // validatingwebhookconfiguration is configured to FAIL-OPEN in the default install. For the // re-install case we want to apply the validatingwebhookconfiguration first to reset any // orphaned validatingwebhookconfiguration that is FAIL-CLOSE. case gk == "admissionregistration.k8s.io/ValidatingWebhookConfiguration": return 3 case istioCustomResources(o.Group): return 4 // Pods might need configmap or secrets - avoid backoff by creating them first case gk == "/ConfigMap" || gk == "/Secrets": return 100 // Create the pods after we've created other things they might be waiting for case gk == "extensions/Deployment" || gk == "app/Deployment": return 1000 // Autoscalers typically act on a deployment case gk == "autoscaling/HorizontalPodAutoscaler": return 1001 // Create services late - after pods have been started case gk == "/Service": return 10000 default: return 1000 } } } func ObjectsNotInLists(objects K8sObjects, lists ...K8sObjects) K8sObjects { var ret K8sObjects filterMap := make(map[*K8sObject]bool) for _, list := range lists { for _, object := range list { filterMap[object] = true } } for _, o := range objects { if !filterMap[o] { ret = append(ret, o) } } return ret } // KindObjects returns the subset of objs with the given kind. func KindObjects(objs K8sObjects, kind string) K8sObjects { var ret K8sObjects for _, o := range objs { if o.Kind == kind { ret = append(ret, o) } } return ret } // ParseK8SYAMLToIstioOperator parses a IstioOperator CustomResource YAML string and unmarshals in into // an IstioOperatorSpec object. It returns the object and an API group/version with it. func ParseK8SYAMLToIstioOperator(yml string) (*v1alpha1.IstioOperator, *schema.GroupVersionKind, error) { o, err := ParseYAMLToK8sObject([]byte(yml)) if err != nil { return nil, nil, err } iop := &v1alpha1.IstioOperator{} if err := yaml.UnmarshalStrict([]byte(yml), iop); err != nil { return nil, nil, err } gvk := o.GroupVersionKind() v1alpha1.SetNamespace(iop.Spec, o.Namespace) return iop, &gvk, nil } // AllObjectHashes returns a map with object hashes of all the objects contained in cmm as the keys. func AllObjectHashes(m string) map[string]bool { ret := make(map[string]bool) objs, err := ParseK8sObjectsFromYAMLManifest(m) if err != nil { log.Error(err.Error()) } for _, o := range objs { ret[o.Hash()] = true } return ret } // resolvePDBConflict When user uses both minAvailable and // maxUnavailable to configure istio instances, these two // parameters are mutually exclusive, care must be taken // to resolve the issue func resolvePDBConflict(o *K8sObject) *K8sObject { if o.json == nil { return o } if o.object.Object["spec"] == nil { return o } spec := o.object.Object["spec"].(map[string]interface{}) isDefault := func(item interface{}) bool { var ii intstr.IntOrString switch item := item.(type) { case int: ii = intstr.FromInt(item) case int64: ii = intstr.FromInt(int(item)) case string: ii = intstr.FromString(item) default: ii = intstr.FromInt(0) } intVal, err := intstr.GetScaledValueFromIntOrPercent(&ii, 100, false) if err != nil || intVal == 0 { return true } return false } if spec["maxUnavailable"] != nil && spec["minAvailable"] != nil { // When both maxUnavailable and minAvailable present and // neither has value 0, this is considered a conflict, // then maxUnavailale will take precedence. if !isDefault(spec["maxUnavailable"]) && !isDefault(spec["minAvailable"]) { delete(spec, "minAvailable") // Make sure that the json and yaml representation of the object // is consistent with the changed object o.json = nil o.json, _ = o.JSON() if o.yaml != nil { o.yaml = nil o.yaml, _ = o.YAML() } } } return o }