diff --git a/aci/backend.go b/aci/backend.go index 0a192ef82..9f015f71f 100644 --- a/aci/backend.go +++ b/aci/backend.go @@ -19,6 +19,7 @@ package aci import ( "context" "fmt" + "io" "net/http" "strconv" "strings" @@ -31,8 +32,6 @@ import ( "github.com/pkg/errors" "github.com/sirupsen/logrus" - ecstypes "github.com/docker/ecs-plugin/pkg/compose" - "github.com/docker/api/aci/convert" "github.com/docker/api/aci/login" "github.com/docker/api/backend" @@ -42,6 +41,7 @@ import ( "github.com/docker/api/context/cloud" "github.com/docker/api/context/store" "github.com/docker/api/errdefs" + "github.com/docker/api/secrets" ) const ( @@ -127,6 +127,10 @@ func (a *aciAPIService) ComposeService() compose.Service { return a.aciComposeService } +func (a *aciAPIService) SecretsService() secrets.Service { + return nil +} + type aciContainerService struct { ctx store.AciContext } @@ -382,8 +386,8 @@ type aciComposeService struct { ctx store.AciContext } -func (cs *aciComposeService) Up(ctx context.Context, opts cli.ProjectOptions) error { - project, err := cli.ProjectFromOptions(&opts) +func (cs *aciComposeService) Up(ctx context.Context, opts *cli.ProjectOptions) error { + project, err := cli.ProjectFromOptions(opts) if err != nil { return err } @@ -397,13 +401,13 @@ func (cs *aciComposeService) Up(ctx context.Context, opts cli.ProjectOptions) er return createOrUpdateACIContainers(ctx, cs.ctx, groupDefinition) } -func (cs *aciComposeService) Down(ctx context.Context, opts cli.ProjectOptions) error { +func (cs *aciComposeService) Down(ctx context.Context, opts *cli.ProjectOptions) error { var project types.Project if opts.Name != "" { project = types.Project{Name: opts.Name} } else { - fullProject, err := cli.ProjectFromOptions(&opts) + fullProject, err := cli.ProjectFromOptions(opts) if err != nil { return err } @@ -422,14 +426,18 @@ func (cs *aciComposeService) Down(ctx context.Context, opts cli.ProjectOptions) return err } -func (cs *aciComposeService) Ps(ctx context.Context, opts cli.ProjectOptions) ([]ecstypes.ServiceStatus, error) { +func (cs *aciComposeService) Ps(ctx context.Context, opts *cli.ProjectOptions) ([]compose.ServiceStatus, error) { return nil, errdefs.ErrNotImplemented } -func (cs *aciComposeService) Logs(ctx context.Context, opts cli.ProjectOptions) error { +func (cs *aciComposeService) Logs(ctx context.Context, opts *cli.ProjectOptions, w io.Writer) error { return errdefs.ErrNotImplemented } +func (cs *aciComposeService) Convert(ctx context.Context, opts *cli.ProjectOptions) ([]byte, error) { + return nil, errdefs.ErrNotImplemented +} + type aciCloudService struct { loginService login.AzureLoginServiceAPI } diff --git a/backend/backend.go b/backend/backend.go index 53b8df17b..e44b218b8 100644 --- a/backend/backend.go +++ b/backend/backend.go @@ -26,6 +26,7 @@ import ( "github.com/docker/api/compose" "github.com/docker/api/containers" "github.com/docker/api/context/cloud" + "github.com/docker/api/secrets" ) var ( @@ -51,6 +52,7 @@ var backends = struct { // Service aggregates the service interfaces type Service interface { ContainerService() containers.Service + SecretsService() secrets.Service ComposeService() compose.Service } diff --git a/cli/cmd/compose/compose.go b/cli/cmd/compose/compose.go index f2afad296..1e2e52e47 100644 --- a/cli/cmd/compose/compose.go +++ b/cli/cmd/compose/compose.go @@ -19,6 +19,7 @@ package compose import ( "context" + "github.com/compose-spec/compose-go/cli" "github.com/pkg/errors" "github.com/spf13/cobra" @@ -28,6 +29,21 @@ import ( "github.com/docker/api/errdefs" ) +type composeOptions struct { + Name string + WorkingDir string + ConfigPaths []string + Environment []string +} + +func (o *composeOptions) toProjectOptions() (*cli.ProjectOptions, error) { + return cli.NewProjectOptions(o.ConfigPaths, + cli.WithOsEnv, + cli.WithEnv(o.Environment), + cli.WithWorkingDirectory(o.WorkingDir), + cli.WithName(o.Name)) +} + // Command returns the compose command with its child commands func Command() *cobra.Command { command := &cobra.Command{ diff --git a/cli/cmd/compose/down.go b/cli/cmd/compose/down.go index 20544358b..56052f28f 100644 --- a/cli/cmd/compose/down.go +++ b/cli/cmd/compose/down.go @@ -20,14 +20,13 @@ import ( "context" "errors" - "github.com/compose-spec/compose-go/cli" "github.com/spf13/cobra" "github.com/docker/api/client" ) func downCommand() *cobra.Command { - opts := cli.ProjectOptions{} + opts := composeOptions{} downCmd := &cobra.Command{ Use: "down", RunE: func(cmd *cobra.Command, args []string) error { @@ -41,7 +40,7 @@ func downCommand() *cobra.Command { return downCmd } -func runDown(ctx context.Context, opts cli.ProjectOptions) error { +func runDown(ctx context.Context, opts composeOptions) error { c, err := client.New(ctx) if err != nil { return err @@ -52,5 +51,9 @@ func runDown(ctx context.Context, opts cli.ProjectOptions) error { return errors.New("compose not implemented in current context") } - return composeService.Down(ctx, opts) + options, err := opts.toProjectOptions() + if err != nil { + return err + } + return composeService.Down(ctx, options) } diff --git a/cli/cmd/compose/logs.go b/cli/cmd/compose/logs.go index 15e0a2600..aa22e63d5 100644 --- a/cli/cmd/compose/logs.go +++ b/cli/cmd/compose/logs.go @@ -19,15 +19,15 @@ package compose import ( "context" "errors" + "os" - "github.com/compose-spec/compose-go/cli" "github.com/spf13/cobra" "github.com/docker/api/client" ) func logsCommand() *cobra.Command { - opts := cli.ProjectOptions{} + opts := composeOptions{} logsCmd := &cobra.Command{ Use: "logs", RunE: func(cmd *cobra.Command, args []string) error { @@ -41,7 +41,7 @@ func logsCommand() *cobra.Command { return logsCmd } -func runLogs(ctx context.Context, opts cli.ProjectOptions) error { +func runLogs(ctx context.Context, opts composeOptions) error { c, err := client.New(ctx) if err != nil { return err @@ -52,5 +52,9 @@ func runLogs(ctx context.Context, opts cli.ProjectOptions) error { return errors.New("compose not implemented in current context") } - return composeService.Logs(ctx, opts) + options, err := opts.toProjectOptions() + if err != nil { + return err + } + return composeService.Logs(ctx, options, os.Stdout) } diff --git a/cli/cmd/compose/ps.go b/cli/cmd/compose/ps.go index e5930740b..109e31908 100644 --- a/cli/cmd/compose/ps.go +++ b/cli/cmd/compose/ps.go @@ -25,14 +25,13 @@ import ( "strings" "text/tabwriter" - "github.com/compose-spec/compose-go/cli" "github.com/spf13/cobra" "github.com/docker/api/client" ) func psCommand() *cobra.Command { - opts := cli.ProjectOptions{} + opts := composeOptions{} psCmd := &cobra.Command{ Use: "ps", RunE: func(cmd *cobra.Command, args []string) error { @@ -46,7 +45,7 @@ func psCommand() *cobra.Command { return psCmd } -func runPs(ctx context.Context, opts cli.ProjectOptions) error { +func runPs(ctx context.Context, opts composeOptions) error { c, err := client.New(ctx) if err != nil { return err @@ -57,7 +56,11 @@ func runPs(ctx context.Context, opts cli.ProjectOptions) error { return errors.New("compose not implemented in current context") } - serviceList, err := composeService.Ps(ctx, opts) + options, err := opts.toProjectOptions() + if err != nil { + return err + } + serviceList, err := composeService.Ps(ctx, options) if err != nil { return err } diff --git a/cli/cmd/compose/up.go b/cli/cmd/compose/up.go index 4ac19a433..0cd28e9cf 100644 --- a/cli/cmd/compose/up.go +++ b/cli/cmd/compose/up.go @@ -20,7 +20,6 @@ import ( "context" "errors" - "github.com/compose-spec/compose-go/cli" "github.com/spf13/cobra" "github.com/docker/api/client" @@ -28,7 +27,7 @@ import ( ) func upCommand() *cobra.Command { - opts := cli.ProjectOptions{} + opts := composeOptions{} upCmd := &cobra.Command{ Use: "up", RunE: func(cmd *cobra.Command, args []string) error { @@ -44,7 +43,7 @@ func upCommand() *cobra.Command { return upCmd } -func runUp(ctx context.Context, opts cli.ProjectOptions) error { +func runUp(ctx context.Context, opts composeOptions) error { c, err := client.New(ctx) if err != nil { return err @@ -56,6 +55,10 @@ func runUp(ctx context.Context, opts cli.ProjectOptions) error { } return progress.Run(ctx, func(ctx context.Context) error { - return composeService.Up(ctx, opts) + options, err := opts.toProjectOptions() + if err != nil { + return err + } + return composeService.Up(ctx, options) }) } diff --git a/client/client.go b/client/client.go index f099c84a2..64b98e89e 100644 --- a/client/client.go +++ b/client/client.go @@ -19,12 +19,13 @@ package client import ( "context" - "github.com/docker/api/context/cloud" + "github.com/docker/api/secrets" "github.com/docker/api/backend" "github.com/docker/api/compose" "github.com/docker/api/containers" apicontext "github.com/docker/api/context" + "github.com/docker/api/context/cloud" "github.com/docker/api/context/store" ) @@ -69,3 +70,8 @@ func (c *Client) ContainerService() containers.Service { func (c *Client) ComposeService() compose.Service { return c.bs.ComposeService() } + +// SecretsService returns the backend service for the current context +func (c *Client) SecretsService() secrets.Service { + return c.bs.SecretsService() +} diff --git a/compose/api.go b/compose/api.go index a9592d694..5a917037d 100644 --- a/compose/api.go +++ b/compose/api.go @@ -18,19 +18,39 @@ package compose import ( "context" + "io" "github.com/compose-spec/compose-go/cli" - types "github.com/docker/ecs-plugin/pkg/compose" ) // Service manages a compose project type Service interface { // Up executes the equivalent to a `compose up` - Up(ctx context.Context, opts cli.ProjectOptions) error + Up(ctx context.Context, opts *cli.ProjectOptions) error // Down executes the equivalent to a `compose down` - Down(ctx context.Context, opts cli.ProjectOptions) error + Down(ctx context.Context, opts *cli.ProjectOptions) error // Logs executes the equivalent to a `compose logs` - Logs(ctx context.Context, opts cli.ProjectOptions) error + Logs(ctx context.Context, opts *cli.ProjectOptions, w io.Writer) error // Ps executes the equivalent to a `compose ps` - Ps(ctx context.Context, opts cli.ProjectOptions) ([]types.ServiceStatus, error) + Ps(ctx context.Context, opts *cli.ProjectOptions) ([]ServiceStatus, error) + // Convert translate compose model into backend's native format + Convert(ctx context.Context, opts *cli.ProjectOptions) ([]byte, error) +} + +// PortPublisher hold status about published port +type PortPublisher struct { + URL string + TargetPort int + PublishedPort int + Protocol string +} + +// ServiceStatus hold status about a service +type ServiceStatus struct { + ID string + Name string + Replicas int + Desired int + Ports []string + Publishers []PortPublisher } diff --git a/compose/tags.go b/compose/tags.go new file mode 100644 index 000000000..300e55bbf --- /dev/null +++ b/compose/tags.go @@ -0,0 +1,26 @@ +/* + Copyright 2020 Docker, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package compose + +const ( + // ProjectTag allow to track resource related to a compose project + ProjectTag = "com.docker.compose.project" + // NetworkTag allow to track resource related to a compose network + NetworkTag = "com.docker.compose.network" + // ServiceTag allow to track resource related to a compose service + ServiceTag = "com.docker.compose.service" +) diff --git a/ecs/backend.go b/ecs/backend.go index 3b1e91327..107902f32 100644 --- a/ecs/backend.go +++ b/ecs/backend.go @@ -1,5 +1,3 @@ -// +build ecs - /* Copyright 2020 Docker, Inc. @@ -21,7 +19,10 @@ package ecs import ( "context" - ecsplugin "github.com/docker/ecs-plugin/pkg/amazon/backend" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/session" + + "github.com/docker/api/secrets" "github.com/docker/api/backend" "github.com/docker/api/compose" @@ -61,19 +62,28 @@ func service(ctx context.Context) (backend.Service, error) { } func getEcsAPIService(ecsCtx store.EcsContext) (*ecsAPIService, error) { - backend, err := ecsplugin.NewBackend(ecsCtx.Profile, ecsCtx.Region) + sess, err := session.NewSessionWithOptions(session.Options{ + Profile: ecsCtx.Profile, + SharedConfigState: session.SharedConfigEnable, + Config: aws.Config{ + Region: aws.String(ecsCtx.Region), + }, + }) if err != nil { return nil, err } + return &ecsAPIService{ - ctx: ecsCtx, - composeBackend: backend, + ctx: ecsCtx, + Region: ecsCtx.Region, + SDK: newSDK(sess), }, nil } type ecsAPIService struct { - ctx store.EcsContext - composeBackend *ecsplugin.Backend + ctx store.EcsContext + Region string + SDK sdk } func (a *ecsAPIService) ContainerService() containers.Service { @@ -81,7 +91,11 @@ func (a *ecsAPIService) ContainerService() containers.Service { } func (a *ecsAPIService) ComposeService() compose.Service { - return a.composeBackend + return a +} + +func (a *ecsAPIService) SecretsService() secrets.Service { + return a } func getCloudService() (cloud.Service, error) { diff --git a/ecs/cloudformation.go b/ecs/cloudformation.go new file mode 100644 index 000000000..9481a3503 --- /dev/null +++ b/ecs/cloudformation.go @@ -0,0 +1,619 @@ +/* + Copyright 2020 Docker, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package ecs + +import ( + "context" + "fmt" + "io/ioutil" + "regexp" + "strings" + + "github.com/compose-spec/compose-go/cli" + + "github.com/docker/api/compose" + + ecsapi "github.com/aws/aws-sdk-go/service/ecs" + "github.com/aws/aws-sdk-go/service/elbv2" + cloudmapapi "github.com/aws/aws-sdk-go/service/servicediscovery" + "github.com/awslabs/goformation/v4/cloudformation" + "github.com/awslabs/goformation/v4/cloudformation/ec2" + "github.com/awslabs/goformation/v4/cloudformation/ecs" + "github.com/awslabs/goformation/v4/cloudformation/elasticloadbalancingv2" + "github.com/awslabs/goformation/v4/cloudformation/iam" + "github.com/awslabs/goformation/v4/cloudformation/logs" + "github.com/awslabs/goformation/v4/cloudformation/secretsmanager" + cloudmap "github.com/awslabs/goformation/v4/cloudformation/servicediscovery" + "github.com/awslabs/goformation/v4/cloudformation/tags" + "github.com/compose-spec/compose-go/compatibility" + "github.com/compose-spec/compose-go/errdefs" + "github.com/compose-spec/compose-go/types" + "github.com/sirupsen/logrus" +) + +const ( + parameterClusterName = "ParameterClusterName" + parameterVPCId = "ParameterVPCId" + parameterSubnet1Id = "ParameterSubnet1Id" + parameterSubnet2Id = "ParameterSubnet2Id" + parameterLoadBalancerARN = "ParameterLoadBalancerARN" +) + +func (b *ecsAPIService) Convert(ctx context.Context, opts *cli.ProjectOptions) ([]byte, error) { + project, err := cli.ProjectFromOptions(opts) + if err != nil { + return nil, err + } + template, err := b.convert(project) + if err != nil { + return nil, err + } + return marshall(template) +} + +// Convert a compose project into a CloudFormation template +func (b *ecsAPIService) convert(project *types.Project) (*cloudformation.Template, error) { //nolint:gocyclo + var checker compatibility.Checker = &fargateCompatibilityChecker{ + compatibility.AllowList{ + Supported: compatibleComposeAttributes, + }, + } + compatibility.Check(project, checker) + for _, err := range checker.Errors() { + if errdefs.IsIncompatibleError(err) { + logrus.Error(err.Error()) + } else { + logrus.Warn(err.Error()) + } + } + if !compatibility.IsCompatible(checker) { + return nil, fmt.Errorf("compose file is incompatible with Amazon ECS") + } + + template := cloudformation.NewTemplate() + template.Description = "CloudFormation template created by Docker for deploying applications on Amazon ECS" + template.Parameters[parameterClusterName] = cloudformation.Parameter{ + Type: "String", + Description: "Name of the ECS cluster to deploy to (optional)", + } + + template.Parameters[parameterVPCId] = cloudformation.Parameter{ + Type: "AWS::EC2::VPC::Id", + Description: "ID of the VPC", + } + + /* + FIXME can't set subnets: Ref("SubnetIds") see https://github.com/awslabs/goformation/issues/282 + template.Parameters["SubnetIds"] = cloudformation.Parameter{ + Type: "List", + Description: "The list of SubnetIds, for at least two Availability Zones in the region in your VPC", + } + */ + template.Parameters[parameterSubnet1Id] = cloudformation.Parameter{ + Type: "AWS::EC2::Subnet::Id", + Description: "SubnetId, for Availability Zone 1 in the region in your VPC", + } + template.Parameters[parameterSubnet2Id] = cloudformation.Parameter{ + Type: "AWS::EC2::Subnet::Id", + Description: "SubnetId, for Availability Zone 2 in the region in your VPC", + } + + template.Parameters[parameterLoadBalancerARN] = cloudformation.Parameter{ + Type: "String", + Description: "Name of the LoadBalancer to connect to (optional)", + } + + // Create Cluster is `ParameterClusterName` parameter is not set + template.Conditions["CreateCluster"] = cloudformation.Equals("", cloudformation.Ref(parameterClusterName)) + + cluster := createCluster(project, template) + + networks := map[string]string{} + for _, net := range project.Networks { + networks[net.Name] = convertNetwork(project, net, cloudformation.Ref(parameterVPCId), template) + } + + for i, s := range project.Secrets { + if s.External.External { + continue + } + secret, err := ioutil.ReadFile(s.File) + if err != nil { + return nil, err + } + + name := fmt.Sprintf("%sSecret", normalizeResourceName(s.Name)) + template.Resources[name] = &secretsmanager.Secret{ + Description: "", + SecretString: string(secret), + Tags: []tags.Tag{ + { + Key: compose.ProjectTag, + Value: project.Name, + }, + }, + } + s.Name = cloudformation.Ref(name) + project.Secrets[i] = s + } + + createLogGroup(project, template) + + // Private DNS namespace will allow DNS name for the services to be ..local + createCloudMap(project, template) + + loadBalancerARN := createLoadBalancer(project, template) + + for _, service := range project.Services { + + definition, err := convert(project, service) + if err != nil { + return nil, err + } + + taskExecutionRole, err := createTaskExecutionRole(service, err, definition, template) + if err != nil { + return template, err + } + definition.ExecutionRoleArn = cloudformation.Ref(taskExecutionRole) + + taskDefinition := fmt.Sprintf("%sTaskDefinition", normalizeResourceName(service.Name)) + template.Resources[taskDefinition] = definition + + var healthCheck *cloudmap.Service_HealthCheckConfig + + serviceRegistry := createServiceRegistry(service, template, healthCheck) + + serviceSecurityGroups := []string{} + for net := range service.Networks { + serviceSecurityGroups = append(serviceSecurityGroups, networks[net]) + } + + dependsOn := []string{} + serviceLB := []ecs.Service_LoadBalancer{} + if len(service.Ports) > 0 { + for _, port := range service.Ports { + protocol := strings.ToUpper(port.Protocol) + if getLoadBalancerType(project) == elbv2.LoadBalancerTypeEnumApplication { + protocol = elbv2.ProtocolEnumHttps + if port.Published == 80 { + protocol = elbv2.ProtocolEnumHttp + } + } + if loadBalancerARN != "" { + targetGroupName := createTargetGroup(project, service, port, template, protocol) + listenerName := createListener(service, port, template, targetGroupName, loadBalancerARN, protocol) + dependsOn = append(dependsOn, listenerName) + serviceLB = append(serviceLB, ecs.Service_LoadBalancer{ + ContainerName: service.Name, + ContainerPort: int(port.Target), + TargetGroupArn: cloudformation.Ref(targetGroupName), + }) + } + } + } + + desiredCount := 1 + if service.Deploy != nil && service.Deploy.Replicas != nil { + desiredCount = int(*service.Deploy.Replicas) + } + + for _, dependency := range service.DependsOn { + dependsOn = append(dependsOn, serviceResourceName(dependency)) + } + + minPercent, maxPercent, err := computeRollingUpdateLimits(service) + if err != nil { + return nil, err + } + + template.Resources[serviceResourceName(service.Name)] = &ecs.Service{ + AWSCloudFormationDependsOn: dependsOn, + Cluster: cluster, + DesiredCount: desiredCount, + DeploymentController: &ecs.Service_DeploymentController{ + Type: ecsapi.DeploymentControllerTypeEcs, + }, + DeploymentConfiguration: &ecs.Service_DeploymentConfiguration{ + MaximumPercent: maxPercent, + MinimumHealthyPercent: minPercent, + }, + LaunchType: ecsapi.LaunchTypeFargate, + LoadBalancers: serviceLB, + NetworkConfiguration: &ecs.Service_NetworkConfiguration{ + AwsvpcConfiguration: &ecs.Service_AwsVpcConfiguration{ + AssignPublicIp: ecsapi.AssignPublicIpEnabled, + SecurityGroups: serviceSecurityGroups, + Subnets: []string{ + cloudformation.Ref(parameterSubnet1Id), + cloudformation.Ref(parameterSubnet2Id), + }, + }, + }, + PropagateTags: ecsapi.PropagateTagsService, + SchedulingStrategy: ecsapi.SchedulingStrategyReplica, + ServiceRegistries: []ecs.Service_ServiceRegistry{serviceRegistry}, + Tags: []tags.Tag{ + { + Key: compose.ProjectTag, + Value: project.Name, + }, + { + Key: compose.ServiceTag, + Value: service.Name, + }, + }, + TaskDefinition: cloudformation.Ref(normalizeResourceName(taskDefinition)), + } + } + return template, nil +} + +func createLogGroup(project *types.Project, template *cloudformation.Template) { + retention := 0 + if v, ok := project.Extensions[extensionRetention]; ok { + retention = v.(int) + } + logGroup := fmt.Sprintf("/docker-compose/%s", project.Name) + template.Resources["LogGroup"] = &logs.LogGroup{ + LogGroupName: logGroup, + RetentionInDays: retention, + } +} + +func computeRollingUpdateLimits(service types.ServiceConfig) (int, int, error) { + maxPercent := 200 + minPercent := 100 + if service.Deploy == nil || service.Deploy.UpdateConfig == nil { + return minPercent, maxPercent, nil + } + updateConfig := service.Deploy.UpdateConfig + min, okMin := updateConfig.Extensions[extensionMinPercent] + if okMin { + minPercent = min.(int) + } + max, okMax := updateConfig.Extensions[extensionMaxPercent] + if okMax { + maxPercent = max.(int) + } + if okMin && okMax { + return minPercent, maxPercent, nil + } + + if updateConfig.Parallelism != nil { + parallelism := int(*updateConfig.Parallelism) + if service.Deploy.Replicas == nil { + return minPercent, maxPercent, + fmt.Errorf("rolling update configuration require deploy.replicas to be set") + } + replicas := int(*service.Deploy.Replicas) + if replicas < parallelism { + return minPercent, maxPercent, + fmt.Errorf("deploy.replicas (%d) must be greater than deploy.update_config.parallelism (%d)", replicas, parallelism) + } + if !okMin { + minPercent = (replicas - parallelism) * 100 / replicas + } + if !okMax { + maxPercent = (replicas + parallelism) * 100 / replicas + } + } + return minPercent, maxPercent, nil +} + +func getLoadBalancerType(project *types.Project) string { + for _, service := range project.Services { + for _, port := range service.Ports { + if port.Published != 80 && port.Published != 443 { + return elbv2.LoadBalancerTypeEnumNetwork + } + } + } + return elbv2.LoadBalancerTypeEnumApplication +} + +func getLoadBalancerSecurityGroups(project *types.Project, template *cloudformation.Template) []string { + securityGroups := []string{} + for _, network := range project.Networks { + if !network.Internal { + net := convertNetwork(project, network, cloudformation.Ref(parameterVPCId), template) + securityGroups = append(securityGroups, net) + } + } + return uniqueStrings(securityGroups) +} + +func createLoadBalancer(project *types.Project, template *cloudformation.Template) string { + ports := 0 + for _, service := range project.Services { + ports += len(service.Ports) + } + if ports == 0 { + // Project do not expose any port (batch jobs?) + // So no need to create a PortPublisher + return "" + } + + // load balancer names are limited to 32 characters total + loadBalancerName := fmt.Sprintf("%.32s", fmt.Sprintf("%sLoadBalancer", strings.Title(project.Name))) + // Create PortPublisher if `ParameterLoadBalancerName` is not set + template.Conditions["CreateLoadBalancer"] = cloudformation.Equals("", cloudformation.Ref(parameterLoadBalancerARN)) + + loadBalancerType := getLoadBalancerType(project) + securityGroups := []string{} + if loadBalancerType == elbv2.LoadBalancerTypeEnumApplication { + securityGroups = getLoadBalancerSecurityGroups(project, template) + } + + template.Resources[loadBalancerName] = &elasticloadbalancingv2.LoadBalancer{ + Name: loadBalancerName, + Scheme: elbv2.LoadBalancerSchemeEnumInternetFacing, + SecurityGroups: securityGroups, + Subnets: []string{ + cloudformation.Ref(parameterSubnet1Id), + cloudformation.Ref(parameterSubnet2Id), + }, + Tags: []tags.Tag{ + { + Key: compose.ProjectTag, + Value: project.Name, + }, + }, + Type: loadBalancerType, + AWSCloudFormationCondition: "CreateLoadBalancer", + } + return cloudformation.If("CreateLoadBalancer", cloudformation.Ref(loadBalancerName), cloudformation.Ref(parameterLoadBalancerARN)) +} + +func createListener(service types.ServiceConfig, port types.ServicePortConfig, template *cloudformation.Template, targetGroupName string, loadBalancerARN string, protocol string) string { + listenerName := fmt.Sprintf( + "%s%s%dListener", + normalizeResourceName(service.Name), + strings.ToUpper(port.Protocol), + port.Target, + ) + //add listener to dependsOn + //https://stackoverflow.com/questions/53971873/the-target-group-does-not-have-an-associated-load-balancer + template.Resources[listenerName] = &elasticloadbalancingv2.Listener{ + DefaultActions: []elasticloadbalancingv2.Listener_Action{ + { + ForwardConfig: &elasticloadbalancingv2.Listener_ForwardConfig{ + TargetGroups: []elasticloadbalancingv2.Listener_TargetGroupTuple{ + { + TargetGroupArn: cloudformation.Ref(targetGroupName), + }, + }, + }, + Type: elbv2.ActionTypeEnumForward, + }, + }, + LoadBalancerArn: loadBalancerARN, + Protocol: protocol, + Port: int(port.Target), + } + return listenerName +} + +func createTargetGroup(project *types.Project, service types.ServiceConfig, port types.ServicePortConfig, template *cloudformation.Template, protocol string) string { + targetGroupName := fmt.Sprintf( + "%s%s%dTargetGroup", + normalizeResourceName(service.Name), + strings.ToUpper(port.Protocol), + port.Published, + ) + template.Resources[targetGroupName] = &elasticloadbalancingv2.TargetGroup{ + Port: int(port.Target), + Protocol: protocol, + Tags: []tags.Tag{ + { + Key: compose.ProjectTag, + Value: project.Name, + }, + }, + VpcId: cloudformation.Ref(parameterVPCId), + TargetType: elbv2.TargetTypeEnumIp, + } + return targetGroupName +} + +func createServiceRegistry(service types.ServiceConfig, template *cloudformation.Template, healthCheck *cloudmap.Service_HealthCheckConfig) ecs.Service_ServiceRegistry { + serviceRegistration := fmt.Sprintf("%sServiceDiscoveryEntry", normalizeResourceName(service.Name)) + serviceRegistry := ecs.Service_ServiceRegistry{ + RegistryArn: cloudformation.GetAtt(serviceRegistration, "Arn"), + } + + template.Resources[serviceRegistration] = &cloudmap.Service{ + Description: fmt.Sprintf("%q service discovery entry in Cloud Map", service.Name), + HealthCheckConfig: healthCheck, + HealthCheckCustomConfig: &cloudmap.Service_HealthCheckCustomConfig{ + FailureThreshold: 1, + }, + Name: service.Name, + NamespaceId: cloudformation.Ref("CloudMap"), + DnsConfig: &cloudmap.Service_DnsConfig{ + DnsRecords: []cloudmap.Service_DnsRecord{ + { + TTL: 60, + Type: cloudmapapi.RecordTypeA, + }, + }, + RoutingPolicy: cloudmapapi.RoutingPolicyMultivalue, + }, + } + return serviceRegistry +} + +func createTaskExecutionRole(service types.ServiceConfig, err error, definition *ecs.TaskDefinition, template *cloudformation.Template) (string, error) { + taskExecutionRole := fmt.Sprintf("%sTaskExecutionRole", normalizeResourceName(service.Name)) + policy := getPolicy(definition) + if err != nil { + return taskExecutionRole, err + } + rolePolicies := []iam.Role_Policy{} + if policy != nil { + rolePolicies = append(rolePolicies, iam.Role_Policy{ + PolicyDocument: policy, + PolicyName: fmt.Sprintf("%sGrantAccessToSecrets", service.Name), + }) + } + + if roles, ok := service.Extensions[extensionRole]; ok { + rolePolicies = append(rolePolicies, iam.Role_Policy{ + PolicyDocument: roles, + }) + } + managedPolicies := []string{ + ecsTaskExecutionPolicy, + ecrReadOnlyPolicy, + } + if v, ok := service.Extensions[extensionManagedPolicies]; ok { + for _, s := range v.([]interface{}) { + managedPolicies = append(managedPolicies, s.(string)) + } + } + template.Resources[taskExecutionRole] = &iam.Role{ + AssumeRolePolicyDocument: assumeRolePolicyDocument, + Policies: rolePolicies, + ManagedPolicyArns: managedPolicies, + } + return taskExecutionRole, nil +} + +func createCluster(project *types.Project, template *cloudformation.Template) string { + template.Resources["Cluster"] = &ecs.Cluster{ + ClusterName: project.Name, + Tags: []tags.Tag{ + { + Key: compose.ProjectTag, + Value: project.Name, + }, + }, + AWSCloudFormationCondition: "CreateCluster", + } + cluster := cloudformation.If("CreateCluster", cloudformation.Ref("Cluster"), cloudformation.Ref(parameterClusterName)) + return cluster +} + +func createCloudMap(project *types.Project, template *cloudformation.Template) { + template.Resources["CloudMap"] = &cloudmap.PrivateDnsNamespace{ + Description: fmt.Sprintf("Service Map for Docker Compose project %s", project.Name), + Name: fmt.Sprintf("%s.local", project.Name), + Vpc: cloudformation.Ref(parameterVPCId), + } +} + +func convertNetwork(project *types.Project, net types.NetworkConfig, vpc string, template *cloudformation.Template) string { + if sg, ok := net.Extensions[extensionSecurityGroup]; ok { + logrus.Debugf("Security Group for network %q set by user to %q", net.Name, sg) + return sg.(string) + } + + var ingresses []ec2.SecurityGroup_Ingress + if !net.Internal { + for _, service := range project.Services { + if _, ok := service.Networks[net.Name]; ok { + for _, port := range service.Ports { + ingresses = append(ingresses, ec2.SecurityGroup_Ingress{ + CidrIp: "0.0.0.0/0", + Description: fmt.Sprintf("%s:%d/%s", service.Name, port.Target, port.Protocol), + FromPort: int(port.Target), + IpProtocol: strings.ToUpper(port.Protocol), + ToPort: int(port.Target), + }) + } + } + } + } + + securityGroup := networkResourceName(project, net.Name) + template.Resources[securityGroup] = &ec2.SecurityGroup{ + GroupDescription: fmt.Sprintf("%s %s Security Group", project.Name, net.Name), + GroupName: securityGroup, + SecurityGroupIngress: ingresses, + VpcId: vpc, + Tags: []tags.Tag{ + { + Key: compose.ProjectTag, + Value: project.Name, + }, + { + Key: compose.NetworkTag, + Value: net.Name, + }, + }, + } + + ingress := securityGroup + "Ingress" + template.Resources[ingress] = &ec2.SecurityGroupIngress{ + Description: fmt.Sprintf("Allow communication within network %s", net.Name), + IpProtocol: "-1", // all protocols + GroupId: cloudformation.Ref(securityGroup), + SourceSecurityGroupId: cloudformation.Ref(securityGroup), + } + + return cloudformation.Ref(securityGroup) +} + +func networkResourceName(project *types.Project, network string) string { + return fmt.Sprintf("%s%sNetwork", normalizeResourceName(project.Name), normalizeResourceName(network)) +} + +func serviceResourceName(dependency string) string { + return fmt.Sprintf("%sService", normalizeResourceName(dependency)) +} + +func normalizeResourceName(s string) string { + return strings.Title(regexp.MustCompile("[^a-zA-Z0-9]+").ReplaceAllString(s, "")) +} + +func getPolicy(taskDef *ecs.TaskDefinition) *PolicyDocument { + arns := []string{} + for _, container := range taskDef.ContainerDefinitions { + if container.RepositoryCredentials != nil { + arns = append(arns, container.RepositoryCredentials.CredentialsParameter) + } + if len(container.Secrets) > 0 { + for _, s := range container.Secrets { + arns = append(arns, s.ValueFrom) + } + } + + } + if len(arns) > 0 { + return &PolicyDocument{ + Statement: []PolicyStatement{ + { + Effect: "Allow", + Action: []string{actionGetSecretValue, actionGetParameters, actionDecrypt}, + Resource: arns, + }}, + } + } + return nil +} + +func uniqueStrings(items []string) []string { + keys := make(map[string]bool) + unique := []string{} + for _, item := range items { + if _, val := keys[item]; !val { + keys[item] = true + unique = append(unique, item) + } + } + return unique +} diff --git a/ecs/cloudformation_test.go b/ecs/cloudformation_test.go new file mode 100644 index 000000000..029591207 --- /dev/null +++ b/ecs/cloudformation_test.go @@ -0,0 +1,396 @@ +/* + Copyright 2020 Docker, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package ecs + +import ( + "fmt" + "reflect" + "testing" + + "github.com/docker/api/compose" + + "github.com/aws/aws-sdk-go/service/elbv2" + "github.com/awslabs/goformation/v4/cloudformation" + "github.com/awslabs/goformation/v4/cloudformation/ec2" + "github.com/awslabs/goformation/v4/cloudformation/ecs" + "github.com/awslabs/goformation/v4/cloudformation/elasticloadbalancingv2" + "github.com/awslabs/goformation/v4/cloudformation/iam" + "github.com/awslabs/goformation/v4/cloudformation/logs" + "github.com/compose-spec/compose-go/cli" + "github.com/compose-spec/compose-go/loader" + "github.com/compose-spec/compose-go/types" + "gotest.tools/v3/assert" + "gotest.tools/v3/golden" +) + +func TestSimpleConvert(t *testing.T) { + project := load(t, "testdata/input/simple-single-service.yaml") + result := convertResultAsString(t, project) + expected := "simple/simple-cloudformation-conversion.golden" + golden.Assert(t, result, expected) +} + +func TestLogging(t *testing.T) { + template := convertYaml(t, ` +services: + foo: + image: hello_world + logging: + options: + awslogs-datetime-pattern: "FOO" + +x-aws-logs_retention: 10 +`) + def := template.Resources["FooTaskDefinition"].(*ecs.TaskDefinition) + logging := def.ContainerDefinitions[0].LogConfiguration + assert.Equal(t, logging.Options["awslogs-datetime-pattern"], "FOO") + + logGroup := template.Resources["LogGroup"].(*logs.LogGroup) + assert.Equal(t, logGroup.RetentionInDays, 10) +} + +func TestEnvFile(t *testing.T) { + template := convertYaml(t, ` +services: + foo: + image: hello_world + env_file: + - testdata/input/envfile +`) + def := template.Resources["FooTaskDefinition"].(*ecs.TaskDefinition) + env := def.ContainerDefinitions[0].Environment + var found bool + for _, pair := range env { + if pair.Name == "FOO" { + assert.Equal(t, pair.Value, "BAR") + found = true + } + } + assert.Check(t, found, "environment variable FOO not set") +} + +func TestEnvFileAndEnv(t *testing.T) { + template := convertYaml(t, ` +services: + foo: + image: hello_world + env_file: + - testdata/input/envfile + environment: + - "FOO=ZOT" +`) + def := template.Resources["FooTaskDefinition"].(*ecs.TaskDefinition) + env := def.ContainerDefinitions[0].Environment + var found bool + for _, pair := range env { + if pair.Name == "FOO" { + assert.Equal(t, pair.Value, "ZOT") + found = true + } + } + assert.Check(t, found, "environment variable FOO not set") +} + +func TestRollingUpdateLimits(t *testing.T) { + template := convertYaml(t, ` +services: + foo: + image: hello_world + deploy: + replicas: 4 + update_config: + parallelism: 2 +`) + service := template.Resources["FooService"].(*ecs.Service) + assert.Check(t, service.DeploymentConfiguration.MaximumPercent == 150) + assert.Check(t, service.DeploymentConfiguration.MinimumHealthyPercent == 50) +} + +func TestRollingUpdateExtension(t *testing.T) { + template := convertYaml(t, ` +services: + foo: + image: hello_world + deploy: + update_config: + x-aws-min_percent: 25 + x-aws-max_percent: 125 +`) + service := template.Resources["FooService"].(*ecs.Service) + assert.Check(t, service.DeploymentConfiguration.MaximumPercent == 125) + assert.Check(t, service.DeploymentConfiguration.MinimumHealthyPercent == 25) +} + +func TestRolePolicy(t *testing.T) { + template := convertYaml(t, ` +services: + foo: + image: hello_world + x-aws-pull_credentials: "secret" +`) + x := template.Resources["FooTaskExecutionRole"] + assert.Check(t, x != nil) + role := *(x.(*iam.Role)) + assert.Check(t, role.ManagedPolicyArns[0] == ecsTaskExecutionPolicy) + assert.Check(t, role.ManagedPolicyArns[1] == ecrReadOnlyPolicy) + // We expect an extra policy has been created for x-aws-pull_credentials + assert.Check(t, len(role.Policies) == 1) + policy := role.Policies[0].PolicyDocument.(*PolicyDocument) + expected := []string{"secretsmanager:GetSecretValue", "ssm:GetParameters", "kms:Decrypt"} + assert.DeepEqual(t, expected, policy.Statement[0].Action) + assert.DeepEqual(t, []string{"secret"}, policy.Statement[0].Resource) +} + +func TestMapNetworksToSecurityGroups(t *testing.T) { + template := convertYaml(t, ` +services: + test: + image: hello_world + networks: + - front-tier + - back-tier + +networks: + front-tier: + name: public + back-tier: + internal: true +`) + assert.Check(t, template.Resources["TestPublicNetwork"] != nil) + assert.Check(t, template.Resources["TestBacktierNetwork"] != nil) + assert.Check(t, template.Resources["TestBacktierNetworkIngress"] != nil) + i := template.Resources["TestPublicNetworkIngress"] + assert.Check(t, i != nil) + ingress := *i.(*ec2.SecurityGroupIngress) + assert.Check(t, ingress.SourceSecurityGroupId == cloudformation.Ref("TestPublicNetwork")) + +} + +func TestLoadBalancerTypeApplication(t *testing.T) { + template := convertYaml(t, ` +services: + test: + image: nginx + ports: + - 80:80 +`) + lb := template.Resources["TestLoadBalancer"] + assert.Check(t, lb != nil) + loadBalancer := *lb.(*elasticloadbalancingv2.LoadBalancer) + assert.Check(t, len(loadBalancer.Name) <= 32) + assert.Check(t, loadBalancer.Type == elbv2.LoadBalancerTypeEnumApplication) + assert.Check(t, len(loadBalancer.SecurityGroups) > 0) +} + +func TestNoLoadBalancerIfNoPortExposed(t *testing.T) { + template := convertYaml(t, ` +services: + test: + image: nginx + foo: + image: bar +`) + for _, r := range template.Resources { + assert.Check(t, r.AWSCloudFormationType() != "AWS::ElasticLoadBalancingV2::TargetGroup") + assert.Check(t, r.AWSCloudFormationType() != "AWS::ElasticLoadBalancingV2::Listener") + assert.Check(t, r.AWSCloudFormationType() != "AWS::ElasticLoadBalancingV2::PortPublisher") + } +} + +func TestServiceReplicas(t *testing.T) { + template := convertYaml(t, ` +services: + test: + image: nginx + deploy: + replicas: 10 +`) + s := template.Resources["TestService"] + assert.Check(t, s != nil) + service := *s.(*ecs.Service) + assert.Check(t, service.DesiredCount == 10) +} + +func TestTaskSizeConvert(t *testing.T) { + template := convertYaml(t, ` +services: + test: + image: nginx + deploy: + resources: + limits: + cpus: '0.5' + memory: 2048M + reservations: + cpus: '0.5' + memory: 2048M +`) + def := template.Resources["TestTaskDefinition"].(*ecs.TaskDefinition) + assert.Equal(t, def.Cpu, "512") + assert.Equal(t, def.Memory, "2048") + + template = convertYaml(t, ` +services: + test: + image: nginx + deploy: + resources: + limits: + cpus: '4' + memory: 8192M + reservations: + cpus: '4' + memory: 8192M +`) + def = template.Resources["TestTaskDefinition"].(*ecs.TaskDefinition) + assert.Equal(t, def.Cpu, "4096") + assert.Equal(t, def.Memory, "8192") +} +func TestTaskSizeConvertFailure(t *testing.T) { + model := loadConfig(t, ` +services: + test: + image: nginx + deploy: + resources: + limits: + cpus: '0.5' + memory: 2043248M +`) + backend := &ecsAPIService{} + _, err := backend.convert(model) + assert.ErrorContains(t, err, "the resources requested are not supported by ECS/Fargate") +} + +func TestLoadBalancerTypeNetwork(t *testing.T) { + template := convertYaml(t, ` +services: + test: + image: nginx + ports: + - 80:80 + - 88:88 +`) + lb := template.Resources["TestLoadBalancer"] + assert.Check(t, lb != nil) + loadBalancer := *lb.(*elasticloadbalancingv2.LoadBalancer) + assert.Check(t, loadBalancer.Type == elbv2.LoadBalancerTypeEnumNetwork) +} + +func TestServiceMapping(t *testing.T) { + template := convertYaml(t, ` +services: + test: + image: "image" + command: "command" + entrypoint: "entrypoint" + environment: + - "FOO=BAR" + cap_add: + - SYS_PTRACE + cap_drop: + - SYSLOG + init: true + user: "user" + working_dir: "working_dir" +`) + def := template.Resources["TestTaskDefinition"].(*ecs.TaskDefinition) + container := def.ContainerDefinitions[0] + assert.Equal(t, container.Image, "image") + assert.Equal(t, container.Command[0], "command") + assert.Equal(t, container.EntryPoint[0], "entrypoint") + assert.Equal(t, get(container.Environment, "FOO"), "BAR") + assert.Check(t, container.LinuxParameters.InitProcessEnabled) + assert.Equal(t, container.LinuxParameters.Capabilities.Add[0], "SYS_PTRACE") + assert.Equal(t, container.LinuxParameters.Capabilities.Drop[0], "SYSLOG") + assert.Equal(t, container.User, "user") + assert.Equal(t, container.WorkingDirectory, "working_dir") +} + +func get(l []ecs.TaskDefinition_KeyValuePair, name string) string { + for _, e := range l { + if e.Name == name { + return e.Value + } + } + return "" +} + +func TestResourcesHaveProjectTagSet(t *testing.T) { + template := convertYaml(t, ` +services: + test: + image: nginx + ports: + - 80:80 + - 88:88 +`) + for _, r := range template.Resources { + tags := reflect.Indirect(reflect.ValueOf(r)).FieldByName("Tags") + if !tags.IsValid() { + continue + } + for i := 0; i < tags.Len(); i++ { + k := tags.Index(i).FieldByName("Key").String() + v := tags.Index(i).FieldByName("Value").String() + if k == compose.ProjectTag { + assert.Equal(t, v, "Test") + } + } + } +} + +func convertResultAsString(t *testing.T, project *types.Project) string { + backend := &ecsAPIService{} + template, err := backend.convert(project) + assert.NilError(t, err) + resultAsJSON, err := marshall(template) + assert.NilError(t, err) + return fmt.Sprintf("%s\n", string(resultAsJSON)) +} + +func load(t *testing.T, paths ...string) *types.Project { + options := cli.ProjectOptions{ + Name: t.Name(), + ConfigPaths: paths, + } + project, err := cli.ProjectFromOptions(&options) + assert.NilError(t, err) + return project +} + +func convertYaml(t *testing.T, yaml string) *cloudformation.Template { + project := loadConfig(t, yaml) + backend := &ecsAPIService{} + template, err := backend.convert(project) + assert.NilError(t, err) + return template +} + +func loadConfig(t *testing.T, yaml string) *types.Project { + dict, err := loader.ParseYAML([]byte(yaml)) + assert.NilError(t, err) + model, err := loader.Load(types.ConfigDetails{ + ConfigFiles: []types.ConfigFile{ + {Config: dict}, + }, + }, func(options *loader.Options) { + options.Name = "Test" + }) + assert.NilError(t, err) + return model +} diff --git a/ecs/colors.go b/ecs/colors.go new file mode 100644 index 000000000..1486e1381 --- /dev/null +++ b/ecs/colors.go @@ -0,0 +1,81 @@ +/* + Copyright 2020 Docker, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package ecs + +import ( + "fmt" + "strconv" +) + +var names = []string{ + "grey", + "red", + "green", + "yellow", + "blue", + "magenta", + "cyan", + "white", +} + +// colorFunc use ANSI codes to render colored text on console +type colorFunc func(s string) string + +func ansiColor(code, s string) string { + return fmt.Sprintf("%s%s%s", ansi(code), s, ansi("0")) +} + +func ansi(code string) string { + return fmt.Sprintf("\033[%sm", code) +} + +func makeColorFunc(code string) colorFunc { + return func(s string) string { + return ansiColor(code, s) + } +} + +var loop = make(chan colorFunc) + +func init() { + colors := map[string]colorFunc{} + for i, name := range names { + colors[name] = makeColorFunc(strconv.Itoa(30 + i)) + colors["intense_"+name] = makeColorFunc(strconv.Itoa(30+i) + ";1") + } + + go func() { + i := 0 + rainbow := []colorFunc{ + colors["cyan"], + colors["yellow"], + colors["green"], + colors["magenta"], + colors["blue"], + colors["intense_cyan"], + colors["intense_yellow"], + colors["intense_green"], + colors["intense_magenta"], + colors["intense_blue"], + } + + for { + loop <- rainbow[i] + i = (i + 1) % len(rainbow) + } + }() +} diff --git a/ecs/compatibility.go b/ecs/compatibility.go new file mode 100644 index 000000000..803943d1f --- /dev/null +++ b/ecs/compatibility.go @@ -0,0 +1,103 @@ +/* + Copyright 2020 Docker, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package ecs + +import ( + "github.com/compose-spec/compose-go/compatibility" + "github.com/compose-spec/compose-go/types" +) + +type fargateCompatibilityChecker struct { + compatibility.AllowList +} + +var compatibleComposeAttributes = []string{ + "services.command", + "services.container_name", + "services.cap_drop", + "services.depends_on", + "services.deploy", + "services.deploy.replicas", + "services.deploy.resources.limits", + "services.deploy.resources.limits.cpus", + "services.deploy.resources.limits.memory", + "services.deploy.resources.reservations", + "services.deploy.resources.reservations.cpus", + "services.deploy.resources.reservations.memory", + "services.deploy.update_config", + "services.deploy.update_config.parallelism", + "services.entrypoint", + "services.environment", + "services.env_file", + "services.healthcheck", + "services.healthcheck.interval", + "services.healthcheck.retries", + "services.healthcheck.start_period", + "services.healthcheck.test", + "services.healthcheck.timeout", + "services.image", + "services.init", + "services.logging", + "services.logging.options", + "services.networks", + "services.ports", + "services.ports.mode", + "services.ports.target", + "services.ports.protocol", + "services.secrets", + "services.secrets.source", + "services.secrets.target", + "services.user", + "services.working_dir", + "secrets.external", + "secrets.name", + "secrets.file", +} + +func (c *fargateCompatibilityChecker) CheckImage(service *types.ServiceConfig) { + if service.Image == "" { + c.Incompatible("service %s doesn't define a Docker image to run", service.Name) + } +} + +func (c *fargateCompatibilityChecker) CheckPortsPublished(p *types.ServicePortConfig) { + if p.Published == 0 { + p.Published = p.Target + } + if p.Published != p.Target { + c.Incompatible("published port can't be set to a distinct value than container port") + } +} + +func (c *fargateCompatibilityChecker) CheckCapAdd(service *types.ServiceConfig) { + add := []string{} + for _, cap := range service.CapAdd { + switch cap { + case "SYS_PTRACE": + add = append(add, cap) + default: + c.Incompatible("ECS doesn't allow to add capability %s", cap) + } + } + service.CapAdd = add +} + +func (c *fargateCompatibilityChecker) CheckLoggingDriver(config *types.LoggingConfig) { + if config.Driver != "" && config.Driver != "awslogs" { + c.Unsupported("services.logging.driver %s is not supported", config.Driver) + } +} diff --git a/ecs/context.go b/ecs/context.go index f6eaa0cab..65f59e818 100644 --- a/ecs/context.go +++ b/ecs/context.go @@ -1,5 +1,3 @@ -// +build ecs - /* Copyright 2020 Docker, Inc. @@ -105,7 +103,7 @@ func (h contextCreateAWSHelper) saveCredentials(profile string, accessKeyID stri p := credentials.SharedCredentialsProvider{Profile: profile} _, err := p.Retrieve() if err == nil { - return fmt.Errorf("credentials already exists!") + return fmt.Errorf("credentials already exist") } if err.(awserr.Error).Code() == "SharedCredsLoad" && err.(awserr.Error).Message() == "failed to load shared credentials file" { diff --git a/ecs/convert.go b/ecs/convert.go new file mode 100644 index 000000000..0f1f503bf --- /dev/null +++ b/ecs/convert.go @@ -0,0 +1,476 @@ +/* + Copyright 2020 Docker, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package ecs + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "sort" + "strconv" + "strings" + "time" + + "github.com/aws/aws-sdk-go/aws" + ecsapi "github.com/aws/aws-sdk-go/service/ecs" + "github.com/awslabs/goformation/v4/cloudformation" + "github.com/awslabs/goformation/v4/cloudformation/ecs" + "github.com/compose-spec/compose-go/types" + "github.com/docker/cli/opts" + "github.com/joho/godotenv" + + "github.com/docker/api/ecs/secrets" +) + +const secretsInitContainerImage = "docker/ecs-secrets-sidecar" + +func convert(project *types.Project, service types.ServiceConfig) (*ecs.TaskDefinition, error) { + cpu, mem, err := toLimits(service) + if err != nil { + return nil, err + } + _, memReservation := toContainerReservation(service) + credential := getRepoCredentials(service) + + // override resolve.conf search directive to also search .local + // TODO remove once ECS support hostname-only service discovery + service.Environment["LOCALDOMAIN"] = aws.String( + cloudformation.Join("", []string{ + cloudformation.Ref("AWS::Region"), + ".compute.internal", + fmt.Sprintf(" %s.local", project.Name), + })) + + logConfiguration := getLogConfiguration(service, project) + + var ( + initContainers []ecs.TaskDefinition_ContainerDefinition + volumes []ecs.TaskDefinition_Volume + mounts []ecs.TaskDefinition_MountPoint + ) + if len(service.Secrets) > 0 { + secretsVolume, secretsMount, secretsSideCar, err := createSecretsSideCar(project, service, logConfiguration) + if err != nil { + return nil, err + } + initContainers = append(initContainers, secretsSideCar) + volumes = append(volumes, secretsVolume) + mounts = append(mounts, secretsMount) + } + + var dependencies []ecs.TaskDefinition_ContainerDependency + for _, c := range initContainers { + dependencies = append(dependencies, ecs.TaskDefinition_ContainerDependency{ + Condition: ecsapi.ContainerConditionSuccess, + ContainerName: c.Name, + }) + } + + pairs, err := createEnvironment(project, service) + if err != nil { + return nil, err + } + + containers := append(initContainers, ecs.TaskDefinition_ContainerDefinition{ + Command: service.Command, + DisableNetworking: service.NetworkMode == "none", + DependsOnProp: dependencies, + DnsSearchDomains: service.DNSSearch, + DnsServers: service.DNS, + DockerSecurityOptions: service.SecurityOpt, + EntryPoint: service.Entrypoint, + Environment: pairs, + Essential: true, + ExtraHosts: toHostEntryPtr(service.ExtraHosts), + FirelensConfiguration: nil, + HealthCheck: toHealthCheck(service.HealthCheck), + Hostname: service.Hostname, + Image: service.Image, + Interactive: false, + Links: nil, + LinuxParameters: toLinuxParameters(service), + LogConfiguration: logConfiguration, + MemoryReservation: memReservation, + MountPoints: mounts, + Name: service.Name, + PortMappings: toPortMappings(service.Ports), + Privileged: service.Privileged, + PseudoTerminal: service.Tty, + ReadonlyRootFilesystem: service.ReadOnly, + RepositoryCredentials: credential, + ResourceRequirements: nil, + StartTimeout: 0, + StopTimeout: durationToInt(service.StopGracePeriod), + SystemControls: toSystemControls(service.Sysctls), + Ulimits: toUlimits(service.Ulimits), + User: service.User, + VolumesFrom: nil, + WorkingDirectory: service.WorkingDir, + }) + + return &ecs.TaskDefinition{ + ContainerDefinitions: containers, + Cpu: cpu, + Family: fmt.Sprintf("%s-%s", project.Name, service.Name), + IpcMode: service.Ipc, + Memory: mem, + NetworkMode: ecsapi.NetworkModeAwsvpc, // FIXME could be set by service.NetworkMode, Fargate only supports network mode ‘awsvpc’. + PidMode: service.Pid, + PlacementConstraints: toPlacementConstraints(service.Deploy), + ProxyConfiguration: nil, + RequiresCompatibilities: []string{ecsapi.LaunchTypeFargate}, + Volumes: volumes, + }, nil +} + +func createSecretsSideCar(project *types.Project, service types.ServiceConfig, logConfiguration *ecs.TaskDefinition_LogConfiguration) ( + ecs.TaskDefinition_Volume, + ecs.TaskDefinition_MountPoint, + ecs.TaskDefinition_ContainerDefinition, + error) { + initContainerName := fmt.Sprintf("%s_Secrets_InitContainer", normalizeResourceName(service.Name)) + secretsVolume := ecs.TaskDefinition_Volume{ + Name: "secrets", + } + secretsMount := ecs.TaskDefinition_MountPoint{ + ContainerPath: "/run/secrets/", + ReadOnly: true, + SourceVolume: "secrets", + } + + var ( + args []secrets.Secret + taskSecrets []ecs.TaskDefinition_Secret + ) + for _, s := range service.Secrets { + secretConfig := project.Secrets[s.Source] + if s.Target == "" { + s.Target = s.Source + } + taskSecrets = append(taskSecrets, ecs.TaskDefinition_Secret{ + Name: s.Target, + ValueFrom: secretConfig.Name, + }) + var keys []string + if ext, ok := secretConfig.Extensions[extensionKeys]; ok { + if key, ok := ext.(string); ok { + keys = append(keys, key) + } else { + for _, k := range ext.([]interface{}) { + keys = append(keys, k.(string)) + } + } + } + args = append(args, secrets.Secret{ + Name: s.Target, + Keys: keys, + }) + } + command, err := json.Marshal(args) + if err != nil { + return ecs.TaskDefinition_Volume{}, ecs.TaskDefinition_MountPoint{}, ecs.TaskDefinition_ContainerDefinition{}, err + } + secretsSideCar := ecs.TaskDefinition_ContainerDefinition{ + Name: initContainerName, + Image: secretsInitContainerImage, + Command: []string{string(command)}, + Essential: false, // FIXME this will be ignored, see https://github.com/awslabs/goformation/issues/61#issuecomment-625139607 + LogConfiguration: logConfiguration, + MountPoints: []ecs.TaskDefinition_MountPoint{ + { + ContainerPath: "/run/secrets/", + ReadOnly: false, + SourceVolume: "secrets", + }, + }, + Secrets: taskSecrets, + } + return secretsVolume, secretsMount, secretsSideCar, nil +} + +func createEnvironment(project *types.Project, service types.ServiceConfig) ([]ecs.TaskDefinition_KeyValuePair, error) { + environment := map[string]*string{} + for _, f := range service.EnvFile { + if !filepath.IsAbs(f) { + f = filepath.Join(project.WorkingDir, f) + } + if _, err := os.Stat(f); os.IsNotExist(err) { + return nil, err + } + file, err := os.Open(f) + if err != nil { + return nil, err + } + defer file.Close() // nolint:errcheck + + env, err := godotenv.Parse(file) + if err != nil { + return nil, err + } + for k, v := range env { + environment[k] = &v + } + } + for k, v := range service.Environment { + environment[k] = v + } + + var pairs []ecs.TaskDefinition_KeyValuePair + for k, v := range environment { + name := k + var value string + if v != nil { + value = *v + } + pairs = append(pairs, ecs.TaskDefinition_KeyValuePair{ + Name: name, + Value: value, + }) + } + return pairs, nil +} + +func getLogConfiguration(service types.ServiceConfig, project *types.Project) *ecs.TaskDefinition_LogConfiguration { + options := map[string]string{ + "awslogs-region": cloudformation.Ref("AWS::Region"), + "awslogs-group": cloudformation.Ref("LogGroup"), + "awslogs-stream-prefix": project.Name, + } + if service.Logging != nil { + for k, v := range service.Logging.Options { + if strings.HasPrefix(k, "awslogs-") { + options[k] = v + } + } + } + logConfiguration := &ecs.TaskDefinition_LogConfiguration{ + LogDriver: ecsapi.LogDriverAwslogs, + Options: options, + } + return logConfiguration +} + +func toSystemControls(sysctls types.Mapping) []ecs.TaskDefinition_SystemControl { + sys := []ecs.TaskDefinition_SystemControl{} + for k, v := range sysctls { + sys = append(sys, ecs.TaskDefinition_SystemControl{ + Namespace: k, + Value: v, + }) + } + return sys +} + +const miB = 1024 * 1024 + +func toLimits(service types.ServiceConfig) (string, string, error) { + // All possible cpu/mem values for Fargate + cpuToMem := map[int64][]types.UnitBytes{ + 256: {512, 1024, 2048}, + 512: {1024, 2048, 3072, 4096}, + 1024: {2048, 3072, 4096, 5120, 6144, 7168, 8192}, + 2048: {4096, 5120, 6144, 7168, 8192, 9216, 10240, 11264, 12288, 13312, 14336, 15360, 16384}, + 4096: {8192, 9216, 10240, 11264, 12288, 13312, 14336, 15360, 16384, 17408, 18432, 19456, 20480, 21504, 22528, 23552, 24576, 25600, 26624, 27648, 28672, 29696, 30720}, + } + cpuLimit := "256" + memLimit := "512" + + if service.Deploy == nil { + return cpuLimit, memLimit, nil + } + + limits := service.Deploy.Resources.Limits + if limits == nil { + return cpuLimit, memLimit, nil + } + + if limits.NanoCPUs == "" { + return cpuLimit, memLimit, nil + } + + v, err := opts.ParseCPUs(limits.NanoCPUs) + if err != nil { + return "", "", err + } + + var cpus []int64 + for k := range cpuToMem { + cpus = append(cpus, k) + } + sort.Slice(cpus, func(i, j int) bool { return cpus[i] < cpus[j] }) + + for _, cpu := range cpus { + mem := cpuToMem[cpu] + if v <= cpu*miB { + for _, m := range mem { + if limits.MemoryBytes <= m*miB { + cpuLimit = strconv.FormatInt(cpu, 10) + memLimit = strconv.FormatInt(int64(m), 10) + return cpuLimit, memLimit, nil + } + } + } + } + return "", "", fmt.Errorf("the resources requested are not supported by ECS/Fargate") +} + +func toContainerReservation(service types.ServiceConfig) (string, int) { + cpuReservation := ".0" + memReservation := 0 + + if service.Deploy == nil { + return cpuReservation, memReservation + } + + reservations := service.Deploy.Resources.Reservations + if reservations == nil { + return cpuReservation, memReservation + } + return reservations.NanoCPUs, int(reservations.MemoryBytes / miB) +} + +func toPlacementConstraints(deploy *types.DeployConfig) []ecs.TaskDefinition_TaskDefinitionPlacementConstraint { + if deploy == nil || deploy.Placement.Constraints == nil || len(deploy.Placement.Constraints) == 0 { + return nil + } + pl := []ecs.TaskDefinition_TaskDefinitionPlacementConstraint{} + for _, c := range deploy.Placement.Constraints { + pl = append(pl, ecs.TaskDefinition_TaskDefinitionPlacementConstraint{ + Expression: c, + Type: "", + }) + } + return pl +} + +func toPortMappings(ports []types.ServicePortConfig) []ecs.TaskDefinition_PortMapping { + if len(ports) == 0 { + return nil + } + m := []ecs.TaskDefinition_PortMapping{} + for _, p := range ports { + m = append(m, ecs.TaskDefinition_PortMapping{ + ContainerPort: int(p.Target), + HostPort: int(p.Published), + Protocol: p.Protocol, + }) + } + return m +} + +func toUlimits(ulimits map[string]*types.UlimitsConfig) []ecs.TaskDefinition_Ulimit { + if len(ulimits) == 0 { + return nil + } + u := []ecs.TaskDefinition_Ulimit{} + for k, v := range ulimits { + u = append(u, ecs.TaskDefinition_Ulimit{ + Name: k, + SoftLimit: v.Soft, + HardLimit: v.Hard, + }) + } + return u +} + +func toLinuxParameters(service types.ServiceConfig) *ecs.TaskDefinition_LinuxParameters { + return &ecs.TaskDefinition_LinuxParameters{ + Capabilities: toKernelCapabilities(service.CapAdd, service.CapDrop), + Devices: nil, + InitProcessEnabled: service.Init != nil && *service.Init, + MaxSwap: 0, + // FIXME SharedMemorySize: service.ShmSize, + Swappiness: 0, + Tmpfs: toTmpfs(service.Tmpfs), + } +} + +func toTmpfs(tmpfs types.StringList) []ecs.TaskDefinition_Tmpfs { + if tmpfs == nil || len(tmpfs) == 0 { + return nil + } + o := []ecs.TaskDefinition_Tmpfs{} + for _, path := range tmpfs { + o = append(o, ecs.TaskDefinition_Tmpfs{ + ContainerPath: path, + Size: 100, // size is required on ECS, unlimited by the compose spec + }) + } + return o +} + +func toKernelCapabilities(add []string, drop []string) *ecs.TaskDefinition_KernelCapabilities { + if len(add) == 0 && len(drop) == 0 { + return nil + } + return &ecs.TaskDefinition_KernelCapabilities{ + Add: add, + Drop: drop, + } + +} + +func toHealthCheck(check *types.HealthCheckConfig) *ecs.TaskDefinition_HealthCheck { + if check == nil { + return nil + } + retries := 0 + if check.Retries != nil { + retries = int(*check.Retries) + } + return &ecs.TaskDefinition_HealthCheck{ + Command: check.Test, + Interval: durationToInt(check.Interval), + Retries: retries, + StartPeriod: durationToInt(check.StartPeriod), + Timeout: durationToInt(check.Timeout), + } +} + +func durationToInt(interval *types.Duration) int { + if interval == nil { + return 0 + } + v := int(time.Duration(*interval).Seconds()) + return v +} + +func toHostEntryPtr(hosts types.HostsList) []ecs.TaskDefinition_HostEntry { + if hosts == nil || len(hosts) == 0 { + return nil + } + e := []ecs.TaskDefinition_HostEntry{} + for _, h := range hosts { + parts := strings.SplitN(h, ":", 2) // FIXME this should be handled by compose-go + e = append(e, ecs.TaskDefinition_HostEntry{ + Hostname: parts[0], + IpAddress: parts[1], + }) + } + return e +} + +func getRepoCredentials(service types.ServiceConfig) *ecs.TaskDefinition_RepositoryCredentials { + // extract registry and namespace string from image name + for key, value := range service.Extensions { + if key == extensionPullCredentials { + return &ecs.TaskDefinition_RepositoryCredentials{CredentialsParameter: value.(string)} + } + } + return nil +} diff --git a/ecs/down.go b/ecs/down.go new file mode 100644 index 000000000..4852f8151 --- /dev/null +++ b/ecs/down.go @@ -0,0 +1,48 @@ +/* + Copyright 2020 Docker, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package ecs + +import ( + "context" + + "github.com/compose-spec/compose-go/cli" +) + +func (b *ecsAPIService) Down(ctx context.Context, options *cli.ProjectOptions) error { + name, err := b.projectName(options) + if err != nil { + return err + } + + err = b.SDK.DeleteStack(ctx, name) + if err != nil { + return err + } + return b.WaitStackCompletion(ctx, name, stackDelete) +} + +func (b *ecsAPIService) projectName(options *cli.ProjectOptions) (string, error) { + name := options.Name + if name == "" { + project, err := cli.ProjectFromOptions(options) + if err != nil { + return "", err + } + name = project.Name + } + return name, nil +} diff --git a/ecs/iam.go b/ecs/iam.go new file mode 100644 index 000000000..7a4510ea5 --- /dev/null +++ b/ecs/iam.go @@ -0,0 +1,59 @@ +/* + Copyright 2020 Docker, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package ecs + +const ( + ecsTaskExecutionPolicy = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy" + ecrReadOnlyPolicy = "arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly" + + actionGetSecretValue = "secretsmanager:GetSecretValue" + actionGetParameters = "ssm:GetParameters" + actionDecrypt = "kms:Decrypt" +) + +var assumeRolePolicyDocument = PolicyDocument{ + Version: "2012-10-17", // https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_version.html + Statement: []PolicyStatement{ + { + Effect: "Allow", + Principal: PolicyPrincipal{ + Service: "ecs-tasks.amazonaws.com", + }, + Action: []string{"sts:AssumeRole"}, + }, + }, +} + +// PolicyDocument describes an IAM policy document +// could alternatively depend on https://github.com/kubernetes-sigs/cluster-api-provider-aws/blob/master/cmd/clusterawsadm/api/iam/v1alpha1/types.go +type PolicyDocument struct { + Version string `json:",omitempty"` + Statement []PolicyStatement `json:",omitempty"` +} + +// PolicyStatement describes an IAM policy statement +type PolicyStatement struct { + Effect string `json:",omitempty"` + Action []string `json:",omitempty"` + Principal PolicyPrincipal `json:",omitempty"` + Resource []string `json:",omitempty"` +} + +// PolicyPrincipal describes an IAM policy principal +type PolicyPrincipal struct { + Service string `json:",omitempty"` +} diff --git a/ecs/list.go b/ecs/list.go new file mode 100644 index 000000000..3c9ea5adf --- /dev/null +++ b/ecs/list.go @@ -0,0 +1,76 @@ +/* + Copyright 2020 Docker, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package ecs + +import ( + "context" + "fmt" + "strings" + + "github.com/docker/api/compose" + + "github.com/compose-spec/compose-go/cli" +) + +func (b *ecsAPIService) Ps(ctx context.Context, options *cli.ProjectOptions) ([]compose.ServiceStatus, error) { + projectName, err := b.projectName(options) + if err != nil { + return nil, err + } + parameters, err := b.SDK.ListStackParameters(ctx, projectName) + if err != nil { + return nil, err + } + cluster := parameters[parameterClusterName] + + resources, err := b.SDK.ListStackResources(ctx, projectName) + if err != nil { + return nil, err + } + + servicesARN := []string{} + for _, r := range resources { + switch r.Type { + case "AWS::ECS::Service": + servicesARN = append(servicesARN, r.ARN) + case "AWS::ECS::Cluster": + cluster = r.ARN + } + } + if len(servicesARN) == 0 { + return nil, nil + } + status, err := b.SDK.DescribeServices(ctx, cluster, servicesARN) + if err != nil { + return nil, err + } + + for i, state := range status { + ports := []string{} + for _, lb := range state.Publishers { + ports = append(ports, fmt.Sprintf( + "%s:%d->%d/%s", + lb.URL, + lb.PublishedPort, + lb.TargetPort, + strings.ToLower(lb.Protocol))) + } + state.Ports = ports + status[i] = state + } + return status, nil +} diff --git a/ecs/logs.go b/ecs/logs.go new file mode 100644 index 000000000..84e913ea6 --- /dev/null +++ b/ecs/logs.go @@ -0,0 +1,87 @@ +/* + Copyright 2020 Docker, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package ecs + +import ( + "bytes" + "context" + "fmt" + "io" + "os" + "os/signal" + "strconv" + "strings" + + "github.com/compose-spec/compose-go/cli" +) + +func (b *ecsAPIService) Logs(ctx context.Context, options *cli.ProjectOptions, writer io.Writer) error { + name := options.Name + if name == "" { + project, err := cli.ProjectFromOptions(options) + if err != nil { + return err + } + name = project.Name + } + + consumer := logConsumer{ + colors: map[string]colorFunc{}, + width: 0, + writer: writer, + } + err := b.SDK.GetLogs(ctx, name, consumer.Log) + if err != nil { + return err + } + + signalChan := make(chan os.Signal, 1) + signal.Notify(signalChan, os.Interrupt) + <-signalChan + return nil +} + +func (l *logConsumer) Log(service, container, message string) { + cf, ok := l.colors[service] + if !ok { + cf = <-loop + l.colors[service] = cf + l.computeWidth() + } + prefix := fmt.Sprintf("%-"+strconv.Itoa(l.width)+"s |", service) + + for _, line := range strings.Split(message, "\n") { + buf := bytes.NewBufferString(fmt.Sprintf("%s %s\n", cf(prefix), line)) + l.writer.Write(buf.Bytes()) // nolint:errcheck + } +} + +func (l *logConsumer) computeWidth() { + width := 0 + for n := range l.colors { + if len(n) > width { + width = len(n) + } + } + l.width = width + 3 +} + +type logConsumer struct { + colors map[string]colorFunc + width int + writer io.Writer +} diff --git a/ecs/marshall.go b/ecs/marshall.go new file mode 100644 index 000000000..d595e7c55 --- /dev/null +++ b/ecs/marshall.go @@ -0,0 +1,61 @@ +/* + Copyright 2020 Docker, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package ecs + +import ( + "encoding/json" + "fmt" + "strings" + + "github.com/awslabs/goformation/v4/cloudformation" +) + +func marshall(template *cloudformation.Template) ([]byte, error) { + raw, err := template.JSON() + if err != nil { + return nil, err + } + + var unmarshalled interface{} + if err := json.Unmarshal(raw, &unmarshalled); err != nil { + return nil, fmt.Errorf("invalid JSON: %s", err) + } + + if input, ok := unmarshalled.(map[string]interface{}); ok { + if resources, ok := input["Resources"]; ok { + for _, uresource := range resources.(map[string]interface{}) { + if resource, ok := uresource.(map[string]interface{}); ok { + if resource["Type"] == "AWS::ECS::TaskDefinition" { + properties := resource["Properties"].(map[string]interface{}) + for _, def := range properties["ContainerDefinitions"].([]interface{}) { + containerDefinition := def.(map[string]interface{}) + if strings.HasSuffix(containerDefinition["Name"].(string), "_InitContainer") { + containerDefinition["Essential"] = "false" + } + } + } + } + } + } + } + + raw, err = json.MarshalIndent(unmarshalled, "", " ") + if err != nil { + return nil, fmt.Errorf("invalid JSON: %s", err) + } + return raw, err +} diff --git a/ecs/sdk.go b/ecs/sdk.go new file mode 100644 index 000000000..3f4429766 --- /dev/null +++ b/ecs/sdk.go @@ -0,0 +1,616 @@ +/* + Copyright 2020 Docker, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package ecs + +import ( + "context" + "fmt" + "strings" + "time" + + "github.com/aws/aws-sdk-go/aws/client" + + "github.com/docker/api/compose" + "github.com/docker/api/secrets" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/cloudformation" + "github.com/aws/aws-sdk-go/service/cloudformation/cloudformationiface" + "github.com/aws/aws-sdk-go/service/cloudwatchlogs" + "github.com/aws/aws-sdk-go/service/cloudwatchlogs/cloudwatchlogsiface" + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/aws/aws-sdk-go/service/ec2/ec2iface" + "github.com/aws/aws-sdk-go/service/ecs" + "github.com/aws/aws-sdk-go/service/ecs/ecsiface" + "github.com/aws/aws-sdk-go/service/elbv2" + "github.com/aws/aws-sdk-go/service/elbv2/elbv2iface" + "github.com/aws/aws-sdk-go/service/iam" + "github.com/aws/aws-sdk-go/service/iam/iamiface" + "github.com/aws/aws-sdk-go/service/secretsmanager" + "github.com/aws/aws-sdk-go/service/secretsmanager/secretsmanageriface" + cf "github.com/awslabs/goformation/v4/cloudformation" + "github.com/sirupsen/logrus" +) + +type sdk struct { + ECS ecsiface.ECSAPI + EC2 ec2iface.EC2API + ELB elbv2iface.ELBV2API + CW cloudwatchlogsiface.CloudWatchLogsAPI + IAM iamiface.IAMAPI + CF cloudformationiface.CloudFormationAPI + SM secretsmanageriface.SecretsManagerAPI +} + +func newSDK(sess client.ConfigProvider) sdk { + return sdk{ + ECS: ecs.New(sess), + EC2: ec2.New(sess), + ELB: elbv2.New(sess), + CW: cloudwatchlogs.New(sess), + IAM: iam.New(sess), + CF: cloudformation.New(sess), + SM: secretsmanager.New(sess), + } +} + +func (s sdk) CheckRequirements(ctx context.Context, region string) error { + settings, err := s.ECS.ListAccountSettingsWithContext(ctx, &ecs.ListAccountSettingsInput{ + EffectiveSettings: aws.Bool(true), + Name: aws.String("serviceLongArnFormat"), + }) + if err != nil { + return err + } + serviceLongArnFormat := settings.Settings[0].Value + if *serviceLongArnFormat != "enabled" { + return fmt.Errorf("this tool requires the \"new ARN resource ID format\".\n"+ + "Check https://%s.console.aws.amazon.com/ecs/home#/settings\n"+ + "Learn more: https://aws.amazon.com/blogs/compute/migrating-your-amazon-ecs-deployment-to-the-new-arn-and-resource-id-format-2", region) + } + return nil +} + +func (s sdk) ClusterExists(ctx context.Context, name string) (bool, error) { + logrus.Debug("CheckRequirements if cluster was already created: ", name) + clusters, err := s.ECS.DescribeClustersWithContext(ctx, &ecs.DescribeClustersInput{ + Clusters: []*string{aws.String(name)}, + }) + if err != nil { + return false, err + } + return len(clusters.Clusters) > 0, nil +} + +func (s sdk) CreateCluster(ctx context.Context, name string) (string, error) { + logrus.Debug("Create cluster ", name) + response, err := s.ECS.CreateClusterWithContext(ctx, &ecs.CreateClusterInput{ClusterName: aws.String(name)}) + if err != nil { + return "", err + } + return *response.Cluster.Status, nil +} + +func (s sdk) VpcExists(ctx context.Context, vpcID string) (bool, error) { + logrus.Debug("CheckRequirements if VPC exists: ", vpcID) + _, err := s.EC2.DescribeVpcsWithContext(ctx, &ec2.DescribeVpcsInput{VpcIds: []*string{&vpcID}}) + return err == nil, err +} + +func (s sdk) GetDefaultVPC(ctx context.Context) (string, error) { + logrus.Debug("Retrieve default VPC") + vpcs, err := s.EC2.DescribeVpcsWithContext(ctx, &ec2.DescribeVpcsInput{ + Filters: []*ec2.Filter{ + { + Name: aws.String("isDefault"), + Values: []*string{aws.String("true")}, + }, + }, + }) + if err != nil { + return "", err + } + if len(vpcs.Vpcs) == 0 { + return "", fmt.Errorf("account has not default VPC") + } + return *vpcs.Vpcs[0].VpcId, nil +} + +func (s sdk) GetSubNets(ctx context.Context, vpcID string) ([]string, error) { + logrus.Debug("Retrieve SubNets") + subnets, err := s.EC2.DescribeSubnetsWithContext(ctx, &ec2.DescribeSubnetsInput{ + DryRun: nil, + Filters: []*ec2.Filter{ + { + Name: aws.String("vpc-id"), + Values: []*string{aws.String(vpcID)}, + }, + }, + }) + if err != nil { + return nil, err + } + + ids := []string{} + for _, subnet := range subnets.Subnets { + ids = append(ids, *subnet.SubnetId) + } + return ids, nil +} + +func (s sdk) GetRoleArn(ctx context.Context, name string) (string, error) { + role, err := s.IAM.GetRoleWithContext(ctx, &iam.GetRoleInput{ + RoleName: aws.String(name), + }) + if err != nil { + return "", err + } + return *role.Role.Arn, nil +} + +func (s sdk) StackExists(ctx context.Context, name string) (bool, error) { + stacks, err := s.CF.DescribeStacksWithContext(ctx, &cloudformation.DescribeStacksInput{ + StackName: aws.String(name), + }) + if err != nil { + if strings.HasPrefix(err.Error(), fmt.Sprintf("ValidationError: Stack with id %s does not exist", name)) { + return false, nil + } + return false, nil + } + return len(stacks.Stacks) > 0, nil +} + +func (s sdk) CreateStack(ctx context.Context, name string, template *cf.Template, parameters map[string]string) error { + logrus.Debug("Create CloudFormation stack") + json, err := marshall(template) + if err != nil { + return err + } + + param := []*cloudformation.Parameter{} + for name, value := range parameters { + param = append(param, &cloudformation.Parameter{ + ParameterKey: aws.String(name), + ParameterValue: aws.String(value), + }) + } + + _, err = s.CF.CreateStackWithContext(ctx, &cloudformation.CreateStackInput{ + OnFailure: aws.String("DELETE"), + StackName: aws.String(name), + TemplateBody: aws.String(string(json)), + Parameters: param, + TimeoutInMinutes: nil, + Capabilities: []*string{ + aws.String(cloudformation.CapabilityCapabilityIam), + }, + }) + return err +} + +func (s sdk) CreateChangeSet(ctx context.Context, name string, template *cf.Template, parameters map[string]string) (string, error) { + logrus.Debug("Create CloudFormation Changeset") + json, err := marshall(template) + if err != nil { + return "", err + } + + param := []*cloudformation.Parameter{} + for name := range parameters { + param = append(param, &cloudformation.Parameter{ + ParameterKey: aws.String(name), + UsePreviousValue: aws.Bool(true), + }) + } + + update := fmt.Sprintf("Update%s", time.Now().Format("2006-01-02-15-04-05")) + changeset, err := s.CF.CreateChangeSetWithContext(ctx, &cloudformation.CreateChangeSetInput{ + ChangeSetName: aws.String(update), + ChangeSetType: aws.String(cloudformation.ChangeSetTypeUpdate), + StackName: aws.String(name), + TemplateBody: aws.String(string(json)), + Parameters: param, + Capabilities: []*string{ + aws.String(cloudformation.CapabilityCapabilityIam), + }, + }) + if err != nil { + return "", err + } + + err = s.CF.WaitUntilChangeSetCreateCompleteWithContext(ctx, &cloudformation.DescribeChangeSetInput{ + ChangeSetName: changeset.Id, + }) + return *changeset.Id, err +} + +func (s sdk) UpdateStack(ctx context.Context, changeset string) error { + desc, err := s.CF.DescribeChangeSetWithContext(ctx, &cloudformation.DescribeChangeSetInput{ + ChangeSetName: aws.String(changeset), + }) + if err != nil { + return err + } + + if strings.HasPrefix(aws.StringValue(desc.StatusReason), "The submitted information didn't contain changes.") { + return nil + } + + _, err = s.CF.ExecuteChangeSet(&cloudformation.ExecuteChangeSetInput{ + ChangeSetName: aws.String(changeset), + }) + return err +} + +const ( + stackCreate = iota + stackUpdate + stackDelete +) + +func (s sdk) WaitStackComplete(ctx context.Context, name string, operation int) error { + input := &cloudformation.DescribeStacksInput{ + StackName: aws.String(name), + } + switch operation { + case stackCreate: + return s.CF.WaitUntilStackCreateCompleteWithContext(ctx, input) + case stackDelete: + return s.CF.WaitUntilStackDeleteCompleteWithContext(ctx, input) + default: + return fmt.Errorf("internal error: unexpected stack operation %d", operation) + } +} + +func (s sdk) GetStackID(ctx context.Context, name string) (string, error) { + stacks, err := s.CF.DescribeStacksWithContext(ctx, &cloudformation.DescribeStacksInput{ + StackName: aws.String(name), + }) + if err != nil { + return "", err + } + return *stacks.Stacks[0].StackId, nil +} + +func (s sdk) DescribeStackEvents(ctx context.Context, stackID string) ([]*cloudformation.StackEvent, error) { + // Fixme implement Paginator on Events and return as a chan(events) + events := []*cloudformation.StackEvent{} + var nextToken *string + for { + resp, err := s.CF.DescribeStackEventsWithContext(ctx, &cloudformation.DescribeStackEventsInput{ + StackName: aws.String(stackID), + NextToken: nextToken, + }) + if err != nil { + return nil, err + } + events = append(events, resp.StackEvents...) + if resp.NextToken == nil { + return events, nil + } + nextToken = resp.NextToken + } +} + +func (s sdk) ListStackParameters(ctx context.Context, name string) (map[string]string, error) { + st, err := s.CF.DescribeStacksWithContext(ctx, &cloudformation.DescribeStacksInput{ + NextToken: nil, + StackName: aws.String(name), + }) + if err != nil { + return nil, err + } + parameters := map[string]string{} + for _, parameter := range st.Stacks[0].Parameters { + parameters[aws.StringValue(parameter.ParameterKey)] = aws.StringValue(parameter.ParameterValue) + } + return parameters, nil +} + +type stackResource struct { + LogicalID string + Type string + ARN string + Status string +} + +func (s sdk) ListStackResources(ctx context.Context, name string) ([]stackResource, error) { + // FIXME handle pagination + res, err := s.CF.ListStackResourcesWithContext(ctx, &cloudformation.ListStackResourcesInput{ + StackName: aws.String(name), + }) + if err != nil { + return nil, err + } + + resources := []stackResource{} + for _, r := range res.StackResourceSummaries { + resources = append(resources, stackResource{ + LogicalID: aws.StringValue(r.LogicalResourceId), + Type: aws.StringValue(r.ResourceType), + ARN: aws.StringValue(r.PhysicalResourceId), + Status: aws.StringValue(r.ResourceStatus), + }) + } + return resources, nil +} + +func (s sdk) DeleteStack(ctx context.Context, name string) error { + logrus.Debug("Delete CloudFormation stack") + _, err := s.CF.DeleteStackWithContext(ctx, &cloudformation.DeleteStackInput{ + StackName: aws.String(name), + }) + return err +} + +func (s sdk) CreateSecret(ctx context.Context, secret secrets.Secret) (string, error) { + logrus.Debug("Create secret " + secret.Name) + secretStr, err := secret.GetCredString() + if err != nil { + return "", err + } + + response, err := s.SM.CreateSecret(&secretsmanager.CreateSecretInput{ + Name: &secret.Name, + SecretString: &secretStr, + Description: &secret.Description, + }) + if err != nil { + return "", err + } + return aws.StringValue(response.ARN), nil +} + +func (s sdk) InspectSecret(ctx context.Context, id string) (secrets.Secret, error) { + logrus.Debug("Inspect secret " + id) + response, err := s.SM.DescribeSecret(&secretsmanager.DescribeSecretInput{SecretId: &id}) + if err != nil { + return secrets.Secret{}, err + } + labels := map[string]string{} + for _, tag := range response.Tags { + labels[aws.StringValue(tag.Key)] = aws.StringValue(tag.Value) + } + secret := secrets.Secret{ + ID: aws.StringValue(response.ARN), + Name: aws.StringValue(response.Name), + Labels: labels, + } + if response.Description != nil { + secret.Description = *response.Description + } + return secret, nil +} + +func (s sdk) ListSecrets(ctx context.Context) ([]secrets.Secret, error) { + logrus.Debug("List secrets ...") + response, err := s.SM.ListSecrets(&secretsmanager.ListSecretsInput{}) + if err != nil { + return nil, err + } + + var ls []secrets.Secret + for _, sec := range response.SecretList { + + labels := map[string]string{} + for _, tag := range sec.Tags { + labels[*tag.Key] = *tag.Value + } + description := "" + if sec.Description != nil { + description = *sec.Description + } + ls = append(ls, secrets.Secret{ + ID: *sec.ARN, + Name: *sec.Name, + Labels: labels, + Description: description, + }) + } + return ls, nil +} + +func (s sdk) DeleteSecret(ctx context.Context, id string, recover bool) error { + logrus.Debug("List secrets ...") + force := !recover + _, err := s.SM.DeleteSecret(&secretsmanager.DeleteSecretInput{SecretId: &id, ForceDeleteWithoutRecovery: &force}) + return err +} + +func (s sdk) GetLogs(ctx context.Context, name string, consumer func(service, container, message string)) error { + logGroup := fmt.Sprintf("/docker-compose/%s", name) + var startTime int64 + for { + var hasMore = true + var token *string + for hasMore { + events, err := s.CW.FilterLogEvents(&cloudwatchlogs.FilterLogEventsInput{ + LogGroupName: aws.String(logGroup), + NextToken: token, + StartTime: aws.Int64(startTime), + }) + if err != nil { + return err + } + if events.NextToken == nil { + hasMore = false + } else { + token = events.NextToken + } + + for _, event := range events.Events { + p := strings.Split(aws.StringValue(event.LogStreamName), "/") + consumer(p[1], p[2], aws.StringValue(event.Message)) + startTime = *event.IngestionTime + } + } + time.Sleep(500 * time.Millisecond) + } +} + +func (s sdk) DescribeServices(ctx context.Context, cluster string, arns []string) ([]compose.ServiceStatus, error) { + services, err := s.ECS.DescribeServicesWithContext(ctx, &ecs.DescribeServicesInput{ + Cluster: aws.String(cluster), + Services: aws.StringSlice(arns), + Include: aws.StringSlice([]string{"TAGS"}), + }) + if err != nil { + return nil, err + } + + status := []compose.ServiceStatus{} + for _, service := range services.Services { + var name string + for _, t := range service.Tags { + if *t.Key == compose.ServiceTag { + name = aws.StringValue(t.Value) + } + } + if name == "" { + return nil, fmt.Errorf("service %s doesn't have a %s tag", *service.ServiceArn, compose.ServiceTag) + } + targetGroupArns := []string{} + for _, lb := range service.LoadBalancers { + targetGroupArns = append(targetGroupArns, *lb.TargetGroupArn) + } + // getURLwithPortMapping makes 2 queries + // one to get the target groups and another for load balancers + loadBalancers, err := s.getURLWithPortMapping(ctx, targetGroupArns) + if err != nil { + return nil, err + } + status = append(status, compose.ServiceStatus{ + ID: aws.StringValue(service.ServiceName), + Name: name, + Replicas: int(aws.Int64Value(service.RunningCount)), + Desired: int(aws.Int64Value(service.DesiredCount)), + Publishers: loadBalancers, + }) + } + return status, nil +} + +func (s sdk) getURLWithPortMapping(ctx context.Context, targetGroupArns []string) ([]compose.PortPublisher, error) { + if len(targetGroupArns) == 0 { + return nil, nil + } + groups, err := s.ELB.DescribeTargetGroups(&elbv2.DescribeTargetGroupsInput{ + TargetGroupArns: aws.StringSlice(targetGroupArns), + }) + if err != nil { + return nil, err + } + lbarns := []*string{} + for _, tg := range groups.TargetGroups { + lbarns = append(lbarns, tg.LoadBalancerArns...) + } + + lbs, err := s.ELB.DescribeLoadBalancersWithContext(ctx, &elbv2.DescribeLoadBalancersInput{ + LoadBalancerArns: lbarns, + }) + + if err != nil { + return nil, err + } + filterLB := func(arn *string, lbs []*elbv2.LoadBalancer) *elbv2.LoadBalancer { + if aws.StringValue(arn) == "" { + // load balancer arn is nil/"" + return nil + } + for _, lb := range lbs { + if aws.StringValue(lb.LoadBalancerArn) == aws.StringValue(arn) { + return lb + } + } + return nil + } + loadBalancers := []compose.PortPublisher{} + for _, tg := range groups.TargetGroups { + for _, lbarn := range tg.LoadBalancerArns { + lb := filterLB(lbarn, lbs.LoadBalancers) + if lb == nil { + continue + } + loadBalancers = append(loadBalancers, compose.PortPublisher{ + URL: aws.StringValue(lb.DNSName), + TargetPort: int(aws.Int64Value(tg.Port)), + PublishedPort: int(aws.Int64Value(tg.Port)), + Protocol: aws.StringValue(tg.Protocol), + }) + + } + } + return loadBalancers, nil +} + +func (s sdk) ListTasks(ctx context.Context, cluster string, family string) ([]string, error) { + tasks, err := s.ECS.ListTasksWithContext(ctx, &ecs.ListTasksInput{ + Cluster: aws.String(cluster), + Family: aws.String(family), + }) + if err != nil { + return nil, err + } + arns := []string{} + for _, arn := range tasks.TaskArns { + arns = append(arns, *arn) + } + return arns, nil +} + +func (s sdk) GetPublicIPs(ctx context.Context, interfaces ...string) (map[string]string, error) { + desc, err := s.EC2.DescribeNetworkInterfaces(&ec2.DescribeNetworkInterfacesInput{ + NetworkInterfaceIds: aws.StringSlice(interfaces), + }) + if err != nil { + return nil, err + } + publicIPs := map[string]string{} + for _, interf := range desc.NetworkInterfaces { + if interf.Association != nil { + publicIPs[aws.StringValue(interf.NetworkInterfaceId)] = aws.StringValue(interf.Association.PublicIp) + } + } + return publicIPs, nil +} + +func (s sdk) LoadBalancerExists(ctx context.Context, arn string) (bool, error) { + logrus.Debug("CheckRequirements if PortPublisher exists: ", arn) + lbs, err := s.ELB.DescribeLoadBalancersWithContext(ctx, &elbv2.DescribeLoadBalancersInput{ + LoadBalancerArns: []*string{aws.String(arn)}, + }) + if err != nil { + return false, err + } + return len(lbs.LoadBalancers) > 0, nil +} + +func (s sdk) GetLoadBalancerURL(ctx context.Context, arn string) (string, error) { + logrus.Debug("Retrieve load balancer URL: ", arn) + lbs, err := s.ELB.DescribeLoadBalancersWithContext(ctx, &elbv2.DescribeLoadBalancersInput{ + LoadBalancerArns: []*string{aws.String(arn)}, + }) + if err != nil { + return "", err + } + dnsName := aws.StringValue(lbs.LoadBalancers[0].DNSName) + if dnsName == "" { + return "", fmt.Errorf("Load balancer %s doesn't have a dns name", aws.StringValue(lbs.LoadBalancers[0].LoadBalancerArn)) + } + return dnsName, nil +} diff --git a/ecs/secrets.go b/ecs/secrets.go new file mode 100644 index 000000000..60c8964f1 --- /dev/null +++ b/ecs/secrets.go @@ -0,0 +1,39 @@ +/* + Copyright 2020 Docker, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package ecs + +import ( + "context" + + "github.com/docker/api/secrets" +) + +func (b *ecsAPIService) CreateSecret(ctx context.Context, secret secrets.Secret) (string, error) { + return b.SDK.CreateSecret(ctx, secret) +} + +func (b *ecsAPIService) InspectSecret(ctx context.Context, id string) (secrets.Secret, error) { + return b.SDK.InspectSecret(ctx, id) +} + +func (b *ecsAPIService) ListSecrets(ctx context.Context) ([]secrets.Secret, error) { + return b.SDK.ListSecrets(ctx) +} + +func (b *ecsAPIService) DeleteSecret(ctx context.Context, id string, recover bool) error { + return b.SDK.DeleteSecret(ctx, id, recover) +} diff --git a/ecs/secrets/Dockerfile b/ecs/secrets/Dockerfile new file mode 100644 index 000000000..5490ffc41 --- /dev/null +++ b/ecs/secrets/Dockerfile @@ -0,0 +1,22 @@ +# Copyright 2020 Docker, Inc. + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +FROM golang:1.14.4-alpine AS builder +WORKDIR $GOPATH/src/github.com/docker/api/ecs/secrets +COPY . . +RUN GOOS=linux GOARCH=amd64 go build -ldflags="-w -s" -o /go/bin/secrets main/main.go + +FROM scratch +COPY --from=builder /go/bin/secrets /secrets +ENTRYPOINT ["/secrets"] diff --git a/ecs/secrets/init.go b/ecs/secrets/init.go new file mode 100644 index 000000000..729afe104 --- /dev/null +++ b/ecs/secrets/init.go @@ -0,0 +1,105 @@ +/* + Copyright 2020 Docker, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package secrets + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "os" + "path/filepath" +) + +// Secret define sensitive data to be bound as file +type Secret struct { + Name string + Keys []string +} + +// CreateSecretFiles retrieve sensitive data from env and store as plain text a a file in path +func CreateSecretFiles(secret Secret, path string) error { + value, ok := os.LookupEnv(secret.Name) + if !ok { + return fmt.Errorf("%q variable not set", secret.Name) + } + + secrets := filepath.Join(path, secret.Name) + + if len(secret.Keys) == 0 { + // raw Secret + fmt.Printf("inject Secret %q info %s\n", secret.Name, secrets) + return ioutil.WriteFile(secrets, []byte(value), 0444) + } + + var unmarshalled interface{} + err := json.Unmarshal([]byte(value), &unmarshalled) + if err != nil { + return fmt.Errorf("%q Secret is not a valid JSON document: %w", secret.Name, err) + } + + dict, ok := unmarshalled.(map[string]interface{}) + if !ok { + return fmt.Errorf("%q Secret is not a JSON dictionary: %w", secret.Name, err) + } + err = os.MkdirAll(secrets, 0755) + if err != nil { + return err + } + + if contains(secret.Keys, "*") { + var keys []string + for k := range dict { + keys = append(keys, k) + } + secret.Keys = keys + } + + for _, k := range secret.Keys { + path := filepath.Join(secrets, k) + fmt.Printf("inject Secret %q info %s\n", k, path) + + v, ok := dict[k] + if !ok { + return fmt.Errorf("%q Secret has no %q key", secret.Name, k) + } + + var raw []byte + if s, ok := v.(string); ok { + raw = []byte(s) + } else { + raw, err = json.Marshal(v) + if err != nil { + return err + } + } + + err = ioutil.WriteFile(path, raw, 0444) + if err != nil { + return err + } + } + return nil +} + +func contains(keys []string, s string) bool { + for _, k := range keys { + if k == s { + return true + } + } + return false +} diff --git a/ecs/secrets/init_test.go b/ecs/secrets/init_test.go new file mode 100644 index 000000000..5389a5c5c --- /dev/null +++ b/ecs/secrets/init_test.go @@ -0,0 +1,104 @@ +/* + Copyright 2020 Docker, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package secrets + +import ( + "io/ioutil" + "os" + "path/filepath" + "testing" + + "gotest.tools/v3/assert" + "gotest.tools/v3/fs" +) + +func TestRawSecret(t *testing.T) { + dir := fs.NewDir(t, "secrets").Path() + err := os.Setenv("raw", "something_secret") + assert.NilError(t, err) + defer os.Unsetenv("raw") // nolint:errcheck + + err = CreateSecretFiles(Secret{ + Name: "raw", + Keys: nil, + }, dir) + assert.NilError(t, err) + file, err := ioutil.ReadFile(filepath.Join(dir, "raw")) + assert.NilError(t, err) + content := string(file) + assert.Equal(t, content, "something_secret") +} + +func TestSelectedKeysSecret(t *testing.T) { + dir := fs.NewDir(t, "secrets").Path() + err := os.Setenv("json", ` +{ + "foo": "bar", + "zot": "qix" +}`) + assert.NilError(t, err) + defer os.Unsetenv("json") // nolint:errcheck + + err = CreateSecretFiles(Secret{ + Name: "json", + Keys: []string{"foo"}, + }, dir) + assert.NilError(t, err) + file, err := ioutil.ReadFile(filepath.Join(dir, "json", "foo")) + assert.NilError(t, err) + content := string(file) + assert.Equal(t, content, "bar") + + _, err = os.Stat(filepath.Join(dir, "json", "zot")) + assert.Check(t, os.IsNotExist(err)) +} + +func TestAllKeysSecret(t *testing.T) { + dir := fs.NewDir(t, "secrets").Path() + err := os.Setenv("json", ` +{ + "foo": "bar", + "zot": "qix" +}`) + assert.NilError(t, err) + defer os.Unsetenv("json") // nolint:errcheck + + err = CreateSecretFiles(Secret{ + Name: "json", + Keys: []string{"*"}, + }, dir) + assert.NilError(t, err) + file, err := ioutil.ReadFile(filepath.Join(dir, "json", "foo")) + assert.NilError(t, err) + content := string(file) + assert.Equal(t, content, "bar") + + file, err = ioutil.ReadFile(filepath.Join(dir, "json", "zot")) + assert.NilError(t, err) + content = string(file) + assert.Equal(t, content, "qix") +} + +func TestUnknownSecret(t *testing.T) { + dir := fs.NewDir(t, "secrets").Path() + + err := CreateSecretFiles(Secret{ + Name: "not_set", + Keys: nil, + }, dir) + assert.Check(t, err != nil) +} diff --git a/ecs/secrets/main/main.go b/ecs/secrets/main/main.go new file mode 100644 index 000000000..d4b23876a --- /dev/null +++ b/ecs/secrets/main/main.go @@ -0,0 +1,49 @@ +/* + Copyright 2020 Docker, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package main + +import ( + "encoding/json" + "fmt" + "os" + + "github.com/docker/api/ecs/secrets" +) + +const secretsFolder = "/run/secrets" + +func main() { + if len(os.Args) != 2 { + fmt.Fprint(os.Stderr, "usage: secrets ") + os.Exit(1) + } + + var input []secrets.Secret + err := json.Unmarshal([]byte(os.Args[1]), &input) + if err != nil { + fmt.Fprint(os.Stderr, err.Error()) + os.Exit(1) + } + + for _, secret := range input { + err := secrets.CreateSecretFiles(secret, secretsFolder) + if err != nil { + fmt.Fprint(os.Stderr, err.Error()) + os.Exit(1) + } + } +} diff --git a/ecs/testdata/input/envfile b/ecs/testdata/input/envfile new file mode 100644 index 000000000..6ac867af7 --- /dev/null +++ b/ecs/testdata/input/envfile @@ -0,0 +1 @@ +FOO=BAR diff --git a/ecs/testdata/input/simple-single-service.yaml b/ecs/testdata/input/simple-single-service.yaml new file mode 100644 index 000000000..448f21108 --- /dev/null +++ b/ecs/testdata/input/simple-single-service.yaml @@ -0,0 +1,6 @@ +version: "3" +services: + simple: + image: nginx + ports: + - "80:80" \ No newline at end of file diff --git a/ecs/testdata/invalid_network_mode.yaml b/ecs/testdata/invalid_network_mode.yaml new file mode 100644 index 000000000..ce8ed8ad5 --- /dev/null +++ b/ecs/testdata/invalid_network_mode.yaml @@ -0,0 +1,5 @@ +version: "3" +services: + simple: + image: nginx + network_mode: bridge \ No newline at end of file diff --git a/ecs/testdata/simple/simple-cloudformation-conversion.golden b/ecs/testdata/simple/simple-cloudformation-conversion.golden new file mode 100644 index 000000000..d42a2464d --- /dev/null +++ b/ecs/testdata/simple/simple-cloudformation-conversion.golden @@ -0,0 +1,378 @@ +{ + "AWSTemplateFormatVersion": "2010-09-09", + "Conditions": { + "CreateCluster": { + "Fn::Equals": [ + "", + { + "Ref": "ParameterClusterName" + } + ] + }, + "CreateLoadBalancer": { + "Fn::Equals": [ + "", + { + "Ref": "ParameterLoadBalancerARN" + } + ] + } + }, + "Description": "CloudFormation template created by Docker for deploying applications on Amazon ECS", + "Parameters": { + "ParameterClusterName": { + "Description": "Name of the ECS cluster to deploy to (optional)", + "Type": "String" + }, + "ParameterLoadBalancerARN": { + "Description": "Name of the LoadBalancer to connect to (optional)", + "Type": "String" + }, + "ParameterSubnet1Id": { + "Description": "SubnetId, for Availability Zone 1 in the region in your VPC", + "Type": "AWS::EC2::Subnet::Id" + }, + "ParameterSubnet2Id": { + "Description": "SubnetId, for Availability Zone 2 in the region in your VPC", + "Type": "AWS::EC2::Subnet::Id" + }, + "ParameterVPCId": { + "Description": "ID of the VPC", + "Type": "AWS::EC2::VPC::Id" + } + }, + "Resources": { + "CloudMap": { + "Properties": { + "Description": "Service Map for Docker Compose project TestSimpleConvert", + "Name": "TestSimpleConvert.local", + "Vpc": { + "Ref": "ParameterVPCId" + } + }, + "Type": "AWS::ServiceDiscovery::PrivateDnsNamespace" + }, + "Cluster": { + "Condition": "CreateCluster", + "Properties": { + "ClusterName": "TestSimpleConvert", + "Tags": [ + { + "Key": "com.docker.compose.project", + "Value": "TestSimpleConvert" + } + ] + }, + "Type": "AWS::ECS::Cluster" + }, + "LogGroup": { + "Properties": { + "LogGroupName": "/docker-compose/TestSimpleConvert" + }, + "Type": "AWS::Logs::LogGroup" + }, + "SimpleService": { + "DependsOn": [ + "SimpleTCP80Listener" + ], + "Properties": { + "Cluster": { + "Fn::If": [ + "CreateCluster", + { + "Ref": "Cluster" + }, + { + "Ref": "ParameterClusterName" + } + ] + }, + "DeploymentConfiguration": { + "MaximumPercent": 200, + "MinimumHealthyPercent": 100 + }, + "DeploymentController": { + "Type": "ECS" + }, + "DesiredCount": 1, + "LaunchType": "FARGATE", + "LoadBalancers": [ + { + "ContainerName": "simple", + "ContainerPort": 80, + "TargetGroupArn": { + "Ref": "SimpleTCP80TargetGroup" + } + } + ], + "NetworkConfiguration": { + "AwsvpcConfiguration": { + "AssignPublicIp": "ENABLED", + "SecurityGroups": [ + { + "Ref": "TestSimpleConvertDefaultNetwork" + } + ], + "Subnets": [ + { + "Ref": "ParameterSubnet1Id" + }, + { + "Ref": "ParameterSubnet2Id" + } + ] + } + }, + "PropagateTags": "SERVICE", + "SchedulingStrategy": "REPLICA", + "ServiceRegistries": [ + { + "RegistryArn": { + "Fn::GetAtt": [ + "SimpleServiceDiscoveryEntry", + "Arn" + ] + } + } + ], + "Tags": [ + { + "Key": "com.docker.compose.project", + "Value": "TestSimpleConvert" + }, + { + "Key": "com.docker.compose.service", + "Value": "simple" + } + ], + "TaskDefinition": { + "Ref": "SimpleTaskDefinition" + } + }, + "Type": "AWS::ECS::Service" + }, + "SimpleServiceDiscoveryEntry": { + "Properties": { + "Description": "\"simple\" service discovery entry in Cloud Map", + "DnsConfig": { + "DnsRecords": [ + { + "TTL": 60, + "Type": "A" + } + ], + "RoutingPolicy": "MULTIVALUE" + }, + "HealthCheckCustomConfig": { + "FailureThreshold": 1 + }, + "Name": "simple", + "NamespaceId": { + "Ref": "CloudMap" + } + }, + "Type": "AWS::ServiceDiscovery::Service" + }, + "SimpleTCP80Listener": { + "Properties": { + "DefaultActions": [ + { + "ForwardConfig": { + "TargetGroups": [ + { + "TargetGroupArn": { + "Ref": "SimpleTCP80TargetGroup" + } + } + ] + }, + "Type": "forward" + } + ], + "LoadBalancerArn": { + "Fn::If": [ + "CreateLoadBalancer", + { + "Ref": "TestSimpleConvertLoadBalancer" + }, + { + "Ref": "ParameterLoadBalancerARN" + } + ] + }, + "Port": 80, + "Protocol": "HTTP" + }, + "Type": "AWS::ElasticLoadBalancingV2::Listener" + }, + "SimpleTCP80TargetGroup": { + "Properties": { + "Port": 80, + "Protocol": "HTTP", + "Tags": [ + { + "Key": "com.docker.compose.project", + "Value": "TestSimpleConvert" + } + ], + "TargetType": "ip", + "VpcId": { + "Ref": "ParameterVPCId" + } + }, + "Type": "AWS::ElasticLoadBalancingV2::TargetGroup" + }, + "SimpleTaskDefinition": { + "Properties": { + "ContainerDefinitions": [ + { + "Environment": [ + { + "Name": "LOCALDOMAIN", + "Value": { + "Fn::Join": [ + "", + [ + { + "Ref": "AWS::Region" + }, + ".compute.internal", + " TestSimpleConvert.local" + ] + ] + } + } + ], + "Essential": true, + "Image": "nginx", + "LinuxParameters": {}, + "LogConfiguration": { + "LogDriver": "awslogs", + "Options": { + "awslogs-group": { + "Ref": "LogGroup" + }, + "awslogs-region": { + "Ref": "AWS::Region" + }, + "awslogs-stream-prefix": "TestSimpleConvert" + } + }, + "Name": "simple", + "PortMappings": [ + { + "ContainerPort": 80, + "HostPort": 80, + "Protocol": "tcp" + } + ] + } + ], + "Cpu": "256", + "ExecutionRoleArn": { + "Ref": "SimpleTaskExecutionRole" + }, + "Family": "TestSimpleConvert-simple", + "Memory": "512", + "NetworkMode": "awsvpc", + "RequiresCompatibilities": [ + "FARGATE" + ] + }, + "Type": "AWS::ECS::TaskDefinition" + }, + "SimpleTaskExecutionRole": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": "ecs-tasks.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy", + "arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly" + ] + }, + "Type": "AWS::IAM::Role" + }, + "TestSimpleConvertDefaultNetwork": { + "Properties": { + "GroupDescription": "TestSimpleConvert default Security Group", + "GroupName": "TestSimpleConvertDefaultNetwork", + "SecurityGroupIngress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "simple:80/tcp", + "FromPort": 80, + "IpProtocol": "TCP", + "ToPort": 80 + } + ], + "Tags": [ + { + "Key": "com.docker.compose.project", + "Value": "TestSimpleConvert" + }, + { + "Key": "com.docker.compose.network", + "Value": "default" + } + ], + "VpcId": { + "Ref": "ParameterVPCId" + } + }, + "Type": "AWS::EC2::SecurityGroup" + }, + "TestSimpleConvertDefaultNetworkIngress": { + "Properties": { + "Description": "Allow communication within network default", + "GroupId": { + "Ref": "TestSimpleConvertDefaultNetwork" + }, + "IpProtocol": "-1", + "SourceSecurityGroupId": { + "Ref": "TestSimpleConvertDefaultNetwork" + } + }, + "Type": "AWS::EC2::SecurityGroupIngress" + }, + "TestSimpleConvertLoadBalancer": { + "Condition": "CreateLoadBalancer", + "Properties": { + "Name": "TestSimpleConvertLoadBalancer", + "Scheme": "internet-facing", + "SecurityGroups": [ + { + "Ref": "TestSimpleConvertDefaultNetwork" + } + ], + "Subnets": [ + { + "Ref": "ParameterSubnet1Id" + }, + { + "Ref": "ParameterSubnet2Id" + } + ], + "Tags": [ + { + "Key": "com.docker.compose.project", + "Value": "TestSimpleConvert" + } + ], + "Type": "application" + }, + "Type": "AWS::ElasticLoadBalancingV2::LoadBalancer" + } + } +} diff --git a/ecs/up.go b/ecs/up.go new file mode 100644 index 000000000..140f804f6 --- /dev/null +++ b/ecs/up.go @@ -0,0 +1,161 @@ +/* + Copyright 2020 Docker, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package ecs + +import ( + "context" + "fmt" + "os" + "os/signal" + "syscall" + + "github.com/compose-spec/compose-go/cli" + "github.com/compose-spec/compose-go/types" +) + +func (b *ecsAPIService) Up(ctx context.Context, options *cli.ProjectOptions) error { + project, err := cli.ProjectFromOptions(options) + if err != nil { + return err + } + + err = b.SDK.CheckRequirements(ctx, b.Region) + if err != nil { + return err + } + + cluster, err := b.GetCluster(ctx, project) + if err != nil { + return err + } + + template, err := b.convert(project) + if err != nil { + return err + } + + vpc, err := b.GetVPC(ctx, project) + if err != nil { + return err + } + + subNets, err := b.SDK.GetSubNets(ctx, vpc) + if err != nil { + return err + } + if len(subNets) < 2 { + return fmt.Errorf("VPC %s should have at least 2 associated subnets in different availability zones", vpc) + } + + lb, err := b.GetLoadBalancer(ctx, project) + if err != nil { + return err + } + + parameters := map[string]string{ + parameterClusterName: cluster, + parameterVPCId: vpc, + parameterSubnet1Id: subNets[0], + parameterSubnet2Id: subNets[1], + parameterLoadBalancerARN: lb, + } + + update, err := b.SDK.StackExists(ctx, project.Name) + if err != nil { + return err + } + operation := stackCreate + if update { + operation = stackUpdate + changeset, err := b.SDK.CreateChangeSet(ctx, project.Name, template, parameters) + if err != nil { + return err + } + err = b.SDK.UpdateStack(ctx, changeset) + if err != nil { + return err + } + } else { + err = b.SDK.CreateStack(ctx, project.Name, template, parameters) + if err != nil { + return err + } + } + + signalChan := make(chan os.Signal, 1) + signal.Notify(signalChan, syscall.SIGINT, syscall.SIGTERM) + go func() { + <-signalChan + fmt.Println("user interrupted deployment. Deleting stack...") + b.Down(ctx, options) // nolint:errcheck + }() + + err = b.WaitStackCompletion(ctx, project.Name, operation) + return err +} + +func (b ecsAPIService) GetVPC(ctx context.Context, project *types.Project) (string, error) { + //check compose file for custom VPC selected + if vpc, ok := project.Extensions[extensionVPC]; ok { + vpcID := vpc.(string) + ok, err := b.SDK.VpcExists(ctx, vpcID) + if err != nil { + return "", err + } + if !ok { + return "", fmt.Errorf("VPC does not exist: %s", vpc) + } + return vpcID, nil + } + defaultVPC, err := b.SDK.GetDefaultVPC(ctx) + if err != nil { + return "", err + } + return defaultVPC, nil +} + +func (b ecsAPIService) GetLoadBalancer(ctx context.Context, project *types.Project) (string, error) { + //check compose file for custom VPC selected + if ext, ok := project.Extensions[extensionLB]; ok { + lb := ext.(string) + ok, err := b.SDK.LoadBalancerExists(ctx, lb) + if err != nil { + return "", err + } + if !ok { + return "", fmt.Errorf("load balancer does not exist: %s", lb) + } + return lb, nil + } + return "", nil +} + +func (b ecsAPIService) GetCluster(ctx context.Context, project *types.Project) (string, error) { + //check compose file for custom VPC selected + if ext, ok := project.Extensions[extensionCluster]; ok { + cluster := ext.(string) + ok, err := b.SDK.ClusterExists(ctx, cluster) + if err != nil { + return "", err + } + if !ok { + return "", fmt.Errorf("cluster does not exist: %s", cluster) + } + return cluster, nil + } + return "", nil +} diff --git a/ecs/wait.go b/ecs/wait.go new file mode 100644 index 000000000..a6933ddec --- /dev/null +++ b/ecs/wait.go @@ -0,0 +1,109 @@ +/* + Copyright 2020 Docker, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package ecs + +import ( + "context" + "fmt" + "sort" + "strings" + "time" + + "github.com/docker/api/progress" + + "github.com/aws/aws-sdk-go/aws" +) + +func (b *ecsAPIService) WaitStackCompletion(ctx context.Context, name string, operation int) error { //nolint:gocyclo + knownEvents := map[string]struct{}{} + // progress writer + w := progress.ContextWriter(ctx) + // Get the unique Stack ID so we can collect events without getting some from previous deployments with same name + stackID, err := b.SDK.GetStackID(ctx, name) + if err != nil { + return err + } + + ticker := time.NewTicker(1 * time.Second) + done := make(chan bool) + go func() { + b.SDK.WaitStackComplete(ctx, stackID, operation) //nolint:errcheck + ticker.Stop() + done <- true + }() + + var completed bool + var stackErr error + for !completed { + select { + case <-done: + completed = true + case <-ticker.C: + } + events, err := b.SDK.DescribeStackEvents(ctx, stackID) + if err != nil { + return err + } + + sort.Slice(events, func(i, j int) bool { + return events[i].Timestamp.Before(*events[j].Timestamp) + }) + + for _, event := range events { + if _, ok := knownEvents[*event.EventId]; ok { + continue + } + knownEvents[*event.EventId] = struct{}{} + + resource := aws.StringValue(event.LogicalResourceId) + reason := aws.StringValue(event.ResourceStatusReason) + status := aws.StringValue(event.ResourceStatus) + progressStatus := progress.Working + + switch status { + case "CREATE_COMPLETE": + if operation == stackCreate { + progressStatus = progress.Done + + } + case "UPDATE_COMPLETE": + if operation == stackUpdate { + progressStatus = progress.Done + } + case "DELETE_COMPLETE": + if operation == stackDelete { + progressStatus = progress.Done + } + default: + if strings.HasSuffix(status, "_FAILED") { + progressStatus = progress.Error + if stackErr == nil { + operation = stackDelete + stackErr = fmt.Errorf(reason) + } + } + } + w.Event(progress.Event{ + ID: resource, + Status: progressStatus, + StatusText: status, + }) + } + } + + return stackErr +} diff --git a/ecs/x.go b/ecs/x.go new file mode 100644 index 000000000..63af9c2ec --- /dev/null +++ b/ecs/x.go @@ -0,0 +1,31 @@ +/* + Copyright 2020 Docker, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package ecs + +const ( + extensionSecurityGroup = "x-aws-securitygroup" + extensionVPC = "x-aws-vpc" + extensionPullCredentials = "x-aws-pull_credentials" + extensionLB = "x-aws-loadbalancer" + extensionCluster = "x-aws-cluster" + extensionKeys = "x-aws-keys" + extensionMinPercent = "x-aws-min_percent" + extensionMaxPercent = "x-aws-max_percent" + extensionRetention = "x-aws-logs_retention" + extensionRole = "x-aws-role" + extensionManagedPolicies = "x-aws-policies" +) diff --git a/example/backend.go b/example/backend.go index 9cb54004e..9ddd0573a 100644 --- a/example/backend.go +++ b/example/backend.go @@ -22,15 +22,16 @@ import ( "context" "errors" "fmt" + "io" "github.com/compose-spec/compose-go/cli" - ecstypes "github.com/docker/ecs-plugin/pkg/compose" "github.com/docker/api/backend" "github.com/docker/api/compose" "github.com/docker/api/containers" "github.com/docker/api/context/cloud" "github.com/docker/api/errdefs" + "github.com/docker/api/secrets" ) type apiService struct { @@ -46,6 +47,11 @@ func (a *apiService) ComposeService() compose.Service { return &a.composeService } +func (a *apiService) SecretsService() secrets.Service { + return nil +} + + func init() { backend.Register("example", "example", service, cloud.NotImplementedCloudService) } @@ -117,8 +123,8 @@ func (cs *containerService) Delete(ctx context.Context, id string, request conta type composeService struct{} -func (cs *composeService) Up(ctx context.Context, opts cli.ProjectOptions) error { - prj, err := cli.ProjectFromOptions(&opts) +func (cs *composeService) Up(ctx context.Context, opts *cli.ProjectOptions) error { + prj, err := cli.ProjectFromOptions(opts) if err != nil { return err } @@ -126,8 +132,8 @@ func (cs *composeService) Up(ctx context.Context, opts cli.ProjectOptions) error return nil } -func (cs *composeService) Down(ctx context.Context, opts cli.ProjectOptions) error { - prj, err := cli.ProjectFromOptions(&opts) +func (cs *composeService) Down(ctx context.Context, opts *cli.ProjectOptions) error { + prj, err := cli.ProjectFromOptions(opts) if err != nil { return err } @@ -135,10 +141,14 @@ func (cs *composeService) Down(ctx context.Context, opts cli.ProjectOptions) err return nil } -func (cs *composeService) Ps(ctx context.Context, opts cli.ProjectOptions) ([]ecstypes.ServiceStatus, error) { +func (cs *composeService) Ps(ctx context.Context, opts *cli.ProjectOptions) ([]compose.ServiceStatus, error) { return nil, errdefs.ErrNotImplemented } -func (cs *composeService) Logs(ctx context.Context, opts cli.ProjectOptions) error { +func (cs *composeService) Logs(ctx context.Context, opts *cli.ProjectOptions, w io.Writer) error { return errdefs.ErrNotImplemented } + +func (cs *composeService) Convert(ctx context.Context, opts *cli.ProjectOptions) ([]byte, error) { + return nil, errdefs.ErrNotImplemented +} diff --git a/go.mod b/go.mod index eb7fbf106..0f3a4472d 100644 --- a/go.mod +++ b/go.mod @@ -20,25 +20,26 @@ require ( github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5 github.com/Microsoft/hcsshim v0.8.9 // indirect github.com/aws/aws-sdk-go v1.34.6 + github.com/awslabs/goformation/v4 v4.14.0 github.com/buger/goterm v0.0.0-20200322175922-2f3e71b85129 - github.com/compose-spec/compose-go v0.0.0-20200710075715-6fcc35384ee1 + github.com/compose-spec/compose-go v0.0.0-20200818070525-eb1188aae4a2 github.com/containerd/console v1.0.0 github.com/containerd/containerd v1.3.5 // indirect github.com/docker/cli v0.0.0-20200528204125-dd360c7c0de8 + github.com/docker/distribution v0.0.0-00010101000000-000000000000 // indirect github.com/docker/docker v17.12.0-ce-rc1.0.20200309214505-aa6a9891b09c+incompatible - github.com/docker/ecs-plugin v1.0.0-beta.4 + github.com/docker/docker-credential-helpers v0.6.3 // indirect github.com/docker/go-connections v0.4.0 github.com/docker/go-units v0.4.0 github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee // indirect github.com/gobwas/pool v0.2.0 // indirect github.com/gobwas/ws v1.0.3 - github.com/gogo/googleapis v1.4.0 // indirect github.com/golang/protobuf v1.4.2 github.com/google/go-cmp v0.5.1 github.com/google/uuid v1.1.1 github.com/gorilla/mux v1.7.4 // indirect github.com/hashicorp/go-multierror v1.1.0 - github.com/hashicorp/go-version v1.2.1 // indirect + github.com/joho/godotenv v1.3.0 github.com/moby/term v0.0.0-20200611042045-63b9a826fb74 github.com/morikuni/aec v1.0.0 github.com/onsi/gomega v1.10.1 // indirect @@ -46,12 +47,13 @@ require ( github.com/opencontainers/runc v0.1.1 // indirect github.com/pkg/errors v0.9.1 github.com/sirupsen/logrus v1.6.0 + github.com/smartystreets/goconvey v1.6.4 // indirect github.com/spf13/cobra v1.0.0 github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.6.1 - golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7 + golang.org/x/net v0.0.0-20200625001655-4c5254603344 golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 - golang.org/x/sync v0.0.0-20190423024810-112230192c58 + golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208 golang.org/x/sys v0.0.0-20200519105757-fe76b779f299 // indirect google.golang.org/grpc v1.31.0 google.golang.org/protobuf v1.25.0 diff --git a/go.sum b/go.sum index e70e25380..9312e8a04 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,3 @@ -bazil.org/fuse v0.0.0-20160811212531-371fbbdaa898/go.mod h1:Xbm+BRKSBEpa4q4hTSxohYNQpsxXPbPry4JJWOB3LB8= -bitbucket.org/liamstask/goose v0.0.0-20150115234039-8488cc47d90c/go.mod h1:hSVuE3qU7grINVSwrmzHfpg9k87ALBk+XaualNyUzI4= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= github.com/AlecAivazis/survey/v2 v2.1.1 h1:LEMbHE0pLj75faaVEKClEX1TM4AJmmnOh9eimREzLWI= @@ -53,11 +51,8 @@ github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUM github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo4seqhv0i0kdATSkM0= -github.com/GeertJohan/go.rice v1.0.0/go.mod h1:eH6gbSOAUv07dQuZVnBmoDP8mgsM1rtixis4Tib9if0= github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5 h1:ygIc8M6trr62pF5DucadTWGdEB4mEyvzi0e2nbcmcyA= github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw= -github.com/Microsoft/hcsshim v0.8.7/go.mod h1:OHd7sQqRFrYd3RmSgbgji+ctCwkbq2wbEYNSzOYtcBQ= github.com/Microsoft/hcsshim v0.8.9 h1:VrfodqvztU8YSOvygU+DN1BGaSGxmrNfqOv5oOuX2Bk= github.com/Microsoft/hcsshim v0.8.9/go.mod h1:5692vkUqntj1idxauYlpoINNKeqCiG6Sg38RRsjT5y8= github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8 h1:xzYJEypr/85nBpB11F9br+3HUrpgb+fcm5iADzXXYEw= @@ -65,73 +60,45 @@ github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8/go.mod h1:oX5x61 github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d h1:UrqY+r/OJnIp5u0s1SbQ8dVfLCZJsnvazdBP5hS4iRs= github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ= -github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412 h1:w1UutsfOrms1J05zt7ISrnJIXKzwaspym5BTKGx93EI= -github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412/go.mod h1:WPjqKcmVOxf0XSf3YxCJs6N6AOSrOx3obionmG7T0y0= -github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/aws/aws-sdk-go v1.15.11/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0= -github.com/aws/aws-sdk-go v1.30.22/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0= github.com/aws/aws-sdk-go v1.34.6 h1:2aPXQGkR6xeheN5dns13mSoDWeUlj4wDmfZ+8ZDHauw= github.com/aws/aws-sdk-go v1.34.6/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0= -github.com/awslabs/goformation/v4 v4.8.0 h1:UiUhyokRy3suEqBXTnipvY8klqY3Eyl4GCH17brraEc= -github.com/awslabs/goformation/v4 v4.8.0/go.mod h1:GcJULxCJfloT+3pbqCluXftdEK2AD/UqpS3hkaaBntg= +github.com/awslabs/goformation/v4 v4.14.0 h1:E2Pet9eIqA4qzt3dzzzE4YN83V4Kyfbcio0VokBC9TA= +github.com/awslabs/goformation/v4 v4.14.0/go.mod h1:GcJULxCJfloT+3pbqCluXftdEK2AD/UqpS3hkaaBntg= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/bitly/go-hostpool v0.1.0 h1:XKmsF6k5el6xHG3WPJ8U0Ku/ye7njX7W81Ng7O2ioR0= -github.com/bitly/go-hostpool v0.1.0/go.mod h1:4gOCgp6+NZnVqlKyZ/iBZFTAJKembaVENUpMkpg42fw= github.com/bitly/go-simplejson v0.5.0 h1:6IH+V8/tVMab511d5bn4M7EwGXZf9Hj6i2xSwkNEM+Y= github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA= -github.com/blang/semver v3.1.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= github.com/bshuster-repo/logrus-logstash-hook v0.4.1/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk= github.com/buger/goterm v0.0.0-20200322175922-2f3e71b85129 h1:gfAMKE626QEuKG3si0pdTRcr/YEbBoxY+3GOH3gWvl4= github.com/buger/goterm v0.0.0-20200322175922-2f3e71b85129/go.mod h1:u9UyCz2eTrSGy6fbupqJ54eY5c4IC8gREQ1053dK12U= github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8= -github.com/bugsnag/bugsnag-go v1.5.3 h1:yeRUT3mUE13jL1tGwvoQsKdVbAsQx9AJ+fqahKveP04= -github.com/bugsnag/bugsnag-go v1.5.3/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8= github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0BsqsP2LwDJ9aOkm/6J86V6lyAXCoQWGw3K50= github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE= -github.com/bugsnag/panicwrap v1.2.0 h1:OzrKrRvXis8qEvOkfcxNcYbOd2O7xXS2nnKMEMABFQA= -github.com/bugsnag/panicwrap v1.2.0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE= -github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= -github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/certifi/gocertifi v0.0.0-20180118203423-deb3ae2ef261/go.mod h1:GJKEexRPVJrBSOjoqN5VNOIKJ5Q3RViH6eu3puDRwx4= github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= -github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cloudflare/backoff v0.0.0-20161212185259-647f3cdfc87a/go.mod h1:rzgs2ZOiguV6/NpiDgADjRLPNyZlApIWxKpkT+X8SdY= -github.com/cloudflare/cfssl v1.4.1 h1:vScfU2DrIUI9VPHBVeeAQ0q5A+9yshO1Gz+3QoUQiKw= -github.com/cloudflare/cfssl v1.4.1/go.mod h1:KManx/OJPb5QY+y0+o/898AMcM128sF0bURvoVUSjTo= -github.com/cloudflare/go-metrics v0.0.0-20151117154305-6a9aea36fb41/go.mod h1:eaZPlJWD+G9wseg1BuRXlHnjntPMrywMsyxf+LTOdP4= -github.com/cloudflare/redoctober v0.0.0-20171127175943-746a508df14c/go.mod h1:6Se34jNoqrd8bTxrmJB2Bg2aoZ2CdSXonils9NsiNgo= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/compose-spec/compose-go v0.0.0-20200710075715-6fcc35384ee1 h1:F+YIkKDMHdgZBacawhFY1P9RAIgO+6uv2te6hjsjzF0= -github.com/compose-spec/compose-go v0.0.0-20200710075715-6fcc35384ee1/go.mod h1:ArodJ6gsEB7iWKrbV3fSHZ08LlBvSVB0Oqg04fX86t4= +github.com/compose-spec/compose-go v0.0.0-20200818070525-eb1188aae4a2 h1:b3JmHJVJt8zXy112yGtRq74G32sPQ8XLJxfHKaP/DOg= +github.com/compose-spec/compose-go v0.0.0-20200818070525-eb1188aae4a2/go.mod h1:P7PZ0svgjrZ8nv/XvxObbl8o0DCIE9ZbL8pllg6uL4w= github.com/containerd/cgroups v0.0.0-20190919134610-bf292b21730f h1:tSNMc+rJDfmYntojat8lljbt1mgKNpTxUZJsSzJ9Y1s= github.com/containerd/cgroups v0.0.0-20190919134610-bf292b21730f/go.mod h1:OApqhQ4XNSNC13gXIwDjhOQxjWa/NxkwZXJ1EvqT0ko= github.com/containerd/console v0.0.0-20180822173158-c12b1e7919c1/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw= github.com/containerd/console v1.0.0 h1:fU3UuQapBs+zLJu82NhR11Rif1ny2zfMMAyPJzSN5tQ= github.com/containerd/console v1.0.0/go.mod h1:8Pf4gM6VEbTNRIT26AyyU7hxdQU3MvAvxVI0sc00XBE= -github.com/containerd/containerd v1.3.0-beta.2.0.20190828155532-0293cbd26c69/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= github.com/containerd/containerd v1.3.2/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= github.com/containerd/containerd v1.3.5 h1:l0iDHQtFwcOUmOvdepI6BB67q7beT6sRp2JYsfHS08c= github.com/containerd/containerd v1.3.5/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc h1:TP+534wVlf61smEIq1nwLLAjQVEK2EADoW3CX9AuT+8= github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= -github.com/containerd/continuity v0.0.0-20200413184840-d3ef23f19fbb h1:nXPkFq8X1a9ycY3GYQpFNxHh3j2JgY7zDZfq2EXMIzk= -github.com/containerd/continuity v0.0.0-20200413184840-d3ef23f19fbb/go.mod h1:Dq467ZllaHgAtVp4p1xUQWBrFXR9s/wyoTpG8zOJGkY= github.com/containerd/fifo v0.0.0-20190226154929-a9fb20d87448 h1:PUD50EuOMkXVcpBIA/R95d56duJR9VxhwncsFbNnxW4= github.com/containerd/fifo v0.0.0-20190226154929-a9fb20d87448/go.mod h1:ODA38xgv3Kuk8dQz2ZQXpnv/UZZUHUCL7pnLehbXgQI= github.com/containerd/go-runc v0.0.0-20180907222934-5a6d9f37cfa3/go.mod h1:IV7qH3hrUgRmyYrtgEeGWJfWbgcHL9CSRruz2Vqcph0= @@ -141,21 +108,16 @@ github.com/containerd/typeurl v0.0.0-20180627222232-a93fcdb778cd h1:JNn81o/xG+8N github.com/containerd/typeurl v0.0.0-20180627222232-a93fcdb778cd/go.mod h1:Cm3kwCdlkCfMSHURc+r6fwoGH6/F1hH3S4sg0rLFWPc= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= -github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e h1:Wf6HqHfScWJN9/ZjdUKyjop4mf3Qdd+1TvvltAvM3m8= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= -github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/creack/pty v1.1.9 h1:uDmaGzcdjhF4i/plgjmEsriH11Y0o7RKapEf/LDaM3w= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/daaku/go.zipexe v1.0.0/go.mod h1:z8IiR6TsVLEYKwXAoE/I+8ys/sDkgTzSL0CLnGVd57E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd h1:83Wprp6ROGeiHFAP8WJdI2RoxALQYgdllERc3N5N2DM= -github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= github.com/denverdino/aliyungo v0.0.0-20190125010748-a747050bb1ba/go.mod h1:dV8lFg6daOBZbT6/BDGIz6Y3WFGn8juu6G+CQ6LHtl0= github.com/dgrijalva/jwt-go v0.0.0-20170104182250-a601269ab70c/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= @@ -164,20 +126,14 @@ github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8 github.com/dimchansky/utfbom v1.1.0 h1:FcM3g+nofKgUteL8dm/UpdRXNC9KmADgTpLKsu0TRo4= github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8= github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E= -github.com/docker/cli v0.0.0-20200130152716-5d0cf8839492/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/cli v0.0.0-20200528204125-dd360c7c0de8 h1:JRquW4uqIU+eSilDhuo9X9QFX4NEmGj5B1x97ZA8djM= github.com/docker/cli v0.0.0-20200528204125-dd360c7c0de8/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/distribution v0.0.0-20200708230824-53e18a9d9bfe h1:pni13lAFm1g4cjHU6c3n4qGvvJGZQK4VvKRKMseQ42E= github.com/docker/distribution v0.0.0-20200708230824-53e18a9d9bfe/go.mod h1:Oqz4IonmMNc2N7GqfTL2xkhCQx0yS6nR+HrOZJnmKIk= -github.com/docker/docker v1.4.2-0.20200128034134-2ebaeef943cc/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker v17.12.0-ce-rc1.0.20200309214505-aa6a9891b09c+incompatible h1:G2hY8RD7jB9QaSmcb8mYEIg8QbEvVAB7se8+lXHZHfg= github.com/docker/docker v17.12.0-ce-rc1.0.20200309214505-aa6a9891b09c+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker-credential-helpers v0.6.3 h1:zI2p9+1NQYdnG6sMU26EX4aVGlqbInSQxQXLvzJ4RPQ= github.com/docker/docker-credential-helpers v0.6.3/go.mod h1:WRaJzqw3CTB9bk10avuGsjVBZsD05qeibJ1/TYlvc0Y= -github.com/docker/ecs-plugin v1.0.0-beta.4 h1:hZKojW0tqsdhJjfMKPw6piMw/GJgfX6CVXd1YUuXLg4= -github.com/docker/ecs-plugin v1.0.0-beta.4/go.mod h1:1YaNZwrNr0dFjTP3v7zwepluaZgVNV94s0M6fL+i/iA= -github.com/docker/go v1.5.1-1 h1:hr4w35acWBPhGBXlzPoHpmZ/ygPjnmFVxGxxGnMyP7k= -github.com/docker/go v1.5.1-1/go.mod h1:CADgU4DSXK5QUlFslkQu2yW2TKzFZcXq/leZfM0UH5Q= github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c h1:+pKlWGMw7gf6bQ+oDZB4KHQFypsfjYlq/C4rfL7D3g8= @@ -187,26 +143,18 @@ github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHz github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE= -github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 h1:UhxFibDNY/bfvqU5CAUmr9zpesgbU6SWc8/B4mflAE4= -github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE= -github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y= -github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0= github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/garyburd/redigo v0.0.0-20150301180006-535138d7bcd7/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY= -github.com/getsentry/raven-go v0.0.0-20180121060056-563b81fc02b7/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-ini/ini v1.25.4/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= -github.com/go-sql-driver/mysql v1.3.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= -github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= @@ -218,16 +166,10 @@ github.com/gobwas/ws v1.0.3 h1:ZOigqf7iBxkA4jdQ3am7ATzdlOFp9YzA6NmuvEEZc9g= github.com/gobwas/ws v1.0.3/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e h1:BWhy2j3IXJhjCbC68FptL43tDKIq8FladmaTs3Xs7Z8= github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= -github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE= -github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= -github.com/gogo/googleapis v1.4.0 h1:zgVt4UpGxcqVOw97aRGxT4svlcmdK35fynLNctY32zI= -github.com/gogo/googleapis v1.4.0/go.mod h1:5YRNX2z1oM5gXdAkurHa942MDgEJyk02w4OecKY87+c= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls= github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= -github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY= -github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= @@ -244,8 +186,6 @@ github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QD github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/certificate-transparency-go v1.0.21 h1:Yf1aXowfZ2nuboBsg7iYGLmwsOARdV86pfH3g95wXmE= -github.com/google/certificate-transparency-go v1.0.21/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= @@ -262,7 +202,6 @@ github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGa github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/handlers v0.0.0-20150720190736-60c7bfde3e33/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= github.com/gorilla/mux v1.7.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= -github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc= github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= @@ -270,16 +209,10 @@ github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmg github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= -github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8= -github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4= -github.com/hashicorp/errwrap v0.0.0-20141028054710-7554cd9344ce/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/go-multierror v0.0.0-20161216184304-ed905158d874/go.mod h1:JMRHfdO9jKNzS/+BTlxCjKNQHg/jZAft8U7LloJvN7I= github.com/hashicorp/go-multierror v1.1.0 h1:B9UzwGQJehnUY1yNrnwREHc3fGbC2xefo8g4TbElacI= github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= -github.com/hashicorp/go-version v1.2.1 h1:zEfKbn2+PDgroKdiOzqiE8rsmLqU2uwi5PB5pBJ3TkI= -github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= @@ -289,23 +222,14 @@ github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174/go.mod h1:DqJ97dSdRW github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= -github.com/imdario/mergo v0.3.8 h1:CGgOkSJeqMRmt0D9XLWExdT4m4F1vd3FV3VPt+0VxkQ= -github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA= +github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= -github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= -github.com/jinzhu/gorm v1.9.12 h1:Drgk1clyWT9t9ERbzHza6Mj/8FY/CqMyVzOiHviMo6Q= -github.com/jinzhu/gorm v1.9.12/go.mod h1:vhTjlKSJUTWNtcbQtrMBFCxy7eXTzeCAzfL5fBZT/Qs= -github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= -github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= -github.com/jinzhu/now v1.0.1 h1:HjfetcXq097iXP0uoPCdnM4Efp5/9MsM0/M+XOTeR3M= -github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/jmespath/go-jmespath v0.0.0-20160803190731-bd40a432e4c7/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/jmespath/go-jmespath v0.3.0 h1:OS12ieG61fsCg5+qLJ+SsW9NicxNkg3b25OyT2yCeUc= github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik= -github.com/jmhodges/clock v0.0.0-20160418191101-880ee4c33548/go.mod h1:hGT6jSUVzF6no3QaDSMLGLEHtHSBSefs+MgcDWnmhmo= -github.com/jmoiron/sqlx v0.0.0-20180124204410-05cef0741ade/go.mod h1:IiEW3SEiiErVyFdH8NTuWjSifiEQKUoyK3LNqr2kCHU= github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= @@ -313,18 +237,12 @@ github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCV github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= -github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a h1:FaWFmfWdAUKbSCtOU2QjDaorUexogfaMgbipgYATUMU= -github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a/go.mod h1:UJSiEoRfvx3hP73CvoARgeLjaIOjybY9vj8PUPPFGeU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 h1:iQTw/8FWTuc7uiaSepXwyf3o52HaUYcV+Tu66S3F5GA= -github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/kisielk/sqlstruct v0.0.0-20150923205031-648daed35d49/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE= -github.com/kisom/goutils v1.1.0/go.mod h1:+UBTfd78habUYWFbNWTJNG+jNG/i/lGURakr4A/yNRw= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= @@ -337,44 +255,28 @@ github.com/kr/pty v1.1.5 h1:hyz3dwM5QLc1Rfoz4FuWJQG5BN7tc6K1MndAUnGpQr4= github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/kylelemons/go-gypsy v0.0.0-20160905020020-08cad365cd28/go.mod h1:T/T7jsxVqf9k/zYOqbgNAsANsjxTd1Yq3htjDhQ1H0c= -github.com/lib/pq v0.0.0-20180201184707-88edab080323/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.3.0 h1:/qkRGz8zljWiDcFvgpwUpwIAPu3r07TDvs3Rws+o/pU= -github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a h1:weJVJJRzAJBFRlAiJQROKQs8oC9vOxvm4rZmBBk0ONw= -github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= -github.com/manifoldco/promptui v0.7.0 h1:3l11YT8tm9MnwGFQ4kETwkzpAwY2Jt9lCrumCUW4+z4= -github.com/manifoldco/promptui v0.7.0/go.mod h1:n4zTdgP0vr0S3w7/O/g98U+e0gwLScEXGwov2nIKuGQ= github.com/marstr/guid v1.1.0/go.mod h1:74gB1z2wpxxInTG6yaqA7KrtM0NZ+RbrcqDvYHefzho= -github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-ieproxy v0.0.0-20190610004146-91bb50d98149 h1:HfxbT6/JcvIljmERptWhwa8XzP7H3T+Z2N26gTsaDaA= github.com/mattn/go-ieproxy v0.0.0-20190610004146-91bb50d98149/go.mod h1:31jz6HNzdxOmlERGGEc4v/dMssOfmp2p5bT/okiKFFc= -github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-shellwords v1.0.10 h1:Y7Xqm8piKOO3v10Thp7Z36h4FYFjt5xB//6XvOrs2Gw= github.com/mattn/go-shellwords v1.0.10/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= -github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= -github.com/mattn/go-sqlite3 v2.0.1+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= -github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U= -github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4= github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= -github.com/miekg/pkcs11 v1.0.3 h1:iMwmD7I5225wv84WxIG/bmxz9AXjWvTWIbM/TYHvWtw= -github.com/miekg/pkcs11 v1.0.3/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/mitchellh/mapstructure v1.3.2 h1:mRS76wmkOn3KkKAyXDu42V+6ebnXWIztFSYGN7GeoRg= -github.com/mitchellh/mapstructure v1.3.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.3.3 h1:SzB1nHZ2Xi+17FP0zVQBHIZqvwRN9408fJO8h+eeNA8= +github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/osext v0.0.0-20151018003038-5e2d6d41470f/go.mod h1:OkQIRizQZAeMln+1tSwduZz7+Af5oFlKirV/MSYes2A= +github.com/mjibson/esc v0.2.0/go.mod h1:9Hw9gxxfHulMF5OJKCyhYD7PzlSdhzXyaGEBRPH1OPs= github.com/moby/term v0.0.0-20200611042045-63b9a826fb74 h1:kvRIeqJNICemq2UFLx8q/Pj+1IRNZS0XPTaMFkuNsvg= github.com/moby/term v0.0.0-20200611042045-63b9a826fb74/go.mod h1:pJ0Ot5YGdTcMdxnPMyGCfAr6fKXe0g9cDlz16MuFEBE= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -383,26 +285,19 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= -github.com/mreiferson/go-httpclient v0.0.0-20160630210159-31f0106b4474/go.mod h1:OQA4XLvDbMgS8P0CevmM4m9Q3Jq4phKUzcocxuGJ5m8= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/ncw/swift v1.0.47/go.mod h1:23YIA4yWVnGwv2dQlN4bB7egfYX6YLn0Yo/S6zZO/ZM= -github.com/nkovacs/streamquote v0.0.0-20170412213628-49af9bddb229/go.mod h1:0aYXnNPJ8l7uZxf45rWW1a/uME32OF0rhiYGNQ2oF2E= github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/onsi/ginkgo v1.5.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.10.1 h1:q/mM8GF/n0shIN8SaAZ0V+jnLPzen6WIVZdiwrRlMlo= -github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1 h1:mFwc4LvZ0xpSvDZ3E+k8Yte0hLOMxXUlP+yXtJqkYfQ= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/gomega v1.2.0/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= -github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= -github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= github.com/opencontainers/go-digest v0.0.0-20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= @@ -414,11 +309,9 @@ github.com/opencontainers/runc v0.1.1 h1:GlxAyO6x8rfZYN9Tt0Kti5a/cP41iuiO2yYT0IJ github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= github.com/opencontainers/runtime-spec v0.1.2-0.20190507144316-5b71a03e2700 h1:eNUVfm/RFLIi1G7flU5/ZRTHvd4kcVuzfRnL6OFlzCI= github.com/opencontainers/runtime-spec v0.1.2-0.20190507144316-5b71a03e2700/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= -github.com/opencontainers/runtime-tools v0.0.0-20181011054405-1d69bd0f9c39/go.mod h1:r3f7wjNzSs2extwzU3Y+6pKfobzPh+kKFJ3ofN+3nfs= github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.8.1-0.20171018195549-f15c970de5b7/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -443,11 +336,8 @@ github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= -github.com/prometheus/procfs v0.0.5 h1:3+auTFlqw+ZaQYJARz6ArODtkaIwtvBTx3N2NehQlL8= -github.com/prometheus/procfs v0.0.5/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= -github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sanathkr/go-yaml v0.0.0-20170819195128-ed9d249f429b h1:jUK33OXuZP/l6babJtnLo1qsGvq6G9so9KMflGAm4YA= github.com/sanathkr/go-yaml v0.0.0-20170819195128-ed9d249f429b/go.mod h1:8458kAagoME2+LN5//WxE71ysZ3B7r22fdgb7qVmXSY= @@ -455,9 +345,7 @@ github.com/sanathkr/yaml v0.0.0-20170819201035-0056894fa522 h1:fOCp11H0yuyAt2wql github.com/sanathkr/yaml v0.0.0-20170819201035-0056894fa522/go.mod h1:tQTYKOQgxoH3v6dEmdHiz4JG+nbxWwM5fgPQUpSZqVQ= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= -github.com/sirupsen/logrus v1.0.4-0.20170822132746-89742aefa4b2/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I= @@ -473,18 +361,14 @@ github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= -github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= github.com/spf13/cobra v1.0.0 h1:6m/oheQuQ13N9ks4hubMG6BnvwOeaJrqSPLahSnczz8= github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= -github.com/spf13/pflag v1.0.1-0.20171106142849-4c012f6dcd95/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= github.com/spf13/viper v1.4.0 h1:yXHLWeravcrgGyFSyCgdYpXQ9dR9c/WED3pg1RhxqEU= github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -497,43 +381,24 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8 h1:zLV6q4e8Jv9EHjNg/iHfzwDkCve6Ua5jCygptrtXHvI= -github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= -github.com/theupdateframework/notary v0.6.1 h1:7wshjstgS9x9F5LuB1L5mBI2xNMObWqjz+cjWoom6l0= -github.com/theupdateframework/notary v0.6.1/go.mod h1:MOfgIfmox8s7/7fduvB2xyPPMJCrjRLRizA8OFwpnKY= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= -github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= github.com/urfave/cli v0.0.0-20171014202726-7bc6a0acffa5/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= -github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= -github.com/weppos/publicsuffix-go v0.4.0/go.mod h1:z3LCPQ38eedDQSwmsSRW4Y7t2L8Ln16JPQ02lHAdn5k= -github.com/weppos/publicsuffix-go v0.5.0 h1:rutRtjBJViU/YjcI5d80t4JAVvDltS6bciJg2K1HrLU= -github.com/weppos/publicsuffix-go v0.5.0/go.mod h1:z3LCPQ38eedDQSwmsSRW4Y7t2L8Ln16JPQ02lHAdn5k= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= -github.com/xeipuuv/gojsonschema v0.0.0-20180618132009-1d523034197f/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs= github.com/xeipuuv/gojsonschema v0.0.0-20181112162635-ac52e6811b56/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs= github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= -github.com/xlab/handysort v0.0.0-20150421192137-fb3537ed64a1 h1:j2hhcujLRHAg872RWAV5yaUrEjHEObwDv3aImCaNLek= -github.com/xlab/handysort v0.0.0-20150421192137-fb3537ed64a1/go.mod h1:QcJo0QPSfTONNIgpN5RA8prR7fF8nkF6cTWTcNerRO8= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43/go.mod h1:aX5oPXxHm3bOH+xeAttToC8pqch2ScQN/JoXYupl6xs= github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50/go.mod h1:NUSPSUX/bi6SeDMUh6brw0nXpxHnc96TguQh0+r/ssA= github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f/go.mod h1:GlGEuHIJweS1mbCqG+7vt2nvWLzLLnRHbXz5JKd/Qbg= -github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0= -github.com/zmap/rc2 v0.0.0-20131011165748-24b9757f5521/go.mod h1:3YZ9o3WnatTIZhuOtot4IcUfzoKVjUHqu6WALIyI0nE= -github.com/zmap/zcertificate v0.0.0-20180516150559-0e3d58b1bac4/go.mod h1:5iU54tB79AMBcySS0R2XIyZBAVmeHranShAFELYx7is= -github.com/zmap/zcrypto v0.0.0-20190729165852-9051775e6a2e h1:mvOa4+/DXStR4ZXOks/UsjeFdn5O5JpLUtzqk9U8xXw= -github.com/zmap/zcrypto v0.0.0-20190729165852-9051775e6a2e/go.mod h1:w7kd3qXHh8FNaczNjslXqvFQiv5mMWRXlL9klTUAHc8= -github.com/zmap/zlint v0.0.0-20190806154020-fd021b4cfbeb h1:vxqkjztXSaPVDc8FQCdHTaejm2x747f6yPbnu1h2xkg= -github.com/zmap/zlint v0.0.0-20190806154020-fd021b4cfbeb/go.mod h1:29UiAJNsiVdvTBFCJW8e3q6dcDbOoPkhMgttOSCIMMY= go.etcd.io/bbolt v1.3.2 h1:Z/90sZLPOeCy2PwprqkFa25PdkusRzaj9P8zm/KNyvk= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.opencensus.io v0.22.0 h1:C9hSCOW830chIVkdja34wa6Ky+IzWllkUinR+BtRZd4= @@ -541,14 +406,10 @@ go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= -golang.org/x/crypto v0.0.0-20171113213409-9f005a07e0d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200128174031-69ecbb4d6d5d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= @@ -556,7 +417,7 @@ golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -570,12 +431,15 @@ golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190619014844-b5b0513f8c1b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7 h1:AeiKBIuRw3UomYXSbLy0Mc2dDLfdtbT/IVn4keq83P0= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344 h1:vGXIOMxbNfDTk/aXCmfdLgkrSV+Z2tcbze+pEc3v5W4= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be h1:vEDujvNQGv4jgYKudGeI/+DAX4Jffq6hpD55MmoEvKs= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0= @@ -586,13 +450,13 @@ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208 h1:qwRHBd0NqMbJxfbotnDhm2ByMI1Shq4Y6oRJo21SGJA= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -604,7 +468,6 @@ golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200519105757-fe76b779f299 h1:DYfZAGf2WMFjMxbgTjaC+2HC7NkNAQs+6Q8b9WEB/F4= @@ -614,8 +477,6 @@ golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -625,6 +486,10 @@ golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3 golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200717024301-6ddee64345a6/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.0.0-20160322025152-9bf6e6e569ff/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= @@ -658,23 +523,14 @@ google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2 google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20141024133853-64131543e789/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/dancannon/gorethink.v3 v3.0.5 h1:/g7PWP7zUS6vSNmHSDbjCHQh1Rqn8Jy6zSMQxAsBSMQ= -gopkg.in/dancannon/gorethink.v3 v3.0.5/go.mod h1:GXsi1e3N2OcKhcP6nsYABTiUejbWMFO4GY5a4pEaeEc= -gopkg.in/fatih/pool.v2 v2.0.0 h1:xIFeWtxifuQJGk/IEPKsTduEKcKvPmhoiVDGpC40nKg= -gopkg.in/fatih/pool.v2 v2.0.0/go.mod h1:8xVGeu1/2jr2wm5V9SPuMht2H5AEmf5aFMGSQixtjTY= gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= -gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= -gopkg.in/gorethink/gorethink.v3 v3.0.5 h1:e2Uc/Xe+hpcVQFsj6MuHlYog3r0JYpnTzwDj/y2O4MU= -gopkg.in/gorethink/gorethink.v3 v3.0.5/go.mod h1:+3yIIHJUGMBK+wyPH+iN5TP+88ikFDfZdqTlK3Y9q8I= -gopkg.in/ini.v1 v1.55.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.60.0 h1:P5ZzC7RJO04094NJYlEnBdFK2wwmnCAy/+7sAzvWs60= gopkg.in/ini.v1 v1.60.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= @@ -696,6 +552,3 @@ gotest.tools/v3 v3.0.2 h1:kG1BFyqVHuQoVQiR1bWGnfz/fmHvvuiSPIV7rvl360E= gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -k8s.io/kubernetes v1.13.0/go.mod h1:ocZa8+6APFNC2tX1DZASIbocyYT5jHzqFVsY5aoB7Jk= -vbom.ml/util v0.0.0-20180919145318-efcd4e0f9787 h1:O69FD9pJA4WUZlEwYatBEEkRWKQ5cKodWpdKTrCS/iQ= -vbom.ml/util v0.0.0-20180919145318-efcd4e0f9787/go.mod h1:so/NYdZXCz+E3ZpW0uAoCj6uzU2+8OWDFv/HxUSs7kI= diff --git a/local/backend.go b/local/backend.go index 5973062db..30a901fdd 100644 --- a/local/backend.go +++ b/local/backend.go @@ -41,6 +41,7 @@ import ( "github.com/docker/api/containers" "github.com/docker/api/context/cloud" "github.com/docker/api/errdefs" + "github.com/docker/api/secrets" ) type local struct { @@ -70,6 +71,10 @@ func (ms *local) ComposeService() compose.Service { return nil } +func (ms *local) SecretsService() secrets.Service { + return nil +} + func (ms *local) Inspect(ctx context.Context, id string) (containers.Container, error) { c, err := ms.apiClient.ContainerInspect(ctx, id) if err != nil { diff --git a/secrets/api.go b/secrets/api.go new file mode 100644 index 000000000..53fafe96c --- /dev/null +++ b/secrets/api.go @@ -0,0 +1,72 @@ +/* + Copyright 2020 Docker, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package secrets + +import ( + "context" + "encoding/json" +) + +// Service interacts with the underlying secrets backend +type Service interface { + CreateSecret(ctx context.Context, secret Secret) (string, error) + InspectSecret(ctx context.Context, id string) (Secret, error) + ListSecrets(ctx context.Context) ([]Secret, error) + DeleteSecret(ctx context.Context, id string, recover bool) error +} + +// Secret hold sensitive data +type Secret struct { + ID string `json:"ID"` + Name string `json:"Name"` + Labels map[string]string `json:"Labels"` + Description string `json:"Description"` + username string + password string +} + +// NewSecret builds a secret +func NewSecret(name, username, password, description string) Secret { + return Secret{ + Name: name, + username: username, + password: password, + Description: description, + } +} + +// ToJSON marshall a Secret into JSON string +func (s Secret) ToJSON() (string, error) { + b, err := json.MarshalIndent(&s, "", "\t") + if err != nil { + return "", err + } + return string(b), nil +} + +// GetCredString marshall a Secret's sensitive data into JSON string +func (s Secret) GetCredString() (string, error) { + creds := map[string]string{ + "username": s.username, + "password": s.password, + } + b, err := json.Marshal(&creds) + if err != nil { + return "", err + } + return string(b), nil +}