diff --git a/app/di/wire.go b/app/di/wire.go index edec966..b18e095 100644 --- a/app/di/wire.go +++ b/app/di/wire.go @@ -18,6 +18,8 @@ import ( type S3App struct { // S3BucketCreator is the usecase for creating a new S3 bucket. S3BucketCreator usecase.S3BucketCreator + // S3BucketLister is the usecase for listing S3 buckets. + S3BucketLister usecase.S3BucketLister } // NewS3App creates a new S3App. @@ -26,14 +28,18 @@ func NewS3App(ctx context.Context, profile model.AWSProfile, region model.Region model.NewAWSConfig, external.NewS3Client, external.S3BucketCreatorSet, + external.S3BucketListerSet, + external.S3BucketLocationGetterSet, interactor.S3bucketCreatorSet, + interactor.S3bucketListerSet, newS3App, ) return nil, nil } -func newS3App(s3bucketCreator usecase.S3BucketCreator) *S3App { +func newS3App(s3bucketCreator usecase.S3BucketCreator, s3bucketLister usecase.S3BucketLister) *S3App { return &S3App{ S3BucketCreator: s3bucketCreator, + S3BucketLister: s3bucketLister, } } diff --git a/app/di/wire_gen.go b/app/di/wire_gen.go index a419816..587a98f 100644 --- a/app/di/wire_gen.go +++ b/app/di/wire_gen.go @@ -28,7 +28,10 @@ func NewS3App(ctx context.Context, profile model.AWSProfile, region model.Region } s3BucketCreator := external.NewS3BucketCreator(client) interactorS3BucketCreator := interactor.NewS3BucketCreator(s3BucketCreator) - s3App := newS3App(interactorS3BucketCreator) + s3BucketLister := external.NewS3BucketLister(client) + s3BucketLocationGetter := external.NewS3BucketLocationGetter(client) + interactorS3BucketLister := interactor.NewS3BucketLister(s3BucketLister, s3BucketLocationGetter) + s3App := newS3App(interactorS3BucketCreator, interactorS3BucketLister) return s3App, nil } @@ -38,10 +41,13 @@ func NewS3App(ctx context.Context, profile model.AWSProfile, region model.Region type S3App struct { // S3BucketCreator is the usecase for creating a new S3 bucket. S3BucketCreator usecase.S3BucketCreator + // S3BucketLister is the usecase for listing S3 buckets. + S3BucketLister usecase.S3BucketLister } -func newS3App(s3bucketCreator usecase.S3BucketCreator) *S3App { +func newS3App(s3bucketCreator usecase.S3BucketCreator, s3bucketLister usecase.S3BucketLister) *S3App { return &S3App{ S3BucketCreator: s3bucketCreator, + S3BucketLister: s3bucketLister, } } diff --git a/app/domain/model/s3.go b/app/domain/model/s3.go index e48e493..fd1bc27 100644 --- a/app/domain/model/s3.go +++ b/app/domain/model/s3.go @@ -4,6 +4,7 @@ package model import ( "fmt" "strings" + "time" "github.com/nao1215/rainbow/utils/errfmt" "github.com/nao1215/rainbow/utils/xregex" @@ -219,3 +220,27 @@ func (b Bucket) validateCharSequence() error { } return nil } + +// BucketSets is the set of the BucketSet. +type BucketSets []BucketSet + +// BucketSet is the set of the Bucket and the Region. +type BucketSet struct { + // Bucket is the name of the S3 bucket. + Bucket Bucket + // Region is the name of the AWS region. + Region Region + // CreationDate is date the bucket was created. + // This date can change when making changes to your bucket, such as editing its bucket policy. + CreationDate time.Time +} + +// Len returns the length of the BucketSets. +func (b BucketSets) Len() int { + return len(b) +} + +// Empty returns true if the BucketSets is empty. +func (b BucketSets) Empty() bool { + return b.Len() == 0 +} diff --git a/app/domain/service/s3.go b/app/domain/service/s3.go index 0ee4fef..0f3b341 100644 --- a/app/domain/service/s3.go +++ b/app/domain/service/s3.go @@ -22,3 +22,33 @@ type S3BucketCreatorOutput struct{} type S3BucketCreator interface { CreateBucket(ctx context.Context, input *S3BucketCreatorInput) (*S3BucketCreatorOutput, error) } + +// S3BucketListerInput is the input of the ListBuckets method. +type S3BucketListerInput struct{} + +// S3BucketListerOutput is the output of the ListBuckets method. +type S3BucketListerOutput struct { + // Buckets is the list of the buckets. + Buckets model.BucketSets +} + +// S3BucketLister is the interface that wraps the basic ListBuckets method. +type S3BucketLister interface { + ListBuckets(ctx context.Context, input *S3BucketListerInput) (*S3BucketListerOutput, error) +} + +// S3BucketLocationGetterInput is the input of the GetBucketLocation method. +type S3BucketLocationGetterInput struct { + Bucket model.Bucket +} + +// S3BucketLocationGetterOutput is the output of the GetBucketLocation method. +type S3BucketLocationGetterOutput struct { + // Region is the region of the bucket. + Region model.Region +} + +// S3BucketLocationGetter is the interface that wraps the basic GetBucketLocation method. +type S3BucketLocationGetter interface { + GetBucketLocation(ctx context.Context, input *S3BucketLocationGetterInput) (*S3BucketLocationGetterOutput, error) +} diff --git a/app/external/s3.go b/app/external/s3.go index 06ae449..c4f3347 100644 --- a/app/external/s3.go +++ b/app/external/s3.go @@ -60,3 +60,79 @@ func (c *S3BucketCreator) CreateBucket(ctx context.Context, input *service.S3Buc } return &service.S3BucketCreatorOutput{}, nil } + +// S3BucketLister implements the S3BucketLister interface. +type S3BucketLister struct { + client *s3.Client +} + +// S3BucketListerSet is a provider set for S3BucketLister. +// +//nolint:gochecknoglobals +var S3BucketListerSet = wire.NewSet( + NewS3BucketLister, + wire.Bind(new(service.S3BucketLister), new(*S3BucketLister)), +) + +var _ service.S3BucketLister = (*S3BucketLister)(nil) + +// NewS3BucketLister creates a new S3BucketLister. +func NewS3BucketLister(client *s3.Client) *S3BucketLister { + return &S3BucketLister{client: client} +} + +// ListBuckets lists the buckets. +func (c *S3BucketLister) ListBuckets(ctx context.Context, _ *service.S3BucketListerInput) (*service.S3BucketListerOutput, error) { + out, err := c.client.ListBuckets(ctx, &s3.ListBucketsInput{}) + if err != nil { + return nil, err + } + + var buckets model.BucketSets + for _, b := range out.Buckets { + buckets = append(buckets, model.BucketSet{ + Bucket: model.Bucket(*b.Name), + CreationDate: *b.CreationDate, + }) + } + return &service.S3BucketListerOutput{Buckets: buckets}, nil +} + +// S3BucketLocationGetter implements the S3BucketLocationGetter interface. +type S3BucketLocationGetter struct { + client *s3.Client +} + +// S3BucketLocationGetterSet is a provider set for S3BucketLocationGetter. +// +//nolint:gochecknoglobals +var S3BucketLocationGetterSet = wire.NewSet( + NewS3BucketLocationGetter, + wire.Bind(new(service.S3BucketLocationGetter), new(*S3BucketLocationGetter)), +) + +var _ service.S3BucketLocationGetter = (*S3BucketLocationGetter)(nil) + +// NewS3BucketLocationGetter creates a new S3BucketLocationGetter. +func NewS3BucketLocationGetter(client *s3.Client) *S3BucketLocationGetter { + return &S3BucketLocationGetter{client: client} +} + +// GetBucketLocation gets the location of the bucket. +func (c *S3BucketLocationGetter) GetBucketLocation(ctx context.Context, input *service.S3BucketLocationGetterInput) (*service.S3BucketLocationGetterOutput, error) { + out, err := c.client.GetBucketLocation(ctx, &s3.GetBucketLocationInput{ + Bucket: aws.String(input.Bucket.String()), + }) + if err != nil { + return nil, err + } + + region := model.Region(out.LocationConstraint) + if region == "" { + region = model.RegionUSEast1 + } + + return &service.S3BucketLocationGetterOutput{ + Region: region, + }, nil +} diff --git a/app/interactor/s3.go b/app/interactor/s3.go index ec2ffe1..d573f46 100644 --- a/app/interactor/s3.go +++ b/app/interactor/s3.go @@ -49,3 +49,50 @@ func (s *S3BucketCreator) CreateBucket(ctx context.Context, input *usecase.S3Buc } return &usecase.S3BucketCreatorOutput{}, nil } + +// S3bucketListerSet is a provider set for S3BucketLister. +// +//nolint:gochecknoglobals +var S3bucketListerSet = wire.NewSet( + NewS3BucketLister, + wire.Bind(new(usecase.S3BucketLister), new(*S3BucketLister)), +) + +var _ usecase.S3BucketLister = (*S3BucketLister)(nil) + +// S3BucketLister implements the S3BucketLister interface. +type S3BucketLister struct { + service.S3BucketLister + service.S3BucketLocationGetter +} + +// NewS3BucketLister creates a new S3BucketLister. +func NewS3BucketLister(l service.S3BucketLister, g service.S3BucketLocationGetter) *S3BucketLister { + return &S3BucketLister{ + S3BucketLister: l, + S3BucketLocationGetter: g, + } +} + +// ListBuckets lists the buckets. +func (s *S3BucketLister) ListBuckets(ctx context.Context, _ *usecase.S3BucketListerInput) (*usecase.S3BucketListerOutput, error) { + out, err := s.S3BucketLister.ListBuckets(ctx, &service.S3BucketListerInput{}) + if err != nil { + return nil, err + } + + for i, b := range out.Buckets { + in := service.S3BucketLocationGetterInput{ + Bucket: b.Bucket, + } + o, err := s.S3BucketLocationGetter.GetBucketLocation(ctx, &in) + if err != nil { + return nil, err + } + out.Buckets[i].Region = o.Region + } + + return &usecase.S3BucketListerOutput{ + Buckets: out.Buckets, + }, nil +} diff --git a/app/usecase/s3.go b/app/usecase/s3.go index af9389d..e565bd3 100644 --- a/app/usecase/s3.go +++ b/app/usecase/s3.go @@ -22,3 +22,17 @@ type S3BucketCreatorOutput struct{} type S3BucketCreator interface { CreateBucket(ctx context.Context, input *S3BucketCreatorInput) (*S3BucketCreatorOutput, error) } + +// S3BucketListerInput is the input of the ListBuckets method. +type S3BucketListerInput struct{} + +// S3BucketListerOutput is the output of the ListBuckets method. +type S3BucketListerOutput struct { + // Buckets is the list of the buckets. + Buckets model.BucketSets +} + +// S3BucketLister is the interface that wraps the basic ListBuckets method. +type S3BucketLister interface { + ListBuckets(ctx context.Context, input *S3BucketListerInput) (*S3BucketListerOutput, error) +} diff --git a/cmd/subcmd/common.go b/cmd/subcmd/common.go new file mode 100644 index 0000000..9d5e2a5 --- /dev/null +++ b/cmd/subcmd/common.go @@ -0,0 +1,22 @@ +package subcmd + +import "github.com/spf13/cobra" + +// Doer is an interface that represents the behavior of a command. +type Doer interface { + Do() error +} + +// SubCommand is an interface that represents the behavior of a command. +type SubCommand interface { + Parse(cmd *cobra.Command, args []string) error + Doer +} + +// Run runs the subcommand. +func Run(cmd *cobra.Command, args []string, subCmd SubCommand) error { + if err := subCmd.Parse(cmd, args); err != nil { + return err + } + return subCmd.Do() +} diff --git a/cmd/subcmd/s3hub/common.go b/cmd/subcmd/s3hub/common.go new file mode 100644 index 0000000..f08ec3c --- /dev/null +++ b/cmd/subcmd/s3hub/common.go @@ -0,0 +1,75 @@ +package s3hub + +import ( + "context" + + "github.com/nao1215/rainbow/app/di" + "github.com/nao1215/rainbow/app/domain/model" + "github.com/nao1215/rainbow/utils/errfmt" + "github.com/spf13/cobra" +) + +// s3hub have common fields and methods for s3hub commands. +type s3hub struct { + // S3App is the application service for S3. + *di.S3App + // command is the cobra command. + command *cobra.Command + // ctx is the context of s3hub command. + ctx context.Context + // profile is the AWS profile name. + profile model.AWSProfile + // region is the AWS region name. + region model.Region +} + +// newS3hub returns a new s3hub. +func newS3hub() *s3hub { + return &s3hub{} +} + +// parse parses command line arguments. +func (s *s3hub) parse(cmd *cobra.Command) error { + s.command = cmd + s.ctx = context.Background() + + p, err := cmd.Flags().GetString("profile") + if err != nil { + return err + } + s.profile = model.NewAWSProfile(p) + + r, err := cmd.Flags().GetString("region") + if err != nil { + return err + } + s.region = model.Region(r) + + cfg, err := model.NewAWSConfig(s.ctx, s.profile, s.region) + if err != nil { + return errfmt.Wrap(err, "can not get aws config") + } + if s.region == "" { + if cfg.Config.Region == "" { + s.region = model.RegionUSEast1 + } else { + s.region = model.Region(cfg.Config.Region) + } + } + + s.S3App, err = di.NewS3App(s.ctx, s.profile, s.region) + if err != nil { + return errfmt.Wrap(err, "can not create s3 application service") + } + return nil +} + +// printf prints a formatted string. +func (s *s3hub) printf(format string, a ...interface{}) { + s.command.Printf(format, a...) +} + +// commandName returns the s3hub command name. +func commandName() string { + return "s3hub" +} diff --git a/cmd/subcmd/s3hub/ls.go b/cmd/subcmd/s3hub/ls.go index 2e33120..ac2f9fd 100644 --- a/cmd/subcmd/s3hub/ls.go +++ b/cmd/subcmd/s3hub/ls.go @@ -1,18 +1,63 @@ package s3hub -import "github.com/spf13/cobra" +import ( + "github.com/fatih/color" + "github.com/nao1215/rainbow/app/domain/model" + "github.com/nao1215/rainbow/app/usecase" + "github.com/nao1215/rainbow/cmd/subcmd" + "github.com/spf13/cobra" +) // newLsCmd return ls command. func newLsCmd() *cobra.Command { - return &cobra.Command{ - Use: "ls", - Short: "List S3 buckets", - RunE: ls, + cmd := &cobra.Command{ + Use: "ls [flags] [BUCKET_NAME]", + Short: "List S3 buckets or contents of a bucket", + Example: ` s3hub ls -p myprofile -r us-east-1`, + RunE: func(cmd *cobra.Command, args []string) error { + return subcmd.Run(cmd, args, &lsCmd{}) + }, } + cmd.Flags().StringP("profile", "p", "", "AWS profile name. if this is empty, use $AWS_PROFILE") + // not used, however, this is common flag. + cmd.Flags().StringP("region", "r", "", "AWS region name. if this is empty, use us-east-1") + return cmd } -// ls is the entrypoint of ls command. -func ls(cmd *cobra.Command, _ []string) error { - cmd.Println("ls is not implemented yet") +// lsCmd is the command for ls. +type lsCmd struct { + // s3hub have common fields and methods for s3hub commands. + *s3hub + // bucket is the name of the bucket. + bucket model.Bucket +} + +// Parse parses command line arguments. +func (l *lsCmd) Parse(cmd *cobra.Command, args []string) error { + if len(args) >= 1 { + l.bucket = model.Bucket(args[0]) + } + + l.s3hub = newS3hub() + return l.s3hub.parse(cmd) +} + +func (l *lsCmd) Do() error { + out, err := l.s3hub.S3BucketLister.ListBuckets(l.ctx, &usecase.S3BucketListerInput{}) + if err != nil { + return err + } + + l.printf("[Buckets (profile=%s)]\n", l.profile.String()) + if len(out.Buckets) == 0 { + l.printf(" No Buckets\n") + return nil + } + for _, b := range out.Buckets { + l.printf(" %s (region=%s, updated_at=%s)\n", + color.GreenString("%s", b.Bucket), + color.YellowString("%s", b.Region), + b.CreationDate.Format("2006-01-02 15:04:05 MST")) + } return nil } diff --git a/cmd/subcmd/s3hub/mb.go b/cmd/subcmd/s3hub/mb.go index baa9af0..b0a53d2 100644 --- a/cmd/subcmd/s3hub/mb.go +++ b/cmd/subcmd/s3hub/mb.go @@ -1,13 +1,12 @@ package s3hub import ( - "context" "errors" "github.com/fatih/color" - "github.com/nao1215/rainbow/app/di" "github.com/nao1215/rainbow/app/domain/model" "github.com/nao1215/rainbow/app/usecase" + "github.com/nao1215/rainbow/cmd/subcmd" "github.com/nao1215/rainbow/utils/errfmt" "github.com/spf13/cobra" ) @@ -18,7 +17,9 @@ func newMbCmd() *cobra.Command { Use: "mb [flags] BUCKET_NAME", Short: "Make S3 bucket", Example: " s3hub mb -p myprofile -r us-east-1 BUCKET_NAME", - RunE: mb, + RunE: func(cmd *cobra.Command, args []string) error { + return subcmd.Run(cmd, args, &mbCmd{}) + }, } cmd.Flags().StringP("profile", "p", "", "AWS profile name. if this is empty, use $AWS_PROFILE") cmd.Flags().StringP("region", "r", "", "AWS region name. if this is empty, use us-east-1") @@ -27,87 +28,36 @@ func newMbCmd() *cobra.Command { // mbCmd is the command for mb. type mbCmd struct { - *cobra.Command - // ctx is the context of mb command. - ctx context.Context - // app is the application service for S3. - app *di.S3App - // profile is the AWS profile name. - profile model.AWSProfile - // region is the AWS region name. - region model.Region + // s3hub have common fields and methods for s3hub commands. + *s3hub // bucket is the name of the bucket to create. bucket model.Bucket } -// mb is the entrypoint of mb command. -func mb(cmd *cobra.Command, args []string) error { - mb, err := parse(cmd, args) - if err != nil { - return err - } - return mb.do() -} - -// parse parses command line arguments. -func parse(cmd *cobra.Command, args []string) (*mbCmd, error) { +// Parse parses command line arguments. +func (m *mbCmd) Parse(cmd *cobra.Command, args []string) error { if len(args) != 1 { - return nil, errors.New("you must specify a bucket name") - } - - ctx := context.Background() - p, err := cmd.Flags().GetString("profile") - if err != nil { - return nil, errfmt.Wrap(err, "can not parse command line argument (--profile)") - } - profile := model.NewAWSProfile(p) - - r, err := cmd.Flags().GetString("region") - if err != nil { - return nil, errfmt.Wrap(err, "can not parse command line argument (--region)") - } - region := model.Region(r) - - cfg, err := model.NewAWSConfig(ctx, profile, region) - if err != nil { - return nil, errfmt.Wrap(err, "can not get aws config") - } - if region == "" { - if cfg.Config.Region == "" { - region = model.RegionUSEast1 - } else { - region = model.Region(cfg.Config.Region) - } - } - - app, err := di.NewS3App(ctx, profile, region) - if err != nil { - return nil, errfmt.Wrap(err, "can not create s3 application service") + return errors.New("you must specify a bucket name") } + m.bucket = model.Bucket(args[0]) - return &mbCmd{ - Command: cmd, - ctx: ctx, - app: app, - profile: profile, - region: region, - bucket: model.Bucket(args[0]), - }, nil + m.s3hub = newS3hub() + return m.s3hub.parse(cmd) } -// do executes mb command. -func (mb *mbCmd) do() error { - _, err := mb.app.S3BucketCreator.CreateBucket(mb.ctx, &usecase.S3BucketCreatorInput{ - Bucket: mb.bucket, - Region: mb.region, +// Do executes mb command. +func (m *mbCmd) Do() error { + _, err := m.S3BucketCreator.CreateBucket(m.ctx, &usecase.S3BucketCreatorInput{ + Bucket: m.bucket, + Region: m.region, }) if err != nil { return errfmt.Wrap(err, "can not create bucket") } - mb.Printf("[Success]\n") - mb.Printf(" profile: %s\n", mb.profile.String()) - mb.Printf(" region : %s\n", mb.region) - mb.Printf(" bucket : %s\n", color.YellowString("%s", mb.bucket)) + m.printf("[Success]\n") + m.printf(" profile: %s\n", m.profile.String()) + m.printf(" region : %s\n", m.region) + m.printf(" bucket : %s\n", color.YellowString("%s", m.bucket)) return nil } diff --git a/cmd/subcmd/s3hub/root.go b/cmd/subcmd/s3hub/root.go index e89a5ec..be6077c 100644 --- a/cmd/subcmd/s3hub/root.go +++ b/cmd/subcmd/s3hub/root.go @@ -12,7 +12,10 @@ func Execute() error { if len(os.Args) == 1 { return interactive() } - return newRootCmd().Execute() + if err := newRootCmd().Execute(); err != nil { + return err + } + return nil } // newRootCmd returns a root command for s3hub. @@ -32,11 +35,5 @@ If you want to use interactive mode, run s3hub without any arguments.`, cmd.AddCommand(newLsCmd()) cmd.AddCommand(newRmCmd()) cmd.AddCommand(newCpCmd()) - return cmd } - -// commandName returns the s3hub command name. -func commandName() string { - return "s3hub" -}