Refined engine implementations

Adapt the CLI to the host install model for 18.09.

Signed-off-by: Daniel Hiltgen <daniel.hiltgen@docker.com>
This commit is contained in:
Daniel Hiltgen
2018-09-19 20:10:31 -07:00
parent cfec8027ed
commit 342afe44fb
19 changed files with 482 additions and 438 deletions
@@ -24,6 +24,8 @@ type (
getImageFunc func(ctx context.Context, ref string) (containerd.Image, error)
contentStoreFunc func() content.Store
containerServiceFunc func() containers.Store
installFunc func(context.Context, containerd.Image, ...containerd.InstallOpts) error
versionFunc func(ctx context.Context) (containerd.Version, error)
}
fakeContainer struct {
idFunc func() string
@@ -109,6 +111,18 @@ func (w *fakeContainerdClient) ContainerService() containers.Store {
func (w *fakeContainerdClient) Close() error {
return nil
}
func (w *fakeContainerdClient) Install(ctx context.Context, image containerd.Image, args ...containerd.InstallOpts) error {
if w.installFunc != nil {
return w.installFunc(ctx, image, args...)
}
return nil
}
func (w *fakeContainerdClient) Version(ctx context.Context) (containerd.Version, error) {
if w.versionFunc != nil {
return w.versionFunc(ctx)
}
return containerd.Version{}, nil
}
func (c *fakeContainer) ID() string {
if c.idFunc != nil {
+14
View File
@@ -64,6 +64,12 @@ var (
NoNewPrivileges: false,
},
}
// RuntimeMetadataName is the name of the runtime metadata file
RuntimeMetadataName = "distribution_based_engine"
// ReleaseNotePrefix is where to point users to for release notes
ReleaseNotePrefix = "https://docs.docker.com/releasenotes"
)
type baseClient struct {
@@ -80,4 +86,12 @@ type containerdClient interface {
ContentStore() content.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"`
}
+156 -1
View File
@@ -2,13 +2,22 @@ package containerizedengine
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"path/filepath"
"strings"
"github.com/containerd/containerd"
"github.com/containerd/containerd/content"
"github.com/containerd/containerd/errdefs"
"github.com/containerd/containerd/images"
"github.com/containerd/containerd/namespaces"
clitypes "github.com/docker/cli/types"
"github.com/docker/distribution/reference"
"github.com/docker/docker/api/types"
ver "github.com/hashicorp/go-version"
"github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
)
@@ -34,6 +43,20 @@ func (c *baseClient) DoUpdate(ctx context.Context, opts clitypes.EngineInitOptio
return fmt.Errorf("please pick the version you want to update to")
}
localMetadata, err := c.GetCurrentRuntimeMetadata(ctx, "")
if err == nil {
if opts.EngineImage == "" {
if strings.Contains(strings.ToLower(localMetadata.Platform), "enterprise") {
opts.EngineImage = "engine-enterprise"
} else {
opts.EngineImage = "engine-community"
}
}
}
if opts.EngineImage == "" {
return fmt.Errorf("please pick the engine image to update with (engine-community or engine-enterprise)")
}
imageName := fmt.Sprintf("%s/%s:%s", opts.RegistryPrefix, opts.EngineImage, opts.EngineVersion)
// Look for desired image
@@ -48,5 +71,137 @@ func (c *baseClient) DoUpdate(ctx context.Context, opts clitypes.EngineInitOptio
return errors.Wrapf(err, "unable to check for image %s", imageName)
}
}
return c.cclient.Install(ctx, image, containerd.WithInstallReplace, containerd.WithInstallPath("/usr"))
// Make sure we're safe to proceed
newMetadata, err := c.PreflightCheck(ctx, image)
if err != nil {
return err
}
// Grab current metadata for comparison purposes
if localMetadata != nil {
if localMetadata.Platform != newMetadata.Platform {
fmt.Fprintf(out, "\nNotice: you have switched to \"%s\". Please refer to %s for update instructions.\n\n", newMetadata.Platform, c.GetReleaseNotesURL(imageName))
}
}
err = c.cclient.Install(ctx, image, containerd.WithInstallReplace, containerd.WithInstallPath("/usr"))
if err != nil {
return err
}
return c.WriteRuntimeMetadata(ctx, "", 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
}
func (c *baseClient) WriteRuntimeMetadata(_ context.Context, 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
}
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[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 runtime: %s\nPlease consult the release notes at %s for upgrade instructions", metadata.Runtime, c.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.\nPlease consult the release notes at %s for upgrade instructions",
localVersion.Version, metadata.ContainerdMinVersion, c.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 (c *baseClient) 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", ReleaseNotePrefix, versionTag)
}
+113 -167
View File
@@ -4,6 +4,9 @@ import (
"bytes"
"context"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"testing"
"github.com/containerd/containerd"
@@ -12,162 +15,20 @@ import (
"github.com/docker/cli/cli/command"
clitypes "github.com/docker/cli/types"
"github.com/docker/docker/api/types"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"gotest.tools/assert"
)
func TestGetCurrentEngineVersionHappy(t *testing.T) {
ctx := context.Background()
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) {
ctx := context.Background()
image := &fakeImage{
nameFunc: func() string {
return "docker.io/store/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/store/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) {
func TestActivateConfigFailure(t *testing.T) {
ctx := context.Background()
registryPrefix := "registryprefixgoeshere"
image := &fakeImage{
nameFunc: func() string {
return registryPrefix + "/" + clitypes.EnterpriseEngineImage + ":engineversion"
},
configFunc: func(ctx context.Context) (ocispec.Descriptor, error) {
return ocispec.Descriptor{}, fmt.Errorf("config lookup failure")
},
}
container := &fakeContainer{
imageFunc: func(context.Context) (containerd.Image, error) {
@@ -185,6 +46,9 @@ func TestActivateNoChange(t *testing.T) {
containersFunc: func(ctx context.Context, filters ...string) ([]containerd.Container, error) {
return []containerd.Container{container}, nil
},
getImageFunc: func(ctx context.Context, ref string) (containerd.Image, error) {
return image, nil
},
},
}
opts := clitypes.EngineInitOptions{
@@ -195,7 +59,7 @@ func TestActivateNoChange(t *testing.T) {
}
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) {
@@ -292,30 +156,112 @@ func TestDoUpdatePullFail(t *testing.T) {
assert.ErrorContains(t, err, "pull failure")
}
func TestDoUpdateEngineMissing(t *testing.T) {
func TestActivateDoUpdateVerifyImageName(t *testing.T) {
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{
EngineVersion: "engineversiongoeshere",
RegistryPrefix: "registryprefixgoeshere",
ConfigFile: "/tmp/configfilegoeshere",
EngineImage: "testnamegoeshere",
//EngineImage: clitypes.EnterpriseEngineImage,
}
image := &fakeImage{
nameFunc: func() string {
return "imagenamehere"
},
}
client := baseClient{
cclient: &fakeContainerdClient{
getImageFunc: func(ctx context.Context, ref string) (containerd.Image, error) {
return image, nil
},
containersFunc: func(ctx context.Context, filters ...string) ([]containerd.Container, error) {
return []containerd.Container{}, nil
},
},
}
err := client.DoUpdate(ctx, opts, command.NewOutStream(&bytes.Buffer{}), &types.AuthConfig{}, healthfnHappy)
assert.ErrorContains(t, err, "unable to find existing engine")
tmpdir, err := ioutil.TempDir("", "docker-root")
assert.NilError(t, err)
defer os.RemoveAll(tmpdir)
defaultDockerRoot = tmpdir
metadata := RuntimeMetadata{Platform: "platformgoeshere"}
err = client.WriteRuntimeMetadata(ctx, 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-community", opts.EngineVersion)
assert.Assert(t, requestedImage == expectedImage, "%s != %s", requestedImage, expectedImage)
// Redo with enterprise set
metadata = RuntimeMetadata{Platform: "Docker Engine - Enterprise"}
err = client.WriteRuntimeMetadata(ctx, 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(ctx, 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) {
client := baseClient{}
imageName := "bogus image name #$%&@!"
url := client.GetReleaseNotesURL(imageName)
assert.Equal(t, url, ReleaseNotePrefix+"/")
imageName = "foo.bar/valid/repowithouttag"
url = client.GetReleaseNotesURL(imageName)
assert.Equal(t, url, ReleaseNotePrefix+"/")
imageName = "foo.bar/valid/repowithouttag:tag123"
url = client.GetReleaseNotesURL(imageName)
assert.Equal(t, url, ReleaseNotePrefix+"/tag123")
}
@@ -1,19 +1,23 @@
package containerizedengine
package versions
import (
"context"
"path"
"sort"
"strings"
registryclient "github.com/docker/cli/cli/registry/client"
clitypes "github.com/docker/cli/types"
"github.com/docker/distribution/reference"
"github.com/docker/docker/api/types"
ver "github.com/hashicorp/go-version"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
// GetEngineVersions reports the versions of the engine that are available
func (c *baseClient) GetEngineVersions(ctx context.Context, registryClient registryclient.RegistryClient, currentVersion, imageName string) (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)
if err != nil {
return clitypes.AvailableVersions{}, err
@@ -24,7 +28,23 @@ func (c *baseClient) GetEngineVersions(ctx context.Context, registryClient regis
return clitypes.AvailableVersions{}, err
}
return parseTags(tags, currentVersion)
return parseTags(tags, serverVersion.Version)
}
func getEngineImage(registryPrefix string, serverVersion types.Version) string {
communityImage := "engine-community"
enterpriseImage := "engine-enterprise"
platform := strings.ToLower(serverVersion.Platform.Name)
if platform != "" {
if strings.Contains(platform, "enterprise") {
return path.Join(registryPrefix, enterpriseImage)
}
return path.Join(registryPrefix, communityImage)
}
if strings.Contains(serverVersion.Version, "ee") {
return path.Join(registryPrefix, enterpriseImage)
}
return path.Join(registryPrefix, communityImage)
}
func parseTags(tags []string, currentVersion string) (clitypes.AvailableVersions, error) {
@@ -1,19 +1,19 @@
package containerizedengine
package versions
import (
"context"
"testing"
"github.com/docker/docker/api/types"
"gotest.tools/assert"
)
func TestGetEngineVersionsBadImage(t *testing.T) {
ctx := context.Background()
client := baseClient{}
currentVersion := "currentversiongoeshere"
imageName := "this is an illegal image $%^&"
_, err := client.GetEngineVersions(ctx, nil, currentVersion, imageName)
registryPrefix := "this is an illegal image $%^&"
currentVersion := types.Version{Version: "currentversiongoeshere"}
_, err := GetEngineVersions(ctx, nil, registryPrefix, currentVersion)
assert.ErrorContains(t, err, "invalid reference format")
}