Compare commits
42 Commits
v18.09.0-c
...
v18.09.0-b
| Author | SHA1 | Date | |
|---|---|---|---|
| 4280972d65 | |||
| 984bc7411e | |||
| 92932647d3 | |||
| dee37936e5 | |||
| 3e1a0bdc23 | |||
| f2b2061cc3 | |||
| 4925fd9c34 | |||
| 5d3ab5bc0c | |||
| c12e23a4c1 | |||
| aca3f2d382 | |||
| a7488d1bcd | |||
| 5a97a93ae1 | |||
| 41910b6d68 | |||
| 1a087e87c9 | |||
| 0b11120060 | |||
| e57b20642d | |||
| b8702b8a9a | |||
| a31b20d7db | |||
| 5ba5678898 | |||
| 9de1318e36 | |||
| 19e1ab273e | |||
| ec1812188f | |||
| 6004d74b1f | |||
| e79e591ee9 | |||
| 0f22d7e295 | |||
| f250152bf4 | |||
| f9d666b057 | |||
| 342afe44fb | |||
| cfec8027ed | |||
| 78c42cf031 | |||
| dd2f13bed4 | |||
| 3b991ec615 | |||
| 34ea8bb5a5 | |||
| afb17ec70b | |||
| 62aed95bc1 | |||
| 649e4916bb | |||
| 3597d75281 | |||
| 5673816fec | |||
| a8c69c8287 | |||
| fc3dc8f058 | |||
| 2a46a3d46c | |||
| b2cf18ac2e |
4
Makefile
4
Makefile
@ -12,14 +12,14 @@ clean: ## remove build artifacts
|
||||
|
||||
.PHONY: test-unit
|
||||
test-unit: ## run unit test
|
||||
./scripts/test/unit $(shell go list ./... | grep -vE '/vendor/|/e2e/|/e2eengine/')
|
||||
./scripts/test/unit $(shell go list ./... | grep -vE '/vendor/|/e2e/')
|
||||
|
||||
.PHONY: test
|
||||
test: test-unit ## run tests
|
||||
|
||||
.PHONY: test-coverage
|
||||
test-coverage: ## run test coverage
|
||||
./scripts/test/unit-with-coverage $(shell go list ./... | grep -vE '/vendor/|/e2e/|/e2eengine/')
|
||||
./scripts/test/unit-with-coverage $(shell go list ./... | grep -vE '/vendor/|/e2e/')
|
||||
|
||||
.PHONY: lint
|
||||
lint: ## run all the lint tools
|
||||
|
||||
@ -19,8 +19,8 @@ import (
|
||||
manifeststore "github.com/docker/cli/cli/manifest/store"
|
||||
registryclient "github.com/docker/cli/cli/registry/client"
|
||||
"github.com/docker/cli/cli/trust"
|
||||
"github.com/docker/cli/internal/containerizedengine"
|
||||
dopts "github.com/docker/cli/opts"
|
||||
clitypes "github.com/docker/cli/types"
|
||||
"github.com/docker/docker/api"
|
||||
"github.com/docker/docker/api/types"
|
||||
registrytypes "github.com/docker/docker/api/types/registry"
|
||||
@ -55,20 +55,21 @@ type Cli interface {
|
||||
ManifestStore() manifeststore.Store
|
||||
RegistryClient(bool) registryclient.RegistryClient
|
||||
ContentTrustEnabled() bool
|
||||
NewContainerizedEngineClient(sockPath string) (containerizedengine.Client, error)
|
||||
NewContainerizedEngineClient(sockPath string) (clitypes.ContainerizedClient, error)
|
||||
}
|
||||
|
||||
// DockerCli is an instance the docker command line client.
|
||||
// Instances of the client can be returned from NewDockerCli.
|
||||
type DockerCli struct {
|
||||
configFile *configfile.ConfigFile
|
||||
in *InStream
|
||||
out *OutStream
|
||||
err io.Writer
|
||||
client client.APIClient
|
||||
serverInfo ServerInfo
|
||||
clientInfo ClientInfo
|
||||
contentTrust bool
|
||||
configFile *configfile.ConfigFile
|
||||
in *InStream
|
||||
out *OutStream
|
||||
err io.Writer
|
||||
client client.APIClient
|
||||
serverInfo ServerInfo
|
||||
clientInfo ClientInfo
|
||||
contentTrust bool
|
||||
newContainerizeClient func(string) (clitypes.ContainerizedClient, error)
|
||||
}
|
||||
|
||||
// DefaultVersion returns api.defaultVersion or DOCKER_API_VERSION if specified.
|
||||
@ -233,8 +234,8 @@ func (cli *DockerCli) NotaryClient(imgRefAndAuth trust.ImageRefAndAuth, actions
|
||||
}
|
||||
|
||||
// NewContainerizedEngineClient returns a containerized engine client
|
||||
func (cli *DockerCli) NewContainerizedEngineClient(sockPath string) (containerizedengine.Client, error) {
|
||||
return containerizedengine.NewClient(sockPath)
|
||||
func (cli *DockerCli) NewContainerizedEngineClient(sockPath string) (clitypes.ContainerizedClient, error) {
|
||||
return cli.newContainerizeClient(sockPath)
|
||||
}
|
||||
|
||||
// ServerInfo stores details about the supported features and platform of the
|
||||
@ -252,8 +253,8 @@ type ClientInfo struct {
|
||||
}
|
||||
|
||||
// NewDockerCli returns a DockerCli instance with IO output and error streams set by in, out and err.
|
||||
func NewDockerCli(in io.ReadCloser, out, err io.Writer, isTrusted bool) *DockerCli {
|
||||
return &DockerCli{in: NewInStream(in), out: NewOutStream(out), err: err, contentTrust: isTrusted}
|
||||
func NewDockerCli(in io.ReadCloser, out, err io.Writer, isTrusted bool, containerizedFn func(string) (clitypes.ContainerizedClient, error)) *DockerCli {
|
||||
return &DockerCli{in: NewInStream(in), out: NewOutStream(out), err: err, contentTrust: isTrusted, newContainerizeClient: containerizedFn}
|
||||
}
|
||||
|
||||
// NewAPIClientFromFlags creates a new APIClient from command line flags
|
||||
|
||||
@ -2,6 +2,7 @@ package commands
|
||||
|
||||
import (
|
||||
"os"
|
||||
"runtime"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/builder"
|
||||
@ -85,9 +86,6 @@ func AddCommands(cmd *cobra.Command, dockerCli command.Cli) {
|
||||
// volume
|
||||
volume.NewVolumeCommand(dockerCli),
|
||||
|
||||
// engine
|
||||
engine.NewEngineCommand(dockerCli),
|
||||
|
||||
// legacy commands may be hidden
|
||||
hide(system.NewEventsCommand(dockerCli)),
|
||||
hide(system.NewInfoCommand(dockerCli)),
|
||||
@ -124,7 +122,10 @@ func AddCommands(cmd *cobra.Command, dockerCli command.Cli) {
|
||||
hide(image.NewSaveCommand(dockerCli)),
|
||||
hide(image.NewTagCommand(dockerCli)),
|
||||
)
|
||||
|
||||
if runtime.GOOS == "linux" {
|
||||
// engine
|
||||
cmd.AddCommand(engine.NewEngineCommand(dockerCli))
|
||||
}
|
||||
}
|
||||
|
||||
func hide(cmd *cobra.Command) *cobra.Command {
|
||||
|
||||
@ -3,11 +3,12 @@ package engine
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
"github.com/docker/cli/internal/containerizedengine"
|
||||
"github.com/docker/cli/internal/licenseutils"
|
||||
clitypes "github.com/docker/cli/types"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/licensing/model"
|
||||
"github.com/pkg/errors"
|
||||
@ -15,19 +16,21 @@ import (
|
||||
)
|
||||
|
||||
type activateOptions struct {
|
||||
licenseFile string
|
||||
version string
|
||||
registryPrefix string
|
||||
format string
|
||||
image string
|
||||
quiet bool
|
||||
displayOnly bool
|
||||
sockPath string
|
||||
licenseFile string
|
||||
version string
|
||||
registryPrefix string
|
||||
format string
|
||||
image string
|
||||
quiet bool
|
||||
displayOnly bool
|
||||
sockPath string
|
||||
licenseLoginFunc func(ctx context.Context, authConfig *types.AuthConfig) (licenseutils.HubUser, error)
|
||||
}
|
||||
|
||||
// newActivateCommand creates a new `docker engine activate` command
|
||||
func newActivateCommand(dockerCli command.Cli) *cobra.Command {
|
||||
var options activateOptions
|
||||
options.licenseLoginFunc = licenseutils.Login
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "activate [OPTIONS]",
|
||||
@ -56,10 +59,10 @@ https://hub.docker.com/ then specify the file with the '--license' flag.
|
||||
|
||||
flags.StringVar(&options.licenseFile, "license", "", "License File")
|
||||
flags.StringVar(&options.version, "version", "", "Specify engine version (default is to use currently running version)")
|
||||
flags.StringVar(&options.registryPrefix, "registry-prefix", "docker.io/docker", "Override the default location where engine images are pulled")
|
||||
flags.StringVar(&options.image, "engine-image", containerizedengine.EnterpriseEngineImage, "Specify engine image")
|
||||
flags.StringVar(&options.registryPrefix, "registry-prefix", clitypes.RegistryPrefix, "Override the default location where engine images are pulled")
|
||||
flags.StringVar(&options.image, "engine-image", "", "Specify engine image")
|
||||
flags.StringVar(&options.format, "format", "", "Pretty-print licenses using a Go template")
|
||||
flags.BoolVar(&options.displayOnly, "display-only", false, "only display the available licenses and exit")
|
||||
flags.BoolVar(&options.displayOnly, "display-only", false, "only display license information and exit")
|
||||
flags.BoolVar(&options.quiet, "quiet", false, "Only display available licenses by ID")
|
||||
flags.StringVar(&options.sockPath, "containerd", "", "override default location of containerd endpoint")
|
||||
|
||||
@ -67,6 +70,9 @@ https://hub.docker.com/ then specify the file with the '--license' flag.
|
||||
}
|
||||
|
||||
func runActivate(cli command.Cli, options activateOptions) error {
|
||||
if !isRoot() {
|
||||
return errors.New("this command must be run as a privileged user")
|
||||
}
|
||||
ctx := context.Background()
|
||||
client, err := cli.NewContainerizedEngineClient(options.sockPath)
|
||||
if err != nil {
|
||||
@ -94,26 +100,48 @@ func runActivate(cli command.Cli, options activateOptions) error {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err = licenseutils.ApplyLicense(ctx, cli.Client(), license); err != nil {
|
||||
summary, err := licenseutils.GetLicenseSummary(ctx, *license)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(cli.Out(), "License: %s\n", summary)
|
||||
if options.displayOnly {
|
||||
return nil
|
||||
}
|
||||
dclient := cli.Client()
|
||||
if err = licenseutils.ApplyLicense(ctx, dclient, license); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
opts := containerizedengine.EngineInitOptions{
|
||||
// Short circuit if the user didn't specify a version and we're already running enterprise
|
||||
if options.version == "" {
|
||||
serverVersion, err := dclient.ServerVersion(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if strings.Contains(strings.ToLower(serverVersion.Platform.Name), "enterprise") {
|
||||
fmt.Fprintln(cli.Out(), "Successfully activated engine license on existing enterprise engine.")
|
||||
return nil
|
||||
}
|
||||
options.version = serverVersion.Version
|
||||
}
|
||||
|
||||
opts := clitypes.EngineInitOptions{
|
||||
RegistryPrefix: options.registryPrefix,
|
||||
EngineImage: options.image,
|
||||
EngineVersion: options.version,
|
||||
}
|
||||
|
||||
return client.ActivateEngine(ctx, opts, cli.Out(), authConfig,
|
||||
func(ctx context.Context) error {
|
||||
client := cli.Client()
|
||||
_, err := client.Ping(ctx)
|
||||
return err
|
||||
})
|
||||
if err := client.ActivateEngine(ctx, opts, cli.Out(), authConfig); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintln(cli.Out(), `Successfully activated engine.
|
||||
Restart docker with 'systemctl restart docker' to complete the activation.`)
|
||||
return nil
|
||||
}
|
||||
|
||||
func getLicenses(ctx context.Context, authConfig *types.AuthConfig, cli command.Cli, options activateOptions) (*model.IssuedLicense, error) {
|
||||
user, err := licenseutils.Login(ctx, authConfig)
|
||||
user, err := options.licenseLoginFunc(ctx, authConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@ -1,19 +1,35 @@
|
||||
package engine
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/docker/cli/internal/containerizedengine"
|
||||
"github.com/docker/cli/internal/licenseutils"
|
||||
"github.com/docker/cli/internal/test"
|
||||
clitypes "github.com/docker/cli/types"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/docker/licensing"
|
||||
"github.com/docker/licensing/model"
|
||||
"gotest.tools/assert"
|
||||
"gotest.tools/fs"
|
||||
"gotest.tools/golden"
|
||||
)
|
||||
|
||||
const (
|
||||
// nolint: lll
|
||||
expiredLicense = `{"key_id":"irlYm3b9fdD8hMUXjazF39im7VQSSbAm9tfHK8cKUxJt","private_key":"aH5tTRDAVJpCRS2CRetTQVXIKgWUPfoCHODhDvNPvAbz","authorization":"ewogICAicGF5bG9hZCI6ICJleUpsZUhCcGNtRjBhVzl1SWpvaU1qQXhPQzB3TXkweE9GUXdOem93TURvd01Gb2lMQ0owYjJ0bGJpSTZJbkZtTVMxMlVtRmtialp5YjFaMldXdHJlVXN4VFdKMGNGUmpXR1ozVjA4MVRWZFFTM2cwUnpJd2NIYzlJaXdpYldGNFJXNW5hVzVsY3lJNk1Td2ljMk5oYm01cGJtZEZibUZpYkdWa0lqcDBjblZsTENKc2FXTmxibk5sVkhsd1pTSTZJazltWm14cGJtVWlMQ0owYVdWeUlqb2lVSEp2WkhWamRHbHZiaUo5IiwKICAgInNpZ25hdHVyZXMiOiBbCiAgICAgIHsKICAgICAgICAgImhlYWRlciI6IHsKICAgICAgICAgICAgImp3ayI6IHsKICAgICAgICAgICAgICAgImUiOiAiQVFBQiIsCiAgICAgICAgICAgICAgICJrZXlJRCI6ICJKN0xEOjY3VlI6TDVIWjpVN0JBOjJPNEc6NEFMMzpPRjJOOkpIR0I6RUZUSDo1Q1ZROk1GRU86QUVJVCIsCiAgICAgICAgICAgICAgICJraWQiOiAiSjdMRDo2N1ZSOkw1SFo6VTdCQToyTzRHOjRBTDM6T0YyTjpKSEdCOkVGVEg6NUNWUTpNRkVPOkFFSVQiLAogICAgICAgICAgICAgICAia3R5IjogIlJTQSIsCiAgICAgICAgICAgICAgICJuIjogInlkSXktbFU3bzdQY2VZLTQtcy1DUTVPRWdDeUY4Q3hJY1FJV3VLODRwSWlaY2lZNjczMHlDWW53TFNLVGx3LVU2VUNfUVJlV1Jpb01OTkU1RHM1VFlFWGJHRzZvbG0ycWRXYkJ3Y0NnLTJVVUhfT2NCOVd1UDZnUlBIcE1GTXN4RHpXd3ZheThKVXVIZ1lVTFVwbTFJdi1tcTdscDVuUV9SeHJUMEtaUkFRVFlMRU1FZkd3bTNoTU9fZ2VMUFMtaGdLUHRJSGxrZzZfV2NveFRHb0tQNzlkX3dhSFl4R05sN1doU25laUJTeGJwYlFBS2syMWxnNzk4WGI3dlp5RUFURE1yUlI5TWVFNkFkajVISnBZM0NveVJBUENtYUtHUkNLNHVvWlNvSXUwaEZWbEtVUHliYncwMDBHTy13YTJLTjhVd2dJSW0waTVJMXVXOUdrcTR6akJ5NXpoZ3F1VVhiRzliV1BBT1lycTVRYTgxRHhHY0JsSnlIWUFwLUREUEU5VEdnNHpZbVhqSm54WnFIRWR1R3FkZXZaOFhNSTB1a2ZrR0lJMTR3VU9pTUlJSXJYbEVjQmZfNDZJOGdRV0R6eHljWmVfSkdYLUxBdWF5WHJ5clVGZWhWTlVkWlVsOXdYTmFKQi1rYUNxejVRd2FSOTNzR3ctUVNmdEQwTnZMZTdDeU9ILUU2dmc2U3RfTmVUdmd2OFluaENpWElsWjhIT2ZJd05lN3RFRl9VY3o1T2JQeWttM3R5bHJOVWp0MFZ5QW10dGFjVkkyaUdpaGNVUHJtazRsVklaN1ZEX0xTVy1pN3lvU3VydHBzUFhjZTJwS0RJbzMwbEpHaE9fM0tVbWwyU1VaQ3F6SjF5RW1LcHlzSDVIRFc5Y3NJRkNBM2RlQWpmWlV2TjdVIgogICAgICAgICAgICB9LAogICAgICAgICAgICAiYWxnIjogIlJTMjU2IgogICAgICAgICB9LAogICAgICAgICAic2lnbmF0dXJlIjogIm5saTZIdzRrbW5KcTBSUmRXaGVfbkhZS2VJLVpKenM1U0d5SUpDakh1dWtnVzhBYklpVzFZYWJJR2NqWUt0QTY4dWN6T1hyUXZreGxWQXJLSlgzMDJzN0RpbzcxTlNPRzJVcnhsSjlibDFpd0F3a3ZyTEQ2T0p5MGxGLVg4WnRabXhPVmNQZmwzcmJwZFQ0dnlnWTdNcU1QRXdmb0IxTmlWZDYyZ1cxU2NSREZZcWw3R0FVaFVKNkp4QU15VzVaOXl5YVE0NV8wd0RMUk5mRjA5YWNXeVowTjRxVS1hZjhrUTZUUWZUX05ERzNCR3pRb2V3cHlEajRiMFBHb0diOFhLdDlwekpFdEdxM3lQM25VMFFBbk90a2gwTnZac1l1UFcyUnhDT3lRNEYzVlR3UkF2eF9HSTZrMVRpYmlKNnByUWluUy16Sjh6RE8zUjBuakE3OFBwNXcxcVpaUE9BdmtzZFNSYzJDcVMtcWhpTmF5YUhOVHpVNnpyOXlOZHR2S0o1QjNST0FmNUtjYXNiWURjTnVpeXBUNk90LUtqQ2I1dmYtWVpnc2FRNzJBdFBhSU4yeUpNREZHbmEwM0hpSjMxcTJRUlp5eTZrd3RYaGtwcDhTdEdIcHYxSWRaV09SVWttb0g5SFBzSGk4SExRLTZlM0tEY2x1RUQyMTNpZnljaVhtN0YzdHdaTTNHeDd1UXR1SldHaUlTZ2Z0QW9lVjZfUmI2VThkMmZxNzZuWHYxak5nckRRcE5waEZFd2tCdGRtZHZ2THByZVVYX3BWangza1AxN3pWbXFKNmNOOWkwWUc4WHg2VmRzcUxsRXUxQ2Rhd3Q0eko1M3VHMFlKTjRnUDZwc25yUS1uM0U1aFdlMDJ3d3dBZ3F3bGlPdmd4V1RTeXJyLXY2eDI0IiwKICAgICAgICAgInByb3RlY3RlZCI6ICJleUptYjNKdFlYUk1aVzVuZEdnaU9qRTNNeXdpWm05eWJXRjBWR0ZwYkNJNkltWlJJaXdpZEdsdFpTSTZJakl3TVRjdE1EVXRNRFZVTWpFNk5UYzZNek5hSW4wIgogICAgICB9CiAgIF0KfQ=="}`
|
||||
)
|
||||
|
||||
func TestActivateNoContainerd(t *testing.T) {
|
||||
testCli.SetContainerizedEngineClient(
|
||||
func(string) (containerizedengine.Client, error) {
|
||||
func(string) (clitypes.ContainerizedClient, error) {
|
||||
return nil, fmt.Errorf("some error")
|
||||
},
|
||||
)
|
||||
isRoot = func() bool { return true }
|
||||
cmd := newActivateCommand(testCli)
|
||||
cmd.Flags().Set("license", "invalidpath")
|
||||
cmd.SilenceUsage = true
|
||||
@ -24,10 +40,11 @@ func TestActivateNoContainerd(t *testing.T) {
|
||||
|
||||
func TestActivateBadLicense(t *testing.T) {
|
||||
testCli.SetContainerizedEngineClient(
|
||||
func(string) (containerizedengine.Client, error) {
|
||||
func(string) (clitypes.ContainerizedClient, error) {
|
||||
return &fakeContainerizedEngineClient{}, nil
|
||||
},
|
||||
)
|
||||
isRoot = func() bool { return true }
|
||||
cmd := newActivateCommand(testCli)
|
||||
cmd.SilenceUsage = true
|
||||
cmd.SilenceErrors = true
|
||||
@ -35,3 +52,95 @@ func TestActivateBadLicense(t *testing.T) {
|
||||
err := cmd.Execute()
|
||||
assert.Error(t, err, "open invalidpath: no such file or directory")
|
||||
}
|
||||
|
||||
func TestActivateExpiredLicenseDryRun(t *testing.T) {
|
||||
dir := fs.NewDir(t, "license", fs.WithFile("docker.lic", expiredLicense, fs.WithMode(0644)))
|
||||
defer dir.Remove()
|
||||
filename := dir.Join("docker.lic")
|
||||
isRoot = func() bool { return true }
|
||||
c := test.NewFakeCli(&verClient{client.Client{}, types.Version{}, nil, types.Info{}, nil})
|
||||
c.SetContainerizedEngineClient(
|
||||
func(string) (clitypes.ContainerizedClient, error) {
|
||||
return &fakeContainerizedEngineClient{}, nil
|
||||
},
|
||||
)
|
||||
cmd := newActivateCommand(c)
|
||||
cmd.SilenceUsage = true
|
||||
cmd.SilenceErrors = true
|
||||
cmd.Flags().Set("license", filename)
|
||||
cmd.Flags().Set("display-only", "true")
|
||||
c.OutBuffer().Reset()
|
||||
err := cmd.Execute()
|
||||
assert.NilError(t, err)
|
||||
golden.Assert(t, c.OutBuffer().String(), "expired-license-display-only.golden")
|
||||
}
|
||||
|
||||
type mockLicenseClient struct{}
|
||||
|
||||
func (c mockLicenseClient) LoginViaAuth(ctx context.Context, username, password string) (authToken string, err error) {
|
||||
return "", fmt.Errorf("not implemented")
|
||||
}
|
||||
|
||||
func (c mockLicenseClient) GetHubUserOrgs(ctx context.Context, authToken string) (orgs []model.Org, err error) {
|
||||
return nil, fmt.Errorf("not implemented")
|
||||
}
|
||||
func (c mockLicenseClient) GetHubUserByName(ctx context.Context, username string) (user *model.User, err error) {
|
||||
return nil, fmt.Errorf("not implemented")
|
||||
}
|
||||
func (c mockLicenseClient) VerifyLicense(ctx context.Context, license model.IssuedLicense) (res *model.CheckResponse, err error) {
|
||||
return nil, fmt.Errorf("not implemented")
|
||||
}
|
||||
func (c mockLicenseClient) GenerateNewTrialSubscription(ctx context.Context, authToken, dockerID, email string) (subscriptionID string, err error) {
|
||||
return "", fmt.Errorf("not implemented")
|
||||
}
|
||||
func (c mockLicenseClient) ListSubscriptions(ctx context.Context, authToken, dockerID string) (response []*model.Subscription, err error) {
|
||||
expires := time.Date(2010, time.January, 1, 0, 0, 0, 0, time.UTC)
|
||||
return []*model.Subscription{
|
||||
{
|
||||
State: "active",
|
||||
Expires: &expires,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
func (c mockLicenseClient) ListSubscriptionsDetails(ctx context.Context, authToken, dockerID string) (response []*model.SubscriptionDetail, err error) {
|
||||
return nil, fmt.Errorf("not implemented")
|
||||
}
|
||||
func (c mockLicenseClient) DownloadLicenseFromHub(ctx context.Context, authToken, subscriptionID string) (license *model.IssuedLicense, err error) {
|
||||
return nil, fmt.Errorf("not implemented")
|
||||
}
|
||||
func (c mockLicenseClient) ParseLicense(license []byte) (parsedLicense *model.IssuedLicense, err error) {
|
||||
return nil, fmt.Errorf("not implemented")
|
||||
}
|
||||
func (c mockLicenseClient) StoreLicense(ctx context.Context, dclnt licensing.WrappedDockerClient, licenses *model.IssuedLicense, localRootDir string) error {
|
||||
return fmt.Errorf("not implemented")
|
||||
}
|
||||
func (c mockLicenseClient) LoadLocalLicense(ctx context.Context, dclnt licensing.WrappedDockerClient) (*model.Subscription, error) {
|
||||
return nil, fmt.Errorf("not implemented")
|
||||
}
|
||||
func (c mockLicenseClient) SummarizeLicense(res *model.CheckResponse, keyID string) *model.Subscription {
|
||||
return nil
|
||||
}
|
||||
func TestActivateDisplayOnlyHub(t *testing.T) {
|
||||
isRoot = func() bool { return true }
|
||||
c := test.NewFakeCli(&verClient{client.Client{}, types.Version{}, nil, types.Info{}, nil})
|
||||
c.SetContainerizedEngineClient(
|
||||
func(string) (clitypes.ContainerizedClient, error) {
|
||||
return &fakeContainerizedEngineClient{}, nil
|
||||
},
|
||||
)
|
||||
|
||||
hubUser := licenseutils.HubUser{
|
||||
Client: mockLicenseClient{},
|
||||
}
|
||||
options := activateOptions{
|
||||
licenseLoginFunc: func(ctx context.Context, authConfig *types.AuthConfig) (licenseutils.HubUser, error) {
|
||||
return hubUser, nil
|
||||
},
|
||||
displayOnly: true,
|
||||
}
|
||||
c.OutBuffer().Reset()
|
||||
err := runActivate(c, options)
|
||||
|
||||
assert.NilError(t, err)
|
||||
golden.Assert(t, c.OutBuffer().String(), "expired-hub-license-display-only.golden")
|
||||
}
|
||||
|
||||
13
cli/command/engine/activate_unix.go
Normal file
13
cli/command/engine/activate_unix.go
Normal file
@ -0,0 +1,13 @@
|
||||
// +build !windows
|
||||
|
||||
package engine
|
||||
|
||||
import (
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
var (
|
||||
isRoot = func() bool {
|
||||
return unix.Geteuid() == 0
|
||||
}
|
||||
)
|
||||
9
cli/command/engine/activate_windows.go
Normal file
9
cli/command/engine/activate_windows.go
Normal file
@ -0,0 +1,9 @@
|
||||
// +build windows
|
||||
|
||||
package engine
|
||||
|
||||
var (
|
||||
isRoot = func() bool {
|
||||
return true
|
||||
}
|
||||
)
|
||||
@ -5,6 +5,7 @@ import (
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/trust"
|
||||
clitypes "github.com/docker/cli/types"
|
||||
"github.com/docker/distribution/reference"
|
||||
"github.com/docker/docker/api/types"
|
||||
registrytypes "github.com/docker/docker/api/types/registry"
|
||||
@ -13,7 +14,7 @@ import (
|
||||
|
||||
func getRegistryAuth(cli command.Cli, registryPrefix string) (*types.AuthConfig, error) {
|
||||
if registryPrefix == "" {
|
||||
registryPrefix = "docker.io/docker"
|
||||
registryPrefix = clitypes.RegistryPrefix
|
||||
}
|
||||
distributionRef, err := reference.ParseNormalizedNamed(registryPrefix)
|
||||
if err != nil {
|
||||
|
||||
@ -7,18 +7,16 @@ import (
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/cli/command/formatter"
|
||||
"github.com/docker/cli/internal/containerizedengine"
|
||||
"github.com/docker/cli/internal/versions"
|
||||
clitypes "github.com/docker/cli/types"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const (
|
||||
releaseNotePrefix = "https://docs.docker.com/releasenotes"
|
||||
)
|
||||
|
||||
type checkOptions struct {
|
||||
registryPrefix string
|
||||
preReleases bool
|
||||
engineImage string
|
||||
downgrades bool
|
||||
upgrades bool
|
||||
format string
|
||||
@ -38,9 +36,10 @@ func newCheckForUpdatesCommand(dockerCli command.Cli) *cobra.Command {
|
||||
},
|
||||
}
|
||||
flags := cmd.Flags()
|
||||
flags.StringVar(&options.registryPrefix, "registry-prefix", "", "Override the existing location where engine images are pulled")
|
||||
flags.StringVar(&options.registryPrefix, "registry-prefix", clitypes.RegistryPrefix, "Override the existing location where engine images are pulled")
|
||||
flags.BoolVar(&options.downgrades, "downgrades", false, "Report downgrades (default omits older versions)")
|
||||
flags.BoolVar(&options.preReleases, "pre-releases", false, "Include pre-release versions")
|
||||
flags.StringVar(&options.engineImage, "engine-image", "", "Specify engine image (default uses the same image as currently running)")
|
||||
flags.BoolVar(&options.upgrades, "upgrades", true, "Report available upgrades")
|
||||
flags.StringVar(&options.format, "format", "", "Pretty-print updates using a Go template")
|
||||
flags.BoolVarP(&options.quiet, "quiet", "q", false, "Only display available versions")
|
||||
@ -50,54 +49,47 @@ func newCheckForUpdatesCommand(dockerCli command.Cli) *cobra.Command {
|
||||
}
|
||||
|
||||
func runCheck(dockerCli command.Cli, options checkOptions) error {
|
||||
if !isRoot() {
|
||||
return errors.New("this command must be run as a privileged user")
|
||||
}
|
||||
ctx := context.Background()
|
||||
client, err := dockerCli.NewContainerizedEngineClient(options.sockPath)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "unable to access local containerd")
|
||||
}
|
||||
defer client.Close()
|
||||
currentOpts, err := client.GetCurrentEngineVersion(ctx)
|
||||
client := dockerCli.Client()
|
||||
serverVersion, err := client.ServerVersion(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// override with user provided prefix if specified
|
||||
if options.registryPrefix != "" {
|
||||
currentOpts.RegistryPrefix = options.registryPrefix
|
||||
}
|
||||
imageName := currentOpts.RegistryPrefix + "/" + currentOpts.EngineImage
|
||||
currentVersion := currentOpts.EngineVersion
|
||||
versions, err := client.GetEngineVersions(ctx, dockerCli.RegistryClient(false), currentVersion, imageName)
|
||||
availVersions, err := versions.GetEngineVersions(ctx, dockerCli.RegistryClient(false), options.registryPrefix, options.engineImage, serverVersion.Version)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
availUpdates := []containerizedengine.Update{
|
||||
{Type: "current", Version: currentVersion},
|
||||
availUpdates := []clitypes.Update{
|
||||
{Type: "current", Version: serverVersion.Version},
|
||||
}
|
||||
if len(versions.Patches) > 0 {
|
||||
if len(availVersions.Patches) > 0 {
|
||||
availUpdates = append(availUpdates,
|
||||
processVersions(
|
||||
currentVersion,
|
||||
serverVersion.Version,
|
||||
"patch",
|
||||
options.preReleases,
|
||||
versions.Patches)...)
|
||||
availVersions.Patches)...)
|
||||
}
|
||||
if options.upgrades {
|
||||
availUpdates = append(availUpdates,
|
||||
processVersions(
|
||||
currentVersion,
|
||||
serverVersion.Version,
|
||||
"upgrade",
|
||||
options.preReleases,
|
||||
versions.Upgrades)...)
|
||||
availVersions.Upgrades)...)
|
||||
}
|
||||
if options.downgrades {
|
||||
availUpdates = append(availUpdates,
|
||||
processVersions(
|
||||
currentVersion,
|
||||
serverVersion.Version,
|
||||
"downgrade",
|
||||
options.preReleases,
|
||||
versions.Downgrades)...)
|
||||
availVersions.Downgrades)...)
|
||||
}
|
||||
|
||||
format := options.format
|
||||
@ -115,17 +107,17 @@ func runCheck(dockerCli command.Cli, options checkOptions) error {
|
||||
|
||||
func processVersions(currentVersion, verType string,
|
||||
includePrerelease bool,
|
||||
versions []containerizedengine.DockerVersion) []containerizedengine.Update {
|
||||
availUpdates := []containerizedengine.Update{}
|
||||
for _, ver := range versions {
|
||||
availVersions []clitypes.DockerVersion) []clitypes.Update {
|
||||
availUpdates := []clitypes.Update{}
|
||||
for _, ver := range availVersions {
|
||||
if !includePrerelease && ver.Prerelease() != "" {
|
||||
continue
|
||||
}
|
||||
if ver.Tag != currentVersion {
|
||||
availUpdates = append(availUpdates, containerizedengine.Update{
|
||||
availUpdates = append(availUpdates, clitypes.Update{
|
||||
Type: verType,
|
||||
Version: ver.Tag,
|
||||
Notes: fmt.Sprintf("%s/%s", releaseNotePrefix, ver.Tag),
|
||||
Notes: fmt.Sprintf("%s/%s", clitypes.ReleaseNotePrefix, ver.Tag),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,11 +5,13 @@ import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
registryclient "github.com/docker/cli/cli/registry/client"
|
||||
"github.com/docker/cli/internal/containerizedengine"
|
||||
manifesttypes "github.com/docker/cli/cli/manifest/types"
|
||||
"github.com/docker/cli/internal/test"
|
||||
"github.com/docker/distribution"
|
||||
"github.com/docker/distribution/reference"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/client"
|
||||
ver "github.com/hashicorp/go-version"
|
||||
"github.com/opencontainers/go-digest"
|
||||
"gotest.tools/assert"
|
||||
"gotest.tools/golden"
|
||||
)
|
||||
@ -18,126 +20,95 @@ var (
|
||||
testCli = test.NewFakeCli(&client.Client{})
|
||||
)
|
||||
|
||||
func TestCheckForUpdatesNoContainerd(t *testing.T) {
|
||||
testCli.SetContainerizedEngineClient(
|
||||
func(string) (containerizedengine.Client, error) {
|
||||
return nil, fmt.Errorf("some error")
|
||||
},
|
||||
)
|
||||
cmd := newCheckForUpdatesCommand(testCli)
|
||||
cmd.SilenceUsage = true
|
||||
cmd.SilenceErrors = true
|
||||
err := cmd.Execute()
|
||||
assert.ErrorContains(t, err, "unable to access local containerd")
|
||||
type verClient struct {
|
||||
client.Client
|
||||
ver types.Version
|
||||
verErr error
|
||||
info types.Info
|
||||
infoErr error
|
||||
}
|
||||
|
||||
func (c *verClient) ServerVersion(ctx context.Context) (types.Version, error) {
|
||||
return c.ver, c.verErr
|
||||
}
|
||||
|
||||
func (c *verClient) Info(ctx context.Context) (types.Info, error) {
|
||||
return c.info, c.infoErr
|
||||
}
|
||||
|
||||
type testRegistryClient struct {
|
||||
tags []string
|
||||
}
|
||||
|
||||
func (c testRegistryClient) GetManifest(ctx context.Context, ref reference.Named) (manifesttypes.ImageManifest, error) {
|
||||
return manifesttypes.ImageManifest{}, nil
|
||||
}
|
||||
func (c testRegistryClient) GetManifestList(ctx context.Context, ref reference.Named) ([]manifesttypes.ImageManifest, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (c testRegistryClient) MountBlob(ctx context.Context, source reference.Canonical, target reference.Named) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c testRegistryClient) PutManifest(ctx context.Context, ref reference.Named, manifest distribution.Manifest) (digest.Digest, error) {
|
||||
return "", nil
|
||||
}
|
||||
func (c testRegistryClient) GetTags(ctx context.Context, ref reference.Named) ([]string, error) {
|
||||
return c.tags, nil
|
||||
}
|
||||
|
||||
func TestCheckForUpdatesNoCurrentVersion(t *testing.T) {
|
||||
retErr := fmt.Errorf("some failure")
|
||||
getCurrentEngineVersionFunc := func(ctx context.Context) (containerizedengine.EngineInitOptions, error) {
|
||||
return containerizedengine.EngineInitOptions{}, retErr
|
||||
}
|
||||
testCli.SetContainerizedEngineClient(
|
||||
func(string) (containerizedengine.Client, error) {
|
||||
return &fakeContainerizedEngineClient{
|
||||
getCurrentEngineVersionFunc: getCurrentEngineVersionFunc,
|
||||
}, nil
|
||||
},
|
||||
)
|
||||
cmd := newCheckForUpdatesCommand(testCli)
|
||||
isRoot = func() bool { return true }
|
||||
c := test.NewFakeCli(&verClient{client.Client{}, types.Version{}, nil, types.Info{}, nil})
|
||||
c.SetRegistryClient(testRegistryClient{})
|
||||
cmd := newCheckForUpdatesCommand(c)
|
||||
cmd.SilenceUsage = true
|
||||
cmd.SilenceErrors = true
|
||||
err := cmd.Execute()
|
||||
assert.Assert(t, err == retErr)
|
||||
}
|
||||
|
||||
func TestCheckForUpdatesGetEngineVersionsFail(t *testing.T) {
|
||||
retErr := fmt.Errorf("some failure")
|
||||
getEngineVersionsFunc := func(ctx context.Context,
|
||||
registryClient registryclient.RegistryClient,
|
||||
currentVersion, imageName string) (containerizedengine.AvailableVersions, error) {
|
||||
return containerizedengine.AvailableVersions{}, retErr
|
||||
}
|
||||
testCli.SetContainerizedEngineClient(
|
||||
func(string) (containerizedengine.Client, error) {
|
||||
return &fakeContainerizedEngineClient{
|
||||
getEngineVersionsFunc: getEngineVersionsFunc,
|
||||
}, nil
|
||||
},
|
||||
)
|
||||
cmd := newCheckForUpdatesCommand(testCli)
|
||||
cmd.SilenceUsage = true
|
||||
cmd.SilenceErrors = true
|
||||
err := cmd.Execute()
|
||||
assert.Assert(t, err == retErr)
|
||||
assert.ErrorContains(t, err, "no such file or directory")
|
||||
}
|
||||
|
||||
func TestCheckForUpdatesGetEngineVersionsHappy(t *testing.T) {
|
||||
getCurrentEngineVersionFunc := func(ctx context.Context) (containerizedengine.EngineInitOptions, error) {
|
||||
return containerizedengine.EngineInitOptions{
|
||||
EngineImage: "current engine",
|
||||
EngineVersion: "1.1.0",
|
||||
}, nil
|
||||
}
|
||||
getEngineVersionsFunc := func(ctx context.Context,
|
||||
registryClient registryclient.RegistryClient,
|
||||
currentVersion, imageName string) (containerizedengine.AvailableVersions, error) {
|
||||
return containerizedengine.AvailableVersions{
|
||||
Downgrades: parseVersions(t, "1.0.1", "1.0.2", "1.0.3-beta1"),
|
||||
Patches: parseVersions(t, "1.1.1", "1.1.2", "1.1.3-beta1"),
|
||||
Upgrades: parseVersions(t, "1.2.0", "2.0.0", "2.1.0-beta1"),
|
||||
}, nil
|
||||
}
|
||||
testCli.SetContainerizedEngineClient(
|
||||
func(string) (containerizedengine.Client, error) {
|
||||
return &fakeContainerizedEngineClient{
|
||||
getEngineVersionsFunc: getEngineVersionsFunc,
|
||||
getCurrentEngineVersionFunc: getCurrentEngineVersionFunc,
|
||||
}, nil
|
||||
},
|
||||
)
|
||||
cmd := newCheckForUpdatesCommand(testCli)
|
||||
c := test.NewFakeCli(&verClient{client.Client{}, types.Version{Version: "1.1.0"}, nil, types.Info{ServerVersion: "1.1.0"}, nil})
|
||||
c.SetRegistryClient(testRegistryClient{[]string{
|
||||
"1.0.1", "1.0.2", "1.0.3-beta1",
|
||||
"1.1.1", "1.1.2", "1.1.3-beta1",
|
||||
"1.2.0", "2.0.0", "2.1.0-beta1",
|
||||
}})
|
||||
|
||||
isRoot = func() bool { return true }
|
||||
cmd := newCheckForUpdatesCommand(c)
|
||||
cmd.Flags().Set("pre-releases", "true")
|
||||
cmd.Flags().Set("downgrades", "true")
|
||||
cmd.Flags().Set("engine-image", "engine-community")
|
||||
cmd.SilenceUsage = true
|
||||
cmd.SilenceErrors = true
|
||||
err := cmd.Execute()
|
||||
assert.NilError(t, err)
|
||||
golden.Assert(t, testCli.OutBuffer().String(), "check-all.golden")
|
||||
golden.Assert(t, c.OutBuffer().String(), "check-all.golden")
|
||||
|
||||
testCli.OutBuffer().Reset()
|
||||
c.OutBuffer().Reset()
|
||||
cmd.Flags().Set("pre-releases", "false")
|
||||
cmd.Flags().Set("downgrades", "true")
|
||||
err = cmd.Execute()
|
||||
assert.NilError(t, err)
|
||||
fmt.Println(testCli.OutBuffer().String())
|
||||
golden.Assert(t, testCli.OutBuffer().String(), "check-no-prerelease.golden")
|
||||
fmt.Println(c.OutBuffer().String())
|
||||
golden.Assert(t, c.OutBuffer().String(), "check-no-prerelease.golden")
|
||||
|
||||
testCli.OutBuffer().Reset()
|
||||
c.OutBuffer().Reset()
|
||||
cmd.Flags().Set("pre-releases", "false")
|
||||
cmd.Flags().Set("downgrades", "false")
|
||||
err = cmd.Execute()
|
||||
assert.NilError(t, err)
|
||||
fmt.Println(testCli.OutBuffer().String())
|
||||
golden.Assert(t, testCli.OutBuffer().String(), "check-no-downgrades.golden")
|
||||
fmt.Println(c.OutBuffer().String())
|
||||
golden.Assert(t, c.OutBuffer().String(), "check-no-downgrades.golden")
|
||||
|
||||
testCli.OutBuffer().Reset()
|
||||
c.OutBuffer().Reset()
|
||||
cmd.Flags().Set("pre-releases", "false")
|
||||
cmd.Flags().Set("downgrades", "false")
|
||||
cmd.Flags().Set("upgrades", "false")
|
||||
err = cmd.Execute()
|
||||
assert.NilError(t, err)
|
||||
fmt.Println(testCli.OutBuffer().String())
|
||||
golden.Assert(t, testCli.OutBuffer().String(), "check-patches-only.golden")
|
||||
}
|
||||
|
||||
func makeVersion(t *testing.T, tag string) containerizedengine.DockerVersion {
|
||||
v, err := ver.NewVersion(tag)
|
||||
assert.NilError(t, err)
|
||||
return containerizedengine.DockerVersion{Version: *v, Tag: tag}
|
||||
}
|
||||
|
||||
func parseVersions(t *testing.T, tags ...string) []containerizedengine.DockerVersion {
|
||||
ret := make([]containerizedengine.DockerVersion, len(tags))
|
||||
for i, tag := range tags {
|
||||
ret[i] = makeVersion(t, tag)
|
||||
}
|
||||
return ret
|
||||
fmt.Println(c.OutBuffer().String())
|
||||
golden.Assert(t, c.OutBuffer().String(), "check-patches-only.golden")
|
||||
}
|
||||
|
||||
@ -5,7 +5,7 @@ import (
|
||||
|
||||
"github.com/containerd/containerd"
|
||||
registryclient "github.com/docker/cli/cli/registry/client"
|
||||
"github.com/docker/cli/internal/containerizedengine"
|
||||
clitypes "github.com/docker/cli/types"
|
||||
"github.com/docker/docker/api/types"
|
||||
)
|
||||
|
||||
@ -13,28 +13,26 @@ type (
|
||||
fakeContainerizedEngineClient struct {
|
||||
closeFunc func() error
|
||||
activateEngineFunc func(ctx context.Context,
|
||||
opts containerizedengine.EngineInitOptions,
|
||||
out containerizedengine.OutStream,
|
||||
authConfig *types.AuthConfig,
|
||||
healthfn func(context.Context) error) error
|
||||
opts clitypes.EngineInitOptions,
|
||||
out clitypes.OutStream,
|
||||
authConfig *types.AuthConfig) error
|
||||
initEngineFunc func(ctx context.Context,
|
||||
opts containerizedengine.EngineInitOptions,
|
||||
out containerizedengine.OutStream,
|
||||
opts clitypes.EngineInitOptions,
|
||||
out clitypes.OutStream,
|
||||
authConfig *types.AuthConfig,
|
||||
healthfn func(context.Context) error) error
|
||||
doUpdateFunc func(ctx context.Context,
|
||||
opts containerizedengine.EngineInitOptions,
|
||||
out containerizedengine.OutStream,
|
||||
authConfig *types.AuthConfig,
|
||||
healthfn func(context.Context) error) error
|
||||
opts clitypes.EngineInitOptions,
|
||||
out clitypes.OutStream,
|
||||
authConfig *types.AuthConfig) error
|
||||
getEngineVersionsFunc func(ctx context.Context,
|
||||
registryClient registryclient.RegistryClient,
|
||||
currentVersion,
|
||||
imageName string) (containerizedengine.AvailableVersions, error)
|
||||
imageName string) (clitypes.AvailableVersions, error)
|
||||
|
||||
getEngineFunc func(ctx context.Context) (containerd.Container, error)
|
||||
removeEngineFunc func(ctx context.Context, engine containerd.Container) error
|
||||
getCurrentEngineVersionFunc func(ctx context.Context) (containerizedengine.EngineInitOptions, error)
|
||||
removeEngineFunc func(ctx context.Context) error
|
||||
getCurrentEngineVersionFunc func(ctx context.Context) (clitypes.EngineInitOptions, error)
|
||||
}
|
||||
)
|
||||
|
||||
@ -46,18 +44,17 @@ func (w *fakeContainerizedEngineClient) Close() error {
|
||||
}
|
||||
|
||||
func (w *fakeContainerizedEngineClient) ActivateEngine(ctx context.Context,
|
||||
opts containerizedengine.EngineInitOptions,
|
||||
out containerizedengine.OutStream,
|
||||
authConfig *types.AuthConfig,
|
||||
healthfn func(context.Context) error) error {
|
||||
opts clitypes.EngineInitOptions,
|
||||
out clitypes.OutStream,
|
||||
authConfig *types.AuthConfig) error {
|
||||
if w.activateEngineFunc != nil {
|
||||
return w.activateEngineFunc(ctx, opts, out, authConfig, healthfn)
|
||||
return w.activateEngineFunc(ctx, opts, out, authConfig)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (w *fakeContainerizedEngineClient) InitEngine(ctx context.Context,
|
||||
opts containerizedengine.EngineInitOptions,
|
||||
out containerizedengine.OutStream,
|
||||
opts clitypes.EngineInitOptions,
|
||||
out clitypes.OutStream,
|
||||
authConfig *types.AuthConfig,
|
||||
healthfn func(context.Context) error) error {
|
||||
if w.initEngineFunc != nil {
|
||||
@ -66,23 +63,22 @@ func (w *fakeContainerizedEngineClient) InitEngine(ctx context.Context,
|
||||
return nil
|
||||
}
|
||||
func (w *fakeContainerizedEngineClient) DoUpdate(ctx context.Context,
|
||||
opts containerizedengine.EngineInitOptions,
|
||||
out containerizedengine.OutStream,
|
||||
authConfig *types.AuthConfig,
|
||||
healthfn func(context.Context) error) error {
|
||||
opts clitypes.EngineInitOptions,
|
||||
out clitypes.OutStream,
|
||||
authConfig *types.AuthConfig) error {
|
||||
if w.doUpdateFunc != nil {
|
||||
return w.doUpdateFunc(ctx, opts, out, authConfig, healthfn)
|
||||
return w.doUpdateFunc(ctx, opts, out, authConfig)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (w *fakeContainerizedEngineClient) GetEngineVersions(ctx context.Context,
|
||||
registryClient registryclient.RegistryClient,
|
||||
currentVersion, imageName string) (containerizedengine.AvailableVersions, error) {
|
||||
currentVersion, imageName string) (clitypes.AvailableVersions, error) {
|
||||
|
||||
if w.getEngineVersionsFunc != nil {
|
||||
return w.getEngineVersionsFunc(ctx, registryClient, currentVersion, imageName)
|
||||
}
|
||||
return containerizedengine.AvailableVersions{}, nil
|
||||
return clitypes.AvailableVersions{}, nil
|
||||
}
|
||||
|
||||
func (w *fakeContainerizedEngineClient) GetEngine(ctx context.Context) (containerd.Container, error) {
|
||||
@ -91,15 +87,15 @@ func (w *fakeContainerizedEngineClient) GetEngine(ctx context.Context) (containe
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
func (w *fakeContainerizedEngineClient) RemoveEngine(ctx context.Context, engine containerd.Container) error {
|
||||
func (w *fakeContainerizedEngineClient) RemoveEngine(ctx context.Context) error {
|
||||
if w.removeEngineFunc != nil {
|
||||
return w.removeEngineFunc(ctx, engine)
|
||||
return w.removeEngineFunc(ctx)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (w *fakeContainerizedEngineClient) GetCurrentEngineVersion(ctx context.Context) (containerizedengine.EngineInitOptions, error) {
|
||||
func (w *fakeContainerizedEngineClient) GetCurrentEngineVersion(ctx context.Context) (clitypes.EngineInitOptions, error) {
|
||||
if w.getCurrentEngineVersionFunc != nil {
|
||||
return w.getCurrentEngineVersionFunc(ctx)
|
||||
}
|
||||
return containerizedengine.EngineInitOptions{}, nil
|
||||
return clitypes.EngineInitOptions{}, nil
|
||||
}
|
||||
|
||||
@ -15,11 +15,9 @@ func NewEngineCommand(dockerCli command.Cli) *cobra.Command {
|
||||
RunE: command.ShowHelp(dockerCli.Err()),
|
||||
}
|
||||
cmd.AddCommand(
|
||||
newInitCommand(dockerCli),
|
||||
newActivateCommand(dockerCli),
|
||||
newCheckForUpdatesCommand(dockerCli),
|
||||
newUpdateCommand(dockerCli),
|
||||
newRmCommand(dockerCli),
|
||||
)
|
||||
return cmd
|
||||
}
|
||||
|
||||
@ -10,5 +10,5 @@ func TestNewEngineCommand(t *testing.T) {
|
||||
cmd := NewEngineCommand(testCli)
|
||||
|
||||
subcommands := cmd.Commands()
|
||||
assert.Assert(t, len(subcommands) == 5)
|
||||
assert.Assert(t, len(subcommands) == 3)
|
||||
}
|
||||
|
||||
@ -1,62 +1,10 @@
|
||||
package engine
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/internal/containerizedengine"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
clitypes "github.com/docker/cli/types"
|
||||
)
|
||||
|
||||
type extendedEngineInitOptions struct {
|
||||
containerizedengine.EngineInitOptions
|
||||
clitypes.EngineInitOptions
|
||||
sockPath string
|
||||
}
|
||||
|
||||
func newInitCommand(dockerCli command.Cli) *cobra.Command {
|
||||
var options extendedEngineInitOptions
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "init [OPTIONS]",
|
||||
Short: "Initialize a local engine",
|
||||
Long: `This command will initialize a local engine running on containerd.
|
||||
|
||||
Configuration of the engine is managed through the daemon.json configuration
|
||||
file on the host and may be pre-created before running the 'init' command.
|
||||
`,
|
||||
Args: cli.NoArgs,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runInit(dockerCli, options)
|
||||
},
|
||||
Annotations: map[string]string{"experimentalCLI": ""},
|
||||
}
|
||||
flags := cmd.Flags()
|
||||
flags.StringVar(&options.EngineVersion, "version", cli.Version, "Specify engine version")
|
||||
flags.StringVar(&options.EngineImage, "engine-image", containerizedengine.CommunityEngineImage, "Specify engine image")
|
||||
flags.StringVar(&options.RegistryPrefix, "registry-prefix", "docker.io/docker", "Override the default location where engine images are pulled")
|
||||
flags.StringVar(&options.ConfigFile, "config-file", "/etc/docker/daemon.json", "Specify the location of the daemon configuration file on the host")
|
||||
flags.StringVar(&options.sockPath, "containerd", "", "override default location of containerd endpoint")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runInit(dockerCli command.Cli, options extendedEngineInitOptions) error {
|
||||
ctx := context.Background()
|
||||
client, err := dockerCli.NewContainerizedEngineClient(options.sockPath)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "unable to access local containerd")
|
||||
}
|
||||
defer client.Close()
|
||||
authConfig, err := getRegistryAuth(dockerCli, options.RegistryPrefix)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return client.InitEngine(ctx, options.EngineInitOptions, dockerCli.Out(), authConfig,
|
||||
func(ctx context.Context) error {
|
||||
client := dockerCli.Client()
|
||||
_, err := client.Ping(ctx)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
@ -1,33 +0,0 @@
|
||||
package engine
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/internal/containerizedengine"
|
||||
"gotest.tools/assert"
|
||||
)
|
||||
|
||||
func TestInitNoContainerd(t *testing.T) {
|
||||
testCli.SetContainerizedEngineClient(
|
||||
func(string) (containerizedengine.Client, error) {
|
||||
return nil, fmt.Errorf("some error")
|
||||
},
|
||||
)
|
||||
cmd := newInitCommand(testCli)
|
||||
cmd.SilenceUsage = true
|
||||
cmd.SilenceErrors = true
|
||||
err := cmd.Execute()
|
||||
assert.ErrorContains(t, err, "unable to access local containerd")
|
||||
}
|
||||
|
||||
func TestInitHappy(t *testing.T) {
|
||||
testCli.SetContainerizedEngineClient(
|
||||
func(string) (containerizedengine.Client, error) {
|
||||
return &fakeContainerizedEngineClient{}, nil
|
||||
},
|
||||
)
|
||||
cmd := newInitCommand(testCli)
|
||||
err := cmd.Execute()
|
||||
assert.NilError(t, err)
|
||||
}
|
||||
@ -1,54 +0,0 @@
|
||||
package engine
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// TODO - consider adding a "purge" flag that also removes
|
||||
// configuration files and the docker root dir.
|
||||
|
||||
type rmOptions struct {
|
||||
sockPath string
|
||||
}
|
||||
|
||||
func newRmCommand(dockerCli command.Cli) *cobra.Command {
|
||||
var options rmOptions
|
||||
cmd := &cobra.Command{
|
||||
Use: "rm [OPTIONS]",
|
||||
Short: "Remove the local engine",
|
||||
Long: `This command will remove the local engine running on containerd.
|
||||
|
||||
No state files will be removed from the host filesystem.
|
||||
`,
|
||||
Args: cli.NoArgs,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return runRm(dockerCli, options)
|
||||
},
|
||||
Annotations: map[string]string{"experimentalCLI": ""},
|
||||
}
|
||||
flags := cmd.Flags()
|
||||
flags.StringVar(&options.sockPath, "containerd", "", "override default location of containerd endpoint")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runRm(dockerCli command.Cli, options rmOptions) error {
|
||||
ctx := context.Background()
|
||||
client, err := dockerCli.NewContainerizedEngineClient(options.sockPath)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "unable to access local containerd")
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
engine, err := client.GetEngine(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return client.RemoveEngine(ctx, engine)
|
||||
}
|
||||
@ -1,33 +0,0 @@
|
||||
package engine
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/internal/containerizedengine"
|
||||
"gotest.tools/assert"
|
||||
)
|
||||
|
||||
func TestRmNoContainerd(t *testing.T) {
|
||||
testCli.SetContainerizedEngineClient(
|
||||
func(string) (containerizedengine.Client, error) {
|
||||
return nil, fmt.Errorf("some error")
|
||||
},
|
||||
)
|
||||
cmd := newRmCommand(testCli)
|
||||
cmd.SilenceUsage = true
|
||||
cmd.SilenceErrors = true
|
||||
err := cmd.Execute()
|
||||
assert.ErrorContains(t, err, "unable to access local containerd")
|
||||
}
|
||||
|
||||
func TestRmHappy(t *testing.T) {
|
||||
testCli.SetContainerizedEngineClient(
|
||||
func(string) (containerizedengine.Client, error) {
|
||||
return &fakeContainerizedEngineClient{}, nil
|
||||
},
|
||||
)
|
||||
cmd := newRmCommand(testCli)
|
||||
err := cmd.Execute()
|
||||
assert.NilError(t, err)
|
||||
}
|
||||
3
cli/command/engine/testdata/expired-hub-license-display-only.golden
vendored
Normal file
3
cli/command/engine/testdata/expired-hub-license-display-only.golden
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
Looking for existing licenses for ...
|
||||
NUM OWNER PRODUCT ID EXPIRES PRICING COMPONENTS
|
||||
0 2010-01-01 00:00:00 +0000 UTC
|
||||
1
cli/command/engine/testdata/expired-license-display-only.golden
vendored
Normal file
1
cli/command/engine/testdata/expired-license-display-only.golden
vendored
Normal file
@ -0,0 +1 @@
|
||||
License: Quantity: 1 Nodes Expiration date: 2018-03-18 Expired! You will no longer receive updates. Please renew at https://docker.com/licensing
|
||||
@ -6,6 +6,7 @@ import (
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
clitypes "github.com/docker/cli/types"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
@ -24,45 +25,31 @@ func newUpdateCommand(dockerCli command.Cli) *cobra.Command {
|
||||
flags := cmd.Flags()
|
||||
|
||||
flags.StringVar(&options.EngineVersion, "version", "", "Specify engine version")
|
||||
flags.StringVar(&options.EngineImage, "engine-image", "", "Specify engine image")
|
||||
flags.StringVar(&options.RegistryPrefix, "registry-prefix", "", "Override the current location where engine images are pulled")
|
||||
flags.StringVar(&options.EngineImage, "engine-image", "", "Specify engine image (default uses the same image as currently running)")
|
||||
flags.StringVar(&options.RegistryPrefix, "registry-prefix", clitypes.RegistryPrefix, "Override the current location where engine images are pulled")
|
||||
flags.StringVar(&options.sockPath, "containerd", "", "override default location of containerd endpoint")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runUpdate(dockerCli command.Cli, options extendedEngineInitOptions) error {
|
||||
if !isRoot() {
|
||||
return errors.New("this command must be run as a privileged user")
|
||||
}
|
||||
ctx := context.Background()
|
||||
client, err := dockerCli.NewContainerizedEngineClient(options.sockPath)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "unable to access local containerd")
|
||||
}
|
||||
defer client.Close()
|
||||
if options.EngineImage == "" || options.RegistryPrefix == "" {
|
||||
currentOpts, err := client.GetCurrentEngineVersion(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if options.EngineImage == "" {
|
||||
options.EngineImage = currentOpts.EngineImage
|
||||
}
|
||||
if options.RegistryPrefix == "" {
|
||||
options.RegistryPrefix = currentOpts.RegistryPrefix
|
||||
}
|
||||
}
|
||||
authConfig, err := getRegistryAuth(dockerCli, options.RegistryPrefix)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := client.DoUpdate(ctx, options.EngineInitOptions, dockerCli.Out(), authConfig,
|
||||
func(ctx context.Context) error {
|
||||
client := dockerCli.Client()
|
||||
_, err := client.Ping(ctx)
|
||||
return err
|
||||
}); err != nil {
|
||||
if err := client.DoUpdate(ctx, options.EngineInitOptions, dockerCli.Out(), authConfig); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintln(dockerCli.Out(), "Success! The docker engine is now running.")
|
||||
fmt.Fprintln(dockerCli.Out(), `Successfully updated engine.
|
||||
Restart docker with 'systemctl restart docker' to complete the update.`)
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -4,13 +4,16 @@ import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/internal/containerizedengine"
|
||||
"github.com/docker/cli/internal/test"
|
||||
clitypes "github.com/docker/cli/types"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/client"
|
||||
"gotest.tools/assert"
|
||||
)
|
||||
|
||||
func TestUpdateNoContainerd(t *testing.T) {
|
||||
testCli.SetContainerizedEngineClient(
|
||||
func(string) (containerizedengine.Client, error) {
|
||||
func(string) (clitypes.ContainerizedClient, error) {
|
||||
return nil, fmt.Errorf("some error")
|
||||
},
|
||||
)
|
||||
@ -22,14 +25,16 @@ func TestUpdateNoContainerd(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestUpdateHappy(t *testing.T) {
|
||||
testCli.SetContainerizedEngineClient(
|
||||
func(string) (containerizedengine.Client, error) {
|
||||
c := test.NewFakeCli(&verClient{client.Client{}, types.Version{Version: "1.1.0"}, nil, types.Info{ServerVersion: "1.1.0"}, nil})
|
||||
c.SetContainerizedEngineClient(
|
||||
func(string) (clitypes.ContainerizedClient, error) {
|
||||
return &fakeContainerizedEngineClient{}, nil
|
||||
},
|
||||
)
|
||||
cmd := newUpdateCommand(testCli)
|
||||
cmd.Flags().Set("registry-prefix", "docker.io/docker")
|
||||
cmd := newUpdateCommand(c)
|
||||
cmd.Flags().Set("registry-prefix", clitypes.RegistryPrefix)
|
||||
cmd.Flags().Set("version", "someversion")
|
||||
cmd.Flags().Set("engine-image", "someimage")
|
||||
err := cmd.Execute()
|
||||
assert.NilError(t, err)
|
||||
}
|
||||
|
||||
@ -15,6 +15,7 @@ const (
|
||||
defaultBuildCacheTableFormat = "table {{.ID}}\t{{.Type}}\t{{.Size}}\t{{.CreatedSince}}\t{{.LastUsedSince}}\t{{.UsageCount}}\t{{.Shared}}\t{{.Description}}"
|
||||
|
||||
cacheIDHeader = "CACHE ID"
|
||||
cacheTypeHeader = "CACHE TYPE"
|
||||
parentHeader = "PARENT"
|
||||
lastUsedSinceHeader = "LAST USED"
|
||||
usageCountHeader = "USAGE"
|
||||
@ -36,10 +37,12 @@ func NewBuildCacheFormat(source string, quiet bool) Format {
|
||||
}
|
||||
format := `build_cache_id: {{.ID}}
|
||||
parent_id: {{.Parent}}
|
||||
type: {{.Type}}
|
||||
build_cache_type: {{.CacheType}}
|
||||
description: {{.Description}}
|
||||
created_at: {{.CreatedSince}}
|
||||
last_used_at: {{.LastUsedSince}}
|
||||
created_at: {{.CreatedAt}}
|
||||
created_since: {{.CreatedSince}}
|
||||
last_used_at: {{.LastUsedAt}}
|
||||
last_used_since: {{.LastUsedSince}}
|
||||
usage_count: {{.UsageCount}}
|
||||
in_use: {{.InUse}}
|
||||
shared: {{.Shared}}
|
||||
@ -95,7 +98,7 @@ func newBuildCacheContext() *buildCacheContext {
|
||||
buildCacheCtx.header = buildCacheHeaderContext{
|
||||
"ID": cacheIDHeader,
|
||||
"Parent": parentHeader,
|
||||
"Type": typeHeader,
|
||||
"CacheType": cacheTypeHeader,
|
||||
"Size": sizeHeader,
|
||||
"CreatedSince": createdSinceHeader,
|
||||
"LastUsedSince": lastUsedSinceHeader,
|
||||
@ -129,7 +132,7 @@ func (c *buildCacheContext) Parent() string {
|
||||
return c.v.Parent
|
||||
}
|
||||
|
||||
func (c *buildCacheContext) Type() string {
|
||||
func (c *buildCacheContext) CacheType() string {
|
||||
return c.v.Type
|
||||
}
|
||||
|
||||
@ -141,10 +144,21 @@ func (c *buildCacheContext) Size() string {
|
||||
return units.HumanSizeWithPrecision(float64(c.v.Size), 3)
|
||||
}
|
||||
|
||||
func (c *buildCacheContext) CreatedAt() string {
|
||||
return c.v.CreatedAt.String()
|
||||
}
|
||||
|
||||
func (c *buildCacheContext) CreatedSince() string {
|
||||
return units.HumanDuration(time.Now().UTC().Sub(c.v.CreatedAt)) + " ago"
|
||||
}
|
||||
|
||||
func (c *buildCacheContext) LastUsedAt() string {
|
||||
if c.v.LastUsedAt == nil {
|
||||
return ""
|
||||
}
|
||||
return c.v.LastUsedAt.String()
|
||||
}
|
||||
|
||||
func (c *buildCacheContext) LastUsedSince() string {
|
||||
if c.v.LastUsedAt == nil {
|
||||
return ""
|
||||
|
||||
@ -15,8 +15,8 @@ const (
|
||||
defaultDiskUsageImageTableFormat = "table {{.Repository}}\t{{.Tag}}\t{{.ID}}\t{{.CreatedSince}}\t{{.VirtualSize}}\t{{.SharedSize}}\t{{.UniqueSize}}\t{{.Containers}}"
|
||||
defaultDiskUsageContainerTableFormat = "table {{.ID}}\t{{.Image}}\t{{.Command}}\t{{.LocalVolumes}}\t{{.Size}}\t{{.RunningFor}}\t{{.Status}}\t{{.Names}}"
|
||||
defaultDiskUsageVolumeTableFormat = "table {{.Name}}\t{{.Links}}\t{{.Size}}"
|
||||
defaultDiskUsageBuildCacheTableFormat = "table {{.ID}}\t{{.CacheType}}\t{{.Size}}\t{{.CreatedSince}}\t{{.LastUsedSince}}\t{{.UsageCount}}\t{{.Shared}}"
|
||||
defaultDiskUsageTableFormat = "table {{.Type}}\t{{.TotalCount}}\t{{.Active}}\t{{.Size}}\t{{.Reclaimable}}"
|
||||
defaultDiskUsageBuildCacheTableFormat = "table {{.ID}}\t{{.Type}}\t{{.Size}}\t{{.CreatedSince}}\t{{.LastUsedSince}}\t{{.UsageCount}}\t{{.Shared}}"
|
||||
|
||||
typeHeader = "TYPE"
|
||||
totalHeader = "TOTAL"
|
||||
@ -49,12 +49,25 @@ func (ctx *DiskUsageContext) startSubsection(format string) (*template.Template,
|
||||
}
|
||||
|
||||
// NewDiskUsageFormat returns a format for rendering an DiskUsageContext
|
||||
func NewDiskUsageFormat(source string) Format {
|
||||
switch source {
|
||||
case TableFormatKey:
|
||||
format := defaultDiskUsageTableFormat
|
||||
return Format(format)
|
||||
case RawFormatKey:
|
||||
func NewDiskUsageFormat(source string, verbose bool) Format {
|
||||
switch {
|
||||
case verbose && source == RawFormatKey:
|
||||
format := `{{range .Images}}type: Image
|
||||
` + NewImageFormat(source, false, true) + `
|
||||
{{end -}}
|
||||
{{range .Containers}}type: Container
|
||||
` + NewContainerFormat(source, false, true) + `
|
||||
{{end -}}
|
||||
{{range .Volumes}}type: Volume
|
||||
` + NewVolumeFormat(source, false) + `
|
||||
{{end -}}
|
||||
{{range .BuildCache}}type: Build Cache
|
||||
` + NewBuildCacheFormat(source, false) + `
|
||||
{{end -}}`
|
||||
return format
|
||||
case !verbose && source == TableFormatKey:
|
||||
return Format(defaultDiskUsageTableFormat)
|
||||
case !verbose && source == RawFormatKey:
|
||||
format := `type: {{.Type}}
|
||||
total: {{.TotalCount}}
|
||||
active: {{.Active}}
|
||||
@ -62,8 +75,9 @@ size: {{.Size}}
|
||||
reclaimable: {{.Reclaimable}}
|
||||
`
|
||||
return Format(format)
|
||||
default:
|
||||
return Format(source)
|
||||
}
|
||||
return Format(source)
|
||||
}
|
||||
|
||||
func (ctx *DiskUsageContext) Write() (err error) {
|
||||
@ -120,15 +134,23 @@ func (ctx *DiskUsageContext) Write() (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
// nolint: gocyclo
|
||||
func (ctx *DiskUsageContext) verboseWrite() error {
|
||||
// First images
|
||||
tmpl, err := ctx.startSubsection(defaultDiskUsageImageTableFormat)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
type diskUsageContext struct {
|
||||
Images []*imageContext
|
||||
Containers []*containerContext
|
||||
Volumes []*volumeContext
|
||||
BuildCache []*buildCacheContext
|
||||
}
|
||||
|
||||
ctx.Output.Write([]byte("Images space usage:\n\n"))
|
||||
func (ctx *DiskUsageContext) verboseWrite() error {
|
||||
duc := &diskUsageContext{
|
||||
Images: make([]*imageContext, 0, len(ctx.Images)),
|
||||
Containers: make([]*containerContext, 0, len(ctx.Containers)),
|
||||
Volumes: make([]*volumeContext, 0, len(ctx.Volumes)),
|
||||
BuildCache: make([]*buildCacheContext, 0, len(ctx.BuildCache)),
|
||||
}
|
||||
trunc := ctx.Format.IsTable()
|
||||
|
||||
// First images
|
||||
for _, i := range ctx.Images {
|
||||
repo := "<none>"
|
||||
tag := "<none>"
|
||||
@ -144,57 +166,88 @@ func (ctx *DiskUsageContext) verboseWrite() error {
|
||||
}
|
||||
}
|
||||
|
||||
err := ctx.contextFormat(tmpl, &imageContext{
|
||||
duc.Images = append(duc.Images, &imageContext{
|
||||
repo: repo,
|
||||
tag: tag,
|
||||
trunc: true,
|
||||
trunc: trunc,
|
||||
i: *i,
|
||||
})
|
||||
if err != nil {
|
||||
}
|
||||
|
||||
// Now containers
|
||||
for _, c := range ctx.Containers {
|
||||
// Don't display the virtual size
|
||||
c.SizeRootFs = 0
|
||||
duc.Containers = append(duc.Containers, &containerContext{trunc: trunc, c: *c})
|
||||
}
|
||||
|
||||
// And volumes
|
||||
for _, v := range ctx.Volumes {
|
||||
duc.Volumes = append(duc.Volumes, &volumeContext{v: *v})
|
||||
}
|
||||
|
||||
// And build cache
|
||||
buildCacheSort(ctx.BuildCache)
|
||||
for _, v := range ctx.BuildCache {
|
||||
duc.BuildCache = append(duc.BuildCache, &buildCacheContext{v: v, trunc: trunc})
|
||||
}
|
||||
|
||||
if ctx.Format == TableFormatKey {
|
||||
return ctx.verboseWriteTable(duc)
|
||||
}
|
||||
|
||||
ctx.preFormat()
|
||||
tmpl, err := ctx.parseFormat()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return tmpl.Execute(ctx.Output, duc)
|
||||
}
|
||||
|
||||
func (ctx *DiskUsageContext) verboseWriteTable(duc *diskUsageContext) error {
|
||||
tmpl, err := ctx.startSubsection(defaultDiskUsageImageTableFormat)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ctx.Output.Write([]byte("Images space usage:\n\n"))
|
||||
for _, img := range duc.Images {
|
||||
if err := ctx.contextFormat(tmpl, img); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
ctx.postFormat(tmpl, newImageContext())
|
||||
|
||||
// Now containers
|
||||
ctx.Output.Write([]byte("\nContainers space usage:\n\n"))
|
||||
tmpl, err = ctx.startSubsection(defaultDiskUsageContainerTableFormat)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, c := range ctx.Containers {
|
||||
// Don't display the virtual size
|
||||
c.SizeRootFs = 0
|
||||
err := ctx.contextFormat(tmpl, &containerContext{trunc: true, c: *c})
|
||||
if err != nil {
|
||||
ctx.Output.Write([]byte("\nContainers space usage:\n\n"))
|
||||
for _, c := range duc.Containers {
|
||||
if err := ctx.contextFormat(tmpl, c); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
ctx.postFormat(tmpl, newContainerContext())
|
||||
|
||||
// And volumes
|
||||
ctx.Output.Write([]byte("\nLocal Volumes space usage:\n\n"))
|
||||
tmpl, err = ctx.startSubsection(defaultDiskUsageVolumeTableFormat)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, v := range ctx.Volumes {
|
||||
if err := ctx.contextFormat(tmpl, &volumeContext{v: *v}); err != nil {
|
||||
ctx.Output.Write([]byte("\nLocal Volumes space usage:\n\n"))
|
||||
for _, v := range duc.Volumes {
|
||||
if err := ctx.contextFormat(tmpl, v); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
ctx.postFormat(tmpl, newVolumeContext())
|
||||
|
||||
// And build cache
|
||||
fmt.Fprintf(ctx.Output, "\nBuild cache usage: %s\n\n", units.HumanSize(float64(ctx.BuilderSize)))
|
||||
|
||||
tmpl, err = ctx.startSubsection(defaultDiskUsageBuildCacheTableFormat)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
buildCacheSort(ctx.BuildCache)
|
||||
for _, v := range ctx.BuildCache {
|
||||
if err := ctx.contextFormat(tmpl, &buildCacheContext{v: v, trunc: true}); err != nil {
|
||||
fmt.Fprintf(ctx.Output, "\nBuild cache usage: %s\n\n", units.HumanSize(float64(ctx.BuilderSize)))
|
||||
for _, v := range duc.BuildCache {
|
||||
if err := ctx.contextFormat(tmpl, v); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
@ -18,7 +18,7 @@ func TestDiskUsageContextFormatWrite(t *testing.T) {
|
||||
{
|
||||
DiskUsageContext{
|
||||
Context: Context{
|
||||
Format: NewDiskUsageFormat("table"),
|
||||
Format: NewDiskUsageFormat("table", false),
|
||||
},
|
||||
Verbose: false},
|
||||
`TYPE TOTAL ACTIVE SIZE RECLAIMABLE
|
||||
@ -29,7 +29,7 @@ Build Cache 0 0 0B
|
||||
`,
|
||||
},
|
||||
{
|
||||
DiskUsageContext{Verbose: true},
|
||||
DiskUsageContext{Verbose: true, Context: Context{Format: NewDiskUsageFormat("table", true)}},
|
||||
`Images space usage:
|
||||
|
||||
REPOSITORY TAG IMAGE ID CREATED SIZE SHARED SIZE UNIQUE SIZE CONTAINERS
|
||||
@ -44,9 +44,17 @@ VOLUME NAME LINKS SIZE
|
||||
|
||||
Build cache usage: 0B
|
||||
|
||||
CACHE ID TYPE SIZE CREATED LAST USED USAGE SHARED
|
||||
CACHE ID CACHE TYPE SIZE CREATED LAST USED USAGE SHARED
|
||||
`,
|
||||
},
|
||||
{
|
||||
DiskUsageContext{Verbose: true, Context: Context{Format: NewDiskUsageFormat("raw", true)}},
|
||||
``,
|
||||
},
|
||||
{
|
||||
DiskUsageContext{Verbose: true, Context: Context{Format: NewDiskUsageFormat("{{json .}}", true)}},
|
||||
`{"Images":[],"Containers":[],"Volumes":[],"BuildCache":[]}`,
|
||||
},
|
||||
// Errors
|
||||
{
|
||||
DiskUsageContext{
|
||||
@ -70,7 +78,7 @@ CACHE ID TYPE SIZE CREATED
|
||||
{
|
||||
DiskUsageContext{
|
||||
Context: Context{
|
||||
Format: NewDiskUsageFormat("table"),
|
||||
Format: NewDiskUsageFormat("table", false),
|
||||
},
|
||||
},
|
||||
`TYPE TOTAL ACTIVE SIZE RECLAIMABLE
|
||||
@ -83,7 +91,7 @@ Build Cache 0 0 0B
|
||||
{
|
||||
DiskUsageContext{
|
||||
Context: Context{
|
||||
Format: NewDiskUsageFormat("table {{.Type}}\t{{.Active}}"),
|
||||
Format: NewDiskUsageFormat("table {{.Type}}\t{{.Active}}", false),
|
||||
},
|
||||
},
|
||||
string(golden.Get(t, "disk-usage-context-write-custom.golden")),
|
||||
@ -92,7 +100,7 @@ Build Cache 0 0 0B
|
||||
{
|
||||
DiskUsageContext{
|
||||
Context: Context{
|
||||
Format: NewDiskUsageFormat("raw"),
|
||||
Format: NewDiskUsageFormat("raw", false),
|
||||
},
|
||||
},
|
||||
string(golden.Get(t, "disk-usage-raw-format.golden")),
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
package formatter
|
||||
|
||||
import (
|
||||
"github.com/docker/cli/internal/containerizedengine"
|
||||
clitypes "github.com/docker/cli/types"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -31,7 +31,7 @@ func NewUpdatesFormat(source string, quiet bool) Format {
|
||||
}
|
||||
|
||||
// UpdatesWrite writes the context
|
||||
func UpdatesWrite(ctx Context, availableUpdates []containerizedengine.Update) error {
|
||||
func UpdatesWrite(ctx Context, availableUpdates []clitypes.Update) error {
|
||||
render := func(format func(subContext subContext) error) error {
|
||||
for _, update := range availableUpdates {
|
||||
updatesCtx := &updateContext{trunc: ctx.Trunc, u: update}
|
||||
@ -53,7 +53,7 @@ func UpdatesWrite(ctx Context, availableUpdates []containerizedengine.Update) er
|
||||
type updateContext struct {
|
||||
HeaderContext
|
||||
trunc bool
|
||||
u containerizedengine.Update
|
||||
u clitypes.Update
|
||||
}
|
||||
|
||||
func (c *updateContext) MarshalJSON() ([]byte, error) {
|
||||
|
||||
@ -6,7 +6,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/internal/containerizedengine"
|
||||
clitypes "github.com/docker/cli/types"
|
||||
"gotest.tools/assert"
|
||||
is "gotest.tools/assert/cmp"
|
||||
)
|
||||
@ -84,7 +84,7 @@ version2
|
||||
}
|
||||
|
||||
for _, testcase := range cases {
|
||||
updates := []containerizedengine.Update{
|
||||
updates := []clitypes.Update{
|
||||
{Type: "updateType1", Version: "version1", Notes: "description 1"},
|
||||
{Type: "updateType2", Version: "version2", Notes: "description 2"},
|
||||
}
|
||||
@ -100,7 +100,7 @@ version2
|
||||
}
|
||||
|
||||
func TestUpdateContextWriteJSON(t *testing.T) {
|
||||
updates := []containerizedengine.Update{
|
||||
updates := []clitypes.Update{
|
||||
{Type: "updateType1", Version: "version1", Notes: "note1"},
|
||||
{Type: "updateType2", Version: "version2", Notes: "note2"},
|
||||
}
|
||||
@ -124,7 +124,7 @@ func TestUpdateContextWriteJSON(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestUpdateContextWriteJSONField(t *testing.T) {
|
||||
updates := []containerizedengine.Update{
|
||||
updates := []clitypes.Update{
|
||||
{Type: "updateType1", Version: "version1"},
|
||||
{Type: "updateType2", Version: "version2"},
|
||||
}
|
||||
|
||||
@ -19,6 +19,7 @@ type importOptions struct {
|
||||
reference string
|
||||
changes dockeropts.ListOpts
|
||||
message string
|
||||
platform string
|
||||
}
|
||||
|
||||
// NewImportCommand creates a new `docker import` command
|
||||
@ -43,6 +44,7 @@ func NewImportCommand(dockerCli command.Cli) *cobra.Command {
|
||||
options.changes = dockeropts.NewListOpts(nil)
|
||||
flags.VarP(&options.changes, "change", "c", "Apply Dockerfile instruction to the created image")
|
||||
flags.StringVarP(&options.message, "message", "m", "", "Set commit message for imported image")
|
||||
command.AddPlatformFlag(flags, &options.platform)
|
||||
|
||||
return cmd
|
||||
}
|
||||
@ -71,8 +73,9 @@ func runImport(dockerCli command.Cli, options importOptions) error {
|
||||
}
|
||||
|
||||
importOptions := types.ImageImportOptions{
|
||||
Message: options.message,
|
||||
Changes: options.changes.GetAll(),
|
||||
Message: options.message,
|
||||
Changes: options.changes.GetAll(),
|
||||
Platform: options.platform,
|
||||
}
|
||||
|
||||
clnt := dockerCli.Client()
|
||||
|
||||
@ -2,7 +2,6 @@ package system
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli/command"
|
||||
@ -38,10 +37,6 @@ func newDiskUsageCommand(dockerCli command.Cli) *cobra.Command {
|
||||
}
|
||||
|
||||
func runDiskUsage(dockerCli command.Cli, opts diskUsageOptions) error {
|
||||
if opts.verbose && len(opts.format) != 0 {
|
||||
return errors.New("the verbose and the format options conflict")
|
||||
}
|
||||
|
||||
du, err := dockerCli.Client().DiskUsage(context.Background())
|
||||
if err != nil {
|
||||
return err
|
||||
@ -62,7 +57,7 @@ func runDiskUsage(dockerCli command.Cli, opts diskUsageOptions) error {
|
||||
duCtx := formatter.DiskUsageContext{
|
||||
Context: formatter.Context{
|
||||
Output: dockerCli.Out(),
|
||||
Format: formatter.NewDiskUsageFormat(format),
|
||||
Format: formatter.NewDiskUsageFormat(format, opts.verbose),
|
||||
},
|
||||
LayersSize: du.LayersSize,
|
||||
BuilderSize: bsz,
|
||||
|
||||
@ -176,15 +176,21 @@ func extractVariable(value interface{}, pattern *regexp.Regexp) ([]extractedValu
|
||||
|
||||
// Soft default (fall back if unset or empty)
|
||||
func softDefault(substitution string, mapping Mapping) (string, bool, error) {
|
||||
return withDefault(substitution, mapping, "-:")
|
||||
sep := ":-"
|
||||
if !strings.Contains(substitution, sep) {
|
||||
return "", false, nil
|
||||
}
|
||||
name, defaultValue := partition(substitution, sep)
|
||||
value, ok := mapping(name)
|
||||
if !ok || value == "" {
|
||||
return defaultValue, true, nil
|
||||
}
|
||||
return value, true, nil
|
||||
}
|
||||
|
||||
// Hard default (fall back if-and-only-if empty)
|
||||
func hardDefault(substitution string, mapping Mapping) (string, bool, error) {
|
||||
return withDefault(substitution, mapping, "-")
|
||||
}
|
||||
|
||||
func withDefault(substitution string, mapping Mapping, sep string) (string, bool, error) {
|
||||
sep := "-"
|
||||
if !strings.Contains(substitution, sep) {
|
||||
return "", false, nil
|
||||
}
|
||||
|
||||
@ -78,6 +78,12 @@ func TestEmptyValueWithSoftDefault(t *testing.T) {
|
||||
assert.Check(t, is.Equal("ok def", result))
|
||||
}
|
||||
|
||||
func TestValueWithSoftDefault(t *testing.T) {
|
||||
result, err := Substitute("ok ${FOO:-def}", defaultMapping)
|
||||
assert.NilError(t, err)
|
||||
assert.Check(t, is.Equal("ok first", result))
|
||||
}
|
||||
|
||||
func TestEmptyValueWithHardDefault(t *testing.T) {
|
||||
result, err := Substitute("ok ${BAR-def}", defaultMapping)
|
||||
assert.NilError(t, err)
|
||||
|
||||
@ -10,8 +10,10 @@ import (
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/docker/cli/cli/connhelper/ssh"
|
||||
@ -82,6 +84,9 @@ func newCommandConn(ctx context.Context, cmd string, args ...string) (net.Conn,
|
||||
// commandConn implements net.Conn
|
||||
type commandConn struct {
|
||||
cmd *exec.Cmd
|
||||
cmdExited bool
|
||||
cmdWaitErr error
|
||||
cmdMutex sync.Mutex
|
||||
stdin io.WriteCloser
|
||||
stdout io.ReadCloser
|
||||
stderrMu sync.Mutex
|
||||
@ -101,23 +106,75 @@ func (c *commandConn) killIfStdioClosed() error {
|
||||
if !stdioClosed {
|
||||
return nil
|
||||
}
|
||||
var err error
|
||||
// NOTE: maybe already killed here
|
||||
if err = c.cmd.Process.Kill(); err == nil {
|
||||
err = c.cmd.Wait()
|
||||
return c.kill()
|
||||
}
|
||||
|
||||
// killAndWait tries sending SIGTERM to the process before sending SIGKILL.
|
||||
func killAndWait(cmd *exec.Cmd) error {
|
||||
var werr error
|
||||
if runtime.GOOS != "windows" {
|
||||
werrCh := make(chan error)
|
||||
go func() { werrCh <- cmd.Wait() }()
|
||||
cmd.Process.Signal(syscall.SIGTERM)
|
||||
select {
|
||||
case werr = <-werrCh:
|
||||
case <-time.After(3 * time.Second):
|
||||
cmd.Process.Kill()
|
||||
werr = <-werrCh
|
||||
}
|
||||
} else {
|
||||
cmd.Process.Kill()
|
||||
werr = cmd.Wait()
|
||||
}
|
||||
if err != nil {
|
||||
// err is typically "os: process already finished".
|
||||
// we check ProcessState here instead of `strings.Contains(err, "os: process already finished")`
|
||||
if c.cmd.ProcessState.Exited() {
|
||||
err = nil
|
||||
return werr
|
||||
}
|
||||
|
||||
// kill returns nil if the command terminated, regardless to the exit status.
|
||||
func (c *commandConn) kill() error {
|
||||
var werr error
|
||||
c.cmdMutex.Lock()
|
||||
if c.cmdExited {
|
||||
werr = c.cmdWaitErr
|
||||
} else {
|
||||
werr = killAndWait(c.cmd)
|
||||
c.cmdWaitErr = werr
|
||||
c.cmdExited = true
|
||||
}
|
||||
c.cmdMutex.Unlock()
|
||||
if werr == nil {
|
||||
return nil
|
||||
}
|
||||
wExitErr, ok := werr.(*exec.ExitError)
|
||||
if ok {
|
||||
if wExitErr.ProcessState.Exited() {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return err
|
||||
return errors.Wrapf(werr, "connhelper: failed to wait")
|
||||
}
|
||||
|
||||
func (c *commandConn) onEOF(eof error) error {
|
||||
werr := c.cmd.Wait()
|
||||
// when we got EOF, the command is going to be terminated
|
||||
var werr error
|
||||
c.cmdMutex.Lock()
|
||||
if c.cmdExited {
|
||||
werr = c.cmdWaitErr
|
||||
} else {
|
||||
werrCh := make(chan error)
|
||||
go func() { werrCh <- c.cmd.Wait() }()
|
||||
select {
|
||||
case werr = <-werrCh:
|
||||
c.cmdWaitErr = werr
|
||||
c.cmdExited = true
|
||||
case <-time.After(10 * time.Second):
|
||||
c.cmdMutex.Unlock()
|
||||
c.stderrMu.Lock()
|
||||
stderr := c.stderr.String()
|
||||
c.stderrMu.Unlock()
|
||||
return errors.Errorf("command %v did not exit after %v: stderr=%q", c.cmd.Args, eof, stderr)
|
||||
}
|
||||
}
|
||||
c.cmdMutex.Unlock()
|
||||
if werr == nil {
|
||||
return eof
|
||||
}
|
||||
@ -148,7 +205,10 @@ func (c *commandConn) CloseRead() error {
|
||||
c.stdioClosedMu.Lock()
|
||||
c.stdoutClosed = true
|
||||
c.stdioClosedMu.Unlock()
|
||||
return c.killIfStdioClosed()
|
||||
if err := c.killIfStdioClosed(); err != nil {
|
||||
logrus.Warnf("commandConn.CloseRead: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *commandConn) Read(p []byte) (int, error) {
|
||||
@ -167,7 +227,10 @@ func (c *commandConn) CloseWrite() error {
|
||||
c.stdioClosedMu.Lock()
|
||||
c.stdinClosed = true
|
||||
c.stdioClosedMu.Unlock()
|
||||
return c.killIfStdioClosed()
|
||||
if err := c.killIfStdioClosed(); err != nil {
|
||||
logrus.Warnf("commandConn.CloseWrite: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *commandConn) Write(p []byte) (int, error) {
|
||||
|
||||
@ -200,7 +200,7 @@ func continueOnError(err error) bool {
|
||||
}
|
||||
|
||||
func (c *client) iterateEndpoints(ctx context.Context, namedRef reference.Named, each func(context.Context, distribution.Repository, reference.Named) (bool, error)) error {
|
||||
endpoints, err := allEndpoints(namedRef)
|
||||
endpoints, err := allEndpoints(namedRef, c.insecureRegistry)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -262,12 +262,18 @@ func (c *client) iterateEndpoints(ctx context.Context, namedRef reference.Named,
|
||||
}
|
||||
|
||||
// allEndpoints returns a list of endpoints ordered by priority (v2, https, v1).
|
||||
func allEndpoints(namedRef reference.Named) ([]registry.APIEndpoint, error) {
|
||||
func allEndpoints(namedRef reference.Named, insecure bool) ([]registry.APIEndpoint, error) {
|
||||
repoInfo, err := registry.ParseRepositoryInfo(namedRef)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
registryService, err := registry.NewService(registry.ServiceOptions{})
|
||||
|
||||
var serviceOpts registry.ServiceOptions
|
||||
if insecure {
|
||||
logrus.Debugf("allowing insecure registry for: %s", reference.Domain(namedRef))
|
||||
serviceOpts.InsecureRegistries = []string{reference.Domain(namedRef)}
|
||||
}
|
||||
registryService, err := registry.NewService(serviceOpts)
|
||||
if err != nil {
|
||||
return []registry.APIEndpoint{}, err
|
||||
}
|
||||
|
||||
@ -13,6 +13,7 @@ import (
|
||||
cliconfig "github.com/docker/cli/cli/config"
|
||||
"github.com/docker/cli/cli/debug"
|
||||
cliflags "github.com/docker/cli/cli/flags"
|
||||
"github.com/docker/cli/internal/containerizedengine"
|
||||
"github.com/docker/docker/api/types/versions"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/docker/docker/pkg/term"
|
||||
@ -168,7 +169,7 @@ func main() {
|
||||
stdin, stdout, stderr := term.StdStreams()
|
||||
logrus.SetOutput(stderr)
|
||||
|
||||
dockerCli := command.NewDockerCli(stdin, stdout, stderr, contentTrustEnabled())
|
||||
dockerCli := command.NewDockerCli(stdin, stdout, stderr, contentTrustEnabled(), containerizedengine.NewClient)
|
||||
cmd := newDockerCommand(dockerCli)
|
||||
|
||||
if err := cmd.Execute(); err != nil {
|
||||
|
||||
@ -26,7 +26,7 @@ func TestClientDebugEnabled(t *testing.T) {
|
||||
|
||||
func TestExitStatusForInvalidSubcommandWithHelpFlag(t *testing.T) {
|
||||
discard := ioutil.Discard
|
||||
cmd := newDockerCommand(command.NewDockerCli(os.Stdin, discard, discard, false))
|
||||
cmd := newDockerCommand(command.NewDockerCli(os.Stdin, discard, discard, false, nil))
|
||||
cmd.SetArgs([]string{"help", "invalid"})
|
||||
err := cmd.Execute()
|
||||
assert.Error(t, err, "unknown help topic: invalid")
|
||||
|
||||
@ -105,7 +105,7 @@ shellcheck: build_shell_validate_image ## run shellcheck validation
|
||||
docker run -ti --rm $(ENVVARS) $(MOUNTS) $(VALIDATE_IMAGE_NAME) make shellcheck
|
||||
|
||||
.PHONY: test-e2e ## run e2e tests
|
||||
test-e2e: test-e2e-non-experimental test-e2e-experimental test-e2e-containerized
|
||||
test-e2e: test-e2e-non-experimental test-e2e-experimental
|
||||
|
||||
.PHONY: test-e2e-experimental
|
||||
test-e2e-experimental: build_e2e_image
|
||||
@ -115,14 +115,6 @@ test-e2e-experimental: build_e2e_image
|
||||
test-e2e-non-experimental: build_e2e_image
|
||||
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock $(E2E_IMAGE_NAME)
|
||||
|
||||
.PHONY: test-e2e-containerized
|
||||
test-e2e-containerized: build_e2e_image
|
||||
docker run --rm --privileged \
|
||||
-v /var/lib/docker \
|
||||
-v /var/lib/containerd \
|
||||
-v /lib/modules:/lib/modules \
|
||||
$(E2E_IMAGE_NAME) /go/src/github.com/docker/cli/scripts/test/engine/entry
|
||||
|
||||
.PHONY: help
|
||||
help: ## print this help
|
||||
@awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {sub("\\\\n",sprintf("\n%22c"," "), $$2);printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST)
|
||||
|
||||
@ -15,28 +15,6 @@ RUN apt-get update && apt-get install -y \
|
||||
iptables \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# TODO - consider replacing with an official image and a multi-stage build to pluck the binaries out
|
||||
#ARG CONTAINERD_VERSION=v1.1.2
|
||||
#ARG CONTAINERD_VERSION=47a128d
|
||||
#ARG CONTAINERD_VERSION=6c3e782f
|
||||
ARG CONTAINERD_VERSION=65839a47a88b0a1c5dc34981f1741eccefc9f2b0
|
||||
RUN git clone https://github.com/containerd/containerd.git /go/src/github.com/containerd/containerd && \
|
||||
cd /go/src/github.com/containerd/containerd && \
|
||||
git checkout ${CONTAINERD_VERSION} && \
|
||||
make && \
|
||||
make install
|
||||
COPY e2eengine/config.toml /etc/containerd/config.toml
|
||||
COPY --from=containerd-shim-process /bin/containerd-shim-process-v1 /bin/
|
||||
|
||||
|
||||
# TODO - consider replacing with an official image and a multi-stage build to pluck the binaries out
|
||||
ARG RUNC_VERSION=v1.0.0-rc5
|
||||
RUN git clone https://github.com/opencontainers/runc.git /go/src/github.com/opencontainers/runc && \
|
||||
cd /go/src/github.com/opencontainers/runc && \
|
||||
git checkout ${RUNC_VERSION} && \
|
||||
make && \
|
||||
make install
|
||||
|
||||
ARG COMPOSE_VERSION=1.21.2
|
||||
RUN curl -L https://github.com/docker/compose/releases/download/${COMPOSE_VERSION}/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose \
|
||||
&& chmod +x /usr/local/bin/docker-compose
|
||||
|
||||
@ -48,8 +48,11 @@ Options:
|
||||
'<network-name>|<network-id>': connect to a user-defined network
|
||||
--no-cache Do not use cache when building the image
|
||||
--pull Always attempt to pull a newer version of the image
|
||||
--progress Set type of progress output (only if BuildKit enabled) (auto, plain, tty).
|
||||
Use plain to show container output
|
||||
-q, --quiet Suppress the build output and print image ID on success
|
||||
--rm Remove intermediate containers after a successful build (default true)
|
||||
--secret Secret file to expose to the build (only if BuildKit enabled): id=mysecret,src=/local/secret"
|
||||
--security-opt value Security Options (default [])
|
||||
--shm-size bytes Size of /dev/shm
|
||||
The format is `<number><unit>`. `number` must be greater than `0`.
|
||||
|
||||
@ -24,6 +24,7 @@ Options:
|
||||
-c, --change value Apply Dockerfile instruction to the created image (default [])
|
||||
--help Print usage
|
||||
-m, --message string Set commit message for imported image
|
||||
--platform string Set platform if server is multi-platform capable
|
||||
```
|
||||
|
||||
## Description
|
||||
@ -87,3 +88,11 @@ Note the `sudo` in this example – you must preserve
|
||||
the ownership of the files (especially root ownership) during the
|
||||
archiving with tar. If you are not root (or the sudo command) when you
|
||||
tar, then the ownerships might not get preserved.
|
||||
|
||||
## When the daemon supports multiple operating systems
|
||||
If the daemon supports multiple operating systems, and the image being imported
|
||||
does not match the default operating system, it may be necessary to add
|
||||
`--platform`. This would be necessary when importing a Linux image into a Windows
|
||||
daemon.
|
||||
|
||||
# docker import --platform=linux .\linuximage.tar
|
||||
|
||||
@ -19,7 +19,7 @@ const descriptionSourcePath = "docs/reference/commandline/"
|
||||
|
||||
func generateCliYaml(opts *options) error {
|
||||
stdin, stdout, stderr := term.StdStreams()
|
||||
dockerCli := command.NewDockerCli(stdin, stdout, stderr, false)
|
||||
dockerCli := command.NewDockerCli(stdin, stdout, stderr, false, nil)
|
||||
cmd := &cobra.Command{Use: "docker"}
|
||||
commands.AddCommands(cmd, dockerCli)
|
||||
source := filepath.Join(opts.source, descriptionSourcePath)
|
||||
|
||||
@ -1,42 +0,0 @@
|
||||
package check
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/e2eengine"
|
||||
|
||||
"gotest.tools/icmd"
|
||||
)
|
||||
|
||||
func TestDockerEngineOnContainerdAltRootConfig(t *testing.T) {
|
||||
defer func() {
|
||||
err := e2eengine.CleanupEngine(t)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to cleanup engine: %s", err)
|
||||
}
|
||||
}()
|
||||
|
||||
t.Log("First engine init")
|
||||
// First init
|
||||
result := icmd.RunCmd(icmd.Command("docker", "engine", "init", "--config-file", "/tmp/etc/docker/daemon.json"),
|
||||
func(c *icmd.Cmd) {
|
||||
c.Env = append(c.Env, "DOCKER_CLI_EXPERIMENTAL=enabled")
|
||||
})
|
||||
result.Assert(t, icmd.Expected{
|
||||
Out: "Success! The docker engine is now running.",
|
||||
Err: "",
|
||||
ExitCode: 0,
|
||||
})
|
||||
|
||||
// Make sure update doesn't blow up with alternate config path
|
||||
t.Log("perform update")
|
||||
// Now update and succeed
|
||||
targetVersion := os.Getenv("VERSION")
|
||||
result = icmd.RunCmd(icmd.Command("docker", "engine", "update", "--version", targetVersion))
|
||||
result.Assert(t, icmd.Expected{
|
||||
Out: "Success! The docker engine is now running.",
|
||||
Err: "",
|
||||
ExitCode: 0,
|
||||
})
|
||||
}
|
||||
@ -1,14 +0,0 @@
|
||||
root = "/var/lib/containerd"
|
||||
state = "/run/containerd"
|
||||
oom_score = 0
|
||||
|
||||
[grpc]
|
||||
address = "/run/containerd/containerd.sock"
|
||||
uid = 0
|
||||
gid = 0
|
||||
|
||||
[debug]
|
||||
address = "/run/containerd/debug.sock"
|
||||
uid = 0
|
||||
gid = 0
|
||||
level = "debug"
|
||||
@ -1,85 +0,0 @@
|
||||
package multi
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/e2eengine"
|
||||
|
||||
"gotest.tools/icmd"
|
||||
)
|
||||
|
||||
func TestDockerEngineOnContainerdMultiTest(t *testing.T) {
|
||||
defer func() {
|
||||
err := e2eengine.CleanupEngine(t)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to cleanup engine: %s", err)
|
||||
}
|
||||
}()
|
||||
|
||||
t.Log("Attempt engine init without experimental")
|
||||
// First init
|
||||
result := icmd.RunCmd(icmd.Command("docker", "engine", "init"),
|
||||
func(c *icmd.Cmd) {
|
||||
c.Env = append(c.Env, "DOCKER_CLI_EXPERIMENTAL=disabled")
|
||||
})
|
||||
result.Assert(t, icmd.Expected{
|
||||
Out: "",
|
||||
Err: "docker engine init is only supported",
|
||||
ExitCode: 1,
|
||||
})
|
||||
|
||||
t.Log("First engine init")
|
||||
// First init
|
||||
result = icmd.RunCmd(icmd.Command("docker", "engine", "init"),
|
||||
func(c *icmd.Cmd) {
|
||||
c.Env = append(c.Env, "DOCKER_CLI_EXPERIMENTAL=enabled")
|
||||
})
|
||||
result.Assert(t, icmd.Expected{
|
||||
Out: "Success! The docker engine is now running.",
|
||||
Err: "",
|
||||
ExitCode: 0,
|
||||
})
|
||||
|
||||
t.Log("checking for updates")
|
||||
// Check for updates
|
||||
result = icmd.RunCmd(icmd.Command("docker", "engine", "check", "--downgrades", "--pre-releases"))
|
||||
result.Assert(t, icmd.Expected{
|
||||
Out: "VERSION",
|
||||
Err: "",
|
||||
ExitCode: 0,
|
||||
})
|
||||
|
||||
t.Log("attempt second init (should fail)")
|
||||
// Attempt to init a second time and fail
|
||||
result = icmd.RunCmd(icmd.Command("docker", "engine", "init"),
|
||||
func(c *icmd.Cmd) {
|
||||
c.Env = append(c.Env, "DOCKER_CLI_EXPERIMENTAL=enabled")
|
||||
})
|
||||
result.Assert(t, icmd.Expected{
|
||||
Out: "",
|
||||
Err: "engine already present",
|
||||
ExitCode: 1,
|
||||
})
|
||||
|
||||
t.Log("perform update")
|
||||
// Now update and succeed
|
||||
targetVersion := os.Getenv("VERSION")
|
||||
result = icmd.RunCmd(icmd.Command("docker", "engine", "update", "--version", targetVersion))
|
||||
result.Assert(t, icmd.Expected{
|
||||
Out: "Success! The docker engine is now running.",
|
||||
Err: "",
|
||||
ExitCode: 0,
|
||||
})
|
||||
|
||||
t.Log("remove engine")
|
||||
result = icmd.RunCmd(icmd.Command("docker", "engine", "rm"),
|
||||
func(c *icmd.Cmd) {
|
||||
c.Env = append(c.Env, "DOCKER_CLI_EXPERIMENTAL=enabled")
|
||||
})
|
||||
result.Assert(t, icmd.Expected{
|
||||
Out: "",
|
||||
Err: "",
|
||||
ExitCode: 0,
|
||||
})
|
||||
}
|
||||
@ -1,39 +0,0 @@
|
||||
package e2eengine
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/internal/containerizedengine"
|
||||
)
|
||||
|
||||
// CleanupEngine ensures the local engine has been removed between testcases
|
||||
func CleanupEngine(t *testing.T) error {
|
||||
t.Log("doing engine cleanup")
|
||||
ctx := context.Background()
|
||||
|
||||
client, err := containerizedengine.NewClient("")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// See if the engine exists first
|
||||
engine, err := client.GetEngine(ctx)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "not present") {
|
||||
t.Log("engine was not detected, no cleanup to perform")
|
||||
// Nothing to do, it's not defined
|
||||
return nil
|
||||
}
|
||||
t.Logf("failed to lookup engine: %s", err)
|
||||
// Any other error is not good...
|
||||
return err
|
||||
}
|
||||
// TODO Consider nuking the docker dir too so there's no cached content between test cases
|
||||
err = client.RemoveEngine(ctx, engine)
|
||||
if err != nil {
|
||||
t.Logf("Failed to remove engine: %s", err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
@ -1,12 +1,9 @@
|
||||
package containerizedengine
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"syscall"
|
||||
|
||||
"github.com/containerd/containerd"
|
||||
containerdtypes "github.com/containerd/containerd/api/types"
|
||||
"github.com/containerd/containerd/cio"
|
||||
"github.com/containerd/containerd/containers"
|
||||
"github.com/containerd/containerd/content"
|
||||
@ -14,7 +11,6 @@ import (
|
||||
prototypes "github.com/gogo/protobuf/types"
|
||||
"github.com/opencontainers/go-digest"
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"github.com/opencontainers/runtime-spec/specs-go"
|
||||
)
|
||||
|
||||
type (
|
||||
@ -25,6 +21,8 @@ type (
|
||||
getImageFunc func(ctx context.Context, ref string) (containerd.Image, error)
|
||||
contentStoreFunc func() content.Store
|
||||
containerServiceFunc func() containers.Store
|
||||
installFunc func(context.Context, containerd.Image, ...containerd.InstallOpts) error
|
||||
versionFunc func(ctx context.Context) (containerd.Version, error)
|
||||
}
|
||||
fakeContainer struct {
|
||||
idFunc func() string
|
||||
@ -49,30 +47,6 @@ type (
|
||||
isUnpackedFunc func(context.Context, string) (bool, error)
|
||||
contentStoreFunc func() content.Store
|
||||
}
|
||||
fakeTask struct {
|
||||
idFunc func() string
|
||||
pidFunc func() uint32
|
||||
startFunc func(context.Context) error
|
||||
deleteFunc func(context.Context, ...containerd.ProcessDeleteOpts) (*containerd.ExitStatus, error)
|
||||
killFunc func(context.Context, syscall.Signal, ...containerd.KillOpts) error
|
||||
waitFunc func(context.Context) (<-chan containerd.ExitStatus, error)
|
||||
closeIOFunc func(context.Context, ...containerd.IOCloserOpts) error
|
||||
resizeFunc func(ctx context.Context, w, h uint32) error
|
||||
ioFunc func() cio.IO
|
||||
statusFunc func(context.Context) (containerd.Status, error)
|
||||
pauseFunc func(context.Context) error
|
||||
resumeFunc func(context.Context) error
|
||||
execFunc func(context.Context, string, *specs.Process, cio.Creator) (containerd.Process, error)
|
||||
pidsFunc func(context.Context) ([]containerd.ProcessInfo, error)
|
||||
checkpointFunc func(context.Context, ...containerd.CheckpointTaskOpts) (containerd.Image, error)
|
||||
updateFunc func(context.Context, ...containerd.UpdateTaskOpts) error
|
||||
loadProcessFunc func(context.Context, string, cio.Attach) (containerd.Process, error)
|
||||
metricsFunc func(context.Context) (*containerdtypes.Metric, error)
|
||||
}
|
||||
|
||||
testOutStream struct {
|
||||
bytes.Buffer
|
||||
}
|
||||
)
|
||||
|
||||
func (w *fakeContainerdClient) Containers(ctx context.Context, filters ...string) ([]containerd.Container, error) {
|
||||
@ -114,6 +88,18 @@ func (w *fakeContainerdClient) ContainerService() containers.Store {
|
||||
func (w *fakeContainerdClient) Close() error {
|
||||
return nil
|
||||
}
|
||||
func (w *fakeContainerdClient) Install(ctx context.Context, image containerd.Image, args ...containerd.InstallOpts) error {
|
||||
if w.installFunc != nil {
|
||||
return w.installFunc(ctx, image, args...)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (w *fakeContainerdClient) Version(ctx context.Context) (containerd.Version, error) {
|
||||
if w.versionFunc != nil {
|
||||
return w.versionFunc(ctx)
|
||||
}
|
||||
return containerd.Version{}, nil
|
||||
}
|
||||
|
||||
func (c *fakeContainer) ID() string {
|
||||
if c.idFunc != nil {
|
||||
@ -230,119 +216,3 @@ func (i *fakeImage) ContentStore() content.Store {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *fakeTask) ID() string {
|
||||
if t.idFunc != nil {
|
||||
return t.idFunc()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
func (t *fakeTask) Pid() uint32 {
|
||||
if t.pidFunc != nil {
|
||||
return t.pidFunc()
|
||||
}
|
||||
return 0
|
||||
}
|
||||
func (t *fakeTask) Start(ctx context.Context) error {
|
||||
if t.startFunc != nil {
|
||||
return t.startFunc(ctx)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (t *fakeTask) Delete(ctx context.Context, opts ...containerd.ProcessDeleteOpts) (*containerd.ExitStatus, error) {
|
||||
if t.deleteFunc != nil {
|
||||
return t.deleteFunc(ctx, opts...)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
func (t *fakeTask) Kill(ctx context.Context, signal syscall.Signal, opts ...containerd.KillOpts) error {
|
||||
if t.killFunc != nil {
|
||||
return t.killFunc(ctx, signal, opts...)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (t *fakeTask) Wait(ctx context.Context) (<-chan containerd.ExitStatus, error) {
|
||||
if t.waitFunc != nil {
|
||||
return t.waitFunc(ctx)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
func (t *fakeTask) CloseIO(ctx context.Context, opts ...containerd.IOCloserOpts) error {
|
||||
if t.closeIOFunc != nil {
|
||||
return t.closeIOFunc(ctx, opts...)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (t *fakeTask) Resize(ctx context.Context, w, h uint32) error {
|
||||
if t.resizeFunc != nil {
|
||||
return t.resizeFunc(ctx, w, h)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (t *fakeTask) IO() cio.IO {
|
||||
if t.ioFunc != nil {
|
||||
return t.ioFunc()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (t *fakeTask) Status(ctx context.Context) (containerd.Status, error) {
|
||||
if t.statusFunc != nil {
|
||||
return t.statusFunc(ctx)
|
||||
}
|
||||
return containerd.Status{}, nil
|
||||
}
|
||||
func (t *fakeTask) Pause(ctx context.Context) error {
|
||||
if t.pauseFunc != nil {
|
||||
return t.pauseFunc(ctx)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (t *fakeTask) Resume(ctx context.Context) error {
|
||||
if t.resumeFunc != nil {
|
||||
return t.resumeFunc(ctx)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (t *fakeTask) Exec(ctx context.Context, cmd string, proc *specs.Process, ioc cio.Creator) (containerd.Process, error) {
|
||||
if t.execFunc != nil {
|
||||
return t.execFunc(ctx, cmd, proc, ioc)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
func (t *fakeTask) Pids(ctx context.Context) ([]containerd.ProcessInfo, error) {
|
||||
if t.pidsFunc != nil {
|
||||
return t.pidsFunc(ctx)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
func (t *fakeTask) Checkpoint(ctx context.Context, opts ...containerd.CheckpointTaskOpts) (containerd.Image, error) {
|
||||
if t.checkpointFunc != nil {
|
||||
return t.checkpointFunc(ctx, opts...)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
func (t *fakeTask) Update(ctx context.Context, opts ...containerd.UpdateTaskOpts) error {
|
||||
if t.updateFunc != nil {
|
||||
return t.updateFunc(ctx, opts...)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (t *fakeTask) LoadProcess(ctx context.Context, name string, attach cio.Attach) (containerd.Process, error) {
|
||||
if t.loadProcessFunc != nil {
|
||||
return t.loadProcessFunc(ctx, name, attach)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
func (t *fakeTask) Metrics(ctx context.Context) (*containerdtypes.Metric, error) {
|
||||
if t.metricsFunc != nil {
|
||||
return t.metricsFunc(ctx)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (o *testOutStream) FD() uintptr {
|
||||
return 0
|
||||
}
|
||||
func (o *testOutStream) IsTerminal() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
@ -7,6 +7,7 @@ import (
|
||||
"github.com/containerd/containerd"
|
||||
"github.com/containerd/containerd/images"
|
||||
"github.com/containerd/containerd/remotes/docker"
|
||||
clitypes "github.com/docker/cli/types"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/pkg/jsonmessage"
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
@ -15,7 +16,7 @@ import (
|
||||
// NewClient returns a new containerizedengine client
|
||||
// This client can be used to manage the lifecycle of
|
||||
// dockerd running as a container on containerd.
|
||||
func NewClient(sockPath string) (Client, error) {
|
||||
func NewClient(sockPath string) (clitypes.ContainerizedClient, error) {
|
||||
if sockPath == "" {
|
||||
sockPath = containerdSockPath
|
||||
}
|
||||
@ -23,17 +24,17 @@ func NewClient(sockPath string) (Client, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return baseClient{
|
||||
return &baseClient{
|
||||
cclient: cclient,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Close will close the underlying clients
|
||||
func (c baseClient) Close() error {
|
||||
func (c *baseClient) Close() error {
|
||||
return c.cclient.Close()
|
||||
}
|
||||
|
||||
func (c baseClient) pullWithAuth(ctx context.Context, imageName string, out OutStream,
|
||||
func (c *baseClient) pullWithAuth(ctx context.Context, imageName string, out clitypes.OutStream,
|
||||
authConfig *types.AuthConfig) (containerd.Image, error) {
|
||||
|
||||
resolver := docker.NewResolver(docker.ResolverOptions{
|
||||
|
||||
@ -1,11 +1,13 @@
|
||||
package containerizedengine
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/containerd/containerd"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/docker/api/types"
|
||||
"gotest.tools/assert"
|
||||
)
|
||||
@ -22,7 +24,7 @@ func TestPullWithAuthPullFail(t *testing.T) {
|
||||
}
|
||||
imageName := "testnamegoeshere"
|
||||
|
||||
_, err := client.pullWithAuth(ctx, imageName, &testOutStream{}, &types.AuthConfig{})
|
||||
_, err := client.pullWithAuth(ctx, imageName, command.NewOutStream(&bytes.Buffer{}), &types.AuthConfig{})
|
||||
assert.ErrorContains(t, err, "pull failure")
|
||||
}
|
||||
|
||||
@ -38,6 +40,6 @@ func TestPullWithAuthPullPass(t *testing.T) {
|
||||
}
|
||||
imageName := "testnamegoeshere"
|
||||
|
||||
_, err := client.pullWithAuth(ctx, imageName, &testOutStream{}, &types.AuthConfig{})
|
||||
_, err := client.pullWithAuth(ctx, imageName, command.NewOutStream(&bytes.Buffer{}), &types.AuthConfig{})
|
||||
assert.NilError(t, err)
|
||||
}
|
||||
|
||||
@ -1,261 +0,0 @@
|
||||
package containerizedengine
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/containerd/containerd"
|
||||
"github.com/containerd/containerd/errdefs"
|
||||
"github.com/containerd/containerd/namespaces"
|
||||
"github.com/containerd/containerd/runtime/restart"
|
||||
"github.com/docker/cli/internal/pkg/containerized"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// InitEngine is the main entrypoint for `docker engine init`
|
||||
func (c baseClient) InitEngine(ctx context.Context, opts EngineInitOptions, out OutStream,
|
||||
authConfig *types.AuthConfig, healthfn func(context.Context) error) error {
|
||||
|
||||
ctx = namespaces.WithNamespace(ctx, engineNamespace)
|
||||
// Verify engine isn't already running
|
||||
_, err := c.GetEngine(ctx)
|
||||
if err == nil {
|
||||
return ErrEngineAlreadyPresent
|
||||
} else if err != ErrEngineNotPresent {
|
||||
return err
|
||||
}
|
||||
|
||||
imageName := fmt.Sprintf("%s/%s:%s", opts.RegistryPrefix, opts.EngineImage, opts.EngineVersion)
|
||||
// Look for desired image
|
||||
_, err = c.cclient.GetImage(ctx, imageName)
|
||||
if err != nil {
|
||||
if errdefs.IsNotFound(err) {
|
||||
_, err = c.pullWithAuth(ctx, imageName, out, authConfig)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "unable to pull image %s", imageName)
|
||||
}
|
||||
} else {
|
||||
return errors.Wrapf(err, "unable to check for image %s", imageName)
|
||||
}
|
||||
}
|
||||
|
||||
// Spin up the engine
|
||||
err = c.startEngineOnContainerd(ctx, imageName, opts.ConfigFile)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to create docker daemon")
|
||||
}
|
||||
|
||||
// Wait for the daemon to start, verify it's responsive
|
||||
fmt.Fprintf(out, "Waiting for engine to start... ")
|
||||
ctx, cancel := context.WithTimeout(ctx, engineWaitTimeout)
|
||||
defer cancel()
|
||||
if err := c.waitForEngine(ctx, out, healthfn); err != nil {
|
||||
// TODO once we have the logging strategy sorted out
|
||||
// this should likely gather the last few lines of logs to report
|
||||
// why the daemon failed to initialize
|
||||
return errors.Wrap(err, "failed to start docker daemon")
|
||||
}
|
||||
fmt.Fprintf(out, "Success! The docker engine is now running.\n")
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
// GetEngine will return the containerd container running the engine (or error)
|
||||
func (c baseClient) GetEngine(ctx context.Context) (containerd.Container, error) {
|
||||
ctx = namespaces.WithNamespace(ctx, engineNamespace)
|
||||
containers, err := c.cclient.Containers(ctx, "id=="+engineContainerName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(containers) == 0 {
|
||||
return nil, ErrEngineNotPresent
|
||||
}
|
||||
return containers[0], nil
|
||||
}
|
||||
|
||||
// getEngineImage will return the current image used by the engine
|
||||
func (c baseClient) getEngineImage(engine containerd.Container) (string, error) {
|
||||
ctx := namespaces.WithNamespace(context.Background(), engineNamespace)
|
||||
image, err := engine.Image(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return image.Name(), nil
|
||||
}
|
||||
|
||||
// getEngineConfigFilePath will extract the config file location from the engine flags
|
||||
func (c baseClient) getEngineConfigFilePath(ctx context.Context, engine containerd.Container) (string, error) {
|
||||
spec, err := engine.Spec(ctx)
|
||||
configFile := ""
|
||||
if err != nil {
|
||||
return configFile, err
|
||||
}
|
||||
for i := 0; i < len(spec.Process.Args); i++ {
|
||||
arg := spec.Process.Args[i]
|
||||
if strings.HasPrefix(arg, "--config-file") {
|
||||
if strings.Contains(arg, "=") {
|
||||
split := strings.SplitN(arg, "=", 2)
|
||||
configFile = split[1]
|
||||
} else {
|
||||
if i+1 >= len(spec.Process.Args) {
|
||||
return configFile, ErrMalformedConfigFileParam
|
||||
}
|
||||
configFile = spec.Process.Args[i+1]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if configFile == "" {
|
||||
// TODO - any more diagnostics to offer?
|
||||
return configFile, ErrEngineConfigLookupFailure
|
||||
}
|
||||
return configFile, nil
|
||||
}
|
||||
|
||||
var (
|
||||
engineWaitInterval = 500 * time.Millisecond
|
||||
engineWaitTimeout = 60 * time.Second
|
||||
)
|
||||
|
||||
// waitForEngine will wait for the engine to start
|
||||
func (c baseClient) waitForEngine(ctx context.Context, out io.Writer, healthfn func(context.Context) error) error {
|
||||
ticker := time.NewTicker(engineWaitInterval)
|
||||
defer ticker.Stop()
|
||||
defer func() {
|
||||
fmt.Fprintf(out, "\n")
|
||||
}()
|
||||
|
||||
err := c.waitForEngineContainer(ctx, ticker)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(out, "waiting for engine to be responsive... ")
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
err = healthfn(ctx)
|
||||
if err == nil {
|
||||
fmt.Fprintf(out, "engine is online.")
|
||||
return nil
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return errors.Wrap(err, "timeout waiting for engine to be responsive")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c baseClient) waitForEngineContainer(ctx context.Context, ticker *time.Ticker) error {
|
||||
var ret error
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
engine, err := c.GetEngine(ctx)
|
||||
if engine != nil {
|
||||
return nil
|
||||
}
|
||||
ret = err
|
||||
case <-ctx.Done():
|
||||
return errors.Wrap(ret, "timeout waiting for engine to be responsive")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// RemoveEngine gracefully unwinds the current engine
|
||||
func (c baseClient) RemoveEngine(ctx context.Context, engine containerd.Container) error {
|
||||
ctx = namespaces.WithNamespace(ctx, engineNamespace)
|
||||
|
||||
// Make sure the container isn't being restarted while we unwind it
|
||||
stopLabel := map[string]string{}
|
||||
stopLabel[restart.StatusLabel] = string(containerd.Stopped)
|
||||
engine.SetLabels(ctx, stopLabel)
|
||||
|
||||
// Wind down the existing engine
|
||||
task, err := engine.Task(ctx, nil)
|
||||
if err != nil {
|
||||
if !errdefs.IsNotFound(err) {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
status, err := task.Status(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if status.Status == containerd.Running {
|
||||
// It's running, so kill it
|
||||
err := task.Kill(ctx, syscall.SIGTERM, []containerd.KillOpts{}...)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "task kill error")
|
||||
}
|
||||
|
||||
ch, err := task.Wait(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
timeout := time.NewTimer(engineWaitTimeout)
|
||||
select {
|
||||
case <-timeout.C:
|
||||
// TODO - consider a force flag in the future to allow a more aggressive
|
||||
// kill of the engine via
|
||||
// task.Kill(ctx, syscall.SIGKILL, containerd.WithKillAll)
|
||||
return ErrEngineShutdownTimeout
|
||||
case <-ch:
|
||||
}
|
||||
}
|
||||
if _, err := task.Delete(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
deleteOpts := []containerd.DeleteOpts{containerd.WithSnapshotCleanup}
|
||||
err = engine.Delete(ctx, deleteOpts...)
|
||||
if err != nil && errdefs.IsNotFound(err) {
|
||||
return nil
|
||||
}
|
||||
return errors.Wrap(err, "failed to remove existing engine container")
|
||||
}
|
||||
|
||||
// startEngineOnContainerd creates a new docker engine running on containerd
|
||||
func (c baseClient) startEngineOnContainerd(ctx context.Context, imageName, configFile string) error {
|
||||
ctx = namespaces.WithNamespace(ctx, engineNamespace)
|
||||
image, err := c.cclient.GetImage(ctx, imageName)
|
||||
if err != nil {
|
||||
if errdefs.IsNotFound(err) {
|
||||
return fmt.Errorf("engine image missing: %s", imageName)
|
||||
}
|
||||
return errors.Wrap(err, "failed to check for engine image")
|
||||
}
|
||||
|
||||
// Make sure we have a valid config file
|
||||
err = c.verifyDockerConfig(configFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
engineSpec.Process.Args = append(engineSpec.Process.Args,
|
||||
"--config-file", configFile,
|
||||
)
|
||||
|
||||
cOpts := []containerd.NewContainerOpts{
|
||||
containerized.WithNewSnapshot(image),
|
||||
restart.WithStatus(containerd.Running),
|
||||
restart.WithLogPath("/var/log/engine.log"), // TODO - better!
|
||||
genSpec(),
|
||||
containerd.WithRuntime("io.containerd.runtime.process.v1", nil),
|
||||
}
|
||||
|
||||
_, err = c.cclient.NewContainer(
|
||||
ctx,
|
||||
engineContainerName,
|
||||
cOpts...,
|
||||
)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to create engine container")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@ -1,537 +0,0 @@
|
||||
package containerizedengine
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/containerd/containerd"
|
||||
"github.com/containerd/containerd/cio"
|
||||
"github.com/containerd/containerd/errdefs"
|
||||
"github.com/containerd/containerd/oci"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/opencontainers/runtime-spec/specs-go"
|
||||
"gotest.tools/assert"
|
||||
)
|
||||
|
||||
func healthfnHappy(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
func healthfnError(ctx context.Context) error {
|
||||
return fmt.Errorf("ping failure")
|
||||
}
|
||||
|
||||
func TestInitGetEngineFail(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
opts := EngineInitOptions{
|
||||
EngineVersion: "engineversiongoeshere",
|
||||
RegistryPrefix: "registryprefixgoeshere",
|
||||
ConfigFile: "/tmp/configfilegoeshere",
|
||||
EngineImage: CommunityEngineImage,
|
||||
}
|
||||
container := &fakeContainer{}
|
||||
client := baseClient{
|
||||
cclient: &fakeContainerdClient{
|
||||
containersFunc: func(ctx context.Context, filters ...string) ([]containerd.Container, error) {
|
||||
return []containerd.Container{container}, nil
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
err := client.InitEngine(ctx, opts, &testOutStream{}, &types.AuthConfig{}, healthfnHappy)
|
||||
assert.Assert(t, err == ErrEngineAlreadyPresent)
|
||||
}
|
||||
|
||||
func TestInitCheckImageFail(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
opts := EngineInitOptions{
|
||||
EngineVersion: "engineversiongoeshere",
|
||||
RegistryPrefix: "registryprefixgoeshere",
|
||||
ConfigFile: "/tmp/configfilegoeshere",
|
||||
EngineImage: CommunityEngineImage,
|
||||
}
|
||||
client := baseClient{
|
||||
cclient: &fakeContainerdClient{
|
||||
containersFunc: func(ctx context.Context, filters ...string) ([]containerd.Container, error) {
|
||||
return []containerd.Container{}, nil
|
||||
},
|
||||
getImageFunc: func(ctx context.Context, ref string) (containerd.Image, error) {
|
||||
return nil, fmt.Errorf("something went wrong")
|
||||
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
err := client.InitEngine(ctx, opts, &testOutStream{}, &types.AuthConfig{}, healthfnHappy)
|
||||
assert.ErrorContains(t, err, "unable to check for image")
|
||||
assert.ErrorContains(t, err, "something went wrong")
|
||||
}
|
||||
|
||||
func TestInitPullFail(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
opts := EngineInitOptions{
|
||||
EngineVersion: "engineversiongoeshere",
|
||||
RegistryPrefix: "registryprefixgoeshere",
|
||||
ConfigFile: "/tmp/configfilegoeshere",
|
||||
EngineImage: CommunityEngineImage,
|
||||
}
|
||||
client := baseClient{
|
||||
cclient: &fakeContainerdClient{
|
||||
containersFunc: func(ctx context.Context, filters ...string) ([]containerd.Container, error) {
|
||||
return []containerd.Container{}, nil
|
||||
},
|
||||
getImageFunc: func(ctx context.Context, ref string) (containerd.Image, error) {
|
||||
return nil, errdefs.ErrNotFound
|
||||
|
||||
},
|
||||
pullFunc: func(ctx context.Context, ref string, opts ...containerd.RemoteOpt) (containerd.Image, error) {
|
||||
return nil, fmt.Errorf("pull failure")
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
err := client.InitEngine(ctx, opts, &testOutStream{}, &types.AuthConfig{}, healthfnHappy)
|
||||
assert.ErrorContains(t, err, "unable to pull image")
|
||||
assert.ErrorContains(t, err, "pull failure")
|
||||
}
|
||||
|
||||
func TestInitStartFail(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
opts := EngineInitOptions{
|
||||
EngineVersion: "engineversiongoeshere",
|
||||
RegistryPrefix: "registryprefixgoeshere",
|
||||
ConfigFile: "/tmp/configfilegoeshere",
|
||||
EngineImage: CommunityEngineImage,
|
||||
}
|
||||
client := baseClient{
|
||||
cclient: &fakeContainerdClient{
|
||||
containersFunc: func(ctx context.Context, filters ...string) ([]containerd.Container, error) {
|
||||
return []containerd.Container{}, nil
|
||||
},
|
||||
getImageFunc: func(ctx context.Context, ref string) (containerd.Image, error) {
|
||||
return nil, errdefs.ErrNotFound
|
||||
|
||||
},
|
||||
pullFunc: func(ctx context.Context, ref string, opts ...containerd.RemoteOpt) (containerd.Image, error) {
|
||||
return nil, nil
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
err := client.InitEngine(ctx, opts, &testOutStream{}, &types.AuthConfig{}, healthfnHappy)
|
||||
assert.ErrorContains(t, err, "failed to create docker daemon")
|
||||
}
|
||||
|
||||
func TestGetEngineFail(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
client := baseClient{
|
||||
cclient: &fakeContainerdClient{
|
||||
containersFunc: func(ctx context.Context, filters ...string) ([]containerd.Container, error) {
|
||||
return nil, fmt.Errorf("container failure")
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
_, err := client.GetEngine(ctx)
|
||||
assert.ErrorContains(t, err, "failure")
|
||||
}
|
||||
|
||||
func TestGetEngineNotPresent(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
client := baseClient{
|
||||
cclient: &fakeContainerdClient{
|
||||
containersFunc: func(ctx context.Context, filters ...string) ([]containerd.Container, error) {
|
||||
return []containerd.Container{}, nil
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
_, err := client.GetEngine(ctx)
|
||||
assert.Assert(t, err == ErrEngineNotPresent)
|
||||
}
|
||||
|
||||
func TestGetEngineFound(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
container := &fakeContainer{}
|
||||
client := baseClient{
|
||||
cclient: &fakeContainerdClient{
|
||||
containersFunc: func(ctx context.Context, filters ...string) ([]containerd.Container, error) {
|
||||
return []containerd.Container{container}, nil
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
c, err := client.GetEngine(ctx)
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, c, container)
|
||||
}
|
||||
|
||||
func TestGetEngineImageFail(t *testing.T) {
|
||||
client := baseClient{}
|
||||
container := &fakeContainer{
|
||||
imageFunc: func(context.Context) (containerd.Image, error) {
|
||||
return nil, fmt.Errorf("failure")
|
||||
},
|
||||
}
|
||||
|
||||
_, err := client.getEngineImage(container)
|
||||
assert.ErrorContains(t, err, "failure")
|
||||
}
|
||||
|
||||
func TestGetEngineImagePass(t *testing.T) {
|
||||
client := baseClient{}
|
||||
image := &fakeImage{
|
||||
nameFunc: func() string {
|
||||
return "imagenamehere"
|
||||
},
|
||||
}
|
||||
container := &fakeContainer{
|
||||
imageFunc: func(context.Context) (containerd.Image, error) {
|
||||
return image, nil
|
||||
},
|
||||
}
|
||||
|
||||
name, err := client.getEngineImage(container)
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, name, "imagenamehere")
|
||||
}
|
||||
|
||||
func TestWaitForEngineNeverShowsUp(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond)
|
||||
defer cancel()
|
||||
engineWaitInterval = 1 * time.Millisecond
|
||||
client := baseClient{
|
||||
cclient: &fakeContainerdClient{
|
||||
containersFunc: func(ctx context.Context, filters ...string) ([]containerd.Container, error) {
|
||||
return []containerd.Container{}, nil
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
err := client.waitForEngine(ctx, &testOutStream{}, healthfnError)
|
||||
assert.ErrorContains(t, err, "timeout waiting")
|
||||
}
|
||||
|
||||
func TestWaitForEnginePingFail(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond)
|
||||
defer cancel()
|
||||
engineWaitInterval = 1 * time.Millisecond
|
||||
container := &fakeContainer{}
|
||||
client := baseClient{
|
||||
cclient: &fakeContainerdClient{
|
||||
containersFunc: func(ctx context.Context, filters ...string) ([]containerd.Container, error) {
|
||||
return []containerd.Container{container}, nil
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
err := client.waitForEngine(ctx, &testOutStream{}, healthfnError)
|
||||
assert.ErrorContains(t, err, "ping fail")
|
||||
}
|
||||
|
||||
func TestWaitForEngineHealthy(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond)
|
||||
defer cancel()
|
||||
engineWaitInterval = 1 * time.Millisecond
|
||||
container := &fakeContainer{}
|
||||
client := baseClient{
|
||||
cclient: &fakeContainerdClient{
|
||||
containersFunc: func(ctx context.Context, filters ...string) ([]containerd.Container, error) {
|
||||
return []containerd.Container{container}, nil
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
err := client.waitForEngine(ctx, &testOutStream{}, healthfnHappy)
|
||||
assert.NilError(t, err)
|
||||
}
|
||||
|
||||
func TestRemoveEngineBadTaskBadDelete(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
client := baseClient{}
|
||||
container := &fakeContainer{
|
||||
deleteFunc: func(context.Context, ...containerd.DeleteOpts) error {
|
||||
return fmt.Errorf("delete failure")
|
||||
},
|
||||
taskFunc: func(context.Context, cio.Attach) (containerd.Task, error) {
|
||||
return nil, errdefs.ErrNotFound
|
||||
},
|
||||
}
|
||||
|
||||
err := client.RemoveEngine(ctx, container)
|
||||
assert.ErrorContains(t, err, "failed to remove existing engine")
|
||||
assert.ErrorContains(t, err, "delete failure")
|
||||
}
|
||||
|
||||
func TestRemoveEngineTaskNoStatus(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
client := baseClient{}
|
||||
task := &fakeTask{
|
||||
statusFunc: func(context.Context) (containerd.Status, error) {
|
||||
return containerd.Status{}, fmt.Errorf("task status failure")
|
||||
},
|
||||
}
|
||||
container := &fakeContainer{
|
||||
taskFunc: func(context.Context, cio.Attach) (containerd.Task, error) {
|
||||
return task, nil
|
||||
},
|
||||
}
|
||||
|
||||
err := client.RemoveEngine(ctx, container)
|
||||
assert.ErrorContains(t, err, "task status failure")
|
||||
}
|
||||
|
||||
func TestRemoveEngineTaskNotRunningDeleteFail(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
client := baseClient{}
|
||||
task := &fakeTask{
|
||||
statusFunc: func(context.Context) (containerd.Status, error) {
|
||||
return containerd.Status{Status: containerd.Unknown}, nil
|
||||
},
|
||||
deleteFunc: func(context.Context, ...containerd.ProcessDeleteOpts) (*containerd.ExitStatus, error) {
|
||||
return nil, fmt.Errorf("task delete failure")
|
||||
},
|
||||
}
|
||||
container := &fakeContainer{
|
||||
taskFunc: func(context.Context, cio.Attach) (containerd.Task, error) {
|
||||
return task, nil
|
||||
},
|
||||
}
|
||||
|
||||
err := client.RemoveEngine(ctx, container)
|
||||
assert.ErrorContains(t, err, "task delete failure")
|
||||
}
|
||||
|
||||
func TestRemoveEngineTaskRunningKillFail(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
client := baseClient{}
|
||||
task := &fakeTask{
|
||||
statusFunc: func(context.Context) (containerd.Status, error) {
|
||||
return containerd.Status{Status: containerd.Running}, nil
|
||||
},
|
||||
killFunc: func(context.Context, syscall.Signal, ...containerd.KillOpts) error {
|
||||
return fmt.Errorf("task kill failure")
|
||||
},
|
||||
}
|
||||
container := &fakeContainer{
|
||||
taskFunc: func(context.Context, cio.Attach) (containerd.Task, error) {
|
||||
return task, nil
|
||||
},
|
||||
}
|
||||
|
||||
err := client.RemoveEngine(ctx, container)
|
||||
assert.ErrorContains(t, err, "task kill failure")
|
||||
}
|
||||
|
||||
func TestRemoveEngineTaskRunningWaitFail(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
client := baseClient{}
|
||||
task := &fakeTask{
|
||||
statusFunc: func(context.Context) (containerd.Status, error) {
|
||||
return containerd.Status{Status: containerd.Running}, nil
|
||||
},
|
||||
waitFunc: func(context.Context) (<-chan containerd.ExitStatus, error) {
|
||||
return nil, fmt.Errorf("task wait failure")
|
||||
},
|
||||
}
|
||||
container := &fakeContainer{
|
||||
taskFunc: func(context.Context, cio.Attach) (containerd.Task, error) {
|
||||
return task, nil
|
||||
},
|
||||
}
|
||||
|
||||
err := client.RemoveEngine(ctx, container)
|
||||
assert.ErrorContains(t, err, "task wait failure")
|
||||
}
|
||||
|
||||
func TestRemoveEngineTaskRunningHappyPath(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
client := baseClient{}
|
||||
ch := make(chan containerd.ExitStatus, 1)
|
||||
task := &fakeTask{
|
||||
statusFunc: func(context.Context) (containerd.Status, error) {
|
||||
return containerd.Status{Status: containerd.Running}, nil
|
||||
},
|
||||
waitFunc: func(context.Context) (<-chan containerd.ExitStatus, error) {
|
||||
ch <- containerd.ExitStatus{}
|
||||
return ch, nil
|
||||
},
|
||||
}
|
||||
container := &fakeContainer{
|
||||
taskFunc: func(context.Context, cio.Attach) (containerd.Task, error) {
|
||||
return task, nil
|
||||
},
|
||||
}
|
||||
|
||||
err := client.RemoveEngine(ctx, container)
|
||||
assert.NilError(t, err)
|
||||
}
|
||||
|
||||
func TestRemoveEngineTaskKillTimeout(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
ch := make(chan containerd.ExitStatus, 1)
|
||||
client := baseClient{}
|
||||
engineWaitTimeout = 10 * time.Millisecond
|
||||
task := &fakeTask{
|
||||
statusFunc: func(context.Context) (containerd.Status, error) {
|
||||
return containerd.Status{Status: containerd.Running}, nil
|
||||
},
|
||||
waitFunc: func(context.Context) (<-chan containerd.ExitStatus, error) {
|
||||
//ch <- containerd.ExitStatus{} // let it timeout
|
||||
return ch, nil
|
||||
},
|
||||
}
|
||||
container := &fakeContainer{
|
||||
taskFunc: func(context.Context, cio.Attach) (containerd.Task, error) {
|
||||
return task, nil
|
||||
},
|
||||
}
|
||||
|
||||
err := client.RemoveEngine(ctx, container)
|
||||
assert.Assert(t, err == ErrEngineShutdownTimeout)
|
||||
}
|
||||
|
||||
func TestStartEngineOnContainerdImageErr(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
imageName := "testnamegoeshere"
|
||||
configFile := "/tmp/configfilegoeshere"
|
||||
client := baseClient{
|
||||
cclient: &fakeContainerdClient{
|
||||
getImageFunc: func(ctx context.Context, ref string) (containerd.Image, error) {
|
||||
return nil, fmt.Errorf("some image lookup failure")
|
||||
|
||||
},
|
||||
},
|
||||
}
|
||||
err := client.startEngineOnContainerd(ctx, imageName, configFile)
|
||||
assert.ErrorContains(t, err, "some image lookup failure")
|
||||
}
|
||||
|
||||
func TestStartEngineOnContainerdImageNotFound(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
imageName := "testnamegoeshere"
|
||||
configFile := "/tmp/configfilegoeshere"
|
||||
client := baseClient{
|
||||
cclient: &fakeContainerdClient{
|
||||
getImageFunc: func(ctx context.Context, ref string) (containerd.Image, error) {
|
||||
return nil, errdefs.ErrNotFound
|
||||
|
||||
},
|
||||
},
|
||||
}
|
||||
err := client.startEngineOnContainerd(ctx, imageName, configFile)
|
||||
assert.ErrorContains(t, err, "engine image missing")
|
||||
}
|
||||
|
||||
func TestStartEngineOnContainerdHappy(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
imageName := "testnamegoeshere"
|
||||
configFile := "/tmp/configfilegoeshere"
|
||||
ch := make(chan containerd.ExitStatus, 1)
|
||||
streams := cio.Streams{}
|
||||
task := &fakeTask{
|
||||
statusFunc: func(context.Context) (containerd.Status, error) {
|
||||
return containerd.Status{Status: containerd.Running}, nil
|
||||
},
|
||||
waitFunc: func(context.Context) (<-chan containerd.ExitStatus, error) {
|
||||
ch <- containerd.ExitStatus{}
|
||||
return ch, nil
|
||||
},
|
||||
}
|
||||
container := &fakeContainer{
|
||||
newTaskFunc: func(ctx context.Context, creator cio.Creator, opts ...containerd.NewTaskOpts) (containerd.Task, error) {
|
||||
if streams.Stdout != nil {
|
||||
streams.Stdout.Write([]byte("{}"))
|
||||
}
|
||||
return task, nil
|
||||
},
|
||||
}
|
||||
client := baseClient{
|
||||
cclient: &fakeContainerdClient{
|
||||
getImageFunc: func(ctx context.Context, ref string) (containerd.Image, error) {
|
||||
return nil, nil
|
||||
|
||||
},
|
||||
newContainerFunc: func(ctx context.Context, id string, opts ...containerd.NewContainerOpts) (containerd.Container, error) {
|
||||
return container, nil
|
||||
},
|
||||
},
|
||||
}
|
||||
err := client.startEngineOnContainerd(ctx, imageName, configFile)
|
||||
assert.NilError(t, err)
|
||||
}
|
||||
|
||||
func TestGetEngineConfigFilePathBadSpec(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
client := baseClient{}
|
||||
container := &fakeContainer{
|
||||
specFunc: func(context.Context) (*oci.Spec, error) {
|
||||
return nil, fmt.Errorf("spec error")
|
||||
},
|
||||
}
|
||||
_, err := client.getEngineConfigFilePath(ctx, container)
|
||||
assert.ErrorContains(t, err, "spec error")
|
||||
}
|
||||
|
||||
func TestGetEngineConfigFilePathDistinct(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
client := baseClient{}
|
||||
container := &fakeContainer{
|
||||
specFunc: func(context.Context) (*oci.Spec, error) {
|
||||
return &oci.Spec{
|
||||
Process: &specs.Process{
|
||||
Args: []string{
|
||||
"--another-flag",
|
||||
"foo",
|
||||
"--config-file",
|
||||
"configpath",
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
configFile, err := client.getEngineConfigFilePath(ctx, container)
|
||||
assert.NilError(t, err)
|
||||
assert.Assert(t, err, configFile == "configpath")
|
||||
}
|
||||
|
||||
func TestGetEngineConfigFilePathEquals(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
client := baseClient{}
|
||||
container := &fakeContainer{
|
||||
specFunc: func(context.Context) (*oci.Spec, error) {
|
||||
return &oci.Spec{
|
||||
Process: &specs.Process{
|
||||
Args: []string{
|
||||
"--another-flag=foo",
|
||||
"--config-file=configpath",
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
configFile, err := client.getEngineConfigFilePath(ctx, container)
|
||||
assert.NilError(t, err)
|
||||
assert.Assert(t, err, configFile == "configpath")
|
||||
}
|
||||
|
||||
func TestGetEngineConfigFilePathMalformed1(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
client := baseClient{}
|
||||
container := &fakeContainer{
|
||||
specFunc: func(context.Context) (*oci.Spec, error) {
|
||||
return &oci.Spec{
|
||||
Process: &specs.Process{
|
||||
Args: []string{
|
||||
"--another-flag",
|
||||
"--config-file",
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
}
|
||||
_, err := client.getEngineConfigFilePath(ctx, container)
|
||||
assert.Assert(t, err == ErrMalformedConfigFileParam)
|
||||
}
|
||||
@ -1,16 +0,0 @@
|
||||
// +build !windows
|
||||
|
||||
package containerizedengine
|
||||
|
||||
import (
|
||||
"github.com/containerd/containerd"
|
||||
"github.com/containerd/containerd/oci"
|
||||
"github.com/docker/cli/internal/pkg/containerized"
|
||||
)
|
||||
|
||||
func genSpec() containerd.NewContainerOpts {
|
||||
return containerd.WithSpec(&engineSpec,
|
||||
containerized.WithAllCapabilities,
|
||||
oci.WithParentCgroupDevices,
|
||||
)
|
||||
}
|
||||
@ -1,14 +0,0 @@
|
||||
// +build windows
|
||||
|
||||
package containerizedengine
|
||||
|
||||
import (
|
||||
"github.com/containerd/containerd"
|
||||
"github.com/docker/cli/internal/pkg/containerized"
|
||||
)
|
||||
|
||||
func genSpec() containerd.NewContainerOpts {
|
||||
return containerd.WithSpec(&engineSpec,
|
||||
containerized.WithAllCapabilities,
|
||||
)
|
||||
}
|
||||
@ -1,35 +0,0 @@
|
||||
package containerizedengine
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path"
|
||||
)
|
||||
|
||||
func (c baseClient) verifyDockerConfig(configFile string) error {
|
||||
|
||||
// TODO - in the future consider leveraging containerd and a host runtime
|
||||
// to create the file. For now, just create it locally since we have to be
|
||||
// local to talk to containerd
|
||||
|
||||
configDir := path.Dir(configFile)
|
||||
err := os.MkdirAll(configDir, 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fd, err := os.OpenFile(configFile, os.O_RDWR|os.O_CREATE, 0755)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer fd.Close()
|
||||
|
||||
info, err := fd.Stat()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if info.Size() == 0 {
|
||||
_, err := fd.Write([]byte("{}"))
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@ -1,12 +0,0 @@
|
||||
// +build !windows
|
||||
|
||||
package containerizedengine
|
||||
|
||||
import (
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
var (
|
||||
// SIGKILL maps to unix.SIGKILL
|
||||
SIGKILL = unix.SIGKILL
|
||||
)
|
||||
@ -1,12 +0,0 @@
|
||||
// +build windows
|
||||
|
||||
package containerizedengine
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
)
|
||||
|
||||
var (
|
||||
// SIGKILL all signals are ignored by containerd kill windows
|
||||
SIGKILL = syscall.Signal(0)
|
||||
)
|
||||
@ -3,30 +3,15 @@ package containerizedengine
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
|
||||
"github.com/containerd/containerd"
|
||||
"github.com/containerd/containerd/containers"
|
||||
"github.com/containerd/containerd/content"
|
||||
registryclient "github.com/docker/cli/cli/registry/client"
|
||||
"github.com/docker/docker/api/types"
|
||||
ver "github.com/hashicorp/go-version"
|
||||
specs "github.com/opencontainers/runtime-spec/specs-go"
|
||||
)
|
||||
|
||||
const (
|
||||
// CommunityEngineImage is the repo name for the community engine
|
||||
CommunityEngineImage = "engine-community"
|
||||
|
||||
// EnterpriseEngineImage is the repo name for the enterprise engine
|
||||
EnterpriseEngineImage = "engine-enterprise"
|
||||
|
||||
containerdSockPath = "/run/containerd/containerd.sock"
|
||||
engineContainerName = "dockerd"
|
||||
engineNamespace = "docker"
|
||||
|
||||
// Used to signal the containerd-proxy if it should manage
|
||||
proxyLabel = "com.docker/containerd-proxy.scope"
|
||||
containerdSockPath = "/run/containerd/containerd.sock"
|
||||
engineNamespace = "com.docker"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -44,80 +29,12 @@ var (
|
||||
|
||||
// ErrEngineShutdownTimeout returned if the engine failed to shutdown in time
|
||||
ErrEngineShutdownTimeout = errors.New("timeout waiting for engine to exit")
|
||||
|
||||
// ErrEngineImageMissingTag returned if the engine image is missing the version tag
|
||||
ErrEngineImageMissingTag = errors.New("malformed engine image missing tag")
|
||||
|
||||
engineSpec = specs.Spec{
|
||||
Root: &specs.Root{
|
||||
Path: "rootfs",
|
||||
},
|
||||
Process: &specs.Process{
|
||||
Cwd: "/",
|
||||
Args: []string{
|
||||
// In general, configuration should be driven by the config file, not these flags
|
||||
// TODO - consider moving more of these to the config file, and make sure the defaults are set if not present.
|
||||
"/sbin/dockerd",
|
||||
"-s",
|
||||
"overlay2",
|
||||
"--containerd",
|
||||
"/run/containerd/containerd.sock",
|
||||
"--default-runtime",
|
||||
"containerd",
|
||||
"--add-runtime",
|
||||
"containerd=runc",
|
||||
},
|
||||
User: specs.User{
|
||||
UID: 0,
|
||||
GID: 0,
|
||||
},
|
||||
Env: []string{
|
||||
"PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin",
|
||||
},
|
||||
NoNewPrivileges: false,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// Client can be used to manage the lifecycle of
|
||||
// dockerd running as a container on containerd.
|
||||
type Client interface {
|
||||
Close() error
|
||||
ActivateEngine(ctx context.Context,
|
||||
opts EngineInitOptions,
|
||||
out OutStream,
|
||||
authConfig *types.AuthConfig,
|
||||
healthfn func(context.Context) error) error
|
||||
InitEngine(ctx context.Context,
|
||||
opts EngineInitOptions,
|
||||
out OutStream,
|
||||
authConfig *types.AuthConfig,
|
||||
healthfn func(context.Context) error) error
|
||||
DoUpdate(ctx context.Context,
|
||||
opts EngineInitOptions,
|
||||
out OutStream,
|
||||
authConfig *types.AuthConfig,
|
||||
healthfn func(context.Context) error) error
|
||||
GetEngineVersions(ctx context.Context, registryClient registryclient.RegistryClient, currentVersion, imageName string) (AvailableVersions, error)
|
||||
|
||||
GetEngine(ctx context.Context) (containerd.Container, error)
|
||||
RemoveEngine(ctx context.Context, engine containerd.Container) error
|
||||
GetCurrentEngineVersion(ctx context.Context) (EngineInitOptions, error)
|
||||
}
|
||||
type baseClient struct {
|
||||
cclient containerdClient
|
||||
}
|
||||
|
||||
// EngineInitOptions contains the configuration settings
|
||||
// use during initialization of a containerized docker engine
|
||||
type EngineInitOptions struct {
|
||||
RegistryPrefix string
|
||||
EngineImage string
|
||||
EngineVersion string
|
||||
ConfigFile string
|
||||
scope string
|
||||
}
|
||||
|
||||
// containerdClient abstracts the containerd client to aid in testability
|
||||
type containerdClient interface {
|
||||
Containers(ctx context.Context, filters ...string) ([]containerd.Container, error)
|
||||
@ -127,33 +44,6 @@ type containerdClient interface {
|
||||
Close() error
|
||||
ContentStore() content.Store
|
||||
ContainerService() containers.Store
|
||||
}
|
||||
|
||||
// AvailableVersions groups the available versions which were discovered
|
||||
type AvailableVersions struct {
|
||||
Downgrades []DockerVersion
|
||||
Patches []DockerVersion
|
||||
Upgrades []DockerVersion
|
||||
}
|
||||
|
||||
// DockerVersion wraps a semantic version to retain the original tag
|
||||
// since the docker date based versions don't strictly follow semantic
|
||||
// versioning (leading zeros, etc.)
|
||||
type DockerVersion struct {
|
||||
ver.Version
|
||||
Tag string
|
||||
}
|
||||
|
||||
// Update stores available updates for rendering in a table
|
||||
type Update struct {
|
||||
Type string
|
||||
Version string
|
||||
Notes string
|
||||
}
|
||||
|
||||
// OutStream is an output stream used to write normal program output.
|
||||
type OutStream interface {
|
||||
io.Writer
|
||||
FD() uintptr
|
||||
IsTerminal() bool
|
||||
Install(context.Context, containerd.Image, ...containerd.InstallOpts) error
|
||||
Version(ctx context.Context) (containerd.Version, error)
|
||||
}
|
||||
|
||||
@ -2,79 +2,54 @@ package containerizedengine
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/containerd/containerd"
|
||||
"github.com/containerd/containerd/content"
|
||||
"github.com/containerd/containerd/errdefs"
|
||||
"github.com/containerd/containerd/images"
|
||||
"github.com/containerd/containerd/namespaces"
|
||||
"github.com/docker/cli/internal/pkg/containerized"
|
||||
"github.com/docker/cli/internal/versions"
|
||||
clitypes "github.com/docker/cli/types"
|
||||
"github.com/docker/distribution/reference"
|
||||
"github.com/docker/docker/api/types"
|
||||
ver "github.com/hashicorp/go-version"
|
||||
"github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// GetCurrentEngineVersion determines the current type of engine (image) and version
|
||||
func (c baseClient) GetCurrentEngineVersion(ctx context.Context) (EngineInitOptions, error) {
|
||||
ctx = namespaces.WithNamespace(ctx, engineNamespace)
|
||||
ret := EngineInitOptions{}
|
||||
currentEngine := CommunityEngineImage
|
||||
engine, err := c.GetEngine(ctx)
|
||||
if err != nil {
|
||||
if err == ErrEngineNotPresent {
|
||||
return ret, errors.Wrap(err, "failed to find existing engine")
|
||||
}
|
||||
return ret, err
|
||||
}
|
||||
imageName, err := c.getEngineImage(engine)
|
||||
if err != nil {
|
||||
return ret, err
|
||||
}
|
||||
distributionRef, err := reference.ParseNormalizedNamed(imageName)
|
||||
if err != nil {
|
||||
return ret, errors.Wrapf(err, "failed to parse image name: %s", imageName)
|
||||
}
|
||||
|
||||
if strings.Contains(distributionRef.Name(), EnterpriseEngineImage) {
|
||||
currentEngine = EnterpriseEngineImage
|
||||
}
|
||||
taggedRef, ok := distributionRef.(reference.NamedTagged)
|
||||
if !ok {
|
||||
return ret, ErrEngineImageMissingTag
|
||||
}
|
||||
ret.EngineImage = currentEngine
|
||||
ret.EngineVersion = taggedRef.Tag()
|
||||
ret.RegistryPrefix = reference.Domain(taggedRef) + "/" + path.Dir(reference.Path(taggedRef))
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// ActivateEngine will switch the image from the CE to EE image
|
||||
func (c baseClient) ActivateEngine(ctx context.Context, opts EngineInitOptions, out OutStream,
|
||||
authConfig *types.AuthConfig, healthfn func(context.Context) error) error {
|
||||
func (c *baseClient) ActivateEngine(ctx context.Context, opts clitypes.EngineInitOptions, out clitypes.OutStream,
|
||||
authConfig *types.AuthConfig) error {
|
||||
|
||||
// set the proxy scope to "ee" for activate flows
|
||||
opts.scope = "ee"
|
||||
|
||||
ctx = namespaces.WithNamespace(ctx, engineNamespace)
|
||||
|
||||
// If version is unspecified, use the existing engine version
|
||||
if opts.EngineVersion == "" {
|
||||
currentOpts, err := c.GetCurrentEngineVersion(ctx)
|
||||
// If the user didn't specify an image, determine the correct enterprise image to use
|
||||
if opts.EngineImage == "" {
|
||||
localMetadata, err := versions.GetCurrentRuntimeMetadata(opts.RuntimeMetadataDir)
|
||||
if err != nil {
|
||||
return err
|
||||
return errors.Wrap(err, "unable to determine the installed engine version. Specify which engine image to update with --engine-image")
|
||||
}
|
||||
opts.EngineVersion = currentOpts.EngineVersion
|
||||
if currentOpts.EngineImage == EnterpriseEngineImage {
|
||||
// This is a "no-op" activation so the only change would be the license - don't update the engine itself
|
||||
return nil
|
||||
|
||||
engineImage := localMetadata.EngineImage
|
||||
if engineImage == clitypes.EnterpriseEngineImage || engineImage == clitypes.CommunityEngineImage {
|
||||
opts.EngineImage = clitypes.EnterpriseEngineImage
|
||||
} else {
|
||||
// Chop off the standard prefix and retain any trailing OS specific image details
|
||||
// e.g., engine-community-dm -> engine-enterprise-dm
|
||||
engineImage = strings.TrimPrefix(engineImage, clitypes.EnterpriseEngineImage)
|
||||
engineImage = strings.TrimPrefix(engineImage, clitypes.CommunityEngineImage)
|
||||
opts.EngineImage = clitypes.EnterpriseEngineImage + engineImage
|
||||
}
|
||||
}
|
||||
return c.DoUpdate(ctx, opts, out, authConfig, healthfn)
|
||||
|
||||
ctx = namespaces.WithNamespace(ctx, engineNamespace)
|
||||
return c.DoUpdate(ctx, opts, out, authConfig)
|
||||
}
|
||||
|
||||
// DoUpdate performs the underlying engine update
|
||||
func (c baseClient) DoUpdate(ctx context.Context, opts EngineInitOptions, out OutStream,
|
||||
authConfig *types.AuthConfig, healthfn func(context.Context) error) error {
|
||||
func (c *baseClient) DoUpdate(ctx context.Context, opts clitypes.EngineInitOptions, out clitypes.OutStream,
|
||||
authConfig *types.AuthConfig) error {
|
||||
|
||||
ctx = namespaces.WithNamespace(ctx, engineNamespace)
|
||||
if opts.EngineVersion == "" {
|
||||
@ -83,7 +58,16 @@ func (c baseClient) DoUpdate(ctx context.Context, opts EngineInitOptions, out Ou
|
||||
// current engine version and automatically apply it so users
|
||||
// could stay in sync by simply having a scheduled
|
||||
// `docker engine update`
|
||||
return fmt.Errorf("please pick the version you want to update to")
|
||||
return fmt.Errorf("pick the version you want to update to with --version")
|
||||
}
|
||||
var localMetadata *clitypes.RuntimeMetadata
|
||||
if opts.EngineImage == "" {
|
||||
var err error
|
||||
localMetadata, err = versions.GetCurrentRuntimeMetadata(opts.RuntimeMetadataDir)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "unable to determine the installed engine version. Specify which engine image to update with --engine-image set to 'engine-community' or 'engine-enterprise'")
|
||||
}
|
||||
opts.EngineImage = localMetadata.EngineImage
|
||||
}
|
||||
|
||||
imageName := fmt.Sprintf("%s/%s:%s", opts.RegistryPrefix, opts.EngineImage, opts.EngineVersion)
|
||||
@ -101,30 +85,99 @@ func (c baseClient) DoUpdate(ctx context.Context, opts EngineInitOptions, out Ou
|
||||
}
|
||||
}
|
||||
|
||||
// Gather information about the existing engine so we can recreate it
|
||||
engine, err := c.GetEngine(ctx)
|
||||
// Make sure we're safe to proceed
|
||||
newMetadata, err := c.PreflightCheck(ctx, image)
|
||||
if err != nil {
|
||||
if err == ErrEngineNotPresent {
|
||||
return errors.Wrap(err, "unable to find existing engine - please use init")
|
||||
return err
|
||||
}
|
||||
if localMetadata != nil {
|
||||
if localMetadata.Platform != newMetadata.Platform {
|
||||
fmt.Fprintf(out, "\nNotice: you have switched to \"%s\". Refer to %s for update instructions.\n\n", newMetadata.Platform, getReleaseNotesURL(imageName))
|
||||
}
|
||||
}
|
||||
|
||||
if err := c.cclient.Install(ctx, image, containerd.WithInstallReplace, containerd.WithInstallPath("/usr")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// TODO verify the image has changed and don't update if nothing has changed
|
||||
|
||||
err = containerized.AtomicImageUpdate(ctx, engine, image, func() error {
|
||||
ctx, cancel := context.WithTimeout(ctx, engineWaitTimeout)
|
||||
defer cancel()
|
||||
return c.waitForEngine(ctx, out, healthfn)
|
||||
})
|
||||
if err == nil && opts.scope != "" {
|
||||
var labels map[string]string
|
||||
labels, err = engine.Labels(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
labels[proxyLabel] = opts.scope
|
||||
_, err = engine.SetLabels(ctx, labels)
|
||||
}
|
||||
return err
|
||||
return versions.WriteRuntimeMetadata(opts.RuntimeMetadataDir, newMetadata)
|
||||
}
|
||||
|
||||
// PreflightCheck verifies the specified image is compatible with the local system before proceeding to update/activate
|
||||
// If things look good, the RuntimeMetadata for the new image is returned and can be written out to the host
|
||||
func (c *baseClient) PreflightCheck(ctx context.Context, image containerd.Image) (*clitypes.RuntimeMetadata, error) {
|
||||
var metadata clitypes.RuntimeMetadata
|
||||
ic, err := image.Config(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var (
|
||||
ociimage v1.Image
|
||||
config v1.ImageConfig
|
||||
)
|
||||
switch ic.MediaType {
|
||||
case v1.MediaTypeImageConfig, images.MediaTypeDockerSchema2Config:
|
||||
p, err := content.ReadBlob(ctx, image.ContentStore(), ic)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(p, &ociimage); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
config = ociimage.Config
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown image %s config media type %s", image.Name(), ic.MediaType)
|
||||
}
|
||||
|
||||
metadataString, ok := config.Labels["com.docker."+clitypes.RuntimeMetadataName]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("image %s does not contain runtime metadata label %s", image.Name(), clitypes.RuntimeMetadataName)
|
||||
}
|
||||
err = json.Unmarshal([]byte(metadataString), &metadata)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "malformed runtime metadata file in %s", image.Name())
|
||||
}
|
||||
|
||||
// Current CLI only supports host install runtime
|
||||
if metadata.Runtime != "host_install" {
|
||||
return nil, fmt.Errorf("unsupported daemon image: %s\nConsult the release notes at %s for upgrade instructions", metadata.Runtime, getReleaseNotesURL(image.Name()))
|
||||
}
|
||||
|
||||
// Verify local containerd is new enough
|
||||
localVersion, err := c.cclient.Version(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if metadata.ContainerdMinVersion != "" {
|
||||
lv, err := ver.NewVersion(localVersion.Version)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mv, err := ver.NewVersion(metadata.ContainerdMinVersion)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if lv.LessThan(mv) {
|
||||
return nil, fmt.Errorf("local containerd is too old: %s - this engine version requires %s or newer.\nConsult the release notes at %s for upgrade instructions",
|
||||
localVersion.Version, metadata.ContainerdMinVersion, getReleaseNotesURL(image.Name()))
|
||||
}
|
||||
} // If omitted on metadata, no hard dependency on containerd version beyond 18.09 baseline
|
||||
|
||||
// All checks look OK, proceed with update
|
||||
return &metadata, nil
|
||||
}
|
||||
|
||||
// getReleaseNotesURL returns a release notes url
|
||||
// If the image name does not contain a version tag, the base release notes URL is returned
|
||||
func getReleaseNotesURL(imageName string) string {
|
||||
versionTag := ""
|
||||
distributionRef, err := reference.ParseNormalizedNamed(imageName)
|
||||
if err == nil {
|
||||
taggedRef, ok := distributionRef.(reference.NamedTagged)
|
||||
if ok {
|
||||
versionTag = taggedRef.Tag()
|
||||
}
|
||||
}
|
||||
return fmt.Sprintf("%s/%s", clitypes.ReleaseNotePrefix, versionTag)
|
||||
}
|
||||
|
||||
@ -1,169 +1,78 @@
|
||||
package containerizedengine
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/containerd/containerd"
|
||||
"github.com/containerd/containerd/cio"
|
||||
"github.com/containerd/containerd/errdefs"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/docker/cli/internal/versions"
|
||||
clitypes "github.com/docker/cli/types"
|
||||
"github.com/docker/docker/api/types"
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"gotest.tools/assert"
|
||||
)
|
||||
|
||||
func TestGetCurrentEngineVersionHappy(t *testing.T) {
|
||||
func TestActivateImagePermutations(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
image := &fakeImage{
|
||||
nameFunc: func() string {
|
||||
return "acme.com/dockermirror/" + CommunityEngineImage + ":engineversion"
|
||||
},
|
||||
}
|
||||
container := &fakeContainer{
|
||||
imageFunc: func(context.Context) (containerd.Image, error) {
|
||||
return image, nil
|
||||
},
|
||||
}
|
||||
lookedup := "not called yet"
|
||||
expectedError := fmt.Errorf("expected error")
|
||||
client := baseClient{
|
||||
cclient: &fakeContainerdClient{
|
||||
containersFunc: func(ctx context.Context, filters ...string) ([]containerd.Container, error) {
|
||||
return []containerd.Container{container}, nil
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
opts, err := client.GetCurrentEngineVersion(ctx)
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, opts.EngineImage, CommunityEngineImage)
|
||||
assert.Equal(t, opts.RegistryPrefix, "acme.com/dockermirror")
|
||||
assert.Equal(t, opts.EngineVersion, "engineversion")
|
||||
}
|
||||
|
||||
func TestGetCurrentEngineVersionEnterpriseHappy(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
image := &fakeImage{
|
||||
nameFunc: func() string {
|
||||
return "docker.io/docker/" + EnterpriseEngineImage + ":engineversion"
|
||||
},
|
||||
}
|
||||
container := &fakeContainer{
|
||||
imageFunc: func(context.Context) (containerd.Image, error) {
|
||||
return image, nil
|
||||
},
|
||||
}
|
||||
client := baseClient{
|
||||
cclient: &fakeContainerdClient{
|
||||
containersFunc: func(ctx context.Context, filters ...string) ([]containerd.Container, error) {
|
||||
return []containerd.Container{container}, nil
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
opts, err := client.GetCurrentEngineVersion(ctx)
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, opts.EngineImage, EnterpriseEngineImage)
|
||||
assert.Equal(t, opts.EngineVersion, "engineversion")
|
||||
assert.Equal(t, opts.RegistryPrefix, "docker.io/docker")
|
||||
}
|
||||
|
||||
func TestGetCurrentEngineVersionNoEngine(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
client := baseClient{
|
||||
cclient: &fakeContainerdClient{
|
||||
containersFunc: func(ctx context.Context, filters ...string) ([]containerd.Container, error) {
|
||||
return []containerd.Container{}, nil
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
_, err := client.GetCurrentEngineVersion(ctx)
|
||||
assert.ErrorContains(t, err, "failed to find existing engine")
|
||||
}
|
||||
|
||||
func TestGetCurrentEngineVersionMiscEngineError(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
expectedError := fmt.Errorf("some container lookup error")
|
||||
client := baseClient{
|
||||
cclient: &fakeContainerdClient{
|
||||
containersFunc: func(ctx context.Context, filters ...string) ([]containerd.Container, error) {
|
||||
getImageFunc: func(ctx context.Context, ref string) (containerd.Image, error) {
|
||||
lookedup = ref
|
||||
return nil, expectedError
|
||||
},
|
||||
},
|
||||
}
|
||||
tmpdir, err := ioutil.TempDir("", "enginedir")
|
||||
assert.NilError(t, err)
|
||||
defer os.RemoveAll(tmpdir)
|
||||
metadata := clitypes.RuntimeMetadata{EngineImage: clitypes.EnterpriseEngineImage}
|
||||
err = versions.WriteRuntimeMetadata(tmpdir, &metadata)
|
||||
assert.NilError(t, err)
|
||||
|
||||
_, err := client.GetCurrentEngineVersion(ctx)
|
||||
assert.Assert(t, err == expectedError)
|
||||
opts := clitypes.EngineInitOptions{
|
||||
EngineVersion: "engineversiongoeshere",
|
||||
RegistryPrefix: "registryprefixgoeshere",
|
||||
ConfigFile: "/tmp/configfilegoeshere",
|
||||
RuntimeMetadataDir: tmpdir,
|
||||
}
|
||||
|
||||
err = client.ActivateEngine(ctx, opts, command.NewOutStream(&bytes.Buffer{}), &types.AuthConfig{})
|
||||
assert.ErrorContains(t, err, expectedError.Error())
|
||||
assert.Equal(t, lookedup, fmt.Sprintf("%s/%s:%s", opts.RegistryPrefix, clitypes.EnterpriseEngineImage, opts.EngineVersion))
|
||||
|
||||
metadata = clitypes.RuntimeMetadata{EngineImage: clitypes.CommunityEngineImage}
|
||||
err = versions.WriteRuntimeMetadata(tmpdir, &metadata)
|
||||
assert.NilError(t, err)
|
||||
err = client.ActivateEngine(ctx, opts, command.NewOutStream(&bytes.Buffer{}), &types.AuthConfig{})
|
||||
assert.ErrorContains(t, err, expectedError.Error())
|
||||
assert.Equal(t, lookedup, fmt.Sprintf("%s/%s:%s", opts.RegistryPrefix, clitypes.EnterpriseEngineImage, opts.EngineVersion))
|
||||
|
||||
metadata = clitypes.RuntimeMetadata{EngineImage: clitypes.CommunityEngineImage + "-dm"}
|
||||
err = versions.WriteRuntimeMetadata(tmpdir, &metadata)
|
||||
assert.NilError(t, err)
|
||||
err = client.ActivateEngine(ctx, opts, command.NewOutStream(&bytes.Buffer{}), &types.AuthConfig{})
|
||||
assert.ErrorContains(t, err, expectedError.Error())
|
||||
assert.Equal(t, lookedup, fmt.Sprintf("%s/%s:%s", opts.RegistryPrefix, clitypes.EnterpriseEngineImage+"-dm", opts.EngineVersion))
|
||||
}
|
||||
|
||||
func TestGetCurrentEngineVersionImageFailure(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
container := &fakeContainer{
|
||||
imageFunc: func(context.Context) (containerd.Image, error) {
|
||||
return nil, fmt.Errorf("container image failure")
|
||||
},
|
||||
}
|
||||
client := baseClient{
|
||||
cclient: &fakeContainerdClient{
|
||||
containersFunc: func(ctx context.Context, filters ...string) ([]containerd.Container, error) {
|
||||
return []containerd.Container{container}, nil
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
_, err := client.GetCurrentEngineVersion(ctx)
|
||||
assert.ErrorContains(t, err, "container image failure")
|
||||
}
|
||||
|
||||
func TestGetCurrentEngineVersionMalformed(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
image := &fakeImage{
|
||||
nameFunc: func() string {
|
||||
return "imagename"
|
||||
},
|
||||
}
|
||||
container := &fakeContainer{
|
||||
imageFunc: func(context.Context) (containerd.Image, error) {
|
||||
return image, nil
|
||||
},
|
||||
}
|
||||
client := baseClient{
|
||||
cclient: &fakeContainerdClient{
|
||||
containersFunc: func(ctx context.Context, filters ...string) ([]containerd.Container, error) {
|
||||
return []containerd.Container{container}, nil
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
_, err := client.GetCurrentEngineVersion(ctx)
|
||||
assert.Assert(t, err == ErrEngineImageMissingTag)
|
||||
}
|
||||
|
||||
func TestActivateNoEngine(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
client := baseClient{
|
||||
cclient: &fakeContainerdClient{
|
||||
containersFunc: func(ctx context.Context, filters ...string) ([]containerd.Container, error) {
|
||||
return []containerd.Container{}, nil
|
||||
},
|
||||
},
|
||||
}
|
||||
opts := EngineInitOptions{
|
||||
EngineVersion: "engineversiongoeshere",
|
||||
RegistryPrefix: "registryprefixgoeshere",
|
||||
ConfigFile: "/tmp/configfilegoeshere",
|
||||
EngineImage: EnterpriseEngineImage,
|
||||
}
|
||||
|
||||
err := client.ActivateEngine(ctx, opts, &testOutStream{}, &types.AuthConfig{}, healthfnHappy)
|
||||
assert.ErrorContains(t, err, "unable to find")
|
||||
}
|
||||
|
||||
func TestActivateNoChange(t *testing.T) {
|
||||
func TestActivateConfigFailure(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
registryPrefix := "registryprefixgoeshere"
|
||||
image := &fakeImage{
|
||||
nameFunc: func() string {
|
||||
return registryPrefix + "/" + EnterpriseEngineImage + ":engineversion"
|
||||
return registryPrefix + "/" + clitypes.EnterpriseEngineImage + ":engineversion"
|
||||
},
|
||||
configFunc: func(ctx context.Context) (ocispec.Descriptor, error) {
|
||||
return ocispec.Descriptor{}, fmt.Errorf("config lookup failure")
|
||||
},
|
||||
}
|
||||
container := &fakeContainer{
|
||||
@ -182,17 +91,27 @@ func TestActivateNoChange(t *testing.T) {
|
||||
containersFunc: func(ctx context.Context, filters ...string) ([]containerd.Container, error) {
|
||||
return []containerd.Container{container}, nil
|
||||
},
|
||||
getImageFunc: func(ctx context.Context, ref string) (containerd.Image, error) {
|
||||
return image, nil
|
||||
},
|
||||
},
|
||||
}
|
||||
opts := EngineInitOptions{
|
||||
EngineVersion: "engineversiongoeshere",
|
||||
RegistryPrefix: "registryprefixgoeshere",
|
||||
ConfigFile: "/tmp/configfilegoeshere",
|
||||
EngineImage: EnterpriseEngineImage,
|
||||
tmpdir, err := ioutil.TempDir("", "engindir")
|
||||
assert.NilError(t, err)
|
||||
defer os.RemoveAll(tmpdir)
|
||||
metadata := clitypes.RuntimeMetadata{EngineImage: clitypes.CommunityEngineImage}
|
||||
err = versions.WriteRuntimeMetadata(tmpdir, &metadata)
|
||||
assert.NilError(t, err)
|
||||
opts := clitypes.EngineInitOptions{
|
||||
EngineVersion: "engineversiongoeshere",
|
||||
RegistryPrefix: "registryprefixgoeshere",
|
||||
ConfigFile: "/tmp/configfilegoeshere",
|
||||
EngineImage: clitypes.EnterpriseEngineImage,
|
||||
RuntimeMetadataDir: tmpdir,
|
||||
}
|
||||
|
||||
err := client.ActivateEngine(ctx, opts, &testOutStream{}, &types.AuthConfig{}, healthfnHappy)
|
||||
assert.NilError(t, err)
|
||||
err = client.ActivateEngine(ctx, opts, command.NewOutStream(&bytes.Buffer{}), &types.AuthConfig{})
|
||||
assert.ErrorContains(t, err, "config lookup failure")
|
||||
}
|
||||
|
||||
func TestActivateDoUpdateFail(t *testing.T) {
|
||||
@ -219,38 +138,60 @@ func TestActivateDoUpdateFail(t *testing.T) {
|
||||
},
|
||||
},
|
||||
}
|
||||
opts := EngineInitOptions{
|
||||
EngineVersion: "engineversiongoeshere",
|
||||
RegistryPrefix: "registryprefixgoeshere",
|
||||
ConfigFile: "/tmp/configfilegoeshere",
|
||||
EngineImage: EnterpriseEngineImage,
|
||||
tmpdir, err := ioutil.TempDir("", "enginedir")
|
||||
assert.NilError(t, err)
|
||||
defer os.RemoveAll(tmpdir)
|
||||
metadata := clitypes.RuntimeMetadata{EngineImage: clitypes.CommunityEngineImage}
|
||||
err = versions.WriteRuntimeMetadata(tmpdir, &metadata)
|
||||
assert.NilError(t, err)
|
||||
opts := clitypes.EngineInitOptions{
|
||||
EngineVersion: "engineversiongoeshere",
|
||||
RegistryPrefix: "registryprefixgoeshere",
|
||||
ConfigFile: "/tmp/configfilegoeshere",
|
||||
EngineImage: clitypes.EnterpriseEngineImage,
|
||||
RuntimeMetadataDir: tmpdir,
|
||||
}
|
||||
|
||||
err := client.ActivateEngine(ctx, opts, &testOutStream{}, &types.AuthConfig{}, healthfnHappy)
|
||||
err = client.ActivateEngine(ctx, opts, command.NewOutStream(&bytes.Buffer{}), &types.AuthConfig{})
|
||||
assert.ErrorContains(t, err, "check for image")
|
||||
assert.ErrorContains(t, err, "something went wrong")
|
||||
}
|
||||
|
||||
func TestDoUpdateNoVersion(t *testing.T) {
|
||||
tmpdir, err := ioutil.TempDir("", "enginedir")
|
||||
assert.NilError(t, err)
|
||||
defer os.RemoveAll(tmpdir)
|
||||
metadata := clitypes.RuntimeMetadata{EngineImage: clitypes.EnterpriseEngineImage}
|
||||
err = versions.WriteRuntimeMetadata(tmpdir, &metadata)
|
||||
assert.NilError(t, err)
|
||||
ctx := context.Background()
|
||||
opts := EngineInitOptions{
|
||||
EngineVersion: "",
|
||||
RegistryPrefix: "registryprefixgoeshere",
|
||||
ConfigFile: "/tmp/configfilegoeshere",
|
||||
EngineImage: EnterpriseEngineImage,
|
||||
opts := clitypes.EngineInitOptions{
|
||||
EngineVersion: "",
|
||||
RegistryPrefix: "registryprefixgoeshere",
|
||||
ConfigFile: "/tmp/configfilegoeshere",
|
||||
EngineImage: clitypes.EnterpriseEngineImage,
|
||||
RuntimeMetadataDir: tmpdir,
|
||||
}
|
||||
|
||||
client := baseClient{}
|
||||
err := client.DoUpdate(ctx, opts, &testOutStream{}, &types.AuthConfig{}, healthfnHappy)
|
||||
assert.ErrorContains(t, err, "please pick the version you")
|
||||
err = client.DoUpdate(ctx, opts, command.NewOutStream(&bytes.Buffer{}), &types.AuthConfig{})
|
||||
assert.ErrorContains(t, err, "pick the version you")
|
||||
}
|
||||
|
||||
func TestDoUpdateImageMiscError(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
opts := EngineInitOptions{
|
||||
EngineVersion: "engineversiongoeshere",
|
||||
RegistryPrefix: "registryprefixgoeshere",
|
||||
ConfigFile: "/tmp/configfilegoeshere",
|
||||
EngineImage: "testnamegoeshere",
|
||||
tmpdir, err := ioutil.TempDir("", "enginedir")
|
||||
assert.NilError(t, err)
|
||||
defer os.RemoveAll(tmpdir)
|
||||
metadata := clitypes.RuntimeMetadata{EngineImage: clitypes.EnterpriseEngineImage}
|
||||
err = versions.WriteRuntimeMetadata(tmpdir, &metadata)
|
||||
assert.NilError(t, err)
|
||||
opts := clitypes.EngineInitOptions{
|
||||
EngineVersion: "engineversiongoeshere",
|
||||
RegistryPrefix: "registryprefixgoeshere",
|
||||
ConfigFile: "/tmp/configfilegoeshere",
|
||||
EngineImage: "testnamegoeshere",
|
||||
RuntimeMetadataDir: tmpdir,
|
||||
}
|
||||
client := baseClient{
|
||||
cclient: &fakeContainerdClient{
|
||||
@ -260,18 +201,26 @@ func TestDoUpdateImageMiscError(t *testing.T) {
|
||||
},
|
||||
},
|
||||
}
|
||||
err := client.DoUpdate(ctx, opts, &testOutStream{}, &types.AuthConfig{}, healthfnHappy)
|
||||
|
||||
err = client.DoUpdate(ctx, opts, command.NewOutStream(&bytes.Buffer{}), &types.AuthConfig{})
|
||||
assert.ErrorContains(t, err, "check for image")
|
||||
assert.ErrorContains(t, err, "something went wrong")
|
||||
}
|
||||
|
||||
func TestDoUpdatePullFail(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
opts := EngineInitOptions{
|
||||
EngineVersion: "engineversiongoeshere",
|
||||
RegistryPrefix: "registryprefixgoeshere",
|
||||
ConfigFile: "/tmp/configfilegoeshere",
|
||||
EngineImage: "testnamegoeshere",
|
||||
tmpdir, err := ioutil.TempDir("", "enginedir")
|
||||
assert.NilError(t, err)
|
||||
defer os.RemoveAll(tmpdir)
|
||||
metadata := clitypes.RuntimeMetadata{EngineImage: clitypes.EnterpriseEngineImage}
|
||||
err = versions.WriteRuntimeMetadata(tmpdir, &metadata)
|
||||
assert.NilError(t, err)
|
||||
opts := clitypes.EngineInitOptions{
|
||||
EngineVersion: "engineversiongoeshere",
|
||||
RegistryPrefix: "registryprefixgoeshere",
|
||||
ConfigFile: "/tmp/configfilegoeshere",
|
||||
EngineImage: "testnamegoeshere",
|
||||
RuntimeMetadataDir: tmpdir,
|
||||
}
|
||||
client := baseClient{
|
||||
cclient: &fakeContainerdClient{
|
||||
@ -284,35 +233,68 @@ func TestDoUpdatePullFail(t *testing.T) {
|
||||
},
|
||||
},
|
||||
}
|
||||
err := client.DoUpdate(ctx, opts, &testOutStream{}, &types.AuthConfig{}, healthfnHappy)
|
||||
|
||||
err = client.DoUpdate(ctx, opts, command.NewOutStream(&bytes.Buffer{}), &types.AuthConfig{})
|
||||
assert.ErrorContains(t, err, "unable to pull")
|
||||
assert.ErrorContains(t, err, "pull failure")
|
||||
}
|
||||
|
||||
func TestDoUpdateEngineMissing(t *testing.T) {
|
||||
func TestActivateDoUpdateVerifyImageName(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
opts := EngineInitOptions{
|
||||
EngineVersion: "engineversiongoeshere",
|
||||
RegistryPrefix: "registryprefixgoeshere",
|
||||
ConfigFile: "/tmp/configfilegoeshere",
|
||||
EngineImage: "testnamegoeshere",
|
||||
}
|
||||
registryPrefix := "registryprefixgoeshere"
|
||||
image := &fakeImage{
|
||||
nameFunc: func() string {
|
||||
return "imagenamehere"
|
||||
return registryPrefix + "/ce-engine:engineversion"
|
||||
},
|
||||
}
|
||||
container := &fakeContainer{
|
||||
imageFunc: func(context.Context) (containerd.Image, error) {
|
||||
return image, nil
|
||||
},
|
||||
}
|
||||
requestedImage := "unset"
|
||||
client := baseClient{
|
||||
cclient: &fakeContainerdClient{
|
||||
getImageFunc: func(ctx context.Context, ref string) (containerd.Image, error) {
|
||||
return image, nil
|
||||
|
||||
},
|
||||
containersFunc: func(ctx context.Context, filters ...string) ([]containerd.Container, error) {
|
||||
return []containerd.Container{}, nil
|
||||
return []containerd.Container{container}, nil
|
||||
},
|
||||
getImageFunc: func(ctx context.Context, ref string) (containerd.Image, error) {
|
||||
requestedImage = ref
|
||||
return nil, fmt.Errorf("something went wrong")
|
||||
|
||||
},
|
||||
},
|
||||
}
|
||||
err := client.DoUpdate(ctx, opts, &testOutStream{}, &types.AuthConfig{}, healthfnHappy)
|
||||
assert.ErrorContains(t, err, "unable to find existing engine")
|
||||
tmpdir, err := ioutil.TempDir("", "enginedir")
|
||||
assert.NilError(t, err)
|
||||
defer os.RemoveAll(tmpdir)
|
||||
metadata := clitypes.RuntimeMetadata{EngineImage: clitypes.EnterpriseEngineImage}
|
||||
err = versions.WriteRuntimeMetadata(tmpdir, &metadata)
|
||||
assert.NilError(t, err)
|
||||
|
||||
opts := clitypes.EngineInitOptions{
|
||||
EngineVersion: "engineversiongoeshere",
|
||||
RegistryPrefix: "registryprefixgoeshere",
|
||||
EngineImage: "testnamegoeshere",
|
||||
ConfigFile: "/tmp/configfilegoeshere",
|
||||
RuntimeMetadataDir: tmpdir,
|
||||
}
|
||||
|
||||
err = client.ActivateEngine(ctx, opts, command.NewOutStream(&bytes.Buffer{}), &types.AuthConfig{})
|
||||
assert.ErrorContains(t, err, "check for image")
|
||||
assert.ErrorContains(t, err, "something went wrong")
|
||||
expectedImage := fmt.Sprintf("%s/%s:%s", opts.RegistryPrefix, opts.EngineImage, opts.EngineVersion)
|
||||
assert.Assert(t, requestedImage == expectedImage, "%s != %s", requestedImage, expectedImage)
|
||||
}
|
||||
|
||||
func TestGetReleaseNotesURL(t *testing.T) {
|
||||
imageName := "bogus image name #$%&@!"
|
||||
url := getReleaseNotesURL(imageName)
|
||||
assert.Equal(t, url, clitypes.ReleaseNotePrefix+"/")
|
||||
imageName = "foo.bar/valid/repowithouttag"
|
||||
url = getReleaseNotesURL(imageName)
|
||||
assert.Equal(t, url, clitypes.ReleaseNotePrefix+"/")
|
||||
imageName = "foo.bar/valid/repowithouttag:tag123"
|
||||
url = getReleaseNotesURL(imageName)
|
||||
assert.Equal(t, url, clitypes.ReleaseNotePrefix+"/tag123")
|
||||
}
|
||||
|
||||
@ -1,72 +0,0 @@
|
||||
package containerizedengine
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sort"
|
||||
|
||||
registryclient "github.com/docker/cli/cli/registry/client"
|
||||
"github.com/docker/distribution/reference"
|
||||
ver "github.com/hashicorp/go-version"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// GetEngineVersions reports the versions of the engine that are available
|
||||
func (c baseClient) GetEngineVersions(ctx context.Context, registryClient registryclient.RegistryClient, currentVersion, imageName string) (AvailableVersions, error) {
|
||||
imageRef, err := reference.ParseNormalizedNamed(imageName)
|
||||
if err != nil {
|
||||
return AvailableVersions{}, err
|
||||
}
|
||||
|
||||
tags, err := registryClient.GetTags(ctx, imageRef)
|
||||
if err != nil {
|
||||
return AvailableVersions{}, err
|
||||
}
|
||||
|
||||
return parseTags(tags, currentVersion)
|
||||
}
|
||||
|
||||
func parseTags(tags []string, currentVersion string) (AvailableVersions, error) {
|
||||
var ret AvailableVersions
|
||||
currentVer, err := ver.NewVersion(currentVersion)
|
||||
if err != nil {
|
||||
return ret, errors.Wrapf(err, "failed to parse existing version %s", currentVersion)
|
||||
}
|
||||
downgrades := []DockerVersion{}
|
||||
patches := []DockerVersion{}
|
||||
upgrades := []DockerVersion{}
|
||||
currentSegments := currentVer.Segments()
|
||||
for _, tag := range tags {
|
||||
tmp, err := ver.NewVersion(tag)
|
||||
if err != nil {
|
||||
logrus.Debugf("Unable to parse %s: %s", tag, err)
|
||||
continue
|
||||
}
|
||||
testVersion := DockerVersion{Version: *tmp, Tag: tag}
|
||||
if testVersion.LessThan(currentVer) {
|
||||
downgrades = append(downgrades, testVersion)
|
||||
continue
|
||||
}
|
||||
testSegments := testVersion.Segments()
|
||||
// lib always provides min 3 segments
|
||||
if testSegments[0] == currentSegments[0] &&
|
||||
testSegments[1] == currentSegments[1] {
|
||||
patches = append(patches, testVersion)
|
||||
} else {
|
||||
upgrades = append(upgrades, testVersion)
|
||||
}
|
||||
}
|
||||
sort.Slice(downgrades, func(i, j int) bool {
|
||||
return downgrades[i].Version.LessThan(&downgrades[j].Version)
|
||||
})
|
||||
sort.Slice(patches, func(i, j int) bool {
|
||||
return patches[i].Version.LessThan(&patches[j].Version)
|
||||
})
|
||||
sort.Slice(upgrades, func(i, j int) bool {
|
||||
return upgrades[i].Version.LessThan(&upgrades[j].Version)
|
||||
})
|
||||
ret.Downgrades = downgrades
|
||||
ret.Patches = patches
|
||||
ret.Upgrades = upgrades
|
||||
return ret, nil
|
||||
}
|
||||
@ -20,6 +20,7 @@ type (
|
||||
parseLicenseFunc func(license []byte) (parsedLicense *model.IssuedLicense, err error)
|
||||
storeLicenseFunc func(ctx context.Context, dclnt licensing.WrappedDockerClient, licenses *model.IssuedLicense, localRootDir string) error
|
||||
loadLocalLicenseFunc func(ctx context.Context, dclnt licensing.WrappedDockerClient) (*model.Subscription, error)
|
||||
summarizeLicenseFunc func(*model.CheckResponse, string) *model.Subscription
|
||||
}
|
||||
)
|
||||
|
||||
@ -102,3 +103,10 @@ func (c *fakeLicensingClient) LoadLocalLicense(ctx context.Context, dclnt licens
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (c *fakeLicensingClient) SummarizeLicense(cr *model.CheckResponse, keyid string) *model.Subscription {
|
||||
if c.summarizeLicenseFunc != nil {
|
||||
return c.summarizeLicenseFunc(cr, keyid)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package licenseutils
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
@ -19,7 +20,7 @@ import (
|
||||
// HubUser wraps a licensing client and holds key information
|
||||
// for a user to avoid multiple lookups
|
||||
type HubUser struct {
|
||||
client licensing.Client
|
||||
Client licensing.Client
|
||||
token string
|
||||
User model.User
|
||||
Orgs []model.Org
|
||||
@ -35,18 +36,22 @@ func (u HubUser) GetOrgByID(orgID string) (model.Org, error) {
|
||||
return model.Org{}, fmt.Errorf("org %s not found", orgID)
|
||||
}
|
||||
|
||||
// Login to the license server and return a client that can be used to look up and download license files or generate new trial licenses
|
||||
func Login(ctx context.Context, authConfig *types.AuthConfig) (HubUser, error) {
|
||||
func getClient() (licensing.Client, error) {
|
||||
baseURI, err := url.Parse(licensingDefaultBaseURI)
|
||||
if err != nil {
|
||||
return HubUser{}, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
lclient, err := licensing.New(&licensing.Config{
|
||||
return licensing.New(&licensing.Config{
|
||||
BaseURI: *baseURI,
|
||||
HTTPClient: &http.Client{},
|
||||
PublicKeys: licensingPublicKeys,
|
||||
})
|
||||
}
|
||||
|
||||
// Login to the license server and return a client that can be used to look up and download license files or generate new trial licenses
|
||||
func Login(ctx context.Context, authConfig *types.AuthConfig) (HubUser, error) {
|
||||
lclient, err := getClient()
|
||||
if err != nil {
|
||||
return HubUser{}, err
|
||||
}
|
||||
@ -68,7 +73,7 @@ func Login(ctx context.Context, authConfig *types.AuthConfig) (HubUser, error) {
|
||||
return HubUser{}, err
|
||||
}
|
||||
return HubUser{
|
||||
client: lclient,
|
||||
Client: lclient,
|
||||
token: token,
|
||||
User: *user,
|
||||
Orgs: orgs,
|
||||
@ -78,12 +83,12 @@ func Login(ctx context.Context, authConfig *types.AuthConfig) (HubUser, error) {
|
||||
|
||||
// GetAvailableLicenses finds all available licenses for a given account and their orgs
|
||||
func (u HubUser) GetAvailableLicenses(ctx context.Context) ([]LicenseDisplay, error) {
|
||||
subs, err := u.client.ListSubscriptions(ctx, u.token, u.User.ID)
|
||||
subs, err := u.Client.ListSubscriptions(ctx, u.token, u.User.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, org := range u.Orgs {
|
||||
orgSub, err := u.client.ListSubscriptions(ctx, u.token, org.ID)
|
||||
orgSub, err := u.Client.ListSubscriptions(ctx, u.token, org.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -97,7 +102,7 @@ func (u HubUser) GetAvailableLicenses(ctx context.Context) ([]LicenseDisplay, er
|
||||
// Filter out expired licenses
|
||||
i := 0
|
||||
for _, s := range subs {
|
||||
if s.State != "expired" && s.Expires != nil {
|
||||
if s.State == "active" && s.Expires != nil {
|
||||
owner := ""
|
||||
if s.DockerID == u.User.ID {
|
||||
owner = u.User.Username
|
||||
@ -129,42 +134,50 @@ func (u HubUser) GetAvailableLicenses(ctx context.Context) ([]LicenseDisplay, er
|
||||
|
||||
// GenerateTrialLicense will generate a new trial license for the specified user or org
|
||||
func (u HubUser) GenerateTrialLicense(ctx context.Context, targetID string) (*model.IssuedLicense, error) {
|
||||
subID, err := u.client.GenerateNewTrialSubscription(ctx, u.token, targetID, u.User.Email)
|
||||
subID, err := u.Client.GenerateNewTrialSubscription(ctx, u.token, targetID, u.User.Email)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return u.client.DownloadLicenseFromHub(ctx, u.token, subID)
|
||||
return u.Client.DownloadLicenseFromHub(ctx, u.token, subID)
|
||||
}
|
||||
|
||||
// GetIssuedLicense will download a license by ID
|
||||
func (u HubUser) GetIssuedLicense(ctx context.Context, ID string) (*model.IssuedLicense, error) {
|
||||
return u.client.DownloadLicenseFromHub(ctx, u.token, ID)
|
||||
return u.Client.DownloadLicenseFromHub(ctx, u.token, ID)
|
||||
}
|
||||
|
||||
// LoadLocalIssuedLicense will load a local license file
|
||||
func LoadLocalIssuedLicense(ctx context.Context, filename string) (*model.IssuedLicense, error) {
|
||||
baseURI, err := url.Parse(licensingDefaultBaseURI)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
lclient, err := licensing.New(&licensing.Config{
|
||||
BaseURI: *baseURI,
|
||||
HTTPClient: &http.Client{},
|
||||
PublicKeys: licensingPublicKeys,
|
||||
})
|
||||
lclient, err := getClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return doLoadLocalIssuedLicense(ctx, filename, lclient)
|
||||
}
|
||||
|
||||
// GetLicenseSummary summarizes the license for the user
|
||||
func GetLicenseSummary(ctx context.Context, license model.IssuedLicense) (string, error) {
|
||||
lclient, err := getClient()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
cr, err := lclient.VerifyLicense(ctx, license)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return lclient.SummarizeLicense(cr, license.KeyID).String(), nil
|
||||
}
|
||||
|
||||
func doLoadLocalIssuedLicense(ctx context.Context, filename string, lclient licensing.Client) (*model.IssuedLicense, error) {
|
||||
var license model.IssuedLicense
|
||||
data, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// The file may contain a leading BOM, which will choke the
|
||||
// json deserializer.
|
||||
data = bytes.TrimPrefix(data, []byte("\xef\xbb\xbf"))
|
||||
|
||||
err = json.Unmarshal(data, &license)
|
||||
if err != nil {
|
||||
|
||||
@ -43,7 +43,7 @@ func TestGetOrgByID(t *testing.T) {
|
||||
func TestGetAvailableLicensesListFail(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
user := HubUser{
|
||||
client: &fakeLicensingClient{
|
||||
Client: &fakeLicensingClient{
|
||||
listSubscriptionsFunc: func(ctx context.Context, authToken, dockerID string) (response []*model.Subscription, err error) {
|
||||
return nil, fmt.Errorf("list subscriptions error")
|
||||
},
|
||||
@ -59,7 +59,7 @@ func TestGetAvailableLicensesOrgFail(t *testing.T) {
|
||||
Orgs: []model.Org{
|
||||
{ID: "orgid"},
|
||||
},
|
||||
client: &fakeLicensingClient{
|
||||
Client: &fakeLicensingClient{
|
||||
listSubscriptionsFunc: func(ctx context.Context, authToken, dockerID string) (response []*model.Subscription, err error) {
|
||||
if dockerID == "orgid" {
|
||||
return nil, fmt.Errorf("list subscriptions org error")
|
||||
@ -86,7 +86,7 @@ func TestGetAvailableLicensesHappy(t *testing.T) {
|
||||
Orgname: "orgname",
|
||||
},
|
||||
},
|
||||
client: &fakeLicensingClient{
|
||||
Client: &fakeLicensingClient{
|
||||
listSubscriptionsFunc: func(ctx context.Context, authToken, dockerID string) (response []*model.Subscription, err error) {
|
||||
if dockerID == "orgid" {
|
||||
return []*model.Subscription{
|
||||
@ -146,7 +146,7 @@ func TestGetAvailableLicensesHappy(t *testing.T) {
|
||||
func TestGenerateTrialFail(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
user := HubUser{
|
||||
client: &fakeLicensingClient{
|
||||
Client: &fakeLicensingClient{
|
||||
generateNewTrialSubscriptionFunc: func(ctx context.Context, authToken, dockerID, email string) (subscriptionID string, err error) {
|
||||
return "", fmt.Errorf("generate trial failure")
|
||||
},
|
||||
@ -160,7 +160,7 @@ func TestGenerateTrialFail(t *testing.T) {
|
||||
func TestGenerateTrialHappy(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
user := HubUser{
|
||||
client: &fakeLicensingClient{
|
||||
Client: &fakeLicensingClient{
|
||||
generateNewTrialSubscriptionFunc: func(ctx context.Context, authToken, dockerID, email string) (subscriptionID string, err error) {
|
||||
return "subid", nil
|
||||
},
|
||||
@ -174,7 +174,7 @@ func TestGenerateTrialHappy(t *testing.T) {
|
||||
func TestGetIssuedLicense(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
user := HubUser{
|
||||
client: &fakeLicensingClient{},
|
||||
Client: &fakeLicensingClient{},
|
||||
}
|
||||
id := "idgoeshere"
|
||||
_, err := user.GetIssuedLicense(ctx, id)
|
||||
|
||||
@ -12,7 +12,7 @@ import (
|
||||
manifeststore "github.com/docker/cli/cli/manifest/store"
|
||||
registryclient "github.com/docker/cli/cli/registry/client"
|
||||
"github.com/docker/cli/cli/trust"
|
||||
"github.com/docker/cli/internal/containerizedengine"
|
||||
clitypes "github.com/docker/cli/types"
|
||||
"github.com/docker/docker/client"
|
||||
notaryclient "github.com/theupdateframework/notary/client"
|
||||
)
|
||||
@ -20,7 +20,7 @@ import (
|
||||
// NotaryClientFuncType defines a function that returns a fake notary client
|
||||
type NotaryClientFuncType func(imgRefAndAuth trust.ImageRefAndAuth, actions []string) (notaryclient.Repository, error)
|
||||
type clientInfoFuncType func() command.ClientInfo
|
||||
type containerizedEngineFuncType func(string) (containerizedengine.Client, error)
|
||||
type containerizedEngineFuncType func(string) (clitypes.ContainerizedClient, error)
|
||||
|
||||
// FakeCli emulates the default DockerCli
|
||||
type FakeCli struct {
|
||||
@ -172,7 +172,7 @@ func EnableContentTrust(c *FakeCli) {
|
||||
}
|
||||
|
||||
// NewContainerizedEngineClient returns a containerized engine client
|
||||
func (c *FakeCli) NewContainerizedEngineClient(sockPath string) (containerizedengine.Client, error) {
|
||||
func (c *FakeCli) NewContainerizedEngineClient(sockPath string) (clitypes.ContainerizedClient, error) {
|
||||
if c.containerizedEngineClientFunc != nil {
|
||||
return c.containerizedEngineClientFunc(sockPath)
|
||||
}
|
||||
|
||||
127
internal/versions/versions.go
Normal file
127
internal/versions/versions.go
Normal file
@ -0,0 +1,127 @@
|
||||
package versions
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
|
||||
registryclient "github.com/docker/cli/cli/registry/client"
|
||||
clitypes "github.com/docker/cli/types"
|
||||
"github.com/docker/distribution/reference"
|
||||
ver "github.com/hashicorp/go-version"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const (
|
||||
// defaultRuntimeMetadataDir is the location where the metadata file is stored
|
||||
defaultRuntimeMetadataDir = "/var/lib/docker-engine"
|
||||
)
|
||||
|
||||
// GetEngineVersions reports the versions of the engine that are available
|
||||
func GetEngineVersions(ctx context.Context, registryClient registryclient.RegistryClient, registryPrefix, imageName, versionString string) (clitypes.AvailableVersions, error) {
|
||||
|
||||
if imageName == "" {
|
||||
var err error
|
||||
localMetadata, err := GetCurrentRuntimeMetadata("")
|
||||
if err != nil {
|
||||
return clitypes.AvailableVersions{}, err
|
||||
}
|
||||
imageName = localMetadata.EngineImage
|
||||
}
|
||||
imageRef, err := reference.ParseNormalizedNamed(path.Join(registryPrefix, imageName))
|
||||
if err != nil {
|
||||
return clitypes.AvailableVersions{}, err
|
||||
}
|
||||
|
||||
tags, err := registryClient.GetTags(ctx, imageRef)
|
||||
if err != nil {
|
||||
return clitypes.AvailableVersions{}, err
|
||||
}
|
||||
|
||||
return parseTags(tags, versionString)
|
||||
}
|
||||
|
||||
func parseTags(tags []string, currentVersion string) (clitypes.AvailableVersions, error) {
|
||||
var ret clitypes.AvailableVersions
|
||||
currentVer, err := ver.NewVersion(currentVersion)
|
||||
if err != nil {
|
||||
return ret, errors.Wrapf(err, "failed to parse existing version %s", currentVersion)
|
||||
}
|
||||
downgrades := []clitypes.DockerVersion{}
|
||||
patches := []clitypes.DockerVersion{}
|
||||
upgrades := []clitypes.DockerVersion{}
|
||||
currentSegments := currentVer.Segments()
|
||||
for _, tag := range tags {
|
||||
tmp, err := ver.NewVersion(tag)
|
||||
if err != nil {
|
||||
logrus.Debugf("Unable to parse %s: %s", tag, err)
|
||||
continue
|
||||
}
|
||||
testVersion := clitypes.DockerVersion{Version: *tmp, Tag: tag}
|
||||
if testVersion.LessThan(currentVer) {
|
||||
downgrades = append(downgrades, testVersion)
|
||||
continue
|
||||
}
|
||||
testSegments := testVersion.Segments()
|
||||
// lib always provides min 3 segments
|
||||
if testSegments[0] == currentSegments[0] &&
|
||||
testSegments[1] == currentSegments[1] {
|
||||
patches = append(patches, testVersion)
|
||||
} else {
|
||||
upgrades = append(upgrades, testVersion)
|
||||
}
|
||||
}
|
||||
sort.Slice(downgrades, func(i, j int) bool {
|
||||
return downgrades[i].Version.LessThan(&downgrades[j].Version)
|
||||
})
|
||||
sort.Slice(patches, func(i, j int) bool {
|
||||
return patches[i].Version.LessThan(&patches[j].Version)
|
||||
})
|
||||
sort.Slice(upgrades, func(i, j int) bool {
|
||||
return upgrades[i].Version.LessThan(&upgrades[j].Version)
|
||||
})
|
||||
ret.Downgrades = downgrades
|
||||
ret.Patches = patches
|
||||
ret.Upgrades = upgrades
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// GetCurrentRuntimeMetadata loads the current daemon runtime metadata information from the local host
|
||||
func GetCurrentRuntimeMetadata(metadataDir string) (*clitypes.RuntimeMetadata, error) {
|
||||
if metadataDir == "" {
|
||||
metadataDir = defaultRuntimeMetadataDir
|
||||
}
|
||||
filename := filepath.Join(metadataDir, clitypes.RuntimeMetadataName+".json")
|
||||
|
||||
data, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var res clitypes.RuntimeMetadata
|
||||
err = json.Unmarshal(data, &res)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "malformed runtime metadata file %s", filename)
|
||||
}
|
||||
return &res, nil
|
||||
}
|
||||
|
||||
// WriteRuntimeMetadata stores the metadata on the local system
|
||||
func WriteRuntimeMetadata(metadataDir string, metadata *clitypes.RuntimeMetadata) error {
|
||||
if metadataDir == "" {
|
||||
metadataDir = defaultRuntimeMetadataDir
|
||||
}
|
||||
filename := filepath.Join(metadataDir, clitypes.RuntimeMetadataName+".json")
|
||||
|
||||
data, err := json.Marshal(metadata)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
os.Remove(filename)
|
||||
return ioutil.WriteFile(filename, data, 0644)
|
||||
}
|
||||
@ -1,22 +1,15 @@
|
||||
package containerizedengine
|
||||
package versions
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
clitypes "github.com/docker/cli/types"
|
||||
"gotest.tools/assert"
|
||||
)
|
||||
|
||||
func TestGetEngineVersionsBadImage(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
client := baseClient{}
|
||||
|
||||
currentVersion := "currentversiongoeshere"
|
||||
imageName := "this is an illegal image $%^&"
|
||||
_, err := client.GetEngineVersions(ctx, nil, currentVersion, imageName)
|
||||
assert.ErrorContains(t, err, "invalid reference format")
|
||||
}
|
||||
|
||||
func TestParseTagsSimple(t *testing.T) {
|
||||
tags := []string{"1.0.0", "1.1.2", "1.1.1", "1.2.2"}
|
||||
currentVersion := "1.1.0"
|
||||
@ -78,3 +71,35 @@ func TestParseBadCurrent2(t *testing.T) {
|
||||
_, err := parseTags(tags, currentVersion)
|
||||
assert.ErrorContains(t, err, "failed to parse existing")
|
||||
}
|
||||
|
||||
func TestGetCurrentRuntimeMetadataNotPresent(t *testing.T) {
|
||||
tmpdir, err := ioutil.TempDir("", "docker-root")
|
||||
assert.NilError(t, err)
|
||||
defer os.RemoveAll(tmpdir)
|
||||
_, err = GetCurrentRuntimeMetadata(tmpdir)
|
||||
assert.ErrorType(t, err, os.IsNotExist)
|
||||
}
|
||||
|
||||
func TestGetCurrentRuntimeMetadataBadJson(t *testing.T) {
|
||||
tmpdir, err := ioutil.TempDir("", "docker-root")
|
||||
assert.NilError(t, err)
|
||||
defer os.RemoveAll(tmpdir)
|
||||
filename := filepath.Join(tmpdir, clitypes.RuntimeMetadataName+".json")
|
||||
err = ioutil.WriteFile(filename, []byte("not json"), 0644)
|
||||
assert.NilError(t, err)
|
||||
_, err = GetCurrentRuntimeMetadata(tmpdir)
|
||||
assert.ErrorContains(t, err, "malformed runtime metadata file")
|
||||
}
|
||||
|
||||
func TestGetCurrentRuntimeMetadataHappyPath(t *testing.T) {
|
||||
tmpdir, err := ioutil.TempDir("", "docker-root")
|
||||
assert.NilError(t, err)
|
||||
defer os.RemoveAll(tmpdir)
|
||||
metadata := clitypes.RuntimeMetadata{Platform: "platformgoeshere"}
|
||||
err = WriteRuntimeMetadata(tmpdir, &metadata)
|
||||
assert.NilError(t, err)
|
||||
|
||||
res, err := GetCurrentRuntimeMetadata(tmpdir)
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, res.Platform, "platformgoeshere")
|
||||
}
|
||||
@ -25,7 +25,7 @@ func generateManPages(opts *options) error {
|
||||
}
|
||||
|
||||
stdin, stdout, stderr := term.StdStreams()
|
||||
dockerCli := command.NewDockerCli(stdin, stdout, stderr, false)
|
||||
dockerCli := command.NewDockerCli(stdin, stdout, stderr, false, nil)
|
||||
cmd := &cobra.Command{Use: "docker"}
|
||||
commands.AddCommands(cmd, dockerCli)
|
||||
source := filepath.Join(opts.source, descriptionSourcePath)
|
||||
|
||||
@ -38,5 +38,13 @@ This example sets the docker image ENV variable DEBUG to true by default.
|
||||
|
||||
# tar -c . | docker image import -c="ENV DEBUG true" - exampleimagedir
|
||||
|
||||
## When the daemon supports multiple operating systems
|
||||
If the daemon supports multiple operating systems, and the image being imported
|
||||
does not match the default operating system, it may be necessary to add
|
||||
`--platform`. This would be necessary when importing a Linux image into a Windows
|
||||
daemon.
|
||||
|
||||
# docker image import --platform=linux .\linuximage.tar
|
||||
|
||||
# See also
|
||||
**docker-export(1)** to export the contents of a filesystem as a tar archive to STDOUT.
|
||||
|
||||
@ -1,5 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -eu -o pipefail
|
||||
|
||||
# TODO fetch images?
|
||||
./scripts/test/engine/wrapper
|
||||
@ -1,107 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Run engine specific integration tests against the latest containerd-in-docker
|
||||
set -eu -o pipefail
|
||||
|
||||
function container_ip {
|
||||
local cid=$1
|
||||
local network=$2
|
||||
docker inspect \
|
||||
-f "{{.NetworkSettings.Networks.${network}.IPAddress}}" "$cid"
|
||||
}
|
||||
|
||||
function fetch_images {
|
||||
## TODO - not yet implemented
|
||||
./scripts/test/engine/load-image fetch-only
|
||||
}
|
||||
|
||||
function setup {
|
||||
### start containerd and log to a file
|
||||
echo "Starting containerd in the background"
|
||||
containerd 2&> /tmp/containerd.err &
|
||||
echo "Waiting for containerd to be responsive"
|
||||
# shellcheck disable=SC2034
|
||||
for i in $(seq 1 60); do
|
||||
if ctr namespace ls > /dev/null; then
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
ctr namespace ls > /dev/null
|
||||
echo "containerd is ready"
|
||||
|
||||
# TODO Once https://github.com/moby/moby/pull/33355 or equivalent
|
||||
# is merged, then this can be optimized to preload the image
|
||||
# saved during the build phase
|
||||
}
|
||||
|
||||
function cleanup {
|
||||
#### if testexit is non-zero dump the containerd logs with a banner
|
||||
if [ "${testexit}" -ne 0 ] ; then
|
||||
echo "FAIL: dumping containerd logs"
|
||||
echo ""
|
||||
cat /tmp/containerd.err
|
||||
if [ -f /var/log/engine.log ] ; then
|
||||
echo ""
|
||||
echo "FAIL: dumping engine log"
|
||||
echo ""
|
||||
else
|
||||
echo ""
|
||||
echo "FAIL: engine log missing"
|
||||
echo ""
|
||||
fi
|
||||
echo "FAIL: remaining namespaces"
|
||||
ctr namespace ls || /bin/tru
|
||||
echo "FAIL: remaining containers"
|
||||
ctr --namespace docker container ls || /bin/tru
|
||||
echo "FAIL: remaining tasks"
|
||||
ctr --namespace docker task ls || /bin/tru
|
||||
echo "FAIL: remaining snapshots"
|
||||
ctr --namespace docker snapshots ls || /bin/tru
|
||||
echo "FAIL: remaining images"
|
||||
ctr --namespace docker image ls || /bin/tru
|
||||
fi
|
||||
}
|
||||
|
||||
function runtests {
|
||||
# shellcheck disable=SC2086
|
||||
env -i \
|
||||
GOPATH="$GOPATH" \
|
||||
PATH="$PWD/build/:${PATH}" \
|
||||
VERSION=${VERSION} \
|
||||
"$(which go)" test -p 1 -parallel 1 -v ./e2eengine/... ${TESTFLAGS-}
|
||||
}
|
||||
|
||||
cmd=${1-}
|
||||
|
||||
case "$cmd" in
|
||||
setup)
|
||||
setup
|
||||
exit
|
||||
;;
|
||||
cleanup)
|
||||
cleanup
|
||||
exit
|
||||
;;
|
||||
fetch-images)
|
||||
fetch_images
|
||||
exit
|
||||
;;
|
||||
test)
|
||||
runtests
|
||||
;;
|
||||
run|"")
|
||||
testexit=0
|
||||
runtests || testexit=$?
|
||||
cleanup
|
||||
exit $testexit
|
||||
;;
|
||||
shell)
|
||||
$SHELL
|
||||
;;
|
||||
*)
|
||||
echo "Unknown command: $cmd"
|
||||
echo "Usage: "
|
||||
echo " $0 [setup | cleanup | test | run]"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
@ -1,18 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Setup, run and teardown engine test suite in containers.
|
||||
set -eu -o pipefail
|
||||
|
||||
./scripts/test/engine/run setup
|
||||
|
||||
testexit=0
|
||||
|
||||
test_cmd="test"
|
||||
if [[ -n "${TEST_DEBUG-}" ]]; then
|
||||
test_cmd="shell"
|
||||
fi
|
||||
|
||||
./scripts/test/engine/run "$test_cmd" || testexit="$?"
|
||||
|
||||
export testexit
|
||||
./scripts/test/engine/run cleanup
|
||||
exit "$testexit"
|
||||
88
types/types.go
Normal file
88
types/types.go
Normal file
@ -0,0 +1,88 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
ver "github.com/hashicorp/go-version"
|
||||
)
|
||||
|
||||
const (
|
||||
// CommunityEngineImage is the repo name for the community engine
|
||||
CommunityEngineImage = "engine-community"
|
||||
|
||||
// EnterpriseEngineImage is the repo name for the enterprise engine
|
||||
EnterpriseEngineImage = "engine-enterprise"
|
||||
|
||||
// RegistryPrefix is the default prefix used to pull engine images
|
||||
RegistryPrefix = "docker.io/store/docker"
|
||||
|
||||
// ReleaseNotePrefix is where to point users to for release notes
|
||||
ReleaseNotePrefix = "https://docs.docker.com/releasenotes"
|
||||
|
||||
// RuntimeMetadataName is the name of the runtime metadata file
|
||||
// When stored as a label on the container it is prefixed by "com.docker."
|
||||
RuntimeMetadataName = "distribution_based_engine"
|
||||
)
|
||||
|
||||
// ContainerizedClient can be used to manage the lifecycle of
|
||||
// dockerd running as a container on containerd.
|
||||
type ContainerizedClient interface {
|
||||
Close() error
|
||||
ActivateEngine(ctx context.Context,
|
||||
opts EngineInitOptions,
|
||||
out OutStream,
|
||||
authConfig *types.AuthConfig) error
|
||||
DoUpdate(ctx context.Context,
|
||||
opts EngineInitOptions,
|
||||
out OutStream,
|
||||
authConfig *types.AuthConfig) error
|
||||
}
|
||||
|
||||
// EngineInitOptions contains the configuration settings
|
||||
// use during initialization of a containerized docker engine
|
||||
type EngineInitOptions struct {
|
||||
RegistryPrefix string
|
||||
EngineImage string
|
||||
EngineVersion string
|
||||
ConfigFile string
|
||||
RuntimeMetadataDir string
|
||||
}
|
||||
|
||||
// AvailableVersions groups the available versions which were discovered
|
||||
type AvailableVersions struct {
|
||||
Downgrades []DockerVersion
|
||||
Patches []DockerVersion
|
||||
Upgrades []DockerVersion
|
||||
}
|
||||
|
||||
// DockerVersion wraps a semantic version to retain the original tag
|
||||
// since the docker date based versions don't strictly follow semantic
|
||||
// versioning (leading zeros, etc.)
|
||||
type DockerVersion struct {
|
||||
ver.Version
|
||||
Tag string
|
||||
}
|
||||
|
||||
// Update stores available updates for rendering in a table
|
||||
type Update struct {
|
||||
Type string
|
||||
Version string
|
||||
Notes string
|
||||
}
|
||||
|
||||
// OutStream is an output stream used to write normal program output.
|
||||
type OutStream interface {
|
||||
io.Writer
|
||||
FD() uintptr
|
||||
IsTerminal() bool
|
||||
}
|
||||
|
||||
// RuntimeMetadata holds platform information about the daemon
|
||||
type RuntimeMetadata struct {
|
||||
Platform string `json:"platform"`
|
||||
ContainerdMinVersion string `json:"containerd_min_version"`
|
||||
Runtime string `json:"runtime"`
|
||||
EngineImage string `json:"engine_image"`
|
||||
}
|
||||
@ -3,7 +3,7 @@ github.com/asaskevich/govalidator f9ffefc3facfbe0caee3fea233cbb6e8208f4541
|
||||
github.com/Azure/go-ansiterm d6e3b3328b783f23731bc4d058875b0371ff8109
|
||||
github.com/beorn7/perks 3a771d992973f24aa725d07868b467d1ddfceafb
|
||||
github.com/containerd/console c12b1e7919c14469339a5d38f2f8ed9b64a9de23
|
||||
github.com/containerd/containerd v1.2.0-beta.2
|
||||
github.com/containerd/containerd bb0f83ab6eec47c3316bb763d5c20a82c7750c31
|
||||
github.com/containerd/continuity d8fb8589b0e8e85b8c8bbaa8840226d0dfeb7371
|
||||
github.com/containerd/fifo 3d5202a
|
||||
github.com/containerd/typeurl f694355
|
||||
@ -22,7 +22,7 @@ github.com/docker/go-events 9461782956ad83b30282bf90e31fa6a70c255ba9
|
||||
github.com/docker/go-metrics d466d4f6fd960e01820085bd7e1a24426ee7ef18
|
||||
github.com/docker/go-units 47565b4f722fb6ceae66b95f853feed578a4a51c # v0.3.3
|
||||
github.com/docker/libtrust 9cbd2a1374f46905c68a4eb3694a130610adc62a
|
||||
github.com/docker/licensing 5c4c7b4
|
||||
github.com/docker/licensing f2eae57157a06681b024f1690923d03e414179a0
|
||||
github.com/docker/swarmkit cfa742c8abe6f8e922f6e4e920153c408e7d9c3b
|
||||
github.com/flynn-archive/go-shlex 3f9db97f856818214da2e1057f8ad84803971cff
|
||||
github.com/ghodss/yaml 0ca9ea5df5451ffdf184b4428c902747c2c11cd7 # v1.0.0
|
||||
|
||||
1
vendor/github.com/containerd/containerd/README.md
generated
vendored
1
vendor/github.com/containerd/containerd/README.md
generated
vendored
@ -2,6 +2,7 @@
|
||||
|
||||
[](https://godoc.org/github.com/containerd/containerd)
|
||||
[](https://travis-ci.org/containerd/containerd)
|
||||
[](https://ci.appveyor.com/project/mlaventure/containerd-3g73f?branch=master)
|
||||
[](https://app.fossa.io/projects/git%2Bhttps%3A%2F%2Fgithub.com%2Fcontainerd%2Fcontainerd?ref=badge_shield)
|
||||
[](https://goreportcard.com/report/github.com/containerd/containerd)
|
||||
[](https://bestpractices.coreinfrastructure.org/projects/1271)
|
||||
|
||||
4
vendor/github.com/containerd/containerd/api/services/events/v1/events.pb.go
generated
vendored
4
vendor/github.com/containerd/containerd/api/services/events/v1/events.pb.go
generated
vendored
@ -141,7 +141,7 @@ type EventsClient interface {
|
||||
// Forward sends an event that has already been packaged into an envelope
|
||||
// with a timestamp and namespace.
|
||||
//
|
||||
// This is useful if earlier timestamping is required or when fowarding on
|
||||
// This is useful if earlier timestamping is required or when forwarding on
|
||||
// behalf of another component, namespace or publisher.
|
||||
Forward(ctx context.Context, in *ForwardRequest, opts ...grpc.CallOption) (*google_protobuf2.Empty, error)
|
||||
// Subscribe to a stream of events, possibly returning only that match any
|
||||
@ -223,7 +223,7 @@ type EventsServer interface {
|
||||
// Forward sends an event that has already been packaged into an envelope
|
||||
// with a timestamp and namespace.
|
||||
//
|
||||
// This is useful if earlier timestamping is required or when fowarding on
|
||||
// This is useful if earlier timestamping is required or when forwarding on
|
||||
// behalf of another component, namespace or publisher.
|
||||
Forward(context.Context, *ForwardRequest) (*google_protobuf2.Empty, error)
|
||||
// Subscribe to a stream of events, possibly returning only that match any
|
||||
|
||||
2
vendor/github.com/containerd/containerd/api/services/events/v1/events.proto
generated
vendored
2
vendor/github.com/containerd/containerd/api/services/events/v1/events.proto
generated
vendored
@ -20,7 +20,7 @@ service Events {
|
||||
// Forward sends an event that has already been packaged into an envelope
|
||||
// with a timestamp and namespace.
|
||||
//
|
||||
// This is useful if earlier timestamping is required or when fowarding on
|
||||
// This is useful if earlier timestamping is required or when forwarding on
|
||||
// behalf of another component, namespace or publisher.
|
||||
rpc Forward(ForwardRequest) returns (google.protobuf.Empty);
|
||||
|
||||
|
||||
9
vendor/github.com/containerd/containerd/cio/io.go
generated
vendored
9
vendor/github.com/containerd/containerd/cio/io.go
generated
vendored
@ -141,6 +141,15 @@ func NewCreator(opts ...Opt) Creator {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if streams.Stdin == nil {
|
||||
fifos.Stdin = ""
|
||||
}
|
||||
if streams.Stdout == nil {
|
||||
fifos.Stdout = ""
|
||||
}
|
||||
if streams.Stderr == nil {
|
||||
fifos.Stderr = ""
|
||||
}
|
||||
return copyIO(fifos, streams)
|
||||
}
|
||||
}
|
||||
|
||||
58
vendor/github.com/containerd/containerd/container_opts_unix.go
generated
vendored
58
vendor/github.com/containerd/containerd/container_opts_unix.go
generated
vendored
@ -20,25 +20,21 @@ package containerd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
|
||||
"github.com/containerd/containerd/api/types"
|
||||
"github.com/containerd/containerd/containers"
|
||||
"github.com/containerd/containerd/content"
|
||||
"github.com/containerd/containerd/errdefs"
|
||||
"github.com/containerd/containerd/images"
|
||||
"github.com/containerd/containerd/mount"
|
||||
"github.com/containerd/containerd/platforms"
|
||||
"github.com/containerd/containerd/runtime/linux/runctypes"
|
||||
"github.com/gogo/protobuf/proto"
|
||||
protobuf "github.com/gogo/protobuf/types"
|
||||
"github.com/opencontainers/image-spec/identity"
|
||||
"github.com/opencontainers/image-spec/specs-go/v1"
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
@ -105,44 +101,6 @@ func WithCheckpoint(im Image, snapshotKey string) NewContainerOpts {
|
||||
}
|
||||
}
|
||||
|
||||
// WithTaskCheckpoint allows a task to be created with live runtime and memory data from a
|
||||
// previous checkpoint. Additional software such as CRIU may be required to
|
||||
// restore a task from a checkpoint
|
||||
func WithTaskCheckpoint(im Image) NewTaskOpts {
|
||||
return func(ctx context.Context, c *Client, info *TaskInfo) error {
|
||||
desc := im.Target()
|
||||
id := desc.Digest
|
||||
index, err := decodeIndex(ctx, c.ContentStore(), desc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, m := range index.Manifests {
|
||||
if m.MediaType == images.MediaTypeContainerd1Checkpoint {
|
||||
info.Checkpoint = &types.Descriptor{
|
||||
MediaType: m.MediaType,
|
||||
Size_: m.Size,
|
||||
Digest: m.Digest,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("checkpoint not found in index %s", id)
|
||||
}
|
||||
}
|
||||
|
||||
func decodeIndex(ctx context.Context, store content.Provider, desc ocispec.Descriptor) (*v1.Index, error) {
|
||||
var index v1.Index
|
||||
p, err := content.ReadBlob(ctx, store, desc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := json.Unmarshal(p, &index); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &index, nil
|
||||
}
|
||||
|
||||
// WithRemappedSnapshot creates a new snapshot and remaps the uid/gid for the
|
||||
// filesystem to be used by a container with user namespaces
|
||||
func WithRemappedSnapshot(id string, i Image, uid, gid uint32) NewContainerOpts {
|
||||
@ -221,19 +179,3 @@ func incrementFS(root string, uidInc, gidInc uint32) filepath.WalkFunc {
|
||||
return os.Lchown(path, u, g)
|
||||
}
|
||||
}
|
||||
|
||||
// WithNoPivotRoot instructs the runtime not to you pivot_root
|
||||
func WithNoPivotRoot(_ context.Context, _ *Client, info *TaskInfo) error {
|
||||
if info.Options == nil {
|
||||
info.Options = &runctypes.CreateOptions{
|
||||
NoPivotRoot: true,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
copts, ok := info.Options.(*runctypes.CreateOptions)
|
||||
if !ok {
|
||||
return errors.New("invalid options type, expected runctypes.CreateOptions")
|
||||
}
|
||||
copts.NoPivotRoot = true
|
||||
return nil
|
||||
}
|
||||
|
||||
2
vendor/github.com/containerd/containerd/events/exchange/exchange.go
generated
vendored
2
vendor/github.com/containerd/containerd/events/exchange/exchange.go
generated
vendored
@ -52,7 +52,7 @@ var _ events.Subscriber = &Exchange{}
|
||||
|
||||
// Forward accepts an envelope to be direcly distributed on the exchange.
|
||||
//
|
||||
// This is useful when an event is forwaded on behalf of another namespace or
|
||||
// This is useful when an event is forwarded on behalf of another namespace or
|
||||
// when the event is propagated on behalf of another publisher.
|
||||
func (e *Exchange) Forward(ctx context.Context, envelope *events.Envelope) (err error) {
|
||||
if err := validateEnvelope(envelope); err != nil {
|
||||
|
||||
57
vendor/github.com/containerd/containerd/export.go
generated
vendored
Normal file
57
vendor/github.com/containerd/containerd/export.go
generated
vendored
Normal file
@ -0,0 +1,57 @@
|
||||
/*
|
||||
Copyright The containerd Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package containerd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
|
||||
"github.com/containerd/containerd/images"
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
)
|
||||
|
||||
type exportOpts struct {
|
||||
}
|
||||
|
||||
// ExportOpt allows the caller to specify export-specific options
|
||||
type ExportOpt func(c *exportOpts) error
|
||||
|
||||
func resolveExportOpt(opts ...ExportOpt) (exportOpts, error) {
|
||||
var eopts exportOpts
|
||||
for _, o := range opts {
|
||||
if err := o(&eopts); err != nil {
|
||||
return eopts, err
|
||||
}
|
||||
}
|
||||
return eopts, nil
|
||||
}
|
||||
|
||||
// Export exports an image to a Tar stream.
|
||||
// OCI format is used by default.
|
||||
// It is up to caller to put "org.opencontainers.image.ref.name" annotation to desc.
|
||||
// TODO(AkihiroSuda): support exporting multiple descriptors at once to a single archive stream.
|
||||
func (c *Client) Export(ctx context.Context, exporter images.Exporter, desc ocispec.Descriptor, opts ...ExportOpt) (io.ReadCloser, error) {
|
||||
_, err := resolveExportOpt(opts...) // unused now
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pr, pw := io.Pipe()
|
||||
go func() {
|
||||
pw.CloseWithError(exporter.Export(ctx, c.ContentStore(), desc, pw))
|
||||
}()
|
||||
return pr, nil
|
||||
}
|
||||
33
vendor/github.com/containerd/containerd/import.go
generated
vendored
33
vendor/github.com/containerd/containerd/import.go
generated
vendored
@ -22,7 +22,6 @@ import (
|
||||
|
||||
"github.com/containerd/containerd/errdefs"
|
||||
"github.com/containerd/containerd/images"
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
)
|
||||
|
||||
type importOpts struct {
|
||||
@ -84,35 +83,3 @@ func (c *Client) Import(ctx context.Context, importer images.Importer, reader io
|
||||
}
|
||||
return images, nil
|
||||
}
|
||||
|
||||
type exportOpts struct {
|
||||
}
|
||||
|
||||
// ExportOpt allows the caller to specify export-specific options
|
||||
type ExportOpt func(c *exportOpts) error
|
||||
|
||||
func resolveExportOpt(opts ...ExportOpt) (exportOpts, error) {
|
||||
var eopts exportOpts
|
||||
for _, o := range opts {
|
||||
if err := o(&eopts); err != nil {
|
||||
return eopts, err
|
||||
}
|
||||
}
|
||||
return eopts, nil
|
||||
}
|
||||
|
||||
// Export exports an image to a Tar stream.
|
||||
// OCI format is used by default.
|
||||
// It is up to caller to put "org.opencontainers.image.ref.name" annotation to desc.
|
||||
// TODO(AkihiroSuda): support exporting multiple descriptors at once to a single archive stream.
|
||||
func (c *Client) Export(ctx context.Context, exporter images.Exporter, desc ocispec.Descriptor, opts ...ExportOpt) (io.ReadCloser, error) {
|
||||
_, err := resolveExportOpt(opts...) // unused now
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pr, pw := io.Pipe()
|
||||
go func() {
|
||||
pw.CloseWithError(exporter.Export(ctx, c.ContentStore(), desc, pw))
|
||||
}()
|
||||
return pr, nil
|
||||
}
|
||||
|
||||
41
vendor/github.com/containerd/containerd/install.go
generated
vendored
41
vendor/github.com/containerd/containerd/install.go
generated
vendored
@ -33,25 +33,14 @@ import (
|
||||
|
||||
// Install a binary image into the opt service
|
||||
func (c *Client) Install(ctx context.Context, image Image, opts ...InstallOpts) error {
|
||||
resp, err := c.IntrospectionService().Plugins(ctx, &introspectionapi.PluginsRequest{
|
||||
Filters: []string{
|
||||
"id==opt",
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(resp.Plugins) != 1 {
|
||||
return errors.New("opt service not enabled")
|
||||
}
|
||||
path := resp.Plugins[0].Exports["path"]
|
||||
if path == "" {
|
||||
return errors.New("opt path not exported")
|
||||
}
|
||||
var config InstallConfig
|
||||
for _, o := range opts {
|
||||
o(&config)
|
||||
}
|
||||
path, err := c.getInstallPath(ctx, config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var (
|
||||
cs = image.ContentStore()
|
||||
platform = platforms.Default()
|
||||
@ -89,3 +78,25 @@ func (c *Client) Install(ctx context.Context, image Image, opts ...InstallOpts)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) getInstallPath(ctx context.Context, config InstallConfig) (string, error) {
|
||||
if config.Path != "" {
|
||||
return config.Path, nil
|
||||
}
|
||||
resp, err := c.IntrospectionService().Plugins(ctx, &introspectionapi.PluginsRequest{
|
||||
Filters: []string{
|
||||
"id==opt",
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(resp.Plugins) != 1 {
|
||||
return "", errors.New("opt service not enabled")
|
||||
}
|
||||
path := resp.Plugins[0].Exports["path"]
|
||||
if path == "" {
|
||||
return "", errors.New("opt path not exported")
|
||||
}
|
||||
return path, nil
|
||||
}
|
||||
|
||||
9
vendor/github.com/containerd/containerd/install_opts.go
generated
vendored
9
vendor/github.com/containerd/containerd/install_opts.go
generated
vendored
@ -25,6 +25,8 @@ type InstallConfig struct {
|
||||
Libs bool
|
||||
// Replace will overwrite existing binaries or libs in the opt directory
|
||||
Replace bool
|
||||
// Path to install libs and binaries to
|
||||
Path string
|
||||
}
|
||||
|
||||
// WithInstallLibs installs libs from the image
|
||||
@ -36,3 +38,10 @@ func WithInstallLibs(c *InstallConfig) {
|
||||
func WithInstallReplace(c *InstallConfig) {
|
||||
c.Replace = true
|
||||
}
|
||||
|
||||
// WithInstallPath sets the optional install path
|
||||
func WithInstallPath(path string) InstallOpts {
|
||||
return func(c *InstallConfig) {
|
||||
c.Path = path
|
||||
}
|
||||
}
|
||||
|
||||
4
vendor/github.com/containerd/containerd/mount/mount_windows.go
generated
vendored
4
vendor/github.com/containerd/containerd/mount/mount_windows.go
generated
vendored
@ -32,6 +32,10 @@ var (
|
||||
|
||||
// Mount to the provided target
|
||||
func (m *Mount) Mount(target string) error {
|
||||
if m.Type != "windows-layer" {
|
||||
return errors.Errorf("invalid windows mount type: '%s'", m.Type)
|
||||
}
|
||||
|
||||
home, layerID := filepath.Split(m.Source)
|
||||
|
||||
parentLayerPaths, err := m.GetParentPaths()
|
||||
|
||||
218
vendor/github.com/containerd/containerd/oci/spec.go
generated
vendored
218
vendor/github.com/containerd/containerd/oci/spec.go
generated
vendored
@ -18,11 +18,27 @@ package oci
|
||||
|
||||
import (
|
||||
"context"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
|
||||
"github.com/containerd/containerd/namespaces"
|
||||
"github.com/containerd/containerd/platforms"
|
||||
|
||||
"github.com/containerd/containerd/containers"
|
||||
specs "github.com/opencontainers/runtime-spec/specs-go"
|
||||
)
|
||||
|
||||
const (
|
||||
rwm = "rwm"
|
||||
defaultRootfsPath = "rootfs"
|
||||
)
|
||||
|
||||
var (
|
||||
defaultUnixEnv = []string{
|
||||
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
|
||||
}
|
||||
)
|
||||
|
||||
// Spec is a type alias to the OCI runtime spec to allow third part SpecOpts
|
||||
// to be created without the "issues" with go vendoring and package imports
|
||||
type Spec = specs.Spec
|
||||
@ -30,12 +46,36 @@ type Spec = specs.Spec
|
||||
// GenerateSpec will generate a default spec from the provided image
|
||||
// for use as a containerd container
|
||||
func GenerateSpec(ctx context.Context, client Client, c *containers.Container, opts ...SpecOpts) (*Spec, error) {
|
||||
s, err := createDefaultSpec(ctx, c.ID)
|
||||
if err != nil {
|
||||
return GenerateSpecWithPlatform(ctx, client, platforms.DefaultString(), c, opts...)
|
||||
}
|
||||
|
||||
// GenerateSpecWithPlatform will generate a default spec from the provided image
|
||||
// for use as a containerd container in the platform requested.
|
||||
func GenerateSpecWithPlatform(ctx context.Context, client Client, platform string, c *containers.Container, opts ...SpecOpts) (*Spec, error) {
|
||||
var s Spec
|
||||
if err := generateDefaultSpecWithPlatform(ctx, platform, c.ID, &s); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s, ApplyOpts(ctx, client, c, s, opts...)
|
||||
return &s, ApplyOpts(ctx, client, c, &s, opts...)
|
||||
}
|
||||
|
||||
func generateDefaultSpecWithPlatform(ctx context.Context, platform, id string, s *Spec) error {
|
||||
plat, err := platforms.Parse(platform)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if plat.OS == "windows" {
|
||||
err = populateDefaultWindowsSpec(ctx, s, id)
|
||||
} else {
|
||||
err = populateDefaultUnixSpec(ctx, s, id)
|
||||
if err == nil && runtime.GOOS == "windows" {
|
||||
// To run LCOW we have a Linux and Windows section. Add an empty one now.
|
||||
s.Windows = &specs.Windows{}
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// ApplyOpts applys the options to the given spec, injecting data from the
|
||||
@ -50,7 +90,173 @@ func ApplyOpts(ctx context.Context, client Client, c *containers.Container, s *S
|
||||
return nil
|
||||
}
|
||||
|
||||
func createDefaultSpec(ctx context.Context, id string) (*Spec, error) {
|
||||
var s Spec
|
||||
return &s, populateDefaultSpec(ctx, &s, id)
|
||||
func defaultUnixCaps() []string {
|
||||
return []string{
|
||||
"CAP_CHOWN",
|
||||
"CAP_DAC_OVERRIDE",
|
||||
"CAP_FSETID",
|
||||
"CAP_FOWNER",
|
||||
"CAP_MKNOD",
|
||||
"CAP_NET_RAW",
|
||||
"CAP_SETGID",
|
||||
"CAP_SETUID",
|
||||
"CAP_SETFCAP",
|
||||
"CAP_SETPCAP",
|
||||
"CAP_NET_BIND_SERVICE",
|
||||
"CAP_SYS_CHROOT",
|
||||
"CAP_KILL",
|
||||
"CAP_AUDIT_WRITE",
|
||||
}
|
||||
}
|
||||
|
||||
func defaultUnixNamespaces() []specs.LinuxNamespace {
|
||||
return []specs.LinuxNamespace{
|
||||
{
|
||||
Type: specs.PIDNamespace,
|
||||
},
|
||||
{
|
||||
Type: specs.IPCNamespace,
|
||||
},
|
||||
{
|
||||
Type: specs.UTSNamespace,
|
||||
},
|
||||
{
|
||||
Type: specs.MountNamespace,
|
||||
},
|
||||
{
|
||||
Type: specs.NetworkNamespace,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func populateDefaultUnixSpec(ctx context.Context, s *Spec, id string) error {
|
||||
ns, err := namespaces.NamespaceRequired(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*s = Spec{
|
||||
Version: specs.Version,
|
||||
Root: &specs.Root{
|
||||
Path: defaultRootfsPath,
|
||||
},
|
||||
Process: &specs.Process{
|
||||
Env: defaultUnixEnv,
|
||||
Cwd: "/",
|
||||
NoNewPrivileges: true,
|
||||
User: specs.User{
|
||||
UID: 0,
|
||||
GID: 0,
|
||||
},
|
||||
Capabilities: &specs.LinuxCapabilities{
|
||||
Bounding: defaultUnixCaps(),
|
||||
Permitted: defaultUnixCaps(),
|
||||
Inheritable: defaultUnixCaps(),
|
||||
Effective: defaultUnixCaps(),
|
||||
},
|
||||
Rlimits: []specs.POSIXRlimit{
|
||||
{
|
||||
Type: "RLIMIT_NOFILE",
|
||||
Hard: uint64(1024),
|
||||
Soft: uint64(1024),
|
||||
},
|
||||
},
|
||||
},
|
||||
Mounts: []specs.Mount{
|
||||
{
|
||||
Destination: "/proc",
|
||||
Type: "proc",
|
||||
Source: "proc",
|
||||
},
|
||||
{
|
||||
Destination: "/dev",
|
||||
Type: "tmpfs",
|
||||
Source: "tmpfs",
|
||||
Options: []string{"nosuid", "strictatime", "mode=755", "size=65536k"},
|
||||
},
|
||||
{
|
||||
Destination: "/dev/pts",
|
||||
Type: "devpts",
|
||||
Source: "devpts",
|
||||
Options: []string{"nosuid", "noexec", "newinstance", "ptmxmode=0666", "mode=0620", "gid=5"},
|
||||
},
|
||||
{
|
||||
Destination: "/dev/shm",
|
||||
Type: "tmpfs",
|
||||
Source: "shm",
|
||||
Options: []string{"nosuid", "noexec", "nodev", "mode=1777", "size=65536k"},
|
||||
},
|
||||
{
|
||||
Destination: "/dev/mqueue",
|
||||
Type: "mqueue",
|
||||
Source: "mqueue",
|
||||
Options: []string{"nosuid", "noexec", "nodev"},
|
||||
},
|
||||
{
|
||||
Destination: "/sys",
|
||||
Type: "sysfs",
|
||||
Source: "sysfs",
|
||||
Options: []string{"nosuid", "noexec", "nodev", "ro"},
|
||||
},
|
||||
{
|
||||
Destination: "/run",
|
||||
Type: "tmpfs",
|
||||
Source: "tmpfs",
|
||||
Options: []string{"nosuid", "strictatime", "mode=755", "size=65536k"},
|
||||
},
|
||||
},
|
||||
Linux: &specs.Linux{
|
||||
MaskedPaths: []string{
|
||||
"/proc/acpi",
|
||||
"/proc/kcore",
|
||||
"/proc/keys",
|
||||
"/proc/latency_stats",
|
||||
"/proc/timer_list",
|
||||
"/proc/timer_stats",
|
||||
"/proc/sched_debug",
|
||||
"/sys/firmware",
|
||||
"/proc/scsi",
|
||||
},
|
||||
ReadonlyPaths: []string{
|
||||
"/proc/asound",
|
||||
"/proc/bus",
|
||||
"/proc/fs",
|
||||
"/proc/irq",
|
||||
"/proc/sys",
|
||||
"/proc/sysrq-trigger",
|
||||
},
|
||||
CgroupsPath: filepath.Join("/", ns, id),
|
||||
Resources: &specs.LinuxResources{
|
||||
Devices: []specs.LinuxDeviceCgroup{
|
||||
{
|
||||
Allow: false,
|
||||
Access: rwm,
|
||||
},
|
||||
},
|
||||
},
|
||||
Namespaces: defaultUnixNamespaces(),
|
||||
},
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func populateDefaultWindowsSpec(ctx context.Context, s *Spec, id string) error {
|
||||
*s = Spec{
|
||||
Version: specs.Version,
|
||||
Root: &specs.Root{},
|
||||
Process: &specs.Process{
|
||||
Cwd: `C:\`,
|
||||
ConsoleSize: &specs.Box{
|
||||
Width: 80,
|
||||
Height: 20,
|
||||
},
|
||||
},
|
||||
Windows: &specs.Windows{
|
||||
IgnoreFlushesDuringBoot: true,
|
||||
Network: &specs.WindowsNetwork{
|
||||
AllowUnqualifiedDNSQuery: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
899
vendor/github.com/containerd/containerd/oci/spec_opts.go
generated
vendored
899
vendor/github.com/containerd/containerd/oci/spec_opts.go
generated
vendored
@ -19,12 +19,25 @@ package oci
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/containerd/containerd/containers"
|
||||
"github.com/containerd/containerd/content"
|
||||
"github.com/containerd/containerd/images"
|
||||
"github.com/containerd/containerd/mount"
|
||||
"github.com/containerd/containerd/namespaces"
|
||||
"github.com/containerd/containerd/platforms"
|
||||
"github.com/containerd/continuity/fs"
|
||||
"github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"github.com/opencontainers/runc/libcontainer/user"
|
||||
specs "github.com/opencontainers/runtime-spec/specs-go"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/syndtr/gocapability/capability"
|
||||
)
|
||||
|
||||
// SpecOpts sets spec specific information to a newly generated OCI spec
|
||||
@ -49,13 +62,45 @@ func setProcess(s *Spec) {
|
||||
}
|
||||
}
|
||||
|
||||
// setRoot sets Root to empty if unset
|
||||
func setRoot(s *Spec) {
|
||||
if s.Root == nil {
|
||||
s.Root = &specs.Root{}
|
||||
}
|
||||
}
|
||||
|
||||
// setLinux sets Linux to empty if unset
|
||||
func setLinux(s *Spec) {
|
||||
if s.Linux == nil {
|
||||
s.Linux = &specs.Linux{}
|
||||
}
|
||||
}
|
||||
|
||||
// setCapabilities sets Linux Capabilities to empty if unset
|
||||
func setCapabilities(s *Spec) {
|
||||
setProcess(s)
|
||||
if s.Process.Capabilities == nil {
|
||||
s.Process.Capabilities = &specs.LinuxCapabilities{}
|
||||
}
|
||||
}
|
||||
|
||||
// WithDefaultSpec returns a SpecOpts that will populate the spec with default
|
||||
// values.
|
||||
//
|
||||
// Use as the first option to clear the spec, then apply options afterwards.
|
||||
func WithDefaultSpec() SpecOpts {
|
||||
return func(ctx context.Context, _ Client, c *containers.Container, s *Spec) error {
|
||||
return populateDefaultSpec(ctx, s, c.ID)
|
||||
return generateDefaultSpecWithPlatform(ctx, platforms.DefaultString(), c.ID, s)
|
||||
}
|
||||
}
|
||||
|
||||
// WithDefaultSpecForPlatform returns a SpecOpts that will populate the spec
|
||||
// with default values for a given platform.
|
||||
//
|
||||
// Use as the first option to clear the spec, then apply options afterwards.
|
||||
func WithDefaultSpecForPlatform(platform string) SpecOpts {
|
||||
return func(ctx context.Context, _ Client, c *containers.Container, s *Spec) error {
|
||||
return generateDefaultSpecWithPlatform(ctx, platform, c.ID, s)
|
||||
}
|
||||
}
|
||||
|
||||
@ -81,32 +126,6 @@ func WithSpecFromFile(filename string) SpecOpts {
|
||||
}
|
||||
}
|
||||
|
||||
// WithProcessArgs replaces the args on the generated spec
|
||||
func WithProcessArgs(args ...string) SpecOpts {
|
||||
return func(_ context.Context, _ Client, _ *containers.Container, s *Spec) error {
|
||||
setProcess(s)
|
||||
s.Process.Args = args
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithProcessCwd replaces the current working directory on the generated spec
|
||||
func WithProcessCwd(cwd string) SpecOpts {
|
||||
return func(_ context.Context, _ Client, _ *containers.Container, s *Spec) error {
|
||||
setProcess(s)
|
||||
s.Process.Cwd = cwd
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithHostname sets the container's hostname
|
||||
func WithHostname(name string) SpecOpts {
|
||||
return func(_ context.Context, _ Client, _ *containers.Container, s *Spec) error {
|
||||
s.Hostname = name
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithEnv appends environment variables
|
||||
func WithEnv(environmentVariables []string) SpecOpts {
|
||||
return func(_ context.Context, _ Client, _ *containers.Container, s *Spec) error {
|
||||
@ -118,14 +137,6 @@ func WithEnv(environmentVariables []string) SpecOpts {
|
||||
}
|
||||
}
|
||||
|
||||
// WithMounts appends mounts
|
||||
func WithMounts(mounts []specs.Mount) SpecOpts {
|
||||
return func(_ context.Context, _ Client, _ *containers.Container, s *Spec) error {
|
||||
s.Mounts = append(s.Mounts, mounts...)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// replaceOrAppendEnvValues returns the defaults with the overrides either
|
||||
// replaced by env key or appended to the list
|
||||
func replaceOrAppendEnvValues(defaults, overrides []string) []string {
|
||||
@ -163,3 +174,821 @@ func replaceOrAppendEnvValues(defaults, overrides []string) []string {
|
||||
|
||||
return defaults
|
||||
}
|
||||
|
||||
// WithProcessArgs replaces the args on the generated spec
|
||||
func WithProcessArgs(args ...string) SpecOpts {
|
||||
return func(_ context.Context, _ Client, _ *containers.Container, s *Spec) error {
|
||||
setProcess(s)
|
||||
s.Process.Args = args
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithProcessCwd replaces the current working directory on the generated spec
|
||||
func WithProcessCwd(cwd string) SpecOpts {
|
||||
return func(_ context.Context, _ Client, _ *containers.Container, s *Spec) error {
|
||||
setProcess(s)
|
||||
s.Process.Cwd = cwd
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithTTY sets the information on the spec as well as the environment variables for
|
||||
// using a TTY
|
||||
func WithTTY(_ context.Context, _ Client, _ *containers.Container, s *Spec) error {
|
||||
setProcess(s)
|
||||
s.Process.Terminal = true
|
||||
if s.Linux != nil {
|
||||
s.Process.Env = append(s.Process.Env, "TERM=xterm")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// WithTTYSize sets the information on the spec as well as the environment variables for
|
||||
// using a TTY
|
||||
func WithTTYSize(width, height int) SpecOpts {
|
||||
return func(_ context.Context, _ Client, _ *containers.Container, s *Spec) error {
|
||||
setProcess(s)
|
||||
if s.Process.ConsoleSize == nil {
|
||||
s.Process.ConsoleSize = &specs.Box{}
|
||||
}
|
||||
s.Process.ConsoleSize.Width = uint(width)
|
||||
s.Process.ConsoleSize.Height = uint(height)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithHostname sets the container's hostname
|
||||
func WithHostname(name string) SpecOpts {
|
||||
return func(_ context.Context, _ Client, _ *containers.Container, s *Spec) error {
|
||||
s.Hostname = name
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithMounts appends mounts
|
||||
func WithMounts(mounts []specs.Mount) SpecOpts {
|
||||
return func(_ context.Context, _ Client, _ *containers.Container, s *Spec) error {
|
||||
s.Mounts = append(s.Mounts, mounts...)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithHostNamespace allows a task to run inside the host's linux namespace
|
||||
func WithHostNamespace(ns specs.LinuxNamespaceType) SpecOpts {
|
||||
return func(_ context.Context, _ Client, _ *containers.Container, s *Spec) error {
|
||||
setLinux(s)
|
||||
for i, n := range s.Linux.Namespaces {
|
||||
if n.Type == ns {
|
||||
s.Linux.Namespaces = append(s.Linux.Namespaces[:i], s.Linux.Namespaces[i+1:]...)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithLinuxNamespace uses the passed in namespace for the spec. If a namespace of the same type already exists in the
|
||||
// spec, the existing namespace is replaced by the one provided.
|
||||
func WithLinuxNamespace(ns specs.LinuxNamespace) SpecOpts {
|
||||
return func(_ context.Context, _ Client, _ *containers.Container, s *Spec) error {
|
||||
setLinux(s)
|
||||
for i, n := range s.Linux.Namespaces {
|
||||
if n.Type == ns.Type {
|
||||
before := s.Linux.Namespaces[:i]
|
||||
after := s.Linux.Namespaces[i+1:]
|
||||
s.Linux.Namespaces = append(before, ns)
|
||||
s.Linux.Namespaces = append(s.Linux.Namespaces, after...)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
s.Linux.Namespaces = append(s.Linux.Namespaces, ns)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithImageConfig configures the spec to from the configuration of an Image
|
||||
func WithImageConfig(image Image) SpecOpts {
|
||||
return WithImageConfigArgs(image, nil)
|
||||
}
|
||||
|
||||
// WithImageConfigArgs configures the spec to from the configuration of an Image with additional args that
|
||||
// replaces the CMD of the image
|
||||
func WithImageConfigArgs(image Image, args []string) SpecOpts {
|
||||
return func(ctx context.Context, client Client, c *containers.Container, s *Spec) error {
|
||||
ic, err := image.Config(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var (
|
||||
ociimage v1.Image
|
||||
config v1.ImageConfig
|
||||
)
|
||||
switch ic.MediaType {
|
||||
case v1.MediaTypeImageConfig, images.MediaTypeDockerSchema2Config:
|
||||
p, err := content.ReadBlob(ctx, image.ContentStore(), ic)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(p, &ociimage); err != nil {
|
||||
return err
|
||||
}
|
||||
config = ociimage.Config
|
||||
default:
|
||||
return fmt.Errorf("unknown image config media type %s", ic.MediaType)
|
||||
}
|
||||
|
||||
setProcess(s)
|
||||
if s.Linux != nil {
|
||||
s.Process.Env = append(s.Process.Env, config.Env...)
|
||||
cmd := config.Cmd
|
||||
if len(args) > 0 {
|
||||
cmd = args
|
||||
}
|
||||
s.Process.Args = append(config.Entrypoint, cmd...)
|
||||
|
||||
cwd := config.WorkingDir
|
||||
if cwd == "" {
|
||||
cwd = "/"
|
||||
}
|
||||
s.Process.Cwd = cwd
|
||||
if config.User != "" {
|
||||
return WithUser(config.User)(ctx, client, c, s)
|
||||
}
|
||||
} else if s.Windows != nil {
|
||||
s.Process.Env = config.Env
|
||||
s.Process.Args = append(config.Entrypoint, config.Cmd...)
|
||||
s.Process.User = specs.User{
|
||||
Username: config.User,
|
||||
}
|
||||
} else {
|
||||
return errors.New("spec does not contain Linux or Windows section")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithRootFSPath specifies unmanaged rootfs path.
|
||||
func WithRootFSPath(path string) SpecOpts {
|
||||
return func(_ context.Context, _ Client, _ *containers.Container, s *Spec) error {
|
||||
setRoot(s)
|
||||
s.Root.Path = path
|
||||
// Entrypoint is not set here (it's up to caller)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithRootFSReadonly sets specs.Root.Readonly to true
|
||||
func WithRootFSReadonly() SpecOpts {
|
||||
return func(_ context.Context, _ Client, _ *containers.Container, s *Spec) error {
|
||||
setRoot(s)
|
||||
s.Root.Readonly = true
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithNoNewPrivileges sets no_new_privileges on the process for the container
|
||||
func WithNoNewPrivileges(_ context.Context, _ Client, _ *containers.Container, s *Spec) error {
|
||||
setProcess(s)
|
||||
s.Process.NoNewPrivileges = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// WithHostHostsFile bind-mounts the host's /etc/hosts into the container as readonly
|
||||
func WithHostHostsFile(_ context.Context, _ Client, _ *containers.Container, s *Spec) error {
|
||||
s.Mounts = append(s.Mounts, specs.Mount{
|
||||
Destination: "/etc/hosts",
|
||||
Type: "bind",
|
||||
Source: "/etc/hosts",
|
||||
Options: []string{"rbind", "ro"},
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
// WithHostResolvconf bind-mounts the host's /etc/resolv.conf into the container as readonly
|
||||
func WithHostResolvconf(_ context.Context, _ Client, _ *containers.Container, s *Spec) error {
|
||||
s.Mounts = append(s.Mounts, specs.Mount{
|
||||
Destination: "/etc/resolv.conf",
|
||||
Type: "bind",
|
||||
Source: "/etc/resolv.conf",
|
||||
Options: []string{"rbind", "ro"},
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
// WithHostLocaltime bind-mounts the host's /etc/localtime into the container as readonly
|
||||
func WithHostLocaltime(_ context.Context, _ Client, _ *containers.Container, s *Spec) error {
|
||||
s.Mounts = append(s.Mounts, specs.Mount{
|
||||
Destination: "/etc/localtime",
|
||||
Type: "bind",
|
||||
Source: "/etc/localtime",
|
||||
Options: []string{"rbind", "ro"},
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
// WithUserNamespace sets the uid and gid mappings for the task
|
||||
// this can be called multiple times to add more mappings to the generated spec
|
||||
func WithUserNamespace(container, host, size uint32) SpecOpts {
|
||||
return func(_ context.Context, _ Client, _ *containers.Container, s *Spec) error {
|
||||
var hasUserns bool
|
||||
setLinux(s)
|
||||
for _, ns := range s.Linux.Namespaces {
|
||||
if ns.Type == specs.UserNamespace {
|
||||
hasUserns = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasUserns {
|
||||
s.Linux.Namespaces = append(s.Linux.Namespaces, specs.LinuxNamespace{
|
||||
Type: specs.UserNamespace,
|
||||
})
|
||||
}
|
||||
mapping := specs.LinuxIDMapping{
|
||||
ContainerID: container,
|
||||
HostID: host,
|
||||
Size: size,
|
||||
}
|
||||
s.Linux.UIDMappings = append(s.Linux.UIDMappings, mapping)
|
||||
s.Linux.GIDMappings = append(s.Linux.GIDMappings, mapping)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithCgroup sets the container's cgroup path
|
||||
func WithCgroup(path string) SpecOpts {
|
||||
return func(_ context.Context, _ Client, _ *containers.Container, s *Spec) error {
|
||||
setLinux(s)
|
||||
s.Linux.CgroupsPath = path
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithNamespacedCgroup uses the namespace set on the context to create a
|
||||
// root directory for containers in the cgroup with the id as the subcgroup
|
||||
func WithNamespacedCgroup() SpecOpts {
|
||||
return func(ctx context.Context, _ Client, c *containers.Container, s *Spec) error {
|
||||
namespace, err := namespaces.NamespaceRequired(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
setLinux(s)
|
||||
s.Linux.CgroupsPath = filepath.Join("/", namespace, c.ID)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithUser sets the user to be used within the container.
|
||||
// It accepts a valid user string in OCI Image Spec v1.0.0:
|
||||
// user, uid, user:group, uid:gid, uid:group, user:gid
|
||||
func WithUser(userstr string) SpecOpts {
|
||||
return func(ctx context.Context, client Client, c *containers.Container, s *Spec) error {
|
||||
setProcess(s)
|
||||
parts := strings.Split(userstr, ":")
|
||||
switch len(parts) {
|
||||
case 1:
|
||||
v, err := strconv.Atoi(parts[0])
|
||||
if err != nil {
|
||||
// if we cannot parse as a uint they try to see if it is a username
|
||||
return WithUsername(userstr)(ctx, client, c, s)
|
||||
}
|
||||
return WithUserID(uint32(v))(ctx, client, c, s)
|
||||
case 2:
|
||||
var (
|
||||
username string
|
||||
groupname string
|
||||
)
|
||||
var uid, gid uint32
|
||||
v, err := strconv.Atoi(parts[0])
|
||||
if err != nil {
|
||||
username = parts[0]
|
||||
} else {
|
||||
uid = uint32(v)
|
||||
}
|
||||
if v, err = strconv.Atoi(parts[1]); err != nil {
|
||||
groupname = parts[1]
|
||||
} else {
|
||||
gid = uint32(v)
|
||||
}
|
||||
if username == "" && groupname == "" {
|
||||
s.Process.User.UID, s.Process.User.GID = uid, gid
|
||||
return nil
|
||||
}
|
||||
f := func(root string) error {
|
||||
if username != "" {
|
||||
uid, _, err = getUIDGIDFromPath(root, func(u user.User) bool {
|
||||
return u.Name == username
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if groupname != "" {
|
||||
gid, err = getGIDFromPath(root, func(g user.Group) bool {
|
||||
return g.Name == groupname
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
s.Process.User.UID, s.Process.User.GID = uid, gid
|
||||
return nil
|
||||
}
|
||||
if c.Snapshotter == "" && c.SnapshotKey == "" {
|
||||
if !isRootfsAbs(s.Root.Path) {
|
||||
return errors.New("rootfs absolute path is required")
|
||||
}
|
||||
return f(s.Root.Path)
|
||||
}
|
||||
if c.Snapshotter == "" {
|
||||
return errors.New("no snapshotter set for container")
|
||||
}
|
||||
if c.SnapshotKey == "" {
|
||||
return errors.New("rootfs snapshot not created for container")
|
||||
}
|
||||
snapshotter := client.SnapshotService(c.Snapshotter)
|
||||
mounts, err := snapshotter.Mounts(ctx, c.SnapshotKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return mount.WithTempMount(ctx, mounts, f)
|
||||
default:
|
||||
return fmt.Errorf("invalid USER value %s", userstr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WithUIDGID allows the UID and GID for the Process to be set
|
||||
func WithUIDGID(uid, gid uint32) SpecOpts {
|
||||
return func(_ context.Context, _ Client, _ *containers.Container, s *Spec) error {
|
||||
setProcess(s)
|
||||
s.Process.User.UID = uid
|
||||
s.Process.User.GID = gid
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithUserID sets the correct UID and GID for the container based
|
||||
// on the image's /etc/passwd contents. If /etc/passwd does not exist,
|
||||
// or uid is not found in /etc/passwd, it sets the requested uid,
|
||||
// additionally sets the gid to 0, and does not return an error.
|
||||
func WithUserID(uid uint32) SpecOpts {
|
||||
return func(ctx context.Context, client Client, c *containers.Container, s *Spec) (err error) {
|
||||
setProcess(s)
|
||||
if c.Snapshotter == "" && c.SnapshotKey == "" {
|
||||
if !isRootfsAbs(s.Root.Path) {
|
||||
return errors.Errorf("rootfs absolute path is required")
|
||||
}
|
||||
uuid, ugid, err := getUIDGIDFromPath(s.Root.Path, func(u user.User) bool {
|
||||
return u.Uid == int(uid)
|
||||
})
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) || err == errNoUsersFound {
|
||||
s.Process.User.UID, s.Process.User.GID = uid, 0
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
s.Process.User.UID, s.Process.User.GID = uuid, ugid
|
||||
return nil
|
||||
|
||||
}
|
||||
if c.Snapshotter == "" {
|
||||
return errors.Errorf("no snapshotter set for container")
|
||||
}
|
||||
if c.SnapshotKey == "" {
|
||||
return errors.Errorf("rootfs snapshot not created for container")
|
||||
}
|
||||
snapshotter := client.SnapshotService(c.Snapshotter)
|
||||
mounts, err := snapshotter.Mounts(ctx, c.SnapshotKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return mount.WithTempMount(ctx, mounts, func(root string) error {
|
||||
uuid, ugid, err := getUIDGIDFromPath(root, func(u user.User) bool {
|
||||
return u.Uid == int(uid)
|
||||
})
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) || err == errNoUsersFound {
|
||||
s.Process.User.UID, s.Process.User.GID = uid, 0
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
s.Process.User.UID, s.Process.User.GID = uuid, ugid
|
||||
return nil
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// WithUsername sets the correct UID and GID for the container
|
||||
// based on the the image's /etc/passwd contents. If /etc/passwd
|
||||
// does not exist, or the username is not found in /etc/passwd,
|
||||
// it returns error.
|
||||
func WithUsername(username string) SpecOpts {
|
||||
return func(ctx context.Context, client Client, c *containers.Container, s *Spec) (err error) {
|
||||
setProcess(s)
|
||||
if s.Linux != nil {
|
||||
if c.Snapshotter == "" && c.SnapshotKey == "" {
|
||||
if !isRootfsAbs(s.Root.Path) {
|
||||
return errors.Errorf("rootfs absolute path is required")
|
||||
}
|
||||
uid, gid, err := getUIDGIDFromPath(s.Root.Path, func(u user.User) bool {
|
||||
return u.Name == username
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.Process.User.UID, s.Process.User.GID = uid, gid
|
||||
return nil
|
||||
}
|
||||
if c.Snapshotter == "" {
|
||||
return errors.Errorf("no snapshotter set for container")
|
||||
}
|
||||
if c.SnapshotKey == "" {
|
||||
return errors.Errorf("rootfs snapshot not created for container")
|
||||
}
|
||||
snapshotter := client.SnapshotService(c.Snapshotter)
|
||||
mounts, err := snapshotter.Mounts(ctx, c.SnapshotKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return mount.WithTempMount(ctx, mounts, func(root string) error {
|
||||
uid, gid, err := getUIDGIDFromPath(root, func(u user.User) bool {
|
||||
return u.Name == username
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.Process.User.UID, s.Process.User.GID = uid, gid
|
||||
return nil
|
||||
})
|
||||
} else if s.Windows != nil {
|
||||
s.Process.User.Username = username
|
||||
} else {
|
||||
return errors.New("spec does not contain Linux or Windows section")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithAdditionalGIDs sets the OCI spec's additionalGids array to any additional groups listed
|
||||
// for a particular user in the /etc/groups file of the image's root filesystem
|
||||
func WithAdditionalGIDs(username string) SpecOpts {
|
||||
return func(ctx context.Context, client Client, c *containers.Container, s *Spec) (err error) {
|
||||
setProcess(s)
|
||||
if c.Snapshotter == "" && c.SnapshotKey == "" {
|
||||
if !isRootfsAbs(s.Root.Path) {
|
||||
return errors.Errorf("rootfs absolute path is required")
|
||||
}
|
||||
gids, err := getSupplementalGroupsFromPath(s.Root.Path, func(g user.Group) bool {
|
||||
// we only want supplemental groups
|
||||
if g.Name == username {
|
||||
return false
|
||||
}
|
||||
for _, entry := range g.List {
|
||||
if entry == username {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.Process.User.AdditionalGids = gids
|
||||
return nil
|
||||
}
|
||||
if c.Snapshotter == "" {
|
||||
return errors.Errorf("no snapshotter set for container")
|
||||
}
|
||||
if c.SnapshotKey == "" {
|
||||
return errors.Errorf("rootfs snapshot not created for container")
|
||||
}
|
||||
snapshotter := client.SnapshotService(c.Snapshotter)
|
||||
mounts, err := snapshotter.Mounts(ctx, c.SnapshotKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return mount.WithTempMount(ctx, mounts, func(root string) error {
|
||||
gids, err := getSupplementalGroupsFromPath(root, func(g user.Group) bool {
|
||||
// we only want supplemental groups
|
||||
if g.Name == username {
|
||||
return false
|
||||
}
|
||||
for _, entry := range g.List {
|
||||
if entry == username {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.Process.User.AdditionalGids = gids
|
||||
return nil
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// WithCapabilities sets Linux capabilities on the process
|
||||
func WithCapabilities(caps []string) SpecOpts {
|
||||
return func(_ context.Context, _ Client, _ *containers.Container, s *Spec) error {
|
||||
setCapabilities(s)
|
||||
|
||||
s.Process.Capabilities.Bounding = caps
|
||||
s.Process.Capabilities.Effective = caps
|
||||
s.Process.Capabilities.Permitted = caps
|
||||
s.Process.Capabilities.Inheritable = caps
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithAllCapabilities sets all linux capabilities for the process
|
||||
var WithAllCapabilities = WithCapabilities(getAllCapabilities())
|
||||
|
||||
func getAllCapabilities() []string {
|
||||
last := capability.CAP_LAST_CAP
|
||||
// hack for RHEL6 which has no /proc/sys/kernel/cap_last_cap
|
||||
if last == capability.Cap(63) {
|
||||
last = capability.CAP_BLOCK_SUSPEND
|
||||
}
|
||||
var caps []string
|
||||
for _, cap := range capability.List() {
|
||||
if cap > last {
|
||||
continue
|
||||
}
|
||||
caps = append(caps, "CAP_"+strings.ToUpper(cap.String()))
|
||||
}
|
||||
return caps
|
||||
}
|
||||
|
||||
// WithAmbientCapabilities set the Linux ambient capabilities for the process
|
||||
// Ambient capabilities should only be set for non-root users or the caller should
|
||||
// understand how these capabilities are used and set
|
||||
func WithAmbientCapabilities(caps []string) SpecOpts {
|
||||
return func(_ context.Context, _ Client, _ *containers.Container, s *Spec) error {
|
||||
setCapabilities(s)
|
||||
|
||||
s.Process.Capabilities.Ambient = caps
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
var errNoUsersFound = errors.New("no users found")
|
||||
|
||||
func getUIDGIDFromPath(root string, filter func(user.User) bool) (uid, gid uint32, err error) {
|
||||
ppath, err := fs.RootPath(root, "/etc/passwd")
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
users, err := user.ParsePasswdFileFilter(ppath, filter)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
if len(users) == 0 {
|
||||
return 0, 0, errNoUsersFound
|
||||
}
|
||||
u := users[0]
|
||||
return uint32(u.Uid), uint32(u.Gid), nil
|
||||
}
|
||||
|
||||
var errNoGroupsFound = errors.New("no groups found")
|
||||
|
||||
func getGIDFromPath(root string, filter func(user.Group) bool) (gid uint32, err error) {
|
||||
gpath, err := fs.RootPath(root, "/etc/group")
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
groups, err := user.ParseGroupFileFilter(gpath, filter)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if len(groups) == 0 {
|
||||
return 0, errNoGroupsFound
|
||||
}
|
||||
g := groups[0]
|
||||
return uint32(g.Gid), nil
|
||||
}
|
||||
|
||||
func getSupplementalGroupsFromPath(root string, filter func(user.Group) bool) ([]uint32, error) {
|
||||
gpath, err := fs.RootPath(root, "/etc/group")
|
||||
if err != nil {
|
||||
return []uint32{}, err
|
||||
}
|
||||
groups, err := user.ParseGroupFileFilter(gpath, filter)
|
||||
if err != nil {
|
||||
return []uint32{}, err
|
||||
}
|
||||
if len(groups) == 0 {
|
||||
// if there are no additional groups; just return an empty set
|
||||
return []uint32{}, nil
|
||||
}
|
||||
addlGids := []uint32{}
|
||||
for _, grp := range groups {
|
||||
addlGids = append(addlGids, uint32(grp.Gid))
|
||||
}
|
||||
return addlGids, nil
|
||||
}
|
||||
|
||||
func isRootfsAbs(root string) bool {
|
||||
return filepath.IsAbs(root)
|
||||
}
|
||||
|
||||
// WithMaskedPaths sets the masked paths option
|
||||
func WithMaskedPaths(paths []string) SpecOpts {
|
||||
return func(_ context.Context, _ Client, _ *containers.Container, s *Spec) error {
|
||||
setLinux(s)
|
||||
s.Linux.MaskedPaths = paths
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithReadonlyPaths sets the read only paths option
|
||||
func WithReadonlyPaths(paths []string) SpecOpts {
|
||||
return func(_ context.Context, _ Client, _ *containers.Container, s *Spec) error {
|
||||
setLinux(s)
|
||||
s.Linux.ReadonlyPaths = paths
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithWriteableSysfs makes any sysfs mounts writeable
|
||||
func WithWriteableSysfs(_ context.Context, _ Client, _ *containers.Container, s *Spec) error {
|
||||
for i, m := range s.Mounts {
|
||||
if m.Type == "sysfs" {
|
||||
var options []string
|
||||
for _, o := range m.Options {
|
||||
if o == "ro" {
|
||||
o = "rw"
|
||||
}
|
||||
options = append(options, o)
|
||||
}
|
||||
s.Mounts[i].Options = options
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// WithWriteableCgroupfs makes any cgroup mounts writeable
|
||||
func WithWriteableCgroupfs(_ context.Context, _ Client, _ *containers.Container, s *Spec) error {
|
||||
for i, m := range s.Mounts {
|
||||
if m.Type == "cgroup" {
|
||||
var options []string
|
||||
for _, o := range m.Options {
|
||||
if o == "ro" {
|
||||
o = "rw"
|
||||
}
|
||||
options = append(options, o)
|
||||
}
|
||||
s.Mounts[i].Options = options
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// WithSelinuxLabel sets the process SELinux label
|
||||
func WithSelinuxLabel(label string) SpecOpts {
|
||||
return func(_ context.Context, _ Client, _ *containers.Container, s *Spec) error {
|
||||
setProcess(s)
|
||||
s.Process.SelinuxLabel = label
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithApparmorProfile sets the Apparmor profile for the process
|
||||
func WithApparmorProfile(profile string) SpecOpts {
|
||||
return func(_ context.Context, _ Client, _ *containers.Container, s *Spec) error {
|
||||
setProcess(s)
|
||||
s.Process.ApparmorProfile = profile
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithSeccompUnconfined clears the seccomp profile
|
||||
func WithSeccompUnconfined(_ context.Context, _ Client, _ *containers.Container, s *Spec) error {
|
||||
setLinux(s)
|
||||
s.Linux.Seccomp = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
// WithParentCgroupDevices uses the default cgroup setup to inherit the container's parent cgroup's
|
||||
// allowed and denied devices
|
||||
func WithParentCgroupDevices(_ context.Context, _ Client, _ *containers.Container, s *Spec) error {
|
||||
setLinux(s)
|
||||
if s.Linux.Resources == nil {
|
||||
s.Linux.Resources = &specs.LinuxResources{}
|
||||
}
|
||||
s.Linux.Resources.Devices = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
// WithDefaultUnixDevices adds the default devices for unix such as /dev/null, /dev/random to
|
||||
// the container's resource cgroup spec
|
||||
func WithDefaultUnixDevices(_ context.Context, _ Client, _ *containers.Container, s *Spec) error {
|
||||
setLinux(s)
|
||||
if s.Linux.Resources == nil {
|
||||
s.Linux.Resources = &specs.LinuxResources{}
|
||||
}
|
||||
intptr := func(i int64) *int64 {
|
||||
return &i
|
||||
}
|
||||
s.Linux.Resources.Devices = append(s.Linux.Resources.Devices, []specs.LinuxDeviceCgroup{
|
||||
{
|
||||
// "/dev/null",
|
||||
Type: "c",
|
||||
Major: intptr(1),
|
||||
Minor: intptr(3),
|
||||
Access: rwm,
|
||||
Allow: true,
|
||||
},
|
||||
{
|
||||
// "/dev/random",
|
||||
Type: "c",
|
||||
Major: intptr(1),
|
||||
Minor: intptr(8),
|
||||
Access: rwm,
|
||||
Allow: true,
|
||||
},
|
||||
{
|
||||
// "/dev/full",
|
||||
Type: "c",
|
||||
Major: intptr(1),
|
||||
Minor: intptr(7),
|
||||
Access: rwm,
|
||||
Allow: true,
|
||||
},
|
||||
{
|
||||
// "/dev/tty",
|
||||
Type: "c",
|
||||
Major: intptr(5),
|
||||
Minor: intptr(0),
|
||||
Access: rwm,
|
||||
Allow: true,
|
||||
},
|
||||
{
|
||||
// "/dev/zero",
|
||||
Type: "c",
|
||||
Major: intptr(1),
|
||||
Minor: intptr(5),
|
||||
Access: rwm,
|
||||
Allow: true,
|
||||
},
|
||||
{
|
||||
// "/dev/urandom",
|
||||
Type: "c",
|
||||
Major: intptr(1),
|
||||
Minor: intptr(9),
|
||||
Access: rwm,
|
||||
Allow: true,
|
||||
},
|
||||
{
|
||||
// "/dev/console",
|
||||
Type: "c",
|
||||
Major: intptr(5),
|
||||
Minor: intptr(1),
|
||||
Access: rwm,
|
||||
Allow: true,
|
||||
},
|
||||
// /dev/pts/ - pts namespaces are "coming soon"
|
||||
{
|
||||
Type: "c",
|
||||
Major: intptr(136),
|
||||
Access: rwm,
|
||||
Allow: true,
|
||||
},
|
||||
{
|
||||
Type: "c",
|
||||
Major: intptr(5),
|
||||
Minor: intptr(2),
|
||||
Access: rwm,
|
||||
Allow: true,
|
||||
},
|
||||
{
|
||||
// tuntap
|
||||
Type: "c",
|
||||
Major: intptr(10),
|
||||
Minor: intptr(200),
|
||||
Access: rwm,
|
||||
Allow: true,
|
||||
},
|
||||
}...)
|
||||
return nil
|
||||
}
|
||||
|
||||
// WithPrivileged sets up options for a privileged container
|
||||
// TODO(justincormack) device handling
|
||||
var WithPrivileged = Compose(
|
||||
WithAllCapabilities,
|
||||
WithMaskedPaths(nil),
|
||||
WithReadonlyPaths(nil),
|
||||
WithWriteableSysfs,
|
||||
WithWriteableCgroupfs,
|
||||
WithSelinuxLabel(""),
|
||||
WithApparmorProfile(""),
|
||||
WithSeccompUnconfined,
|
||||
)
|
||||
|
||||
733
vendor/github.com/containerd/containerd/oci/spec_opts_unix.go
generated
vendored
733
vendor/github.com/containerd/containerd/oci/spec_opts_unix.go
generated
vendored
@ -1,733 +0,0 @@
|
||||
// +build !windows
|
||||
|
||||
/*
|
||||
Copyright The containerd Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package oci
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/containerd/containerd/containers"
|
||||
"github.com/containerd/containerd/content"
|
||||
"github.com/containerd/containerd/images"
|
||||
"github.com/containerd/containerd/mount"
|
||||
"github.com/containerd/containerd/namespaces"
|
||||
"github.com/containerd/continuity/fs"
|
||||
"github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"github.com/opencontainers/runc/libcontainer/user"
|
||||
specs "github.com/opencontainers/runtime-spec/specs-go"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/syndtr/gocapability/capability"
|
||||
)
|
||||
|
||||
// WithTTY sets the information on the spec as well as the environment variables for
|
||||
// using a TTY
|
||||
func WithTTY(_ context.Context, _ Client, _ *containers.Container, s *Spec) error {
|
||||
setProcess(s)
|
||||
s.Process.Terminal = true
|
||||
s.Process.Env = append(s.Process.Env, "TERM=xterm")
|
||||
return nil
|
||||
}
|
||||
|
||||
// setRoot sets Root to empty if unset
|
||||
func setRoot(s *Spec) {
|
||||
if s.Root == nil {
|
||||
s.Root = &specs.Root{}
|
||||
}
|
||||
}
|
||||
|
||||
// setLinux sets Linux to empty if unset
|
||||
func setLinux(s *Spec) {
|
||||
if s.Linux == nil {
|
||||
s.Linux = &specs.Linux{}
|
||||
}
|
||||
}
|
||||
|
||||
// setCapabilities sets Linux Capabilities to empty if unset
|
||||
func setCapabilities(s *Spec) {
|
||||
setProcess(s)
|
||||
if s.Process.Capabilities == nil {
|
||||
s.Process.Capabilities = &specs.LinuxCapabilities{}
|
||||
}
|
||||
}
|
||||
|
||||
// WithHostNamespace allows a task to run inside the host's linux namespace
|
||||
func WithHostNamespace(ns specs.LinuxNamespaceType) SpecOpts {
|
||||
return func(_ context.Context, _ Client, _ *containers.Container, s *Spec) error {
|
||||
setLinux(s)
|
||||
for i, n := range s.Linux.Namespaces {
|
||||
if n.Type == ns {
|
||||
s.Linux.Namespaces = append(s.Linux.Namespaces[:i], s.Linux.Namespaces[i+1:]...)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithLinuxNamespace uses the passed in namespace for the spec. If a namespace of the same type already exists in the
|
||||
// spec, the existing namespace is replaced by the one provided.
|
||||
func WithLinuxNamespace(ns specs.LinuxNamespace) SpecOpts {
|
||||
return func(_ context.Context, _ Client, _ *containers.Container, s *Spec) error {
|
||||
setLinux(s)
|
||||
for i, n := range s.Linux.Namespaces {
|
||||
if n.Type == ns.Type {
|
||||
before := s.Linux.Namespaces[:i]
|
||||
after := s.Linux.Namespaces[i+1:]
|
||||
s.Linux.Namespaces = append(before, ns)
|
||||
s.Linux.Namespaces = append(s.Linux.Namespaces, after...)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
s.Linux.Namespaces = append(s.Linux.Namespaces, ns)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithImageConfig configures the spec to from the configuration of an Image
|
||||
func WithImageConfig(image Image) SpecOpts {
|
||||
return WithImageConfigArgs(image, nil)
|
||||
}
|
||||
|
||||
// WithImageConfigArgs configures the spec to from the configuration of an Image with additional args that
|
||||
// replaces the CMD of the image
|
||||
func WithImageConfigArgs(image Image, args []string) SpecOpts {
|
||||
return func(ctx context.Context, client Client, c *containers.Container, s *Spec) error {
|
||||
ic, err := image.Config(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var (
|
||||
ociimage v1.Image
|
||||
config v1.ImageConfig
|
||||
)
|
||||
switch ic.MediaType {
|
||||
case v1.MediaTypeImageConfig, images.MediaTypeDockerSchema2Config:
|
||||
p, err := content.ReadBlob(ctx, image.ContentStore(), ic)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(p, &ociimage); err != nil {
|
||||
return err
|
||||
}
|
||||
config = ociimage.Config
|
||||
default:
|
||||
return fmt.Errorf("unknown image config media type %s", ic.MediaType)
|
||||
}
|
||||
|
||||
setProcess(s)
|
||||
s.Process.Env = append(s.Process.Env, config.Env...)
|
||||
cmd := config.Cmd
|
||||
if len(args) > 0 {
|
||||
cmd = args
|
||||
}
|
||||
s.Process.Args = append(config.Entrypoint, cmd...)
|
||||
|
||||
cwd := config.WorkingDir
|
||||
if cwd == "" {
|
||||
cwd = "/"
|
||||
}
|
||||
s.Process.Cwd = cwd
|
||||
if config.User != "" {
|
||||
return WithUser(config.User)(ctx, client, c, s)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithRootFSPath specifies unmanaged rootfs path.
|
||||
func WithRootFSPath(path string) SpecOpts {
|
||||
return func(_ context.Context, _ Client, _ *containers.Container, s *Spec) error {
|
||||
setRoot(s)
|
||||
s.Root.Path = path
|
||||
// Entrypoint is not set here (it's up to caller)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithRootFSReadonly sets specs.Root.Readonly to true
|
||||
func WithRootFSReadonly() SpecOpts {
|
||||
return func(_ context.Context, _ Client, _ *containers.Container, s *Spec) error {
|
||||
setRoot(s)
|
||||
s.Root.Readonly = true
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithNoNewPrivileges sets no_new_privileges on the process for the container
|
||||
func WithNoNewPrivileges(_ context.Context, _ Client, _ *containers.Container, s *Spec) error {
|
||||
setProcess(s)
|
||||
s.Process.NoNewPrivileges = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// WithHostHostsFile bind-mounts the host's /etc/hosts into the container as readonly
|
||||
func WithHostHostsFile(_ context.Context, _ Client, _ *containers.Container, s *Spec) error {
|
||||
s.Mounts = append(s.Mounts, specs.Mount{
|
||||
Destination: "/etc/hosts",
|
||||
Type: "bind",
|
||||
Source: "/etc/hosts",
|
||||
Options: []string{"rbind", "ro"},
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
// WithHostResolvconf bind-mounts the host's /etc/resolv.conf into the container as readonly
|
||||
func WithHostResolvconf(_ context.Context, _ Client, _ *containers.Container, s *Spec) error {
|
||||
s.Mounts = append(s.Mounts, specs.Mount{
|
||||
Destination: "/etc/resolv.conf",
|
||||
Type: "bind",
|
||||
Source: "/etc/resolv.conf",
|
||||
Options: []string{"rbind", "ro"},
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
// WithHostLocaltime bind-mounts the host's /etc/localtime into the container as readonly
|
||||
func WithHostLocaltime(_ context.Context, _ Client, _ *containers.Container, s *Spec) error {
|
||||
s.Mounts = append(s.Mounts, specs.Mount{
|
||||
Destination: "/etc/localtime",
|
||||
Type: "bind",
|
||||
Source: "/etc/localtime",
|
||||
Options: []string{"rbind", "ro"},
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
// WithUserNamespace sets the uid and gid mappings for the task
|
||||
// this can be called multiple times to add more mappings to the generated spec
|
||||
func WithUserNamespace(container, host, size uint32) SpecOpts {
|
||||
return func(_ context.Context, _ Client, _ *containers.Container, s *Spec) error {
|
||||
var hasUserns bool
|
||||
setLinux(s)
|
||||
for _, ns := range s.Linux.Namespaces {
|
||||
if ns.Type == specs.UserNamespace {
|
||||
hasUserns = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasUserns {
|
||||
s.Linux.Namespaces = append(s.Linux.Namespaces, specs.LinuxNamespace{
|
||||
Type: specs.UserNamespace,
|
||||
})
|
||||
}
|
||||
mapping := specs.LinuxIDMapping{
|
||||
ContainerID: container,
|
||||
HostID: host,
|
||||
Size: size,
|
||||
}
|
||||
s.Linux.UIDMappings = append(s.Linux.UIDMappings, mapping)
|
||||
s.Linux.GIDMappings = append(s.Linux.GIDMappings, mapping)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithCgroup sets the container's cgroup path
|
||||
func WithCgroup(path string) SpecOpts {
|
||||
return func(_ context.Context, _ Client, _ *containers.Container, s *Spec) error {
|
||||
setLinux(s)
|
||||
s.Linux.CgroupsPath = path
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithNamespacedCgroup uses the namespace set on the context to create a
|
||||
// root directory for containers in the cgroup with the id as the subcgroup
|
||||
func WithNamespacedCgroup() SpecOpts {
|
||||
return func(ctx context.Context, _ Client, c *containers.Container, s *Spec) error {
|
||||
namespace, err := namespaces.NamespaceRequired(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
setLinux(s)
|
||||
s.Linux.CgroupsPath = filepath.Join("/", namespace, c.ID)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithUser sets the user to be used within the container.
|
||||
// It accepts a valid user string in OCI Image Spec v1.0.0:
|
||||
// user, uid, user:group, uid:gid, uid:group, user:gid
|
||||
func WithUser(userstr string) SpecOpts {
|
||||
return func(ctx context.Context, client Client, c *containers.Container, s *Spec) error {
|
||||
setProcess(s)
|
||||
parts := strings.Split(userstr, ":")
|
||||
switch len(parts) {
|
||||
case 1:
|
||||
v, err := strconv.Atoi(parts[0])
|
||||
if err != nil {
|
||||
// if we cannot parse as a uint they try to see if it is a username
|
||||
return WithUsername(userstr)(ctx, client, c, s)
|
||||
}
|
||||
return WithUserID(uint32(v))(ctx, client, c, s)
|
||||
case 2:
|
||||
var (
|
||||
username string
|
||||
groupname string
|
||||
)
|
||||
var uid, gid uint32
|
||||
v, err := strconv.Atoi(parts[0])
|
||||
if err != nil {
|
||||
username = parts[0]
|
||||
} else {
|
||||
uid = uint32(v)
|
||||
}
|
||||
if v, err = strconv.Atoi(parts[1]); err != nil {
|
||||
groupname = parts[1]
|
||||
} else {
|
||||
gid = uint32(v)
|
||||
}
|
||||
if username == "" && groupname == "" {
|
||||
s.Process.User.UID, s.Process.User.GID = uid, gid
|
||||
return nil
|
||||
}
|
||||
f := func(root string) error {
|
||||
if username != "" {
|
||||
uid, _, err = getUIDGIDFromPath(root, func(u user.User) bool {
|
||||
return u.Name == username
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if groupname != "" {
|
||||
gid, err = getGIDFromPath(root, func(g user.Group) bool {
|
||||
return g.Name == groupname
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
s.Process.User.UID, s.Process.User.GID = uid, gid
|
||||
return nil
|
||||
}
|
||||
if c.Snapshotter == "" && c.SnapshotKey == "" {
|
||||
if !isRootfsAbs(s.Root.Path) {
|
||||
return errors.New("rootfs absolute path is required")
|
||||
}
|
||||
return f(s.Root.Path)
|
||||
}
|
||||
if c.Snapshotter == "" {
|
||||
return errors.New("no snapshotter set for container")
|
||||
}
|
||||
if c.SnapshotKey == "" {
|
||||
return errors.New("rootfs snapshot not created for container")
|
||||
}
|
||||
snapshotter := client.SnapshotService(c.Snapshotter)
|
||||
mounts, err := snapshotter.Mounts(ctx, c.SnapshotKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return mount.WithTempMount(ctx, mounts, f)
|
||||
default:
|
||||
return fmt.Errorf("invalid USER value %s", userstr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WithUIDGID allows the UID and GID for the Process to be set
|
||||
func WithUIDGID(uid, gid uint32) SpecOpts {
|
||||
return func(_ context.Context, _ Client, _ *containers.Container, s *Spec) error {
|
||||
setProcess(s)
|
||||
s.Process.User.UID = uid
|
||||
s.Process.User.GID = gid
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithUserID sets the correct UID and GID for the container based
|
||||
// on the image's /etc/passwd contents. If /etc/passwd does not exist,
|
||||
// or uid is not found in /etc/passwd, it sets the requested uid,
|
||||
// additionally sets the gid to 0, and does not return an error.
|
||||
func WithUserID(uid uint32) SpecOpts {
|
||||
return func(ctx context.Context, client Client, c *containers.Container, s *Spec) (err error) {
|
||||
setProcess(s)
|
||||
if c.Snapshotter == "" && c.SnapshotKey == "" {
|
||||
if !isRootfsAbs(s.Root.Path) {
|
||||
return errors.Errorf("rootfs absolute path is required")
|
||||
}
|
||||
uuid, ugid, err := getUIDGIDFromPath(s.Root.Path, func(u user.User) bool {
|
||||
return u.Uid == int(uid)
|
||||
})
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) || err == errNoUsersFound {
|
||||
s.Process.User.UID, s.Process.User.GID = uid, 0
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
s.Process.User.UID, s.Process.User.GID = uuid, ugid
|
||||
return nil
|
||||
|
||||
}
|
||||
if c.Snapshotter == "" {
|
||||
return errors.Errorf("no snapshotter set for container")
|
||||
}
|
||||
if c.SnapshotKey == "" {
|
||||
return errors.Errorf("rootfs snapshot not created for container")
|
||||
}
|
||||
snapshotter := client.SnapshotService(c.Snapshotter)
|
||||
mounts, err := snapshotter.Mounts(ctx, c.SnapshotKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return mount.WithTempMount(ctx, mounts, func(root string) error {
|
||||
uuid, ugid, err := getUIDGIDFromPath(root, func(u user.User) bool {
|
||||
return u.Uid == int(uid)
|
||||
})
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) || err == errNoUsersFound {
|
||||
s.Process.User.UID, s.Process.User.GID = uid, 0
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
s.Process.User.UID, s.Process.User.GID = uuid, ugid
|
||||
return nil
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// WithUsername sets the correct UID and GID for the container
|
||||
// based on the the image's /etc/passwd contents. If /etc/passwd
|
||||
// does not exist, or the username is not found in /etc/passwd,
|
||||
// it returns error.
|
||||
func WithUsername(username string) SpecOpts {
|
||||
return func(ctx context.Context, client Client, c *containers.Container, s *Spec) (err error) {
|
||||
setProcess(s)
|
||||
if c.Snapshotter == "" && c.SnapshotKey == "" {
|
||||
if !isRootfsAbs(s.Root.Path) {
|
||||
return errors.Errorf("rootfs absolute path is required")
|
||||
}
|
||||
uid, gid, err := getUIDGIDFromPath(s.Root.Path, func(u user.User) bool {
|
||||
return u.Name == username
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.Process.User.UID, s.Process.User.GID = uid, gid
|
||||
return nil
|
||||
}
|
||||
if c.Snapshotter == "" {
|
||||
return errors.Errorf("no snapshotter set for container")
|
||||
}
|
||||
if c.SnapshotKey == "" {
|
||||
return errors.Errorf("rootfs snapshot not created for container")
|
||||
}
|
||||
snapshotter := client.SnapshotService(c.Snapshotter)
|
||||
mounts, err := snapshotter.Mounts(ctx, c.SnapshotKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return mount.WithTempMount(ctx, mounts, func(root string) error {
|
||||
uid, gid, err := getUIDGIDFromPath(root, func(u user.User) bool {
|
||||
return u.Name == username
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.Process.User.UID, s.Process.User.GID = uid, gid
|
||||
return nil
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// WithCapabilities sets Linux capabilities on the process
|
||||
func WithCapabilities(caps []string) SpecOpts {
|
||||
return func(_ context.Context, _ Client, _ *containers.Container, s *Spec) error {
|
||||
setCapabilities(s)
|
||||
|
||||
s.Process.Capabilities.Bounding = caps
|
||||
s.Process.Capabilities.Effective = caps
|
||||
s.Process.Capabilities.Permitted = caps
|
||||
s.Process.Capabilities.Inheritable = caps
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithAllCapabilities sets all linux capabilities for the process
|
||||
var WithAllCapabilities = WithCapabilities(getAllCapabilities())
|
||||
|
||||
func getAllCapabilities() []string {
|
||||
last := capability.CAP_LAST_CAP
|
||||
// hack for RHEL6 which has no /proc/sys/kernel/cap_last_cap
|
||||
if last == capability.Cap(63) {
|
||||
last = capability.CAP_BLOCK_SUSPEND
|
||||
}
|
||||
var caps []string
|
||||
for _, cap := range capability.List() {
|
||||
if cap > last {
|
||||
continue
|
||||
}
|
||||
caps = append(caps, "CAP_"+strings.ToUpper(cap.String()))
|
||||
}
|
||||
return caps
|
||||
}
|
||||
|
||||
// WithAmbientCapabilities set the Linux ambient capabilities for the process
|
||||
// Ambient capabilities should only be set for non-root users or the caller should
|
||||
// understand how these capabilities are used and set
|
||||
func WithAmbientCapabilities(caps []string) SpecOpts {
|
||||
return func(_ context.Context, _ Client, _ *containers.Container, s *Spec) error {
|
||||
setCapabilities(s)
|
||||
|
||||
s.Process.Capabilities.Ambient = caps
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
var errNoUsersFound = errors.New("no users found")
|
||||
|
||||
func getUIDGIDFromPath(root string, filter func(user.User) bool) (uid, gid uint32, err error) {
|
||||
ppath, err := fs.RootPath(root, "/etc/passwd")
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
users, err := user.ParsePasswdFileFilter(ppath, filter)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
if len(users) == 0 {
|
||||
return 0, 0, errNoUsersFound
|
||||
}
|
||||
u := users[0]
|
||||
return uint32(u.Uid), uint32(u.Gid), nil
|
||||
}
|
||||
|
||||
var errNoGroupsFound = errors.New("no groups found")
|
||||
|
||||
func getGIDFromPath(root string, filter func(user.Group) bool) (gid uint32, err error) {
|
||||
gpath, err := fs.RootPath(root, "/etc/group")
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
groups, err := user.ParseGroupFileFilter(gpath, filter)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if len(groups) == 0 {
|
||||
return 0, errNoGroupsFound
|
||||
}
|
||||
g := groups[0]
|
||||
return uint32(g.Gid), nil
|
||||
}
|
||||
|
||||
func isRootfsAbs(root string) bool {
|
||||
return filepath.IsAbs(root)
|
||||
}
|
||||
|
||||
// WithMaskedPaths sets the masked paths option
|
||||
func WithMaskedPaths(paths []string) SpecOpts {
|
||||
return func(_ context.Context, _ Client, _ *containers.Container, s *Spec) error {
|
||||
setLinux(s)
|
||||
s.Linux.MaskedPaths = paths
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithReadonlyPaths sets the read only paths option
|
||||
func WithReadonlyPaths(paths []string) SpecOpts {
|
||||
return func(_ context.Context, _ Client, _ *containers.Container, s *Spec) error {
|
||||
setLinux(s)
|
||||
s.Linux.ReadonlyPaths = paths
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithWriteableSysfs makes any sysfs mounts writeable
|
||||
func WithWriteableSysfs(_ context.Context, _ Client, _ *containers.Container, s *Spec) error {
|
||||
for i, m := range s.Mounts {
|
||||
if m.Type == "sysfs" {
|
||||
var options []string
|
||||
for _, o := range m.Options {
|
||||
if o == "ro" {
|
||||
o = "rw"
|
||||
}
|
||||
options = append(options, o)
|
||||
}
|
||||
s.Mounts[i].Options = options
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// WithWriteableCgroupfs makes any cgroup mounts writeable
|
||||
func WithWriteableCgroupfs(_ context.Context, _ Client, _ *containers.Container, s *Spec) error {
|
||||
for i, m := range s.Mounts {
|
||||
if m.Type == "cgroup" {
|
||||
var options []string
|
||||
for _, o := range m.Options {
|
||||
if o == "ro" {
|
||||
o = "rw"
|
||||
}
|
||||
options = append(options, o)
|
||||
}
|
||||
s.Mounts[i].Options = options
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// WithSelinuxLabel sets the process SELinux label
|
||||
func WithSelinuxLabel(label string) SpecOpts {
|
||||
return func(_ context.Context, _ Client, _ *containers.Container, s *Spec) error {
|
||||
setProcess(s)
|
||||
s.Process.SelinuxLabel = label
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithApparmorProfile sets the Apparmor profile for the process
|
||||
func WithApparmorProfile(profile string) SpecOpts {
|
||||
return func(_ context.Context, _ Client, _ *containers.Container, s *Spec) error {
|
||||
setProcess(s)
|
||||
s.Process.ApparmorProfile = profile
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithSeccompUnconfined clears the seccomp profile
|
||||
func WithSeccompUnconfined(_ context.Context, _ Client, _ *containers.Container, s *Spec) error {
|
||||
setLinux(s)
|
||||
s.Linux.Seccomp = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
// WithParentCgroupDevices uses the default cgroup setup to inherit the container's parent cgroup's
|
||||
// allowed and denied devices
|
||||
func WithParentCgroupDevices(_ context.Context, _ Client, _ *containers.Container, s *Spec) error {
|
||||
setLinux(s)
|
||||
if s.Linux.Resources == nil {
|
||||
s.Linux.Resources = &specs.LinuxResources{}
|
||||
}
|
||||
s.Linux.Resources.Devices = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
// WithDefaultUnixDevices adds the default devices for unix such as /dev/null, /dev/random to
|
||||
// the container's resource cgroup spec
|
||||
func WithDefaultUnixDevices(_ context.Context, _ Client, _ *containers.Container, s *Spec) error {
|
||||
setLinux(s)
|
||||
if s.Linux.Resources == nil {
|
||||
s.Linux.Resources = &specs.LinuxResources{}
|
||||
}
|
||||
intptr := func(i int64) *int64 {
|
||||
return &i
|
||||
}
|
||||
s.Linux.Resources.Devices = append(s.Linux.Resources.Devices, []specs.LinuxDeviceCgroup{
|
||||
{
|
||||
// "/dev/null",
|
||||
Type: "c",
|
||||
Major: intptr(1),
|
||||
Minor: intptr(3),
|
||||
Access: rwm,
|
||||
Allow: true,
|
||||
},
|
||||
{
|
||||
// "/dev/random",
|
||||
Type: "c",
|
||||
Major: intptr(1),
|
||||
Minor: intptr(8),
|
||||
Access: rwm,
|
||||
Allow: true,
|
||||
},
|
||||
{
|
||||
// "/dev/full",
|
||||
Type: "c",
|
||||
Major: intptr(1),
|
||||
Minor: intptr(7),
|
||||
Access: rwm,
|
||||
Allow: true,
|
||||
},
|
||||
{
|
||||
// "/dev/tty",
|
||||
Type: "c",
|
||||
Major: intptr(5),
|
||||
Minor: intptr(0),
|
||||
Access: rwm,
|
||||
Allow: true,
|
||||
},
|
||||
{
|
||||
// "/dev/zero",
|
||||
Type: "c",
|
||||
Major: intptr(1),
|
||||
Minor: intptr(5),
|
||||
Access: rwm,
|
||||
Allow: true,
|
||||
},
|
||||
{
|
||||
// "/dev/urandom",
|
||||
Type: "c",
|
||||
Major: intptr(1),
|
||||
Minor: intptr(9),
|
||||
Access: rwm,
|
||||
Allow: true,
|
||||
},
|
||||
{
|
||||
// "/dev/console",
|
||||
Type: "c",
|
||||
Major: intptr(5),
|
||||
Minor: intptr(1),
|
||||
Access: rwm,
|
||||
Allow: true,
|
||||
},
|
||||
// /dev/pts/ - pts namespaces are "coming soon"
|
||||
{
|
||||
Type: "c",
|
||||
Major: intptr(136),
|
||||
Access: rwm,
|
||||
Allow: true,
|
||||
},
|
||||
{
|
||||
Type: "c",
|
||||
Major: intptr(5),
|
||||
Minor: intptr(2),
|
||||
Access: rwm,
|
||||
Allow: true,
|
||||
},
|
||||
{
|
||||
// tuntap
|
||||
Type: "c",
|
||||
Major: intptr(10),
|
||||
Minor: intptr(200),
|
||||
Access: rwm,
|
||||
Allow: true,
|
||||
},
|
||||
}...)
|
||||
return nil
|
||||
}
|
||||
|
||||
// WithPrivileged sets up options for a privileged container
|
||||
// TODO(justincormack) device handling
|
||||
var WithPrivileged = Compose(
|
||||
WithAllCapabilities,
|
||||
WithMaskedPaths(nil),
|
||||
WithReadonlyPaths(nil),
|
||||
WithWriteableSysfs,
|
||||
WithWriteableCgroupfs,
|
||||
WithSelinuxLabel(""),
|
||||
WithApparmorProfile(""),
|
||||
WithSeccompUnconfined,
|
||||
)
|
||||
89
vendor/github.com/containerd/containerd/oci/spec_opts_windows.go
generated
vendored
89
vendor/github.com/containerd/containerd/oci/spec_opts_windows.go
generated
vendored
@ -1,89 +0,0 @@
|
||||
// +build windows
|
||||
|
||||
/*
|
||||
Copyright The containerd Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package oci
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/containerd/containerd/containers"
|
||||
"github.com/containerd/containerd/content"
|
||||
"github.com/containerd/containerd/images"
|
||||
"github.com/opencontainers/image-spec/specs-go/v1"
|
||||
specs "github.com/opencontainers/runtime-spec/specs-go"
|
||||
)
|
||||
|
||||
// WithImageConfig configures the spec to from the configuration of an Image
|
||||
func WithImageConfig(image Image) SpecOpts {
|
||||
return func(ctx context.Context, client Client, _ *containers.Container, s *Spec) error {
|
||||
setProcess(s)
|
||||
ic, err := image.Config(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var (
|
||||
ociimage v1.Image
|
||||
config v1.ImageConfig
|
||||
)
|
||||
switch ic.MediaType {
|
||||
case v1.MediaTypeImageConfig, images.MediaTypeDockerSchema2Config:
|
||||
p, err := content.ReadBlob(ctx, image.ContentStore(), ic)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := json.Unmarshal(p, &ociimage); err != nil {
|
||||
return err
|
||||
}
|
||||
config = ociimage.Config
|
||||
default:
|
||||
return fmt.Errorf("unknown image config media type %s", ic.MediaType)
|
||||
}
|
||||
s.Process.Env = config.Env
|
||||
s.Process.Args = append(config.Entrypoint, config.Cmd...)
|
||||
s.Process.User = specs.User{
|
||||
Username: config.User,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithTTY sets the information on the spec as well as the environment variables for
|
||||
// using a TTY
|
||||
func WithTTY(width, height int) SpecOpts {
|
||||
return func(_ context.Context, _ Client, _ *containers.Container, s *Spec) error {
|
||||
setProcess(s)
|
||||
s.Process.Terminal = true
|
||||
if s.Process.ConsoleSize == nil {
|
||||
s.Process.ConsoleSize = &specs.Box{}
|
||||
}
|
||||
s.Process.ConsoleSize.Width = uint(width)
|
||||
s.Process.ConsoleSize.Height = uint(height)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithUsername sets the username on the process
|
||||
func WithUsername(username string) SpecOpts {
|
||||
return func(ctx context.Context, client Client, c *containers.Container, s *Spec) error {
|
||||
setProcess(s)
|
||||
s.Process.User.Username = username
|
||||
return nil
|
||||
}
|
||||
}
|
||||
188
vendor/github.com/containerd/containerd/oci/spec_unix.go
generated
vendored
188
vendor/github.com/containerd/containerd/oci/spec_unix.go
generated
vendored
@ -1,188 +0,0 @@
|
||||
// +build !windows
|
||||
|
||||
/*
|
||||
Copyright The containerd Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package oci
|
||||
|
||||
import (
|
||||
"context"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/containerd/containerd/namespaces"
|
||||
specs "github.com/opencontainers/runtime-spec/specs-go"
|
||||
)
|
||||
|
||||
const (
|
||||
rwm = "rwm"
|
||||
defaultRootfsPath = "rootfs"
|
||||
)
|
||||
|
||||
var (
|
||||
defaultEnv = []string{
|
||||
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
|
||||
}
|
||||
)
|
||||
|
||||
func defaultCaps() []string {
|
||||
return []string{
|
||||
"CAP_CHOWN",
|
||||
"CAP_DAC_OVERRIDE",
|
||||
"CAP_FSETID",
|
||||
"CAP_FOWNER",
|
||||
"CAP_MKNOD",
|
||||
"CAP_NET_RAW",
|
||||
"CAP_SETGID",
|
||||
"CAP_SETUID",
|
||||
"CAP_SETFCAP",
|
||||
"CAP_SETPCAP",
|
||||
"CAP_NET_BIND_SERVICE",
|
||||
"CAP_SYS_CHROOT",
|
||||
"CAP_KILL",
|
||||
"CAP_AUDIT_WRITE",
|
||||
}
|
||||
}
|
||||
|
||||
func defaultNamespaces() []specs.LinuxNamespace {
|
||||
return []specs.LinuxNamespace{
|
||||
{
|
||||
Type: specs.PIDNamespace,
|
||||
},
|
||||
{
|
||||
Type: specs.IPCNamespace,
|
||||
},
|
||||
{
|
||||
Type: specs.UTSNamespace,
|
||||
},
|
||||
{
|
||||
Type: specs.MountNamespace,
|
||||
},
|
||||
{
|
||||
Type: specs.NetworkNamespace,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func populateDefaultSpec(ctx context.Context, s *Spec, id string) error {
|
||||
ns, err := namespaces.NamespaceRequired(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*s = Spec{
|
||||
Version: specs.Version,
|
||||
Root: &specs.Root{
|
||||
Path: defaultRootfsPath,
|
||||
},
|
||||
Process: &specs.Process{
|
||||
Env: defaultEnv,
|
||||
Cwd: "/",
|
||||
NoNewPrivileges: true,
|
||||
User: specs.User{
|
||||
UID: 0,
|
||||
GID: 0,
|
||||
},
|
||||
Capabilities: &specs.LinuxCapabilities{
|
||||
Bounding: defaultCaps(),
|
||||
Permitted: defaultCaps(),
|
||||
Inheritable: defaultCaps(),
|
||||
Effective: defaultCaps(),
|
||||
},
|
||||
Rlimits: []specs.POSIXRlimit{
|
||||
{
|
||||
Type: "RLIMIT_NOFILE",
|
||||
Hard: uint64(1024),
|
||||
Soft: uint64(1024),
|
||||
},
|
||||
},
|
||||
},
|
||||
Mounts: []specs.Mount{
|
||||
{
|
||||
Destination: "/proc",
|
||||
Type: "proc",
|
||||
Source: "proc",
|
||||
},
|
||||
{
|
||||
Destination: "/dev",
|
||||
Type: "tmpfs",
|
||||
Source: "tmpfs",
|
||||
Options: []string{"nosuid", "strictatime", "mode=755", "size=65536k"},
|
||||
},
|
||||
{
|
||||
Destination: "/dev/pts",
|
||||
Type: "devpts",
|
||||
Source: "devpts",
|
||||
Options: []string{"nosuid", "noexec", "newinstance", "ptmxmode=0666", "mode=0620", "gid=5"},
|
||||
},
|
||||
{
|
||||
Destination: "/dev/shm",
|
||||
Type: "tmpfs",
|
||||
Source: "shm",
|
||||
Options: []string{"nosuid", "noexec", "nodev", "mode=1777", "size=65536k"},
|
||||
},
|
||||
{
|
||||
Destination: "/dev/mqueue",
|
||||
Type: "mqueue",
|
||||
Source: "mqueue",
|
||||
Options: []string{"nosuid", "noexec", "nodev"},
|
||||
},
|
||||
{
|
||||
Destination: "/sys",
|
||||
Type: "sysfs",
|
||||
Source: "sysfs",
|
||||
Options: []string{"nosuid", "noexec", "nodev", "ro"},
|
||||
},
|
||||
{
|
||||
Destination: "/run",
|
||||
Type: "tmpfs",
|
||||
Source: "tmpfs",
|
||||
Options: []string{"nosuid", "strictatime", "mode=755", "size=65536k"},
|
||||
},
|
||||
},
|
||||
Linux: &specs.Linux{
|
||||
MaskedPaths: []string{
|
||||
"/proc/acpi",
|
||||
"/proc/kcore",
|
||||
"/proc/keys",
|
||||
"/proc/latency_stats",
|
||||
"/proc/timer_list",
|
||||
"/proc/timer_stats",
|
||||
"/proc/sched_debug",
|
||||
"/sys/firmware",
|
||||
"/proc/scsi",
|
||||
},
|
||||
ReadonlyPaths: []string{
|
||||
"/proc/asound",
|
||||
"/proc/bus",
|
||||
"/proc/fs",
|
||||
"/proc/irq",
|
||||
"/proc/sys",
|
||||
"/proc/sysrq-trigger",
|
||||
},
|
||||
CgroupsPath: filepath.Join("/", ns, id),
|
||||
Resources: &specs.LinuxResources{
|
||||
Devices: []specs.LinuxDeviceCgroup{
|
||||
{
|
||||
Allow: false,
|
||||
Access: rwm,
|
||||
},
|
||||
},
|
||||
},
|
||||
Namespaces: defaultNamespaces(),
|
||||
},
|
||||
}
|
||||
return nil
|
||||
}
|
||||
5
vendor/github.com/containerd/containerd/platforms/defaults.go
generated
vendored
5
vendor/github.com/containerd/containerd/platforms/defaults.go
generated
vendored
@ -22,11 +22,6 @@ import (
|
||||
specs "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
)
|
||||
|
||||
// Default returns the default matcher for the platform.
|
||||
func Default() MatchComparer {
|
||||
return Only(DefaultSpec())
|
||||
}
|
||||
|
||||
// DefaultString returns the default string specifier for the platform.
|
||||
func DefaultString() string {
|
||||
return Format(DefaultSpec())
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
// +build !windows
|
||||
|
||||
/*
|
||||
Copyright The containerd Authors.
|
||||
|
||||
@ -14,31 +16,9 @@
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package oci
|
||||
package platforms
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
specs "github.com/opencontainers/runtime-spec/specs-go"
|
||||
)
|
||||
|
||||
func populateDefaultSpec(ctx context.Context, s *Spec, id string) error {
|
||||
*s = Spec{
|
||||
Version: specs.Version,
|
||||
Root: &specs.Root{},
|
||||
Process: &specs.Process{
|
||||
Cwd: `C:\`,
|
||||
ConsoleSize: &specs.Box{
|
||||
Width: 80,
|
||||
Height: 20,
|
||||
},
|
||||
},
|
||||
Windows: &specs.Windows{
|
||||
IgnoreFlushesDuringBoot: true,
|
||||
Network: &specs.WindowsNetwork{
|
||||
AllowUnqualifiedDNSQuery: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
return nil
|
||||
// Default returns the default matcher for the platform.
|
||||
func Default() MatchComparer {
|
||||
return Only(DefaultSpec())
|
||||
}
|
||||
@ -1,3 +1,5 @@
|
||||
// +build windows
|
||||
|
||||
/*
|
||||
Copyright The containerd Authors.
|
||||
|
||||
@ -14,18 +16,16 @@
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package containerd
|
||||
package platforms
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
specs "github.com/opencontainers/runtime-spec/specs-go"
|
||||
specs "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
)
|
||||
|
||||
// WithResources sets the provided resources on the spec for task updates
|
||||
func WithResources(resources *specs.WindowsResources) UpdateTaskOpts {
|
||||
return func(ctx context.Context, client *Client, r *UpdateTaskInfo) error {
|
||||
r.Resources = resources
|
||||
return nil
|
||||
}
|
||||
// Default returns the default matcher for the platform.
|
||||
func Default() MatchComparer {
|
||||
return Ordered(DefaultSpec(), specs.Platform{
|
||||
OS: "linux",
|
||||
Architecture: "amd64",
|
||||
})
|
||||
}
|
||||
2
vendor/github.com/containerd/containerd/remotes/docker/fetcher.go
generated
vendored
2
vendor/github.com/containerd/containerd/remotes/docker/fetcher.go
generated
vendored
@ -117,7 +117,7 @@ func (r dockerFetcher) open(ctx context.Context, u, mediatype string, offset int
|
||||
}
|
||||
} else {
|
||||
// TODO: Should any cases where use of content range
|
||||
// without the proper header be considerd?
|
||||
// without the proper header be considered?
|
||||
// 206 responses?
|
||||
|
||||
// Discard up to offset
|
||||
|
||||
2
vendor/github.com/containerd/containerd/remotes/docker/httpreadseeker.go
generated
vendored
2
vendor/github.com/containerd/containerd/remotes/docker/httpreadseeker.go
generated
vendored
@ -134,7 +134,7 @@ func (hrs *httpReadSeeker) reader() (io.Reader, error) {
|
||||
// There is an edge case here where offset == size of the content. If
|
||||
// we seek, we will probably get an error for content that cannot be
|
||||
// sought (?). In that case, we should err on committing the content,
|
||||
// as the length is already satisified but we just return the empty
|
||||
// as the length is already satisfied but we just return the empty
|
||||
// reader instead.
|
||||
|
||||
hrs.rc = ioutil.NopCloser(bytes.NewReader([]byte{}))
|
||||
|
||||
55
vendor/github.com/containerd/containerd/remotes/docker/schema1/converter.go
generated
vendored
55
vendor/github.com/containerd/containerd/remotes/docker/schema1/converter.go
generated
vendored
@ -272,8 +272,14 @@ func (c *Converter) fetchBlob(ctx context.Context, desc ocispec.Descriptor) erro
|
||||
return err
|
||||
}
|
||||
|
||||
// TODO: Check if blob -> diff id mapping already exists
|
||||
// TODO: Check if blob empty label exists
|
||||
reuse, err := c.reuseLabelBlobState(ctx, desc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if reuse {
|
||||
return nil
|
||||
}
|
||||
|
||||
ra, err := c.contentStore.ReaderAt(ctx, desc)
|
||||
if err != nil {
|
||||
@ -343,6 +349,17 @@ func (c *Converter) fetchBlob(ctx context.Context, desc ocispec.Descriptor) erro
|
||||
|
||||
state := calc.State()
|
||||
|
||||
cinfo := content.Info{
|
||||
Digest: desc.Digest,
|
||||
Labels: map[string]string{
|
||||
"containerd.io/uncompressed": state.diffID.String(),
|
||||
},
|
||||
}
|
||||
|
||||
if _, err := c.contentStore.Update(ctx, cinfo, "labels.containerd.io/uncompressed"); err != nil {
|
||||
return errors.Wrap(err, "failed to update uncompressed label")
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
c.blobMap[desc.Digest] = state
|
||||
c.layerBlobs[state.diffID] = desc
|
||||
@ -351,6 +368,40 @@ func (c *Converter) fetchBlob(ctx context.Context, desc ocispec.Descriptor) erro
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Converter) reuseLabelBlobState(ctx context.Context, desc ocispec.Descriptor) (bool, error) {
|
||||
cinfo, err := c.contentStore.Info(ctx, desc.Digest)
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "failed to get blob info")
|
||||
}
|
||||
desc.Size = cinfo.Size
|
||||
|
||||
diffID, ok := cinfo.Labels["containerd.io/uncompressed"]
|
||||
if !ok {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
bState := blobState{empty: false}
|
||||
|
||||
if bState.diffID, err = digest.Parse(diffID); err != nil {
|
||||
log.G(ctx).WithField("id", desc.Digest).Warnf("failed to parse digest from label containerd.io/uncompressed: %v", diffID)
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// NOTE: there is no need to read header to get compression method
|
||||
// because there are only two kinds of methods.
|
||||
if bState.diffID == desc.Digest {
|
||||
desc.MediaType = images.MediaTypeDockerSchema2Layer
|
||||
} else {
|
||||
desc.MediaType = images.MediaTypeDockerSchema2LayerGzip
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
c.blobMap[desc.Digest] = bState
|
||||
c.layerBlobs[bState.diffID] = desc
|
||||
c.mu.Unlock()
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (c *Converter) schema1ManifestHistory() ([]ocispec.History, []digest.Digest, error) {
|
||||
if c.pulledManifest == nil {
|
||||
return nil, nil, errors.New("missing schema 1 manifest for conversion")
|
||||
|
||||
78
vendor/github.com/containerd/containerd/runtime/restart/restart.go
generated
vendored
78
vendor/github.com/containerd/containerd/runtime/restart/restart.go
generated
vendored
@ -1,78 +0,0 @@
|
||||
/*
|
||||
Copyright The containerd Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// Package restart enables containers to have labels added and monitored to
|
||||
// keep the container's task running if it is killed.
|
||||
//
|
||||
// Setting the StatusLabel on a container instructs the restart monitor to keep
|
||||
// that container's task in a specific status.
|
||||
// Setting the LogPathLabel on a container will setup the task's IO to be redirected
|
||||
// to a log file when running a task within the restart manager.
|
||||
//
|
||||
// The restart labels can be cleared off of a container using the WithNoRestarts Opt.
|
||||
//
|
||||
// The restart monitor has one option in the containerd config under the [plugins.restart]
|
||||
// section. `interval = "10s" sets the reconcile interval that the restart monitor checks
|
||||
// for task state and reconciles the desired status for that task.
|
||||
package restart
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/containerd/containerd"
|
||||
"github.com/containerd/containerd/containers"
|
||||
)
|
||||
|
||||
const (
|
||||
// StatusLabel sets the restart status label for a container
|
||||
StatusLabel = "containerd.io/restart.status"
|
||||
// LogPathLabel sets the restart log path label for a container
|
||||
LogPathLabel = "containerd.io/restart.logpath"
|
||||
)
|
||||
|
||||
// WithLogPath sets the log path for a container
|
||||
func WithLogPath(path string) func(context.Context, *containerd.Client, *containers.Container) error {
|
||||
return func(_ context.Context, _ *containerd.Client, c *containers.Container) error {
|
||||
ensureLabels(c)
|
||||
c.Labels[LogPathLabel] = path
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithStatus sets the status for a container
|
||||
func WithStatus(status containerd.ProcessStatus) func(context.Context, *containerd.Client, *containers.Container) error {
|
||||
return func(_ context.Context, _ *containerd.Client, c *containers.Container) error {
|
||||
ensureLabels(c)
|
||||
c.Labels[StatusLabel] = string(status)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithNoRestarts clears any restart information from the container
|
||||
func WithNoRestarts(_ context.Context, _ *containerd.Client, c *containers.Container) error {
|
||||
if c.Labels == nil {
|
||||
return nil
|
||||
}
|
||||
delete(c.Labels, StatusLabel)
|
||||
delete(c.Labels, LogPathLabel)
|
||||
return nil
|
||||
}
|
||||
|
||||
func ensureLabels(c *containers.Container) {
|
||||
if c.Labels == nil {
|
||||
c.Labels = make(map[string]string)
|
||||
}
|
||||
}
|
||||
2
vendor/github.com/containerd/containerd/sys/socket_unix.go
generated
vendored
2
vendor/github.com/containerd/containerd/sys/socket_unix.go
generated
vendored
@ -42,7 +42,7 @@ func CreateUnixSocket(path string) (net.Listener, error) {
|
||||
return net.Listen("unix", path)
|
||||
}
|
||||
|
||||
// GetLocalListener returns a listerner out of a unix socket.
|
||||
// GetLocalListener returns a listener out of a unix socket.
|
||||
func GetLocalListener(path string, uid, gid int) (net.Listener, error) {
|
||||
// Ensure parent directory is created
|
||||
if err := mkdirAs(filepath.Dir(path), uid, gid); err != nil {
|
||||
|
||||
62
vendor/github.com/containerd/containerd/task_opts.go
generated
vendored
62
vendor/github.com/containerd/containerd/task_opts.go
generated
vendored
@ -18,10 +18,18 @@ package containerd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"syscall"
|
||||
|
||||
"github.com/containerd/containerd/api/types"
|
||||
"github.com/containerd/containerd/content"
|
||||
"github.com/containerd/containerd/errdefs"
|
||||
"github.com/containerd/containerd/images"
|
||||
"github.com/containerd/containerd/mount"
|
||||
imagespec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"github.com/opencontainers/runtime-spec/specs-go"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// NewTaskOpts allows the caller to set options on a new task
|
||||
@ -35,6 +43,44 @@ func WithRootFS(mounts []mount.Mount) NewTaskOpts {
|
||||
}
|
||||
}
|
||||
|
||||
// WithTaskCheckpoint allows a task to be created with live runtime and memory data from a
|
||||
// previous checkpoint. Additional software such as CRIU may be required to
|
||||
// restore a task from a checkpoint
|
||||
func WithTaskCheckpoint(im Image) NewTaskOpts {
|
||||
return func(ctx context.Context, c *Client, info *TaskInfo) error {
|
||||
desc := im.Target()
|
||||
id := desc.Digest
|
||||
index, err := decodeIndex(ctx, c.ContentStore(), desc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, m := range index.Manifests {
|
||||
if m.MediaType == images.MediaTypeContainerd1Checkpoint {
|
||||
info.Checkpoint = &types.Descriptor{
|
||||
MediaType: m.MediaType,
|
||||
Size_: m.Size,
|
||||
Digest: m.Digest,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("checkpoint not found in index %s", id)
|
||||
}
|
||||
}
|
||||
|
||||
func decodeIndex(ctx context.Context, store content.Provider, desc imagespec.Descriptor) (*imagespec.Index, error) {
|
||||
var index imagespec.Index
|
||||
p, err := content.ReadBlob(ctx, store, desc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := json.Unmarshal(p, &index); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &index, nil
|
||||
}
|
||||
|
||||
// WithCheckpointName sets the image name for the checkpoint
|
||||
func WithCheckpointName(name string) CheckpointTaskOpts {
|
||||
return func(r *CheckpointTaskInfo) error {
|
||||
@ -92,3 +138,19 @@ func WithKillExecID(execID string) KillOpts {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithResources sets the provided resources for task updates. Resources must be
|
||||
// either a *specs.LinuxResources or a *specs.WindowsResources
|
||||
func WithResources(resources interface{}) UpdateTaskOpts {
|
||||
return func(ctx context.Context, client *Client, r *UpdateTaskInfo) error {
|
||||
switch resources.(type) {
|
||||
case *specs.LinuxResources:
|
||||
case *specs.WindowsResources:
|
||||
default:
|
||||
return errors.New("WithResources requires a *specs.LinuxResources or *specs.WindowsResources")
|
||||
}
|
||||
|
||||
r.Resources = resources
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
// +build !windows
|
||||
|
||||
/*
|
||||
Copyright The containerd Authors.
|
||||
|
||||
@ -18,20 +20,11 @@ package containerd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/containerd/containerd/runtime/linux/runctypes"
|
||||
"github.com/opencontainers/runtime-spec/specs-go"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// WithResources sets the provided resources for task updates
|
||||
func WithResources(resources *specs.LinuxResources) UpdateTaskOpts {
|
||||
return func(ctx context.Context, client *Client, r *UpdateTaskInfo) error {
|
||||
r.Resources = resources
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithNoNewKeyring causes tasks not to be created with a new keyring for secret storage.
|
||||
// There is an upper limit on the number of keyrings in a linux system
|
||||
func WithNoNewKeyring(ctx context.Context, c *Client, ti *TaskInfo) error {
|
||||
@ -46,3 +39,19 @@ func WithNoNewKeyring(ctx context.Context, c *Client, ti *TaskInfo) error {
|
||||
opts.NoNewKeyring = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// WithNoPivotRoot instructs the runtime not to you pivot_root
|
||||
func WithNoPivotRoot(_ context.Context, _ *Client, info *TaskInfo) error {
|
||||
if info.Options == nil {
|
||||
info.Options = &runctypes.CreateOptions{
|
||||
NoPivotRoot: true,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
opts, ok := info.Options.(*runctypes.CreateOptions)
|
||||
if !ok {
|
||||
return errors.New("invalid options type, expected runctypes.CreateOptions")
|
||||
}
|
||||
opts.NoPivotRoot = true
|
||||
return nil
|
||||
}
|
||||
8
vendor/github.com/containerd/containerd/vendor.conf
generated
vendored
8
vendor/github.com/containerd/containerd/vendor.conf
generated
vendored
@ -1,10 +1,10 @@
|
||||
github.com/containerd/go-runc acb7c88cac264acca9b5eae187a117f4d77a1292
|
||||
github.com/containerd/go-runc 5a6d9f37cfa36b15efba46dc7ea349fa9b7143c3
|
||||
github.com/containerd/console c12b1e7919c14469339a5d38f2f8ed9b64a9de23
|
||||
github.com/containerd/cgroups 5e610833b72089b37d0e615de9a92dfc043757c2
|
||||
github.com/containerd/typeurl a93fcdb778cd272c6e9b3028b2f42d813e785d40
|
||||
github.com/containerd/fifo 3d5202aec260678c48179c56f40e6f38a095738c
|
||||
github.com/containerd/btrfs 2e1aa0ddf94f91fa282b6ed87c23bf0d64911244
|
||||
github.com/containerd/continuity d3c23511c1bf5851696cba83143d9cbcd666869b
|
||||
github.com/containerd/continuity f44b615e492bdfb371aae2f76ec694d9da1db537
|
||||
github.com/coreos/go-systemd 48702e0da86bd25e76cfef347e2adeb434a0d0a6
|
||||
github.com/docker/go-metrics 4ea375f7759c82740c893fc030bc37088d2ec098
|
||||
github.com/docker/go-events 9461782956ad83b30282bf90e31fa6a70c255ba9
|
||||
@ -71,7 +71,7 @@ github.com/xeipuuv/gojsonschema 1d523034197ff1f222f6429836dd36a2457a1874
|
||||
golang.org/x/crypto 49796115aa4b964c318aad4f3084fdb41e9aa067
|
||||
golang.org/x/time f51c12702a4d776e4c1fa9b0fabab841babae631
|
||||
gopkg.in/inf.v0 3887ee99ecf07df5b447e9b00d9c0b2adaa9f3e4
|
||||
gopkg.in/yaml.v2 53feefa2559fb8dfa8d81baad31be332c97d6c77
|
||||
gopkg.in/yaml.v2 v2.2.1
|
||||
k8s.io/api 9e5ffd1f1320950b238cfce291b926411f0af722
|
||||
k8s.io/apimachinery ed135c5b96450fd24e5e981c708114fbbd950697
|
||||
k8s.io/apiserver a90e3a95c2e91b944bfca8225c4e0d12e42a9eb5
|
||||
@ -85,4 +85,4 @@ github.com/mistifyio/go-zfs 166add352731e515512690329794ee593f1aaff2
|
||||
github.com/pborman/uuid c65b2f87fee37d1c7854c9164a450713c28d50cd
|
||||
|
||||
# aufs dependencies
|
||||
github.com/containerd/aufs a7fbd554da7a9eafbe5a460a421313a9fd18d988
|
||||
github.com/containerd/aufs ffa39970e26ad01d81f540b21e65f9c1841a5f92
|
||||
|
||||
6
vendor/github.com/docker/licensing/client.go
generated
vendored
6
vendor/github.com/docker/licensing/client.go
generated
vendored
@ -1,6 +1,7 @@
|
||||
package licensing
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
@ -34,6 +35,7 @@ type Client interface {
|
||||
ParseLicense(license []byte) (parsedLicense *model.IssuedLicense, err error)
|
||||
StoreLicense(ctx context.Context, dclnt WrappedDockerClient, licenses *model.IssuedLicense, localRootDir string) error
|
||||
LoadLocalLicense(ctx context.Context, dclnt WrappedDockerClient) (*model.Subscription, error)
|
||||
SummarizeLicense(res *model.CheckResponse, keyID string) *model.Subscription
|
||||
}
|
||||
|
||||
func (c *client) LoginViaAuth(ctx context.Context, username, password string) (string, error) {
|
||||
@ -185,6 +187,10 @@ func (c *client) DownloadLicenseFromHub(ctx context.Context, authToken, subscrip
|
||||
|
||||
func (c *client) ParseLicense(license []byte) (*model.IssuedLicense, error) {
|
||||
parsedLicense := &model.IssuedLicense{}
|
||||
// The file may contain a leading BOM, which will choke the
|
||||
// json deserializer.
|
||||
license = bytes.Trim(license, "\xef\xbb\xbf")
|
||||
|
||||
if err := json.Unmarshal(license, &parsedLicense); err != nil {
|
||||
return nil, errors.WithMessage(err, "failed to parse license")
|
||||
}
|
||||
|
||||
2
vendor/github.com/docker/licensing/model/subscriptions.go
generated
vendored
2
vendor/github.com/docker/licensing/model/subscriptions.go
generated
vendored
@ -34,7 +34,7 @@ type Subscription struct {
|
||||
}
|
||||
|
||||
func (s *Subscription) String() string {
|
||||
storeURL := "https://store.docker.com"
|
||||
storeURL := "https://docker.com/licensing"
|
||||
|
||||
var nameMsg, expirationMsg, statusMsg string
|
||||
switch s.State {
|
||||
|
||||
27
vendor/github.com/docker/licensing/storage.go
generated
vendored
27
vendor/github.com/docker/licensing/storage.go
generated
vendored
@ -87,18 +87,23 @@ func (c *client) LoadLocalLicense(ctx context.Context, clnt WrappedDockerClient)
|
||||
licenseData, err = readLicenseFromHost(ctx, info.DockerRootDir)
|
||||
} else {
|
||||
// Load the latest license index
|
||||
latestVersion, err := getLatestNamedConfig(clnt, licenseNamePrefix)
|
||||
var latestVersion int
|
||||
latestVersion, err = getLatestNamedConfig(clnt, licenseNamePrefix)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "not a swarm manager.") {
|
||||
return nil, ErrWorkerNode
|
||||
}
|
||||
return nil, fmt.Errorf("unable to get latest license version: %s", err)
|
||||
}
|
||||
cfg, _, err := clnt.ConfigInspectWithRaw(ctx, fmt.Sprintf("%s-%d", licenseNamePrefix, latestVersion))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to load license from swarm config: %s", err)
|
||||
if latestVersion >= 0 {
|
||||
cfg, _, err := clnt.ConfigInspectWithRaw(ctx, fmt.Sprintf("%s-%d", licenseNamePrefix, latestVersion))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to load license from swarm config: %s", err)
|
||||
}
|
||||
licenseData = cfg.Spec.Data
|
||||
} else {
|
||||
licenseData, err = readLicenseFromHost(ctx, info.DockerRootDir)
|
||||
}
|
||||
licenseData = cfg.Spec.Data
|
||||
}
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
@ -115,6 +120,10 @@ func (c *client) LoadLocalLicense(ctx context.Context, clnt WrappedDockerClient)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return checkResponseToSubscription(checkResponse, parsedLicense.KeyID), nil
|
||||
}
|
||||
|
||||
func checkResponseToSubscription(checkResponse *model.CheckResponse, keyID string) *model.Subscription {
|
||||
|
||||
// TODO - this translation still needs some work
|
||||
// Primary missing piece is how to distinguish from basic, vs std/advanced
|
||||
@ -144,7 +153,7 @@ func (c *client) LoadLocalLicense(ctx context.Context, clnt WrappedDockerClient)
|
||||
// Translate the legacy structure into the new Subscription fields
|
||||
return &model.Subscription{
|
||||
// Name
|
||||
ID: parsedLicense.KeyID, // This is not actually the same, but is unique
|
||||
ID: keyID, // This is not actually the same, but is unique
|
||||
// DockerID
|
||||
ProductID: productID,
|
||||
ProductRatePlan: ratePlan,
|
||||
@ -159,7 +168,11 @@ func (c *client) LoadLocalLicense(ctx context.Context, clnt WrappedDockerClient)
|
||||
Value: checkResponse.MaxEngines,
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (c *client) SummarizeLicense(checkResponse *model.CheckResponse, keyID string) *model.Subscription {
|
||||
return checkResponseToSubscription(checkResponse, keyID)
|
||||
}
|
||||
|
||||
// getLatestNamedConfig looks for versioned instances of configs with the
|
||||
|
||||
Reference in New Issue
Block a user