implement secret management

Signed-off-by: aiordache <anca.iordache@docker.com>
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
This commit is contained in:
aiordache 2020-04-29 20:36:00 +02:00 committed by Nicolas De Loof
parent 2ad9504d15
commit 41aaf802e3
No known key found for this signature in database
GPG Key ID: 9858809D6F8F6E7E
9 changed files with 417 additions and 285 deletions

View File

@ -2,7 +2,7 @@ clean:
rm -rf dist/
build:
go build -v -o dist/docker-ecs cmd/main/main.go
go build -v -o dist/docker-ecs cmd/main.go
test: ## Run tests
go test ./... -v

117
ecs/cmd/commands/compose.go Normal file
View File

@ -0,0 +1,117 @@
package commands
import (
"context"
"fmt"
"github.com/docker/ecs-plugin/pkg/amazon"
"github.com/docker/ecs-plugin/pkg/compose"
"github.com/spf13/cobra"
)
type ClusterOptions struct {
Profile string
Region string
Cluster string
}
func ComposeCommand(clusteropts *ClusterOptions) *cobra.Command {
cmd := &cobra.Command{
Use: "compose",
}
opts := &compose.ProjectOptions{}
opts.AddFlags(cmd.Flags())
cmd.AddCommand(
ConvertCommand(clusteropts, opts),
UpCommand(clusteropts, opts),
DownCommand(clusteropts, opts),
)
return cmd
}
type upOptions struct {
loadBalancerArn string
}
func (o upOptions) LoadBalancerArn() *string {
if o.loadBalancerArn == "" {
return nil
}
return &o.loadBalancerArn
}
func ConvertCommand(clusteropts *ClusterOptions, projectOpts *compose.ProjectOptions) *cobra.Command {
cmd := &cobra.Command{
Use: "convert",
RunE: compose.WithProject(projectOpts, func(project *compose.Project, args []string) error {
client, err := amazon.NewClient(clusteropts.Profile, clusteropts.Cluster, clusteropts.Region)
if err != nil {
return err
}
template, err := client.Convert(context.Background(), project)
if err != nil {
return err
}
j, err := template.JSON()
if err != nil {
fmt.Printf("Failed to generate JSON: %s\n", err)
} else {
fmt.Printf("%s\n", string(j))
}
return nil
}),
}
return cmd
}
func UpCommand(clusteropts *ClusterOptions, projectOpts *compose.ProjectOptions) *cobra.Command {
opts := upOptions{}
cmd := &cobra.Command{
Use: "up",
RunE: compose.WithProject(projectOpts, func(project *compose.Project, args []string) error {
client, err := amazon.NewClient(clusteropts.Profile, clusteropts.Cluster, clusteropts.Region)
if err != nil {
return err
}
return client.ComposeUp(context.Background(), project)
}),
}
cmd.Flags().StringVar(&opts.loadBalancerArn, "load-balancer", "", "")
return cmd
}
type downOptions struct {
DeleteCluster bool
}
func DownCommand(clusteropts *ClusterOptions, projectOpts *compose.ProjectOptions) *cobra.Command {
opts := downOptions{}
cmd := &cobra.Command{
Use: "down",
RunE: func(cmd *cobra.Command, args []string) error {
client, err := amazon.NewClient(clusteropts.Profile, clusteropts.Cluster, clusteropts.Region)
if err != nil {
return err
}
if len(args) == 0 {
project, err := compose.ProjectFromOptions(projectOpts)
if err != nil {
return err
}
return client.ComposeDown(context.Background(), project.Name, opts.DeleteCluster)
}
// project names passed as parameters
for _, name := range args {
err := client.ComposeDown(context.Background(), name, opts.DeleteCluster)
if err != nil {
return err
}
}
return nil
},
}
cmd.Flags().BoolVar(&opts.DeleteCluster, "delete-cluster", false, "Delete cluster")
return cmd
}

147
ecs/cmd/commands/secret.go Normal file
View File

@ -0,0 +1,147 @@
package commands
import (
"context"
"errors"
"fmt"
"io"
"os"
"strings"
"text/tabwriter"
"github.com/docker/ecs-plugin/pkg/amazon"
"github.com/docker/ecs-plugin/pkg/docker"
"github.com/spf13/cobra"
)
type createSecretOptions struct {
Label string
}
type deleteSecretOptions struct {
recover bool
}
func SecretCommand(clusteropts *ClusterOptions) *cobra.Command {
cmd := &cobra.Command{
Use: "secret",
Short: "Manages secrets",
}
cmd.AddCommand(
CreateSecret(clusteropts),
InspectSecret(clusteropts),
ListSecrets(clusteropts),
DeleteSecret(clusteropts),
)
return cmd
}
func CreateSecret(clusteropts *ClusterOptions) *cobra.Command {
//opts := createSecretOptions{}
cmd := &cobra.Command{
Use: "create NAME SECRET",
Short: "Creates a secret.",
RunE: func(cmd *cobra.Command, args []string) error {
client, err := amazon.NewClient(clusteropts.Profile, clusteropts.Cluster, clusteropts.Region)
if err != nil {
return err
}
if len(args) == 0 {
return errors.New("Missing mandatory parameter: NAME")
}
name := args[0]
secret := args[1]
id, err := client.CreateSecret(context.Background(), name, secret)
fmt.Println(id)
return err
},
}
return cmd
}
func InspectSecret(clusteropts *ClusterOptions) *cobra.Command {
cmd := &cobra.Command{
Use: "inspect ID",
Short: "Displays secret details",
RunE: func(cmd *cobra.Command, args []string) error {
client, err := amazon.NewClient(clusteropts.Profile, clusteropts.Cluster, clusteropts.Region)
if err != nil {
return err
}
if len(args) == 0 {
return errors.New("Missing mandatory parameter: ID")
}
id := args[0]
secret, err := client.InspectSecret(context.Background(), id)
if err != nil {
return err
}
out, err := secret.ToJSON()
if err != nil {
return err
}
fmt.Println(out)
return nil
},
}
return cmd
}
func ListSecrets(clusteropts *ClusterOptions) *cobra.Command {
cmd := &cobra.Command{
Use: "list",
Aliases: []string{"ls"},
Short: "List secrets stored for the existing account.",
RunE: func(cmd *cobra.Command, args []string) error {
client, err := amazon.NewClient(clusteropts.Profile, clusteropts.Cluster, clusteropts.Region)
if err != nil {
return err
}
secrets, err := client.ListSecrets(context.Background())
if err != nil {
return err
}
printList(os.Stdout, secrets)
return nil
},
}
return cmd
}
func DeleteSecret(clusteropts *ClusterOptions) *cobra.Command {
opts := deleteSecretOptions{}
cmd := &cobra.Command{
Use: "delete NAME",
Aliases: []string{"rm", "remove"},
Short: "Removes a secret.",
RunE: func(cmd *cobra.Command, args []string) error {
client, err := amazon.NewClient(clusteropts.Profile, clusteropts.Cluster, clusteropts.Region)
if err != nil {
return err
}
if len(args) == 0 {
return errors.New("Missing mandatory parameter: [NAME]")
}
return client.DeleteSecret(context.Background(), args[0], opts.recover)
},
}
cmd.Flags().BoolVar(&opts.recover, "recover", false, "Enable recovery.")
return cmd
}
func printList(out io.Writer, secrets []docker.Secret) {
printSection(out, len(secrets), func(w io.Writer) {
for _, secret := range secrets {
fmt.Fprintf(w, "%s\t%s\t%s\n", secret.ID, secret.Name, secret.Description)
}
}, "ID", "NAME", "DESCRIPTION")
}
func printSection(out io.Writer, len int, printer func(io.Writer), headers ...string) {
w := tabwriter.NewWriter(out, 20, 1, 3, ' ', 0)
fmt.Fprintln(w, strings.Join(headers, "\t"))
printer(w)
w.Flush()
}

58
ecs/cmd/main.go Normal file
View File

@ -0,0 +1,58 @@
package main
import (
"fmt"
"github.com/docker/cli/cli-plugins/manager"
"github.com/docker/cli/cli-plugins/plugin"
"github.com/docker/cli/cli/command"
commands "github.com/docker/ecs-plugin/cmd/commands"
"github.com/spf13/cobra"
)
const version = "0.0.1"
func main() {
plugin.Run(func(dockerCli command.Cli) *cobra.Command {
cmd := NewRootCmd("ecs", dockerCli)
return cmd
}, manager.Metadata{
SchemaVersion: "0.1.0",
Vendor: "Docker Inc.",
Version: version,
Experimental: true,
})
}
// NewRootCmd returns the base root command.
func NewRootCmd(name string, dockerCli command.Cli) *cobra.Command {
var opts commands.ClusterOptions
cmd := &cobra.Command{
Short: "Docker ECS",
Long: `run multi-container applications on Amazon ECS.`,
Use: name,
Annotations: map[string]string{"experimentalCLI": "true"},
}
cmd.AddCommand(
VersionCommand(),
commands.ComposeCommand(&opts),
commands.SecretCommand(&opts),
)
cmd.Flags().StringVarP(&opts.Profile, "profile", "p", "default", "AWS Profile")
cmd.Flags().StringVarP(&opts.Cluster, "cluster", "c", "default", "ECS cluster")
cmd.Flags().StringVarP(&opts.Region, "region", "r", "", "AWS region")
return cmd
}
func VersionCommand() *cobra.Command {
return &cobra.Command{
Use: "version",
Short: "Show version.",
RunE: func(cmd *cobra.Command, args []string) error {
fmt.Printf("Docker ECS plugin %s\n", version)
return nil
},
}
}

View File

@ -1,261 +0,0 @@
package main
import (
"context"
"errors"
"fmt"
"github.com/docker/cli/cli-plugins/manager"
"github.com/docker/cli/cli-plugins/plugin"
"github.com/docker/cli/cli/command"
"github.com/docker/ecs-plugin/pkg/amazon"
"github.com/docker/ecs-plugin/pkg/compose"
"github.com/spf13/cobra"
)
const version = "0.0.1"
func main() {
plugin.Run(func(dockerCli command.Cli) *cobra.Command {
cmd := NewRootCmd("ecs", dockerCli)
return cmd
}, manager.Metadata{
SchemaVersion: "0.1.0",
Vendor: "Docker Inc.",
Version: version,
Experimental: true,
})
}
type clusterOptions struct {
profile string
region string
cluster string
}
// NewRootCmd returns the base root command.
func NewRootCmd(name string, dockerCli command.Cli) *cobra.Command {
var opts clusterOptions
cmd := &cobra.Command{
Short: "Docker ECS",
Long: `run multi-container applications on Amazon ECS.`,
Use: name,
Annotations: map[string]string{"experimentalCLI": "true"},
}
cmd.AddCommand(
VersionCommand(),
ComposeCommand(&opts),
SecretCommand(&opts),
)
cmd.Flags().StringVarP(&opts.profile, "profile", "p", "default", "AWS Profile")
cmd.Flags().StringVarP(&opts.cluster, "cluster", "c", "default", "ECS cluster")
cmd.Flags().StringVarP(&opts.region, "region", "r", "", "AWS region")
return cmd
}
func VersionCommand() *cobra.Command {
return &cobra.Command{
Use: "version",
Short: "Show version.",
RunE: func(cmd *cobra.Command, args []string) error {
fmt.Printf("Docker ECS plugin %s\n", version)
return nil
},
}
}
func ComposeCommand(clusteropts *clusterOptions) *cobra.Command {
cmd := &cobra.Command{
Use: "compose",
}
opts := &compose.ProjectOptions{}
opts.AddFlags(cmd.Flags())
cmd.AddCommand(
ConvertCommand(clusteropts, opts),
UpCommand(clusteropts, opts),
DownCommand(clusteropts, opts),
)
return cmd
}
type upOptions struct {
loadBalancerArn string
}
func (o upOptions) LoadBalancerArn() *string {
if o.loadBalancerArn == "" {
return nil
}
return &o.loadBalancerArn
}
func ConvertCommand(clusteropts *clusterOptions, projectOpts *compose.ProjectOptions) *cobra.Command {
cmd := &cobra.Command{
Use: "convert",
RunE: compose.WithProject(projectOpts, func(project *compose.Project, args []string) error {
client, err := amazon.NewClient(clusteropts.profile, clusteropts.cluster, clusteropts.region)
if err != nil {
return err
}
template, err := client.Convert(context.Background(), project)
if err != nil {
return err
}
j, err := template.JSON()
if err != nil {
fmt.Printf("Failed to generate JSON: %s\n", err)
} else {
fmt.Printf("%s\n", string(j))
}
return nil
}),
}
return cmd
}
func UpCommand(clusteropts *clusterOptions, projectOpts *compose.ProjectOptions) *cobra.Command {
opts := upOptions{}
cmd := &cobra.Command{
Use: "up",
RunE: compose.WithProject(projectOpts, func(project *compose.Project, args []string) error {
client, err := amazon.NewClient(clusteropts.profile, clusteropts.cluster, clusteropts.region)
if err != nil {
return err
}
return client.ComposeUp(context.Background(), project)
}),
}
cmd.Flags().StringVar(&opts.loadBalancerArn, "load-balancer", "", "")
return cmd
}
type downOptions struct {
DeleteCluster bool
}
func DownCommand(clusteropts *clusterOptions, projectOpts *compose.ProjectOptions) *cobra.Command {
opts := downOptions{}
cmd := &cobra.Command{
Use: "down",
RunE: func(cmd *cobra.Command, args []string) error {
client, err := amazon.NewClient(clusteropts.profile, clusteropts.cluster, clusteropts.region)
if err != nil {
return err
}
if len(args) == 0 {
project, err := compose.ProjectFromOptions(projectOpts)
if err != nil {
return err
}
return client.ComposeDown(context.Background(), project.Name, opts.DeleteCluster)
}
// project names passed as parameters
for _, name := range args {
err := client.ComposeDown(context.Background(), name, opts.DeleteCluster)
if err != nil {
return err
}
}
return nil
},
}
cmd.Flags().BoolVar(&opts.DeleteCluster, "delete-cluster", false, "Delete cluster")
return cmd
}
func SecretCommand(clusteropts *clusterOptions) *cobra.Command {
cmd := &cobra.Command{
Use: "secret",
}
opts := &compose.ProjectOptions{}
opts.AddFlags(cmd.Flags())
cmd.AddCommand(
CreateSecret(clusteropts),
InspectSecret(clusteropts),
ListSecrets(clusteropts),
DeleteSecret(clusteropts),
)
return cmd
}
type createSecretOptions struct {
Label string
}
func CreateSecret(clusteropts *clusterOptions) *cobra.Command {
//opts := createSecretOptions{}
cmd := &cobra.Command{
Use: "create [NAME]",
RunE: func(cmd *cobra.Command, args []string) error {
client, err := amazon.NewClient(clusteropts.profile, clusteropts.cluster, clusteropts.region)
if err != nil {
return err
}
if len(args) == 0 {
return errors.New("Missing mandatory parameter: [NAME]")
}
name := args[0]
content := "blabla"
id, err := client.CreateSecret(context.Background(), name, content)
fmt.Println(id)
return err
},
}
//cmd.Flags().BoolVar(&opts.Label, "label", false, "Secret label")
return cmd
}
func InspectSecret(clusteropts *clusterOptions) *cobra.Command {
cmd := &cobra.Command{
Use: "inspect [NAME]",
RunE: func(cmd *cobra.Command, args []string) error {
client, err := amazon.NewClient(clusteropts.profile, clusteropts.cluster, clusteropts.region)
if err != nil {
return err
}
if len(args) == 0 {
return errors.New("Missing mandatory parameter: [NAME]")
}
name := args[0]
return client.InspectSecret(context.Background(), name)
},
}
return cmd
}
func ListSecrets(clusteropts *clusterOptions) *cobra.Command {
cmd := &cobra.Command{
Use: "list",
Aliases: []string{"ls"},
RunE: func(cmd *cobra.Command, args []string) error {
client, err := amazon.NewClient(clusteropts.profile, clusteropts.cluster, clusteropts.region)
if err != nil {
return err
}
return client.ListSecrets(context.Background())
},
}
return cmd
}
func DeleteSecret(clusteropts *clusterOptions) *cobra.Command {
cmd := &cobra.Command{
Use: "delete [NAME]",
RunE: func(cmd *cobra.Command, args []string) error {
client, err := amazon.NewClient(clusteropts.profile, clusteropts.cluster, clusteropts.region)
if err != nil {
return err
}
if len(args) == 0 {
return errors.New("Missing mandatory parameter: [NAME]")
}
return client.DeleteSecret(context.Background(), args[0])
},
}
return cmd
}

View File

@ -22,6 +22,8 @@ import (
"github.com/aws/aws-sdk-go/service/secretsmanager/secretsmanageriface"
cf "github.com/awslabs/goformation/v4/cloudformation"
"github.com/sirupsen/logrus"
"github.com/docker/ecs-plugin/pkg/docker"
)
type sdk struct {
@ -198,22 +200,68 @@ func (s sdk) DeleteStack(ctx context.Context, name string) error {
return err
}
func (s sdk) CreateSecret(ctx context.Context, name string, content string) (string, error) {
func (s sdk) CreateSecret(ctx context.Context, name string, secret string) (string, error) {
logrus.Debug("Create secret " + name)
return "test", nil
response, err := s.SM.CreateSecret(&secretsmanager.CreateSecretInput{Name: &name, SecretString: &secret})
if err != nil {
return "", err
}
return *response.ARN, nil
}
func (s sdk) InspectSecret(ctx context.Context, name string) error {
fmt.Printf("... done. \n")
return nil
func (s sdk) InspectSecret(ctx context.Context, id string) (docker.Secret, error) {
logrus.Debug("Inspect secret " + id)
response, err := s.SM.DescribeSecret(&secretsmanager.DescribeSecretInput{SecretId: &id})
if err != nil {
return docker.Secret{}, err
}
labels := map[string]string{}
for _, tag := range response.Tags {
labels[*tag.Key] = *tag.Value
}
secret := docker.Secret{
ID: *response.ARN,
Name: *response.Name,
Labels: labels,
}
if response.Description != nil {
secret.Description = *response.Description
}
return secret, nil
}
func (s sdk) ListSecrets(ctx context.Context) error {
fmt.Printf("... done. \n")
return nil
func (s sdk) ListSecrets(ctx context.Context) ([]docker.Secret, error) {
logrus.Debug("List secrets ...")
response, err := s.SM.ListSecrets(&secretsmanager.ListSecretsInput{})
if err != nil {
return []docker.Secret{}, err
}
var secrets []docker.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
}
secrets = append(secrets, docker.Secret{
ID: *sec.ARN,
Name: *sec.Name,
Labels: labels,
Description: description,
})
}
return secrets, nil
}
func (s sdk) DeleteSecret(ctx context.Context, name string) error {
fmt.Printf("... done. \n")
return 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
}

View File

@ -2,27 +2,29 @@ package amazon
import (
"context"
"github.com/docker/ecs-plugin/pkg/docker"
)
type secretsAPI interface {
CreateSecret(ctx context.Context, name string, content string) (string, error)
InspectSecret(ctx context.Context, name string) error
ListSecrets(ctx context.Context) error
DeleteSecret(ctx context.Context, name string) error
InspectSecret(ctx context.Context, id string) (docker.Secret, error)
ListSecrets(ctx context.Context) ([]docker.Secret, error)
DeleteSecret(ctx context.Context, id string, recover bool) error
}
func (c client) CreateSecret(ctx context.Context, name string, content string) (string, error) {
return c.api.CreateSecret(ctx, name, content)
}
func (c client) InspectSecret(ctx context.Context, name string) error {
return c.api.InspectSecret(ctx, name)
func (c client) InspectSecret(ctx context.Context, id string) (docker.Secret, error) {
return c.api.InspectSecret(ctx, id)
}
func (c client) ListSecrets(ctx context.Context) error {
func (c client) ListSecrets(ctx context.Context) ([]docker.Secret, error) {
return c.api.ListSecrets(ctx)
}
func (c client) DeleteSecret(ctx context.Context, name string) error {
return c.api.DeleteSecret(ctx, name)
func (c client) DeleteSecret(ctx context.Context, id string, recover bool) error {
return c.api.DeleteSecret(ctx, id, recover)
}

View File

@ -4,6 +4,7 @@ import (
"context"
"github.com/awslabs/goformation/v4/cloudformation"
"github.com/docker/ecs-plugin/pkg/docker"
)
type API interface {
@ -11,8 +12,8 @@ type API interface {
ComposeUp(ctx context.Context, project *Project) error
ComposeDown(ctx context.Context, projectName string, deleteCluster bool) error
CreateSecret(ctx context.Context, name string, content string) (string, error)
InspectSecret(ctx context.Context, name string) error
ListSecrets(ctx context.Context) error
DeleteSecret(ctx context.Context, name string) error
CreateSecret(ctx context.Context, name string, secret string) (string, error)
InspectSecret(ctx context.Context, id string) (docker.Secret, error)
ListSecrets(ctx context.Context) ([]docker.Secret, error)
DeleteSecret(ctx context.Context, id string, recover bool) error
}

20
ecs/pkg/docker/secret.go Normal file
View File

@ -0,0 +1,20 @@
package docker
import (
"encoding/json"
)
type Secret struct {
ID string `json:"ID"`
Name string `json:"Name"`
Labels map[string]string `json:"Labels"`
Description string `json:"Description"`
}
func (s Secret) ToJSON() (string, error) {
b, err := json.MarshalIndent(&s, "", "\t")
if err != nil {
return "", err
}
return string(b), nil
}