diff --git a/apis/server/container_bridge.go b/apis/server/container_bridge.go index 543bdb3031..8f07a0b07c 100644 --- a/apis/server/container_bridge.go +++ b/apis/server/container_bridge.go @@ -43,6 +43,11 @@ func (s *Server) createContainer(ctx context.Context, rw http.ResponseWriter, re logCreateOptions("container", config) name := req.FormValue("name") + //consider set specific id by url params + specificID := req.FormValue("recover-id") + if specificID != "" { + config.SpecificID = specificID + } // to do compensation to potential nil pointer after validation if config.HostConfig == nil { diff --git a/apis/swagger.yml b/apis/swagger.yml index acdd7c99d6..935011029f 100644 --- a/apis/swagger.yml +++ b/apis/swagger.yml @@ -2209,6 +2209,13 @@ definitions: description: "net priority." type: "integer" default: 0 + SpecificID: + type: "string" + description: | + Create container with given id. + The length of given id should be in [12,64]. + The characters of given id should be in 0123456789abcdef. + By default, given id is unnecessary. ContainerCreateResp: description: "response returned by daemon when container create successfully" diff --git a/apis/types/container_config.go b/apis/types/container_config.go index 88ca86a404..be7c90d790 100644 --- a/apis/types/container_config.go +++ b/apis/types/container_config.go @@ -108,6 +108,13 @@ type ContainerConfig struct { // annotations send to runtime spec. SpecAnnotation map[string]string `json:"SpecAnnotation,omitempty"` + // Create container with given id. + // The length of given id should be in [12,64]. + // The characters of given id should be in 0123456789abcdef. + // By default, given id is unnecessary. + // + SpecificID string `json:"SpecificID,omitempty"` + // Close `stdin` after one attached client disconnects StdinOnce bool `json:"StdinOnce,omitempty"` @@ -180,6 +187,8 @@ type ContainerConfig struct { /* polymorph ContainerConfig SpecAnnotation false */ +/* polymorph ContainerConfig SpecificID false */ + /* polymorph ContainerConfig StdinOnce false */ /* polymorph ContainerConfig StopSignal false */ diff --git a/cli/common_flags.go b/cli/common_flags.go index 35a36b71c3..a83cd9c1f3 100644 --- a/cli/common_flags.go +++ b/cli/common_flags.go @@ -61,6 +61,7 @@ func addCommonFlags(flagSet *pflag.FlagSet) *container { flagSet.Int64Var(&c.oomScoreAdj, "oom-score-adj", -500, "Tune host's OOM preferences (-1000 to 1000)") flagSet.StringVar(&c.name, "name", "", "Specify name of container") + flagSet.StringVar(&c.specificID, "specific-id", "", "Specify id of container, length of id should be in [12,64], characters of id should be in '0123456789abcdef'") flagSet.StringSliceVar(&c.networks, "net", nil, "Set networks to container") flagSet.StringSliceVarP(&c.ports, "port", "p", nil, "Set container ports mapping") diff --git a/cli/container.go b/cli/container.go index f4ec48799e..769782baa2 100644 --- a/cli/container.go +++ b/cli/container.go @@ -25,6 +25,7 @@ type container struct { hostname string rm bool disableNetworkFiles bool + specificID string blkioWeight uint16 blkioWeightDevice config.WeightDevice @@ -194,6 +195,7 @@ func (c *container) config() (*types.ContainerCreateConfig, error) { QuotaID: c.quotaID, SpecAnnotation: specAnnotation, NetPriority: c.netPriority, + SpecificID: c.specificID, }, HostConfig: &types.HostConfig{ diff --git a/daemon/mgr/container.go b/daemon/mgr/container.go index c28b85e12c..c7d87a1255 100644 --- a/daemon/mgr/container.go +++ b/daemon/mgr/container.go @@ -263,6 +263,9 @@ func (mgr *ContainerManager) Restore(ctx context.Context) error { // Create checks passed in parameters and create a Container object whose status is set at Created. func (mgr *ContainerManager) Create(ctx context.Context, name string, config *types.ContainerCreateConfig) (resp *types.ContainerCreateResp, err error) { + var ( + id = "" + ) // cleanup allocated resources when failed cleanups := []func() error{} defer func() { @@ -291,16 +294,43 @@ func (mgr *ContainerManager) Create(ctx context.Context, name string, config *ty return nil, fmt.Errorf("NetworkingConfig cannot be nil") } - id, err := mgr.generateID() - if err != nil { - return nil, err + if config.SpecificID != "" { + if len(config.SpecificID) < 12 || len(config.SpecificID) > 64 { + return nil, errors.Wrap(errtypes.ErrInvalidParam, "Container id length should be in [12,64]") + } + //characters of containerID should be in "0123456789abcdef" + for _, c := range []byte(config.SpecificID) { + if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f')) { + return nil, errors.Wrap(errtypes.ErrInvalidParam, "The characters of container id should be in '0123456789abcdef'") + } + } + + if mgr.cache.Get(config.SpecificID).Exist() { + return nil, errors.Wrap(errtypes.ErrAlreadyExisted, "container id: "+config.SpecificID) + } + id = config.SpecificID + } else { + tmpId, err := mgr.generateID() + if err != nil { + return nil, err + } + id = tmpId } + //put container id to cache to prevent concurrent containerCreateReq with same specific id + mgr.cache.Put(id, nil) + defer func() { + //clear cache + if err != nil { + mgr.cache.Remove(id) + } + }() if name == "" { name = mgr.generateName(id) } else if mgr.NameToID.Get(name).Exist() { return nil, errors.Wrap(errtypes.ErrAlreadyExisted, "container name: "+name) } + //todo: consider to do as the former ? // set hostname. if config.Hostname.String() == "" { diff --git a/test/api_container_create_test.go b/test/api_container_create_test.go index 1ae903796e..94e8eca8ac 100644 --- a/test/api_container_create_test.go +++ b/test/api_container_create_test.go @@ -1,6 +1,7 @@ package main import ( + "net/http" "net/url" "sort" @@ -231,3 +232,86 @@ func (suite *APIContainerCreateSuite) TestCreateNvidiaConfig(c *check.C) { DelContainerForceMultyTime(c, cname) } + +func (suite *APIContainerCreateSuite) TestBasicWithSpecificID(c *check.C) { + { + specificID := "0123456789abcdef" + obj := map[string]interface{}{ + "Image": busyboxImage, + "HostConfig": map[string]interface{}{}, + "SpecificID": specificID, + } + + body := request.WithJSONBody(obj) + resp, err := request.Post("/containers/create", body) + c.Assert(err, check.IsNil) + CheckRespStatus(c, resp, 201) + + // Decode response + got := types.ContainerCreateResp{} + c.Assert(request.DecodeBody(&got, resp.Body), check.IsNil) + c.Assert(got.ID, check.Equals, specificID) + c.Assert(got.Name, check.NotNil) + + DelContainerForceMultyTime(c, got.ID) + } + + //transfer specific id by url params + { + specificID := "0123456789fedcba" + q := url.Values{} + q.Add("recover-id", specificID) + query := request.WithQuery(q) + obj := map[string]interface{}{ + "Image": busyboxImage, + "HostConfig": map[string]interface{}{}, + } + + body := request.WithJSONBody(obj) + resp, err := request.Post("/containers/create", query, body) + c.Assert(err, check.IsNil) + CheckRespStatus(c, resp, 201) + + // Decode response + got := types.ContainerCreateResp{} + c.Assert(request.DecodeBody(&got, resp.Body), check.IsNil) + c.Assert(got.ID, check.Equals, specificID) + c.Assert(got.Name, check.NotNil) + + DelContainerForceMultyTime(c, got.ID) + } +} + +func (suite *APIContainerCreateSuite) TestInvalidSpecificID(c *check.C) { + obj := map[string]interface{}{ + "Image": busyboxImage, + "HostConfig": map[string]interface{}{}, + } + + //case1: specificID len is less than 12 + { + obj["SpecificID"] = "123456789ab" + body := request.WithJSONBody(obj) + resp, err := request.Post("/containers/create", body) + c.Assert(err, check.IsNil) + CheckRespStatus(c, resp, http.StatusBadRequest) + } + + //case2: specificID len is more than 64 + { + obj["SpecificID"] = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0" + body := request.WithJSONBody(obj) + resp, err := request.Post("/containers/create", body) + c.Assert(err, check.IsNil) + CheckRespStatus(c, resp, http.StatusBadRequest) + } + + //case3: characters of specificID is not in "0123456789abcdef" + { + obj["SpecificID"] = "0123456789abcdefhi" + body := request.WithJSONBody(obj) + resp, err := request.Post("/containers/create", body) + c.Assert(err, check.IsNil) + CheckRespStatus(c, resp, http.StatusBadRequest) + } +}