Merge "Create StatusMaps from current cluster state"

This commit is contained in:
Zuul 2020-04-30 12:01:01 +00:00 committed by Gerrit Code Review
commit 2cba93cbaf
20 changed files with 137 additions and 450 deletions

View File

@ -39,35 +39,35 @@ const (
// a resource may be in, as well as the Expression used to check for that
// status.
type StatusMap struct {
client client.Interface
mapping map[schema.GroupVersionResource]map[Status]Expression
restMapper *meta.DefaultRESTMapper
}
// NewStatusMap creates a StatusMap for a given document bundle. It iterates
// over all CustomResourceDefinitions that are annotated with the
// NewStatusMap creates a cluster-wide StatusMap. It iterates over all
// CustomResourceDefinitions in the cluster that are annotated with the
// airshipit.org/status-check annotation and creates a mapping from the
// GroupVersionResource to the various statuses and their associated
// expressions.
func NewStatusMap(docBundle document.Bundle) (*StatusMap, error) {
func NewStatusMap(client client.Interface) (*StatusMap, error) {
statusMap := &StatusMap{
client: client,
mapping: make(map[schema.GroupVersionResource]map[Status]Expression),
restMapper: meta.NewDefaultRESTMapper([]schema.GroupVersion{}),
}
v1CRDS, err := docBundle.GetByGvk("apiextensions.k8s.io", "v1", "CustomResourceDefinition")
crds, err := statusMap.client.ApiextensionsClientSet().
ApiextensionsV1().
CustomResourceDefinitions().
List(metav1.ListOptions{})
if err != nil {
return nil, err
}
// for legacy support
v1beta1CRDS, err := docBundle.GetByGvk("apiextensions.k8s.io", "v1beta1", "CustomResourceDefinition")
if err != nil {
return nil, err
}
crds := append(v1CRDS, v1beta1CRDS...)
if err = statusMap.addCRDs(crds); err != nil {
return nil, err
for _, crd := range crds.Items {
if err = statusMap.addCRD(crd); err != nil {
return nil, err
}
}
return statusMap, nil
@ -75,7 +75,7 @@ func NewStatusMap(docBundle document.Bundle) (*StatusMap, error) {
// GetStatusForResource iterates over all of the stored conditions for the
// resource and returns the first status whose conditions are met.
func (sm *StatusMap) GetStatusForResource(client client.Interface, resource document.Document) (Status, error) {
func (sm *StatusMap) GetStatusForResource(resource document.Document) (Status, error) {
gvk, err := getGVK(resource)
if err != nil {
return "", err
@ -87,7 +87,7 @@ func (sm *StatusMap) GetStatusForResource(client client.Interface, resource docu
}
gvr := restMapping.Resource
obj, err := client.DynamicClient().Resource(gvr).Namespace(resource.GetNamespace()).
obj, err := sm.client.DynamicClient().Resource(gvr).Namespace(resource.GetNamespace()).
Get(resource.GetName(), metav1.GetOptions{})
if err != nil {
return "", err
@ -109,39 +109,28 @@ func (sm *StatusMap) GetStatusForResource(client client.Interface, resource docu
return UnknownStatus, nil
}
// addCRDs adds the mappings from each crd to their associated statuses.
func (sm *StatusMap) addCRDs(crdDocs []document.Document) error {
for _, crdDoc := range crdDocs {
annotations := crdDoc.GetAnnotations()
rawStatusChecks, ok := annotations["airshipit.org/status-check"]
if !ok {
// This crdDoc doesn't have a status-check
// annotation, so we should skip it.
continue
}
statusChecks, err := parseStatusChecks(rawStatusChecks)
if err != nil {
return err
}
jsonData, err := crdDoc.MarshalJSON()
if err != nil {
return err
}
var crd apiextensions.CustomResourceDefinition
err = json.Unmarshal(jsonData, &crd)
if err != nil {
return err
}
gvrs := getGVRs(crd)
for _, gvr := range gvrs {
gvk := gvr.GroupVersion().WithKind(crd.Spec.Names.Kind)
gvrSingular := gvr.GroupVersion().WithResource(crd.Spec.Names.Singular)
sm.mapping[gvr] = statusChecks
sm.restMapper.AddSpecific(gvk, gvr, gvrSingular, meta.RESTScopeNamespace)
}
// addCRD adds the mappings from the CRD to its associated statuses
func (sm *StatusMap) addCRD(crd apiextensions.CustomResourceDefinition) error {
annotations := crd.GetAnnotations()
rawStatusChecks, ok := annotations["airshipit.org/status-check"]
if !ok {
// This crd doesn't have a status-check
// annotation, so we should skip it.
return nil
}
statusChecks, err := parseStatusChecks(rawStatusChecks)
if err != nil {
return err
}
gvrs := getGVRs(crd)
for _, gvr := range gvrs {
gvk := gvr.GroupVersion().WithKind(crd.Spec.Names.Kind)
gvrSingular := gvr.GroupVersion().WithResource(crd.Spec.Names.Singular)
sm.mapping[gvr] = statusChecks
sm.restMapper.AddSpecific(gvk, gvr, gvrSingular, meta.RESTScopeNamespace)
}
return nil
}

View File

@ -15,12 +15,13 @@
package cluster_test
import (
"errors"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"opendev.org/airship/airshipctl/pkg/cluster"
@ -29,65 +30,35 @@ import (
"opendev.org/airship/airshipctl/testutil"
)
type fakeBundle struct {
document.Bundle
mockGetByGvk func(string, string, string) ([]document.Document, error)
}
func (fb fakeBundle) GetByGvk(group, version, kind string) ([]document.Document, error) {
return fb.mockGetByGvk(group, version, kind)
}
func TestNewStatusMapErrorCases(t *testing.T) {
dummyError := errors.New("test error")
func TestNewStatusMap(t *testing.T) {
tests := []struct {
name string
bundle document.Bundle
client *fake.Client
err error
}{
{
name: "bundle-fails-retrieving-v1-resources",
bundle: fakeBundle{
mockGetByGvk: func(_, version, _ string) ([]document.Document, error) {
if version == "v1" {
return nil, dummyError
}
return nil, nil
},
},
err: dummyError,
},
{
name: "bundle-fails-retrieving-v1beta1-resources",
bundle: fakeBundle{
mockGetByGvk: func(_, version, _ string) ([]document.Document, error) {
if version == "v1beta1" {
return nil, dummyError
}
return nil, nil
},
},
err: dummyError,
name: "no-failure-on-valid-status-check-annotation",
client: fake.NewClient(fake.WithCRDs(makeResourceCRD(annotationValidStatusCheck()))),
err: nil,
},
{
name: "no-failure-when-missing-status-check-annotation",
bundle: testutil.NewTestBundle(t, "testdata/missing-status-check"),
client: fake.NewClient(fake.WithCRDs(makeResourceCRD(nil))),
err: nil,
},
{
name: "missing-status",
bundle: testutil.NewTestBundle(t, "testdata/missing-status"),
client: fake.NewClient(fake.WithCRDs(makeResourceCRD(annotationMissingStatus()))),
err: cluster.ErrInvalidStatusCheck{What: "missing status field"},
},
{
name: "missing-condition",
bundle: testutil.NewTestBundle(t, "testdata/missing-condition"),
client: fake.NewClient(fake.WithCRDs(makeResourceCRD(annotationMissingCondition()))),
err: cluster.ErrInvalidStatusCheck{What: "missing condition field"},
},
{
name: "malformed-status-check",
bundle: testutil.NewTestBundle(t, "testdata/malformed-status-check"),
client: fake.NewClient(fake.WithCRDs(makeResourceCRD(annotationMalformedStatusCheck()))),
err: cluster.ErrInvalidStatusCheck{What: `unable to parse jsonpath: ` +
`"{invalid json": invalid character 'i' looking for beginning of object key string`},
},
@ -95,7 +66,7 @@ func TestNewStatusMapErrorCases(t *testing.T) {
for _, tt := range tests {
tt := tt
_, err := cluster.NewStatusMap(tt.bundle)
_, err := cluster.NewStatusMap(tt.client)
assert.Equal(t, tt.err, err)
}
}
@ -104,7 +75,7 @@ func TestGetStatusForResource(t *testing.T) {
tests := []struct {
name string
selector document.Selector
testClient *fake.Client
client *fake.Client
expectedStatus cluster.Status
err error
}{
@ -113,7 +84,8 @@ func TestGetStatusForResource(t *testing.T) {
selector: document.NewSelector().
ByGvk("example.com", "v1", "Resource").
ByName("stable-resource"),
testClient: fake.NewClient(
client: fake.NewClient(
fake.WithCRDs(makeResourceCRD(annotationValidStatusCheck())),
fake.WithDynamicObjects(makeResource("Resource", "stable-resource", "stable")),
),
expectedStatus: cluster.Status("Stable"),
@ -123,7 +95,8 @@ func TestGetStatusForResource(t *testing.T) {
selector: document.NewSelector().
ByGvk("example.com", "v1", "Resource").
ByName("pending-resource"),
testClient: fake.NewClient(
client: fake.NewClient(
fake.WithCRDs(makeResourceCRD(annotationValidStatusCheck())),
fake.WithDynamicObjects(makeResource("Resource", "pending-resource", "pending")),
),
expectedStatus: cluster.Status("Pending"),
@ -133,28 +106,19 @@ func TestGetStatusForResource(t *testing.T) {
selector: document.NewSelector().
ByGvk("example.com", "v1", "Resource").
ByName("unknown"),
testClient: fake.NewClient(
client: fake.NewClient(
fake.WithCRDs(makeResourceCRD(annotationValidStatusCheck())),
fake.WithDynamicObjects(makeResource("Resource", "unknown", "unknown")),
),
expectedStatus: cluster.UnknownStatus,
},
{
name: "stable-legacy-is-stable",
selector: document.NewSelector().
ByGvk("example.com", "v1", "Legacy").
ByName("stable-legacy"),
testClient: fake.NewClient(
fake.WithDynamicObjects(makeResource("Legacy", "stable-legacy", "stable")),
),
expectedStatus: cluster.Status("Stable"),
},
{
name: "missing-resource-returns-error",
selector: document.NewSelector().
ByGvk("example.com", "v1", "Missing").
ByName("missing-resource"),
testClient: fake.NewClient(),
err: cluster.ErrResourceNotFound{Resource: "missing-resource"},
client: fake.NewClient(),
err: cluster.ErrResourceNotFound{Resource: "missing-resource"},
},
}
@ -163,13 +127,13 @@ func TestGetStatusForResource(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
bundle := testutil.NewTestBundle(t, "testdata/statusmap")
testStatusMap, err := cluster.NewStatusMap(bundle)
testStatusMap, err := cluster.NewStatusMap(tt.client)
require.NoError(t, err)
doc, err := bundle.SelectOne(tt.selector)
require.NoError(t, err)
actualStatus, err := testStatusMap.GetStatusForResource(tt.testClient, doc)
actualStatus, err := testStatusMap.GetStatusForResource(doc)
if tt.err != nil {
assert.EqualError(t, err, tt.err.Error())
// We expected an error - no need to check anything else
@ -197,3 +161,81 @@ func makeResource(kind, name, state string) *unstructured.Unstructured {
},
}
}
func makeResourceCRD(annotations map[string]string) *apiextensionsv1.CustomResourceDefinition {
return &apiextensionsv1.CustomResourceDefinition{
TypeMeta: metav1.TypeMeta{
Kind: "CustomResourceDefinition",
APIVersion: "apiextensions.k8s.io/v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: "resources.example.com",
Annotations: annotations,
},
Spec: apiextensionsv1.CustomResourceDefinitionSpec{
Group: "example.com",
Versions: []apiextensionsv1.CustomResourceDefinitionVersion{
{
Name: "v1",
Served: true,
Storage: true,
},
},
// omitting the openAPIV3Schema for brevity
Scope: "Namespaced",
Names: apiextensionsv1.CustomResourceDefinitionNames{
Kind: "Resource",
Plural: "resources",
Singular: "resource",
},
},
}
}
func annotationValidStatusCheck() map[string]string {
return map[string]string{
"airshipit.org/status-check": `
[
{
"status": "Stable",
"condition": "@.status.state==\"stable\""
},
{
"status": "Pending",
"condition": "@.status.state==\"pending\""
}
]`,
}
}
func annotationMissingStatus() map[string]string {
return map[string]string{
"airshipit.org/status-check": `
[
{
"condition": "@.status.state==\"stable\""
},
{
"condition": "@.status.state==\"pending\""
}
]`,
}
}
func annotationMissingCondition() map[string]string {
return map[string]string{
"airshipit.org/status-check": `
[
{
"status": "Stable"
},
{
"status": "Pending"
}
]`,
}
}
func annotationMalformedStatusCheck() map[string]string {
return map[string]string{"airshipit.org/status-check": "{invalid json"}
}

View File

@ -1,31 +0,0 @@
# this CRD defines a type, but does not have a status-check defined in the
# annotations. This is not an error, but a StatusMap won't be able to perform
# any validation on resources
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: resources.example.com
annotations:
airshipit.org/status-check: "{invalid json"
spec:
group: example.com
versions:
- name: v1
served: true
storage: true
schema:
openAPIV3Schema:
type: object
properties:
status:
type: object
properties:
state:
type: string
scope: Namespaced
names:
plural: resources
singular: resource
kind: Resource
shortNames:
- rsc

View File

@ -1,2 +0,0 @@
resources:
- crd.yaml

View File

@ -1,39 +0,0 @@
# this CRD defines a type, but does not have a status-check defined in the
# annotations. This is not an error, but a StatusMap won't be able to perform
# any validation on resources
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: resources.example.com
annotations:
airshipit.org/status-check: |
[
{
"status": "Stable"
},
{
"status": "Pending"
}
]
spec:
group: example.com
versions:
- name: v1
served: true
storage: true
schema:
openAPIV3Schema:
type: object
properties:
status:
type: object
properties:
state:
type: string
scope: Namespaced
names:
plural: resources
singular: resource
kind: Resource
shortNames:
- rsc

View File

@ -1,2 +0,0 @@
resources:
- crd.yaml

View File

@ -1,40 +0,0 @@
# this CRD defines a type, but does not have a status-check defined in the
# annotations. This is not an error, but a StatusMap won't be able to perform
# any validation on resources
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: resources.example.com
annotations:
airshipit.org/status-check: |
[
{
"status": "Stable",
"condition": "@.status.state==\"stable\""
},
{
"status": "Pending",
"condition": "@.status.state==\"pending\""
}
]
spec:
versions:
- name: v1
served: true
storage: true
schema:
openAPIV3Schema:
type: object
properties:
status:
type: object
properties:
state:
type: string
scope: Namespaced
names:
plural: resources
singular: resource
kind: Resource
shortNames:
- rsc

View File

@ -1,2 +0,0 @@
resources:
- crd.yaml

View File

@ -1,40 +0,0 @@
# this CRD defines a type, but does not have a status-check defined in the
# annotations. This is not an error, but a StatusMap won't be able to perform
# any validation on resources
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: resources.example.com
annotations:
airshipit.org/status-check: |
[
{
"status": "Stable",
"condition": "@.status.state==\"stable\""
},
{
"status": "Pending",
"condition": "@.status.state==\"pending\""
}
]
spec:
group: example.com
versions:
- name: v1
served: true
storage: true
schema:
openAPIV3Schema:
type: object
properties:
status:
type: object
properties:
state:
type: string
scope: Namespaced
names:
plural: resources
singular: resource
shortNames:
- rsc

View File

@ -1,2 +0,0 @@
resources:
- crd.yaml

View File

@ -1,40 +0,0 @@
# this CRD defines a type, but does not have a status-check defined in the
# annotations. This is not an error, but a StatusMap won't be able to perform
# any validation on resources
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: resources.example.com
annotations:
airshipit.org/status-check: |
[
{
"status": "Stable",
"condition": "@.status.state==\"stable\""
},
{
"status": "Pending",
"condition": "@.status.state==\"pending\""
}
]
spec:
group: example.com
versions:
- name: v1
served: true
storage: true
schema:
openAPIV3Schema:
type: object
properties:
status:
type: object
properties:
state:
type: string
scope: Namespaced
names:
singular: resource
kind: Resource
shortNames:
- rsc

View File

@ -1,2 +0,0 @@
resources:
- crd.yaml

View File

@ -1,40 +0,0 @@
# this CRD defines a type, but does not have a status-check defined in the
# annotations. This is not an error, but a StatusMap won't be able to perform
# any validation on resources
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: resources.example.com
annotations:
airshipit.org/status-check: |
[
{
"status": "Stable",
"condition": "@.status.state==\"stable\""
},
{
"status": "Pending",
"condition": "@.status.state==\"pending\""
}
]
spec:
group: example.com
versions:
- name: v1
served: true
storage: true
schema:
openAPIV3Schema:
type: object
properties:
status:
type: object
properties:
state:
type: string
scope: Namespaced
names:
plural: resources
kind: Resource
shortNames:
- rsc

View File

@ -1,2 +0,0 @@
resources:
- crd.yaml

View File

@ -1,29 +0,0 @@
# this CRD defines a type, but does not have a status-check defined in the
# annotations. This is not an error, but a StatusMap won't be able to perform
# any validation on resources
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: resources.example.com
spec:
group: example.com
versions:
- name: v1
served: true
storage: true
schema:
openAPIV3Schema:
type: object
properties:
status:
type: object
properties:
state:
type: string
scope: Namespaced
names:
plural: resources
singular: resource
kind: Resource
shortNames:
- rsc

View File

@ -1,2 +0,0 @@
resources:
- crd.yaml

View File

@ -1,39 +0,0 @@
# this CRD defines a type, but does not have a status-check defined in the
# annotations. This is not an error, but a StatusMap won't be able to perform
# any validation on resources
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: resources.example.com
annotations:
airshipit.org/status-check: |
[
{
"condition": "@.status.state==\"stable\""
},
{
"condition": "@.status.state==\"pending\""
}
]
spec:
group: example.com
versions:
- name: v1
served: true
storage: true
schema:
openAPIV3Schema:
type: object
properties:
status:
type: object
properties:
state:
type: string
scope: Namespaced
names:
plural: resources
singular: resource
kind: Resource
shortNames:
- rsc

View File

@ -1,2 +0,0 @@
resources:
- crd.yaml

View File

@ -1,28 +0,0 @@
# this CRD defines a type, but does not have a status-check defined in the
# annotations. This is not an error, but a StatusMap won't be able to perform
# any validation on resources
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: resources.example.com
annotations:
airshipit.org/status-check: |
[
{
"status": "Stable",
"condition": "@.status.state==\"stable\""
},
{
"status": "Pending",
"condition": "@.status.state==\"pending\""
}
]
spec:
group: example.com
scope: Namespaced
names:
plural: resources
singular: resource
kind: Resource
shortNames:
- rsc

View File

@ -1,2 +0,0 @@
resources:
- crd.yaml