mirror of https://github.com/docker/cli.git
Add branch/tag name to the app path (URL fragment) for multiple version support
Support force install if app has already been installed Add default args/envs Support VERSION build arg Signed-off-by: Qiang Li <liqiang@gmail.com>
This commit is contained in:
parent
01db39ab1b
commit
a696827ba7
|
@ -2,16 +2,20 @@ package app
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"os/user"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/docker/cli/cli"
|
"github.com/docker/cli/cli"
|
||||||
"github.com/docker/cli/cli/command"
|
"github.com/docker/cli/cli/command"
|
||||||
"github.com/docker/cli/cli/command/container"
|
"github.com/docker/cli/cli/command/container"
|
||||||
"github.com/docker/cli/cli/command/image"
|
"github.com/docker/cli/cli/command/image"
|
||||||
|
"github.com/docker/docker/errdefs"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/pflag"
|
"github.com/spf13/pflag"
|
||||||
)
|
)
|
||||||
|
@ -58,6 +62,8 @@ func addInstallFlags(flags *pflag.FlagSet, dest string, trust bool) *AppOptions
|
||||||
flags.StringVar(&options.destination, "destination", dest, "Set local host path for app")
|
flags.StringVar(&options.destination, "destination", dest, "Set local host path for app")
|
||||||
flags.BoolVar(&options.launch, "launch", false, "Start app after installation")
|
flags.BoolVar(&options.launch, "launch", false, "Start app after installation")
|
||||||
flags.BoolVarP(&options.detach, "detach", "d", false, "Do not wait for app to finish")
|
flags.BoolVarP(&options.detach, "detach", "d", false, "Do not wait for app to finish")
|
||||||
|
flags.BoolVar(&options.force, "force", false, "Force install even if the app exists")
|
||||||
|
flags.StringVar(&options.name, "name", "", "App name")
|
||||||
|
|
||||||
// build/run flags
|
// build/run flags
|
||||||
flags.StringVar(&options.imageIDFile, "iidfile", imageIDFile, "Write the image ID to the file")
|
flags.StringVar(&options.imageIDFile, "iidfile", imageIDFile, "Write the image ID to the file")
|
||||||
|
@ -109,18 +115,45 @@ func addInstallFlags(flags *pflag.FlagSet, dest string, trust bool) *AppOptions
|
||||||
// `docker cp` flags
|
// `docker cp` flags
|
||||||
include(flags, eflags, []string{"archive", "follow-link"})
|
include(flags, eflags, []string{"archive", "follow-link"})
|
||||||
|
|
||||||
// install specific build args
|
|
||||||
addBuildArgs(flags)
|
|
||||||
|
|
||||||
return options
|
return options
|
||||||
}
|
}
|
||||||
|
|
||||||
func addBuildArgs(flags *pflag.FlagSet) {
|
func setBuildArgs(options *AppOptions) error {
|
||||||
flag := flags.Lookup("build-arg")
|
bopts := options.buildOpts
|
||||||
if flag != nil {
|
if bopts == nil {
|
||||||
flag.Value.Set("HOSTOS=" + runtime.GOOS)
|
return errors.New("build options not set")
|
||||||
flag.Value.Set("HOSTARCH=" + runtime.GOARCH)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
set := func(n, v string) {
|
||||||
|
bopts.SetBuildArg(n + "=" + v)
|
||||||
|
}
|
||||||
|
|
||||||
|
set("DOCKER_APP_BASE", options._appBase)
|
||||||
|
appPath, err := options.appPath()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
set("DOCKER_APP_PATH", appPath)
|
||||||
|
|
||||||
|
set("HOSTOS", runtime.GOOS)
|
||||||
|
set("HOSTARCH", runtime.GOARCH)
|
||||||
|
|
||||||
|
version := options.buildVersion()
|
||||||
|
if version != "" {
|
||||||
|
set("VERSION", version)
|
||||||
|
}
|
||||||
|
|
||||||
|
// user info
|
||||||
|
u, err := user.Current()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
set("USERNAME", u.Username)
|
||||||
|
set("USERHOME", u.HomeDir)
|
||||||
|
set("USERID", u.Uid)
|
||||||
|
set("USERGID", u.Gid)
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func installApp(ctx context.Context, adapter cliAdapter, flags *pflag.FlagSet, options *AppOptions) error {
|
func installApp(ctx context.Context, adapter cliAdapter, flags *pflag.FlagSet, options *AppOptions) error {
|
||||||
|
@ -133,7 +166,7 @@ func installApp(ctx context.Context, adapter cliAdapter, flags *pflag.FlagSet, o
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
bin, err := runPostInstall(adapter, dir, options)
|
bin, err := runPostInstall(ctx, adapter, dir, options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -146,8 +179,25 @@ func installApp(ctx context.Context, adapter cliAdapter, flags *pflag.FlagSet, o
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func setDefaultEnv() {
|
||||||
|
if os.Getenv("DOCKER_BUILDKIT") == "" {
|
||||||
|
os.Setenv("DOCKER_BUILDKIT", "1")
|
||||||
|
}
|
||||||
|
|
||||||
|
platform := fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH)
|
||||||
|
if os.Getenv("DOCKER_DEFAULT_PLATFORM") == "" {
|
||||||
|
os.Setenv("DOCKER_DEFAULT_PLATFORM", platform)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// runInstall calls the build, run, and cp commands
|
// runInstall calls the build, run, and cp commands
|
||||||
func runInstall(ctx context.Context, dockerCli cliAdapter, flags *pflag.FlagSet, options *AppOptions) (string, error) {
|
func runInstall(ctx context.Context, dockerCli cliAdapter, flags *pflag.FlagSet, options *AppOptions) (string, error) {
|
||||||
|
setDefaultEnv()
|
||||||
|
|
||||||
|
if err := setBuildArgs(options); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
iid, err := buildImage(ctx, dockerCli, options)
|
iid, err := buildImage(ctx, dockerCli, options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
|
@ -218,22 +268,51 @@ func copyFiles(ctx context.Context, dockerCli cliAdapter, cid string, options *A
|
||||||
return filepath.Join(dir, filepath.Base(options.egress)), nil
|
return filepath.Join(dir, filepath.Base(options.egress)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func runPostInstall(dockerCli cliAdapter, dir string, options *AppOptions) (string, error) {
|
const appExistWarn = `WARNING! This will replace the existing app.
|
||||||
|
Are you sure you want to continue?`
|
||||||
|
|
||||||
|
func runPostInstall(ctx context.Context, dockerCli cliAdapter, dir string, options *AppOptions) (string, error) {
|
||||||
if !options.isDockerAppBase() {
|
if !options.isDockerAppBase() {
|
||||||
return "", installCustom(dir, options.destination, options)
|
return "", installCustom(dir, options.destination, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
// for the default destination
|
|
||||||
|
|
||||||
// if there is only one file, create symlink for the file
|
|
||||||
if fp, err := oneChild(dir); err == nil && fp != "" {
|
|
||||||
binPath := options.binPath()
|
binPath := options.binPath()
|
||||||
|
if err := os.MkdirAll(binPath, 0o755); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
appPath, err := options.appPath()
|
appPath, err := options.appPath()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
link, err := installOne(dir, fp, binPath, appPath)
|
if fileExist(appPath) {
|
||||||
|
if !options.force {
|
||||||
|
r, err := command.PromptForConfirmation(ctx, dockerCli.In(), dockerCli.Out(), appExistWarn)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if !r {
|
||||||
|
return "", errdefs.Cancelled(errors.New("app install has been canceled"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := removeApp(dockerCli, binPath, appPath, options); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// for the default destination
|
||||||
|
// if there is only one file, create symlink for the file
|
||||||
|
if fp, err := oneChild(dir); err == nil && fp != "" {
|
||||||
|
appName := options.name
|
||||||
|
if appName == "" {
|
||||||
|
appName = makeAppName(fp)
|
||||||
|
}
|
||||||
|
if err := validateName(appName); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
link, err := installOne(appName, dir, fp, binPath, appPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
@ -243,13 +322,15 @@ func runPostInstall(dockerCli cliAdapter, dir string, options *AppOptions) (stri
|
||||||
|
|
||||||
// if there is a run file, create symlink for the run file.
|
// if there is a run file, create symlink for the run file.
|
||||||
if fp, err := locateFile(dir, runnerName); err == nil && fp != "" {
|
if fp, err := locateFile(dir, runnerName); err == nil && fp != "" {
|
||||||
binPath := options.binPath()
|
appName := options.name
|
||||||
appPath, err := options.appPath()
|
if appName == "" {
|
||||||
if err != nil {
|
appName = makeAppName(appPath)
|
||||||
|
}
|
||||||
|
if err := validateName(appName); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
link, err := installRunFile(dir, fp, binPath, appPath)
|
link, err := installRunFile(appName, dir, fp, binPath, appPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
@ -258,10 +339,6 @@ func runPostInstall(dockerCli cliAdapter, dir string, options *AppOptions) (stri
|
||||||
}
|
}
|
||||||
|
|
||||||
// custom install
|
// custom install
|
||||||
appPath, err := options.appPath()
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
if err := installCustom(dir, appPath, options); err != nil {
|
if err := installCustom(dir, appPath, options); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
@ -270,38 +347,39 @@ func runPostInstall(dockerCli cliAdapter, dir string, options *AppOptions) (stri
|
||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// installOne creates a symlink to the only file in appPath
|
// removeApp removes the existing app
|
||||||
func installOne(egress, runPath, binPath, appPath string) (string, error) {
|
func removeApp(dockerCli cliAdapter, binPath, appPath string, options *AppOptions) error {
|
||||||
appName := filepath.Base(runPath)
|
envs, _ := options.makeEnvs()
|
||||||
if err := validateName(appName); err != nil {
|
runUninstaller(dockerCli, appPath, envs)
|
||||||
return "", err
|
|
||||||
|
if err := os.RemoveAll(appPath); err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
targets, err := findSymlinks(binPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
cleanupSymlink(dockerCli, appPath, targets)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// installOne creates a symlink to the only file in appPath
|
||||||
|
func installOne(appName, egress, runPath, binPath, appPath string) (string, error) {
|
||||||
link := filepath.Join(binPath, appName)
|
link := filepath.Join(binPath, appName)
|
||||||
target := filepath.Join(appPath, appName)
|
target := filepath.Join(appPath, appName)
|
||||||
return install(link, target, egress, binPath, appPath)
|
return install(link, target, egress, appPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
// installRunFile creates a symlink to the run file in appPath
|
// installRunFile creates a symlink to the run file in appPath
|
||||||
// use the base name as the app name
|
func installRunFile(appName, egress, runPath, binPath, appPath string) (string, error) {
|
||||||
func installRunFile(egress, runPath, binPath, appPath string) (string, error) {
|
|
||||||
appName := filepath.Base(appPath)
|
|
||||||
if err := validateName(appName); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
link := filepath.Join(binPath, appName)
|
link := filepath.Join(binPath, appName)
|
||||||
target := filepath.Join(appPath, filepath.Base(runPath))
|
target := filepath.Join(appPath, filepath.Base(runPath))
|
||||||
return install(link, target, egress, binPath, appPath)
|
return install(link, target, egress, appPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
// instal creates a symlink to the target file
|
// instal creates a symlink to the target file
|
||||||
func install(link, target, egress, binPath, appPath string) (string, error) {
|
func install(link, target, egress, appPath string) (string, error) {
|
||||||
if _, err := os.Stat(appPath); err == nil {
|
if ok, err := isSymlinkOK(link, target); err != nil {
|
||||||
return "", fmt.Errorf("app package exists: %s! Try again after removing it", appPath)
|
|
||||||
} else if !os.IsNotExist(err) {
|
|
||||||
return "", fmt.Errorf("installation failed: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if ok, err := isSymlinkToOK(link, target); err != nil {
|
|
||||||
return "", err
|
return "", err
|
||||||
} else {
|
} else {
|
||||||
if !ok {
|
if !ok {
|
||||||
|
@ -314,10 +392,6 @@ func install(link, target, egress, binPath, appPath string) (string, error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := os.MkdirAll(binPath, 0o755); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := os.MkdirAll(filepath.Dir(appPath), 0o755); err != nil {
|
if err := os.MkdirAll(filepath.Dir(appPath), 0o755); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
@ -337,18 +411,6 @@ func install(link, target, egress, binPath, appPath string) (string, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func installCustom(dir string, appPath string, options *AppOptions) error {
|
func installCustom(dir string, appPath string, options *AppOptions) error {
|
||||||
exist := func(p string) bool {
|
|
||||||
_, err := os.Stat(p)
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return err == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if exist(appPath) {
|
|
||||||
return fmt.Errorf("destination exists: %s", appPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := os.MkdirAll(filepath.Dir(appPath), 0o755); err != nil {
|
if err := os.MkdirAll(filepath.Dir(appPath), 0o755); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -358,7 +420,7 @@ func installCustom(dir string, appPath string, options *AppOptions) error {
|
||||||
|
|
||||||
// optionally run the installer if it exists
|
// optionally run the installer if it exists
|
||||||
installer := filepath.Join(appPath, installerName)
|
installer := filepath.Join(appPath, installerName)
|
||||||
if !exist(installer) {
|
if !fileExist(installer) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if err := os.Chmod(installer, 0o755); err != nil {
|
if err := os.Chmod(installer, 0o755); err != nil {
|
||||||
|
@ -367,3 +429,17 @@ func installCustom(dir string, appPath string, options *AppOptions) error {
|
||||||
|
|
||||||
return launch(installer, options)
|
return launch(installer, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func fileExist(path string) bool {
|
||||||
|
_, err := os.Stat(path)
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// makeAppName derives the app name from the base name of the path
|
||||||
|
// after removing the version and extension
|
||||||
|
func makeAppName(path string) string {
|
||||||
|
n := filepath.Base(path)
|
||||||
|
n = strings.SplitN(n, "@", 2)[0]
|
||||||
|
n = strings.SplitN(n, ".", 2)[0]
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
|
@ -6,7 +6,6 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/docker/cli/cli"
|
"github.com/docker/cli/cli"
|
||||||
|
@ -64,7 +63,11 @@ func runLaunch(dir string, options *AppOptions) error {
|
||||||
if fp, err := oneChild(dir); err == nil && fp != "" {
|
if fp, err := oneChild(dir); err == nil && fp != "" {
|
||||||
return fp, nil
|
return fp, nil
|
||||||
}
|
}
|
||||||
if fp, err := locateFile(dir, runnerName); err == nil && fp != "" {
|
appName := options.name
|
||||||
|
if appName == "" {
|
||||||
|
appName = runnerName
|
||||||
|
}
|
||||||
|
if fp, err := locateFile(dir, appName); err == nil && fp != "" {
|
||||||
return fp, nil
|
return fp, nil
|
||||||
}
|
}
|
||||||
return "", errors.New("no app file found")
|
return "", errors.New("no app file found")
|
||||||
|
@ -80,15 +83,9 @@ func runLaunch(dir string, options *AppOptions) error {
|
||||||
|
|
||||||
// launch copies the current environment and set DOCKER_APP_BASE before spawning the app
|
// launch copies the current environment and set DOCKER_APP_BASE before spawning the app
|
||||||
func launch(app string, options *AppOptions) error {
|
func launch(app string, options *AppOptions) error {
|
||||||
envs := make(map[string]string)
|
envs, err := options.makeEnvs()
|
||||||
|
if err != nil {
|
||||||
// copy the current environment
|
return err
|
||||||
for _, v := range os.Environ() {
|
|
||||||
kv := strings.SplitN(v, "=", 2)
|
|
||||||
envs[kv[0]] = kv[1]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
envs["DOCKER_APP_BASE"] = options._appBase
|
|
||||||
|
|
||||||
return spawn(app, options.launchArgs(), envs, options.detach)
|
return spawn(app, options.launchArgs(), envs, options.detach)
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,8 +4,10 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"os/user"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"runtime"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
@ -14,7 +16,7 @@ import (
|
||||||
"github.com/docker/cli/cli/command/image"
|
"github.com/docker/cli/cli/command/image"
|
||||||
)
|
)
|
||||||
|
|
||||||
// runnerName is the executable name for starting the app
|
// runnerName is the default executable app name for starting the app
|
||||||
const runnerName = "run"
|
const runnerName = "run"
|
||||||
|
|
||||||
// installerName is the executable name for custom installation
|
// installerName is the executable name for custom installation
|
||||||
|
@ -23,7 +25,7 @@ const installerName = "install"
|
||||||
// uninstallerName is the executable name for custom installation
|
// uninstallerName is the executable name for custom installation
|
||||||
const uninstallerName = "uninstall"
|
const uninstallerName = "uninstall"
|
||||||
|
|
||||||
// namePattern is for validating egress and app name
|
// namePattern is for validating app name
|
||||||
const namePattern = "^[a-zA-Z0-9][a-zA-Z0-9_.+-]+$"
|
const namePattern = "^[a-zA-Z0-9][a-zA-Z0-9_.+-]+$"
|
||||||
|
|
||||||
var nameRegexp = regexp.MustCompile(namePattern)
|
var nameRegexp = regexp.MustCompile(namePattern)
|
||||||
|
@ -35,12 +37,43 @@ func validateName(s string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// semverPattern is for splitting semver from a context path/URL
|
||||||
|
const semverPattern = `@v?\d+(\.\d+)?(\.\d+)?$`
|
||||||
|
|
||||||
|
var semverRegexp = regexp.MustCompile(semverPattern)
|
||||||
|
|
||||||
|
func splitSemver(s string) (string, string) {
|
||||||
|
if semverRegexp.MatchString(s) {
|
||||||
|
idx := strings.LastIndex(s, "@")
|
||||||
|
// unlikely otherwise ignore
|
||||||
|
if idx == -1 {
|
||||||
|
return s, ""
|
||||||
|
}
|
||||||
|
v := s[idx+1:]
|
||||||
|
if v[0] == 'v' {
|
||||||
|
v = v[1:]
|
||||||
|
}
|
||||||
|
return s[:idx], v
|
||||||
|
}
|
||||||
|
return s, ""
|
||||||
|
}
|
||||||
|
|
||||||
// defaultAppBase is docker app's base location specified by
|
// defaultAppBase is docker app's base location specified by
|
||||||
// DOCKER_APP_BASE environment variable defaulted to ~/.docker-app/
|
// DOCKER_APP_BASE environment variable defaulted to ~/.docker/app/
|
||||||
func defaultAppBase() string {
|
func defaultAppBase() string {
|
||||||
if base := os.Getenv("DOCKER_APP_BASE"); base != "" {
|
if base := os.Getenv("DOCKER_APP_BASE"); base != "" {
|
||||||
return filepath.Clean(base)
|
return filepath.Clean(base)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// locate .docker/app starting from the current working directory
|
||||||
|
// for supporting apps on a per project basis
|
||||||
|
wd, err := os.Getwd()
|
||||||
|
if err == nil {
|
||||||
|
if dir, err := locateDir(wd, ".docker"); err == nil {
|
||||||
|
return filepath.Join(dir, "app")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// default ~/.docker/app
|
// default ~/.docker/app
|
||||||
// ignore error and use the current working directory
|
// ignore error and use the current working directory
|
||||||
// if home directory is not available
|
// if home directory is not available
|
||||||
|
@ -48,8 +81,108 @@ func defaultAppBase() string {
|
||||||
return filepath.Join(home, ".docker", "app")
|
return filepath.Join(home, ".docker", "app")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type commonOptions struct {
|
||||||
|
// command line args
|
||||||
|
_args []string
|
||||||
|
|
||||||
|
// docker app base location, fixed once set
|
||||||
|
_appBase string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *commonOptions) setArgs(args []string) {
|
||||||
|
o._args = args
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildContext returns the build context for building image
|
||||||
|
func (o *commonOptions) buildContext() string {
|
||||||
|
if len(o._args) == 0 {
|
||||||
|
return "."
|
||||||
|
}
|
||||||
|
c, _ := splitSemver(o._args[0])
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *commonOptions) buildVersion() string {
|
||||||
|
if len(o._args) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
_, v := splitSemver(o._args[0])
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
// appPath returns the app directory under the default app base
|
||||||
|
func (o *commonOptions) appPath() (string, error) {
|
||||||
|
if len(o._args) == 0 {
|
||||||
|
return "", errors.New("missing args")
|
||||||
|
}
|
||||||
|
return o.makeAppPath(o._args[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
// binPath returns the bin directory under the default app base
|
||||||
|
func (o *commonOptions) binPath() string {
|
||||||
|
return filepath.Join(o._appBase, "bin")
|
||||||
|
}
|
||||||
|
|
||||||
|
// pkgPath returns the pkg directory under the default app base
|
||||||
|
func (o *commonOptions) pkgPath() string {
|
||||||
|
return filepath.Join(o._appBase, "pkg")
|
||||||
|
}
|
||||||
|
|
||||||
|
// makeAppPath builds the default app path
|
||||||
|
// in the format: appBase/pkg/scheme/host/path
|
||||||
|
func (o *commonOptions) makeAppPath(s string) (string, error) {
|
||||||
|
u, err := parseURL(s)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if u.Path == "" {
|
||||||
|
return "", fmt.Errorf("missing path: %v", u)
|
||||||
|
}
|
||||||
|
p := filepath.Join(o._appBase, "pkg", u.Scheme, u.Host, shortenPath(u.Path))
|
||||||
|
if u.Fragment == "" {
|
||||||
|
return p, nil
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s#%s", p, u.Fragment), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *commonOptions) makeEnvs() (map[string]string, error) {
|
||||||
|
envs := make(map[string]string)
|
||||||
|
|
||||||
|
// copy the current environment
|
||||||
|
for _, v := range os.Environ() {
|
||||||
|
kv := strings.SplitN(v, "=", 2)
|
||||||
|
envs[kv[0]] = kv[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
envs["DOCKER_APP_BASE"] = o._appBase
|
||||||
|
appPath, err := o.appPath()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
envs["DOCKER_APP_PATH"] = appPath
|
||||||
|
|
||||||
|
envs["VERSION"] = o.buildVersion()
|
||||||
|
|
||||||
|
envs["HOSTOS"] = runtime.GOOS
|
||||||
|
envs["HOSTARCH"] = runtime.GOARCH
|
||||||
|
|
||||||
|
// user info
|
||||||
|
u, err := user.Current()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
envs["USERNAME"] = u.Username
|
||||||
|
envs["USERHOME"] = u.HomeDir
|
||||||
|
envs["USERID"] = u.Uid
|
||||||
|
envs["USERGID"] = u.Gid
|
||||||
|
|
||||||
|
return envs, nil
|
||||||
|
}
|
||||||
|
|
||||||
// AppOptions holds the options for the `app` subcommands
|
// AppOptions holds the options for the `app` subcommands
|
||||||
type AppOptions struct {
|
type AppOptions struct {
|
||||||
|
commonOptions
|
||||||
|
|
||||||
// flags for install
|
// flags for install
|
||||||
|
|
||||||
// path on local host
|
// path on local host
|
||||||
|
@ -64,6 +197,12 @@ type AppOptions struct {
|
||||||
// start exported app
|
// start exported app
|
||||||
launch bool
|
launch bool
|
||||||
|
|
||||||
|
// overwrite existing app
|
||||||
|
force bool
|
||||||
|
|
||||||
|
// app name
|
||||||
|
name string
|
||||||
|
|
||||||
// the following are existing flags
|
// the following are existing flags
|
||||||
|
|
||||||
// build flags
|
// build flags
|
||||||
|
@ -82,25 +221,6 @@ type AppOptions struct {
|
||||||
runOpts *container.RunOptions
|
runOpts *container.RunOptions
|
||||||
containerOpts *container.ContainerOptions
|
containerOpts *container.ContainerOptions
|
||||||
copyOpts *container.CopyOptions
|
copyOpts *container.CopyOptions
|
||||||
|
|
||||||
// runtime
|
|
||||||
// command line args
|
|
||||||
_args []string
|
|
||||||
|
|
||||||
// docker app base location, fixed once set
|
|
||||||
_appBase string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *AppOptions) setArgs(args []string) {
|
|
||||||
o._args = args
|
|
||||||
}
|
|
||||||
|
|
||||||
// buildContext returns the build context for building image
|
|
||||||
func (o *AppOptions) buildContext() string {
|
|
||||||
if len(o._args) == 0 {
|
|
||||||
return "."
|
|
||||||
}
|
|
||||||
return o._args[0]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// runArgs returns the command line args for running the container
|
// runArgs returns the command line args for running the container
|
||||||
|
@ -128,19 +248,6 @@ func (o *AppOptions) isDockerAppBase() bool {
|
||||||
return strings.HasPrefix(s, o._appBase)
|
return strings.HasPrefix(s, o._appBase)
|
||||||
}
|
}
|
||||||
|
|
||||||
// binPath returns the bin directory under the default app base
|
|
||||||
func (o *AppOptions) binPath() string {
|
|
||||||
return filepath.Join(o._appBase, "bin")
|
|
||||||
}
|
|
||||||
|
|
||||||
// appPath returns the app directory under the default app base
|
|
||||||
func (o *AppOptions) appPath() (string, error) {
|
|
||||||
if len(o._args) == 0 {
|
|
||||||
return "", errors.New("missing args")
|
|
||||||
}
|
|
||||||
return makeAppPath(o._appBase, o._args[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
// cacheDir returns a temp cache directory under the default app base
|
// cacheDir returns a temp cache directory under the default app base
|
||||||
// appBase is chosen as the parent directory to avoid issues such as:
|
// appBase is chosen as the parent directory to avoid issues such as:
|
||||||
// permission, disk space, renaming across partitions.
|
// permission, disk space, renaming across partitions.
|
||||||
|
@ -170,7 +277,9 @@ func (o *AppOptions) containerID() (string, error) {
|
||||||
|
|
||||||
func newAppOptions() *AppOptions {
|
func newAppOptions() *AppOptions {
|
||||||
return &AppOptions{
|
return &AppOptions{
|
||||||
|
commonOptions: commonOptions{
|
||||||
_appBase: defaultAppBase(),
|
_appBase: defaultAppBase(),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -182,53 +291,17 @@ func validateAppOptions(options *AppOptions) error {
|
||||||
return errors.New("egress is required")
|
return errors.New("egress is required")
|
||||||
}
|
}
|
||||||
|
|
||||||
name := filepath.Base(options.egress)
|
|
||||||
if err := validateName(name); err != nil {
|
|
||||||
return fmt.Errorf("invalid egress path: %s %v", options.egress, err)
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type removeOptions struct {
|
type removeOptions struct {
|
||||||
_appBase string
|
commonOptions
|
||||||
}
|
|
||||||
|
|
||||||
// makeAppPath returns the app directory under the default app base
|
|
||||||
// appBase/pkg/scheme/host/path
|
|
||||||
func (o *removeOptions) makeAppPath(s string) (string, error) {
|
|
||||||
return makeAppPath(o._appBase, s)
|
|
||||||
}
|
|
||||||
|
|
||||||
// binPath returns the bin directory under the default app base
|
|
||||||
func (o *removeOptions) binPath() string {
|
|
||||||
return filepath.Join(o._appBase, "bin")
|
|
||||||
}
|
|
||||||
|
|
||||||
// pkgPath returns the pkg directory under the default app base
|
|
||||||
func (o *removeOptions) pkgPath() string {
|
|
||||||
return filepath.Join(o._appBase, "pkg")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func newRemoveOptions() *removeOptions {
|
func newRemoveOptions() *removeOptions {
|
||||||
return &removeOptions{
|
return &removeOptions{
|
||||||
|
commonOptions: commonOptions{
|
||||||
_appBase: defaultAppBase(),
|
_appBase: defaultAppBase(),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// makeAppPath builds the default app path
|
|
||||||
// in the format: appBase/pkg/scheme/host/path
|
|
||||||
func makeAppPath(appBase, s string) (string, error) {
|
|
||||||
u, err := parseURL(s)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
if u.Path == "" {
|
|
||||||
return "", fmt.Errorf("missing path: %v", u)
|
|
||||||
}
|
|
||||||
|
|
||||||
name := filepath.Base(u.Path)
|
|
||||||
if err := validateName(name); err != nil {
|
|
||||||
return "", fmt.Errorf("invalid path: %s %v", u.Path, err)
|
|
||||||
}
|
|
||||||
return filepath.Join(appBase, "pkg", u.Scheme, u.Host, u.Path), nil
|
|
||||||
}
|
|
||||||
|
|
|
@ -39,35 +39,24 @@ func NewRemoveCommand(dockerCli command.Cli) *cobra.Command {
|
||||||
// remove symlinks of the app under bin path
|
// remove symlinks of the app under bin path
|
||||||
func runRemove(dockerCli command.Cli, apps []string, options *removeOptions) error {
|
func runRemove(dockerCli command.Cli, apps []string, options *removeOptions) error {
|
||||||
binPath := options.binPath()
|
binPath := options.binPath()
|
||||||
|
targets, err := findSymlinks(binPath)
|
||||||
// find all symlinks in binPath to remove later
|
if err != nil {
|
||||||
// if they point to the app package
|
return err
|
||||||
targets := make(map[string]string)
|
|
||||||
if links, err := findLinks(binPath); err == nil {
|
|
||||||
for _, link := range links {
|
|
||||||
if target, err := os.Readlink(link); err == nil {
|
|
||||||
targets[target] = link
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var failed []string
|
var failed []string
|
||||||
|
|
||||||
for _, app := range apps {
|
for _, app := range apps {
|
||||||
appPath, err := options.makeAppPath(app)
|
options.setArgs([]string{app})
|
||||||
|
appPath, err := options.appPath()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
failed = append(failed, app)
|
failed = append(failed, app)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// optionally run uninstall if provided
|
// optionally run uninstall if provided
|
||||||
uninstaller := filepath.Join(appPath, uninstallerName)
|
envs, _ := options.makeEnvs()
|
||||||
if _, err := os.Stat(uninstaller); err == nil {
|
runUninstaller(dockerCli, appPath, envs)
|
||||||
err := spawn(uninstaller, nil, nil, false)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(dockerCli.Err(), "%s failed to run: %v\n", uninstaller, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// remove all files under the app path
|
// remove all files under the app path
|
||||||
if err := os.RemoveAll(appPath); err != nil {
|
if err := os.RemoveAll(appPath); err != nil {
|
||||||
|
@ -77,9 +66,62 @@ func runRemove(dockerCli command.Cli, apps []string, options *removeOptions) err
|
||||||
removeEmptyPath(options.pkgPath(), appPath)
|
removeEmptyPath(options.pkgPath(), appPath)
|
||||||
fmt.Fprintf(dockerCli.Out(), "app package removed %s\n", appPath)
|
fmt.Fprintf(dockerCli.Out(), "app package removed %s\n", appPath)
|
||||||
|
|
||||||
// remove symlinks of the app if any
|
cleanupSymlink(dockerCli, appPath, targets)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(failed) > 0 {
|
||||||
|
return fmt.Errorf("failed to remove some apps: %v", failed)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// find all symlinks in binPath for removal
|
||||||
|
func findSymlinks(binPath string) (map[string]string, error) {
|
||||||
|
targets := make(map[string]string)
|
||||||
|
readlink := func(link string) (string, error) {
|
||||||
|
target, err := os.Readlink(link)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if !filepath.IsAbs(target) {
|
||||||
|
target = filepath.Join(filepath.Dir(link), target)
|
||||||
|
}
|
||||||
|
abs, err := filepath.Abs(target)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return abs, nil
|
||||||
|
}
|
||||||
|
if links, err := findLinks(binPath); err == nil {
|
||||||
|
for _, link := range links {
|
||||||
|
if target, err := readlink(link); err == nil {
|
||||||
|
targets[target] = link
|
||||||
|
} else {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return targets, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// runUninstaller optionally runs uninstall if provided
|
||||||
|
func runUninstaller(dockerCli command.Cli, appPath string, envs map[string]string) {
|
||||||
|
uninstaller := filepath.Join(appPath, uninstallerName)
|
||||||
|
if _, err := os.Stat(uninstaller); err == nil {
|
||||||
|
err := spawn(uninstaller, nil, envs, false)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(dockerCli.Err(), "%s failed to run: %v\n", uninstaller, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// cleanupSymlink removes symlinks of the app if any
|
||||||
|
func cleanupSymlink(dockerCli command.Cli, appPath string, targets map[string]string) {
|
||||||
|
owns := func(app, target string) bool {
|
||||||
|
return strings.Contains(target, app)
|
||||||
|
}
|
||||||
for target, link := range targets {
|
for target, link := range targets {
|
||||||
if strings.Contains(target, appPath) {
|
if owns(appPath, target) {
|
||||||
if err := os.Remove(link); err != nil {
|
if err := os.Remove(link); err != nil {
|
||||||
fmt.Fprintf(dockerCli.Err(), "failed to remove %s: %v\n", link, err)
|
fmt.Fprintf(dockerCli.Err(), "failed to remove %s: %v\n", link, err)
|
||||||
} else {
|
} else {
|
||||||
|
@ -88,9 +130,3 @@ func runRemove(dockerCli command.Cli, apps []string, options *removeOptions) err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(failed) > 0 {
|
|
||||||
return fmt.Errorf("failed to remove some apps: %v", failed)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
|
@ -41,8 +41,10 @@ func TestRunRemove(t *testing.T) {
|
||||||
|
|
||||||
createApp := func(name string, args []string) ([]string, error) {
|
createApp := func(name string, args []string) ([]string, error) {
|
||||||
o := &AppOptions{
|
o := &AppOptions{
|
||||||
|
commonOptions: commonOptions{
|
||||||
_appBase: appBase,
|
_appBase: appBase,
|
||||||
_args: args,
|
_args: args,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
appPath, err := o.appPath()
|
appPath, err := o.appPath()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -77,7 +79,7 @@ func TestRunRemove(t *testing.T) {
|
||||||
expectErr: "",
|
expectErr: "",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "a few apps", args: []string{"example.com/org/one", "example.com/org/two", "example.com/org/three"},
|
name: "a few apps", args: []string{"example.com/org/one", "example.com/org/two", "example.com/org/three@v1.2.3"},
|
||||||
fakeInstall: func(args []string) []string {
|
fakeInstall: func(args []string) []string {
|
||||||
var files []string
|
var files []string
|
||||||
for _, a := range args {
|
for _, a := range args {
|
||||||
|
|
|
@ -11,8 +11,7 @@ import (
|
||||||
"syscall"
|
"syscall"
|
||||||
)
|
)
|
||||||
|
|
||||||
// spawn runs the specified command and returns the PID
|
// spawn runs the specified command
|
||||||
// of the spawned process
|
|
||||||
func spawn(bin string, args []string, envMap map[string]string, detach bool) error {
|
func spawn(bin string, args []string, envMap map[string]string, detach bool) error {
|
||||||
toEnv := func() []string {
|
toEnv := func() []string {
|
||||||
var env []string
|
var env []string
|
||||||
|
@ -138,10 +137,12 @@ func parseURL(s string) (*url.URL, error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// isSymlinkToOK checks if it is ok to create a symlink to the target
|
// isSymlinkOK checks if it is ok to create a symlink to the target
|
||||||
// it is ok if the path does not exist
|
// it is ok if the path does not exist
|
||||||
// or if the path is a symlink that points to this same target
|
// or if the path is a symlink that points to the same target.
|
||||||
func isSymlinkToOK(path, target string) (bool, error) {
|
// it is considered the same target if the symlink is identical up to
|
||||||
|
// the first @ or # sign, i.e. they are the same app but different versions.
|
||||||
|
func isSymlinkOK(path, target string) (bool, error) {
|
||||||
fi, err := os.Lstat(path)
|
fi, err := os.Lstat(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
|
@ -158,7 +159,14 @@ func isSymlinkToOK(path, target string) (bool, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
return link == target, nil
|
|
||||||
|
pkg := func(s string) string {
|
||||||
|
s = filepath.Dir(s)
|
||||||
|
s = strings.Split(s, "#")[0]
|
||||||
|
return strings.Split(s, "@")[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
return pkg(link) == pkg(target), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// splitAtDashDash splits a string array into two parts
|
// splitAtDashDash splits a string array into two parts
|
||||||
|
@ -219,3 +227,47 @@ func removeEmptyPath(root, dir string) error {
|
||||||
|
|
||||||
return rm(filepath.Dir(dir))
|
return rm(filepath.Dir(dir))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// shortenPath removes home directory from path to make it shorter
|
||||||
|
func shortenPath(path string) string {
|
||||||
|
home, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
return strings.ReplaceAll(path, home+"/", "_")
|
||||||
|
}
|
||||||
|
|
||||||
|
// locateDir walks back the path and looks for directory with the given name.
|
||||||
|
// If found, it returns the directory; otherwise an empty string.
|
||||||
|
func locateDir(path, name string) (string, error) {
|
||||||
|
check := func(dir string) (bool, string) {
|
||||||
|
if filepath.Base(dir) == name {
|
||||||
|
return true, dir
|
||||||
|
}
|
||||||
|
child := filepath.Join(dir, name)
|
||||||
|
info, err := os.Stat(child)
|
||||||
|
if err != nil {
|
||||||
|
return false, ""
|
||||||
|
}
|
||||||
|
return info.IsDir(), child
|
||||||
|
}
|
||||||
|
|
||||||
|
dir, err := filepath.Abs(path)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
if found, d := check(dir); found {
|
||||||
|
return d, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
parent := filepath.Dir(dir)
|
||||||
|
if parent == "/" || parent == dir {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
dir = parent
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", fmt.Errorf("not found: %s", name)
|
||||||
|
}
|
||||||
|
|
|
@ -189,9 +189,6 @@ func runContainer(ctx context.Context, dockerCli command.Cli, runOpts *RunOption
|
||||||
detachKeys = runOpts.detachKeys
|
detachKeys = runOpts.detachKeys
|
||||||
}
|
}
|
||||||
|
|
||||||
// ctx should not be cancellable here, as this would kill the stream to the container
|
|
||||||
// and we want to keep the stream open until the process in the container exits or until
|
|
||||||
// the user forcefully terminates the CLI.
|
|
||||||
closeFn, err := attachContainer(ctx, dockerCli, containerID, &errCh, config, container.AttachOptions{
|
closeFn, err := attachContainer(ctx, dockerCli, containerID, &errCh, config, container.AttachOptions{
|
||||||
Stream: true,
|
Stream: true,
|
||||||
Stdin: config.AttachStdin,
|
Stdin: config.AttachStdin,
|
||||||
|
|
|
@ -90,6 +90,10 @@ func (o *BuildOptions) SetImageIDFile(imageIDFile string) {
|
||||||
o.imageIDFile = imageIDFile
|
o.imageIDFile = imageIDFile
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (o *BuildOptions) SetBuildArg(value string) {
|
||||||
|
o.buildArgs.Set(value)
|
||||||
|
}
|
||||||
|
|
||||||
func newBuildOptions() BuildOptions {
|
func newBuildOptions() BuildOptions {
|
||||||
ulimits := make(map[string]*container.Ulimit)
|
ulimits := make(map[string]*container.Ulimit)
|
||||||
return BuildOptions{
|
return BuildOptions{
|
||||||
|
|
|
@ -8,7 +8,7 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"strings"
|
"os"
|
||||||
|
|
||||||
"github.com/containerd/platforms"
|
"github.com/containerd/platforms"
|
||||||
"github.com/distribution/reference"
|
"github.com/distribution/reference"
|
||||||
|
@ -58,13 +58,8 @@ func NewPushCommand(dockerCli command.Cli) *cobra.Command {
|
||||||
flags.BoolVarP(&opts.all, "all-tags", "a", false, "Push all tags of an image to the repository")
|
flags.BoolVarP(&opts.all, "all-tags", "a", false, "Push all tags of an image to the repository")
|
||||||
flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Suppress verbose output")
|
flags.BoolVarP(&opts.quiet, "quiet", "q", false, "Suppress verbose output")
|
||||||
command.AddTrustSigningFlags(flags, &opts.untrusted, dockerCli.ContentTrustEnabled())
|
command.AddTrustSigningFlags(flags, &opts.untrusted, dockerCli.ContentTrustEnabled())
|
||||||
|
flags.StringVar(&opts.platform, "platform", os.Getenv("DOCKER_DEFAULT_PLATFORM"),
|
||||||
// Don't default to DOCKER_DEFAULT_PLATFORM env variable, always default to
|
|
||||||
// pushing the image as-is. This also avoids forcing the platform selection
|
|
||||||
// on older APIs which don't support it.
|
|
||||||
flags.StringVar(&opts.platform, "platform", "",
|
|
||||||
`Push a platform-specific manifest as a single-platform image to the registry.
|
`Push a platform-specific manifest as a single-platform image to the registry.
|
||||||
Image index won't be pushed, meaning that other manifests, including attestations won't be preserved.
|
|
||||||
'os[/arch[/variant]]': Explicit platform (eg. linux/amd64)`)
|
'os[/arch[/variant]]': Explicit platform (eg. linux/amd64)`)
|
||||||
flags.SetAnnotation("platform", "version", []string{"1.46"})
|
flags.SetAnnotation("platform", "version", []string{"1.46"})
|
||||||
|
|
||||||
|
@ -84,9 +79,9 @@ func RunPush(ctx context.Context, dockerCli command.Cli, opts pushOptions) error
|
||||||
}
|
}
|
||||||
platform = &p
|
platform = &p
|
||||||
|
|
||||||
printNote(dockerCli, `Using --platform pushes only the specified platform manifest of a multi-platform image index.
|
printNote(dockerCli, `Selecting a single platform will only push one matching image manifest from a multi-platform image index.
|
||||||
Other components, like attestations, will not be included.
|
This means that any other components attached to the multi-platform image index (like Buildkit attestations) won't be pushed.
|
||||||
To push the complete multi-platform image, remove the --platform flag.
|
If you want to only push a single platform image while preserving the attestations, please use 'docker convert\n'
|
||||||
`)
|
`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -184,22 +179,9 @@ func handleAux(dockerCli command.Cli) func(jm jsonmessage.JSONMessage) {
|
||||||
|
|
||||||
func printNote(dockerCli command.Cli, format string, args ...any) {
|
func printNote(dockerCli command.Cli, format string, args ...any) {
|
||||||
if dockerCli.Err().IsTerminal() {
|
if dockerCli.Err().IsTerminal() {
|
||||||
format = strings.ReplaceAll(format, "--platform", aec.Bold.Apply("--platform"))
|
_, _ = fmt.Fprint(dockerCli.Err(), aec.WhiteF.Apply(aec.CyanB.Apply("[ NOTE ]"))+" ")
|
||||||
}
|
} else {
|
||||||
|
_, _ = fmt.Fprint(dockerCli.Err(), "[ NOTE ] ")
|
||||||
header := " Info -> "
|
|
||||||
padding := len(header)
|
|
||||||
if dockerCli.Err().IsTerminal() {
|
|
||||||
padding = len("i Info > ")
|
|
||||||
header = aec.Bold.Apply(aec.LightCyanB.Apply(aec.BlackF.Apply("i")) + " " + aec.LightCyanF.Apply("Info → "))
|
|
||||||
}
|
|
||||||
|
|
||||||
_, _ = fmt.Fprint(dockerCli.Err(), header)
|
|
||||||
s := fmt.Sprintf(format, args...)
|
|
||||||
for idx, line := range strings.Split(s, "\n") {
|
|
||||||
if idx > 0 {
|
|
||||||
_, _ = fmt.Fprint(dockerCli.Err(), strings.Repeat(" ", padding))
|
|
||||||
}
|
|
||||||
_, _ = fmt.Fprintln(dockerCli.Err(), aec.Italic.Apply(line))
|
|
||||||
}
|
}
|
||||||
|
_, _ = fmt.Fprintf(dockerCli.Err(), aec.Bold.Apply(format)+"\n", args...)
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,10 +10,10 @@ Upload an image to a registry
|
||||||
### Options
|
### Options
|
||||||
|
|
||||||
| Name | Type | Default | Description |
|
| Name | Type | Default | Description |
|
||||||
|:---------------------------------------------|:---------|:--------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|:---------------------------------------------|:---------|:--------|:--------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| [`-a`](#all-tags), [`--all-tags`](#all-tags) | `bool` | | Push all tags of an image to the repository |
|
| [`-a`](#all-tags), [`--all-tags`](#all-tags) | `bool` | | Push all tags of an image to the repository |
|
||||||
| `--disable-content-trust` | `bool` | `true` | Skip image signing |
|
| `--disable-content-trust` | `bool` | `true` | Skip image signing |
|
||||||
| `--platform` | `string` | | Push a platform-specific manifest as a single-platform image to the registry.<br>Image index won't be pushed, meaning that other manifests, including attestations won't be preserved.<br>'os[/arch[/variant]]': Explicit platform (eg. linux/amd64) |
|
| `--platform` | `string` | | Push a platform-specific manifest as a single-platform image to the registry.<br>'os[/arch[/variant]]': Explicit platform (eg. linux/amd64) |
|
||||||
| `-q`, `--quiet` | `bool` | | Suppress verbose output |
|
| `-q`, `--quiet` | `bool` | | Suppress verbose output |
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -10,10 +10,10 @@ Upload an image to a registry
|
||||||
### Options
|
### Options
|
||||||
|
|
||||||
| Name | Type | Default | Description |
|
| Name | Type | Default | Description |
|
||||||
|:--------------------------|:---------|:--------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
|:--------------------------|:---------|:--------|:--------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| `-a`, `--all-tags` | `bool` | | Push all tags of an image to the repository |
|
| `-a`, `--all-tags` | `bool` | | Push all tags of an image to the repository |
|
||||||
| `--disable-content-trust` | `bool` | `true` | Skip image signing |
|
| `--disable-content-trust` | `bool` | `true` | Skip image signing |
|
||||||
| `--platform` | `string` | | Push a platform-specific manifest as a single-platform image to the registry.<br>Image index won't be pushed, meaning that other manifests, including attestations won't be preserved.<br>'os[/arch[/variant]]': Explicit platform (eg. linux/amd64) |
|
| `--platform` | `string` | | Push a platform-specific manifest as a single-platform image to the registry.<br>'os[/arch[/variant]]': Explicit platform (eg. linux/amd64) |
|
||||||
| `-q`, `--quiet` | `bool` | | Suppress verbose output |
|
| `-q`, `--quiet` | `bool` | | Suppress verbose output |
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,8 @@ import (
|
||||||
"gotest.tools/v3/icmd"
|
"gotest.tools/v3/icmd"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const defaultArgs = "DOCKER_APP_BASE DOCKER_APP_PATH VERSION HOSTARCH HOSTOS USERGID USERHOME USERID USERNAME"
|
||||||
|
|
||||||
func TestInstallOne(t *testing.T) {
|
func TestInstallOne(t *testing.T) {
|
||||||
const buildCtx = "one"
|
const buildCtx = "one"
|
||||||
const coolApp = "cool"
|
const coolApp = "cool"
|
||||||
|
@ -25,10 +27,10 @@ func TestInstallOne(t *testing.T) {
|
||||||
fs.WithDir(buildCtx,
|
fs.WithDir(buildCtx,
|
||||||
fs.WithFile("Dockerfile", fmt.Sprintf(`
|
fs.WithFile("Dockerfile", fmt.Sprintf(`
|
||||||
FROM %s
|
FROM %s
|
||||||
ARG HOSTOS HOSTARCH
|
ARG %s
|
||||||
COPY cool /egress/%s
|
COPY cool /egress/%s
|
||||||
CMD ["echo", "'cool' app successfully built!"]
|
CMD ["echo", "'cool' app successfully built!"]
|
||||||
`, fixtures.AlpineImage, coolApp)),
|
`, fixtures.AlpineImage, defaultArgs, coolApp)),
|
||||||
fs.WithFile("cool", coolScript, fs.WithMode(0o755)),
|
fs.WithFile("cool", coolScript, fs.WithMode(0o755)),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
@ -74,10 +76,10 @@ func TestInstallMulti(t *testing.T) {
|
||||||
fs.WithDir(buildCtx,
|
fs.WithDir(buildCtx,
|
||||||
fs.WithFile("Dockerfile", fmt.Sprintf(`
|
fs.WithFile("Dockerfile", fmt.Sprintf(`
|
||||||
FROM %s
|
FROM %s
|
||||||
ARG HOSTOS HOSTARCH
|
ARG %s
|
||||||
COPY . /egress
|
COPY . /egress
|
||||||
CMD ["echo", "'multi' app successfully built!"]
|
CMD ["echo", "'multi' app successfully built!"]
|
||||||
`, fixtures.AlpineImage)),
|
`, fixtures.AlpineImage, defaultArgs)),
|
||||||
fs.WithFile(".dockerignore", `
|
fs.WithFile(".dockerignore", `
|
||||||
Dockerfile
|
Dockerfile
|
||||||
.dockerignore
|
.dockerignore
|
||||||
|
@ -147,10 +149,10 @@ func TestInstallCustom(t *testing.T) {
|
||||||
fs.WithDir(buildCtx,
|
fs.WithDir(buildCtx,
|
||||||
fs.WithFile("Dockerfile", fmt.Sprintf(`
|
fs.WithFile("Dockerfile", fmt.Sprintf(`
|
||||||
FROM %s
|
FROM %s
|
||||||
ARG HOSTOS HOSTARCH
|
ARG %s
|
||||||
COPY . /egress
|
COPY . /egress
|
||||||
CMD ["echo", "'custom' app successfully built!"]
|
CMD ["echo", "'custom' app successfully built!"]
|
||||||
`, fixtures.AlpineImage)),
|
`, fixtures.AlpineImage, defaultArgs)),
|
||||||
fs.WithFile(".dockerignore", `
|
fs.WithFile(".dockerignore", `
|
||||||
Dockerfile
|
Dockerfile
|
||||||
.dockerignore
|
.dockerignore
|
||||||
|
@ -208,10 +210,10 @@ func TestInstallCustomDestination(t *testing.T) {
|
||||||
fs.WithDir(buildCtx,
|
fs.WithDir(buildCtx,
|
||||||
fs.WithFile("Dockerfile", fmt.Sprintf(`
|
fs.WithFile("Dockerfile", fmt.Sprintf(`
|
||||||
FROM %s
|
FROM %s
|
||||||
ARG HOSTOS HOSTARCH
|
ARG %s
|
||||||
COPY . %s
|
COPY . %s
|
||||||
CMD ["echo", "'service' successfully built!"]
|
CMD ["echo", "'service' successfully built!"]
|
||||||
`, fixtures.AlpineImage, egress)),
|
`, fixtures.AlpineImage, defaultArgs, egress)),
|
||||||
fs.WithFile("config", "", fs.WithMode(0o644)),
|
fs.WithFile("config", "", fs.WithMode(0o644)),
|
||||||
fs.WithFile("install", deployScript, fs.WithMode(0o755)),
|
fs.WithFile("install", deployScript, fs.WithMode(0o755)),
|
||||||
),
|
),
|
||||||
|
|
|
@ -34,10 +34,10 @@ func TestLaunchOne(t *testing.T) {
|
||||||
fs.WithDir(buildCtx,
|
fs.WithDir(buildCtx,
|
||||||
fs.WithFile("Dockerfile", fmt.Sprintf(`
|
fs.WithFile("Dockerfile", fmt.Sprintf(`
|
||||||
FROM %s
|
FROM %s
|
||||||
ARG HOSTOS HOSTARCH
|
ARG %s
|
||||||
COPY cool /egress/%s
|
COPY cool /egress/%s
|
||||||
CMD ["echo", "'cool' app successfully built!"]
|
CMD ["echo", "'cool' app successfully built!"]
|
||||||
`, fixtures.AlpineImage, coolApp)),
|
`, fixtures.AlpineImage, defaultArgs, coolApp)),
|
||||||
fs.WithFile("cool", coolScript, fs.WithMode(0o755)),
|
fs.WithFile("cool", coolScript, fs.WithMode(0o755)),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
@ -87,10 +87,10 @@ func TestLaunchMulti(t *testing.T) {
|
||||||
fs.WithDir(buildCtx,
|
fs.WithDir(buildCtx,
|
||||||
fs.WithFile("Dockerfile", fmt.Sprintf(`
|
fs.WithFile("Dockerfile", fmt.Sprintf(`
|
||||||
FROM %s
|
FROM %s
|
||||||
ARG HOSTOS HOSTARCH
|
ARG %s
|
||||||
COPY . /egress
|
COPY . /egress
|
||||||
CMD ["echo", "'multi' app successfully built!"]
|
CMD ["echo", "'multi' app successfully built!"]
|
||||||
`, fixtures.AlpineImage)),
|
`, fixtures.AlpineImage, defaultArgs)),
|
||||||
fs.WithFile(".dockerignore", `
|
fs.WithFile(".dockerignore", `
|
||||||
Dockerfile
|
Dockerfile
|
||||||
.dockerignore
|
.dockerignore
|
||||||
|
|
|
@ -1,10 +1,8 @@
|
||||||
package container
|
package container
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -15,7 +13,6 @@ import (
|
||||||
is "gotest.tools/v3/assert/cmp"
|
is "gotest.tools/v3/assert/cmp"
|
||||||
"gotest.tools/v3/golden"
|
"gotest.tools/v3/golden"
|
||||||
"gotest.tools/v3/icmd"
|
"gotest.tools/v3/icmd"
|
||||||
"gotest.tools/v3/poll"
|
|
||||||
"gotest.tools/v3/skip"
|
"gotest.tools/v3/skip"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -224,26 +221,3 @@ func TestMountSubvolume(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestProcessTermination(t *testing.T) {
|
|
||||||
var out bytes.Buffer
|
|
||||||
cmd := icmd.Command("docker", "run", "--rm", "-i", fixtures.AlpineImage,
|
|
||||||
"sh", "-c", "echo 'starting trap'; trap 'echo got signal; exit 0;' TERM; while true; do sleep 10; done")
|
|
||||||
cmd.Stdout = &out
|
|
||||||
cmd.Stderr = &out
|
|
||||||
|
|
||||||
result := icmd.StartCmd(cmd).Assert(t, icmd.Success)
|
|
||||||
|
|
||||||
poll.WaitOn(t, func(t poll.LogT) poll.Result {
|
|
||||||
if strings.Contains(result.Stdout(), "starting trap") {
|
|
||||||
return poll.Success()
|
|
||||||
}
|
|
||||||
return poll.Continue("waiting for process to trap signal")
|
|
||||||
}, poll.WithDelay(1*time.Second), poll.WithTimeout(5*time.Second))
|
|
||||||
|
|
||||||
assert.NilError(t, result.Cmd.Process.Signal(syscall.SIGTERM))
|
|
||||||
|
|
||||||
icmd.WaitOnCmd(time.Second*10, result).Assert(t, icmd.Expected{
|
|
||||||
ExitCode: 0,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
Loading…
Reference in New Issue