diff --git a/cmd/cluster/checkexpiration/checkexpiration.go b/cmd/cluster/checkexpiration/checkexpiration.go index 9f61d9a9f..78d0801ba 100644 --- a/cmd/cluster/checkexpiration/checkexpiration.go +++ b/cmd/cluster/checkexpiration/checkexpiration.go @@ -17,8 +17,9 @@ package checkexpiration import ( "github.com/spf13/cobra" + "opendev.org/airship/airshipctl/pkg/cluster/checkexpiration" "opendev.org/airship/airshipctl/pkg/config" - "opendev.org/airship/airshipctl/pkg/errors" + "opendev.org/airship/airshipctl/pkg/k8s/client" "opendev.org/airship/airshipctl/pkg/log" ) @@ -54,25 +55,31 @@ airshipctl cluster check-certificate-expiration -t 30 -o yaml --kubeconfig testc // NewCheckCommand creates a new command for generating secret information func NewCheckCommand(cfgFactory config.Factory) *cobra.Command { - var threshold int - var contentType, kubeconfig string + c := &checkexpiration.CheckCommand{ + Options: checkexpiration.CheckFlags{}, + CfgFactory: cfgFactory, + ClientFactory: client.DefaultClient, + } + checkCmd := &cobra.Command{ Use: "check-certificate-expiration", Short: "Check for expiring TLS certificates, secrets and kubeconfigs in the kubernetes cluster", Long: checkLong[1:], Example: checkExample, RunE: func(cmd *cobra.Command, args []string) error { - return errors.ErrNotImplemented{What: "check certificate expiration"} + return c.RunE(cmd.OutOrStdout()) }, } - checkCmd.Flags().IntVarP(&threshold, "threshold", "t", -1, + checkCmd.Flags().IntVarP(&c.Options.Threshold, "threshold", "t", -1, "The max expiration threshold in days before a certificate is"+ " expiring. Displays all the certificates by default") - checkCmd.Flags().StringVarP(&contentType, "output", "o", "json", "Convert "+ + checkCmd.Flags().StringVarP(&c.Options.FormatType, "output", "o", "json", "Convert "+ "output to yaml or json") - checkCmd.Flags().StringVar(&kubeconfig, kubeconfigFlag, "", + checkCmd.Flags().StringVar(&c.Options.Kubeconfig, kubeconfigFlag, "", "Path to kubeconfig associated with cluster being managed") + checkCmd.Flags().StringVar(&c.Options.KubeContext, "kubecontext", "", + "Kubeconfig context to be used") err := checkCmd.MarkFlagRequired(kubeconfigFlag) if err != nil { diff --git a/cmd/cluster/checkexpiration/testdata/TestCheckExpirationGoldenOutput/check-expiration-with-help.golden b/cmd/cluster/checkexpiration/testdata/TestCheckExpirationGoldenOutput/check-expiration-with-help.golden index b56f7459c..17ad92dcd 100644 --- a/cmd/cluster/checkexpiration/testdata/TestCheckExpirationGoldenOutput/check-expiration-with-help.golden +++ b/cmd/cluster/checkexpiration/testdata/TestCheckExpirationGoldenOutput/check-expiration-with-help.golden @@ -28,7 +28,8 @@ airshipctl cluster check-certificate-expiration -t 30 -o yaml --kubeconfig testc Flags: - -h, --help help for check-certificate-expiration - --kubeconfig string Path to kubeconfig associated with cluster being managed - -o, --output string Convert output to yaml or json (default "json") - -t, --threshold int The max expiration threshold in days before a certificate is expiring. Displays all the certificates by default (default -1) + -h, --help help for check-certificate-expiration + --kubeconfig string Path to kubeconfig associated with cluster being managed + --kubecontext string Kubeconfig context to be used + -o, --output string Convert output to yaml or json (default "json") + -t, --threshold int The max expiration threshold in days before a certificate is expiring. Displays all the certificates by default (default -1) diff --git a/docs/source/cli/airshipctl_cluster_check-certificate-expiration.md b/docs/source/cli/airshipctl_cluster_check-certificate-expiration.md index 0d1f22560..efed78c70 100644 --- a/docs/source/cli/airshipctl_cluster_check-certificate-expiration.md +++ b/docs/source/cli/airshipctl_cluster_check-certificate-expiration.md @@ -40,10 +40,11 @@ airshipctl cluster check-certificate-expiration -t 30 -o yaml --kubeconfig testc ### Options ``` - -h, --help help for check-certificate-expiration - --kubeconfig string Path to kubeconfig associated with cluster being managed - -o, --output string Convert output to yaml or json (default "json") - -t, --threshold int The max expiration threshold in days before a certificate is expiring. Displays all the certificates by default (default -1) + -h, --help help for check-certificate-expiration + --kubeconfig string Path to kubeconfig associated with cluster being managed + --kubecontext string Kubeconfig context to be used + -o, --output string Convert output to yaml or json (default "json") + -t, --threshold int The max expiration threshold in days before a certificate is expiring. Displays all the certificates by default (default -1) ``` ### Options inherited from parent commands diff --git a/pkg/cluster/checkexpiration/checkexpiration.go b/pkg/cluster/checkexpiration/checkexpiration.go new file mode 100644 index 000000000..7b39492f1 --- /dev/null +++ b/pkg/cluster/checkexpiration/checkexpiration.go @@ -0,0 +1,60 @@ +/* + 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 checkexpiration + +import ( + "opendev.org/airship/airshipctl/pkg/config" + "opendev.org/airship/airshipctl/pkg/errors" + "opendev.org/airship/airshipctl/pkg/k8s/client" +) + +// CertificateExpirationStore is the customized client store +type CertificateExpirationStore struct { + Kclient client.Interface + Settings config.Factory +} + +// Expiration captures expiration information of all expirable entities in the cluster +type Expiration struct{} + +// NewStore returns an instance of a CertificateExpirationStore +func NewStore(cfgFactory config.Factory, clientFactory client.Factory, + kubeconfig string, _ string) (CertificateExpirationStore, error) { + airshipconfig, err := cfgFactory() + if err != nil { + return CertificateExpirationStore{}, err + } + + // TODO (guhan) Allow kube context to be passed to client Factory + // 4th argument in NewStore takes kube context and is ignored for now. + // To be modified post #388. Refer to + // https://review.opendev.org/#/c/760501/7/pkg/cluster/checkexpiration/command.go@31 + kclient, err := clientFactory(airshipconfig.LoadedConfigPath(), kubeconfig) + if err != nil { + return CertificateExpirationStore{}, err + } + + return CertificateExpirationStore{ + Kclient: kclient, + Settings: cfgFactory, + }, nil +} + +// GetExpiringCertificates checks for the expiration data +// NOT IMPLEMENTED (guhan) +// TODO (guhan) check for TLS certificates, workload kubeconfig and node certificates +func (store CertificateExpirationStore) GetExpiringCertificates(expirationThreshold int) (Expiration, error) { + return Expiration{}, errors.ErrNotImplemented{What: "check certificate expiration logic"} +} diff --git a/pkg/cluster/checkexpiration/command.go b/pkg/cluster/checkexpiration/command.go new file mode 100644 index 000000000..edd0334ac --- /dev/null +++ b/pkg/cluster/checkexpiration/command.go @@ -0,0 +1,73 @@ +/* + 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 checkexpiration + +import ( + "encoding/json" + "io" + "strings" + + "opendev.org/airship/airshipctl/pkg/config" + "opendev.org/airship/airshipctl/pkg/k8s/client" + "opendev.org/airship/airshipctl/pkg/util/yaml" +) + +// CheckFlags flags given for checking the expiration +type CheckFlags struct { + Threshold int + FormatType string + Kubeconfig string + KubeContext string +} + +// CheckCommand check expiration command +type CheckCommand struct { + Options CheckFlags + CfgFactory config.Factory + ClientFactory client.Factory +} + +// RunE is the implementation of check command +func (c *CheckCommand) RunE(w io.Writer) error { + if !strings.EqualFold(c.Options.FormatType, "json") && !strings.EqualFold(c.Options.FormatType, "yaml") { + return ErrInvalidFormat{RequestedFormat: c.Options.FormatType} + } + + secretStore, err := NewStore(c.CfgFactory, c.ClientFactory, c.Options.Kubeconfig, c.Options.KubeContext) + if err != nil { + return err + } + + expirationInfo, err := secretStore.GetExpiringCertificates(c.Options.Threshold) + if err != nil { + return err + } + if c.Options.FormatType == "yaml" { + err = yaml.WriteOut(w, expirationInfo) + if err != nil { + return err + } + } else { + buffer, err := json.MarshalIndent(expirationInfo, "", " ") + if err != nil { + return err + } + _, err = w.Write(buffer) + if err != nil { + return err + } + } + return nil +} diff --git a/pkg/cluster/checkexpiration/command_test.go b/pkg/cluster/checkexpiration/command_test.go new file mode 100644 index 000000000..87a400b13 --- /dev/null +++ b/pkg/cluster/checkexpiration/command_test.go @@ -0,0 +1,106 @@ +/* + 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 checkexpiration_test + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "k8s.io/apimachinery/pkg/runtime" + + "opendev.org/airship/airshipctl/pkg/cluster/checkexpiration" + "opendev.org/airship/airshipctl/pkg/config" + "opendev.org/airship/airshipctl/pkg/k8s/client" + "opendev.org/airship/airshipctl/pkg/k8s/client/fake" + "opendev.org/airship/airshipctl/testutil" +) + +const ( + testNotImplementedErr = "not implemented: check certificate expiration logic" +) + +func TestRunE(t *testing.T) { + tests := []struct { + testCaseName string + testErr string + checkFlags checkexpiration.CheckFlags + cfgFactory config.Factory + expectedOutput string + }{ + { + testCaseName: "invalid-input-format", + cfgFactory: func() (*config.Config, error) { + return nil, nil + }, + checkFlags: checkexpiration.CheckFlags{ + Threshold: 0, + FormatType: "test-yaml", + }, + testErr: checkexpiration.ErrInvalidFormat{RequestedFormat: "test-yaml"}.Error(), + }, + { + testCaseName: "valid-input-format-json", + cfgFactory: func() (*config.Config, error) { + cfg, _ := testutil.InitConfig(t) + return cfg, nil + }, + checkFlags: checkexpiration.CheckFlags{ + Threshold: 5000, + FormatType: "json", + Kubeconfig: "", + }, + testErr: testNotImplementedErr, + }, + { + testCaseName: "valid-input-format-yaml", + cfgFactory: func() (*config.Config, error) { + cfg, _ := testutil.InitConfig(t) + return cfg, nil + }, + checkFlags: checkexpiration.CheckFlags{ + Threshold: 5000, + FormatType: "yaml", + }, + testErr: testNotImplementedErr, + }, + } + + for _, tt := range tests { + t.Run(tt.testCaseName, func(t *testing.T) { + var objects []runtime.Object + // TODO (guhan) append a dummy object for testing + ra := fake.WithTypedObjects(objects...) + + command := checkexpiration.CheckCommand{ + Options: tt.checkFlags, + CfgFactory: tt.cfgFactory, + ClientFactory: func(_ string, _ string) (client.Interface, error) { + return fake.NewClient(ra), nil + }, + } + + var buffer bytes.Buffer + err := command.RunE(&buffer) + + if tt.testErr != "" { + require.Error(t, err) + assert.Contains(t, err.Error(), tt.testErr) + } + // TODO (guhan) add else part to check the actual vs expected o/p + }) + } +} diff --git a/pkg/cluster/checkexpiration/errors.go b/pkg/cluster/checkexpiration/errors.go new file mode 100644 index 000000000..e98e2ca52 --- /dev/null +++ b/pkg/cluster/checkexpiration/errors.go @@ -0,0 +1,26 @@ +/* + 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 checkexpiration + +import "fmt" + +// ErrInvalidFormat is called when the user provides format other than yaml/json +type ErrInvalidFormat struct { + RequestedFormat string +} + +func (e ErrInvalidFormat) Error() string { + return fmt.Sprintf("invalid output format specified %s. Allowed values are json|yaml", e.RequestedFormat) +}