Merge pull request #516 from docker/ecs-import

This commit is contained in:
Nicolas De loof 2020-08-19 13:33:27 +02:00 committed by GitHub
commit f5a2c4c663
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
40 changed files with 3900 additions and 223 deletions

View File

@ -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
}

View File

@ -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
}

View File

@ -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{

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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)
})
}

View File

@ -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()
}

View File

@ -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
}

26
compose/tags.go Normal file
View File

@ -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"
)

View File

@ -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) {

619
ecs/cloudformation.go Normal file
View File

@ -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<AWS::EC2::Subnet::Id>",
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 <service>.<project>.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
}

396
ecs/cloudformation_test.go Normal file
View File

@ -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
}

81
ecs/colors.go Normal file
View File

@ -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)
}
}()
}

103
ecs/compatibility.go Normal file
View File

@ -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)
}
}

View File

@ -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" {

476
ecs/convert.go Normal file
View File

@ -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 <project>.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
}

48
ecs/down.go Normal file
View File

@ -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
}

59
ecs/iam.go Normal file
View File

@ -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"`
}

76
ecs/list.go Normal file
View File

@ -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
}

87
ecs/logs.go Normal file
View File

@ -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
}

61
ecs/marshall.go Normal file
View File

@ -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
}

616
ecs/sdk.go Normal file
View File

@ -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
}

39
ecs/secrets.go Normal file
View File

@ -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)
}

22
ecs/secrets/Dockerfile Normal file
View File

@ -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"]

105
ecs/secrets/init.go Normal file
View File

@ -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
}

104
ecs/secrets/init_test.go Normal file
View File

@ -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)
}

49
ecs/secrets/main/main.go Normal file
View File

@ -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 <json encoded []Secret>")
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)
}
}
}

1
ecs/testdata/input/envfile vendored Normal file
View File

@ -0,0 +1 @@
FOO=BAR

View File

@ -0,0 +1,6 @@
version: "3"
services:
simple:
image: nginx
ports:
- "80:80"

View File

@ -0,0 +1,5 @@
version: "3"
services:
simple:
image: nginx
network_mode: bridge

View File

@ -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"
}
}
}

161
ecs/up.go Normal file
View File

@ -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
}

109
ecs/wait.go Normal file
View File

@ -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
}

31
ecs/x.go Normal file
View File

@ -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"
)

View File

@ -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
}

14
go.mod
View File

@ -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

189
go.sum
View File

@ -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=

View File

@ -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 {

72
secrets/api.go Normal file
View File

@ -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
}