| // 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 |
| } |