diff --git a/build/docker/Dockerfile.host b/build/docker/Dockerfile.host index 3b040b0f40..abd54d052a 100644 --- a/build/docker/Dockerfile.host +++ b/build/docker/Dockerfile.host @@ -1,4 +1,4 @@ -FROM registry.cn-beijing.aliyuncs.com/yunionio/host-base:v0.5.0 +FROM registry.cn-beijing.aliyuncs.com/yunionio/host-base:v0.6.0 MAINTAINER "Yaoqi Wan wanyaoqi@yunionyun.com" diff --git a/build/docker/multi-arch/Dockerfile.host b/build/docker/multi-arch/Dockerfile.host index 20ba679311..609bb4b7a2 100644 --- a/build/docker/multi-arch/Dockerfile.host +++ b/build/docker/multi-arch/Dockerfile.host @@ -7,7 +7,7 @@ COPY . /root/go/src/yunion.io/x/onecloud WORKDIR /root/go/src/yunion.io/x/onecloud RUN make cmd/host -FROM registry.cn-beijing.aliyuncs.com/yunionio/host-base:v0.4.0 +FROM registry.cn-beijing.aliyuncs.com/yunionio/host-base:v0.6.0 MAINTAINER "Yaoqi Wan wanyaoqi@yunionyun.com" diff --git a/cmd/climc/shell/compute/containers.go b/cmd/climc/shell/compute/containers.go index 0030cb0f8d..5dadad723c 100644 --- a/cmd/climc/shell/compute/containers.go +++ b/cmd/climc/shell/compute/containers.go @@ -45,6 +45,7 @@ func init() { cmd.Perform("save-volume-mount-image", new(options.ContainerSaveVolumeMountImage)) cmd.Perform("exec-sync", new(options.ContainerExecSyncOptions)) cmd.BatchPerform("set-resources-limit", new(options.ContainerSetResourcesLimitOptions)) + cmd.Perform("commit", new(options.ContainerCommitOptions)) type UpdateSpecOptions struct { ID string `help:"ID or name of server" json:"-"` diff --git a/pkg/apis/compute/container.go b/pkg/apis/compute/container.go index f00e18204b..ea13a7c420 100644 --- a/pkg/apis/compute/container.go +++ b/pkg/apis/compute/container.go @@ -74,6 +74,8 @@ const ( CONTAINER_STATUS_RUNNING = "running" CONTAINER_STATUS_DELETING = "deleting" CONTAINER_STATUS_DELETE_FAILED = "delete_failed" + CONTAINER_STATUS_COMMITTING = "committing" + CONTAINER_STATUS_COMMIT_FAILED = "commit_failed" // for health check CONTAINER_STATUS_PROBING = "probing" CONTAINER_STATUS_PROBE_FAILED = "probe_failed" @@ -185,3 +187,47 @@ type ContainerExecSyncResponse struct { Stderr string `json:"stderr"` ExitCode int32 `json:"exit_code"` } + +type ContainerCommitExternalRegistry struct { + // e.g.: registry.cn-beijing.aliyuncs.com/yunionio + Url string `json:"url"` + // authentication configuration + Auth *apis.ContainerPullImageAuthConfig `json:"auth"` +} + +type ContainerCommitInput struct { + // Container registry id from kubeserver + RegistryId string `json:"registry_id"` + ExternalRegistry *ContainerCommitExternalRegistry `json:"external_registry"` + // image name + ImageName string `json:"image_name"` + // image tag + Tag string `json:"tag"` +} + +type ContainerCommitOutput struct { + Repository string `json:"repository"` +} + +type KubeServerContainerRegistryConfigCommon struct { + Username string `json:"username"` + Password string `json:"password"` +} + +type KubeServerContainerRegistryConfigHarbor struct { + KubeServerContainerRegistryConfigCommon +} + +type KubeServerContainerRegistryConfig struct { + Type string `json:"type"` + Common *KubeServerContainerRegistryConfigCommon `json:"common"` + Harbor *KubeServerContainerRegistryConfigHarbor `json:"harbor"` +} + +type KubeServerContainerRegistryDetails struct { + Id string `json:"id"` + Name string `json:"name"` + Url string `json:"url"` + Type string `json:"type"` + Config *KubeServerContainerRegistryConfig `json:"config"` +} diff --git a/pkg/apis/container.go b/pkg/apis/container.go index b601e10e9f..1dd636ef54 100644 --- a/pkg/apis/container.go +++ b/pkg/apis/container.go @@ -230,3 +230,15 @@ type ContainerVolumeMountText struct { type ContainerVolumeMountCephFS struct { Id string `json:"id"` } + +type ContainerPullImageAuthConfig struct { + Username string `json:"username,omitempty"` + Password string `json:"password,omitempty"` + Auth string `json:"auth,omitempty"` + ServerAddress string `json:"server_address,omitempty"` + // IdentityToken is used to authenticate the user and get + // an access token for the registry. + IdentityToken string `json:"identity_token,omitempty"` + // RegistryToken is a bearer token to be sent to a registry + RegistryToken string `json:"registry_token,omitempty"` +} diff --git a/pkg/apis/host/container.go b/pkg/apis/host/container.go index 31ca0ffc24..f9073be88d 100644 --- a/pkg/apis/host/container.go +++ b/pkg/apis/host/container.go @@ -91,22 +91,15 @@ type ContainerCreateInput struct { Spec *ContainerSpec `json:"spec"` } -type ContainerPullImageAuthConfig struct { - Username string `json:"username,omitempty"` - Password string `json:"password,omitempty"` - Auth string `json:"auth,omitempty"` - ServerAddress string `json:"server_address,omitempty"` - // IdentityToken is used to authenticate the user and get - // an access token for the registry. - IdentityToken string `json:"identity_token,omitempty"` - // RegistryToken is a bearer token to be sent to a registry - RegistryToken string `json:"registry_token,omitempty"` +type ContainerPullImageInput struct { + Image string `json:"image"` + PullPolicy apis.ImagePullPolicy `json:"pull_policy"` + Auth *apis.ContainerPullImageAuthConfig `json:"auth"` } -type ContainerPullImageInput struct { - Image string `json:"image"` - PullPolicy apis.ImagePullPolicy `json:"pull_policy"` - Auth *ContainerPullImageAuthConfig `json:"auth"` +type ContainerPushImageInput struct { + Image string `json:"image"` + Auth *apis.ContainerPullImageAuthConfig `json:"auth"` } type ContainerDesc struct { @@ -121,3 +114,8 @@ type ContainerSaveVolumeMountToImageInput struct { VolumeMountIndex int `json:"volume_mount_index"` VolumeMount *ContainerVolumeMount `json:"volume_mount"` } + +type ContainerCommitInput struct { + Repository string `json:"repository"` + Auth *apis.ContainerPullImageAuthConfig `json:"auth"` +} diff --git a/pkg/compute/guestdrivers/pod.go b/pkg/compute/guestdrivers/pod.go index 2b2bae8371..4369817892 100644 --- a/pkg/compute/guestdrivers/pod.go +++ b/pkg/compute/guestdrivers/pod.go @@ -551,6 +551,10 @@ func (p *SPodDriver) RequestSaveVolumeMountImage(ctx context.Context, userCred m return p.performContainerAction(ctx, userCred, task, "save-volume-mount-to-image", task.GetParams()) } +func (p *SPodDriver) RequestCommitContainer(ctx context.Context, userCred mcclient.TokenCredential, task models.IContainerTask) error { + return p.performContainerAction(ctx, userCred, task, "commit", task.GetParams()) +} + func (p *SPodDriver) RequestDiskSnapshot(ctx context.Context, guest *models.SGuest, task taskman.ITask, snapshotId, diskId string) error { /*if guest.GetStatus() != api.VM_READY { return httperrors.NewNotAcceptableError("pod status %s is not ready", guest.GetStatus()) diff --git a/pkg/compute/models/containers.go b/pkg/compute/models/containers.go index 9e90fdf7ee..ea73087a86 100644 --- a/pkg/compute/models/containers.go +++ b/pkg/compute/models/containers.go @@ -17,6 +17,9 @@ package models import ( "context" "fmt" + "path/filepath" + "strings" + "time" "yunion.io/x/jsonutils" "yunion.io/x/log" @@ -29,10 +32,13 @@ import ( api "yunion.io/x/onecloud/pkg/apis/compute" hostapi "yunion.io/x/onecloud/pkg/apis/host" imageapi "yunion.io/x/onecloud/pkg/apis/image" + "yunion.io/x/onecloud/pkg/cloudcommon/consts" "yunion.io/x/onecloud/pkg/cloudcommon/db" "yunion.io/x/onecloud/pkg/cloudcommon/db/taskman" "yunion.io/x/onecloud/pkg/httperrors" "yunion.io/x/onecloud/pkg/mcclient" + "yunion.io/x/onecloud/pkg/mcclient/auth" + kubemod "yunion.io/x/onecloud/pkg/mcclient/modules/k8s" ) var containerManager *SContainerManager @@ -757,3 +763,85 @@ func (c *SContainer) PerformStatus(ctx context.Context, userCred mcclient.TokenC } return c.SVirtualResourceBase.PerformStatus(ctx, userCred, query, input) } + +func (c *SContainer) getContainerHostCommitInput(ctx context.Context, userCred mcclient.TokenCredential, input *api.ContainerCommitInput) (*hostapi.ContainerCommitInput, error) { + var hostInput = &hostapi.ContainerCommitInput{ + Auth: new(apis.ContainerPullImageAuthConfig), + } + var repoUrl string + imageName := input.ImageName + tag := input.Tag + if imageName == "" { + imageName = c.GetName() + } + if tag == "" { + tag = time.Now().Format("20060102150405") + } + if input.RegistryId != "" { + s := auth.GetSession(ctx, userCred, consts.GetRegion()) + obj, err := kubemod.ContainerRegistries.Get(s, input.RegistryId, nil) + if err != nil { + return nil, httperrors.NewGeneralError(err) + } + reg := new(api.KubeServerContainerRegistryDetails) + if err := obj.Unmarshal(reg); err != nil { + return nil, errors.Wrap(err, "Unmarshal kube server registry details") + } + if reg.Config == nil { + confObj, err := kubemod.ContainerRegistries.GetSpecific(s, input.RegistryId, "config", nil) + if err != nil { + return nil, errors.Wrap(err, "Get kube server registry config") + } + reg.Config = new(api.KubeServerContainerRegistryConfig) + if err := confObj.Unmarshal(reg.Config); err != nil { + return nil, errors.Wrap(err, "Unmarshal kube server registry config") + } + } + repoUrl = reg.Url + if reg.Config != nil { + switch reg.Type { + case "common": + cfg := reg.Config.Common + hostInput.Auth.Username = cfg.Username + hostInput.Auth.Password = cfg.Password + case "harbor": + cfg := reg.Config.Harbor + hostInput.Auth.Username = cfg.Username + hostInput.Auth.Password = cfg.Password + default: + return nil, httperrors.NewInputParameterError("invalid registry type %s", reg.Type) + } + } + } else if input.ExternalRegistry != nil { + repoUrl = input.ExternalRegistry.Url + if repoUrl == "" { + return nil, httperrors.NewNotEmptyError("empty external registry url") + } + hostInput.Auth = input.ExternalRegistry.Auth + } else { + return nil, httperrors.NewInputParameterError("one of registry_id or external_registry must provided") + } + repoPrefix := strings.TrimPrefix(strings.TrimPrefix(repoUrl, "http://"), "https://") + hostInput.Repository = fmt.Sprintf("%s:%s", filepath.Join(repoPrefix, imageName), tag) + return hostInput, nil +} + +func (c *SContainer) PerformCommit(ctx context.Context, userCred mcclient.TokenCredential, _ jsonutils.JSONObject, input *api.ContainerCommitInput) (*api.ContainerCommitOutput, error) { + hostInput, err := c.getContainerHostCommitInput(ctx, userCred, input) + if err != nil { + return nil, err + } + out := &api.ContainerCommitOutput{ + Repository: hostInput.Repository, + } + return out, c.StartCommit(ctx, userCred, hostInput, "") +} + +func (c *SContainer) StartCommit(ctx context.Context, userCred mcclient.TokenCredential, input *hostapi.ContainerCommitInput, parentTaskId string) error { + c.SetStatus(ctx, userCred, api.CONTAINER_STATUS_COMMITTING, "") + task, err := taskman.TaskManager.NewTask(ctx, "ContainerCommitTask", c, userCred, jsonutils.Marshal(input).(*jsonutils.JSONDict), parentTaskId, "", nil) + if err != nil { + return errors.Wrap(err, "NewTask") + } + return task.ScheduleRun(nil) +} diff --git a/pkg/compute/models/pod_driver.go b/pkg/compute/models/pod_driver.go index f646b4e7e9..3fff890cc0 100644 --- a/pkg/compute/models/pod_driver.go +++ b/pkg/compute/models/pod_driver.go @@ -41,6 +41,7 @@ type IPodDriver interface { RequestDeleteContainer(ctx context.Context, userCred mcclient.TokenCredential, task IContainerTask) error RequestSyncContainerStatus(ctx context.Context, userCred mcclient.TokenCredential, task IContainerTask) error RequestPullContainerImage(ctx context.Context, userCred mcclient.TokenCredential, task IContainerTask) error + RequestCommitContainer(ctx context.Context, userCred mcclient.TokenCredential, task IContainerTask) error RequestSaveVolumeMountImage(ctx context.Context, userCred mcclient.TokenCredential, task IContainerTask) error RequestExecSyncContainer(ctx context.Context, userCred mcclient.TokenCredential, ctr *SContainer, input *compute.ContainerExecSyncInput) (jsonutils.JSONObject, error) RequestSetContainerResourcesLimit(ctx context.Context, cred mcclient.TokenCredential, c *SContainer, limit *apis.ContainerResources) (jsonutils.JSONObject, error) diff --git a/pkg/compute/tasks/container_commit_task.go b/pkg/compute/tasks/container_commit_task.go new file mode 100644 index 0000000000..0aa5fa1d18 --- /dev/null +++ b/pkg/compute/tasks/container_commit_task.go @@ -0,0 +1,63 @@ +// Copyright 2019 Yunion +// +// 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 tasks + +import ( + "context" + + "yunion.io/x/jsonutils" + + api "yunion.io/x/onecloud/pkg/apis/compute" + "yunion.io/x/onecloud/pkg/cloudcommon/db" + "yunion.io/x/onecloud/pkg/cloudcommon/db/taskman" + "yunion.io/x/onecloud/pkg/compute/models" +) + +func init() { + taskman.RegisterTask(ContainerCommitTask{}) +} + +type ContainerCommitTask struct { + ContainerBaseTask +} + +func (t *ContainerCommitTask) OnInit(ctx context.Context, obj db.IStandaloneModel, body jsonutils.JSONObject) { + t.requestCommitImage(ctx, obj.(*models.SContainer)) +} + +func (t *ContainerCommitTask) requestCommitImage(ctx context.Context, container *models.SContainer) { + t.SetStage("OnCommitted", nil) + if err := t.GetPodDriver().RequestCommitContainer(ctx, t.GetUserCred(), t); err != nil { + t.OnCommittedFailed(ctx, container, jsonutils.NewString(err.Error())) + return + } +} + +func (t *ContainerCommitTask) OnCommitted(ctx context.Context, container *models.SContainer, data jsonutils.JSONObject) { + t.SetStage("OnSyncStatus", nil) + container.StartSyncStatusTask(ctx, t.GetUserCred(), t.GetTaskId()) +} + +func (t *ContainerCommitTask) OnCommittedFailed(ctx context.Context, container *models.SContainer, reason jsonutils.JSONObject) { + container.SetStatus(ctx, t.GetUserCred(), api.CONTAINER_STATUS_COMMIT_FAILED, reason.String()) + t.SetStageFailed(ctx, reason) +} + +func (t *ContainerCommitTask) OnSyncStatus(ctx context.Context, container *models.SContainer, data jsonutils.JSONObject) { + t.SetStageComplete(ctx, nil) +} +func (t *ContainerCommitTask) OnSyncStatusFailed(ctx context.Context, container *models.SContainer, reason jsonutils.JSONObject) { + t.SetStageFailed(ctx, reason) +} diff --git a/pkg/hostman/guestman/pod.go b/pkg/hostman/guestman/pod.go index 2668775571..6bdbdeaf8c 100644 --- a/pkg/hostman/guestman/pod.go +++ b/pkg/hostman/guestman/pod.go @@ -57,8 +57,8 @@ import ( "yunion.io/x/onecloud/pkg/util/cgrouputils/cpuset" "yunion.io/x/onecloud/pkg/util/fileutils2" "yunion.io/x/onecloud/pkg/util/pod" - "yunion.io/x/onecloud/pkg/util/pod/image" "yunion.io/x/onecloud/pkg/util/pod/logs" + "yunion.io/x/onecloud/pkg/util/pod/nerdctl" "yunion.io/x/onecloud/pkg/util/procutils" ) @@ -117,6 +117,7 @@ type PodInstance interface { ExecContainer(ctx context.Context, userCred mcclient.TokenCredential, ctrId string, input *computeapi.ContainerExecInput) (*url.URL, error) ContainerExecSync(ctx context.Context, userCred mcclient.TokenCredential, ctrId string, input *computeapi.ContainerExecSyncInput) (jsonutils.JSONObject, error) SetContainerResourceLimit(ctrId string, limit *apis.ContainerResources) (jsonutils.JSONObject, error) + CommitContainer(ctx context.Context, userCred mcclient.TokenCredential, ctrId string, input *hostapi.ContainerCommitInput) (jsonutils.JSONObject, error) ReadLogs(ctx context.Context, userCred mcclient.TokenCredential, ctrId string, input *computeapi.PodLogOptions, stdout, stderr io.Writer) error } @@ -1624,27 +1625,8 @@ func (s *sPodGuestInstance) PullImage(ctx context.Context, userCred mcclient.Tok } func (s *sPodGuestInstance) pullImageByCtrCmd(ctx context.Context, userCred mcclient.TokenCredential, ctrId string, input *hostapi.ContainerPullImageInput) (jsonutils.JSONObject, error) { - opt := &image.PullOptions{ - SkipVerify: true, - } - if input.Auth != nil { - opt.Username = input.Auth.Username - opt.Password = input.Auth.Password - } - addr := options.HostOptions.ContainerRuntimeEndpoint - addr = strings.TrimPrefix(addr, "unix://") - imgTool := image.NewImageTool(addr, "k8s.io") - output, err := imgTool.Pull(input.Image, opt) - errs := make([]error, 0) - if err != nil { - // try http protocol - errs = append(errs, errors.Wrapf(err, "pullImageByCtrCmd: %s", output)) - opt.PlainHttp = true - log.Infof("try pull image %s by http", input.Image) - if output2, err := imgTool.Pull(input.Image, opt); err != nil { - errs = append(errs, errors.Wrapf(err, "pullImageByCtrCmd by http: %s", output2)) - return nil, errors.NewAggregate(errs) - } + if err := PullContainerdImage(input); err != nil { + return nil, errors.Wrap(err, "pull containerd image") } return jsonutils.Marshal(&runtimeapi.PullImageResponse{ ImageRef: input.Image, @@ -2003,3 +1985,31 @@ func (s *sPodGuestInstance) ReadLogs(ctx context.Context, userCred mcclient.Toke opts := logs.NewLogOptions(input, time.Now()) return logs.ReadLogs(ctx, logPath, ctrCriId, opts, s.getCRI().GetRuntimeClient(), stdout, stderr) } + +func (s *sPodGuestInstance) CommitContainer(ctx context.Context, userCred mcclient.TokenCredential, ctrId string, input *hostapi.ContainerCommitInput) (jsonutils.JSONObject, error) { + criId, err := s.getContainerCRIId(ctrId) + if err != nil { + return nil, errors.Wrap(err, "get container cri id") + } + + // 1. commit + tool := NewContainerdNerdctl() + imgRepo, err := tool.Commit(criId, &nerdctl.CommitOptions{Repository: input.Repository}) + if err != nil { + return nil, errors.Wrapf(err, "commit container %s image", ctrId) + } + log.Infof("container %s was commited to %s", ctrId, imgRepo) + + // 2. push to repository + if err := PushContainerdImage(&hostapi.ContainerPushImageInput{ + Image: imgRepo, + Auth: input.Auth, + }); err != nil { + return nil, errors.Wrapf(err, "push container %s image", ctrId) + } + log.Infof("container %s was pushed to %s", ctrId, imgRepo) + + return jsonutils.Marshal(map[string]interface{}{ + "image_repository": imgRepo, + }), nil +} diff --git a/pkg/hostman/guestman/pod_helper.go b/pkg/hostman/guestman/pod_helper.go new file mode 100644 index 0000000000..7ef8cd90d7 --- /dev/null +++ b/pkg/hostman/guestman/pod_helper.go @@ -0,0 +1,96 @@ +// Copyright 2019 Yunion +// +// 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 guestman + +import ( + "strings" + + "yunion.io/x/log" + "yunion.io/x/pkg/errors" + + hostapi "yunion.io/x/onecloud/pkg/apis/host" + "yunion.io/x/onecloud/pkg/hostman/options" + "yunion.io/x/onecloud/pkg/util/pod/image" + "yunion.io/x/onecloud/pkg/util/pod/nerdctl" +) + +func GetContainerdConnectionInfo() (string, string) { + addr := options.HostOptions.ContainerRuntimeEndpoint + addr = strings.TrimPrefix(addr, "unix://") + namespace := "k8s.io" + return addr, namespace +} + +func NewContainerdImageTool() image.ImageTool { + addr, namespace := GetContainerdConnectionInfo() + return image.NewImageTool(addr, namespace) +} + +func NewContainerdNerdctl() nerdctl.Nerdctl { + addr, namespace := GetContainerdConnectionInfo() + return nerdctl.NewNerdctl(addr, namespace) +} + +func PullContainerdImage(input *hostapi.ContainerPullImageInput) error { + opt := &image.PullOptions{ + RepoCommonOptions: image.RepoCommonOptions{ + SkipVerify: true, + }, + } + if input.Auth != nil { + opt.Username = input.Auth.Username + opt.Password = input.Auth.Password + } + imgTool := NewContainerdImageTool() + output, err := imgTool.Pull(input.Image, opt) + errs := make([]error, 0) + if err != nil { + // try http protocol + errs = append(errs, errors.Wrapf(err, "pullImageByCtrCmd: %s", output)) + opt.PlainHttp = true + log.Infof("try pull image %s by http", input.Image) + if output2, err := imgTool.Pull(input.Image, opt); err != nil { + errs = append(errs, errors.Wrapf(err, "pullImageByCtrCmd by http: %s", output2)) + return errors.NewAggregate(errs) + } + } + return nil +} + +func PushContainerdImage(input *hostapi.ContainerPushImageInput) error { + opt := &image.PushOptions{ + RepoCommonOptions: image.RepoCommonOptions{ + SkipVerify: true, + }, + } + if input.Auth != nil { + opt.Username = input.Auth.Username + opt.Password = input.Auth.Password + } + imgTool := NewContainerdImageTool() + err := imgTool.Push(input.Image, opt) + errs := make([]error, 0) + if err != nil { + // try http protocol + errs = append(errs, errors.Wrap(err, "pushImageByCtrCmd: %s")) + opt.PlainHttp = true + log.Infof("try push image %s by http", input.Image) + if err := imgTool.Push(input.Image, opt); err != nil { + errs = append(errs, errors.Wrapf(err, "pushImageByCtrCmd by http")) + return errors.NewAggregate(errs) + } + } + return nil +} diff --git a/pkg/hostman/guestman/podhandlers/podhandlers.go b/pkg/hostman/guestman/podhandlers/podhandlers.go index cf6c92137b..27d4493fd5 100644 --- a/pkg/hostman/guestman/podhandlers/podhandlers.go +++ b/pkg/hostman/guestman/podhandlers/podhandlers.go @@ -118,6 +118,7 @@ func AddPodHandlers(prefix string, app *appsrv.Application) { "sync-status": syncContainerStatus, "pull-image": pullImage, "save-volume-mount-to-image": saveVolumeMountToImage, + "commit": commitContainer, } for action, f := range ctrHandlers { app.AddHandler("POST", @@ -309,3 +310,11 @@ func containerSetResourcesLimit(ctx context.Context, userCred mcclient.TokenCred } return pod.SetContainerResourceLimit(containerId, input) } + +func commitContainer(ctx context.Context, userCred mcclient.TokenCredential, pod guestman.PodInstance, ctrId string, body jsonutils.JSONObject) (jsonutils.JSONObject, error) { + input := new(hostapi.ContainerCommitInput) + if err := body.Unmarshal(input); err != nil { + return nil, errors.Wrap(err, "unmarshal to ContainerCommitInput") + } + return pod.CommitContainer(ctx, userCred, ctrId, input) +} diff --git a/pkg/mcclient/options/compute/containers.go b/pkg/mcclient/options/compute/containers.go index 8aeff10dce..174bd20ff0 100644 --- a/pkg/mcclient/options/compute/containers.go +++ b/pkg/mcclient/options/compute/containers.go @@ -413,3 +413,34 @@ func (o *ContainerLogOptions) ToAPIInput() (*computeapi.PodLogOptions, error) { } return opt, nil } + +type ContainerCommitOptions struct { + ServerIdOptions + RegistryId string `help:"Registry ID from kubeserver"` + ImageName string `help:"Image name"` + Tag string `help:"Tag"` + ExternalRegistryUrl string `help:"External registry URL, e.g.: registry.cn-beijing.aliyuncs.com/yunionio"` + ExternalRegistryUsername string `help:"External registry username"` + ExternalRegistryPassword string `help:"External registry password"` +} + +func (o *ContainerCommitOptions) Params() (jsonutils.JSONObject, error) { + input := &computeapi.ContainerCommitInput{ + RegistryId: o.RegistryId, + ImageName: o.ImageName, + Tag: o.Tag, + ExternalRegistry: &computeapi.ContainerCommitExternalRegistry{ + Auth: &apis.ContainerPullImageAuthConfig{}, + }, + } + if o.ExternalRegistryUrl != "" { + input.ExternalRegistry.Url = o.ExternalRegistryUrl + } + if o.ExternalRegistryUsername != "" { + input.ExternalRegistry.Auth.Username = o.ExternalRegistryUsername + } + if o.ExternalRegistryPassword != "" { + input.ExternalRegistry.Auth.Password = o.ExternalRegistryPassword + } + return jsonutils.Marshal(input), nil +} diff --git a/pkg/util/pod/image/image.go b/pkg/util/pod/image/image.go index b0fd66c418..828ef500ee 100644 --- a/pkg/util/pod/image/image.go +++ b/pkg/util/pod/image/image.go @@ -10,6 +10,7 @@ import ( type ImageTool interface { Pull(image string, opt *PullOptions) (string, error) + Push(image string, opt *PushOptions) error } type imageTool struct { @@ -26,23 +27,26 @@ func NewImageTool(address, namespace string) ImageTool { func (i imageTool) newCtrCmd(args ...string) *procutils.Command { reqArgs := []string{"--address", i.address} + if i.namespace != "" { + reqArgs = append(reqArgs, "--namespace", i.namespace) + } args = append(reqArgs, args...) return procutils.NewRemoteCommandAsFarAsPossible("ctr", args...) } -type PullOptions struct { +type RepoCommonOptions struct { SkipVerify bool PlainHttp bool Username string Password string } -func (i imageTool) Pull(image string, opt *PullOptions) (string, error) { +type PullOptions struct { + RepoCommonOptions +} + +func (i imageTool) newRepoCommonArgs(opt RepoCommonOptions) []string { args := []string{} - if i.namespace != "" { - args = append(args, "--namespace", i.namespace) - } - args = append(args, []string{"images", "pull"}...) if opt.PlainHttp { args = append(args, "--plain-http") } @@ -52,7 +56,15 @@ func (i imageTool) Pull(image string, opt *PullOptions) (string, error) { if opt.Username != "" && opt.Password != "" { args = append(args, "--user", fmt.Sprintf("%s:%s", opt.Username, opt.Password)) } + return args +} + +func (i imageTool) Pull(image string, opt *PullOptions) (string, error) { + args := []string{} + args = append(args, []string{"images", "pull"}...) + args = append(args, i.newRepoCommonArgs(opt.RepoCommonOptions)...) args = append(args, []string{image}...) + cmd := i.newCtrCmd(args...) out, err := cmd.Output() if err != nil { @@ -60,3 +72,21 @@ func (i imageTool) Pull(image string, opt *PullOptions) (string, error) { } return image, nil } + +type PushOptions struct { + RepoCommonOptions +} + +func (i imageTool) Push(image string, opt *PushOptions) error { + args := []string{} + args = append(args, []string{"images", "push"}...) + args = append(args, i.newRepoCommonArgs(opt.RepoCommonOptions)...) + args = append(args, []string{image}...) + + cmd := i.newCtrCmd(args...) + out, err := cmd.Output() + if err != nil { + return errors.Wrapf(err, "push %s: %s", image, out) + } + return nil +} diff --git a/pkg/util/pod/nerdctl/doc.go b/pkg/util/pod/nerdctl/doc.go new file mode 100644 index 0000000000..88c954b6aa --- /dev/null +++ b/pkg/util/pod/nerdctl/doc.go @@ -0,0 +1 @@ +package nerdctl // import "yunion.io/x/onecloud/pkg/util/pod/nerdctl" diff --git a/pkg/util/pod/nerdctl/nerdctl.go b/pkg/util/pod/nerdctl/nerdctl.go new file mode 100644 index 0000000000..7b8135b33e --- /dev/null +++ b/pkg/util/pod/nerdctl/nerdctl.go @@ -0,0 +1,62 @@ +// Copyright 2019 Yunion +// +// 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 nerdctl + +import ( + "yunion.io/x/pkg/errors" + + "yunion.io/x/onecloud/pkg/util/procutils" +) + +type Nerdctl interface { + Commit(ctrId string, opt *CommitOptions) (string, error) +} + +type CommitOptions struct { + Repository string +} + +type nerdctl struct { + address string + namespace string +} + +func NewNerdctl(address, namespace string) Nerdctl { + return &nerdctl{ + address: address, + namespace: namespace, + } +} + +func (n nerdctl) newCmd(args ...string) *procutils.Command { + newArgs := []string{"--address", n.address} + if n.namespace != "" { + newArgs = append(newArgs, "--namespace", n.namespace) + } + newArgs = append(newArgs, args...) + return procutils.NewCommand("nerdctl", newArgs...) +} + +func (n nerdctl) Commit(ctrId string, opt *CommitOptions) (string, error) { + if opt.Repository == "" { + return "", errors.Wrap(errors.ErrEmpty, "repository") + } + cmd := n.newCmd("commit", ctrId, opt.Repository) + out, err := cmd.Output() + if err != nil { + return "", errors.Wrapf(err, "commit %s %s: %s", ctrId, opt.Repository, out) + } + return opt.Repository, nil +}