forked from toolshed/abra
Compare commits
1 Commits
fix-secret
...
cp-enhance
Author | SHA1 | Date | |
---|---|---|---|
296b2e0312 |
362
cli/app/cp.go
362
cli/app/cp.go
@ -2,19 +2,24 @@ 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"
|
||||||
"coopcloud.tech/abra/pkg/config"
|
containerPkg "coopcloud.tech/abra/pkg/container"
|
||||||
"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"
|
||||||
@ -49,46 +54,14 @@ 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")
|
||||||
}
|
}
|
||||||
|
|
||||||
parsedSrc := strings.SplitN(src, ":", 2)
|
srcPath, dstPath, service, toContainer, err := parseSrcAndDst(src, dst)
|
||||||
parsedDst := strings.SplitN(dst, ":", 2)
|
if err != nil {
|
||||||
errorMsg := "one of <src>/<dest> arguments must take $SERVICE:$PATH form"
|
logrus.Fatal(err)
|
||||||
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)
|
||||||
@ -96,7 +69,18 @@ And if you want to copy that file back to your current working directory locally
|
|||||||
logrus.Fatal(err)
|
logrus.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := configureAndCp(c, cl, app, srcPath, dstPath, service, isToContainer); err != nil {
|
container, err := containerPkg.GetContainerFromStackAndService(cl, app.StackName(), service)
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -104,46 +88,292 @@ And if you want to copy that file back to your current working directory locally
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func configureAndCp(
|
var errServiceMissing = errors.New("one of <src>/<dest> arguments must take $SERVICE:$PATH form")
|
||||||
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))
|
|
||||||
|
|
||||||
container, err := container.GetContainer(context.Background(), cl, filters, internal.NoInput)
|
// parseSrcAndDst parses src and dest string. One of src or dst must be of the form $SERVICE:$PATH
|
||||||
|
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 {
|
||||||
logrus.Fatal(err)
|
return fmt.Errorf("local %s ", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
logrus.Debugf("retrieved %s as target container on %s", formatter.ShortenID(container.ID), app.Server)
|
dstStat, err := cl.ContainerStatPath(context.Background(), containerID, dstPath)
|
||||||
|
dstExists := true
|
||||||
|
if err != nil {
|
||||||
|
if errdefs.IsNotFound(err) {
|
||||||
|
dstExists = false
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("remote path: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if isToContainer {
|
mode, err := copyMode(srcPath, dstPath, srcStat.Mode(), dstStat.Mode, dstExists)
|
||||||
toTarOpts := &archive.TarOptions{NoOverwriteDirNonDir: true, Compression: archive.Gzip}
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
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 {
|
||||||
|
return 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}
|
||||||
content, err := archive.TarWithOptions(srcPath, toTarOpts)
|
content, err := archive.TarWithOptions(srcPath, toTarOpts)
|
||||||
if 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}
|
copyOpts := types.CopyToContainerOptions{AllowOverwriteDirWithFile: false, CopyUIDGID: false}
|
||||||
if err := cl.CopyToContainer(context.Background(), container.ID, dstPath, content, copyOpts); err != nil {
|
if err := cl.CopyToContainer(context.Background(), containerID, dstPath, content, copyOpts); err != nil {
|
||||||
logrus.Fatal(err)
|
return err
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
content, _, err := cl.CopyFromContainer(context.Background(), container.ID, srcPath)
|
if movePath != "" {
|
||||||
|
_, srcFile := path.Split(srcPath)
|
||||||
|
dcli, err := command.NewDockerCli()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatal(err)
|
return err
|
||||||
}
|
}
|
||||||
defer content.Close()
|
if err := container.RunExec(dcli, cl, containerID, &types.ExecConfig{
|
||||||
fromTarOpts := &archive.TarOptions{NoOverwriteDirNonDir: true, Compression: archive.Gzip}
|
AttachStderr: true,
|
||||||
if err := archive.Untar(content, dstPath, fromTarOpts); err != nil {
|
AttachStdin: true,
|
||||||
logrus.Fatal(err)
|
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
|
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 {
|
||||||
|
dstMode = dstStat.Mode()
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
newPath := path.Join(destPath, strings.TrimPrefix(p, sourcePath))
|
||||||
|
if info.IsDir() {
|
||||||
|
err := os.Mkdir(newPath, info.Mode())
|
||||||
|
if err != nil {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
113
cli/app/cp_test.go
Normal file
113
cli/app/cp_test.go
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
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, config.StackName(internal.Domain))
|
secretsConfig, err := secret.ReadSecretsConfig(envSamplePath, composeFiles, recipe.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -168,8 +168,14 @@ 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.Secret, sanitisedAppName string) (AppSecrets, error) {
|
func createSecrets(cl *dockerClient.Client, secretsConfig map[string]secret.SecretValue, sanitisedAppName string) (AppSecrets, error) {
|
||||||
secrets, err := secret.GenerateSecrets(cl, secretsConfig, internal.NewAppServer)
|
// NOTE(d1): trim to match app.StackName() implementation
|
||||||
|
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
|
||||||
}
|
}
|
||||||
@ -211,7 +217,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.Secret) error {
|
func promptForSecrets(recipeName string, secretsConfig map[string]secret.SecretValue) 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.StackName())
|
secrets, err := secret.ReadSecretsConfig(app.Path, composeFiles, app.Recipe)
|
||||||
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.Secret{
|
secrets = map[string]secret.SecretValue{
|
||||||
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.Server)
|
secretVals, err := secret.GenerateSecrets(cl, secrets, app.StackName(), 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.StackName())
|
secrets, err := secret.ReadSecretsConfig(app.Path, composeFiles, app.Recipe)
|
||||||
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.20231130100509-01bff8284355
|
git.coopcloud.tech/coop-cloud/godotenv v1.5.2-0.20231130100106-7462d91acefd
|
||||||
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.20231130100509-01bff8284355 h1:tCv2B4qoN6RMheKDnCzIafOkWS5BB1h7hwhmo+9bVeE=
|
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/go.mod h1:Q8V1zbtPAlzYSr/Dvky3wS6x58IQAl3rot2me1oSO2Q=
|
git.coopcloud.tech/coop-cloud/godotenv v1.5.2-0.20231130100106-7462d91acefd/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,6 +1315,7 @@ 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,30 +50,23 @@ type App struct {
|
|||||||
Path string
|
Path string
|
||||||
}
|
}
|
||||||
|
|
||||||
// See documentation of config.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 (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 := StackName(a.Name)
|
stackName := SanitiseAppName(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,3 +68,15 @@ 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,24 +21,11 @@ import (
|
|||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Secret represents a secret.
|
// SecretValue represents a parsed `SECRET_FOO=v1 # length=bar` env var config
|
||||||
type Secret struct {
|
// secret definition.
|
||||||
// Version comes from the secret version environment variable.
|
type SecretValue struct {
|
||||||
// For example:
|
|
||||||
// SECRET_FOO=v1
|
|
||||||
Version string
|
Version string
|
||||||
// Length comes from the length modifier at the secret version environment
|
|
||||||
// variable. For Example:
|
|
||||||
// SECRET_FOO=v1 # length=12
|
|
||||||
Length int
|
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.
|
||||||
@ -80,13 +67,11 @@ 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, stackName string) (map[string]Secret, error) {
|
func ReadSecretsConfig(appEnvPath string, composeFiles []string, recipeName string) (map[string]SecretValue, 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)
|
||||||
@ -110,7 +95,7 @@ func ReadSecretsConfig(appEnvPath string, composeFiles []string, stackName strin
|
|||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
secretValues := map[string]Secret{}
|
secretValues := map[string]SecretValue{}
|
||||||
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)
|
||||||
@ -123,7 +108,7 @@ func ReadSecretsConfig(appEnvPath string, composeFiles []string, stackName strin
|
|||||||
|
|
||||||
lastIdx := strings.LastIndex(secretConfig.Name, "_")
|
lastIdx := strings.LastIndex(secretConfig.Name, "_")
|
||||||
secretVersion := secretConfig.Name[lastIdx+1:]
|
secretVersion := secretConfig.Name[lastIdx+1:]
|
||||||
value := Secret{Version: secretVersion, RemoteName: secretConfig.Name}
|
value := SecretValue{Version: secretVersion}
|
||||||
|
|
||||||
// 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 {
|
||||||
@ -148,7 +133,7 @@ func ReadSecretsConfig(appEnvPath string, composeFiles []string, stackName strin
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 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]Secret, server string) (map[string]string, error) {
|
func GenerateSecrets(cl *dockerClient.Client, secrets map[string]SecretValue, appName, 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
|
||||||
@ -156,10 +141,11 @@ func GenerateSecrets(cl *dockerClient.Client, secrets map[string]Secret, server
|
|||||||
for n, v := range secrets {
|
for n, v := range secrets {
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
|
|
||||||
go func(secretName string, secret Secret) {
|
go func(secretName string, secret SecretValue) {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
|
|
||||||
logrus.Debugf("attempting to generate and store %s on %s", secret.RemoteName, server)
|
secretRemoteName := fmt.Sprintf("%s_%s_%s", appName, secretName, secret.Version)
|
||||||
|
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))
|
||||||
@ -168,9 +154,9 @@ func GenerateSecrets(cl *dockerClient.Client, secrets map[string]Secret, server
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := client.StoreSecret(cl, secret.RemoteName, passwords[0], server); err != nil {
|
if err := client.StoreSecret(cl, secretRemoteName, passwords[0], server); err != nil {
|
||||||
if strings.Contains(err.Error(), "AlreadyExists") {
|
if strings.Contains(err.Error(), "AlreadyExists") {
|
||||||
logrus.Warnf("%s already exists, moving on...", secret.RemoteName)
|
logrus.Warnf("%s already exists, moving on...", secretRemoteName)
|
||||||
ch <- nil
|
ch <- nil
|
||||||
} else {
|
} else {
|
||||||
ch <- err
|
ch <- err
|
||||||
@ -188,9 +174,9 @@ func GenerateSecrets(cl *dockerClient.Client, secrets map[string]Secret, server
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := client.StoreSecret(cl, secret.RemoteName, passphrases[0], server); err != nil {
|
if err := client.StoreSecret(cl, secretRemoteName, passphrases[0], server); err != nil {
|
||||||
if strings.Contains(err.Error(), "AlreadyExists") {
|
if strings.Contains(err.Error(), "AlreadyExists") {
|
||||||
logrus.Warnf("%s already exists, moving on...", secret.RemoteName)
|
logrus.Warnf("%s already exists, moving on...", secretRemoteName)
|
||||||
ch <- nil
|
ch <- nil
|
||||||
} else {
|
} else {
|
||||||
ch <- err
|
ch <- err
|
||||||
@ -239,7 +225,7 @@ func PollSecretsStatus(cl *dockerClient.Client, app config.App) (secretStatuses,
|
|||||||
return secStats, err
|
return secStats, err
|
||||||
}
|
}
|
||||||
|
|
||||||
secretsConfig, err := ReadSecretsConfig(app.Path, composeFiles, app.StackName())
|
secretsConfig, err := ReadSecretsConfig(app.Path, composeFiles, app.Recipe)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return secStats, err
|
return secStats, err
|
||||||
}
|
}
|
||||||
|
@ -1,30 +1,42 @@
|
|||||||
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) {
|
||||||
composeFiles := []string{"./testdir/compose.yaml"}
|
offline := true
|
||||||
secretsFromConfig, err := ReadSecretsConfig("./testdir/.env.sample", composeFiles, "test_example_com")
|
recipe, err := recipe.Get("matrix-synapse", offline)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Simple secret
|
sampleEnv, err := recipe.SampleEnv()
|
||||||
assert.Equal(t, "test_example_com_test_pass_one_v2", secretsFromConfig["test_pass_one"].RemoteName)
|
if err != nil {
|
||||||
assert.Equal(t, "v2", secretsFromConfig["test_pass_one"].Version)
|
t.Fatal(err)
|
||||||
assert.Equal(t, 0, secretsFromConfig["test_pass_one"].Length)
|
}
|
||||||
|
|
||||||
// Has a length modifier
|
composeFiles := []string{path.Join(config.RECIPES_DIR, recipe.Name, "compose.yml")}
|
||||||
assert.Equal(t, "test_example_com_test_pass_two_v1", secretsFromConfig["test_pass_two"].RemoteName)
|
envSamplePath := path.Join(config.RECIPES_DIR, recipe.Name, ".env.sample")
|
||||||
assert.Equal(t, "v1", secretsFromConfig["test_pass_two"].Version)
|
secretsFromConfig, err := ReadSecretsConfig(envSamplePath, composeFiles, recipe.Name)
|
||||||
assert.Equal(t, 10, secretsFromConfig["test_pass_two"].Length)
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
// Secret name does not include the secret id
|
opts := stack.Deploy{Composefiles: composeFiles}
|
||||||
assert.Equal(t, "test_example_com_pass_three_v2", secretsFromConfig["test_pass_three"].RemoteName)
|
config, err := loader.LoadComposefile(opts, sampleEnv)
|
||||||
assert.Equal(t, "v2", secretsFromConfig["test_pass_three"].Version)
|
if err != nil {
|
||||||
assert.Equal(t, 0, secretsFromConfig["test_pass_three"].Length)
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for secretId := range config.Secrets {
|
||||||
|
assert.Contains(t, secretsFromConfig, secretId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,3 +0,0 @@
|
|||||||
SECRET_TEST_PASS_ONE_VERSION=v2
|
|
||||||
SECRET_TEST_PASS_TWO_VERSION=v1 # length=10
|
|
||||||
SECRET_TEST_PASS_THREE_VERSION=v2
|
|
@ -1,21 +0,0 @@
|
|||||||
---
|
|
||||||
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,9 +5,11 @@ 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
|
||||||
}
|
}
|
||||||
@ -17,11 +19,29 @@ setup(){
|
|||||||
_common_setup
|
_common_setup
|
||||||
}
|
}
|
||||||
|
|
||||||
teardown(){
|
_mkfile() {
|
||||||
# https://github.com/bats-core/bats-core/issues/383#issuecomment-738628888
|
run bash -c "echo $2 > $1"
|
||||||
if [[ -z "${BATS_TEST_COMPLETED}" ]]; then
|
assert_success
|
||||||
_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" {
|
||||||
@ -54,68 +74,120 @@ teardown(){
|
|||||||
assert_output --partial 'arguments must take $SERVICE:$PATH form'
|
assert_output --partial 'arguments must take $SERVICE:$PATH form'
|
||||||
}
|
}
|
||||||
|
|
||||||
@test "detect 'coming FROM' syntax" {
|
|
||||||
run $ABRA app cp "$TEST_APP_DOMAIN" app:/myfile.txt . --debug
|
|
||||||
assert_failure
|
|
||||||
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" {
|
@test "error if local file missing" {
|
||||||
run $ABRA app cp "$TEST_APP_DOMAIN" myfile.txt app:/somewhere
|
run $ABRA app cp "$TEST_APP_DOMAIN" thisfileshouldnotexist.txt app:/somewhere
|
||||||
assert_failure
|
assert_failure
|
||||||
assert_output --partial 'myfile.txt does not exist locally?'
|
assert_output --partial 'local stat thisfileshouldnotexist.txt: no such file or directory'
|
||||||
}
|
}
|
||||||
|
|
||||||
# bats test_tags=slow
|
# bats test_tags=slow
|
||||||
@test "error if service doesn't exist" {
|
@test "error if service doesn't exist" {
|
||||||
_deploy_app
|
_mkfile "$BATS_TMPDIR/myfile.txt" "foo"
|
||||||
|
|
||||||
run bash -c "echo foo >> $BATS_TMPDIR/myfile.txt"
|
run $ABRA app cp "$TEST_APP_DOMAIN" "$BATS_TMPDIR/myfile.txt" doesnt_exist:/ --debug
|
||||||
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'
|
||||||
|
|
||||||
run rm -rf "$BATS_TMPDIR/myfile.txt"
|
_rm "$BATS_TMPDIR/myfile.txt"
|
||||||
assert_success
|
|
||||||
|
|
||||||
_undeploy_app
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# bats test_tags=slow
|
# bats test_tags=slow
|
||||||
@test "copy to container" {
|
@test "copy local file to container directory" {
|
||||||
_deploy_app
|
_mkfile "$BATS_TMPDIR/myfile.txt" "foo"
|
||||||
|
|
||||||
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 rm -rf "$BATS_TMPDIR/myfile.txt"
|
run $ABRA app run "$TEST_APP_DOMAIN" app cat /etc/myfile.txt
|
||||||
assert_success
|
assert_success
|
||||||
|
assert_output --partial "foo"
|
||||||
|
|
||||||
_undeploy_app
|
_rm "$BATS_TMPDIR/myfile.txt"
|
||||||
|
_rm_remote "/etc/myfile.txt"
|
||||||
}
|
}
|
||||||
|
|
||||||
# bats test_tags=slow
|
# bats test_tags=slow
|
||||||
@test "copy from container" {
|
@test "copy local file to container file (and override on remote)" {
|
||||||
_deploy_app
|
_mkfile "$BATS_TMPDIR/myfile.txt" "foo"
|
||||||
|
|
||||||
run bash -c "echo foo >> $BATS_TMPDIR/myfile.txt"
|
# create
|
||||||
|
run $ABRA app cp "$TEST_APP_DOMAIN" "$BATS_TMPDIR/myfile.txt" app:/etc/myfile.txt
|
||||||
assert_success
|
assert_success
|
||||||
|
|
||||||
run $ABRA app cp "$TEST_APP_DOMAIN" "$BATS_TMPDIR/myfile.txt" app:/etc
|
run $ABRA app run "$TEST_APP_DOMAIN" app cat /etc/myfile.txt
|
||||||
|
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 rm -rf "$BATS_TMPDIR/myfile.txt"
|
run $ABRA app run "$TEST_APP_DOMAIN" app cat /etc/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"
|
||||||
@ -123,8 +195,76 @@ teardown(){
|
|||||||
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"
|
||||||
|
|
||||||
run rm -rf "$BATS_TMPDIR/myfile.txt"
|
_rm "$BATS_TMPDIR/myfile.txt"
|
||||||
|
_rm_remote "/etc/myfile.txt"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 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
|
assert_success
|
||||||
|
|
||||||
_undeploy_app
|
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