Browse code

Add config APIs

Signed-off-by: Aaron Lehmann <aaron.lehmann@docker.com>

Aaron Lehmann authored on 2017/03/16 06:52:17
Showing 12 changed files
... ...
@@ -16,21 +16,32 @@ type Backend interface {
16 16
 	Update(uint64, types.Spec, types.UpdateFlags) error
17 17
 	GetUnlockKey() (string, error)
18 18
 	UnlockSwarm(req types.UnlockRequest) error
19
+
19 20
 	GetServices(basictypes.ServiceListOptions) ([]types.Service, error)
20 21
 	GetService(idOrName string, insertDefaults bool) (types.Service, error)
21 22
 	CreateService(types.ServiceSpec, string) (*basictypes.ServiceCreateResponse, error)
22 23
 	UpdateService(string, uint64, types.ServiceSpec, basictypes.ServiceUpdateOptions) (*basictypes.ServiceUpdateResponse, error)
23 24
 	RemoveService(string) error
25
+
24 26
 	ServiceLogs(context.Context, *backend.LogSelector, *basictypes.ContainerLogsOptions) (<-chan *backend.LogMessage, error)
27
+
25 28
 	GetNodes(basictypes.NodeListOptions) ([]types.Node, error)
26 29
 	GetNode(string) (types.Node, error)
27 30
 	UpdateNode(string, uint64, types.NodeSpec) error
28 31
 	RemoveNode(string, bool) error
32
+
29 33
 	GetTasks(basictypes.TaskListOptions) ([]types.Task, error)
30 34
 	GetTask(string) (types.Task, error)
35
+
31 36
 	GetSecrets(opts basictypes.SecretListOptions) ([]types.Secret, error)
32 37
 	CreateSecret(s types.SecretSpec) (string, error)
33 38
 	RemoveSecret(idOrName string) error
34 39
 	GetSecret(id string) (types.Secret, error)
35 40
 	UpdateSecret(idOrName string, version uint64, spec types.SecretSpec) error
41
+
42
+	GetConfigs(opts basictypes.ConfigListOptions) ([]types.Config, error)
43
+	CreateConfig(s types.ConfigSpec) (string, error)
44
+	RemoveConfig(id string) error
45
+	GetConfig(id string) (types.Config, error)
46
+	UpdateConfig(idOrName string, version uint64, spec types.ConfigSpec) error
36 47
 }
... ...
@@ -31,23 +31,33 @@ func (sr *swarmRouter) initRoutes() {
31 31
 		router.NewGetRoute("/swarm/unlockkey", sr.getUnlockKey),
32 32
 		router.NewPostRoute("/swarm/update", sr.updateCluster),
33 33
 		router.NewPostRoute("/swarm/unlock", sr.unlockCluster),
34
+
34 35
 		router.NewGetRoute("/services", sr.getServices),
35 36
 		router.NewGetRoute("/services/{id}", sr.getService),
36 37
 		router.NewPostRoute("/services/create", sr.createService),
37 38
 		router.NewPostRoute("/services/{id}/update", sr.updateService),
38 39
 		router.NewDeleteRoute("/services/{id}", sr.removeService),
39 40
 		router.NewGetRoute("/services/{id}/logs", sr.getServiceLogs, router.WithCancel),
41
+
40 42
 		router.NewGetRoute("/nodes", sr.getNodes),
41 43
 		router.NewGetRoute("/nodes/{id}", sr.getNode),
42 44
 		router.NewDeleteRoute("/nodes/{id}", sr.removeNode),
43 45
 		router.NewPostRoute("/nodes/{id}/update", sr.updateNode),
46
+
44 47
 		router.NewGetRoute("/tasks", sr.getTasks),
45 48
 		router.NewGetRoute("/tasks/{id}", sr.getTask),
46 49
 		router.NewGetRoute("/tasks/{id}/logs", sr.getTaskLogs, router.WithCancel),
50
+
47 51
 		router.NewGetRoute("/secrets", sr.getSecrets),
48 52
 		router.NewPostRoute("/secrets/create", sr.createSecret),
49 53
 		router.NewDeleteRoute("/secrets/{id}", sr.removeSecret),
50 54
 		router.NewGetRoute("/secrets/{id}", sr.getSecret),
51 55
 		router.NewPostRoute("/secrets/{id}/update", sr.updateSecret),
56
+
57
+		router.NewGetRoute("/configs", sr.getConfigs),
58
+		router.NewPostRoute("/configs/create", sr.createConfig),
59
+		router.NewDeleteRoute("/configs/{id}", sr.removeConfig),
60
+		router.NewGetRoute("/configs/{id}", sr.getConfig),
61
+		router.NewPostRoute("/configs/{id}/update", sr.updateConfig),
52 62
 	}
53 63
 }
... ...
@@ -408,3 +408,74 @@ func (sr *swarmRouter) updateSecret(ctx context.Context, w http.ResponseWriter,
408 408
 
409 409
 	return nil
410 410
 }
411
+
412
+func (sr *swarmRouter) getConfigs(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
413
+	if err := httputils.ParseForm(r); err != nil {
414
+		return err
415
+	}
416
+	filters, err := filters.FromParam(r.Form.Get("filters"))
417
+	if err != nil {
418
+		return err
419
+	}
420
+
421
+	configs, err := sr.backend.GetConfigs(basictypes.ConfigListOptions{Filters: filters})
422
+	if err != nil {
423
+		return err
424
+	}
425
+
426
+	return httputils.WriteJSON(w, http.StatusOK, configs)
427
+}
428
+
429
+func (sr *swarmRouter) createConfig(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
430
+	var config types.ConfigSpec
431
+	if err := json.NewDecoder(r.Body).Decode(&config); err != nil {
432
+		return err
433
+	}
434
+
435
+	id, err := sr.backend.CreateConfig(config)
436
+	if err != nil {
437
+		return err
438
+	}
439
+
440
+	return httputils.WriteJSON(w, http.StatusCreated, &basictypes.ConfigCreateResponse{
441
+		ID: id,
442
+	})
443
+}
444
+
445
+func (sr *swarmRouter) removeConfig(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
446
+	if err := sr.backend.RemoveConfig(vars["id"]); err != nil {
447
+		return err
448
+	}
449
+	w.WriteHeader(http.StatusNoContent)
450
+
451
+	return nil
452
+}
453
+
454
+func (sr *swarmRouter) getConfig(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
455
+	config, err := sr.backend.GetConfig(vars["id"])
456
+	if err != nil {
457
+		return err
458
+	}
459
+
460
+	return httputils.WriteJSON(w, http.StatusOK, config)
461
+}
462
+
463
+func (sr *swarmRouter) updateConfig(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error {
464
+	var config types.ConfigSpec
465
+	if err := json.NewDecoder(r.Body).Decode(&config); err != nil {
466
+		return errors.NewBadRequestError(err)
467
+	}
468
+
469
+	rawVersion := r.URL.Query().Get("version")
470
+	version, err := strconv.ParseUint(rawVersion, 10, 64)
471
+	if err != nil {
472
+		return errors.NewBadRequestError(fmt.Errorf("invalid config version"))
473
+	}
474
+
475
+	id := vars["id"]
476
+	if err := sr.backend.UpdateConfig(id, version, config); err != nil {
477
+		return err
478
+	}
479
+
480
+	return nil
481
+}
411 482
new file mode 100644
... ...
@@ -0,0 +1,31 @@
0
+package swarm
1
+
2
+import "os"
3
+
4
+// Config represents a config.
5
+type Config struct {
6
+	ID string
7
+	Meta
8
+	Spec ConfigSpec
9
+}
10
+
11
+// ConfigSpec represents a config specification from a config in swarm
12
+type ConfigSpec struct {
13
+	Annotations
14
+	Data []byte `json:",omitempty"`
15
+}
16
+
17
+// ConfigReferenceFileTarget is a file target in a config reference
18
+type ConfigReferenceFileTarget struct {
19
+	Name string
20
+	UID  string
21
+	GID  string
22
+	Mode os.FileMode
23
+}
24
+
25
+// ConfigReference is a reference to a config in swarm
26
+type ConfigReference struct {
27
+	File       *ConfigReferenceFileTarget
28
+	ConfigID   string
29
+	ConfigName string
30
+}
... ...
@@ -68,4 +68,5 @@ type ContainerSpec struct {
68 68
 	Hosts     []string           `json:",omitempty"`
69 69
 	DNSConfig *DNSConfig         `json:",omitempty"`
70 70
 	Secrets   []*SecretReference `json:",omitempty"`
71
+	Configs   []*ConfigReference `json:",omitempty"`
71 72
 }
... ...
@@ -522,6 +522,18 @@ type SecretListOptions struct {
522 522
 	Filters filters.Args
523 523
 }
524 524
 
525
+// ConfigCreateResponse contains the information returned to a client
526
+// on the creation of a new config.
527
+type ConfigCreateResponse struct {
528
+	// ID is the id of the created config.
529
+	ID string
530
+}
531
+
532
+// ConfigListOptions holds parameters to list configs
533
+type ConfigListOptions struct {
534
+	Filters filters.Args
535
+}
536
+
525 537
 // PushResult contains the tag, manifest digest, and manifest size from the
526 538
 // push. It's used to signal this information to the trust code in the client
527 539
 // so it can sign the manifest if necessary.
528 540
new file mode 100644
... ...
@@ -0,0 +1,117 @@
0
+package cluster
1
+
2
+import (
3
+	apitypes "github.com/docker/docker/api/types"
4
+	types "github.com/docker/docker/api/types/swarm"
5
+	"github.com/docker/docker/daemon/cluster/convert"
6
+	swarmapi "github.com/docker/swarmkit/api"
7
+	"golang.org/x/net/context"
8
+)
9
+
10
+// GetConfig returns a config from a managed swarm cluster
11
+func (c *Cluster) GetConfig(input string) (types.Config, error) {
12
+	var config *swarmapi.Config
13
+
14
+	if err := c.lockedManagerAction(func(ctx context.Context, state nodeState) error {
15
+		s, err := getConfig(ctx, state.controlClient, input)
16
+		if err != nil {
17
+			return err
18
+		}
19
+		config = s
20
+		return nil
21
+	}); err != nil {
22
+		return types.Config{}, err
23
+	}
24
+	return convert.ConfigFromGRPC(config), nil
25
+}
26
+
27
+// GetConfigs returns all configs of a managed swarm cluster.
28
+func (c *Cluster) GetConfigs(options apitypes.ConfigListOptions) ([]types.Config, error) {
29
+	c.mu.RLock()
30
+	defer c.mu.RUnlock()
31
+
32
+	state := c.currentNodeState()
33
+	if !state.IsActiveManager() {
34
+		return nil, c.errNoManager(state)
35
+	}
36
+
37
+	filters, err := newListConfigsFilters(options.Filters)
38
+	if err != nil {
39
+		return nil, err
40
+	}
41
+	ctx, cancel := c.getRequestContext()
42
+	defer cancel()
43
+
44
+	r, err := state.controlClient.ListConfigs(ctx,
45
+		&swarmapi.ListConfigsRequest{Filters: filters})
46
+	if err != nil {
47
+		return nil, err
48
+	}
49
+
50
+	configs := []types.Config{}
51
+
52
+	for _, config := range r.Configs {
53
+		configs = append(configs, convert.ConfigFromGRPC(config))
54
+	}
55
+
56
+	return configs, nil
57
+}
58
+
59
+// CreateConfig creates a new config in a managed swarm cluster.
60
+func (c *Cluster) CreateConfig(s types.ConfigSpec) (string, error) {
61
+	var resp *swarmapi.CreateConfigResponse
62
+	if err := c.lockedManagerAction(func(ctx context.Context, state nodeState) error {
63
+		configSpec := convert.ConfigSpecToGRPC(s)
64
+
65
+		r, err := state.controlClient.CreateConfig(ctx,
66
+			&swarmapi.CreateConfigRequest{Spec: &configSpec})
67
+		if err != nil {
68
+			return err
69
+		}
70
+		resp = r
71
+		return nil
72
+	}); err != nil {
73
+		return "", err
74
+	}
75
+	return resp.Config.ID, nil
76
+}
77
+
78
+// RemoveConfig removes a config from a managed swarm cluster.
79
+func (c *Cluster) RemoveConfig(input string) error {
80
+	return c.lockedManagerAction(func(ctx context.Context, state nodeState) error {
81
+		config, err := getConfig(ctx, state.controlClient, input)
82
+		if err != nil {
83
+			return err
84
+		}
85
+
86
+		req := &swarmapi.RemoveConfigRequest{
87
+			ConfigID: config.ID,
88
+		}
89
+
90
+		_, err = state.controlClient.RemoveConfig(ctx, req)
91
+		return err
92
+	})
93
+}
94
+
95
+// UpdateConfig updates a config in a managed swarm cluster.
96
+// Note: this is not exposed to the CLI but is available from the API only
97
+func (c *Cluster) UpdateConfig(input string, version uint64, spec types.ConfigSpec) error {
98
+	return c.lockedManagerAction(func(ctx context.Context, state nodeState) error {
99
+		config, err := getConfig(ctx, state.controlClient, input)
100
+		if err != nil {
101
+			return err
102
+		}
103
+
104
+		configSpec := convert.ConfigSpecToGRPC(spec)
105
+
106
+		_, err = state.controlClient.UpdateConfig(ctx,
107
+			&swarmapi.UpdateConfigRequest{
108
+				ConfigID: config.ID,
109
+				ConfigVersion: &swarmapi.Version{
110
+					Index: version,
111
+				},
112
+				Spec: &configSpec,
113
+			})
114
+		return err
115
+	})
116
+}
0 117
new file mode 100644
... ...
@@ -0,0 +1,61 @@
0
+package convert
1
+
2
+import (
3
+	swarmtypes "github.com/docker/docker/api/types/swarm"
4
+	swarmapi "github.com/docker/swarmkit/api"
5
+	gogotypes "github.com/gogo/protobuf/types"
6
+)
7
+
8
+// ConfigFromGRPC converts a grpc Config to a Config.
9
+func ConfigFromGRPC(s *swarmapi.Config) swarmtypes.Config {
10
+	config := swarmtypes.Config{
11
+		ID: s.ID,
12
+		Spec: swarmtypes.ConfigSpec{
13
+			Annotations: annotationsFromGRPC(s.Spec.Annotations),
14
+			Data:        s.Spec.Data,
15
+		},
16
+	}
17
+
18
+	config.Version.Index = s.Meta.Version.Index
19
+	// Meta
20
+	config.CreatedAt, _ = gogotypes.TimestampFromProto(s.Meta.CreatedAt)
21
+	config.UpdatedAt, _ = gogotypes.TimestampFromProto(s.Meta.UpdatedAt)
22
+
23
+	return config
24
+}
25
+
26
+// ConfigSpecToGRPC converts Config to a grpc Config.
27
+func ConfigSpecToGRPC(s swarmtypes.ConfigSpec) swarmapi.ConfigSpec {
28
+	return swarmapi.ConfigSpec{
29
+		Annotations: swarmapi.Annotations{
30
+			Name:   s.Name,
31
+			Labels: s.Labels,
32
+		},
33
+		Data: s.Data,
34
+	}
35
+}
36
+
37
+// ConfigReferencesFromGRPC converts a slice of grpc ConfigReference to ConfigReference
38
+func ConfigReferencesFromGRPC(s []*swarmapi.ConfigReference) []*swarmtypes.ConfigReference {
39
+	refs := []*swarmtypes.ConfigReference{}
40
+
41
+	for _, r := range s {
42
+		ref := &swarmtypes.ConfigReference{
43
+			ConfigID:   r.ConfigID,
44
+			ConfigName: r.ConfigName,
45
+		}
46
+
47
+		if t, ok := r.Target.(*swarmapi.ConfigReference_File); ok {
48
+			ref.File = &swarmtypes.ConfigReferenceFileTarget{
49
+				Name: t.File.Name,
50
+				UID:  t.File.UID,
51
+				GID:  t.File.GID,
52
+				Mode: t.File.Mode,
53
+			}
54
+		}
55
+
56
+		refs = append(refs, ref)
57
+	}
58
+
59
+	return refs
60
+}
... ...
@@ -30,6 +30,7 @@ func containerSpecFromGRPC(c *swarmapi.ContainerSpec) types.ContainerSpec {
30 30
 		ReadOnly:   c.ReadOnly,
31 31
 		Hosts:      c.Hosts,
32 32
 		Secrets:    secretReferencesFromGRPC(c.Secrets),
33
+		Configs:    configReferencesFromGRPC(c.Configs),
33 34
 	}
34 35
 
35 36
 	if c.DNSConfig != nil {
... ...
@@ -137,6 +138,7 @@ func secretReferencesToGRPC(sr []*types.SecretReference) []*swarmapi.SecretRefer
137 137
 
138 138
 	return refs
139 139
 }
140
+
140 141
 func secretReferencesFromGRPC(sr []*swarmapi.SecretReference) []*types.SecretReference {
141 142
 	refs := make([]*types.SecretReference, 0, len(sr))
142 143
 	for _, s := range sr {
... ...
@@ -161,6 +163,54 @@ func secretReferencesFromGRPC(sr []*swarmapi.SecretReference) []*types.SecretRef
161 161
 	return refs
162 162
 }
163 163
 
164
+func configReferencesToGRPC(sr []*types.ConfigReference) []*swarmapi.ConfigReference {
165
+	refs := make([]*swarmapi.ConfigReference, 0, len(sr))
166
+	for _, s := range sr {
167
+		ref := &swarmapi.ConfigReference{
168
+			ConfigID:   s.ConfigID,
169
+			ConfigName: s.ConfigName,
170
+		}
171
+		if s.File != nil {
172
+			ref.Target = &swarmapi.ConfigReference_File{
173
+				File: &swarmapi.FileTarget{
174
+					Name: s.File.Name,
175
+					UID:  s.File.UID,
176
+					GID:  s.File.GID,
177
+					Mode: s.File.Mode,
178
+				},
179
+			}
180
+		}
181
+
182
+		refs = append(refs, ref)
183
+	}
184
+
185
+	return refs
186
+}
187
+
188
+func configReferencesFromGRPC(sr []*swarmapi.ConfigReference) []*types.ConfigReference {
189
+	refs := make([]*types.ConfigReference, 0, len(sr))
190
+	for _, s := range sr {
191
+		target := s.GetFile()
192
+		if target == nil {
193
+			// not a file target
194
+			logrus.Warnf("config target not a file: config=%s", s.ConfigID)
195
+			continue
196
+		}
197
+		refs = append(refs, &types.ConfigReference{
198
+			File: &types.ConfigReferenceFileTarget{
199
+				Name: target.Name,
200
+				UID:  target.UID,
201
+				GID:  target.GID,
202
+				Mode: target.Mode,
203
+			},
204
+			ConfigID:   s.ConfigID,
205
+			ConfigName: s.ConfigName,
206
+		})
207
+	}
208
+
209
+	return refs
210
+}
211
+
164 212
 func containerToGRPC(c types.ContainerSpec) (*swarmapi.ContainerSpec, error) {
165 213
 	containerSpec := &swarmapi.ContainerSpec{
166 214
 		Image:      c.Image,
... ...
@@ -178,6 +228,7 @@ func containerToGRPC(c types.ContainerSpec) (*swarmapi.ContainerSpec, error) {
178 178
 		ReadOnly:   c.ReadOnly,
179 179
 		Hosts:      c.Hosts,
180 180
 		Secrets:    secretReferencesToGRPC(c.Secrets),
181
+		Configs:    configReferencesToGRPC(c.Configs),
181 182
 	}
182 183
 
183 184
 	if c.DNSConfig != nil {
... ...
@@ -103,3 +103,19 @@ func newListSecretsFilters(filter filters.Args) (*swarmapi.ListSecretsRequest_Fi
103 103
 		Labels:       runconfigopts.ConvertKVStringsToMap(filter.Get("label")),
104 104
 	}, nil
105 105
 }
106
+
107
+func newListConfigsFilters(filter filters.Args) (*swarmapi.ListConfigsRequest_Filters, error) {
108
+	accepted := map[string]bool{
109
+		"name":  true,
110
+		"id":    true,
111
+		"label": true,
112
+	}
113
+	if err := filter.Validate(accepted); err != nil {
114
+		return nil, err
115
+	}
116
+	return &swarmapi.ListConfigsRequest_Filters{
117
+		NamePrefixes: filter.Get("name"),
118
+		IDPrefixes:   filter.Get("id"),
119
+		Labels:       runconfigopts.ConvertKVStringsToMap(filter.Get("label")),
120
+	}, nil
121
+}
... ...
@@ -55,3 +55,48 @@ func TestNewListSecretsFilters(t *testing.T) {
55 55
 		}
56 56
 	}
57 57
 }
58
+
59
+func TestNewListConfigsFilters(t *testing.T) {
60
+	validNameFilter := filters.NewArgs()
61
+	validNameFilter.Add("name", "test_name")
62
+
63
+	validIDFilter := filters.NewArgs()
64
+	validIDFilter.Add("id", "7c9009d6720f6de3b492f5")
65
+
66
+	validLabelFilter := filters.NewArgs()
67
+	validLabelFilter.Add("label", "type=test")
68
+	validLabelFilter.Add("label", "storage=ssd")
69
+	validLabelFilter.Add("label", "memory")
70
+
71
+	validAllFilter := filters.NewArgs()
72
+	validAllFilter.Add("name", "nodeName")
73
+	validAllFilter.Add("id", "7c9009d6720f6de3b492f5")
74
+	validAllFilter.Add("label", "type=test")
75
+	validAllFilter.Add("label", "memory")
76
+
77
+	validFilters := []filters.Args{
78
+		validNameFilter,
79
+		validIDFilter,
80
+		validLabelFilter,
81
+		validAllFilter,
82
+	}
83
+
84
+	invalidTypeFilter := filters.NewArgs()
85
+	invalidTypeFilter.Add("nonexist", "aaaa")
86
+
87
+	invalidFilters := []filters.Args{
88
+		invalidTypeFilter,
89
+	}
90
+
91
+	for _, filter := range validFilters {
92
+		if _, err := newListConfigsFilters(filter); err != nil {
93
+			t.Fatalf("Should get no error, got %v", err)
94
+		}
95
+	}
96
+
97
+	for _, filter := range invalidFilters {
98
+		if _, err := newListConfigsFilters(filter); err == nil {
99
+			t.Fatalf("Should get an error for filter %s, while got nil", filter)
100
+		}
101
+	}
102
+}
... ...
@@ -174,6 +174,42 @@ func getSecret(ctx context.Context, c swarmapi.ControlClient, input string) (*sw
174 174
 	return rl.Secrets[0], nil
175 175
 }
176 176
 
177
+func getConfig(ctx context.Context, c swarmapi.ControlClient, input string) (*swarmapi.Config, error) {
178
+	// attempt to lookup config by full ID
179
+	if rg, err := c.GetConfig(ctx, &swarmapi.GetConfigRequest{ConfigID: input}); err == nil {
180
+		return rg.Config, nil
181
+	}
182
+
183
+	// If any error (including NotFound), ListConfigs to match via full name.
184
+	rl, err := c.ListConfigs(ctx, &swarmapi.ListConfigsRequest{
185
+		Filters: &swarmapi.ListConfigsRequest_Filters{
186
+			Names: []string{input},
187
+		},
188
+	})
189
+	if err != nil || len(rl.Configs) == 0 {
190
+		// If any error or 0 result, ListConfigs to match via ID prefix.
191
+		rl, err = c.ListConfigs(ctx, &swarmapi.ListConfigsRequest{
192
+			Filters: &swarmapi.ListConfigsRequest_Filters{
193
+				IDPrefixes: []string{input},
194
+			},
195
+		})
196
+	}
197
+	if err != nil {
198
+		return nil, err
199
+	}
200
+
201
+	if len(rl.Configs) == 0 {
202
+		err := fmt.Errorf("config %s not found", input)
203
+		return nil, errors.NewRequestNotFoundError(err)
204
+	}
205
+
206
+	if l := len(rl.Configs); l > 1 {
207
+		return nil, fmt.Errorf("config %s is ambiguous (%d matches found)", input, l)
208
+	}
209
+
210
+	return rl.Configs[0], nil
211
+}
212
+
177 213
 func getNetwork(ctx context.Context, c swarmapi.ControlClient, input string) (*swarmapi.Network, error) {
178 214
 	// GetNetwork to match via full ID.
179 215
 	if rg, err := c.GetNetwork(ctx, &swarmapi.GetNetworkRequest{NetworkID: input}); err == nil {