Repository navigation
Expand file tree
/
Copy pathrun.go
More file actions
204 lines (180 loc) · 6.14 KB
/
Copy pathrun.go
File metadata and controls
204 lines (180 loc) · 6.14 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
package capability
import (
"context"
"errors"
"fmt"
"log"
"os"
"os/signal"
"path/filepath"
"strconv"
"syscall"
"github.com/hashicorp/go-plugin"
"github.com/prometheus/client_golang/prometheus"
"github.com/samber/do/v2"
"github.com/smartcontractkit/chainlink-common/x/config/markup"
"github.com/spf13/cobra"
sdkmetric "go.opentelemetry.io/otel/sdk/metric"
"github.com/smartcontractkit/chainlink-common/pkg/logger"
"github.com/smartcontractkit/chainlink-common/pkg/loop"
"github.com/smartcontractkit/chainlink-common/pkg/services"
)
// Run builds and runs the capability constructor makes, and does not return until the process is
// signalled. To stop it with a context, use RunErr.
//
// func main() { capability.Run("cron", trigger.NewCron) }
func Run[T Capability](appName string, constructor func(do.Injector) (T, error), opts ...Option) {
if err := RunErr(context.Background(), appName, constructor, opts...); err != nil {
log.Fatal(err)
}
}
// RunErr is Run, returning the error rather than exiting on it.
//
// A run blocks until it is told to stop, which is either ctx being cancelled or this process being
// signalled - see below.
func RunErr[T Capability](ctx context.Context, appName string, ctor func(do.Injector) (T, error), opts ...Option) error {
if appName == "" {
return errors.New("must provide an app name")
}
if ctor == nil {
return errors.New("must provide a capability constructor")
}
constructor := func(i do.Injector) (Capability, error) { return ctor(i) }
// TODO: Run should accept some kind of Info struct
root := &cobra.Command{
Use: filepath.Base(os.Args[0]),
Short: "A CRE capability",
}
args := os.Args[1:]
if len(args) == 0 {
args = []string{"compat"}
}
root.SetArgs(args)
cfg := defaultConfig(appName)
cfg.runInjector = RunInjector.Clone()
cfg.embedInjector = EmbedInjector.Clone()
for _, opt := range opts {
err := opt(cfg)
if err != nil {
return err
}
}
lggr := cfg.lggr
if lggr == nil {
l, err := loop.NewLogger()
if err != nil {
return fmt.Errorf("failed to create logger: %w", err)
}
lggr = l
}
lggr = logger.Named(lggr, appName)
b, err := cfg.bind(root)
if err != nil {
return err
}
compatCmd := &cobra.Command{
Use: "compat",
Short: "Run the capability as a LOOP of the core node",
Hidden: true,
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, _ []string) error {
if !underPluginHost() {
return errors.New("compatibility mode must be started by a go-plugin host, such as the core node. To run the capability alone, use the run command")
}
if cfg.observability.PrometheusPort == 0 {
return errors.New("compatibility mode needs CL_PROMETHEUS_PORT")
}
serve := func(_ context.Context, s services.Service) { loop.ServeEmpty(s, plugin.DefaultGRPCServer) }
if cfg.serve != nil {
serve = cfg.serve
}
return newRunner(lggr, appName, cfg, constructor, cfg.runInjector, 1, cfg.capabilities, HTTPConfig{Port: cfg.observability.PrometheusPort}, serve).run(cmd.Context())
},
}
runCmd := &cobra.Command{
Use: "run",
Short: "Run the capability",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, _ []string) error {
return newRunner(lggr, appName, cfg, constructor, cfg.runInjector, 1, cfg.capabilities, cfg.observability.http, nil).run(cmd.Context())
},
}
embedCmd := &cobra.Command{
Use: "embed <instances>",
Short: "Run instances of the capability in this process, with no node behind them",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
total, err := strconv.Atoi(args[0])
if err != nil || total < 1 {
return fmt.Errorf("instances must be a positive integer, got %q", args[0])
}
capCfg := cfg.capabilities
capCfg.ProxyURL = ""
return newRunner(lggr, appName, cfg, constructor, cfg.embedInjector, total, capCfg, cfg.observability.http, nil).run(cmd.Context())
},
}
if err := errors.Join(
cfg.runInjector.RegisterConfig(compatCmd, b),
cfg.runInjector.RegisterConfig(runCmd, b),
cfg.embedInjector.RegisterConfig(embedCmd, b),
); err != nil {
return err
}
root.AddCommand(compatCmd, runCmd, embedCmd, newDocgenCmd())
// Wire up SIGTERM + SIGINT to the context
ctx, stop := signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM)
defer stop()
return root.ExecuteContext(ctx)
}
type Option func(*config) error
// withLogger gives a run the logger of a test. Other runs make their own logger.
func withLogger(lggr logger.Logger) Option {
return func(c *config) error {
c.lggr = lggr
return nil
}
}
// withServe replaces plugin.Serve in compatibility mode. plugin.Serve takes over the stdio of the
// process, so tests can't call it.
func withServe(serve func(context.Context, services.Service)) Option {
return func(c *config) error {
c.serve = serve
return nil
}
}
// withRegisterer gives a run its own metrics registry. Tests start many runs in one process.
// The default registry panics if you register the same metrics two times.
func withRegisterer(r prometheus.Registerer) Option {
return func(c *config) error {
c.grpc.Registerer = r
return nil
}
}
// WithOtelViews sets otel metric views - histogram bucket boundaries, typically - on the beholder
// client this process reports through.
//
// capability.Run("cron", trigger.NewCron, capability.WithOtelViews(trigger.MetricViews()...))
func WithOtelViews(views ...sdkmetric.View) Option {
return func(c *config) error {
c.observability.otelViews = append(c.observability.otelViews, views...)
return nil
}
}
// WithMarkup sets the markup language to use for config files.
// When unset, YAML is used
func WithMarkup(m markup.Markup) Option {
return func(c *config) error {
c.markup = m
return nil
}
}
// WithConfig provides a *T to the capability constructor, which gets it with do.MustInvoke[*T]. The
// keys of T are under the app name: flags are --<app>.<key> and env vars are CL_<APP>_<KEY>. Giving the
// same T two times via WithConfig is an error.
//
// capability.Run("cron", trigger.NewCron, capability.WithConfig[trigger.CronConfig]())
func WithConfig[T any]() Option {
return func(c *config) error {
return MultiInjector{c.runInjector, c.embedInjector}.ProvideConfig(c.appName, new(T))
}
}