mirror of https://github.com/docker/cli.git
Merge pull request #1385 from dhiltgen/revamp_master
Forward port #1381 to master
This commit is contained in:
commit
7d313cf865
4
Makefile
4
Makefile
|
@ -12,14 +12,14 @@ clean: ## remove build artifacts
|
||||||
|
|
||||||
.PHONY: test-unit
|
.PHONY: test-unit
|
||||||
test-unit: ## run unit test
|
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
|
.PHONY: test
|
||||||
test: test-unit ## run tests
|
test: test-unit ## run tests
|
||||||
|
|
||||||
.PHONY: test-coverage
|
.PHONY: test-coverage
|
||||||
test-coverage: ## run 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
|
.PHONY: lint
|
||||||
lint: ## run all the lint tools
|
lint: ## run all the lint tools
|
||||||
|
|
|
@ -56,7 +56,7 @@ https://hub.docker.com/ then specify the file with the '--license' flag.
|
||||||
|
|
||||||
flags.StringVar(&options.licenseFile, "license", "", "License File")
|
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.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.registryPrefix, "registry-prefix", clitypes.RegistryPrefix, "Override the default location where engine images are pulled")
|
||||||
flags.StringVar(&options.image, "engine-image", clitypes.EnterpriseEngineImage, "Specify engine image")
|
flags.StringVar(&options.image, "engine-image", clitypes.EnterpriseEngineImage, "Specify engine image")
|
||||||
flags.StringVar(&options.format, "format", "", "Pretty-print licenses using a Go template")
|
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 the available licenses and exit")
|
||||||
|
@ -67,6 +67,9 @@ https://hub.docker.com/ then specify the file with the '--license' flag.
|
||||||
}
|
}
|
||||||
|
|
||||||
func runActivate(cli command.Cli, options activateOptions) error {
|
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()
|
ctx := context.Background()
|
||||||
client, err := cli.NewContainerizedEngineClient(options.sockPath)
|
client, err := cli.NewContainerizedEngineClient(options.sockPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -104,12 +107,17 @@ func runActivate(cli command.Cli, options activateOptions) error {
|
||||||
EngineVersion: options.version,
|
EngineVersion: options.version,
|
||||||
}
|
}
|
||||||
|
|
||||||
return client.ActivateEngine(ctx, opts, cli.Out(), authConfig,
|
if err := client.ActivateEngine(ctx, opts, cli.Out(), authConfig,
|
||||||
func(ctx context.Context) error {
|
func(ctx context.Context) error {
|
||||||
client := cli.Client()
|
client := cli.Client()
|
||||||
_, err := client.Ping(ctx)
|
_, err := client.Ping(ctx)
|
||||||
return err
|
return err
|
||||||
})
|
}); 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) {
|
func getLicenses(ctx context.Context, authConfig *types.AuthConfig, cli command.Cli, options activateOptions) (*model.IssuedLicense, error) {
|
||||||
|
|
|
@ -14,6 +14,7 @@ func TestActivateNoContainerd(t *testing.T) {
|
||||||
return nil, fmt.Errorf("some error")
|
return nil, fmt.Errorf("some error")
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
isRoot = func() bool { return true }
|
||||||
cmd := newActivateCommand(testCli)
|
cmd := newActivateCommand(testCli)
|
||||||
cmd.Flags().Set("license", "invalidpath")
|
cmd.Flags().Set("license", "invalidpath")
|
||||||
cmd.SilenceUsage = true
|
cmd.SilenceUsage = true
|
||||||
|
@ -28,6 +29,7 @@ func TestActivateBadLicense(t *testing.T) {
|
||||||
return &fakeContainerizedEngineClient{}, nil
|
return &fakeContainerizedEngineClient{}, nil
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
isRoot = func() bool { return true }
|
||||||
cmd := newActivateCommand(testCli)
|
cmd := newActivateCommand(testCli)
|
||||||
cmd.SilenceUsage = true
|
cmd.SilenceUsage = true
|
||||||
cmd.SilenceErrors = true
|
cmd.SilenceErrors = true
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
// +build !windows
|
||||||
|
|
||||||
|
package engine
|
||||||
|
|
||||||
|
import (
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
isRoot = func() bool {
|
||||||
|
return unix.Geteuid() == 0
|
||||||
|
}
|
||||||
|
)
|
|
@ -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/command"
|
||||||
"github.com/docker/cli/cli/trust"
|
"github.com/docker/cli/cli/trust"
|
||||||
|
clitypes "github.com/docker/cli/types"
|
||||||
"github.com/docker/distribution/reference"
|
"github.com/docker/distribution/reference"
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
registrytypes "github.com/docker/docker/api/types/registry"
|
registrytypes "github.com/docker/docker/api/types/registry"
|
||||||
|
@ -13,7 +14,7 @@ import (
|
||||||
|
|
||||||
func getRegistryAuth(cli command.Cli, registryPrefix string) (*types.AuthConfig, error) {
|
func getRegistryAuth(cli command.Cli, registryPrefix string) (*types.AuthConfig, error) {
|
||||||
if registryPrefix == "" {
|
if registryPrefix == "" {
|
||||||
registryPrefix = "docker.io/docker"
|
registryPrefix = clitypes.RegistryPrefix
|
||||||
}
|
}
|
||||||
distributionRef, err := reference.ParseNormalizedNamed(registryPrefix)
|
distributionRef, err := reference.ParseNormalizedNamed(registryPrefix)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -7,15 +7,12 @@ import (
|
||||||
"github.com/docker/cli/cli"
|
"github.com/docker/cli/cli"
|
||||||
"github.com/docker/cli/cli/command"
|
"github.com/docker/cli/cli/command"
|
||||||
"github.com/docker/cli/cli/command/formatter"
|
"github.com/docker/cli/cli/command/formatter"
|
||||||
|
"github.com/docker/cli/internal/versions"
|
||||||
clitypes "github.com/docker/cli/types"
|
clitypes "github.com/docker/cli/types"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
releaseNotePrefix = "https://docs.docker.com/releasenotes"
|
|
||||||
)
|
|
||||||
|
|
||||||
type checkOptions struct {
|
type checkOptions struct {
|
||||||
registryPrefix string
|
registryPrefix string
|
||||||
preReleases bool
|
preReleases bool
|
||||||
|
@ -38,7 +35,7 @@ func newCheckForUpdatesCommand(dockerCli command.Cli) *cobra.Command {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
flags := cmd.Flags()
|
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.downgrades, "downgrades", false, "Report downgrades (default omits older versions)")
|
||||||
flags.BoolVar(&options.preReleases, "pre-releases", false, "Include pre-release versions")
|
flags.BoolVar(&options.preReleases, "pre-releases", false, "Include pre-release versions")
|
||||||
flags.BoolVar(&options.upgrades, "upgrades", true, "Report available upgrades")
|
flags.BoolVar(&options.upgrades, "upgrades", true, "Report available upgrades")
|
||||||
|
@ -50,54 +47,47 @@ func newCheckForUpdatesCommand(dockerCli command.Cli) *cobra.Command {
|
||||||
}
|
}
|
||||||
|
|
||||||
func runCheck(dockerCli command.Cli, options checkOptions) error {
|
func runCheck(dockerCli command.Cli, options checkOptions) error {
|
||||||
ctx := context.Background()
|
if !isRoot() {
|
||||||
client, err := dockerCli.NewContainerizedEngineClient(options.sockPath)
|
return errors.New("this command must be run as a privileged user")
|
||||||
if err != nil {
|
|
||||||
return errors.Wrap(err, "unable to access local containerd")
|
|
||||||
}
|
}
|
||||||
defer client.Close()
|
ctx := context.Background()
|
||||||
currentOpts, err := client.GetCurrentEngineVersion(ctx)
|
client := dockerCli.Client()
|
||||||
|
serverVersion, err := client.ServerVersion(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// override with user provided prefix if specified
|
availVersions, err := versions.GetEngineVersions(ctx, dockerCli.RegistryClient(false), options.registryPrefix, serverVersion)
|
||||||
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)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
availUpdates := []clitypes.Update{
|
availUpdates := []clitypes.Update{
|
||||||
{Type: "current", Version: currentVersion},
|
{Type: "current", Version: serverVersion.Version},
|
||||||
}
|
}
|
||||||
if len(versions.Patches) > 0 {
|
if len(availVersions.Patches) > 0 {
|
||||||
availUpdates = append(availUpdates,
|
availUpdates = append(availUpdates,
|
||||||
processVersions(
|
processVersions(
|
||||||
currentVersion,
|
serverVersion.Version,
|
||||||
"patch",
|
"patch",
|
||||||
options.preReleases,
|
options.preReleases,
|
||||||
versions.Patches)...)
|
availVersions.Patches)...)
|
||||||
}
|
}
|
||||||
if options.upgrades {
|
if options.upgrades {
|
||||||
availUpdates = append(availUpdates,
|
availUpdates = append(availUpdates,
|
||||||
processVersions(
|
processVersions(
|
||||||
currentVersion,
|
serverVersion.Version,
|
||||||
"upgrade",
|
"upgrade",
|
||||||
options.preReleases,
|
options.preReleases,
|
||||||
versions.Upgrades)...)
|
availVersions.Upgrades)...)
|
||||||
}
|
}
|
||||||
if options.downgrades {
|
if options.downgrades {
|
||||||
availUpdates = append(availUpdates,
|
availUpdates = append(availUpdates,
|
||||||
processVersions(
|
processVersions(
|
||||||
currentVersion,
|
serverVersion.Version,
|
||||||
"downgrade",
|
"downgrade",
|
||||||
options.preReleases,
|
options.preReleases,
|
||||||
versions.Downgrades)...)
|
availVersions.Downgrades)...)
|
||||||
}
|
}
|
||||||
|
|
||||||
format := options.format
|
format := options.format
|
||||||
|
@ -115,9 +105,9 @@ func runCheck(dockerCli command.Cli, options checkOptions) error {
|
||||||
|
|
||||||
func processVersions(currentVersion, verType string,
|
func processVersions(currentVersion, verType string,
|
||||||
includePrerelease bool,
|
includePrerelease bool,
|
||||||
versions []clitypes.DockerVersion) []clitypes.Update {
|
availVersions []clitypes.DockerVersion) []clitypes.Update {
|
||||||
availUpdates := []clitypes.Update{}
|
availUpdates := []clitypes.Update{}
|
||||||
for _, ver := range versions {
|
for _, ver := range availVersions {
|
||||||
if !includePrerelease && ver.Prerelease() != "" {
|
if !includePrerelease && ver.Prerelease() != "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
@ -125,7 +115,7 @@ func processVersions(currentVersion, verType string,
|
||||||
availUpdates = append(availUpdates, clitypes.Update{
|
availUpdates = append(availUpdates, clitypes.Update{
|
||||||
Type: verType,
|
Type: verType,
|
||||||
Version: ver.Tag,
|
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"
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
registryclient "github.com/docker/cli/cli/registry/client"
|
manifesttypes "github.com/docker/cli/cli/manifest/types"
|
||||||
"github.com/docker/cli/internal/test"
|
"github.com/docker/cli/internal/test"
|
||||||
clitypes "github.com/docker/cli/types"
|
"github.com/docker/distribution"
|
||||||
|
"github.com/docker/distribution/reference"
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
"github.com/docker/docker/client"
|
"github.com/docker/docker/client"
|
||||||
ver "github.com/hashicorp/go-version"
|
"github.com/opencontainers/go-digest"
|
||||||
"gotest.tools/assert"
|
"gotest.tools/assert"
|
||||||
"gotest.tools/golden"
|
"gotest.tools/golden"
|
||||||
)
|
)
|
||||||
|
@ -18,126 +20,87 @@ var (
|
||||||
testCli = test.NewFakeCli(&client.Client{})
|
testCli = test.NewFakeCli(&client.Client{})
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestCheckForUpdatesNoContainerd(t *testing.T) {
|
type verClient struct {
|
||||||
testCli.SetContainerizedEngineClient(
|
client.Client
|
||||||
func(string) (clitypes.ContainerizedClient, error) {
|
ver types.Version
|
||||||
return nil, fmt.Errorf("some error")
|
verErr error
|
||||||
},
|
}
|
||||||
)
|
|
||||||
cmd := newCheckForUpdatesCommand(testCli)
|
func (c *verClient) ServerVersion(ctx context.Context) (types.Version, error) {
|
||||||
cmd.SilenceUsage = true
|
return c.ver, c.verErr
|
||||||
cmd.SilenceErrors = true
|
}
|
||||||
err := cmd.Execute()
|
|
||||||
assert.ErrorContains(t, err, "unable to access local containerd")
|
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) {
|
func TestCheckForUpdatesNoCurrentVersion(t *testing.T) {
|
||||||
retErr := fmt.Errorf("some failure")
|
isRoot = func() bool { return true }
|
||||||
getCurrentEngineVersionFunc := func(ctx context.Context) (clitypes.EngineInitOptions, error) {
|
c := test.NewFakeCli(&verClient{client.Client{}, types.Version{}, nil})
|
||||||
return clitypes.EngineInitOptions{}, retErr
|
c.SetRegistryClient(testRegistryClient{})
|
||||||
}
|
cmd := newCheckForUpdatesCommand(c)
|
||||||
testCli.SetContainerizedEngineClient(
|
|
||||||
func(string) (clitypes.ContainerizedClient, error) {
|
|
||||||
return &fakeContainerizedEngineClient{
|
|
||||||
getCurrentEngineVersionFunc: getCurrentEngineVersionFunc,
|
|
||||||
}, nil
|
|
||||||
},
|
|
||||||
)
|
|
||||||
cmd := newCheckForUpdatesCommand(testCli)
|
|
||||||
cmd.SilenceUsage = true
|
cmd.SilenceUsage = true
|
||||||
cmd.SilenceErrors = true
|
cmd.SilenceErrors = true
|
||||||
err := cmd.Execute()
|
err := cmd.Execute()
|
||||||
assert.Assert(t, err == retErr)
|
assert.ErrorContains(t, err, "alformed version")
|
||||||
}
|
|
||||||
|
|
||||||
func TestCheckForUpdatesGetEngineVersionsFail(t *testing.T) {
|
|
||||||
retErr := fmt.Errorf("some failure")
|
|
||||||
getEngineVersionsFunc := func(ctx context.Context,
|
|
||||||
registryClient registryclient.RegistryClient,
|
|
||||||
currentVersion, imageName string) (clitypes.AvailableVersions, error) {
|
|
||||||
return clitypes.AvailableVersions{}, retErr
|
|
||||||
}
|
|
||||||
testCli.SetContainerizedEngineClient(
|
|
||||||
func(string) (clitypes.ContainerizedClient, error) {
|
|
||||||
return &fakeContainerizedEngineClient{
|
|
||||||
getEngineVersionsFunc: getEngineVersionsFunc,
|
|
||||||
}, nil
|
|
||||||
},
|
|
||||||
)
|
|
||||||
cmd := newCheckForUpdatesCommand(testCli)
|
|
||||||
cmd.SilenceUsage = true
|
|
||||||
cmd.SilenceErrors = true
|
|
||||||
err := cmd.Execute()
|
|
||||||
assert.Assert(t, err == retErr)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCheckForUpdatesGetEngineVersionsHappy(t *testing.T) {
|
func TestCheckForUpdatesGetEngineVersionsHappy(t *testing.T) {
|
||||||
getCurrentEngineVersionFunc := func(ctx context.Context) (clitypes.EngineInitOptions, error) {
|
c := test.NewFakeCli(&verClient{client.Client{}, types.Version{Version: "1.1.0"}, nil})
|
||||||
return clitypes.EngineInitOptions{
|
c.SetRegistryClient(testRegistryClient{[]string{
|
||||||
EngineImage: "current engine",
|
"1.0.1", "1.0.2", "1.0.3-beta1",
|
||||||
EngineVersion: "1.1.0",
|
"1.1.1", "1.1.2", "1.1.3-beta1",
|
||||||
}, nil
|
"1.2.0", "2.0.0", "2.1.0-beta1",
|
||||||
}
|
}})
|
||||||
getEngineVersionsFunc := func(ctx context.Context,
|
isRoot = func() bool { return true }
|
||||||
registryClient registryclient.RegistryClient,
|
cmd := newCheckForUpdatesCommand(c)
|
||||||
currentVersion, imageName string) (clitypes.AvailableVersions, error) {
|
|
||||||
return clitypes.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) (clitypes.ContainerizedClient, error) {
|
|
||||||
return &fakeContainerizedEngineClient{
|
|
||||||
getEngineVersionsFunc: getEngineVersionsFunc,
|
|
||||||
getCurrentEngineVersionFunc: getCurrentEngineVersionFunc,
|
|
||||||
}, nil
|
|
||||||
},
|
|
||||||
)
|
|
||||||
cmd := newCheckForUpdatesCommand(testCli)
|
|
||||||
cmd.Flags().Set("pre-releases", "true")
|
cmd.Flags().Set("pre-releases", "true")
|
||||||
cmd.Flags().Set("downgrades", "true")
|
cmd.Flags().Set("downgrades", "true")
|
||||||
|
cmd.SilenceUsage = true
|
||||||
|
cmd.SilenceErrors = true
|
||||||
err := cmd.Execute()
|
err := cmd.Execute()
|
||||||
assert.NilError(t, err)
|
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("pre-releases", "false")
|
||||||
cmd.Flags().Set("downgrades", "true")
|
cmd.Flags().Set("downgrades", "true")
|
||||||
err = cmd.Execute()
|
err = cmd.Execute()
|
||||||
assert.NilError(t, err)
|
assert.NilError(t, err)
|
||||||
fmt.Println(testCli.OutBuffer().String())
|
fmt.Println(c.OutBuffer().String())
|
||||||
golden.Assert(t, testCli.OutBuffer().String(), "check-no-prerelease.golden")
|
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("pre-releases", "false")
|
||||||
cmd.Flags().Set("downgrades", "false")
|
cmd.Flags().Set("downgrades", "false")
|
||||||
err = cmd.Execute()
|
err = cmd.Execute()
|
||||||
assert.NilError(t, err)
|
assert.NilError(t, err)
|
||||||
fmt.Println(testCli.OutBuffer().String())
|
fmt.Println(c.OutBuffer().String())
|
||||||
golden.Assert(t, testCli.OutBuffer().String(), "check-no-downgrades.golden")
|
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("pre-releases", "false")
|
||||||
cmd.Flags().Set("downgrades", "false")
|
cmd.Flags().Set("downgrades", "false")
|
||||||
cmd.Flags().Set("upgrades", "false")
|
cmd.Flags().Set("upgrades", "false")
|
||||||
err = cmd.Execute()
|
err = cmd.Execute()
|
||||||
assert.NilError(t, err)
|
assert.NilError(t, err)
|
||||||
fmt.Println(testCli.OutBuffer().String())
|
fmt.Println(c.OutBuffer().String())
|
||||||
golden.Assert(t, testCli.OutBuffer().String(), "check-patches-only.golden")
|
golden.Assert(t, c.OutBuffer().String(), "check-patches-only.golden")
|
||||||
}
|
|
||||||
|
|
||||||
func makeVersion(t *testing.T, tag string) clitypes.DockerVersion {
|
|
||||||
v, err := ver.NewVersion(tag)
|
|
||||||
assert.NilError(t, err)
|
|
||||||
return clitypes.DockerVersion{Version: *v, Tag: tag}
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseVersions(t *testing.T, tags ...string) []clitypes.DockerVersion {
|
|
||||||
ret := make([]clitypes.DockerVersion, len(tags))
|
|
||||||
for i, tag := range tags {
|
|
||||||
ret[i] = makeVersion(t, tag)
|
|
||||||
}
|
|
||||||
return ret
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,11 +15,9 @@ func NewEngineCommand(dockerCli command.Cli) *cobra.Command {
|
||||||
RunE: command.ShowHelp(dockerCli.Err()),
|
RunE: command.ShowHelp(dockerCli.Err()),
|
||||||
}
|
}
|
||||||
cmd.AddCommand(
|
cmd.AddCommand(
|
||||||
newInitCommand(dockerCli),
|
|
||||||
newActivateCommand(dockerCli),
|
newActivateCommand(dockerCli),
|
||||||
newCheckForUpdatesCommand(dockerCli),
|
newCheckForUpdatesCommand(dockerCli),
|
||||||
newUpdateCommand(dockerCli),
|
newUpdateCommand(dockerCli),
|
||||||
newRmCommand(dockerCli),
|
|
||||||
)
|
)
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,5 +10,5 @@ func TestNewEngineCommand(t *testing.T) {
|
||||||
cmd := NewEngineCommand(testCli)
|
cmd := NewEngineCommand(testCli)
|
||||||
|
|
||||||
subcommands := cmd.Commands()
|
subcommands := cmd.Commands()
|
||||||
assert.Assert(t, len(subcommands) == 5)
|
assert.Assert(t, len(subcommands) == 3)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,62 +1,10 @@
|
||||||
package engine
|
package engine
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/docker/cli/cli"
|
|
||||||
"github.com/docker/cli/cli/command"
|
|
||||||
clitypes "github.com/docker/cli/types"
|
clitypes "github.com/docker/cli/types"
|
||||||
"github.com/pkg/errors"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type extendedEngineInitOptions struct {
|
type extendedEngineInitOptions struct {
|
||||||
clitypes.EngineInitOptions
|
clitypes.EngineInitOptions
|
||||||
sockPath string
|
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", clitypes.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"
|
|
||||||
|
|
||||||
clitypes "github.com/docker/cli/types"
|
|
||||||
"gotest.tools/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestInitNoContainerd(t *testing.T) {
|
|
||||||
testCli.SetContainerizedEngineClient(
|
|
||||||
func(string) (clitypes.ContainerizedClient, 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) (clitypes.ContainerizedClient, error) {
|
|
||||||
return &fakeContainerizedEngineClient{}, nil
|
|
||||||
},
|
|
||||||
)
|
|
||||||
cmd := newInitCommand(testCli)
|
|
||||||
err := cmd.Execute()
|
|
||||||
assert.NilError(t, err)
|
|
||||||
}
|
|
|
@ -1,49 +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()
|
|
||||||
|
|
||||||
return client.RemoveEngine(ctx)
|
|
||||||
}
|
|
|
@ -1,33 +0,0 @@
|
||||||
package engine
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
clitypes "github.com/docker/cli/types"
|
|
||||||
"gotest.tools/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestRmNoContainerd(t *testing.T) {
|
|
||||||
testCli.SetContainerizedEngineClient(
|
|
||||||
func(string) (clitypes.ContainerizedClient, 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) (clitypes.ContainerizedClient, error) {
|
|
||||||
return &fakeContainerizedEngineClient{}, nil
|
|
||||||
},
|
|
||||||
)
|
|
||||||
cmd := newRmCommand(testCli)
|
|
||||||
err := cmd.Execute()
|
|
||||||
assert.NilError(t, err)
|
|
||||||
}
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
|
|
||||||
"github.com/docker/cli/cli"
|
"github.com/docker/cli/cli"
|
||||||
"github.com/docker/cli/cli/command"
|
"github.com/docker/cli/cli/command"
|
||||||
|
clitypes "github.com/docker/cli/types"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
@ -25,31 +26,22 @@ func newUpdateCommand(dockerCli command.Cli) *cobra.Command {
|
||||||
|
|
||||||
flags.StringVar(&options.EngineVersion, "version", "", "Specify engine version")
|
flags.StringVar(&options.EngineVersion, "version", "", "Specify engine version")
|
||||||
flags.StringVar(&options.EngineImage, "engine-image", "", "Specify engine image")
|
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.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")
|
flags.StringVar(&options.sockPath, "containerd", "", "override default location of containerd endpoint")
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
func runUpdate(dockerCli command.Cli, options extendedEngineInitOptions) error {
|
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()
|
ctx := context.Background()
|
||||||
client, err := dockerCli.NewContainerizedEngineClient(options.sockPath)
|
client, err := dockerCli.NewContainerizedEngineClient(options.sockPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "unable to access local containerd")
|
return errors.Wrap(err, "unable to access local containerd")
|
||||||
}
|
}
|
||||||
defer client.Close()
|
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)
|
authConfig, err := getRegistryAuth(dockerCli, options.RegistryPrefix)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -63,6 +55,7 @@ func runUpdate(dockerCli command.Cli, options extendedEngineInitOptions) error {
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return err
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,7 +28,7 @@ func TestUpdateHappy(t *testing.T) {
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
cmd := newUpdateCommand(testCli)
|
cmd := newUpdateCommand(testCli)
|
||||||
cmd.Flags().Set("registry-prefix", "docker.io/docker")
|
cmd.Flags().Set("registry-prefix", clitypes.RegistryPrefix)
|
||||||
cmd.Flags().Set("version", "someversion")
|
cmd.Flags().Set("version", "someversion")
|
||||||
err := cmd.Execute()
|
err := cmd.Execute()
|
||||||
assert.NilError(t, err)
|
assert.NilError(t, err)
|
||||||
|
|
|
@ -105,7 +105,7 @@ shellcheck: build_shell_validate_image ## run shellcheck validation
|
||||||
docker run -ti --rm $(ENVVARS) $(MOUNTS) $(VALIDATE_IMAGE_NAME) make shellcheck
|
docker run -ti --rm $(ENVVARS) $(MOUNTS) $(VALIDATE_IMAGE_NAME) make shellcheck
|
||||||
|
|
||||||
.PHONY: test-e2e ## run e2e tests
|
.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
|
.PHONY: test-e2e-experimental
|
||||||
test-e2e-experimental: build_e2e_image
|
test-e2e-experimental: build_e2e_image
|
||||||
|
@ -115,14 +115,6 @@ test-e2e-experimental: build_e2e_image
|
||||||
test-e2e-non-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)
|
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
|
.PHONY: help
|
||||||
help: ## print this 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)
|
@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 \
|
iptables \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& 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
|
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 \
|
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
|
&& chmod +x /usr/local/bin/docker-compose
|
||||||
|
|
|
@ -1,43 +0,0 @@
|
||||||
package check
|
|
||||||
|
|
||||||
import (
|
|
||||||
"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)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Use a fixed version to prevent failures when development of the next version starts, and no image is available yet.
|
|
||||||
targetVersion := "18.09.0-dev"
|
|
||||||
|
|
||||||
t.Log("First engine init")
|
|
||||||
// First init
|
|
||||||
result := icmd.RunCmd(icmd.Command("docker", "engine", "init", "--config-file", "/tmp/etc/docker/daemon.json", "--version", targetVersion),
|
|
||||||
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
|
|
||||||
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,86 +0,0 @@
|
||||||
package multi
|
|
||||||
|
|
||||||
import (
|
|
||||||
"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)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Use a fixed version to prevent failures when development of the next version starts, and no image is available yet.
|
|
||||||
targetVersion := "18.09.0-dev"
|
|
||||||
|
|
||||||
t.Log("Attempt engine init without experimental")
|
|
||||||
// First init
|
|
||||||
result := icmd.RunCmd(icmd.Command("docker", "engine", "init", "--version", targetVersion),
|
|
||||||
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", "--version", targetVersion),
|
|
||||||
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
|
|
||||||
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,46 +0,0 @@
|
||||||
package e2eengine
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/containerd/containerd"
|
|
||||||
"github.com/docker/cli/internal/containerizedengine"
|
|
||||||
"github.com/docker/cli/types"
|
|
||||||
)
|
|
||||||
|
|
||||||
type containerizedclient interface {
|
|
||||||
types.ContainerizedClient
|
|
||||||
GetEngine(context.Context) (containerd.Container, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
_, err = client.(containerizedclient).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)
|
|
||||||
if err != nil {
|
|
||||||
t.Logf("Failed to remove engine: %s", err)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
|
@ -2,10 +2,8 @@ package containerizedengine
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"syscall"
|
|
||||||
|
|
||||||
"github.com/containerd/containerd"
|
"github.com/containerd/containerd"
|
||||||
containerdtypes "github.com/containerd/containerd/api/types"
|
|
||||||
"github.com/containerd/containerd/cio"
|
"github.com/containerd/containerd/cio"
|
||||||
"github.com/containerd/containerd/containers"
|
"github.com/containerd/containerd/containers"
|
||||||
"github.com/containerd/containerd/content"
|
"github.com/containerd/containerd/content"
|
||||||
|
@ -13,7 +11,6 @@ import (
|
||||||
prototypes "github.com/gogo/protobuf/types"
|
prototypes "github.com/gogo/protobuf/types"
|
||||||
"github.com/opencontainers/go-digest"
|
"github.com/opencontainers/go-digest"
|
||||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||||
"github.com/opencontainers/runtime-spec/specs-go"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
|
@ -24,6 +21,8 @@ type (
|
||||||
getImageFunc func(ctx context.Context, ref string) (containerd.Image, error)
|
getImageFunc func(ctx context.Context, ref string) (containerd.Image, error)
|
||||||
contentStoreFunc func() content.Store
|
contentStoreFunc func() content.Store
|
||||||
containerServiceFunc func() containers.Store
|
containerServiceFunc func() containers.Store
|
||||||
|
installFunc func(context.Context, containerd.Image, ...containerd.InstallOpts) error
|
||||||
|
versionFunc func(ctx context.Context) (containerd.Version, error)
|
||||||
}
|
}
|
||||||
fakeContainer struct {
|
fakeContainer struct {
|
||||||
idFunc func() string
|
idFunc func() string
|
||||||
|
@ -48,26 +47,6 @@ type (
|
||||||
isUnpackedFunc func(context.Context, string) (bool, error)
|
isUnpackedFunc func(context.Context, string) (bool, error)
|
||||||
contentStoreFunc func() content.Store
|
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)
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (w *fakeContainerdClient) Containers(ctx context.Context, filters ...string) ([]containerd.Container, error) {
|
func (w *fakeContainerdClient) Containers(ctx context.Context, filters ...string) ([]containerd.Container, error) {
|
||||||
|
@ -109,6 +88,18 @@ func (w *fakeContainerdClient) ContainerService() containers.Store {
|
||||||
func (w *fakeContainerdClient) Close() error {
|
func (w *fakeContainerdClient) Close() error {
|
||||||
return nil
|
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 {
|
func (c *fakeContainer) ID() string {
|
||||||
if c.idFunc != nil {
|
if c.idFunc != nil {
|
||||||
|
@ -225,112 +216,3 @@ func (i *fakeImage) ContentStore() content.Store {
|
||||||
}
|
}
|
||||||
return nil
|
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
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,242 +0,0 @@
|
||||||
package containerizedengine
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"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"
|
|
||||||
clitypes "github.com/docker/cli/types"
|
|
||||||
"github.com/docker/docker/api/types"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
var _ clitypes.ContainerizedClient = &baseClient{}
|
|
||||||
|
|
||||||
// InitEngine is the main entrypoint for `docker engine init`
|
|
||||||
func (c *baseClient) InitEngine(ctx context.Context, opts clitypes.EngineInitOptions, out clitypes.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
|
|
||||||
}
|
|
||||||
|
|
||||||
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) error {
|
|
||||||
engine, err := c.GetEngine(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return c.removeEngine(ctx, 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,570 +0,0 @@
|
||||||
package containerizedengine
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
"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/cli/cli/command"
|
|
||||||
clitypes "github.com/docker/cli/types"
|
|
||||||
"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 := clitypes.EngineInitOptions{
|
|
||||||
EngineVersion: "engineversiongoeshere",
|
|
||||||
RegistryPrefix: "registryprefixgoeshere",
|
|
||||||
ConfigFile: "/tmp/configfilegoeshere",
|
|
||||||
EngineImage: clitypes.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, command.NewOutStream(&bytes.Buffer{}), &types.AuthConfig{}, healthfnHappy)
|
|
||||||
assert.Assert(t, err == ErrEngineAlreadyPresent)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestInitCheckImageFail(t *testing.T) {
|
|
||||||
ctx := context.Background()
|
|
||||||
opts := clitypes.EngineInitOptions{
|
|
||||||
EngineVersion: "engineversiongoeshere",
|
|
||||||
RegistryPrefix: "registryprefixgoeshere",
|
|
||||||
ConfigFile: "/tmp/configfilegoeshere",
|
|
||||||
EngineImage: clitypes.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, command.NewOutStream(&bytes.Buffer{}), &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 := clitypes.EngineInitOptions{
|
|
||||||
EngineVersion: "engineversiongoeshere",
|
|
||||||
RegistryPrefix: "registryprefixgoeshere",
|
|
||||||
ConfigFile: "/tmp/configfilegoeshere",
|
|
||||||
EngineImage: clitypes.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, command.NewOutStream(&bytes.Buffer{}), &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 := clitypes.EngineInitOptions{
|
|
||||||
EngineVersion: "engineversiongoeshere",
|
|
||||||
RegistryPrefix: "registryprefixgoeshere",
|
|
||||||
ConfigFile: "/tmp/configfilegoeshere",
|
|
||||||
EngineImage: clitypes.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, command.NewOutStream(&bytes.Buffer{}), &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, command.NewOutStream(&bytes.Buffer{}), 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, command.NewOutStream(&bytes.Buffer{}), 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, command.NewOutStream(&bytes.Buffer{}), 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
)
|
|
|
@ -7,16 +7,15 @@ import (
|
||||||
"github.com/containerd/containerd"
|
"github.com/containerd/containerd"
|
||||||
"github.com/containerd/containerd/containers"
|
"github.com/containerd/containerd/containers"
|
||||||
"github.com/containerd/containerd/content"
|
"github.com/containerd/containerd/content"
|
||||||
specs "github.com/opencontainers/runtime-spec/specs-go"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
containerdSockPath = "/run/containerd/containerd.sock"
|
containerdSockPath = "/run/containerd/containerd.sock"
|
||||||
engineContainerName = "dockerd"
|
engineNamespace = "com.docker"
|
||||||
engineNamespace = "docker"
|
|
||||||
|
|
||||||
// Used to signal the containerd-proxy if it should manage
|
// runtimeMetadataName is the name of the runtime metadata file
|
||||||
proxyLabel = "com.docker/containerd-proxy.scope"
|
// When stored as a label on the container it is prefixed by "com.docker."
|
||||||
|
runtimeMetadataName = "distribution_based_engine"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -34,39 +33,6 @@ var (
|
||||||
|
|
||||||
// ErrEngineShutdownTimeout returned if the engine failed to shutdown in time
|
// ErrEngineShutdownTimeout returned if the engine failed to shutdown in time
|
||||||
ErrEngineShutdownTimeout = errors.New("timeout waiting for engine to exit")
|
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,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type baseClient struct {
|
type baseClient struct {
|
||||||
|
@ -82,4 +48,13 @@ type containerdClient interface {
|
||||||
Close() error
|
Close() error
|
||||||
ContentStore() content.Store
|
ContentStore() content.Store
|
||||||
ContainerService() containers.Store
|
ContainerService() containers.Store
|
||||||
|
Install(context.Context, containerd.Image, ...containerd.InstallOpts) error
|
||||||
|
Version(ctx context.Context) (containerd.Version, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RuntimeMetadata holds platform information about the daemon
|
||||||
|
type RuntimeMetadata struct {
|
||||||
|
Platform string `json:"platform"`
|
||||||
|
ContainerdMinVersion string `json:"containerd_min_version"`
|
||||||
|
Runtime string `json:"runtime"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,74 +2,31 @@ package containerizedengine
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"path"
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/containerd/containerd"
|
||||||
|
"github.com/containerd/containerd/content"
|
||||||
"github.com/containerd/containerd/errdefs"
|
"github.com/containerd/containerd/errdefs"
|
||||||
|
"github.com/containerd/containerd/images"
|
||||||
"github.com/containerd/containerd/namespaces"
|
"github.com/containerd/containerd/namespaces"
|
||||||
"github.com/docker/cli/internal/pkg/containerized"
|
|
||||||
clitypes "github.com/docker/cli/types"
|
clitypes "github.com/docker/cli/types"
|
||||||
"github.com/docker/distribution/reference"
|
"github.com/docker/distribution/reference"
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
|
ver "github.com/hashicorp/go-version"
|
||||||
|
"github.com/opencontainers/image-spec/specs-go/v1"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetCurrentEngineVersion determines the current type of engine (image) and version
|
|
||||||
func (c *baseClient) GetCurrentEngineVersion(ctx context.Context) (clitypes.EngineInitOptions, error) {
|
|
||||||
ctx = namespaces.WithNamespace(ctx, engineNamespace)
|
|
||||||
ret := clitypes.EngineInitOptions{}
|
|
||||||
currentEngine := clitypes.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(), clitypes.EnterpriseEngineImage) {
|
|
||||||
currentEngine = clitypes.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
|
// ActivateEngine will switch the image from the CE to EE image
|
||||||
func (c *baseClient) ActivateEngine(ctx context.Context, opts clitypes.EngineInitOptions, out clitypes.OutStream,
|
func (c *baseClient) ActivateEngine(ctx context.Context, opts clitypes.EngineInitOptions, out clitypes.OutStream,
|
||||||
authConfig *types.AuthConfig, healthfn func(context.Context) error) error {
|
authConfig *types.AuthConfig, healthfn func(context.Context) error) error {
|
||||||
|
|
||||||
// set the proxy scope to "ee" for activate flows
|
|
||||||
opts.Scope = "ee"
|
|
||||||
|
|
||||||
ctx = namespaces.WithNamespace(ctx, engineNamespace)
|
ctx = namespaces.WithNamespace(ctx, engineNamespace)
|
||||||
|
|
||||||
// If version is unspecified, use the existing engine version
|
|
||||||
if opts.EngineVersion == "" {
|
|
||||||
currentOpts, err := c.GetCurrentEngineVersion(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
opts.EngineVersion = currentOpts.EngineVersion
|
|
||||||
if currentOpts.EngineImage == clitypes.EnterpriseEngineImage {
|
|
||||||
// This is a "no-op" activation so the only change would be the license - don't update the engine itself
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return c.DoUpdate(ctx, opts, out, authConfig, healthfn)
|
return c.DoUpdate(ctx, opts, out, authConfig, healthfn)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -84,7 +41,21 @@ func (c *baseClient) DoUpdate(ctx context.Context, opts clitypes.EngineInitOptio
|
||||||
// current engine version and automatically apply it so users
|
// current engine version and automatically apply it so users
|
||||||
// could stay in sync by simply having a scheduled
|
// could stay in sync by simply having a scheduled
|
||||||
// `docker engine update`
|
// `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")
|
||||||
|
}
|
||||||
|
|
||||||
|
localMetadata, err := c.GetCurrentRuntimeMetadata(ctx, "")
|
||||||
|
if err == nil {
|
||||||
|
if opts.EngineImage == "" {
|
||||||
|
if strings.Contains(strings.ToLower(localMetadata.Platform), "community") {
|
||||||
|
opts.EngineImage = clitypes.CommunityEngineImage
|
||||||
|
} else {
|
||||||
|
opts.EngineImage = clitypes.EnterpriseEngineImage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if opts.EngineImage == "" {
|
||||||
|
return fmt.Errorf("unable to determine the installed engine version. Specify which engine image to update with --engine-image set to 'engine-community' or 'engine-enterprise'")
|
||||||
}
|
}
|
||||||
|
|
||||||
imageName := fmt.Sprintf("%s/%s:%s", opts.RegistryPrefix, opts.EngineImage, opts.EngineVersion)
|
imageName := fmt.Sprintf("%s/%s:%s", opts.RegistryPrefix, opts.EngineImage, opts.EngineVersion)
|
||||||
|
@ -102,30 +73,137 @@ func (c *baseClient) DoUpdate(ctx context.Context, opts clitypes.EngineInitOptio
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gather information about the existing engine so we can recreate it
|
// Make sure we're safe to proceed
|
||||||
engine, err := c.GetEngine(ctx)
|
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
labels[proxyLabel] = opts.Scope
|
// Grab current metadata for comparison purposes
|
||||||
_, err = engine.SetLabels(ctx, labels)
|
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
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.WriteRuntimeMetadata("", newMetadata)
|
||||||
|
}
|
||||||
|
|
||||||
|
var defaultDockerRoot = "/var/lib/docker"
|
||||||
|
|
||||||
|
// GetCurrentRuntimeMetadata loads the current daemon runtime metadata information from the local host
|
||||||
|
func (c *baseClient) GetCurrentRuntimeMetadata(_ context.Context, dockerRoot string) (*RuntimeMetadata, error) {
|
||||||
|
if dockerRoot == "" {
|
||||||
|
dockerRoot = defaultDockerRoot
|
||||||
|
}
|
||||||
|
filename := filepath.Join(dockerRoot, runtimeMetadataName+".json")
|
||||||
|
|
||||||
|
data, err := ioutil.ReadFile(filename)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var res 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 (c *baseClient) WriteRuntimeMetadata(dockerRoot string, metadata *RuntimeMetadata) error {
|
||||||
|
if dockerRoot == "" {
|
||||||
|
dockerRoot = defaultDockerRoot
|
||||||
|
}
|
||||||
|
filename := filepath.Join(dockerRoot, runtimeMetadataName+".json")
|
||||||
|
|
||||||
|
data, err := json.Marshal(metadata)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
os.Remove(filename)
|
||||||
|
return ioutil.WriteFile(filename, data, 0644)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) (*RuntimeMetadata, error) {
|
||||||
|
var metadata 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."+runtimeMetadataName]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("image %s does not contain runtime metadata label %s", image.Name(), 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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,9 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/containerd/containerd"
|
"github.com/containerd/containerd"
|
||||||
|
@ -12,162 +15,24 @@ import (
|
||||||
"github.com/docker/cli/cli/command"
|
"github.com/docker/cli/cli/command"
|
||||||
clitypes "github.com/docker/cli/types"
|
clitypes "github.com/docker/cli/types"
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
|
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||||
"gotest.tools/assert"
|
"gotest.tools/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestGetCurrentEngineVersionHappy(t *testing.T) {
|
func healthfnHappy(ctx context.Context) error {
|
||||||
ctx := context.Background()
|
return nil
|
||||||
image := &fakeImage{
|
|
||||||
nameFunc: func() string {
|
|
||||||
return "acme.com/dockermirror/" + clitypes.CommunityEngineImage + ":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, clitypes.CommunityEngineImage)
|
|
||||||
assert.Equal(t, opts.RegistryPrefix, "acme.com/dockermirror")
|
|
||||||
assert.Equal(t, opts.EngineVersion, "engineversion")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetCurrentEngineVersionEnterpriseHappy(t *testing.T) {
|
func TestActivateConfigFailure(t *testing.T) {
|
||||||
ctx := context.Background()
|
|
||||||
image := &fakeImage{
|
|
||||||
nameFunc: func() string {
|
|
||||||
return "docker.io/docker/" + clitypes.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, clitypes.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) {
|
|
||||||
return nil, expectedError
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := client.GetCurrentEngineVersion(ctx)
|
|
||||||
assert.Assert(t, err == expectedError)
|
|
||||||
}
|
|
||||||
|
|
||||||
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 := clitypes.EngineInitOptions{
|
|
||||||
EngineVersion: "engineversiongoeshere",
|
|
||||||
RegistryPrefix: "registryprefixgoeshere",
|
|
||||||
ConfigFile: "/tmp/configfilegoeshere",
|
|
||||||
EngineImage: clitypes.EnterpriseEngineImage,
|
|
||||||
}
|
|
||||||
|
|
||||||
err := client.ActivateEngine(ctx, opts, command.NewOutStream(&bytes.Buffer{}), &types.AuthConfig{}, healthfnHappy)
|
|
||||||
assert.ErrorContains(t, err, "unable to find")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestActivateNoChange(t *testing.T) {
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
registryPrefix := "registryprefixgoeshere"
|
registryPrefix := "registryprefixgoeshere"
|
||||||
image := &fakeImage{
|
image := &fakeImage{
|
||||||
nameFunc: func() string {
|
nameFunc: func() string {
|
||||||
return registryPrefix + "/" + clitypes.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{
|
container := &fakeContainer{
|
||||||
imageFunc: func(context.Context) (containerd.Image, error) {
|
imageFunc: func(context.Context) (containerd.Image, error) {
|
||||||
|
@ -185,6 +50,9 @@ func TestActivateNoChange(t *testing.T) {
|
||||||
containersFunc: func(ctx context.Context, filters ...string) ([]containerd.Container, error) {
|
containersFunc: func(ctx context.Context, filters ...string) ([]containerd.Container, error) {
|
||||||
return []containerd.Container{container}, nil
|
return []containerd.Container{container}, nil
|
||||||
},
|
},
|
||||||
|
getImageFunc: func(ctx context.Context, ref string) (containerd.Image, error) {
|
||||||
|
return image, nil
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
opts := clitypes.EngineInitOptions{
|
opts := clitypes.EngineInitOptions{
|
||||||
|
@ -195,7 +63,7 @@ func TestActivateNoChange(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
err := client.ActivateEngine(ctx, opts, command.NewOutStream(&bytes.Buffer{}), &types.AuthConfig{}, healthfnHappy)
|
err := client.ActivateEngine(ctx, opts, command.NewOutStream(&bytes.Buffer{}), &types.AuthConfig{}, healthfnHappy)
|
||||||
assert.NilError(t, err)
|
assert.ErrorContains(t, err, "config lookup failure")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestActivateDoUpdateFail(t *testing.T) {
|
func TestActivateDoUpdateFail(t *testing.T) {
|
||||||
|
@ -244,7 +112,7 @@ func TestDoUpdateNoVersion(t *testing.T) {
|
||||||
}
|
}
|
||||||
client := baseClient{}
|
client := baseClient{}
|
||||||
err := client.DoUpdate(ctx, opts, command.NewOutStream(&bytes.Buffer{}), &types.AuthConfig{}, healthfnHappy)
|
err := client.DoUpdate(ctx, opts, command.NewOutStream(&bytes.Buffer{}), &types.AuthConfig{}, healthfnHappy)
|
||||||
assert.ErrorContains(t, err, "please pick the version you")
|
assert.ErrorContains(t, err, "pick the version you")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDoUpdateImageMiscError(t *testing.T) {
|
func TestDoUpdateImageMiscError(t *testing.T) {
|
||||||
|
@ -292,30 +160,114 @@ func TestDoUpdatePullFail(t *testing.T) {
|
||||||
assert.ErrorContains(t, err, "pull failure")
|
assert.ErrorContains(t, err, "pull failure")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDoUpdateEngineMissing(t *testing.T) {
|
func TestActivateDoUpdateVerifyImageName(t *testing.T) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
registryPrefix := "registryprefixgoeshere"
|
||||||
|
image := &fakeImage{
|
||||||
|
nameFunc: func() string {
|
||||||
|
return registryPrefix + "/ce-engine:engineversion"
|
||||||
|
},
|
||||||
|
}
|
||||||
|
container := &fakeContainer{
|
||||||
|
imageFunc: func(context.Context) (containerd.Image, error) {
|
||||||
|
return image, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
requestedImage := "unset"
|
||||||
|
client := baseClient{
|
||||||
|
cclient: &fakeContainerdClient{
|
||||||
|
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) {
|
||||||
|
requestedImage = ref
|
||||||
|
return nil, fmt.Errorf("something went wrong")
|
||||||
|
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
opts := clitypes.EngineInitOptions{
|
opts := clitypes.EngineInitOptions{
|
||||||
EngineVersion: "engineversiongoeshere",
|
EngineVersion: "engineversiongoeshere",
|
||||||
RegistryPrefix: "registryprefixgoeshere",
|
RegistryPrefix: "registryprefixgoeshere",
|
||||||
ConfigFile: "/tmp/configfilegoeshere",
|
ConfigFile: "/tmp/configfilegoeshere",
|
||||||
EngineImage: "testnamegoeshere",
|
|
||||||
}
|
}
|
||||||
image := &fakeImage{
|
|
||||||
nameFunc: func() string {
|
|
||||||
return "imagenamehere"
|
|
||||||
},
|
|
||||||
}
|
|
||||||
client := baseClient{
|
|
||||||
cclient: &fakeContainerdClient{
|
|
||||||
getImageFunc: func(ctx context.Context, ref string) (containerd.Image, error) {
|
|
||||||
return image, nil
|
|
||||||
|
|
||||||
},
|
tmpdir, err := ioutil.TempDir("", "docker-root")
|
||||||
containersFunc: func(ctx context.Context, filters ...string) ([]containerd.Container, error) {
|
assert.NilError(t, err)
|
||||||
return []containerd.Container{}, nil
|
defer os.RemoveAll(tmpdir)
|
||||||
},
|
tmpDockerRoot := defaultDockerRoot
|
||||||
},
|
defaultDockerRoot = tmpdir
|
||||||
}
|
defer func() {
|
||||||
err := client.DoUpdate(ctx, opts, command.NewOutStream(&bytes.Buffer{}), &types.AuthConfig{}, healthfnHappy)
|
defaultDockerRoot = tmpDockerRoot
|
||||||
assert.ErrorContains(t, err, "unable to find existing engine")
|
}()
|
||||||
|
metadata := RuntimeMetadata{Platform: "platformgoeshere"}
|
||||||
|
err = client.WriteRuntimeMetadata(tmpdir, &metadata)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
err = client.ActivateEngine(ctx, opts, command.NewOutStream(&bytes.Buffer{}), &types.AuthConfig{}, healthfnHappy)
|
||||||
|
assert.ErrorContains(t, err, "check for image")
|
||||||
|
assert.ErrorContains(t, err, "something went wrong")
|
||||||
|
expectedImage := fmt.Sprintf("%s/%s:%s", opts.RegistryPrefix, "engine-enterprise", opts.EngineVersion)
|
||||||
|
assert.Assert(t, requestedImage == expectedImage, "%s != %s", requestedImage, expectedImage)
|
||||||
|
|
||||||
|
// Redo with enterprise set
|
||||||
|
metadata = RuntimeMetadata{Platform: "Docker Engine - Enterprise"}
|
||||||
|
err = client.WriteRuntimeMetadata(tmpdir, &metadata)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
err = client.ActivateEngine(ctx, opts, command.NewOutStream(&bytes.Buffer{}), &types.AuthConfig{}, healthfnHappy)
|
||||||
|
assert.ErrorContains(t, err, "check for image")
|
||||||
|
assert.ErrorContains(t, err, "something went wrong")
|
||||||
|
expectedImage = fmt.Sprintf("%s/%s:%s", opts.RegistryPrefix, "engine-enterprise", opts.EngineVersion)
|
||||||
|
assert.Assert(t, requestedImage == expectedImage, "%s != %s", requestedImage, expectedImage)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetCurrentRuntimeMetadataNotPresent(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
tmpdir, err := ioutil.TempDir("", "docker-root")
|
||||||
|
assert.NilError(t, err)
|
||||||
|
defer os.RemoveAll(tmpdir)
|
||||||
|
client := baseClient{}
|
||||||
|
_, err = client.GetCurrentRuntimeMetadata(ctx, tmpdir)
|
||||||
|
assert.ErrorType(t, err, os.IsNotExist)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetCurrentRuntimeMetadataBadJson(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
tmpdir, err := ioutil.TempDir("", "docker-root")
|
||||||
|
assert.NilError(t, err)
|
||||||
|
defer os.RemoveAll(tmpdir)
|
||||||
|
filename := filepath.Join(tmpdir, runtimeMetadataName+".json")
|
||||||
|
err = ioutil.WriteFile(filename, []byte("not json"), 0644)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
client := baseClient{}
|
||||||
|
_, err = client.GetCurrentRuntimeMetadata(ctx, tmpdir)
|
||||||
|
assert.ErrorContains(t, err, "malformed runtime metadata file")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetCurrentRuntimeMetadataHappyPath(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
tmpdir, err := ioutil.TempDir("", "docker-root")
|
||||||
|
assert.NilError(t, err)
|
||||||
|
defer os.RemoveAll(tmpdir)
|
||||||
|
client := baseClient{}
|
||||||
|
metadata := RuntimeMetadata{Platform: "platformgoeshere"}
|
||||||
|
err = client.WriteRuntimeMetadata(tmpdir, &metadata)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
res, err := client.GetCurrentRuntimeMetadata(ctx, tmpdir)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
assert.Equal(t, res.Platform, "platformgoeshere")
|
||||||
|
}
|
||||||
|
|
||||||
|
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,19 +1,23 @@
|
||||||
package containerizedengine
|
package versions
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"path"
|
||||||
"sort"
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
registryclient "github.com/docker/cli/cli/registry/client"
|
registryclient "github.com/docker/cli/cli/registry/client"
|
||||||
clitypes "github.com/docker/cli/types"
|
clitypes "github.com/docker/cli/types"
|
||||||
"github.com/docker/distribution/reference"
|
"github.com/docker/distribution/reference"
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
ver "github.com/hashicorp/go-version"
|
ver "github.com/hashicorp/go-version"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetEngineVersions reports the versions of the engine that are available
|
// GetEngineVersions reports the versions of the engine that are available
|
||||||
func (c *baseClient) GetEngineVersions(ctx context.Context, registryClient registryclient.RegistryClient, currentVersion, imageName string) (clitypes.AvailableVersions, error) {
|
func GetEngineVersions(ctx context.Context, registryClient registryclient.RegistryClient, registryPrefix string, serverVersion types.Version) (clitypes.AvailableVersions, error) {
|
||||||
|
imageName := getEngineImage(registryPrefix, serverVersion)
|
||||||
imageRef, err := reference.ParseNormalizedNamed(imageName)
|
imageRef, err := reference.ParseNormalizedNamed(imageName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return clitypes.AvailableVersions{}, err
|
return clitypes.AvailableVersions{}, err
|
||||||
|
@ -24,7 +28,25 @@ func (c *baseClient) GetEngineVersions(ctx context.Context, registryClient regis
|
||||||
return clitypes.AvailableVersions{}, err
|
return clitypes.AvailableVersions{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return parseTags(tags, currentVersion)
|
return parseTags(tags, serverVersion.Version)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getEngineImage(registryPrefix string, serverVersion types.Version) string {
|
||||||
|
platform := strings.ToLower(serverVersion.Platform.Name)
|
||||||
|
if platform != "" {
|
||||||
|
if strings.Contains(platform, "enterprise") {
|
||||||
|
return path.Join(registryPrefix, clitypes.EnterpriseEngineImage)
|
||||||
|
}
|
||||||
|
return path.Join(registryPrefix, clitypes.CommunityEngineImage)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO This check is only applicable for early 18.09 builds that had some packaging bugs
|
||||||
|
// and can be removed once we're no longer testing with them
|
||||||
|
if strings.Contains(serverVersion.Version, "ee") {
|
||||||
|
return path.Join(registryPrefix, clitypes.EnterpriseEngineImage)
|
||||||
|
}
|
||||||
|
|
||||||
|
return path.Join(registryPrefix, clitypes.CommunityEngineImage)
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseTags(tags []string, currentVersion string) (clitypes.AvailableVersions, error) {
|
func parseTags(tags []string, currentVersion string) (clitypes.AvailableVersions, error) {
|
|
@ -1,19 +1,19 @@
|
||||||
package containerizedengine
|
package versions
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
"gotest.tools/assert"
|
"gotest.tools/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestGetEngineVersionsBadImage(t *testing.T) {
|
func TestGetEngineVersionsBadImage(t *testing.T) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
client := baseClient{}
|
|
||||||
|
|
||||||
currentVersion := "currentversiongoeshere"
|
registryPrefix := "this is an illegal image $%^&"
|
||||||
imageName := "this is an illegal image $%^&"
|
currentVersion := types.Version{Version: "currentversiongoeshere"}
|
||||||
_, err := client.GetEngineVersions(ctx, nil, currentVersion, imageName)
|
_, err := GetEngineVersions(ctx, nil, registryPrefix, currentVersion)
|
||||||
assert.ErrorContains(t, err, "invalid reference format")
|
assert.ErrorContains(t, err, "invalid reference format")
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
|
|
@ -4,7 +4,6 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
registryclient "github.com/docker/cli/cli/registry/client"
|
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
ver "github.com/hashicorp/go-version"
|
ver "github.com/hashicorp/go-version"
|
||||||
)
|
)
|
||||||
|
@ -15,6 +14,12 @@ const (
|
||||||
|
|
||||||
// EnterpriseEngineImage is the repo name for the enterprise engine
|
// EnterpriseEngineImage is the repo name for the enterprise engine
|
||||||
EnterpriseEngineImage = "engine-enterprise"
|
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"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ContainerizedClient can be used to manage the lifecycle of
|
// ContainerizedClient can be used to manage the lifecycle of
|
||||||
|
@ -26,19 +31,11 @@ type ContainerizedClient interface {
|
||||||
out OutStream,
|
out OutStream,
|
||||||
authConfig *types.AuthConfig,
|
authConfig *types.AuthConfig,
|
||||||
healthfn func(context.Context) error) error
|
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,
|
DoUpdate(ctx context.Context,
|
||||||
opts EngineInitOptions,
|
opts EngineInitOptions,
|
||||||
out OutStream,
|
out OutStream,
|
||||||
authConfig *types.AuthConfig,
|
authConfig *types.AuthConfig,
|
||||||
healthfn func(context.Context) error) error
|
healthfn func(context.Context) error) error
|
||||||
GetEngineVersions(ctx context.Context, registryClient registryclient.RegistryClient, currentVersion, imageName string) (AvailableVersions, error)
|
|
||||||
GetCurrentEngineVersion(ctx context.Context) (EngineInitOptions, error)
|
|
||||||
RemoveEngine(ctx context.Context) error
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// EngineInitOptions contains the configuration settings
|
// EngineInitOptions contains the configuration settings
|
||||||
|
@ -48,7 +45,6 @@ type EngineInitOptions struct {
|
||||||
EngineImage string
|
EngineImage string
|
||||||
EngineVersion string
|
EngineVersion string
|
||||||
ConfigFile string
|
ConfigFile string
|
||||||
Scope string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// AvailableVersions groups the available versions which were discovered
|
// AvailableVersions groups the available versions which were discovered
|
||||||
|
|
|
@ -3,7 +3,7 @@ github.com/asaskevich/govalidator f9ffefc3facfbe0caee3fea233cbb6e8208f4541
|
||||||
github.com/Azure/go-ansiterm d6e3b3328b783f23731bc4d058875b0371ff8109
|
github.com/Azure/go-ansiterm d6e3b3328b783f23731bc4d058875b0371ff8109
|
||||||
github.com/beorn7/perks 3a771d992973f24aa725d07868b467d1ddfceafb
|
github.com/beorn7/perks 3a771d992973f24aa725d07868b467d1ddfceafb
|
||||||
github.com/containerd/console c12b1e7919c14469339a5d38f2f8ed9b64a9de23
|
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/continuity d8fb8589b0e8e85b8c8bbaa8840226d0dfeb7371
|
||||||
github.com/containerd/fifo 3d5202a
|
github.com/containerd/fifo 3d5202a
|
||||||
github.com/containerd/typeurl f694355
|
github.com/containerd/typeurl f694355
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
[![GoDoc](https://godoc.org/github.com/containerd/containerd?status.svg)](https://godoc.org/github.com/containerd/containerd)
|
[![GoDoc](https://godoc.org/github.com/containerd/containerd?status.svg)](https://godoc.org/github.com/containerd/containerd)
|
||||||
[![Build Status](https://travis-ci.org/containerd/containerd.svg?branch=master)](https://travis-ci.org/containerd/containerd)
|
[![Build Status](https://travis-ci.org/containerd/containerd.svg?branch=master)](https://travis-ci.org/containerd/containerd)
|
||||||
|
[![Windows Build Status](https://ci.appveyor.com/api/projects/status/github/containerd/containerd?branch=master&svg=true)](https://ci.appveyor.com/project/mlaventure/containerd-3g73f?branch=master)
|
||||||
[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bhttps%3A%2F%2Fgithub.com%2Fcontainerd%2Fcontainerd.svg?type=shield)](https://app.fossa.io/projects/git%2Bhttps%3A%2F%2Fgithub.com%2Fcontainerd%2Fcontainerd?ref=badge_shield)
|
[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bhttps%3A%2F%2Fgithub.com%2Fcontainerd%2Fcontainerd.svg?type=shield)](https://app.fossa.io/projects/git%2Bhttps%3A%2F%2Fgithub.com%2Fcontainerd%2Fcontainerd?ref=badge_shield)
|
||||||
[![Go Report Card](https://goreportcard.com/badge/github.com/containerd/containerd)](https://goreportcard.com/report/github.com/containerd/containerd)
|
[![Go Report Card](https://goreportcard.com/badge/github.com/containerd/containerd)](https://goreportcard.com/report/github.com/containerd/containerd)
|
||||||
[![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/1271/badge)](https://bestpractices.coreinfrastructure.org/projects/1271)
|
[![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/1271/badge)](https://bestpractices.coreinfrastructure.org/projects/1271)
|
||||||
|
|
|
@ -141,7 +141,7 @@ type EventsClient interface {
|
||||||
// Forward sends an event that has already been packaged into an envelope
|
// Forward sends an event that has already been packaged into an envelope
|
||||||
// with a timestamp and namespace.
|
// 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.
|
// behalf of another component, namespace or publisher.
|
||||||
Forward(ctx context.Context, in *ForwardRequest, opts ...grpc.CallOption) (*google_protobuf2.Empty, error)
|
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
|
// 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
|
// Forward sends an event that has already been packaged into an envelope
|
||||||
// with a timestamp and namespace.
|
// 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.
|
// behalf of another component, namespace or publisher.
|
||||||
Forward(context.Context, *ForwardRequest) (*google_protobuf2.Empty, error)
|
Forward(context.Context, *ForwardRequest) (*google_protobuf2.Empty, error)
|
||||||
// Subscribe to a stream of events, possibly returning only that match any
|
// Subscribe to a stream of events, possibly returning only that match any
|
||||||
|
|
|
@ -20,7 +20,7 @@ service Events {
|
||||||
// Forward sends an event that has already been packaged into an envelope
|
// Forward sends an event that has already been packaged into an envelope
|
||||||
// with a timestamp and namespace.
|
// 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.
|
// behalf of another component, namespace or publisher.
|
||||||
rpc Forward(ForwardRequest) returns (google.protobuf.Empty);
|
rpc Forward(ForwardRequest) returns (google.protobuf.Empty);
|
||||||
|
|
||||||
|
|
|
@ -141,6 +141,15 @@ func NewCreator(opts ...Opt) Creator {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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)
|
return copyIO(fifos, streams)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,25 +20,21 @@ package containerd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"github.com/containerd/containerd/api/types"
|
|
||||||
"github.com/containerd/containerd/containers"
|
"github.com/containerd/containerd/containers"
|
||||||
"github.com/containerd/containerd/content"
|
"github.com/containerd/containerd/content"
|
||||||
"github.com/containerd/containerd/errdefs"
|
"github.com/containerd/containerd/errdefs"
|
||||||
"github.com/containerd/containerd/images"
|
"github.com/containerd/containerd/images"
|
||||||
"github.com/containerd/containerd/mount"
|
"github.com/containerd/containerd/mount"
|
||||||
"github.com/containerd/containerd/platforms"
|
"github.com/containerd/containerd/platforms"
|
||||||
"github.com/containerd/containerd/runtime/linux/runctypes"
|
|
||||||
"github.com/gogo/protobuf/proto"
|
"github.com/gogo/protobuf/proto"
|
||||||
protobuf "github.com/gogo/protobuf/types"
|
protobuf "github.com/gogo/protobuf/types"
|
||||||
"github.com/opencontainers/image-spec/identity"
|
"github.com/opencontainers/image-spec/identity"
|
||||||
"github.com/opencontainers/image-spec/specs-go/v1"
|
"github.com/opencontainers/image-spec/specs-go/v1"
|
||||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
|
||||||
"github.com/pkg/errors"
|
"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
|
// WithRemappedSnapshot creates a new snapshot and remaps the uid/gid for the
|
||||||
// filesystem to be used by a container with user namespaces
|
// filesystem to be used by a container with user namespaces
|
||||||
func WithRemappedSnapshot(id string, i Image, uid, gid uint32) NewContainerOpts {
|
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)
|
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
|
|
||||||
}
|
|
||||||
|
|
|
@ -52,7 +52,7 @@ var _ events.Subscriber = &Exchange{}
|
||||||
|
|
||||||
// Forward accepts an envelope to be direcly distributed on the 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.
|
// when the event is propagated on behalf of another publisher.
|
||||||
func (e *Exchange) Forward(ctx context.Context, envelope *events.Envelope) (err error) {
|
func (e *Exchange) Forward(ctx context.Context, envelope *events.Envelope) (err error) {
|
||||||
if err := validateEnvelope(envelope); err != nil {
|
if err := validateEnvelope(envelope); err != nil {
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -22,7 +22,6 @@ import (
|
||||||
|
|
||||||
"github.com/containerd/containerd/errdefs"
|
"github.com/containerd/containerd/errdefs"
|
||||||
"github.com/containerd/containerd/images"
|
"github.com/containerd/containerd/images"
|
||||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type importOpts struct {
|
type importOpts struct {
|
||||||
|
@ -84,35 +83,3 @@ func (c *Client) Import(ctx context.Context, importer images.Importer, reader io
|
||||||
}
|
}
|
||||||
return images, nil
|
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
|
|
||||||
}
|
|
||||||
|
|
|
@ -33,25 +33,14 @@ import (
|
||||||
|
|
||||||
// Install a binary image into the opt service
|
// Install a binary image into the opt service
|
||||||
func (c *Client) Install(ctx context.Context, image Image, opts ...InstallOpts) error {
|
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
|
var config InstallConfig
|
||||||
for _, o := range opts {
|
for _, o := range opts {
|
||||||
o(&config)
|
o(&config)
|
||||||
}
|
}
|
||||||
|
path, err := c.getInstallPath(ctx, config)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
var (
|
var (
|
||||||
cs = image.ContentStore()
|
cs = image.ContentStore()
|
||||||
platform = platforms.Default()
|
platform = platforms.Default()
|
||||||
|
@ -89,3 +78,25 @@ func (c *Client) Install(ctx context.Context, image Image, opts ...InstallOpts)
|
||||||
}
|
}
|
||||||
return nil
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -25,6 +25,8 @@ type InstallConfig struct {
|
||||||
Libs bool
|
Libs bool
|
||||||
// Replace will overwrite existing binaries or libs in the opt directory
|
// Replace will overwrite existing binaries or libs in the opt directory
|
||||||
Replace bool
|
Replace bool
|
||||||
|
// Path to install libs and binaries to
|
||||||
|
Path string
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithInstallLibs installs libs from the image
|
// WithInstallLibs installs libs from the image
|
||||||
|
@ -36,3 +38,10 @@ func WithInstallLibs(c *InstallConfig) {
|
||||||
func WithInstallReplace(c *InstallConfig) {
|
func WithInstallReplace(c *InstallConfig) {
|
||||||
c.Replace = true
|
c.Replace = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithInstallPath sets the optional install path
|
||||||
|
func WithInstallPath(path string) InstallOpts {
|
||||||
|
return func(c *InstallConfig) {
|
||||||
|
c.Path = path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -32,6 +32,10 @@ var (
|
||||||
|
|
||||||
// Mount to the provided target
|
// Mount to the provided target
|
||||||
func (m *Mount) Mount(target string) error {
|
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)
|
home, layerID := filepath.Split(m.Source)
|
||||||
|
|
||||||
parentLayerPaths, err := m.GetParentPaths()
|
parentLayerPaths, err := m.GetParentPaths()
|
||||||
|
|
|
@ -18,11 +18,27 @@ package oci
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
|
||||||
|
"github.com/containerd/containerd/namespaces"
|
||||||
|
"github.com/containerd/containerd/platforms"
|
||||||
|
|
||||||
"github.com/containerd/containerd/containers"
|
"github.com/containerd/containerd/containers"
|
||||||
specs "github.com/opencontainers/runtime-spec/specs-go"
|
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
|
// 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
|
// to be created without the "issues" with go vendoring and package imports
|
||||||
type Spec = specs.Spec
|
type Spec = specs.Spec
|
||||||
|
@ -30,12 +46,36 @@ type Spec = specs.Spec
|
||||||
// GenerateSpec will generate a default spec from the provided image
|
// GenerateSpec will generate a default spec from the provided image
|
||||||
// for use as a containerd container
|
// for use as a containerd container
|
||||||
func GenerateSpec(ctx context.Context, client Client, c *containers.Container, opts ...SpecOpts) (*Spec, error) {
|
func GenerateSpec(ctx context.Context, client Client, c *containers.Container, opts ...SpecOpts) (*Spec, error) {
|
||||||
s, err := createDefaultSpec(ctx, c.ID)
|
return GenerateSpecWithPlatform(ctx, client, platforms.DefaultString(), c, opts...)
|
||||||
if err != nil {
|
}
|
||||||
|
|
||||||
|
// 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 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
|
// 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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func createDefaultSpec(ctx context.Context, id string) (*Spec, error) {
|
func defaultUnixCaps() []string {
|
||||||
var s Spec
|
return []string{
|
||||||
return &s, populateDefaultSpec(ctx, &s, id)
|
"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
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,12 +19,25 @@ package oci
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/containerd/containerd/containers"
|
"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"
|
specs "github.com/opencontainers/runtime-spec/specs-go"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
"github.com/syndtr/gocapability/capability"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SpecOpts sets spec specific information to a newly generated OCI spec
|
// 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
|
// WithDefaultSpec returns a SpecOpts that will populate the spec with default
|
||||||
// values.
|
// values.
|
||||||
//
|
//
|
||||||
// Use as the first option to clear the spec, then apply options afterwards.
|
// Use as the first option to clear the spec, then apply options afterwards.
|
||||||
func WithDefaultSpec() SpecOpts {
|
func WithDefaultSpec() SpecOpts {
|
||||||
return func(ctx context.Context, _ Client, c *containers.Container, s *Spec) error {
|
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
|
// WithEnv appends environment variables
|
||||||
func WithEnv(environmentVariables []string) SpecOpts {
|
func WithEnv(environmentVariables []string) SpecOpts {
|
||||||
return func(_ context.Context, _ Client, _ *containers.Container, s *Spec) error {
|
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
|
// replaceOrAppendEnvValues returns the defaults with the overrides either
|
||||||
// replaced by env key or appended to the list
|
// replaced by env key or appended to the list
|
||||||
func replaceOrAppendEnvValues(defaults, overrides []string) []string {
|
func replaceOrAppendEnvValues(defaults, overrides []string) []string {
|
||||||
|
@ -163,3 +174,821 @@ func replaceOrAppendEnvValues(defaults, overrides []string) []string {
|
||||||
|
|
||||||
return defaults
|
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,
|
||||||
|
)
|
||||||
|
|
|
@ -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,
|
|
||||||
)
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -22,11 +22,6 @@ import (
|
||||||
specs "github.com/opencontainers/image-spec/specs-go/v1"
|
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.
|
// DefaultString returns the default string specifier for the platform.
|
||||||
func DefaultString() string {
|
func DefaultString() string {
|
||||||
return Format(DefaultSpec())
|
return Format(DefaultSpec())
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
// +build !windows
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Copyright The containerd Authors.
|
Copyright The containerd Authors.
|
||||||
|
|
||||||
|
@ -14,31 +16,9 @@
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package oci
|
package platforms
|
||||||
|
|
||||||
import (
|
// Default returns the default matcher for the platform.
|
||||||
"context"
|
func Default() MatchComparer {
|
||||||
|
return Only(DefaultSpec())
|
||||||
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
|
|
||||||
}
|
}
|
|
@ -1,3 +1,5 @@
|
||||||
|
// +build windows
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Copyright The containerd Authors.
|
Copyright The containerd Authors.
|
||||||
|
|
||||||
|
@ -14,18 +16,16 @@
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package containerd
|
package platforms
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
specs "github.com/opencontainers/image-spec/specs-go/v1"
|
||||||
|
|
||||||
specs "github.com/opencontainers/runtime-spec/specs-go"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// WithResources sets the provided resources on the spec for task updates
|
// Default returns the default matcher for the platform.
|
||||||
func WithResources(resources *specs.WindowsResources) UpdateTaskOpts {
|
func Default() MatchComparer {
|
||||||
return func(ctx context.Context, client *Client, r *UpdateTaskInfo) error {
|
return Ordered(DefaultSpec(), specs.Platform{
|
||||||
r.Resources = resources
|
OS: "linux",
|
||||||
return nil
|
Architecture: "amd64",
|
||||||
}
|
})
|
||||||
}
|
}
|
|
@ -117,7 +117,7 @@ func (r dockerFetcher) open(ctx context.Context, u, mediatype string, offset int
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// TODO: Should any cases where use of content range
|
// TODO: Should any cases where use of content range
|
||||||
// without the proper header be considerd?
|
// without the proper header be considered?
|
||||||
// 206 responses?
|
// 206 responses?
|
||||||
|
|
||||||
// Discard up to offset
|
// Discard up to offset
|
||||||
|
|
|
@ -134,7 +134,7 @@ func (hrs *httpReadSeeker) reader() (io.Reader, error) {
|
||||||
// There is an edge case here where offset == size of the content. If
|
// 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
|
// we seek, we will probably get an error for content that cannot be
|
||||||
// sought (?). In that case, we should err on committing the content,
|
// 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.
|
// reader instead.
|
||||||
|
|
||||||
hrs.rc = ioutil.NopCloser(bytes.NewReader([]byte{}))
|
hrs.rc = ioutil.NopCloser(bytes.NewReader([]byte{}))
|
||||||
|
|
|
@ -272,8 +272,14 @@ func (c *Converter) fetchBlob(ctx context.Context, desc ocispec.Descriptor) erro
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Check if blob -> diff id mapping already exists
|
reuse, err := c.reuseLabelBlobState(ctx, desc)
|
||||||
// TODO: Check if blob empty label exists
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if reuse {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
ra, err := c.contentStore.ReaderAt(ctx, desc)
|
ra, err := c.contentStore.ReaderAt(ctx, desc)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -343,6 +349,17 @@ func (c *Converter) fetchBlob(ctx context.Context, desc ocispec.Descriptor) erro
|
||||||
|
|
||||||
state := calc.State()
|
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.mu.Lock()
|
||||||
c.blobMap[desc.Digest] = state
|
c.blobMap[desc.Digest] = state
|
||||||
c.layerBlobs[state.diffID] = desc
|
c.layerBlobs[state.diffID] = desc
|
||||||
|
@ -351,6 +368,40 @@ func (c *Converter) fetchBlob(ctx context.Context, desc ocispec.Descriptor) erro
|
||||||
return nil
|
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) {
|
func (c *Converter) schema1ManifestHistory() ([]ocispec.History, []digest.Digest, error) {
|
||||||
if c.pulledManifest == nil {
|
if c.pulledManifest == nil {
|
||||||
return nil, nil, errors.New("missing schema 1 manifest for conversion")
|
return nil, nil, errors.New("missing schema 1 manifest for conversion")
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -42,7 +42,7 @@ func CreateUnixSocket(path string) (net.Listener, error) {
|
||||||
return net.Listen("unix", path)
|
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) {
|
func GetLocalListener(path string, uid, gid int) (net.Listener, error) {
|
||||||
// Ensure parent directory is created
|
// Ensure parent directory is created
|
||||||
if err := mkdirAs(filepath.Dir(path), uid, gid); err != nil {
|
if err := mkdirAs(filepath.Dir(path), uid, gid); err != nil {
|
||||||
|
|
|
@ -18,10 +18,18 @@ package containerd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
|
"github.com/containerd/containerd/api/types"
|
||||||
|
"github.com/containerd/containerd/content"
|
||||||
"github.com/containerd/containerd/errdefs"
|
"github.com/containerd/containerd/errdefs"
|
||||||
|
"github.com/containerd/containerd/images"
|
||||||
"github.com/containerd/containerd/mount"
|
"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
|
// 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
|
// WithCheckpointName sets the image name for the checkpoint
|
||||||
func WithCheckpointName(name string) CheckpointTaskOpts {
|
func WithCheckpointName(name string) CheckpointTaskOpts {
|
||||||
return func(r *CheckpointTaskInfo) error {
|
return func(r *CheckpointTaskInfo) error {
|
||||||
|
@ -92,3 +138,19 @@ func WithKillExecID(execID string) KillOpts {
|
||||||
return nil
|
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.
|
Copyright The containerd Authors.
|
||||||
|
|
||||||
|
@ -18,20 +20,11 @@ package containerd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
|
|
||||||
"github.com/containerd/containerd/runtime/linux/runctypes"
|
"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.
|
// 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
|
// There is an upper limit on the number of keyrings in a linux system
|
||||||
func WithNoNewKeyring(ctx context.Context, c *Client, ti *TaskInfo) error {
|
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
|
opts.NoNewKeyring = true
|
||||||
return nil
|
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
|
||||||
|
}
|
|
@ -1,10 +1,10 @@
|
||||||
github.com/containerd/go-runc acb7c88cac264acca9b5eae187a117f4d77a1292
|
github.com/containerd/go-runc 5a6d9f37cfa36b15efba46dc7ea349fa9b7143c3
|
||||||
github.com/containerd/console c12b1e7919c14469339a5d38f2f8ed9b64a9de23
|
github.com/containerd/console c12b1e7919c14469339a5d38f2f8ed9b64a9de23
|
||||||
github.com/containerd/cgroups 5e610833b72089b37d0e615de9a92dfc043757c2
|
github.com/containerd/cgroups 5e610833b72089b37d0e615de9a92dfc043757c2
|
||||||
github.com/containerd/typeurl a93fcdb778cd272c6e9b3028b2f42d813e785d40
|
github.com/containerd/typeurl a93fcdb778cd272c6e9b3028b2f42d813e785d40
|
||||||
github.com/containerd/fifo 3d5202aec260678c48179c56f40e6f38a095738c
|
github.com/containerd/fifo 3d5202aec260678c48179c56f40e6f38a095738c
|
||||||
github.com/containerd/btrfs 2e1aa0ddf94f91fa282b6ed87c23bf0d64911244
|
github.com/containerd/btrfs 2e1aa0ddf94f91fa282b6ed87c23bf0d64911244
|
||||||
github.com/containerd/continuity d3c23511c1bf5851696cba83143d9cbcd666869b
|
github.com/containerd/continuity f44b615e492bdfb371aae2f76ec694d9da1db537
|
||||||
github.com/coreos/go-systemd 48702e0da86bd25e76cfef347e2adeb434a0d0a6
|
github.com/coreos/go-systemd 48702e0da86bd25e76cfef347e2adeb434a0d0a6
|
||||||
github.com/docker/go-metrics 4ea375f7759c82740c893fc030bc37088d2ec098
|
github.com/docker/go-metrics 4ea375f7759c82740c893fc030bc37088d2ec098
|
||||||
github.com/docker/go-events 9461782956ad83b30282bf90e31fa6a70c255ba9
|
github.com/docker/go-events 9461782956ad83b30282bf90e31fa6a70c255ba9
|
||||||
|
@ -71,7 +71,7 @@ github.com/xeipuuv/gojsonschema 1d523034197ff1f222f6429836dd36a2457a1874
|
||||||
golang.org/x/crypto 49796115aa4b964c318aad4f3084fdb41e9aa067
|
golang.org/x/crypto 49796115aa4b964c318aad4f3084fdb41e9aa067
|
||||||
golang.org/x/time f51c12702a4d776e4c1fa9b0fabab841babae631
|
golang.org/x/time f51c12702a4d776e4c1fa9b0fabab841babae631
|
||||||
gopkg.in/inf.v0 3887ee99ecf07df5b447e9b00d9c0b2adaa9f3e4
|
gopkg.in/inf.v0 3887ee99ecf07df5b447e9b00d9c0b2adaa9f3e4
|
||||||
gopkg.in/yaml.v2 53feefa2559fb8dfa8d81baad31be332c97d6c77
|
gopkg.in/yaml.v2 v2.2.1
|
||||||
k8s.io/api 9e5ffd1f1320950b238cfce291b926411f0af722
|
k8s.io/api 9e5ffd1f1320950b238cfce291b926411f0af722
|
||||||
k8s.io/apimachinery ed135c5b96450fd24e5e981c708114fbbd950697
|
k8s.io/apimachinery ed135c5b96450fd24e5e981c708114fbbd950697
|
||||||
k8s.io/apiserver a90e3a95c2e91b944bfca8225c4e0d12e42a9eb5
|
k8s.io/apiserver a90e3a95c2e91b944bfca8225c4e0d12e42a9eb5
|
||||||
|
@ -85,4 +85,4 @@ github.com/mistifyio/go-zfs 166add352731e515512690329794ee593f1aaff2
|
||||||
github.com/pborman/uuid c65b2f87fee37d1c7854c9164a450713c28d50cd
|
github.com/pborman/uuid c65b2f87fee37d1c7854c9164a450713c28d50cd
|
||||||
|
|
||||||
# aufs dependencies
|
# aufs dependencies
|
||||||
github.com/containerd/aufs a7fbd554da7a9eafbe5a460a421313a9fd18d988
|
github.com/containerd/aufs ffa39970e26ad01d81f540b21e65f9c1841a5f92
|
||||||
|
|
Loading…
Reference in New Issue