|
|
|
@ -0,0 +1,305 @@
|
|
|
|
|
package app
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"fmt"
|
|
|
|
|
"os"
|
|
|
|
|
"os/exec"
|
|
|
|
|
"strings"
|
|
|
|
|
|
|
|
|
|
"coopcloud.tech/abra/cli/internal"
|
|
|
|
|
appPkg "coopcloud.tech/abra/pkg/app"
|
|
|
|
|
"coopcloud.tech/abra/pkg/autocomplete"
|
|
|
|
|
"coopcloud.tech/abra/pkg/client"
|
|
|
|
|
containerPkg "coopcloud.tech/abra/pkg/container"
|
|
|
|
|
"coopcloud.tech/abra/pkg/log"
|
|
|
|
|
"coopcloud.tech/abra/pkg/secret"
|
|
|
|
|
"coopcloud.tech/abra/pkg/upstream/convert"
|
|
|
|
|
"coopcloud.tech/abra/pkg/upstream/stack"
|
|
|
|
|
"github.com/docker/docker/api/types"
|
|
|
|
|
containertypes "github.com/docker/docker/api/types/container"
|
|
|
|
|
"github.com/docker/docker/api/types/filters"
|
|
|
|
|
"github.com/docker/docker/api/types/mount"
|
|
|
|
|
"github.com/spf13/cobra"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
var AppMoveCommand = &cobra.Command{
|
|
|
|
|
Use: "move <domain> <server> [flags]",
|
|
|
|
|
Aliases: []string{"d"},
|
|
|
|
|
Short: "Moves an app to a different server",
|
|
|
|
|
Long: `Deploy an app.
|
|
|
|
|
|
|
|
|
|
This command supports chaos operations. Use "--chaos/-C" to deploy your recipe
|
|
|
|
|
checkout as-is. Recipe commit hashes are also supported as values for
|
|
|
|
|
"[version]". Please note, "upgrade"/"rollback" do not support chaos operations.`,
|
|
|
|
|
Example: ` # standard deployment
|
|
|
|
|
abra app deploy 1312.net
|
|
|
|
|
|
|
|
|
|
# chaos deployment
|
|
|
|
|
abra app deploy 1312.net --chaos
|
|
|
|
|
|
|
|
|
|
# deploy specific version
|
|
|
|
|
abra app deploy 1312.net 2.0.0+1.2.3
|
|
|
|
|
|
|
|
|
|
# deploy a specific git hash
|
|
|
|
|
abra app deploy 1312.net 886db76d`,
|
|
|
|
|
Args: cobra.RangeArgs(1, 2),
|
|
|
|
|
ValidArgsFunction: func(
|
|
|
|
|
cmd *cobra.Command,
|
|
|
|
|
args []string,
|
|
|
|
|
toComplete string,
|
|
|
|
|
) ([]string, cobra.ShellCompDirective) {
|
|
|
|
|
switch l := len(args); l {
|
|
|
|
|
case 0:
|
|
|
|
|
return autocomplete.AppNameComplete()
|
|
|
|
|
case 1:
|
|
|
|
|
return autocomplete.ServerNameComplete()
|
|
|
|
|
default:
|
|
|
|
|
return nil, cobra.ShellCompDirectiveDefault
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
|
|
|
app := internal.ValidateApp(args)
|
|
|
|
|
|
|
|
|
|
if err := app.Recipe.Ensure(internal.GetEnsureContext()); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cl, err := client.New(app.Server)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
filter := filters.NewArgs()
|
|
|
|
|
filter.Add("label", fmt.Sprintf("%s=%s", convert.LabelNamespace, app.StackName()))
|
|
|
|
|
services, err := cl.ServiceList(context.Background(), types.ServiceListOptions{Filters: filter})
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
secretsToStore := map[string]string{}
|
|
|
|
|
volumes := map[string]containertypes.MountPoint{}
|
|
|
|
|
|
|
|
|
|
composeFiles, err := app.Recipe.GetComposeFiles(app.Env)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
filtersSecret, err := app.Filters(false, false)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
secretList, err := cl.SecretList(context.Background(), types.SecretListOptions{Filters: filtersSecret})
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
secretConfigs, err := secret.ReadSecretsConfig(app.Path, composeFiles, app.StackName())
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
opts := stack.Deploy{Composefiles: composeFiles, Namespace: app.StackName()}
|
|
|
|
|
compose, err := appPkg.GetAppComposeConfig(app.Name, opts, app.Env)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
for _, s := range services {
|
|
|
|
|
log.Info("service", s.Spec.Name)
|
|
|
|
|
|
|
|
|
|
secretNames := map[string]string{}
|
|
|
|
|
for _, serviceCompose := range compose.Services {
|
|
|
|
|
if app.StackName()+"_"+serviceCompose.Name != s.Spec.Name {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, secret := range serviceCompose.Secrets {
|
|
|
|
|
for _, s := range secretList {
|
|
|
|
|
if s.Spec.Name == app.StackName()+"_"+secret.Source+"_"+secretConfigs[secret.Source].Version {
|
|
|
|
|
secretNames[secret.Source] = s.ID
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
f := filters.NewArgs()
|
|
|
|
|
f.Add("name", s.Spec.Name)
|
|
|
|
|
targetContainer, err := containerPkg.GetContainer(context.Background(), cl, f, true)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Error(err)
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
for _, m := range targetContainer.Mounts {
|
|
|
|
|
if m.Type == mount.TypeVolume {
|
|
|
|
|
volumes[m.Name] = m
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for secretName, secretID := range secretNames {
|
|
|
|
|
if _, ok := secretsToStore[secretName]; ok {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
log.Debugf("extracting secret %s", secretName)
|
|
|
|
|
|
|
|
|
|
out, err := exec.Command("ssh", app.Server, "-tt", fmt.Sprintf("sudo cat /var/lib/docker/containers/%s/mounts/secrets/%s", targetContainer.ID, secretID)).Output()
|
|
|
|
|
if err != nil {
|
|
|
|
|
fmt.Println(string(out))
|
|
|
|
|
fmt.Println(err)
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
secretsToStore[secretName] = string(out)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if internal.Dry {
|
|
|
|
|
fmt.Println(secretsToStore)
|
|
|
|
|
fmt.Println(volumes)
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
stack.WaitTimeout, err = appPkg.GetTimeoutFromLabel(compose, app.StackName())
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
rmOpts := stack.Remove{
|
|
|
|
|
Namespaces: []string{app.StackName()},
|
|
|
|
|
Detach: false,
|
|
|
|
|
}
|
|
|
|
|
if err := stack.RunRemove(context.Background(), cl, rmOpts); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
cl2, err := client.New(args[1])
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
for _, s := range secretList {
|
|
|
|
|
sname := strings.Split(strings.TrimPrefix(s.Spec.Name, app.StackName()+"_"), "_")
|
|
|
|
|
secretName := strings.Join(sname[:len(sname)-1], "_")
|
|
|
|
|
data := secretsToStore[secretName]
|
|
|
|
|
if err := client.StoreSecret(cl2, s.Spec.Name, data); err != nil {
|
|
|
|
|
log.Info(err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, volume := range volumes {
|
|
|
|
|
fileName := fmt.Sprintf("%s.tar.gz", volume.Name)
|
|
|
|
|
|
|
|
|
|
log.Infof("moving volume: %s", volume.Name)
|
|
|
|
|
log.Debug("creating %s", fileName)
|
|
|
|
|
cmd := exec.Command("ssh", app.Server, "-tt", fmt.Sprintf("sudo tar --same-owner -czhpf %s -C /var/lib/docker/volumes %s", fileName, volume.Name))
|
|
|
|
|
if out, err := cmd.CombinedOutput(); err != nil {
|
|
|
|
|
fmt.Println(string(out))
|
|
|
|
|
fmt.Println(err)
|
|
|
|
|
}
|
|
|
|
|
log.Debug("copying %s to local machine", fileName)
|
|
|
|
|
cmd = exec.Command("scp", fmt.Sprintf("%s:%s", app.Server, fileName), fileName)
|
|
|
|
|
if out, err := cmd.CombinedOutput(); err != nil {
|
|
|
|
|
fmt.Println(string(out))
|
|
|
|
|
fmt.Println(err)
|
|
|
|
|
}
|
|
|
|
|
log.Debug("copying %s to %s", fileName, args[1])
|
|
|
|
|
cmd = exec.Command("scp", fileName, fmt.Sprintf("%s:%s", args[1], fileName))
|
|
|
|
|
if out, err := cmd.CombinedOutput(); err != nil {
|
|
|
|
|
fmt.Println(string(out))
|
|
|
|
|
fmt.Println(err)
|
|
|
|
|
}
|
|
|
|
|
log.Debug("extracting %s on %s", fileName, args[1])
|
|
|
|
|
cmd = exec.Command("ssh", args[1], "-tt", fmt.Sprintf("sudo tar --same-owner -xzpf %s -C /var/lib/docker/volumes", fileName))
|
|
|
|
|
if out, err := cmd.CombinedOutput(); err != nil {
|
|
|
|
|
fmt.Println(string(out))
|
|
|
|
|
fmt.Println(err)
|
|
|
|
|
}
|
|
|
|
|
cmd = exec.Command("ssh", args[1], "-tt", fmt.Sprintf("sudo rm %s", fileName))
|
|
|
|
|
if out, err := cmd.CombinedOutput(); err != nil {
|
|
|
|
|
fmt.Println(string(out))
|
|
|
|
|
fmt.Println(err)
|
|
|
|
|
}
|
|
|
|
|
cmd = exec.Command("ssh", app.Server, "-tt", fmt.Sprintf("sudo rm %s", fileName))
|
|
|
|
|
if out, err := cmd.CombinedOutput(); err != nil {
|
|
|
|
|
fmt.Println(string(out))
|
|
|
|
|
fmt.Println(err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := copyFile(app.Path, strings.ReplaceAll(app.Path, app.Server, args[1])); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
if err := os.Remove(app.Path); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// AppDeployCommand.Run(cmd, args)
|
|
|
|
|
|
|
|
|
|
// app = internal.ValidateApp(args)
|
|
|
|
|
// cl, err := client.New(app.Server)
|
|
|
|
|
// if err != nil {
|
|
|
|
|
// return err
|
|
|
|
|
// }
|
|
|
|
|
//
|
|
|
|
|
// composeFiles, err := app.Recipe.GetComposeFiles(app.Env)
|
|
|
|
|
// if err != nil {
|
|
|
|
|
// log.Fatal(err)
|
|
|
|
|
// }
|
|
|
|
|
// stackName := app.StackName()
|
|
|
|
|
// deployOpts := stack.Deploy{
|
|
|
|
|
// Composefiles: composeFiles,
|
|
|
|
|
// Namespace: stackName,
|
|
|
|
|
// Prune: false,
|
|
|
|
|
// ResolveImage: stack.ResolveImageAlways,
|
|
|
|
|
// Detach: false,
|
|
|
|
|
// }
|
|
|
|
|
// compose, err := appPkg.GetAppComposeConfig(app.Name, deployOpts, app.Env)
|
|
|
|
|
// if err != nil {
|
|
|
|
|
// return err
|
|
|
|
|
// }
|
|
|
|
|
// serviceNames, err := appPkg.GetAppServiceNames(app.Name)
|
|
|
|
|
// if err != nil {
|
|
|
|
|
// log.Fatal(err)
|
|
|
|
|
// }
|
|
|
|
|
//
|
|
|
|
|
// f, err := app.Filters(true, false, serviceNames...)
|
|
|
|
|
// if err != nil {
|
|
|
|
|
// log.Fatal(err)
|
|
|
|
|
// }
|
|
|
|
|
// if err := stack.RunDeploy(
|
|
|
|
|
// cl,
|
|
|
|
|
// deployOpts,
|
|
|
|
|
// compose,
|
|
|
|
|
// app.Name,
|
|
|
|
|
// app.Server,
|
|
|
|
|
// internal.DontWaitConverge,
|
|
|
|
|
// f,
|
|
|
|
|
// ); err != nil {
|
|
|
|
|
// log.Fatal(err)
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func copyFile(src string, dst string) error {
|
|
|
|
|
// Read all content of src to data, may cause OOM for a large file.
|
|
|
|
|
data, err := os.ReadFile(src)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
// Write data to dst
|
|
|
|
|
err = os.WriteFile(dst, data, 0o644)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func init() {
|
|
|
|
|
AppMoveCommand.Flags().BoolVarP(
|
|
|
|
|
&internal.Dry,
|
|
|
|
|
"dry-run",
|
|
|
|
|
"r",
|
|
|
|
|
false,
|
|
|
|
|
"report changes that would be made",
|
|
|
|
|
)
|
|
|
|
|
}
|