forked from toolshed/abra
Compare commits
3 Commits
cp-enhance
...
fix-secret
Author | SHA1 | Date | |
---|---|---|---|
964d4efca4 | |||
cb49cf06d1
|
|||
9affda8a70
|
364
cli/app/cp.go
364
cli/app/cp.go
@ -2,24 +2,19 @@ package app
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"os"
|
"os"
|
||||||
"path"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"coopcloud.tech/abra/cli/internal"
|
"coopcloud.tech/abra/cli/internal"
|
||||||
"coopcloud.tech/abra/pkg/autocomplete"
|
"coopcloud.tech/abra/pkg/autocomplete"
|
||||||
"coopcloud.tech/abra/pkg/client"
|
"coopcloud.tech/abra/pkg/client"
|
||||||
containerPkg "coopcloud.tech/abra/pkg/container"
|
"coopcloud.tech/abra/pkg/config"
|
||||||
|
"coopcloud.tech/abra/pkg/container"
|
||||||
"coopcloud.tech/abra/pkg/formatter"
|
"coopcloud.tech/abra/pkg/formatter"
|
||||||
"coopcloud.tech/abra/pkg/upstream/container"
|
|
||||||
"github.com/docker/cli/cli/command"
|
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
|
"github.com/docker/docker/api/types/filters"
|
||||||
dockerClient "github.com/docker/docker/client"
|
dockerClient "github.com/docker/docker/client"
|
||||||
"github.com/docker/docker/errdefs"
|
|
||||||
"github.com/docker/docker/pkg/archive"
|
"github.com/docker/docker/pkg/archive"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"github.com/urfave/cli"
|
"github.com/urfave/cli"
|
||||||
@ -54,14 +49,46 @@ And if you want to copy that file back to your current working directory locally
|
|||||||
dst := c.Args().Get(2)
|
dst := c.Args().Get(2)
|
||||||
if src == "" {
|
if src == "" {
|
||||||
logrus.Fatal("missing <src> argument")
|
logrus.Fatal("missing <src> argument")
|
||||||
}
|
} else if dst == "" {
|
||||||
if dst == "" {
|
|
||||||
logrus.Fatal("missing <dest> argument")
|
logrus.Fatal("missing <dest> argument")
|
||||||
}
|
}
|
||||||
|
|
||||||
srcPath, dstPath, service, toContainer, err := parseSrcAndDst(src, dst)
|
parsedSrc := strings.SplitN(src, ":", 2)
|
||||||
if err != nil {
|
parsedDst := strings.SplitN(dst, ":", 2)
|
||||||
logrus.Fatal(err)
|
errorMsg := "one of <src>/<dest> arguments must take $SERVICE:$PATH form"
|
||||||
|
if len(parsedSrc) == 2 && len(parsedDst) == 2 {
|
||||||
|
logrus.Fatal(errorMsg)
|
||||||
|
} else if len(parsedSrc) != 2 {
|
||||||
|
if len(parsedDst) != 2 {
|
||||||
|
logrus.Fatal(errorMsg)
|
||||||
|
}
|
||||||
|
} else if len(parsedDst) != 2 {
|
||||||
|
if len(parsedSrc) != 2 {
|
||||||
|
logrus.Fatal(errorMsg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var service string
|
||||||
|
var srcPath string
|
||||||
|
var dstPath string
|
||||||
|
isToContainer := false // <container:src> <dst>
|
||||||
|
if len(parsedSrc) == 2 {
|
||||||
|
service = parsedSrc[0]
|
||||||
|
srcPath = parsedSrc[1]
|
||||||
|
dstPath = dst
|
||||||
|
logrus.Debugf("assuming transfer is coming FROM the container")
|
||||||
|
} else if len(parsedDst) == 2 {
|
||||||
|
service = parsedDst[0]
|
||||||
|
dstPath = parsedDst[1]
|
||||||
|
srcPath = src
|
||||||
|
isToContainer = true // <src> <container:dst>
|
||||||
|
logrus.Debugf("assuming transfer is going TO the container")
|
||||||
|
}
|
||||||
|
|
||||||
|
if isToContainer {
|
||||||
|
if _, err := os.Stat(srcPath); os.IsNotExist(err) {
|
||||||
|
logrus.Fatalf("%s does not exist locally?", srcPath)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cl, err := client.New(app.Server)
|
cl, err := client.New(app.Server)
|
||||||
@ -69,18 +96,7 @@ And if you want to copy that file back to your current working directory locally
|
|||||||
logrus.Fatal(err)
|
logrus.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
container, err := containerPkg.GetContainerFromStackAndService(cl, app.StackName(), service)
|
if err := configureAndCp(c, cl, app, srcPath, dstPath, service, isToContainer); err != nil {
|
||||||
if err != nil {
|
|
||||||
logrus.Fatal(err)
|
|
||||||
}
|
|
||||||
logrus.Debugf("retrieved %s as target container on %s", formatter.ShortenID(container.ID), app.Server)
|
|
||||||
|
|
||||||
if toContainer {
|
|
||||||
err = copyToContainer(cl, container.ID, srcPath, dstPath)
|
|
||||||
} else {
|
|
||||||
err = copyFromContainer(cl, container.ID, srcPath, dstPath)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
logrus.Fatal(err)
|
logrus.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -88,292 +104,46 @@ And if you want to copy that file back to your current working directory locally
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
var errServiceMissing = errors.New("one of <src>/<dest> arguments must take $SERVICE:$PATH form")
|
func configureAndCp(
|
||||||
|
c *cli.Context,
|
||||||
|
cl *dockerClient.Client,
|
||||||
|
app config.App,
|
||||||
|
srcPath string,
|
||||||
|
dstPath string,
|
||||||
|
service string,
|
||||||
|
isToContainer bool) error {
|
||||||
|
filters := filters.NewArgs()
|
||||||
|
filters.Add("name", fmt.Sprintf("^%s_%s", app.StackName(), service))
|
||||||
|
|
||||||
// parseSrcAndDst parses src and dest string. One of src or dst must be of the form $SERVICE:$PATH
|
container, err := container.GetContainer(context.Background(), cl, filters, internal.NoInput)
|
||||||
func parseSrcAndDst(src, dst string) (srcPath string, dstPath string, service string, toContainer bool, err error) {
|
|
||||||
parsedSrc := strings.SplitN(src, ":", 2)
|
|
||||||
parsedDst := strings.SplitN(dst, ":", 2)
|
|
||||||
if len(parsedSrc)+len(parsedDst) != 3 {
|
|
||||||
return "", "", "", false, errServiceMissing
|
|
||||||
}
|
|
||||||
if len(parsedSrc) == 2 {
|
|
||||||
return parsedSrc[1], dst, parsedSrc[0], false, nil
|
|
||||||
}
|
|
||||||
if len(parsedDst) == 2 {
|
|
||||||
return src, parsedDst[1], parsedDst[0], true, nil
|
|
||||||
}
|
|
||||||
return "", "", "", false, errServiceMissing
|
|
||||||
}
|
|
||||||
|
|
||||||
// copyToContainer copies a file or directory from the local file system to the container.
|
|
||||||
// See the possible copy modes and their documentation.
|
|
||||||
func copyToContainer(cl *dockerClient.Client, containerID, srcPath, dstPath string) error {
|
|
||||||
srcStat, err := os.Stat(srcPath)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("local %s ", err)
|
logrus.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
dstStat, err := cl.ContainerStatPath(context.Background(), containerID, dstPath)
|
logrus.Debugf("retrieved %s as target container on %s", formatter.ShortenID(container.ID), app.Server)
|
||||||
dstExists := true
|
|
||||||
if err != nil {
|
|
||||||
if errdefs.IsNotFound(err) {
|
|
||||||
dstExists = false
|
|
||||||
} else {
|
|
||||||
return fmt.Errorf("remote path: %s", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mode, err := copyMode(srcPath, dstPath, srcStat.Mode(), dstStat.Mode, dstExists)
|
if isToContainer {
|
||||||
if err != nil {
|
toTarOpts := &archive.TarOptions{NoOverwriteDirNonDir: true, Compression: archive.Gzip}
|
||||||
return err
|
content, err := archive.TarWithOptions(srcPath, toTarOpts)
|
||||||
}
|
|
||||||
movePath := ""
|
|
||||||
switch mode {
|
|
||||||
case CopyModeDirToDir:
|
|
||||||
// Add the src directory to the destination path
|
|
||||||
_, srcDir := path.Split(srcPath)
|
|
||||||
dstPath = path.Join(dstPath, srcDir)
|
|
||||||
|
|
||||||
// Make sure the dst directory exits.
|
|
||||||
dcli, err := command.NewDockerCli()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
logrus.Fatal(err)
|
||||||
}
|
}
|
||||||
if err := container.RunExec(dcli, cl, containerID, &types.ExecConfig{
|
|
||||||
AttachStderr: true,
|
|
||||||
AttachStdin: true,
|
|
||||||
AttachStdout: true,
|
|
||||||
Cmd: []string{"mkdir", "-p", dstPath},
|
|
||||||
Detach: false,
|
|
||||||
Tty: true,
|
|
||||||
}); err != nil {
|
|
||||||
return fmt.Errorf("create remote directory: %s", err)
|
|
||||||
}
|
|
||||||
case CopyModeFileToFile:
|
|
||||||
// Remove the file component from the path, since docker can only copy
|
|
||||||
// to a directory.
|
|
||||||
dstPath, _ = path.Split(dstPath)
|
|
||||||
case CopyModeFileToFileRename:
|
|
||||||
// Copy the file to the temp directory and move it to its dstPath
|
|
||||||
// afterwards.
|
|
||||||
movePath = dstPath
|
|
||||||
dstPath = "/tmp"
|
|
||||||
}
|
|
||||||
|
|
||||||
toTarOpts := &archive.TarOptions{IncludeSourceDir: true, NoOverwriteDirNonDir: true, Compression: archive.Gzip}
|
copyOpts := types.CopyToContainerOptions{AllowOverwriteDirWithFile: false, CopyUIDGID: false}
|
||||||
content, err := archive.TarWithOptions(srcPath, toTarOpts)
|
if err := cl.CopyToContainer(context.Background(), container.ID, dstPath, content, copyOpts); err != nil {
|
||||||
if err != nil {
|
logrus.Fatal(err)
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
logrus.Debugf("copy %s from local to %s on container", srcPath, dstPath)
|
|
||||||
copyOpts := types.CopyToContainerOptions{AllowOverwriteDirWithFile: false, CopyUIDGID: false}
|
|
||||||
if err := cl.CopyToContainer(context.Background(), containerID, dstPath, content, copyOpts); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if movePath != "" {
|
|
||||||
_, srcFile := path.Split(srcPath)
|
|
||||||
dcli, err := command.NewDockerCli()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := container.RunExec(dcli, cl, containerID, &types.ExecConfig{
|
|
||||||
AttachStderr: true,
|
|
||||||
AttachStdin: true,
|
|
||||||
AttachStdout: true,
|
|
||||||
Cmd: []string{"mv", path.Join("/tmp", srcFile), movePath},
|
|
||||||
Detach: false,
|
|
||||||
Tty: true,
|
|
||||||
}); err != nil {
|
|
||||||
return fmt.Errorf("create remote directory: %s", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// copyFromContainer copies a file or directory from the given container to the local file system.
|
|
||||||
// See the possible copy modes and their documentation.
|
|
||||||
func copyFromContainer(cl *dockerClient.Client, containerID, srcPath, dstPath string) error {
|
|
||||||
srcStat, err := cl.ContainerStatPath(context.Background(), containerID, srcPath)
|
|
||||||
if err != nil {
|
|
||||||
if errdefs.IsNotFound(err) {
|
|
||||||
return fmt.Errorf("remote: %s does not exist", srcPath)
|
|
||||||
} else {
|
|
||||||
return fmt.Errorf("remote path: %s", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dstStat, err := os.Stat(dstPath)
|
|
||||||
dstExists := true
|
|
||||||
var dstMode os.FileMode
|
|
||||||
if err != nil {
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
dstExists = false
|
|
||||||
} else {
|
|
||||||
return fmt.Errorf("remote path: %s", err)
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
dstMode = dstStat.Mode()
|
content, _, err := cl.CopyFromContainer(context.Background(), container.ID, srcPath)
|
||||||
}
|
|
||||||
|
|
||||||
mode, err := copyMode(srcPath, dstPath, srcStat.Mode, dstMode, dstExists)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
moveDstDir := ""
|
|
||||||
moveDstFile := ""
|
|
||||||
switch mode {
|
|
||||||
case CopyModeFileToFile:
|
|
||||||
// Remove the file component from the path, since docker can only copy
|
|
||||||
// to a directory.
|
|
||||||
dstPath, _ = path.Split(dstPath)
|
|
||||||
case CopyModeFileToFileRename:
|
|
||||||
// Copy the file to the temp directory and move it to its dstPath
|
|
||||||
// afterwards.
|
|
||||||
moveDstFile = dstPath
|
|
||||||
dstPath = "/tmp"
|
|
||||||
case CopyModeFilesToDir:
|
|
||||||
// Copy the directory to the temp directory and move it to its
|
|
||||||
// dstPath afterwards.
|
|
||||||
moveDstDir = path.Join(dstPath, "/")
|
|
||||||
dstPath = "/tmp"
|
|
||||||
|
|
||||||
// Make sure the temp directory always gets removed
|
|
||||||
defer os.Remove(path.Join("/tmp"))
|
|
||||||
}
|
|
||||||
|
|
||||||
content, _, err := cl.CopyFromContainer(context.Background(), containerID, srcPath)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("copy: %s", err)
|
|
||||||
}
|
|
||||||
defer content.Close()
|
|
||||||
if err := archive.Untar(content, dstPath, &archive.TarOptions{
|
|
||||||
NoOverwriteDirNonDir: true,
|
|
||||||
Compression: archive.Gzip,
|
|
||||||
NoLchown: true,
|
|
||||||
}); err != nil {
|
|
||||||
return fmt.Errorf("untar: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if moveDstFile != "" {
|
|
||||||
_, srcFile := path.Split(strings.TrimSuffix(srcPath, "/"))
|
|
||||||
if err := moveFile(path.Join("/tmp", srcFile), moveDstFile); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if moveDstDir != "" {
|
|
||||||
_, srcDir := path.Split(strings.TrimSuffix(srcPath, "/"))
|
|
||||||
if err := moveDir(path.Join("/tmp", srcDir), moveDstDir); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
ErrCopyDirToFile = fmt.Errorf("can't copy dir to file")
|
|
||||||
ErrDstDirNotExist = fmt.Errorf("destination directory does not exist")
|
|
||||||
)
|
|
||||||
|
|
||||||
type CopyMode int
|
|
||||||
|
|
||||||
const (
|
|
||||||
// Copy a src file to a dest file. The src and dest file names are the same.
|
|
||||||
// <dir_src>/<file> + <dir_dst>/<file> -> <dir_dst>/<file>
|
|
||||||
CopyModeFileToFile = CopyMode(iota)
|
|
||||||
// Copy a src file to a dest file. The src and dest file names are not the same.
|
|
||||||
// <dir_src>/<file_src> + <dir_dst>/<file_dst> -> <dir_dst>/<file_dst>
|
|
||||||
CopyModeFileToFileRename
|
|
||||||
// Copy a src file to dest directory. The dest file gets created in the dest
|
|
||||||
// folder with the src filename.
|
|
||||||
// <dir_src>/<file> + <dir_dst> -> <dir_dst>/<file>
|
|
||||||
CopyModeFileToDir
|
|
||||||
// Copy a src directory to dest directory.
|
|
||||||
// <dir_src> + <dir_dst> -> <dir_dst>/<dir_src>
|
|
||||||
CopyModeDirToDir
|
|
||||||
// Copy all files in the src directory to the dest directory. This works recursively.
|
|
||||||
// <dir_src>/ + <dir_dst> -> <dir_dst>/<files_from_dir_src>
|
|
||||||
CopyModeFilesToDir
|
|
||||||
)
|
|
||||||
|
|
||||||
// copyMode takes a src and dest path and file mode to determine the copy mode.
|
|
||||||
// See the possible copy modes and their documentation.
|
|
||||||
func copyMode(srcPath, dstPath string, srcMode os.FileMode, dstMode os.FileMode, dstExists bool) (CopyMode, error) {
|
|
||||||
_, srcFile := path.Split(srcPath)
|
|
||||||
_, dstFile := path.Split(dstPath)
|
|
||||||
if srcMode.IsDir() {
|
|
||||||
if !dstExists {
|
|
||||||
return -1, ErrDstDirNotExist
|
|
||||||
}
|
|
||||||
if dstMode.IsDir() {
|
|
||||||
if strings.HasSuffix(srcPath, "/") {
|
|
||||||
return CopyModeFilesToDir, nil
|
|
||||||
}
|
|
||||||
return CopyModeDirToDir, nil
|
|
||||||
}
|
|
||||||
return -1, ErrCopyDirToFile
|
|
||||||
}
|
|
||||||
|
|
||||||
if dstMode.IsDir() {
|
|
||||||
return CopyModeFileToDir, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if srcFile != dstFile {
|
|
||||||
return CopyModeFileToFileRename, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return CopyModeFileToFile, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// moveDir moves all files from a source path to the destination path recursively.
|
|
||||||
func moveDir(sourcePath, destPath string) error {
|
|
||||||
return filepath.Walk(sourcePath, func(p string, info os.FileInfo, err error) error {
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
logrus.Fatal(err)
|
||||||
}
|
}
|
||||||
newPath := path.Join(destPath, strings.TrimPrefix(p, sourcePath))
|
defer content.Close()
|
||||||
if info.IsDir() {
|
fromTarOpts := &archive.TarOptions{NoOverwriteDirNonDir: true, Compression: archive.Gzip}
|
||||||
err := os.Mkdir(newPath, info.Mode())
|
if err := archive.Untar(content, dstPath, fromTarOpts); err != nil {
|
||||||
if err != nil {
|
logrus.Fatal(err)
|
||||||
if os.IsExist(err) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if info.Mode().IsRegular() {
|
|
||||||
return moveFile(p, newPath)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// moveFile moves a file from a source path to a destination path.
|
|
||||||
func moveFile(sourcePath, destPath string) error {
|
|
||||||
inputFile, err := os.Open(sourcePath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
outputFile, err := os.Create(destPath)
|
|
||||||
if err != nil {
|
|
||||||
inputFile.Close()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer outputFile.Close()
|
|
||||||
_, err = io.Copy(outputFile, inputFile)
|
|
||||||
inputFile.Close()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove file after succesfull copy.
|
|
||||||
err = os.Remove(sourcePath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -1,113 +0,0 @@
|
|||||||
package app
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestParse(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
src string
|
|
||||||
dst string
|
|
||||||
srcPath string
|
|
||||||
dstPath string
|
|
||||||
service string
|
|
||||||
toContainer bool
|
|
||||||
err error
|
|
||||||
}{
|
|
||||||
{src: "foo", dst: "bar", err: errServiceMissing},
|
|
||||||
{src: "app:foo", dst: "app:bar", err: errServiceMissing},
|
|
||||||
{src: "app:foo", dst: "bar", srcPath: "foo", dstPath: "bar", service: "app", toContainer: false},
|
|
||||||
{src: "foo", dst: "app:bar", srcPath: "foo", dstPath: "bar", service: "app", toContainer: true},
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, tc := range tests {
|
|
||||||
srcPath, dstPath, service, toContainer, err := parseSrcAndDst(tc.src, tc.dst)
|
|
||||||
if srcPath != tc.srcPath {
|
|
||||||
t.Errorf("[%d] srcPath: want (%s), got(%s)", i, tc.srcPath, srcPath)
|
|
||||||
}
|
|
||||||
if dstPath != tc.dstPath {
|
|
||||||
t.Errorf("[%d] dstPath: want (%s), got(%s)", i, tc.dstPath, dstPath)
|
|
||||||
}
|
|
||||||
if service != tc.service {
|
|
||||||
t.Errorf("[%d] service: want (%s), got(%s)", i, tc.service, service)
|
|
||||||
}
|
|
||||||
if toContainer != tc.toContainer {
|
|
||||||
t.Errorf("[%d] toConainer: want (%t), got(%t)", i, tc.toContainer, toContainer)
|
|
||||||
}
|
|
||||||
if err == nil && tc.err != nil && err.Error() != tc.err.Error() {
|
|
||||||
t.Errorf("[%d] err: want (%s), got(%s)", i, tc.err, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCopyMode(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
srcPath string
|
|
||||||
dstPath string
|
|
||||||
srcMode os.FileMode
|
|
||||||
dstMode os.FileMode
|
|
||||||
dstExists bool
|
|
||||||
mode CopyMode
|
|
||||||
err error
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
srcPath: "foo.txt",
|
|
||||||
dstPath: "foo.txt",
|
|
||||||
srcMode: os.ModePerm,
|
|
||||||
dstMode: os.ModePerm,
|
|
||||||
dstExists: true,
|
|
||||||
mode: CopyModeFileToFile,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
srcPath: "foo.txt",
|
|
||||||
dstPath: "bar.txt",
|
|
||||||
srcMode: os.ModePerm,
|
|
||||||
dstExists: true,
|
|
||||||
mode: CopyModeFileToFileRename,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
srcPath: "foo",
|
|
||||||
dstPath: "foo",
|
|
||||||
srcMode: os.ModeDir,
|
|
||||||
dstMode: os.ModeDir,
|
|
||||||
dstExists: true,
|
|
||||||
mode: CopyModeDirToDir,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
srcPath: "foo/",
|
|
||||||
dstPath: "foo",
|
|
||||||
srcMode: os.ModeDir,
|
|
||||||
dstMode: os.ModeDir,
|
|
||||||
dstExists: true,
|
|
||||||
mode: CopyModeFilesToDir,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
srcPath: "foo",
|
|
||||||
dstPath: "foo",
|
|
||||||
srcMode: os.ModeDir,
|
|
||||||
dstExists: false,
|
|
||||||
mode: -1,
|
|
||||||
err: ErrDstDirNotExist,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
srcPath: "foo",
|
|
||||||
dstPath: "foo",
|
|
||||||
srcMode: os.ModeDir,
|
|
||||||
dstMode: os.ModePerm,
|
|
||||||
dstExists: true,
|
|
||||||
mode: -1,
|
|
||||||
err: ErrCopyDirToFile,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, tc := range tests {
|
|
||||||
mode, err := copyMode(tc.srcPath, tc.dstPath, tc.srcMode, tc.dstMode, tc.dstExists)
|
|
||||||
if mode != tc.mode {
|
|
||||||
t.Errorf("[%d] mode: want (%d), got(%d)", i, tc.mode, mode)
|
|
||||||
}
|
|
||||||
if err != tc.err {
|
|
||||||
t.Errorf("[%d] err: want (%s), got(%s)", i, tc.err, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -108,7 +108,7 @@ var appNewCommand = cli.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
envSamplePath := path.Join(config.RECIPES_DIR, recipe.Name, ".env.sample")
|
envSamplePath := path.Join(config.RECIPES_DIR, recipe.Name, ".env.sample")
|
||||||
secretsConfig, err := secret.ReadSecretsConfig(envSamplePath, composeFiles, recipe.Name)
|
secretsConfig, err := secret.ReadSecretsConfig(envSamplePath, composeFiles, config.StackName(internal.Domain))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -168,14 +168,8 @@ var appNewCommand = cli.Command{
|
|||||||
type AppSecrets map[string]string
|
type AppSecrets map[string]string
|
||||||
|
|
||||||
// createSecrets creates all secrets for a new app.
|
// createSecrets creates all secrets for a new app.
|
||||||
func createSecrets(cl *dockerClient.Client, secretsConfig map[string]secret.SecretValue, sanitisedAppName string) (AppSecrets, error) {
|
func createSecrets(cl *dockerClient.Client, secretsConfig map[string]secret.Secret, sanitisedAppName string) (AppSecrets, error) {
|
||||||
// NOTE(d1): trim to match app.StackName() implementation
|
secrets, err := secret.GenerateSecrets(cl, secretsConfig, internal.NewAppServer)
|
||||||
if len(sanitisedAppName) > 45 {
|
|
||||||
logrus.Debugf("trimming %s to %s to avoid runtime limits", sanitisedAppName, sanitisedAppName[:45])
|
|
||||||
sanitisedAppName = sanitisedAppName[:45]
|
|
||||||
}
|
|
||||||
|
|
||||||
secrets, err := secret.GenerateSecrets(cl, secretsConfig, sanitisedAppName, internal.NewAppServer)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -217,7 +211,7 @@ func ensureDomainFlag(recipe recipe.Recipe, server string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// promptForSecrets asks if we should generate secrets for a new app.
|
// promptForSecrets asks if we should generate secrets for a new app.
|
||||||
func promptForSecrets(recipeName string, secretsConfig map[string]secret.SecretValue) error {
|
func promptForSecrets(recipeName string, secretsConfig map[string]secret.Secret) error {
|
||||||
if len(secretsConfig) == 0 {
|
if len(secretsConfig) == 0 {
|
||||||
logrus.Debugf("%s has no secrets to generate, skipping...", recipeName)
|
logrus.Debugf("%s has no secrets to generate, skipping...", recipeName)
|
||||||
return nil
|
return nil
|
||||||
|
@ -91,7 +91,7 @@ var appSecretGenerateCommand = cli.Command{
|
|||||||
logrus.Fatal(err)
|
logrus.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
secrets, err := secret.ReadSecretsConfig(app.Path, composeFiles, app.Recipe)
|
secrets, err := secret.ReadSecretsConfig(app.Path, composeFiles, app.StackName())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
logrus.Fatal(err)
|
||||||
}
|
}
|
||||||
@ -104,7 +104,7 @@ var appSecretGenerateCommand = cli.Command{
|
|||||||
logrus.Fatalf("%s doesn't exist in the env config?", secretName)
|
logrus.Fatalf("%s doesn't exist in the env config?", secretName)
|
||||||
}
|
}
|
||||||
s.Version = secretVersion
|
s.Version = secretVersion
|
||||||
secrets = map[string]secret.SecretValue{
|
secrets = map[string]secret.Secret{
|
||||||
secretName: s,
|
secretName: s,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -114,7 +114,7 @@ var appSecretGenerateCommand = cli.Command{
|
|||||||
logrus.Fatal(err)
|
logrus.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
secretVals, err := secret.GenerateSecrets(cl, secrets, app.StackName(), app.Server)
|
secretVals, err := secret.GenerateSecrets(cl, secrets, app.Server)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
logrus.Fatal(err)
|
||||||
}
|
}
|
||||||
@ -274,7 +274,7 @@ Example:
|
|||||||
logrus.Fatal(err)
|
logrus.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
secrets, err := secret.ReadSecretsConfig(app.Path, composeFiles, app.Recipe)
|
secrets, err := secret.ReadSecretsConfig(app.Path, composeFiles, app.StackName())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
logrus.Fatal(err)
|
||||||
}
|
}
|
||||||
|
2
go.mod
2
go.mod
@ -4,7 +4,7 @@ go 1.21
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
coopcloud.tech/tagcmp v0.0.0-20211103052201-885b22f77d52
|
coopcloud.tech/tagcmp v0.0.0-20211103052201-885b22f77d52
|
||||||
git.coopcloud.tech/coop-cloud/godotenv v1.5.2-0.20231130100106-7462d91acefd
|
git.coopcloud.tech/coop-cloud/godotenv v1.5.2-0.20231130100509-01bff8284355
|
||||||
github.com/AlecAivazis/survey/v2 v2.3.7
|
github.com/AlecAivazis/survey/v2 v2.3.7
|
||||||
github.com/Gurpartap/logrus-stack v0.0.0-20170710170904-89c00d8a28f4
|
github.com/Gurpartap/logrus-stack v0.0.0-20170710170904-89c00d8a28f4
|
||||||
github.com/docker/cli v24.0.7+incompatible
|
github.com/docker/cli v24.0.7+incompatible
|
||||||
|
5
go.sum
5
go.sum
@ -51,8 +51,8 @@ coopcloud.tech/tagcmp v0.0.0-20211103052201-885b22f77d52/go.mod h1:ESVm0wQKcbcFi
|
|||||||
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
|
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
|
||||||
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
||||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||||
git.coopcloud.tech/coop-cloud/godotenv v1.5.2-0.20231130100106-7462d91acefd h1:dctCkMhcsgIWMrkB1Br8S0RJF17eG+LKiqcXXVr3mdU=
|
git.coopcloud.tech/coop-cloud/godotenv v1.5.2-0.20231130100509-01bff8284355 h1:tCv2B4qoN6RMheKDnCzIafOkWS5BB1h7hwhmo+9bVeE=
|
||||||
git.coopcloud.tech/coop-cloud/godotenv v1.5.2-0.20231130100106-7462d91acefd/go.mod h1:Q8V1zbtPAlzYSr/Dvky3wS6x58IQAl3rot2me1oSO2Q=
|
git.coopcloud.tech/coop-cloud/godotenv v1.5.2-0.20231130100509-01bff8284355/go.mod h1:Q8V1zbtPAlzYSr/Dvky3wS6x58IQAl3rot2me1oSO2Q=
|
||||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230106234847-43070de90fa1 h1:EKPd1INOIyr5hWOWhvpmQpY6tKjeG0hT1s3AMC/9fic=
|
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230106234847-43070de90fa1 h1:EKPd1INOIyr5hWOWhvpmQpY6tKjeG0hT1s3AMC/9fic=
|
||||||
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230106234847-43070de90fa1/go.mod h1:VzwV+t+dZ9j/H867F1M2ziD+yLHtB46oM35FxxMJ4d0=
|
github.com/AdaLogics/go-fuzz-headers v0.0.0-20230106234847-43070de90fa1/go.mod h1:VzwV+t+dZ9j/H867F1M2ziD+yLHtB46oM35FxxMJ4d0=
|
||||||
github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ=
|
github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ=
|
||||||
@ -1315,7 +1315,6 @@ golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|||||||
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q=
|
golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q=
|
||||||
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||||
|
@ -50,23 +50,30 @@ type App struct {
|
|||||||
Path string
|
Path string
|
||||||
}
|
}
|
||||||
|
|
||||||
// StackName gets whatever the docker safe (uses the right delimiting
|
// See documentation of config.StackName
|
||||||
// character, e.g. "_") stack name is for the app. In general, you don't want
|
|
||||||
// to use this to show anything to end-users, you want use a.Name instead.
|
|
||||||
func (a App) StackName() string {
|
func (a App) StackName() string {
|
||||||
if _, exists := a.Env["STACK_NAME"]; exists {
|
if _, exists := a.Env["STACK_NAME"]; exists {
|
||||||
return a.Env["STACK_NAME"]
|
return a.Env["STACK_NAME"]
|
||||||
}
|
}
|
||||||
|
|
||||||
stackName := SanitiseAppName(a.Name)
|
stackName := StackName(a.Name)
|
||||||
|
|
||||||
|
a.Env["STACK_NAME"] = stackName
|
||||||
|
|
||||||
|
return stackName
|
||||||
|
}
|
||||||
|
|
||||||
|
// StackName gets whatever the docker safe (uses the right delimiting
|
||||||
|
// character, e.g. "_") stack name is for the app. In general, you don't want
|
||||||
|
// to use this to show anything to end-users, you want use a.Name instead.
|
||||||
|
func StackName(appName string) string {
|
||||||
|
stackName := SanitiseAppName(appName)
|
||||||
|
|
||||||
if len(stackName) > 45 {
|
if len(stackName) > 45 {
|
||||||
logrus.Debugf("trimming %s to %s to avoid runtime limits", stackName, stackName[:45])
|
logrus.Debugf("trimming %s to %s to avoid runtime limits", stackName, stackName[:45])
|
||||||
stackName = stackName[:45]
|
stackName = stackName[:45]
|
||||||
}
|
}
|
||||||
|
|
||||||
a.Env["STACK_NAME"] = stackName
|
|
||||||
|
|
||||||
return stackName
|
return stackName
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -68,15 +68,3 @@ func GetContainer(c context.Context, cl *client.Client, filters filters.Args, no
|
|||||||
|
|
||||||
return containers[0], nil
|
return containers[0], nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetContainerFromStackAndService retrieves the container for the given stack and service.
|
|
||||||
func GetContainerFromStackAndService(cl *client.Client, stack, service string) (types.Container, error) {
|
|
||||||
filters := filters.NewArgs()
|
|
||||||
filters.Add("name", fmt.Sprintf("^%s_%s", stack, service))
|
|
||||||
|
|
||||||
container, err := GetContainer(context.Background(), cl, filters, true)
|
|
||||||
if err != nil {
|
|
||||||
return types.Container{}, err
|
|
||||||
}
|
|
||||||
return container, nil
|
|
||||||
}
|
|
||||||
|
@ -21,11 +21,24 @@ import (
|
|||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SecretValue represents a parsed `SECRET_FOO=v1 # length=bar` env var config
|
// Secret represents a secret.
|
||||||
// secret definition.
|
type Secret struct {
|
||||||
type SecretValue struct {
|
// Version comes from the secret version environment variable.
|
||||||
|
// For example:
|
||||||
|
// SECRET_FOO=v1
|
||||||
Version string
|
Version string
|
||||||
Length int
|
// Length comes from the length modifier at the secret version environment
|
||||||
|
// variable. For Example:
|
||||||
|
// SECRET_FOO=v1 # length=12
|
||||||
|
Length int
|
||||||
|
// RemoteName is the name of the secret on the server. For example:
|
||||||
|
// name: ${STACK_NAME}_test_pass_two_${SECRET_TEST_PASS_TWO_VERSION}
|
||||||
|
// With the following:
|
||||||
|
// STACK_NAME=test_example_com
|
||||||
|
// SECRET_TEST_PASS_TWO_VERSION=v2
|
||||||
|
// Will have this remote name:
|
||||||
|
// test_example_com_test_pass_two_v2
|
||||||
|
RemoteName string
|
||||||
}
|
}
|
||||||
|
|
||||||
// GeneratePasswords generates passwords.
|
// GeneratePasswords generates passwords.
|
||||||
@ -67,11 +80,13 @@ func GeneratePassphrases(count uint) ([]string, error) {
|
|||||||
// and some times you don't (as the caller). We need to be able to handle the
|
// and some times you don't (as the caller). We need to be able to handle the
|
||||||
// "app new" case where we pass in the .env.sample and the "secret generate"
|
// "app new" case where we pass in the .env.sample and the "secret generate"
|
||||||
// case where the app is created.
|
// case where the app is created.
|
||||||
func ReadSecretsConfig(appEnvPath string, composeFiles []string, recipeName string) (map[string]SecretValue, error) {
|
func ReadSecretsConfig(appEnvPath string, composeFiles []string, stackName string) (map[string]Secret, error) {
|
||||||
appEnv, appModifiers, err := config.ReadEnvWithModifiers(appEnvPath)
|
appEnv, appModifiers, err := config.ReadEnvWithModifiers(appEnvPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
// Set the STACK_NAME to be able to generate the remote name correctly.
|
||||||
|
appEnv["STACK_NAME"] = stackName
|
||||||
|
|
||||||
opts := stack.Deploy{Composefiles: composeFiles}
|
opts := stack.Deploy{Composefiles: composeFiles}
|
||||||
config, err := loader.LoadComposefile(opts, appEnv)
|
config, err := loader.LoadComposefile(opts, appEnv)
|
||||||
@ -95,7 +110,7 @@ func ReadSecretsConfig(appEnvPath string, composeFiles []string, recipeName stri
|
|||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
secretValues := map[string]SecretValue{}
|
secretValues := map[string]Secret{}
|
||||||
for secretId, secretConfig := range config.Secrets {
|
for secretId, secretConfig := range config.Secrets {
|
||||||
if string(secretConfig.Name[len(secretConfig.Name)-1]) == "_" {
|
if string(secretConfig.Name[len(secretConfig.Name)-1]) == "_" {
|
||||||
return nil, fmt.Errorf("missing version for secret? (%s)", secretId)
|
return nil, fmt.Errorf("missing version for secret? (%s)", secretId)
|
||||||
@ -108,7 +123,7 @@ func ReadSecretsConfig(appEnvPath string, composeFiles []string, recipeName stri
|
|||||||
|
|
||||||
lastIdx := strings.LastIndex(secretConfig.Name, "_")
|
lastIdx := strings.LastIndex(secretConfig.Name, "_")
|
||||||
secretVersion := secretConfig.Name[lastIdx+1:]
|
secretVersion := secretConfig.Name[lastIdx+1:]
|
||||||
value := SecretValue{Version: secretVersion}
|
value := Secret{Version: secretVersion, RemoteName: secretConfig.Name}
|
||||||
|
|
||||||
// Check if the length modifier is set for this secret.
|
// Check if the length modifier is set for this secret.
|
||||||
for k, v := range appModifiers {
|
for k, v := range appModifiers {
|
||||||
@ -133,7 +148,7 @@ func ReadSecretsConfig(appEnvPath string, composeFiles []string, recipeName stri
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GenerateSecrets generates secrets locally and sends them to a remote server for storage.
|
// GenerateSecrets generates secrets locally and sends them to a remote server for storage.
|
||||||
func GenerateSecrets(cl *dockerClient.Client, secrets map[string]SecretValue, appName, server string) (map[string]string, error) {
|
func GenerateSecrets(cl *dockerClient.Client, secrets map[string]Secret, server string) (map[string]string, error) {
|
||||||
secretsGenerated := map[string]string{}
|
secretsGenerated := map[string]string{}
|
||||||
var mutex sync.Mutex
|
var mutex sync.Mutex
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
@ -141,11 +156,10 @@ func GenerateSecrets(cl *dockerClient.Client, secrets map[string]SecretValue, ap
|
|||||||
for n, v := range secrets {
|
for n, v := range secrets {
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
|
|
||||||
go func(secretName string, secret SecretValue) {
|
go func(secretName string, secret Secret) {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
|
|
||||||
secretRemoteName := fmt.Sprintf("%s_%s_%s", appName, secretName, secret.Version)
|
logrus.Debugf("attempting to generate and store %s on %s", secret.RemoteName, server)
|
||||||
logrus.Debugf("attempting to generate and store %s on %s", secretRemoteName, server)
|
|
||||||
|
|
||||||
if secret.Length > 0 {
|
if secret.Length > 0 {
|
||||||
passwords, err := GeneratePasswords(1, uint(secret.Length))
|
passwords, err := GeneratePasswords(1, uint(secret.Length))
|
||||||
@ -154,9 +168,9 @@ func GenerateSecrets(cl *dockerClient.Client, secrets map[string]SecretValue, ap
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := client.StoreSecret(cl, secretRemoteName, passwords[0], server); err != nil {
|
if err := client.StoreSecret(cl, secret.RemoteName, passwords[0], server); err != nil {
|
||||||
if strings.Contains(err.Error(), "AlreadyExists") {
|
if strings.Contains(err.Error(), "AlreadyExists") {
|
||||||
logrus.Warnf("%s already exists, moving on...", secretRemoteName)
|
logrus.Warnf("%s already exists, moving on...", secret.RemoteName)
|
||||||
ch <- nil
|
ch <- nil
|
||||||
} else {
|
} else {
|
||||||
ch <- err
|
ch <- err
|
||||||
@ -174,9 +188,9 @@ func GenerateSecrets(cl *dockerClient.Client, secrets map[string]SecretValue, ap
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := client.StoreSecret(cl, secretRemoteName, passphrases[0], server); err != nil {
|
if err := client.StoreSecret(cl, secret.RemoteName, passphrases[0], server); err != nil {
|
||||||
if strings.Contains(err.Error(), "AlreadyExists") {
|
if strings.Contains(err.Error(), "AlreadyExists") {
|
||||||
logrus.Warnf("%s already exists, moving on...", secretRemoteName)
|
logrus.Warnf("%s already exists, moving on...", secret.RemoteName)
|
||||||
ch <- nil
|
ch <- nil
|
||||||
} else {
|
} else {
|
||||||
ch <- err
|
ch <- err
|
||||||
@ -225,7 +239,7 @@ func PollSecretsStatus(cl *dockerClient.Client, app config.App) (secretStatuses,
|
|||||||
return secStats, err
|
return secStats, err
|
||||||
}
|
}
|
||||||
|
|
||||||
secretsConfig, err := ReadSecretsConfig(app.Path, composeFiles, app.Recipe)
|
secretsConfig, err := ReadSecretsConfig(app.Path, composeFiles, app.StackName())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return secStats, err
|
return secStats, err
|
||||||
}
|
}
|
||||||
|
@ -1,42 +1,30 @@
|
|||||||
package secret
|
package secret
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"path"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"coopcloud.tech/abra/pkg/config"
|
|
||||||
"coopcloud.tech/abra/pkg/recipe"
|
|
||||||
"coopcloud.tech/abra/pkg/upstream/stack"
|
|
||||||
loader "coopcloud.tech/abra/pkg/upstream/stack"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestReadSecretsConfig(t *testing.T) {
|
func TestReadSecretsConfig(t *testing.T) {
|
||||||
offline := true
|
composeFiles := []string{"./testdir/compose.yaml"}
|
||||||
recipe, err := recipe.Get("matrix-synapse", offline)
|
secretsFromConfig, err := ReadSecretsConfig("./testdir/.env.sample", composeFiles, "test_example_com")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
sampleEnv, err := recipe.SampleEnv()
|
// Simple secret
|
||||||
if err != nil {
|
assert.Equal(t, "test_example_com_test_pass_one_v2", secretsFromConfig["test_pass_one"].RemoteName)
|
||||||
t.Fatal(err)
|
assert.Equal(t, "v2", secretsFromConfig["test_pass_one"].Version)
|
||||||
}
|
assert.Equal(t, 0, secretsFromConfig["test_pass_one"].Length)
|
||||||
|
|
||||||
composeFiles := []string{path.Join(config.RECIPES_DIR, recipe.Name, "compose.yml")}
|
// Has a length modifier
|
||||||
envSamplePath := path.Join(config.RECIPES_DIR, recipe.Name, ".env.sample")
|
assert.Equal(t, "test_example_com_test_pass_two_v1", secretsFromConfig["test_pass_two"].RemoteName)
|
||||||
secretsFromConfig, err := ReadSecretsConfig(envSamplePath, composeFiles, recipe.Name)
|
assert.Equal(t, "v1", secretsFromConfig["test_pass_two"].Version)
|
||||||
if err != nil {
|
assert.Equal(t, 10, secretsFromConfig["test_pass_two"].Length)
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
opts := stack.Deploy{Composefiles: composeFiles}
|
// Secret name does not include the secret id
|
||||||
config, err := loader.LoadComposefile(opts, sampleEnv)
|
assert.Equal(t, "test_example_com_pass_three_v2", secretsFromConfig["test_pass_three"].RemoteName)
|
||||||
if err != nil {
|
assert.Equal(t, "v2", secretsFromConfig["test_pass_three"].Version)
|
||||||
t.Fatal(err)
|
assert.Equal(t, 0, secretsFromConfig["test_pass_three"].Length)
|
||||||
}
|
|
||||||
|
|
||||||
for secretId := range config.Secrets {
|
|
||||||
assert.Contains(t, secretsFromConfig, secretId)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
3
pkg/secret/testdir/.env.sample
Normal file
3
pkg/secret/testdir/.env.sample
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
SECRET_TEST_PASS_ONE_VERSION=v2
|
||||||
|
SECRET_TEST_PASS_TWO_VERSION=v1 # length=10
|
||||||
|
SECRET_TEST_PASS_THREE_VERSION=v2
|
21
pkg/secret/testdir/compose.yaml
Normal file
21
pkg/secret/testdir/compose.yaml
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
---
|
||||||
|
version: "3.8"
|
||||||
|
|
||||||
|
services:
|
||||||
|
app:
|
||||||
|
image: nginx:1.21.0
|
||||||
|
secrets:
|
||||||
|
- test_pass_one
|
||||||
|
- test_pass_two
|
||||||
|
- test_pass_three
|
||||||
|
|
||||||
|
secrets:
|
||||||
|
test_pass_one:
|
||||||
|
external: true
|
||||||
|
name: ${STACK_NAME}_test_pass_one_${SECRET_TEST_PASS_ONE_VERSION} # should be removed
|
||||||
|
test_pass_two:
|
||||||
|
external: true
|
||||||
|
name: ${STACK_NAME}_test_pass_two_${SECRET_TEST_PASS_TWO_VERSION}
|
||||||
|
test_pass_three:
|
||||||
|
external: true
|
||||||
|
name: ${STACK_NAME}_pass_three_${SECRET_TEST_PASS_THREE_VERSION} # secretId and name don't match
|
@ -5,11 +5,9 @@ setup_file(){
|
|||||||
_common_setup
|
_common_setup
|
||||||
_add_server
|
_add_server
|
||||||
_new_app
|
_new_app
|
||||||
_deploy_app
|
|
||||||
}
|
}
|
||||||
|
|
||||||
teardown_file(){
|
teardown_file(){
|
||||||
_undeploy_app
|
|
||||||
_rm_app
|
_rm_app
|
||||||
_rm_server
|
_rm_server
|
||||||
}
|
}
|
||||||
@ -19,29 +17,11 @@ setup(){
|
|||||||
_common_setup
|
_common_setup
|
||||||
}
|
}
|
||||||
|
|
||||||
_mkfile() {
|
teardown(){
|
||||||
run bash -c "echo $2 > $1"
|
# https://github.com/bats-core/bats-core/issues/383#issuecomment-738628888
|
||||||
assert_success
|
if [[ -z "${BATS_TEST_COMPLETED}" ]]; then
|
||||||
}
|
_undeploy_app
|
||||||
|
fi
|
||||||
_mkfile_remote() {
|
|
||||||
run $ABRA app run "$TEST_APP_DOMAIN" app "bash -c \"echo $2 > $1\""
|
|
||||||
assert_success
|
|
||||||
}
|
|
||||||
|
|
||||||
_mkdir() {
|
|
||||||
run bash -c "mkdir -p $1"
|
|
||||||
assert_success
|
|
||||||
}
|
|
||||||
|
|
||||||
_rm() {
|
|
||||||
run rm -rf "$1"
|
|
||||||
assert_success
|
|
||||||
}
|
|
||||||
|
|
||||||
_rm_remote() {
|
|
||||||
run "$ABRA" app run "$TEST_APP_DOMAIN" app rm -rf "$1"
|
|
||||||
assert_success
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "validate app argument" {
|
@test "validate app argument" {
|
||||||
@ -74,120 +54,68 @@ _rm_remote() {
|
|||||||
assert_output --partial 'arguments must take $SERVICE:$PATH form'
|
assert_output --partial 'arguments must take $SERVICE:$PATH form'
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "error if local file missing" {
|
@test "detect 'coming FROM' syntax" {
|
||||||
run $ABRA app cp "$TEST_APP_DOMAIN" thisfileshouldnotexist.txt app:/somewhere
|
run $ABRA app cp "$TEST_APP_DOMAIN" app:/myfile.txt . --debug
|
||||||
assert_failure
|
assert_failure
|
||||||
assert_output --partial 'local stat thisfileshouldnotexist.txt: no such file or directory'
|
assert_output --partial 'coming FROM the container'
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "detect 'going TO' syntax" {
|
||||||
|
run $ABRA app cp "$TEST_APP_DOMAIN" myfile.txt app:/somewhere --debug
|
||||||
|
assert_failure
|
||||||
|
assert_output --partial 'going TO the container'
|
||||||
|
}
|
||||||
|
|
||||||
|
@test "error if local file missing" {
|
||||||
|
run $ABRA app cp "$TEST_APP_DOMAIN" myfile.txt app:/somewhere
|
||||||
|
assert_failure
|
||||||
|
assert_output --partial 'myfile.txt does not exist locally?'
|
||||||
}
|
}
|
||||||
|
|
||||||
# bats test_tags=slow
|
# bats test_tags=slow
|
||||||
@test "error if service doesn't exist" {
|
@test "error if service doesn't exist" {
|
||||||
_mkfile "$BATS_TMPDIR/myfile.txt" "foo"
|
_deploy_app
|
||||||
|
|
||||||
run $ABRA app cp "$TEST_APP_DOMAIN" "$BATS_TMPDIR/myfile.txt" doesnt_exist:/ --debug
|
run bash -c "echo foo >> $BATS_TMPDIR/myfile.txt"
|
||||||
|
assert_success
|
||||||
|
|
||||||
|
run $ABRA app cp "$TEST_APP_DOMAIN" "$BATS_TMPDIR/myfile.txt" doesnt_exist:/
|
||||||
assert_failure
|
assert_failure
|
||||||
assert_output --partial 'no containers matching'
|
assert_output --partial 'no containers matching'
|
||||||
|
|
||||||
_rm "$BATS_TMPDIR/myfile.txt"
|
run rm -rf "$BATS_TMPDIR/myfile.txt"
|
||||||
|
assert_success
|
||||||
|
|
||||||
|
_undeploy_app
|
||||||
}
|
}
|
||||||
|
|
||||||
# bats test_tags=slow
|
# bats test_tags=slow
|
||||||
@test "copy local file to container directory" {
|
@test "copy to container" {
|
||||||
_mkfile "$BATS_TMPDIR/myfile.txt" "foo"
|
_deploy_app
|
||||||
|
|
||||||
|
run bash -c "echo foo >> $BATS_TMPDIR/myfile.txt"
|
||||||
|
assert_success
|
||||||
|
|
||||||
run $ABRA app cp "$TEST_APP_DOMAIN" "$BATS_TMPDIR/myfile.txt" app:/etc
|
run $ABRA app cp "$TEST_APP_DOMAIN" "$BATS_TMPDIR/myfile.txt" app:/etc
|
||||||
assert_success
|
assert_success
|
||||||
|
|
||||||
run $ABRA app run "$TEST_APP_DOMAIN" app cat /etc/myfile.txt
|
run rm -rf "$BATS_TMPDIR/myfile.txt"
|
||||||
assert_success
|
assert_success
|
||||||
assert_output --partial "foo"
|
|
||||||
|
|
||||||
_rm "$BATS_TMPDIR/myfile.txt"
|
_undeploy_app
|
||||||
_rm_remote "/etc/myfile.txt"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# bats test_tags=slow
|
# bats test_tags=slow
|
||||||
@test "copy local file to container file (and override on remote)" {
|
@test "copy from container" {
|
||||||
_mkfile "$BATS_TMPDIR/myfile.txt" "foo"
|
_deploy_app
|
||||||
|
|
||||||
# create
|
run bash -c "echo foo >> $BATS_TMPDIR/myfile.txt"
|
||||||
run $ABRA app cp "$TEST_APP_DOMAIN" "$BATS_TMPDIR/myfile.txt" app:/etc/myfile.txt
|
|
||||||
assert_success
|
assert_success
|
||||||
|
|
||||||
run $ABRA app run "$TEST_APP_DOMAIN" app cat /etc/myfile.txt
|
run $ABRA app cp "$TEST_APP_DOMAIN" "$BATS_TMPDIR/myfile.txt" app:/etc
|
||||||
assert_success
|
|
||||||
assert_output --partial "foo"
|
|
||||||
|
|
||||||
_mkfile "$BATS_TMPDIR/myfile.txt" "bar"
|
|
||||||
|
|
||||||
# override
|
|
||||||
run $ABRA app cp "$TEST_APP_DOMAIN" "$BATS_TMPDIR/myfile.txt" app:/etc/myfile.txt
|
|
||||||
assert_success
|
assert_success
|
||||||
|
|
||||||
run $ABRA app run "$TEST_APP_DOMAIN" app cat /etc/myfile.txt
|
run rm -rf "$BATS_TMPDIR/myfile.txt"
|
||||||
assert_success
|
|
||||||
assert_output --partial "bar"
|
|
||||||
|
|
||||||
_rm "$BATS_TMPDIR/myfile.txt"
|
|
||||||
_rm_remote "/etc/myfile.txt"
|
|
||||||
}
|
|
||||||
|
|
||||||
# bats test_tags=slow
|
|
||||||
@test "copy local file to container file (and rename)" {
|
|
||||||
_mkfile "$BATS_TMPDIR/myfile.txt" "foo"
|
|
||||||
|
|
||||||
# rename
|
|
||||||
run $ABRA app cp "$TEST_APP_DOMAIN" "$BATS_TMPDIR/myfile.txt" app:/etc/myfile2.txt
|
|
||||||
assert_success
|
|
||||||
|
|
||||||
run $ABRA app run "$TEST_APP_DOMAIN" app cat /etc/myfile2.txt
|
|
||||||
assert_success
|
|
||||||
assert_output --partial "foo"
|
|
||||||
|
|
||||||
_rm "$BATS_TMPDIR/myfile.txt"
|
|
||||||
_rm_remote "/etc/myfile2.txt"
|
|
||||||
}
|
|
||||||
|
|
||||||
# bats test_tags=slow
|
|
||||||
@test "copy local directory to container directory (and creates missing directory)" {
|
|
||||||
_mkdir "$BATS_TMPDIR/mydir"
|
|
||||||
_mkfile "$BATS_TMPDIR/mydir/myfile.txt" "foo"
|
|
||||||
|
|
||||||
run $ABRA app cp "$TEST_APP_DOMAIN" "$BATS_TMPDIR/mydir" app:/etc
|
|
||||||
assert_success
|
|
||||||
|
|
||||||
run $ABRA app run "$TEST_APP_DOMAIN" app ls /etc/mydir
|
|
||||||
assert_success
|
|
||||||
assert_output --partial "myfile.txt"
|
|
||||||
|
|
||||||
_rm "$BATS_TMPDIR/mydir"
|
|
||||||
_rm_remote "/etc/mydir"
|
|
||||||
}
|
|
||||||
|
|
||||||
# bats test_tags=slow
|
|
||||||
@test "copy local files to container directory" {
|
|
||||||
_mkdir "$BATS_TMPDIR/mydir"
|
|
||||||
_mkfile "$BATS_TMPDIR/mydir/myfile.txt" "foo"
|
|
||||||
_mkfile "$BATS_TMPDIR/mydir/myfile2.txt" "foo"
|
|
||||||
|
|
||||||
run $ABRA app cp "$TEST_APP_DOMAIN" "$BATS_TMPDIR/mydir/" app:/etc
|
|
||||||
assert_success
|
|
||||||
|
|
||||||
run $ABRA app run "$TEST_APP_DOMAIN" app ls /etc/myfile.txt
|
|
||||||
assert_success
|
|
||||||
assert_output --partial "myfile.txt"
|
|
||||||
|
|
||||||
run $ABRA app run "$TEST_APP_DOMAIN" app ls /etc/myfile2.txt
|
|
||||||
assert_success
|
|
||||||
assert_output --partial "myfile2.txt"
|
|
||||||
|
|
||||||
_rm "$BATS_TMPDIR/mydir"
|
|
||||||
_rm_remote "/etc/myfile*"
|
|
||||||
}
|
|
||||||
|
|
||||||
# bats test_tags=slow
|
|
||||||
@test "copy container file to local directory" {
|
|
||||||
run $ABRA app run "$TEST_APP_DOMAIN" app bash -c "echo foo > /etc/myfile.txt"
|
|
||||||
assert_success
|
assert_success
|
||||||
|
|
||||||
run $ABRA app cp "$TEST_APP_DOMAIN" app:/etc/myfile.txt "$BATS_TMPDIR"
|
run $ABRA app cp "$TEST_APP_DOMAIN" app:/etc/myfile.txt "$BATS_TMPDIR"
|
||||||
@ -195,76 +123,8 @@ _rm_remote() {
|
|||||||
assert_exists "$BATS_TMPDIR/myfile.txt"
|
assert_exists "$BATS_TMPDIR/myfile.txt"
|
||||||
assert bash -c "cat $BATS_TMPDIR/myfile.txt | grep -q foo"
|
assert bash -c "cat $BATS_TMPDIR/myfile.txt | grep -q foo"
|
||||||
|
|
||||||
_rm "$BATS_TMPDIR/myfile.txt"
|
run rm -rf "$BATS_TMPDIR/myfile.txt"
|
||||||
_rm_remote "/etc/myfile.txt"
|
assert_success
|
||||||
}
|
|
||||||
|
_undeploy_app
|
||||||
# bats test_tags=slow
|
|
||||||
@test "copy container file to local file" {
|
|
||||||
run $ABRA app run "$TEST_APP_DOMAIN" app bash -c "echo foo > /etc/myfile.txt"
|
|
||||||
assert_success
|
|
||||||
|
|
||||||
run $ABRA app cp "$TEST_APP_DOMAIN" app:/etc/myfile.txt "$BATS_TMPDIR/myfile.txt"
|
|
||||||
assert_success
|
|
||||||
assert_exists "$BATS_TMPDIR/myfile.txt"
|
|
||||||
assert bash -c "cat $BATS_TMPDIR/myfile.txt | grep -q foo"
|
|
||||||
|
|
||||||
_rm "$BATS_TMPDIR/myfile.txt"
|
|
||||||
_rm_remote "/etc/myfile.txt"
|
|
||||||
}
|
|
||||||
|
|
||||||
# bats test_tags=slow
|
|
||||||
@test "copy container file to local file and rename" {
|
|
||||||
run $ABRA app run "$TEST_APP_DOMAIN" app bash -c "echo foo > /etc/myfile.txt"
|
|
||||||
assert_success
|
|
||||||
|
|
||||||
run $ABRA app cp "$TEST_APP_DOMAIN" app:/etc/myfile.txt "$BATS_TMPDIR/myfile2.txt"
|
|
||||||
assert_success
|
|
||||||
assert_exists "$BATS_TMPDIR/myfile2.txt"
|
|
||||||
assert bash -c "cat $BATS_TMPDIR/myfile2.txt | grep -q foo"
|
|
||||||
|
|
||||||
_rm "$BATS_TMPDIR/myfile2.txt"
|
|
||||||
_rm_remote "/etc/myfile.txt"
|
|
||||||
}
|
|
||||||
|
|
||||||
# bats test_tags=slow
|
|
||||||
@test "copy container directory to local directory" {
|
|
||||||
run $ABRA app run "$TEST_APP_DOMAIN" app bash -c "echo foo > /etc/myfile.txt"
|
|
||||||
assert_success
|
|
||||||
|
|
||||||
run $ABRA app run "$TEST_APP_DOMAIN" app bash -c "echo bar > /etc/myfile2.txt"
|
|
||||||
assert_success
|
|
||||||
|
|
||||||
mkdir "$BATS_TMPDIR/mydir"
|
|
||||||
|
|
||||||
run $ABRA app cp "$TEST_APP_DOMAIN" app:/etc "$BATS_TMPDIR/mydir"
|
|
||||||
assert_success
|
|
||||||
assert_exists "$BATS_TMPDIR/mydir/etc/myfile.txt"
|
|
||||||
assert_success
|
|
||||||
assert_exists "$BATS_TMPDIR/mydir/etc/myfile2.txt"
|
|
||||||
|
|
||||||
_rm "$BATS_TMPDIR/mydir"
|
|
||||||
_rm_remote "/etc/myfile.txt"
|
|
||||||
_rm_remote "/etc/myfile2.txt"
|
|
||||||
}
|
|
||||||
|
|
||||||
# bats test_tags=slow
|
|
||||||
@test "copy container files to local directory" {
|
|
||||||
run $ABRA app run "$TEST_APP_DOMAIN" app bash -c "echo foo > /etc/myfile.txt"
|
|
||||||
assert_success
|
|
||||||
|
|
||||||
run $ABRA app run "$TEST_APP_DOMAIN" app bash -c "echo bar > /etc/myfile2.txt"
|
|
||||||
assert_success
|
|
||||||
|
|
||||||
mkdir "$BATS_TMPDIR/mydir"
|
|
||||||
|
|
||||||
run $ABRA app cp "$TEST_APP_DOMAIN" app:/etc/ "$BATS_TMPDIR/mydir"
|
|
||||||
assert_success
|
|
||||||
assert_exists "$BATS_TMPDIR/mydir/myfile.txt"
|
|
||||||
assert_success
|
|
||||||
assert_exists "$BATS_TMPDIR/mydir/myfile2.txt"
|
|
||||||
|
|
||||||
_rm "$BATS_TMPDIR/mydir"
|
|
||||||
_rm_remote "/etc/myfile.txt"
|
|
||||||
_rm_remote "/etc/myfile2.txt"
|
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user