diff --git a/apis/server/container_bridge.go b/apis/server/container_bridge.go index 008512f07..da355b936 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("specificId") + 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 1c61f5cd1..8614984bb 100644 --- a/apis/swagger.yml +++ b/apis/swagger.yml @@ -2233,6 +2233,14 @@ definitions: description: "net priority." type: "integer" default: 0 + SpecificID: + type: "string" + description: | + Create container with given id. + MinLength: 64 + MaxLength: 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 0ac8a3a56..8c920ec33 100644 --- a/apis/types/container_config.go +++ b/apis/types/container_config.go @@ -108,6 +108,14 @@ type ContainerConfig struct { // annotations send to runtime spec. SpecAnnotation map[string]string `json:"SpecAnnotation,omitempty"` + // Create container with given id. + // MinLength: 64 + // MaxLength: 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"` diff --git a/cli/common_flags.go b/cli/common_flags.go index 35a36b71c..3397e095e 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 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 f4ec48799..769782baa 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 30ab0800d..cfa75386c 100644 --- a/daemon/mgr/container.go +++ b/daemon/mgr/container.go @@ -302,10 +302,18 @@ func (mgr *ContainerManager) Create(ctx context.Context, name string, config *ty return nil, errors.Wrapf(errtypes.ErrInvalidParam, "NetworkingConfig cannot be empty") } - id, err := mgr.generateID() + id, err := mgr.generateContainerID(config.SpecificID) if err != nil { return nil, err } + //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) @@ -1874,3 +1882,24 @@ func (mgr *ContainerManager) execProcessGC() { func (mgr *ContainerManager) NewSnapshotsSyncer(snapshotStore *SnapshotStore, duration time.Duration) *SnapshotsSyncer { return newSnapshotsSyncer(snapshotStore, mgr.Client, duration) } + +func (mgr *ContainerManager) generateContainerID(specificID string) (string, error) { + if specificID != "" { + if len(specificID) != 64 { + return "", errors.Wrap(errtypes.ErrInvalidParam, "Container id length should be 64") + } + //characters of containerID should be in "0123456789abcdef" + for _, c := range []byte(specificID) { + if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f')) { + return "", errors.Wrap(errtypes.ErrInvalidParam, "The characters of container id should be in '0123456789abcdef'") + } + } + + if mgr.cache.Get(specificID).Exist() { + return "", errors.Wrap(errtypes.ErrAlreadyExisted, "container id: "+specificID) + } + return specificID, nil + } + + return mgr.generateID() +} diff --git a/test/api_container_create_test.go b/test/api_container_create_test.go index 85a3371bf..705d1c0be 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" "strings" @@ -279,3 +280,114 @@ func (suite *APIContainerCreateSuite) TestCreateWithMacAddress(c *check.C) { DelContainerForceMultyTime(c, cname) } + +func (suite *APIContainerCreateSuite) TestBasicWithSpecificID(c *check.C) { + { + specificID := "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + 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 := "0123456789fedcbaab0123456789abcdef0123456789abcdef0123456789abcd" + q := url.Values{} + q.Add("specificId", 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 64 + { + 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"] = "0123456789abcdefhi0123456789abcdef0123456789abcdef0123456789abcd" + body := request.WithJSONBody(obj) + resp, err := request.Post("/containers/create", body) + c.Assert(err, check.IsNil) + CheckRespStatus(c, resp, http.StatusBadRequest) + } + + //case4: specificID is conflict with existing one + { + specificID := "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + 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) + + defer DelContainerForceMultyTime(c, got.ID) + + //create container with existing id + resp, err = request.Post("/containers/create", body) + c.Assert(err, check.IsNil) + CheckRespStatus(c, resp, http.StatusConflict) + } +}