blob: d1e2c6c628f8bbf1175d3438f5d906b96cc56d39 [file] [log] [blame]
// 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
}