Skip to content

Commit 1745afd

Browse files
authored
feat: add pipelines ls cmd (+client API) (#2063)
* rename pkg name * feat: add pipelines ls * pipeline ls work * hide processors for now * include alias for pipelines cmd * hide columns for now We'll augment these on pipeline describe and see how it feels * fix api flags (+test) * use pipeline service directly * update flag names * remove testing boolean * add ExecuteWithClient decorator * fix double conn err msg * better approach * include comment * read from the same addresss * fix ci * fix ci II * fix tests * pipelines ls with config * uses ecdysis parsing * fix ci * remove test * return default value in case it's not parsed * give additional context if there's an error * early return * fix lint
1 parent b8bfbfa commit 1745afd

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

61 files changed

+360
-75
lines changed

Diff for: cmd/conduit/api/client.go

+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
// Copyright © 2025 Meroxa, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package api
16+
17+
import (
18+
"context"
19+
"fmt"
20+
21+
apiv1 "github.com/conduitio/conduit/proto/api/v1"
22+
"google.golang.org/grpc"
23+
"google.golang.org/grpc/credentials/insecure"
24+
healthgrpc "google.golang.org/grpc/health/grpc_health_v1"
25+
)
26+
27+
type Client struct {
28+
conn *grpc.ClientConn
29+
apiv1.PipelineServiceClient
30+
healthgrpc.HealthClient
31+
}
32+
33+
func NewClient(ctx context.Context, address string) (*Client, error) {
34+
conn, err := grpc.NewClient(
35+
address,
36+
grpc.WithTransportCredentials(insecure.NewCredentials()),
37+
)
38+
if err != nil {
39+
return nil, fmt.Errorf("failed to create gRPC client: %w", err)
40+
}
41+
42+
client := &Client{
43+
conn: conn,
44+
PipelineServiceClient: apiv1.NewPipelineServiceClient(conn),
45+
HealthClient: healthgrpc.NewHealthClient(conn),
46+
}
47+
48+
if err := client.CheckHealth(ctx, address); err != nil {
49+
client.Close()
50+
return nil, err
51+
}
52+
53+
return client, nil
54+
}
55+
56+
func (c *Client) CheckHealth(ctx context.Context, address string) error {
57+
healthResp, err := c.HealthClient.Check(ctx, &healthgrpc.HealthCheckRequest{})
58+
if err != nil || healthResp.Status != healthgrpc.HealthCheckResponse_SERVING {
59+
return fmt.Errorf("we couldn't connect to Conduit at the configured address %q\n"+
60+
"Please execute `conduit run` to start it.\nTo check the current configured `api.grpc.address`, run `conduit config`\n\n"+
61+
"Error details: %v", address, err)
62+
}
63+
return nil
64+
}
65+
66+
func (c *Client) Close() error {
67+
return c.conn.Close()
68+
}

Diff for: cmd/conduit/cecdysis/decorators.go

+106
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
// Copyright © 2025 Meroxa, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package cecdysis
16+
17+
import (
18+
"context"
19+
"fmt"
20+
"os"
21+
"path/filepath"
22+
23+
"github.com/conduitio/conduit/cmd/conduit/api"
24+
"github.com/conduitio/conduit/pkg/conduit"
25+
"github.com/conduitio/ecdysis"
26+
"github.com/spf13/cobra"
27+
)
28+
29+
// ------------------- CommandWithClient
30+
31+
// CommandWithExecuteWithClient can be implemented by a command that requires a client to interact
32+
// with the Conduit API during the execution.
33+
type CommandWithExecuteWithClient interface {
34+
ecdysis.Command
35+
36+
// ExecuteWithClient is the actual work function. Most commands will implement this.
37+
ExecuteWithClient(context.Context, *api.Client) error
38+
}
39+
40+
// CommandWithExecuteWithClientDecorator is a decorator that adds a Conduit API client to the command execution.
41+
type CommandWithExecuteWithClientDecorator struct{}
42+
43+
func (CommandWithExecuteWithClientDecorator) Decorate(_ *ecdysis.Ecdysis, cmd *cobra.Command, c ecdysis.Command) error {
44+
v, ok := c.(CommandWithExecuteWithClient)
45+
if !ok {
46+
return nil
47+
}
48+
49+
old := cmd.RunE
50+
cmd.RunE = func(cmd *cobra.Command, args []string) error {
51+
if old != nil {
52+
err := old(cmd, args)
53+
if err != nil {
54+
return err
55+
}
56+
}
57+
58+
grpcAddress, err := getGRPCAddress(cmd)
59+
if err != nil {
60+
return fmt.Errorf("error reading gRPC address: %w", err)
61+
}
62+
63+
client, err := api.NewClient(cmd.Context(), grpcAddress)
64+
if err != nil {
65+
// Not an error we need to escalate to the main CLI execution. We'll print it out and not execute further.
66+
_, _ = fmt.Fprintf(os.Stderr, "%v\n", err)
67+
return nil
68+
}
69+
defer client.Close()
70+
71+
ctx := ecdysis.ContextWithCobraCommand(cmd.Context(), cmd)
72+
return v.ExecuteWithClient(ctx, client)
73+
}
74+
75+
return nil
76+
}
77+
78+
func getGRPCAddress(cmd *cobra.Command) (string, error) {
79+
var (
80+
path string
81+
err error
82+
)
83+
84+
path, err = cmd.Flags().GetString("config.path")
85+
if err != nil || path == "" {
86+
path = conduit.DefaultConfig().ConduitCfgPath
87+
}
88+
89+
var usrCfg conduit.Config
90+
defaultConfigValues := conduit.DefaultConfigWithBasePath(filepath.Dir(path))
91+
92+
cfg := ecdysis.Config{
93+
EnvPrefix: "CONDUIT",
94+
Parsed: &usrCfg,
95+
Path: path,
96+
DefaultValues: defaultConfigValues,
97+
}
98+
99+
// If it can't be parsed, we return the default value
100+
err = ecdysis.ParseConfig(cfg, cmd)
101+
if err != nil || usrCfg.API.GRPC.Address == "" {
102+
return defaultConfigValues.API.GRPC.Address, nil
103+
}
104+
105+
return usrCfg.API.GRPC.Address, nil
106+
}

Diff for: cmd/conduit/main.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,13 @@ import (
1818
"fmt"
1919
"os"
2020

21+
"github.com/conduitio/conduit/cmd/conduit/cecdysis"
2122
"github.com/conduitio/conduit/cmd/conduit/root"
2223
"github.com/conduitio/ecdysis"
2324
)
2425

2526
func main() {
26-
e := ecdysis.New()
27+
e := ecdysis.New(ecdysis.WithDecorators(cecdysis.CommandWithExecuteWithClientDecorator{}))
2728

2829
cmd := e.MustBuildCobraCommand(&root.RootCommand{})
2930
cmd.CompletionOptions.DisableDefaultCmd = true

Diff for: cmd/conduit/root/config/config_test.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,8 @@ func TestPrintStructOutput(t *testing.T) {
5656
"db.postgres.table: conduit_kv_store",
5757
"db.sqlite.table: conduit_kv_store",
5858
"api.enabled: true",
59-
"http.address: :8080",
60-
"grpc.address: :8084",
59+
"api.http.address: :8080",
60+
"api.grpc.address: :8084",
6161
"log.level: info",
6262
"log.format: cli",
6363
"pipelines.exit-on-degraded: false",

Diff for: cmd/conduit/root/pipelines/list.go

+89
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
// Copyright © 2025 Meroxa, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package pipelines
16+
17+
import (
18+
"context"
19+
"fmt"
20+
21+
"github.com/alexeyco/simpletable"
22+
"github.com/conduitio/conduit/cmd/conduit/api"
23+
"github.com/conduitio/conduit/cmd/conduit/cecdysis"
24+
apiv1 "github.com/conduitio/conduit/proto/api/v1"
25+
"github.com/conduitio/ecdysis"
26+
)
27+
28+
var (
29+
_ cecdysis.CommandWithExecuteWithClient = (*ListCommand)(nil)
30+
_ ecdysis.CommandWithAliases = (*ListCommand)(nil)
31+
_ ecdysis.CommandWithDocs = (*ListCommand)(nil)
32+
)
33+
34+
type ListCommand struct{}
35+
36+
func (c *ListCommand) Docs() ecdysis.Docs {
37+
return ecdysis.Docs{
38+
Short: "List existing Conduit pipelines",
39+
Long: `This command requires Conduit to be already running since it will list all pipelines registered
40+
by Conduit. This will depend on the configured pipelines directory, which by default is /pipelines; however, it could
41+
be configured via --pipelines.path at the time of running Conduit.`,
42+
Example: "conduit pipelines ls",
43+
}
44+
}
45+
46+
func (c *ListCommand) Aliases() []string { return []string{"ls"} }
47+
48+
func (c *ListCommand) Usage() string { return "list" }
49+
50+
func (c *ListCommand) ExecuteWithClient(ctx context.Context, client *api.Client) error {
51+
resp, err := client.PipelineServiceClient.ListPipelines(ctx, &apiv1.ListPipelinesRequest{})
52+
if err != nil {
53+
return fmt.Errorf("failed to list pipelines: %w", err)
54+
}
55+
56+
displayPipelines(resp.Pipelines)
57+
58+
return nil
59+
}
60+
61+
func displayPipelines(pipelines []*apiv1.Pipeline) {
62+
if len(pipelines) == 0 {
63+
return
64+
}
65+
66+
table := simpletable.New()
67+
68+
table.Header = &simpletable.Header{
69+
Cells: []*simpletable.Cell{
70+
{Align: simpletable.AlignCenter, Text: "ID"},
71+
{Align: simpletable.AlignCenter, Text: "STATE"},
72+
{Align: simpletable.AlignCenter, Text: "CREATED"},
73+
{Align: simpletable.AlignCenter, Text: "LAST_UPDATED"},
74+
},
75+
}
76+
77+
for _, p := range pipelines {
78+
r := []*simpletable.Cell{
79+
{Align: simpletable.AlignRight, Text: p.Id},
80+
{Align: simpletable.AlignLeft, Text: p.State.Status.String()},
81+
{Align: simpletable.AlignLeft, Text: p.CreatedAt.AsTime().String()},
82+
{Align: simpletable.AlignLeft, Text: p.UpdatedAt.AsTime().String()},
83+
}
84+
85+
table.Body.Cells = append(table.Body.Cells, r)
86+
}
87+
table.SetStyle(simpletable.StyleCompact)
88+
fmt.Println(table.String())
89+
}

Diff for: cmd/conduit/root/pipelines/pipelines.go

+4
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,17 @@ import (
2121
var (
2222
_ ecdysis.CommandWithDocs = (*PipelinesCommand)(nil)
2323
_ ecdysis.CommandWithSubCommands = (*PipelinesCommand)(nil)
24+
_ ecdysis.CommandWithAliases = (*PipelinesCommand)(nil)
2425
)
2526

2627
type PipelinesCommand struct{}
2728

29+
func (c *PipelinesCommand) Aliases() []string { return []string{"pipeline"} }
30+
2831
func (c *PipelinesCommand) SubCommands() []ecdysis.Command {
2932
return []ecdysis.Command{
3033
&InitCommand{},
34+
&ListCommand{},
3135
}
3236
}
3337

Diff for: cmd/conduit/root/root.go

+5-1
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ var (
3737

3838
type RootFlags struct {
3939
Version bool `long:"version" short:"v" usage:"show the current Conduit version"`
40+
41+
// Global Flags
42+
GRPCAddress string `long:"api.grpc.address" usage:"address where Conduit is running" persistent:"true"`
43+
ConfigPath string `long:"config.path" usage:"path to the configuration file" persistent:"true"`
4044
}
4145

4246
type RootCommand struct {
@@ -77,6 +81,6 @@ func (c *RootCommand) SubCommands() []ecdysis.Command {
7781
&initialize.InitCommand{Cfg: &runCmd.Cfg},
7882
&version.VersionCommand{},
7983
&pipelines.PipelinesCommand{},
80-
&run.RunCommand{},
84+
runCmd,
8185
}
8286
}

Diff for: cmd/conduit/root/root_test.go

+2
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ func TestRootCommandFlags(t *testing.T) {
3232
persistent bool
3333
}{
3434
{longName: "version", shortName: "v", usage: "show the current Conduit version"},
35+
{longName: "api.grpc.address", usage: "address where Conduit is running", persistent: true},
36+
{longName: "config.path", usage: "path to the configuration file", persistent: true},
3537
}
3638

3739
e := ecdysis.New()

Diff for: cmd/conduit/root/run/run.go

+9-2
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ package run
1616

1717
import (
1818
"context"
19+
"fmt"
1920
"os"
2021
"path/filepath"
2122

@@ -42,6 +43,12 @@ type RunCommand struct {
4243

4344
func (c *RunCommand) Execute(_ context.Context) error {
4445
e := &conduit.Entrypoint{}
46+
47+
if !c.Cfg.API.Enabled {
48+
fmt.Print("Warning: API is currently disabled. Most Conduit CLI commands won't work without the API enabled." +
49+
"To enable it, run conduit with `--api.enabled=true` or set `CONDUIT_API_ENABLED=true` in your environment.")
50+
}
51+
4552
e.Serve(c.Cfg)
4653
return nil
4754
}
@@ -76,8 +83,8 @@ func (c *RunCommand) Flags() []ecdysis.Flag {
7683
flags.SetDefault("db.sqlite.path", c.Cfg.DB.SQLite.Path)
7784
flags.SetDefault("db.sqlite.table", c.Cfg.DB.SQLite.Table)
7885
flags.SetDefault("api.enabled", c.Cfg.API.Enabled)
79-
flags.SetDefault("http.address", c.Cfg.API.HTTP.Address)
80-
flags.SetDefault("grpc.address", c.Cfg.API.GRPC.Address)
86+
flags.SetDefault("api.http.address", c.Cfg.API.HTTP.Address)
87+
flags.SetDefault("api.grpc.address", c.Cfg.API.GRPC.Address)
8188
flags.SetDefault("log.level", c.Cfg.Log.Level)
8289
flags.SetDefault("log.format", c.Cfg.Log.Format)
8390
flags.SetDefault("connectors.path", c.Cfg.Connectors.Path)

Diff for: cmd/conduit/root/run/run_test.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,8 @@ func TestRunCommandFlags(t *testing.T) {
3939
{longName: "db.sqlite.path", usage: "path to sqlite3 DB"},
4040
{longName: "db.sqlite.table", usage: "sqlite3 table in which to store data (will be created if it does not exist)"},
4141
{longName: "api.enabled", usage: "enable HTTP and gRPC API"},
42-
{longName: "http.address", usage: "address for serving the HTTP API"},
43-
{longName: "grpc.address", usage: "address for serving the gRPC API"},
42+
{longName: "api.http.address", usage: "address for serving the HTTP API"},
43+
{longName: "api.grpc.address", usage: "address for serving the gRPC API"},
4444
{longName: "log.level", usage: "sets logging level; accepts debug, info, warn, error, trace"},
4545
{longName: "log.format", usage: "sets the format of the logging; accepts json, cli"},
4646
{longName: "connectors.path", usage: "path to standalone connectors' directory"},

0 commit comments

Comments
 (0)