From 2b3280503a1d599e2c08256632adf94795099a83 Mon Sep 17 00:00:00 2001 From: Andrey Melnikov Date: Wed, 25 Nov 2020 20:24:41 -0800 Subject: [PATCH 1/8] feat: added basic delete command --- cmd/delete.go | 66 +++++++++++++++++++++++++++++++++++++++++++++++++ util/kubectl.go | 61 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 127 insertions(+) create mode 100644 cmd/delete.go diff --git a/cmd/delete.go b/cmd/delete.go new file mode 100644 index 0000000..45823bb --- /dev/null +++ b/cmd/delete.go @@ -0,0 +1,66 @@ +package cmd + +import ( + "fmt" + "github.com/onepanelio/cli/files" + "github.com/onepanelio/cli/util" + "github.com/spf13/cobra" + "path/filepath" +) + +var ( + // skipConfirmDelete if true, will skip the confirmation prompt of the delete command + skipConfirmDelete bool +) + +var deleteCmd = &cobra.Command{ + Use: "delete", + Short: "Deletes onepanel cluster resources", + Long: "Delete all onepanel kubernetes cluster resources. Does not delete database unless it is in-cluster.", + Example: "delete", + Run: func(cmd *cobra.Command, args []string) { + if skipConfirmDelete == false { + fmt.Print("Are you sure you want to delete onepanel? ('y' or 'yes' to confirm. Anything else to cancel): ") + userInput := "" + if _, err := fmt.Scanln(&userInput); err != nil { + fmt.Printf("Unable to get response\n") + return + } + + if userInput != "y" && userInput != "yes" { + return + } + } + + filesToDelete := []string{ + filepath.Join(".onepanel", "kubernetes.yaml"), + filepath.Join(".onepanel", "application.kubernetes.yaml"), + } + + for _, filePath := range filesToDelete { + exists, err := files.Exists(filePath) + if err != nil { + fmt.Printf("Error checking if onepanel files exist: %v\n", err.Error()) + return + } + + if !exists { + fmt.Printf("'%v' file does not exist. Are you in the directory where you run 'opctl init'?\n", filePath) + return + } + } + + fmt.Printf("Deleting onepanel from your cluster...\n") + for _, filePath := range filesToDelete { + if err := util.KubectlDelete(filePath); err != nil { + fmt.Printf("Unable to delete: %v\n", err.Error()) + return + } + } + }, +} + +func init() { + rootCmd.AddCommand(deleteCmd) + deleteCmd.Flags().BoolVarP(&skipConfirmDelete, "yes", "y", false, "Add this in to skip the confirmation prompt") +} diff --git a/util/kubectl.go b/util/kubectl.go index 17b516d..5a43847 100644 --- a/util/kubectl.go +++ b/util/kubectl.go @@ -6,15 +6,18 @@ import ( "fmt" opConfig "github.com/onepanelio/cli/config" "github.com/spf13/cobra" + k8error "k8s.io/apimachinery/pkg/util/errors" "k8s.io/cli-runtime/pkg/genericclioptions" _ "k8s.io/client-go/plugin/pkg/client/auth/azure" _ "k8s.io/client-go/plugin/pkg/client/auth/gcp" "k8s.io/kubectl/pkg/cmd/apply" + k8delete "k8s.io/kubectl/pkg/cmd/delete" "k8s.io/kubectl/pkg/cmd/get" cmdutil "k8s.io/kubectl/pkg/cmd/util" "os" "runtime" "strconv" + "strings" ) func KubectlGet(resource string, resourceName string, namespace string, extraArgs []string, flags map[string]interface{}) (stdout string, stderr string, err error) { @@ -125,6 +128,64 @@ func KubectlApply(filePath string) (stdout string, stderr string, err error) { return } +// KubectlDelete run's kubectl delete using the input filePath +func KubectlDelete(filePath string) (err error) { + kubeConfigFlags := genericclioptions.NewConfigFlags(true).WithDeprecatedPasswordFlag() + matchVersionKubeConfigFlags := cmdutil.NewMatchVersionFlags(kubeConfigFlags) + + f := cmdutil.NewFactory(matchVersionKubeConfigFlags) + ioStreams := genericclioptions.IOStreams{ + In: os.Stdin, + Out: os.Stdout, + ErrOut: os.Stderr, + } + cmd := k8delete.NewCmdDelete(f, ioStreams) + + deleteOptions := k8delete.DeleteOptions{IOStreams: ioStreams} + deleteOptions.Filenames = []string{filePath} + err = cmd.Flags().Set("filename", filePath) + if err != nil { + return err + } + + if err := deleteOptions.Complete(f, []string{}, cmd); err != nil { + return err + } + + if err := deleteOptions.RunDelete(f); err != nil { + errorAggregate, ok := err.(k8error.Aggregate) + if ok { + finalErrors := make([]error, 0) + // Skip any errors that mean "not found" + for _, errItem := range errorAggregate.Errors() { + if strings.Contains(errItem.Error(), "not found") { + continue + } + + if strings.Contains(errItem.Error(), "no matches for kind") { + continue + } + + if strings.Contains(errItem.Error(), "the server could not find the requested resource") { + continue + } + + finalErrors = append(finalErrors, errItem) + } + + if len(finalErrors) == 0 { + return nil + } + + return k8error.NewAggregate(finalErrors) + } + + return err + } + + return +} + func validateArgs(cmd *cobra.Command, args []string) error { if len(args) != 0 { return cmdutil.UsageErrorf(cmd, "Unexpected args: %v", args) From 50dc739471ae278238b5dd2c756a008bbed3691e Mon Sep 17 00:00:00 2001 From: Andrey Melnikov Date: Wed, 25 Nov 2020 20:25:34 -0800 Subject: [PATCH 2/8] fix: wording in error message --- cmd/delete.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/delete.go b/cmd/delete.go index 45823bb..f2d9666 100644 --- a/cmd/delete.go +++ b/cmd/delete.go @@ -45,7 +45,7 @@ var deleteCmd = &cobra.Command{ } if !exists { - fmt.Printf("'%v' file does not exist. Are you in the directory where you run 'opctl init'?\n", filePath) + fmt.Printf("'%v' file does not exist. Are you in the directory where you ran 'opctl init'?\n", filePath) return } } From 1b65a1a1ec4aebc412ecd25ef27c44a703a0b405 Mon Sep 17 00:00:00 2001 From: Andrey Melnikov Date: Wed, 25 Nov 2020 20:24:41 -0800 Subject: [PATCH 3/8] feat: added basic delete command --- cmd/delete.go | 66 +++++++++++++++++++++++++++++++++++++++++++++++++ util/kubectl.go | 61 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 127 insertions(+) create mode 100644 cmd/delete.go diff --git a/cmd/delete.go b/cmd/delete.go new file mode 100644 index 0000000..45823bb --- /dev/null +++ b/cmd/delete.go @@ -0,0 +1,66 @@ +package cmd + +import ( + "fmt" + "github.com/onepanelio/cli/files" + "github.com/onepanelio/cli/util" + "github.com/spf13/cobra" + "path/filepath" +) + +var ( + // skipConfirmDelete if true, will skip the confirmation prompt of the delete command + skipConfirmDelete bool +) + +var deleteCmd = &cobra.Command{ + Use: "delete", + Short: "Deletes onepanel cluster resources", + Long: "Delete all onepanel kubernetes cluster resources. Does not delete database unless it is in-cluster.", + Example: "delete", + Run: func(cmd *cobra.Command, args []string) { + if skipConfirmDelete == false { + fmt.Print("Are you sure you want to delete onepanel? ('y' or 'yes' to confirm. Anything else to cancel): ") + userInput := "" + if _, err := fmt.Scanln(&userInput); err != nil { + fmt.Printf("Unable to get response\n") + return + } + + if userInput != "y" && userInput != "yes" { + return + } + } + + filesToDelete := []string{ + filepath.Join(".onepanel", "kubernetes.yaml"), + filepath.Join(".onepanel", "application.kubernetes.yaml"), + } + + for _, filePath := range filesToDelete { + exists, err := files.Exists(filePath) + if err != nil { + fmt.Printf("Error checking if onepanel files exist: %v\n", err.Error()) + return + } + + if !exists { + fmt.Printf("'%v' file does not exist. Are you in the directory where you run 'opctl init'?\n", filePath) + return + } + } + + fmt.Printf("Deleting onepanel from your cluster...\n") + for _, filePath := range filesToDelete { + if err := util.KubectlDelete(filePath); err != nil { + fmt.Printf("Unable to delete: %v\n", err.Error()) + return + } + } + }, +} + +func init() { + rootCmd.AddCommand(deleteCmd) + deleteCmd.Flags().BoolVarP(&skipConfirmDelete, "yes", "y", false, "Add this in to skip the confirmation prompt") +} diff --git a/util/kubectl.go b/util/kubectl.go index 17b516d..5a43847 100644 --- a/util/kubectl.go +++ b/util/kubectl.go @@ -6,15 +6,18 @@ import ( "fmt" opConfig "github.com/onepanelio/cli/config" "github.com/spf13/cobra" + k8error "k8s.io/apimachinery/pkg/util/errors" "k8s.io/cli-runtime/pkg/genericclioptions" _ "k8s.io/client-go/plugin/pkg/client/auth/azure" _ "k8s.io/client-go/plugin/pkg/client/auth/gcp" "k8s.io/kubectl/pkg/cmd/apply" + k8delete "k8s.io/kubectl/pkg/cmd/delete" "k8s.io/kubectl/pkg/cmd/get" cmdutil "k8s.io/kubectl/pkg/cmd/util" "os" "runtime" "strconv" + "strings" ) func KubectlGet(resource string, resourceName string, namespace string, extraArgs []string, flags map[string]interface{}) (stdout string, stderr string, err error) { @@ -125,6 +128,64 @@ func KubectlApply(filePath string) (stdout string, stderr string, err error) { return } +// KubectlDelete run's kubectl delete using the input filePath +func KubectlDelete(filePath string) (err error) { + kubeConfigFlags := genericclioptions.NewConfigFlags(true).WithDeprecatedPasswordFlag() + matchVersionKubeConfigFlags := cmdutil.NewMatchVersionFlags(kubeConfigFlags) + + f := cmdutil.NewFactory(matchVersionKubeConfigFlags) + ioStreams := genericclioptions.IOStreams{ + In: os.Stdin, + Out: os.Stdout, + ErrOut: os.Stderr, + } + cmd := k8delete.NewCmdDelete(f, ioStreams) + + deleteOptions := k8delete.DeleteOptions{IOStreams: ioStreams} + deleteOptions.Filenames = []string{filePath} + err = cmd.Flags().Set("filename", filePath) + if err != nil { + return err + } + + if err := deleteOptions.Complete(f, []string{}, cmd); err != nil { + return err + } + + if err := deleteOptions.RunDelete(f); err != nil { + errorAggregate, ok := err.(k8error.Aggregate) + if ok { + finalErrors := make([]error, 0) + // Skip any errors that mean "not found" + for _, errItem := range errorAggregate.Errors() { + if strings.Contains(errItem.Error(), "not found") { + continue + } + + if strings.Contains(errItem.Error(), "no matches for kind") { + continue + } + + if strings.Contains(errItem.Error(), "the server could not find the requested resource") { + continue + } + + finalErrors = append(finalErrors, errItem) + } + + if len(finalErrors) == 0 { + return nil + } + + return k8error.NewAggregate(finalErrors) + } + + return err + } + + return +} + func validateArgs(cmd *cobra.Command, args []string) error { if len(args) != 0 { return cmdutil.UsageErrorf(cmd, "Unexpected args: %v", args) From a86adca8f8c53d2cae10ed82b129afbf99d51a3e Mon Sep 17 00:00:00 2001 From: Andrey Melnikov Date: Wed, 25 Nov 2020 20:25:34 -0800 Subject: [PATCH 4/8] fix: wording in error message --- cmd/delete.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/delete.go b/cmd/delete.go index 45823bb..f2d9666 100644 --- a/cmd/delete.go +++ b/cmd/delete.go @@ -45,7 +45,7 @@ var deleteCmd = &cobra.Command{ } if !exists { - fmt.Printf("'%v' file does not exist. Are you in the directory where you run 'opctl init'?\n", filePath) + fmt.Printf("'%v' file does not exist. Are you in the directory where you ran 'opctl init'?\n", filePath) return } } From 424b9276bf48bd52d8ddf6f778b8cbe73a1044fa Mon Sep 17 00:00:00 2001 From: Andrey Melnikov Date: Fri, 4 Dec 2020 09:48:33 -0800 Subject: [PATCH 5/8] fix: added (default value) to the list of not-allowed namespace values --- cmd/apply.go | 12 ++++++------ cmd/build.go | 4 +++- manifest/manifest.go | 4 ++++ 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/cmd/apply.go b/cmd/apply.go index f4b0fb2..b736599 100644 --- a/cmd/apply.go +++ b/cmd/apply.go @@ -35,7 +35,7 @@ var applyCmd = &cobra.Command{ return } - overlayComponentFirst := filepath.Join("common/application/base") + overlayComponentFirst := filepath.Join("common", "application", "base") baseOverlayComponent := config.GetOverlayComponent(overlayComponentFirst) applicationBaseKustomizeTemplate := TemplateFromSimpleOverlayedComponents(baseOverlayComponent) applicationResult, err := GenerateKustomizeResult(*config, applicationBaseKustomizeTemplate) @@ -44,11 +44,11 @@ var applyCmd = &cobra.Command{ return } - applicationKubernetesYamlFilePath := filepath.Join(".onepanel/application.kubernetes.yaml") + applicationKubernetesYamlFilePath := filepath.Join(".onepanel", "application.kubernetes.yaml") existsApp, err := files.Exists(applicationKubernetesYamlFilePath) if err != nil { - log.Printf("Unable to check if file %v exists", applicationKubernetesYamlFilePath) + log.Printf("Unable to check if file '%v' exists", applicationKubernetesYamlFilePath) return } @@ -77,9 +77,9 @@ var applyCmd = &cobra.Command{ resApp, errResApp, err = applyKubernetesFile(applicationKubernetesYamlFilePath) - log.Printf("%v", resApp) + log.Printf("res: %v", resApp) if errResApp != "" { - log.Printf("%v", errResApp) + log.Printf("err: %v", errResApp) } if err != nil { @@ -130,7 +130,7 @@ var applyCmd = &cobra.Command{ return } - finalKubernetesYamlFilePath := filepath.Join(".onepanel/kubernetes.yaml") + finalKubernetesYamlFilePath := filepath.Join(".onepanel", "kubernetes.yaml") exists, err := files.Exists(finalKubernetesYamlFilePath) if err != nil { diff --git a/cmd/build.go b/cmd/build.go index f70e1cf..0c0edc7 100644 --- a/cmd/build.go +++ b/cmd/build.go @@ -81,7 +81,7 @@ func GenerateKustomizeResult(config opConfig.Config, kustomizeTemplate template. } manifestPath := config.Spec.ManifestsRepo - localManifestsCopyPath := filepath.Join(".onepanel/manifests/cache") + localManifestsCopyPath := filepath.Join(".onepanel", "manifests", "cache") exists, err := files.Exists(localManifestsCopyPath) if err != nil { @@ -614,6 +614,8 @@ func HumanizeKustomizeError(err error) string { switch paramsError.ErrorType { case "missing": return fmt.Sprintf("%s is missing in your params.yaml", paramsError.Key) + case "parameter": + return fmt.Sprintf("%s can not be '%s', please enter a namespace", paramsError.Key, *paramsError.Value) case "blank": return fmt.Sprintf("%s can not be blank, please use a different namespace in your params.yaml", paramsError.Key) case "reserved": diff --git a/manifest/manifest.go b/manifest/manifest.go index df0f78a..7cac9dc 100644 --- a/manifest/manifest.go +++ b/manifest/manifest.go @@ -118,6 +118,7 @@ func (m *Manifest) GetOverlay(path string) *Overlay { // Validate checks if the manifest is valid. If it is, nil is returned. Otherwise an error is returned. func Validate(manifest *util.DynamicYaml) error { reservedNamespaces := map[string]bool{ + "default": true, "onepanel": true, "application-system": true, "cert-manager": true, @@ -134,6 +135,9 @@ func Validate(manifest *util.DynamicYaml) error { if defaultNamespace.Value == "" { return &ParamsError{Key: "application.defaultNamespace", ErrorType: "blank"} } + if defaultNamespace.Value == "" { + return &ParamsError{Key: "application.defaultNamespace", Value: &defaultNamespace.Value, ErrorType: "parameter"} + } if _, ok := reservedNamespaces[defaultNamespace.Value]; ok { return &ParamsError{Key: "application.defaultNamespace", Value: &defaultNamespace.Value, ErrorType: "reserved"} } From da2c3b15a24038db5baa9a402f8f0e74119724a9 Mon Sep 17 00:00:00 2001 From: Andrey Melnikov Date: Fri, 4 Dec 2020 12:59:35 -0800 Subject: [PATCH 6/8] update: don't allow deleting default namespace, but do allow it in `apply` or `build` --- cmd/delete.go | 24 ++++++++++++++++++++++++ manifest/manifest.go | 1 - 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/cmd/delete.go b/cmd/delete.go index f2d9666..f02828c 100644 --- a/cmd/delete.go +++ b/cmd/delete.go @@ -2,6 +2,7 @@ package cmd import ( "fmt" + opConfig "github.com/onepanelio/cli/config" "github.com/onepanelio/cli/files" "github.com/onepanelio/cli/util" "github.com/spf13/cobra" @@ -32,6 +33,29 @@ var deleteCmd = &cobra.Command{ } } + config, err := opConfig.FromFile("config.yaml") + if err != nil { + fmt.Printf("Unable to read configuration file: %v", err.Error()) + return + } + + paramsYamlFile, err := util.LoadDynamicYamlFromFile(config.Spec.Params) + if err != nil { + fmt.Println("Error parsing configuration file.") + return + } + + defaultNamespaceNode := paramsYamlFile.GetValue("application.defaultNamespace") + if defaultNamespaceNode == nil { + fmt.Printf("application.defaultNamespace is missing from your '%s' file\n", config.Spec.Params) + return + } + + if defaultNamespaceNode.Value == "default" { + fmt.Println("Unable to delete onepanel in the 'default' namespace") + return + } + filesToDelete := []string{ filepath.Join(".onepanel", "kubernetes.yaml"), filepath.Join(".onepanel", "application.kubernetes.yaml"), diff --git a/manifest/manifest.go b/manifest/manifest.go index 7cac9dc..62c6dec 100644 --- a/manifest/manifest.go +++ b/manifest/manifest.go @@ -118,7 +118,6 @@ func (m *Manifest) GetOverlay(path string) *Overlay { // Validate checks if the manifest is valid. If it is, nil is returned. Otherwise an error is returned. func Validate(manifest *util.DynamicYaml) error { reservedNamespaces := map[string]bool{ - "default": true, "onepanel": true, "application-system": true, "cert-manager": true, From ff6741239ae366e22a559aaf256e4247ad17224c Mon Sep 17 00:00:00 2001 From: rushtehrani Date: Fri, 4 Dec 2020 18:50:58 -0800 Subject: [PATCH 7/8] add default to reserved ns list --- util/status.go | 1 + 1 file changed, 1 insertion(+) diff --git a/util/status.go b/util/status.go index 279a026..e9db6b9 100644 --- a/util/status.go +++ b/util/status.go @@ -12,6 +12,7 @@ func DeploymentStatus(yamlFile *DynamicYaml) (ready bool, err error) { namespacesToCheck["application-system"] = true namespacesToCheck["onepanel"] = true namespacesToCheck["istio-system"] = true + namespacesToCheck["default"] = true if yamlFile.HasKey("certManager") { namespacesToCheck["cert-manager"] = true From fe77f35f22ca397f5d3b7901b294676f32571135 Mon Sep 17 00:00:00 2001 From: Andrey Melnikov Date: Tue, 22 Dec 2020 13:12:48 -0800 Subject: [PATCH 8/8] fix: edge case where no namespace was set when trying to delete --- cmd/delete.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/cmd/delete.go b/cmd/delete.go index f02828c..8cabf28 100644 --- a/cmd/delete.go +++ b/cmd/delete.go @@ -56,6 +56,11 @@ var deleteCmd = &cobra.Command{ return } + if defaultNamespaceNode.Value == "" { + fmt.Println("Unable to delete onepanel. No namespace set.") + return + } + filesToDelete := []string{ filepath.Join(".onepanel", "kubernetes.yaml"), filepath.Join(".onepanel", "application.kubernetes.yaml"),