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 (
|
||||
"github.com/docker/app/internal"
|
||||
"github.com/docker/app/internal/packager"
|
||||
"github.com/docker/app/internal/render"
|
||||
"github.com/docker/app/internal/types"
|
||||
"github.com/docker/app/render"
|
||||
"github.com/docker/app/types"
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"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
|
||||
if stackName == "" {
|
||||
stackName = internal.AppNameFromDir(app.Path)
|
||||
stackName = internal.AppNameFromDir(app.Name)
|
||||
}
|
||||
return stack.RunDeploy(dockerCli, flags, rendered, deployOrchestrator, options.Deploy{
|
||||
Namespace: stackName,
|
||||
|
|
|
@ -6,7 +6,7 @@ import (
|
|||
"github.com/docker/app/internal"
|
||||
"github.com/docker/app/internal/helm"
|
||||
"github.com/docker/app/internal/packager"
|
||||
"github.com/docker/app/internal/types"
|
||||
"github.com/docker/app/types"
|
||||
"github.com/docker/cli/cli"
|
||||
cliopts "github.com/docker/cli/opts"
|
||||
"github.com/spf13/cobra"
|
||||
|
|
|
@ -6,8 +6,8 @@ import (
|
|||
"github.com/docker/app/internal"
|
||||
"github.com/docker/app/internal/image"
|
||||
"github.com/docker/app/internal/packager"
|
||||
"github.com/docker/app/internal/render"
|
||||
"github.com/docker/app/internal/types"
|
||||
"github.com/docker/app/render"
|
||||
"github.com/docker/app/types"
|
||||
cliopts "github.com/docker/cli/opts"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
@ -41,7 +41,7 @@ subdirectory.`,
|
|||
if err != nil {
|
||||
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
|
||||
}
|
||||
// check if source was a tarball
|
||||
|
@ -60,7 +60,7 @@ subdirectory.`,
|
|||
return err
|
||||
}
|
||||
// source was a tarball, rebuild it
|
||||
return packager.Pack(app.Path, target)
|
||||
return packager.Pack(app.Name, target)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
|
|
|
@ -17,7 +17,7 @@ func imageLoadCmd() *cobra.Command {
|
|||
return err
|
||||
}
|
||||
defer app.Cleanup()
|
||||
return image.Load(app.Path, args[1:])
|
||||
return image.Load(app.Name, args[1:])
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"github.com/docker/app/internal/inspect"
|
||||
"github.com/docker/app/internal/packager"
|
||||
"github.com/docker/app/internal/render"
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/spf13/cobra"
|
||||
|
@ -20,7 +20,7 @@ func inspectCmd(dockerCli command.Cli) *cobra.Command {
|
|||
return err
|
||||
}
|
||||
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")
|
||||
}
|
||||
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
|
||||
if mergeOutputFile == "-" {
|
||||
|
@ -70,7 +70,7 @@ func mergeCmd(dockerCli command.Cli) *cobra.Command {
|
|||
return err
|
||||
}
|
||||
}
|
||||
if err := packager.Merge(extractedApp.Path, target); err != nil {
|
||||
if err := packager.Merge(extractedApp, target); err != nil {
|
||||
return err
|
||||
}
|
||||
if mergeOutputFile != "-" {
|
||||
|
@ -78,10 +78,10 @@ func mergeCmd(dockerCli command.Cli) *cobra.Command {
|
|||
target.(io.WriteCloser).Close()
|
||||
}
|
||||
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")
|
||||
}
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,7 +18,12 @@ func pushCmd() *cobra.Command {
|
|||
Short: "Push the application to a registry",
|
||||
Args: cli.RequiresMaxArgs(1),
|
||||
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)")
|
||||
|
|
|
@ -6,8 +6,8 @@ import (
|
|||
|
||||
"github.com/docker/app/internal"
|
||||
"github.com/docker/app/internal/packager"
|
||||
"github.com/docker/app/internal/render"
|
||||
"github.com/docker/app/internal/types"
|
||||
"github.com/docker/app/render"
|
||||
"github.com/docker/app/types"
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
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)",
|
||||
Args: cli.RequiresMaxArgs(1),
|
||||
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 {
|
||||
fmt.Fprintf(dockerCli.Out(), "Saved application as image: %s\n", imageName)
|
||||
}
|
||||
|
|
|
@ -24,16 +24,16 @@ func splitCmd() *cobra.Command {
|
|||
defer extractedApp.Cleanup()
|
||||
inPlace := splitOutputDir == ""
|
||||
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
|
||||
}
|
||||
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")
|
||||
}
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,8 +2,8 @@ package main
|
|||
|
||||
import (
|
||||
"github.com/docker/app/internal/packager"
|
||||
"github.com/docker/app/internal/types"
|
||||
"github.com/docker/app/internal/validator"
|
||||
"github.com/docker/app/types"
|
||||
"github.com/docker/cli/cli"
|
||||
cliopts "github.com/docker/cli/opts"
|
||||
"github.com/spf13/cobra"
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
package e2e
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"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) {
|
||||
getDockerAppBinary(t)
|
||||
composeData := `version: "3.2"
|
||||
|
@ -119,11 +108,11 @@ maintainers:
|
|||
email: joe@joe.com
|
||||
`
|
||||
envData := "# some comment\nNGINX_VERSION=latest"
|
||||
inputDir := randomName("app_input_")
|
||||
os.Mkdir(inputDir, 0755)
|
||||
ioutil.WriteFile(filepath.Join(inputDir, internal.ComposeFileName), []byte(composeData), 0644)
|
||||
ioutil.WriteFile(filepath.Join(inputDir, ".env"), []byte(envData), 0644)
|
||||
defer os.RemoveAll(inputDir)
|
||||
dir := fs.NewDir(t, "app_input",
|
||||
fs.WithFile(internal.ComposeFileName, composeData),
|
||||
fs.WithFile(".env", envData),
|
||||
)
|
||||
defer dir.Remove()
|
||||
|
||||
testAppName := "app-test"
|
||||
dirName := internal.DirNameFromAppName(testAppName)
|
||||
|
@ -133,7 +122,7 @@ maintainers:
|
|||
"init",
|
||||
testAppName,
|
||||
"-c",
|
||||
filepath.Join(inputDir, internal.ComposeFileName),
|
||||
dir.Join(internal.ComposeFileName),
|
||||
"-d",
|
||||
"my cool app",
|
||||
"-m", "bob",
|
||||
|
@ -157,7 +146,7 @@ maintainers:
|
|||
"init",
|
||||
"tac",
|
||||
"-c",
|
||||
filepath.Join(inputDir, internal.ComposeFileName),
|
||||
dir.Join(internal.ComposeFileName),
|
||||
"-d",
|
||||
"my cool app",
|
||||
"-m", "bob",
|
||||
|
@ -166,7 +155,8 @@ maintainers:
|
|||
}
|
||||
assertCommand(t, dockerApp, args...)
|
||||
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")
|
||||
// Check various commands work on single-file app package
|
||||
assertCommand(t, dockerApp, "inspect", "tac")
|
||||
|
@ -179,11 +169,11 @@ func TestDetectAppBinary(t *testing.T) {
|
|||
assertCommand(t, dockerApp, "inspect")
|
||||
cwd, err := os.Getwd()
|
||||
assert.NilError(t, err)
|
||||
assert.NilError(t, os.Chdir("helm.dockerapp"))
|
||||
defer os.Chdir(cwd)
|
||||
os.Chdir("helm.dockerapp")
|
||||
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")
|
||||
}
|
||||
|
||||
|
@ -206,17 +196,17 @@ func TestPackBinary(t *testing.T) {
|
|||
assert.Assert(t, strings.Contains(result.Stdout(), "nginx"))
|
||||
cwd, err := os.Getwd()
|
||||
assert.NilError(t, err)
|
||||
os.Chdir(tempDir)
|
||||
assert.NilError(t, os.Chdir(tempDir))
|
||||
defer os.Chdir(cwd)
|
||||
result = icmd.RunCommand(dockerApp, "helm", "test")
|
||||
result.Assert(t, icmd.Success)
|
||||
_, err = os.Stat("test.chart/Chart.yaml")
|
||||
assert.NilError(t, err)
|
||||
os.Mkdir("output", 0755)
|
||||
assert.NilError(t, os.Mkdir("output", 0755))
|
||||
result = icmd.RunCommand(dockerApp, "unpack", "test", "-o", "output")
|
||||
result.Assert(t, icmd.Success)
|
||||
_, err = os.Stat("output/test.dockerapp/docker-compose.yml")
|
||||
assert.NilError(t, err)
|
||||
os.Chdir(cwd)
|
||||
}
|
||||
|
||||
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"
|
||||
|
||||
"github.com/docker/app/internal"
|
||||
"github.com/docker/app/internal/compose"
|
||||
"github.com/docker/app/internal/helm/templateconversion"
|
||||
"github.com/docker/app/internal/helm/templateloader"
|
||||
"github.com/docker/app/internal/helm/templatev1beta2"
|
||||
"github.com/docker/app/internal/render"
|
||||
"github.com/docker/app/internal/settings"
|
||||
"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/compose/loader"
|
||||
"github.com/docker/cli/cli/compose/template"
|
||||
"github.com/docker/cli/kubernetes/compose/v1beta1"
|
||||
"github.com/docker/cli/kubernetes/compose/v1beta2"
|
||||
"github.com/pkg/errors"
|
||||
|
@ -44,51 +44,47 @@ with the appropriate content (value or template)
|
|||
*/
|
||||
|
||||
// Helm renders an app as an Helm Chart
|
||||
func Helm(app types.App, env map[string]string, shouldRender bool, stackVersion string) error {
|
||||
targetDir := internal.AppNameFromDir(app.Path) + ".chart"
|
||||
func Helm(app *types.App, env map[string]string, shouldRender bool, stackVersion string) error {
|
||||
targetDir := internal.AppNameFromDir(app.Name) + ".chart"
|
||||
if err := os.MkdirAll(targetDir, 0755); err != nil {
|
||||
return errors.Wrap(err, "failed to create Chart directory")
|
||||
}
|
||||
err := makeChart(app.Path, targetDir)
|
||||
err := makeChart(app.Metadata(), targetDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if shouldRender {
|
||||
return helmRender(app, targetDir, env, stackVersion)
|
||||
}
|
||||
// FIXME(vdemeester) handle that
|
||||
data, err := ioutil.ReadFile(filepath.Join(app.Path, internal.ComposeFileName))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to read application Compose file")
|
||||
// FIXME(vdemeester) support multiple file for helm
|
||||
if len(app.Composes()) > 1 {
|
||||
return errors.New("helm rendering doesn't support multiple composefiles")
|
||||
}
|
||||
cfgMap, err := loader.ParseYAML(data)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to parse compose file")
|
||||
}
|
||||
vars := template.ExtractVariables(cfgMap, render.Pattern)
|
||||
data := app.Composes()[0]
|
||||
// FIXME(vdemeester): remove the need to create this slice
|
||||
variables := []string{}
|
||||
vars, err := compose.ExtractVariables(data, render.Pattern)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for k := range vars {
|
||||
variables = append(variables, k)
|
||||
}
|
||||
err = makeStack(app.Path, targetDir, data, stackVersion)
|
||||
err = makeStack(app.Name, targetDir, data, stackVersion)
|
||||
if err != nil {
|
||||
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
|
||||
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
|
||||
sf := []string{filepath.Join(appname, internal.SettingsFileName)}
|
||||
sf = append(sf, settingsFile...)
|
||||
s, err := settings.LoadFiles(sf)
|
||||
s, err := settings.LoadMultiple(app.Settings())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
metaFile := filepath.Join(appname, internal.MetadataFileName)
|
||||
metaPrefixed, err := settings.LoadFile(metaFile, settings.WithPrefix("app"))
|
||||
metaPrefixed, err := settings.Load(app.Metadata(), settings.WithPrefix("app"))
|
||||
if err != nil {
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
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)
|
||||
}
|
||||
|
||||
func makeChart(appname, 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")
|
||||
}
|
||||
func makeChart(metadata []byte, targetDir string) error {
|
||||
var meta types.AppMetadata
|
||||
err = yaml.Unmarshal(metaContent, &meta)
|
||||
err := yaml.Unmarshal(metadata, &meta)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to parse application metadata")
|
||||
}
|
||||
|
|
|
@ -1,35 +1,26 @@
|
|||
package render
|
||||
package inspect
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/docker/app/internal"
|
||||
"github.com/docker/app/internal/settings"
|
||||
"github.com/docker/app/internal/types"
|
||||
"github.com/docker/app/types"
|
||||
"github.com/pkg/errors"
|
||||
yaml "gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
// Inspect dumps the metadata of an app
|
||||
func Inspect(out io.Writer, appname 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")
|
||||
}
|
||||
func Inspect(out io.Writer, app *types.App) error {
|
||||
var meta types.AppMetadata
|
||||
err = yaml.Unmarshal(metaContent, &meta)
|
||||
err := yaml.Unmarshal(app.Metadata(), &meta)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to parse application metadata")
|
||||
}
|
||||
// extract settings
|
||||
settingsFile := filepath.Join(appname, internal.SettingsFileName)
|
||||
s, err := settings.LoadFile(settingsFile)
|
||||
s, err := settings.LoadMultiple(app.Settings())
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to load application settings")
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package render
|
||||
package inspect
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
@ -7,6 +7,7 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/docker/app/internal"
|
||||
"github.com/docker/app/types"
|
||||
|
||||
"gotest.tools/assert"
|
||||
is "gotest.tools/assert/cmp"
|
||||
|
@ -14,16 +15,23 @@ import (
|
|||
"gotest.tools/golden"
|
||||
)
|
||||
|
||||
const (
|
||||
composeYAML = `version: "3.1"
|
||||
|
||||
services:
|
||||
web:
|
||||
image: nginx`
|
||||
)
|
||||
|
||||
func TestInspectErrorsOnFiles(t *testing.T) {
|
||||
dir := fs.NewDir(t, "inspect-errors",
|
||||
fs.WithDir("empty-app"),
|
||||
fs.WithDir("unparseable-metadata-app",
|
||||
fs.WithFile(internal.ComposeFileName, composeYAML),
|
||||
fs.WithFile(internal.MetadataFileName, `something is wrong`),
|
||||
),
|
||||
fs.WithDir("no-settings-app",
|
||||
fs.WithFile(internal.MetadataFileName, `{}`),
|
||||
fs.WithFile(internal.SettingsFileName, "foo"),
|
||||
),
|
||||
fs.WithDir("unparseable-settings-app",
|
||||
fs.WithFile(internal.ComposeFileName, composeYAML),
|
||||
fs.WithFile(internal.MetadataFileName, `{}`),
|
||||
fs.WithFile(internal.SettingsFileName, "foo"),
|
||||
),
|
||||
|
@ -31,13 +39,12 @@ func TestInspectErrorsOnFiles(t *testing.T) {
|
|||
defer dir.Remove()
|
||||
|
||||
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",
|
||||
"no-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))
|
||||
}
|
||||
}
|
||||
|
@ -45,12 +52,14 @@ func TestInspectErrorsOnFiles(t *testing.T) {
|
|||
func TestInspect(t *testing.T) {
|
||||
dir := fs.NewDir(t, "inspect",
|
||||
fs.WithDir("no-maintainers",
|
||||
fs.WithFile(internal.ComposeFileName, composeYAML),
|
||||
fs.WithFile(internal.MetadataFileName, `
|
||||
version: 0.1.0
|
||||
name: foo`),
|
||||
fs.WithFile(internal.SettingsFileName, ``),
|
||||
),
|
||||
fs.WithDir("no-description",
|
||||
fs.WithFile(internal.ComposeFileName, composeYAML),
|
||||
fs.WithFile(internal.MetadataFileName, `
|
||||
version: 0.1.0
|
||||
name: foo
|
||||
|
@ -60,6 +69,7 @@ maintainers:
|
|||
fs.WithFile(internal.SettingsFileName, ""),
|
||||
),
|
||||
fs.WithDir("no-settings",
|
||||
fs.WithFile(internal.ComposeFileName, composeYAML),
|
||||
fs.WithFile(internal.MetadataFileName, `
|
||||
version: 0.1.0
|
||||
name: foo
|
||||
|
@ -70,6 +80,7 @@ description: "this is sparta !"`),
|
|||
fs.WithFile(internal.SettingsFileName, ""),
|
||||
),
|
||||
fs.WithDir("full",
|
||||
fs.WithFile(internal.ComposeFileName, composeYAML),
|
||||
fs.WithFile(internal.MetadataFileName, `
|
||||
version: 0.1.0
|
||||
name: foo
|
||||
|
@ -88,8 +99,10 @@ text: hello`),
|
|||
"no-maintainers", "no-description", "no-settings", "full",
|
||||
} {
|
||||
outBuffer := new(bytes.Buffer)
|
||||
err := Inspect(outBuffer, dir.Join(appname))
|
||||
app, err := types.NewAppFromDefaultFiles(dir.Join(appname))
|
||||
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
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
|
@ -11,14 +9,11 @@ import (
|
|||
"strings"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
var (
|
||||
noop = func() {}
|
||||
)
|
||||
|
||||
// findApp looks for an app in CWD or subdirs
|
||||
func findApp() (string, error) {
|
||||
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
|
||||
func extractImage(appname string) (types.App, error) {
|
||||
func extractImage(appname string, ops ...func(*types.App) error) (*types.App, error) {
|
||||
var imagename string
|
||||
if strings.Contains(appname, ":") {
|
||||
nametag := strings.Split(appname, ":")
|
||||
|
@ -66,161 +61,67 @@ func extractImage(appname string) (types.App, error) {
|
|||
}
|
||||
tempDir, err := ioutil.TempDir("", "dockerapp")
|
||||
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)
|
||||
err = Load(imagename, tempDir)
|
||||
if err != nil {
|
||||
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
|
||||
cmd := exec.Command("docker", "pull", imagename)
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Stdout = os.Stdout
|
||||
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 {
|
||||
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
|
||||
app, err := Extract(filepath.Join(tempDir, appname))
|
||||
return types.App{
|
||||
Path: app.Path,
|
||||
Cleanup: app.Cleanup,
|
||||
}, err
|
||||
return loader.LoadFromTar(filepath.Join(tempDir, appname), ops...)
|
||||
}
|
||||
|
||||
// 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
|
||||
// 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
|
||||
func Extract(appname string, ops ...func(*types.App)) (types.App, error) {
|
||||
if appname == "" {
|
||||
func Extract(name string, ops ...func(*types.App) error) (*types.App, error) {
|
||||
if name == "" {
|
||||
var err error
|
||||
if appname, err = findApp(); err != nil {
|
||||
return types.App{}, err
|
||||
if name, err = findApp(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if appname == "." {
|
||||
if name == "." {
|
||||
var err error
|
||||
if appname, err = os.Getwd(); err != nil {
|
||||
return types.App{}, errors.Wrap(err, "cannot resolve current working directory")
|
||||
if name, err = os.Getwd(); err != nil {
|
||||
return nil, errors.Wrap(err, "cannot resolve current working directory")
|
||||
}
|
||||
}
|
||||
originalAppname := appname
|
||||
appname = filepath.Clean(appname)
|
||||
// try appending our extension
|
||||
appname = internal.DirNameFromAppName(appname)
|
||||
ops = append(ops, types.WithName(name))
|
||||
appname := internal.DirNameFromAppName(name)
|
||||
s, err := os.Stat(appname)
|
||||
if err != nil {
|
||||
// try verbatim
|
||||
s, err = os.Stat(originalAppname)
|
||||
}
|
||||
if err != nil {
|
||||
// look for a docker image
|
||||
return extractImage(originalAppname)
|
||||
return extractImage(name, ops...)
|
||||
}
|
||||
if s.IsDir() {
|
||||
// directory: already decompressed
|
||||
ops = append([]func(*types.App){
|
||||
types.WithOriginalPath(appname),
|
||||
types.WithCleanup(noop),
|
||||
}, ops...)
|
||||
return types.NewApp(appname, ops...), nil
|
||||
appOpts := append(ops,
|
||||
types.WithPath(appname),
|
||||
)
|
||||
return loader.LoadFromDirectory(appname, appOpts...)
|
||||
}
|
||||
// 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 {
|
||||
return types.App{}, errors.Wrap(err, "failed to create temporary directory")
|
||||
}
|
||||
defer func() {
|
||||
f, err := os.Open(appname)
|
||||
if err != nil {
|
||||
os.RemoveAll(tempDir)
|
||||
return nil, err
|
||||
}
|
||||
}()
|
||||
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")
|
||||
return loader.LoadFromSingleFile(appname, f, ops...)
|
||||
}
|
||||
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)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to open application package")
|
||||
}
|
||||
defer f.Close()
|
||||
tarReader := tar.NewReader(f)
|
||||
outputDir = outputDir + "/"
|
||||
for {
|
||||
header, err := tarReader.Next()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != 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
|
||||
return app, nil
|
||||
}
|
||||
|
|
|
@ -12,11 +12,12 @@ import (
|
|||
"text/template"
|
||||
|
||||
"github.com/docker/app/internal"
|
||||
"github.com/docker/app/internal/render"
|
||||
"github.com/docker/app/internal/types"
|
||||
"github.com/docker/cli/cli/compose/loader"
|
||||
"github.com/docker/app/internal/compose"
|
||||
"github.com/docker/app/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"
|
||||
dtemplate "github.com/docker/cli/cli/compose/template"
|
||||
"github.com/docker/cli/opts"
|
||||
"github.com/pkg/errors"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
@ -89,7 +90,11 @@ func Init(name string, composeFile string, description string, maintainers []str
|
|||
return err
|
||||
}
|
||||
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 {
|
||||
|
@ -124,7 +129,7 @@ func initFromComposeFile(name string, composeFile string) error {
|
|||
if err != nil {
|
||||
return errors.Wrap(err, "failed to read compose file")
|
||||
}
|
||||
cfgMap, err := loader.ParseYAML(composeRaw)
|
||||
cfgMap, err := composeloader.ParseYAML(composeRaw)
|
||||
if err != nil {
|
||||
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
|
||||
for k, v := range vars {
|
||||
if _, ok := settings[k]; !ok {
|
||||
|
|
|
@ -9,6 +9,7 @@ import (
|
|||
"path/filepath"
|
||||
|
||||
"github.com/docker/app/internal"
|
||||
"github.com/docker/docker/pkg/archive"
|
||||
)
|
||||
|
||||
func tarAdd(tarout *tar.Writer, path, file string) error {
|
||||
|
@ -87,5 +88,11 @@ func Unpack(appname, targetDir string) error {
|
|||
if err != nil {
|
||||
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"
|
||||
|
||||
"github.com/docker/app/internal"
|
||||
"github.com/docker/app/internal/types"
|
||||
"github.com/docker/app/types"
|
||||
"github.com/docker/distribution/reference"
|
||||
"github.com/pkg/errors"
|
||||
yaml "gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
// Save saves an app to docker and returns the image name.
|
||||
func Save(appname, 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")
|
||||
}
|
||||
func Save(app *types.App, namespace, tag string) (string, error) {
|
||||
var meta types.AppMetadata
|
||||
err = yaml.Unmarshal(metaContent, &meta)
|
||||
err := yaml.Unmarshal(app.Metadata(), &meta)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to parse application metadata")
|
||||
}
|
||||
|
@ -60,7 +50,7 @@ COPY / /
|
|||
return "", errors.Wrapf(err, "cannot create file %s", 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}
|
||||
cmd := exec.Command("docker", args...)
|
||||
cmd.Stdout = ioutil.Discard
|
||||
|
@ -111,13 +101,8 @@ func Load(repotag string, outputDir string) error {
|
|||
}
|
||||
|
||||
// Push pushes an app to a registry
|
||||
func Push(appname, namespace, tag string) error {
|
||||
app, err := Extract(appname)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer app.Cleanup()
|
||||
imageName, err := Save(app.Path, namespace, tag)
|
||||
func Push(app *types.App, namespace, tag string) error {
|
||||
imageName, err := Save(app, namespace, tag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -7,21 +7,28 @@ import (
|
|||
"path/filepath"
|
||||
|
||||
"github.com/docker/app/internal"
|
||||
"github.com/docker/app/types"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// Split converts an app package to the split version
|
||||
func Split(appname string, outputDir string) error {
|
||||
err := os.Mkdir(outputDir, 0755)
|
||||
func Split(app *types.App, outputDir string) error {
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
for _, n := range internal.FileNames {
|
||||
input, err := ioutil.ReadFile(filepath.Join(appname, n))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = ioutil.WriteFile(filepath.Join(outputDir, n), input, 0644)
|
||||
if err != nil {
|
||||
for file, data := range map[string][]byte{
|
||||
internal.MetadataFileName: app.Metadata(),
|
||||
internal.ComposeFileName: app.Composes()[0],
|
||||
internal.SettingsFileName: app.Settings()[0],
|
||||
} {
|
||||
if err := ioutil.WriteFile(filepath.Join(outputDir, file), data, 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
@ -29,20 +36,23 @@ func Split(appname string, outputDir string) error {
|
|||
}
|
||||
|
||||
// Merge converts an app-package to the single-file merged version
|
||||
func Merge(appname string, target io.Writer) error {
|
||||
for i, n := range internal.FileNames {
|
||||
input, err := ioutil.ReadFile(filepath.Join(appname, n))
|
||||
if err != nil {
|
||||
func Merge(app *types.App, target io.Writer) error {
|
||||
if len(app.Composes()) > 1 {
|
||||
return errors.New("merge: multiple compose files is not supported")
|
||||
}
|
||||
if len(app.Settings()) > 1 {
|
||||
return errors.New("merge: multiple setting files is not supported")
|
||||
}
|
||||
for _, data := range [][]byte{
|
||||
app.Metadata(),
|
||||
[]byte(types.SingleFileSeparator),
|
||||
app.Composes()[0],
|
||||
[]byte(types.SingleFileSeparator),
|
||||
app.Settings()[0],
|
||||
} {
|
||||
if _, err := target.Write(data); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := target.Write(input); err != nil {
|
||||
return err
|
||||
}
|
||||
if i != 2 {
|
||||
if _, err := io.WriteString(target, "\n---\n"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -1,21 +1,23 @@
|
|||
package settings
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"io/ioutil"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
yaml "gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
// Load loads the given reader in settings
|
||||
func Load(r io.Reader, ops ...func(*Options)) (Settings, error) {
|
||||
// Load loads the given data in settings
|
||||
func Load(data []byte, ops ...func(*Options)) (Settings, error) {
|
||||
options := &Options{}
|
||||
for _, op := range ops {
|
||||
op(options)
|
||||
}
|
||||
|
||||
r := bytes.NewReader(data)
|
||||
s := make(map[interface{}]interface{})
|
||||
decoder := yaml.NewDecoder(r)
|
||||
if err := decoder.Decode(&s); err != nil {
|
||||
|
@ -37,13 +39,29 @@ func Load(r io.Reader, ops ...func(*Options)) (Settings, error) {
|
|||
return settings, nil
|
||||
}
|
||||
|
||||
// LoadMultiple loads multiple data in settings
|
||||
func LoadMultiple(datas [][]byte, ops ...func(*Options)) (Settings, error) {
|
||||
m := Settings(map[string]interface{}{})
|
||||
for _, data := range datas {
|
||||
settings, err := Load(data, ops...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
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) {
|
||||
r, err := os.Open(path)
|
||||
data, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return Load(r, ops...)
|
||||
return Load(data, ops...)
|
||||
}
|
||||
|
||||
// LoadFiles loads multiple path in settings, merging them.
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package settings
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gotest.tools/assert"
|
||||
|
@ -10,15 +9,15 @@ import (
|
|||
)
|
||||
|
||||
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"))
|
||||
|
||||
_, err = Load(strings.NewReader(`
|
||||
_, err = Load([]byte(`
|
||||
foo: bar
|
||||
1: baz`))
|
||||
assert.Check(t, is.ErrorContains(err, "Non-string key at top level: 1"))
|
||||
|
||||
_, err = Load(strings.NewReader(`
|
||||
_, err = Load([]byte(`
|
||||
foo:
|
||||
bar: baz
|
||||
1: banana`))
|
||||
|
@ -26,7 +25,7 @@ foo:
|
|||
}
|
||||
|
||||
func TestLoad(t *testing.T) {
|
||||
settings, err := Load(strings.NewReader(`
|
||||
settings, err := Load([]byte(`
|
||||
foo: bar
|
||||
bar:
|
||||
baz: banana
|
||||
|
@ -45,7 +44,7 @@ baz:
|
|||
}
|
||||
|
||||
func TestLoadWithPrefix(t *testing.T) {
|
||||
settings, err := Load(strings.NewReader(`
|
||||
settings, err := Load([]byte(`
|
||||
foo: bar
|
||||
bar: baz
|
||||
`), WithPrefix("p"))
|
||||
|
@ -78,3 +77,25 @@ bar:
|
|||
"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 (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/app/internal"
|
||||
"github.com/docker/app/internal/render"
|
||||
"github.com/docker/app/internal/types"
|
||||
"github.com/docker/app/render"
|
||||
"github.com/docker/app/specification"
|
||||
"github.com/docker/app/types"
|
||||
"github.com/docker/cli/cli/compose/loader"
|
||||
)
|
||||
|
||||
// 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
|
||||
if err := checkExistingFiles(app.Path); err != nil {
|
||||
errs = append(errs, err.Error())
|
||||
}
|
||||
if err := validateMetadata(app.Path); err != nil {
|
||||
if err := validateMetadata(app.Metadata()); err != nil {
|
||||
errs = append(errs, err.Error())
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
func checkExistingFiles(appname string) 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)
|
||||
}
|
||||
func validateMetadata(metadata []byte) error {
|
||||
metadataYaml, err := loader.ParseYAML(metadata)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse application metadata: %s", err)
|
||||
|
|
|
@ -6,21 +6,10 @@ import (
|
|||
"gotest.tools/assert"
|
||||
|
||||
"github.com/docker/app/internal"
|
||||
"github.com/docker/app/internal/types"
|
||||
"github.com/docker/app/types"
|
||||
"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) {
|
||||
brokenMetadata := `#version: 0.1.0-missing
|
||||
name: _INVALID-name
|
||||
|
@ -38,7 +27,9 @@ unknown: property`
|
|||
fs.WithFile(internal.ComposeFileName, composeFile),
|
||||
fs.WithFile(internal.SettingsFileName, ""))
|
||||
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:
|
||||
- maintainers.2.email: Does not match format 'email'
|
||||
- name: Does not match format 'hostname'
|
||||
|
@ -57,7 +48,9 @@ my-settings:
|
|||
fs.WithFile(internal.ComposeFileName, composeFile),
|
||||
fs.WithFile(internal.SettingsFileName, brokenSettings))
|
||||
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`)
|
||||
}
|
||||
|
||||
|
@ -72,7 +65,9 @@ unknown-property: value`
|
|||
fs.WithFile(internal.ComposeFileName, brokenComposeFile),
|
||||
fs.WithFile(internal.SettingsFileName, ""))
|
||||
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")
|
||||
}
|
||||
|
||||
|
@ -90,6 +85,8 @@ services:
|
|||
fs.WithFile(internal.ComposeFileName, composeFile),
|
||||
fs.WithFile(internal.SettingsFileName, settings))
|
||||
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)
|
||||
}
|
||||
|
|
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 (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/app/internal/compose"
|
||||
"github.com/docker/app/internal/renderer"
|
||||
"github.com/docker/app/internal/settings"
|
||||
"github.com/docker/app/internal/slices"
|
||||
"github.com/docker/app/internal/types"
|
||||
"github.com/docker/app/types"
|
||||
"github.com/docker/cli/cli/compose/loader"
|
||||
composetemplate "github.com/docker/cli/cli/compose/template"
|
||||
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
|
||||
// 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
|
||||
// load the settings into a struct
|
||||
fileSettings, err := settings.LoadFiles(app.SettingsFiles)
|
||||
fileSettings, err := settings.LoadMultiple(app.Settings())
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to load settings")
|
||||
}
|
||||
// inject our metadata
|
||||
metaPrefixed, err := settings.LoadFile(app.MetadataFile, settings.WithPrefix("app"))
|
||||
metaPrefixed, err := settings.Load(app.Metadata(), settings.WithPrefix("app"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -70,21 +70,11 @@ func Render(app types.App, env map[string]string) (*composetypes.Config, error)
|
|||
}
|
||||
renderers = rl
|
||||
}
|
||||
configFiles := []composetypes.ConfigFile{}
|
||||
for _, c := range app.ComposeFiles {
|
||||
data, err := ioutil.ReadFile(c)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to read Compose file %s", c)
|
||||
}
|
||||
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})
|
||||
configFiles, err := compose.Load(app.Composes(), func(data string) (string, error) {
|
||||
return renderer.Apply(data, allSettings, renderers...)
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to load composefiles")
|
||||
}
|
||||
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))
|
||||
}
|
Загрузка…
Ссылка в новой задаче