Skip to content

Commit fa78941

Browse files
authored
feat(handler): implements handle workflow registered event (smartcontractkit#15383)
* feat(workflows): adds orm methods for managing specs * feat(handler): implements handle workflow registered event * chore(syncer): lint changes * refactor(workflows/handler): check event type higher up * refactor(workflows/handler): force update secrets with beholder * refactor(workflows/handler): use custom error function * fix(workflows): wires up emitter
1 parent f5d228e commit fa78941

File tree

7 files changed

+486
-25
lines changed

7 files changed

+486
-25
lines changed

core/services/relay/evm/capabilities/workflows/syncer/workflow_syncer_test.go

+3
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"github.com/ethereum/go-ethereum/accounts/abi/bind"
1212
"github.com/ethereum/go-ethereum/common"
1313

14+
"github.com/smartcontractkit/chainlink-common/pkg/custmsg"
1415
"github.com/smartcontractkit/chainlink-common/pkg/services/servicetest"
1516
"github.com/smartcontractkit/chainlink-common/pkg/types"
1617
"github.com/smartcontractkit/chainlink/v2/core/gethwrappers/workflow/generated/workflow_registry_wrapper"
@@ -29,6 +30,7 @@ func Test_SecretsWorker(t *testing.T) {
2930
var (
3031
ctx = coretestutils.Context(t)
3132
lggr = logger.TestLogger(t)
33+
emitter = custmsg.NewLabeler()
3234
backendTH = testutils.NewEVMBackendTH(t)
3335
db = pgtest.NewSqlxDB(t)
3436
orm = syncer.NewWorkflowRegistryDS(db, lggr)
@@ -119,6 +121,7 @@ func Test_SecretsWorker(t *testing.T) {
119121
wfRegistryAddr.Hex(),
120122
nil,
121123
nil,
124+
emitter,
122125
syncer.WithTicker(giveTicker.C),
123126
)
124127

core/services/workflows/syncer/handler.go

+174-18
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,18 @@ package syncer
22

33
import (
44
"context"
5+
"crypto/sha256"
56
"encoding/hex"
67
"errors"
78
"fmt"
89

10+
"github.com/smartcontractkit/chainlink-common/pkg/custmsg"
911
"github.com/smartcontractkit/chainlink-common/pkg/types/core"
12+
"github.com/smartcontractkit/chainlink-common/pkg/workflows/wasm/host"
1013
"github.com/smartcontractkit/chainlink/v2/core/logger"
14+
"github.com/smartcontractkit/chainlink/v2/core/platform"
15+
"github.com/smartcontractkit/chainlink/v2/core/services/job"
16+
"github.com/smartcontractkit/chainlink/v2/core/services/workflows"
1117
"github.com/smartcontractkit/chainlink/v2/core/services/workflows/store"
1218
)
1319

@@ -87,15 +93,28 @@ type WorkflowRegistryWorkflowDeletedV1 struct {
8793
WorkflowName string
8894
}
8995

96+
type secretsFetcher interface {
97+
SecretsFor(ctx context.Context, workflowOwner, workflowName string) (map[string]string, error)
98+
}
99+
100+
// secretsFetcherFunc implements the secretsFetcher interface for a function.
101+
type secretsFetcherFunc func(ctx context.Context, workflowOwner, workflowName string) (map[string]string, error)
102+
103+
func (f secretsFetcherFunc) SecretsFor(ctx context.Context, workflowOwner, workflowName string) (map[string]string, error) {
104+
return f(ctx, workflowOwner, workflowName)
105+
}
106+
90107
// eventHandler is a handler for WorkflowRegistryEvent events. Each event type has a corresponding
91108
// method that handles the event.
92109
type eventHandler struct {
93110
lggr logger.Logger
94-
orm WorkflowSecretsDS
111+
orm WorkflowRegistryDS
95112
fetcher FetcherFunc
96113
workflowStore store.Store
97114
capRegistry core.CapabilitiesRegistry
98115
engineRegistry *engineRegistry
116+
emitter custmsg.MessageEmitter
117+
secretsFetcher secretsFetcher
99118
}
100119

101120
// newEventHandler returns a new eventHandler instance.
@@ -106,6 +125,8 @@ func newEventHandler(
106125
workflowStore store.Store,
107126
capRegistry core.CapabilitiesRegistry,
108127
engineRegistry *engineRegistry,
128+
emitter custmsg.MessageEmitter,
129+
secretsFetcher secretsFetcher,
109130
) *eventHandler {
110131
return &eventHandler{
111132
lggr: lggr,
@@ -114,15 +135,50 @@ func newEventHandler(
114135
workflowStore: workflowStore,
115136
capRegistry: capRegistry,
116137
engineRegistry: engineRegistry,
138+
emitter: emitter,
139+
secretsFetcher: secretsFetcher,
117140
}
118141
}
119142

120143
func (h *eventHandler) Handle(ctx context.Context, event WorkflowRegistryEvent) error {
121144
switch event.EventType {
122145
case ForceUpdateSecretsEvent:
123-
return h.forceUpdateSecretsEvent(ctx, event)
146+
payload, ok := event.Data.(WorkflowRegistryForceUpdateSecretsRequestedV1)
147+
if !ok {
148+
return newHandlerTypeError(event.Data)
149+
}
150+
151+
cma := h.emitter.With(
152+
platform.KeyWorkflowName, payload.WorkflowName,
153+
platform.KeyWorkflowOwner, hex.EncodeToString(payload.Owner),
154+
)
155+
156+
if err := h.forceUpdateSecretsEvent(ctx, payload); err != nil {
157+
logCustMsg(ctx, cma, fmt.Sprintf("failed to handle force update secrets event: %v", err), h.lggr)
158+
return err
159+
}
160+
161+
return nil
124162
case WorkflowRegisteredEvent:
125-
return h.workflowRegisteredEvent(ctx, event)
163+
payload, ok := event.Data.(WorkflowRegistryWorkflowRegisteredV1)
164+
if !ok {
165+
return newHandlerTypeError(event.Data)
166+
}
167+
wfID := hex.EncodeToString(payload.WorkflowID[:])
168+
169+
cma := h.emitter.With(
170+
platform.KeyWorkflowID, wfID,
171+
platform.KeyWorkflowName, payload.WorkflowName,
172+
platform.KeyWorkflowOwner, hex.EncodeToString(payload.WorkflowOwner),
173+
)
174+
175+
if err := h.workflowRegisteredEvent(ctx, payload); err != nil {
176+
logCustMsg(ctx, cma, fmt.Sprintf("failed to handle workflow registered event: %v", err), h.lggr)
177+
return err
178+
}
179+
180+
h.lggr.Debugf("workflow 0x%x registered and started", wfID)
181+
return nil
126182
case WorkflowUpdatedEvent:
127183
return h.workflowUpdatedEvent(ctx, event)
128184
case WorkflowPausedEvent:
@@ -135,12 +191,97 @@ func (h *eventHandler) Handle(ctx context.Context, event WorkflowRegistryEvent)
135191
}
136192

137193
// workflowRegisteredEvent handles the WorkflowRegisteredEvent event type.
138-
// TODO: Implement this method
139194
func (h *eventHandler) workflowRegisteredEvent(
140-
_ context.Context,
141-
_ WorkflowRegistryEvent,
195+
ctx context.Context,
196+
payload WorkflowRegistryWorkflowRegisteredV1,
142197
) error {
143-
return ErrNotImplemented
198+
wfID := hex.EncodeToString(payload.WorkflowID[:])
199+
200+
// Download the contents of binaryURL, configURL and secretsURL and cache them locally.
201+
binary, err := h.fetcher(ctx, payload.BinaryURL)
202+
if err != nil {
203+
return fmt.Errorf("failed to fetch binary from %s : %w", payload.BinaryURL, err)
204+
}
205+
206+
config, err := h.fetcher(ctx, payload.ConfigURL)
207+
if err != nil {
208+
return fmt.Errorf("failed to fetch config from %s : %w", payload.ConfigURL, err)
209+
}
210+
211+
secrets, err := h.fetcher(ctx, payload.SecretsURL)
212+
if err != nil {
213+
return fmt.Errorf("failed to fetch secrets from %s : %w", payload.SecretsURL, err)
214+
}
215+
216+
// Calculate the hash of the binary and config files
217+
hash := workflowID(binary, config, []byte(payload.SecretsURL))
218+
219+
// Pre-check: verify that the workflowID matches; if it doesn’t abort and log an error via Beholder.
220+
if hash != wfID {
221+
return fmt.Errorf("workflowID mismatch: %s != %s", hash, wfID)
222+
}
223+
224+
// Save the workflow secrets
225+
urlHash, err := h.orm.GetSecretsURLHash(payload.WorkflowOwner, []byte(payload.SecretsURL))
226+
if err != nil {
227+
return fmt.Errorf("failed to get secrets URL hash: %w", err)
228+
}
229+
230+
// Create a new entry in the workflow_spec table corresponding for the new workflow, with the contents of the binaryURL + configURL in the table
231+
status := job.WorkflowSpecStatusActive
232+
if payload.Status == 1 {
233+
status = job.WorkflowSpecStatusPaused
234+
}
235+
236+
entry := &job.WorkflowSpec{
237+
Workflow: hex.EncodeToString(binary),
238+
Config: string(config),
239+
WorkflowID: wfID,
240+
Status: status,
241+
WorkflowOwner: hex.EncodeToString(payload.WorkflowOwner),
242+
WorkflowName: payload.WorkflowName,
243+
SpecType: job.WASMFile,
244+
BinaryURL: payload.BinaryURL,
245+
ConfigURL: payload.ConfigURL,
246+
}
247+
if _, err = h.orm.UpsertWorkflowSpecWithSecrets(ctx, entry, payload.SecretsURL, hex.EncodeToString(urlHash), string(secrets)); err != nil {
248+
return fmt.Errorf("failed to upsert workflow spec with secrets: %w", err)
249+
}
250+
251+
if status != job.WorkflowSpecStatusActive {
252+
return nil
253+
}
254+
255+
// If status == active, start a new WorkflowEngine instance, and add it to local engine registry
256+
moduleConfig := &host.ModuleConfig{Logger: h.lggr, Labeler: h.emitter}
257+
sdkSpec, err := host.GetWorkflowSpec(ctx, moduleConfig, binary, config)
258+
if err != nil {
259+
return fmt.Errorf("failed to get workflow sdk spec: %w", err)
260+
}
261+
262+
cfg := workflows.Config{
263+
Lggr: h.lggr,
264+
Workflow: *sdkSpec,
265+
WorkflowID: wfID,
266+
WorkflowOwner: hex.EncodeToString(payload.WorkflowOwner),
267+
WorkflowName: payload.WorkflowName,
268+
Registry: h.capRegistry,
269+
Store: h.workflowStore,
270+
Config: config,
271+
Binary: binary,
272+
SecretsFetcher: h.secretsFetcher,
273+
}
274+
e, err := workflows.NewEngine(ctx, cfg)
275+
if err != nil {
276+
return fmt.Errorf("failed to create workflow engine: %w", err)
277+
}
278+
279+
if err := e.Start(ctx); err != nil {
280+
return fmt.Errorf("failed to start workflow engine: %w", err)
281+
}
282+
283+
h.engineRegistry.Add(wfID, e)
284+
return nil
144285
}
145286

146287
// workflowUpdatedEvent handles the WorkflowUpdatedEvent event type.
@@ -170,32 +311,47 @@ func (h *eventHandler) workflowActivatedEvent(
170311
// forceUpdateSecretsEvent handles the ForceUpdateSecretsEvent event type.
171312
func (h *eventHandler) forceUpdateSecretsEvent(
172313
ctx context.Context,
173-
event WorkflowRegistryEvent,
314+
payload WorkflowRegistryForceUpdateSecretsRequestedV1,
174315
) error {
175316
// Get the URL of the secrets file from the event data
176-
data, ok := event.Data.(WorkflowRegistryForceUpdateSecretsRequestedV1)
177-
if !ok {
178-
return fmt.Errorf("invalid data type %T for event", event.Data)
179-
}
180-
181-
hash := hex.EncodeToString(data.SecretsURLHash)
317+
hash := hex.EncodeToString(payload.SecretsURLHash)
182318

183319
url, err := h.orm.GetSecretsURLByHash(ctx, hash)
184320
if err != nil {
185-
h.lggr.Errorf("failed to get URL by hash %s : %s", hash, err)
186-
return err
321+
return fmt.Errorf("failed to get URL by hash %s : %w", hash, err)
187322
}
188323

189324
// Fetch the contents of the secrets file from the url via the fetcher
190325
secrets, err := h.fetcher(ctx, url)
191326
if err != nil {
192-
return err
327+
return fmt.Errorf("failed to fetch secrets from url %s : %w", url, err)
193328
}
194329

195330
// Update the secrets in the ORM
196331
if _, err := h.orm.Update(ctx, hash, string(secrets)); err != nil {
197-
return err
332+
return fmt.Errorf("failed to update secrets: %w", err)
198333
}
199334

200335
return nil
201336
}
337+
338+
// workflowID returns a hex encoded sha256 hash of the wasm, config and secretsURL.
339+
func workflowID(wasm, config, secretsURL []byte) string {
340+
sum := sha256.New()
341+
sum.Write(wasm)
342+
sum.Write(config)
343+
sum.Write(secretsURL)
344+
return hex.EncodeToString(sum.Sum(nil))
345+
}
346+
347+
// logCustMsg emits a custom message to the external sink and logs an error if that fails.
348+
func logCustMsg(ctx context.Context, cma custmsg.MessageEmitter, msg string, log logger.Logger) {
349+
err := cma.Emit(ctx, msg)
350+
if err != nil {
351+
log.Helper(1).Errorf("failed to send custom message with msg: %s, err: %v", msg, err)
352+
}
353+
}
354+
355+
func newHandlerTypeError(data any) error {
356+
return fmt.Errorf("invalid data type %T for event", data)
357+
}

0 commit comments

Comments
 (0)