Implement airshipctl cluster status

This change introduces the subcommand `status` to the `airshipctl
cluster` command. The `status` command will iterate over all defined
custom resources in the target path, check their status against the
resources currently in the cluster, and then report results on stdout.

Change-Id: Ieaff6b91fd9055f995c5ba8ce79959356a2a8a02
Relates-To: #73
This commit is contained in:
Ian Howell 2020-03-20 15:05:35 -05:00
parent fcb1202e11
commit eecc417495
17 changed files with 404 additions and 4 deletions

View File

@ -18,6 +18,7 @@ import (
"github.com/spf13/cobra"
"opendev.org/airship/airshipctl/pkg/environment"
"opendev.org/airship/airshipctl/pkg/k8s/client"
"opendev.org/airship/airshipctl/pkg/log"
)
@ -45,6 +46,7 @@ func NewClusterCommand(rootSettings *environment.AirshipCTLSettings) *cobra.Comm
clusterRootCmd.AddCommand(NewInitCommand(rootSettings))
clusterRootCmd.AddCommand(NewMoveCommand(rootSettings))
clusterRootCmd.AddCommand(NewStatusCommand(rootSettings, client.DefaultClient))
return clusterRootCmd
}

82
cmd/cluster/status.go Normal file
View File

@ -0,0 +1,82 @@
/*
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
https://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 cluster
import (
"fmt"
"github.com/spf13/cobra"
"opendev.org/airship/airshipctl/pkg/cluster"
"opendev.org/airship/airshipctl/pkg/document"
"opendev.org/airship/airshipctl/pkg/environment"
"opendev.org/airship/airshipctl/pkg/k8s/client"
"opendev.org/airship/airshipctl/pkg/log"
"opendev.org/airship/airshipctl/pkg/util"
)
// NewStatusCommand creates a command which reports the statuses of a cluster's deployed components.
func NewStatusCommand(rootSettings *environment.AirshipCTLSettings, factory client.Factory) *cobra.Command {
cmd := &cobra.Command{
Use: "status",
Short: "Retrieve statuses of deployed cluster components",
RunE: func(cmd *cobra.Command, args []string) error {
conf := rootSettings.Config
if err := conf.EnsureComplete(); err != nil {
return err
}
manifest, err := conf.CurrentContextManifest()
if err != nil {
return err
}
docBundle, err := document.NewBundleByPath(manifest.TargetPath)
if err != nil {
return err
}
docs, err := docBundle.GetAllDocuments()
if err != nil {
return err
}
client, err := factory(rootSettings)
if err != nil {
return err
}
statusMap, err := cluster.NewStatusMap(client)
if err != nil {
return err
}
tw := util.NewTabWriter(cmd.OutOrStdout())
fmt.Fprintf(tw, "Kind\tName\tStatus\n")
for _, doc := range docs {
status, err := statusMap.GetStatusForResource(doc)
if err != nil {
log.Debug(err)
} else {
fmt.Fprintf(tw, "%s\t%s\t%s\n", doc.GetKind(), doc.GetName(), status)
}
}
tw.Flush()
return nil
},
}
return cmd
}

153
cmd/cluster/status_test.go Normal file
View File

@ -0,0 +1,153 @@
/*
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
https://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 cluster_test
import (
"testing"
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"
"k8s.io/apimachinery/pkg/runtime"
"opendev.org/airship/airshipctl/cmd/cluster"
"opendev.org/airship/airshipctl/pkg/config"
"opendev.org/airship/airshipctl/pkg/environment"
"opendev.org/airship/airshipctl/pkg/k8s/client"
"opendev.org/airship/airshipctl/pkg/k8s/client/fake"
"opendev.org/airship/airshipctl/testutil"
)
const (
fixturesPath = "testdata/statusmap"
)
func TestStatusCmd(t *testing.T) {
tests := []struct {
cmdTest *testutil.CmdTest
resources []runtime.Object
CRDs []runtime.Object
}{
{
cmdTest: &testutil.CmdTest{
Name: "check-status-no-resources",
CmdLine: "",
},
},
{
cmdTest: &testutil.CmdTest{
Name: "check-status-with-resources",
CmdLine: "",
},
resources: []runtime.Object{
makeResource("Resource", "stable-resource", "stable"),
makeResource("Resource", "pending-resource", "pending"),
},
CRDs: []runtime.Object{
makeResourceCRD(annotationValidStatusCheck()),
},
},
}
for _, tt := range tests {
tt := tt
testClientFactory := func(_ *environment.AirshipCTLSettings) (client.Interface, error) {
return fake.NewClient(
fake.WithDynamicObjects(tt.resources...),
fake.WithCRDs(tt.CRDs...),
), nil
}
tt.cmdTest.Cmd = cluster.NewStatusCommand(clusterStatusTestSettings(), testClientFactory)
testutil.RunTest(t, tt.cmdTest)
}
}
func clusterStatusTestSettings() *environment.AirshipCTLSettings {
return &environment.AirshipCTLSettings{
Config: &config.Config{
Clusters: map[string]*config.ClusterPurpose{"testCluster": nil},
AuthInfos: map[string]*config.AuthInfo{"testAuthInfo": nil},
Contexts: map[string]*config.Context{
"testContext": {Manifest: "testManifest"},
},
Manifests: map[string]*config.Manifest{
"testManifest": {TargetPath: fixturesPath},
},
CurrentContext: "testContext",
},
}
}
func makeResource(kind, name, state string) *unstructured.Unstructured {
return &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "example.com/v1",
"kind": kind,
"metadata": map[string]interface{}{
"name": name,
"namespace": "default",
},
"status": map[string]interface{}{
"state": state,
},
},
}
}
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 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",
},
},
}
}

View File

@ -8,6 +8,7 @@ Available Commands:
help Help about any command
init Deploy cluster-api provider components
move Move Cluster API objects, provider specific objects and all dependencies to the target cluster
status Retrieve statuses of deployed cluster components
Flags:
-h, --help help for cluster

View File

@ -0,0 +1 @@
Kind Name Status

View File

@ -0,0 +1,3 @@
Kind Name Status
Resource pending-resource Pending
Resource stable-resource Stable

40
cmd/cluster/testdata/statusmap/crd.yaml vendored Normal file
View File

@ -0,0 +1,40 @@
# this CRD defines a type whose status can be checked using the condition in
# the annotations
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
kind: Resource
shortNames:
- rsc

View File

@ -0,0 +1,8 @@
resources:
- crd.yaml
- stable-resource.yaml
- pending-resource.yaml
- missing.yaml
- unknown.yaml
- legacy-crd.yaml
- legacy-resource.yaml

View File

@ -0,0 +1,43 @@
# this is a legacy CRD which defines a type whose status can be checked using
# the condition in the annotations
# It is included in tests to assure backward compatibility
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
metadata:
name: legacies.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
scope: Namespaced
names:
plural: legacies
singular: legacy
kind: Legacy
shortNames:
- lgc
preserveUnknownFields: false
validation:
openAPIV3Schema:
type: object
properties:
status:
type: object
properties:
state:
type: string

View File

@ -0,0 +1,7 @@
# this legacy-resource is stable because the fake version in the cluster will
# have .status.state == "stable"
apiVersion: "example.com/v1"
kind: Legacy
metadata:
name: stable-legacy
namespace: default

View File

@ -0,0 +1,7 @@
# This resource doesn't have a status-check defined by its CRD (which is also
# missing for brevity). Requesting its status is an error
apiVersion: "example.com/v1"
kind: Missing
metadata:
name: missing-resource
namespace: default

View File

@ -0,0 +1,7 @@
# this resource is pending because the fake version in the cluster will
# have .status.state == "pending"
apiVersion: "example.com/v1"
kind: Resource
metadata:
name: pending-resource
namespace: default

View File

@ -0,0 +1,7 @@
# this resource is stable because the fake version in the cluster will have
# .status.state == "stable"
apiVersion: "example.com/v1"
kind: Resource
metadata:
name: stable-resource
namespace: default

View File

@ -0,0 +1,8 @@
# this resource is in an unknown state because the fake version in the cluster
# will have .status.state == "unknown", which does not correlate to any of the
# status checks in the CRD.
apiVersion: "example.com/v1"
kind: Resource
metadata:
name: unknown
namespace: default

View File

@ -27,4 +27,5 @@ such as getting status and deploying initial infrastructure.
* [airshipctl](airshipctl.md) - A unified entrypoint to various airship components
* [airshipctl cluster init](airshipctl_cluster_init.md) - Deploy cluster-api provider components
* [airshipctl cluster move](airshipctl_cluster_move.md) - Move Cluster API objects, provider specific objects and all dependencies to the target cluster
* [airshipctl cluster status](airshipctl_cluster_status.md) - Retrieve statuses of deployed cluster components

View File

@ -0,0 +1,30 @@
## airshipctl cluster status
Retrieve statuses of deployed cluster components
### Synopsis
Retrieve statuses of deployed cluster components
```
airshipctl cluster status [flags]
```
### Options
```
-h, --help help for status
```
### Options inherited from parent commands
```
--airshipconf string Path to file for airshipctl configuration. (default "$HOME/.airship/config")
--debug enable verbose output
--kubeconfig string Path to kubeconfig associated with airshipctl configuration. (default "$HOME/.airship/kubeconfig")
```
### SEE ALSO
* [airshipctl cluster](airshipctl_cluster.md) - Manage Kubernetes clusters

View File

@ -14,12 +14,12 @@ airshipctl phase render PHASE_NAME [flags]
```
#Get all 'initinfra' phase documents containing labels "app=helm" and
#"service=tiller"
# Get all 'initinfra' phase documents containing labels "app=helm" and
# "service=tiller"
airshipctl phase render initinfra -l app=helm,service=tiller
#Get all documents containing labels "app=helm" and "service=tiller"
#and kind 'Deployment'
# Get all documents containing labels "app=helm" and "service=tiller"
# and kind 'Deployment'
airshipctl phase render initinfra -l app=helm,service=tiller -k Deployment
```