blob: 27960120f9c3316a26eea703822d7300c8f41b99 [file] [log] [blame]
/*
* 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 config
import (
"bytes"
"encoding/json"
"fmt"
"reflect"
"time"
"github.com/apache/dubbo-kubernetes/pkg/slices"
gogojsonpb "github.com/gogo/protobuf/jsonpb" // nolint: depguard
gogoproto "github.com/gogo/protobuf/proto" // nolint: depguard
gogotypes "github.com/gogo/protobuf/types" // nolint: depguard
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/types/known/anypb"
"google.golang.org/protobuf/types/known/structpb"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
kubetypes "k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/yaml"
"github.com/apache/dubbo-kubernetes/pkg/cluster"
"github.com/apache/dubbo-kubernetes/pkg/maps"
"github.com/apache/dubbo-kubernetes/pkg/ptr"
"github.com/apache/dubbo-kubernetes/pkg/util/gogoprotomarshal"
"github.com/apache/dubbo-kubernetes/pkg/util/protomarshal"
"github.com/apache/dubbo-kubernetes/pkg/util/sets"
"github.com/apache/dubbo-kubernetes/dubbod/planet/pkg/util/protoconv"
)
// Meta is metadata attached to each configuration unit.
// The revision is optional, and if provided, identifies the
// last update operation on the object.
type Meta struct {
// GroupVersionKind is a short configuration name that matches the content message type
// (e.g. "route-rule")
GroupVersionKind GroupVersionKind `json:"type,omitempty"`
// UID
UID string `json:"uid,omitempty"`
// Name is a unique immutable identifier in a namespace
Name string `json:"name,omitempty"`
// Namespace defines the space for names (optional for some types),
// applications may choose to use namespaces for a variety of purposes
// (security domains, fault domains, organizational domains)
Namespace string `json:"namespace,omitempty"`
// Domain defines the suffix of the fully qualified name past the namespace.
// Domain is not a part of the unique key unlike name and namespace.
Domain string `json:"domain,omitempty"`
// Map of string keys and values that can be used to organize and categorize
// (scope and select) objects.
Labels map[string]string `json:"labels,omitempty"`
// Annotations is an unstructured key value map stored with a resource that may be
// set by external tools to store and retrieve arbitrary metadata. They are not
// queryable and should be preserved when modifying objects.
Annotations map[string]string `json:"annotations,omitempty"`
// ResourceVersion is an opaque identifier for tracking updates to the config registry.
// The implementation may use a change index or a commit log for the revision.
// The config client should not make any assumptions about revisions and rely only on
// exact equality to implement optimistic concurrency of read-write operations.
//
// The lifetime of an object of a particular revision depends on the underlying data store.
// The data store may compactify old revisions in the interest of storage optimization.
//
// An empty revision carries a special meaning that the associated object has
// not been stored and assigned a revision.
ResourceVersion string `json:"resourceVersion,omitempty"`
// CreationTimestamp records the creation time
CreationTimestamp time.Time `json:"creationTimestamp,omitempty"`
// OwnerReferences allows specifying in-namespace owning objects.
OwnerReferences []metav1.OwnerReference `json:"ownerReferences,omitempty"`
// A sequence number representing a specific generation of the desired state. Populated by the system. Read-only.
Generation int64 `json:"generation,omitempty"`
}
// Config is a configuration unit consisting of the type of configuration, the
// key identifier that is unique per type, and the content represented as a
// protobuf message.
type Config struct {
Meta
// Spec holds the configuration object as a gogo protobuf message
Spec Spec
// Status holds long-running status.
Status Status
// Extra holds additional, non-spec information for internal processing.
Extra map[string]any
}
type ObjectWithCluster[T any] struct {
ClusterID cluster.ID
Object *T
}
// We can't refer to krt directly without causing an import cycle, but this function
// implements an interface that allows the krt helper to know how to get the object key
func (o ObjectWithCluster[T]) GetObjectKeyable() any {
if o.Object == nil {
return nil
}
return *o.Object
}
func LabelsInRevision(lbls map[string]string, rev string) bool {
configEnv, f := lbls["dubbo.apache.org/rev"]
if !f {
// This is a global object, and always included
return true
}
// If the revision is empty, this means we don't specify a revision, and
// we should always include it
if rev == "" {
return true
}
// Otherwise, only return true if revisions equal
return configEnv == rev
}
func LabelsInRevisionOrTags(lbls map[string]string, rev string, tags sets.Set[string]) bool {
if LabelsInRevision(lbls, rev) {
return true
}
configEnv := lbls["dubbo.apache.org/rev"]
// Otherwise, only return true if revisions equal
return tags.Contains(configEnv)
}
func ObjectInRevision(o *Config, rev string) bool {
return LabelsInRevision(o.Labels, rev)
}
// Spec defines the spec for the config. In order to use below helper methods,
// this must be one of:
// * golang/protobuf Message
// * gogo/protobuf Message
// * Able to marshal/unmarshal using json
type Spec any
func ToProto(s Spec) (*anypb.Any, error) {
// golang protobuf. Use protoreflect.ProtoMessage to distinguish from gogo
// golang/protobuf 1.4+ will have this interface. Older golang/protobuf are gogo compatible
// but also not used by Istio at all.
if pb, ok := s.(protoreflect.ProtoMessage); ok {
return protoconv.MessageToAnyWithError(pb)
}
// gogo protobuf
if pb, ok := s.(gogoproto.Message); ok {
gogoany, err := gogotypes.MarshalAny(pb)
if err != nil {
return nil, err
}
return &anypb.Any{
TypeUrl: gogoany.TypeUrl,
Value: gogoany.Value,
}, nil
}
js, err := json.Marshal(s)
if err != nil {
return nil, err
}
pbs := &structpb.Struct{}
if err := protomarshal.Unmarshal(js, pbs); err != nil {
return nil, err
}
return protoconv.MessageToAnyWithError(pbs)
}
func ToMap(s Spec) (map[string]any, error) {
js, err := ToJSON(s)
if err != nil {
return nil, err
}
// Unmarshal from json bytes to go map
var data map[string]any
err = json.Unmarshal(js, &data)
if err != nil {
return nil, err
}
return data, nil
}
func ToRaw(s Spec) (json.RawMessage, error) {
js, err := ToJSON(s)
if err != nil {
return nil, err
}
// Unmarshal from json bytes to go map
return js, nil
}
func ToJSON(s Spec) ([]byte, error) {
return toJSON(s, false)
}
func ToPrettyJSON(s Spec) ([]byte, error) {
return toJSON(s, true)
}
func toJSON(s Spec, pretty bool) ([]byte, error) {
indent := ""
if pretty {
indent = " "
}
// golang protobuf. Use protoreflect.ProtoMessage to distinguish from gogo
// golang/protobuf 1.4+ will have this interface. Older golang/protobuf are gogo compatible
// but also not used by Istio at all.
if _, ok := s.(protoreflect.ProtoMessage); ok {
if pb, ok := s.(proto.Message); ok {
b, err := protomarshal.MarshalIndent(pb, indent)
return b, err
}
}
b := &bytes.Buffer{}
// gogo protobuf
if pb, ok := s.(gogoproto.Message); ok {
err := (&gogojsonpb.Marshaler{Indent: indent}).Marshal(b, pb)
return b.Bytes(), err
}
if pretty {
return json.MarshalIndent(s, "", indent)
}
return json.Marshal(s)
}
type deepCopier interface {
DeepCopyInterface() any
}
func ApplyYAML(s Spec, yml string) error {
js, err := yaml.YAMLToJSON([]byte(yml))
if err != nil {
return err
}
return ApplyJSON(s, string(js))
}
func ApplyJSONStrict(s Spec, js string) error {
// golang protobuf. Use protoreflect.ProtoMessage to distinguish from gogo
// golang/protobuf 1.4+ will have this interface. Older golang/protobuf are gogo compatible
// but also not used by Istio at all.
if _, ok := s.(protoreflect.ProtoMessage); ok {
if pb, ok := s.(proto.Message); ok {
err := protomarshal.ApplyJSONStrict(js, pb)
return err
}
}
// gogo protobuf
if pb, ok := s.(gogoproto.Message); ok {
err := gogoprotomarshal.ApplyJSONStrict(js, pb)
return err
}
d := json.NewDecoder(bytes.NewReader([]byte(js)))
d.DisallowUnknownFields()
return d.Decode(&s)
}
func ApplyJSON(s Spec, js string) error {
// golang protobuf. Use protoreflect.ProtoMessage to distinguish from gogo
// golang/protobuf 1.4+ will have this interface. Older golang/protobuf are gogo compatible
// but also not used by Istio at all.
if _, ok := s.(protoreflect.ProtoMessage); ok {
if pb, ok := s.(proto.Message); ok {
err := protomarshal.ApplyJSON(js, pb)
return err
}
}
// gogo protobuf
if pb, ok := s.(gogoproto.Message); ok {
err := gogoprotomarshal.ApplyJSON(js, pb)
return err
}
return json.Unmarshal([]byte(js), &s)
}
func DeepCopy(s any) any {
if s == nil {
return nil
}
// If deep copy is defined, use that
if dc, ok := s.(deepCopier); ok {
return dc.DeepCopyInterface()
}
// golang protobuf. Use protoreflect.ProtoMessage to distinguish from gogo
// golang/protobuf 1.4+ will have this interface. Older golang/protobuf are gogo compatible
// but also not used by Istio at all.
if _, ok := s.(protoreflect.ProtoMessage); ok {
if pb, ok := s.(proto.Message); ok {
return protomarshal.Clone(pb)
}
}
// gogo protobuf
if pb, ok := s.(gogoproto.Message); ok {
return gogoproto.Clone(pb)
}
// If we don't have a deep copy method, we will have to do some reflection magic. Its not ideal,
// but all Istio types have an efficient deep copy.
js, err := json.Marshal(s)
if err != nil {
return nil
}
data := reflect.New(reflect.TypeOf(s)).Interface()
if err := json.Unmarshal(js, data); err != nil {
return nil
}
data = reflect.ValueOf(data).Elem().Interface()
return data
}
func (c *Config) Equals(other *Config) bool {
am, bm := c.Meta, other.Meta
if am.GroupVersionKind != bm.GroupVersionKind {
return false
}
if am.UID != bm.UID {
return false
}
if am.Name != bm.Name {
return false
}
if am.Namespace != bm.Namespace {
return false
}
if am.Domain != bm.Domain {
return false
}
if !maps.Equal(am.Labels, bm.Labels) {
return false
}
if !maps.Equal(am.Annotations, bm.Annotations) {
return false
}
if am.ResourceVersion != bm.ResourceVersion {
return false
}
if am.CreationTimestamp != bm.CreationTimestamp {
return false
}
if !slices.EqualFunc(am.OwnerReferences, bm.OwnerReferences, func(a metav1.OwnerReference, b metav1.OwnerReference) bool {
if a.APIVersion != b.APIVersion {
return false
}
if a.Kind != b.Kind {
return false
}
if a.Name != b.Name {
return false
}
if a.UID != b.UID {
return false
}
if !ptr.Equal(a.Controller, b.Controller) {
return false
}
if !ptr.Equal(a.BlockOwnerDeletion, b.BlockOwnerDeletion) {
return false
}
return true
}) {
return false
}
if am.Generation != bm.Generation {
return false
}
if !equals(c.Spec, other.Spec) {
return false
}
if !equals(c.Status, other.Status) {
return false
}
// Can't use map.Equal because store maps as the value
if !equals(c.Extra, other.Extra) {
return false
}
return true
}
func equals(a any, b any) bool {
if _, ok := a.(protoreflect.ProtoMessage); ok {
if pb, ok := a.(proto.Message); ok {
return proto.Equal(pb, b.(proto.Message))
}
}
// We do NOT do gogo here. The reason is Kubernetes has hacked up almost-gogo types that do not allow Equals() calls
return reflect.DeepEqual(a, b)
}
type Status any
// Key function for the configuration objects
func Key(grp, ver, typ, name, namespace string) string {
return grp + "/" + ver + "/" + typ + "/" + namespace + "/" + name // Format: %s/%s/%s/%s/%s
}
// Key is the unique identifier for a configuration object
func (meta *Meta) Key() string {
return Key(
meta.GroupVersionKind.Group, meta.GroupVersionKind.Version, meta.GroupVersionKind.Kind,
meta.Name, meta.Namespace)
}
func (meta *Meta) ToObjectMeta() metav1.ObjectMeta {
return metav1.ObjectMeta{
Name: meta.Name,
Namespace: meta.Namespace,
UID: kubetypes.UID(meta.UID),
ResourceVersion: meta.ResourceVersion,
Generation: meta.Generation,
CreationTimestamp: metav1.NewTime(meta.CreationTimestamp),
Labels: meta.Labels,
Annotations: meta.Annotations,
OwnerReferences: meta.OwnerReferences,
}
}
func (meta Meta) DeepCopy() Meta {
nm := meta
nm.Labels = maps.Clone(meta.Labels)
nm.Annotations = maps.Clone(meta.Annotations)
return nm
}
func (c Config) DeepCopy() Config {
var clone Config
clone.Meta = c.Meta.DeepCopy()
clone.Spec = DeepCopy(c.Spec)
if c.Status != nil {
clone.Status = DeepCopy(c.Status)
}
// Note that this is effectively a shallow clone, but this is fine as it is not manipulated.
if c.Extra != nil {
clone.Extra = maps.Clone(c.Extra)
}
return clone
}
func (c Config) GetName() string {
return c.Name
}
func (c Config) GetNamespace() string {
return c.Namespace
}
func (c Config) GetCreationTimestamp() time.Time {
return c.CreationTimestamp
}
func (c Config) NamespacedName() kubetypes.NamespacedName {
return kubetypes.NamespacedName{
Namespace: c.Namespace,
Name: c.Name,
}
}
var _ fmt.Stringer = GroupVersionKind{}
type GroupVersionKind struct {
Group string `json:"group"`
Version string `json:"version"`
Kind string `json:"kind"`
}
func (g GroupVersionKind) String() string {
return g.CanonicalGroup() + "/" + g.Version + "/" + g.Kind
}
// GroupVersion returns the group/version similar to what would be found in the apiVersion field of a Kubernetes resource.
func (g GroupVersionKind) GroupVersion() string {
if g.Group == "" {
return g.Version
}
return g.Group + "/" + g.Version
}
func FromKubernetesGVK(gvk schema.GroupVersionKind) GroupVersionKind {
return GroupVersionKind{
Group: gvk.Group,
Version: gvk.Version,
Kind: gvk.Kind,
}
}
// Kubernetes returns the same GVK, using the Kubernetes object type
func (g GroupVersionKind) Kubernetes() schema.GroupVersionKind {
return schema.GroupVersionKind{
Group: g.Group,
Version: g.Version,
Kind: g.Kind,
}
}
func CanonicalGroup(group string) string {
if group != "" {
return group
}
return "core"
}
// CanonicalGroup returns the group with defaulting applied. This means an empty group will
// be treated as "core", following Kubernetes API standards
func (g GroupVersionKind) CanonicalGroup() string {
return CanonicalGroup(g.Group)
}
// PatchFunc provides the cached config as a base for modification. Only diff the between the cfg
// parameter and the returned Config will be applied.
type PatchFunc func(cfg Config) (Config, kubetypes.PatchType)
type Namer interface {
GetName() string
GetNamespace() string
}
func NamespacedName[T Namer](o T) kubetypes.NamespacedName {
return kubetypes.NamespacedName{
Namespace: o.GetNamespace(),
Name: o.GetName(),
}
}