Skip to content

Commit ba11ea0

Browse files
[CLD-570]: fix(JD): migrate dry run client to CLDF (#345)
MIgrating dry run JD client from CLD to CLDF as this is needed for loadning environment. Also introduced ability to create dry run client with the JD provider JIRA: https://smartcontract-it.atlassian.net/browse/CLD-570
1 parent d207c3b commit ba11ea0

File tree

5 files changed

+309
-3
lines changed

5 files changed

+309
-3
lines changed

.changeset/slimy-forks-worry.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"chainlink-deployments-framework": patch
3+
---
4+
5+
Migrated Dry Run JD Client from CLD

offchain/jd/doc.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,5 +240,68 @@ The provider automatically validates configurations:
240240
// Handle validation error
241241
log.Printf("Invalid configuration: %v", err)
242242
}
243+
244+
# Dry Run Mode
245+
246+
The package includes a dry run client that provides safe testing capabilities without
247+
affecting real Job Distributor operations:
248+
249+
import (
250+
"github.com/smartcontractkit/chainlink-deployments-framework/offchain/jd"
251+
"github.com/smartcontractkit/chainlink-common/pkg/logger"
252+
)
253+
254+
// Create a real client for read operations
255+
realClient, err := jd.NewJDClient(jd.JDConfig{
256+
GRPC: "localhost:9090",
257+
Creds: insecure.NewCredentials(),
258+
})
259+
if err != nil {
260+
log.Fatal(err)
261+
}
262+
263+
// Wrap with dry run client
264+
dryRunClient := jd.NewDryRunJobDistributor(realClient, logger.DefaultLogger)
265+
266+
// Read operations work normally (forwarded to real backend)
267+
jobs, err := dryRunClient.ListJobs(ctx, &jobv1.ListJobsRequest{})
268+
269+
// Write operations are simulated (logged but not executed)
270+
response, err := dryRunClient.ProposeJob(ctx, &jobv1.ProposeJobRequest{
271+
NodeId: "test-node",
272+
Spec: "test job spec",
273+
})
274+
// Returns mock response without actually proposing the job
275+
276+
# Dry Run with Provider (Recommended)
277+
278+
For a cleaner approach, use the provider's functional option:
279+
280+
import (
281+
"github.com/smartcontractkit/chainlink-deployments-framework/offchain/jd/provider"
282+
"github.com/smartcontractkit/chainlink-common/pkg/logger"
283+
)
284+
285+
// Create provider with dry run mode enabled
286+
jdProvider := provider.NewClientOffchainProvider(
287+
provider.ClientOffchainProviderConfig{
288+
GRPC: "localhost:9090",
289+
Creds: insecure.NewCredentials(),
290+
},
291+
provider.WithDryRun(logger),
292+
)
293+
294+
// Initialize - returns a dry run client automatically
295+
client, err := jdProvider.Initialize(ctx)
296+
if err != nil {
297+
log.Fatal(err)
298+
}
299+
300+
// All operations now use dry run mode
301+
jobs, err := client.ListJobs(ctx, &jobv1.ListJobsRequest{}) // Read: forwarded
302+
response, err := client.ProposeJob(ctx, &jobv1.ProposeJobRequest{ // Write: simulated
303+
NodeId: "test-node",
304+
Spec: "test job spec",
305+
})
243306
*/
244307
package jd

offchain/jd/dry_run_client.go

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
package jd
2+
3+
import (
4+
"context"
5+
6+
"google.golang.org/grpc"
7+
8+
cldf_offchain "github.com/smartcontractkit/chainlink-deployments-framework/offchain"
9+
10+
"github.com/smartcontractkit/chainlink-common/pkg/logger"
11+
csav1 "github.com/smartcontractkit/chainlink-protos/job-distributor/v1/csa"
12+
jobv1 "github.com/smartcontractkit/chainlink-protos/job-distributor/v1/job"
13+
nodev1 "github.com/smartcontractkit/chainlink-protos/job-distributor/v1/node"
14+
)
15+
16+
// DryRunJobDistributor is a readonly JD client.
17+
// Read operations are forwarded to the real backend, while write operations are ignored.
18+
type DryRunJobDistributor struct {
19+
// Used for read-only commands
20+
realBackend cldf_offchain.Client
21+
lggr logger.Logger
22+
}
23+
24+
var _ cldf_offchain.Client = (*DryRunJobDistributor)(nil)
25+
26+
// NewDryRunJobDistributor creates a new DryRunJobDistributor.
27+
func NewDryRunJobDistributor(realBackend cldf_offchain.Client, lggr logger.Logger) *DryRunJobDistributor {
28+
return &DryRunJobDistributor{
29+
realBackend: realBackend,
30+
lggr: lggr,
31+
}
32+
}
33+
34+
// GetJob retrieves a specific job by its ID from the Job Distributor.
35+
// This operation is forwarded to the real backend since it's a read-only operation.
36+
func (d *DryRunJobDistributor) GetJob(ctx context.Context, in *jobv1.GetJobRequest, opts ...grpc.CallOption) (*jobv1.GetJobResponse, error) {
37+
d.lggr.Infow("DryRunJobDistributor.GetJob", "in", in)
38+
return d.realBackend.GetJob(ctx, in)
39+
}
40+
41+
// GetProposal retrieves a specific job proposal by its ID from the Job Distributor.
42+
// This operation is forwarded to the real backend since it's a read-only operation.
43+
func (d *DryRunJobDistributor) GetProposal(ctx context.Context, in *jobv1.GetProposalRequest, opts ...grpc.CallOption) (*jobv1.GetProposalResponse, error) {
44+
d.lggr.Infow("DryRunJobDistributor.GetProposal", "in", in)
45+
return d.realBackend.GetProposal(ctx, in)
46+
}
47+
48+
// ListJobs retrieves a list of all jobs from the Job Distributor.
49+
// This operation is forwarded to the real backend since it's a read-only operation.
50+
func (d *DryRunJobDistributor) ListJobs(ctx context.Context, in *jobv1.ListJobsRequest, opts ...grpc.CallOption) (*jobv1.ListJobsResponse, error) {
51+
d.lggr.Infow("DryRunJobDistributor.ListJobs", "in", in)
52+
return d.realBackend.ListJobs(ctx, in)
53+
}
54+
55+
// ListProposals retrieves a list of all job proposals from the Job Distributor.
56+
// This operation is forwarded to the real backend since it's a read-only operation.
57+
func (d *DryRunJobDistributor) ListProposals(ctx context.Context, in *jobv1.ListProposalsRequest, opts ...grpc.CallOption) (*jobv1.ListProposalsResponse, error) {
58+
d.lggr.Infow("DryRunJobDistributor.ListProposals", "in", in)
59+
return d.realBackend.ListProposals(ctx, in)
60+
}
61+
62+
// ProposeJob simulates proposing a new job to the Job Distributor without actually submitting it.
63+
// In dry run mode, this returns a mock proposal response with a dummy job ID indicating
64+
// the job was not actually proposed to the node.
65+
func (d *DryRunJobDistributor) ProposeJob(ctx context.Context, in *jobv1.ProposeJobRequest, opts ...grpc.CallOption) (*jobv1.ProposeJobResponse, error) {
66+
d.lggr.Infow("DryRunJobDistributor.ProposeJob", "in", in)
67+
return &jobv1.ProposeJobResponse{
68+
Proposal: &jobv1.Proposal{
69+
JobId: "dryRunJobId_NOT_PROPOSED_on_node_" + in.NodeId,
70+
Spec: in.Spec,
71+
Status: jobv1.ProposalStatus_PROPOSAL_STATUS_UNSPECIFIED,
72+
},
73+
}, nil
74+
}
75+
76+
// BatchProposeJob simulates proposing multiple jobs in a batch to the Job Distributor without actually submitting them.
77+
// In dry run mode, this returns an empty response indicating the batch operation was logged but not executed.
78+
func (d *DryRunJobDistributor) BatchProposeJob(ctx context.Context, in *jobv1.BatchProposeJobRequest, opts ...grpc.CallOption) (*jobv1.BatchProposeJobResponse, error) {
79+
d.lggr.Infow("DryRunJobDistributor.BatchProposeJob", "in", in)
80+
return &jobv1.BatchProposeJobResponse{}, nil
81+
}
82+
83+
// RevokeJob simulates revoking a job from the Job Distributor without actually executing the revocation.
84+
// In dry run mode, this returns an empty response indicating the revocation was logged but not executed.
85+
func (d *DryRunJobDistributor) RevokeJob(ctx context.Context, in *jobv1.RevokeJobRequest, opts ...grpc.CallOption) (*jobv1.RevokeJobResponse, error) {
86+
d.lggr.Infow("DryRunJobDistributor.RevokeJob", "in", in)
87+
return &jobv1.RevokeJobResponse{}, nil
88+
}
89+
90+
// DeleteJob simulates deleting a job from the Job Distributor without actually executing the deletion.
91+
// In dry run mode, this returns an empty response indicating the deletion was logged but not executed.
92+
func (d *DryRunJobDistributor) DeleteJob(ctx context.Context, in *jobv1.DeleteJobRequest, opts ...grpc.CallOption) (*jobv1.DeleteJobResponse, error) {
93+
d.lggr.Infow("DryRunJobDistributor.DeleteJob", "in", in)
94+
return &jobv1.DeleteJobResponse{}, nil
95+
}
96+
97+
// UpdateJob simulates updating an existing job in the Job Distributor without actually executing the update.
98+
// In dry run mode, this returns an empty response indicating the update was logged but not executed.
99+
func (d *DryRunJobDistributor) UpdateJob(ctx context.Context, in *jobv1.UpdateJobRequest, opts ...grpc.CallOption) (*jobv1.UpdateJobResponse, error) {
100+
d.lggr.Infow("DryRunJobDistributor.UpdateJob", "in", in)
101+
return &jobv1.UpdateJobResponse{}, nil
102+
}
103+
104+
// DisableNode simulates disabling a node in the Job Distributor without actually executing the operation.
105+
// In dry run mode, this returns an empty response indicating the node disable operation was logged but not executed.
106+
func (d *DryRunJobDistributor) DisableNode(ctx context.Context, in *nodev1.DisableNodeRequest, opts ...grpc.CallOption) (*nodev1.DisableNodeResponse, error) {
107+
d.lggr.Infow("DryRunJobDistributor.DisableNode", "in", in)
108+
return &nodev1.DisableNodeResponse{}, nil
109+
}
110+
111+
// EnableNode simulates enabling a node in the Job Distributor without actually executing the operation.
112+
// In dry run mode, this returns an empty response indicating the node enable operation was logged but not executed.
113+
func (d *DryRunJobDistributor) EnableNode(ctx context.Context, in *nodev1.EnableNodeRequest, opts ...grpc.CallOption) (*nodev1.EnableNodeResponse, error) {
114+
d.lggr.Infow("DryRunJobDistributor.EnableNode", "in", in)
115+
return &nodev1.EnableNodeResponse{}, nil
116+
}
117+
118+
// GetNode retrieves information about a specific node from the Job Distributor.
119+
// This operation is forwarded to the real backend since it's a read-only operation.
120+
func (d *DryRunJobDistributor) GetNode(ctx context.Context, in *nodev1.GetNodeRequest, opts ...grpc.CallOption) (*nodev1.GetNodeResponse, error) {
121+
d.lggr.Infow("DryRunJobDistributor.GetNode", "in", in)
122+
return d.realBackend.GetNode(ctx, in)
123+
}
124+
125+
// ListNodes retrieves a list of all nodes registered with the Job Distributor.
126+
// This operation is forwarded to the real backend since it's a read-only operation.
127+
func (d *DryRunJobDistributor) ListNodes(ctx context.Context, in *nodev1.ListNodesRequest, opts ...grpc.CallOption) (*nodev1.ListNodesResponse, error) {
128+
d.lggr.Infow("DryRunJobDistributor.ListNodes", "in", in)
129+
return d.realBackend.ListNodes(ctx, in)
130+
}
131+
132+
// ListNodeChainConfigs retrieves chain configuration information for nodes from the Job Distributor.
133+
// This operation is forwarded to the real backend since it's a read-only operation.
134+
func (d *DryRunJobDistributor) ListNodeChainConfigs(ctx context.Context, in *nodev1.ListNodeChainConfigsRequest, opts ...grpc.CallOption) (*nodev1.ListNodeChainConfigsResponse, error) {
135+
d.lggr.Infow("DryRunJobDistributor.ListNodeChainConfigs", "in", in)
136+
return d.realBackend.ListNodeChainConfigs(ctx, in)
137+
}
138+
139+
// RegisterNode simulates registering a new node with the Job Distributor without actually executing the registration.
140+
// In dry run mode, this returns an empty response indicating the node registration was logged but not executed.
141+
func (d *DryRunJobDistributor) RegisterNode(ctx context.Context, in *nodev1.RegisterNodeRequest, opts ...grpc.CallOption) (*nodev1.RegisterNodeResponse, error) {
142+
d.lggr.Infow("DryRunJobDistributor.RegisterNode", "in", in)
143+
return &nodev1.RegisterNodeResponse{}, nil
144+
}
145+
146+
// UpdateNode simulates updating an existing node in the Job Distributor without actually executing the update.
147+
// In dry run mode, this returns an empty response indicating the node update was logged but not executed.
148+
func (d *DryRunJobDistributor) UpdateNode(ctx context.Context, in *nodev1.UpdateNodeRequest, opts ...grpc.CallOption) (*nodev1.UpdateNodeResponse, error) {
149+
d.lggr.Infow("DryRunJobDistributor.UpdateNode", "in", in)
150+
return &nodev1.UpdateNodeResponse{}, nil
151+
}
152+
153+
// GetKeypair retrieves a specific CSA keypair from the Job Distributor.
154+
// This operation is forwarded to the real backend since it's a read-only operation.
155+
func (d *DryRunJobDistributor) GetKeypair(ctx context.Context, in *csav1.GetKeypairRequest, opts ...grpc.CallOption) (*csav1.GetKeypairResponse, error) {
156+
d.lggr.Infow("DryRunJobDistributor.GetKeypair", "in", in)
157+
return d.realBackend.GetKeypair(ctx, in)
158+
}
159+
160+
// ListKeypairs retrieves a list of all CSA keypairs from the Job Distributor.
161+
// This operation is forwarded to the real backend since it's a read-only operation.
162+
func (d *DryRunJobDistributor) ListKeypairs(ctx context.Context, in *csav1.ListKeypairsRequest, opts ...grpc.CallOption) (*csav1.ListKeypairsResponse, error) {
163+
d.lggr.Infow("DryRunJobDistributor.ListKeypairs", "in", in)
164+
return d.realBackend.ListKeypairs(ctx, in)
165+
}

offchain/jd/provider/client_provider.go

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,15 @@ import (
88
"golang.org/x/oauth2"
99
"google.golang.org/grpc/credentials"
1010

11+
"github.com/smartcontractkit/chainlink-common/pkg/logger"
12+
1113
"github.com/smartcontractkit/chainlink-deployments-framework/offchain"
1214
"github.com/smartcontractkit/chainlink-deployments-framework/offchain/jd"
1315
)
1416

17+
// ClientProviderOption is a functional option for configuring ClientOffchainProvider.
18+
type ClientProviderOption func(*ClientOffchainProviderConfig)
19+
1520
// ClientOffchainProviderConfig holds the configuration to initialize the ClientOffchainProvider.
1621
type ClientOffchainProviderConfig struct {
1722
// Required: The gRPC URL to connect to the Job Distributor service.
@@ -22,6 +27,19 @@ type ClientOffchainProviderConfig struct {
2227
Creds credentials.TransportCredentials
2328
// Optional: OAuth2 token source for authentication.
2429
Auth oauth2.TokenSource
30+
31+
// Private fields for dry run configuration
32+
dryRun bool
33+
dryRunLogger logger.Logger
34+
}
35+
36+
// WithDryRun enables dry run mode, which simulates write operations without executing them.
37+
// Read operations are still forwarded to the real backend.
38+
func WithDryRun(lggr logger.Logger) ClientProviderOption {
39+
return func(c *ClientOffchainProviderConfig) {
40+
c.dryRun = true
41+
c.dryRunLogger = lggr
42+
}
2543
}
2644

2745
// validate checks if the ClientOffchainProviderConfig is valid.
@@ -30,6 +48,10 @@ func (c ClientOffchainProviderConfig) validate() error {
3048
return errors.New("gRPC URL is required")
3149
}
3250

51+
if c.dryRun && c.dryRunLogger == nil {
52+
return errors.New("dry run logger is required when dry run mode is enabled")
53+
}
54+
3355
return nil
3456
}
3557

@@ -41,8 +63,14 @@ type ClientOffchainProvider struct {
4163
client offchain.Client
4264
}
4365

44-
// NewClientOffchainProvider creates a new ClientOffchainProvider with the given configuration.
45-
func NewClientOffchainProvider(config ClientOffchainProviderConfig) *ClientOffchainProvider {
66+
// NewClientOffchainProvider creates a new ClientOffchainProvider with the given configuration and options.
67+
// Available options:
68+
// - WithDryRun(lggr logger.Logger) ClientProviderOption
69+
func NewClientOffchainProvider(config ClientOffchainProviderConfig, opts ...ClientProviderOption) *ClientOffchainProvider {
70+
for _, opt := range opts {
71+
opt(&config)
72+
}
73+
4674
return &ClientOffchainProvider{
4775
config: config,
4876
}
@@ -69,11 +97,17 @@ func (p *ClientOffchainProvider) Initialize(ctx context.Context) (offchain.Clien
6997
}
7098

7199
// Create the JD client
72-
client, err := jd.NewJDClient(jdConfig)
100+
jdClient, err := jd.NewJDClient(jdConfig)
73101
if err != nil {
74102
return nil, fmt.Errorf("failed to create JD client: %w", err)
75103
}
76104

105+
// Conditionally wrap with dry run client if dry run mode is enabled
106+
var client offchain.Client = jdClient
107+
if p.config.dryRun {
108+
client = jd.NewDryRunJobDistributor(jdClient, p.config.dryRunLogger)
109+
}
110+
77111
p.client = client
78112

79113
return client, nil

offchain/jd/provider/client_provider_test.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import (
66

77
"github.com/stretchr/testify/assert"
88
"github.com/stretchr/testify/require"
9+
10+
"github.com/smartcontractkit/chainlink-common/pkg/logger"
911
)
1012

1113
func TestClientOffchainProviderConfig_validate(t *testing.T) {
@@ -115,3 +117,40 @@ func TestInitializeProvider(t *testing.T) {
115117
assert.Nil(t, client)
116118
assert.Contains(t, err.Error(), "failed to validate provider config")
117119
}
120+
121+
func TestWithDryRun(t *testing.T) {
122+
t.Parallel()
123+
124+
lggr := logger.Test(t)
125+
126+
// Test that WithDryRun option sets the dry run fields correctly
127+
config := ClientOffchainProviderConfig{
128+
GRPC: "localhost:9090",
129+
}
130+
131+
provider := NewClientOffchainProvider(config, WithDryRun(lggr))
132+
133+
// Verify that dry run fields are set (we can't access them directly since they're private,
134+
// but we can test validation)
135+
err := provider.config.validate()
136+
require.NoError(t, err, "Config with dry run and logger should be valid")
137+
}
138+
139+
func TestWithDryRun_MissingLogger(t *testing.T) {
140+
t.Parallel()
141+
142+
// Test that dry run without logger fails validation
143+
config := ClientOffchainProviderConfig{
144+
GRPC: "localhost:9090",
145+
}
146+
147+
// Manually set dry run without logger to test validation
148+
config.dryRun = true
149+
// config.dryRunLogger is nil
150+
151+
provider := NewClientOffchainProvider(config)
152+
153+
err := provider.config.validate()
154+
require.Error(t, err)
155+
assert.Contains(t, err.Error(), "dry run logger is required when dry run mode is enabled")
156+
}

0 commit comments

Comments
 (0)