diff --git a/apis/swagger.yml b/apis/swagger.yml index 5263092b3..795275750 100644 --- a/apis/swagger.yml +++ b/apis/swagger.yml @@ -2479,15 +2479,27 @@ definitions: ContainerUpgradeConfig: description: | - ContainerUpgradeConfig is used for API "POST /containers/upgrade". - It wraps all kinds of config used in container upgrade. - It can be used to encode client params in client and unmarshal request body in daemon side. - allOf: - - $ref: "#/definitions/ContainerConfig" - - type: "object" - properties: - HostConfig: - $ref: "#/definitions/HostConfig" + ContainerUpgradeConfig is used for API "POST /containers/{name:.*}/upgrade". when upgrade a container, + we must specify new image used to create a new container, and also can specify `Cmd` and `Entrypoint` for + new container. There is all parameters that upgrade a container, if want to change other parameters, i + think you should use `update` API interface. + required: [Image] + properties: + Image: + type: "string" + x-nullable: false + Cmd: + type: "array" + description: "Execution commands and args" + items: + type: "string" + Entrypoint: + description: | + The entrypoint for the container as a string or an array of strings. + If the array consists of exactly one empty string (`[""]`) then the entry point is reset to system default. + type: "array" + items: + type: "string" LogConfig: description: "The logging configuration for this container" diff --git a/apis/types/container_upgrade_config.go b/apis/types/container_upgrade_config.go index 00bb61400..68b4151ff 100644 --- a/apis/types/container_upgrade_config.go +++ b/apis/types/container_upgrade_config.go @@ -10,78 +10,86 @@ import ( "github.com/go-openapi/errors" "github.com/go-openapi/swag" + "github.com/go-openapi/validate" ) -// ContainerUpgradeConfig ContainerUpgradeConfig is used for API "POST /containers/upgrade". -// It wraps all kinds of config used in container upgrade. -// It can be used to encode client params in client and unmarshal request body in daemon side. +// ContainerUpgradeConfig ContainerUpgradeConfig is used for API "POST /containers/{name:.*}/upgrade". when upgrade a container, +// we must specify new image used to create a new container, and also can specify `Cmd` and `Entrypoint` for +// new container. There is all parameters that upgrade a container, if want to change other parameters, i +// think you should use `update` API interface. // // swagger:model ContainerUpgradeConfig type ContainerUpgradeConfig struct { - ContainerConfig - // host config - HostConfig *HostConfig `json:"HostConfig,omitempty"` + // Execution commands and args + Cmd []string `json:"Cmd"` + + // The entrypoint for the container as a string or an array of strings. + // If the array consists of exactly one empty string (`[""]`) then the entry point is reset to system default. + // + Entrypoint []string `json:"Entrypoint"` + + // image + // Required: true + Image string `json:"Image"` } -// UnmarshalJSON unmarshals this object from a JSON structure -func (m *ContainerUpgradeConfig) UnmarshalJSON(raw []byte) error { +/* polymorph ContainerUpgradeConfig Cmd false */ - var aO0 ContainerConfig - if err := swag.ReadJSON(raw, &aO0); err != nil { - return err - } - m.ContainerConfig = aO0 +/* polymorph ContainerUpgradeConfig Entrypoint false */ - var data struct { - HostConfig *HostConfig `json:"HostConfig,omitempty"` +/* polymorph ContainerUpgradeConfig Image false */ + +// Validate validates this container upgrade config +func (m *ContainerUpgradeConfig) Validate(formats strfmt.Registry) error { + var res []error + + if err := m.validateCmd(formats); err != nil { + // prop + res = append(res, err) } - if err := swag.ReadJSON(raw, &data); err != nil { - return err + + if err := m.validateEntrypoint(formats); err != nil { + // prop + res = append(res, err) } - m.HostConfig = data.HostConfig + if err := m.validateImage(formats); err != nil { + // prop + res = append(res, err) + } + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } return nil } -// MarshalJSON marshals this object to a JSON structure -func (m ContainerUpgradeConfig) MarshalJSON() ([]byte, error) { - var _parts [][]byte +func (m *ContainerUpgradeConfig) validateCmd(formats strfmt.Registry) error { - aO0, err := swag.WriteJSON(m.ContainerConfig) - if err != nil { - return nil, err + if swag.IsZero(m.Cmd) { // not required + return nil } - _parts = append(_parts, aO0) - var data struct { - HostConfig *HostConfig `json:"HostConfig,omitempty"` - } + return nil +} - data.HostConfig = m.HostConfig +func (m *ContainerUpgradeConfig) validateEntrypoint(formats strfmt.Registry) error { - jsonData, err := swag.WriteJSON(data) - if err != nil { - return nil, err + if swag.IsZero(m.Entrypoint) { // not required + return nil } - _parts = append(_parts, jsonData) - return swag.ConcatJSON(_parts...), nil + return nil } -// Validate validates this container upgrade config -func (m *ContainerUpgradeConfig) Validate(formats strfmt.Registry) error { - var res []error +func (m *ContainerUpgradeConfig) validateImage(formats strfmt.Registry) error { - if err := m.ContainerConfig.Validate(formats); err != nil { - res = append(res, err) + if err := validate.RequiredString("Image", "body", string(m.Image)); err != nil { + return err } - if len(res) > 0 { - return errors.CompositeValidationError(res...) - } return nil } diff --git a/cli/upgrade.go b/cli/upgrade.go index 801a0c931..8ae443097 100644 --- a/cli/upgrade.go +++ b/cli/upgrade.go @@ -3,24 +3,31 @@ package main import ( "context" "fmt" + "strings" + + "github.com/alibaba/pouch/apis/types" "github.com/spf13/cobra" ) // upgradeDescription is used to describe upgrade command in detail and auto generate command doc. -var upgradeDescription = "Upgrade a container with new image and args" +var upgradeDescription = "upgrade is a feature to replace a container's image." + + "You can specify the new Entrypoint and Cmd for the new container. When you want to update" + + "a container's image, but inherit the network and volumes of the old container, then you should" + + "think about the upgrade feature." // UpgradeCommand use to implement 'upgrade' command, it is used to upgrade a container. type UpgradeCommand struct { baseCommand - *container + entrypoint string + image string } // Init initialize upgrade command. func (ug *UpgradeCommand) Init(c *Cli) { ug.cli = c ug.cmd = &cobra.Command{ - Use: "upgrade [OPTIONS] IMAGE [COMMAND] [ARG...]", + Use: "upgrade [OPTIONS] CONTAINER [COMMAND] [ARG...]", Short: "Upgrade a container with new image and args", Long: upgradeDescription, Args: cobra.MinimumNArgs(1), @@ -36,47 +43,52 @@ func (ug *UpgradeCommand) Init(c *Cli) { func (ug *UpgradeCommand) addFlags() { flagSet := ug.cmd.Flags() flagSet.SetInterspersed(false) - - c := addCommonFlags(flagSet) - ug.container = c + flagSet.StringVar(&ug.entrypoint, "entrypoint", "", "Overwrite the default ENTRYPOINT of the image") + flagSet.StringVar(&ug.image, "image", "", "Specify image of the new container") } // runUpgrade is the entry of UpgradeCommand command. func (ug *UpgradeCommand) runUpgrade(args []string) error { - config, err := ug.config() - if err != nil { - return fmt.Errorf("failed to upgrade container: %v", err) - } + var cmd []string - config.Image = args[0] + name := args[0] + if name == "" { + return fmt.Errorf("failed to upgrade container: must specify container name") + } if len(args) > 1 { - config.Cmd = args[1:] + cmd = args[1:] } - containerName := ug.name - if containerName == "" { - return fmt.Errorf("failed to upgrade container: must specify container name") + image := ug.image + if image == "" { + return fmt.Errorf("failed to upgrade container: must specify new image") + } + + upgradeConfig := &types.ContainerUpgradeConfig{ + Image: image, + Cmd: cmd, + Entrypoint: strings.Fields(ug.entrypoint), } ctx := context.Background() apiClient := ug.cli.Client() - if err := pullMissingImage(ctx, apiClient, config.Image, false); err != nil { + if err := pullMissingImage(ctx, apiClient, image, false); err != nil { return err } - err = apiClient.ContainerUpgrade(ctx, containerName, config.ContainerConfig, config.HostConfig) - if err == nil { - fmt.Println(containerName) + if err := apiClient.ContainerUpgrade(ctx, name, upgradeConfig); err != nil { + return err } - return err + fmt.Println(name) + return nil } //upgradeExample shows examples in exec command, and is used in auto-generated cli docs. func upgradeExample() string { - return ` $ pouch run -d -m 20m --name test1 registry.hub.docker.com/library/busybox:latest + return ` $ pouch run -d -m 20m --name test registry.hub.docker.com/library/busybox:latest 4c58d27f58d38776dda31c01c897bbf554c802a9b80ae4dc20be1337f8a969f2 -$ pouch upgrade --name test1 registry.hub.docker.com/library/hello-world:latest +$ pouch upgrade --image registry.hub.docker.com/library/hello-world:latest test test1` } diff --git a/client/container_upgrade.go b/client/container_upgrade.go index 24d97aeef..b15959f04 100644 --- a/client/container_upgrade.go +++ b/client/container_upgrade.go @@ -8,12 +8,8 @@ import ( ) // ContainerUpgrade upgrade a container with new image and args. -func (client *APIClient) ContainerUpgrade(ctx context.Context, name string, config types.ContainerConfig, hostConfig *types.HostConfig) error { - upgradeConfig := types.ContainerUpgradeConfig{ - ContainerConfig: config, - HostConfig: hostConfig, - } - resp, err := client.post(ctx, "/containers/"+name+"/upgrade", url.Values{}, upgradeConfig, nil) +func (client *APIClient) ContainerUpgrade(ctx context.Context, name string, config *types.ContainerUpgradeConfig) error { + resp, err := client.post(ctx, "/containers/"+name+"/upgrade", url.Values{}, config, nil) ensureCloseReader(resp) return err diff --git a/client/interface.go b/client/interface.go index e6d387f2e..b0a8a50ea 100644 --- a/client/interface.go +++ b/client/interface.go @@ -36,7 +36,7 @@ type ContainerAPIClient interface { ContainerPause(ctx context.Context, name string) error ContainerUnpause(ctx context.Context, name string) error ContainerUpdate(ctx context.Context, name string, config *types.UpdateConfig) error - ContainerUpgrade(ctx context.Context, name string, config types.ContainerConfig, hostConfig *types.HostConfig) error + ContainerUpgrade(ctx context.Context, name string, config *types.ContainerUpgradeConfig) error ContainerTop(ctx context.Context, name string, arguments []string) (types.ContainerProcessList, error) ContainerLogs(ctx context.Context, name string, options types.ContainerLogsOptions) (io.ReadCloser, error) ContainerResize(ctx context.Context, name, height, width string) error diff --git a/cri/v1alpha1/cri_utils.go b/cri/v1alpha1/cri_utils.go index f2a2ccc94..1160a7af8 100644 --- a/cri/v1alpha1/cri_utils.go +++ b/cri/v1alpha1/cri_utils.go @@ -897,7 +897,7 @@ func (c *CriManager) getContainerMetrics(ctx context.Context, meta *mgr.Containe } // snapshot key may not equals container ID later - sn, err := c.SnapshotStore.Get(meta.ID) + sn, err := c.SnapshotStore.Get(meta.SnapshotID) if err == nil { usedBytes = sn.Size inodesUsed = sn.Inodes diff --git a/cri/v1alpha2/cri_utils.go b/cri/v1alpha2/cri_utils.go index 1b3943594..89f32e397 100644 --- a/cri/v1alpha2/cri_utils.go +++ b/cri/v1alpha2/cri_utils.go @@ -953,7 +953,7 @@ func (c *CriManager) getContainerMetrics(ctx context.Context, meta *mgr.Containe return nil, fmt.Errorf("failed to get metadata of container %q: %v", meta.ID, err) } - sn, err := c.SnapshotStore.Get(meta.ID) + sn, err := c.SnapshotStore.Get(meta.SnapshotID) if err == nil { usedBytes = sn.Size inodesUsed = sn.Inodes diff --git a/ctrd/container.go b/ctrd/container.go index fd5ac4347..dbed7c221 100644 --- a/ctrd/container.go +++ b/ctrd/container.go @@ -490,10 +490,10 @@ func (c *Client) createContainer(ctx context.Context, ref, id, checkpointDir str rootFSPath = container.BaseFS } else { // containers created by pouch must first create snapshot // check snapshot exist or not. - if _, err := c.GetSnapshot(ctx, id); err != nil { + if _, err := c.GetSnapshot(ctx, container.SnapshotID); err != nil { return errors.Wrapf(err, "failed to create container %s", id) } - options = append(options, containerd.WithSnapshot(id)) + options = append(options, containerd.WithSnapshot(container.SnapshotID)) } // specify Spec for new container diff --git a/ctrd/container_types.go b/ctrd/container_types.go index b9e09dbdc..52a16f266 100644 --- a/ctrd/container_types.go +++ b/ctrd/container_types.go @@ -11,12 +11,13 @@ import ( // then create container by specifying the snapshot; // The other is create container by specify container rootfs, we use `RootFSProvided` flag to mark it, type Container struct { - ID string - Image string - Runtime string - Labels map[string]string - IO *containerio.IO - Spec *specs.Spec + ID string + Image string + Runtime string + Labels map[string]string + IO *containerio.IO + Spec *specs.Spec + SnapshotID string // BaseFS is rootfs used by containerd container BaseFS string diff --git a/daemon/mgr/container.go b/daemon/mgr/container.go index bc2414cce..f156ab45c 100644 --- a/daemon/mgr/container.go +++ b/daemon/mgr/container.go @@ -33,12 +33,10 @@ import ( "github.com/containerd/cgroups" containerdtypes "github.com/containerd/containerd/api/types" - "github.com/containerd/containerd/errdefs" "github.com/containerd/containerd/mount" "github.com/docker/docker/pkg/stdcopy" "github.com/docker/libnetwork" "github.com/go-openapi/strfmt" - "github.com/imdario/mergo" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "github.com/pkg/errors" "github.com/sirupsen/logrus" @@ -287,6 +285,7 @@ func (mgr *ContainerManager) Create(ctx context.Context, name string, config *ty if err != nil { return nil, err } + config.Image = primaryRef.String() // TODO: check request validate. if config.HostConfig == nil { @@ -322,9 +321,9 @@ func (mgr *ContainerManager) Create(ctx context.Context, name string, config *ty return nil, errors.Wrapf(errtypes.ErrInvalidParam, "unknown runtime %s", config.HostConfig.Runtime) } - config.Image = primaryRef.String() + snapID := id // create a snapshot with image. - if err := mgr.Client.CreateSnapshot(ctx, id, config.Image); err != nil { + if err := mgr.Client.CreateSnapshot(ctx, snapID, config.Image); err != nil { return nil, err } cleanups = append(cleanups, func() error { @@ -358,26 +357,19 @@ func (mgr *ContainerManager) Create(ctx context.Context, name string, config *ty Config: &config.ContainerConfig, Created: time.Now().UTC().Format(utils.TimeLayout), HostConfig: config.HostConfig, + SnapshotID: snapID, } // merge image's config into container if err := container.merge(func() (ocispec.ImageConfig, error) { - img, err := mgr.Client.GetImage(ctx, config.Image) - if err != nil { - return ocispec.ImageConfig{}, err - } - ociImage, err := containerdImageToOciImage(ctx, img) - if err != nil { - return ocispec.ImageConfig{}, err - } - return ociImage.Config, nil + return mgr.ImageMgr.GetOCIImageConfig(ctx, config.Image) }); err != nil { return nil, err } // set container basefs, basefs is not created in pouchd, it will created // after create options passed to containerd. - mgr.setBaseFS(ctx, container, id) + mgr.setBaseFS(ctx, container) // init container storage module, such as: set volumes, set diskquota, set /etc/mtab, copy image's data to volume. if err := mgr.initContainerStorage(ctx, container); err != nil { @@ -675,6 +667,7 @@ func (mgr *ContainerManager) createContainerdContainer(ctx context.Context, c *C IO: io, RootFSProvided: c.RootFSProvided, BaseFS: c.BaseFS, + SnapshotID: c.SnapshotID, } c.Unlock() @@ -1080,7 +1073,7 @@ func (mgr *ContainerManager) Remove(ctx context.Context, name string, options *t if err := mount.Unmount(c.BaseFS, 0); err != nil { logrus.Errorf("failed to umount rootfs when remove the container %s: %v", c.ID, err) } - } else if err := mgr.Client.RemoveSnapshot(ctx, c.ID); err != nil { + } else if err := mgr.Client.RemoveSnapshot(ctx, c.SnapshotKey()); err != nil { // if the container is created by normal method, remove the // snapshot when delete it. logrus.Errorf("failed to remove snapshot of container %s: %v", c.ID, err) @@ -1229,132 +1222,6 @@ func (mgr *ContainerManager) updateContainerResources(c *Container, resources ty return nil } -// Upgrade upgrades a container with new image and args. -func (mgr *ContainerManager) Upgrade(ctx context.Context, name string, config *types.ContainerUpgradeConfig) error { - c, err := mgr.container(name) - if err != nil { - return err - } - - // check the image existed or not, and convert image id to image ref - _, _, primaryRef, err := mgr.ImageMgr.CheckReference(ctx, config.Image) - if err != nil { - return errors.Wrap(err, "failed to get image") - } - config.Image = primaryRef.String() - - // Nothing changed, no need upgrade. - if config.Image == c.Config.Image { - return fmt.Errorf("failed to upgrade container: image not changed") - } - - var ( - needRollback = false - // FIXME: do a deep copy of container? - backupContainer = c - ) - - defer func() { - if needRollback { - c = backupContainer - if err := mgr.Client.CreateSnapshot(ctx, c.ID, c.Image); err != nil { - logrus.Errorf("failed to create snapshot when rollback upgrade action: %v", err) - return - } - // FIXME: create new containerd container may failed - _ = mgr.createContainerdContainer(ctx, c, "", "") - } - }() - - // FIXME(ziren): mergo.Merge() use AppendSlice to merge slice. - // that is to say, t1 = ["a", "b"], t2 = ["a", "c"], the merge - // result will be ["a", "b", "a", "c"] - // This may occur errors, just take notes to record this. - if err := mergo.MergeWithOverwrite(c.Config, config.ContainerConfig); err != nil { - return errors.Wrapf(err, "failed to merge ContainerConfig") - } - if err := mergo.MergeWithOverwrite(c.HostConfig, config.HostConfig); err != nil { - return errors.Wrapf(err, "failed to merge HostConfig") - } - c.Image = config.Image - - // If container is not running, we just store this data. - if c.State.Status != types.StatusRunning { - // Works fine, store new container info to disk. - if err := c.Write(mgr.Store); err != nil { - logrus.Errorf("failed to update container %s in meta store: %v", c.ID, err) - return err - } - return nil - } - // If container is running, we need change - // configuration and recreate it. Else we just store new meta - // into disk, next time when starts container, the new configurations - // will take effect. - - // Inherit volume configurations from old container, - // New volume configurations may cover the old one. - // c.VolumesFrom = []string{c.ID} - - // FIXME(ziren): here will forcely stop container afer 3s. - // If DestroyContainer failed, we think the old container - // not changed, so just return error, no need recover it. - if _, err := mgr.Client.DestroyContainer(ctx, c.ID, 3); err != nil { - return errors.Wrapf(err, "failed to destroy container %s", c.ID) - } - - // remove snapshot of old container - if err := mgr.Client.RemoveSnapshot(ctx, c.ID); err != nil { - return errors.Wrapf(err, "failed to remove snapshot of container %s", c.ID) - } - - // wait util old snapshot to be deleted - wait := make(chan struct{}) - go func() { - for { - // FIXME(ziren) Ensure the removed snapshot be removed - // by garbage collection. - time.Sleep(100 * time.Millisecond) - - _, err := mgr.Client.GetSnapshot(ctx, c.ID) - if err != nil && errdefs.IsNotFound(err) { - close(wait) - return - } - } - }() - - select { - case <-wait: - // TODO delete snapshot succeeded - case <-time.After(30 * time.Second): - needRollback = true - return fmt.Errorf("failed to deleted old snapshot: wait old snapshot %s to be deleted timeout(30s)", c.ID) - } - - // create a snapshot with image for new container. - if err := mgr.Client.CreateSnapshot(ctx, c.ID, config.Image); err != nil { - needRollback = true - return errors.Wrap(err, "failed to create snapshot") - } - - if err := mgr.createContainerdContainer(ctx, c, "", ""); err != nil { - needRollback = true - return errors.Wrap(err, "failed to create new container") - } - - // Upgrade succeeded, refresh the cache - mgr.cache.Put(c.ID, c) - - // Works fine, store new container info to disk. - if err := c.Write(mgr.Store); err != nil { - logrus.Errorf("failed to update container %s in meta store: %v", c.ID, err) - return err - } - - return nil -} - // Top lists the processes running inside of the given container func (mgr *ContainerManager) Top(ctx context.Context, name string, psArgs string) (*types.ContainerProcessList, error) { if psArgs == "" { @@ -1953,16 +1820,17 @@ func (mgr *ContainerManager) buildContainerEndpoint(c *Container) *networktypes. } // setBaseFS keeps container basefs in meta. -func (mgr *ContainerManager) setBaseFS(ctx context.Context, c *Container, id string) { - info, err := mgr.Client.GetSnapshot(ctx, id) +func (mgr *ContainerManager) setBaseFS(ctx context.Context, c *Container) { + snapshotID := c.SnapshotKey() + _, err := mgr.Client.GetSnapshot(ctx, snapshotID) if err != nil { - logrus.Infof("failed to get container %s snapshot", id) + logrus.Errorf("failed to get container %s snapshot %s: %v", c.Key(), snapshotID, err) return } // io.containerd.runtime.v1.linux as a const used by runc c.Lock() - c.BaseFS = filepath.Join(mgr.Config.HomeDir, "containerd/state", "io.containerd.runtime.v1.linux", mgr.Config.DefaultNamespace, info.Name, "rootfs") + c.BaseFS = filepath.Join(mgr.Config.HomeDir, "containerd/state", "io.containerd.runtime.v1.linux", mgr.Config.DefaultNamespace, c.ID, "rootfs") c.Unlock() } diff --git a/daemon/mgr/container_types.go b/daemon/mgr/container_types.go index 2409086fe..b1d9c373c 100644 --- a/daemon/mgr/container_types.go +++ b/daemon/mgr/container_types.go @@ -261,6 +261,9 @@ type Container struct { // MountFS is used to mark the directory of mount overlayfs for pouch daemon to operate the image. MountFS string `json:"-"` + + // SnapshotID specify id of the snapshot that container using. + SnapshotID string } // Key returns container's id. @@ -270,6 +273,25 @@ func (c *Container) Key() string { return c.ID } +// SnapshotKey returns id of container's snapshot +func (c *Container) SnapshotKey() string { + c.Lock() + defer c.Unlock() + // for old container, SnapshotKey equals to Container ID + if c.SnapshotID == "" { + return c.ID + } + + return c.SnapshotID +} + +// SetSnapshotID sets the snapshot id of container +func (c *Container) SetSnapshotID(snapID string) { + c.Lock() + defer c.Unlock() + c.SnapshotID = snapID +} + // Write writes container's meta data into meta store. func (c *Container) Write(store *meta.Store) error { return store.Put(c) diff --git a/daemon/mgr/container_upgrade.go b/daemon/mgr/container_upgrade.go new file mode 100644 index 000000000..1aaa312d8 --- /dev/null +++ b/daemon/mgr/container_upgrade.go @@ -0,0 +1,197 @@ +package mgr + +import ( + "context" + "fmt" + + "github.com/alibaba/pouch/apis/types" + "github.com/alibaba/pouch/pkg/utils" + + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" +) + +// Upgrade a container with new image and args. when upgrade a container, +// we only support specify cmd and entrypoint. if you want to change other +// parameters of the container, you should think about the update API first. +func (mgr *ContainerManager) Upgrade(ctx context.Context, name string, config *types.ContainerUpgradeConfig) error { + c, err := mgr.container(name) + if err != nil { + return err + } + + var ( + needRollback = false + oldConfig = *c.Config + oldHostconfig = *c.HostConfig + oldImage = c.Image + ) + + defer func() { + if !needRollback { + return + } + + c.Lock() + // recover old container config + c.Config = &oldConfig + c.HostConfig = &oldHostconfig + c.Image = oldImage + c.Unlock() + + if err := mgr.createContainerdContainer(ctx, c, "", ""); err != nil { + logrus.Errorf("failed to rollback upgrade action: %s", err.Error()) + if err := mgr.markStoppedAndRelease(c, nil); err != nil { + logrus.Errorf("failed to mark container %s stop status: %s", c.ID, err.Error()) + } + } + }() + + // merge image config to container config + if err := mgr.mergeImageConfigForUpgrade(ctx, c, config); err != nil { + return errors.Wrap(err, "failed to upgrade container") + } + + // if the container is running, we need first stop it. + if c.IsRunning() { + if _, err := mgr.Client.DestroyContainer(ctx, c.Key(), 10); err != nil { + return errors.Wrapf(err, "failed to destroy container") + } + } + + // prepare new snapshot for the new container + oldSnapID := c.SnapshotKey() + newSnapID, err := mgr.prepareSnapshotForUpgrade(ctx, c.Key(), c.SnapshotKey(), config.Image) + if err != nil { + return err + } + c.SetSnapshotID(newSnapID) + + // initialize container storage config before container started + if err := mgr.initContainerStorage(ctx, c); err != nil { + return errors.Wrapf(err, "failed to init container storage, id: (%s)", c.Key()) + } + + // If container is running, we also should start the container + // after recreate it. + if c.IsRunning() { + if err := mgr.createContainerdContainer(ctx, c, "", ""); err != nil { + needRollback = true + if err := mgr.Client.RemoveSnapshot(ctx, newSnapID); err != nil { + logrus.Errorf("failed to remove snapshot %s: %v", newSnapID, err) + } + + return errors.Wrap(err, "failed to create new container") + } + } + + // Upgrade success, remove snapshot of old container + if err := mgr.Client.RemoveSnapshot(ctx, oldSnapID); err != nil { + // TODO(ziren): remove old snapshot failed, may cause dirty data + logrus.Errorf("failed to remove snapshot %s: %v", oldSnapID, err) + } + + // Upgrade succeeded, refresh the cache + mgr.cache.Put(c.ID, c) + + // Works fine, store new container info to disk. + if err := c.Write(mgr.Store); err != nil { + logrus.Errorf("failed to update container %s in meta store: %v", c.ID, err) + return err + } + + return nil +} + +func (mgr *ContainerManager) prepareContainerEntrypointForUpgrade(ctx context.Context, c *Container, config *types.ContainerUpgradeConfig) error { + // first, use the entrypoint specified by ContainerUpgradeConfig + if len(config.Entrypoint) > 0 { + return nil + } + + // secondly, use the entrypoint of the old container + // because of the entrypoints of old container's CreateConfig and old image being merged, + // we cannot decide the old container's entrypoint belongs which, so just check if the old + // container's entrypoint is different with the old image. + c.Lock() + defer c.Unlock() + + oldImgConfig, err := mgr.ImageMgr.GetOCIImageConfig(ctx, c.Config.Image) + if err != nil { + return err + } + if (c.Config.Entrypoint == nil && oldImgConfig.Entrypoint == nil) || !utils.StringSliceEqual(c.Config.Entrypoint, oldImgConfig.Entrypoint) { + config.Entrypoint = c.Config.Entrypoint + if len(config.Cmd) == 0 { + config.Cmd = c.Config.Cmd + } + + return nil + } + + // thirdly, use the entrypoint of the new image + newImgConfig, err := mgr.ImageMgr.GetOCIImageConfig(ctx, config.Image) + if err != nil { + return err + } + config.Entrypoint = newImgConfig.Entrypoint + if len(config.Cmd) == 0 { + config.Cmd = newImgConfig.Cmd + } + return nil +} + +func (mgr *ContainerManager) prepareSnapshotForUpgrade(ctx context.Context, cID, oldSnapID, image string) (string, error) { + newSnapID := "" + // get a ID for the new snapshot + for { + newSnapID = utils.RandString(8, cID, "") + if newSnapID != oldSnapID { + break + } + } + + // create a snapshot with image for new container. + if err := mgr.Client.CreateSnapshot(ctx, newSnapID, image); err != nil { + return "", errors.Wrap(err, "failed to create snapshot") + } + + return newSnapID, nil +} + +func (mgr *ContainerManager) mergeImageConfigForUpgrade(ctx context.Context, c *Container, config *types.ContainerUpgradeConfig) error { + // check the image existed or not, and convert image id to image ref + imgID, _, primaryRef, err := mgr.ImageMgr.CheckReference(ctx, config.Image) + if err != nil { + return errors.Wrap(err, "failed to get image") + } + + config.Image = primaryRef.String() + // Nothing changed, no need upgrade. + if config.Image == c.Config.Image { + return fmt.Errorf("failed to upgrade container: image not changed") + } + + // prepare entrypoint for the new container of upgrade + if err := mgr.prepareContainerEntrypointForUpgrade(ctx, c, config); err != nil { + return errors.Wrap(err, "failed to check entrypoint for container upgrade") + } + + // merge image's config into the new container of upgrade + if err := c.merge(func() (ocispec.ImageConfig, error) { + return mgr.ImageMgr.GetOCIImageConfig(ctx, config.Image) + }); err != nil { + return errors.Wrap(err, "failed to merge image config when upgrade container") + } + + // set image and entrypoint for new container. + c.Lock() + c.Image = imgID.String() + c.Config.Image = config.Image + c.Config.Entrypoint = config.Entrypoint + c.Config.Cmd = config.Cmd + c.Unlock() + + return nil +} diff --git a/daemon/mgr/image.go b/daemon/mgr/image.go index 92d0e9b91..5a4e548b6 100644 --- a/daemon/mgr/image.go +++ b/daemon/mgr/image.go @@ -66,6 +66,9 @@ type ImageMgr interface { // StoreImageReference update image reference. StoreImageReference(ctx context.Context, img containerd.Image) error + + // GetOCIImageConfig returns the image config of OCI + GetOCIImageConfig(ctx context.Context, image string) (ocispec.ImageConfig, error) } // ImageManager is an implementation of interface ImageMgr. @@ -420,6 +423,19 @@ func (mgr *ImageManager) ListReferences(ctx context.Context, imageID digest.Dige return mgr.localStore.GetPrimaryReferences(imageID), nil } +// GetOCIImageConfig returns the image config of OCI +func (mgr *ImageManager) GetOCIImageConfig(ctx context.Context, image string) (ocispec.ImageConfig, error) { + img, err := mgr.client.GetImage(ctx, image) + if err != nil { + return ocispec.ImageConfig{}, err + } + ociImage, err := containerdImageToOciImage(ctx, img) + if err != nil { + return ocispec.ImageConfig{}, err + } + return ociImage.Config, nil +} + // updateLocalStore updates the local store. func (mgr *ImageManager) updateLocalStore() error { ctx, cancel := context.WithTimeout(context.Background(), deadlineLoadImagesAtBootup) diff --git a/pkg/utils/random_string.go b/pkg/utils/random_string.go new file mode 100644 index 000000000..1d8ffdd88 --- /dev/null +++ b/pkg/utils/random_string.go @@ -0,0 +1,29 @@ +package utils + +import ( + "math/rand" + "time" +) + +const ( + letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ123456789" + stringSeparator = "_" +) + +// RandString is a thread/goroutine safe solution to generate a random string +// of a fixed length +func RandString(n int, prefix, suffix string) string { + r := rand.New(rand.NewSource(time.Now().UnixNano())) + b := make([]byte, n) + for i := range b { + b[i] = letterBytes[r.Intn(len(letterBytes))] + } + randStr := string(b) + if prefix != "" { + randStr = prefix + stringSeparator + randStr + } + if suffix != "" { + randStr += stringSeparator + suffix + } + return randStr +} diff --git a/pkg/utils/random_string_test.go b/pkg/utils/random_string_test.go new file mode 100644 index 000000000..47ca50a10 --- /dev/null +++ b/pkg/utils/random_string_test.go @@ -0,0 +1,24 @@ +package utils + +import ( + "testing" + "time" + + "github.com/sirupsen/logrus" +) + +func TestRandString(t *testing.T) { + start := time.Now() + results := []string{} + for i := 0; i < 1000; i++ { + str := RandString(8, "", "") + if StringInSlice(results, str) { + t.Errorf("RandString got a same random string in the test: %s", str) + } + + results = append(results, str) + } + end := time.Now() + elapsed := end.Sub(start) + logrus.Infof("TestRandString generate 1000 random strings costs: %v s", elapsed.Seconds()) +} diff --git a/test/cli_upgrade_test.go b/test/cli_upgrade_test.go index f51bd0f18..5733b2419 100644 --- a/test/cli_upgrade_test.go +++ b/test/cli_upgrade_test.go @@ -2,9 +2,6 @@ package main import ( "encoding/json" - "os" - "os/exec" - "reflect" "strings" "github.com/alibaba/pouch/apis/types" @@ -29,7 +26,7 @@ func (suite *PouchUpgradeSuite) SetUpSuite(c *check.C) { environment.PruneAllContainers(apiClient) PullImage(c, busyboxImage) - command.PouchRun("pull", busyboxImage125).Assert(c, icmd.Success) + PullImage(c, busyboxImage125) } // TearDownTest does cleanup work in the end of each test. @@ -41,16 +38,28 @@ func (suite *PouchUpgradeSuite) TeadDownTest(c *check.C) { func (suite *PouchUpgradeSuite) TestPouchUpgrade(c *check.C) { name := "TestPouchUpgrade" - res := command.PouchRun("run", "-d", "--name", name, busyboxImage, "top") + command.PouchRun("run", "-d", "--name", name, busyboxImage, "top").Assert(c, icmd.Success) defer DelContainerForceMultyTime(c, name) - res.Assert(c, icmd.Success) - res = command.PouchRun("upgrade", "--name", name, busyboxImage125) + res := command.PouchRun("upgrade", "--image", busyboxImage125, name) res.Assert(c, icmd.Success) - if out := res.Combined(); !strings.Contains(out, name) { c.Fatalf("unexpected output: %s, expected: %s", out, name) } + + // check if the new container is running after upgade a running container + output := command.PouchRun("inspect", name).Stdout() + result := []types.ContainerJSON{} + if err := json.Unmarshal([]byte(output), &result); err != nil { + c.Errorf("failed to decode inspect output: %v", err) + } + c.Assert(result[0].State.Running, check.Equals, true) + + // double check if container is running by executing a exec command + out := command.PouchRun("exec", name, "echo", "test").Stdout() + if !strings.Contains(out, "test") { + c.Errorf("failed to exec in container, expected test got %s", out) + } } // TestPouchUpgradeNoChange is to verify pouch upgrade command with same image. @@ -61,7 +70,7 @@ func (suite *PouchUpgradeSuite) TestPouchUpgradeNoChange(c *check.C) { defer DelContainerForceMultyTime(c, name) res.Assert(c, icmd.Success) - res = command.PouchRun("upgrade", "--name", name, busyboxImage) + res = command.PouchRun("upgrade", "--image", busyboxImage, name) c.Assert(res.Stderr(), check.NotNil) expectedStr := "failed to upgrade container: image not changed" @@ -74,96 +83,71 @@ func (suite *PouchUpgradeSuite) TestPouchUpgradeNoChange(c *check.C) { func (suite *PouchUpgradeSuite) TestPouchUpgradeStoppedContainer(c *check.C) { name := "TestPouchUpgradeStoppedContainer" - res := command.PouchRun("create", "--name", name, busyboxImage) + command.PouchRun("run", "-d", "--name", name, busyboxImage, "top").Assert(c, icmd.Success) defer DelContainerForceMultyTime(c, name) - res.Assert(c, icmd.Success) - res = command.PouchRun("upgrade", "--name", name, busyboxImage125) + command.PouchRun("stop", name).Assert(c, icmd.Success) + + res := command.PouchRun("upgrade", "--image", busyboxImage125, name) res.Assert(c, icmd.Success) if out := res.Combined(); !strings.Contains(out, name) { c.Fatalf("unexpected output: %s, expected %s", out, name) } - command.PouchRun("start", name).Assert(c, icmd.Success) -} - -// TestPouchUpgradeContainerMemCpu is to verify pouch upgrade container's memory -func (suite *PouchUpgradeSuite) TestPouchUpgradeContainerMemCpu(c *check.C) { - name := "TestPouchUpgradeContainerMemCpu" - - res := command.PouchRun("run", "-d", "-m", "300m", - "--cpu-shares", "20", "--name", name, busyboxImage, "top") - defer DelContainerForceMultyTime(c, name) - res.Assert(c, icmd.Success) - - command.PouchRun("upgrade", "-m", "500m", - "--cpu-shares", "40", "--name", name, busyboxImage125).Assert(c, icmd.Success) - + // check if the new container is running after upgade a running container output := command.PouchRun("inspect", name).Stdout() result := []types.ContainerJSON{} if err := json.Unmarshal([]byte(output), &result); err != nil { c.Errorf("failed to decode inspect output: %v", err) } - containerID := result[0].ID - - // Check if metajson has changed - c.Assert(result[0].HostConfig.Memory, check.Equals, int64(524288000)) - c.Assert(result[0].HostConfig.CPUShares, check.Equals, int64(40)) + c.Assert(result[0].State.Status, check.Equals, types.StatusStopped) - // Check if cgroup file has changed - memFile := "/sys/fs/cgroup/memory/default/" + containerID + "/memory.limit_in_bytes" - if _, err := os.Stat(memFile); err != nil { - c.Fatalf("container %s cgroup mountpoint not exists", containerID) - } - - out, err := exec.Command("cat", memFile).Output() - if err != nil { - c.Fatalf("execute cat command failed: %v", err) - } - - if !strings.Contains(string(out), "524288000") { - c.Fatalf("unexpected output %s expected %s\n", string(out), "524288000") - } - - cpuFile := "/sys/fs/cgroup/cpu/default/" + containerID + "/cpu.shares" - if _, err := os.Stat(cpuFile); err != nil { - c.Fatalf("container %s cgroup mountpoint not exists", containerID) - } + command.PouchRun("start", name).Assert(c, icmd.Success) +} - out, err = exec.Command("cat", cpuFile).Output() - if err != nil { - c.Fatalf("execute cat command failed: %v", err) - } +// TestPouchUpgradeWithDifferentImage is to verify pouch upgrade command. +func (suite *PouchUpgradeSuite) TestPouchUpgradeWithDifferentImage(c *check.C) { + name := "TestPouchUpgradeWithDifferentImage" + command.PouchRun("run", "-d", "--name", name, busyboxImage).Assert(c, icmd.Success) + defer DelContainerForceMultyTime(c, name) - if !strings.Contains(string(out), "40") { - c.Fatalf("unexpected output %s expected %s\n", string(out), "40") + res := command.PouchRun("upgrade", "--image", helloworldImage, name) + c.Assert(res.Error, check.IsNil) + if out := res.Combined(); !strings.Contains(out, name) { + c.Fatalf("unexpected output: %s, expected: %s", out, name) } } -// TestPouchUpgradeContainerLabels is to verify pouch upgrade container's labels -func (suite *PouchUpgradeSuite) TestPouchUpgradeContainerLabels(c *check.C) { - name := "TestPouchUpgradeContainerLabels" +// TestPouchUpgradeCheckVolume is to verify if inherit old container's volume +// after upgrade a container +func (suite *PouchUpgradeSuite) TestPouchUpgradeCheckVolume(c *check.C) { + name := "TestPouchUpgradeCheckVolume" - res := command.PouchRun("run", "-d", "--label", "test=foo", "--name", name, busyboxImage, "top") + // create container with a /data volume + command.PouchRun("run", "-d", "-v", "/data", "--name", name, busyboxImage, "top").Assert(c, icmd.Success) defer DelContainerForceMultyTime(c, name) - res.Assert(c, icmd.Success) - command.PouchRun("upgrade", "--label", "test1=bar", - "--name", name, busyboxImage125).Assert(c, icmd.Success) + // create a file in volume and write some data to the file + command.PouchRun("exec", name, "sh", "-c", "echo '5678' >> /data/test").Assert(c, icmd.Success) + + res := command.PouchRun("upgrade", "--image", busyboxImage125, name) + res.Assert(c, icmd.Success) + if out := res.Combined(); !strings.Contains(out, name) { + c.Fatalf("unexpected output: %s, expected: %s", out, name) + } + // check if the new container is running after upgade a running container output := command.PouchRun("inspect", name).Stdout() result := []types.ContainerJSON{} if err := json.Unmarshal([]byte(output), &result); err != nil { c.Errorf("failed to decode inspect output: %v", err) } + c.Assert(result[0].State.Running, check.Equals, true) - labels := map[string]string{ - "test": "foo", - "test1": "bar", - } - - if !reflect.DeepEqual(result[0].Config.Labels, labels) { - c.Errorf("unexpected output: %s, expected: %s", result[0].Config.Labels, labels) + // double check if container is running by executing a exec command + out := command.PouchRun("exec", name, "cat", "/data/test").Stdout() + if !strings.Contains(out, "5678") { + c.Errorf("failed to exec in container, expected 5678 got %s", out) } }