1// Copyright 2019 The Kubernetes Authors.
2// SPDX-License-Identifier: Apache-2.0
3
4package resid
5
6import (
7	"strings"
8
9	"sigs.k8s.io/kustomize/kyaml/openapi"
10	"sigs.k8s.io/kustomize/kyaml/yaml"
11)
12
13// Gvk identifies a Kubernetes API type.
14// https://github.com/kubernetes/community/blob/master/contributors/design-proposals/api-machinery/api-group.md
15type Gvk struct {
16	Group   string `json:"group,omitempty" yaml:"group,omitempty"`
17	Version string `json:"version,omitempty" yaml:"version,omitempty"`
18	Kind    string `json:"kind,omitempty" yaml:"kind,omitempty"`
19	// isClusterScoped is true if the object is known, per the openapi
20	// data in use, to be cluster scoped, and false otherwise.
21	isClusterScoped bool
22}
23
24func NewGvk(g, v, k string) Gvk {
25	result := Gvk{Group: g, Version: v, Kind: k}
26	result.isClusterScoped =
27		openapi.IsCertainlyClusterScoped(result.AsTypeMeta())
28	return result
29}
30
31func GvkFromNode(r *yaml.RNode) Gvk {
32	g, v := ParseGroupVersion(r.GetApiVersion())
33	return NewGvk(g, v, r.GetKind())
34}
35
36// FromKind makes a Gvk with only the kind specified.
37func FromKind(k string) Gvk {
38	return NewGvk("", "", k)
39}
40
41// ParseGroupVersion parses a KRM metadata apiVersion field.
42func ParseGroupVersion(apiVersion string) (group, version string) {
43	if i := strings.Index(apiVersion, "/"); i > -1 {
44		return apiVersion[:i], apiVersion[i+1:]
45	}
46	return "", apiVersion
47}
48
49// GvkFromString makes a Gvk from the output of Gvk.String().
50func GvkFromString(s string) Gvk {
51	values := strings.Split(s, fieldSep)
52	if len(values) != 3 {
53		// ...then the string didn't come from Gvk.String().
54		return Gvk{
55			Group:   noGroup,
56			Version: noVersion,
57			Kind:    noKind,
58		}
59	}
60	g := values[0]
61	if g == noGroup {
62		g = ""
63	}
64	v := values[1]
65	if v == noVersion {
66		v = ""
67	}
68	k := values[2]
69	if k == noKind {
70		k = ""
71	}
72	return NewGvk(g, v, k)
73}
74
75// Values that are brief but meaningful in logs.
76const (
77	noGroup   = "~G"
78	noVersion = "~V"
79	noKind    = "~K"
80	fieldSep  = "_"
81)
82
83// String returns a string representation of the GVK.
84func (x Gvk) String() string {
85	g := x.Group
86	if g == "" {
87		g = noGroup
88	}
89	v := x.Version
90	if v == "" {
91		v = noVersion
92	}
93	k := x.Kind
94	if k == "" {
95		k = noKind
96	}
97	return strings.Join([]string{g, v, k}, fieldSep)
98}
99
100// ApiVersion returns the combination of Group and Version
101func (x Gvk) ApiVersion() string {
102	var sb strings.Builder
103	if x.Group != "" {
104		sb.WriteString(x.Group)
105		sb.WriteString("/")
106	}
107	sb.WriteString(x.Version)
108	return sb.String()
109}
110
111// StringWoEmptyField returns a string representation of the GVK. Non-exist
112// fields will be omitted.
113func (x Gvk) StringWoEmptyField() string {
114	var s []string
115	if x.Group != "" {
116		s = append(s, x.Group)
117	}
118	if x.Version != "" {
119		s = append(s, x.Version)
120	}
121	if x.Kind != "" {
122		s = append(s, x.Kind)
123	}
124	return strings.Join(s, fieldSep)
125}
126
127// Equals returns true if the Gvk's have equal fields.
128func (x Gvk) Equals(o Gvk) bool {
129	return x.Group == o.Group && x.Version == o.Version && x.Kind == o.Kind
130}
131
132// An attempt to order things to help k8s, e.g.
133// a Service should come before things that refer to it.
134// Namespace should be first.
135// In some cases order just specified to provide determinism.
136var orderFirst = []string{
137	"Namespace",
138	"ResourceQuota",
139	"StorageClass",
140	"CustomResourceDefinition",
141	"ServiceAccount",
142	"PodSecurityPolicy",
143	"Role",
144	"ClusterRole",
145	"RoleBinding",
146	"ClusterRoleBinding",
147	"ConfigMap",
148	"Secret",
149	"Endpoints",
150	"Service",
151	"LimitRange",
152	"PriorityClass",
153	"PersistentVolume",
154	"PersistentVolumeClaim",
155	"Deployment",
156	"StatefulSet",
157	"CronJob",
158	"PodDisruptionBudget",
159}
160var orderLast = []string{
161	"MutatingWebhookConfiguration",
162	"ValidatingWebhookConfiguration",
163}
164var typeOrders = func() map[string]int {
165	m := map[string]int{}
166	for i, n := range orderFirst {
167		m[n] = -len(orderFirst) + i
168	}
169	for i, n := range orderLast {
170		m[n] = 1 + i
171	}
172	return m
173}()
174
175// IsLessThan returns true if self is less than the argument.
176func (x Gvk) IsLessThan(o Gvk) bool {
177	indexI := typeOrders[x.Kind]
178	indexJ := typeOrders[o.Kind]
179	if indexI != indexJ {
180		return indexI < indexJ
181	}
182	return x.String() < o.String()
183}
184
185// IsSelected returns true if `selector` selects `x`; otherwise, false.
186// If `selector` and `x` are the same, return true.
187// If `selector` is nil, it is considered a wildcard match, returning true.
188// If selector fields are empty, they are considered wildcards matching
189// anything in the corresponding fields, e.g.
190//
191// this item:
192//       <Group: "extensions", Version: "v1beta1", Kind: "Deployment">
193//
194// is selected by
195//       <Group: "",           Version: "",        Kind: "Deployment">
196//
197// but rejected by
198//       <Group: "apps",       Version: "",        Kind: "Deployment">
199//
200func (x Gvk) IsSelected(selector *Gvk) bool {
201	if selector == nil {
202		return true
203	}
204	if len(selector.Group) > 0 {
205		if x.Group != selector.Group {
206			return false
207		}
208	}
209	if len(selector.Version) > 0 {
210		if x.Version != selector.Version {
211			return false
212		}
213	}
214	if len(selector.Kind) > 0 {
215		if x.Kind != selector.Kind {
216			return false
217		}
218	}
219	return true
220}
221
222// AsTypeMeta returns a yaml.TypeMeta from x's information.
223func (x Gvk) AsTypeMeta() yaml.TypeMeta {
224	return yaml.TypeMeta{
225		APIVersion: x.ApiVersion(),
226		Kind:       x.Kind,
227	}
228}
229
230// IsClusterScoped returns true if the Gvk is certainly cluster scoped
231// with respect to the available openapi data.
232func (x Gvk) IsClusterScoped() bool {
233	return x.isClusterScoped
234}
235