fix: better context handling and less unexpected deleting #262
| @ -8,6 +8,7 @@ import ( | ||||
|  | ||||
| 	"coopcloud.tech/abra/cli/internal" | ||||
| 	"coopcloud.tech/abra/pkg/config" | ||||
| 	"coopcloud.tech/abra/pkg/context" | ||||
| 	"coopcloud.tech/abra/pkg/formatter" | ||||
| 	"coopcloud.tech/abra/pkg/recipe" | ||||
| 	"coopcloud.tech/abra/pkg/ssh" | ||||
| @ -97,6 +98,10 @@ can take some time. | ||||
| 			alreadySeen := make(map[string]bool) | ||||
| 			for _, app := range apps { | ||||
| 				if _, ok := alreadySeen[app.Server]; !ok { | ||||
| 					if err := context.HasDockerContext(app.Server); err != nil { | ||||
| 						logrus.Fatal(err) | ||||
| 					} | ||||
|  | ||||
| 					if err := ssh.EnsureHostKey(app.Server); err != nil { | ||||
| 						logrus.Fatal(fmt.Sprintf(internal.SSHFailMsg, app.Server)) | ||||
| 					} | ||||
|  | ||||
| @ -15,35 +15,43 @@ import ( | ||||
| 	"github.com/urfave/cli" | ||||
| ) | ||||
|  | ||||
| // Volumes stores the variable from VolumesFlag | ||||
| var Volumes bool | ||||
|  | ||||
| // VolumesFlag is used to specify if volumes should be deleted when deleting an app | ||||
| var VolumesFlag = &cli.BoolFlag{ | ||||
| 	Name:        "volumes, V", | ||||
| 	Destination: &Volumes, | ||||
| } | ||||
|  | ||||
| var appRemoveCommand = cli.Command{ | ||||
| 	Name:      "remove", | ||||
| 	Aliases:   []string{"rm"}, | ||||
| 	ArgsUsage: "<domain>", | ||||
| 	Usage:     "Remove an already undeployed app", | ||||
| 	Usage:     "Remove all app data, locally and remotely", | ||||
| 	Description: ` | ||||
| This command removes everything related to an app which is already undeployed. | ||||
|  | ||||
| By default, it will prompt for confirmation before proceeding. All secrets, | ||||
| volumes and the local app env file will be deleted. | ||||
|  | ||||
| Only run this command when you are sure you want to completely remove the app | ||||
| and all associated app data. This is a destructive action, Be Careful! | ||||
|  | ||||
| If you would like to delete specific volumes or secrets, please use removal | ||||
| sub-commands under "app volume" and "app secret" instead. | ||||
|  | ||||
| Please note, if you delete the local app env file without removing volumes and | ||||
| secrets first, Abra will *not* be able to help you remove them afterwards. | ||||
|  | ||||
| To delete everything without prompt, use the "--force/-f" or the "--no-input/n" | ||||
| flag. | ||||
| `, | ||||
| 	Flags: []cli.Flag{ | ||||
| 		VolumesFlag, | ||||
| 		internal.ForceFlag, | ||||
| 		internal.DebugFlag, | ||||
| 		internal.NoInputFlag, | ||||
| 	}, | ||||
| 	Before: internal.SubCommandBefore, | ||||
| 	BashComplete: autocomplete.AppNameComplete, | ||||
| 	Before:       internal.SubCommandBefore, | ||||
| 	Action: func(c *cli.Context) error { | ||||
| 		app := internal.ValidateApp(c) | ||||
|  | ||||
| 		if !internal.Force && !internal.NoInput { | ||||
| 			response := false | ||||
| 			prompt := &survey.Confirm{ | ||||
| 				Message: fmt.Sprintf("about to remove %s, are you sure?", app.Name), | ||||
| 			} | ||||
| 			msg := "ALERTA ALERTA: this will completely remove %s data and configurations locally and remotely, are you sure?" | ||||
| 			prompt := &survey.Confirm{Message: fmt.Sprintf(msg, app.Name)} | ||||
| 			if err := survey.AskOne(prompt, &response); err != nil { | ||||
| 				logrus.Fatal(err) | ||||
| 			} | ||||
| @ -84,26 +92,7 @@ var appRemoveCommand = cli.Command{ | ||||
| 		} | ||||
|  | ||||
| 		if len(secrets) > 0 { | ||||
| 			var secretNamesToRemove []string | ||||
|  | ||||
| 			if !internal.Force && !internal.NoInput { | ||||
| 				secretsPrompt := &survey.MultiSelect{ | ||||
| 					Message: "which secrets do you want to remove?", | ||||
| 					Help:    "'x' indicates selected, enter / return to confirm, ctrl-c to exit, vim mode is enabled", | ||||
| 					VimMode: true, | ||||
| 					Options: secretNames, | ||||
| 					Default: secretNames, | ||||
| 				} | ||||
| 				if err := survey.AskOne(secretsPrompt, &secretNamesToRemove); err != nil { | ||||
| 					logrus.Fatal(err) | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			if internal.Force || internal.NoInput { | ||||
| 				secretNamesToRemove = secretNames | ||||
| 			} | ||||
|  | ||||
| 			for _, name := range secretNamesToRemove { | ||||
| 			for _, name := range secretNames { | ||||
| 				err := cl.SecretRemove(context.Background(), secrets[name]) | ||||
| 				if err != nil { | ||||
| 					logrus.Fatal(err) | ||||
| @ -131,44 +120,24 @@ var appRemoveCommand = cli.Command{ | ||||
| 		} | ||||
|  | ||||
| 		if len(vols) > 0 { | ||||
| 			if Volumes { | ||||
| 				var removeVols []string | ||||
| 				if !internal.Force && !internal.NoInput { | ||||
| 					volumesPrompt := &survey.MultiSelect{ | ||||
| 						Message: "which volumes do you want to remove?", | ||||
| 						Help:    "'x' indicates selected, enter / return to confirm, ctrl-c to exit, vim mode is enabled", | ||||
| 						VimMode: true, | ||||
| 						Options: vols, | ||||
| 						Default: vols, | ||||
| 					} | ||||
| 					if err := survey.AskOne(volumesPrompt, &removeVols); err != nil { | ||||
| 						logrus.Fatal(err) | ||||
| 					} | ||||
| 			var removeVols []string | ||||
| 			for _, vol := range removeVols { | ||||
| 				err := cl.VolumeRemove(context.Background(), vol, internal.Force) // last argument is for force removing | ||||
| 				if err != nil { | ||||
| 					logrus.Fatal(err) | ||||
| 				} | ||||
|  | ||||
| 				for _, vol := range removeVols { | ||||
| 					err := cl.VolumeRemove(context.Background(), vol, internal.Force) // last argument is for force removing | ||||
| 					if err != nil { | ||||
| 						logrus.Fatal(err) | ||||
| 					} | ||||
| 					logrus.Info(fmt.Sprintf("volume %s removed", vol)) | ||||
| 				} | ||||
| 			} else { | ||||
| 				logrus.Info("no volumes were removed") | ||||
| 				logrus.Info(fmt.Sprintf("volume %s removed", vol)) | ||||
| 			} | ||||
| 		} else { | ||||
| 			if Volumes { | ||||
| 				logrus.Info("no volumes to remove") | ||||
| 			} | ||||
| 			logrus.Info("no volumes to remove") | ||||
| 		} | ||||
|  | ||||
| 		err = os.Remove(app.Path) | ||||
| 		if err != nil { | ||||
| 		if err = os.Remove(app.Path); err != nil { | ||||
| 			logrus.Fatal(err) | ||||
| 		} | ||||
|  | ||||
| 		logrus.Info(fmt.Sprintf("file: %s removed", app.Path)) | ||||
|  | ||||
| 		return nil | ||||
| 	}, | ||||
| 	BashComplete: autocomplete.AppNameComplete, | ||||
| } | ||||
|  | ||||
| @ -6,6 +6,7 @@ import ( | ||||
|  | ||||
| 	"coopcloud.tech/abra/pkg/app" | ||||
| 	"coopcloud.tech/abra/pkg/config" | ||||
| 	"coopcloud.tech/abra/pkg/context" | ||||
| 	"coopcloud.tech/abra/pkg/formatter" | ||||
| 	"coopcloud.tech/abra/pkg/jsontable" | ||||
| 	"coopcloud.tech/abra/pkg/recipe" | ||||
| @ -146,6 +147,10 @@ func NewAction(c *cli.Context) error { | ||||
| 	var secrets AppSecrets | ||||
| 	var secretTable *jsontable.JSONTable | ||||
| 	if Secrets { | ||||
| 		if err := context.HasDockerContext(NewAppServer); err != nil { | ||||
| 			logrus.Fatal(err) | ||||
| 		} | ||||
|  | ||||
| 		if err := ssh.EnsureHostKey(NewAppServer); err != nil { | ||||
| 			logrus.Fatal(err) | ||||
| 		} | ||||
|  | ||||
| @ -8,6 +8,7 @@ import ( | ||||
|  | ||||
| 	"coopcloud.tech/abra/pkg/app" | ||||
| 	"coopcloud.tech/abra/pkg/config" | ||||
| 	"coopcloud.tech/abra/pkg/context" | ||||
| 	"coopcloud.tech/abra/pkg/recipe" | ||||
| 	"coopcloud.tech/abra/pkg/ssh" | ||||
| 	"github.com/AlecAivazis/survey/v2" | ||||
| @ -138,6 +139,10 @@ func ValidateApp(c *cli.Context) config.App { | ||||
| 		logrus.Fatal(err) | ||||
| 	} | ||||
|  | ||||
| 	if err := context.HasDockerContext(app.Server); err != nil { | ||||
| 		logrus.Fatal(err) | ||||
| 	} | ||||
|  | ||||
| 	if err := ssh.EnsureHostKey(app.Server); err != nil { | ||||
| 		logrus.Fatal(err) | ||||
| 	} | ||||
|  | ||||
| @ -80,7 +80,19 @@ func cleanUp(domainName string) { | ||||
| 	} | ||||
|  | ||||
| 	logrus.Warnf("cleaning up server directory for %s", domainName) | ||||
| 	if err := os.RemoveAll(filepath.Join(config.SERVERS_DIR, domainName)); err != nil { | ||||
|  | ||||
| 	serverDir := filepath.Join(config.SERVERS_DIR, domainName) | ||||
| 	files, err := config.GetAllFilesInDirectory(serverDir) | ||||
| 	if err != nil { | ||||
| 		logrus.Fatalf("unable to list files in %s: %s", serverDir, err) | ||||
| 	} | ||||
|  | ||||
| 	if len(files) > 0 { | ||||
| 		logrus.Warnf("aborting clean up of %s because it is not empty", serverDir) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	if err := os.RemoveAll(serverDir); err != nil { | ||||
| 		logrus.Fatal(err) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| @ -12,11 +12,20 @@ import ( | ||||
| 	"github.com/urfave/cli" | ||||
| ) | ||||
|  | ||||
| var problemsFilter bool | ||||
|  | ||||
| var problemsFilterFlag = &cli.BoolFlag{ | ||||
| 	Name:        "problems, p", | ||||
| 	Usage:       "Show only servers with potential connection problems", | ||||
| 	Destination: &problemsFilter, | ||||
| } | ||||
|  | ||||
| var serverListCommand = cli.Command{ | ||||
| 	Name:    "list", | ||||
| 	Aliases: []string{"ls"}, | ||||
| 	Usage:   "List managed servers", | ||||
| 	Flags: []cli.Flag{ | ||||
| 		problemsFilterFlag, | ||||
| 		internal.DebugFlag, | ||||
| 		internal.MachineReadableFlag, | ||||
| 	}, | ||||
| @ -48,6 +57,7 @@ var serverListCommand = cli.Command{ | ||||
| 					// No local context found, we can continue safely | ||||
| 					continue | ||||
| 				} | ||||
|  | ||||
| 				if ctx.Name == serverName { | ||||
| 					sp, err := ssh.ParseURL(endpoint) | ||||
| 					if err != nil { | ||||
| @ -56,6 +66,7 @@ var serverListCommand = cli.Command{ | ||||
| 					row = []string{serverName, sp.Host, sp.User, sp.Port} | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			if len(row) == 0 { | ||||
| 				if serverName == "default" { | ||||
| 					row = []string{serverName, "local", "n/a", "n/a"} | ||||
| @ -63,7 +74,14 @@ var serverListCommand = cli.Command{ | ||||
| 					row = []string{serverName, "unknown", "unknown", "unknown"} | ||||
| 				} | ||||
| 			} | ||||
| 			table.Append(row) | ||||
|  | ||||
| 			if problemsFilter { | ||||
| 				if row[1] == "unknown" { | ||||
| 					table.Append(row) | ||||
| 				} | ||||
| 			} else { | ||||
| 				table.Append(row) | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		return nil | ||||
|  | ||||
| @ -203,7 +203,7 @@ func LoadAppFiles(servers ...string) (AppFiles, error) { | ||||
|  | ||||
| 	for _, server := range servers { | ||||
| 		serverDir := path.Join(SERVERS_DIR, server) | ||||
| 		files, err := getAllFilesInDirectory(serverDir) | ||||
| 		files, err := GetAllFilesInDirectory(serverDir) | ||||
| 		if err != nil { | ||||
| 			return appFiles, fmt.Errorf("server %s doesn't exist? Run \"abra server ls\" to check", server) | ||||
| 		} | ||||
|  | ||||
| @ -66,8 +66,8 @@ func ReadServerNames() ([]string, error) { | ||||
| 	return serverNames, nil | ||||
| } | ||||
|  | ||||
| // getAllFilesInDirectory returns filenames of all files in directory | ||||
| func getAllFilesInDirectory(directory string) ([]fs.FileInfo, error) { | ||||
| // GetAllFilesInDirectory returns filenames of all files in directory | ||||
| func GetAllFilesInDirectory(directory string) ([]fs.FileInfo, error) { | ||||
| 	var realFiles []fs.FileInfo | ||||
|  | ||||
| 	files, err := ioutil.ReadDir(directory) | ||||
|  | ||||
| @ -2,6 +2,7 @@ package context | ||||
|  | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
|  | ||||
| 	"github.com/docker/cli/cli/command" | ||||
| 	dConfig "github.com/docker/cli/cli/config" | ||||
| @ -42,3 +43,34 @@ func GetContextEndpoint(ctx contextStore.Metadata) (string, error) { | ||||
| func newContextStore(dir string, config contextStore.Config) contextStore.Store { | ||||
| 	return contextStore.New(dir, config) | ||||
| } | ||||
|  | ||||
| // MissingContextMsg helps end-uers debug missing docker context issues. | ||||
| var missingContextMsg = `unable to find Docker context for %s? | ||||
|  | ||||
| Please run "abra server ls" to confirm. If you see "unknown" in the table | ||||
| output then you need to run the following command: | ||||
|  | ||||
|     abra server add %s <args> | ||||
|  | ||||
| See "abra server add --help" for more. | ||||
| ` | ||||
|  | ||||
| // HasDockerContext figures out if a local setup has a working docker context | ||||
| // configuration or not. This usually tells us if they'll be able to make a SSH | ||||
| // connection to a server or not and can be a useful way to signal to end-users | ||||
| // that they need to fix something up if missing. | ||||
| func HasDockerContext(serverName string) error { | ||||
| 	dockerContextStore := NewDefaultDockerContextStore() | ||||
| 	contexts, err := dockerContextStore.Store.List() | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	for _, ctx := range contexts { | ||||
| 		if ctx.Name == serverName { | ||||
| 			return nil | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return fmt.Errorf(missingContextMsg, serverName, serverName) | ||||
| } | ||||
|  | ||||
| @ -6,6 +6,7 @@ import ( | ||||
| 	"net" | ||||
| 	"net/url" | ||||
|  | ||||
| 	ctxPkg "coopcloud.tech/abra/pkg/context" | ||||
| 	sshPkg "coopcloud.tech/abra/pkg/ssh" | ||||
| 	"github.com/docker/cli/cli/connhelper" | ||||
| 	"github.com/docker/cli/cli/connhelper/ssh" | ||||
| @ -36,6 +37,10 @@ func getConnectionHelper(daemonURL string, sshFlags []string) (*connhelper.Conne | ||||
| 			return nil, errors.Wrap(err, "ssh host connection is not valid") | ||||
| 		} | ||||
|  | ||||
| 		if err := ctxPkg.HasDockerContext(ctxConnDetails.Host); err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
|  | ||||
| 		if err := sshPkg.EnsureHostKey(ctxConnDetails.Host); err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
|  | ||||
		Reference in New Issue
	
	Block a user