diff --git a/cmd/climc/shell/identity/credentials.go b/cmd/climc/shell/identity/credentials.go index 8386d782fa6..4bb35d74ba0 100644 --- a/cmd/climc/shell/identity/credentials.go +++ b/cmd/climc/shell/identity/credentials.go @@ -21,6 +21,7 @@ import ( "yunion.io/x/jsonutils" "yunion.io/x/pkg/util/printutils" + api "yunion.io/x/onecloud/pkg/apis/identity" "yunion.io/x/onecloud/pkg/mcclient" modules "yunion.io/x/onecloud/pkg/mcclient/modules/identity" ) @@ -28,7 +29,7 @@ import ( func init() { type CredentialListOptions struct { Scope string `help:"scope" choices:"project|domain|system"` - Type string `help:"credential type" choices:"totp|recovery_secret|aksk|enc_key"` + Type string `help:"credential type" choices:"totp|recovery_secret|aksk|enc_key|container_image"` User string `help:"filter by user"` UserDomain string `help:"the domain of user"` } @@ -362,4 +363,31 @@ func init() { printObject(result) return nil }) + + type CredentialContainerImageOptions struct { + NAME string `help:"container image credential name"` + USER string `help:"container image credential user"` + PASSWORD string `help:"container image credential password"` + Project string `help:"Project"` + ProjectDomain string `help:"domain of user"` + } + R(&CredentialContainerImageOptions{}, "credential-create-container-image", "Create container image crendential", func(s *mcclient.ClientSession, args *CredentialContainerImageOptions) error { + var pid string + var err error + if len(args.Project) > 0 { + pid, err = modules.Projects.FetchId(s, args.Project, args.ProjectDomain) + if err != nil { + return err + } + } + obj, err := modules.Credentials.CreateContainerImageSecret(s, pid, args.NAME, &api.CredentialContainerImageBlob{ + Username: args.USER, + Password: args.PASSWORD, + }) + if err != nil { + return err + } + printObject(obj) + return nil + }) } diff --git a/pkg/apis/container.go b/pkg/apis/container.go index 1dd636ef547..54e3aff776c 100644 --- a/pkg/apis/container.go +++ b/pkg/apis/container.go @@ -64,6 +64,8 @@ type ContainerSpec struct { Image string `json:"image"` // Image pull policy ImagePullPolicy ImagePullPolicy `json:"image_pull_policy"` + // Image credential id + ImageCredentialId string `json:"image_credential_id"` // Command to execute (i.e., entrypoint for docker) Command []string `json:"command"` // Args for the Command (i.e. command for docker) diff --git a/pkg/apis/host/container.go b/pkg/apis/host/container.go index ecec721b51f..1384f526c03 100644 --- a/pkg/apis/host/container.go +++ b/pkg/apis/host/container.go @@ -58,8 +58,9 @@ type ContainerVolumeMount struct { type ContainerSpec struct { apis.ContainerSpec - VolumeMounts []*ContainerVolumeMount `json:"volume_mounts"` - Devices []*ContainerDevice `json:"devices"` + ImageCredentialToken string `json:"image_credential_token"` + VolumeMounts []*ContainerVolumeMount `json:"volume_mounts"` + Devices []*ContainerDevice `json:"devices"` } type ContainerDevice struct { diff --git a/pkg/apis/identity/aksk.go b/pkg/apis/identity/aksk.go index a41b7b51e5d..715d95cea50 100644 --- a/pkg/apis/identity/aksk.go +++ b/pkg/apis/identity/aksk.go @@ -26,6 +26,7 @@ const ( RECOVERY_SECRETS_TYPE = "recovery_secret" OIDC_CREDENTIAL_TYPE = "oidc" ENCRYPT_KEY_TYPE = "enc_key" + CONTAINER_IMAGE_TYPE = "container_image" ) type SAccessKeySecretBlob struct { diff --git a/pkg/apis/identity/credential.go b/pkg/apis/identity/credential.go index 94ec45d0781..668f3e8d422 100644 --- a/pkg/apis/identity/credential.go +++ b/pkg/apis/identity/credential.go @@ -50,3 +50,15 @@ type CredentialCreateInput struct { // Ignore KeyHash string `json:"key_hash"` } + +type CredentialContainerImageBlob struct { + Username string `json:"username"` + Password string `json:"password"` + Auth string `json:"auth"` + 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/cloudcommon/db/statusbase.go b/pkg/cloudcommon/db/statusbase.go index fde4c803db4..38d894fd319 100644 --- a/pkg/cloudcommon/db/statusbase.go +++ b/pkg/cloudcommon/db/statusbase.go @@ -21,6 +21,7 @@ import ( "yunion.io/x/jsonutils" "yunion.io/x/pkg/errors" + "yunion.io/x/pkg/util/sets" "yunion.io/x/pkg/utils" "yunion.io/x/sqlchemy" @@ -105,7 +106,13 @@ func statusBaseSetStatus(ctx context.Context, model IStatusBaseModel, userCred m } OpsLog.LogEvent(model, ACT_UPDATE_STATUS, notes, userCred) success := true - if strings.Contains(status, "fail") || status == apis.STATUS_UNKNOWN || status == api.CLOUD_PROVIDER_DISCONNECTED { + isFail := false + for _, sub := range []string{"fail", "crash"} { + if strings.Contains(status, sub) { + isFail = true + } + } + if isFail || sets.NewString(apis.STATUS_UNKNOWN, api.CLOUD_PROVIDER_DISCONNECTED, api.CONTAINER_STATUS_CRASH_LOOP_BACK_OFF).Has(status) { success = false } logclient.AddSimpleActionLog(model, logclient.ACT_UPDATE_STATUS, notes, userCred, success) diff --git a/pkg/compute/models/containers.go b/pkg/compute/models/containers.go index 0c167713599..f601b3b5288 100644 --- a/pkg/compute/models/containers.go +++ b/pkg/compute/models/containers.go @@ -16,6 +16,7 @@ package models import ( "context" + "encoding/base64" "fmt" "path/filepath" "strings" @@ -31,13 +32,16 @@ import ( "yunion.io/x/onecloud/pkg/apis" api "yunion.io/x/onecloud/pkg/apis/compute" hostapi "yunion.io/x/onecloud/pkg/apis/host" + identityapi "yunion.io/x/onecloud/pkg/apis/identity" 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/compute/options" "yunion.io/x/onecloud/pkg/httperrors" "yunion.io/x/onecloud/pkg/mcclient" "yunion.io/x/onecloud/pkg/mcclient/auth" + identitymod "yunion.io/x/onecloud/pkg/mcclient/modules/identity" kubemod "yunion.io/x/onecloud/pkg/mcclient/modules/k8s" ) @@ -74,7 +78,7 @@ type SContainer struct { Spec *api.ContainerSpec `length:"long" create:"required" list:"user" update:"user"` // 启动时间 - StartedAt time.Time `nullable:"false" created_at:"false" index:"true" get:"user" list:"user" json:"started_at"` + StartedAt time.Time `nullable:"true" created_at:"false" index:"true" get:"user" list:"user" json:"started_at"` // 上次退出时间 LastFinishedAt time.Time `nullable:"true" created_at:"false" index:"true" get:"user" list:"user" json:"last_finished_at"` @@ -158,6 +162,11 @@ func (m *SContainerManager) ValidateSpec(ctx context.Context, userCred mcclient. if !sets.NewString(apis.ImagePullPolicyAlways, apis.ImagePullPolicyIfNotPresent).Has(string(spec.ImagePullPolicy)) { return httperrors.NewInputParameterError("invalid image_pull_policy %s", spec.ImagePullPolicy) } + if spec.ImageCredentialId != "" { + if _, err := m.GetImageCredential(ctx, userCred, spec.ImageCredentialId); err != nil { + return errors.Wrapf(err, "get image credential by id: %s", spec.ImageCredentialId) + } + } if pod != nil { if err := m.ValidateSpecVolumeMounts(ctx, userCred, pod, spec); err != nil { @@ -541,6 +550,67 @@ func (c *SContainer) StartDeleteTask(ctx context.Context, userCred mcclient.Toke return task.ScheduleRun(nil) } +func (m *SContainerManager) GetImageCredential(ctx context.Context, userCred mcclient.TokenCredential, id string) (*apis.ContainerPullImageAuthConfig, error) { + s := auth.GetSession(ctx, userCred, options.Options.Region) + ret, err := identitymod.Credentials.GetById(s, id, nil) + if err != nil { + if errors.Cause(err) == errors.ErrNotFound || strings.Contains(err.Error(), "NotFound") { + ret, err = identitymod.Credentials.GetByName(s, id, nil) + if err != nil { + return nil, errors.Wrapf(err, "get credential by id or name of %s", id) + } + } + return nil, errors.Wrap(err, "get credentials by id") + } + credType, _ := ret.GetString("type") + if credType != identityapi.CONTAINER_IMAGE_TYPE { + return nil, httperrors.NewNotSupportedError("unsupported credential type %s", credType) + } + blobStr, err := ret.GetString("blob") + if err != nil { + return nil, errors.Wrap(err, "get blob") + } + obj, err := jsonutils.ParseString(blobStr) + if err != nil { + return nil, errors.Wrapf(err, "json parse string: %s", blobStr) + } + blob := new(identityapi.CredentialContainerImageBlob) + if err := obj.Unmarshal(blob); err != nil { + return nil, errors.Wrap(err, "unmarshal blob") + } + out := &apis.ContainerPullImageAuthConfig{ + Username: blob.Username, + Password: blob.Password, + Auth: blob.Auth, + ServerAddress: blob.ServerAddress, + IdentityToken: blob.IdentityToken, + RegistryToken: blob.RegistryToken, + } + return out, nil +} + +func (c *SContainer) GetImageCredential(ctx context.Context, userCred mcclient.TokenCredential) (*apis.ContainerPullImageAuthConfig, error) { + if c.Spec.ImageCredentialId == "" { + return nil, errors.Wrap(errors.ErrEmpty, "image_credential_id is empty") + } + return GetContainerManager().GetImageCredential(ctx, userCred, c.Spec.ImageCredentialId) +} + +func (c *SContainer) GetHostPullImageInput(ctx context.Context, userCred mcclient.TokenCredential) (*hostapi.ContainerPullImageInput, error) { + input := &hostapi.ContainerPullImageInput{ + Image: c.Spec.Image, + PullPolicy: c.Spec.ImagePullPolicy, + } + if c.Spec.ImageCredentialId != "" { + cred, err := c.GetImageCredential(ctx, userCred) + if err != nil { + return nil, errors.Wrapf(err, "GetImageCredential %s", c.Spec.ImageCredentialId) + } + input.Auth = cred + } + return input, nil +} + func (c *SContainer) StartPullImageTask(ctx context.Context, userCred mcclient.TokenCredential, input *hostapi.ContainerPullImageInput, parentTaskId string) error { c.SetStatus(ctx, userCred, api.CONTAINER_STATUS_PULLING_IMAGE, "") task, err := taskman.TaskManager.NewTask(ctx, "ContainerPullImageTask", c, userCred, jsonutils.Marshal(input).(*jsonutils.JSONDict), parentTaskId, "", nil) @@ -592,6 +662,11 @@ func (c *SContainer) ToHostContainerSpec(ctx context.Context, userCred mcclient. VolumeMounts: mounts, Devices: ctrDevs, } + pullInput, err := c.GetHostPullImageInput(ctx, userCred) + if err != nil { + return nil, errors.Wrap(err, "GetHostPullImageInput") + } + hSpec.ImageCredentialToken = base64.StdEncoding.EncodeToString([]byte(jsonutils.Marshal(pullInput.Auth).String())) return hSpec, nil } @@ -776,6 +851,10 @@ func (c *SContainer) PerformStatus(ctx context.Context, userCred mcclient.TokenC if input.RestartCount > 0 { c.RestartCount = input.RestartCount } + if api.ContainerRunningStatus.Has(input.Status) { + // 当容器状态是运行时 restart_count 重新计数 + c.RestartCount = 0 + } if input.StartedAt != nil { c.StartedAt = *input.StartedAt } diff --git a/pkg/compute/tasks/container_create_task.go b/pkg/compute/tasks/container_create_task.go index 6590e27ffed..0508eebd43b 100644 --- a/pkg/compute/tasks/container_create_task.go +++ b/pkg/compute/tasks/container_create_task.go @@ -21,7 +21,6 @@ import ( "yunion.io/x/pkg/errors" api "yunion.io/x/onecloud/pkg/apis/compute" - hostapi "yunion.io/x/onecloud/pkg/apis/host" "yunion.io/x/onecloud/pkg/cloudcommon/db" "yunion.io/x/onecloud/pkg/cloudcommon/db/taskman" "yunion.io/x/onecloud/pkg/compute/models" @@ -61,9 +60,10 @@ func (t *ContainerCreateTask) OnInit(ctx context.Context, obj db.IStandaloneMode func (t *ContainerCreateTask) startPullImage(ctx context.Context, container *models.SContainer) { t.SetStage("OnImagePulled", nil) - input := &hostapi.ContainerPullImageInput{ - Image: container.Spec.Image, - PullPolicy: container.Spec.ImagePullPolicy, + input, err := container.GetHostPullImageInput(ctx, t.GetUserCred()) + if err != nil { + t.SetStageFailed(ctx, jsonutils.NewString(err.Error())) + return } if err := container.StartPullImageTask(ctx, t.GetUserCred(), input, t.GetTaskId()); err != nil { t.SetStageFailed(ctx, jsonutils.NewString(err.Error())) diff --git a/pkg/compute/tasks/pod_start_task.go b/pkg/compute/tasks/pod_start_task.go index c066094ff38..84513b38a16 100644 --- a/pkg/compute/tasks/pod_start_task.go +++ b/pkg/compute/tasks/pod_start_task.go @@ -41,7 +41,6 @@ func (t *PodStartTask) OnInit(ctx context.Context, obj db.IStandaloneModel, body } func (t *PodStartTask) OnPodStarted(ctx context.Context, pod *models.SGuest, _ jsonutils.JSONObject) { - t.SetStage("OnContainerStarted", nil) pod.SetStatus(ctx, t.GetUserCred(), api.POD_STATUS_STARTING_CONTAINER, "") ctrs, err := models.GetContainerManager().GetContainersByPod(pod.GetId()) if err != nil { @@ -50,7 +49,7 @@ func (t *PodStartTask) OnPodStarted(ctx context.Context, pod *models.SGuest, _ j } isAllStarted := true for i := range ctrs { - if ctrs[i].GetStatus() != api.CONTAINER_STATUS_RUNNING { + if !api.ContainerRunningStatus.Has(ctrs[i].GetStatus()) { isAllStarted = false ctrs[i].StartStartTask(ctx, t.GetUserCred(), t.GetTaskId()) } diff --git a/pkg/hostman/guestman/pod.go b/pkg/hostman/guestman/pod.go index 5d1d09bf20a..0f956a540b3 100644 --- a/pkg/hostman/guestman/pod.go +++ b/pkg/hostman/guestman/pod.go @@ -16,6 +16,7 @@ package guestman import ( "context" + "encoding/base64" "fmt" "io" "io/ioutil" @@ -109,6 +110,7 @@ func (cr *containerRunner) RunInContainer(pod *desc.SGuestDesc, containerId stri type PodInstance interface { GuestRuntimeInstance + GetCRIId() string GetContainerById(ctrId string) *hostapi.ContainerDesc CreateContainer(ctx context.Context, userCred mcclient.TokenCredential, id string, input *hostapi.ContainerCreateInput) (jsonutils.JSONObject, error) StartContainer(ctx context.Context, userCred mcclient.TokenCredential, ctrId string, input *hostapi.ContainerCreateInput) (jsonutils.JSONObject, error) @@ -231,7 +233,7 @@ func newPodGuestInstance(id string, man *SGuestManager) PodInstance { } func (s *sPodGuestInstance) CleanGuest(ctx context.Context, params interface{}) (jsonutils.JSONObject, error) { - criId := s.getCRIId() + criId := s.GetCRIId() if criId != "" { if err := s.getCRI().RemovePod(ctx, criId); err != nil { return nil, errors.Wrapf(err, "RemovePod with cri_id %q", criId) @@ -355,7 +357,9 @@ func (s *sPodGuestInstance) SyncStatus(reason string) { } if cs != nil { ctrStatusInput.RestartCount = cs.RestartCount - ctrStatusInput.StartedAt = &cs.StartedAt + if !cs.StartedAt.IsZero() { + ctrStatusInput.StartedAt = &cs.StartedAt + } if !cs.FinishedAt.IsZero() { ctrStatusInput.LastFinishedAt = &cs.FinishedAt } @@ -872,14 +876,14 @@ func (s *sPodGuestInstance) ensurePodRemoved(ctx context.Context, timeout int64) ctx, cancel := context.WithTimeout(ctx, time.Duration(timeout)*time.Second) defer cancel() /*if err := s.getCRI().StopPod(ctx, &runtimeapi.StopPodSandboxRequest{ - PodSandboxId: s.getCRIId(), + PodSandboxId: s.GetCRIId(), }); err != nil { - return errors.Wrapf(err, "stop cri pod: %s", s.getCRIId()) + return errors.Wrapf(err, "stop cri pod: %s", s.GetCRIId()) }*/ - criId := s.getCRIId() + criId := s.GetCRIId() if criId != "" { - if err := s.getCRI().RemovePod(ctx, s.getCRIId()); err != nil { - return errors.Wrapf(err, "remove cri pod: %s", s.getCRIId()) + if err := s.getCRI().RemovePod(ctx, s.GetCRIId()); err != nil { + return errors.Wrapf(err, "remove cri pod: %s", s.GetCRIId()) } } p, _ := s.getPod(ctx) @@ -1140,7 +1144,7 @@ func (s *sPodGuestInstance) StopContainer(ctx context.Context, userCred mcclient return nil, nil } -func (s *sPodGuestInstance) getCRIId() string { +func (s *sPodGuestInstance) GetCRIId() string { return s.GetSourceDesc().Metadata[computeapi.POD_METADATA_CRI_ID] } @@ -1253,10 +1257,26 @@ func (s *sPodGuestInstance) getContainersFilePath() string { func (s *sPodGuestInstance) CreateContainer(ctx context.Context, userCred mcclient.TokenCredential, id string, input *hostapi.ContainerCreateInput) (jsonutils.JSONObject, error) { // always pull image for checking - if _, err := s.PullImage(ctx, userCred, id, &hostapi.ContainerPullImageInput{ + imgInput := &hostapi.ContainerPullImageInput{ Image: input.Spec.Image, PullPolicy: input.Spec.ImagePullPolicy, - }); err != nil { + } + if input.Spec.ImageCredentialToken != "" { + tokenJson, err := base64.StdEncoding.DecodeString(input.Spec.ImageCredentialToken) + if err != nil { + return nil, errors.Wrapf(err, "base64 decode image credential token %s", input.Spec.ImageCredentialToken) + } + authObj, err := jsonutils.Parse(tokenJson) + if err != nil { + return nil, errors.Wrapf(err, "parse image credential token %s", input.Spec.ImageCredentialToken) + } + imgAuth := new(apis.ContainerPullImageAuthConfig) + if err := authObj.Unmarshal(imgAuth); err != nil { + return nil, errors.Wrapf(err, "unmarshal image credential token: %s", authObj) + } + imgInput.Auth = imgAuth + } + if _, err := s.PullImage(ctx, userCred, id, imgInput); err != nil { return nil, errors.Wrapf(err, "pull image %s", input.Spec.Image) } ctrCriId, err := s.createContainer(ctx, userCred, id, input) @@ -1488,7 +1508,7 @@ func (s *sPodGuestInstance) createContainer(ctx context.Context, userCred mcclie // set container namespace options to target /*if ctrCfg.Linux.SecurityContext.NamespaceOptions.Pid == runtimeapi.NamespaceMode_CONTAINER { ctrCfg.Linux.SecurityContext.NamespaceOptions.Pid = runtimeapi.NamespaceMode_TARGET - ctrCfg.Linux.SecurityContext.NamespaceOptions.TargetId = s.getCRIId() + ctrCfg.Linux.SecurityContext.NamespaceOptions.TargetId = s.GetCRIId() }*/ // inherit security context @@ -1572,7 +1592,7 @@ func (s *sPodGuestInstance) createContainer(ctx context.Context, userCred mcclie if len(spec.Args) != 0 { ctrCfg.Args = spec.Args } - criId, err := s.getCRI().CreateContainer(ctx, s.getCRIId(), podCfg, ctrCfg, false) + criId, err := s.getCRI().CreateContainer(ctx, s.GetCRIId(), podCfg, ctrCfg, false) if err != nil { return "", errors.Wrap(err, "cri.CreateContainer") } @@ -1851,7 +1871,7 @@ 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) { if err := PullContainerdImage(input); err != nil { - return nil, errors.Wrap(err, "pull containerd image") + return nil, errors.Errorf("PullContainerdImage: %s", trimPullImageError(err.Error())) } return jsonutils.Marshal(&runtimeapi.PullImageResponse{ ImageRef: input.Image, diff --git a/pkg/hostman/guestman/pod_helper.go b/pkg/hostman/guestman/pod_helper.go index 8580b394ef0..6cd9599c4b9 100644 --- a/pkg/hostman/guestman/pod_helper.go +++ b/pkg/hostman/guestman/pod_helper.go @@ -91,6 +91,27 @@ func PullContainerdImage(input *hostapi.ContainerPullImageInput) error { return nil } +func trimPullImageError(err string) string { + maxLen := 2048 + getLine := func(line string, maxLen int) string { + if len(line) < maxLen { + return line + } + return line[:maxLen] + } + if len(err) <= maxLen { + return err + } + lines := strings.Split(err, "\n") + if len(lines) <= 1 { + return getLine(lines[0], maxLen) + } + return strings.Join([]string{ + getLine(lines[0], maxLen/2), + getLine(lines[len(lines)-1], maxLen/2)}, + "\n") +} + func PushContainerdImage(input *hostapi.ContainerPushImageInput) error { opt := &image.PushOptions{ RepoCommonOptions: image.RepoCommonOptions{ diff --git a/pkg/hostman/guestman/pod_sync_loop.go b/pkg/hostman/guestman/pod_sync_loop.go index 251ca9f55af..371ef0da656 100644 --- a/pkg/hostman/guestman/pod_sync_loop.go +++ b/pkg/hostman/guestman/pod_sync_loop.go @@ -117,7 +117,12 @@ func (m *SGuestManager) syncContainerLoopIteration(plegCh chan *pleg.PodLifecycl } if e.Type == pleg.ContainerStarted { log.Infof("pod container started: %s", jsonutils.Marshal(e)) - podMan.SyncStatus("pod container started") + ctrId := e.Data.(string) + if ctrId == podMan.GetCRIId() { + log.Infof("pod %s(%s) is started", podMan.GetId(), ctrId) + } else { + podMan.SyncStatus("pod container started") + } } if e.Type == pleg.ContainerRemoved { /*isInternalRemoved := podMan.IsInternalRemoved(e) diff --git a/pkg/mcclient/modules/identity/mod_credentials.go b/pkg/mcclient/modules/identity/mod_credentials.go index 57f3792c18d..21b81e957f1 100644 --- a/pkg/mcclient/modules/identity/mod_credentials.go +++ b/pkg/mcclient/modules/identity/mod_credentials.go @@ -490,6 +490,21 @@ func (manager *SCredentialManager) CreateTotpSecret(s *mcclient.ClientSession, u return totp.Totp, nil } +func (manager *SCredentialManager) CreateContainerImageSecret(s *mcclient.ClientSession, projectId string, name string, blob *api.CredentialContainerImageBlob) (jsonutils.JSONObject, error) { + blobJson := jsonutils.Marshal(blob) + input := &api.CredentialCreateInput{ + Type: api.CONTAINER_IMAGE_TYPE, + ProjectId: projectId, + Blob: blobJson.String(), + } + input.Name = name + obj, err := manager.Create(s, jsonutils.Marshal(input)) + if err != nil { + return nil, errors.Wrap(err, "CreateContainerImageSecret") + } + return obj, nil +} + func (manager *SCredentialManager) SaveRecoverySecrets(s *mcclient.ClientSession, uid string, questions []SRecoverySecret) error { _, err := manager.GetRecoverySecrets(s, uid) if err == nil { diff --git a/pkg/mcclient/options/compute/containers.go b/pkg/mcclient/options/compute/containers.go index 174bd20ff04..381f5aa24c4 100644 --- a/pkg/mcclient/options/compute/containers.go +++ b/pkg/mcclient/options/compute/containers.go @@ -47,6 +47,7 @@ type ContainerDeleteOptions struct { type ContainerCreateCommonOptions struct { IMAGE string `help:"Image of container" json:"image"` + ImageCredentialId string `help:"Image credential id" json:"image_credential_id"` Command []string `help:"Command to execute (i.e., entrypoint for docker)" json:"command"` Args []string `help:"Args for the Command (i.e. command for docker)" json:"args"` WorkingDir string `help:"Current working directory of the command" json:"working_dir"` @@ -70,6 +71,7 @@ func (o ContainerCreateCommonOptions) getCreateSpec() (*computeapi.ContainerSpec req := &computeapi.ContainerSpec{ ContainerSpec: apis.ContainerSpec{ Image: o.IMAGE, + ImageCredentialId: o.ImageCredentialId, Command: o.Command, Args: o.Args, WorkingDir: o.WorkingDir,