fix: better context handling and less unexpected deleting #262

Merged
decentral1se merged 3 commits from dont-delete-shit-unexpectedly into main 2023-01-23 14:13:59 +00:00
10 changed files with 120 additions and 69 deletions

View File

@ -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))
} }

View File

@ -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,
} }

View File

@ -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)
} }

View File

@ -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)
} }

View File

@ -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)
} }
} }

View File

@ -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

View File

@ -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)
} }

View File

@ -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)

View File

@ -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)
}

View File

@ -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
} }