Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 0 additions & 40 deletions libs/capability/capability.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
package capability

import (
"reflect"

"google.golang.org/protobuf/reflect/protoreflect"

"github.com/smartcontractkit/chainlink-common/pkg/capabilities"
"github.com/smartcontractkit/chainlink-common/pkg/logger"
"github.com/smartcontractkit/chainlink-common/pkg/services"
"github.com/smartcontractkit/chainlink-common/pkg/settings/limits"
"github.com/smartcontractkit/chainlink-common/pkg/types/core"
)

type Capability interface {
Expand All @@ -20,19 +15,6 @@ type Capability interface {
Service() protoreflect.ServiceDescriptor
}

// Config marks the struct a constructor asks for as the capability's own configuration
//
// type Config struct {
// capability.Config
// FastestScheduleIntervalSeconds int `usage:"fastest cron schedule a workflow may register"`
// }
//
// The run binds the struct's fields as flags on the root command, namespaced by the binary's
// name - cron's are --cron.fastest-schedule-interval-seconds, CRE_CRON_FASTEST_SCHEDULE_INTERVAL_SECONDS,
// or the same key in the config file - and hands the constructor the decoded value. A constructor
// declares at most one.
type Config struct{}

// capabilityConfig is what a capability binary needs from the node it runs beside.
type capabilityConfig struct {
ProxyURL string `usage:"gRPC target of the node's capability registry proxy (e.g. localhost:9000), used to resolve capabilities this binary does not host. Unset resolves only the capabilities this binary hosts"`
Expand All @@ -41,25 +23,3 @@ type capabilityConfig struct {
// Serves the Debug UI
HTTPDebug bool `usage:"serve the capability debug UI on the shared HTTP server, under /debug/capabilities"`
}

type Dependencies struct {
Logger logger.Logger
CapabilityRegistry core.CapabilitiesRegistry
LimitsFactory limits.Factory
}

func (d Dependencies) list() []any {
return []any{d.Logger, d.CapabilityRegistry, d.LimitsFactory}
}

func (d Dependencies) resolve(want reflect.Type) (reflect.Value, bool) {
for _, v := range d.list() {
if v == nil {
continue
}
if got := reflect.TypeOf(v); got.AssignableTo(want) {
return reflect.ValueOf(v), true
}
}
return reflect.Value{}, false
}
138 changes: 7 additions & 131 deletions libs/capability/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,13 @@ package capability
import (
"context"
"errors"
"fmt"
"log"
"os"
"os/signal"
"path/filepath"
"reflect"
"syscall"

"github.com/samber/do/v2"
"github.com/spf13/cobra"
sdkmetric "go.opentelemetry.io/otel/sdk/metric"

Expand All @@ -21,7 +20,7 @@ import (
// process is signalled.
//
// func main() { capability.Run(context.Background(), trigger.NewCron) }
func Run(ctx context.Context, constructor any, opts ...Option) {
func Run(ctx context.Context, constructor func(do.Injector) (Capability, error), opts ...Option) {
lggr, err := NewLogger()
if err != nil {
log.Fatal(err)
Expand All @@ -36,15 +35,12 @@ func Run(ctx context.Context, constructor any, opts ...Option) {
//
// A run blocks until it is told to stop, which is either ctx being cancelled or this process being
// signalled - see below.
func RunErr(ctx context.Context, lggr logger.Logger, constructor any, opts ...Option) error {
func RunErr(ctx context.Context, lggr logger.Logger, constructor func(do.Injector) (Capability, error), opts ...Option) error {
if lggr == nil {
return errors.New("must provide a logger")
}
Comment thread
Copilot marked this conversation as resolved.

// We instantiate this up-front so we can bubble up an error message about an invalid `constructor` early.
c, err := newConstructor(constructor)
if err != nil {
return err
if constructor == nil {
return errors.New("must provide a capability constructor")
}

// TODO: Run should accept some kind of Info struct
Expand All @@ -64,19 +60,12 @@ func RunErr(ctx context.Context, lggr logger.Logger, constructor any, opts ...Op
return err
}

// Bind the capability's config if the constructor function declares a config struct.
// This will be namespaced using the name of the root command.
// eg. --cron.fastest-schedule-interval-seconds
if err := c.bindCapabilityConfig(root); err != nil {
return err
}

root.AddCommand(&cobra.Command{
Use: "run",
Short: "Run the capability",
RunE: func(cmd *cobra.Command, _ []string) error {
// TODO: pass in constructors here for our runnable/embeddable dependencies?
return run(cmd.Context(), lggr, cmd.Root().Name(), cfg, c)
return run(cmd.Context(), lggr, cmd.Root().Name(), cfg, constructor)
Comment thread
nolag marked this conversation as resolved.
},
})

Expand All @@ -87,7 +76,7 @@ func RunErr(ctx context.Context, lggr logger.Logger, constructor any, opts ...Op
RunE: func(cmd *cobra.Command, _ []string) error {
// TODO: pass in constructors here for our runnable/embeddable dependencies?
// Something else here
return run(cmd.Context(), lggr, cmd.Root().Name(), cfg, c)
return run(cmd.Context(), lggr, cmd.Root().Name(), cfg, constructor)
},
})

Expand All @@ -98,119 +87,6 @@ func RunErr(ctx context.Context, lggr logger.Logger, constructor any, opts ...Op
return root.ExecuteContext(ctx)
}

type constructorFn struct {
fn reflect.Value
typ reflect.Type

// configIn is the index of the parameter that is the capability's own config, or -1 when the
// constructor declares none.
configIn int
// config is the instance of that parameter the flags decode fills. Kept here rather than
// passed to run because the decode hook fills it in place when the command runs: call has to
// read that same memory, not a copy made when it was bound. Valid only when configIn >= 0.
config reflect.Value
}

var capabilityType = reflect.TypeFor[Capability]()

// newConstructor validates that constructor has the right shape, i.e.:
// - it's a function
// - and the function returns a capability and optionally an error
func newConstructor(ctor any) (constructorFn, error) {
t := reflect.TypeOf(ctor)
if t == nil || t.Kind() != reflect.Func {
return constructorFn{}, fmt.Errorf("a capability constructor must be a function, got %T", ctor)
}

switch t.NumOut() {
case 1:
case 2:
if t.Out(1) != reflect.TypeFor[error]() {
return constructorFn{}, fmt.Errorf(
"a capability constructor returning two values must return an error second, got %s", t)
}
default:
return constructorFn{}, fmt.Errorf(
"a capability constructor must return the capability, and optionally an error, got %s", t)
}

if out := t.Out(0); !out.Implements(capabilityType) {
return constructorFn{}, fmt.Errorf("%s does not implement capability.Capability", out)
}

configIn := -1
for i := range t.NumIn() {
if !isConfig(t.In(i)) {
continue
}
if configIn >= 0 {
return constructorFn{}, fmt.Errorf("a capability constructor takes its config once, got %s", t)
}
configIn = i
}
return constructorFn{fn: reflect.ValueOf(ctor), typ: t, configIn: configIn}, nil
}

var isConfigType = reflect.TypeFor[Config]()

// isConfig reports whether t declares itself a capability's config by embedding Config. The
// embedding has to be direct: the marker declares the struct it is embedded in, not the structs
// that embed that one.
func isConfig(t reflect.Type) bool {
if t.Kind() != reflect.Struct {
return false
}
for i := range t.NumField() {
if f := t.Field(i); f.Anonymous && f.Type == isConfigType {
return true
}
}
return false
}

// bindCapabilityConfig creates the config the constructor declares, if any, and binds its fields as flags
// on root, under root's name.
func (c *constructorFn) bindCapabilityConfig(root *cobra.Command) error {
if c.configIn < 0 {
return nil
}

v := reflect.New(c.typ.In(c.configIn))
if err := bindFlags(root, root.Name(), v.Interface()); err != nil {
return fmt.Errorf("failed to register the capability's config: %w", err)
}
c.config = v.Elem()
return nil
}

// call builds the capability by calling the contstructor.
// The constructor's parameters are scanned, and type matched against the dependency set.
// The config parameter is treated exceptionally and hydrated from the configuration that was passed in.
func (c constructorFn) call(deps Dependencies) (Capability, error) {
args := make([]reflect.Value, c.typ.NumIn())
for i := range args {
if i == c.configIn {
args[i] = c.config
continue
}

want := c.typ.In(i)

v, ok := deps.resolve(want)
if !ok {
return nil, fmt.Errorf("the capability constructor asks for a %s, and nothing in this run "+
"provides one: %s", want, c.typ)
}
args[i] = v
}

out := c.fn.Call(args)
if len(out) == 2 && !out[1].IsNil() {
return nil, fmt.Errorf("failed to build the capability: %w", out[1].Interface().(error))
}
return out[0].Interface().(Capability), nil
}

type Option func(*config)

// WithOtelViews sets otel metric views - histogram bucket boundaries, typically - on the beholder
Expand Down
Loading
Loading