Skip to content

Commit 2ede955

Browse files
authored
BCF-2685: move types from core to support Solana plugin command extraction (#193)
1 parent bcced69 commit 2ede955

9 files changed

Lines changed: 819 additions & 2 deletions

File tree

pkg/loop/adapter.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package loop
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
8+
"github.com/smartcontractkit/chainlink-relay/pkg/services"
9+
"github.com/smartcontractkit/chainlink-relay/pkg/types"
10+
)
11+
12+
// RelayerExt is a subset of [loop.Relayer] for adapting [types.Relayer], typically with a Chain. See [RelayerAdapter].
13+
type RelayerExt interface {
14+
types.ChainService
15+
ID() string
16+
}
17+
18+
var _ Relayer = (*RelayerAdapter)(nil)
19+
20+
// RelayerAdapter adapts a [types.Relayer] and [RelayerExt] to implement [Relayer].
21+
type RelayerAdapter struct {
22+
types.Relayer
23+
RelayerExt
24+
}
25+
26+
func (r *RelayerAdapter) NewConfigProvider(ctx context.Context, rargs types.RelayArgs) (types.ConfigProvider, error) {
27+
return r.Relayer.NewConfigProvider(rargs)
28+
}
29+
30+
func (r *RelayerAdapter) NewMedianProvider(ctx context.Context, rargs types.RelayArgs, pargs types.PluginArgs) (types.MedianProvider, error) {
31+
return r.Relayer.NewMedianProvider(rargs, pargs)
32+
}
33+
34+
func (r *RelayerAdapter) NewMercuryProvider(ctx context.Context, rargs types.RelayArgs, pargs types.PluginArgs) (types.MercuryProvider, error) {
35+
return r.Relayer.NewMercuryProvider(rargs, pargs)
36+
}
37+
38+
func (r *RelayerAdapter) NewFunctionsProvider(ctx context.Context, rargs types.RelayArgs, pargs types.PluginArgs) (types.FunctionsProvider, error) {
39+
return r.Relayer.NewFunctionsProvider(rargs, pargs)
40+
}
41+
42+
func (r *RelayerAdapter) NewPluginProvider(ctx context.Context, rargs types.RelayArgs, pargs types.PluginArgs) (types.PluginProvider, error) {
43+
return nil, fmt.Errorf("unexpected call to NewPluginProvider: did you forget to wrap RelayerAdapter in a relayerServerAdapter?")
44+
}
45+
46+
func (r *RelayerAdapter) Start(ctx context.Context) error {
47+
var ms services.MultiStart
48+
return ms.Start(ctx, r.RelayerExt, r.Relayer)
49+
}
50+
51+
func (r *RelayerAdapter) Close() error {
52+
return services.CloseAll(r.Relayer, r.RelayerExt)
53+
}
54+
55+
func (r *RelayerAdapter) Name() string {
56+
return fmt.Sprintf("%s-%s", r.Relayer.Name(), r.RelayerExt.Name())
57+
}
58+
59+
func (r *RelayerAdapter) Ready() (err error) {
60+
return errors.Join(r.Relayer.Ready(), r.RelayerExt.Ready())
61+
}
62+
63+
func (r *RelayerAdapter) HealthReport() map[string]error {
64+
hr := make(map[string]error)
65+
services.CopyHealth(hr, r.Relayer.HealthReport())
66+
return hr
67+
}

pkg/loop/config.go

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
package loop
2+
3+
import (
4+
"fmt"
5+
"net/url"
6+
"os"
7+
"strconv"
8+
"strings"
9+
)
10+
11+
const (
12+
envPromPort = "CL_PROMETHEUS_PORT"
13+
envTracingEnabled = "CL_TRACING_ENABLED"
14+
envTracingCollectorTarget = "CL_TRACING_COLLECTOR_TARGET"
15+
envTracingSamplingRatio = "CL_TRACING_SAMPLING_RATIO"
16+
envTracingAttribute = "CL_TRACING_ATTRIBUTE_"
17+
)
18+
19+
// EnvConfig is the configuration between the application and the LOOP executable. The values
20+
// are fully resolved and static and passed via the environment.
21+
type EnvConfig struct {
22+
PrometheusPort int
23+
24+
TracingEnabled bool
25+
TracingCollectorTarget string
26+
TracingSamplingRatio float64
27+
TracingAttributes map[string]string
28+
}
29+
30+
// AsCmdEnv returns a slice of environment variable key/value pairs for an exec.Cmd.
31+
func (e *EnvConfig) AsCmdEnv() (env []string) {
32+
injectEnv := map[string]string{
33+
envPromPort: strconv.Itoa(e.PrometheusPort),
34+
envTracingEnabled: strconv.FormatBool(e.TracingEnabled),
35+
envTracingCollectorTarget: e.TracingCollectorTarget,
36+
envTracingSamplingRatio: strconv.FormatFloat(e.TracingSamplingRatio, 'f', -1, 64),
37+
}
38+
39+
for k, v := range e.TracingAttributes {
40+
injectEnv[envTracingAttribute+k] = v
41+
}
42+
43+
for k, v := range injectEnv {
44+
env = append(env, k+"="+v)
45+
}
46+
return
47+
}
48+
49+
// parse deserializes environment variables
50+
func (e *EnvConfig) parse() error {
51+
promPortStr := os.Getenv(envPromPort)
52+
var err error
53+
e.PrometheusPort, err = strconv.Atoi(promPortStr)
54+
if err != nil {
55+
return fmt.Errorf("failed to parse %s = %q: %w", envPromPort, promPortStr, err)
56+
}
57+
58+
e.TracingEnabled, err = getTracingEnabled()
59+
if err != nil {
60+
return fmt.Errorf("failed to parse %s: %w", envTracingEnabled, err)
61+
}
62+
63+
if e.TracingEnabled {
64+
e.TracingCollectorTarget, err = getValidCollectorTarget()
65+
if err != nil {
66+
return err
67+
}
68+
e.TracingAttributes = getTracingAttributes()
69+
e.TracingSamplingRatio = getTracingSamplingRatio()
70+
}
71+
return nil
72+
}
73+
74+
// isTracingEnabled parses and validates the TRACING_ENABLED environment variable.
75+
func getTracingEnabled() (bool, error) {
76+
tracingEnabledString := os.Getenv(envTracingEnabled)
77+
if tracingEnabledString == "" {
78+
return false, nil
79+
}
80+
return strconv.ParseBool(tracingEnabledString)
81+
}
82+
83+
// getValidCollectorTarget validates TRACING_COLLECTOR_TARGET as a URL.
84+
func getValidCollectorTarget() (string, error) {
85+
tracingCollectorTarget := os.Getenv(envTracingCollectorTarget)
86+
_, err := url.ParseRequestURI(tracingCollectorTarget)
87+
if err != nil {
88+
return "", fmt.Errorf("invalid %s: %w", envTracingCollectorTarget, err)
89+
}
90+
return tracingCollectorTarget, nil
91+
}
92+
93+
// getTracingAttributes collects attributes prefixed with TRACING_ATTRIBUTE_.
94+
func getTracingAttributes() map[string]string {
95+
tracingAttributes := make(map[string]string)
96+
for _, env := range os.Environ() {
97+
if strings.HasPrefix(env, envTracingAttribute) {
98+
tracingAttributes[strings.TrimPrefix(env, envTracingAttribute)] = os.Getenv(env)
99+
}
100+
}
101+
return tracingAttributes
102+
}
103+
104+
// getTracingSamplingRatio parses the TRACING_SAMPLING_RATIO environment variable.
105+
// Any errors in parsing result in a sampling ratio of 0.0.
106+
func getTracingSamplingRatio() float64 {
107+
tracingSamplingRatio := os.Getenv(envTracingSamplingRatio)
108+
if tracingSamplingRatio == "" {
109+
return 0.0
110+
}
111+
samplingRatio, err := strconv.ParseFloat(tracingSamplingRatio, 64)
112+
if err != nil {
113+
return 0.0
114+
}
115+
return samplingRatio
116+
}

pkg/loop/config_test.go

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
package loop
2+
3+
import (
4+
"strconv"
5+
"strings"
6+
"testing"
7+
8+
"github.com/stretchr/testify/assert"
9+
"github.com/stretchr/testify/require"
10+
)
11+
12+
func TestEnvConfig_parse(t *testing.T) {
13+
cases := []struct {
14+
name string
15+
envVars map[string]string
16+
expectError bool
17+
expectedPrometheusPort int
18+
expectedTracingEnabled bool
19+
expectedTracingCollectorTarget string
20+
expectedTracingSamplingRatio float64
21+
}{
22+
{
23+
name: "All variables set correctly",
24+
envVars: map[string]string{
25+
envPromPort: "8080",
26+
envTracingEnabled: "true",
27+
envTracingCollectorTarget: "some:target",
28+
envTracingSamplingRatio: "1.0",
29+
envTracingAttribute + "XYZ": "value",
30+
},
31+
expectError: false,
32+
expectedPrometheusPort: 8080,
33+
expectedTracingEnabled: true,
34+
expectedTracingCollectorTarget: "some:target",
35+
expectedTracingSamplingRatio: 1.0,
36+
},
37+
{
38+
name: "CL_PROMETHEUS_PORT parse error",
39+
envVars: map[string]string{
40+
envPromPort: "abc",
41+
},
42+
expectError: true,
43+
},
44+
{
45+
name: "TRACING_ENABLED parse error",
46+
envVars: map[string]string{
47+
envPromPort: "8080",
48+
envTracingEnabled: "invalid_bool",
49+
},
50+
expectError: true,
51+
},
52+
}
53+
54+
for _, tc := range cases {
55+
t.Run(tc.name, func(t *testing.T) {
56+
for k, v := range tc.envVars {
57+
t.Setenv(k, v)
58+
}
59+
60+
var config EnvConfig
61+
err := config.parse()
62+
63+
if tc.expectError {
64+
if err == nil {
65+
t.Errorf("Expected error, got nil")
66+
}
67+
} else {
68+
if err != nil {
69+
t.Errorf("Unexpected error: %v", err)
70+
} else {
71+
if config.PrometheusPort != tc.expectedPrometheusPort {
72+
t.Errorf("Expected Prometheus port %d, got %d", tc.expectedPrometheusPort, config.PrometheusPort)
73+
}
74+
if config.TracingEnabled != tc.expectedTracingEnabled {
75+
t.Errorf("Expected tracingEnabled %v, got %v", tc.expectedTracingEnabled, config.TracingEnabled)
76+
}
77+
if config.TracingCollectorTarget != tc.expectedTracingCollectorTarget {
78+
t.Errorf("Expected tracingCollectorTarget %s, got %s", tc.expectedTracingCollectorTarget, config.TracingCollectorTarget)
79+
}
80+
if config.TracingSamplingRatio != tc.expectedTracingSamplingRatio {
81+
t.Errorf("Expected tracingSamplingRatio %f, got %f", tc.expectedTracingSamplingRatio, config.TracingSamplingRatio)
82+
}
83+
}
84+
}
85+
})
86+
}
87+
}
88+
89+
func TestEnvConfig_AsCmdEnv(t *testing.T) {
90+
envCfg := EnvConfig{
91+
PrometheusPort: 9090,
92+
TracingEnabled: true,
93+
TracingCollectorTarget: "http://localhost:9000",
94+
TracingSamplingRatio: 0.1,
95+
TracingAttributes: map[string]string{"key": "value"},
96+
}
97+
got := map[string]string{}
98+
for _, kv := range envCfg.AsCmdEnv() {
99+
pair := strings.SplitN(kv, "=", 2)
100+
require.Len(t, pair, 2)
101+
got[pair[0]] = pair[1]
102+
}
103+
104+
assert.Equal(t, strconv.Itoa(9090), got[envPromPort])
105+
assert.Equal(t, "true", got[envTracingEnabled])
106+
assert.Equal(t, "http://localhost:9000", got[envTracingCollectorTarget])
107+
assert.Equal(t, "0.1", got[envTracingSamplingRatio])
108+
assert.Equal(t, "value", got[envTracingAttribute+"key"])
109+
}

pkg/loop/plugin.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package loop
2+
3+
import (
4+
"sync"
5+
6+
"github.com/smartcontractkit/chainlink-relay/pkg/logger"
7+
"github.com/smartcontractkit/chainlink-relay/pkg/services"
8+
)
9+
10+
// Plugin is a base layer for plugins to easily manage sub-[types.Service]s.
11+
// Useful for implementing PluginRelayer and PluginMedian.
12+
type Plugin struct {
13+
Logger logger.Logger
14+
15+
mu sync.RWMutex
16+
ss []services.Service
17+
}
18+
19+
func (p *Plugin) Ready() error { return nil }
20+
func (p *Plugin) Name() string { return p.Logger.Name() }
21+
22+
func (p *Plugin) SubService(s services.Service) {
23+
p.mu.Lock()
24+
p.ss = append(p.ss, s)
25+
p.mu.Unlock()
26+
}
27+
28+
func (p *Plugin) HealthReport() map[string]error {
29+
hr := map[string]error{p.Name(): nil}
30+
p.mu.RLock()
31+
defer p.mu.RUnlock()
32+
for _, s := range p.ss {
33+
services.CopyHealth(hr, s.HealthReport())
34+
}
35+
return hr
36+
}
37+
38+
func (p *Plugin) Close() (err error) {
39+
p.mu.RLock()
40+
defer p.mu.RUnlock()
41+
return services.MultiCloser(p.ss).Close()
42+
}

0 commit comments

Comments
 (0)