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

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

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

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

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

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

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

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

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