Skip to content

Commit 65720ec

Browse files
committed
Add plugin CLI and rework example
This commit modifies plugins to leverage cobra to give them a nicer command-line interface with help and version output. The test plugin has also been changed to a ping-ponger with a type managing the entire plugin. Signed-off-by: David Bond <davidsbond93@gmail.com>
1 parent 9055092 commit 65720ec

File tree

4 files changed

+83
-30
lines changed

4 files changed

+83
-30
lines changed

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ tool (
1010

1111
require (
1212
github.com/rs/xid v1.6.0
13+
github.com/spf13/cobra v1.9.1
1314
github.com/stretchr/testify v1.10.0
1415
golang.org/x/sync v0.15.0
1516
google.golang.org/grpc v1.73.0
@@ -85,7 +86,6 @@ require (
8586
github.com/segmentio/asm v1.2.0 // indirect
8687
github.com/segmentio/encoding v0.5.1 // indirect
8788
github.com/sirupsen/logrus v1.9.3 // indirect
88-
github.com/spf13/cobra v1.9.1 // indirect
8989
github.com/spf13/pflag v1.0.6 // indirect
9090
github.com/stoewer/go-strcase v1.3.0 // indirect
9191
github.com/tetratelabs/wazero v1.9.0 // indirect

plugin.go

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
"time"
2525

2626
"github.com/rs/xid"
27+
"github.com/spf13/cobra"
2728
"golang.org/x/sync/errgroup"
2829
"google.golang.org/grpc"
2930
"google.golang.org/grpc/codes"
@@ -99,19 +100,38 @@ func (ch Command[Input, Output]) Execute(ctx context.Context, input *anypb.Any)
99100

100101
// Run a plugin using the provided configuration. This function blocks until the process receives an SIGINT, SIGTERM
101102
// or SIGKILL signal. At which point it will gracefully stop the gRPC server and remove its UNIX domain socket.
102-
func Run(config Config) error {
103+
func Run(config Config) {
103104
ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM, syscall.SIGKILL)
104105
defer cancel()
105106

106-
if len(os.Args) == 0 {
107-
return errors.New("plugin expects at least one argument")
107+
cmd := &cobra.Command{
108+
Use: fmt.Sprintf("%s [socket id]", config.Name),
109+
Version: getPluginVersion(),
110+
Short: fmt.Sprintf("Starts the %q plugin", config.Name),
111+
Long: fmt.Sprintf("Starts the %q plugin.\n\nOnce started, the plugin will begin listening for commands on a UNIX domain socket under /tmp. This socket name is specified by the first argument passed to the command.", config.Name),
112+
CompletionOptions: cobra.CompletionOptions{
113+
DisableDefaultCmd: true,
114+
},
115+
SilenceUsage: true,
116+
SilenceErrors: true,
117+
Args: cobra.ExactArgs(1),
118+
RunE: func(cmd *cobra.Command, args []string) error {
119+
return startPlugin(cmd.Context(), config, args[0], cmd.Version)
120+
},
108121
}
109122

123+
if err := cmd.ExecuteContext(ctx); err != nil {
124+
fmt.Printf("failed to start plugin %q: %v\n", config.Name, err)
125+
os.Exit(1)
126+
}
127+
}
128+
129+
func startPlugin(ctx context.Context, config Config, id, version string) error {
110130
server := grpc.NewServer()
111131

112132
info := plugin.Info{
113133
Name: config.Name,
114-
Version: getPluginVersion(),
134+
Version: version,
115135
}
116136

117137
handlers := plugin.CommandHandlers{}
@@ -122,7 +142,7 @@ func Run(config Config) error {
122142

123143
plugin.NewAPI(info, handlers).Register(server)
124144

125-
socket := "/tmp/" + os.Args[0] + ".sock"
145+
socket := "/tmp/" + id + ".sock"
126146
listener, err := net.Listen("unix", socket)
127147
if err != nil {
128148
return err
@@ -181,6 +201,7 @@ func Use(ctx context.Context, path string) (*Plugin, error) {
181201
cmd := &exec.Cmd{
182202
Path: path,
183203
Args: []string{
204+
path,
184205
socket,
185206
},
186207
}

plugin_test.go

Lines changed: 31 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import (
88
"github.com/stretchr/testify/assert"
99
"github.com/stretchr/testify/require"
1010
"google.golang.org/protobuf/types/known/durationpb"
11-
"google.golang.org/protobuf/types/known/timestamppb"
11+
"google.golang.org/protobuf/types/known/wrapperspb"
1212

1313
"github.com/davidsbond/plugin"
1414
)
@@ -27,33 +27,51 @@ func TestUse(t *testing.T) {
2727
assert.NotEmpty(t, p.Version())
2828

2929
if assert.Len(t, p.Commands(), 1) {
30-
assert.EqualValues(t, "test", p.Commands()[0])
30+
assert.EqualValues(t, "pingpong", p.Commands()[0])
3131
}
3232

33-
t.Run("known command", func(t *testing.T) {
34-
input := timestamppb.Now()
35-
output := &timestamppb.Timestamp{}
36-
err := p.Exec(t.Context(), "test", input, output)
33+
t.Run("command pings", func(t *testing.T) {
34+
input := wrapperspb.String("ping")
35+
output := &wrapperspb.StringValue{}
36+
err = p.Exec(t.Context(), "pingpong", input, output)
3737

3838
require.NoError(t, err)
3939
assert.NotNil(t, output)
40-
assert.EqualValues(t, input.AsTime(), output.AsTime())
40+
assert.EqualValues(t, "pong", output.GetValue())
41+
})
42+
43+
t.Run("command pongs", func(t *testing.T) {
44+
input := wrapperspb.String("pong")
45+
output := &wrapperspb.StringValue{}
46+
err = p.Exec(t.Context(), "pingpong", input, output)
47+
48+
require.NoError(t, err)
49+
assert.NotNil(t, output)
50+
assert.EqualValues(t, "ping", output.GetValue())
51+
})
52+
53+
t.Run("command errors if not ping or pong", func(t *testing.T) {
54+
input := wrapperspb.String("pung")
55+
output := &wrapperspb.StringValue{}
56+
err = p.Exec(t.Context(), "pingpong", input, output)
57+
58+
assert.Error(t, err)
4159
})
4260

4361
t.Run("unknown command", func(t *testing.T) {
44-
input := timestamppb.Now()
45-
output := &timestamppb.Timestamp{}
46-
err := p.Exec(t.Context(), "unknown", input, output)
62+
input := wrapperspb.String("pong")
63+
output := &wrapperspb.StringValue{}
64+
err = p.Exec(t.Context(), "unknown", input, output)
4765

4866
require.Error(t, err)
4967
assert.True(t, errors.Is(err, plugin.ErrUnknownCommand))
5068
})
5169

52-
t.Run("error if invalid input", func(t *testing.T) {
70+
t.Run("error if invalid input type", func(t *testing.T) {
5371
input := durationpb.New(time.Hour)
54-
output := &timestamppb.Timestamp{}
72+
output := &wrapperspb.StringValue{}
5573

56-
err := p.Exec(t.Context(), "test", input, output)
74+
err = p.Exec(t.Context(), "test", input, output)
5775
require.Error(t, err)
5876
})
5977
}

testdata/test_plugin/main.go

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,41 @@ package main
22

33
import (
44
"context"
5-
"log"
5+
"fmt"
66

7-
"google.golang.org/protobuf/types/known/timestamppb"
7+
"google.golang.org/protobuf/types/known/wrapperspb"
88

99
"github.com/davidsbond/plugin"
1010
)
1111

12-
func main() {
13-
config := plugin.Config{
12+
type (
13+
PingPongPlugin struct{}
14+
)
15+
16+
func (tp *PingPongPlugin) Run() {
17+
plugin.Run(plugin.Config{
1418
Name: "test_plugin",
1519
Commands: []plugin.CommandHandler{
16-
&plugin.Command[*timestamppb.Timestamp, *timestamppb.Timestamp]{
17-
Use: "test",
18-
Run: func(ctx context.Context, input *timestamppb.Timestamp) (*timestamppb.Timestamp, error) {
19-
return input, nil
20-
},
20+
&plugin.Command[*wrapperspb.StringValue, *wrapperspb.StringValue]{
21+
Use: "pingpong",
22+
Run: tp.PingPong,
2123
},
2224
},
25+
})
26+
}
27+
28+
func (tp *PingPongPlugin) PingPong(ctx context.Context, input *wrapperspb.StringValue) (*wrapperspb.StringValue, error) {
29+
if input.GetValue() == "ping" {
30+
return &wrapperspb.StringValue{Value: "pong"}, nil
2331
}
2432

25-
if err := plugin.Run(config); err != nil {
26-
log.Fatal(err)
33+
if input.GetValue() == "pong" {
34+
return &wrapperspb.StringValue{Value: "ping"}, nil
2735
}
36+
37+
return nil, fmt.Errorf(`invalid input %q, expected "ping" or "pong"`, input.Value)
38+
}
39+
40+
func main() {
41+
(&PingPongPlugin{}).Run()
2842
}

0 commit comments

Comments
 (0)