-
Notifications
You must be signed in to change notification settings - Fork 13
Add workflow list command #397
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
dd8f379
5e94224
d217a76
7ade8db
2896754
5f8e4f4
9f53ed7
82d1a32
a8e07c5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,99 @@ | ||
| package list | ||
|
|
||
| import ( | ||
| "context" | ||
| "fmt" | ||
|
|
||
| "github.com/spf13/cobra" | ||
|
|
||
| "github.com/smartcontractkit/cre-cli/internal/client/graphqlclient" | ||
| "github.com/smartcontractkit/cre-cli/internal/credentials" | ||
| "github.com/smartcontractkit/cre-cli/internal/runtime" | ||
| "github.com/smartcontractkit/cre-cli/internal/tenantctx" | ||
| "github.com/smartcontractkit/cre-cli/internal/ui" | ||
| ) | ||
|
|
||
| // Handler loads workflows via the list client and prints them. | ||
| type Handler struct { | ||
| credentials *credentials.Credentials | ||
| tenantCtx *tenantctx.EnvironmentContext | ||
| gql Executor | ||
| } | ||
|
|
||
| // NewHandler builds a handler with the real GraphQL executor. | ||
| func NewHandler(ctx *runtime.Context) *Handler { | ||
| return NewHandlerWithClient(ctx, nil) | ||
| } | ||
|
|
||
| // NewHandlerWithClient builds a handler with an optional GraphQL executor (nil uses graphqlclient.New). | ||
| func NewHandlerWithClient(ctx *runtime.Context, gql Executor) *Handler { | ||
| if gql == nil { | ||
| gql = graphqlclient.New(ctx.Credentials, ctx.EnvironmentSet, ctx.Logger) | ||
| } | ||
| return &Handler{ | ||
| credentials: ctx.Credentials, | ||
| tenantCtx: ctx.TenantContext, | ||
| gql: gql, | ||
| } | ||
| } | ||
|
|
||
| // Execute lists workflows, optionally filtering by registry ID from user context. | ||
| // Deleted workflows are omitted unless includeDeleted is true. | ||
| func (h *Handler) Execute(ctx context.Context, registryFilter string, includeDeleted bool) error { | ||
| if h.tenantCtx == nil { | ||
| return fmt.Errorf("user context not available — run `cre login` and retry") | ||
| } | ||
|
|
||
| if h.credentials == nil { | ||
| return fmt.Errorf("credentials not available — run `cre login` and retry") | ||
| } | ||
|
|
||
| if registryFilter != "" { | ||
| if findRegistry(h.tenantCtx.Registries, registryFilter) == nil { | ||
| return fmt.Errorf("registry %q not found in user context; available: [%s]", | ||
| registryFilter, availableRegistryIDs(h.tenantCtx.Registries)) | ||
| } | ||
| } | ||
|
|
||
| spinner := ui.NewSpinner() | ||
| spinner.Start("Listing workflows...") | ||
| rows, err := ListAll(ctx, h.gql, DefaultPageSize) | ||
| spinner.Stop() | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| if registryFilter != "" { | ||
| reg := findRegistry(h.tenantCtx.Registries, registryFilter) | ||
| rows = filterRowsByRegistry(rows, reg, h.tenantCtx.Registries) | ||
| } | ||
|
|
||
| afterRegistryFilter := len(rows) | ||
| if !includeDeleted { | ||
| rows = omitDeleted(rows) | ||
| } | ||
|
|
||
| printWorkflowTable(rows, h.tenantCtx.Registries, afterRegistryFilter, includeDeleted) | ||
| return nil | ||
| } | ||
|
|
||
| // New returns the cobra command. | ||
| func New(runtimeContext *runtime.Context) *cobra.Command { | ||
| var registryID string | ||
| var includeDeleted bool | ||
|
|
||
| cmd := &cobra.Command{ | ||
| Use: "list", | ||
| Short: "Lists workflows deployed for your organization", | ||
| Long: `Lists workflows across registries in your organization. Requires authentication and user context. Deleted workflows are hidden by default.`, | ||
| Example: "cre workflow list\n cre workflow list --registry private\n cre workflow list --include-deleted", | ||
| Args: cobra.NoArgs, | ||
| RunE: func(cmd *cobra.Command, args []string) error { | ||
| return NewHandler(runtimeContext).Execute(cmd.Context(), registryID, includeDeleted) | ||
| }, | ||
| } | ||
|
|
||
| cmd.Flags().StringVar(®istryID, "registry", "", "Filter by registry ID from user context") | ||
| cmd.Flags().BoolVar(&includeDeleted, "include-deleted", false, "Include workflows in DELETED status") | ||
| return cmd | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,100 @@ | ||
| package list | ||
|
|
||
| import ( | ||
| "context" | ||
| "fmt" | ||
|
|
||
| "github.com/machinebox/graphql" | ||
| ) | ||
|
|
||
| const DefaultPageSize = 100 | ||
|
|
||
| // Workflow is a workflow row from the platform list API, decoupled from transport JSON. | ||
| type Workflow struct { | ||
| Name string | ||
| WorkflowID string | ||
| OwnerAddress string | ||
| Status string | ||
| WorkflowSource string | ||
| } | ||
|
|
||
| const listWorkflowsQuery = ` | ||
| query ListWorkflows($input: WorkflowsInput!) { | ||
| workflows(input: $input) { | ||
| data { | ||
| name | ||
| workflowId | ||
| ownerAddress | ||
| status | ||
| workflowSource | ||
| } | ||
| count | ||
| } | ||
| } | ||
| ` | ||
|
|
||
| // Executor runs a GraphQL request (e.g. graphqlclient.Client). | ||
| type Executor interface { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think more natural name would be something like |
||
| Execute(ctx context.Context, req *graphql.Request, resp any) error | ||
| } | ||
|
|
||
| type gqlWorkflow struct { | ||
| Name string `json:"name"` | ||
| WorkflowID string `json:"workflowId"` | ||
| OwnerAddress string `json:"ownerAddress"` | ||
| Status string `json:"status"` | ||
| WorkflowSource string `json:"workflowSource"` | ||
| } | ||
|
|
||
| type listWorkflowsEnvelope struct { | ||
| Workflows struct { | ||
| Data []gqlWorkflow `json:"data"` | ||
| Count int `json:"count"` | ||
| } `json:"workflows"` | ||
| } | ||
|
|
||
| // ListAll pages through ListWorkflows and returns the aggregated workflows. | ||
| func ListAll(ctx context.Context, exec Executor, pageSize int) ([]Workflow, error) { | ||
| if pageSize <= 0 { | ||
| pageSize = DefaultPageSize | ||
| } | ||
|
|
||
| var total int | ||
| all := make([]Workflow, 0) | ||
|
|
||
| for pageNum := 0; ; pageNum++ { | ||
| req := graphql.NewRequest(listWorkflowsQuery) | ||
| req.Var("input", map[string]any{ | ||
| "page": map[string]any{ | ||
| "number": pageNum, | ||
| "size": pageSize, | ||
| }, | ||
| }) | ||
|
|
||
| var env listWorkflowsEnvelope | ||
| if err := exec.Execute(ctx, req, &env); err != nil { | ||
| return nil, fmt.Errorf("list workflows: %w", err) | ||
| } | ||
|
|
||
| if pageNum == 0 { | ||
| total = env.Workflows.Count | ||
| } | ||
|
|
||
| batch := env.Workflows.Data | ||
| for _, g := range batch { | ||
| all = append(all, Workflow{ | ||
| Name: g.Name, | ||
| WorkflowID: g.WorkflowID, | ||
| OwnerAddress: g.OwnerAddress, | ||
| Status: g.Status, | ||
| WorkflowSource: g.WorkflowSource, | ||
| }) | ||
| } | ||
|
|
||
| if len(all) >= total || len(batch) == 0 { | ||
| break | ||
| } | ||
| } | ||
|
|
||
| return all, nil | ||
| } | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What about using |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,83 @@ | ||
| package list_test | ||
|
|
||
| import ( | ||
| "context" | ||
| "encoding/json" | ||
| "testing" | ||
|
|
||
| "github.com/machinebox/graphql" | ||
|
|
||
| cmdlist "github.com/smartcontractkit/cre-cli/cmd/workflow/list" | ||
| ) | ||
|
|
||
| type listAllSeqExecutor struct { | ||
| call int | ||
| } | ||
|
|
||
| func (s *listAllSeqExecutor) Execute(ctx context.Context, req *graphql.Request, resp any) error { | ||
| s.call++ | ||
| var body []byte | ||
| var err error | ||
| switch s.call { | ||
| case 1: | ||
| data := make([]map[string]string, cmdlist.DefaultPageSize) | ||
| for i := range data { | ||
| data[i] = map[string]string{ | ||
| "name": "mock-wf-page", | ||
| "workflowId": "a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0", | ||
| "ownerAddress": "b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0", | ||
| "status": "ACTIVE", | ||
| "workflowSource": "contract:77766655544433322211:0xfeedface00000000000000000000000000c0ffee", | ||
| } | ||
| } | ||
| body, err = json.Marshal(map[string]any{ | ||
| "workflows": map[string]any{ | ||
| "count": cmdlist.DefaultPageSize + 1, | ||
| "data": data, | ||
| }, | ||
| }) | ||
| case 2: | ||
| body, err = json.Marshal(map[string]any{ | ||
| "workflows": map[string]any{ | ||
| "count": cmdlist.DefaultPageSize + 1, | ||
| "data": []map[string]string{ | ||
| { | ||
| "name": "mock-wf-last", | ||
| "workflowId": "c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0", | ||
| "ownerAddress": "b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0", | ||
| "status": "ACTIVE", | ||
| "workflowSource": "private", | ||
| }, | ||
| }, | ||
| }, | ||
| }) | ||
| default: | ||
| body, err = json.Marshal(map[string]any{ | ||
| "workflows": map[string]any{"count": cmdlist.DefaultPageSize + 1, "data": []any{}}, | ||
| }) | ||
| } | ||
| if err != nil { | ||
| return err | ||
| } | ||
| return json.Unmarshal(body, resp) | ||
| } | ||
|
|
||
| func TestListAll_PaginatesAndMapsRows(t *testing.T) { | ||
| ex := &listAllSeqExecutor{} | ||
| got, err := cmdlist.ListAll(context.Background(), ex, cmdlist.DefaultPageSize) | ||
| if err != nil { | ||
| t.Fatal(err) | ||
| } | ||
| if len(got) != cmdlist.DefaultPageSize+1 { | ||
| t.Fatalf("got %d workflows, want %d", len(got), cmdlist.DefaultPageSize+1) | ||
| } | ||
| if got[0].WorkflowSource != "contract:77766655544433322211:0xfeedface00000000000000000000000000c0ffee" { | ||
| t.Errorf("first row source: %q", got[0].WorkflowSource) | ||
| } | ||
| if got[len(got)-1].Name != "mock-wf-last" || got[len(got)-1].WorkflowSource != "private" { | ||
| t.Errorf("last row: %+v", got[len(got)-1]) | ||
| } | ||
| if ex.call != 2 { | ||
| t.Errorf("executor calls = %d, want 2", ex.call) | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Both storage and privateregistry clients have gql as their dependency. I think it would be good to have the same consistent way to structure such clients.