mirror of https://github.com/docker/cli.git
Implement secret types for compose file.
Signed-off-by: Daniel Nephin <dnephin@docker.com>
This commit is contained in:
parent
7215ebffa8
commit
0382f4f365
|
@ -62,7 +62,7 @@ func runCreate(dockerCli *command.DockerCli, opts *serviceOptions) error {
|
||||||
specifiedSecrets := opts.secrets.Value()
|
specifiedSecrets := opts.secrets.Value()
|
||||||
if len(specifiedSecrets) > 0 {
|
if len(specifiedSecrets) > 0 {
|
||||||
// parse and validate secrets
|
// parse and validate secrets
|
||||||
secrets, err := parseSecrets(apiClient, specifiedSecrets)
|
secrets, err := ParseSecrets(apiClient, specifiedSecrets)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,9 +10,9 @@ import (
|
||||||
"golang.org/x/net/context"
|
"golang.org/x/net/context"
|
||||||
)
|
)
|
||||||
|
|
||||||
// parseSecrets retrieves the secrets from the requested names and converts
|
// ParseSecrets retrieves the secrets from the requested names and converts
|
||||||
// them to secret references to use with the spec
|
// them to secret references to use with the spec
|
||||||
func parseSecrets(client client.SecretAPIClient, requestedSecrets []*types.SecretRequestOption) ([]*swarmtypes.SecretReference, error) {
|
func ParseSecrets(client client.SecretAPIClient, requestedSecrets []*types.SecretRequestOption) ([]*swarmtypes.SecretReference, error) {
|
||||||
secretRefs := make(map[string]*swarmtypes.SecretReference)
|
secretRefs := make(map[string]*swarmtypes.SecretReference)
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
|
|
|
@ -443,7 +443,7 @@ func getUpdatedSecrets(apiClient client.SecretAPIClient, flags *pflag.FlagSet, s
|
||||||
if flags.Changed(flagSecretAdd) {
|
if flags.Changed(flagSecretAdd) {
|
||||||
values := flags.Lookup(flagSecretAdd).Value.(*opts.SecretOpt).Value()
|
values := flags.Lookup(flagSecretAdd).Value.(*opts.SecretOpt).Value()
|
||||||
|
|
||||||
addSecrets, err := parseSecrets(apiClient, values)
|
addSecrets, err := ParseSecrets(apiClient, values)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
@ -126,7 +126,16 @@ func deployCompose(ctx context.Context, dockerCli *command.DockerCli, opts deplo
|
||||||
if err := createNetworks(ctx, dockerCli, namespace, networks); err != nil {
|
if err := createNetworks(ctx, dockerCli, namespace, networks); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
services, err := convert.Services(namespace, config)
|
|
||||||
|
secrets, err := convert.Secrets(namespace, config.Secrets)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := createSecrets(ctx, dockerCli, namespace, secrets); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
services, err := convert.Services(namespace, config, dockerCli.Client())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -211,6 +220,24 @@ func validateExternalNetworks(
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func createSecrets(
|
||||||
|
ctx context.Context,
|
||||||
|
dockerCli *command.DockerCli,
|
||||||
|
namespace convert.Namespace,
|
||||||
|
secrets []swarm.SecretSpec,
|
||||||
|
) error {
|
||||||
|
client := dockerCli.Client()
|
||||||
|
|
||||||
|
for _, secret := range secrets {
|
||||||
|
fmt.Fprintf(dockerCli.Out(), "Creating secret %s\n", secret.Name)
|
||||||
|
_, err := client.SecretCreate(ctx, secret)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func createNetworks(
|
func createNetworks(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
dockerCli *command.DockerCli,
|
dockerCli *command.DockerCli,
|
||||||
|
|
|
@ -1,8 +1,11 @@
|
||||||
package convert
|
package convert
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
networktypes "github.com/docker/docker/api/types/network"
|
networktypes "github.com/docker/docker/api/types/network"
|
||||||
|
"github.com/docker/docker/api/types/swarm"
|
||||||
composetypes "github.com/docker/docker/cli/compose/types"
|
composetypes "github.com/docker/docker/cli/compose/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -82,3 +85,27 @@ func Networks(namespace Namespace, networks networkMap, servicesNetworks map[str
|
||||||
|
|
||||||
return result, externalNetworks
|
return result, externalNetworks
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Secrets converts secrets from the Compose type to the engine API type
|
||||||
|
func Secrets(namespace Namespace, secrets map[string]composetypes.SecretConfig) ([]swarm.SecretSpec, error) {
|
||||||
|
result := []swarm.SecretSpec{}
|
||||||
|
for name, secret := range secrets {
|
||||||
|
if secret.External.External {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := ioutil.ReadFile(secret.File)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
result = append(result, swarm.SecretSpec{
|
||||||
|
Annotations: swarm.Annotations{
|
||||||
|
Name: namespace.Scope(name),
|
||||||
|
Labels: AddStackLabel(namespace, secret.Labels),
|
||||||
|
},
|
||||||
|
Data: data,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
|
@ -2,20 +2,26 @@ package convert
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
"github.com/docker/docker/api/types/container"
|
"github.com/docker/docker/api/types/container"
|
||||||
"github.com/docker/docker/api/types/swarm"
|
"github.com/docker/docker/api/types/swarm"
|
||||||
|
servicecli "github.com/docker/docker/cli/command/service"
|
||||||
composetypes "github.com/docker/docker/cli/compose/types"
|
composetypes "github.com/docker/docker/cli/compose/types"
|
||||||
|
"github.com/docker/docker/client"
|
||||||
"github.com/docker/docker/opts"
|
"github.com/docker/docker/opts"
|
||||||
runconfigopts "github.com/docker/docker/runconfig/opts"
|
runconfigopts "github.com/docker/docker/runconfig/opts"
|
||||||
"github.com/docker/go-connections/nat"
|
"github.com/docker/go-connections/nat"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Services from compose-file types to engine API types
|
// Services from compose-file types to engine API types
|
||||||
|
// TODO: fix secrets API so that SecretAPIClient is not required here
|
||||||
func Services(
|
func Services(
|
||||||
namespace Namespace,
|
namespace Namespace,
|
||||||
config *composetypes.Config,
|
config *composetypes.Config,
|
||||||
|
client client.SecretAPIClient,
|
||||||
) (map[string]swarm.ServiceSpec, error) {
|
) (map[string]swarm.ServiceSpec, error) {
|
||||||
result := make(map[string]swarm.ServiceSpec)
|
result := make(map[string]swarm.ServiceSpec)
|
||||||
|
|
||||||
|
@ -24,7 +30,12 @@ func Services(
|
||||||
networks := config.Networks
|
networks := config.Networks
|
||||||
|
|
||||||
for _, service := range services {
|
for _, service := range services {
|
||||||
serviceSpec, err := convertService(namespace, service, networks, volumes)
|
|
||||||
|
secrets, err := convertServiceSecrets(client, namespace, service.Secrets)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
serviceSpec, err := convertService(namespace, service, networks, volumes, secrets)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -39,6 +50,7 @@ func convertService(
|
||||||
service composetypes.ServiceConfig,
|
service composetypes.ServiceConfig,
|
||||||
networkConfigs map[string]composetypes.NetworkConfig,
|
networkConfigs map[string]composetypes.NetworkConfig,
|
||||||
volumes map[string]composetypes.VolumeConfig,
|
volumes map[string]composetypes.VolumeConfig,
|
||||||
|
secrets []*swarm.SecretReference,
|
||||||
) (swarm.ServiceSpec, error) {
|
) (swarm.ServiceSpec, error) {
|
||||||
name := namespace.Scope(service.Name)
|
name := namespace.Scope(service.Name)
|
||||||
|
|
||||||
|
@ -108,6 +120,7 @@ func convertService(
|
||||||
StopGracePeriod: service.StopGracePeriod,
|
StopGracePeriod: service.StopGracePeriod,
|
||||||
TTY: service.Tty,
|
TTY: service.Tty,
|
||||||
OpenStdin: service.StdinOpen,
|
OpenStdin: service.StdinOpen,
|
||||||
|
Secrets: secrets,
|
||||||
},
|
},
|
||||||
LogDriver: logDriver,
|
LogDriver: logDriver,
|
||||||
Resources: resources,
|
Resources: resources,
|
||||||
|
@ -163,6 +176,30 @@ func convertServiceNetworks(
|
||||||
return nets, nil
|
return nets, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: fix secrets API so that SecretAPIClient is not required here
|
||||||
|
func convertServiceSecrets(
|
||||||
|
client client.SecretAPIClient,
|
||||||
|
namespace Namespace,
|
||||||
|
secrets []composetypes.ServiceSecretConfig,
|
||||||
|
) ([]*swarm.SecretReference, error) {
|
||||||
|
opts := []*types.SecretRequestOption{}
|
||||||
|
for _, secret := range secrets {
|
||||||
|
target := secret.Target
|
||||||
|
if target == "" {
|
||||||
|
target = secret.Source
|
||||||
|
}
|
||||||
|
opts = append(opts, &types.SecretRequestOption{
|
||||||
|
Source: namespace.Scope(secret.Source),
|
||||||
|
Target: target,
|
||||||
|
UID: secret.UID,
|
||||||
|
GID: secret.GID,
|
||||||
|
Mode: os.FileMode(secret.Mode),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return servicecli.ParseSecrets(client, opts)
|
||||||
|
}
|
||||||
|
|
||||||
func convertExtraHosts(extraHosts map[string]string) []string {
|
func convertExtraHosts(extraHosts map[string]string) []string {
|
||||||
hosts := []string{}
|
hosts := []string{}
|
||||||
for host, ip := range extraHosts {
|
for host, ip := range extraHosts {
|
||||||
|
|
|
@ -109,6 +109,20 @@ func Load(configDetails types.ConfigDetails) (*types.Config, error) {
|
||||||
cfg.Volumes = volumesMapping
|
cfg.Volumes = volumesMapping
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if secrets, ok := configDict["secrets"]; ok {
|
||||||
|
secretsConfig, err := interpolation.Interpolate(secrets.(types.Dict), "secret", os.LookupEnv)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
secretsMapping, err := loadSecrets(secretsConfig, configDetails.WorkingDir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.Secrets = secretsMapping
|
||||||
|
}
|
||||||
|
|
||||||
return &cfg, nil
|
return &cfg, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -210,13 +224,15 @@ func transformHook(
|
||||||
) (interface{}, error) {
|
) (interface{}, error) {
|
||||||
switch target {
|
switch target {
|
||||||
case reflect.TypeOf(types.External{}):
|
case reflect.TypeOf(types.External{}):
|
||||||
return transformExternal(source, target, data)
|
return transformExternal(data)
|
||||||
case reflect.TypeOf(make(map[string]string, 0)):
|
case reflect.TypeOf(make(map[string]string, 0)):
|
||||||
return transformMapStringString(source, target, data)
|
return transformMapStringString(source, target, data)
|
||||||
case reflect.TypeOf(types.UlimitsConfig{}):
|
case reflect.TypeOf(types.UlimitsConfig{}):
|
||||||
return transformUlimits(source, target, data)
|
return transformUlimits(data)
|
||||||
case reflect.TypeOf(types.UnitBytes(0)):
|
case reflect.TypeOf(types.UnitBytes(0)):
|
||||||
return loadSize(data)
|
return loadSize(data)
|
||||||
|
case reflect.TypeOf(types.ServiceSecretConfig{}):
|
||||||
|
return transformServiceSecret(data)
|
||||||
}
|
}
|
||||||
switch target.Kind() {
|
switch target.Kind() {
|
||||||
case reflect.Struct:
|
case reflect.Struct:
|
||||||
|
@ -311,7 +327,7 @@ func resolveEnvironment(serviceConfig *types.ServiceConfig, serviceDict types.Di
|
||||||
var envVars []string
|
var envVars []string
|
||||||
|
|
||||||
for _, file := range envFiles {
|
for _, file := range envFiles {
|
||||||
filePath := path.Join(workingDir, file)
|
filePath := absPath(workingDir, file)
|
||||||
fileVars, err := opts.ParseEnvFile(filePath)
|
fileVars, err := opts.ParseEnvFile(filePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -341,7 +357,7 @@ func resolveVolumePaths(volumes []string, workingDir string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.HasPrefix(parts[0], ".") {
|
if strings.HasPrefix(parts[0], ".") {
|
||||||
parts[0] = path.Join(workingDir, parts[0])
|
parts[0] = absPath(workingDir, parts[0])
|
||||||
}
|
}
|
||||||
parts[0] = expandUser(parts[0])
|
parts[0] = expandUser(parts[0])
|
||||||
|
|
||||||
|
@ -359,11 +375,7 @@ func expandUser(path string) string {
|
||||||
return path
|
return path
|
||||||
}
|
}
|
||||||
|
|
||||||
func transformUlimits(
|
func transformUlimits(data interface{}) (interface{}, error) {
|
||||||
source reflect.Type,
|
|
||||||
target reflect.Type,
|
|
||||||
data interface{},
|
|
||||||
) (interface{}, error) {
|
|
||||||
switch value := data.(type) {
|
switch value := data.(type) {
|
||||||
case int:
|
case int:
|
||||||
return types.UlimitsConfig{Single: value}, nil
|
return types.UlimitsConfig{Single: value}, nil
|
||||||
|
@ -407,6 +419,32 @@ func loadVolumes(source types.Dict) (map[string]types.VolumeConfig, error) {
|
||||||
return volumes, nil
|
return volumes, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: remove duplicate with networks/volumes
|
||||||
|
func loadSecrets(source types.Dict, workingDir string) (map[string]types.SecretConfig, error) {
|
||||||
|
secrets := make(map[string]types.SecretConfig)
|
||||||
|
err := transform(source, &secrets)
|
||||||
|
if err != nil {
|
||||||
|
return secrets, err
|
||||||
|
}
|
||||||
|
for name, secret := range secrets {
|
||||||
|
if secret.External.External && secret.External.Name == "" {
|
||||||
|
secret.External.Name = name
|
||||||
|
secrets[name] = secret
|
||||||
|
}
|
||||||
|
if secret.File != "" {
|
||||||
|
secret.File = absPath(workingDir, secret.File)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return secrets, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func absPath(workingDir string, filepath string) string {
|
||||||
|
if path.IsAbs(filepath) {
|
||||||
|
return filepath
|
||||||
|
}
|
||||||
|
return path.Join(workingDir, filepath)
|
||||||
|
}
|
||||||
|
|
||||||
func transformStruct(
|
func transformStruct(
|
||||||
source reflect.Type,
|
source reflect.Type,
|
||||||
target reflect.Type,
|
target reflect.Type,
|
||||||
|
@ -490,11 +528,7 @@ func convertField(
|
||||||
return data, nil
|
return data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func transformExternal(
|
func transformExternal(data interface{}) (interface{}, error) {
|
||||||
source reflect.Type,
|
|
||||||
target reflect.Type,
|
|
||||||
data interface{},
|
|
||||||
) (interface{}, error) {
|
|
||||||
switch value := data.(type) {
|
switch value := data.(type) {
|
||||||
case bool:
|
case bool:
|
||||||
return map[string]interface{}{"external": value}, nil
|
return map[string]interface{}{"external": value}, nil
|
||||||
|
@ -507,6 +541,20 @@ func transformExternal(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func transformServiceSecret(data interface{}) (interface{}, error) {
|
||||||
|
switch value := data.(type) {
|
||||||
|
case string:
|
||||||
|
return map[string]interface{}{"source": value}, nil
|
||||||
|
case types.Dict:
|
||||||
|
return data, nil
|
||||||
|
case map[string]interface{}:
|
||||||
|
return data, nil
|
||||||
|
default:
|
||||||
|
return data, fmt.Errorf("invalid type %T for external", value)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
func toYAMLName(name string) string {
|
func toYAMLName(name string) string {
|
||||||
nameParts := fieldNameRegexp.FindAllString(name, -1)
|
nameParts := fieldNameRegexp.FindAllString(name, -1)
|
||||||
for i, p := range nameParts {
|
for i, p := range nameParts {
|
||||||
|
|
|
@ -163,6 +163,24 @@ func TestLoad(t *testing.T) {
|
||||||
assert.Equal(t, sampleConfig.Volumes, actual.Volumes)
|
assert.Equal(t, sampleConfig.Volumes, actual.Volumes)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestLoadV31(t *testing.T) {
|
||||||
|
actual, err := loadYAML(`
|
||||||
|
version: "3.1"
|
||||||
|
services:
|
||||||
|
foo:
|
||||||
|
image: busybox
|
||||||
|
secrets: [super]
|
||||||
|
secrets:
|
||||||
|
super:
|
||||||
|
external: true
|
||||||
|
`)
|
||||||
|
if !assert.NoError(t, err) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
assert.Equal(t, len(actual.Services), 1)
|
||||||
|
assert.Equal(t, len(actual.Secrets), 1)
|
||||||
|
}
|
||||||
|
|
||||||
func TestParseAndLoad(t *testing.T) {
|
func TestParseAndLoad(t *testing.T) {
|
||||||
actual, err := loadYAML(sampleYAML)
|
actual, err := loadYAML(sampleYAML)
|
||||||
if !assert.NoError(t, err) {
|
if !assert.NoError(t, err) {
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -374,9 +374,9 @@
|
||||||
"properties": {
|
"properties": {
|
||||||
"name": {"type": "string"}
|
"name": {"type": "string"}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"labels": {"$ref": "#/definitions/list_or_dict"}
|
||||||
},
|
},
|
||||||
"labels": {"$ref": "#/definitions/list_or_dict"},
|
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -71,6 +71,7 @@ type Config struct {
|
||||||
Services []ServiceConfig
|
Services []ServiceConfig
|
||||||
Networks map[string]NetworkConfig
|
Networks map[string]NetworkConfig
|
||||||
Volumes map[string]VolumeConfig
|
Volumes map[string]VolumeConfig
|
||||||
|
Secrets map[string]SecretConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServiceConfig is the configuration of one service
|
// ServiceConfig is the configuration of one service
|
||||||
|
@ -108,6 +109,7 @@ type ServiceConfig struct {
|
||||||
Privileged bool
|
Privileged bool
|
||||||
ReadOnly bool `mapstructure:"read_only"`
|
ReadOnly bool `mapstructure:"read_only"`
|
||||||
Restart string
|
Restart string
|
||||||
|
Secrets []ServiceSecretConfig
|
||||||
SecurityOpt []string `mapstructure:"security_opt"`
|
SecurityOpt []string `mapstructure:"security_opt"`
|
||||||
StdinOpen bool `mapstructure:"stdin_open"`
|
StdinOpen bool `mapstructure:"stdin_open"`
|
||||||
StopGracePeriod *time.Duration `mapstructure:"stop_grace_period"`
|
StopGracePeriod *time.Duration `mapstructure:"stop_grace_period"`
|
||||||
|
@ -191,6 +193,15 @@ type ServiceNetworkConfig struct {
|
||||||
Ipv6Address string `mapstructure:"ipv6_address"`
|
Ipv6Address string `mapstructure:"ipv6_address"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ServiceSecretConfig is the secret configuration for a service
|
||||||
|
type ServiceSecretConfig struct {
|
||||||
|
Source string
|
||||||
|
Target string
|
||||||
|
UID string
|
||||||
|
GID string
|
||||||
|
Mode uint32
|
||||||
|
}
|
||||||
|
|
||||||
// UlimitsConfig the ulimit configuration
|
// UlimitsConfig the ulimit configuration
|
||||||
type UlimitsConfig struct {
|
type UlimitsConfig struct {
|
||||||
Single int
|
Single int
|
||||||
|
@ -233,3 +244,10 @@ type External struct {
|
||||||
Name string
|
Name string
|
||||||
External bool
|
External bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SecretConfig for a secret
|
||||||
|
type SecretConfig struct {
|
||||||
|
File string
|
||||||
|
External External
|
||||||
|
Labels map[string]string `compose:"list_or_dict_equals"`
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue