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:
Vincent Demeester 2018-08-07 16:32:07 +02:00
Родитель ae5fefef1c
Коммит 7a79d880b3
41 изменённых файлов: 822 добавлений и 442 удалений

Просмотреть файл

@ -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)
} }

Просмотреть файл

@ -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)
}

80
loader/loader.go Normal file
Просмотреть файл

@ -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...)
}

117
loader/loader_test.go Normal file
Просмотреть файл

@ -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())
} }

Просмотреть файл

Просмотреть файл

Просмотреть файл

Просмотреть файл

Просмотреть файл

175
types/types.go Normal file
Просмотреть файл

@ -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
}
}

146
types/types_test.go Normal file
Просмотреть файл

@ -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))
}