Heavy docker-app loading refactoring
- introduces a `types` package, moving it from `internal/types` - introduces a `loader` package, with function to lead each type of app (folder, tarball, single-file) — only docker image is still in `internal/packager`. - introduces a small `compose` helper package - updates some `settings` functions - split `internal/render` into `render` package and `internal/inspect` Signed-off-by: Vincent Demeester <vincent@sbr.pm>
This commit is contained in:
Родитель
ae5fefef1c
Коммит
7a79d880b3
|
@ -3,8 +3,8 @@ package main
|
||||||
import (
|
import (
|
||||||
"github.com/docker/app/internal"
|
"github.com/docker/app/internal"
|
||||||
"github.com/docker/app/internal/packager"
|
"github.com/docker/app/internal/packager"
|
||||||
"github.com/docker/app/internal/render"
|
"github.com/docker/app/render"
|
||||||
"github.com/docker/app/internal/types"
|
"github.com/docker/app/types"
|
||||||
"github.com/docker/cli/cli"
|
"github.com/docker/cli/cli"
|
||||||
"github.com/docker/cli/cli/command"
|
"github.com/docker/cli/cli/command"
|
||||||
"github.com/docker/cli/cli/command/stack"
|
"github.com/docker/cli/cli/command/stack"
|
||||||
|
@ -71,7 +71,7 @@ func runDeploy(dockerCli command.Cli, flags *pflag.FlagSet, appname string, opts
|
||||||
}
|
}
|
||||||
stackName := opts.deployStackName
|
stackName := opts.deployStackName
|
||||||
if stackName == "" {
|
if stackName == "" {
|
||||||
stackName = internal.AppNameFromDir(app.Path)
|
stackName = internal.AppNameFromDir(app.Name)
|
||||||
}
|
}
|
||||||
return stack.RunDeploy(dockerCli, flags, rendered, deployOrchestrator, options.Deploy{
|
return stack.RunDeploy(dockerCli, flags, rendered, deployOrchestrator, options.Deploy{
|
||||||
Namespace: stackName,
|
Namespace: stackName,
|
||||||
|
|
|
@ -6,7 +6,7 @@ import (
|
||||||
"github.com/docker/app/internal"
|
"github.com/docker/app/internal"
|
||||||
"github.com/docker/app/internal/helm"
|
"github.com/docker/app/internal/helm"
|
||||||
"github.com/docker/app/internal/packager"
|
"github.com/docker/app/internal/packager"
|
||||||
"github.com/docker/app/internal/types"
|
"github.com/docker/app/types"
|
||||||
"github.com/docker/cli/cli"
|
"github.com/docker/cli/cli"
|
||||||
cliopts "github.com/docker/cli/opts"
|
cliopts "github.com/docker/cli/opts"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|
|
@ -6,8 +6,8 @@ import (
|
||||||
"github.com/docker/app/internal"
|
"github.com/docker/app/internal"
|
||||||
"github.com/docker/app/internal/image"
|
"github.com/docker/app/internal/image"
|
||||||
"github.com/docker/app/internal/packager"
|
"github.com/docker/app/internal/packager"
|
||||||
"github.com/docker/app/internal/render"
|
"github.com/docker/app/render"
|
||||||
"github.com/docker/app/internal/types"
|
"github.com/docker/app/types"
|
||||||
cliopts "github.com/docker/cli/opts"
|
cliopts "github.com/docker/cli/opts"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
@ -41,7 +41,7 @@ subdirectory.`,
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := image.Add(app.Path, args[1:], config); err != nil {
|
if err := image.Add(app.Name, args[1:], config); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// check if source was a tarball
|
// check if source was a tarball
|
||||||
|
@ -60,7 +60,7 @@ subdirectory.`,
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// source was a tarball, rebuild it
|
// source was a tarball, rebuild it
|
||||||
return packager.Pack(app.Path, target)
|
return packager.Pack(app.Name, target)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
|
|
|
@ -17,7 +17,7 @@ func imageLoadCmd() *cobra.Command {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer app.Cleanup()
|
defer app.Cleanup()
|
||||||
return image.Load(app.Path, args[1:])
|
return image.Load(app.Name, args[1:])
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/docker/app/internal/inspect"
|
||||||
"github.com/docker/app/internal/packager"
|
"github.com/docker/app/internal/packager"
|
||||||
"github.com/docker/app/internal/render"
|
|
||||||
"github.com/docker/cli/cli"
|
"github.com/docker/cli/cli"
|
||||||
"github.com/docker/cli/cli/command"
|
"github.com/docker/cli/cli/command"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
@ -20,7 +20,7 @@ func inspectCmd(dockerCli command.Cli) *cobra.Command {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer app.Cleanup()
|
defer app.Cleanup()
|
||||||
return render.Inspect(dockerCli.Out(), app.Path)
|
return inspect.Inspect(dockerCli.Out(), app)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -57,9 +57,9 @@ func mergeCmd(dockerCli command.Cli) *cobra.Command {
|
||||||
return errors.Wrap(err, "error scanning application directory")
|
return errors.Wrap(err, "error scanning application directory")
|
||||||
}
|
}
|
||||||
if len(extra) != 0 {
|
if len(extra) != 0 {
|
||||||
return fmt.Errorf("refusing to overwrite %s: extra files would be deleted: %s", extractedApp.OriginalPath, strings.Join(extra, ","))
|
return fmt.Errorf("refusing to overwrite %s: extra files would be deleted: %s", extractedApp.Path, strings.Join(extra, ","))
|
||||||
}
|
}
|
||||||
mergeOutputFile = extractedApp.OriginalPath + ".tmp"
|
mergeOutputFile = extractedApp.Path + ".tmp"
|
||||||
}
|
}
|
||||||
var target io.Writer
|
var target io.Writer
|
||||||
if mergeOutputFile == "-" {
|
if mergeOutputFile == "-" {
|
||||||
|
@ -70,7 +70,7 @@ func mergeCmd(dockerCli command.Cli) *cobra.Command {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err := packager.Merge(extractedApp.Path, target); err != nil {
|
if err := packager.Merge(extractedApp, target); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if mergeOutputFile != "-" {
|
if mergeOutputFile != "-" {
|
||||||
|
@ -78,10 +78,10 @@ func mergeCmd(dockerCli command.Cli) *cobra.Command {
|
||||||
target.(io.WriteCloser).Close()
|
target.(io.WriteCloser).Close()
|
||||||
}
|
}
|
||||||
if inPlace {
|
if inPlace {
|
||||||
if err := os.RemoveAll(extractedApp.OriginalPath); err != nil {
|
if err := os.RemoveAll(extractedApp.Path); err != nil {
|
||||||
return errors.Wrap(err, "failed to erase previous application")
|
return errors.Wrap(err, "failed to erase previous application")
|
||||||
}
|
}
|
||||||
if err := os.Rename(mergeOutputFile, extractedApp.OriginalPath); err != nil {
|
if err := os.Rename(mergeOutputFile, extractedApp.Path); err != nil {
|
||||||
return errors.Wrap(err, "failed to rename new application")
|
return errors.Wrap(err, "failed to rename new application")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,12 @@ func pushCmd() *cobra.Command {
|
||||||
Short: "Push the application to a registry",
|
Short: "Push the application to a registry",
|
||||||
Args: cli.RequiresMaxArgs(1),
|
Args: cli.RequiresMaxArgs(1),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
return packager.Push(firstOrEmpty(args), opts.namespace, opts.tag)
|
app, err := packager.Extract(firstOrEmpty(args))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer app.Cleanup()
|
||||||
|
return packager.Push(app, opts.namespace, opts.tag)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
cmd.Flags().StringVar(&opts.namespace, "namespace", "", "namespace to use (default: namespace in metadata)")
|
cmd.Flags().StringVar(&opts.namespace, "namespace", "", "namespace to use (default: namespace in metadata)")
|
||||||
|
|
|
@ -6,8 +6,8 @@ import (
|
||||||
|
|
||||||
"github.com/docker/app/internal"
|
"github.com/docker/app/internal"
|
||||||
"github.com/docker/app/internal/packager"
|
"github.com/docker/app/internal/packager"
|
||||||
"github.com/docker/app/internal/render"
|
"github.com/docker/app/render"
|
||||||
"github.com/docker/app/internal/types"
|
"github.com/docker/app/types"
|
||||||
"github.com/docker/cli/cli"
|
"github.com/docker/cli/cli"
|
||||||
"github.com/docker/cli/cli/command"
|
"github.com/docker/cli/cli/command"
|
||||||
cliopts "github.com/docker/cli/opts"
|
cliopts "github.com/docker/cli/opts"
|
||||||
|
|
|
@ -21,7 +21,12 @@ func saveCmd(dockerCli command.Cli) *cobra.Command {
|
||||||
Short: "Save the application as an image to the docker daemon(in preparation for push)",
|
Short: "Save the application as an image to the docker daemon(in preparation for push)",
|
||||||
Args: cli.RequiresMaxArgs(1),
|
Args: cli.RequiresMaxArgs(1),
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
imageName, err := packager.Save(firstOrEmpty(args), opts.namespace, opts.tag)
|
app, err := packager.Extract(firstOrEmpty(args))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer app.Cleanup()
|
||||||
|
imageName, err := packager.Save(app, opts.namespace, opts.tag)
|
||||||
if imageName != "" && err == nil {
|
if imageName != "" && err == nil {
|
||||||
fmt.Fprintf(dockerCli.Out(), "Saved application as image: %s\n", imageName)
|
fmt.Fprintf(dockerCli.Out(), "Saved application as image: %s\n", imageName)
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,16 +24,16 @@ func splitCmd() *cobra.Command {
|
||||||
defer extractedApp.Cleanup()
|
defer extractedApp.Cleanup()
|
||||||
inPlace := splitOutputDir == ""
|
inPlace := splitOutputDir == ""
|
||||||
if inPlace {
|
if inPlace {
|
||||||
splitOutputDir = extractedApp.OriginalPath + ".tmp"
|
splitOutputDir = extractedApp.Path + ".tmp"
|
||||||
}
|
}
|
||||||
if err := packager.Split(extractedApp.Path, splitOutputDir); err != nil {
|
if err := packager.Split(extractedApp, splitOutputDir); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if inPlace {
|
if inPlace {
|
||||||
if err := os.RemoveAll(extractedApp.OriginalPath); err != nil {
|
if err := os.RemoveAll(extractedApp.Path); err != nil {
|
||||||
return errors.Wrap(err, "failed to erase previous application directory")
|
return errors.Wrap(err, "failed to erase previous application directory")
|
||||||
}
|
}
|
||||||
if err := os.Rename(splitOutputDir, extractedApp.OriginalPath); err != nil {
|
if err := os.Rename(splitOutputDir, extractedApp.Path); err != nil {
|
||||||
return errors.Wrap(err, "failed to rename new application directory")
|
return errors.Wrap(err, "failed to rename new application directory")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,8 +2,8 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/docker/app/internal/packager"
|
"github.com/docker/app/internal/packager"
|
||||||
"github.com/docker/app/internal/types"
|
|
||||||
"github.com/docker/app/internal/validator"
|
"github.com/docker/app/internal/validator"
|
||||||
|
"github.com/docker/app/types"
|
||||||
"github.com/docker/cli/cli"
|
"github.com/docker/cli/cli"
|
||||||
cliopts "github.com/docker/cli/opts"
|
cliopts "github.com/docker/cli/opts"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
package e2e
|
package e2e
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/rand"
|
|
||||||
"encoding/hex"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
|
@ -86,15 +84,6 @@ func TestRenderBinary(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func randomName(prefix string) string {
|
|
||||||
b := make([]byte, 16)
|
|
||||||
_, err := rand.Read(b)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return prefix + hex.EncodeToString(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestInitBinary(t *testing.T) {
|
func TestInitBinary(t *testing.T) {
|
||||||
getDockerAppBinary(t)
|
getDockerAppBinary(t)
|
||||||
composeData := `version: "3.2"
|
composeData := `version: "3.2"
|
||||||
|
@ -119,11 +108,11 @@ maintainers:
|
||||||
email: joe@joe.com
|
email: joe@joe.com
|
||||||
`
|
`
|
||||||
envData := "# some comment\nNGINX_VERSION=latest"
|
envData := "# some comment\nNGINX_VERSION=latest"
|
||||||
inputDir := randomName("app_input_")
|
dir := fs.NewDir(t, "app_input",
|
||||||
os.Mkdir(inputDir, 0755)
|
fs.WithFile(internal.ComposeFileName, composeData),
|
||||||
ioutil.WriteFile(filepath.Join(inputDir, internal.ComposeFileName), []byte(composeData), 0644)
|
fs.WithFile(".env", envData),
|
||||||
ioutil.WriteFile(filepath.Join(inputDir, ".env"), []byte(envData), 0644)
|
)
|
||||||
defer os.RemoveAll(inputDir)
|
defer dir.Remove()
|
||||||
|
|
||||||
testAppName := "app-test"
|
testAppName := "app-test"
|
||||||
dirName := internal.DirNameFromAppName(testAppName)
|
dirName := internal.DirNameFromAppName(testAppName)
|
||||||
|
@ -133,7 +122,7 @@ maintainers:
|
||||||
"init",
|
"init",
|
||||||
testAppName,
|
testAppName,
|
||||||
"-c",
|
"-c",
|
||||||
filepath.Join(inputDir, internal.ComposeFileName),
|
dir.Join(internal.ComposeFileName),
|
||||||
"-d",
|
"-d",
|
||||||
"my cool app",
|
"my cool app",
|
||||||
"-m", "bob",
|
"-m", "bob",
|
||||||
|
@ -157,7 +146,7 @@ maintainers:
|
||||||
"init",
|
"init",
|
||||||
"tac",
|
"tac",
|
||||||
"-c",
|
"-c",
|
||||||
filepath.Join(inputDir, internal.ComposeFileName),
|
dir.Join(internal.ComposeFileName),
|
||||||
"-d",
|
"-d",
|
||||||
"my cool app",
|
"my cool app",
|
||||||
"-m", "bob",
|
"-m", "bob",
|
||||||
|
@ -166,7 +155,8 @@ maintainers:
|
||||||
}
|
}
|
||||||
assertCommand(t, dockerApp, args...)
|
assertCommand(t, dockerApp, args...)
|
||||||
defer os.Remove("tac.dockerapp")
|
defer os.Remove("tac.dockerapp")
|
||||||
appData, _ := ioutil.ReadFile("tac.dockerapp")
|
appData, err := ioutil.ReadFile("tac.dockerapp")
|
||||||
|
assert.NilError(t, err)
|
||||||
golden.Assert(t, string(appData), "init-singlefile.dockerapp")
|
golden.Assert(t, string(appData), "init-singlefile.dockerapp")
|
||||||
// Check various commands work on single-file app package
|
// Check various commands work on single-file app package
|
||||||
assertCommand(t, dockerApp, "inspect", "tac")
|
assertCommand(t, dockerApp, "inspect", "tac")
|
||||||
|
@ -179,11 +169,11 @@ func TestDetectAppBinary(t *testing.T) {
|
||||||
assertCommand(t, dockerApp, "inspect")
|
assertCommand(t, dockerApp, "inspect")
|
||||||
cwd, err := os.Getwd()
|
cwd, err := os.Getwd()
|
||||||
assert.NilError(t, err)
|
assert.NilError(t, err)
|
||||||
|
assert.NilError(t, os.Chdir("helm.dockerapp"))
|
||||||
defer os.Chdir(cwd)
|
defer os.Chdir(cwd)
|
||||||
os.Chdir("helm.dockerapp")
|
|
||||||
assertCommand(t, dockerApp, "inspect")
|
assertCommand(t, dockerApp, "inspect")
|
||||||
assertCommand(t, dockerApp, "inspect", ".")
|
assertCommand(t, dockerApp, "inspect", ".")
|
||||||
os.Chdir(filepath.Join(cwd, "render"))
|
assert.NilError(t, os.Chdir(filepath.Join(cwd, "render")))
|
||||||
assertCommandFailureOutput(t, "inspect-multiple-apps.golden", dockerApp, "inspect")
|
assertCommandFailureOutput(t, "inspect-multiple-apps.golden", dockerApp, "inspect")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -206,17 +196,17 @@ func TestPackBinary(t *testing.T) {
|
||||||
assert.Assert(t, strings.Contains(result.Stdout(), "nginx"))
|
assert.Assert(t, strings.Contains(result.Stdout(), "nginx"))
|
||||||
cwd, err := os.Getwd()
|
cwd, err := os.Getwd()
|
||||||
assert.NilError(t, err)
|
assert.NilError(t, err)
|
||||||
os.Chdir(tempDir)
|
assert.NilError(t, os.Chdir(tempDir))
|
||||||
|
defer os.Chdir(cwd)
|
||||||
result = icmd.RunCommand(dockerApp, "helm", "test")
|
result = icmd.RunCommand(dockerApp, "helm", "test")
|
||||||
result.Assert(t, icmd.Success)
|
result.Assert(t, icmd.Success)
|
||||||
_, err = os.Stat("test.chart/Chart.yaml")
|
_, err = os.Stat("test.chart/Chart.yaml")
|
||||||
assert.NilError(t, err)
|
assert.NilError(t, err)
|
||||||
os.Mkdir("output", 0755)
|
assert.NilError(t, os.Mkdir("output", 0755))
|
||||||
result = icmd.RunCommand(dockerApp, "unpack", "test", "-o", "output")
|
result = icmd.RunCommand(dockerApp, "unpack", "test", "-o", "output")
|
||||||
result.Assert(t, icmd.Success)
|
result.Assert(t, icmd.Success)
|
||||||
_, err = os.Stat("output/test.dockerapp/docker-compose.yml")
|
_, err = os.Stat("output/test.dockerapp/docker-compose.yml")
|
||||||
assert.NilError(t, err)
|
assert.NilError(t, err)
|
||||||
os.Chdir(cwd)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func runHelmCommand(t *testing.T, args ...string) *fs.Dir {
|
func runHelmCommand(t *testing.T, args ...string) *fs.Dir {
|
||||||
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
package compose
|
||||||
|
|
||||||
|
import (
|
||||||
|
"regexp"
|
||||||
|
|
||||||
|
"github.com/docker/cli/cli/compose/loader"
|
||||||
|
"github.com/docker/cli/cli/compose/template"
|
||||||
|
composetypes "github.com/docker/cli/cli/compose/types"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Load applies the specified function when loading a slice of compose data
|
||||||
|
func Load(composes [][]byte, apply func(string) (string, error)) ([]composetypes.ConfigFile, error) {
|
||||||
|
configFiles := []composetypes.ConfigFile{}
|
||||||
|
for _, data := range composes {
|
||||||
|
s, err := apply(string(data))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
parsed, err := loader.ParseYAML([]byte(s))
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrapf(err, "failed to parse Compose file %s", data)
|
||||||
|
}
|
||||||
|
configFiles = append(configFiles, composetypes.ConfigFile{Config: parsed})
|
||||||
|
}
|
||||||
|
return configFiles, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtractVariables extracts the variables from the specified compose data
|
||||||
|
// This is a small helper to docker/cli template.ExtractVariables function
|
||||||
|
func ExtractVariables(data []byte, pattern *regexp.Regexp) (map[string]string, error) {
|
||||||
|
cfgMap, err := loader.ParseYAML(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return template.ExtractVariables(cfgMap, pattern), nil
|
||||||
|
}
|
|
@ -11,16 +11,16 @@ import (
|
||||||
yaml "gopkg.in/yaml.v2"
|
yaml "gopkg.in/yaml.v2"
|
||||||
|
|
||||||
"github.com/docker/app/internal"
|
"github.com/docker/app/internal"
|
||||||
|
"github.com/docker/app/internal/compose"
|
||||||
"github.com/docker/app/internal/helm/templateconversion"
|
"github.com/docker/app/internal/helm/templateconversion"
|
||||||
"github.com/docker/app/internal/helm/templateloader"
|
"github.com/docker/app/internal/helm/templateloader"
|
||||||
"github.com/docker/app/internal/helm/templatev1beta2"
|
"github.com/docker/app/internal/helm/templatev1beta2"
|
||||||
"github.com/docker/app/internal/render"
|
|
||||||
"github.com/docker/app/internal/settings"
|
"github.com/docker/app/internal/settings"
|
||||||
"github.com/docker/app/internal/slices"
|
"github.com/docker/app/internal/slices"
|
||||||
"github.com/docker/app/internal/types"
|
"github.com/docker/app/render"
|
||||||
|
"github.com/docker/app/types"
|
||||||
"github.com/docker/cli/cli/command/stack/kubernetes"
|
"github.com/docker/cli/cli/command/stack/kubernetes"
|
||||||
"github.com/docker/cli/cli/compose/loader"
|
"github.com/docker/cli/cli/compose/loader"
|
||||||
"github.com/docker/cli/cli/compose/template"
|
|
||||||
"github.com/docker/cli/kubernetes/compose/v1beta1"
|
"github.com/docker/cli/kubernetes/compose/v1beta1"
|
||||||
"github.com/docker/cli/kubernetes/compose/v1beta2"
|
"github.com/docker/cli/kubernetes/compose/v1beta2"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
@ -44,51 +44,47 @@ with the appropriate content (value or template)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Helm renders an app as an Helm Chart
|
// Helm renders an app as an Helm Chart
|
||||||
func Helm(app types.App, env map[string]string, shouldRender bool, stackVersion string) error {
|
func Helm(app *types.App, env map[string]string, shouldRender bool, stackVersion string) error {
|
||||||
targetDir := internal.AppNameFromDir(app.Path) + ".chart"
|
targetDir := internal.AppNameFromDir(app.Name) + ".chart"
|
||||||
if err := os.MkdirAll(targetDir, 0755); err != nil {
|
if err := os.MkdirAll(targetDir, 0755); err != nil {
|
||||||
return errors.Wrap(err, "failed to create Chart directory")
|
return errors.Wrap(err, "failed to create Chart directory")
|
||||||
}
|
}
|
||||||
err := makeChart(app.Path, targetDir)
|
err := makeChart(app.Metadata(), targetDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if shouldRender {
|
if shouldRender {
|
||||||
return helmRender(app, targetDir, env, stackVersion)
|
return helmRender(app, targetDir, env, stackVersion)
|
||||||
}
|
}
|
||||||
// FIXME(vdemeester) handle that
|
// FIXME(vdemeester) support multiple file for helm
|
||||||
data, err := ioutil.ReadFile(filepath.Join(app.Path, internal.ComposeFileName))
|
if len(app.Composes()) > 1 {
|
||||||
if err != nil {
|
return errors.New("helm rendering doesn't support multiple composefiles")
|
||||||
return errors.Wrap(err, "failed to read application Compose file")
|
|
||||||
}
|
}
|
||||||
cfgMap, err := loader.ParseYAML(data)
|
data := app.Composes()[0]
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "failed to parse compose file")
|
|
||||||
}
|
|
||||||
vars := template.ExtractVariables(cfgMap, render.Pattern)
|
|
||||||
// FIXME(vdemeester): remove the need to create this slice
|
// FIXME(vdemeester): remove the need to create this slice
|
||||||
variables := []string{}
|
variables := []string{}
|
||||||
|
vars, err := compose.ExtractVariables(data, render.Pattern)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
for k := range vars {
|
for k := range vars {
|
||||||
variables = append(variables, k)
|
variables = append(variables, k)
|
||||||
}
|
}
|
||||||
err = makeStack(app.Path, targetDir, data, stackVersion)
|
err = makeStack(app.Name, targetDir, data, stackVersion)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return makeValues(app.Path, targetDir, app.SettingsFiles, env, variables)
|
return makeValues(app, targetDir, env, variables)
|
||||||
}
|
}
|
||||||
|
|
||||||
// makeValues updates helm values.yaml with used variables from settings and env
|
// makeValues updates helm values.yaml with used variables from settings and env
|
||||||
func makeValues(appname, targetDir string, settingsFile []string, env map[string]string, variables []string) error {
|
func makeValues(app *types.App, targetDir string, env map[string]string, variables []string) error {
|
||||||
// merge our variables into Values.yaml
|
// merge our variables into Values.yaml
|
||||||
sf := []string{filepath.Join(appname, internal.SettingsFileName)}
|
s, err := settings.LoadMultiple(app.Settings())
|
||||||
sf = append(sf, settingsFile...)
|
|
||||||
s, err := settings.LoadFiles(sf)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
metaFile := filepath.Join(appname, internal.MetadataFileName)
|
metaPrefixed, err := settings.Load(app.Metadata(), settings.WithPrefix("app"))
|
||||||
metaPrefixed, err := settings.LoadFile(metaFile, settings.WithPrefix("app"))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -174,7 +170,7 @@ func makeStack(appname string, targetDir string, data []byte, stackVersion strin
|
||||||
return ioutil.WriteFile(filepath.Join(targetDir, "templates", "stack.yaml"), stackData, 0644)
|
return ioutil.WriteFile(filepath.Join(targetDir, "templates", "stack.yaml"), stackData, 0644)
|
||||||
}
|
}
|
||||||
|
|
||||||
func helmRender(app types.App, targetDir string, env map[string]string, stackVersion string) error {
|
func helmRender(app *types.App, targetDir string, env map[string]string, stackVersion string) error {
|
||||||
rendered, err := render.Render(app, env)
|
rendered, err := render.Render(app, env)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -214,14 +210,9 @@ func helmRender(app types.App, targetDir string, env map[string]string, stackVer
|
||||||
return ioutil.WriteFile(filepath.Join(targetDir, "templates", "stack.yaml"), stackData, 0644)
|
return ioutil.WriteFile(filepath.Join(targetDir, "templates", "stack.yaml"), stackData, 0644)
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeChart(appname, targetDir string) error {
|
func makeChart(metadata []byte, targetDir string) error {
|
||||||
metaFile := filepath.Join(appname, internal.MetadataFileName)
|
|
||||||
metaContent, err := ioutil.ReadFile(metaFile)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "failed to read application metadata")
|
|
||||||
}
|
|
||||||
var meta types.AppMetadata
|
var meta types.AppMetadata
|
||||||
err = yaml.Unmarshal(metaContent, &meta)
|
err := yaml.Unmarshal(metadata, &meta)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "failed to parse application metadata")
|
return errors.Wrap(err, "failed to parse application metadata")
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,35 +1,26 @@
|
||||||
package render
|
package inspect
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
|
||||||
"path/filepath"
|
|
||||||
"sort"
|
"sort"
|
||||||
"text/tabwriter"
|
"text/tabwriter"
|
||||||
|
|
||||||
"github.com/docker/app/internal"
|
|
||||||
"github.com/docker/app/internal/settings"
|
"github.com/docker/app/internal/settings"
|
||||||
"github.com/docker/app/internal/types"
|
"github.com/docker/app/types"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
yaml "gopkg.in/yaml.v2"
|
yaml "gopkg.in/yaml.v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Inspect dumps the metadata of an app
|
// Inspect dumps the metadata of an app
|
||||||
func Inspect(out io.Writer, appname string) error {
|
func Inspect(out io.Writer, app *types.App) error {
|
||||||
metaFile := filepath.Join(appname, internal.MetadataFileName)
|
|
||||||
metaContent, err := ioutil.ReadFile(metaFile)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "failed to read application metadata")
|
|
||||||
}
|
|
||||||
var meta types.AppMetadata
|
var meta types.AppMetadata
|
||||||
err = yaml.Unmarshal(metaContent, &meta)
|
err := yaml.Unmarshal(app.Metadata(), &meta)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "failed to parse application metadata")
|
return errors.Wrap(err, "failed to parse application metadata")
|
||||||
}
|
}
|
||||||
// extract settings
|
// extract settings
|
||||||
settingsFile := filepath.Join(appname, internal.SettingsFileName)
|
s, err := settings.LoadMultiple(app.Settings())
|
||||||
s, err := settings.LoadFile(settingsFile)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "failed to load application settings")
|
return errors.Wrap(err, "failed to load application settings")
|
||||||
}
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package render
|
package inspect
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
@ -7,6 +7,7 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/docker/app/internal"
|
"github.com/docker/app/internal"
|
||||||
|
"github.com/docker/app/types"
|
||||||
|
|
||||||
"gotest.tools/assert"
|
"gotest.tools/assert"
|
||||||
is "gotest.tools/assert/cmp"
|
is "gotest.tools/assert/cmp"
|
||||||
|
@ -14,16 +15,23 @@ import (
|
||||||
"gotest.tools/golden"
|
"gotest.tools/golden"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
composeYAML = `version: "3.1"
|
||||||
|
|
||||||
|
services:
|
||||||
|
web:
|
||||||
|
image: nginx`
|
||||||
|
)
|
||||||
|
|
||||||
func TestInspectErrorsOnFiles(t *testing.T) {
|
func TestInspectErrorsOnFiles(t *testing.T) {
|
||||||
dir := fs.NewDir(t, "inspect-errors",
|
dir := fs.NewDir(t, "inspect-errors",
|
||||||
fs.WithDir("empty-app"),
|
|
||||||
fs.WithDir("unparseable-metadata-app",
|
fs.WithDir("unparseable-metadata-app",
|
||||||
|
fs.WithFile(internal.ComposeFileName, composeYAML),
|
||||||
fs.WithFile(internal.MetadataFileName, `something is wrong`),
|
fs.WithFile(internal.MetadataFileName, `something is wrong`),
|
||||||
),
|
fs.WithFile(internal.SettingsFileName, "foo"),
|
||||||
fs.WithDir("no-settings-app",
|
|
||||||
fs.WithFile(internal.MetadataFileName, `{}`),
|
|
||||||
),
|
),
|
||||||
fs.WithDir("unparseable-settings-app",
|
fs.WithDir("unparseable-settings-app",
|
||||||
|
fs.WithFile(internal.ComposeFileName, composeYAML),
|
||||||
fs.WithFile(internal.MetadataFileName, `{}`),
|
fs.WithFile(internal.MetadataFileName, `{}`),
|
||||||
fs.WithFile(internal.SettingsFileName, "foo"),
|
fs.WithFile(internal.SettingsFileName, "foo"),
|
||||||
),
|
),
|
||||||
|
@ -31,13 +39,12 @@ func TestInspectErrorsOnFiles(t *testing.T) {
|
||||||
defer dir.Remove()
|
defer dir.Remove()
|
||||||
|
|
||||||
for appname, expectedError := range map[string]string{
|
for appname, expectedError := range map[string]string{
|
||||||
"inexistent-app": "failed to read application metadata",
|
|
||||||
"empty-app": "failed to read application metadata",
|
|
||||||
"unparseable-metadata-app": "failed to parse application metadat",
|
"unparseable-metadata-app": "failed to parse application metadat",
|
||||||
"no-settings-app": "failed to load application settings",
|
|
||||||
"unparseable-settings-app": "failed to load application settings",
|
"unparseable-settings-app": "failed to load application settings",
|
||||||
} {
|
} {
|
||||||
err := Inspect(ioutil.Discard, dir.Join(appname))
|
app, err := types.NewAppFromDefaultFiles(dir.Join(appname))
|
||||||
|
assert.NilError(t, err)
|
||||||
|
err = Inspect(ioutil.Discard, app)
|
||||||
assert.Check(t, is.ErrorContains(err, expectedError))
|
assert.Check(t, is.ErrorContains(err, expectedError))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -45,12 +52,14 @@ func TestInspectErrorsOnFiles(t *testing.T) {
|
||||||
func TestInspect(t *testing.T) {
|
func TestInspect(t *testing.T) {
|
||||||
dir := fs.NewDir(t, "inspect",
|
dir := fs.NewDir(t, "inspect",
|
||||||
fs.WithDir("no-maintainers",
|
fs.WithDir("no-maintainers",
|
||||||
|
fs.WithFile(internal.ComposeFileName, composeYAML),
|
||||||
fs.WithFile(internal.MetadataFileName, `
|
fs.WithFile(internal.MetadataFileName, `
|
||||||
version: 0.1.0
|
version: 0.1.0
|
||||||
name: foo`),
|
name: foo`),
|
||||||
fs.WithFile(internal.SettingsFileName, ``),
|
fs.WithFile(internal.SettingsFileName, ``),
|
||||||
),
|
),
|
||||||
fs.WithDir("no-description",
|
fs.WithDir("no-description",
|
||||||
|
fs.WithFile(internal.ComposeFileName, composeYAML),
|
||||||
fs.WithFile(internal.MetadataFileName, `
|
fs.WithFile(internal.MetadataFileName, `
|
||||||
version: 0.1.0
|
version: 0.1.0
|
||||||
name: foo
|
name: foo
|
||||||
|
@ -60,6 +69,7 @@ maintainers:
|
||||||
fs.WithFile(internal.SettingsFileName, ""),
|
fs.WithFile(internal.SettingsFileName, ""),
|
||||||
),
|
),
|
||||||
fs.WithDir("no-settings",
|
fs.WithDir("no-settings",
|
||||||
|
fs.WithFile(internal.ComposeFileName, composeYAML),
|
||||||
fs.WithFile(internal.MetadataFileName, `
|
fs.WithFile(internal.MetadataFileName, `
|
||||||
version: 0.1.0
|
version: 0.1.0
|
||||||
name: foo
|
name: foo
|
||||||
|
@ -70,6 +80,7 @@ description: "this is sparta !"`),
|
||||||
fs.WithFile(internal.SettingsFileName, ""),
|
fs.WithFile(internal.SettingsFileName, ""),
|
||||||
),
|
),
|
||||||
fs.WithDir("full",
|
fs.WithDir("full",
|
||||||
|
fs.WithFile(internal.ComposeFileName, composeYAML),
|
||||||
fs.WithFile(internal.MetadataFileName, `
|
fs.WithFile(internal.MetadataFileName, `
|
||||||
version: 0.1.0
|
version: 0.1.0
|
||||||
name: foo
|
name: foo
|
||||||
|
@ -88,8 +99,10 @@ text: hello`),
|
||||||
"no-maintainers", "no-description", "no-settings", "full",
|
"no-maintainers", "no-description", "no-settings", "full",
|
||||||
} {
|
} {
|
||||||
outBuffer := new(bytes.Buffer)
|
outBuffer := new(bytes.Buffer)
|
||||||
err := Inspect(outBuffer, dir.Join(appname))
|
app, err := types.NewAppFromDefaultFiles(dir.Join(appname))
|
||||||
assert.NilError(t, err)
|
assert.NilError(t, err)
|
||||||
golden.Assert(t, outBuffer.String(), fmt.Sprintf("inspect-%s.golden", appname))
|
err = Inspect(outBuffer, app)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
golden.Assert(t, outBuffer.String(), fmt.Sprintf("inspect-%s.golden", appname), appname)
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,9 +1,7 @@
|
||||||
package packager
|
package packager
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"archive/tar"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
@ -11,14 +9,11 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/docker/app/internal"
|
"github.com/docker/app/internal"
|
||||||
"github.com/docker/app/internal/types"
|
"github.com/docker/app/loader"
|
||||||
|
"github.com/docker/app/types"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
|
||||||
noop = func() {}
|
|
||||||
)
|
|
||||||
|
|
||||||
// findApp looks for an app in CWD or subdirs
|
// findApp looks for an app in CWD or subdirs
|
||||||
func findApp() (string, error) {
|
func findApp() (string, error) {
|
||||||
cwd, err := os.Getwd()
|
cwd, err := os.Getwd()
|
||||||
|
@ -48,7 +43,7 @@ func findApp() (string, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// extractImage extracts a docker application in a docker image to a temporary directory
|
// extractImage extracts a docker application in a docker image to a temporary directory
|
||||||
func extractImage(appname string) (types.App, error) {
|
func extractImage(appname string, ops ...func(*types.App) error) (*types.App, error) {
|
||||||
var imagename string
|
var imagename string
|
||||||
if strings.Contains(appname, ":") {
|
if strings.Contains(appname, ":") {
|
||||||
nametag := strings.Split(appname, ":")
|
nametag := strings.Split(appname, ":")
|
||||||
|
@ -66,161 +61,67 @@ func extractImage(appname string) (types.App, error) {
|
||||||
}
|
}
|
||||||
tempDir, err := ioutil.TempDir("", "dockerapp")
|
tempDir, err := ioutil.TempDir("", "dockerapp")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return types.App{}, errors.Wrap(err, "failed to create temporary directory")
|
return nil, errors.Wrap(err, "failed to create temporary directory")
|
||||||
}
|
}
|
||||||
defer os.RemoveAll(tempDir)
|
defer os.RemoveAll(tempDir)
|
||||||
err = Load(imagename, tempDir)
|
err = Load(imagename, tempDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !strings.Contains(imagename, "/") {
|
if !strings.Contains(imagename, "/") {
|
||||||
return types.App{}, fmt.Errorf("could not locate application in either filesystem or docker image")
|
return nil, fmt.Errorf("could not locate application in either filesystem or docker image")
|
||||||
}
|
}
|
||||||
// Try to pull it
|
// Try to pull it
|
||||||
cmd := exec.Command("docker", "pull", imagename)
|
cmd := exec.Command("docker", "pull", imagename)
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
if err := cmd.Run(); err != nil {
|
if err := cmd.Run(); err != nil {
|
||||||
return types.App{}, fmt.Errorf("could not locate application in filesystem, docker image or registry")
|
return nil, fmt.Errorf("could not locate application in filesystem, docker image or registry")
|
||||||
}
|
}
|
||||||
if err := Load(imagename, tempDir); err != nil {
|
if err := Load(imagename, tempDir); err != nil {
|
||||||
return types.App{}, errors.Wrap(err, "failed to load pulled image")
|
return nil, errors.Wrap(err, "failed to load pulled image")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// this gave us a compressed app, run through extract again
|
return loader.LoadFromTar(filepath.Join(tempDir, appname), ops...)
|
||||||
app, err := Extract(filepath.Join(tempDir, appname))
|
|
||||||
return types.App{
|
|
||||||
Path: app.Path,
|
|
||||||
Cleanup: app.Cleanup,
|
|
||||||
}, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract extracts the app content if argument is an archive, or does nothing if a dir.
|
// Extract extracts the app content if argument is an archive, or does nothing if a dir.
|
||||||
// It returns source file, effective app name, and cleanup function
|
// It returns source file, effective app name, and cleanup function
|
||||||
// If appname is empty, it looks into cwd, and all subdirs for a single matching .dockerapp
|
// If appname is empty, it looks into cwd, and all subdirs for a single matching .dockerapp
|
||||||
// If nothing is found, it looks for an image and loads it
|
// If nothing is found, it looks for an image and loads it
|
||||||
func Extract(appname string, ops ...func(*types.App)) (types.App, error) {
|
func Extract(name string, ops ...func(*types.App) error) (*types.App, error) {
|
||||||
if appname == "" {
|
if name == "" {
|
||||||
var err error
|
var err error
|
||||||
if appname, err = findApp(); err != nil {
|
if name, err = findApp(); err != nil {
|
||||||
return types.App{}, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if appname == "." {
|
if name == "." {
|
||||||
var err error
|
var err error
|
||||||
if appname, err = os.Getwd(); err != nil {
|
if name, err = os.Getwd(); err != nil {
|
||||||
return types.App{}, errors.Wrap(err, "cannot resolve current working directory")
|
return nil, errors.Wrap(err, "cannot resolve current working directory")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
originalAppname := appname
|
ops = append(ops, types.WithName(name))
|
||||||
appname = filepath.Clean(appname)
|
appname := internal.DirNameFromAppName(name)
|
||||||
// try appending our extension
|
|
||||||
appname = internal.DirNameFromAppName(appname)
|
|
||||||
s, err := os.Stat(appname)
|
s, err := os.Stat(appname)
|
||||||
if err != nil {
|
|
||||||
// try verbatim
|
|
||||||
s, err = os.Stat(originalAppname)
|
|
||||||
}
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// look for a docker image
|
// look for a docker image
|
||||||
return extractImage(originalAppname)
|
return extractImage(name, ops...)
|
||||||
}
|
}
|
||||||
if s.IsDir() {
|
if s.IsDir() {
|
||||||
// directory: already decompressed
|
// directory: already decompressed
|
||||||
ops = append([]func(*types.App){
|
appOpts := append(ops,
|
||||||
types.WithOriginalPath(appname),
|
types.WithPath(appname),
|
||||||
types.WithCleanup(noop),
|
)
|
||||||
}, ops...)
|
return loader.LoadFromDirectory(appname, appOpts...)
|
||||||
return types.NewApp(appname, ops...), nil
|
|
||||||
}
|
}
|
||||||
// not a dir: single-file or a tarball package, extract that in a temp dir
|
// not a dir: single-file or a tarball package, extract that in a temp dir
|
||||||
tempDir, err := ioutil.TempDir("", "dockerapp")
|
app, err := loader.LoadFromTar(appname, ops...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return types.App{}, errors.Wrap(err, "failed to create temporary directory")
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
if err != nil {
|
|
||||||
os.RemoveAll(tempDir)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
appDir := filepath.Join(tempDir, filepath.Base(appname))
|
|
||||||
if err = os.Mkdir(appDir, 0755); err != nil {
|
|
||||||
return types.App{}, errors.Wrap(err, "failed to create application in temporary directory")
|
|
||||||
}
|
|
||||||
if err = extract(appname, appDir); err == nil {
|
|
||||||
ops = append([]func(*types.App){
|
|
||||||
types.WithOriginalPath(appname),
|
|
||||||
types.WithCleanup(func() { os.RemoveAll(tempDir) }),
|
|
||||||
}, ops...)
|
|
||||||
return types.NewApp(appDir, ops...), nil
|
|
||||||
}
|
|
||||||
if err = extractSingleFile(appname, appDir); err != nil {
|
|
||||||
return types.App{}, err
|
|
||||||
}
|
|
||||||
// not a tarball, single-file then
|
|
||||||
ops = append([]func(*types.App){
|
|
||||||
types.WithOriginalPath(appname),
|
|
||||||
types.WithCleanup(func() { os.RemoveAll(tempDir) }),
|
|
||||||
}, ops...)
|
|
||||||
return types.NewApp(appDir, ops...), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func extractSingleFile(appname, appDir string) error {
|
|
||||||
// not a tarball, single-file then
|
|
||||||
data, err := ioutil.ReadFile(appname)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "failed to read single-file application package")
|
|
||||||
}
|
|
||||||
parts := strings.Split(string(data), "\n---")
|
|
||||||
if len(parts) != 3 {
|
|
||||||
return fmt.Errorf("malformed single-file application: expected 3 documents")
|
|
||||||
}
|
|
||||||
for i, p := range parts {
|
|
||||||
data := ""
|
|
||||||
if i == 0 {
|
|
||||||
data = p
|
|
||||||
} else {
|
|
||||||
d := strings.SplitN(p, "\n", 2)
|
|
||||||
if len(d) > 1 {
|
|
||||||
data = d[1]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
err = ioutil.WriteFile(filepath.Join(appDir, internal.FileNames[i]), []byte(data), 0644)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "failed to write application file")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func extract(appname, outputDir string) error {
|
|
||||||
f, err := os.Open(appname)
|
f, err := os.Open(appname)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "failed to open application package")
|
return nil, err
|
||||||
}
|
}
|
||||||
defer f.Close()
|
return loader.LoadFromSingleFile(appname, f, ops...)
|
||||||
tarReader := tar.NewReader(f)
|
|
||||||
outputDir = outputDir + "/"
|
|
||||||
for {
|
|
||||||
header, err := tarReader.Next()
|
|
||||||
if err == io.EOF {
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
if err != nil {
|
return app, nil
|
||||||
return errors.Wrap(err, "error reading from tar header")
|
|
||||||
}
|
|
||||||
switch header.Typeflag {
|
|
||||||
case tar.TypeDir: // = directory
|
|
||||||
if err := os.Mkdir(outputDir+header.Name, 0755); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
case tar.TypeReg: // = regular file
|
|
||||||
data := make([]byte, header.Size)
|
|
||||||
_, err := tarReader.Read(data)
|
|
||||||
if err != nil && err != io.EOF {
|
|
||||||
return errors.Wrap(err, "error reading from tar data")
|
|
||||||
}
|
|
||||||
err = ioutil.WriteFile(outputDir+header.Name, data, 0644)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "error writing output file")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,11 +12,12 @@ import (
|
||||||
"text/template"
|
"text/template"
|
||||||
|
|
||||||
"github.com/docker/app/internal"
|
"github.com/docker/app/internal"
|
||||||
"github.com/docker/app/internal/render"
|
"github.com/docker/app/internal/compose"
|
||||||
"github.com/docker/app/internal/types"
|
"github.com/docker/app/loader"
|
||||||
"github.com/docker/cli/cli/compose/loader"
|
"github.com/docker/app/render"
|
||||||
|
"github.com/docker/app/types"
|
||||||
|
composeloader "github.com/docker/cli/cli/compose/loader"
|
||||||
"github.com/docker/cli/cli/compose/schema"
|
"github.com/docker/cli/cli/compose/schema"
|
||||||
dtemplate "github.com/docker/cli/cli/compose/template"
|
|
||||||
"github.com/docker/cli/opts"
|
"github.com/docker/cli/opts"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
@ -89,7 +90,11 @@ func Init(name string, composeFile string, description string, maintainers []str
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer target.(io.WriteCloser).Close()
|
defer target.(io.WriteCloser).Close()
|
||||||
return Merge(temp, target)
|
app, err := loader.LoadFromDirectory(temp)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return Merge(app, target)
|
||||||
}
|
}
|
||||||
|
|
||||||
func initFromScratch(name string) error {
|
func initFromScratch(name string) error {
|
||||||
|
@ -124,7 +129,7 @@ func initFromComposeFile(name string, composeFile string) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "failed to read compose file")
|
return errors.Wrap(err, "failed to read compose file")
|
||||||
}
|
}
|
||||||
cfgMap, err := loader.ParseYAML(composeRaw)
|
cfgMap, err := composeloader.ParseYAML(composeRaw)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "failed to parse compose file")
|
return errors.Wrap(err, "failed to parse compose file")
|
||||||
}
|
}
|
||||||
|
@ -141,7 +146,10 @@ func initFromComposeFile(name string, composeFile string) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
vars := dtemplate.ExtractVariables(cfgMap, render.Pattern)
|
vars, err := compose.ExtractVariables(composeRaw, render.Pattern)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrap(err, "failed to parse compose file")
|
||||||
|
}
|
||||||
needsFilling := false
|
needsFilling := false
|
||||||
for k, v := range vars {
|
for k, v := range vars {
|
||||||
if _, ok := settings[k]; !ok {
|
if _, ok := settings[k]; !ok {
|
||||||
|
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/docker/app/internal"
|
"github.com/docker/app/internal"
|
||||||
|
"github.com/docker/docker/pkg/archive"
|
||||||
)
|
)
|
||||||
|
|
||||||
func tarAdd(tarout *tar.Writer, path, file string) error {
|
func tarAdd(tarout *tar.Writer, path, file string) error {
|
||||||
|
@ -87,5 +88,11 @@ func Unpack(appname, targetDir string) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return extract(appname, out)
|
f, err := os.Open(appname)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return archive.Untar(f, out, &archive.TarOptions{
|
||||||
|
NoLchown: true,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,26 +12,16 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/docker/app/internal"
|
"github.com/docker/app/internal"
|
||||||
"github.com/docker/app/internal/types"
|
"github.com/docker/app/types"
|
||||||
"github.com/docker/distribution/reference"
|
"github.com/docker/distribution/reference"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
yaml "gopkg.in/yaml.v2"
|
yaml "gopkg.in/yaml.v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Save saves an app to docker and returns the image name.
|
// Save saves an app to docker and returns the image name.
|
||||||
func Save(appname, namespace, tag string) (string, error) {
|
func Save(app *types.App, namespace, tag string) (string, error) {
|
||||||
app, err := Extract(appname)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
defer app.Cleanup()
|
|
||||||
metaFile := filepath.Join(app.Path, internal.MetadataFileName)
|
|
||||||
metaContent, err := ioutil.ReadFile(metaFile)
|
|
||||||
if err != nil {
|
|
||||||
return "", errors.Wrap(err, "failed to read application metadata")
|
|
||||||
}
|
|
||||||
var meta types.AppMetadata
|
var meta types.AppMetadata
|
||||||
err = yaml.Unmarshal(metaContent, &meta)
|
err := yaml.Unmarshal(app.Metadata(), &meta)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", errors.Wrap(err, "failed to parse application metadata")
|
return "", errors.Wrap(err, "failed to parse application metadata")
|
||||||
}
|
}
|
||||||
|
@ -60,7 +50,7 @@ COPY / /
|
||||||
return "", errors.Wrapf(err, "cannot create file %s", di)
|
return "", errors.Wrapf(err, "cannot create file %s", di)
|
||||||
}
|
}
|
||||||
defer os.Remove(di)
|
defer os.Remove(di)
|
||||||
imageName := namespace + internal.AppNameFromDir(app.Path) + internal.AppExtension + ":" + tag
|
imageName := namespace + internal.AppNameFromDir(app.Name) + internal.AppExtension + ":" + tag
|
||||||
args := []string{"build", "-t", imageName, "-f", df, app.Path}
|
args := []string{"build", "-t", imageName, "-f", df, app.Path}
|
||||||
cmd := exec.Command("docker", args...)
|
cmd := exec.Command("docker", args...)
|
||||||
cmd.Stdout = ioutil.Discard
|
cmd.Stdout = ioutil.Discard
|
||||||
|
@ -111,13 +101,8 @@ func Load(repotag string, outputDir string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Push pushes an app to a registry
|
// Push pushes an app to a registry
|
||||||
func Push(appname, namespace, tag string) error {
|
func Push(app *types.App, namespace, tag string) error {
|
||||||
app, err := Extract(appname)
|
imageName, err := Save(app, namespace, tag)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer app.Cleanup()
|
|
||||||
imageName, err := Save(app.Path, namespace, tag)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,21 +7,28 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/docker/app/internal"
|
"github.com/docker/app/internal"
|
||||||
|
"github.com/docker/app/types"
|
||||||
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Split converts an app package to the split version
|
// Split converts an app package to the split version
|
||||||
func Split(appname string, outputDir string) error {
|
func Split(app *types.App, outputDir string) error {
|
||||||
err := os.Mkdir(outputDir, 0755)
|
if len(app.Composes()) > 1 {
|
||||||
|
return errors.New("split: multiple compose files is not supported")
|
||||||
|
}
|
||||||
|
if len(app.Settings()) > 1 {
|
||||||
|
return errors.New("split: multiple setting files is not supported")
|
||||||
|
}
|
||||||
|
err := os.MkdirAll(outputDir, 0755)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for _, n := range internal.FileNames {
|
for file, data := range map[string][]byte{
|
||||||
input, err := ioutil.ReadFile(filepath.Join(appname, n))
|
internal.MetadataFileName: app.Metadata(),
|
||||||
if err != nil {
|
internal.ComposeFileName: app.Composes()[0],
|
||||||
return err
|
internal.SettingsFileName: app.Settings()[0],
|
||||||
}
|
} {
|
||||||
err = ioutil.WriteFile(filepath.Join(outputDir, n), input, 0644)
|
if err := ioutil.WriteFile(filepath.Join(outputDir, file), data, 0644); err != nil {
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -29,20 +36,23 @@ func Split(appname string, outputDir string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Merge converts an app-package to the single-file merged version
|
// Merge converts an app-package to the single-file merged version
|
||||||
func Merge(appname string, target io.Writer) error {
|
func Merge(app *types.App, target io.Writer) error {
|
||||||
for i, n := range internal.FileNames {
|
if len(app.Composes()) > 1 {
|
||||||
input, err := ioutil.ReadFile(filepath.Join(appname, n))
|
return errors.New("merge: multiple compose files is not supported")
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
if _, err := target.Write(input); err != nil {
|
if len(app.Settings()) > 1 {
|
||||||
return err
|
return errors.New("merge: multiple setting files is not supported")
|
||||||
}
|
}
|
||||||
if i != 2 {
|
for _, data := range [][]byte{
|
||||||
if _, err := io.WriteString(target, "\n---\n"); err != nil {
|
app.Metadata(),
|
||||||
|
[]byte(types.SingleFileSeparator),
|
||||||
|
app.Composes()[0],
|
||||||
|
[]byte(types.SingleFileSeparator),
|
||||||
|
app.Settings()[0],
|
||||||
|
} {
|
||||||
|
if _, err := target.Write(data); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,21 +1,23 @@
|
||||||
package settings
|
package settings
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"io/ioutil"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
yaml "gopkg.in/yaml.v2"
|
yaml "gopkg.in/yaml.v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Load loads the given reader in settings
|
// Load loads the given data in settings
|
||||||
func Load(r io.Reader, ops ...func(*Options)) (Settings, error) {
|
func Load(data []byte, ops ...func(*Options)) (Settings, error) {
|
||||||
options := &Options{}
|
options := &Options{}
|
||||||
for _, op := range ops {
|
for _, op := range ops {
|
||||||
op(options)
|
op(options)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
r := bytes.NewReader(data)
|
||||||
s := make(map[interface{}]interface{})
|
s := make(map[interface{}]interface{})
|
||||||
decoder := yaml.NewDecoder(r)
|
decoder := yaml.NewDecoder(r)
|
||||||
if err := decoder.Decode(&s); err != nil {
|
if err := decoder.Decode(&s); err != nil {
|
||||||
|
@ -37,13 +39,29 @@ func Load(r io.Reader, ops ...func(*Options)) (Settings, error) {
|
||||||
return settings, nil
|
return settings, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadFile loads a file (path) in settings (i.e. flatten map)
|
// LoadMultiple loads multiple data in settings
|
||||||
func LoadFile(path string, ops ...func(*Options)) (Settings, error) {
|
func LoadMultiple(datas [][]byte, ops ...func(*Options)) (Settings, error) {
|
||||||
r, err := os.Open(path)
|
m := Settings(map[string]interface{}{})
|
||||||
|
for _, data := range datas {
|
||||||
|
settings, err := Load(data, ops...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return Load(r, ops...)
|
m, err = Merge(m, settings)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadFile loads a file (path) in settings (i.e. flatten map)
|
||||||
|
func LoadFile(path string, ops ...func(*Options)) (Settings, error) {
|
||||||
|
data, err := ioutil.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return Load(data, ops...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadFiles loads multiple path in settings, merging them.
|
// LoadFiles loads multiple path in settings, merging them.
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package settings
|
package settings
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"strings"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"gotest.tools/assert"
|
"gotest.tools/assert"
|
||||||
|
@ -10,15 +9,15 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestLoadErrors(t *testing.T) {
|
func TestLoadErrors(t *testing.T) {
|
||||||
_, err := Load(strings.NewReader("invalid yaml"))
|
_, err := Load([]byte("invalid yaml"))
|
||||||
assert.Check(t, is.ErrorContains(err, "failed to read settings"))
|
assert.Check(t, is.ErrorContains(err, "failed to read settings"))
|
||||||
|
|
||||||
_, err = Load(strings.NewReader(`
|
_, err = Load([]byte(`
|
||||||
foo: bar
|
foo: bar
|
||||||
1: baz`))
|
1: baz`))
|
||||||
assert.Check(t, is.ErrorContains(err, "Non-string key at top level: 1"))
|
assert.Check(t, is.ErrorContains(err, "Non-string key at top level: 1"))
|
||||||
|
|
||||||
_, err = Load(strings.NewReader(`
|
_, err = Load([]byte(`
|
||||||
foo:
|
foo:
|
||||||
bar: baz
|
bar: baz
|
||||||
1: banana`))
|
1: banana`))
|
||||||
|
@ -26,7 +25,7 @@ foo:
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLoad(t *testing.T) {
|
func TestLoad(t *testing.T) {
|
||||||
settings, err := Load(strings.NewReader(`
|
settings, err := Load([]byte(`
|
||||||
foo: bar
|
foo: bar
|
||||||
bar:
|
bar:
|
||||||
baz: banana
|
baz: banana
|
||||||
|
@ -45,7 +44,7 @@ baz:
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLoadWithPrefix(t *testing.T) {
|
func TestLoadWithPrefix(t *testing.T) {
|
||||||
settings, err := Load(strings.NewReader(`
|
settings, err := Load([]byte(`
|
||||||
foo: bar
|
foo: bar
|
||||||
bar: baz
|
bar: baz
|
||||||
`), WithPrefix("p"))
|
`), WithPrefix("p"))
|
||||||
|
@ -78,3 +77,25 @@ bar:
|
||||||
"bar.port": "10",
|
"bar.port": "10",
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestLoadMultiples(t *testing.T) {
|
||||||
|
datas := [][]byte{
|
||||||
|
[]byte(`
|
||||||
|
foo: bar
|
||||||
|
bar:
|
||||||
|
baz: banana
|
||||||
|
port: 80`),
|
||||||
|
[]byte(`
|
||||||
|
foo: baz
|
||||||
|
bar:
|
||||||
|
port: 10`),
|
||||||
|
}
|
||||||
|
|
||||||
|
settings, err := LoadMultiple(datas)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
assert.Check(t, is.DeepEqual(settings.Flatten(), map[string]string{
|
||||||
|
"foo": "baz",
|
||||||
|
"bar.baz": "banana",
|
||||||
|
"bar.port": "10",
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
|
@ -1,61 +0,0 @@
|
||||||
package types
|
|
||||||
|
|
||||||
import (
|
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
"github.com/docker/app/internal"
|
|
||||||
)
|
|
||||||
|
|
||||||
// App represents an app (extracted or not)
|
|
||||||
type App struct {
|
|
||||||
Path string
|
|
||||||
OriginalPath string
|
|
||||||
ComposeFiles []string
|
|
||||||
SettingsFiles []string
|
|
||||||
MetadataFile string
|
|
||||||
Cleanup func()
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewApp creates a new docker app with the specified path and struct modifiers
|
|
||||||
func NewApp(path string, ops ...func(*App)) App {
|
|
||||||
app := &App{
|
|
||||||
Path: path,
|
|
||||||
ComposeFiles: []string{filepath.Join(path, internal.ComposeFileName)},
|
|
||||||
SettingsFiles: []string{filepath.Join(path, internal.SettingsFileName)},
|
|
||||||
MetadataFile: filepath.Join(path, internal.MetadataFileName),
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, op := range ops {
|
|
||||||
op(app)
|
|
||||||
}
|
|
||||||
|
|
||||||
return *app
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithOriginalPath sets the original path of the app
|
|
||||||
func WithOriginalPath(path string) func(*App) {
|
|
||||||
return func(app *App) {
|
|
||||||
app.OriginalPath = path
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithCleanup sets the cleanup function of the app
|
|
||||||
func WithCleanup(f func()) func(*App) {
|
|
||||||
return func(app *App) {
|
|
||||||
app.Cleanup = f
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithSettingsFiles adds the specified settings files of the app
|
|
||||||
func WithSettingsFiles(files ...string) func(*App) {
|
|
||||||
return func(app *App) {
|
|
||||||
app.SettingsFiles = append(app.SettingsFiles, files...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithComposeFiles adds the specified compose files of the app
|
|
||||||
func WithComposeFiles(files ...string) func(*App) {
|
|
||||||
return func(app *App) {
|
|
||||||
app.ComposeFiles = append(app.ComposeFiles, files...)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -3,25 +3,19 @@ package validator
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/docker/app/internal"
|
"github.com/docker/app/internal"
|
||||||
"github.com/docker/app/internal/render"
|
"github.com/docker/app/render"
|
||||||
"github.com/docker/app/internal/types"
|
|
||||||
"github.com/docker/app/specification"
|
"github.com/docker/app/specification"
|
||||||
|
"github.com/docker/app/types"
|
||||||
"github.com/docker/cli/cli/compose/loader"
|
"github.com/docker/cli/cli/compose/loader"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Validate checks an application definition meets the specifications (metadata and rendered compose file)
|
// Validate checks an application definition meets the specifications (metadata and rendered compose file)
|
||||||
func Validate(app types.App, env map[string]string) error {
|
func Validate(app *types.App, env map[string]string) error {
|
||||||
var errs []string
|
var errs []string
|
||||||
if err := checkExistingFiles(app.Path); err != nil {
|
if err := validateMetadata(app.Metadata()); err != nil {
|
||||||
errs = append(errs, err.Error())
|
|
||||||
}
|
|
||||||
if err := validateMetadata(app.Path); err != nil {
|
|
||||||
errs = append(errs, err.Error())
|
errs = append(errs, err.Error())
|
||||||
}
|
}
|
||||||
if _, err := render.Render(app, env); err != nil {
|
if _, err := render.Render(app, env); err != nil {
|
||||||
|
@ -30,25 +24,7 @@ func Validate(app types.App, env map[string]string) error {
|
||||||
return concatenateErrors(errs)
|
return concatenateErrors(errs)
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkExistingFiles(appname string) error {
|
func validateMetadata(metadata []byte) error {
|
||||||
var errs []string
|
|
||||||
if _, err := os.Stat(filepath.Join(appname, internal.SettingsFileName)); err != nil {
|
|
||||||
errs = append(errs, "failed to read application settings")
|
|
||||||
}
|
|
||||||
if _, err := os.Stat(filepath.Join(appname, internal.MetadataFileName)); err != nil {
|
|
||||||
errs = append(errs, "failed to read application metadata")
|
|
||||||
}
|
|
||||||
if _, err := os.Stat(filepath.Join(appname, internal.ComposeFileName)); err != nil {
|
|
||||||
errs = append(errs, "failed to read application compose")
|
|
||||||
}
|
|
||||||
return concatenateErrors(errs)
|
|
||||||
}
|
|
||||||
|
|
||||||
func validateMetadata(appname string) error {
|
|
||||||
metadata, err := ioutil.ReadFile(filepath.Join(appname, internal.MetadataFileName))
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to read application metadata: %s", err)
|
|
||||||
}
|
|
||||||
metadataYaml, err := loader.ParseYAML(metadata)
|
metadataYaml, err := loader.ParseYAML(metadata)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to parse application metadata: %s", err)
|
return fmt.Errorf("failed to parse application metadata: %s", err)
|
||||||
|
|
|
@ -6,21 +6,10 @@ import (
|
||||||
"gotest.tools/assert"
|
"gotest.tools/assert"
|
||||||
|
|
||||||
"github.com/docker/app/internal"
|
"github.com/docker/app/internal"
|
||||||
"github.com/docker/app/internal/types"
|
"github.com/docker/app/types"
|
||||||
"gotest.tools/fs"
|
"gotest.tools/fs"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestValidateMissingFileApplication(t *testing.T) {
|
|
||||||
dir := fs.NewDir(t, t.Name(),
|
|
||||||
fs.WithDir("bad-app"),
|
|
||||||
)
|
|
||||||
defer dir.Remove()
|
|
||||||
errs := Validate(types.NewApp(dir.Join("bad-app")), nil)
|
|
||||||
assert.ErrorContains(t, errs, "failed to read application settings")
|
|
||||||
assert.ErrorContains(t, errs, "failed to read application metadata")
|
|
||||||
assert.ErrorContains(t, errs, "failed to read application compose")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestValidateBrokenMetadata(t *testing.T) {
|
func TestValidateBrokenMetadata(t *testing.T) {
|
||||||
brokenMetadata := `#version: 0.1.0-missing
|
brokenMetadata := `#version: 0.1.0-missing
|
||||||
name: _INVALID-name
|
name: _INVALID-name
|
||||||
|
@ -38,7 +27,9 @@ unknown: property`
|
||||||
fs.WithFile(internal.ComposeFileName, composeFile),
|
fs.WithFile(internal.ComposeFileName, composeFile),
|
||||||
fs.WithFile(internal.SettingsFileName, ""))
|
fs.WithFile(internal.SettingsFileName, ""))
|
||||||
defer dir.Remove()
|
defer dir.Remove()
|
||||||
err := Validate(types.NewApp(dir.Path()), nil)
|
app, err := types.NewAppFromDefaultFiles(dir.Path())
|
||||||
|
assert.NilError(t, err)
|
||||||
|
err = Validate(app, nil)
|
||||||
assert.Error(t, err, `failed to validate metadata:
|
assert.Error(t, err, `failed to validate metadata:
|
||||||
- maintainers.2.email: Does not match format 'email'
|
- maintainers.2.email: Does not match format 'email'
|
||||||
- name: Does not match format 'hostname'
|
- name: Does not match format 'hostname'
|
||||||
|
@ -57,7 +48,9 @@ my-settings:
|
||||||
fs.WithFile(internal.ComposeFileName, composeFile),
|
fs.WithFile(internal.ComposeFileName, composeFile),
|
||||||
fs.WithFile(internal.SettingsFileName, brokenSettings))
|
fs.WithFile(internal.SettingsFileName, brokenSettings))
|
||||||
defer dir.Remove()
|
defer dir.Remove()
|
||||||
err := Validate(types.NewApp(dir.Path()), nil)
|
app, err := types.NewAppFromDefaultFiles(dir.Path())
|
||||||
|
assert.NilError(t, err)
|
||||||
|
err = Validate(app, nil)
|
||||||
assert.ErrorContains(t, err, `Non-string key in my-settings: 1`)
|
assert.ErrorContains(t, err, `Non-string key in my-settings: 1`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -72,7 +65,9 @@ unknown-property: value`
|
||||||
fs.WithFile(internal.ComposeFileName, brokenComposeFile),
|
fs.WithFile(internal.ComposeFileName, brokenComposeFile),
|
||||||
fs.WithFile(internal.SettingsFileName, ""))
|
fs.WithFile(internal.SettingsFileName, ""))
|
||||||
defer dir.Remove()
|
defer dir.Remove()
|
||||||
err := Validate(types.NewApp(dir.Path()), nil)
|
app, err := types.NewAppFromDefaultFiles(dir.Path())
|
||||||
|
assert.NilError(t, err)
|
||||||
|
err = Validate(app, nil)
|
||||||
assert.Error(t, err, "failed to load Compose file: unknown-property Additional property unknown-property is not allowed")
|
assert.Error(t, err, "failed to load Compose file: unknown-property Additional property unknown-property is not allowed")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -90,6 +85,8 @@ services:
|
||||||
fs.WithFile(internal.ComposeFileName, composeFile),
|
fs.WithFile(internal.ComposeFileName, composeFile),
|
||||||
fs.WithFile(internal.SettingsFileName, settings))
|
fs.WithFile(internal.SettingsFileName, settings))
|
||||||
defer dir.Remove()
|
defer dir.Remove()
|
||||||
err := Validate(types.NewApp(dir.Path()), nil)
|
app, err := types.NewAppFromDefaultFiles(dir.Path())
|
||||||
|
assert.NilError(t, err)
|
||||||
|
err = Validate(app, nil)
|
||||||
assert.NilError(t, err)
|
assert.NilError(t, err)
|
||||||
}
|
}
|
||||||
|
|
22
lib/lib.go
22
lib/lib.go
|
@ -1,22 +0,0 @@
|
||||||
package lib
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/docker/app/internal/packager"
|
|
||||||
"github.com/docker/app/internal/render"
|
|
||||||
"github.com/docker/app/internal/types"
|
|
||||||
yaml "gopkg.in/yaml.v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Render renders the application into a Compose file.
|
|
||||||
func Render(appname string, settingsFiles []string, settings map[string]string) ([]byte, error) {
|
|
||||||
app, err := packager.Extract(appname, types.WithSettingsFiles(settingsFiles...))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer app.Cleanup()
|
|
||||||
rendered, err := render.Render(app, settings)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return yaml.Marshal(rendered)
|
|
||||||
}
|
|
|
@ -0,0 +1,80 @@
|
||||||
|
package loader
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/docker/app/internal"
|
||||||
|
"github.com/docker/app/types"
|
||||||
|
"github.com/docker/docker/pkg/archive"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LoadFromSingleFile loads a docker app from a single-file format (as a reader)
|
||||||
|
func LoadFromSingleFile(path string, r io.Reader, ops ...func(*types.App) error) (*types.App, error) {
|
||||||
|
data, err := ioutil.ReadAll(r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "error reading single-file")
|
||||||
|
}
|
||||||
|
parts := strings.Split(string(data), types.SingleFileSeparator)
|
||||||
|
if len(parts) != 3 {
|
||||||
|
return nil, errors.Errorf("malformed single-file application: expected 3 documents, got %d", len(parts))
|
||||||
|
}
|
||||||
|
// 0. is metadata
|
||||||
|
metadata := strings.NewReader(parts[0])
|
||||||
|
// 1. is compose
|
||||||
|
compose := strings.NewReader(parts[1])
|
||||||
|
// 2. is settings
|
||||||
|
setting := strings.NewReader(parts[2])
|
||||||
|
appOps := append([]func(*types.App) error{
|
||||||
|
types.WithComposes(compose),
|
||||||
|
types.WithSettings(setting),
|
||||||
|
types.Metadata(metadata),
|
||||||
|
}, ops...)
|
||||||
|
return types.NewApp(path, appOps...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadFromDirectory loads a docker app from a directory
|
||||||
|
func LoadFromDirectory(path string, ops ...func(*types.App) error) (*types.App, error) {
|
||||||
|
appOps := append([]func(*types.App) error{
|
||||||
|
types.MetadataFile(filepath.Join(path, internal.MetadataFileName)),
|
||||||
|
types.WithComposeFiles(filepath.Join(path, internal.ComposeFileName)),
|
||||||
|
types.WithSettingsFiles(filepath.Join(path, internal.SettingsFileName)),
|
||||||
|
}, ops...)
|
||||||
|
return types.NewApp(path, appOps...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadFromTar loads a docker app from a tarball
|
||||||
|
func LoadFromTar(tar string, ops ...func(*types.App) error) (*types.App, error) {
|
||||||
|
f, err := os.Open(tar)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "cannot load app from tar")
|
||||||
|
}
|
||||||
|
appOps := append(ops, types.WithPath(tar))
|
||||||
|
return LoadFromTarReader(f, appOps...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadFromTarReader loads a docker app from a tarball reader
|
||||||
|
func LoadFromTarReader(r io.Reader, ops ...func(*types.App) error) (*types.App, error) {
|
||||||
|
dir, err := ioutil.TempDir("", "load-from-tar")
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "cannot load app from tar")
|
||||||
|
}
|
||||||
|
if err := archive.Untar(r, dir, &archive.TarOptions{
|
||||||
|
NoLchown: true,
|
||||||
|
}); err != nil {
|
||||||
|
if err := os.RemoveAll(dir); err != nil {
|
||||||
|
return nil, errors.Wrap(err, "cannot remove temporary folder")
|
||||||
|
}
|
||||||
|
return nil, errors.Wrap(err, "cannot load app from tar")
|
||||||
|
}
|
||||||
|
appOps := append([]func(*types.App) error{
|
||||||
|
types.WithCleanup(func() {
|
||||||
|
os.RemoveAll(dir)
|
||||||
|
}),
|
||||||
|
}, ops...)
|
||||||
|
return LoadFromDirectory(dir, appOps...)
|
||||||
|
}
|
|
@ -0,0 +1,117 @@
|
||||||
|
package loader
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/docker/app/internal"
|
||||||
|
"github.com/docker/app/types"
|
||||||
|
"github.com/docker/docker/pkg/archive"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
|
"gotest.tools/assert"
|
||||||
|
is "gotest.tools/assert/cmp"
|
||||||
|
"gotest.tools/fs"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
metadata = "foo"
|
||||||
|
yaml = `version: "3.1"
|
||||||
|
|
||||||
|
services:
|
||||||
|
web:
|
||||||
|
image: nginx`
|
||||||
|
settings = "foo=bar"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestLoadFromSingleFile(t *testing.T) {
|
||||||
|
singlefile := fmt.Sprintf(`%s
|
||||||
|
---
|
||||||
|
%s
|
||||||
|
---
|
||||||
|
%s`, metadata, yaml, settings)
|
||||||
|
app, err := LoadFromSingleFile("my-app", strings.NewReader(singlefile))
|
||||||
|
assert.NilError(t, err)
|
||||||
|
assert.Assert(t, app != nil)
|
||||||
|
assert.Assert(t, is.Equal(app.Path, "my-app"))
|
||||||
|
assertAppContent(t, app)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadFromSingleFileInvalidReader(t *testing.T) {
|
||||||
|
_, err := LoadFromSingleFile("my-app", &faultyReader{})
|
||||||
|
assert.ErrorContains(t, err, "faulty reader")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadFromSingleFileMalformed(t *testing.T) {
|
||||||
|
_, err := LoadFromSingleFile("my-app", strings.NewReader(`foo
|
||||||
|
---
|
||||||
|
bar`))
|
||||||
|
assert.ErrorContains(t, err, "malformed single-file application")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadFromDirectory(t *testing.T) {
|
||||||
|
dir := fs.NewDir(t, "my-app",
|
||||||
|
fs.WithFile(internal.MetadataFileName, metadata),
|
||||||
|
fs.WithFile(internal.SettingsFileName, settings),
|
||||||
|
fs.WithFile(internal.ComposeFileName, yaml),
|
||||||
|
)
|
||||||
|
defer dir.Remove()
|
||||||
|
app, err := LoadFromDirectory(dir.Path())
|
||||||
|
assert.NilError(t, err)
|
||||||
|
assert.Assert(t, app != nil)
|
||||||
|
assert.Assert(t, is.Equal(app.Path, dir.Path()))
|
||||||
|
assertAppContent(t, app)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadFromTarInexistent(t *testing.T) {
|
||||||
|
_, err := LoadFromTar("any-tar.tar")
|
||||||
|
assert.ErrorContains(t, err, "open any-tar.tar")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadFromTar(t *testing.T) {
|
||||||
|
myapp := createAppTar(t)
|
||||||
|
defer myapp.Remove()
|
||||||
|
app, err := LoadFromTar(myapp.Path())
|
||||||
|
assert.NilError(t, err)
|
||||||
|
assert.Assert(t, app != nil)
|
||||||
|
assert.Assert(t, is.Equal(app.Path, myapp.Path()))
|
||||||
|
assertAppContent(t, app)
|
||||||
|
}
|
||||||
|
|
||||||
|
func createAppTar(t *testing.T) *fs.File {
|
||||||
|
t.Helper()
|
||||||
|
dir := fs.NewDir(t, "my-app",
|
||||||
|
fs.WithFile(internal.MetadataFileName, metadata),
|
||||||
|
fs.WithFile(internal.SettingsFileName, settings),
|
||||||
|
fs.WithFile(internal.ComposeFileName, yaml),
|
||||||
|
)
|
||||||
|
defer dir.Remove()
|
||||||
|
r, err := archive.TarWithOptions(dir.Path(), &archive.TarOptions{
|
||||||
|
Compression: archive.Uncompressed,
|
||||||
|
})
|
||||||
|
assert.NilError(t, err)
|
||||||
|
data, err := ioutil.ReadAll(r)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
return fs.NewFile(t, "app", fs.WithBytes(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertContentIs(t *testing.T, actual []byte, expected string) {
|
||||||
|
t.Helper()
|
||||||
|
assert.Assert(t, is.Equal(string(actual), expected))
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertAppContent(t *testing.T, app *types.App) {
|
||||||
|
assert.Assert(t, is.Len(app.Settings(), 1))
|
||||||
|
assertContentIs(t, app.Settings()[0], settings)
|
||||||
|
assert.Assert(t, is.Len(app.Composes(), 1))
|
||||||
|
assertContentIs(t, app.Composes()[0], yaml)
|
||||||
|
assertContentIs(t, app.Metadata(), metadata)
|
||||||
|
}
|
||||||
|
|
||||||
|
type faultyReader struct{}
|
||||||
|
|
||||||
|
func (r *faultyReader) Read(_ []byte) (int, error) {
|
||||||
|
return 0, errors.New("faulty reader")
|
||||||
|
}
|
|
@ -2,15 +2,15 @@ package render
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
"os"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/docker/app/internal/compose"
|
||||||
"github.com/docker/app/internal/renderer"
|
"github.com/docker/app/internal/renderer"
|
||||||
"github.com/docker/app/internal/settings"
|
"github.com/docker/app/internal/settings"
|
||||||
"github.com/docker/app/internal/slices"
|
"github.com/docker/app/internal/slices"
|
||||||
"github.com/docker/app/internal/types"
|
"github.com/docker/app/types"
|
||||||
"github.com/docker/cli/cli/compose/loader"
|
"github.com/docker/cli/cli/compose/loader"
|
||||||
composetemplate "github.com/docker/cli/cli/compose/template"
|
composetemplate "github.com/docker/cli/cli/compose/template"
|
||||||
composetypes "github.com/docker/cli/cli/compose/types"
|
composetypes "github.com/docker/cli/cli/compose/types"
|
||||||
|
@ -39,15 +39,15 @@ var (
|
||||||
|
|
||||||
// Render renders the Compose file for this app, merging in settings files, other compose files, and env
|
// Render renders the Compose file for this app, merging in settings files, other compose files, and env
|
||||||
// appname string, composeFiles []string, settingsFiles []string
|
// appname string, composeFiles []string, settingsFiles []string
|
||||||
func Render(app types.App, env map[string]string) (*composetypes.Config, error) {
|
func Render(app *types.App, env map[string]string) (*composetypes.Config, error) {
|
||||||
// prepend the app settings to the argument settings
|
// prepend the app settings to the argument settings
|
||||||
// load the settings into a struct
|
// load the settings into a struct
|
||||||
fileSettings, err := settings.LoadFiles(app.SettingsFiles)
|
fileSettings, err := settings.LoadMultiple(app.Settings())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrap(err, "failed to load settings")
|
return nil, errors.Wrap(err, "failed to load settings")
|
||||||
}
|
}
|
||||||
// inject our metadata
|
// inject our metadata
|
||||||
metaPrefixed, err := settings.LoadFile(app.MetadataFile, settings.WithPrefix("app"))
|
metaPrefixed, err := settings.Load(app.Metadata(), settings.WithPrefix("app"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -70,21 +70,11 @@ func Render(app types.App, env map[string]string) (*composetypes.Config, error)
|
||||||
}
|
}
|
||||||
renderers = rl
|
renderers = rl
|
||||||
}
|
}
|
||||||
configFiles := []composetypes.ConfigFile{}
|
configFiles, err := compose.Load(app.Composes(), func(data string) (string, error) {
|
||||||
for _, c := range app.ComposeFiles {
|
return renderer.Apply(data, allSettings, renderers...)
|
||||||
data, err := ioutil.ReadFile(c)
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.Wrapf(err, "failed to read Compose file %s", c)
|
return nil, errors.Wrap(err, "failed to load composefiles")
|
||||||
}
|
|
||||||
s, err := renderer.Apply(string(data), allSettings, renderers...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
parsed, err := loader.ParseYAML([]byte(s))
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.Wrapf(err, "failed to parse Compose file %s", c)
|
|
||||||
}
|
|
||||||
configFiles = append(configFiles, composetypes.ConfigFile{Config: parsed})
|
|
||||||
}
|
}
|
||||||
return render(configFiles, allSettings.Flatten())
|
return render(configFiles, allSettings.Flatten())
|
||||||
}
|
}
|
|
@ -0,0 +1,175 @@
|
||||||
|
package types
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/docker/app/internal"
|
||||||
|
)
|
||||||
|
|
||||||
|
const SingleFileSeparator = "\n---\n"
|
||||||
|
|
||||||
|
// App represents an app
|
||||||
|
type App struct {
|
||||||
|
Name string
|
||||||
|
Path string
|
||||||
|
Cleanup func()
|
||||||
|
|
||||||
|
composesContent [][]byte
|
||||||
|
settingsContent [][]byte
|
||||||
|
metadataContent []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// Composes returns compose files content
|
||||||
|
func (a *App) Composes() [][]byte {
|
||||||
|
return a.composesContent
|
||||||
|
}
|
||||||
|
|
||||||
|
// Settings returns setting files content
|
||||||
|
func (a *App) Settings() [][]byte {
|
||||||
|
return a.settingsContent
|
||||||
|
}
|
||||||
|
|
||||||
|
// Metadata returns metadata file content
|
||||||
|
func (a *App) Metadata() []byte {
|
||||||
|
return a.metadataContent
|
||||||
|
}
|
||||||
|
|
||||||
|
func noop() {}
|
||||||
|
|
||||||
|
// NewApp creates a new docker app with the specified path and struct modifiers
|
||||||
|
func NewApp(path string, ops ...func(*App) error) (*App, error) {
|
||||||
|
app := &App{
|
||||||
|
Name: path,
|
||||||
|
Path: path,
|
||||||
|
Cleanup: noop,
|
||||||
|
|
||||||
|
composesContent: [][]byte{},
|
||||||
|
settingsContent: [][]byte{},
|
||||||
|
metadataContent: []byte{},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, op := range ops {
|
||||||
|
if err := op(app); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return app, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewAppFromDefaultFiles creates a new docker app using the default files in the specified path.
|
||||||
|
// If one of those file doesn't exists, it will error out.
|
||||||
|
func NewAppFromDefaultFiles(path string, ops ...func(*App) error) (*App, error) {
|
||||||
|
appOps := append([]func(*App) error{
|
||||||
|
MetadataFile(filepath.Join(path, internal.MetadataFileName)),
|
||||||
|
WithComposeFiles(filepath.Join(path, internal.ComposeFileName)),
|
||||||
|
WithSettingsFiles(filepath.Join(path, internal.SettingsFileName)),
|
||||||
|
}, ops...)
|
||||||
|
return NewApp(path, appOps...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithName sets the application name
|
||||||
|
func WithName(name string) func(*App) error {
|
||||||
|
return func(app *App) error {
|
||||||
|
app.Name = name
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithPath sets the original path of the app
|
||||||
|
func WithPath(path string) func(*App) error {
|
||||||
|
return func(app *App) error {
|
||||||
|
app.Path = path
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithCleanup sets the cleanup function of the app
|
||||||
|
func WithCleanup(f func()) func(*App) error {
|
||||||
|
return func(app *App) error {
|
||||||
|
app.Cleanup = f
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithSettingsFiles adds the specified settings files to the app
|
||||||
|
func WithSettingsFiles(files ...string) func(*App) error {
|
||||||
|
return func(app *App) error {
|
||||||
|
for _, file := range files {
|
||||||
|
d, err := ioutil.ReadFile(file)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
app.settingsContent = append(app.settingsContent, d)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithSettings adds the specified settings readers to the app
|
||||||
|
func WithSettings(readers ...io.Reader) func(*App) error {
|
||||||
|
return func(app *App) error {
|
||||||
|
for _, r := range readers {
|
||||||
|
d, err := ioutil.ReadAll(r)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
app.settingsContent = append(app.settingsContent, d)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MetadataFile adds the specified metadata file to the app
|
||||||
|
func MetadataFile(file string) func(*App) error {
|
||||||
|
return func(app *App) error {
|
||||||
|
d, err := ioutil.ReadFile(file)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
app.metadataContent = d
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Metadata adds the specified metadata reader to the app
|
||||||
|
func Metadata(r io.Reader) func(*App) error {
|
||||||
|
return func(app *App) error {
|
||||||
|
d, err := ioutil.ReadAll(r)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
app.metadataContent = d
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithComposeFiles adds the specified compose files to the app
|
||||||
|
func WithComposeFiles(files ...string) func(*App) error {
|
||||||
|
return func(app *App) error {
|
||||||
|
for _, file := range files {
|
||||||
|
d, err := ioutil.ReadFile(file)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
app.composesContent = append(app.composesContent, d)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithComposes adds the specified compose readers to the app
|
||||||
|
func WithComposes(readers ...io.Reader) func(*App) error {
|
||||||
|
return func(app *App) error {
|
||||||
|
for _, r := range readers {
|
||||||
|
d, err := ioutil.ReadAll(r)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
app.composesContent = append(app.composesContent, d)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,146 @@
|
||||||
|
package types
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/docker/app/internal"
|
||||||
|
|
||||||
|
"gotest.tools/assert"
|
||||||
|
is "gotest.tools/assert/cmp"
|
||||||
|
"gotest.tools/fs"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
yaml = `version: "3.0"
|
||||||
|
services:
|
||||||
|
web:
|
||||||
|
image: nginx`
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewApp(t *testing.T) {
|
||||||
|
app, err := NewApp("any-app")
|
||||||
|
assert.NilError(t, err)
|
||||||
|
assert.Assert(t, is.Equal(app.Path, "any-app"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewAppFromDefaultFiles(t *testing.T) {
|
||||||
|
dir := fs.NewDir(t, "my-app",
|
||||||
|
fs.WithFile(internal.MetadataFileName, "foo"),
|
||||||
|
fs.WithFile(internal.SettingsFileName, "foo=bar"),
|
||||||
|
fs.WithFile(internal.ComposeFileName, yaml),
|
||||||
|
)
|
||||||
|
defer dir.Remove()
|
||||||
|
app, err := NewAppFromDefaultFiles(dir.Path())
|
||||||
|
assert.NilError(t, err)
|
||||||
|
assert.Assert(t, is.Len(app.Settings(), 1))
|
||||||
|
assertContentIs(t, app.Settings()[0], "foo=bar")
|
||||||
|
assert.Assert(t, is.Len(app.Composes(), 1))
|
||||||
|
assertContentIs(t, app.Composes()[0], yaml)
|
||||||
|
assertContentIs(t, app.Metadata(), "foo")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewAppWithOpError(t *testing.T) {
|
||||||
|
_, err := NewApp("any-app", func(_ *App) error { return errors.New("error creating") })
|
||||||
|
assert.ErrorContains(t, err, "error creating")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWithPath(t *testing.T) {
|
||||||
|
app := &App{Path: "any-app"}
|
||||||
|
err := WithPath("any-path")(app)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
assert.Assert(t, is.Equal(app.Path, "any-path"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWithCleanup(t *testing.T) {
|
||||||
|
app := &App{Path: "any-app"}
|
||||||
|
err := WithCleanup(func() {})(app)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
assert.Assert(t, app.Cleanup != nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWithSettingsFilesError(t *testing.T) {
|
||||||
|
app := &App{Path: "any-app"}
|
||||||
|
err := WithSettingsFiles("any-settings-file")(app)
|
||||||
|
assert.ErrorContains(t, err, "open any-settings-file")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWithSettingsFiles(t *testing.T) {
|
||||||
|
dir := fs.NewDir(t, "settings",
|
||||||
|
fs.WithFile("my-settings-file", "foo"),
|
||||||
|
)
|
||||||
|
defer dir.Remove()
|
||||||
|
app := &App{Path: "my-app"}
|
||||||
|
err := WithSettingsFiles(dir.Join("my-settings-file"))(app)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
assert.Assert(t, is.Len(app.Settings(), 1))
|
||||||
|
assertContentIs(t, app.Settings()[0], "foo")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWithSettings(t *testing.T) {
|
||||||
|
r := strings.NewReader("foo")
|
||||||
|
app := &App{Path: "my-app"}
|
||||||
|
err := WithSettings(r)(app)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
assert.Assert(t, is.Len(app.Settings(), 1))
|
||||||
|
assertContentIs(t, app.Settings()[0], "foo")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWithComposeFilesError(t *testing.T) {
|
||||||
|
app := &App{Path: "any-app"}
|
||||||
|
err := WithComposeFiles("any-compose-file")(app)
|
||||||
|
assert.ErrorContains(t, err, "open any-compose-file")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWithComposeFiles(t *testing.T) {
|
||||||
|
dir := fs.NewDir(t, "composes",
|
||||||
|
fs.WithFile("my-compose-file", yaml),
|
||||||
|
)
|
||||||
|
defer dir.Remove()
|
||||||
|
app := &App{Path: "my-app"}
|
||||||
|
err := WithComposeFiles(dir.Join("my-compose-file"))(app)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
assert.Assert(t, is.Len(app.Composes(), 1))
|
||||||
|
assertContentIs(t, app.Composes()[0], yaml)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWithComposes(t *testing.T) {
|
||||||
|
r := strings.NewReader(yaml)
|
||||||
|
app := &App{Path: "my-app"}
|
||||||
|
err := WithComposes(r)(app)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
assert.Assert(t, is.Len(app.Composes(), 1))
|
||||||
|
assertContentIs(t, app.Composes()[0], yaml)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMetadataFileError(t *testing.T) {
|
||||||
|
app := &App{Path: "any-app"}
|
||||||
|
err := MetadataFile("any-metadata-file")(app)
|
||||||
|
assert.ErrorContains(t, err, "open any-metadata-file")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMetadataFile(t *testing.T) {
|
||||||
|
dir := fs.NewDir(t, "metadata",
|
||||||
|
fs.WithFile("my-metadata-file", "foo"),
|
||||||
|
)
|
||||||
|
defer dir.Remove()
|
||||||
|
app := &App{Path: "my-app"}
|
||||||
|
err := MetadataFile(dir.Join("my-metadata-file"))(app)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
assert.Assert(t, app.Metadata() != nil)
|
||||||
|
assertContentIs(t, app.Metadata(), "foo")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMetadata(t *testing.T) {
|
||||||
|
r := strings.NewReader("foo")
|
||||||
|
app := &App{Path: "my-app"}
|
||||||
|
err := Metadata(r)(app)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
assertContentIs(t, app.Metadata(), "foo")
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertContentIs(t *testing.T, data []byte, expected string) {
|
||||||
|
t.Helper()
|
||||||
|
assert.Assert(t, is.Equal(string(data), expected))
|
||||||
|
}
|
Загрузка…
Ссылка в новой задаче