fix: better context handling and less unexpected deleting #262
| @ -8,6 +8,7 @@ import ( | |||||||
|  |  | ||||||
| 	"coopcloud.tech/abra/cli/internal" | 	"coopcloud.tech/abra/cli/internal" | ||||||
| 	"coopcloud.tech/abra/pkg/config" | 	"coopcloud.tech/abra/pkg/config" | ||||||
|  | 	"coopcloud.tech/abra/pkg/context" | ||||||
| 	"coopcloud.tech/abra/pkg/formatter" | 	"coopcloud.tech/abra/pkg/formatter" | ||||||
| 	"coopcloud.tech/abra/pkg/recipe" | 	"coopcloud.tech/abra/pkg/recipe" | ||||||
| 	"coopcloud.tech/abra/pkg/ssh" | 	"coopcloud.tech/abra/pkg/ssh" | ||||||
| @ -97,6 +98,10 @@ can take some time. | |||||||
| 			alreadySeen := make(map[string]bool) | 			alreadySeen := make(map[string]bool) | ||||||
| 			for _, app := range apps { | 			for _, app := range apps { | ||||||
| 				if _, ok := alreadySeen[app.Server]; !ok { | 				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 { | 					if err := ssh.EnsureHostKey(app.Server); err != nil { | ||||||
| 						logrus.Fatal(fmt.Sprintf(internal.SSHFailMsg, app.Server)) | 						logrus.Fatal(fmt.Sprintf(internal.SSHFailMsg, app.Server)) | ||||||
| 					} | 					} | ||||||
|  | |||||||
| @ -15,35 +15,43 @@ import ( | |||||||
| 	"github.com/urfave/cli" | 	"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{ | var appRemoveCommand = cli.Command{ | ||||||
| 	Name:      "remove", | 	Name:      "remove", | ||||||
| 	Aliases:   []string{"rm"}, | 	Aliases:   []string{"rm"}, | ||||||
| 	ArgsUsage: "<domain>", | 	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{ | 	Flags: []cli.Flag{ | ||||||
| 		VolumesFlag, |  | ||||||
| 		internal.ForceFlag, | 		internal.ForceFlag, | ||||||
| 		internal.DebugFlag, | 		internal.DebugFlag, | ||||||
| 		internal.NoInputFlag, | 		internal.NoInputFlag, | ||||||
| 	}, | 	}, | ||||||
| 	Before: internal.SubCommandBefore, | 	BashComplete: autocomplete.AppNameComplete, | ||||||
|  | 	Before:       internal.SubCommandBefore, | ||||||
| 	Action: func(c *cli.Context) error { | 	Action: func(c *cli.Context) error { | ||||||
| 		app := internal.ValidateApp(c) | 		app := internal.ValidateApp(c) | ||||||
|  |  | ||||||
| 		if !internal.Force && !internal.NoInput { | 		if !internal.Force && !internal.NoInput { | ||||||
| 			response := false | 			response := false | ||||||
| 			prompt := &survey.Confirm{ | 			msg := "ALERTA ALERTA: this will completely remove %s data and configurations locally and remotely, are you sure?" | ||||||
| 				Message: fmt.Sprintf("about to remove %s, are you sure?", app.Name), | 			prompt := &survey.Confirm{Message: fmt.Sprintf(msg, app.Name)} | ||||||
| 			} |  | ||||||
| 			if err := survey.AskOne(prompt, &response); err != nil { | 			if err := survey.AskOne(prompt, &response); err != nil { | ||||||
| 				logrus.Fatal(err) | 				logrus.Fatal(err) | ||||||
| 			} | 			} | ||||||
| @ -84,26 +92,7 @@ var appRemoveCommand = cli.Command{ | |||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		if len(secrets) > 0 { | 		if len(secrets) > 0 { | ||||||
| 			var secretNamesToRemove []string | 			for _, name := range secretNames { | ||||||
|  |  | ||||||
| 			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 { |  | ||||||
| 				err := cl.SecretRemove(context.Background(), secrets[name]) | 				err := cl.SecretRemove(context.Background(), secrets[name]) | ||||||
| 				if err != nil { | 				if err != nil { | ||||||
| 					logrus.Fatal(err) | 					logrus.Fatal(err) | ||||||
| @ -131,44 +120,24 @@ var appRemoveCommand = cli.Command{ | |||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		if len(vols) > 0 { | 		if len(vols) > 0 { | ||||||
| 			if Volumes { | 			var removeVols []string | ||||||
| 				var removeVols []string | 			for _, vol := range removeVols { | ||||||
| 				if !internal.Force && !internal.NoInput { | 				err := cl.VolumeRemove(context.Background(), vol, internal.Force) // last argument is for force removing | ||||||
| 					volumesPrompt := &survey.MultiSelect{ | 				if err != nil { | ||||||
| 						Message: "which volumes do you want to remove?", | 					logrus.Fatal(err) | ||||||
| 						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) |  | ||||||
| 					} |  | ||||||
| 				} | 				} | ||||||
|  | 				logrus.Info(fmt.Sprintf("volume %s removed", vol)) | ||||||
| 				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") |  | ||||||
| 			} | 			} | ||||||
| 		} else { | 		} else { | ||||||
| 			if Volumes { | 			logrus.Info("no volumes to remove") | ||||||
| 				logrus.Info("no volumes to remove") |  | ||||||
| 			} |  | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		err = os.Remove(app.Path) | 		if err = os.Remove(app.Path); err != nil { | ||||||
| 		if err != nil { |  | ||||||
| 			logrus.Fatal(err) | 			logrus.Fatal(err) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		logrus.Info(fmt.Sprintf("file: %s removed", app.Path)) | 		logrus.Info(fmt.Sprintf("file: %s removed", app.Path)) | ||||||
|  |  | ||||||
| 		return nil | 		return nil | ||||||
| 	}, | 	}, | ||||||
| 	BashComplete: autocomplete.AppNameComplete, |  | ||||||
| } | } | ||||||
|  | |||||||
| @ -6,6 +6,7 @@ import ( | |||||||
|  |  | ||||||
| 	"coopcloud.tech/abra/pkg/app" | 	"coopcloud.tech/abra/pkg/app" | ||||||
| 	"coopcloud.tech/abra/pkg/config" | 	"coopcloud.tech/abra/pkg/config" | ||||||
|  | 	"coopcloud.tech/abra/pkg/context" | ||||||
| 	"coopcloud.tech/abra/pkg/formatter" | 	"coopcloud.tech/abra/pkg/formatter" | ||||||
| 	"coopcloud.tech/abra/pkg/jsontable" | 	"coopcloud.tech/abra/pkg/jsontable" | ||||||
| 	"coopcloud.tech/abra/pkg/recipe" | 	"coopcloud.tech/abra/pkg/recipe" | ||||||
| @ -146,6 +147,10 @@ func NewAction(c *cli.Context) error { | |||||||
| 	var secrets AppSecrets | 	var secrets AppSecrets | ||||||
| 	var secretTable *jsontable.JSONTable | 	var secretTable *jsontable.JSONTable | ||||||
| 	if Secrets { | 	if Secrets { | ||||||
|  | 		if err := context.HasDockerContext(NewAppServer); err != nil { | ||||||
|  | 			logrus.Fatal(err) | ||||||
|  | 		} | ||||||
|  |  | ||||||
| 		if err := ssh.EnsureHostKey(NewAppServer); err != nil { | 		if err := ssh.EnsureHostKey(NewAppServer); err != nil { | ||||||
| 			logrus.Fatal(err) | 			logrus.Fatal(err) | ||||||
| 		} | 		} | ||||||
|  | |||||||
| @ -8,6 +8,7 @@ import ( | |||||||
|  |  | ||||||
| 	"coopcloud.tech/abra/pkg/app" | 	"coopcloud.tech/abra/pkg/app" | ||||||
| 	"coopcloud.tech/abra/pkg/config" | 	"coopcloud.tech/abra/pkg/config" | ||||||
|  | 	"coopcloud.tech/abra/pkg/context" | ||||||
| 	"coopcloud.tech/abra/pkg/recipe" | 	"coopcloud.tech/abra/pkg/recipe" | ||||||
| 	"coopcloud.tech/abra/pkg/ssh" | 	"coopcloud.tech/abra/pkg/ssh" | ||||||
| 	"github.com/AlecAivazis/survey/v2" | 	"github.com/AlecAivazis/survey/v2" | ||||||
| @ -138,6 +139,10 @@ func ValidateApp(c *cli.Context) config.App { | |||||||
| 		logrus.Fatal(err) | 		logrus.Fatal(err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	if err := context.HasDockerContext(app.Server); err != nil { | ||||||
|  | 		logrus.Fatal(err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	if err := ssh.EnsureHostKey(app.Server); err != nil { | 	if err := ssh.EnsureHostKey(app.Server); err != nil { | ||||||
| 		logrus.Fatal(err) | 		logrus.Fatal(err) | ||||||
| 	} | 	} | ||||||
|  | |||||||
| @ -80,7 +80,19 @@ func cleanUp(domainName string) { | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	logrus.Warnf("cleaning up server directory for %s", domainName) | 	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) | 		logrus.Fatal(err) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  | |||||||
| @ -12,11 +12,20 @@ import ( | |||||||
| 	"github.com/urfave/cli" | 	"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{ | var serverListCommand = cli.Command{ | ||||||
| 	Name:    "list", | 	Name:    "list", | ||||||
| 	Aliases: []string{"ls"}, | 	Aliases: []string{"ls"}, | ||||||
| 	Usage:   "List managed servers", | 	Usage:   "List managed servers", | ||||||
| 	Flags: []cli.Flag{ | 	Flags: []cli.Flag{ | ||||||
|  | 		problemsFilterFlag, | ||||||
| 		internal.DebugFlag, | 		internal.DebugFlag, | ||||||
| 		internal.MachineReadableFlag, | 		internal.MachineReadableFlag, | ||||||
| 	}, | 	}, | ||||||
| @ -48,6 +57,7 @@ var serverListCommand = cli.Command{ | |||||||
| 					// No local context found, we can continue safely | 					// No local context found, we can continue safely | ||||||
| 					continue | 					continue | ||||||
| 				} | 				} | ||||||
|  |  | ||||||
| 				if ctx.Name == serverName { | 				if ctx.Name == serverName { | ||||||
| 					sp, err := ssh.ParseURL(endpoint) | 					sp, err := ssh.ParseURL(endpoint) | ||||||
| 					if err != nil { | 					if err != nil { | ||||||
| @ -56,6 +66,7 @@ var serverListCommand = cli.Command{ | |||||||
| 					row = []string{serverName, sp.Host, sp.User, sp.Port} | 					row = []string{serverName, sp.Host, sp.User, sp.Port} | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			if len(row) == 0 { | 			if len(row) == 0 { | ||||||
| 				if serverName == "default" { | 				if serverName == "default" { | ||||||
| 					row = []string{serverName, "local", "n/a", "n/a"} | 					row = []string{serverName, "local", "n/a", "n/a"} | ||||||
| @ -63,7 +74,14 @@ var serverListCommand = cli.Command{ | |||||||
| 					row = []string{serverName, "unknown", "unknown", "unknown"} | 					row = []string{serverName, "unknown", "unknown", "unknown"} | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
| 			table.Append(row) |  | ||||||
|  | 			if problemsFilter { | ||||||
|  | 				if row[1] == "unknown" { | ||||||
|  | 					table.Append(row) | ||||||
|  | 				} | ||||||
|  | 			} else { | ||||||
|  | 				table.Append(row) | ||||||
|  | 			} | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		return nil | 		return nil | ||||||
|  | |||||||
| @ -203,7 +203,7 @@ func LoadAppFiles(servers ...string) (AppFiles, error) { | |||||||
|  |  | ||||||
| 	for _, server := range servers { | 	for _, server := range servers { | ||||||
| 		serverDir := path.Join(SERVERS_DIR, server) | 		serverDir := path.Join(SERVERS_DIR, server) | ||||||
| 		files, err := getAllFilesInDirectory(serverDir) | 		files, err := GetAllFilesInDirectory(serverDir) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return appFiles, fmt.Errorf("server %s doesn't exist? Run \"abra server ls\" to check", server) | 			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 | 	return serverNames, nil | ||||||
| } | } | ||||||
|  |  | ||||||
| // getAllFilesInDirectory returns filenames of all files in directory | // GetAllFilesInDirectory returns filenames of all files in directory | ||||||
| func getAllFilesInDirectory(directory string) ([]fs.FileInfo, error) { | func GetAllFilesInDirectory(directory string) ([]fs.FileInfo, error) { | ||||||
| 	var realFiles []fs.FileInfo | 	var realFiles []fs.FileInfo | ||||||
|  |  | ||||||
| 	files, err := ioutil.ReadDir(directory) | 	files, err := ioutil.ReadDir(directory) | ||||||
|  | |||||||
| @ -2,6 +2,7 @@ package context | |||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"errors" | 	"errors" | ||||||
|  | 	"fmt" | ||||||
|  |  | ||||||
| 	"github.com/docker/cli/cli/command" | 	"github.com/docker/cli/cli/command" | ||||||
| 	dConfig "github.com/docker/cli/cli/config" | 	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 { | func newContextStore(dir string, config contextStore.Config) contextStore.Store { | ||||||
| 	return contextStore.New(dir, config) | 	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" | ||||||
| 	"net/url" | 	"net/url" | ||||||
|  |  | ||||||
|  | 	ctxPkg "coopcloud.tech/abra/pkg/context" | ||||||
| 	sshPkg "coopcloud.tech/abra/pkg/ssh" | 	sshPkg "coopcloud.tech/abra/pkg/ssh" | ||||||
| 	"github.com/docker/cli/cli/connhelper" | 	"github.com/docker/cli/cli/connhelper" | ||||||
| 	"github.com/docker/cli/cli/connhelper/ssh" | 	"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") | 			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 { | 		if err := sshPkg.EnsureHostKey(ctxConnDetails.Host); err != nil { | ||||||
| 			return nil, err | 			return nil, err | ||||||
| 		} | 		} | ||||||
|  | |||||||
		Reference in New Issue
	
	Block a user