package main
import (
"github.com/docker/app/internal"
app "github.com/docker/app/internal/commands"
"github.com/docker/cli/cli-plugins/manager"
"github.com/docker/cli/cli-plugins/plugin"
"github.com/docker/cli/cli/command"
"github.com/spf13/cobra"
)
func main() {
plugin.Run(func(dockerCli command.Cli) *cobra.Command {
return app.NewRootCmd("app", dockerCli)
}, manager.Metadata{
SchemaVersion: "0.1.0",
Vendor: "Docker Inc.",
Version: internal.Version,
})
}
package commands
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io/ioutil"
"github.com/deislabs/duffle/pkg/bundle"
"github.com/docker/app/internal/packager"
"github.com/docker/app/types"
"github.com/docker/app/types/metadata"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/distribution/reference"
dockertypes "github.com/docker/docker/api/types"
"github.com/docker/docker/pkg/jsonmessage"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)
type bundleOptions struct {
out string
}
func bundleCmd(dockerCli command.Cli) *cobra.Command {
var opts bundleOptions
cmd := &cobra.Command{
Use: "bundle [<app-name>]",
Short: "Create a CNAB invocation image and bundle.json for the application.",
Args: cli.RequiresMaxArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
return runBundle(dockerCli, firstOrEmpty(args), opts)
},
}
cmd.Flags().StringVarP(&opts.out, "out", "o", "bundle.json", "path to the output bundle.json (- for stdout)")
return cmd
}
func runBundle(dockerCli command.Cli, appName string, opts bundleOptions) error {
bundle, err := makeBundle(dockerCli, appName)
if err != nil {
return err
}
if bundle == nil || len(bundle.InvocationImages) == 0 {
return fmt.Errorf("failed to create bundle %q", appName)
}
fmt.Fprintf(dockerCli.Out(), "Invocation image %q successfully built\n", bundle.InvocationImages[0].Image)
bundleBytes, err := json.MarshalIndent(bundle, "", "\t")
if err != nil {
return err
}
if opts.out == "-" {
_, err = dockerCli.Out().Write(bundleBytes)
return err
}
return ioutil.WriteFile(opts.out, bundleBytes, 0644)
}
func makeBundle(dockerCli command.Cli, appName string) (*bundle.Bundle, error) {
app, err := packager.Extract(appName)
if err != nil {
return nil, err
}
defer app.Cleanup()
return makeBundleFromApp(dockerCli, app)
}
func makeBundleFromApp(dockerCli command.Cli, app *types.App) (*bundle.Bundle, error) {
meta := app.Metadata()
invocationImageName, err := makeImageName(meta)
if err != nil {
return nil, err
}
if _, err := makeImageName(meta); err != nil {
return nil, err
}
buildContext := bytes.NewBuffer(nil)
if err := packager.PackInvocationImageContext(app, buildContext); err != nil {
return nil, err
}
buildResp, err := dockerCli.Client().ImageBuild(context.TODO(), buildContext, dockertypes.ImageBuildOptions{
Dockerfile: "Dockerfile",
Tags: []string{invocationImageName},
})
if err != nil {
return nil, err
}
defer buildResp.Body.Close()
if err := jsonmessage.DisplayJSONMessagesStream(buildResp.Body, ioutil.Discard, 0, false, func(jsonmessage.JSONMessage) {}); err != nil {
return nil, err
}
return packager.ToCNAB(app, invocationImageName)
}
func makeImageName(meta metadata.AppMetadata) (string, error) {
name := fmt.Sprintf("%s:%s-invoc", meta.Name, meta.Version)
if _, err := reference.ParseNormalizedNamed(name); err != nil {
return "", errors.Wrapf(err, "image name %q is invalid, please check name and version fields", name)
}
return name, nil
}
package commands
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
"github.com/deislabs/duffle/pkg/bundle"
"github.com/deislabs/duffle/pkg/credentials"
"github.com/deislabs/duffle/pkg/driver"
"github.com/deislabs/duffle/pkg/duffle/home"
"github.com/deislabs/duffle/pkg/loader"
"github.com/docker/app/internal"
"github.com/docker/app/internal/packager"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/context/store"
"github.com/pkg/errors"
)
func prepareCredentialSet(contextName string, contextStore store.Store, b *bundle.Bundle, namedCredentialsets []string) (map[string]string, error) {
creds := map[string]string{}
for _, file := range namedCredentialsets {
if _, err := os.Stat(file); err != nil {
file = filepath.Join(duffleHome().Credentials(), file+".yaml")
}
c, err := credentials.Load(file)
if err != nil {
return nil, err
}
values, err := c.Resolve()
if err != nil {
return nil, err
}
for k, v := range values {
if _, ok := creds[k]; ok {
return nil, fmt.Errorf("ambiguous credential resolution: %q is present in multiple credential sets", k)
}
creds[k] = v
}
}
if contextName != "" {
data, err := ioutil.ReadAll(store.Export(contextName, contextStore))
if err != nil {
return nil, err
}
creds["docker.context"] = string(data)
}
_, requiresDockerContext := b.Credentials["docker.context"]
_, hasDockerContext := creds["docker.context"]
if requiresDockerContext && !hasDockerContext {
return nil, errors.New("no target context specified. Use --target-context= or DOCKER_TARGET_CONTEXT= to define it")
}
return creds, nil
}
func getTargetContext(optstargetContext, currentContext string) string {
var targetContext string
switch {
case optstargetContext != "":
targetContext = optstargetContext
case os.Getenv("DOCKER_TARGET_CONTEXT") != "":
targetContext = os.Getenv("DOCKER_TARGET_CONTEXT")
}
if targetContext == "" {
targetContext = currentContext
}
return targetContext
}
func duffleHome() home.Home {
return home.Home(home.DefaultHome())
}
// prepareDriver prepares a driver per the user's request.
func prepareDriver(dockerCli command.Cli) (driver.Driver, error) {
driverImpl, err := driver.Lookup("docker")
if err != nil {
return driverImpl, err
}
if d, ok := driverImpl.(*driver.DockerDriver); ok {
d.SetDockerCli(dockerCli)
}
// Load any driver-specific config out of the environment.
if configurable, ok := driverImpl.(driver.Configurable); ok {
driverCfg := map[string]string{}
for env := range configurable.Config() {
driverCfg[env] = os.Getenv(env)
}
configurable.SetConfig(driverCfg)
}
return driverImpl, err
}
func getAppNameKind(name string) (string, nameKind) {
if name == "" {
return name, nameKindEmpty
}
// name can be a bundle.json or bundle.cnab file, a single dockerapp file, or a dockerapp directory
st, err := os.Stat(name)
if os.IsNotExist(err) {
// try with .dockerapp extension
st, err = os.Stat(name + internal.AppExtension)
if err == nil {
name += internal.AppExtension
}
}
if err != nil {
return name, nameKindReference
}
if st.IsDir() {
return name, nameKindDir
}
return name, nameKindFile
}
func extractAndLoadAppBasedBundle(dockerCli command.Cli, name string) (*bundle.Bundle, error) {
app, err := packager.Extract(name)
if err != nil {
return nil, err
}
defer app.Cleanup()
return makeBundleFromApp(dockerCli, app)
}
func resolveBundle(dockerCli command.Cli, name string) (*bundle.Bundle, error) {
// resolution logic:
// - if there is a docker-app package in working directory, or an http:// / https:// prefix, use packager.Extract result
// - the name has a .json or .cnab extension and refers to an existing file or web resource: load the bundle
// - name matches a bundle name:version stored in duffle bundle store: use it
// - pull the bundle from the registry and add it to the bundle store
name, kind := getAppNameKind(name)
switch kind {
case nameKindFile:
if strings.HasSuffix(name, internal.AppExtension) {
return extractAndLoadAppBasedBundle(dockerCli, name)
}
return loader.NewDetectingLoader().Load(name)
case nameKindDir, nameKindEmpty:
return extractAndLoadAppBasedBundle(dockerCli, name)
case nameKindReference:
// TODO: pull the bundle
fmt.Fprintln(dockerCli.Err(), "WARNING: pulling a CNAB is not yet supported")
}
return nil, fmt.Errorf("could not resolve bundle %q", name)
}
package commands
import (
"bytes"
"fmt"
"io"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/spf13/cobra"
)
func completionCmd(dockerCli command.Cli, rootCmd *cobra.Command) *cobra.Command {
return &cobra.Command{
Use: "completion SHELL",
Short: "Generates completion scripts for the specified shell (bash or zsh)",
Long: `# Load the docker-app completion code for bash into the current shell
. <(docker-app completion bash)
# Set the docker-app completion code for bash to autoload on startup in your ~/.bashrc,
# ~/.profile or ~/.bash_profile
. <(docker-app completion bash)
# Note: bash-completion is needed.
# Load the docker-app completion code for zsh into the current shell
source <(docker-app completion zsh)
# Set the docker-app completion code for zsh to autoload on startup in your ~/.zshrc
source <(docker-app completion zsh)
`,
Args: cli.RequiresMaxArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
switch {
case len(args) == 0:
return rootCmd.GenBashCompletion(dockerCli.Out())
case args[0] == "bash":
return rootCmd.GenBashCompletion(dockerCli.Out())
case args[0] == "zsh":
return runCompletionZsh(dockerCli.Out(), rootCmd)
default:
return fmt.Errorf("%q is not a supported shell", args[0])
}
},
}
}
const (
// Largely inspired by https://github.com/kubernetes/kubernetes/blob/master/pkg/kubectl/cmd/completion.go
zshHead = `#compdef dockerapp
__dockerapp_bash_source() {
alias shopt=':'
alias _expand=_bash_expand
alias _complete=_bash_comp
emulate -L sh
setopt kshglob noshglob braceexpand
source "$@"
}
__dockerapp_type() {
# -t is not supported by zsh
if [ "$1" == "-t" ]; then
shift
# fake Bash 4 to disable "complete -o nospace". Instead
# "compopt +-o nospace" is used in the code to toggle trailing
# spaces. We don't support that, but leave trailing spaces on
# all the time
if [ "$1" = "__dockerapp_compopt" ]; then
echo builtin
return 0
fi
fi
type "$@"
}
__dockerapp_compgen() {
local completions w
completions=( $(compgen "$@") ) || return $?
# filter by given word as prefix
while [[ "$1" = -* && "$1" != -- ]]; do
shift
shift
done
if [[ "$1" == -- ]]; then
shift
fi
for w in "${completions[@]}"; do
if [[ "${w}" = "$1"* ]]; then
echo "${w}"
fi
done
}
__dockerapp_compopt() {
true # don't do anything. Not supported by bashcompinit in zsh
}
__dockerapp_ltrim_colon_completions()
{
if [[ "$1" == *:* && "$COMP_WORDBREAKS" == *:* ]]; then
# Remove colon-word prefix from COMPREPLY items
local colon_word=${1%${1##*:}}
local i=${#COMPREPLY[*]}
while [[ $((--i)) -ge 0 ]]; do
COMPREPLY[$i]=${COMPREPLY[$i]#"$colon_word"}
done
fi
}
__dockerapp_get_comp_words_by_ref() {
cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[${COMP_CWORD}-1]}"
words=("${COMP_WORDS[@]}")
cword=("${COMP_CWORD[@]}")
}
__dockerapp_filedir() {
local RET OLD_IFS w qw
__debug "_filedir $@ cur=$cur"
if [[ "$1" = \~* ]]; then
# somehow does not work. Maybe, zsh does not call this at all
eval echo "$1"
return 0
fi
OLD_IFS="$IFS"
IFS=$'\n'
if [ "$1" = "-d" ]; then
shift
RET=( $(compgen -d) )
else
RET=( $(compgen -f) )
fi
IFS="$OLD_IFS"
IFS="," __debug "RET=${RET[@]} len=${#RET[@]}"
for w in ${RET[@]}; do
if [[ ! "${w}" = "${cur}"* ]]; then
continue
fi
if eval "[[ \"\${w}\" = *.$1 || -d \"\${w}\" ]]"; then
qw="$(__dockerapp_quote "${w}")"
if [ -d "${w}" ]; then
COMPREPLY+=("${qw}/")
else
COMPREPLY+=("${qw}")
fi
fi
done
}
__dockerapp_quote() {
if [[ $1 == \'* || $1 == \"* ]]; then
# Leave out first character
printf %q "${1:1}"
else
printf %q "$1"
fi
}
autoload -U +X bashcompinit && bashcompinit
# use word boundary patterns for BSD or GNU sed
LWORD='[[:<:]]'
RWORD='[[:>:]]'
if sed --help 2>&1 | grep -q GNU; then
LWORD='\<'
RWORD='\>'
fi
__dockerapp_convert_bash_to_zsh() {
sed \
-e 's/declare -F/whence -w/' \
-e 's/_get_comp_words_by_ref "\$@"/_get_comp_words_by_ref "\$*"/' \
-e 's/local \([a-zA-Z0-9_]*\)=/local \1; \1=/' \
-e 's/flags+=("\(--.*\)=")/flags+=("\1"); two_word_flags+=("\1")/' \
-e 's/must_have_one_flag+=("\(--.*\)=")/must_have_one_flag+=("\1")/' \
-e "s/${LWORD}_filedir${RWORD}/__dockerapp_filedir/g" \
-e "s/${LWORD}_get_comp_words_by_ref${RWORD}/__dockerapp_get_comp_words_by_ref/g" \
-e "s/${LWORD}__ltrim_colon_completions${RWORD}/__dockerapp_ltrim_colon_completions/g" \
-e "s/${LWORD}compgen${RWORD}/__dockerapp_compgen/g" \
-e "s/${LWORD}compopt${RWORD}/__dockerapp_compopt/g" \
-e "s/${LWORD}declare${RWORD}/builtin declare/g" \
-e "s/\\\$(type${RWORD}/\$(__dockerapp_type/g" \
<<'BASH_COMPLETION_EOF'
`
zshTail = `
BASH_COMPLETION_EOF
}
__dockerapp_bash_source <(__dockerapp_convert_bash_to_zsh)
_complete dockerapp 2>/dev/null
`
)
func runCompletionZsh(out io.Writer, rootCmd *cobra.Command) error {
fmt.Fprint(out, zshHead)
buf := new(bytes.Buffer)
rootCmd.GenBashCompletion(buf)
fmt.Fprint(out, buf.String())
fmt.Fprint(out, zshTail)
return nil
}
package commands
import (
"github.com/docker/app/internal/packager"
"github.com/docker/cli/cli"
"github.com/spf13/cobra"
)
var (
initComposeFile string
initDescription string
initMaintainers []string
initSingleFile bool
)
func initCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "init <app-name> [-c <compose-file>] [-d <description>] [-m name:email ...]",
Short: "Start building a Docker application",
Long: `Start building a Docker application. Will automatically detect a docker-compose.yml file in the current directory.`,
Args: cli.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
return packager.Init(args[0], initComposeFile, initDescription, initMaintainers, initSingleFile)
},
}
cmd.Flags().StringVarP(&initComposeFile, "compose-file", "c", "", "Initial Compose file (optional)")
cmd.Flags().StringVarP(&initDescription, "description", "d", "", "Initial description (optional)")
cmd.Flags().StringArrayVarP(&initMaintainers, "maintainer", "m", []string{}, "Maintainer (name:email) (optional)")
cmd.Flags().BoolVarP(&initSingleFile, "single-file", "s", false, "Create a single-file application")
return cmd
}
package commands
import (
"github.com/deislabs/duffle/pkg/action"
"github.com/deislabs/duffle/pkg/claim"
"github.com/docker/app/internal"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)
func inspectCmd(dockerCli command.Cli) *cobra.Command {
var opts parametersOptions
cmd := &cobra.Command{
Use: "inspect [<app-name>] [-s key=value...] [-f parameters-file...]",
Short: "Shows metadata, parameters and a summary of the compose file for a given application",
Args: cli.RequiresMaxArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
defer muteDockerCli(dockerCli)()
appname := firstOrEmpty(args)
c, err := claim.New("inspect")
if err != nil {
return err
}
driverImpl, err := prepareDriver(dockerCli)
if err != nil {
return err
}
bundle, err := resolveBundle(dockerCli, appname)
if err != nil {
return err
}
c.Bundle = bundle
parameters, err := mergeBundleParameters(c.Bundle,
withFileParameters(opts.parametersFiles),
withCommandLineParameters(opts.overrides),
)
if err != nil {
return err
}
c.Parameters = parameters
a := &action.RunCustom{
Action: internal.Namespace + "inspect",
Driver: driverImpl,
}
err = a.Run(c, map[string]string{"docker.context": ""}, dockerCli.Out())
return errors.Wrap(err, "Inspect failed")
},
}
opts.addFlags(cmd.Flags())
return cmd
}
package commands
import (
"fmt"
"github.com/deislabs/duffle/pkg/action"
"github.com/deislabs/duffle/pkg/claim"
"github.com/deislabs/duffle/pkg/credentials"
"github.com/deislabs/duffle/pkg/utils/crud"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)
type installOptions struct {
parametersOptions
credentialOptions
orchestrator string
kubeNamespace string
stackName string
insecure bool
sendRegistryAuth bool
}
type nameKind uint
const (
_ nameKind = iota
nameKindEmpty
nameKindFile
nameKindDir
nameKindReference
)
const longDescription = `Install the application on either Swarm or Kubernetes.
Bundle name is optional, and can:
- be empty and resolve to any *.dockerapp in working directory
- be a BUNDLE file path and resolve to any *.dockerapp file or dir, or any CNAB file (signed or unsigned)
- match a bundle name in the local duffle bundle repository
- refer to a CNAB in a container registry
`
func installCmd(dockerCli command.Cli) *cobra.Command {
var opts installOptions
cmd := &cobra.Command{
Use: "install [<bundle name>] [OPTIONS]",
Aliases: []string{"deploy"},
Short: "Install an application",
Long: longDescription,
Args: cli.RequiresMaxArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
return runInstall(dockerCli, firstOrEmpty(args), opts)
},
}
opts.parametersOptions.addFlags(cmd.Flags())
opts.credentialOptions.addFlags(cmd.Flags())
cmd.Flags().StringVarP(&opts.orchestrator, "orchestrator", "o", "", "Orchestrator to install on (swarm, kubernetes)")
cmd.Flags().StringVar(&opts.kubeNamespace, "kubernetes-namespace", "default", "Kubernetes namespace to install into")
cmd.Flags().StringVar(&opts.stackName, "name", "", "Installation name (defaults to application name)")
cmd.Flags().BoolVar(&opts.insecure, "insecure", false, "Use insecure registry, without SSL")
cmd.Flags().BoolVar(&opts.sendRegistryAuth, "with-registry-auth", false, "Sends registry auth")
return cmd
}
func runInstall(dockerCli command.Cli, appname string, opts installOptions) error {
defer muteDockerCli(dockerCli)()
if opts.sendRegistryAuth {
return errors.New("with-registry-auth is not supported at the moment")
}
targetContext := getTargetContext(opts.targetContext, dockerCli.CurrentContext())
bndl, err := resolveBundle(dockerCli, appname)
if err != nil {
return err
}
if err := bndl.Validate(); err != nil {
return err
}
h := duffleHome()
claimName := opts.stackName
if claimName == "" {
claimName = bndl.Name
}
claimStore := claim.NewClaimStore(crud.NewFileSystemStore(h.Claims(), "json"))
if _, err = claimStore.Read(claimName); err == nil {
return fmt.Errorf("installation %q already exists", claimName)
}
c, err := claim.New(claimName)
if err != nil {
return err
}
driverImpl, err := prepareDriver(dockerCli)
if err != nil {
return err
}
creds, err := prepareCredentialSet(targetContext, dockerCli.ContextStore(), bndl, opts.credentialsets)
if err != nil {
return err
}
if err := credentials.Validate(creds, bndl.Credentials); err != nil {
return err
}
c.Bundle = bndl
c.Parameters, err = mergeBundleParameters(bndl,
withFileParameters(opts.parametersFiles),
withCommandLineParameters(opts.overrides),
withOrchestratorParameters(opts.orchestrator, opts.kubeNamespace),
)
if err != nil {
return err
}
inst := &action.Install{
Driver: driverImpl,
}
err = inst.Run(c, creds, dockerCli.Out())
// Even if the installation failed, the claim is persisted with its failure status,
// so any installation needs a clean uninstallation.
err2 := claimStore.Store(*c)
if err != nil {
return fmt.Errorf("install failed: %v", err)
}
return err2
}
package commands
import (
"fmt"
"io"
"io/ioutil"
"os"
"strings"
"github.com/docker/app/internal"
"github.com/docker/app/internal/packager"
"github.com/docker/app/types"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)
var mergeOutputFile string
// Check appname directory for extra files and return them
func extraFiles(appname string) ([]string, error) {
files, err := ioutil.ReadDir(appname)
if err != nil {
return nil, err
}
var res []string
for _, f := range files {
hit := false
for _, afn := range internal.FileNames {
if afn == f.Name() {
hit = true
break
}
}
if !hit {
res = append(res, f.Name())
}
}
return res, nil
}
//handleInPlace returns the operation target path and if it's in-place
func handleInPlace(app *types.App) (string, bool) {
if app.Source == types.AppSourceImage {
return internal.DirNameFromAppName(app.Name), false
}
return app.Path + ".tmp", true
}
// removeAndRename removes target and rename source into target
func removeAndRename(source, target string) error {
if err := os.RemoveAll(target); err != nil {
return errors.Wrap(err, "failed to erase previous application")
}
if err := os.Rename(source, target); err != nil {
return errors.Wrap(err, "failed to rename new application")
}
return nil
}
func mergeCmd(dockerCli command.Cli) *cobra.Command {
cmd := &cobra.Command{
Use: "merge [<app-name>] [-o output_file]",
Short: "Merge a multi-file application into a single file",
Args: cli.RequiresMaxArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
extractedApp, err := packager.Extract(firstOrEmpty(args))
if err != nil {
return err
}
defer extractedApp.Cleanup()
inPlace := false
if mergeOutputFile == "" {
mergeOutputFile, inPlace = handleInPlace(extractedApp)
}
if inPlace {
extra, err := extraFiles(extractedApp.Path)
if err != nil {
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.Path, strings.Join(extra, ","))
}
}
var target io.Writer
if mergeOutputFile == "-" {
target = dockerCli.Out()
} else {
target, err = os.Create(mergeOutputFile)
if err != nil {
return err
}
}
if err := packager.Merge(extractedApp, target); err != nil {
return err
}
if mergeOutputFile != "-" {
// Need to close for the Rename to work on windows.
target.(io.WriteCloser).Close()
}
if inPlace {
return removeAndRename(mergeOutputFile, extractedApp.Path)
}
return nil
},
}
cmd.Flags().StringVarP(&mergeOutputFile, "output", "o", "", "Output file (default: in-place)")
return cmd
}
package commands
import (
"fmt"
"github.com/deislabs/duffle/pkg/bundle"
"github.com/docker/app/types/parameters"
cliopts "github.com/docker/cli/opts"
"github.com/pkg/errors"
)
type parameterOperation func(bndl *bundle.Bundle, params map[string]string) error
func withFileParameters(parametersFiles []string) parameterOperation {
return func(bndl *bundle.Bundle, params map[string]string) error {
p, err := parameters.LoadFiles(parametersFiles)
if err != nil {
return err
}
for k, v := range p.Flatten() {
params[k] = v
}
return nil
}
}
func withCommandLineParameters(overrides []string) parameterOperation {
return func(bndl *bundle.Bundle, params map[string]string) error {
d := cliopts.ConvertKVStringsToMap(overrides)
for k, v := range d {
params[k] = v
}
return nil
}
}
func withOrchestratorParameters(orchestrator string, kubeNamespace string) parameterOperation {
return func(bndl *bundle.Bundle, params map[string]string) error {
if _, ok := bndl.Parameters["docker.orchestrator"]; ok {
params["docker.orchestrator"] = orchestrator
}
if _, ok := bndl.Parameters["docker.kubernetes-namespace"]; ok {
params["docker.kubernetes-namespace"] = kubeNamespace
}
return nil
}
}
func mergeBundleParameters(bndl *bundle.Bundle, ops ...parameterOperation) (map[string]interface{}, error) {
userParams := map[string]string{}
for _, op := range ops {
if err := op(bndl, userParams); err != nil {
return nil, err
}
}
convertedParams, err := matchParametersDefinition(userParams, bndl.Parameters)
if err != nil {
return nil, err
}
return bundle.ValuesOrDefaults(convertedParams, bndl)
}
func matchParametersDefinition(parameterValues map[string]string, parameterDefinitions map[string]bundle.ParameterDefinition) (map[string]interface{}, error) {
finalValues := map[string]interface{}{}
for k, v := range parameterValues {
definition, ok := parameterDefinitions[k]
if !ok {
return nil, fmt.Errorf("parameter %q is not defined in the bundle", k)
}
value, err := definition.ConvertValue(v)
if err != nil {
return nil, errors.Wrapf(err, "invalid value for parameter %q", k)
}
if err := definition.ValidateParameterValue(value); err != nil {
return nil, errors.Wrapf(err, "invalid value for parameter %q", k)
}
finalValues[k] = value
}
return finalValues, nil
}
package commands
import (
"github.com/docker/app/internal/packager"
"github.com/docker/cli/cli"
"github.com/spf13/cobra"
)
func pullCmd() *cobra.Command {
return &cobra.Command{
Use: "pull <repotag>",
Short: "Pull an application from a registry",
Args: cli.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
_, err := packager.Pull(args[0], ".")
return err
},
}
}
package commands
import (
"fmt"
"github.com/docker/app/internal/packager"
"github.com/docker/cli/cli"
"github.com/spf13/cobra"
)
type pushOptions struct {
tag string
}
func pushCmd() *cobra.Command {
var opts pushOptions
cmd := &cobra.Command{
Use: "push [<app-name>]",
Short: "Push the application to a registry",
Args: cli.RequiresMaxArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
app, err := packager.Extract(firstOrEmpty(args))
if err != nil {
return err
}
defer app.Cleanup()
dgst, err := packager.Push(app, opts.tag)
if err == nil {
fmt.Println(dgst)
}
return err
},
}
cmd.Flags().StringVarP(&opts.tag, "tag", "t", "", "Target registry reference (default is : from metadata)")
return cmd
}
package commands
import (
"fmt"
"os"
"github.com/docker/app/internal"
"github.com/docker/app/internal/formatter"
"github.com/docker/app/internal/packager"
"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"
"github.com/spf13/cobra"
)
var (
formatDriver string
renderComposeFiles []string
renderParametersFile []string
renderEnv []string
renderOutput string
)
func renderCmd(dockerCli command.Cli) *cobra.Command {
cmd := &cobra.Command{
Use: "render <app-name> [-s key=value...] [-f parameters-file...]",
Short: "Render the Compose file for the application",
Long: `Render the Compose file for the application.`,
Args: cli.RequiresMaxArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
app, err := packager.Extract(firstOrEmpty(args),
types.WithParametersFiles(renderParametersFile...),
types.WithComposeFiles(renderComposeFiles...),
)
if err != nil {
return err
}
defer app.Cleanup()
d := cliopts.ConvertKVStringsToMap(renderEnv)
rendered, err := render.Render(app, d, nil)
if err != nil {
return err
}
res, err := formatter.Format(rendered, formatDriver)
if err != nil {
return err
}
if renderOutput == "-" {
fmt.Fprint(dockerCli.Out(), res)
} else {
f, err := os.Create(renderOutput)
if err != nil {
return err
}
fmt.Fprint(f, res)
}
return nil
},
}
if internal.Experimental == "on" {
cmd.Use += " [-c <compose-files>...]"
cmd.Long += `- External Compose files or template Compose files can be specified with the -c flag.
(Repeat the flag for multiple files). These files will be merged in order with
the app's own Compose file.`
cmd.Flags().StringArrayVarP(&renderComposeFiles, "compose-files", "c", []string{}, "Override Compose file")
}
cmd.Flags().StringArrayVarP(&renderParametersFile, "parameters-files", "f", []string{}, "Override with parameters from files")
cmd.Flags().StringArrayVarP(&renderEnv, "set", "s", []string{}, "Override parameters values")
cmd.Flags().StringVarP(&renderOutput, "output", "o", "-", "Output file")
cmd.Flags().StringVar(&formatDriver, "formatter", "yaml", "Configure the output format (yaml|json)")
return cmd
}
package commands
import (
"io/ioutil"
"github.com/docker/app/internal"
"github.com/docker/cli/cli/command"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
// NewRootCmd returns the base root command.
func NewRootCmd(use string, dockerCli command.Cli) *cobra.Command {
cmd := &cobra.Command{
Short: "Docker Application Packages",
Long: `Build and deploy Docker Application Packages.`,
Use: use,
}
addCommands(cmd, dockerCli)
return cmd
}
func addCommands(cmd *cobra.Command, dockerCli command.Cli) {
cmd.AddCommand(
installCmd(dockerCli),
upgradeCmd(dockerCli),
uninstallCmd(dockerCli),
statusCmd(dockerCli),
initCmd(),
inspectCmd(dockerCli),
mergeCmd(dockerCli),
pushCmd(),
renderCmd(dockerCli),
splitCmd(),
validateCmd(),
versionCmd(dockerCli),
completionCmd(dockerCli, cmd),
bundleCmd(dockerCli),
)
if internal.Experimental == "on" {
cmd.AddCommand(
pullCmd(),
)
}
}
func firstOrEmpty(list []string) string {
if len(list) != 0 {
return list[0]
}
return ""
}
func muteDockerCli(dockerCli command.Cli) func() {
stdout := dockerCli.Out()
stderr := dockerCli.Err()
dockerCli.Apply(command.WithCombinedStreams(ioutil.Discard))
return func() {
dockerCli.Apply(command.WithOutputStream(stdout), command.WithErrorStream(stderr))
}
}
type parametersOptions struct {
parametersFiles []string
overrides []string
}
func (o *parametersOptions) addFlags(flags *pflag.FlagSet) {
flags.StringArrayVarP(&o.parametersFiles, "parameters-files", "f", []string{}, "Override parameters files")
flags.StringArrayVarP(&o.overrides, "set", "s", []string{}, "Override parameters values")
}
type credentialOptions struct {
targetContext string
credentialsets []string
}
func (o *credentialOptions) addFlags(flags *pflag.FlagSet) {
flags.StringVar(&o.targetContext, "target-context", "", "Context on which the application is executed")
flags.StringArrayVarP(&o.credentialsets, "credential-set", "c", []string{}, "Use a duffle credentialset (either a YAML file, or a credential set present in the duffle credential store)")
}
package commands
import (
"github.com/docker/app/internal/packager"
"github.com/docker/cli/cli"
"github.com/spf13/cobra"
)
var splitOutputDir string
func splitCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "split [<app-name>] [-o output]",
Short: "Split a single-file application into multiple files",
Args: cli.RequiresMaxArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
extractedApp, err := packager.Extract(firstOrEmpty(args))
if err != nil {
return err
}
defer extractedApp.Cleanup()
inPlace := false
if splitOutputDir == "" {
splitOutputDir, inPlace = handleInPlace(extractedApp)
}
if err := packager.Split(extractedApp, splitOutputDir); err != nil {
return err
}
if inPlace {
return removeAndRename(splitOutputDir, extractedApp.Path)
}
return nil
},
}
cmd.Flags().StringVarP(&splitOutputDir, "output", "o", "", "Output application directory (default: in-place)")
return cmd
}
package commands
import (
"github.com/deislabs/duffle/pkg/action"
"github.com/deislabs/duffle/pkg/claim"
"github.com/deislabs/duffle/pkg/credentials"
"github.com/deislabs/duffle/pkg/utils/crud"
"github.com/docker/app/internal"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)
func statusCmd(dockerCli command.Cli) *cobra.Command {
var opts credentialOptions
cmd := &cobra.Command{
Use: "status <installation-name>",
Short: "Get the installation status. If the installation is a docker application, the status shows the stack services.",
Args: cli.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
return runStatus(dockerCli, args[0], opts)
},
}
opts.addFlags(cmd.Flags())
return cmd
}
func runStatus(dockerCli command.Cli, claimName string, opts credentialOptions) error {
defer muteDockerCli(dockerCli)()
h := duffleHome()
claimStore := claim.NewClaimStore(crud.NewFileSystemStore(h.Claims(), "json"))
c, err := claimStore.Read(claimName)
if err != nil {
return err
}
targetContext := getTargetContext(opts.targetContext, dockerCli.CurrentContext())
driverImpl, err := prepareDriver(dockerCli)
if err != nil {
return err
}
creds, err := prepareCredentialSet(targetContext, dockerCli.ContextStore(), c.Bundle, opts.credentialsets)
if err != nil {
return err
}
if err := credentials.Validate(creds, c.Bundle.Credentials); err != nil {
return err
}
status := &action.RunCustom{
Action: internal.Namespace + "status",
Driver: driverImpl,
}
err = status.Run(&c, creds, dockerCli.Out())
return errors.Wrap(err, "Status failed")
}
package commands
import (
"fmt"
"github.com/deislabs/duffle/pkg/action"
"github.com/deislabs/duffle/pkg/claim"
"github.com/deislabs/duffle/pkg/credentials"
"github.com/deislabs/duffle/pkg/utils/crud"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/spf13/cobra"
)
func uninstallCmd(dockerCli command.Cli) *cobra.Command {
var opts credentialOptions
cmd := &cobra.Command{
Use: "uninstall <installation-name>",
Short: "Uninstall an application",
Args: cli.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
return runUninstall(dockerCli, args[0], opts)
},
}
opts.addFlags(cmd.Flags())
return cmd
}
func runUninstall(dockerCli command.Cli, claimName string, opts credentialOptions) error {
defer muteDockerCli(dockerCli)()
h := duffleHome()
claimStore := claim.NewClaimStore(crud.NewFileSystemStore(h.Claims(), "json"))
c, err := claimStore.Read(claimName)
if err != nil {
return err
}
targetContext := getTargetContext(opts.targetContext, dockerCli.CurrentContext())
driverImpl, err := prepareDriver(dockerCli)
if err != nil {
return err
}
creds, err := prepareCredentialSet(targetContext, dockerCli.ContextStore(), c.Bundle, opts.credentialsets)
if err != nil {
return err
}
if err := credentials.Validate(creds, c.Bundle.Credentials); err != nil {
return err
}
uninst := &action.Uninstall{
Driver: driverImpl,
}
err = uninst.Run(&c, creds, dockerCli.Out())
if err == nil {
return claimStore.Delete(claimName)
}
if err2 := claimStore.Store(c); err2 != nil {
fmt.Fprintf(dockerCli.Err(), "failed to update claim: %s\n", err2)
}
return err
}
package commands
import (
"fmt"
"github.com/deislabs/duffle/pkg/action"
"github.com/deislabs/duffle/pkg/claim"
"github.com/deislabs/duffle/pkg/credentials"
"github.com/deislabs/duffle/pkg/utils/crud"
"github.com/docker/cli/cli/command"
"github.com/spf13/cobra"
)
type upgradeOptions struct {
parametersOptions
credentialOptions
bundleOrDockerApp string
insecure bool
}
func upgradeCmd(dockerCli command.Cli) *cobra.Command {
var opts upgradeOptions
cmd := &cobra.Command{
Use: "upgrade <installation-name> [options]",
Short: "Upgrade an installed application",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
return runUpgrade(dockerCli, args[0], opts)
},
}
opts.parametersOptions.addFlags(cmd.Flags())
opts.credentialOptions.addFlags(cmd.Flags())
cmd.Flags().StringVar(&opts.bundleOrDockerApp, "bundle", "", "Override with new bundle or Docker App")
cmd.Flags().BoolVar(&opts.insecure, "insecure", false, "Use insecure registry, without SSL")
return cmd
}
func runUpgrade(dockerCli command.Cli, installationName string, opts upgradeOptions) error {
defer muteDockerCli(dockerCli)()
targetContext := getTargetContext(opts.targetContext, dockerCli.CurrentContext())
h := duffleHome()
claimStore := claim.NewClaimStore(crud.NewFileSystemStore(h.Claims(), "json"))
c, err := claimStore.Read(installationName)
if err != nil {
return err
}
if opts.bundleOrDockerApp != "" {
b, err := resolveBundle(dockerCli, opts.bundleOrDockerApp)
if err != nil {
return err
}
c.Bundle = b
}
driverImpl, err := prepareDriver(dockerCli)
if err != nil {
return err
}
creds, err := prepareCredentialSet(targetContext, dockerCli.ContextStore(), c.Bundle, opts.credentialsets)
if err != nil {
return err
}
if err := credentials.Validate(creds, c.Bundle.Credentials); err != nil {
return err
}
c.Parameters, err = mergeBundleParameters(c.Bundle,
withFileParameters(opts.parametersFiles),
withCommandLineParameters(opts.overrides),
)
if err != nil {
return err
}
u := &action.Upgrade{
Driver: driverImpl,
}
err = u.Run(&c, creds, dockerCli.Out())
err2 := claimStore.Store(c)
if err != nil {
return fmt.Errorf("upgrade failed: %v", err)
}
return err2
}
package commands
import (
"github.com/docker/app/internal/packager"
"github.com/docker/app/render"
"github.com/docker/app/types"
"github.com/docker/cli/cli"
cliopts "github.com/docker/cli/opts"
"github.com/spf13/cobra"
)
var (
validateParametersFile []string
validateEnv []string
)
func validateCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "validate [<app-name>] [-s key=value...] [-f parameters-file...]",
Short: "Checks the rendered application is syntactically correct",
Args: cli.RequiresMaxArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
app, err := packager.Extract(firstOrEmpty(args),
types.WithParametersFiles(validateParametersFile...),
)
if err != nil {
return err
}
defer app.Cleanup()
argParameters := cliopts.ConvertKVStringsToMap(validateEnv)
_, err = render.Render(app, argParameters, nil)
return err
},
}
cmd.Flags().StringArrayVarP(&validateParametersFile, "parameters-files", "f", []string{}, "Override with parameters from files")
cmd.Flags().StringArrayVarP(&validateEnv, "set", "s", []string{}, "Override parameters values")
return cmd
}
package commands
import (
"fmt"
"github.com/docker/app/internal"
"github.com/docker/cli/cli/command"
"github.com/spf13/cobra"
)
func versionCmd(dockerCli command.Cli) *cobra.Command {
return &cobra.Command{
Use: "version",
Short: "Print version information",
Run: func(cmd *cobra.Command, args []string) {
fmt.Fprintln(dockerCli.Out(), internal.FullVersion())
},
}
}
package compose
import (
"fmt"
"regexp"
"strings"
"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"
)
const (
delimiter = "\\$"
substitution = "[_a-z][._a-z0-9]*(?::?[-?][^}]*)?"
)
var (
patternString = fmt.Sprintf(
"%s(?i:(?P<escaped>%s)|(?P<named>%s)|{(?P<braced>%s)}|(?P<invalid>))",
delimiter, delimiter, substitution, substitution,
)
// ExtrapolationPattern is the variable regexp pattern used to interpolate or extract variables when rendering
ExtrapolationPattern = regexp.MustCompile(patternString)
)
// Load applies the specified function when loading a slice of compose data
func Load(composes [][]byte, apply func(string) (string, error)) ([]composetypes.ConfigFile, map[string]string, error) {
configFiles := []composetypes.ConfigFile{}
for _, data := range composes {
s, err := apply(string(data))
if err != nil {
return nil, nil, err
}
parsed, err := loader.ParseYAML([]byte(s))
if err != nil {
return nil, nil, errors.Wrapf(err, "failed to parse Compose file %s", data)
}
configFiles = append(configFiles, composetypes.ConfigFile{Config: parsed})
}
images, err := validateImagesInConfigFiles(configFiles)
if err != nil {
return nil, nil, err
}
return configFiles, images, nil
}
// validateImagesInConfigFiles validates that there is no unsupported variable expensions in service images and returns a map of service name -> image
func validateImagesInConfigFiles(configFiles []composetypes.ConfigFile) (map[string]string, error) {
var errors []string
images := map[string]string{}
for _, configFile := range configFiles {
services, ok := configFile.Config["services"].(map[string]interface{})
if !ok {
continue
}
for serviceName, serviceContent := range services {
serviceMap, ok := serviceContent.(map[string]interface{})
if !ok {
continue
}
imageName, ok := serviceMap["image"].(string)
if !ok {
continue
}
images[serviceName] = imageName
if ExtrapolationPattern.MatchString(imageName) {
errors = append(errors,
fmt.Sprintf("%s: variables are not allowed in the service's image field. Found: '%s'",
serviceName, imageName))
}
}
}
if len(errors) > 0 {
return nil, fmt.Errorf("%s", strings.Join(errors, "\n"))
}
return images, 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
}
package formatter
import (
"sort"
"sync"
"github.com/docker/app/internal/formatter/driver"
composetypes "github.com/docker/cli/cli/compose/types"
"github.com/pkg/errors"
)
var (
driversMu sync.RWMutex
drivers = map[string]driver.Driver{}
)
// Register makes a formatter available by the provided name.
// If Register is called twice with the same name or if driver is nil,
// it panics.
func Register(name string, driver driver.Driver) {
driversMu.Lock()
defer driversMu.Unlock()
if driver == nil {
panic("formatter: Register driver is nil")
}
if _, dup := drivers[name]; dup {
panic("formatter: Register called twice for driver " + name)
}
drivers[name] = driver
}
// Format uses the specified formatter to create a printable output.
// If the formatter is not registered, this errors out.
func Format(config *composetypes.Config, formatter string) (string, error) {
driversMu.RLock()
d, ok := drivers[formatter]
driversMu.RUnlock()
if !ok {
return "", errors.Errorf("unknown formatter %q", formatter)
}
s, err := d.Format(config)
if err != nil {
return "", err
}
return s, nil
}
// Drivers returns a sorted list of the names of the registered drivers.
func Drivers() []string {
list := []string{}
driversMu.RLock()
for name := range drivers {
list = append(list, name)
}
driversMu.RUnlock()
sort.Strings(list)
return list
}
package json
import (
"encoding/json"
"github.com/docker/app/internal/formatter"
composetypes "github.com/docker/cli/cli/compose/types"
"github.com/pkg/errors"
)
func init() {
formatter.Register("json", &Driver{})
}
// Driver is the json implementation of formatter drivers.
type Driver struct{}
// Format creates a JSON document from the source config.
func (d *Driver) Format(config *composetypes.Config) (string, error) {
result, err := json.MarshalIndent(config, "", " ")
if err != nil {
return "", errors.Wrap(err, "failed to produce json structure")
}
return string(result) + "\n", nil
}
package yaml
import (
"github.com/docker/app/internal/formatter"
"github.com/docker/app/internal/yaml"
composetypes "github.com/docker/cli/cli/compose/types"
"github.com/pkg/errors"
)
func init() {
formatter.Register("yaml", &Driver{})
}
// Driver is the yaml implementation of formatter drivers.
type Driver struct{}
// Format creates a YAML document from the source config.
func (d *Driver) Format(config *composetypes.Config) (string, error) {
result, err := yaml.Marshal(config)
if err != nil {
return "", errors.Wrap(err, "failed to produce yaml structure")
}
return string(result), nil
}
package inspect
import (
"fmt"
"io"
"sort"
"strings"
"text/tabwriter"
"github.com/deislabs/duffle/pkg/bundle"
"github.com/docker/app/render"
"github.com/docker/app/types"
"github.com/docker/app/types/parameters"
composetypes "github.com/docker/cli/cli/compose/types"
units "github.com/docker/go-units"
)
// Inspect dumps the metadata of an app
func Inspect(out io.Writer, app *types.App, argParameters map[string]string, imageMap map[string]bundle.Image) error {
// Render the compose file
config, err := render.Render(app, argParameters, imageMap)
if err != nil {
return err
}
// Extract all the parameters
parametersKeys, allParameters, err := extractParameters(app, argParameters)
if err != nil {
return err
}
// Add Meta data
printMetadata(out, app)
// Add Service section
printSection(out, len(config.Services), func(w io.Writer) {
for _, service := range config.Services {
fmt.Fprintf(w, "%s\t%d\t%s\t%s\n", service.Name, getReplicas(service), getPorts(service.Ports), service.Image)
}
}, "Service", "Replicas", "Ports", "Image")
// Add Network section
printSection(out, len(config.Networks), func(w io.Writer) {
for name := range config.Networks {
fmt.Fprintln(w, name)
}
}, "Network")
// Add Volume section
printSection(out, len(config.Volumes), func(w io.Writer) {
for name := range config.Volumes {
fmt.Fprintln(w, name)
}
}, "Volume")
// Add Secret section
printSection(out, len(config.Secrets), func(w io.Writer) {
for name := range config.Secrets {
fmt.Fprintln(w, name)
}
}, "Secret")
// Add Parameter section
printSection(out, len(parametersKeys), func(w io.Writer) {
for _, k := range parametersKeys {
fmt.Fprintf(w, "%s\t%s\n", k, allParameters[k])
}
}, "Parameter", "Value")
// Add Attachments section
attachments := app.Attachments()
printSection(out, len(attachments), func(w io.Writer) {
for _, file := range attachments {
sizeString := units.HumanSize(float64(file.Size()))
fmt.Fprintf(w, "%s\t%s\n", file.Path(), sizeString)
}
}, "Attachment", "Size")
return nil
}
func printMetadata(out io.Writer, app *types.App) {
meta := app.Metadata()
fmt.Fprintln(out, meta.Name, meta.Version)
if maintainers := meta.Maintainers.String(); maintainers != "" {
fmt.Fprintln(out)
fmt.Fprintln(out, "Maintained by:", maintainers)
}
if meta.Description != "" {
fmt.Fprintln(out)
fmt.Fprintln(out, meta.Description)
}
}
func printSection(out io.Writer, len int, printer func(io.Writer), headers ...string) {
if len == 0 {
return
}
fmt.Fprintln(out)
w := tabwriter.NewWriter(out, 0, 0, 1, ' ', 0)
var plural string
if len > 1 {
plural = "s"
}
headers[0] = fmt.Sprintf("%s%s (%d)", headers[0], plural, len)
printHeaders(w, headers...)
printer(w)
w.Flush()
}
func printHeaders(w io.Writer, headers ...string) {
fmt.Fprintln(w, strings.Join(headers, "\t"))
dashes := make([]string, len(headers))
for i, h := range headers {
dashes[i] = strings.Repeat("-", len(h))
}
fmt.Fprintln(w, strings.Join(dashes, "\t"))
}
func getReplicas(service composetypes.ServiceConfig) int {
if service.Deploy.Replicas != nil {
return int(*service.Deploy.Replicas)
}
return 1
}
func extractParameters(app *types.App, argParameters map[string]string) ([]string, map[string]string, error) {
allParameters, err := mergeAndFlattenParameters(app, argParameters)
if err != nil {
return nil, nil, err
}
// sort the keys to get consistent output
var parametersKeys []string
for k := range allParameters {
parametersKeys = append(parametersKeys, k)
}
sort.Slice(parametersKeys, func(i, j int) bool { return parametersKeys[i] < parametersKeys[j] })
return parametersKeys, allParameters, nil
}
func mergeAndFlattenParameters(app *types.App, argParameters map[string]string) (map[string]string, error) {
sArgs, err := parameters.FromFlatten(argParameters)
if err != nil {
return nil, err
}
s, err := parameters.Merge(app.Parameters(), sArgs)
if err != nil {
return nil, err
}
return s.Flatten(), nil
}
package inspect
import (
"fmt"
"sort"
"strings"
composetypes "github.com/docker/cli/cli/compose/types"
)
type portRange struct {
start uint32
end *uint32
}
func newPort(start uint32) *portRange {
return &portRange{start: start}
}
func (p *portRange) add(end uint32) bool {
if p.end == nil {
if p.start+1 == end {
p.end = &end
return true
}
return false
}
if *p.end+1 == end {
p.end = &end
return true
}
return false
}
func (p portRange) String() string {
res := fmt.Sprintf("%d", p.start)
if p.end != nil {
res += fmt.Sprintf("-%d", *p.end)
}
return res
}
// getPorts identifies all the published port ranges, merges them
// if they are consecutive, and return a string with all the published
// ports.
func getPorts(ports []composetypes.ServicePortConfig) string {
var (
portRanges []*portRange
lastPortRange *portRange
)
sort.Slice(ports, func(i int, j int) bool { return ports[i].Published < ports[j].Published })
for _, port := range ports {
if port.Published > 0 {
if lastPortRange == nil {
lastPortRange = newPort(port.Published)
} else if !lastPortRange.add(port.Published) {
portRanges = append(portRanges, lastPortRange)
lastPortRange = newPort(port.Published)
}
}
}
if lastPortRange != nil {
portRanges = append(portRanges, lastPortRange)
}
output := make([]string, len(portRanges))
for i, p := range portRanges {
output[i] = p.String()
}
return strings.Join(output, ",")
}
package internal
import (
"fmt"
"path/filepath"
"regexp"
"strings"
)
const (
// AppExtension is the extension used by an application.
AppExtension = ".dockerapp"
// ImageLabel is the label used to distinguish applications from Docker images.
ImageLabel = "com.docker.application"
// MetadataFileName is metadata file name
MetadataFileName = "metadata.yml"
// ComposeFileName is compose file name
ComposeFileName = "docker-compose.yml"
// ParametersFileName is parameters file name
ParametersFileName = "parameters.yml"
// DeprecatedSettingsFileName is the deprecated settings file name (replaced by ParametersFileName)
DeprecatedSettingsFileName = "settings.yml"
// Namespace is the reverse DNS namespace used with labels and CNAB custom actions.
Namespace = "com.docker.app."
)
var (
// FileNames lists the application file names, in order.
FileNames = []string{MetadataFileName, ComposeFileName, ParametersFileName}
)
var appNameRe, _ = regexp.Compile("^[a-zA-Z][a-zA-Z0-9_-]+$")
// AppNameFromDir takes a path to an app directory and returns
// the application's name
func AppNameFromDir(dirName string) string {
return strings.TrimSuffix(filepath.Base(dirName), AppExtension)
}
// DirNameFromAppName takes an application name and returns the
// corresponding directory name
func DirNameFromAppName(appName string) string {
if strings.HasSuffix(strings.TrimSuffix(appName, "/"), AppExtension) {
return appName
}
return appName + AppExtension
}
// ValidateAppName takes an app name and returns an error if it doesn't
// match the expected format
func ValidateAppName(appName string) error {
if appNameRe.MatchString(appName) {
return nil
}
return fmt.Errorf(
"invalid app name: %s ; app names must start with a letter, and must contain only letters, numbers, '-' and '_' (regexp: %q)",
appName,
appNameRe.String(),
)
}
package packager
import (
"github.com/deislabs/duffle/pkg/bundle"
"github.com/docker/app/internal"
"github.com/docker/app/internal/compose"
"github.com/docker/app/types"
)
// ToCNAB creates a CNAB bundle from an app package
func ToCNAB(app *types.App, invocationImageName string) (*bundle.Bundle, error) {
mapping := ExtractCNABParameterMapping(app.Parameters())
flatParameters := app.Parameters().Flatten()
parameters := map[string]bundle.ParameterDefinition{
"docker.orchestrator": {
DataType: "string",
AllowedValues: []interface{}{
"",
"swarm",
"kubernetes",
},
DefaultValue: "",
Destination: &bundle.Location{
EnvironmentVariable: "DOCKER_STACK_ORCHESTRATOR",
},
Metadata: bundle.ParameterMetadata{
Description: "Orchestrator on which to deploy",
},
},
"docker.kubernetes-namespace": {
DataType: "string",
Destination: &bundle.Location{
EnvironmentVariable: "DOCKER_KUBERNETES_NAMESPACE",
},
Metadata: bundle.ParameterMetadata{
Description: "Namespace in which to deploy",
},
DefaultValue: "",
},
}
for name, envVar := range mapping.ParameterToCNABEnv {
parameters[name] = bundle.ParameterDefinition{
DataType: "string",
Destination: &bundle.Location{
EnvironmentVariable: envVar,
},
DefaultValue: flatParameters[name],
}
}
var maintainers []bundle.Maintainer
for _, m := range app.Metadata().Maintainers {
maintainers = append(maintainers, bundle.Maintainer{
Email: m.Email,
Name: m.Name,
})
}
bundleImages, err := extractBundleImages(app.Composes())
if err != nil {
return nil, err
}
return &bundle.Bundle{
Credentials: map[string]bundle.Location{
"docker.context": {
Path: "/cnab/app/context.dockercontext",
},
},
Description: app.Metadata().Description,
InvocationImages: []bundle.InvocationImage{
{
BaseImage: bundle.BaseImage{
Image: invocationImageName,
ImageType: "docker",
},
},
},
Maintainers: maintainers,
Name: app.Metadata().Name,
Version: app.Metadata().Version,
Parameters: parameters,
Actions: map[string]bundle.Action{
internal.Namespace + "inspect": {
Modifies: false,
},
internal.Namespace + "status": {
Modifies: false,
},
},
Images: bundleImages,
}, nil
}
func extractBundleImages(composeFiles [][]byte) (map[string]bundle.Image, error) {
_, images, err := compose.Load(composeFiles, func(v string) (string, error) { return v, nil })
if err != nil {
return nil, err
}
bundleImages := map[string]bundle.Image{}
for serviceName, imageName := range images {
bundleImages[serviceName] = bundle.Image{
Description: imageName,
BaseImage: bundle.BaseImage{
Image: imageName,
ImageType: "docker",
},
}
}
return bundleImages, nil
}
package packager
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
"github.com/docker/app/internal"
"github.com/docker/app/loader"
"github.com/docker/app/types"
"github.com/docker/distribution/reference"
"github.com/pkg/errors"
)
// findApp looks for an app in CWD or subdirs
func findApp() (string, error) {
cwd, err := os.Getwd()
if err != nil {
return "", errors.Wrap(err, "cannot resolve current working directory")
}
if strings.HasSuffix(cwd, internal.AppExtension) {
return cwd, nil
}
content, err := ioutil.ReadDir(cwd)
if err != nil {
return "", errors.Wrap(err, "failed to read current working directory")
}
hit := ""
for _, c := range content {
if strings.HasSuffix(c.Name(), internal.AppExtension) {
if hit != "" {
return "", fmt.Errorf("Error: multiple applications found in current directory, specify the application name on the command line")
}
hit = c.Name()
}
}
if hit == "" {
return "", fmt.Errorf("no application found in current directory")
}
return filepath.Join(cwd, hit), nil
}
func appNameFromRef(ref reference.Named) string {
parts := strings.Split(ref.Name(), "/")
return internal.DirNameFromAppName(parts[len(parts)-1])
}
func imageNameFromRef(ref reference.Named) string {
if tagged, ok := ref.(reference.Tagged); ok {
name := internal.DirNameFromAppName(ref.Name())
newRef, _ := reference.WithName(name)
newtaggedRef, _ := reference.WithTag(newRef, tagged.Tag())
return newtaggedRef.String()
}
return internal.DirNameFromAppName(ref.String())
}
// extractImage extracts a docker application in a docker image to a temporary directory
func extractImage(appname string, ops ...func(*types.App) error) (*types.App, error) {
ref, err := reference.ParseNormalizedNamed(appname)
if err != nil {
return nil, err
}
literalImageName := appname
imagename := imageNameFromRef(ref)
appname = appNameFromRef(ref)
tempDir, err := ioutil.TempDir("", "dockerapp")
if err != nil {
return nil, errors.Wrap(err, "failed to create temporary directory")
}
// Attempt loading image based on default name permutation
path, err := Pull(imagename, tempDir)
if err != nil {
if literalImageName == imagename {
os.RemoveAll(tempDir)
return nil, err
}
// Attempt loading image based on the literal name
path, err = Pull(literalImageName, tempDir)
if err != nil {
os.RemoveAll(tempDir)
return nil, err
}
}
ops = append(ops, types.WithName(appname), types.WithCleanup(func() { os.RemoveAll(tempDir) }))
return loader.LoadFromDirectory(path, 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(name string, ops ...func(*types.App) error) (*types.App, error) {
if name == "" {
var err error
if name, err = findApp(); err != nil {
return nil, err
}
}
if name == "." {
var err error
if name, err = os.Getwd(); err != nil {
return nil, errors.Wrap(err, "cannot resolve current working directory")
}
}
ops = append(ops, types.WithName(name))
appname := internal.DirNameFromAppName(name)
s, err := os.Stat(appname)
if err != nil {
// look for a docker image
ops = append(ops, types.WithSource(types.AppSourceImage))
app, err := extractImage(name, ops...)
return app, errors.Wrapf(err, "cannot locate application %q in filesystem or registry", name)
}
if s.IsDir() {
// directory: already decompressed
appOpts := append(ops,
types.WithPath(appname),
types.WithSource(types.AppSourceSplit),
)
return loader.LoadFromDirectory(appname, appOpts...)
}
// not a dir: single-file or a tarball package, extract that in a temp dir
app, err := loader.LoadFromTar(appname, ops...)
if err != nil {
f, err := os.Open(appname)
if err != nil {
return nil, err
}
defer f.Close()
ops = append(ops, types.WithSource(types.AppSourceMerged))
return loader.LoadFromSingleFile(appname, f, ops...)
}
app.Source = types.AppSourceArchive
return app, nil
}
package packager
import (
"bytes"
"fmt"
"io"
"io/ioutil"
"os"
"os/user"
"path/filepath"
"strings"
"text/template"
"github.com/docker/app/internal"
"github.com/docker/app/internal/compose"
"github.com/docker/app/internal/yaml"
"github.com/docker/app/loader"
"github.com/docker/app/types"
"github.com/docker/app/types/metadata"
composeloader "github.com/docker/cli/cli/compose/loader"
"github.com/docker/cli/cli/compose/schema"
"github.com/docker/cli/opts"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
)
func prependToFile(filename, text string) error {
content, _ := ioutil.ReadFile(filename)
content = append([]byte(text), content...)
return ioutil.WriteFile(filename, content, 0644)
}
// Init is the entrypoint initialization function.
// It generates a new application package based on the provided parameters.
func Init(name string, composeFile string, description string, maintainers []string, singleFile bool) error {
if err := internal.ValidateAppName(name); err != nil {
return err
}
dirName := internal.DirNameFromAppName(name)
if err := os.Mkdir(dirName, 0755); err != nil {
return errors.Wrap(err, "failed to create application directory")
}
var err error
defer func() {
if err != nil {
os.RemoveAll(dirName)
}
}()
if err = writeMetadataFile(name, dirName, description, maintainers); err != nil {
return err
}
if composeFile == "" {
if _, err := os.Stat(internal.ComposeFileName); err == nil {
composeFile = internal.ComposeFileName
}
}
if composeFile == "" {
err = initFromScratch(name)
} else {
err = initFromComposeFile(name, composeFile)
}
if err != nil {
return err
}
if !singleFile {
return nil
}
// Merge as a single file
// Add some helfpful comments to distinguish the sections
if err := prependToFile(filepath.Join(dirName, internal.ComposeFileName), "# This section contains the Compose file that describes your application services.\n"); err != nil {
return err
}
if err := prependToFile(filepath.Join(dirName, internal.ParametersFileName), "# This section contains the default values for your application parameters.\n"); err != nil {
return err
}
if err := prependToFile(filepath.Join(dirName, internal.MetadataFileName), "# This section contains your application metadata.\n"); err != nil {
return err
}
temp := "_temp_dockerapp__.dockerapp"
err = os.Rename(dirName, temp)
if err != nil {
return err
}
defer os.RemoveAll(temp)
var target io.Writer
target, err = os.Create(dirName)
if err != nil {
return err
}
defer target.(io.WriteCloser).Close()
app, err := loader.LoadFromDirectory(temp)
if err != nil {
return err
}
return Merge(app, target)
}
func initFromScratch(name string) error {
log.Debug("init from scratch")
composeData, err := composeFileFromScratch()
if err != nil {
return err
}
dirName := internal.DirNameFromAppName(name)
if err := ioutil.WriteFile(filepath.Join(dirName, internal.ComposeFileName), composeData, 0644); err != nil {
return err
}
return ioutil.WriteFile(filepath.Join(dirName, internal.ParametersFileName), []byte{'\n'}, 0644)
}
func checkComposeFileVersion(compose map[string]interface{}) error {
version, ok := compose["version"]
if !ok {
return fmt.Errorf("unsupported Compose file version: version 1 is too low")
}
return schema.Validate(compose, fmt.Sprintf("%v", version))
}
func initFromComposeFile(name string, composeFile string) error {
log.Debug("init from compose")
dirName := internal.DirNameFromAppName(name)
composeRaw, err := ioutil.ReadFile(composeFile)
if err != nil {
return errors.Wrap(err, "failed to read compose file")
}
cfgMap, err := composeloader.ParseYAML(composeRaw)
if err != nil {
return errors.Wrap(err, "failed to parse compose file")
}
if err := checkComposeFileVersion(cfgMap); err != nil {
return err
}
parameters := make(map[string]string)
envs, err := opts.ParseEnvFile(filepath.Join(filepath.Dir(composeFile), ".env"))
if err == nil {
for _, v := range envs {
kv := strings.SplitN(v, "=", 2)
if len(kv) == 2 {
parameters[kv[0]] = kv[1]
}
}
}
vars, err := compose.ExtractVariables(composeRaw, compose.ExtrapolationPattern)
if err != nil {
return errors.Wrap(err, "failed to parse compose file")
}
needsFilling := false
for k, v := range vars {
if _, ok := parameters[k]; !ok {
if v != "" {
parameters[k] = v
} else {
parameters[k] = "FILL ME"
needsFilling = true
}
}
}
parametersYAML, err := yaml.Marshal(parameters)
if err != nil {
return errors.Wrap(err, "failed to marshal parameters")
}
err = ioutil.WriteFile(filepath.Join(dirName, internal.ComposeFileName), composeRaw, 0644)
if err != nil {
return errors.Wrap(err, "failed to write docker-compose.yml")
}
err = ioutil.WriteFile(filepath.Join(dirName, internal.ParametersFileName), parametersYAML, 0644)
if err != nil {
return errors.Wrap(err, "failed to write parameters.yml")
}
if needsFilling {
fmt.Println("You will need to edit parameters.yml to fill in default values.")
}
return nil
}
func composeFileFromScratch() ([]byte, error) {
fileStruct := types.NewInitialComposeFile()
return yaml.Marshal(fileStruct)
}
const metaTemplate = `# Version of the application
version: {{ .Version }}
# Name of the application
name: {{ .Name }}
# A short description of the application
description: {{ .Description }}
# List of application maintainers with name and email for each
{{ if len .Maintainers }}maintainers:
{{ range .Maintainers }} - name: {{ .Name }}
email: {{ .Email }}
{{ end }}{{ else }}#maintainers:
# - name: John Doe
# email: john@doe.com
{{ end }}`
func writeMetadataFile(name, dirName string, description string, maintainers []string) error {
meta := newMetadata(name, description, maintainers)
tmpl, err := template.New("metadata").Parse(metaTemplate)
if err != nil {
return errors.Wrap(err, "internal error parsing metadata template")
}
resBuf := &bytes.Buffer{}
if err := tmpl.Execute(resBuf, meta); err != nil {
return errors.Wrap(err, "error generating metadata")
}
return ioutil.WriteFile(filepath.Join(dirName, internal.MetadataFileName), resBuf.Bytes(), 0644)
}
// parseMaintainersData parses user-provided data through the maintainers flag and returns
// a slice of Maintainer instances
func parseMaintainersData(maintainers []string) []metadata.Maintainer {
var res []metadata.Maintainer
for _, m := range maintainers {
ne := strings.SplitN(m, ":", 2)
var email string
if len(ne) > 1 {
email = ne[1]
}
res = append(res, metadata.Maintainer{Name: ne[0], Email: email})
}
return res
}
func newMetadata(name string, description string, maintainers []string) metadata.AppMetadata {
res := metadata.AppMetadata{
Version: "0.1.0",
Name: name,
Description: description,
}
if len(maintainers) == 0 {
userData, _ := user.Current()
if userData != nil && userData.Username != "" {
res.Maintainers = []metadata.Maintainer{{Name: userData.Username}}
}
} else {
res.Maintainers = parseMaintainersData(maintainers)
}
return res
}
package packager
import (
"archive/tar"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"github.com/docker/app/internal"
"github.com/docker/app/types"
"github.com/docker/docker/pkg/archive"
"github.com/pkg/errors"
)
var dockerFile = `FROM docker/cnab-app-base:` + internal.Version + `
COPY . .`
const dockerIgnore = "Dockerfile"
func tarAdd(tarout *tar.Writer, path, file string) error {
payload, err := ioutil.ReadFile(file)
if err != nil {
return err
}
return tarAddBytes(tarout, path, payload)
}
func tarAddBytes(tarout *tar.Writer, path string, payload []byte) error {
h := &tar.Header{
Name: path,
Size: int64(len(payload)),
Mode: 0644,
Typeflag: tar.TypeReg,
}
err := tarout.WriteHeader(h)
if err != nil {
return err
}
_, err = tarout.Write(payload)
return err
}
// PackInvocationImageContext creates a Docker build context for building a CNAB invocation image
func PackInvocationImageContext(app *types.App, target io.Writer) error {
tarout := tar.NewWriter(target)
defer tarout.Close()
prefix := fmt.Sprintf("%s%s/", app.Metadata().Name, internal.AppExtension)
if len(app.Composes()) != 1 {
return errors.New("app should have one and only one compose file")
}
if len(app.ParametersRaw()) != 1 {
return errors.New("app should have one and only one parameters file")
}
if err := tarAddBytes(tarout, "Dockerfile", []byte(dockerFile)); err != nil {
return errors.Wrap(err, "failed to add Dockerfile to the invocation image build context")
}
if err := tarAddBytes(tarout, ".dockerignore", []byte(dockerIgnore)); err != nil {
return errors.Wrap(err, "failed to add .dockerignore to the invocation image build context")
}
if err := tarAddBytes(tarout, prefix+internal.MetadataFileName, app.MetadataRaw()); err != nil {
return errors.Wrapf(err, "failed to add %q to the invocation image build context", prefix+internal.MetadataFileName)
}
if err := tarAddBytes(tarout, prefix+internal.ComposeFileName, app.Composes()[0]); err != nil {
return errors.Wrapf(err, "failed to add %q to the invocation image build context", prefix+internal.ComposeFileName)
}
if err := tarAddBytes(tarout, prefix+internal.ParametersFileName, app.ParametersRaw()[0]); err != nil {
return errors.Wrapf(err, "failed to add %q to the invocation image build context", prefix+internal.ParametersFileName)
}
for _, attachment := range app.Attachments() {
if err := tarAdd(tarout, prefix+attachment.Path(), filepath.Join(app.Path, attachment.Path())); err != nil {
return errors.Wrapf(err, "failed to add attachment %q to the invocation image build context", prefix+attachment.Path())
}
}
return nil
}
// Pack packs the app as a single file
func Pack(appname string, target io.Writer) error {
tarout := tar.NewWriter(target)
for _, f := range internal.FileNames {
err := tarAdd(tarout, f, filepath.Join(appname, f))
if err != nil {
return err
}
}
// check for images
dir := "images"
_, err := os.Stat(filepath.Join(appname, dir))
if err == nil {
if err := tarout.WriteHeader(&tar.Header{
Typeflag: tar.TypeDir,
Name: dir,
Mode: 0755,
}); err != nil {
return err
}
imageDir, err := os.Open(filepath.Join(appname, dir))
if err != nil {
return err
}
defer imageDir.Close()
images, err := imageDir.Readdirnames(0)
if err != nil {
return err
}
for _, i := range images {
err = tarAdd(tarout, filepath.Join(dir, i), filepath.Join(appname, dir, i))
if err != nil {
return err
}
}
}
return tarout.Close()
}
// Unpack extracts a packed app
func Unpack(appname, targetDir string) error {
s, err := os.Stat(appname)
if err != nil {
// try appending our extension
appname = internal.DirNameFromAppName(appname)
s, err = os.Stat(appname)
}
if err != nil {
return err
}
if s.IsDir() {
return fmt.Errorf("app already extracted")
}
out := filepath.Join(targetDir, internal.AppNameFromDir(appname)+internal.AppExtension)
err = os.Mkdir(out, 0755)
if err != nil {
return err
}
f, err := os.Open(appname)
if err != nil {
return err
}
defer f.Close()
return archive.Untar(f, out, &archive.TarOptions{
NoLchown: true,
})
}
package packager
import (
"fmt"
"sort"
"strings"
"github.com/docker/app/types/parameters"
)
// CNABParametersMapping describes the desired mapping between parameters and CNAB environment variables
type CNABParametersMapping struct {
CNABEnvToParameter map[string]string
ParameterToCNABEnv map[string]string
}
// ExtractCNABParameterMapping extracts the CNABParametersMapping from application parameters
func ExtractCNABParameterMapping(parameters parameters.Parameters) CNABParametersMapping {
keys := getKeys("", parameters)
sort.Strings(keys)
mapping := CNABParametersMapping{
CNABEnvToParameter: make(map[string]string),
ParameterToCNABEnv: make(map[string]string),
}
for ix, key := range keys {
env := fmt.Sprintf("docker_param%d", ix+1)
mapping.CNABEnvToParameter[env] = key
mapping.ParameterToCNABEnv[key] = env
}
return mapping
}
func getKeys(prefix string, parameters map[string]interface{}) []string {
var keys []string
for k, v := range parameters {
sub, ok := v.(map[string]interface{})
if ok {
subPrefix := prefix
subPrefix += fmt.Sprintf("%s.", k)
keys = append(keys, getKeys(subPrefix, sub)...)
} else {
keys = append(keys, prefix+k)
}
}
return keys
}
// ExtractCNABParametersValues extracts the parameter values from the given CNAB environment
func ExtractCNABParametersValues(mapping CNABParametersMapping, env []string) map[string]string {
envValues := map[string]string{}
for _, v := range env {
parts := strings.SplitN(v, "=", 2)
if len(parts) == 2 {
if key, ok := mapping.CNABEnvToParameter[parts[0]]; ok {
envValues[key] = parts[1]
}
}
}
return envValues
}
package packager
import (
"context"
"io/ioutil"
"os"
"path/filepath"
"strings"
"github.com/docker/app/internal"
"github.com/docker/app/pkg/resto"
"github.com/docker/app/types"
"github.com/docker/distribution/reference"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
)
type imageComponents struct {
Name string
Repository string
Tag string
}
func splitImageName(repotag string) (*imageComponents, error) {
named, err := reference.ParseNormalizedNamed(repotag)
if err != nil {
return nil, errors.Wrap(err, "failed to parse image name")
}
res := &imageComponents{
Repository: named.Name(),
}
res.Name = res.Repository[strings.LastIndex(res.Repository, "/")+1:]
if tagged, ok := named.(reference.Tagged); ok {
res.Tag = tagged.Tag()
}
return res, nil
}
// Pull loads an app from a registry and returns the extracted dir name
func Pull(repotag string, outputDir string) (string, error) {
imgRef, err := splitImageName(repotag)
if err != nil {
return "", errors.Wrapf(err, "origin %q is not a valid image name", repotag)
}
payload, err := resto.PullConfigMulti(context.Background(), repotag, resto.RegistryOptions{})
if err != nil {
return "", err
}
appDir := filepath.Join(outputDir, internal.DirNameFromAppName(imgRef.Name))
if err := os.Mkdir(appDir, 0755); err != nil {
return "", errors.Wrap(err, "failed to create output application directory")
}
if err := ExtractImagePayloadToDiskFiles(appDir, payload); err != nil {
return "", err
}
return appDir, nil
}
// ExtractImagePayloadToDiskFiles extracts all the files out of the image payload and onto disk
// creating all necessary folders in between.
func ExtractImagePayloadToDiskFiles(appDir string, payload map[string]string) error {
for localFilepath, filedata := range payload {
fileBytes := []byte(filedata)
// Deal with windows/linux slashes
convertedFilepath := filepath.FromSlash(localFilepath)
// Check we aren't doing ./../../../ etc in the path
fullFilepath := filepath.Join(appDir, convertedFilepath)
if _, err := filepath.Rel(appDir, fullFilepath); err != nil {
log.Warnf("dropping image entry %q with unexpected path outside of app dir", localFilepath)
continue
}
// Create the directories for any nested files
basepath := filepath.Dir(fullFilepath)
if err := os.MkdirAll(basepath, os.ModePerm); err != nil {
return errors.Wrapf(err, "failed to create directories for file: %s", fullFilepath)
}
if err := ioutil.WriteFile(fullFilepath, fileBytes, 0644); err != nil {
return errors.Wrapf(err, "failed to write output file: %s", fullFilepath)
}
}
return nil
}
// Push pushes an app to a registry. Returns the image digest.
func Push(app *types.App, tag string) (string, error) {
payload, err := createPayload(app)
if err != nil {
return "", errors.Wrap(err, "failed to read external file while creating payload for push")
}
imageName := createImageName(app, tag)
return resto.PushConfigMulti(context.Background(), payload, imageName, resto.RegistryOptions{}, nil)
}
func createImageName(app *types.App, registryReference string) string {
if registryReference != "" {
return registryReference
}
return app.Metadata().Name + ":" + app.Metadata().Version
}
func createPayload(app *types.App) (map[string]string, error) {
payload := map[string]string{
internal.MetadataFileName: string(app.MetadataRaw()),
internal.ComposeFileName: string(app.Composes()[0]),
internal.ParametersFileName: string(app.ParametersRaw()[0]),
}
if err := readAttachments(payload, app.Path, app.Attachments()); err != nil {
return nil, err
}
return payload, nil
}
func readAttachments(payload map[string]string, parentDirPath string, files []types.Attachment) error {
var errs []string
for _, file := range files {
// Convert to local OS filepath slash syntax
fullFilePath := filepath.Join(parentDirPath, filepath.FromSlash(file.Path()))
filedata, err := ioutil.ReadFile(fullFilePath)
if err != nil {
errs = append(errs, err.Error())
continue
}
payload[file.Path()] = string(filedata)
}
return newErrGroup(errs)
}
func newErrGroup(errs []string) error {
if len(errs) == 0 {
return nil
}
return errors.New(strings.Join(errs, "\n"))
}
package packager
import (
"io"
"io/ioutil"
"os"
"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(app *types.App, outputDir string) error {
if len(app.Composes()) > 1 {
return errors.New("split: multiple compose files is not supported")
}
if len(app.ParametersRaw()) > 1 {
return errors.New("split: multiple parameter files is not supported")
}
err := os.MkdirAll(outputDir, 0755)
if err != nil {
return err
}
for file, data := range map[string][]byte{
internal.MetadataFileName: app.MetadataRaw(),
internal.ComposeFileName: app.Composes()[0],
internal.ParametersFileName: app.ParametersRaw()[0],
} {
if err := ioutil.WriteFile(filepath.Join(outputDir, file), data, 0644); err != nil {
return err
}
}
return nil
}
// Merge converts an app-package to the single-file merged version
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.ParametersRaw()) > 1 {
return errors.New("merge: multiple parameter files is not supported")
}
for _, data := range [][]byte{
app.MetadataRaw(),
[]byte(types.SingleFileSeparator),
app.Composes()[0],
[]byte(types.SingleFileSeparator),
app.ParametersRaw()[0],
} {
if _, err := target.Write(data); err != nil {
return err
}
}
return nil
}
package renderer
import (
"sort"
"sync"
"github.com/docker/app/internal/renderer/driver"
"github.com/pkg/errors"
)
var (
driversMu sync.RWMutex
drivers = map[string]driver.Driver{}
)
// Register makes a renderer available by the provided name.
// If Register is called twice with the same name or if driver is nil,
// it panics.
func Register(name string, driver driver.Driver) {
driversMu.Lock()
defer driversMu.Unlock()
if driver == nil {
panic("renderer: Register driver is nil")
}
if _, dup := drivers[name]; dup {
panic("renderer: Register called twice for driver " + name)
}
drivers[name] = driver
}
// Apply applies the specified render to the specified string with the specified parameters.
// If the render is not present is the registered ones, it errors out.
func Apply(s string, parameters map[string]interface{}, renderers ...string) (string, error) {
var err error
for _, r := range renderers {
if r == "none" {
continue
}
driversMu.RLock()
d, present := drivers[r]
driversMu.RUnlock()
if !present {
return "", errors.Errorf("unknown renderer %s", r)
}
s, err = d.Apply(s, parameters)
if err != nil {
return "", err
}
}
return s, nil
}
// Drivers returns a sorted list of the names of the registered drivers.
func Drivers() []string {
list := []string{"none"}
driversMu.RLock()
for name := range drivers {
list = append(list, name)
}
driversMu.RUnlock()
sort.Strings(list)
return list
}
package slices
// ContainsString checks wether the given string is in the specified slice
func ContainsString(strings []string, s string) bool {
for _, e := range strings {
if e == s {
return true
}
}
return false
}
package internal
import (
"fmt"
"runtime"
"strings"
"time"
"github.com/docker/app/internal/renderer"
)
var (
// Version is the git tag that this was built from.
Version = "unknown"
// GitCommit is the commit that this was built from.
GitCommit = "unknown"
// BuildTime is the time at which the binary was built.
BuildTime = "unknown"
)
// FullVersion returns a string of version information.
func FullVersion() string {
res := []string{
fmt.Sprintf("Version: %s", Version),
fmt.Sprintf("Git commit: %s", GitCommit),
fmt.Sprintf("Built: %s", reformatDate(BuildTime)),
fmt.Sprintf("OS/Arch: %s/%s", runtime.GOOS, runtime.GOARCH),
fmt.Sprintf("Experimental: %s", Experimental),
fmt.Sprintf("Renderers: %s", strings.Join(renderer.Drivers(), ", ")),
}
return strings.Join(res, "\n")
}
// FIXME(chris-crone): use function in docker/cli/cli/command/system/version.go.
func reformatDate(buildTime string) string {
t, errTime := time.Parse(time.RFC3339Nano, buildTime)
if errTime == nil {
return t.Format(time.ANSIC)
}
return buildTime
}
package yaml
import (
"bytes"
"io"
"gopkg.in/yaml.v2"
)
const (
maxDecodedValues = 1000000
)
// Unmarshal decodes the first document found within the in byte slice
// and assigns decoded values into the out value.
//
// See gopkg.in/yaml.v2 documentation
func Unmarshal(in []byte, out interface{}) error {
d := yaml.NewDecoder(bytes.NewBuffer(in), yaml.WithLimitDecodedValuesCount(maxDecodedValues))
err := d.Decode(out)
if err == io.EOF {
return nil
}
return err
}
// Marshal serializes the value provided into a YAML document. The structure
// of the generated document will reflect the structure of the value itself.
// Maps and pointers (to struct, string, int, etc) are accepted as the in value.
//
// See gopkg.in/yaml.v2 documentation
func Marshal(in interface{}) ([]byte, error) {
return yaml.Marshal(in)
}
// NewDecoder returns a new decoder that reads from r.
//
// See gopkg.in/yaml.v2 documentation
func NewDecoder(r io.Reader) *yaml.Decoder {
return yaml.NewDecoder(r, yaml.WithLimitDecodedValuesCount(maxDecodedValues))
}
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 parameters
parameters := strings.NewReader(parts[2])
appOps := append([]func(*types.App) error{
types.WithComposes(compose),
types.WithParameters(parameters),
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) {
if _, err := os.Stat(filepath.Join(path, internal.ParametersFileName)); os.IsNotExist(err) {
if _, err := os.Stat(filepath.Join(path, internal.DeprecatedSettingsFileName)); err == nil {
return nil, errors.Errorf("\"settings.yml\" has been deprecated in favor of \"parameters.yml\"; please rename \"settings.yml\" to \"parameters.yml\"")
}
}
return types.NewAppFromDefaultFiles(path, ops...)
}
// 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")
}
defer f.Close()
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 {
originalErr := errors.Wrap(err, "cannot load app from tar")
if err := os.RemoveAll(dir); err != nil {
return nil, errors.Wrapf(originalErr, "cannot remove temporary folder : %s", err.Error())
}
return nil, originalErr
}
appOps := append([]func(*types.App) error{
types.WithCleanup(func() {
os.RemoveAll(dir)
}),
}, ops...)
return LoadFromDirectory(dir, appOps...)
}
package resto
import (
"context"
"crypto/tls"
"net"
"net/http"
"net/url"
"strings"
"time"
"github.com/docker/distribution"
"github.com/docker/distribution/registry/client/auth"
"github.com/docker/distribution/registry/client/transport"
"github.com/docker/docker/api/types"
dd "github.com/docker/docker/distribution"
"github.com/docker/docker/registry"
digest "github.com/opencontainers/go-digest"
)
type myreference string
func (m myreference) String() string {
return string(m)
}
func (m myreference) Name() string {
return string(m)
}
// MediaTypeConfig is the media type used for configuration files.
const MediaTypeConfig = "application/vndr.docker.config"
// ConfigManifest is a Manifest type holding arbitrary data.
type ConfigManifest struct {
mediaType string
payload []byte
}
// References returns the objects this Manifest refers to.
func (c *ConfigManifest) References() []distribution.Descriptor {
return nil
}
// Payload returns the mediatype and payload of this manifest.
func (c *ConfigManifest) Payload() (string, []byte, error) {
return c.mediaType, c.payload, nil
}
// NewConfigManifest creates and returns an new ConfigManifest.
func NewConfigManifest(mediaType string, payload []byte) *ConfigManifest {
return &ConfigManifest{mediaType, payload}
}
func init() {
distribution.RegisterManifestSchema(MediaTypeConfig, func(b []byte) (distribution.Manifest, distribution.Descriptor, error) {
return &ConfigManifest{
mediaType: MediaTypeConfig,
payload: b,
},
distribution.Descriptor{
MediaType: MediaTypeConfig,
Size: int64(len(b)),
Digest: digest.SHA256.FromBytes(b),
}, nil
})
}
// NewRepository instantiates a distribution.Repository pointing to the given target, with credentials
func NewRepository(ctx context.Context, endpoint string, repository string, opts RegistryOptions) (distribution.Repository, error) {
named := myreference(repository)
authConfig := &types.AuthConfig{
Username: opts.Username,
Password: opts.Password,
}
url, err := url.Parse(endpoint)
if err != nil {
return nil, err
}
apiendpoint := registry.APIEndpoint{
Mirror: false,
URL: url,
Version: 2,
TLSConfig: &tls.Config{InsecureSkipVerify: opts.Insecure},
}
repoInfo, err := registry.ParseRepositoryInfo(named)
if err != nil {
return nil, err
}
repo, _, err := dd.NewV2Repository(ctx, repoInfo, apiendpoint, nil, authConfig, "push", "pull")
if err == nil {
return repo, nil
}
if !strings.Contains(err.Error(), "HTTP response to HTTPS client") {
return nil, err
}
if !opts.CleartextCredentials {
// Don't use credentials over insecure connection unless instnucted to
authConfig.Username = ""
authConfig.Password = ""
}
endpointHTTP := strings.Replace(endpoint, "https://", "http://", 1)
urlHTTP, err := url.Parse(endpointHTTP)
if err != nil {
return nil, err
}
apiendpoint.URL = urlHTTP
repo, _, err = dd.NewV2Repository(ctx, repoInfo, apiendpoint, nil, authConfig, "push", "pull")
return repo, err
}
// NewTransportCatalog returns a transport suitable for a catalog operation
func NewTransportCatalog(endpoint string, opts RegistryOptions) (http.RoundTripper, error) {
// taken from docker/distribution/registry.go
direct := &net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
DualStack: true,
}
base := &http.Transport{
Proxy: http.ProxyFromEnvironment,
Dial: direct.Dial,
TLSHandshakeTimeout: 10 * time.Second,
TLSClientConfig: &tls.Config{InsecureSkipVerify: opts.Insecure},
DisableKeepAlives: true,
}
authTransport := transport.NewTransport(base)
endpointURL, err := url.Parse(endpoint)
if err != nil {
return nil, err
}
challengeManager, _, err := registry.PingV2Registry(endpointURL, authTransport)
if err != nil {
return nil, err
}
scope := auth.RegistryScope{
Name: "catalog",
Actions: []string{"*"},
}
authConfig := &types.AuthConfig{
Username: opts.Username,
Password: opts.Password,
}
creds := registry.NewStaticCredentialStore(authConfig)
tokenHandlerOptions := auth.TokenHandlerOptions{
Transport: authTransport,
Credentials: creds,
Scopes: []auth.Scope{scope},
ClientID: registry.AuthClientID,
}
tokenHandler := auth.NewTokenHandlerWithOptions(tokenHandlerOptions)
basicHandler := auth.NewBasicHandler(creds)
tr := transport.NewTransport(base, auth.NewAuthorizer(challengeManager, tokenHandler, basicHandler))
return tr, nil
}
package resto
import (
"archive/tar"
"bytes"
"compress/gzip"
"context"
"encoding/json"
"fmt"
"io"
"strings"
"time"
"github.com/docker/cli/cli/config"
"github.com/docker/distribution"
"github.com/docker/distribution/manifest"
"github.com/docker/distribution/manifest/schema2"
"github.com/docker/distribution/reference"
"github.com/docker/distribution/registry/client"
digest "github.com/opencontainers/go-digest"
ociv1 "github.com/opencontainers/image-spec/specs-go/v1"
log "github.com/sirupsen/logrus"
)
// RegistryOptions contains optional configuration for Registry operations
type RegistryOptions struct {
Username string
Password string
Insecure bool
CleartextCredentials bool
}
type unsupportedMediaType struct{}
func (u unsupportedMediaType) Error() string {
return "Unsupported media type"
}
// ManifestAny is a manifest type for arbitrary configuration data
type ManifestAny struct {
manifest.Versioned
Payload string `json:"payload,omitempty"`
}
type parsedReference struct {
domain string
path string
tag string
}
func parseRef(repoTag string) (parsedReference, error) {
rawref, err := reference.ParseNormalizedNamed(repoTag)
if err != nil {
return parsedReference{}, err
}
ref, ok := rawref.(reference.Named)
if !ok {
return parseRef("docker.io/" + repoTag)
}
tag := "latest"
if rt, ok := ref.(reference.Tagged); ok {
tag = rt.Tag()
}
domain := reference.Domain(ref)
if domain == "docker.io" {
domain = "registry-1.docker.io"
}
return parsedReference{"https://" + domain, reference.Path(ref), tag}, nil
}
func getCredentials(domain string) (string, string, error) {
cfg, err := config.Load("")
if err != nil {
return "", "", err
}
switch domain {
case "https://registry-1.docker.io":
domain = "https://index.docker.io/v1/"
default:
domain = strings.TrimPrefix(domain, "https://")
}
auth, err := cfg.GetAuthConfig(domain)
if err != nil {
return "", "", err
}
return auth.Username, auth.Password, nil
}
func makeTarGz(content map[string]string) ([]byte, digest.Digest, error) {
buf := bytes.NewBuffer(nil)
err := func() error {
w := tar.NewWriter(buf)
defer w.Close()
for k, v := range content {
if err := w.WriteHeader(&tar.Header{
Typeflag: tar.TypeReg,
Name: k,
Mode: 0600,
Size: int64(len(v)),
}); err != nil {
return err
}
if _, err := w.Write([]byte(v)); err != nil {
return err
}
}
return nil
}()
if err != nil {
return nil, "", err
}
dgst := digest.SHA256.FromBytes(buf.Bytes())
gzbuf := bytes.NewBuffer(nil)
g := gzip.NewWriter(gzbuf)
if _, err := g.Write(buf.Bytes()); err != nil {
return nil, "", err
}
if err := g.Close(); err != nil {
return nil, "", err
}
return gzbuf.Bytes(), dgst, nil
}
const maxRepositoryCount = 10000
// ListRepositories lists all the repositories in a registry
func ListRepositories(ctx context.Context, endpoint string, opts RegistryOptions) ([]string, error) {
tr, err := NewTransportCatalog(endpoint, opts)
if err != nil {
return nil, err
}
registry, err := client.NewRegistry(endpoint, tr)
if err != nil {
return nil, err
}
entries := make([]string, maxRepositoryCount)
count, err := registry.Repositories(ctx, entries, "")
if err != nil && err != io.EOF {
return nil, err
}
return entries[0:count], nil
}
// ListTags lists all the tags in a repository
func ListTags(ctx context.Context, reponame string, opts RegistryOptions) ([]string, error) {
pr, err := parseRef(reponame)
if err != nil {
return nil, err
}
repo, err := NewRepository(ctx, pr.domain, pr.path, opts)
if err != nil {
return nil, err
}
tagService := repo.Tags(ctx)
return tagService.All(ctx)
}
// PullConfig pulls a configuration file from a registry
func PullConfig(ctx context.Context, repoTag string, opts RegistryOptions) (string, error) {
res, err := PullConfigMulti(ctx, repoTag, opts)
if err != nil {
return "", err
}
return res["config"], nil
}
// PullConfigMulti pulls a set of configuration files from a registry
func PullConfigMulti(ctx context.Context, repoTag string, opts RegistryOptions) (map[string]string, error) {
pr, err := parseRef(repoTag)
if err != nil {
return nil, err
}
if opts.Username == "" {
opts.Username, opts.Password, err = getCredentials(pr.domain)
if err != nil {
log.Debugf("failed to get credentials for %s: %s", pr.domain, err)
}
}
repo, err := NewRepository(ctx, pr.domain, pr.path, opts)
if err != nil {
return nil, err
}
tagService := repo.Tags(ctx)
dgst, err := tagService.Get(ctx, pr.tag)
if err != nil {
return nil, err
}
manifestService, err := repo.Manifests(ctx)
if err != nil {
return nil, err
}
manifest, err := manifestService.Get(ctx, dgst.Digest)
if err != nil {
return nil, err
}
mediaType, payload, err := manifest.Payload()
if err != nil {
return nil, err
}
if mediaType == MediaTypeConfig {
var ma ManifestAny
if err := json.Unmarshal(payload, &ma); err != nil {
return nil, err
}
res := make(map[string]string)
err = json.Unmarshal([]byte(ma.Payload), &res)
return res, err
}
// legacy image mode
return pullConfigImage(ctx, manifest, repo)
}
func pullConfigImage(ctx context.Context, manifest distribution.Manifest, repo distribution.Repository) (map[string]string, error) {
refs := manifest.References()
if len(refs) != 2 {
return nil, fmt.Errorf("expected 2 references, found %v", len(refs))
}
// assume second element is the layer (first being the image config)
r := refs[1]
rdgst := r.Digest
blobsService := repo.Blobs(ctx)
payloadGz, err := blobsService.Get(ctx, rdgst)
if err != nil {
return nil, err
}
payloadBuf := bytes.NewBuffer(payloadGz)
gzf, err := gzip.NewReader(payloadBuf)
if err != nil {
return nil, err
}
tarReader := tar.NewReader(gzf)
return tarContent(tarReader)
}
func tarContent(tarReader *tar.Reader) (map[string]string, error) {
res := make(map[string]string)
for {
header, err := tarReader.Next()
if err == io.EOF {
break
}
if err != nil {
return res, err
}
if header.Typeflag == tar.TypeReg {
content := bytes.NewBuffer(nil)
io.Copy(content, tarReader)
res[header.Name] = content.String()
}
}
return res, nil
}
// PushConfig pushes a configuration file to a registry and returns its digest
func PushConfig(ctx context.Context, payload, repoTag string, opts RegistryOptions, labels map[string]string) (string, error) {
return PushConfigMulti(ctx, map[string]string{
"config": payload,
}, repoTag, opts, labels)
}
// PushConfigMulti pushes a set of configuration files to a registry and returns its digest
func PushConfigMulti(ctx context.Context, payload map[string]string, repoTag string, opts RegistryOptions, labels map[string]string) (string, error) {
pr, err := parseRef(repoTag)
if err != nil {
return "", err
}
if opts.Username == "" {
opts.Username, opts.Password, err = getCredentials(pr.domain)
if err != nil {
log.Debugf("failed to get credentials for %s: %s", pr.domain, err)
}
}
repo, err := NewRepository(ctx, pr.domain, pr.path, opts)
if err != nil {
return "", err
}
digest, err := pushConfigMediaType(ctx, payload, pr, repo)
if err == nil {
return digest, err
}
if _, ok := err.(unsupportedMediaType); ok {
return pushConfigLegacy(ctx, payload, pr, repo, labels)
}
return digest, err
}
func pushConfigMediaType(ctx context.Context, payload map[string]string, pr parsedReference, repo distribution.Repository) (string, error) {
j, err := json.Marshal(payload)
if err != nil {
return "", err
}
manifestAny := ManifestAny{
Versioned: manifest.Versioned{
SchemaVersion: 2,
MediaType: MediaTypeConfig,
},
Payload: string(j),
}
raw, err := json.Marshal(manifestAny)
if err != nil {
return "", err
}
manifestService, err := repo.Manifests(ctx)
if err != nil {
return "", err
}
manifest := NewConfigManifest(MediaTypeConfig, raw)
dgst, err := manifestService.Put(ctx, manifest, distribution.WithTag(pr.tag))
if err == nil {
return dgst.String(), nil
}
switch {
case strings.Contains(err.Error(), "manifest invalid"):
return "", unsupportedMediaType{}
case strings.Contains(err.Error(), "manifest Unknown"):
return "", unsupportedMediaType{}
default:
return "", err
}
}
func pushConfigLegacy(ctx context.Context, payload map[string]string, pr parsedReference, repo distribution.Repository, labels map[string]string) (string, error) {
manifestService, err := repo.Manifests(ctx)
if err != nil {
return "", err
}
// try legacy mode
// create payload
payloadGz, payloadUncompressedDigest, err := makeTarGz(payload)
if err != nil {
return "", err
}
blobsService := repo.Blobs(ctx)
payloadDesc, err := blobsService.Put(ctx, schema2.MediaTypeLayer, payloadGz)
if err != nil {
return "", err
}
payloadDesc.MediaType = schema2.MediaTypeLayer
// create dummy image config
now := time.Now()
imageConfig := ociv1.Image{
Created: &now,
Architecture: "config",
OS: "config",
Config: ociv1.ImageConfig{
Labels: labels,
},
RootFS: ociv1.RootFS{
Type: "layers",
DiffIDs: []digest.Digest{payloadUncompressedDigest},
},
History: []ociv1.History{
{CreatedBy: "COPY configfile /"},
},
}
icm, err := json.Marshal(imageConfig)
if err != nil {
return "", err
}
icDesc, err := blobsService.Put(ctx, schema2.MediaTypeImageConfig, icm)
if err != nil {
return "", err
}
icDesc.MediaType = schema2.MediaTypeImageConfig
man := schema2.Manifest{
Versioned: schema2.SchemaVersion,
Config: icDesc,
Layers: []distribution.Descriptor{payloadDesc},
}
dman, err := schema2.FromStruct(man)
if err != nil {
return "", err
}
dgst, err := manifestService.Put(ctx, dman, distribution.WithTag(pr.tag))
return dgst.String(), err
}
package yatee
import (
"fmt"
"io/ioutil"
"os"
"strconv"
"strings"
"github.com/docker/app/internal/yaml"
yml "gopkg.in/yaml.v2"
)
const (
// OptionErrOnMissingKey if set will make rendering fail if a non-existing variable is used
OptionErrOnMissingKey = "ErrOnMissingKey"
)
type options struct {
errOnMissingKey bool
}
// flatten flattens a structure: foo.bar.baz -> 'foo.bar.baz'
func flatten(in map[string]interface{}, out map[string]interface{}, prefix string) {
for k, v := range in {
switch vv := v.(type) {
case string:
out[prefix+k] = vv
case map[string]interface{}:
flatten(vv, out, prefix+k+".")
case []interface{}:
values := []string{}
for _, i := range vv {
values = append(values, fmt.Sprintf("%v", i))
}
out[prefix+k] = strings.Join(values, " ")
default:
out[prefix+k] = v
}
}
}
func merge(res map[string]interface{}, src map[interface{}]interface{}) {
for k, v := range src {
kk, ok := k.(string)
if !ok {
panic(fmt.Sprintf("fatal error, key %v in %#v is not a string", k, src))
}
eval, ok := res[kk]
switch vv := v.(type) {
case map[interface{}]interface{}:
if !ok {
res[kk] = make(map[string]interface{})
} else {
if _, ok2 := eval.(map[string]interface{}); !ok2 {
res[kk] = make(map[string]interface{})
}
}
merge(res[kk].(map[string]interface{}), vv)
default:
res[kk] = vv
}
}
}
// LoadParameters loads a set of parameters file and produce a property dictionary
func LoadParameters(files []string) (map[string]interface{}, error) {
res := make(map[string]interface{})
for _, f := range files {
data, err := ioutil.ReadFile(f)
if err != nil {
return nil, err
}
s := make(map[interface{}]interface{})
err = yaml.Unmarshal(data, &s)
if err != nil {
return nil, err
}
merge(res, s)
}
return res, nil
}
func isIdentNumChar(r byte) bool {
return (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') ||
r == '.' || r == '_'
}
// extract extracts an expression from a string
// nolint: gocyclo
func extract(expr string) (string, error) {
if expr == "" {
return "", nil
}
if expr[0] == '{' {
closing := strings.Index(expr, "}")
if closing == -1 {
return "", fmt.Errorf("Missing '}' at end of expression")
}
return expr[0 : closing+1], nil
}
if expr[0] == '(' {
indent := 1
i := 1
for ; i < len(expr); i++ {
if expr[i] == '(' {
indent++
}
if expr[i] == ')' {
indent--
}
if indent == 0 {
break
}
}
if indent != 0 {
return "", fmt.Errorf("Missing ')' at end of expression")
}
return expr[0 : i+1], nil
}
i := 0
for ; i < len(expr); i++ {
if !((expr[i] >= 'a' && expr[i] <= 'z') || (expr[i] >= 'A' && expr[i] <= 'Z') ||
expr[i] == '.' || expr[i] == '_') {
break
}
}
return expr[0:i], nil
}
func tokenize(expr string) ([]string, error) {
var tokens []string
p := 0
for p < len(expr) {
if isIdentNumChar(expr[p]) {
pp := p + 1
for ; pp < len(expr) && isIdentNumChar(expr[pp]); pp++ {
}
tokens = append(tokens, expr[p:pp])
p = pp
} else {
if expr[p] != ' ' {
tokens = append(tokens, expr[p:p+1])
}
p++
}
}
return tokens, nil
}
func evalValue(comps []string, i int) (int64, int, error) {
c := comps[i]
if c == "(" {
value, ni, error := evalSub(comps, i+1)
if error != nil {
return 0, 0, error
}
return value, ni, nil
}
v, err := strconv.ParseInt(c, 0, 64)
return v, i + 1, err
}
func evalSub(comps []string, i int) (int64, int, error) {
current, next, err := evalValue(comps, i)
if err != nil {
return 0, 0, err
}
i = next
for i < len(comps) {
c := comps[i]
if c == ")" {
return current, i + 1, nil
}
if c == "*" || c == "+" || c == "-" || c == "/" || c == "%" {
rhs, next, err := evalValue(comps, i+1)
if err != nil {
return 0, 0, err
}
switch c {
case "+":
current += rhs
case "-":
current -= rhs
case "/":
current /= rhs
case "*":
current *= rhs
case "%":
current %= rhs
}
i = next
} else {
return 0, 0, fmt.Errorf("expected operator")
}
}
return current, i, nil
}
// resolves an arithmetic expression
func evalExpr(expr string) (int64, error) {
comps, err := tokenize(expr)
if err != nil {
return 0, err
}
v, _, err := evalSub(comps, 0)
return v, err
}
// resolves and evaluate all ${foo.bar}, $foo.bar and $(expr) in epr
// nolint: gocyclo
func eval(expr string, flattened map[string]interface{}, o options) (interface{}, error) {
// Since we go from right to left to support nesting, handling $$ escape is
// painful, so just hide them and restore them at the end
expr = strings.Replace(expr, "$$", "\x00", -1)
end := len(expr)
// If evaluation resolves to a single value, return the type value, not a string
var bypass interface{}
iteration := 0
for {
iteration++
if iteration > 100 {
return "", fmt.Errorf("eval loop detected")
}
i := strings.LastIndex(expr[0:end], "$")
if i == -1 {
break
}
bypass = nil
comp, err := extract(expr[i+1:])
if err != nil {
return "", err
}
var val interface{}
if len(comp) != 0 && comp[0] == '(' {
var err error
val, err = evalExpr(comp[1 : len(comp)-1])
if err != nil {
return "", err
}
} else {
var ok bool
if len(comp) != 0 && comp[0] == '{' {
content := comp[1 : len(comp)-1]
q := strings.Index(content, "?")
if q != -1 {
s := strings.Index(content, ":")
if s == -1 {
return "", fmt.Errorf("parse error in ternary '%s', missing ':'", content)
}
variable := content[0:q]
val, ok = flattened[variable]
if isTrue(fmt.Sprintf("%v", val)) {
val = content[q+1 : s]
} else {
val = content[s+1:]
}
} else {
val, ok = flattened[comp[1:len(comp)-1]]
}
} else {
val, ok = flattened[comp]
}
if !ok {
if o.errOnMissingKey {
return "", fmt.Errorf("variable '%s' not set", comp)
}
fmt.Fprintf(os.Stderr, "variable '%s' not set, expanding to empty string", comp)
}
}
valstr := fmt.Sprintf("%v", val)
expr = expr[0:i] + valstr + expr[i+1+len(comp):]
if strings.Trim(expr, " ") == valstr {
bypass = val
}
end = len(expr)
}
if bypass != nil {
return bypass, nil
}
expr = strings.Replace(expr, "\x00", "$", -1)
return expr, nil
}
func isTrue(cond string) bool {
ct := strings.TrimLeft(cond, " ")
reverse := len(cond) != 0 && cond[0] == '!'
if reverse {
cond = ct[1:]
}
cond = strings.Trim(cond, " ")
return (cond != "" && cond != "false" && cond != "0") != reverse
}
func recurseList(input []interface{}, parameters map[string]interface{}, flattened map[string]interface{}, o options) ([]interface{}, error) {
var res []interface{}
for _, v := range input {
switch vv := v.(type) {
case yml.MapSlice:
newv, err := recurse(vv, parameters, flattened, o)
if err != nil {
return nil, err
}
res = append(res, newv)
case []interface{}:
newv, err := recurseList(vv, parameters, flattened, o)
if err != nil {
return nil, err
}
res = append(res, newv)
case string:
vvv, err := eval(vv, flattened, o)
if err != nil {
return nil, err
}
if vvvs, ok := vvv.(string); ok {
trimed := strings.TrimLeft(vvvs, " ")
if strings.HasPrefix(trimed, "@if") {
be := strings.Index(trimed, "(")
ee := strings.Index(trimed, ")")
if be == -1 || ee == -1 || be > ee {
return nil, fmt.Errorf("parse error looking for if condition in '%s'", vvv)
}
cond := trimed[be+1 : ee]
if isTrue(cond) {
res = append(res, strings.Trim(trimed[ee+1:], " "))
}
continue
}
}
res = append(res, vvv)
default:
res = append(res, v)
}
}
return res, nil
}
// FIXME complexity on this is 47… get it lower than 16
// nolint: gocyclo
func recurse(input yml.MapSlice, parameters map[string]interface{}, flattened map[string]interface{}, o options) (yml.MapSlice, error) {
res := yml.MapSlice{}
for _, kvp := range input {
k := kvp.Key
v := kvp.Value
rk := k
kstr, isks := k.(string)
if isks {
trimed := strings.TrimLeft(kstr, " ")
if strings.HasPrefix(trimed, "@switch ") {
mii, ok := v.(yml.MapSlice)
if !ok {
return nil, fmt.Errorf("@switch value must be a mapping")
}
key, err := eval(strings.TrimPrefix(trimed, "@switch "), flattened, o)
if err != nil {
return nil, err
}
var defaultValue interface{}
hit := false
for _, sval := range mii {
sk := sval.Key
sv := sval.Value
ssk, ok := sk.(string)
if !ok {
return nil, fmt.Errorf("@switch entry key must be a string")
}
if ssk == "default" {
defaultValue = sv
}
if ssk == key {
hit = true
svv, ok := sv.(yml.MapSlice)
if !ok {
return nil, fmt.Errorf("@switch entry must be a mapping")
}
for _, vval := range svv {
res = append(res, yml.MapItem{Key: vval.Key, Value: vval.Value})
}
}
}
if !hit && defaultValue != nil {
svv, ok := defaultValue.(yml.MapSlice)
if !ok {
return nil, fmt.Errorf("@switch entry must be a mapping")
}
for _, vval := range svv {
res = append(res, yml.MapItem{Key: vval.Key, Value: vval.Value})
}
}
continue
}
if strings.HasPrefix(trimed, "@for ") {
mii, ok := v.(yml.MapSlice)
if !ok {
return nil, fmt.Errorf("@for value must be a mapping")
}
comps := strings.SplitN(trimed, " ", 4)
varname := comps[1]
varrangeraw, err := eval(comps[3], flattened, o)
if err != nil {
return nil, err
}
varrange, ok := varrangeraw.(string)
if !ok {
return nil, fmt.Errorf("@for argument must be a string")
}
mayberange := strings.Split(varrange, "..")
if len(mayberange) == 2 {
rangestart, err := strconv.ParseInt(mayberange[0], 0, 64)
if err != nil {
return nil, err
}
rangeend, err := strconv.ParseInt(mayberange[1], 0, 64)
if err != nil {
return nil, err
}
for i := rangestart; i < rangeend; i++ {
flattened[varname] = fmt.Sprintf("%v", i)
val, err := recurse(mii, parameters, flattened, o)
if err != nil {
return nil, err
}
for _, vval := range val {
res = append(res, yml.MapItem{Key: vval.Key, Value: vval.Value})
}
}
} else {
// treat range as a list
rangevalues := strings.Split(varrange, " ")
for _, i := range rangevalues {
flattened[varname] = i
val, err := recurse(mii, parameters, flattened, o)
if err != nil {
return nil, err
}
for _, vval := range val {
res = append(res, yml.MapItem{Key: vval.Key, Value: vval.Value})
}
}
}
continue
}
if strings.HasPrefix(trimed, "@if ") {
cond, err := eval(strings.TrimPrefix(trimed, "@if "), flattened, o)
if err != nil {
return nil, err
}
mii, ok := v.(yml.MapSlice)
if !ok {
return nil, fmt.Errorf("@if value must be a mapping")
}
if isTrue(fmt.Sprintf("%v", cond)) {
val, err := recurse(mii, parameters, flattened, o)
if err != nil {
return nil, err
}
for _, vval := range val {
if vval.Key != "@else" {
res = append(res, yml.MapItem{Key: vval.Key, Value: vval.Value})
}
}
} else {
var elseClause interface{}
for _, miiv := range mii {
if miiv.Key == "@else" {
elseClause = miiv.Value
break
}
}
if elseClause != nil {
elseDict, ok := elseClause.(yml.MapSlice)
if !ok {
return nil, fmt.Errorf("@else value must be a mapping")
}
for _, vval := range elseDict {
res = append(res, yml.MapItem{Key: vval.Key, Value: vval.Value})
}
}
}
continue
}
rstr, err := eval(kstr, flattened, o)
if err != nil {
return nil, err
}
rk = rstr
}
switch vv := v.(type) {
case yml.MapSlice:
newv, err := recurse(vv, parameters, flattened, o)
if err != nil {
return nil, err
}
res = append(res, yml.MapItem{Key: rk, Value: newv})
case []interface{}:
newv, err := recurseList(vv, parameters, flattened, o)
if err != nil {
return nil, err
}
res = append(res, yml.MapItem{Key: rk, Value: newv})
case string:
vvv, err := eval(vv, flattened, o)
if err != nil {
return nil, err
}
res = append(res, yml.MapItem{Key: rk, Value: vvv})
default:
res = append(res, yml.MapItem{Key: rk, Value: v})
}
}
return res, nil
}
// ProcessStrings resolves input templated yaml using values in parameters yaml
func ProcessStrings(input, parameters string) (string, error) {
ps := make(map[interface{}]interface{})
err := yaml.Unmarshal([]byte(parameters), ps)
if err != nil {
return "", err
}
s := make(map[string]interface{})
merge(s, ps)
res, err := Process(input, s)
if err != nil {
return "", err
}
sres, err := yaml.Marshal(res)
if err != nil {
return "", err
}
return string(sres), nil
}
// ProcessWithOrder resolves input templated yaml using values given in parameters, returning a MapSlice with order preserved
func ProcessWithOrder(inputString string, parameters map[string]interface{}, opts ...string) (yml.MapSlice, error) {
var o options
for _, v := range opts {
switch v {
case OptionErrOnMissingKey:
o.errOnMissingKey = true
default:
return nil, fmt.Errorf("unknown option %q", v)
}
}
var input yml.MapSlice
err := yaml.Unmarshal([]byte(inputString), &input)
if err != nil {
return nil, err
}
flattened := make(map[string]interface{})
flatten(parameters, flattened, "")
return recurse(input, parameters, flattened, o)
}
// Process resolves input templated yaml using values given in parameters, returning a map
func Process(inputString string, parameters map[string]interface{}, opts ...string) (map[interface{}]interface{}, error) {
mapSlice, err := ProcessWithOrder(inputString, parameters, opts...)
if err != nil {
return nil, err
}
res, err := convert(mapSlice)
if err != nil {
return nil, err
}
return res, nil
}
func convert(mapSlice yml.MapSlice) (map[interface{}]interface{}, error) {
res := make(map[interface{}]interface{})
for _, kv := range mapSlice {
v := kv.Value
castValue, ok := v.(yml.MapSlice)
if !ok {
res[kv.Key] = kv.Value
} else {
recursed, err := convert(castValue)
if err != nil {
return nil, err
}
res[kv.Key] = recursed
}
}
return res, nil
}
package render
import (
"fmt"
"os"
"strings"
"github.com/deislabs/duffle/pkg/bundle"
"github.com/docker/app/internal/compose"
"github.com/docker/app/internal/renderer"
"github.com/docker/app/internal/slices"
"github.com/docker/app/types"
"github.com/docker/app/types/parameters"
"github.com/docker/cli/cli/compose/loader"
composetemplate "github.com/docker/cli/cli/compose/template"
composetypes "github.com/docker/cli/cli/compose/types"
"github.com/pkg/errors"
// Register gotemplate renderer
_ "github.com/docker/app/internal/renderer/gotemplate"
// Register mustache renderer
_ "github.com/docker/app/internal/renderer/mustache"
// Register yatee renderer
_ "github.com/docker/app/internal/renderer/yatee"
// Register json formatter
_ "github.com/docker/app/internal/formatter/json"
// Register yaml formatter
_ "github.com/docker/app/internal/formatter/yaml"
)
// Render renders the Compose file for this app, merging in parameters files, other compose files, and env
// appname string, composeFiles []string, parametersFiles []string
func Render(app *types.App, env map[string]string, imageMap map[string]bundle.Image) (*composetypes.Config, error) {
// prepend the app parameters to the argument parameters
// load the parameters into a struct
fileParameters := app.Parameters()
// inject our metadata
metaPrefixed, err := parameters.Load(app.MetadataRaw(), parameters.WithPrefix("app"))
if err != nil {
return nil, err
}
envParameters, err := parameters.FromFlatten(env)
if err != nil {
return nil, err
}
allParameters, err := parameters.Merge(fileParameters, metaPrefixed, envParameters)
if err != nil {
return nil, errors.Wrap(err, "failed to merge parameters")
}
// prepend our app compose file to the list
renderers := renderer.Drivers()
if r, ok := os.LookupEnv("DOCKERAPP_RENDERERS"); ok {
rl := strings.Split(r, ",")
for _, r := range rl {
if !slices.ContainsString(renderer.Drivers(), r) {
return nil, fmt.Errorf("renderer '%s' not found", r)
}
}
renderers = rl
}
configFiles, _, err := compose.Load(app.Composes(), func(data string) (string, error) {
return renderer.Apply(data, allParameters, renderers...)
})
if err != nil {
return nil, errors.Wrap(err, "failed to load composefiles")
}
return render(configFiles, allParameters.Flatten(), imageMap)
}
func render(configFiles []composetypes.ConfigFile, finalEnv map[string]string, imageMap map[string]bundle.Image) (*composetypes.Config, error) {
rendered, err := loader.Load(composetypes.ConfigDetails{
WorkingDir: ".",
ConfigFiles: configFiles,
Environment: finalEnv,
}, func(opts *loader.Options) {
opts.Interpolate.Substitute = substitute
})
if err != nil {
return nil, errors.Wrap(err, "failed to load Compose file")
}
if err := processEnabled(rendered); err != nil {
return nil, err
}
for ix, service := range rendered.Services {
if img, ok := imageMap[service.Name]; ok {
service.Image = img.Image
rendered.Services[ix] = service
}
}
return rendered, nil
}
func substitute(template string, mapping composetemplate.Mapping) (string, error) {
return composetemplate.SubstituteWith(template, mapping, compose.ExtrapolationPattern, errorIfMissing)
}
func errorIfMissing(substitution string, mapping composetemplate.Mapping) (string, bool, error) {
value, found := mapping(substitution)
if !found {
return "", true, &composetemplate.InvalidTemplateError{
Template: "required variable " + substitution + " is missing a value",
}
}
return value, true, nil
}
func processEnabled(config *composetypes.Config) error {
services := []composetypes.ServiceConfig{}
for _, service := range config.Services {
if service.Extras != nil {
if xEnabled, ok := service.Extras["x-enabled"]; ok {
enabled, err := isEnabled(xEnabled)
if err != nil {
return err
}
if !enabled {
continue
}
}
}
services = append(services, service)
}
config.Services = services
return nil
}
func isEnabled(e interface{}) (bool, error) {
switch v := e.(type) {
case string:
v = strings.ToLower(strings.TrimSpace(v))
switch {
case v == "1", v == "true":
return true, nil
case v == "", v == "0", v == "false":
return false, nil
case strings.HasPrefix(v, "!"):
nv, err := isEnabled(v[1:])
if err != nil {
return false, err
}
return !nv, nil
default:
return false, errors.Errorf("%s is not a valid value for x-enabled", e)
}
case bool:
return v, nil
}
return false, errors.Errorf("invalid type (%T) for x-enabled", e)
}
// Code generated by "esc -o bindata.go -pkg specification -ignore .*\.go -private -modtime=1518458244 schemas"; DO NOT EDIT.
package specification
import (
"bytes"
"compress/gzip"
"encoding/base64"
"fmt"
"io"
"io/ioutil"
"net/http"
"os"
"path"
"sync"
"time"
)
type _escLocalFS struct{}
var _escLocal _escLocalFS
type _escStaticFS struct{}
var _escStatic _escStaticFS
type _escDirectory struct {
fs http.FileSystem
name string
}
type _escFile struct {
compressed string
size int64
modtime int64
local string
isDir bool
once sync.Once
data []byte
name string
}
func (_escLocalFS) Open(name string) (http.File, error) {
f, present := _escData[path.Clean(name)]
if !present {
return nil, os.ErrNotExist
}
return os.Open(f.local)
}
func (_escStaticFS) prepare(name string) (*_escFile, error) {
f, present := _escData[path.Clean(name)]
if !present {
return nil, os.ErrNotExist
}
var err error
f.once.Do(func() {
f.name = path.Base(name)
if f.size == 0 {
return
}
var gr *gzip.Reader
b64 := base64.NewDecoder(base64.StdEncoding, bytes.NewBufferString(f.compressed))
gr, err = gzip.NewReader(b64)
if err != nil {
return
}
f.data, err = ioutil.ReadAll(gr)
})
if err != nil {
return nil, err
}
return f, nil
}
func (fs _escStaticFS) Open(name string) (http.File, error) {
f, err := fs.prepare(name)
if err != nil {
return nil, err
}
return f.File()
}
func (dir _escDirectory) Open(name string) (http.File, error) {
return dir.fs.Open(dir.name + name)
}
func (f *_escFile) File() (http.File, error) {
type httpFile struct {
*bytes.Reader
*_escFile
}
return &httpFile{
Reader: bytes.NewReader(f.data),
_escFile: f,
}, nil
}
func (f *_escFile) Close() error {
return nil
}
func (f *_escFile) Readdir(count int) ([]os.FileInfo, error) {
if !f.isDir {
return nil, fmt.Errorf(" escFile.Readdir: '%s' is not directory", f.name)
}
fis, ok := _escDirs[f.local]
if !ok {
return nil, fmt.Errorf(" escFile.Readdir: '%s' is directory, but we have no info about content of this dir, local=%s", f.name, f.local)
}
limit := count
if count <= 0 || limit > len(fis) {
limit = len(fis)
}
if len(fis) == 0 && count > 0 {
return nil, io.EOF
}
return []os.FileInfo(fis[0:limit]), nil
}
func (f *_escFile) Stat() (os.FileInfo, error) {
return f, nil
}
func (f *_escFile) Name() string {
return f.name
}
func (f *_escFile) Size() int64 {
return f.size
}
func (f *_escFile) Mode() os.FileMode {
return 0
}
func (f *_escFile) ModTime() time.Time {
return time.Unix(f.modtime, 0)
}
func (f *_escFile) IsDir() bool {
return f.isDir
}
func (f *_escFile) Sys() interface{} {
return f
}
// _escFS returns a http.Filesystem for the embedded assets. If useLocal is true,
// the filesystem's contents are instead used.
func _escFS(useLocal bool) http.FileSystem {
if useLocal {
return _escLocal
}
return _escStatic
}
// _escDir returns a http.Filesystem for the embedded assets on a given prefix dir.
// If useLocal is true, the filesystem's contents are instead used.
func _escDir(useLocal bool, name string) http.FileSystem {
if useLocal {
return _escDirectory{fs: _escLocal, name: name}
}
return _escDirectory{fs: _escStatic, name: name}
}
// _escFSByte returns the named file from the embedded assets. If useLocal is
// true, the filesystem's contents are instead used.
func _escFSByte(useLocal bool, name string) ([]byte, error) {
if useLocal {
f, err := _escLocal.Open(name)
if err != nil {
return nil, err
}
b, err := ioutil.ReadAll(f)
_ = f.Close()
return b, err
}
f, err := _escStatic.prepare(name)
if err != nil {
return nil, err
}
return f.data, nil
}
// _escFSMustByte is the same as _escFSByte, but panics if name is not present.
func _escFSMustByte(useLocal bool, name string) []byte {
b, err := _escFSByte(useLocal, name)
if err != nil {
panic(err)
}
return b
}
// _escFSString is the string version of _escFSByte.
func _escFSString(useLocal bool, name string) (string, error) {
b, err := _escFSByte(useLocal, name)
return string(b), err
}
// _escFSMustString is the string version of _escFSMustByte.
func _escFSMustString(useLocal bool, name string) string {
return string(_escFSMustByte(useLocal, name))
}
var _escData = map[string]*_escFile{
"/schemas/metadata_schema_v0.1.json": {
name: "metadata_schema_v0.1.json",
local: "schemas/metadata_schema_v0.1.json",
size: 1916,
modtime: 1518458244,
compressed: `
H4sIAAAAAAAC/7RUwY7iMAy99yuqsEegrLQnfgUh5G1dCKJJxjFIaMS/j9pAaaZpWoG4OvbLs/38vpM0
TVPxx+YHrECsU3FgNussO1qtFi661LTPCoKSF6t/mYvNxNxVyqIuqpChAIade91dVsu/yxrikcZXg3Wi
/n/EnB9RQ9ogsUQr1qmj0sQVVOhFPAzLJNX+jtG+lpoq4KYDbblBaBNuz1xxQbJSq1H4YHGBNidpOAaw
8aLNS5Cx6/R8OgkvvA1+XPdjDeT4Gu8KpGKQCskOAwARXH+PVTJW/RonGsKyrptlBZZSyXoqNnt+5fd1
CxIzQKj446TcN4OEkg4tQfh1loSFt0onyYCMmsj2Xtr50hd0Zyi9Tt0FDQ5xHp6Ld0jPcYYPKn5Yo0oK
LK6twQrkaRRyE3yN30bkRtpbCVd1vMDR63cyWZrT9nXP/eQ2Rlvt215sb8OG8pYchtz1LdCYe00yjAnG
8aKrhQUVlZgzm+SW/AQAAP//m8slr3wHAAA=
`,
},
"/schemas/metadata_schema_v0.2.json": {
name: "metadata_schema_v0.2.json",
local: "schemas/metadata_schema_v0.2.json",
size: 1732,
modtime: 1518458244,
compressed: `
H4sIAAAAAAAC/7RUzY7yMAy89ymq8B0LRZ/2xKsghLzUBSOSdB2DhFa8+6oNf9mmP2LFdZyxx/bE30ma
pqn65zY71KAWqdqJVIs83ztrph6dWd7mBUMp0/lH7rGJyjyTipqkUaAAgbWPrk/z2f9ZneL2TM4V1g/t
5x43ckMrthWyEDq1SL2UBjegMUCCHE6YzFbdg5fswTwhO7LmNXKBbsNUSV+CZYA2kWvKrB0xx8NBBfAq
WlgDGQEyyK5bOTDD+VcVRYK6zfE7ZSxr3iQvsCRDdVsuf5QKhV2iwipgNPJ2Ub5Mp6DkSZZi/DoSYxHs
wjsm4oMGWV2pTyVDvz0NpdWpN3jnELP4XAKfP8YZ93u/7wctHFncnYMa6DCYchmN9pu7x+R3s8dZqrSs
QepWvLx2J6OtOW5f17fv3MZgqzvrpMk4am9dd+xPZug7M6N+9ogf/uL5iW++1wv+KiSX5CcAAP//f4Kg
RsQGAAA=
`,
},
"/schemas": {
name: "schemas",
local: `schemas`,
isDir: true,
},
}
var _escDirs = map[string][]os.FileInfo{
"schemas": {
_escData["/schemas/metadata_schema_v0.1.json"],
_escData["/schemas/metadata_schema_v0.2.json"],
},
}
package specification
import (
"fmt"
"sort"
"strings"
"github.com/pkg/errors"
"github.com/xeipuuv/gojsonschema"
)
//go:generate esc -o bindata.go -pkg specification -ignore .*\.go -private -modtime=1518458244 schemas
// Validate uses the jsonschema to validate the configuration
func Validate(config map[string]interface{}, version string) error {
schemaData, err := _escFSByte(false, fmt.Sprintf("/schemas/metadata_schema_%s.json", version))
if err != nil {
return errors.Errorf("unsupported metadata version: %s", version)
}
schemaLoader := gojsonschema.NewStringLoader(string(schemaData))
dataLoader := gojsonschema.NewGoLoader(config)
result, err := gojsonschema.Validate(schemaLoader, dataLoader)
if err != nil {
return err
}
if !result.Valid() {
errs := make([]string, len(result.Errors()))
for i, err := range result.Errors() {
errs[i] = fmt.Sprintf("- %s", err)
}
sort.Strings(errs)
return errors.New(strings.Join(errs, "\n"))
}
return nil
}
package types
const defaultComposefileVersion = "3.6"
// InitialComposeFile represents an initial composefile (used by the init command)
type InitialComposeFile struct {
Version string
Services map[string]InitialService
}
// InitialService represents an initial service (used by the init command)
type InitialService struct {
Image string
}
// NewInitialComposeFile returns an empty InitialComposeFile object
func NewInitialComposeFile() InitialComposeFile {
return InitialComposeFile{
Version: defaultComposefileVersion,
Services: map[string]InitialService{},
}
}
package metadata
import (
"fmt"
"github.com/docker/app/internal"
"github.com/docker/app/internal/yaml"
"github.com/docker/app/specification"
"github.com/docker/cli/cli/compose/loader"
"github.com/pkg/errors"
)
// Load validates the given data and loads it into a metadata struct
func Load(data []byte) (AppMetadata, error) {
if err := validateRawMetadata(data); err != nil {
return AppMetadata{}, err
}
var meta AppMetadata
if err := yaml.Unmarshal(data, &meta); err != nil {
return AppMetadata{}, errors.Wrap(err, "failed to unmarshal metadata")
}
return meta, nil
}
func validateRawMetadata(metadata []byte) error {
metadataYaml, err := loader.ParseYAML(metadata)
if err != nil {
return fmt.Errorf("failed to parse application metadata: %s", err)
}
if err := specification.Validate(metadataYaml, internal.MetadataVersion); err != nil {
return fmt.Errorf("failed to validate metadata:\n%s", err)
}
return nil
}
package metadata
import (
"strings"
)
// Maintainer represents one of the apps's maintainers
type Maintainer struct {
Name string `json:"name"`
Email string `json:"email,omitempty"`
}
// Maintainers is a list of maintainers
type Maintainers []Maintainer
// String gives a string representation of a list of maintainers
func (ms Maintainers) String() string {
res := make([]string, len(ms))
for i, m := range ms {
res[i] = m.String()
}
return strings.Join(res, ", ")
}
// String gives a string representation of a maintainer
func (m Maintainer) String() string {
s := m.Name
if m.Email != "" {
s += " <" + m.Email + ">"
}
return s
}
// AppMetadata is the format of the data found inside the metadata.yml file
type AppMetadata struct {
Version string `json:"version"`
Name string `json:"name"`
Description string `json:"description,omitempty"`
Maintainers Maintainers `json:"maintainers,omitempty"`
}
package parameters
import (
"bytes"
"fmt"
"io"
"io/ioutil"
"github.com/docker/app/internal/yaml"
"github.com/pkg/errors"
)
// Load loads the given data in parameters
func Load(data []byte, ops ...func(*Options)) (Parameters, 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 {
if err == io.EOF {
return Parameters{}, nil
}
return nil, errors.Wrap(err, "failed to read parameters")
}
converted, err := convertToStringKeysRecursive(s, "")
if err != nil {
return nil, err
}
parameters := converted.(map[string]interface{})
if options.prefix != "" {
parameters = map[string]interface{}{
options.prefix: parameters,
}
}
return parameters, nil
}
// LoadMultiple loads multiple data in parameters
func LoadMultiple(datas [][]byte, ops ...func(*Options)) (Parameters, error) {
m := Parameters(map[string]interface{}{})
for _, data := range datas {
parameters, err := Load(data, ops...)
if err != nil {
return nil, err
}
m, err = Merge(m, parameters)
if err != nil {
return nil, err
}
}
return m, nil
}
// LoadFile loads a file (path) in parameters (i.e. flatten map)
func LoadFile(path string, ops ...func(*Options)) (Parameters, error) {
data, err := ioutil.ReadFile(path)
if err != nil {
return nil, err
}
return Load(data, ops...)
}
// LoadFiles loads multiple path in parameters, merging them.
func LoadFiles(paths []string, ops ...func(*Options)) (Parameters, error) {
m := Parameters(map[string]interface{}{})
for _, path := range paths {
parameters, err := LoadFile(path, ops...)
if err != nil {
return nil, err
}
m, err = Merge(m, parameters)
if err != nil {
return nil, err
}
}
return m, nil
}
// from cli
func convertToStringKeysRecursive(value interface{}, keyPrefix string) (interface{}, error) {
if mapping, ok := value.(map[interface{}]interface{}); ok {
dict := make(map[string]interface{})
for key, entry := range mapping {
str, ok := key.(string)
if !ok {
return nil, formatInvalidKeyError(keyPrefix, key)
}
var newKeyPrefix string
if keyPrefix == "" {
newKeyPrefix = str
} else {
newKeyPrefix = fmt.Sprintf("%s.%s", keyPrefix, str)
}
convertedEntry, err := convertToStringKeysRecursive(entry, newKeyPrefix)
if err != nil {
return nil, err
}
dict[str] = convertedEntry
}
return dict, nil
}
if list, ok := value.([]interface{}); ok {
var convertedList []interface{}
for index, entry := range list {
newKeyPrefix := fmt.Sprintf("%s[%d]", keyPrefix, index)
convertedEntry, err := convertToStringKeysRecursive(entry, newKeyPrefix)
if err != nil {
return nil, err
}
convertedList = append(convertedList, convertedEntry)
}
return convertedList, nil
}
return value, nil
}
func formatInvalidKeyError(keyPrefix string, key interface{}) error {
var location string
if keyPrefix == "" {
location = "at top level"
} else {
location = fmt.Sprintf("in %s", keyPrefix)
}
return errors.Errorf("Non-string key %s: %#v", location, key)
}
package parameters
import (
"github.com/imdario/mergo"
"github.com/pkg/errors"
)
// Merge merges multiple parameters overriding duplicated keys
func Merge(parameters ...Parameters) (Parameters, error) {
s := Parameters(map[string]interface{}{})
for _, parameter := range parameters {
if err := mergo.Merge(&s, parameter, mergo.WithOverride, mergo.WithAppendSlice); err != nil {
return s, errors.Wrap(err, "cannot merge parameters")
}
}
return s, nil
}
package parameters
// Options contains loading options for parameters
type Options struct {
prefix string
}
// WithPrefix adds the given prefix when loading parameters
func WithPrefix(prefix string) func(*Options) {
return func(o *Options) {
o.prefix = prefix
}
}
package parameters
import (
"fmt"
"reflect"
"strconv"
"strings"
"github.com/docker/app/internal/yaml"
"github.com/pkg/errors"
)
// Parameters represents a parameters map
type Parameters map[string]interface{}
// Flatten returns a flatten view of a parameters
// This becomes a one-level map with keys joined with a dot
func (s Parameters) Flatten() map[string]string {
return flatten(s)
}
func flatten(s Parameters) map[string]string {
m := map[string]string{}
for k, v := range s {
switch vv := v.(type) {
case string:
m[k] = vv
case map[string]interface{}:
im := flatten(vv)
for ik, iv := range im {
m[k+"."+ik] = iv
}
case []string:
for i, e := range vv {
m[fmt.Sprintf("%s.%d", k, i)] = fmt.Sprintf("%v", e)
}
case []interface{}:
for i, e := range vv {
m[fmt.Sprintf("%s.%d", k, i)] = fmt.Sprintf("%v", e)
}
default:
m[k] = fmt.Sprintf("%v", vv)
}
}
return m
}
// FromFlatten takes a flatten map and loads it as a Parameters map
// This uses yaml.Unmarshal to "guess" the type of the value
func FromFlatten(m map[string]string) (Parameters, error) {
s := map[string]interface{}{}
for k, v := range m {
ks := strings.Split(k, ".")
var converted interface{}
if err := yaml.Unmarshal([]byte(v), &converted); err != nil {
return s, err
}
if err := assignKey(s, ks, converted); err != nil {
return s, err
}
}
return Parameters(s), nil
}
func isSupposedSlice(ks []string) (int64, bool) {
if len(ks) != 1 {
return 0, false
}
i, err := strconv.ParseInt(ks[0], 10, 0)
return i, err == nil
}
func assignKey(m map[string]interface{}, keys []string, value interface{}) error {
key := keys[0]
if len(keys) == 1 {
if v, present := m[key]; present {
if reflect.TypeOf(v) != reflect.TypeOf(value) {
return errors.Errorf("key %s is already present and value has a different type (%T vs %T)", key, v, value)
}
}
m[key] = value
return nil
}
ks := keys[1:]
if i, ok := isSupposedSlice(ks); ok {
// it's a slice
if v, present := m[key]; !present {
m[key] = make([]interface{}, i+1)
} else if _, isSlice := v.([]interface{}); !isSlice {
return errors.Errorf("key %s already present and not a slice (%T)", key, v)
}
s := m[key].([]interface{})
if int64(len(s)) <= i {
newSlice := make([]interface{}, i+1)
copy(newSlice, s)
s = newSlice
}
s[i] = value
m[key] = s
return nil
}
if v, present := m[key]; !present {
m[key] = map[string]interface{}{}
} else if _, isMap := v.(map[string]interface{}); !isMap {
return errors.Errorf("key %s already present and not a map (%T)", key, v)
}
return assignKey(m[key].(map[string]interface{}), ks, value)
}
package types
import (
"errors"
"io"
"io/ioutil"
"os"
"path/filepath"
"strings"
"github.com/docker/app/internal"
"github.com/docker/app/types/metadata"
"github.com/docker/app/types/parameters"
)
// SingleFileSeparator is the separator used in single-file app
const SingleFileSeparator = "\n---\n"
// AppSourceKind represents what format the app was in when read
type AppSourceKind int
const (
// AppSourceSplit represents an Application in multiple file format
AppSourceSplit AppSourceKind = iota
// AppSourceMerged represents an Application in single file format
AppSourceMerged
// AppSourceImage represents an Application pulled from an image
AppSourceImage
// AppSourceArchive represents an Application in an archive format
AppSourceArchive
)
// ShouldRunInsideDirectory returns whether the package is run from a directory on disk
func (a AppSourceKind) ShouldRunInsideDirectory() bool {
return a == AppSourceSplit || a == AppSourceImage || a == AppSourceArchive
}
// App represents an app
type App struct {
Name string
Path string
Cleanup func()
Source AppSourceKind
composesContent [][]byte
parametersContent [][]byte
parameters parameters.Parameters
metadataContent []byte
metadata metadata.AppMetadata
attachments []Attachment
}
// Attachment is a summary of an attachment (attached file) stored in the app definition
type Attachment struct {
path string
size int64
}
// Path returns the local file path
func (f *Attachment) Path() string {
return f.path
}
// Size returns the file size in bytes
func (f *Attachment) Size() int64 {
return f.size
}
// Composes returns compose files content
func (a *App) Composes() [][]byte {
return a.composesContent
}
// ParametersRaw returns parameter files content
func (a *App) ParametersRaw() [][]byte {
return a.parametersContent
}
// Parameters returns map of parameters
func (a *App) Parameters() parameters.Parameters {
return a.parameters
}
// MetadataRaw returns metadata file content
func (a *App) MetadataRaw() []byte {
return a.metadataContent
}
// Metadata returns the metadata struct
func (a *App) Metadata() metadata.AppMetadata {
return a.metadata
}
// Attachments returns the external files list
func (a *App) Attachments() []Attachment {
return a.attachments
}
// Extract writes the app in the specified folder
func (a *App) Extract(path string) error {
if err := ioutil.WriteFile(filepath.Join(path, internal.MetadataFileName), a.MetadataRaw(), 0644); err != nil {
return err
}
if err := ioutil.WriteFile(filepath.Join(path, internal.ComposeFileName), a.Composes()[0], 0644); err != nil {
return err
}
if err := ioutil.WriteFile(filepath.Join(path, internal.ParametersFileName), a.ParametersRaw()[0], 0644); err != nil {
return err
}
return nil
}
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{},
parametersContent: [][]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)),
WithParametersFiles(filepath.Join(path, internal.ParametersFileName)),
WithAttachments(path),
}, 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
}
}
// WithSource sets the source of the app
func WithSource(source AppSourceKind) func(*App) error {
return func(app *App) error {
app.Source = source
return nil
}
}
// WithParametersFiles adds the specified parameters files to the app
func WithParametersFiles(files ...string) func(*App) error {
return parametersLoader(func() ([][]byte, error) { return readFiles(files...) })
}
// WithAttachments adds all local files (exc. main files) to the app
func WithAttachments(rootAppDir string) func(*App) error {
return func(app *App) error {
return filepath.Walk(rootAppDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() {
return nil
}
localFilePath, err := filepath.Rel(rootAppDir, path)
if err != nil {
return err
}
switch localFilePath {
case internal.ComposeFileName:
case internal.MetadataFileName:
case internal.ParametersFileName:
default:
externalFile := Attachment{
// Standardise on forward slashes for windows boxes
path: filepath.ToSlash(localFilePath),
size: info.Size(),
}
app.attachments = append(app.attachments, externalFile)
}
return nil
})
}
}
// WithParameters adds the specified parameters readers to the app
func WithParameters(readers ...io.Reader) func(*App) error {
return parametersLoader(func() ([][]byte, error) { return readReaders(readers...) })
}
func parametersLoader(f func() ([][]byte, error)) func(*App) error {
return func(app *App) error {
parametersContent, err := f()
if err != nil {
return err
}
parametersContents := append(app.parametersContent, parametersContent...)
loaded, err := parameters.LoadMultiple(parametersContents)
if err != nil {
return err
}
app.parameters = loaded
app.parametersContent = parametersContents
return nil
}
}
// MetadataFile adds the specified metadata file to the app
func MetadataFile(file string) func(*App) error {
return metadataLoader(func() ([]byte, error) { return ioutil.ReadFile(file) })
}
// Metadata adds the specified metadata reader to the app
func Metadata(r io.Reader) func(*App) error {
return metadataLoader(func() ([]byte, error) { return ioutil.ReadAll(r) })
}
func metadataLoader(f func() ([]byte, error)) func(app *App) error {
return func(app *App) error {
d, err := f()
if err != nil {
return err
}
loaded, err := metadata.Load(d)
if err != nil {
return err
}
app.metadata = loaded
app.metadataContent = d
return nil
}
}
// WithComposeFiles adds the specified compose files to the app
func WithComposeFiles(files ...string) func(*App) error {
return composeLoader(func() ([][]byte, error) { return readFiles(files...) })
}
// WithComposes adds the specified compose readers to the app
func WithComposes(readers ...io.Reader) func(*App) error {
return composeLoader(func() ([][]byte, error) { return readReaders(readers...) })
}
func composeLoader(f func() ([][]byte, error)) func(app *App) error {
return func(app *App) error {
composesContent, err := f()
if err != nil {
return err
}
app.composesContent = append(app.composesContent, composesContent...)
return nil
}
}
func readReaders(readers ...io.Reader) ([][]byte, error) {
content := make([][]byte, len(readers))
var errs []string
for i, r := range readers {
d, err := ioutil.ReadAll(r)
if err != nil {
errs = append(errs, err.Error())
continue
}
content[i] = d
}
return content, newErrGroup(errs)
}
func readFiles(files ...string) ([][]byte, error) {
content := make([][]byte, len(files))
var errs []string
for i, file := range files {
d, err := ioutil.ReadFile(file)
if err != nil {
errs = append(errs, err.Error())
continue
}
content[i] = d
}
return content, newErrGroup(errs)
}
func newErrGroup(errs []string) error {
if len(errs) == 0 {
return nil
}
return errors.New(strings.Join(errs, "\n"))
}