diff --git a/cmd/cluster/cluster.go b/cmd/cluster/cluster.go index 36401a8ce..e49c1b8a4 100644 --- a/cmd/cluster/cluster.go +++ b/cmd/cluster/cluster.go @@ -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 } diff --git a/cmd/cluster/status.go b/cmd/cluster/status.go new file mode 100644 index 000000000..ef77cd211 --- /dev/null +++ b/cmd/cluster/status.go @@ -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 +} diff --git a/cmd/cluster/status_test.go b/cmd/cluster/status_test.go new file mode 100644 index 000000000..3abb0b290 --- /dev/null +++ b/cmd/cluster/status_test.go @@ -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", + }, + }, + } +} diff --git a/cmd/cluster/testdata/TestNewClusterCommandGoldenOutput/cluster-cmd-with-help.golden b/cmd/cluster/testdata/TestNewClusterCommandGoldenOutput/cluster-cmd-with-help.golden index a365fa364..416509fc3 100644 --- a/cmd/cluster/testdata/TestNewClusterCommandGoldenOutput/cluster-cmd-with-help.golden +++ b/cmd/cluster/testdata/TestNewClusterCommandGoldenOutput/cluster-cmd-with-help.golden @@ -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 diff --git a/cmd/cluster/testdata/TestStatusCmdGoldenOutput/check-status-no-resources.golden b/cmd/cluster/testdata/TestStatusCmdGoldenOutput/check-status-no-resources.golden new file mode 100644 index 000000000..c6c460a71 --- /dev/null +++ b/cmd/cluster/testdata/TestStatusCmdGoldenOutput/check-status-no-resources.golden @@ -0,0 +1 @@ +Kind Name Status diff --git a/cmd/cluster/testdata/TestStatusCmdGoldenOutput/check-status-with-resources.golden b/cmd/cluster/testdata/TestStatusCmdGoldenOutput/check-status-with-resources.golden new file mode 100644 index 000000000..54a9f0dbb --- /dev/null +++ b/cmd/cluster/testdata/TestStatusCmdGoldenOutput/check-status-with-resources.golden @@ -0,0 +1,3 @@ +Kind Name Status +Resource pending-resource Pending +Resource stable-resource Stable diff --git a/cmd/cluster/testdata/statusmap/crd.yaml b/cmd/cluster/testdata/statusmap/crd.yaml new file mode 100644 index 000000000..929eeb969 --- /dev/null +++ b/cmd/cluster/testdata/statusmap/crd.yaml @@ -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 diff --git a/cmd/cluster/testdata/statusmap/kustomization.yaml b/cmd/cluster/testdata/statusmap/kustomization.yaml new file mode 100644 index 000000000..f4b156e41 --- /dev/null +++ b/cmd/cluster/testdata/statusmap/kustomization.yaml @@ -0,0 +1,8 @@ +resources: + - crd.yaml + - stable-resource.yaml + - pending-resource.yaml + - missing.yaml + - unknown.yaml + - legacy-crd.yaml + - legacy-resource.yaml diff --git a/cmd/cluster/testdata/statusmap/legacy-crd.yaml b/cmd/cluster/testdata/statusmap/legacy-crd.yaml new file mode 100644 index 000000000..e88f547be --- /dev/null +++ b/cmd/cluster/testdata/statusmap/legacy-crd.yaml @@ -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 diff --git a/cmd/cluster/testdata/statusmap/legacy-resource.yaml b/cmd/cluster/testdata/statusmap/legacy-resource.yaml new file mode 100644 index 000000000..2aca6df17 --- /dev/null +++ b/cmd/cluster/testdata/statusmap/legacy-resource.yaml @@ -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 diff --git a/cmd/cluster/testdata/statusmap/missing.yaml b/cmd/cluster/testdata/statusmap/missing.yaml new file mode 100644 index 000000000..43729dd45 --- /dev/null +++ b/cmd/cluster/testdata/statusmap/missing.yaml @@ -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 diff --git a/cmd/cluster/testdata/statusmap/pending-resource.yaml b/cmd/cluster/testdata/statusmap/pending-resource.yaml new file mode 100644 index 000000000..b1373a07a --- /dev/null +++ b/cmd/cluster/testdata/statusmap/pending-resource.yaml @@ -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 diff --git a/cmd/cluster/testdata/statusmap/stable-resource.yaml b/cmd/cluster/testdata/statusmap/stable-resource.yaml new file mode 100644 index 000000000..a6fff614f --- /dev/null +++ b/cmd/cluster/testdata/statusmap/stable-resource.yaml @@ -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 diff --git a/cmd/cluster/testdata/statusmap/unknown.yaml b/cmd/cluster/testdata/statusmap/unknown.yaml new file mode 100644 index 000000000..58c65e17f --- /dev/null +++ b/cmd/cluster/testdata/statusmap/unknown.yaml @@ -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 diff --git a/docs/source/cli/airshipctl_cluster.md b/docs/source/cli/airshipctl_cluster.md index 2e761cc1f..f6ab19cda 100644 --- a/docs/source/cli/airshipctl_cluster.md +++ b/docs/source/cli/airshipctl_cluster.md @@ -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 diff --git a/docs/source/cli/airshipctl_cluster_status.md b/docs/source/cli/airshipctl_cluster_status.md new file mode 100644 index 000000000..547701c76 --- /dev/null +++ b/docs/source/cli/airshipctl_cluster_status.md @@ -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 + diff --git a/docs/source/cli/airshipctl_phase_render.md b/docs/source/cli/airshipctl_phase_render.md index 17463b4f0..e21c269d3 100644 --- a/docs/source/cli/airshipctl_phase_render.md +++ b/docs/source/cli/airshipctl_phase_render.md @@ -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 ```