Skip to content

Commit

Permalink
feat(pipelines): pipelines ls --local flag support (#3417)
Browse files Browse the repository at this point in the history
By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the Apache 2.0 License.
  • Loading branch information
dannyrandall authored Apr 5, 2022
1 parent 7d94904 commit 5002f11
Show file tree
Hide file tree
Showing 9 changed files with 322 additions and 113 deletions.
1 change: 1 addition & 0 deletions internal/pkg/aws/codepipeline/codepipeline.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ type CodePipeline struct {

// Pipeline represents an existing CodePipeline resource.
type Pipeline struct {
// Name is the resource name of the pipeline in CodePipeline, e.g. myapp-mypipeline-RANDOMSTRING.
Name string `json:"pipelineName"`
Region string `json:"region"`
AccountID string `json:"accountId"`
Expand Down
1 change: 1 addition & 0 deletions internal/pkg/cli/flag.go
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,7 @@ Defaults to all logs. Only one of end-time / follow may be used.`
pipelineResourcesFlagDescription = "Optional. Show the resources in your pipeline."
localSvcFlagDescription = "Only show services in the workspace."
localJobFlagDescription = "Only show jobs in the workspace."
localPipelineFlagDescription = "Only show pipelines in the workspace."
deleteSecretFlagDescription = "Deletes AWS Secrets Manager secret associated with a pipeline source repository."
svcPortFlagDescription = "The port on which your service listens."

Expand Down
2 changes: 1 addition & 1 deletion internal/pkg/cli/job_init.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ func newInitJobOpts(vars initJobVars) (*initJobOpts, error) {
func (o *initJobOpts) Validate() error {
// If this app is pending creation, we'll skip validation.
if !o.wsPendingCreation {
if err := validateInputApp(o.wsAppName, o.appName, o.store); err != nil {
if err := validateWorkspaceApp(o.wsAppName, o.appName, o.store); err != nil {
return err
}
o.appName = o.wsAppName
Expand Down
5 changes: 2 additions & 3 deletions internal/pkg/cli/list/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,10 +135,9 @@ func filterByName(wklds []*config.Workload, wantedNames []string) []*config.Work
}
var filtered []*config.Workload
for _, wkld := range wklds {
if _, ok := isWanted[wkld.Name]; !ok {
continue
if isWanted[wkld.Name] {
filtered = append(filtered, wkld)
}
filtered = append(filtered, wkld)
}
return filtered
}
Expand Down
2 changes: 1 addition & 1 deletion internal/pkg/cli/pipeline_init.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ func (o *initPipelineOpts) Validate() error {
// Ask prompts for required fields that are not passed in and validates them.
func (o *initPipelineOpts) Ask() error {
// This command must be executed in the app's workspace because the pipeline manifest and buildspec will be created and stored.
if err := validateInputApp(o.wsAppName, o.appName, o.store); err != nil {
if err := validateWorkspaceApp(o.wsAppName, o.appName, o.store); err != nil {
return err
}
o.appName = o.wsAppName
Expand Down
224 changes: 185 additions & 39 deletions internal/pkg/cli/pipeline_list.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,26 @@
package cli

import (
"context"
"encoding/json"
"fmt"
"io"
"os"
"strings"
"sort"
"sync"
"time"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/ssm"
"github.com/aws/copilot-cli/internal/pkg/aws/identity"
rg "github.com/aws/copilot-cli/internal/pkg/aws/resourcegroups"
"github.com/aws/copilot-cli/internal/pkg/deploy"
"github.com/aws/copilot-cli/internal/pkg/describe"
"github.com/aws/copilot-cli/internal/pkg/workspace"
"golang.org/x/sync/errgroup"

"github.com/aws/copilot-cli/internal/pkg/aws/codepipeline"
"github.com/aws/copilot-cli/internal/pkg/aws/sessions"
"github.com/aws/copilot-cli/internal/pkg/config"
"github.com/aws/copilot-cli/internal/pkg/deploy"
"github.com/aws/copilot-cli/internal/pkg/term/prompt"
"github.com/aws/copilot-cli/internal/pkg/term/selector"
"github.com/spf13/cobra"
Expand All @@ -27,43 +32,71 @@ import (
const (
pipelineListAppNamePrompt = "Which application are the pipelines in?"
pipelineListAppNameHelper = "An application is a collection of related services."

pipelineListTimeout = 10 * time.Second
)

type listPipelineVars struct {
appName string
shouldOutputJSON bool
appName string
shouldOutputJSON bool
shouldShowLocalPipelines bool
}

type listPipelineOpts struct {
listPipelineVars
codepipeline pipelineGetter
pipelineLister deployedPipelineLister
prompt prompter
sel configSelector
store store
w io.Writer
workspace wsPipelineGetter
pipelineLister deployedPipelineLister

newDescriber newPipelineDescriberFunc

wsAppName string
}

type newPipelineDescriberFunc func(pipeline deploy.Pipeline) (describer, error)

func newListPipelinesOpts(vars listPipelineVars) (*listPipelineOpts, error) {
ws, err := workspace.New()
if err != nil {
return nil, err
}

defaultSession, err := sessions.ImmutableProvider(sessions.UserAgentExtras("pipeline ls")).Default()
if err != nil {
return nil, fmt.Errorf("default session: %w", err)
}

var wsAppName string
if vars.shouldShowLocalPipelines {
wsAppName = tryReadingAppName()
}

store := config.NewSSMStore(identity.New(defaultSession), ssm.New(defaultSession), aws.StringValue(defaultSession.Config.Region))
prompter := prompt.New()
return &listPipelineOpts{
listPipelineVars: vars,
codepipeline: codepipeline.New(defaultSession),
pipelineLister: deploy.NewPipelineStore(rg.New(defaultSession)),
prompt: prompter,
sel: selector.NewConfigSelect(prompter, store),
store: store,
w: os.Stdout,
workspace: ws,
newDescriber: func(pipeline deploy.Pipeline) (describer, error) {
return describe.NewPipelineDescriber(pipeline, false)
},
wsAppName: wsAppName,
}, nil
}

// Ask asks for and validates fields that are required but not passed in.
func (o *listPipelineOpts) Ask() error {
if o.shouldShowLocalPipelines {
return validateWorkspaceApp(o.wsAppName, o.appName, o.store)
}

if o.appName != "" {
if _, err := o.store.GetApplication(o.appName); err != nil {
return fmt.Errorf("validate application: %w", err)
Expand All @@ -75,60 +108,172 @@ func (o *listPipelineOpts) Ask() error {
}
o.appName = app
}

return nil
}

// Execute writes the pipelines.
func (o *listPipelineOpts) Execute() error {
var out string
pipelines, err := o.pipelineLister.ListDeployedPipelines(o.appName)
ctx, cancel := context.WithTimeout(context.Background(), pipelineListTimeout)
defer cancel()

switch {
case o.shouldShowLocalPipelines && o.shouldOutputJSON:
return o.jsonOutputLocal(ctx)
case o.shouldShowLocalPipelines:
return o.humanOutputLocal()
case o.shouldOutputJSON:
return o.jsonOutputDeployed(ctx)
}

return o.humanOutputDeployed()
}

// jsonOutputLocal prints data about all pipelines in the current workspace.
// If a local pipeline has been deployed, data from codepipeline is included.
func (o *listPipelineOpts) jsonOutputLocal(ctx context.Context) error {
local, err := o.workspace.ListPipelines()
if err != nil {
return fmt.Errorf("list deployed pipelines in application %s: %w", o.appName, err)
return err
}
if o.shouldOutputJSON {
var pipelineInfo []*codepipeline.Pipeline
for _, pipeline := range pipelines {
info, err := o.codepipeline.GetPipeline(pipeline.ResourceName)
if err != nil {
return fmt.Errorf("get pipeline info for %s: %w", pipeline.Name, err)
}
pipelineInfo = append(pipelineInfo, info)
}

data, err := o.jsonOutput(pipelineInfo)
if err != nil {
return err
deployed, err := getDeployedPipelines(ctx, o.appName, o.pipelineLister, o.newDescriber)
if err != nil {
return err
}

cp := make(map[string]*describe.Pipeline)
for _, pipeline := range deployed {
cp[pipeline.Name] = pipeline
}

type info struct {
Name string `json:"name"`
ManifestPath string `json:"manifestPath"`
PipelineName string `json:"pipelineName,omitempty"`
}

var out struct {
Pipelines []info `json:"pipelines"`
}
for _, pipeline := range local {
i := info{
Name: pipeline.Name,
ManifestPath: pipeline.Path,
}
out = data
} else {
var pipelineNames []string
for _, pipeline := range pipelines {
pipelineNames = append(pipelineNames, pipeline.Name)

if v, ok := cp[pipeline.Name]; ok {
i.PipelineName = v.Pipeline.Name
}
out = o.humanOutput(pipelineNames)

out.Pipelines = append(out.Pipelines, i)
}

b, err := json.Marshal(out)
if err != nil {
return fmt.Errorf("marshal pipelines: %w", err)
}
fmt.Fprint(o.w, out)

fmt.Fprintf(o.w, "%s\n", b)
return nil
}

func (o *listPipelineOpts) jsonOutput(pipelines []*codepipeline.Pipeline) (string, error) {
// humanOutputLocal prints the name of all pipelines in the current workspace.
func (o *listPipelineOpts) humanOutputLocal() error {
local, err := o.workspace.ListPipelines()
if err != nil {
return err
}

for _, pipeline := range local {
fmt.Fprintln(o.w, pipeline.Name)
}

return nil
}

// jsonOutputDeployed prints data about all pipelines in the given app that have been deployed.
func (o *listPipelineOpts) jsonOutputDeployed(ctx context.Context) error {
pipelines, err := getDeployedPipelines(ctx, o.appName, o.pipelineLister, o.newDescriber)
if err != nil {
return err
}

type serializedPipelines struct {
Pipelines []*codepipeline.Pipeline `json:"pipelines"`
Pipelines []*describe.Pipeline `json:"pipelines"`
}
b, err := json.Marshal(serializedPipelines{Pipelines: pipelines})
if err != nil {
return "", fmt.Errorf("marshal pipelines: %w", err)
return fmt.Errorf("marshal pipelines: %w", err)
}

fmt.Fprintf(o.w, "%s\n", b)
return nil
}

// humanOutputDeployed prints the name of all pipelines in the given app that have been deployed.
func (o *listPipelineOpts) humanOutputDeployed() error {
pipelines, err := o.pipelineLister.ListDeployedPipelines(o.appName)
if err != nil {
return fmt.Errorf("list deployed pipelines: %w", err)
}

sort.Slice(pipelines, func(i, j int) bool {
return pipelines[i].Name < pipelines[j].Name
})

for _, p := range pipelines {
fmt.Fprintln(o.w, p.Name)
}
return fmt.Sprintf("%s\n", b), nil

return nil
}

func (o *listPipelineOpts) humanOutput(pipelines []string) string {
b := &strings.Builder{}
for _, pipeline := range pipelines {
fmt.Fprintln(b, pipeline)
func getDeployedPipelines(ctx context.Context, app string, lister deployedPipelineLister, newDescriber newPipelineDescriberFunc) ([]*describe.Pipeline, error) {
pipelines, err := lister.ListDeployedPipelines(app)
if err != nil {
return nil, fmt.Errorf("list deployed pipelines: %w", err)
}

var mux sync.Mutex
var res []*describe.Pipeline

g, _ := errgroup.WithContext(ctx)

for i := range pipelines {
pipeline := pipelines[i]
g.Go(func() error {
d, err := newDescriber(pipeline)
if err != nil {
return fmt.Errorf("create pipeline describer for %q: %w", pipeline.ResourceName, err)
}

info, err := d.Describe()
if err != nil {
return fmt.Errorf("describe pipeline %q: %w", pipeline.ResourceName, err)
}

p, ok := info.(*describe.Pipeline)
if !ok {
return fmt.Errorf("unexpected describer for %q: %T", pipeline.ResourceName, info)
}

mux.Lock()
defer mux.Unlock()
res = append(res, p)
return nil
})
}

if err := g.Wait(); err != nil {
return nil, err
}
return b.String()

sort.Slice(res, func(i, j int) bool {
return res[i].Name < res[j].Name
})

return res, nil
}

// buildPipelineListCmd builds the command for showing a list of all deployed pipelines.
Expand All @@ -154,5 +299,6 @@ func buildPipelineListCmd() *cobra.Command {

cmd.Flags().StringVarP(&vars.appName, appFlag, appFlagShort, tryReadingAppName(), appFlagDescription)
cmd.Flags().BoolVar(&vars.shouldOutputJSON, jsonFlag, false, jsonFlagDescription)
cmd.Flags().BoolVar(&vars.shouldShowLocalPipelines, localFlag, false, localPipelineFlagDescription)
return cmd
}
Loading

0 comments on commit 5002f11

Please sign in to comment.