Start api directory, which will become a module.

This commit is contained in:
Jeffrey Regan
2019-10-17 11:36:34 -07:00
parent 180429774a
commit e5c8b5ec8f
226 changed files with 623 additions and 689 deletions

200
api/resid/gvk.go Normal file
View File

@@ -0,0 +1,200 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package resid
import (
"strings"
)
// Gvk identifies a Kubernetes API type.
// https://github.com/kubernetes/community/blob/master/contributors/design-proposals/api-machinery/api-group.md
type Gvk struct {
Group string `json:"group,omitempty" yaml:"group,omitempty"`
Version string `json:"version,omitempty" yaml:"version,omitempty"`
Kind string `json:"kind,omitempty" yaml:"kind,omitempty"`
}
// FromKind makes a Gvk with only the kind specified.
func FromKind(k string) Gvk {
return Gvk{
Kind: k,
}
}
// GvkFromString makes a Gvk with a string,
// which is constructed by String() function
func GvkFromString(s string) Gvk {
values := strings.Split(s, fieldSep)
g := values[0]
if g == noGroup {
g = ""
}
v := values[1]
if v == noVersion {
v = ""
}
k := values[2]
if k == noKind {
k = ""
}
return Gvk{
Group: g,
Version: v,
Kind: k,
}
}
// Values that are brief but meaningful in logs.
const (
noGroup = "~G"
noVersion = "~V"
noKind = "~K"
fieldSep = "_"
)
// String returns a string representation of the GVK.
func (x Gvk) String() string {
g := x.Group
if g == "" {
g = noGroup
}
v := x.Version
if v == "" {
v = noVersion
}
k := x.Kind
if k == "" {
k = noKind
}
return strings.Join([]string{g, v, k}, fieldSep)
}
// Equals returns true if the Gvk's have equal fields.
func (x Gvk) Equals(o Gvk) bool {
return x.Group == o.Group && x.Version == o.Version && x.Kind == o.Kind
}
// An attempt to order things to help k8s, e.g.
// a Service should come before things that refer to it.
// Namespace should be first.
// In some cases order just specified to provide determinism.
var orderFirst = []string{
"Namespace",
"ResourceQuota",
"StorageClass",
"CustomResourceDefinition",
"MutatingWebhookConfiguration",
"ServiceAccount",
"PodSecurityPolicy",
"Role",
"ClusterRole",
"RoleBinding",
"ClusterRoleBinding",
"ConfigMap",
"Secret",
"Service",
"LimitRange",
"PriorityClass",
"Deployment",
"StatefulSet",
"CronJob",
"PodDisruptionBudget",
}
var orderLast = []string{
"ValidatingWebhookConfiguration",
}
var typeOrders = func() map[string]int {
m := map[string]int{}
for i, n := range orderFirst {
m[n] = -len(orderFirst) + i
}
for i, n := range orderLast {
m[n] = 1 + i
}
return m
}()
// IsLessThan returns true if self is less than the argument.
func (x Gvk) IsLessThan(o Gvk) bool {
indexI := typeOrders[x.Kind]
indexJ := typeOrders[o.Kind]
if indexI != indexJ {
return indexI < indexJ
}
return x.String() < o.String()
}
// IsSelected returns true if `selector` selects `x`; otherwise, false.
// If `selector` and `x` are the same, return true.
// If `selector` is nil, it is considered a wildcard match, returning true.
// If selector fields are empty, they are considered wildcards matching
// anything in the corresponding fields, e.g.
//
// this item:
// <Group: "extensions", Version: "v1beta1", Kind: "Deployment">
//
// is selected by
// <Group: "", Version: "", Kind: "Deployment">
//
// but rejected by
// <Group: "apps", Version: "", Kind: "Deployment">
//
func (x Gvk) IsSelected(selector *Gvk) bool {
if selector == nil {
return true
}
if len(selector.Group) > 0 {
if x.Group != selector.Group {
return false
}
}
if len(selector.Version) > 0 {
if x.Version != selector.Version {
return false
}
}
if len(selector.Kind) > 0 {
if x.Kind != selector.Kind {
return false
}
}
return true
}
var notNamespaceableKinds = []string{
"APIService",
"CSIDriver",
"CSINode",
"CertificateSigningRequest",
"ClusterRole",
"ClusterRoleBinding",
"ComponentStatus",
"CustomResourceDefinition",
"MutatingWebhookConfiguration",
"Namespace",
"Node",
"PersistentVolume",
"PodSecurityPolicy",
"PodSecurityPolicy",
"PriorityClass",
"RuntimeClass",
"SelfSubjectAccessReview",
"SelfSubjectRulesReview",
"StorageClass",
"SubjectAccessReview",
"TokenReview",
"ValidatingWebhookConfiguration",
"VolumeAttachment",
}
// IsNamespaceableKind returns true if x is a namespaceable Gvk
// Implements https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/#not-all-objects-are-in-a-namespace
func (x Gvk) IsNamespaceableKind() bool {
for _, k := range notNamespaceableKinds {
if k == x.Kind {
return false
}
}
return true
}

218
api/resid/gvk_test.go Normal file
View File

@@ -0,0 +1,218 @@
/*
Copyright 2018 The Kubernetes 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 resid
import (
"testing"
)
var equalsTests = []struct {
x1 Gvk
x2 Gvk
}{
{Gvk{Group: "a", Version: "b", Kind: "c"},
Gvk{Group: "a", Version: "b", Kind: "c"}},
{Gvk{Version: "b", Kind: "c"},
Gvk{Version: "b", Kind: "c"}},
{Gvk{Kind: "c"},
Gvk{Kind: "c"}},
}
func TestEquals(t *testing.T) {
for _, hey := range equalsTests {
if !hey.x1.Equals(hey.x2) {
t.Fatalf("%v should equal %v", hey.x1, hey.x2)
}
}
}
var lessThanTests = []struct {
x1 Gvk
x2 Gvk
}{
{Gvk{Group: "a", Version: "b", Kind: "CustomResourceDefinition"},
Gvk{Group: "a", Version: "b", Kind: "RoleBinding"}},
{Gvk{Group: "a", Version: "b", Kind: "Namespace"},
Gvk{Group: "a", Version: "b", Kind: "ClusterRole"}},
{Gvk{Group: "a", Version: "b", Kind: "a"},
Gvk{Group: "a", Version: "b", Kind: "b"}},
{Gvk{Group: "a", Version: "b", Kind: "Namespace"},
Gvk{Group: "a", Version: "c", Kind: "Namespace"}},
{Gvk{Group: "a", Version: "c", Kind: "Namespace"},
Gvk{Group: "b", Version: "c", Kind: "Namespace"}},
{Gvk{Group: "b", Version: "c", Kind: "Namespace"},
Gvk{Group: "a", Version: "c", Kind: "ClusterRole"}},
{Gvk{Group: "a", Version: "c", Kind: "Namespace"},
Gvk{Group: "a", Version: "b", Kind: "ClusterRole"}},
{Gvk{Group: "a", Version: "d", Kind: "Namespace"},
Gvk{Group: "b", Version: "c", Kind: "Namespace"}},
{Gvk{Group: "a", Version: "b", Kind: orderFirst[len(orderFirst)-1]},
Gvk{Group: "a", Version: "b", Kind: orderLast[0]}},
{Gvk{Group: "a", Version: "b", Kind: orderFirst[len(orderFirst)-1]},
Gvk{Group: "a", Version: "b", Kind: "CustomKindX"}},
{Gvk{Group: "a", Version: "b", Kind: "CustomKindX"},
Gvk{Group: "a", Version: "b", Kind: orderLast[0]}},
{Gvk{Group: "a", Version: "b", Kind: "CustomKindA"},
Gvk{Group: "a", Version: "b", Kind: "CustomKindB"}},
{Gvk{Group: "a", Version: "b", Kind: "CustomKindX"},
Gvk{Group: "a", Version: "b", Kind: "ValidatingWebhookConfiguration"}},
{Gvk{Group: "a", Version: "b", Kind: "APIService"},
Gvk{Group: "a", Version: "b", Kind: "ValidatingWebhookConfiguration"}},
{Gvk{Group: "a", Version: "b", Kind: "Service"},
Gvk{Group: "a", Version: "b", Kind: "APIService"}},
}
func TestIsLessThan1(t *testing.T) {
for _, hey := range lessThanTests {
if !hey.x1.IsLessThan(hey.x2) {
t.Fatalf("%v should be less than %v", hey.x1, hey.x2)
}
if hey.x2.IsLessThan(hey.x1) {
t.Fatalf("%v should not be less than %v", hey.x2, hey.x1)
}
}
}
var stringTests = []struct {
x Gvk
s string
}{
{Gvk{}, "~G_~V_~K"},
{Gvk{Kind: "k"}, "~G_~V_k"},
{Gvk{Version: "v"}, "~G_v_~K"},
{Gvk{Version: "v", Kind: "k"}, "~G_v_k"},
{Gvk{Group: "g"}, "g_~V_~K"},
{Gvk{Group: "g", Kind: "k"}, "g_~V_k"},
{Gvk{Group: "g", Version: "v"}, "g_v_~K"},
{Gvk{Group: "g", Version: "v", Kind: "k"}, "g_v_k"},
}
func TestString(t *testing.T) {
for _, hey := range stringTests {
if hey.x.String() != hey.s {
t.Fatalf("bad string for %v '%s'", hey.x, hey.s)
}
}
}
func TestSelectByGVK(t *testing.T) {
type testCase struct {
description string
in Gvk
filter *Gvk
expected bool
}
testCases := []testCase{
{
description: "nil filter",
in: Gvk{},
filter: nil,
expected: true,
},
{
description: "gvk matches",
in: Gvk{
Group: "group1",
Version: "version1",
Kind: "kind1",
},
filter: &Gvk{
Group: "group1",
Version: "version1",
Kind: "kind1",
},
expected: true,
},
{
description: "group doesn't matches",
in: Gvk{
Group: "group1",
Version: "version1",
Kind: "kind1",
},
filter: &Gvk{
Group: "group2",
Version: "version1",
Kind: "kind1",
},
expected: false,
},
{
description: "version doesn't matches",
in: Gvk{
Group: "group1",
Version: "version1",
Kind: "kind1",
},
filter: &Gvk{
Group: "group1",
Version: "version2",
Kind: "kind1",
},
expected: false,
},
{
description: "kind doesn't matches",
in: Gvk{
Group: "group1",
Version: "version1",
Kind: "kind1",
},
filter: &Gvk{
Group: "group1",
Version: "version1",
Kind: "kind2",
},
expected: false,
},
{
description: "no version in filter",
in: Gvk{
Group: "group1",
Version: "version1",
Kind: "kind1",
},
filter: &Gvk{
Group: "group1",
Version: "",
Kind: "kind1",
},
expected: true,
},
{
description: "only kind is set in filter",
in: Gvk{
Group: "group1",
Version: "version1",
Kind: "kind1",
},
filter: &Gvk{
Group: "",
Version: "",
Kind: "kind1",
},
expected: true,
},
}
for _, tc := range testCases {
filtered := tc.in.IsSelected(tc.filter)
if filtered != tc.expected {
t.Fatalf("unexpected filter result for test case: %v", tc.description)
}
}
}

127
api/resid/resid.go Normal file
View File

@@ -0,0 +1,127 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package resid
import (
"strings"
)
// ResId is an identifier of a k8s resource object.
type ResId struct {
// Gvk of the resource.
Gvk `json:",inline,omitempty" yaml:",inline,omitempty"`
// Name of the resource before transformation.
Name string `json:"name,omitempty" yaml:"name,omitempty"`
// Namespace the resource belongs to.
// An untransformed resource has no namespace.
// A fully transformed resource has the namespace
// from the top most overlay.
Namespace string `json:"namespace,omitempty" yaml:"namespace,omitempty"`
}
// NewResIdWithNamespace creates new ResId
// in a given namespace.
func NewResIdWithNamespace(k Gvk, n, ns string) ResId {
return ResId{Gvk: k, Name: n, Namespace: ns}
}
// NewResId creates new ResId.
func NewResId(k Gvk, n string) ResId {
return ResId{Gvk: k, Name: n}
}
// NewResIdKindOnly creates a new ResId.
func NewResIdKindOnly(k string, n string) ResId {
return ResId{Gvk: FromKind(k), Name: n}
}
const (
noNamespace = "~X"
noName = "~N"
separator = "|"
TotallyNotANamespace = "_non_namespaceable_"
DefaultNamespace = "default"
)
// String of ResId based on GVK, name and prefix
func (id ResId) String() string {
ns := id.Namespace
if ns == "" {
ns = noNamespace
}
nm := id.Name
if nm == "" {
nm = noName
}
return strings.Join(
[]string{id.Gvk.String(), ns, nm}, separator)
}
func FromString(s string) ResId {
values := strings.Split(s, separator)
g := GvkFromString(values[0])
ns := values[1]
if ns == noNamespace {
ns = ""
}
nm := values[2]
if nm == noName {
nm = ""
}
return ResId{
Gvk: g,
Namespace: ns,
Name: nm,
}
}
// GvknString of ResId based on GVK and name
func (id ResId) GvknString() string {
return id.Gvk.String() + separator + id.Name
}
// GvknEquals returns true if the other id matches
// Group/Version/Kind/name.
func (id ResId) GvknEquals(o ResId) bool {
return id.Name == o.Name && id.Gvk.Equals(o.Gvk)
}
// Equals returns true if the other id matches
// namespace/Group/Version/Kind/name.
func (id ResId) Equals(o ResId) bool {
return id.IsNsEquals(o) && id.GvknEquals(o)
}
// IsNsEquals returns true if the id is in
// the same effective namespace.
func (id ResId) IsNsEquals(o ResId) bool {
return id.EffectiveNamespace() == o.EffectiveNamespace()
}
// IsInDefaultNs returns true if id is a namespaceable
// ResId and the Namespace is either not set or set
// to DefaultNamespace.
func (id ResId) IsInDefaultNs() bool {
return id.IsNamespaceableKind() && id.isPutativelyDefaultNs()
}
func (id ResId) isPutativelyDefaultNs() bool {
return id.Namespace == "" || id.Namespace == DefaultNamespace
}
// EffectiveNamespace returns a non-ambiguous, non-empty
// namespace for use in reporting and equality tests.
func (id ResId) EffectiveNamespace() string {
// The order of these checks matters.
if !id.IsNamespaceableKind() {
return TotallyNotANamespace
}
if id.isPutativelyDefaultNs() {
return DefaultNamespace
}
return id.Namespace
}

423
api/resid/resid_test.go Normal file
View File

@@ -0,0 +1,423 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package resid
import (
"testing"
)
var resIdStringTests = []struct {
x ResId
s string
}{
{
ResId{
Namespace: "ns",
Gvk: Gvk{Group: "g", Version: "v", Kind: "k"},
Name: "nm",
},
"g_v_k|ns|nm",
},
{
ResId{
Namespace: "ns",
Gvk: Gvk{Version: "v", Kind: "k"},
Name: "nm",
},
"~G_v_k|ns|nm",
},
{
ResId{
Namespace: "ns",
Gvk: Gvk{Kind: "k"},
Name: "nm",
},
"~G_~V_k|ns|nm",
},
{
ResId{
Namespace: "ns",
Gvk: Gvk{},
Name: "nm",
},
"~G_~V_~K|ns|nm",
},
{
ResId{
Gvk: Gvk{},
Name: "nm",
},
"~G_~V_~K|~X|nm",
},
{
ResId{
Gvk: Gvk{},
Name: "nm",
},
"~G_~V_~K|~X|nm",
},
{
ResId{
Gvk: Gvk{},
},
"~G_~V_~K|~X|~N",
},
{
ResId{
Gvk: Gvk{},
},
"~G_~V_~K|~X|~N",
},
{
ResId{},
"~G_~V_~K|~X|~N",
},
}
func TestResIdString(t *testing.T) {
for _, hey := range resIdStringTests {
if hey.x.String() != hey.s {
t.Fatalf("Actual: %v, Expected: '%s'", hey.x, hey.s)
}
}
}
var gvknStringTests = []struct {
x ResId
s string
}{
{
ResId{
Namespace: "ns",
Gvk: Gvk{Group: "g", Version: "v", Kind: "k"},
Name: "nm",
},
"g_v_k|nm",
},
{
ResId{
Namespace: "ns",
Gvk: Gvk{Version: "v", Kind: "k"},
Name: "nm",
},
"~G_v_k|nm",
},
{
ResId{
Namespace: "ns",
Gvk: Gvk{Kind: "k"},
Name: "nm",
},
"~G_~V_k|nm",
},
{
ResId{
Namespace: "ns",
Gvk: Gvk{},
Name: "nm",
},
"~G_~V_~K|nm",
},
{
ResId{
Gvk: Gvk{},
Name: "nm",
},
"~G_~V_~K|nm",
},
{
ResId{
Gvk: Gvk{},
Name: "nm",
},
"~G_~V_~K|nm",
},
{
ResId{
Gvk: Gvk{},
},
"~G_~V_~K|",
},
{
ResId{
Gvk: Gvk{},
},
"~G_~V_~K|",
},
{
ResId{},
"~G_~V_~K|",
},
}
func TestGvknString(t *testing.T) {
for _, hey := range gvknStringTests {
if hey.x.GvknString() != hey.s {
t.Fatalf("Actual: %s, Expected: '%s'", hey.x.GvknString(), hey.s)
}
}
}
func TestResIdEquals(t *testing.T) {
var GvknEqualsTest = []struct {
id1 ResId
id2 ResId
gVknResult bool
nsEquals bool
equals bool
}{
{
id1: ResId{
Namespace: "X",
Gvk: Gvk{Group: "g", Version: "v", Kind: "k"},
Name: "nm",
},
id2: ResId{
Namespace: "X",
Gvk: Gvk{Group: "g", Version: "v", Kind: "k"},
Name: "nm",
},
gVknResult: true,
nsEquals: true,
equals: true,
},
{
id1: ResId{
Namespace: "X",
Gvk: Gvk{Group: "g", Version: "v", Kind: "k"},
Name: "nm",
},
id2: ResId{
Namespace: "Z",
Gvk: Gvk{Group: "g", Version: "v", Kind: "k"},
Name: "nm",
},
gVknResult: true,
nsEquals: false,
equals: false,
},
{
id1: ResId{
Namespace: "X",
Gvk: Gvk{Group: "g", Version: "v", Kind: "k"},
Name: "nm",
},
id2: ResId{
Gvk: Gvk{Group: "g", Version: "v", Kind: "k"},
Name: "nm",
},
gVknResult: true,
nsEquals: false,
equals: false,
},
{
id1: ResId{
Namespace: "X",
Gvk: Gvk{Version: "v", Kind: "k"},
Name: "nm",
},
id2: ResId{
Namespace: "Z",
Gvk: Gvk{Version: "v", Kind: "k"},
Name: "nm",
},
gVknResult: true,
nsEquals: false,
equals: false,
},
{
id1: ResId{
Namespace: "X",
Gvk: Gvk{Kind: "k"},
Name: "nm",
},
id2: ResId{
Namespace: "Z",
Gvk: Gvk{Kind: "k"},
Name: "nm",
},
gVknResult: true,
nsEquals: false,
equals: false,
},
{
id1: ResId{
Gvk: Gvk{Kind: "k"},
Name: "nm",
},
id2: ResId{
Gvk: Gvk{Kind: "k"},
Name: "nm2",
},
gVknResult: false,
nsEquals: true,
equals: false,
},
{
id1: ResId{
Gvk: Gvk{Kind: "k"},
Name: "nm",
},
id2: ResId{
Gvk: Gvk{Kind: "Node"},
Name: "nm",
},
gVknResult: false,
nsEquals: false,
equals: false,
},
{
id1: ResId{
Gvk: Gvk{Kind: "Node"},
Name: "nm1",
},
id2: ResId{
Gvk: Gvk{Kind: "Node"},
Name: "nm2",
},
gVknResult: false,
nsEquals: true,
equals: false,
},
{
id1: ResId{
Namespace: "default",
Gvk: Gvk{Kind: "k"},
Name: "nm1",
},
id2: ResId{
Gvk: Gvk{Kind: "k"},
Name: "nm2",
},
gVknResult: false,
nsEquals: true,
equals: false,
},
{
id1: ResId{
Namespace: "X",
Name: "nm",
},
id2: ResId{
Namespace: "Z",
Name: "nm",
},
gVknResult: true,
nsEquals: false,
equals: false,
},
}
for _, tst := range GvknEqualsTest {
if tst.id1.GvknEquals(tst.id2) != tst.gVknResult {
t.Fatalf("GvknEquals(\n%v,\n%v\n) should be %v",
tst.id1, tst.id2, tst.gVknResult)
}
if tst.id1.IsNsEquals(tst.id2) != tst.nsEquals {
t.Fatalf("IsNsEquals(\n%v,\n%v\n) should be %v",
tst.id1, tst.id2, tst.equals)
}
if tst.id1.Equals(tst.id2) != tst.equals {
t.Fatalf("Equals(\n%v,\n%v\n) should be %v",
tst.id1, tst.id2, tst.equals)
}
}
}
var ids = []ResId{
{
Namespace: "ns",
Gvk: Gvk{Group: "g", Version: "v", Kind: "k"},
Name: "nm",
},
{
Namespace: "ns",
Gvk: Gvk{Version: "v", Kind: "k"},
Name: "nm",
},
{
Namespace: "ns",
Gvk: Gvk{Kind: "k"},
Name: "nm",
},
{
Namespace: "ns",
Gvk: Gvk{},
Name: "nm",
},
{
Gvk: Gvk{},
Name: "nm",
},
{
Gvk: Gvk{},
Name: "nm",
},
{
Gvk: Gvk{},
},
}
func TestFromString(t *testing.T) {
for _, id := range ids {
newId := FromString(id.String())
if newId != id {
t.Fatalf("Actual: %v, Expected: '%s'", newId, id)
}
}
}
func TestEffectiveNamespace(t *testing.T) {
var test = []struct {
id ResId
expected string
}{
{
id: ResId{
Gvk: Gvk{Group: "g", Version: "v", Kind: "Node"},
Name: "nm",
},
expected: TotallyNotANamespace,
},
{
id: ResId{
Namespace: "foo",
Gvk: Gvk{Group: "g", Version: "v", Kind: "Node"},
Name: "nm",
},
expected: TotallyNotANamespace,
},
{
id: ResId{
Namespace: "foo",
Gvk: Gvk{Group: "g", Version: "v", Kind: "k"},
Name: "nm",
},
expected: "foo",
},
{
id: ResId{
Namespace: "",
Gvk: Gvk{Group: "g", Version: "v", Kind: "k"},
Name: "nm",
},
expected: DefaultNamespace,
},
{
id: ResId{
Gvk: Gvk{Group: "g", Version: "v", Kind: "k"},
Name: "nm",
},
expected: DefaultNamespace,
},
}
for _, tst := range test {
if actual := tst.id.EffectiveNamespace(); actual != tst.expected {
t.Fatalf("EffectiveNamespace was %s, expected %s",
actual, tst.expected)
}
}
}