istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pkg/config/model.go (about)

     1  // Copyright Istio Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package config
    16  
    17  import (
    18  	"bytes"
    19  	"encoding/json"
    20  	"fmt"
    21  	"reflect"
    22  	"time"
    23  
    24  	gogojsonpb "github.com/gogo/protobuf/jsonpb" // nolint: depguard
    25  	gogoproto "github.com/gogo/protobuf/proto"   // nolint: depguard
    26  	gogotypes "github.com/gogo/protobuf/types"   // nolint: depguard
    27  	"google.golang.org/protobuf/proto"
    28  	"google.golang.org/protobuf/reflect/protoreflect"
    29  	"google.golang.org/protobuf/types/known/anypb"
    30  	"google.golang.org/protobuf/types/known/structpb"
    31  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    32  	"k8s.io/apimachinery/pkg/runtime/schema"
    33  	kubetypes "k8s.io/apimachinery/pkg/types"
    34  	"sigs.k8s.io/yaml"
    35  
    36  	"istio.io/api/label"
    37  	"istio.io/istio/pilot/pkg/util/protoconv"
    38  	"istio.io/istio/pkg/util/gogoprotomarshal"
    39  	"istio.io/istio/pkg/util/protomarshal"
    40  )
    41  
    42  // Meta is metadata attached to each configuration unit.
    43  // The revision is optional, and if provided, identifies the
    44  // last update operation on the object.
    45  type Meta struct {
    46  	// GroupVersionKind is a short configuration name that matches the content message type
    47  	// (e.g. "route-rule")
    48  	GroupVersionKind GroupVersionKind `json:"type,omitempty"`
    49  
    50  	// UID
    51  	UID string `json:"uid,omitempty"`
    52  
    53  	// Name is a unique immutable identifier in a namespace
    54  	Name string `json:"name,omitempty"`
    55  
    56  	// Namespace defines the space for names (optional for some types),
    57  	// applications may choose to use namespaces for a variety of purposes
    58  	// (security domains, fault domains, organizational domains)
    59  	Namespace string `json:"namespace,omitempty"`
    60  
    61  	// Domain defines the suffix of the fully qualified name past the namespace.
    62  	// Domain is not a part of the unique key unlike name and namespace.
    63  	Domain string `json:"domain,omitempty"`
    64  
    65  	// Map of string keys and values that can be used to organize and categorize
    66  	// (scope and select) objects.
    67  	Labels map[string]string `json:"labels,omitempty"`
    68  
    69  	// Annotations is an unstructured key value map stored with a resource that may be
    70  	// set by external tools to store and retrieve arbitrary metadata. They are not
    71  	// queryable and should be preserved when modifying objects.
    72  	Annotations map[string]string `json:"annotations,omitempty"`
    73  
    74  	// ResourceVersion is an opaque identifier for tracking updates to the config registry.
    75  	// The implementation may use a change index or a commit log for the revision.
    76  	// The config client should not make any assumptions about revisions and rely only on
    77  	// exact equality to implement optimistic concurrency of read-write operations.
    78  	//
    79  	// The lifetime of an object of a particular revision depends on the underlying data store.
    80  	// The data store may compactify old revisions in the interest of storage optimization.
    81  	//
    82  	// An empty revision carries a special meaning that the associated object has
    83  	// not been stored and assigned a revision.
    84  	ResourceVersion string `json:"resourceVersion,omitempty"`
    85  
    86  	// CreationTimestamp records the creation time
    87  	CreationTimestamp time.Time `json:"creationTimestamp,omitempty"`
    88  
    89  	// OwnerReferences allows specifying in-namespace owning objects.
    90  	OwnerReferences []metav1.OwnerReference `json:"ownerReferences,omitempty"`
    91  
    92  	// A sequence number representing a specific generation of the desired state. Populated by the system. Read-only.
    93  	Generation int64 `json:"generation,omitempty"`
    94  }
    95  
    96  // Config is a configuration unit consisting of the type of configuration, the
    97  // key identifier that is unique per type, and the content represented as a
    98  // protobuf message.
    99  type Config struct {
   100  	Meta
   101  
   102  	// Spec holds the configuration object as a gogo protobuf message
   103  	Spec Spec
   104  
   105  	// Status holds long-running status.
   106  	Status Status
   107  }
   108  
   109  func LabelsInRevision(lbls map[string]string, rev string) bool {
   110  	configEnv, f := lbls[label.IoIstioRev.Name]
   111  	if !f {
   112  		// This is a global object, and always included
   113  		return true
   114  	}
   115  	// If the revision is empty, this means we don't specify a revision, and
   116  	// we should always include it
   117  	if rev == "" {
   118  		return true
   119  	}
   120  	// Otherwise, only return true if revisions equal
   121  	return configEnv == rev
   122  }
   123  
   124  func ObjectInRevision(o *Config, rev string) bool {
   125  	return LabelsInRevision(o.Labels, rev)
   126  }
   127  
   128  // Spec defines the spec for the config. In order to use below helper methods,
   129  // this must be one of:
   130  // * golang/protobuf Message
   131  // * gogo/protobuf Message
   132  // * Able to marshal/unmarshal using json
   133  type Spec any
   134  
   135  func ToProto(s Spec) (*anypb.Any, error) {
   136  	// golang protobuf. Use protoreflect.ProtoMessage to distinguish from gogo
   137  	// golang/protobuf 1.4+ will have this interface. Older golang/protobuf are gogo compatible
   138  	// but also not used by Istio at all.
   139  	if pb, ok := s.(protoreflect.ProtoMessage); ok {
   140  		return protoconv.MessageToAnyWithError(pb)
   141  	}
   142  
   143  	// gogo protobuf
   144  	if pb, ok := s.(gogoproto.Message); ok {
   145  		gogoany, err := gogotypes.MarshalAny(pb)
   146  		if err != nil {
   147  			return nil, err
   148  		}
   149  		return &anypb.Any{
   150  			TypeUrl: gogoany.TypeUrl,
   151  			Value:   gogoany.Value,
   152  		}, nil
   153  	}
   154  
   155  	js, err := json.Marshal(s)
   156  	if err != nil {
   157  		return nil, err
   158  	}
   159  	pbs := &structpb.Struct{}
   160  	if err := protomarshal.Unmarshal(js, pbs); err != nil {
   161  		return nil, err
   162  	}
   163  	return protoconv.MessageToAnyWithError(pbs)
   164  }
   165  
   166  func ToMap(s Spec) (map[string]any, error) {
   167  	js, err := ToJSON(s)
   168  	if err != nil {
   169  		return nil, err
   170  	}
   171  
   172  	// Unmarshal from json bytes to go map
   173  	var data map[string]any
   174  	err = json.Unmarshal(js, &data)
   175  	if err != nil {
   176  		return nil, err
   177  	}
   178  
   179  	return data, nil
   180  }
   181  
   182  func ToRaw(s Spec) (json.RawMessage, error) {
   183  	js, err := ToJSON(s)
   184  	if err != nil {
   185  		return nil, err
   186  	}
   187  
   188  	// Unmarshal from json bytes to go map
   189  	return js, nil
   190  }
   191  
   192  func ToJSON(s Spec) ([]byte, error) {
   193  	return toJSON(s, false)
   194  }
   195  
   196  func ToPrettyJSON(s Spec) ([]byte, error) {
   197  	return toJSON(s, true)
   198  }
   199  
   200  func toJSON(s Spec, pretty bool) ([]byte, error) {
   201  	indent := ""
   202  	if pretty {
   203  		indent = "    "
   204  	}
   205  
   206  	// golang protobuf. Use protoreflect.ProtoMessage to distinguish from gogo
   207  	// golang/protobuf 1.4+ will have this interface. Older golang/protobuf are gogo compatible
   208  	// but also not used by Istio at all.
   209  	if _, ok := s.(protoreflect.ProtoMessage); ok {
   210  		if pb, ok := s.(proto.Message); ok {
   211  			b, err := protomarshal.MarshalIndent(pb, indent)
   212  			return b, err
   213  		}
   214  	}
   215  
   216  	b := &bytes.Buffer{}
   217  	// gogo protobuf
   218  	if pb, ok := s.(gogoproto.Message); ok {
   219  		err := (&gogojsonpb.Marshaler{Indent: indent}).Marshal(b, pb)
   220  		return b.Bytes(), err
   221  	}
   222  	if pretty {
   223  		return json.MarshalIndent(s, "", indent)
   224  	}
   225  	return json.Marshal(s)
   226  }
   227  
   228  type deepCopier interface {
   229  	DeepCopyInterface() any
   230  }
   231  
   232  func ApplyYAML(s Spec, yml string) error {
   233  	js, err := yaml.YAMLToJSON([]byte(yml))
   234  	if err != nil {
   235  		return err
   236  	}
   237  	return ApplyJSON(s, string(js))
   238  }
   239  
   240  func ApplyJSONStrict(s Spec, js string) error {
   241  	// golang protobuf. Use protoreflect.ProtoMessage to distinguish from gogo
   242  	// golang/protobuf 1.4+ will have this interface. Older golang/protobuf are gogo compatible
   243  	// but also not used by Istio at all.
   244  	if _, ok := s.(protoreflect.ProtoMessage); ok {
   245  		if pb, ok := s.(proto.Message); ok {
   246  			err := protomarshal.ApplyJSONStrict(js, pb)
   247  			return err
   248  		}
   249  	}
   250  
   251  	// gogo protobuf
   252  	if pb, ok := s.(gogoproto.Message); ok {
   253  		err := gogoprotomarshal.ApplyJSONStrict(js, pb)
   254  		return err
   255  	}
   256  
   257  	d := json.NewDecoder(bytes.NewReader([]byte(js)))
   258  	d.DisallowUnknownFields()
   259  	return d.Decode(&s)
   260  }
   261  
   262  func ApplyJSON(s Spec, js string) error {
   263  	// golang protobuf. Use protoreflect.ProtoMessage to distinguish from gogo
   264  	// golang/protobuf 1.4+ will have this interface. Older golang/protobuf are gogo compatible
   265  	// but also not used by Istio at all.
   266  	if _, ok := s.(protoreflect.ProtoMessage); ok {
   267  		if pb, ok := s.(proto.Message); ok {
   268  			err := protomarshal.ApplyJSON(js, pb)
   269  			return err
   270  		}
   271  	}
   272  
   273  	// gogo protobuf
   274  	if pb, ok := s.(gogoproto.Message); ok {
   275  		err := gogoprotomarshal.ApplyJSON(js, pb)
   276  		return err
   277  	}
   278  
   279  	return json.Unmarshal([]byte(js), &s)
   280  }
   281  
   282  func DeepCopy(s any) any {
   283  	if s == nil {
   284  		return nil
   285  	}
   286  	// If deep copy is defined, use that
   287  	if dc, ok := s.(deepCopier); ok {
   288  		return dc.DeepCopyInterface()
   289  	}
   290  
   291  	// golang protobuf. Use protoreflect.ProtoMessage to distinguish from gogo
   292  	// golang/protobuf 1.4+ will have this interface. Older golang/protobuf are gogo compatible
   293  	// but also not used by Istio at all.
   294  	if _, ok := s.(protoreflect.ProtoMessage); ok {
   295  		if pb, ok := s.(proto.Message); ok {
   296  			return proto.Clone(pb)
   297  		}
   298  	}
   299  
   300  	// gogo protobuf
   301  	if pb, ok := s.(gogoproto.Message); ok {
   302  		return gogoproto.Clone(pb)
   303  	}
   304  
   305  	// If we don't have a deep copy method, we will have to do some reflection magic. Its not ideal,
   306  	// but all Istio types have an efficient deep copy.
   307  	js, err := json.Marshal(s)
   308  	if err != nil {
   309  		return nil
   310  	}
   311  
   312  	data := reflect.New(reflect.TypeOf(s)).Interface()
   313  	if err := json.Unmarshal(js, data); err != nil {
   314  		return nil
   315  	}
   316  	data = reflect.ValueOf(data).Elem().Interface()
   317  	return data
   318  }
   319  
   320  type Status any
   321  
   322  // Key function for the configuration objects
   323  func Key(grp, ver, typ, name, namespace string) string {
   324  	return grp + "/" + ver + "/" + typ + "/" + namespace + "/" + name // Format: %s/%s/%s/%s/%s
   325  }
   326  
   327  // Key is the unique identifier for a configuration object
   328  func (meta *Meta) Key() string {
   329  	return Key(
   330  		meta.GroupVersionKind.Group, meta.GroupVersionKind.Version, meta.GroupVersionKind.Kind,
   331  		meta.Name, meta.Namespace)
   332  }
   333  
   334  func (meta *Meta) ToObjectMeta() metav1.ObjectMeta {
   335  	return metav1.ObjectMeta{
   336  		Name:              meta.Name,
   337  		Namespace:         meta.Namespace,
   338  		UID:               kubetypes.UID(meta.UID),
   339  		ResourceVersion:   meta.ResourceVersion,
   340  		Generation:        meta.Generation,
   341  		CreationTimestamp: metav1.NewTime(meta.CreationTimestamp),
   342  		Labels:            meta.Labels,
   343  		Annotations:       meta.Annotations,
   344  		OwnerReferences:   meta.OwnerReferences,
   345  	}
   346  }
   347  
   348  func (c Config) DeepCopy() Config {
   349  	var clone Config
   350  	clone.Meta = c.Meta
   351  	if c.Labels != nil {
   352  		clone.Labels = make(map[string]string, len(c.Labels))
   353  		for k, v := range c.Labels {
   354  			clone.Labels[k] = v
   355  		}
   356  	}
   357  	if c.Annotations != nil {
   358  		clone.Annotations = make(map[string]string, len(c.Annotations))
   359  		for k, v := range c.Annotations {
   360  			clone.Annotations[k] = v
   361  		}
   362  	}
   363  	clone.Spec = DeepCopy(c.Spec)
   364  	if c.Status != nil {
   365  		clone.Status = DeepCopy(c.Status)
   366  	}
   367  	return clone
   368  }
   369  
   370  func (c Config) GetName() string {
   371  	return c.Name
   372  }
   373  
   374  func (c Config) GetNamespace() string {
   375  	return c.Namespace
   376  }
   377  
   378  func (c Config) GetCreationTimestamp() time.Time {
   379  	return c.CreationTimestamp
   380  }
   381  
   382  func (c Config) NamespacedName() kubetypes.NamespacedName {
   383  	return kubetypes.NamespacedName{
   384  		Namespace: c.Namespace,
   385  		Name:      c.Name,
   386  	}
   387  }
   388  
   389  var _ fmt.Stringer = GroupVersionKind{}
   390  
   391  type GroupVersionKind struct {
   392  	Group   string `json:"group"`
   393  	Version string `json:"version"`
   394  	Kind    string `json:"kind"`
   395  }
   396  
   397  func (g GroupVersionKind) String() string {
   398  	return g.CanonicalGroup() + "/" + g.Version + "/" + g.Kind
   399  }
   400  
   401  // GroupVersion returns the group/version similar to what would be found in the apiVersion field of a Kubernetes resource.
   402  func (g GroupVersionKind) GroupVersion() string {
   403  	if g.Group == "" {
   404  		return g.Version
   405  	}
   406  	return g.Group + "/" + g.Version
   407  }
   408  
   409  func FromKubernetesGVK(gvk schema.GroupVersionKind) GroupVersionKind {
   410  	return GroupVersionKind{
   411  		Group:   gvk.Group,
   412  		Version: gvk.Version,
   413  		Kind:    gvk.Kind,
   414  	}
   415  }
   416  
   417  // Kubernetes returns the same GVK, using the Kubernetes object type
   418  func (g GroupVersionKind) Kubernetes() schema.GroupVersionKind {
   419  	return schema.GroupVersionKind{
   420  		Group:   g.Group,
   421  		Version: g.Version,
   422  		Kind:    g.Kind,
   423  	}
   424  }
   425  
   426  func CanonicalGroup(group string) string {
   427  	if group != "" {
   428  		return group
   429  	}
   430  	return "core"
   431  }
   432  
   433  // CanonicalGroup returns the group with defaulting applied. This means an empty group will
   434  // be treated as "core", following Kubernetes API standards
   435  func (g GroupVersionKind) CanonicalGroup() string {
   436  	return CanonicalGroup(g.Group)
   437  }
   438  
   439  // PatchFunc provides the cached config as a base for modification. Only diff the between the cfg
   440  // parameter and the returned Config will be applied.
   441  type PatchFunc func(cfg Config) (Config, kubetypes.PatchType)
   442  
   443  type Namer interface {
   444  	GetName() string
   445  	GetNamespace() string
   446  }
   447  
   448  func NamespacedName(o metav1.Object) kubetypes.NamespacedName {
   449  	return kubetypes.NamespacedName{
   450  		Namespace: o.GetNamespace(),
   451  		Name:      o.GetName(),
   452  	}
   453  }