From 26bee47b53eed43f10330b786c2780e1c60b50aa Mon Sep 17 00:00:00 2001 From: deviantony Date: Thu, 21 Sep 2023 09:32:16 +0000 Subject: [PATCH 1/2] feat: introduce reset flag and routine --- Makefile | 7 +- cmd/k2d.go | 34 ++++++++ internal/adapter/reset.go | 174 ++++++++++++++++++++++++++++++++++++++ pkg/filesystem/file.go | 130 ++++++++++++++++------------ pkg/prompt/prompt.go | 31 +++++++ 5 files changed, 321 insertions(+), 55 deletions(-) create mode 100644 internal/adapter/reset.go create mode 100644 pkg/prompt/prompt.go diff --git a/Makefile b/Makefile index 72dfd1b..fe4dbc8 100644 --- a/Makefile +++ b/Makefile @@ -6,7 +6,7 @@ ARCH="$(shell go env GOARCH)" ARM="" VERSION="latest" -.PHONY: pre dev build release image image-arm-6 image-arm-7 image-multiarch clean +.PHONY: pre dev build release image image-arm-6 image-arm-7 image-multiarch clean reset dist := dist bin := $(shell basename $(CURDIR)) @@ -53,4 +53,7 @@ image-multiarch: clean: rm -rf $(dist)/* rm -rf /opt/dev-toolkit/k2d/* - rm -rf /var/lib/k2d \ No newline at end of file + rm -rf /var/lib/k2d + +reset: build + $(dist)/$(bin) -reset \ No newline at end of file diff --git a/cmd/k2d.go b/cmd/k2d.go index bc8adec..7718f57 100644 --- a/cmd/k2d.go +++ b/cmd/k2d.go @@ -2,10 +2,12 @@ package main import ( "context" + "flag" "fmt" "log" "net" "net/http" + "os" "path" restfulspec "github.com/emicklei/go-restful-openapi/v2" @@ -24,6 +26,7 @@ import ( "github.com/portainer/k2d/internal/token" "github.com/portainer/k2d/internal/types" "github.com/portainer/k2d/pkg/network" + "github.com/portainer/k2d/pkg/prompt" "github.com/sethvargo/go-envconfig" ) @@ -38,6 +41,9 @@ func getAdvertiseIpAddr(advertiseAddr string) (net.IP, error) { func main() { ctx := context.Background() + resetMode := flag.Bool("reset", false, "Reset this host by removing all resources created by k2d and created via k2d") + flag.Parse() + var cfg config.Config if err := envconfig.Process(ctx, &cfg); err != nil { log.Fatalf("unable to parse configuration: %s", err) @@ -49,6 +55,34 @@ func main() { } defer logger.Sync() + if *resetMode { + fmt.Println("Are you sure you want to this host? This will remove everything created by or via k2d including workload and data. y/N") + confirm, err := prompt.AskForConfirmation() + if err != nil { + logger.Fatalf("unable to ask for confirmation: %s", err) + } + + if confirm { + kubeDockerAdapterOptions := &adapter.KubeDockerAdapterOptions{ + K2DConfig: &cfg, + Logger: logger, + ServerConfiguration: nil, + } + + kubeDockerAdapter, err := adapter.NewKubeDockerAdapter(kubeDockerAdapterOptions) + if err != nil { + logger.Fatalf("unable to create docker adapter: %s", err) + } + + err = kubeDockerAdapter.ExecuteResetRoutine(ctx, cfg.DataPath) + if err != nil { + logger.Fatalf("an error occured during reset routine: %s", err) + } + } + + os.Exit(0) + } + // We add the logger to the main context ctx = logging.ContextWithLogger(ctx, logger) diff --git a/internal/adapter/reset.go b/internal/adapter/reset.go new file mode 100644 index 0000000..6f84fb0 --- /dev/null +++ b/internal/adapter/reset.go @@ -0,0 +1,174 @@ +package adapter + +import ( + "context" + "fmt" + + "github.com/portainer/k2d/pkg/filesystem" + "k8s.io/apimachinery/pkg/labels" +) + +// ExecuteResetRoutine performs a cleanup routine that removes all k2d resources from the host. +// This function is intended to be used as a "reset mode" operation for cleaning up any resources +// managed by k2d on the host. +// +// Parameters: +// - ctx context.Context: The context for carrying out the reset routine. +// +// Returns: +// - error: Returns an error if any of the resource removal operations fail. +// +// Steps: +// 1. Removes all workload resources (like deployments, pods) by invoking removeAllWorkloads. +// 2. Removes all Persistent Volumes and Persistent Volume Claims by invoking removeAllPersistentVolumeAndClaims. +// 3. Removes all ConfigMaps and Secrets by invoking removeAllConfigMapsAndSecrets. +// 4. Removes all namespaces by invoking removeAllNamespaces. +func (adapter *KubeDockerAdapter) ExecuteResetRoutine(ctx context.Context, k2dDataPath string) error { + adapter.logger.Infoln("reset mode enabled, removing all k2d resources on this host") + + err := adapter.removeAllWorkloads(ctx) + if err != nil { + return fmt.Errorf("unable to remove workloads: %w", err) + } + + err = adapter.removeAllPersistentVolumeAndClaims(ctx) + if err != nil { + return fmt.Errorf("unable to remove persistent volumes and persistent volume claims: %w", err) + } + + err = adapter.removeAllConfigMapsAndSecrets(ctx) + if err != nil { + return fmt.Errorf("unable to remove configmaps and secrets: %w", err) + } + + err = adapter.removeAllNamespaces(ctx) + if err != nil { + return fmt.Errorf("unable to remove namespaces: %w", err) + } + + adapter.logger.Infoln("removing k2d data directory content...") + + err = filesystem.RemoveAllContent(k2dDataPath) + if err != nil { + return fmt.Errorf("unable to remove k2d data directory content: %w", err) + } + + adapter.logger.Infoln("reset routine completed") + return nil +} + +func (adapter *KubeDockerAdapter) removeAllWorkloads(ctx context.Context) error { + adapter.logger.Infoln("removing all workloads (deployments, pods)...") + + deployments, err := adapter.ListDeployments(ctx, "") + if err != nil { + return fmt.Errorf("unable to list deployments: %w", err) + } + + for _, deployment := range deployments.Items { + adapter.logger.Infof("removing deployment %s/%s", deployment.Namespace, deployment.Name) + adapter.DeleteContainer(ctx, deployment.Name, deployment.Namespace) + } + + pods, err := adapter.ListPods(ctx, "") + if err != nil { + return fmt.Errorf("unable to list pods: %w", err) + } + + for _, pod := range pods.Items { + adapter.logger.Infof("removing pod %s/%s", pod.Namespace, pod.Name) + adapter.DeleteContainer(ctx, pod.Name, pod.Namespace) + } + + return nil +} + +func (adapter *KubeDockerAdapter) removeAllPersistentVolumeAndClaims(ctx context.Context) error { + adapter.logger.Infoln("removing all persistent volumes and persistent volume claims...") + + persistentVolumes, err := adapter.ListPersistentVolumes(ctx) + if err != nil { + return fmt.Errorf("unable to list persistent volumes: %w", err) + } + + for _, persistentVolume := range persistentVolumes.Items { + adapter.logger.Infof("removing persistent volume %s", persistentVolume.Name) + + err = adapter.DeletePersistentVolume(ctx, persistentVolume.Name) + if err != nil { + adapter.logger.Warnf("unable to remove persistent volume %s: %s", persistentVolume.Name, err) + } + } + + persistentVolumeClaims, err := adapter.ListPersistentVolumeClaims(ctx, "") + if err != nil { + return fmt.Errorf("unable to list persistent volume claims: %w", err) + } + + for _, persistentVolumeClaim := range persistentVolumeClaims.Items { + adapter.logger.Infof("removing persistent volume claim %s/%s", persistentVolumeClaim.Namespace, persistentVolumeClaim.Name) + + err = adapter.DeletePersistentVolumeClaim(ctx, persistentVolumeClaim.Name, persistentVolumeClaim.Namespace) + if err != nil { + adapter.logger.Warnf("unable to remove persistent volume claim %s/%s: %s", persistentVolumeClaim.Namespace, persistentVolumeClaim.Name, err) + } + } + + return nil +} + +func (adapter *KubeDockerAdapter) removeAllConfigMapsAndSecrets(ctx context.Context) error { + adapter.logger.Infoln("removing all configmaps...") + + configMaps, err := adapter.ListConfigMaps("") + if err != nil { + return fmt.Errorf("unable to list configmaps: %w", err) + } + + for _, configMap := range configMaps.Items { + adapter.logger.Infof("removing configmap %s/%s", configMap.Namespace, configMap.Name) + + err = adapter.DeleteConfigMap(configMap.Name, configMap.Namespace) + if err != nil { + adapter.logger.Warnf("unable to remove configmap %s/%s: %s", configMap.Namespace, configMap.Name, err) + } + } + + adapter.logger.Infoln("removing all secrets...") + + secrets, err := adapter.ListSecrets("", labels.NewSelector()) + if err != nil { + return fmt.Errorf("unable to list secrets: %w", err) + } + + for _, secret := range secrets.Items { + adapter.logger.Infof("removing secret %s/%s", secret.Namespace, secret.Name) + + err = adapter.DeleteSecret(secret.Name, secret.Namespace) + if err != nil { + adapter.logger.Warnf("unable to remove secret %s/%s: %s", secret.Namespace, secret.Name, err) + } + } + + return nil +} + +func (adapter *KubeDockerAdapter) removeAllNamespaces(ctx context.Context) error { + adapter.logger.Infoln("removing all namespaces...") + + namespaces, err := adapter.ListNamespaces(ctx) + if err != nil { + return fmt.Errorf("unable to list namespaces: %w", err) + } + + for _, namespace := range namespaces.Items { + adapter.logger.Infof("removing namespace %s", namespace.Name) + + err = adapter.DeleteNamespace(ctx, namespace.Name) + if err != nil { + adapter.logger.Warnf("unable to remove namespace %s: %s", namespace.Name, err) + } + } + + return nil +} diff --git a/pkg/filesystem/file.go b/pkg/filesystem/file.go index f9bb0e3..28a2cec 100644 --- a/pkg/filesystem/file.go +++ b/pkg/filesystem/file.go @@ -10,17 +10,6 @@ import ( "strings" ) -// FileExists checks if a file exists on the filesystem -func FileExists(path string) (bool, error) { - if _, err := os.Stat(path); err == nil { - return true, nil - } else if errors.Is(err, os.ErrNotExist) { - return false, nil - } else { - return false, fmt.Errorf("an error occurred while checking if the file exists: %w", err) - } -} - // CreateDir creates all directories along a path. // It returns an error, if any occurs during the operation. func CreateDir(path string) error { @@ -62,49 +51,15 @@ func CreateFileWithDirectories(filePath string, content []byte) error { return nil } -// StoreDataMapOnDisk takes a path where the data will be stored (storagePath), a prefix for the filename (filePrefix), -// and a map of strings (data). It iterates through the provided map, and for each key-value pair, -// it creates a file with the filename constructed as the concatenation of the filePrefix and the key. -// It then writes the corresponding value into the file. -func StoreDataMapOnDisk(storagePath, filePrefix string, data map[string]string) error { - for key, value := range data { - - fileName := fmt.Sprintf("%s%s", filePrefix, key) - - file, err := os.Create(path.Join(storagePath, fileName)) - if err != nil { - return fmt.Errorf("an error occurred while creating the file: %w", err) - } - defer file.Close() - - _, err = file.WriteString(value) - if err != nil { - return fmt.Errorf("an error occurred while writing to the file: %w", err) - } +// FileExists checks if a file exists on the filesystem +func FileExists(path string) (bool, error) { + if _, err := os.Stat(path); err == nil { + return true, nil + } else if errors.Is(err, os.ErrNotExist) { + return false, nil + } else { + return false, fmt.Errorf("an error occurred while checking if the file exists: %w", err) } - - return nil -} - -// StoreMetadataOnDisk takes a path where the data will be stored (storagePath), a filename (fileName), -// and a map of strings (data). It creates a file at the specified location with the given filename, -// and writes the key-value pairs from the map into the file in the format "key=value\n". -// If an error occurs during this process, it returns the error. -func StoreMetadataOnDisk(storagePath, fileName string, data map[string]string) error { - file, err := os.Create(path.Join(storagePath, fileName)) - if err != nil { - return fmt.Errorf("an error occurred while creating the file: %w", err) - } - defer file.Close() - - for key, value := range data { - _, err = file.WriteString(fmt.Sprintf("%s=%s\n", key, value)) - if err != nil { - return fmt.Errorf("an error occurred while writing to the file: %w", err) - } - } - - return nil } // LoadMetadataFromDisk takes a path where the data is stored (storagePath) and a filename (fileName), @@ -152,3 +107,72 @@ func ReadFileAsString(filePath string) (string, error) { return string(fileBytes), nil } + +// RemoveAllContent removes all files and folders in the given directory path. +func RemoveAllContent(dir string) error { + d, err := os.Open(dir) + if err != nil { + return fmt.Errorf("an error occurred while opening the directory: %w", err) + } + defer d.Close() + + names, err := d.Readdirnames(-1) + if err != nil { + return fmt.Errorf("an error occurred while reading the directory: %w", err) + } + + for _, name := range names { + filePath := filepath.Join(dir, name) + err = os.RemoveAll(filePath) + if err != nil { + return fmt.Errorf("an error occurred while removing the file %s: %w", filePath, err) + } + } + + return nil +} + +// StoreDataMapOnDisk takes a path where the data will be stored (storagePath), a prefix for the filename (filePrefix), +// and a map of strings (data). It iterates through the provided map, and for each key-value pair, +// it creates a file with the filename constructed as the concatenation of the filePrefix and the key. +// It then writes the corresponding value into the file. +func StoreDataMapOnDisk(storagePath, filePrefix string, data map[string]string) error { + for key, value := range data { + + fileName := fmt.Sprintf("%s%s", filePrefix, key) + + file, err := os.Create(path.Join(storagePath, fileName)) + if err != nil { + return fmt.Errorf("an error occurred while creating the file: %w", err) + } + defer file.Close() + + _, err = file.WriteString(value) + if err != nil { + return fmt.Errorf("an error occurred while writing to the file: %w", err) + } + } + + return nil +} + +// StoreMetadataOnDisk takes a path where the data will be stored (storagePath), a filename (fileName), +// and a map of strings (data). It creates a file at the specified location with the given filename, +// and writes the key-value pairs from the map into the file in the format "key=value\n". +// If an error occurs during this process, it returns the error. +func StoreMetadataOnDisk(storagePath, fileName string, data map[string]string) error { + file, err := os.Create(path.Join(storagePath, fileName)) + if err != nil { + return fmt.Errorf("an error occurred while creating the file: %w", err) + } + defer file.Close() + + for key, value := range data { + _, err = file.WriteString(fmt.Sprintf("%s=%s\n", key, value)) + if err != nil { + return fmt.Errorf("an error occurred while writing to the file: %w", err) + } + } + + return nil +} diff --git a/pkg/prompt/prompt.go b/pkg/prompt/prompt.go new file mode 100644 index 0000000..132811e --- /dev/null +++ b/pkg/prompt/prompt.go @@ -0,0 +1,31 @@ +package prompt + +import ( + "bufio" + "fmt" + "os" + "strings" +) + +// AskForConfirmation asks the user for a confirmation. +func AskForConfirmation() (bool, error) { + r := bufio.NewReader(os.Stdin) + line, _, err := r.ReadLine() + if err != nil { + return false, err + } + + response := string(line) + + switch strings.ToLower(response) { + case "y", "yes": + return true, nil + case "n", "no": + return false, nil + case "": + return false, nil + default: + fmt.Println("Please type [y]es or [n]o and then press enter:") + return AskForConfirmation() + } +} From ce8660fcfe7cb8abe3ccdce4ed4d99ee1d5a6c4e Mon Sep 17 00:00:00 2001 From: deviantony Date: Thu, 21 Sep 2023 09:36:04 +0000 Subject: [PATCH 2/2] docs: update function comment --- internal/adapter/reset.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/internal/adapter/reset.go b/internal/adapter/reset.go index 6f84fb0..4ad0fba 100644 --- a/internal/adapter/reset.go +++ b/internal/adapter/reset.go @@ -8,21 +8,24 @@ import ( "k8s.io/apimachinery/pkg/labels" ) -// ExecuteResetRoutine performs a cleanup routine that removes all k2d resources from the host. +// ExecuteResetRoutine performs a cleanup routine that removes all k2d resources from the host, +// as well as content within the specified k2d data directory. // This function is intended to be used as a "reset mode" operation for cleaning up any resources // managed by k2d on the host. // // Parameters: // - ctx context.Context: The context for carrying out the reset routine. +// - k2dDataPath string: The path to the k2d data directory that needs to be cleaned up. // // Returns: -// - error: Returns an error if any of the resource removal operations fail. +// - error: Returns an error if any of the resource removal or file system operations fail. // // Steps: // 1. Removes all workload resources (like deployments, pods) by invoking removeAllWorkloads. // 2. Removes all Persistent Volumes and Persistent Volume Claims by invoking removeAllPersistentVolumeAndClaims. // 3. Removes all ConfigMaps and Secrets by invoking removeAllConfigMapsAndSecrets. // 4. Removes all namespaces by invoking removeAllNamespaces. +// 5. Removes all content in the k2d data directory by invoking filesystem.RemoveAllContent. func (adapter *KubeDockerAdapter) ExecuteResetRoutine(ctx context.Context, k2dDataPath string) error { adapter.logger.Infoln("reset mode enabled, removing all k2d resources on this host")