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 {
cmd := app.NewRootCmd("app", dockerCli)
originalPreRun := cmd.PersistentPreRunE
cmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error {
if err := plugin.PersistentPreRunE(cmd, args); err != nil {
return err
}
if originalPreRun != nil {
return originalPreRun(cmd, args)
}
return nil
}
return cmd
}, manager.Metadata{
SchemaVersion: "0.1.0",
Vendor: "Docker Inc.",
Version: internal.Version,
Experimental: true,
})
}
package commands
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io/ioutil"
"os"
"github.com/deislabs/cnab-go/bundle"
"github.com/docker/app/internal/packager"
"github.com/docker/app/internal/store"
"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/cli/cli/config"
"github.com/docker/distribution/reference"
dockertypes "github.com/docker/docker/api/types"
"github.com/docker/docker/pkg/jsonmessage"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
type bundleOptions struct {
out string
tag string
}
func bundleCmd(dockerCli command.Cli) *cobra.Command {
var opts bundleOptions
cmd := &cobra.Command{
Use: "bundle [APP_NAME] [--output OUTPUT_FILE]",
Short: "Create a CNAB invocation image and `bundle.json` for the application",
Example: `$ docker app bundle myapp.dockerapp`,
Args: cli.RequiresMaxArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
return runBundle(dockerCli, firstOrEmpty(args), opts)
},
}
cmd.Flags().StringVarP(&opts.out, "output", "o", "bundle.json", "Output file (- for stdout)")
cmd.Flags().StringVarP(&opts.tag, "tag", "t", "", "Name and optionally a tag in the 'name:tag' format")
return cmd
}
func runBundle(dockerCli command.Cli, appName string, opts bundleOptions) error {
ref, err := getNamedTagged(opts.tag)
if err != nil {
return err
}
bundle, err := makeBundle(dockerCli, appName, ref)
if err != nil {
return err
}
if bundle == nil || len(bundle.InvocationImages) == 0 {
return fmt.Errorf("failed to create bundle %q", appName)
}
if err := persistInBundleStore(ref, bundle); err != nil {
return err
}
bundleBytes, err := json.MarshalIndent(bundle, "", "\t")
if err != nil {
return err
}
if opts.out == "-" {
_, err = dockerCli.Out().Write(bundleBytes)
return err
}
fmt.Fprintf(os.Stdout, "Invocation image %q successfully built\n", bundle.InvocationImages[0].Image)
if err := ioutil.WriteFile(opts.out, bundleBytes, 0644); err != nil {
return err
}
fmt.Fprintf(os.Stdout, "Bundle saved to %s\n", opts.out)
return nil
}
func makeBundle(dockerCli command.Cli, appName string, refOverride reference.NamedTagged) (*bundle.Bundle, error) {
app, err := packager.Extract(appName)
if err != nil {
return nil, err
}
defer app.Cleanup()
return makeBundleFromApp(dockerCli, app, refOverride)
}
func makeBundleFromApp(dockerCli command.Cli, app *types.App, refOverride reference.NamedTagged) (*bundle.Bundle, error) {
logrus.Debug("Making app bundle")
meta := app.Metadata()
invocationImageName, err := makeInvocationImageName(meta, refOverride)
if err != nil {
return nil, err
}
buildContext := bytes.NewBuffer(nil)
if err := packager.PackInvocationImageContext(dockerCli, app, buildContext); err != nil {
return nil, err
}
logrus.Debugf("Building invocation image %s", invocationImageName)
buildResp, err := dockerCli.Client().ImageBuild(context.TODO(), buildContext, dockertypes.ImageBuildOptions{
Dockerfile: "Dockerfile",
Tags: []string{invocationImageName},
BuildArgs: map[string]*string{},
})
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 {
// If the invocation image can't be found we will get an error of the form:
// manifest for docker/cnab-app-base:v0.6.0-202-gbaf0b246c7 not found
if err.Error() == fmt.Sprintf("manifest for %s not found", packager.BaseInvocationImage(dockerCli)) {
return nil, fmt.Errorf("unable to resolve Docker App base image: %s", packager.BaseInvocationImage(dockerCli))
}
return nil, err
}
return packager.ToCNAB(app, invocationImageName)
}
func makeInvocationImageName(meta metadata.AppMetadata, refOverride reference.NamedTagged) (string, error) {
if refOverride != nil {
return makeCNABImageName(reference.FamiliarName(refOverride), refOverride.Tag(), "-invoc")
}
return makeCNABImageName(meta.Name, meta.Version, "-invoc")
}
func makeCNABImageName(appName, appVersion, suffix string) (string, error) {
name := fmt.Sprintf("%s:%s%s", appName, appVersion, suffix)
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
}
func persistInBundleStore(ref reference.Named, bndle *bundle.Bundle) error {
if ref == nil {
return nil
}
appstore, err := store.NewApplicationStore(config.Dir())
if err != nil {
return err
}
bundleStore, err := appstore.BundleStore()
if err != nil {
return err
}
return bundleStore.Store(ref, bndle)
}
func getNamedTagged(tag string) (reference.NamedTagged, error) {
if tag == "" {
return nil, nil
}
namedRef, err := reference.ParseNormalizedNamed(tag)
if err != nil {
return nil, err
}
ref, ok := reference.TagNameOnly(namedRef).(reference.NamedTagged)
if !ok {
return nil, fmt.Errorf("tag %q must be name with a tag in the 'name:tag' format", tag)
}
return ref, nil
}
package commands
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"os"
"strings"
"github.com/deislabs/cnab-go/action"
"github.com/deislabs/cnab-go/bundle"
"github.com/deislabs/cnab-go/claim"
"github.com/deislabs/cnab-go/credentials"
"github.com/deislabs/cnab-go/driver"
dockerDriver "github.com/deislabs/cnab-go/driver/docker"
"github.com/docker/app/internal"
"github.com/docker/app/internal/packager"
appstore "github.com/docker/app/internal/store"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/config"
"github.com/docker/cli/cli/context/docker"
"github.com/docker/cli/cli/context/store"
contextstore "github.com/docker/cli/cli/context/store"
"github.com/docker/distribution/reference"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/mount"
"github.com/docker/docker/registry"
"github.com/pkg/errors"
)
type bindMount struct {
required bool
endpoint string
}
const defaultSocketPath string = "/var/run/docker.sock"
type credentialSetOpt func(b *bundle.Bundle, creds credentials.Set) error
func addNamedCredentialSets(credStore appstore.CredentialStore, namedCredentialsets []string) credentialSetOpt {
return func(_ *bundle.Bundle, creds credentials.Set) error {
for _, file := range namedCredentialsets {
var (
c *credentials.CredentialSet
err error
)
// Check the credentialset locally first, then try in the credential store
if _, e := os.Stat(file); e == nil {
c, err = credentials.Load(file)
} else {
c, err = credStore.Read(file)
if os.IsNotExist(err) {
err = e
}
}
if err != nil {
return err
}
values, err := c.Resolve()
if err != nil {
return err
}
if err := creds.Merge(values); err != nil {
return err
}
}
return nil
}
}
func parseCommandlineCredential(c string) (string, string, error) {
split := strings.SplitN(c, "=", 2)
if len(split) != 2 || split[0] == "" {
return "", "", errors.Errorf("failed to parse %q as a credential name=value", c)
}
name := split[0]
value := split[1]
return name, value, nil
}
func addCredentials(strcreds []string) credentialSetOpt {
return func(_ *bundle.Bundle, creds credentials.Set) error {
for _, c := range strcreds {
name, value, err := parseCommandlineCredential(c)
if err != nil {
return err
}
if err := creds.Merge(credentials.Set{
name: value,
}); err != nil {
return err
}
}
return nil
}
}
func addDockerCredentials(contextName string, store contextstore.Store) credentialSetOpt {
// docker desktop contexts require some rewriting for being used within a container
store = dockerDesktopAwareStore{Store: store}
return func(_ *bundle.Bundle, creds credentials.Set) error {
if contextName != "" {
data, err := ioutil.ReadAll(contextstore.Export(contextName, store))
if err != nil {
return err
}
creds[internal.CredentialDockerContextName] = string(data)
}
return nil
}
}
func addRegistryCredentials(shouldPopulate bool, dockerCli command.Cli) credentialSetOpt {
return func(b *bundle.Bundle, creds credentials.Set) error {
if _, ok := b.Credentials[internal.CredentialRegistryName]; !ok {
return nil
}
registryCreds := map[string]types.AuthConfig{}
if shouldPopulate {
for _, img := range b.Images {
named, err := reference.ParseNormalizedNamed(img.Image)
if err != nil {
return err
}
info, err := registry.ParseRepositoryInfo(named)
if err != nil {
return err
}
key := registry.GetAuthConfigKey(info.Index)
if _, ok := registryCreds[key]; !ok {
registryCreds[key] = command.ResolveAuthConfig(context.Background(), dockerCli, info.Index)
}
}
}
registryCredsJSON, err := json.Marshal(registryCreds)
if err != nil {
return err
}
creds[internal.CredentialRegistryName] = string(registryCredsJSON)
return nil
}
}
func prepareCredentialSet(b *bundle.Bundle, opts ...credentialSetOpt) (map[string]string, error) {
creds := map[string]string{}
for _, op := range opts {
if err := op(b, creds); err != nil {
return nil, err
}
}
_, requiresDockerContext := b.Credentials[internal.CredentialDockerContextName]
_, hasDockerContext := creds[internal.CredentialDockerContextName]
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
}
// prepareDriver prepares a driver per the user's request.
func prepareDriver(dockerCli command.Cli, bindMount bindMount, stdout io.Writer) (driver.Driver, *bytes.Buffer) {
d := &dockerDriver.Driver{}
errBuf := bytes.NewBuffer(nil)
d.SetDockerCli(dockerCli)
if stdout != nil {
d.SetContainerOut(stdout)
}
d.SetContainerErr(errBuf)
if bindMount.required {
d.AddConfigurationOptions(func(config *container.Config, hostConfig *container.HostConfig) error {
config.User = "0:0"
mounts := []mount.Mount{
{
Type: mount.TypeBind,
Source: bindMount.endpoint,
Target: bindMount.endpoint,
},
}
hostConfig.Mounts = mounts
return nil
})
}
// Load any driver-specific config out of the environment.
driverCfg := map[string]string{}
for env := range d.Config() {
driverCfg[env] = os.Getenv(env)
}
d.SetConfig(driverCfg)
return d, errBuf
}
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, string, error) {
app, err := packager.Extract(name)
if err != nil {
return nil, "", err
}
defer app.Cleanup()
bndl, err := makeBundleFromApp(dockerCli, app, nil)
return bndl, "", err
}
func loadBundleFromFile(filename string) (*bundle.Bundle, error) {
b := &bundle.Bundle{}
data, err := ioutil.ReadFile(filename)
if err != nil {
return b, err
}
return bundle.Unmarshal(data)
}
//resolveBundle looks for a CNAB bundle which can be in a Docker App Package format or
// a bundle stored locally or in the bundle store. It returns a built or found bundle,
// a reference to the bundle if it is found in the bundlestore, and an error.
func resolveBundle(dockerCli command.Cli, bundleStore appstore.BundleStore, name string, pullRef bool, insecureRegistries []string) (*bundle.Bundle, string, 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 the 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 pullRef {
return nil, "", errors.Errorf("%s: cannot pull when referencing a file based app", name)
}
if strings.HasSuffix(name, internal.AppExtension) {
return extractAndLoadAppBasedBundle(dockerCli, name)
}
bndl, err := loadBundleFromFile(name)
return bndl, "", err
case nameKindDir, nameKindEmpty:
if pullRef {
if kind == nameKindDir {
return nil, "", errors.Errorf("%s: cannot pull when referencing a directory based app", name)
}
return nil, "", errors.Errorf("cannot pull when referencing a directory based app")
}
return extractAndLoadAppBasedBundle(dockerCli, name)
case nameKindReference:
ref, err := reference.ParseNormalizedNamed(name)
if err != nil {
return nil, "", errors.Wrap(err, name)
}
tagRef := reference.TagNameOnly(ref)
bndl, err := bundleStore.LookupOrPullBundle(tagRef, pullRef, dockerCli.ConfigFile(), insecureRegistries)
return bndl, tagRef.String(), err
}
return nil, "", fmt.Errorf("could not resolve bundle %q", name)
}
func requiredClaimBindMount(c claim.Claim, targetContextName string, dockerCli command.Cli) (bindMount, error) {
var specifiedOrchestrator string
if rawOrchestrator, ok := c.Parameters[internal.ParameterOrchestratorName]; ok {
specifiedOrchestrator = rawOrchestrator.(string)
}
return requiredBindMount(targetContextName, specifiedOrchestrator, dockerCli.ContextStore())
}
func requiredBindMount(targetContextName string, targetOrchestrator string, s store.Store) (bindMount, error) {
if targetOrchestrator == "kubernetes" {
return bindMount{}, nil
}
if targetContextName == "" {
targetContextName = "default"
}
// in case of docker desktop, we want to rewrite the context in cases where it targets the local swarm or Kubernetes
s = &dockerDesktopAwareStore{Store: s}
ctxMeta, err := s.GetMetadata(targetContextName)
if err != nil {
return bindMount{}, err
}
dockerCtx, err := command.GetDockerContext(ctxMeta)
if err != nil {
return bindMount{}, err
}
if dockerCtx.StackOrchestrator == command.OrchestratorKubernetes {
return bindMount{}, nil
}
dockerEndpoint, err := docker.EndpointFromContext(ctxMeta)
if err != nil {
return bindMount{}, err
}
host := dockerEndpoint.Host
return bindMount{isDockerHostLocal(host), socketPath(host)}, nil
}
func socketPath(host string) string {
if strings.HasPrefix(host, "unix://") {
return strings.TrimPrefix(host, "unix://")
}
return defaultSocketPath
}
func isDockerHostLocal(host string) bool {
return host == "" || strings.HasPrefix(host, "unix://") || strings.HasPrefix(host, "npipe://")
}
func prepareCustomAction(actionName string, dockerCli command.Cli, appname string, stdout io.Writer,
registryOpts registryOptions, pullOpts pullOptions, paramsOpts parametersOptions) (*action.RunCustom, *appstore.Installation, *bytes.Buffer, error) {
s, err := appstore.NewApplicationStore(config.Dir())
if err != nil {
return nil, nil, nil, err
}
bundleStore, err := s.BundleStore()
if err != nil {
return nil, nil, nil, err
}
driverImpl, errBuf := prepareDriver(dockerCli, bindMount{}, stdout)
bundle, ref, err := resolveBundle(dockerCli, bundleStore, appname, pullOpts.pull, registryOpts.insecureRegistries)
if err != nil {
return nil, nil, nil, err
}
installation, err := appstore.NewInstallation("custom-action", ref)
if err != nil {
return nil, nil, nil, err
}
installation.Bundle = bundle
if err := mergeBundleParameters(installation,
withFileParameters(paramsOpts.parametersFiles),
withCommandLineParameters(paramsOpts.overrides),
withStrictMode(paramsOpts.strictMode),
); err != nil {
return nil, nil, nil, err
}
a := &action.RunCustom{
Action: actionName,
Driver: driverImpl,
}
return a, installation, errBuf, nil
}
func isInstallationFailed(installation *appstore.Installation) bool {
return installation.Result.Action == claim.ActionInstall &&
installation.Result.Status == claim.StatusFailure
}
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)
`,
Example: `$ . <(docker app completion bash)`,
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)
if err := rootCmd.GenBashCompletion(buf); err != nil {
return err
}
fmt.Fprint(out, buf.String())
fmt.Fprint(out, zshTail)
return nil
}
package commands
import (
"fmt"
"net/url"
"runtime"
"github.com/pkg/errors"
"github.com/docker/cli/cli/context/docker"
"github.com/docker/cli/cli/context/kubernetes"
"github.com/docker/cli/cli/context/store"
"github.com/docker/docker/client"
apiv1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
v1 "k8s.io/client-go/kubernetes/typed/core/v1"
)
type dockerDesktopHostProvider func() (string, bool)
func defaultDockerDesktopHostProvider() (string, bool) {
switch runtime.GOOS {
case "windows", "darwin":
default:
// platforms other than windows or mac can't be Docker Desktop
return "", false
}
return client.DefaultDockerHost, true
}
type dockerDesktopLinuxKitIPProvider func() (string, error)
type dockerDesktopDockerEndpointRewriter struct {
defaultHostProvider dockerDesktopHostProvider
}
func (r *dockerDesktopDockerEndpointRewriter) rewrite(ep *docker.EndpointMeta) {
defaultHost, isDockerDesktop := r.defaultHostProvider()
if !isDockerDesktop {
return
}
// on docker desktop, any context with host="" or host=<default host> should be rewritten as host="unix:///var/run/docker.sock" (docker socket path within the linuxkit VM)
if ep.Host == "" || ep.Host == defaultHost {
ep.Host = "unix:///var/run/docker.sock"
}
}
type dockerDesktopKubernetesEndpointRewriter struct {
defaultHostProvider dockerDesktopHostProvider
linuxKitIPProvider dockerDesktopLinuxKitIPProvider
}
func (r *dockerDesktopKubernetesEndpointRewriter) rewrite(ep *kubernetes.EndpointMeta) {
// any error while rewriting makes as if no rewriting rule applies
if _, isDockerDesktop := r.defaultHostProvider(); !isDockerDesktop {
return
}
// if the kube endpoint host points to localhost or 127.0.0.1, we need to rewrite it to whatever is linuxkit VM IP is (with port 6443)
hostURL, err := url.Parse(ep.Host)
if err != nil {
return
}
hostName := hostURL.Hostname()
switch hostName {
case "localhost", "127.0.0.1":
default:
// we are on a context targeting a remote Kubernetes cluster, nothing to rewrite
return
}
ip, err := r.linuxKitIPProvider()
if err != nil {
return
}
ep.Host = fmt.Sprintf("https://%s:6443", ip)
}
// nolint:interfacer
func makeLinuxkitIPProvider(contextName string, s store.Store) dockerDesktopLinuxKitIPProvider {
return func() (string, error) {
clientCfg, err := kubernetes.ConfigFromContext(contextName, s)
if err != nil {
return "", err
}
restCfg, err := clientCfg.ClientConfig()
if err != nil {
return "", err
}
coreClient, err := v1.NewForConfig(restCfg)
if err != nil {
return "", err
}
nodes, err := coreClient.Nodes().List(metav1.ListOptions{})
if err != nil {
return "", err
}
if len(nodes.Items) == 0 {
return "", errors.New("no node found")
}
for _, address := range nodes.Items[0].Status.Addresses {
if address.Type == apiv1.NodeInternalIP {
return address.Address, nil
}
}
return "", errors.New("no ip found")
}
}
func rewriteContextIfDockerDesktop(meta *store.Metadata, s store.Store) {
// errors are treated as "don't rewrite"
rewriter := dockerDesktopDockerEndpointRewriter{
defaultHostProvider: defaultDockerDesktopHostProvider,
}
dockerEp, err := docker.EndpointFromContext(*meta)
if err != nil {
return
}
rewriter.rewrite(&dockerEp)
meta.Endpoints[docker.DockerEndpoint] = dockerEp
kubeEp := kubernetes.EndpointFromContext(*meta)
if kubeEp == nil {
return
}
kubeRewriter := dockerDesktopKubernetesEndpointRewriter{
defaultHostProvider: defaultDockerDesktopHostProvider,
linuxKitIPProvider: makeLinuxkitIPProvider(meta.Name, s),
}
kubeRewriter.rewrite(kubeEp)
meta.Endpoints[kubernetes.KubernetesEndpoint] = *kubeEp
}
type dockerDesktopAwareStore struct {
store.Store
}
func (s dockerDesktopAwareStore) List() ([]store.Metadata, error) {
contexts, err := s.Store.List()
if err != nil {
return nil, err
}
for ix, c := range contexts {
rewriteContextIfDockerDesktop(&c, s.Store)
contexts[ix] = c
}
return contexts, nil
}
func (s dockerDesktopAwareStore) GetMetadata(name string) (store.Metadata, error) {
context, err := s.Store.GetMetadata(name)
if err != nil {
return store.Metadata{}, err
}
rewriteContextIfDockerDesktop(&context, s.Store)
return context, nil
}
package commands
import (
"fmt"
"github.com/docker/app/internal/packager"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/spf13/cobra"
)
var (
initComposeFile string
initDescription string
initMaintainers []string
initSingleFile bool
)
func initCmd(dockerCli command.Cli) *cobra.Command {
cmd := &cobra.Command{
Use: "init APP_NAME [--compose-file COMPOSE_FILE] [--description DESCRIPTION] [--maintainer NAME:EMAIL ...] [OPTIONS]",
Short: "Initialize Docker Application definition",
Long: `Start building a Docker Application package. If there is a docker-compose.yml file in the current directory it will be copied and used.`,
Example: `$ docker app init myapp --description "a useful description"`,
Args: cli.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
created, err := packager.Init(args[0], initComposeFile, initDescription, initMaintainers, initSingleFile)
if err != nil {
return err
}
fmt.Fprintf(dockerCli.Out(), "Created %q\n", created)
return nil
},
}
cmd.Flags().StringVar(&initComposeFile, "compose-file", "", "Compose file to use as application base (optional)")
cmd.Flags().StringVar(&initDescription, "description", "", "Human readable description of your application (optional)")
cmd.Flags().StringArrayVar(&initMaintainers, "maintainer", []string{}, "Name and email address of person responsible for the application (name:email) (optional)")
cmd.Flags().BoolVar(&initSingleFile, "single-file", false, "Create a single-file Docker Application definition")
return cmd
}
package commands
import (
"fmt"
"github.com/docker/app/internal"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/spf13/cobra"
)
type inspectOptions struct {
parametersOptions
registryOptions
pullOptions
}
func inspectCmd(dockerCli command.Cli) *cobra.Command {
var opts inspectOptions
cmd := &cobra.Command{
Use: "inspect [APP_NAME] [OPTIONS]",
Short: "Shows metadata, parameters and a summary of the Compose file for a given application",
Example: `$ docker app inspect myapp.dockerapp`,
Args: cli.RequiresMaxArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
return runInspect(dockerCli, firstOrEmpty(args), opts)
},
}
opts.parametersOptions.addFlags(cmd.Flags())
opts.registryOptions.addFlags(cmd.Flags())
opts.pullOptions.addFlags(cmd.Flags())
return cmd
}
func runInspect(dockerCli command.Cli, appname string, opts inspectOptions) error {
defer muteDockerCli(dockerCli)()
action, installation, errBuf, err := prepareCustomAction(internal.ActionInspectName, dockerCli, appname, nil, opts.registryOptions, opts.pullOptions, opts.parametersOptions)
if err != nil {
return err
}
if err := action.Run(&installation.Claim, nil, nil); err != nil {
return fmt.Errorf("inspect failed: %s\n%s", err, errBuf)
}
return nil
}
package commands
import (
"fmt"
"os"
"github.com/deislabs/cnab-go/action"
"github.com/deislabs/cnab-go/credentials"
"github.com/docker/app/internal/store"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
type installOptions struct {
parametersOptions
credentialOptions
registryOptions
pullOptions
orchestrator string
kubeNamespace string
stackName string
}
type nameKind uint
const (
_ nameKind = iota
nameKindEmpty
nameKindFile
nameKindDir
nameKindReference
)
const longDescription = `Install an application.
By default, the application definition in the current directory will be
installed. The APP_NAME can also be:
- a path to a Docker Application definition (.dockerapp) or a CNAB bundle.json
- a registry Application Package reference`
const example = `$ docker app install myapp.dockerapp --name myinstallation --target-context=mycontext
$ docker app install myrepo/myapp:mytag --name myinstallation --target-context=mycontext
$ docker app install bundle.json --name myinstallation --credential-set=mycredentials.yml`
func installCmd(dockerCli command.Cli) *cobra.Command {
var opts installOptions
cmd := &cobra.Command{
Use: "install [APP_NAME] [--name INSTALLATION_NAME] [--target-context TARGET_CONTEXT] [OPTIONS]",
Aliases: []string{"deploy"},
Short: "Install an application",
Long: longDescription,
Example: example,
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())
opts.registryOptions.addFlags(cmd.Flags())
opts.pullOptions.addFlags(cmd.Flags())
cmd.Flags().StringVar(&opts.orchestrator, "orchestrator", "", "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)")
return cmd
}
func runInstall(dockerCli command.Cli, appname string, opts installOptions) error {
defer muteDockerCli(dockerCli)()
opts.SetDefaultTargetContext(dockerCli)
bind, err := requiredBindMount(opts.targetContext, opts.orchestrator, dockerCli.ContextStore())
if err != nil {
return err
}
bundleStore, installationStore, credentialStore, err := prepareStores(opts.targetContext)
if err != nil {
return err
}
bndl, ref, err := resolveBundle(dockerCli, bundleStore, appname, opts.pull, opts.insecureRegistries)
if err != nil {
return err
}
if err := bndl.Validate(); err != nil {
return err
}
installationName := opts.stackName
if installationName == "" {
installationName = bndl.Name
}
logrus.Debugf(`Looking for a previous installation "%q"`, installationName)
if installation, err := installationStore.Read(installationName); err == nil {
// A failed installation can be overridden, but with a warning
if isInstallationFailed(installation) {
fmt.Fprintf(os.Stderr, "WARNING: installing over previously failed installation %q\n", installationName)
} else {
// Return an error in case of successful installation, or even failed upgrade, which means
// their was already a successful installation.
return fmt.Errorf("Installation %q already exists, use 'docker app upgrade' instead", installationName)
}
} else {
logrus.Debug(err)
}
installation, err := store.NewInstallation(installationName, ref)
if err != nil {
return err
}
driverImpl, errBuf := prepareDriver(dockerCli, bind, nil)
installation.Bundle = bndl
if err := mergeBundleParameters(installation,
withFileParameters(opts.parametersFiles),
withCommandLineParameters(opts.overrides),
withOrchestratorParameters(opts.orchestrator, opts.kubeNamespace),
withSendRegistryAuth(opts.sendRegistryAuth),
withStrictMode(opts.strictMode),
); err != nil {
return err
}
creds, err := prepareCredentialSet(bndl, opts.CredentialSetOpts(dockerCli, credentialStore)...)
if err != nil {
return err
}
if err := credentials.Validate(creds, bndl.Credentials); err != nil {
return err
}
inst := &action.Install{
Driver: driverImpl,
}
err = inst.Run(&installation.Claim, creds, os.Stdout)
// Even if the installation failed, the installation is persisted with its failure status,
// so any installation needs a clean uninstallation.
err2 := installationStore.Store(installation)
if err != nil {
return fmt.Errorf("Installation failed: %s\n%s", err, errBuf)
}
if err2 != nil {
return err2
}
fmt.Fprintf(os.Stdout, "Application %q installed on context %q\n", installationName, opts.targetContext)
return nil
}
package commands
import (
"fmt"
"io"
"sort"
"strings"
"text/tabwriter"
"time"
"github.com/docker/distribution/reference"
"github.com/pkg/errors"
"github.com/docker/app/internal/store"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/config"
units "github.com/docker/go-units"
"github.com/spf13/cobra"
)
type listOptions struct {
targetContext string
allContexts bool
}
func listCmd(dockerCli command.Cli) *cobra.Command {
var opts listOptions
cmd := &cobra.Command{
Use: "list [OPTIONS]",
Short: "List the installations and their last known installation result",
Aliases: []string{"ls"},
Args: cli.NoArgs,
PreRunE: func(cmd *cobra.Command, args []string) error {
if opts.allContexts && opts.targetContext != "" {
return errors.New("--all-contexts and --target-context flags cannot be used at the same time")
}
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
return runList(dockerCli, opts)
},
}
cmd.Flags().StringVar(&opts.targetContext, "target-context", "", "List installations on this context")
cmd.Flags().BoolVar(&opts.allContexts, "all-contexts", false, "List installations on all contexts")
return cmd
}
func runList(dockerCli command.Cli, opts listOptions) error {
var contexts []string
if opts.allContexts {
// List all the contexts from the context store
contextsMeta, err := dockerCli.ContextStore().List()
if err != nil {
return fmt.Errorf("failed to list contexts: %s", err)
}
for _, cm := range contextsMeta {
contexts = append(contexts, cm.Name)
}
// Add a CONTEXT column
listColumns = append(listColumns, installationColumn{"CONTEXT", func(context string, _ *store.Installation) string { return context }})
} else {
// Resolve the current or the specified target context
contexts = append(contexts, getTargetContext(opts.targetContext, dockerCli.CurrentContext()))
}
return printInstallations(dockerCli.Out(), config.Dir(), contexts)
}
type installationColumn struct {
header string
value func(c string, i *store.Installation) string
}
var (
listColumns = []installationColumn{
{"INSTALLATION", func(_ string, i *store.Installation) string { return i.Name }},
{"APPLICATION", func(_ string, i *store.Installation) string {
return fmt.Sprintf("%s (%s)", i.Bundle.Name, i.Bundle.Version)
}},
{"LAST ACTION", func(_ string, i *store.Installation) string { return i.Result.Action }},
{"RESULT", func(_ string, i *store.Installation) string { return i.Result.Status }},
{"CREATED", func(_ string, i *store.Installation) string { return units.HumanDuration(time.Since(i.Created)) }},
{"MODIFIED", func(_ string, i *store.Installation) string { return units.HumanDuration(time.Since(i.Modified)) }},
{"REFERENCE", func(_ string, i *store.Installation) string { return prettyPrintReference(i.Reference) }},
}
)
func printInstallations(out io.Writer, configDir string, contexts []string) error {
w := tabwriter.NewWriter(out, 0, 0, 1, ' ', 0)
printHeaders(w)
for _, context := range contexts {
installations, err := getInstallations(context, configDir)
if err != nil {
return err
}
for _, installation := range installations {
printValues(w, context, installation)
}
}
return w.Flush()
}
func printHeaders(w io.Writer) {
var headers []string
for _, column := range listColumns {
headers = append(headers, column.header)
}
fmt.Fprintln(w, strings.Join(headers, "\t"))
}
func printValues(w io.Writer, context string, installation *store.Installation) {
var values []string
for _, column := range listColumns {
values = append(values, column.value(context, installation))
}
fmt.Fprintln(w, strings.Join(values, "\t"))
}
func getInstallations(targetContext, configDir string) ([]*store.Installation, error) {
appstore, err := store.NewApplicationStore(configDir)
if err != nil {
return nil, err
}
installationStore, err := appstore.InstallationStore(targetContext)
if err != nil {
return nil, err
}
installationNames, err := installationStore.List()
if err != nil {
return nil, err
}
installations := make([]*store.Installation, len(installationNames))
for i, name := range installationNames {
installation, err := installationStore.Read(name)
if err != nil {
return nil, err
}
installations[i] = installation
}
// Sort installations with last modified first
sort.Slice(installations, func(i, j int) bool {
return installations[i].Modified.After(installations[j].Modified)
})
return installations, nil
}
func prettyPrintReference(ref string) string {
if ref == "" {
return ""
}
r, err := reference.Parse(ref)
if err != nil {
return ref
}
return reference.FamiliarString(r)
}
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] [--output OUTPUT_FILE]",
Short: "Merge a directory format Docker Application definition into a single file",
Example: `$ docker app merge myapp.dockerapp --output myapp-single.dockerapp`,
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"
"io"
"os"
"strings"
"github.com/deislabs/cnab-go/bundle"
"github.com/docker/app/internal"
"github.com/docker/app/internal/store"
"github.com/docker/app/types/parameters"
cliopts "github.com/docker/cli/opts"
"github.com/pkg/errors"
)
type mergeBundleConfig struct {
bundle *bundle.Bundle
params map[string]string
strictMode bool
stderr io.Writer
}
type mergeBundleOpt func(c *mergeBundleConfig) error
func withFileParameters(parametersFiles []string) mergeBundleOpt {
return func(c *mergeBundleConfig) error {
p, err := parameters.LoadFiles(parametersFiles)
if err != nil {
return err
}
for k, v := range p.Flatten() {
c.params[k] = v
}
return nil
}
}
func withCommandLineParameters(overrides []string) mergeBundleOpt {
return func(c *mergeBundleConfig) error {
d := cliopts.ConvertKVStringsToMap(overrides)
for k, v := range d {
c.params[k] = v
}
return nil
}
}
func withSendRegistryAuth(sendRegistryAuth bool) mergeBundleOpt {
return func(c *mergeBundleConfig) error {
if _, ok := c.bundle.Definitions[internal.ParameterShareRegistryCredsName]; ok {
val := "false"
if sendRegistryAuth {
val = "true"
}
c.params[internal.ParameterShareRegistryCredsName] = val
}
return nil
}
}
func withOrchestratorParameters(orchestrator string, kubeNamespace string) mergeBundleOpt {
return func(c *mergeBundleConfig) error {
if _, ok := c.bundle.Definitions[internal.ParameterOrchestratorName]; ok {
c.params[internal.ParameterOrchestratorName] = orchestrator
}
if _, ok := c.bundle.Definitions[internal.ParameterKubernetesNamespaceName]; ok {
c.params[internal.ParameterKubernetesNamespaceName] = kubeNamespace
}
return nil
}
}
func withErrorWriter(w io.Writer) mergeBundleOpt {
return func(c *mergeBundleConfig) error {
c.stderr = w
return nil
}
}
func withStrictMode(strict bool) mergeBundleOpt {
return func(c *mergeBundleConfig) error {
c.strictMode = strict
return nil
}
}
func mergeBundleParameters(installation *store.Installation, ops ...mergeBundleOpt) error {
bndl := installation.Bundle
if installation.Parameters == nil {
installation.Parameters = make(map[string]interface{})
}
userParams := map[string]string{}
cfg := &mergeBundleConfig{
bundle: bndl,
params: userParams,
stderr: os.Stderr,
}
for _, op := range ops {
if err := op(cfg); err != nil {
return err
}
}
mergedValues, err := matchAndMergeParametersDefinition(installation.Parameters, cfg)
if err != nil {
return err
}
installation.Parameters, err = bundle.ValuesOrDefaults(mergedValues, bndl)
return err
}
func matchAndMergeParametersDefinition(currentValues map[string]interface{}, cfg *mergeBundleConfig) (map[string]interface{}, error) {
mergedValues := make(map[string]interface{})
for k, v := range currentValues {
mergedValues[k] = v
}
for k, v := range cfg.params {
param, ok := cfg.bundle.Parameters[k]
if !ok {
if cfg.strictMode {
return nil, fmt.Errorf("parameter %q is not defined in the bundle", k)
}
fmt.Fprintf(cfg.stderr, "Warning: parameter %q is not defined in the bundle\n", k)
continue
}
definition, ok := cfg.bundle.Definitions[param.Definition]
if !ok {
return nil, fmt.Errorf("invalid bundle: definition not found for parameter %q", k)
}
value, err := definition.ConvertValue(v)
if err != nil {
return nil, errors.Wrapf(err, "invalid value for parameter %q", k)
}
valErrors, err := definition.Validate(value)
if valErrors != nil {
errs := make([]string, len(valErrors))
for i, v := range valErrors {
errs[i] = v.Error
}
errMsg := strings.Join(errs, ", ")
return nil, errors.Wrapf(fmt.Errorf(errMsg), "invalid value for parameter %q", k)
}
if err != nil {
return nil, errors.Wrapf(err, "invalid value for parameter %q", k)
}
mergedValues[k] = value
}
return mergedValues, nil
}
package commands
import (
"fmt"
"os"
"github.com/docker/app/internal/store"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/config"
"github.com/docker/distribution/reference"
"github.com/pkg/errors"
"github.com/spf13/cobra"
)
func pullCmd(dockerCli command.Cli) *cobra.Command {
var opts registryOptions
cmd := &cobra.Command{
Use: "pull NAME:TAG [OPTIONS]",
Short: "Pull an application package from a registry",
Example: `$ docker app pull docker/app-example:0.1.0`,
Args: cli.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
return runPull(dockerCli, args[0], opts)
},
}
opts.addFlags(cmd.Flags())
return cmd
}
func runPull(dockerCli command.Cli, name string, opts registryOptions) error {
appstore, err := store.NewApplicationStore(config.Dir())
if err != nil {
return err
}
bundleStore, err := appstore.BundleStore()
if err != nil {
return err
}
ref, err := reference.ParseNormalizedNamed(name)
if err != nil {
return errors.Wrap(err, name)
}
bndl, err := bundleStore.LookupOrPullBundle(reference.TagNameOnly(ref), true, dockerCli.ConfigFile(), opts.insecureRegistries)
if err != nil {
return errors.Wrap(err, name)
}
fmt.Fprintf(os.Stdout, "Successfully pulled %q (%s) from %s\n", bndl.Name, bndl.Version, ref.String())
return nil
}
package commands
import (
"bytes"
"context"
"fmt"
"io"
"io/ioutil"
"os"
"strings"
"github.com/containerd/containerd/platforms"
"github.com/deislabs/cnab-go/bundle"
"github.com/docker/app/internal/log"
"github.com/docker/app/types/metadata"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/docker/cnab-to-oci/remotes"
"github.com/docker/distribution/reference"
"github.com/docker/docker/api/types"
"github.com/docker/docker/pkg/jsonmessage"
"github.com/docker/docker/pkg/term"
"github.com/docker/docker/registry"
"github.com/morikuni/aec"
ocischemav1 "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
const ( // Docker specific annotations and values
// DockerAppFormatAnnotation is the top level annotation specifying the kind of the App Bundle
DockerAppFormatAnnotation = "io.docker.app.format"
// DockerAppFormatCNAB is the DockerAppFormatAnnotation value for CNAB
DockerAppFormatCNAB = "cnab"
// DockerTypeAnnotation is the annotation that designates the type of the application
DockerTypeAnnotation = "io.docker.type"
// DockerTypeApp is the value used to fill DockerTypeAnnotation when targeting a docker-app
DockerTypeApp = "app"
)
type pushOptions struct {
registry registryOptions
tag string
platforms []string
allPlatforms bool
}
func pushCmd(dockerCli command.Cli) *cobra.Command {
var opts pushOptions
cmd := &cobra.Command{
Use: "push [APP_NAME] --tag TARGET_REFERENCE [OPTIONS]",
Short: "Push an application package to a registry",
Example: `$ docker app push myapp --tag myrepo/myapp:mytag`,
Args: cli.RequiresMaxArgs(1),
PreRunE: func(cmd *cobra.Command, args []string) error {
return checkFlags(cmd.Flags(), opts)
},
RunE: func(cmd *cobra.Command, args []string) error {
return runPush(dockerCli, firstOrEmpty(args), opts)
},
}
flags := cmd.Flags()
flags.StringVarP(&opts.tag, "tag", "t", "", "Target registry reference (default: <name>:<version> from metadata)")
flags.StringSliceVar(&opts.platforms, "platform", []string{"linux/amd64"}, "For multi-arch service images, push the specified platforms")
flags.BoolVar(&opts.allPlatforms, "all-platforms", false, "If present, push all platforms")
opts.registry.addFlags(flags)
return cmd
}
func runPush(dockerCli command.Cli, name string, opts pushOptions) error {
defer muteDockerCli(dockerCli)()
// Get the bundle
bndl, ref, err := resolveReferenceAndBundle(dockerCli, name)
if err != nil {
return err
}
// Retag invocation image if needed
retag, err := shouldRetagInvocationImage(metadata.FromBundle(bndl), bndl, opts.tag, ref)
if err != nil {
return err
}
if retag.shouldRetag {
logrus.Debugf(`Retagging invocation image "%q"`, retag.invocationImageRef.String())
if err := retagInvocationImage(dockerCli, bndl, retag.invocationImageRef.String()); err != nil {
return err
}
}
// Push the invocation image
if err := pushInvocationImage(dockerCli, retag); err != nil {
return err
}
// Push the bundle
return pushBundle(dockerCli, opts, bndl, retag)
}
func resolveReferenceAndBundle(dockerCli command.Cli, name string) (*bundle.Bundle, string, error) {
bundleStore, err := prepareBundleStore()
if err != nil {
return nil, "", err
}
bndl, ref, err := resolveBundle(dockerCli, bundleStore, name, false, nil)
if err != nil {
return nil, "", err
}
if err := bndl.Validate(); err != nil {
return nil, "", err
}
return bndl, ref, err
}
func pushInvocationImage(dockerCli command.Cli, retag retagResult) error {
logrus.Debugf("Pushing the invocation image %q", retag.invocationImageRef)
repoInfo, err := registry.ParseRepositoryInfo(retag.invocationImageRef)
if err != nil {
return err
}
encodedAuth, err := command.EncodeAuthToBase64(command.ResolveAuthConfig(context.Background(), dockerCli, repoInfo.Index))
if err != nil {
return err
}
reader, err := dockerCli.Client().ImagePush(context.Background(), retag.invocationImageRef.String(), types.ImagePushOptions{
RegistryAuth: encodedAuth,
})
if err != nil {
return errors.Wrapf(err, "starting push of %q", retag.invocationImageRef.String())
}
defer reader.Close()
if err := jsonmessage.DisplayJSONMessagesStream(reader, ioutil.Discard, 0, false, nil); err != nil {
return errors.Wrapf(err, "pushing to %q", retag.invocationImageRef.String())
}
return nil
}
func pushBundle(dockerCli command.Cli, opts pushOptions, bndl *bundle.Bundle, retag retagResult) error {
resolver := remotes.CreateResolver(dockerCli.ConfigFile(), opts.registry.insecureRegistries...)
var display fixupDisplay = &plainDisplay{out: os.Stdout}
if term.IsTerminal(os.Stdout.Fd()) {
display = &interactiveDisplay{out: os.Stdout}
}
fixupOptions := []remotes.FixupOption{
remotes.WithEventCallback(display.onEvent),
}
if platforms := platformFilter(opts); len(platforms) > 0 {
fixupOptions = append(fixupOptions, remotes.WithComponentImagePlatforms(platforms))
}
// bundle fixup
if err := remotes.FixupBundle(context.Background(), bndl, retag.cnabRef, resolver, fixupOptions...); err != nil {
return errors.Wrapf(err, "fixing up %q for push", retag.cnabRef)
}
// push bundle manifest
logrus.Debugf("Pushing the bundle %q", retag.cnabRef)
descriptor, err := remotes.Push(log.WithLogContext(context.Background()), bndl, retag.cnabRef, resolver, true, withAppAnnotations)
if err != nil {
return errors.Wrapf(err, "pushing to %q", retag.cnabRef)
}
fmt.Fprintf(os.Stdout, "Successfully pushed bundle to %s. Digest is %s.\n", retag.cnabRef.String(), descriptor.Digest)
return nil
}
func withAppAnnotations(index *ocischemav1.Index) error {
if index.Annotations == nil {
index.Annotations = make(map[string]string)
}
index.Annotations[DockerAppFormatAnnotation] = DockerAppFormatCNAB
index.Annotations[DockerTypeAnnotation] = DockerTypeApp
return nil
}
func platformFilter(opts pushOptions) []string {
if opts.allPlatforms {
return nil
}
return opts.platforms
}
func retagInvocationImage(dockerCli command.Cli, bndl *bundle.Bundle, newName string) error {
err := dockerCli.Client().ImageTag(context.Background(), bndl.InvocationImages[0].Image, newName)
if err != nil {
return err
}
bndl.InvocationImages[0].Image = newName
return nil
}
type retagResult struct {
shouldRetag bool
cnabRef reference.Named
invocationImageRef reference.Named
}
func shouldRetagInvocationImage(meta metadata.AppMetadata, bndl *bundle.Bundle, tagOverride, bundleRef string) (retagResult, error) {
// Use the bundle reference as a tag override
if tagOverride == "" && bundleRef != "" {
tagOverride = bundleRef
}
imgName := tagOverride
var err error
if imgName == "" {
imgName, err = makeCNABImageName(meta.Name, meta.Version, "")
if err != nil {
return retagResult{}, err
}
}
cnabRef, err := reference.ParseNormalizedNamed(imgName)
if err != nil {
return retagResult{}, errors.Wrap(err, imgName)
}
if _, digested := cnabRef.(reference.Digested); digested {
return retagResult{}, errors.Errorf("%s: can't push to a digested reference", cnabRef)
}
cnabRef = reference.TagNameOnly(cnabRef)
expectedInvocationImageRef, err := reference.ParseNormalizedNamed(reference.TagNameOnly(cnabRef).String() + "-invoc")
if err != nil {
return retagResult{}, errors.Wrap(err, reference.TagNameOnly(cnabRef).String()+"-invoc")
}
currentInvocationImageRef, err := reference.ParseNormalizedNamed(bndl.InvocationImages[0].Image)
if err != nil {
return retagResult{}, errors.Wrap(err, bndl.InvocationImages[0].Image)
}
return retagResult{
cnabRef: cnabRef,
invocationImageRef: expectedInvocationImageRef,
shouldRetag: expectedInvocationImageRef.String() != currentInvocationImageRef.String(),
}, nil
}
type fixupDisplay interface {
onEvent(remotes.FixupEvent)
}
type interactiveDisplay struct {
out io.Writer
previousLineCount int
images []interactiveImageState
}
func (r *interactiveDisplay) onEvent(ev remotes.FixupEvent) {
out := bytes.NewBuffer(nil)
for i := 0; i < r.previousLineCount; i++ {
fmt.Fprint(out, aec.NewBuilder(aec.Up(1), aec.EraseLine(aec.EraseModes.All)).ANSI)
}
switch ev.EventType {
case remotes.FixupEventTypeCopyImageStart:
r.images = append(r.images, interactiveImageState{name: ev.SourceImage})
case remotes.FixupEventTypeCopyImageEnd:
r.images[r.imageIndex(ev.SourceImage)].done = true
case remotes.FixupEventTypeProgress:
r.images[r.imageIndex(ev.SourceImage)].onProgress(ev.Progress)
}
r.previousLineCount = 0
for _, s := range r.images {
r.previousLineCount += s.print(out)
}
r.out.Write(out.Bytes()) //nolint:errcheck // nothing much we can do with an error to write to output.
}
func (r *interactiveDisplay) imageIndex(name string) int {
for ix, state := range r.images {
if state.name == name {
return ix
}
}
return 0
}
type interactiveImageState struct {
name string
progress remotes.ProgressSnapshot
done bool
}
func (s *interactiveImageState) onProgress(p remotes.ProgressSnapshot) {
s.progress = p
}
func (s *interactiveImageState) print(out io.Writer) int {
if s.done {
fmt.Fprint(out, aec.Apply(s.name, aec.BlueF))
} else {
fmt.Fprint(out, s.name)
}
fmt.Fprint(out, "\n")
lineCount := 1
for _, p := range s.progress.Roots {
lineCount += printDescriptorProgress(out, &p, 1)
}
return lineCount
}
func printDescriptorProgress(out io.Writer, p *remotes.DescriptorProgressSnapshot, depth int) int {
fmt.Fprint(out, strings.Repeat(" ", depth))
name := p.MediaType
if p.Platform != nil {
name = platforms.Format(*p.Platform)
}
if len(p.Children) == 0 {
name = fmt.Sprintf("%s...: %s", p.Digest.String()[:15], p.Action)
}
doneCount := 0
for _, c := range p.Children {
if c.Done {
doneCount++
}
}
display := name
if len(p.Children) > 0 {
display = fmt.Sprintf("%s [%d/%d] (%s...)", name, doneCount, len(p.Children), p.Digest.String()[:15])
}
if p.Done {
display = aec.Apply(display, aec.BlueF)
}
if hasError(p) {
display = aec.Apply(display, aec.RedF)
}
fmt.Fprintln(out, display)
lineCount := 1
if p.Done {
return lineCount
}
for _, c := range p.Children {
lineCount += printDescriptorProgress(out, &c, depth+1)
}
return lineCount
}
func hasError(p *remotes.DescriptorProgressSnapshot) bool {
if p.Error != nil {
return true
}
for _, c := range p.Children {
if hasError(&c) {
return true
}
}
return false
}
type plainDisplay struct {
out io.Writer
}
func (r *plainDisplay) onEvent(ev remotes.FixupEvent) {
switch ev.EventType {
case remotes.FixupEventTypeCopyImageStart:
fmt.Fprintf(r.out, "Handling image %s...", ev.SourceImage)
case remotes.FixupEventTypeCopyImageEnd:
if ev.Error != nil {
fmt.Fprintf(r.out, "\nFailure: %s\n", ev.Error)
} else {
fmt.Fprint(r.out, " done!\n")
}
}
}
func checkFlags(flags *pflag.FlagSet, opts pushOptions) error {
if opts.allPlatforms && flags.Changed("all-platforms") && flags.Changed("platform") {
return fmt.Errorf("--all-plaforms and --plaform flags cannot be used at the same time")
}
return nil
}
package commands
import (
"fmt"
"io"
"os"
"github.com/docker/app/internal"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/spf13/cobra"
)
type renderOptions struct {
parametersOptions
registryOptions
pullOptions
formatDriver string
renderOutput string
}
func renderCmd(dockerCli command.Cli) *cobra.Command {
var opts renderOptions
cmd := &cobra.Command{
Use: "render [APP_NAME] [--set KEY=VALUE ...] [--parameters-file PARAMETERS-FILE ...] [OPTIONS]",
Short: "Render the Compose file for an Application Package",
Example: `$ docker app render myapp.dockerapp --set key=value`,
Args: cli.RequiresMaxArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
return runRender(dockerCli, firstOrEmpty(args), opts)
},
}
opts.parametersOptions.addFlags(cmd.Flags())
opts.registryOptions.addFlags(cmd.Flags())
opts.pullOptions.addFlags(cmd.Flags())
cmd.Flags().StringVarP(&opts.renderOutput, "output", "o", "-", "Output file")
cmd.Flags().StringVar(&opts.formatDriver, "formatter", "yaml", "Configure the output format (yaml|json)")
return cmd
}
func runRender(dockerCli command.Cli, appname string, opts renderOptions) error {
defer muteDockerCli(dockerCli)()
var w io.Writer = os.Stdout
if opts.renderOutput != "-" {
f, err := os.Create(opts.renderOutput)
if err != nil {
return err
}
defer f.Close()
w = f
}
action, installation, errBuf, err := prepareCustomAction(internal.ActionRenderName, dockerCli, appname, w, opts.registryOptions, opts.pullOptions, opts.parametersOptions)
if err != nil {
return err
}
installation.Parameters[internal.ParameterRenderFormatName] = opts.formatDriver
if err := action.Run(&installation.Claim, nil, nil); err != nil {
return fmt.Errorf("render failed: %s\n%s", err, errBuf)
}
return nil
}
package commands
import (
"io/ioutil"
"github.com/docker/app/internal/store"
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/config"
"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",
Long: `A tool to build and manage Docker Applications.`,
Use: use,
Annotations: map[string]string{"experimentalCLI": "true"},
}
addCommands(cmd, dockerCli)
return cmd
}
func addCommands(cmd *cobra.Command, dockerCli command.Cli) {
cmd.AddCommand(
installCmd(dockerCli),
upgradeCmd(dockerCli),
uninstallCmd(dockerCli),
listCmd(dockerCli),
statusCmd(dockerCli),
initCmd(dockerCli),
inspectCmd(dockerCli),
mergeCmd(dockerCli),
renderCmd(dockerCli),
splitCmd(),
validateCmd(),
versionCmd(dockerCli),
completionCmd(dockerCli, cmd),
bundleCmd(dockerCli),
pushCmd(dockerCli),
pullCmd(dockerCli),
)
}
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)) //nolint:errcheck // WithCombinedStreams cannot error
return func() {
dockerCli.Apply(command.WithOutputStream(stdout), command.WithErrorStream(stderr)) //nolint:errcheck // as above
}
}
func prepareStores(targetContext string) (store.BundleStore, store.InstallationStore, store.CredentialStore, error) {
appstore, err := store.NewApplicationStore(config.Dir())
if err != nil {
return nil, nil, nil, err
}
installationStore, err := appstore.InstallationStore(targetContext)
if err != nil {
return nil, nil, nil, err
}
bundleStore, err := appstore.BundleStore()
if err != nil {
return nil, nil, nil, err
}
credentialStore, err := appstore.CredentialStore(targetContext)
if err != nil {
return nil, nil, nil, err
}
return bundleStore, installationStore, credentialStore, nil
}
func prepareBundleStore() (store.BundleStore, error) {
appstore, err := store.NewApplicationStore(config.Dir())
if err != nil {
return nil, err
}
bundleStore, err := appstore.BundleStore()
if err != nil {
return nil, err
}
return bundleStore, nil
}
type parametersOptions struct {
parametersFiles []string
overrides []string
strictMode bool
}
func (o *parametersOptions) addFlags(flags *pflag.FlagSet) {
flags.StringArrayVar(&o.parametersFiles, "parameters-file", []string{}, "Override parameters file")
flags.StringArrayVarP(&o.overrides, "set", "s", []string{}, "Override parameter value")
flags.BoolVar(&o.strictMode, "strict", false, "Fail when a paramater is undefined instead of displaying a warning")
}
type credentialOptions struct {
targetContext string
credentialsets []string
credentials []string
sendRegistryAuth bool
}
func (o *credentialOptions) addFlags(flags *pflag.FlagSet) {
flags.StringVar(&o.targetContext, "target-context", "", "Context on which the application is installed (default: <current-context>)")
flags.StringArrayVar(&o.credentialsets, "credential-set", []string{}, "Use a YAML file containing a credential set or a credential set present in the credential store")
flags.StringArrayVar(&o.credentials, "credential", nil, "Add a single credential, additive ontop of any --credential-set used")
flags.BoolVar(&o.sendRegistryAuth, "with-registry-auth", false, "Sends registry auth")
}
func (o *credentialOptions) SetDefaultTargetContext(dockerCli command.Cli) {
o.targetContext = getTargetContext(o.targetContext, dockerCli.CurrentContext())
}
func (o *credentialOptions) CredentialSetOpts(dockerCli command.Cli, credentialStore store.CredentialStore) []credentialSetOpt {
return []credentialSetOpt{
addNamedCredentialSets(credentialStore, o.credentialsets),
addCredentials(o.credentials),
addDockerCredentials(o.targetContext, dockerCli.ContextStore()),
addRegistryCredentials(o.sendRegistryAuth, dockerCli),
}
}
type registryOptions struct {
insecureRegistries []string
}
func (o *registryOptions) addFlags(flags *pflag.FlagSet) {
flags.StringSliceVar(&o.insecureRegistries, "insecure-registries", nil, "Use HTTP instead of HTTPS when pulling from/pushing to those registries")
}
type pullOptions struct {
pull bool
}
func (o *pullOptions) addFlags(flags *pflag.FlagSet) {
flags.BoolVar(&o.pull, "pull", false, "Pull the bundle")
}
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] [--output OUTPUT_DIRECTORY]",
Short: "Split a single-file Docker Application definition into the directory format",
Example: `$ docker app split myapp.dockerapp --output myapp-directory.dockerapp`,
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 directory (default: in-place)")
return cmd
}
package commands
import (
"fmt"
"io"
"os"
"sort"
"strings"
"text/tabwriter"
"time"
"github.com/deislabs/cnab-go/action"
"github.com/deislabs/cnab-go/credentials"
"github.com/docker/app/internal"
"github.com/docker/app/internal/store"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
units "github.com/docker/go-units"
"github.com/spf13/cobra"
)
var (
knownStatusActions = []string{
internal.ActionStatusName,
internal.ActionStatusNameDeprecated,
}
)
func statusCmd(dockerCli command.Cli) *cobra.Command {
var opts credentialOptions
cmd := &cobra.Command{
Use: "status INSTALLATION_NAME [--target-context TARGET_CONTEXT] [OPTIONS]",
Short: "Get the installation status of an application",
Long: "Get the installation status of an application. If the installation is a Docker Application, the status shows the stack services.",
Example: "$ docker app status myinstallation --target-context=mycontext",
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, installationName string, opts credentialOptions) error {
defer muteDockerCli(dockerCli)()
opts.SetDefaultTargetContext(dockerCli)
_, installationStore, credentialStore, err := prepareStores(opts.targetContext)
if err != nil {
return err
}
installation, err := installationStore.Read(installationName)
if err != nil {
return err
}
displayInstallationStatus(os.Stdout, installation)
// Check if the bundle knows the docker app status action, if not just exit without error.
statusAction := resolveStatusAction(installation)
if statusAction == "" {
return nil
}
bind, err := requiredClaimBindMount(installation.Claim, opts.targetContext, dockerCli)
if err != nil {
return err
}
driverImpl, errBuf := prepareDriver(dockerCli, bind, nil)
if err := mergeBundleParameters(installation,
withSendRegistryAuth(opts.sendRegistryAuth),
); err != nil {
return err
}
creds, err := prepareCredentialSet(installation.Bundle, opts.CredentialSetOpts(dockerCli, credentialStore)...)
if err != nil {
return err
}
if err := credentials.Validate(creds, installation.Bundle.Credentials); err != nil {
return err
}
printHeader(os.Stdout, "STATUS")
status := &action.RunCustom{
Action: statusAction,
Driver: driverImpl,
}
if err := status.Run(&installation.Claim, creds, dockerCli.Out()); err != nil {
return fmt.Errorf("status failed: %s\n%s", err, errBuf)
}
return nil
}
func displayInstallationStatus(w io.Writer, installation *store.Installation) {
printHeader(w, "INSTALLATION")
tab := tabwriter.NewWriter(w, 0, 0, 1, ' ', 0)
printValue(tab, "Name", installation.Name)
printValue(tab, "Created", units.HumanDuration(time.Since(installation.Created)))
printValue(tab, "Modified", units.HumanDuration(time.Since(installation.Modified)))
printValue(tab, "Revision", installation.Revision)
printValue(tab, "Last Action", installation.Result.Action)
printValue(tab, "Result", strings.ToUpper(installation.Result.Status))
if o, ok := installation.Parameters[internal.ParameterOrchestratorName]; ok {
orchestrator := fmt.Sprintf("%v", o)
if orchestrator == "" {
orchestrator = string(command.OrchestratorSwarm)
}
printValue(tab, "Orchestrator", orchestrator)
if kubeNamespace, ok := installation.Parameters[internal.ParameterKubernetesNamespaceName]; ok && orchestrator == string(command.OrchestratorKubernetes) {
printValue(tab, "Kubernetes namespace", fmt.Sprintf("%v", kubeNamespace))
}
}
tab.Flush()
fmt.Fprintln(w)
printHeader(w, "APPLICATION")
tab = tabwriter.NewWriter(w, 0, 0, 1, ' ', 0)
printValue(tab, "Name", installation.Bundle.Name)
printValue(tab, "Version", installation.Bundle.Version)
printValue(tab, "Reference", installation.Reference)
tab.Flush()
fmt.Fprintln(w)
if len(installation.Parameters) > 0 {
printHeader(w, "PARAMETERS")
tab = tabwriter.NewWriter(w, 0, 0, 1, ' ', 0)
params := sortParameters(installation)
for _, param := range params {
if !strings.HasPrefix(param, internal.Namespace) {
// TODO: Trim long []byte parameters, maybe add type too (string, int...)
printValue(tab, param, fmt.Sprintf("%v", installation.Parameters[param]))
}
}
tab.Flush()
fmt.Fprintln(w)
}
}
func sortParameters(installation *store.Installation) []string {
var params []string
for name := range installation.Parameters {
params = append(params, name)
}
sort.Strings(params)
return params
}
func printHeader(w io.Writer, header string) {
fmt.Fprintln(w, header)
fmt.Fprintln(w, strings.Repeat("-", len(header)))
}
func printValue(w io.Writer, key, value string) {
fmt.Fprintf(w, "%s:\t%s\n", key, value)
}
func resolveStatusAction(installation *store.Installation) string {
for _, name := range knownStatusActions {
if _, ok := installation.Bundle.Actions[name]; ok {
return name
}
}
return ""
}
package commands
import (
"fmt"
"os"
"github.com/deislabs/cnab-go/action"
"github.com/deislabs/cnab-go/credentials"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
"github.com/spf13/cobra"
)
type uninstallOptions struct {
credentialOptions
force bool
}
func uninstallCmd(dockerCli command.Cli) *cobra.Command {
var opts uninstallOptions
cmd := &cobra.Command{
Use: "uninstall INSTALLATION_NAME [--target-context TARGET_CONTEXT] [OPTIONS]",
Short: "Uninstall an application",
Example: `$ docker app uninstall myinstallation --target-context=mycontext`,
Args: cli.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
return runUninstall(dockerCli, args[0], opts)
},
}
opts.addFlags(cmd.Flags())
cmd.Flags().BoolVar(&opts.force, "force", false, "Force removal of installation")
return cmd
}
func runUninstall(dockerCli command.Cli, installationName string, opts uninstallOptions) (mainErr error) {
defer muteDockerCli(dockerCli)()
opts.SetDefaultTargetContext(dockerCli)
_, installationStore, credentialStore, err := prepareStores(opts.targetContext)
if err != nil {
return err
}
installation, err := installationStore.Read(installationName)
if err != nil {
return err
}
if opts.force {
defer func() {
if mainErr == nil {
return
}
if err := installationStore.Delete(installationName); err != nil {
fmt.Fprintf(os.Stderr, "failed to force deletion of installation %q: %s\n", installationName, err)
return
}
fmt.Fprintf(os.Stderr, "deletion forced for installation %q\n", installationName)
}()
}
bind, err := requiredClaimBindMount(installation.Claim, opts.targetContext, dockerCli)
if err != nil {
return err
}
driverImpl, errBuf := prepareDriver(dockerCli, bind, nil)
creds, err := prepareCredentialSet(installation.Bundle, opts.CredentialSetOpts(dockerCli, credentialStore)...)
if err != nil {
return err
}
if err := credentials.Validate(creds, installation.Bundle.Credentials); err != nil {
return err
}
uninst := &action.Uninstall{
Driver: driverImpl,
}
if err := uninst.Run(&installation.Claim, creds, os.Stdout); err != nil {
if err2 := installationStore.Store(installation); err2 != nil {
return fmt.Errorf("%s while %s", err2, errBuf)
}
return fmt.Errorf("Uninstall failed: %s\n%s", err, errBuf)
}
if err := installationStore.Delete(installationName); err != nil {
return fmt.Errorf("Failed to delete installation %q from the installation store: %s", installationName, err)
}
fmt.Fprintf(os.Stdout, "Application %q uninstalled on context %q\n", installationName, opts.targetContext)
return nil
}
package commands
import (
"fmt"
"os"
"github.com/deislabs/cnab-go/action"
"github.com/deislabs/cnab-go/credentials"
"github.com/docker/cli/cli/command"
"github.com/spf13/cobra"
)
type upgradeOptions struct {
parametersOptions
credentialOptions
registryOptions
pullOptions
bundleOrDockerApp string
}
func upgradeCmd(dockerCli command.Cli) *cobra.Command {
var opts upgradeOptions
cmd := &cobra.Command{
Use: "upgrade INSTALLATION_NAME [--target-context TARGET_CONTEXT] [OPTIONS]",
Short: "Upgrade an installed application",
Example: `$ docker app upgrade myinstallation --target-context=mycontext --set key=value`,
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())
opts.registryOptions.addFlags(cmd.Flags())
opts.pullOptions.addFlags(cmd.Flags())
cmd.Flags().StringVar(&opts.bundleOrDockerApp, "app-name", "", "Override the installation with another Application Package")
return cmd
}
func runUpgrade(dockerCli command.Cli, installationName string, opts upgradeOptions) error {
defer muteDockerCli(dockerCli)()
opts.SetDefaultTargetContext(dockerCli)
bundleStore, installationStore, credentialStore, err := prepareStores(opts.targetContext)
if err != nil {
return err
}
installation, err := installationStore.Read(installationName)
if err != nil {
return err
}
if isInstallationFailed(installation) {
return fmt.Errorf("Installation %q has failed and cannot be upgraded, reinstall it using 'docker app install'", installationName)
}
if opts.bundleOrDockerApp != "" {
b, _, err := resolveBundle(dockerCli, bundleStore, opts.bundleOrDockerApp, opts.pull, opts.insecureRegistries)
if err != nil {
return err
}
installation.Bundle = b
}
if err := mergeBundleParameters(installation,
withFileParameters(opts.parametersFiles),
withCommandLineParameters(opts.overrides),
withSendRegistryAuth(opts.sendRegistryAuth),
withStrictMode(opts.strictMode),
); err != nil {
return err
}
bind, err := requiredClaimBindMount(installation.Claim, opts.targetContext, dockerCli)
if err != nil {
return err
}
driverImpl, errBuf := prepareDriver(dockerCli, bind, nil)
creds, err := prepareCredentialSet(installation.Bundle, opts.CredentialSetOpts(dockerCli, credentialStore)...)
if err != nil {
return err
}
if err := credentials.Validate(creds, installation.Bundle.Credentials); err != nil {
return err
}
u := &action.Upgrade{
Driver: driverImpl,
}
err = u.Run(&installation.Claim, creds, os.Stdout)
err2 := installationStore.Store(installation)
if err != nil {
return fmt.Errorf("Upgrade failed: %s\n%s", err, errBuf)
}
if err2 != nil {
return err2
}
fmt.Fprintf(os.Stdout, "Application %q upgraded on context %q\n", installationName, opts.targetContext)
return nil
}
package commands
import (
"fmt"
"os"
"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"
)
type validateOptions struct {
parametersOptions
}
func validateCmd() *cobra.Command {
var opts validateOptions
cmd := &cobra.Command{
Use: "validate [APP_NAME] [--set KEY=VALUE ...] [--parameters-file 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(opts.parametersFiles...),
)
if err != nil {
return err
}
defer app.Cleanup()
argParameters := cliopts.ConvertKVStringsToMap(opts.overrides)
_, err = render.Render(app, argParameters, nil)
if err != nil {
return err
}
fmt.Fprintf(os.Stdout, "Validated %q\n", app.Path)
return nil
},
}
opts.parametersOptions.addFlags(cmd.Flags())
return cmd
}
package commands
import (
"fmt"
"os"
"github.com/docker/app/internal"
"github.com/docker/app/internal/packager"
"github.com/docker/cli/cli/command"
"github.com/spf13/cobra"
)
func versionCmd(dockerCli command.Cli) *cobra.Command {
var onlyBaseImage bool
cmd := &cobra.Command{
Use: "version",
Short: "Print version information",
Long: `Print version information
The --base-invocation-image will return the base invocation image name only. This can be useful for
docker pull $(docker app version --base-invocation-image)
In order to be able to build an invocation images when using docker app from an offline system.
`,
Run: func(cmd *cobra.Command, args []string) {
image := packager.BaseInvocationImage(dockerCli)
if onlyBaseImage {
fmt.Fprintln(os.Stdout, image)
} else {
fmt.Fprintln(os.Stdout, internal.FullVersion(image))
}
},
}
cmd.Flags().BoolVar(&onlyBaseImage, "base-invocation-image", false, "Print CNAB base invocation image to be used")
return cmd
}
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) ([]composetypes.ConfigFile, map[string]string, error) {
configFiles := []composetypes.ConfigFile{}
for _, data := range composes {
s := string(data)
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/cnab-go/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) {
sort.Slice(config.Services, func(i, j int) bool {
return config.Services[i].Name < config.Services[j].Name
})
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) {
names := make([]string, 0, len(config.Networks))
for name := range config.Networks {
names = append(names, name)
}
sort.Strings(names)
for _, name := range names {
fmt.Fprintln(w, name)
}
}, "Network")
// Add Volume section
printSection(out, len(config.Volumes), func(w io.Writer) {
names := make([]string, 0, len(config.Volumes))
for name := range config.Volumes {
names = append(names, name)
}
sort.Strings(names)
for _, name := range names {
fmt.Fprintln(w, name)
}
}, "Volume")
// Add Secret section
printSection(out, len(config.Secrets), func(w io.Writer) {
names := make([]string, 0, len(config.Secrets))
for name := range config.Secrets {
names = append(names, name)
}
sort.Strings(names)
for _, name := range names {
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 log
import (
"context"
"github.com/containerd/containerd/log"
"github.com/sirupsen/logrus"
)
func WithLogContext(ctx context.Context) context.Context {
logger := logrus.New()
logger.SetLevel(logrus.GetLevel())
return log.WithLogger(ctx, logrus.NewEntry(logger))
}
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."
// CnabNamespace is the namespace used with the CNAB well known custom actions
CnabNamespace = "io.cnab."
// ActionStatusNameDeprecated is the name of the docker custom "status" action
// Deprecated: use ActionStatusName instead
ActionStatusNameDeprecated = Namespace + "status"
// ActionStatusName is the name of the CNAB well known custom "status" action - TODO: Extract this constant to the cnab-go library
ActionStatusName = CnabNamespace + "status"
// ActionInspectName is the name of the custom "inspect" action
ActionInspectName = Namespace + "inspect"
// ActionRenderName is the name of the custom "render" action
ActionRenderName = Namespace + "render"
// CredentialDockerContextName is the name of the credential containing a Docker context
CredentialDockerContextName = "docker.context"
// CredentialDockerContextPath is the path to the credential containing a Docker context
CredentialDockerContextPath = "/cnab/app/context.dockercontext"
// CredentialRegistryName is the name of the credential containing registry credentials
CredentialRegistryName = Namespace + "registry-creds"
// CredentialRegistryPath is the name to the credential containing registry credentials
CredentialRegistryPath = "/cnab/app/registry-creds.json"
// ParameterOrchestratorName is the name of the parameter containing the orchestrator
ParameterOrchestratorName = Namespace + "orchestrator"
// ParameterKubernetesNamespaceName is the name of the parameter containing the kubernetes namespace
ParameterKubernetesNamespaceName = Namespace + "kubernetes-namespace"
// ParameterRenderFormatName is the name of the parameter containing the kubernetes namespace
ParameterRenderFormatName = Namespace + "render-format"
// ParameterShareRegistryCredsName is the name of the parameter which indicates if credentials should be shared
ParameterShareRegistryCredsName = Namespace + "share-registry-creds"
// DockerStackOrchestratorEnvVar is the environment variable set by the CNAB runtime to select
// the stack orchestrator.
DockerStackOrchestratorEnvVar = "DOCKER_STACK_ORCHESTRATOR"
// DockerKubernetesNamespaceEnvVar is the environment variable set by the CNAB runtime to select
// the kubernetes namespace.
DockerKubernetesNamespaceEnvVar = "DOCKER_KUBERNETES_NAMESPACE"
// DockerRenderFormatEnvVar is the environment variable set by the CNAB runtime to select
// the render output format.
DockerRenderFormatEnvVar = "DOCKER_RENDER_FORMAT"
)
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 (
"encoding/json"
"github.com/deislabs/cnab-go/bundle"
"github.com/deislabs/cnab-go/bundle/definition"
"github.com/docker/app/internal"
"github.com/docker/app/internal/compose"
"github.com/docker/app/types"
"github.com/sirupsen/logrus"
)
const (
// CNABVersion1_0_0 is the CNAB Schema version 1.0.0
CNABVersion1_0_0 = "v1.0.0-WD"
)
// 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()
definitions := definition.Definitions{
internal.ParameterOrchestratorName: {
Type: "string",
Enum: []interface{}{
"",
"swarm",
"kubernetes",
},
Default: "",
Title: "Orchestrator",
Description: "Orchestrator on which to deploy",
},
internal.ParameterKubernetesNamespaceName: {
Type: "string",
Default: "",
Title: "Namespace",
Description: "Namespace in which to deploy",
},
internal.ParameterRenderFormatName: {
Type: "string",
Enum: []interface{}{
"yaml",
"json",
},
Default: "yaml",
Title: "Render format",
Description: "Output format for the render command",
},
internal.ParameterShareRegistryCredsName: {
Type: "boolean",
Default: false,
Title: "Share registry credentials",
Description: "Share registry credentials with the invocation image",
},
}
parameters := map[string]bundle.Parameter{
internal.ParameterOrchestratorName: {
Destination: &bundle.Location{
EnvironmentVariable: internal.DockerStackOrchestratorEnvVar,
},
ApplyTo: []string{
"install",
"upgrade",
"uninstall",
internal.ActionStatusName,
},
Definition: internal.ParameterOrchestratorName,
},
internal.ParameterKubernetesNamespaceName: {
Destination: &bundle.Location{
EnvironmentVariable: internal.DockerKubernetesNamespaceEnvVar,
},
ApplyTo: []string{
"install",
"upgrade",
"uninstall",
internal.ActionStatusName,
},
Definition: internal.ParameterKubernetesNamespaceName,
},
internal.ParameterRenderFormatName: {
Destination: &bundle.Location{
EnvironmentVariable: internal.DockerRenderFormatEnvVar,
},
ApplyTo: []string{
internal.ActionRenderName,
},
Definition: internal.ParameterRenderFormatName,
},
internal.ParameterShareRegistryCredsName: {
Destination: &bundle.Location{
EnvironmentVariable: "DOCKER_SHARE_REGISTRY_CREDS",
},
Definition: internal.ParameterShareRegistryCredsName,
},
}
for name, envVar := range mapping.ParameterToCNABEnv {
definitions[name] = &definition.Schema{
Type: "string",
Default: flatParameters[name],
}
parameters[name] = bundle.Parameter{
Destination: &bundle.Location{
EnvironmentVariable: envVar,
},
Definition: 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
}
bndl := &bundle.Bundle{
SchemaVersion: CNABVersion1_0_0,
Credentials: map[string]bundle.Credential{
internal.CredentialDockerContextName: {
Location: bundle.Location{
Path: internal.CredentialDockerContextPath,
},
},
internal.CredentialRegistryName: {
Location: bundle.Location{
Path: internal.CredentialRegistryPath,
},
},
},
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,
Definitions: definitions,
Actions: map[string]bundle.Action{
internal.ActionInspectName: {
Modifies: false,
Stateless: true,
},
internal.ActionRenderName: {
Modifies: false,
Stateless: true,
},
internal.ActionStatusName: {
Modifies: false,
},
},
Images: bundleImages,
}
if js, err := json.Marshal(bndl); err == nil {
logrus.Debugf("App converted to CNAB %q", string(js))
}
return bndl, nil
}
func extractBundleImages(composeFiles [][]byte) (map[string]bundle.Image, error) {
_, images, err := compose.Load(composeFiles)
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/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("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
}
// 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 {
return nil, errors.Wrapf(err, "cannot locate application %q in filesystem", 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"
"github.com/docker/app/types/parameters"
composeloader "github.com/docker/cli/cli/compose/loader"
"github.com/docker/cli/cli/compose/schema"
"github.com/docker/cli/opts"
"github.com/pkg/errors"
"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 definition based on the provided parameters
// and returns the path to the created application definition.
func Init(name string, composeFile string, description string, maintainers []string, singleFile bool) (string, 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 dirName, 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 dirName, Merge(app, target)
}
func initFromScratch(name string) error {
logrus.Debug("Initializing 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 {
logrus.Debugf("Initializing from compose file %s", composeFile)
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
}
params := 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 {
params[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 := params[k]; !ok {
if v != "" {
params[k] = v
} else {
params[k] = "FILL ME"
needsFilling = true
}
}
}
expandedParams, err := parameters.FromFlatten(params)
if err != nil {
return errors.Wrap(err, "failed to expand parameters")
}
parametersYAML, err := yaml.Marshal(expandedParams)
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.Fprintln(os.Stderr, "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"
"path/filepath"
"github.com/docker/app/internal"
"github.com/docker/app/types"
"github.com/docker/cli/cli/command"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
const (
// DefaultCNABBaseImageName is the name of the default base invocation image.
DefaultCNABBaseImageName = "docker/cnab-app-base"
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(cli command.Cli, app *types.App, target io.Writer) error {
logrus.Debug("Packing invocation image context")
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(cli))); 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
}
// BaseInvocationImage returns the name and tag of the CNAB base invocation image
func BaseInvocationImage(cli command.Cli) string {
img := DefaultCNABBaseImageName + `:` + internal.Version
if cfg := cli.ConfigFile(); cfg != nil {
if v, ok := cfg.PluginConfig("app", "base-invocation-image"); ok {
return v
}
}
return img
}
func dockerFile(cli command.Cli) string {
return fmt.Sprintf("FROM %s\nCOPY . .", BaseInvocationImage(cli))
}
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 (
"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(),
types.YamlSingleFileSeparator(app.HasCRLF()),
app.Composes()[0],
types.YamlSingleFileSeparator(app.HasCRLF()),
app.ParametersRaw()[0],
} {
if _, err := target.Write(data); err != nil {
return err
}
}
return nil
}
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 store
import (
_ "crypto/sha256" // ensure ids can be computed
"os"
"path/filepath"
"github.com/deislabs/cnab-go/utils/crud"
digest "github.com/opencontainers/go-digest"
"github.com/pkg/errors"
)
const (
// AppConfigDirectory is the Docker App directory name inside Docker config directory
AppConfigDirectory = "app"
// BundleStoreDirectory is the bundle store directory name
BundleStoreDirectory = "bundles"
// CredentialStoreDirectory is the credential store directory name
CredentialStoreDirectory = "credentials"
// InstallationStoreDirectory is the installations store directory name
InstallationStoreDirectory = "installations"
)
// ApplicationStore is the main point to access different stores:
// - Bundle store persists all bundles built or fetched locally
// - Credential store persists all the credentials, per context basis
// - Installation store persists all the installations, per context basis
type ApplicationStore struct {
path string
}
// NewApplicationStore creates a new application store, nested inside a
// docker configuration directory. It will create all the directory hierarchy
// if anything is missing.
func NewApplicationStore(configDir string) (*ApplicationStore, error) {
storePath := filepath.Join(configDir, AppConfigDirectory)
directories := []struct {
dir string
perm os.FileMode
}{
{BundleStoreDirectory, 0755},
{CredentialStoreDirectory, 0700},
{InstallationStoreDirectory, 0755},
}
for _, d := range directories {
if err := os.MkdirAll(filepath.Join(storePath, d.dir), d.perm); err != nil {
return nil, errors.Wrapf(err, "failed to create application store directory %q", d.dir)
}
}
return &ApplicationStore{path: storePath}, nil
}
// InstallationStore initializes and returns a context based installation store
func (a ApplicationStore) InstallationStore(context string) (InstallationStore, error) {
path := filepath.Join(a.path, InstallationStoreDirectory, makeDigestedDirectory(context))
if err := os.MkdirAll(path, 0755); err != nil {
return nil, errors.Wrapf(err, "failed to create installation store directory for context %q", context)
}
return &installationStore{store: crud.NewFileSystemStore(path, "json")}, nil
}
// CredentialStore initializes and returns a context based credential store
func (a ApplicationStore) CredentialStore(context string) (CredentialStore, error) {
path := filepath.Join(a.path, CredentialStoreDirectory, makeDigestedDirectory(context))
if err := os.MkdirAll(path, 0700); err != nil {
return nil, errors.Wrapf(err, "failed to create credential store directory for context %q", context)
}
return &credentialStore{path: path}, nil
}
// BundleStore initializes and returns a bundle store
func (a ApplicationStore) BundleStore() (BundleStore, error) {
path := filepath.Join(a.path, BundleStoreDirectory)
if err := os.MkdirAll(path, 0755); err != nil {
return nil, errors.Wrapf(err, "failed to create bundle store directory %q", path)
}
return &bundleStore{path: path}, nil
}
func makeDigestedDirectory(context string) string {
return digest.FromString(context).Encoded()
}
package store
import (
"context"
"encoding/json"
"io/ioutil"
"os"
"path/filepath"
"strings"
"github.com/docker/app/internal/log"
"github.com/docker/cli/cli/config/configfile"
"github.com/deislabs/cnab-go/bundle"
"github.com/docker/cnab-to-oci/remotes"
"github.com/docker/distribution/reference"
"github.com/pkg/errors"
)
//
type BundleStore interface {
Store(ref reference.Named, bndle *bundle.Bundle) error
Read(ref reference.Named) (*bundle.Bundle, error)
LookupOrPullBundle(ref reference.Named, pullRef bool, config *configfile.ConfigFile, insecureRegistries []string) (*bundle.Bundle, error)
}
var _ BundleStore = &bundleStore{}
type bundleStore struct {
path string
}
func (b *bundleStore) Store(ref reference.Named, bndle *bundle.Bundle) error {
path, err := b.storePath(ref)
if err != nil {
return errors.Wrapf(err, "failed to store bundle %q", ref)
}
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
return errors.Wrapf(err, "failed to store bundle %q", ref)
}
err = bndle.WriteFile(path, 0644)
return errors.Wrapf(err, "failed to store bundle %q", ref)
}
func (b *bundleStore) Read(ref reference.Named) (*bundle.Bundle, error) {
path, err := b.storePath(ref)
if err != nil {
return nil, errors.Wrapf(err, "failed to read bundle %q", ref)
}
data, err := ioutil.ReadFile(path)
if err != nil {
return nil, errors.Wrapf(err, "failed to read bundle %q", ref)
}
var bndle bundle.Bundle
if err := json.Unmarshal(data, &bndle); err != nil {
return nil, errors.Wrapf(err, "failed to read bundle %q", ref)
}
return &bndle, nil
}
// LookupOrPullBundle will fetch the given bundle from the local
// bundle store, or if it is missing from the registry, and returns
// it. Always pulls if pullRef is true. If it pulls then the local
// bundle store is updated.
func (b *bundleStore) LookupOrPullBundle(ref reference.Named, pullRef bool, config *configfile.ConfigFile, insecureRegistries []string) (*bundle.Bundle, error) {
if !pullRef {
bndl, err := b.Read(ref)
if err == nil {
return bndl, nil
}
if !os.IsNotExist(errors.Cause(err)) {
return nil, err
}
}
bndl, err := remotes.Pull(log.WithLogContext(context.Background()), reference.TagNameOnly(ref), remotes.CreateResolver(config, insecureRegistries...))
if err != nil {
return nil, errors.Wrap(err, ref.String())
}
if err := b.Store(ref, bndl); err != nil {
return nil, err
}
return bndl, nil
}
func (b *bundleStore) storePath(ref reference.Named) (string, error) {
name := ref.Name()
// A name is safe for use as a filesystem path (it is
// alphanumerics + "." + "/") except for the ":" used to
// separate domain from port which is not safe on Windows.
// Replace it with "_" which is not valid in the name.
//
// There can be at most 1 ":" in a valid reference so only
// replace one -- if there are more (and this wasn't caught
// when parsing the ref) then there will be errors when we try
// to use this as a path later.
name = strings.Replace(name, ":", "_", 1)
storeDir := filepath.Join(b.path, filepath.FromSlash(name))
// We rely here on _ not being valid in a name meaning there can be no clashes due to nesting of repositories.
switch t := ref.(type) {
case reference.Digested:
digest := t.Digest()
storeDir = filepath.Join(storeDir, "_digests", digest.Algorithm().String(), digest.Encoded())
case reference.Tagged:
storeDir = filepath.Join(storeDir, "_tags", t.Tag())
default:
return "", errors.Errorf("%s: not tagged or digested", ref.String())
}
return storeDir + ".json", nil
}
package store
import (
"encoding/json"
"io/ioutil"
"path/filepath"
"github.com/deislabs/cnab-go/credentials"
"github.com/pkg/errors"
)
// CredentialStore persists credential sets to a specific path.
type CredentialStore interface {
Store(creds *credentials.CredentialSet) error
Read(credentialSetName string) (*credentials.CredentialSet, error)
}
var _ CredentialStore = &credentialStore{}
type credentialStore struct {
path string
}
func (c *credentialStore) Read(credentialSetName string) (*credentials.CredentialSet, error) {
path := filepath.Join(c.path, credentialSetName+".yaml")
return credentials.Load(path)
}
func (c *credentialStore) Store(creds *credentials.CredentialSet) error {
if creds.Name == "" {
return errors.New("failed to store credential set, name is empty")
}
data, err := json.MarshalIndent(creds, "", " ")
if err != nil {
return errors.Wrapf(err, "failed to store credential set %q", creds.Name)
}
err = ioutil.WriteFile(filepath.Join(c.path, creds.Name+".yaml"), data, 0644)
return errors.Wrapf(err, "failed to store credential set %q", creds.Name)
}
package store
import (
"encoding/json"
"fmt"
"github.com/deislabs/cnab-go/claim"
"github.com/deislabs/cnab-go/utils/crud"
)
// InstallationStore is an interface to persist, delete, list and read installations.
type InstallationStore interface {
List() ([]string, error)
Store(installation *Installation) error
Read(installationName string) (*Installation, error)
Delete(installationName string) error
}
// Installation is a CNAB claim with an information of where the bundle comes from.
// It persists the result of an installation and its parameters and context.
type Installation struct {
claim.Claim
Reference string `json:"reference,omitempty"`
}
func NewInstallation(name string, reference string) (*Installation, error) {
c, err := claim.New(name)
if err != nil {
return nil, err
}
return &Installation{
Claim: *c,
Reference: reference,
}, nil
}
var _ InstallationStore = &installationStore{}
type installationStore struct {
store crud.Store
}
func (i installationStore) List() ([]string, error) {
return i.store.List()
}
func (i installationStore) Store(installation *Installation) error {
data, err := json.MarshalIndent(installation, "", " ")
if err != nil {
return err
}
return i.store.Store(installation.Name, data)
}
func (i installationStore) Read(installationName string) (*Installation, error) {
data, err := i.store.Read(installationName)
if err != nil {
if err == crud.ErrRecordDoesNotExist {
return nil, fmt.Errorf("Installation %q not found", installationName)
}
return nil, err
}
var installation Installation
if err := json.Unmarshal(data, &installation); err != nil {
return nil, err
}
return &installation, nil
}
func (i installationStore) Delete(installationName string) error {
return i.store.Delete(installationName)
}
package internal
import (
"fmt"
"runtime"
"strings"
"time"
)
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(invocationBaseImage string) 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("Invocation Base Image: %s", invocationBaseImage),
}
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 (
"bytes"
"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 (
crlf = []byte{'\r', '\n'}
lf = []byte{'\n'}
delimiters = [][]byte{
[]byte("\r\n---\r\n"),
[]byte("\n---\r\n"),
[]byte("\r\n---\n"),
[]byte("\n---\n"),
}
)
// useCRLF detects which line break should be used
func useCRLF(data []byte) bool {
nbCrlf := bytes.Count(data, crlf)
nbLf := bytes.Count(data, lf)
switch {
case nbCrlf == nbLf:
// document contains only CRLF
return true
case nbCrlf == 0:
// document does not contain any CRLF
return false
default:
// document contains mixed line breaks, so use the OS default
return bytes.Equal(defaultLineBreak, crlf)
}
}
// splitSingleFile split a multidocument using all possible document delimiters
func splitSingleFile(data []byte) [][]byte {
parts := [][]byte{data}
for _, delimiter := range delimiters {
var intermediate [][]byte
for _, part := range parts {
intermediate = append(intermediate, bytes.Split(part, delimiter)...)
}
parts = intermediate
}
return parts
}
// 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 := splitSingleFile(data)
if len(parts) != 3 {
return nil, errors.Errorf("malformed single-file application: expected 3 documents, got %d", len(parts))
}
// 0. is metadata
metadata := bytes.NewBuffer(parts[0])
// 1. is compose
compose := bytes.NewBuffer(parts[1])
// 2. is parameters
params := bytes.NewBuffer(parts[2])
appOps := append([]func(*types.App) error{
types.WithComposes(compose),
types.WithParameters(params),
types.Metadata(metadata),
types.WithCRLF(useCRLF(data)),
}, 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 render
import (
"strings"
"github.com/deislabs/cnab-go/bundle"
"github.com/docker/app/internal/compose"
"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 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")
}
configFiles, _, err := compose.Load(app.Composes())
if err != nil {
return nil, errors.Wrap(err, "failed to load composefiles")
}
return render(app.Path, configFiles, allParameters.Flatten(), imageMap)
}
func render(appPath string, configFiles []composetypes.ConfigFile, finalEnv map[string]string, imageMap map[string]bundle.Image) (*composetypes.Config, error) {
rendered, err := loader.Load(composetypes.ConfigDetails{
WorkingDir: appPath,
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"
"github.com/deislabs/cnab-go/bundle"
)
// 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"`
}
// Metadata extracts the docker-app metadata from the bundle
func FromBundle(bndl *bundle.Bundle) AppMetadata {
meta := AppMetadata{
Name: bndl.Name,
Version: bndl.Version,
Description: bndl.Description,
}
for _, m := range bndl.Maintainers {
meta.Maintainers = append(meta.Maintainers, Maintainer{
Name: m.Name,
Email: m.Email,
})
}
return meta
}
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
}
params := converted.(map[string]interface{})
if options.prefix != "" {
params = map[string]interface{}{
options.prefix: params,
}
}
// Make sure params are always loaded expanded
expandedParams, err := FromFlatten(flatten(params))
if err != nil {
return nil, err
}
return expandedParams, 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.WithTypeCheck); 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 (
"bytes"
"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"
)
// 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
)
// YamlSingleFileSeparator returns the separator used in single-file app, depending detected CRLF
func YamlSingleFileSeparator(hasCRLF bool) []byte {
if hasCRLF {
return []byte("\r\n---\r\n")
}
return []byte("\n---\n")
}
// 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
hasCRLF bool
}
// 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
}
func (a *App) HasCRLF() bool {
return a.hasCRLF
}
// 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
}
}
func WithCRLF(hasCRLF bool) func(*App) error {
return func(app *App) error {
app.hasCRLF = hasCRLF
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
app.hasCRLF = bytes.Contains(d, []byte{'\r', '\n'})
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"))
}