Jelajahi Sumber

Support environment vars in Swarm plugins services

Allow specifying environment variables when installing an engine plugin
as a Swarm service. Invalid environment variable entries (without an
equals (`=`) char) will be ignored.

Signed-off-by: Sune Keller <absukl@almbrand.dk>
Sune Keller 6 tahun lalu
induk
melakukan
fca5ee3bd5

+ 76 - 34
api/types/swarm/runtime/plugin.pb.go

@@ -1,6 +1,5 @@
-// Code generated by protoc-gen-gogo.
+// Code generated by protoc-gen-gogo. DO NOT EDIT.
 // source: plugin.proto
-// DO NOT EDIT!
 
 /*
 	Package runtime is a generated protocol buffer package.
@@ -38,6 +37,7 @@ type PluginSpec struct {
 	Remote     string             `protobuf:"bytes,2,opt,name=remote,proto3" json:"remote,omitempty"`
 	Privileges []*PluginPrivilege `protobuf:"bytes,3,rep,name=privileges" json:"privileges,omitempty"`
 	Disabled   bool               `protobuf:"varint,4,opt,name=disabled,proto3" json:"disabled,omitempty"`
+	Env        []string           `protobuf:"bytes,5,rep,name=env" json:"env,omitempty"`
 }
 
 func (m *PluginSpec) Reset()                    { *m = PluginSpec{} }
@@ -73,6 +73,13 @@ func (m *PluginSpec) GetDisabled() bool {
 	return false
 }
 
+func (m *PluginSpec) GetEnv() []string {
+	if m != nil {
+		return m.Env
+	}
+	return nil
+}
+
 // PluginPrivilege describes a permission the user has to accept
 // upon installing a plugin.
 type PluginPrivilege struct {
@@ -160,6 +167,21 @@ func (m *PluginSpec) MarshalTo(dAtA []byte) (int, error) {
 		}
 		i++
 	}
+	if len(m.Env) > 0 {
+		for _, s := range m.Env {
+			dAtA[i] = 0x2a
+			i++
+			l = len(s)
+			for l >= 1<<7 {
+				dAtA[i] = uint8(uint64(l)&0x7f | 0x80)
+				l >>= 7
+				i++
+			}
+			dAtA[i] = uint8(l)
+			i++
+			i += copy(dAtA[i:], s)
+		}
+	}
 	return i, nil
 }
 
@@ -208,24 +230,6 @@ func (m *PluginPrivilege) MarshalTo(dAtA []byte) (int, error) {
 	return i, nil
 }
 
-func encodeFixed64Plugin(dAtA []byte, offset int, v uint64) int {
-	dAtA[offset] = uint8(v)
-	dAtA[offset+1] = uint8(v >> 8)
-	dAtA[offset+2] = uint8(v >> 16)
-	dAtA[offset+3] = uint8(v >> 24)
-	dAtA[offset+4] = uint8(v >> 32)
-	dAtA[offset+5] = uint8(v >> 40)
-	dAtA[offset+6] = uint8(v >> 48)
-	dAtA[offset+7] = uint8(v >> 56)
-	return offset + 8
-}
-func encodeFixed32Plugin(dAtA []byte, offset int, v uint32) int {
-	dAtA[offset] = uint8(v)
-	dAtA[offset+1] = uint8(v >> 8)
-	dAtA[offset+2] = uint8(v >> 16)
-	dAtA[offset+3] = uint8(v >> 24)
-	return offset + 4
-}
 func encodeVarintPlugin(dAtA []byte, offset int, v uint64) int {
 	for v >= 1<<7 {
 		dAtA[offset] = uint8(v&0x7f | 0x80)
@@ -255,6 +259,12 @@ func (m *PluginSpec) Size() (n int) {
 	if m.Disabled {
 		n += 2
 	}
+	if len(m.Env) > 0 {
+		for _, s := range m.Env {
+			l = len(s)
+			n += 1 + l + sovPlugin(uint64(l))
+		}
+	}
 	return n
 }
 
@@ -429,6 +439,35 @@ func (m *PluginSpec) Unmarshal(dAtA []byte) error {
 				}
 			}
 			m.Disabled = bool(v != 0)
+		case 5:
+			if wireType != 2 {
+				return fmt.Errorf("proto: wrong wireType = %d for field Env", wireType)
+			}
+			var stringLen uint64
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return ErrIntOverflowPlugin
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				stringLen |= (uint64(b) & 0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+			intStringLen := int(stringLen)
+			if intStringLen < 0 {
+				return ErrInvalidLengthPlugin
+			}
+			postIndex := iNdEx + intStringLen
+			if postIndex > l {
+				return io.ErrUnexpectedEOF
+			}
+			m.Env = append(m.Env, string(dAtA[iNdEx:postIndex]))
+			iNdEx = postIndex
 		default:
 			iNdEx = preIndex
 			skippy, err := skipPlugin(dAtA[iNdEx:])
@@ -695,18 +734,21 @@ var (
 func init() { proto.RegisterFile("plugin.proto", fileDescriptorPlugin) }
 
 var fileDescriptorPlugin = []byte{
-	// 196 bytes of a gzipped FileDescriptorProto
-	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xe2, 0xe2, 0x29, 0xc8, 0x29, 0x4d,
-	0xcf, 0xcc, 0xd3, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x57, 0x6a, 0x63, 0xe4, 0xe2, 0x0a, 0x00, 0x0b,
-	0x04, 0x17, 0xa4, 0x26, 0x0b, 0x09, 0x71, 0xb1, 0xe4, 0x25, 0xe6, 0xa6, 0x4a, 0x30, 0x2a, 0x30,
-	0x6a, 0x70, 0x06, 0x81, 0xd9, 0x42, 0x62, 0x5c, 0x6c, 0x45, 0xa9, 0xb9, 0xf9, 0x25, 0xa9, 0x12,
-	0x4c, 0x60, 0x51, 0x28, 0x4f, 0xc8, 0x80, 0x8b, 0xab, 0xa0, 0x28, 0xb3, 0x2c, 0x33, 0x27, 0x35,
-	0x3d, 0xb5, 0x58, 0x82, 0x59, 0x81, 0x59, 0x83, 0xdb, 0x48, 0x40, 0x0f, 0x62, 0x58, 0x00, 0x4c,
-	0x22, 0x08, 0x49, 0x8d, 0x90, 0x14, 0x17, 0x47, 0x4a, 0x66, 0x71, 0x62, 0x52, 0x4e, 0x6a, 0x8a,
-	0x04, 0x8b, 0x02, 0xa3, 0x06, 0x47, 0x10, 0x9c, 0xaf, 0x14, 0xcb, 0xc5, 0x8f, 0xa6, 0x15, 0xab,
-	0x63, 0x14, 0xb8, 0xb8, 0x53, 0x52, 0x8b, 0x93, 0x8b, 0x32, 0x0b, 0x4a, 0x32, 0xf3, 0xf3, 0xa0,
-	0x2e, 0x42, 0x16, 0x12, 0x12, 0xe1, 0x62, 0x2d, 0x4b, 0xcc, 0x29, 0x4d, 0x05, 0xbb, 0x88, 0x33,
-	0x08, 0xc2, 0x71, 0xe2, 0x39, 0xf1, 0x48, 0x8e, 0xf1, 0xc2, 0x23, 0x39, 0xc6, 0x07, 0x8f, 0xe4,
-	0x18, 0x93, 0xd8, 0xc0, 0x9e, 0x37, 0x06, 0x04, 0x00, 0x00, 0xff, 0xff, 0xb8, 0x84, 0xad, 0x79,
-	0x0c, 0x01, 0x00, 0x00,
+	// 256 bytes of a gzipped FileDescriptorProto
+	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x6c, 0x90, 0x4d, 0x4b, 0xc3, 0x30,
+	0x18, 0xc7, 0x89, 0xdd, 0xc6, 0xfa, 0x4c, 0x70, 0x04, 0x91, 0xe2, 0xa1, 0x94, 0x9d, 0x7a, 0x6a,
+	0x45, 0x2f, 0x82, 0x37, 0x0f, 0x9e, 0x47, 0xbc, 0x09, 0x1e, 0xd2, 0xf6, 0xa1, 0x06, 0x9b, 0x17,
+	0x92, 0xb4, 0xe2, 0x37, 0xf1, 0x23, 0x79, 0xf4, 0x23, 0x48, 0x3f, 0x89, 0x98, 0x75, 0x32, 0x64,
+	0xa7, 0xff, 0x4b, 0xc2, 0x9f, 0x1f, 0x0f, 0x9c, 0x9a, 0xae, 0x6f, 0x85, 0x2a, 0x8c, 0xd5, 0x5e,
+	0x6f, 0x3e, 0x08, 0xc0, 0x36, 0x14, 0x8f, 0x06, 0x6b, 0x4a, 0x61, 0xa6, 0xb8, 0xc4, 0x84, 0x64,
+	0x24, 0x8f, 0x59, 0xf0, 0xf4, 0x02, 0x16, 0x16, 0xa5, 0xf6, 0x98, 0x9c, 0x84, 0x76, 0x4a, 0xf4,
+	0x0a, 0xc0, 0x58, 0x31, 0x88, 0x0e, 0x5b, 0x74, 0x49, 0x94, 0x45, 0xf9, 0xea, 0x7a, 0x5d, 0xec,
+	0xc6, 0xb6, 0xfb, 0x07, 0x76, 0xf0, 0x87, 0x5e, 0xc2, 0xb2, 0x11, 0x8e, 0x57, 0x1d, 0x36, 0xc9,
+	0x2c, 0x23, 0xf9, 0x92, 0xfd, 0x65, 0xba, 0x86, 0x08, 0xd5, 0x90, 0xcc, 0xb3, 0x28, 0x8f, 0xd9,
+	0xaf, 0xdd, 0x3c, 0xc3, 0xd9, 0xbf, 0xb1, 0xa3, 0x78, 0x19, 0xac, 0x1a, 0x74, 0xb5, 0x15, 0xc6,
+	0x0b, 0xad, 0x26, 0xc6, 0xc3, 0x8a, 0x9e, 0xc3, 0x7c, 0xe0, 0x5d, 0x8f, 0x81, 0x31, 0x66, 0xbb,
+	0x70, 0xff, 0xf0, 0x39, 0xa6, 0xe4, 0x6b, 0x4c, 0xc9, 0xf7, 0x98, 0x92, 0xa7, 0xdb, 0x56, 0xf8,
+	0x97, 0xbe, 0x2a, 0x6a, 0x2d, 0xcb, 0x46, 0xd7, 0xaf, 0x68, 0xf7, 0xc2, 0x8d, 0x28, 0xfd, 0xbb,
+	0x41, 0x57, 0xba, 0x37, 0x6e, 0x65, 0x69, 0x7b, 0xe5, 0x85, 0xc4, 0xbb, 0x49, 0xab, 0x45, 0x38,
+	0xe4, 0xcd, 0x4f, 0x00, 0x00, 0x00, 0xff, 0xff, 0x99, 0xa8, 0xd9, 0x9b, 0x58, 0x01, 0x00, 0x00,
 }

+ 1 - 0
api/types/swarm/runtime/plugin.proto

@@ -9,6 +9,7 @@ message PluginSpec {
 	string remote = 2;
 	repeated PluginPrivilege privileges = 3;
 	bool disabled = 4;
+	repeated string env = 5;
 }
 
 // PluginPrivilege describes a permission the user has to accept

+ 1 - 1
daemon/cluster/controllers/plugin/controller.go

@@ -122,7 +122,7 @@ func (p *Controller) Prepare(ctx context.Context) (err error) {
 		return p.backend.Upgrade(ctx, remote, p.spec.Name, nil, &authConfig, privs, ioutil.Discard)
 	}
 
-	if err := p.backend.Pull(ctx, remote, p.spec.Name, nil, &authConfig, privs, ioutil.Discard, plugin.WithSwarmService(p.serviceID)); err != nil {
+	if err := p.backend.Pull(ctx, remote, p.spec.Name, nil, &authConfig, privs, ioutil.Discard, plugin.WithSwarmService(p.serviceID), plugin.WithEnv(p.spec.Env)); err != nil {
 		return err
 	}
 	pl, err = p.backend.Get(p.spec.Name)

+ 24 - 0
integration/service/plugin_test.go

@@ -6,9 +6,11 @@ import (
 	"io/ioutil"
 	"os"
 	"path"
+	"strings"
 	"testing"
 
 	"github.com/docker/docker/api/types"
+	"github.com/docker/docker/api/types/filters"
 	swarmtypes "github.com/docker/docker/api/types/swarm"
 	"github.com/docker/docker/api/types/swarm/runtime"
 	"github.com/docker/docker/integration/internal/swarm"
@@ -68,7 +70,25 @@ func TestServicePlugin(t *testing.T) {
 	poll.WaitOn(t, d2.PluginIsRunning(t, name), swarm.ServicePoll)
 	poll.WaitOn(t, d3.PluginIsRunning(t, name), swarm.ServicePoll)
 
+	// test that environment variables are passed from plugin service to plugin instance
 	service := d1.GetService(t, id)
+	tasks := d1.GetServiceTasks(t, service.Spec.Annotations.Name, filters.Arg("runtime", "plugin"))
+	if len(tasks) == 0 {
+		t.Log("No tasks found for plugin service")
+		t.Fail()
+	}
+	plugin, _, err := d1.NewClientT(t).PluginInspectWithRaw(context.Background(), name)
+	assert.NilError(t, err, "Error inspecting service plugin")
+	found := false
+	for _, env := range plugin.Settings.Env {
+		assert.Equal(t, strings.HasPrefix(env, "baz"), false, "Environment variable entry %q is invalid and should not be present", "baz")
+		if strings.HasPrefix(env, "foo=") {
+			found = true
+			assert.Equal(t, env, "foo=bar")
+		}
+	}
+	assert.Equal(t, true, found, "Environment variable %q not found in plugin", "foo")
+
 	d1.UpdateService(t, service, makePlugin(repo2, name, nil))
 	poll.WaitOn(t, d1.PluginReferenceIs(t, name, repo2), swarm.ServicePoll)
 	poll.WaitOn(t, d2.PluginReferenceIs(t, name, repo2), swarm.ServicePoll)
@@ -111,6 +131,10 @@ func makePlugin(repo, name string, constraints []string) func(*swarmtypes.Servic
 		s.Spec.TaskTemplate.PluginSpec = &runtime.PluginSpec{
 			Name:   name,
 			Remote: repo,
+			Env: []string{
+				"baz",     // invalid environment variable entries are ignored
+				"foo=bar", // "foo" will be the single environment variable
+			},
 		}
 		if constraints != nil {
 			s.Spec.TaskTemplate.Placement = &swarmtypes.Placement{

+ 4 - 1
internal/test/daemon/service.go

@@ -56,7 +56,7 @@ func (d *Daemon) GetService(t assert.TestingT, id string) *swarm.Service {
 }
 
 // GetServiceTasks returns the swarm tasks for the specified service
-func (d *Daemon) GetServiceTasks(t assert.TestingT, service string) []swarm.Task {
+func (d *Daemon) GetServiceTasks(t assert.TestingT, service string, additionalFilters ...filters.KeyValuePair) []swarm.Task {
 	if ht, ok := t.(test.HelperT); ok {
 		ht.Helper()
 	}
@@ -66,6 +66,9 @@ func (d *Daemon) GetServiceTasks(t assert.TestingT, service string) []swarm.Task
 	filterArgs := filters.NewArgs()
 	filterArgs.Add("desired-state", "running")
 	filterArgs.Add("service", service)
+	for _, filter := range additionalFilters {
+		filterArgs.Add(filter.Key, filter.Value)
+	}
 
 	options := types.TaskListOptions{
 		Filters: filterArgs,

+ 27 - 0
plugin/defs.go

@@ -1,6 +1,8 @@
 package plugin // import "github.com/docker/docker/plugin"
 
 import (
+	"fmt"
+	"strings"
 	"sync"
 
 	"github.com/docker/docker/pkg/plugins"
@@ -42,6 +44,31 @@ func WithSwarmService(id string) CreateOpt {
 	}
 }
 
+// WithEnv is a CreateOpt that passes the user-provided environment variables
+// to the plugin container, de-duplicating variables with the same names case
+// sensitively and only appends valid key=value pairs
+func WithEnv(env []string) CreateOpt {
+	return func(p *v2.Plugin) {
+		effectiveEnv := make(map[string]string)
+		for _, penv := range p.PluginObj.Config.Env {
+			if penv.Value != nil {
+				effectiveEnv[penv.Name] = *penv.Value
+			}
+		}
+		for _, line := range env {
+			if pair := strings.SplitN(line, "=", 2); len(pair) > 1 {
+				effectiveEnv[pair[0]] = pair[1]
+			}
+		}
+		p.PluginObj.Settings.Env = make([]string, len(effectiveEnv))
+		i := 0
+		for key, value := range effectiveEnv {
+			p.PluginObj.Settings.Env[i] = fmt.Sprintf("%s=%s", key, value)
+			i++
+		}
+	}
+}
+
 // WithSpecMounts is a SpecOpt which appends the provided mounts to the runtime spec
 func WithSpecMounts(mounts []specs.Mount) SpecOpt {
 	return func(s *specs.Spec) {