Skip to content
Open
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
2 changes: 1 addition & 1 deletion cmd/client/workflow_registry_v2_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ type RegisterWorkflowV2Parameters struct {

BinaryURL string // required: URL location for the workflow binary WASM file
ConfigURL string // optional: URL location for the workflow configuration file (default empty string)
Attributes []byte // optional: 1 to pause workflow after registration, 0 to activate it (default is 0)
Attributes []byte // optional: JSON-encoded workflow attributes (e.g. confidential flag, vault secrets)
KeepAlive bool // optional: whether to keep the other workflows of the same name and owner active after the new deploy (default is false)
}

Expand Down
25 changes: 25 additions & 0 deletions cmd/workflow/deploy/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"io"
"strings"
"sync"

"github.com/ethereum/go-ethereum/common"
Expand Down Expand Up @@ -42,6 +43,9 @@ type Inputs struct {

OwnerLabel string `validate:"omitempty"`
SkipConfirmation bool

Confidential bool
Secrets []string
}

func (i *Inputs) ResolveConfigURL(fallbackURL string) string {
Expand Down Expand Up @@ -104,6 +108,8 @@ func New(runtimeContext *runtime.Context) *cobra.Command {
settings.AddSkipConfirmation(deployCmd)
deployCmd.Flags().StringP("output", "o", defaultOutputPath, "The output file for the compiled WASM binary encoded in base64")
deployCmd.Flags().StringP("owner-label", "l", "", "Label for the workflow owner (used during auto-link if owner is not already linked)")
deployCmd.Flags().Bool("confidential", false, "Deploy as a confidential workflow (runs in enclave)")
deployCmd.Flags().StringSlice("secret", nil, "VaultDON secret to request (repeatable, format: KEY or KEY:namespace)")

return deployCmd
}
Expand Down Expand Up @@ -171,10 +177,23 @@ func (h *handler) ResolveInputs(v *viper.Viper) (Inputs, error) {
WorkflowRegistryContractAddress: h.environmentSet.WorkflowRegistryAddress,
OwnerLabel: v.GetString("owner-label"),
SkipConfirmation: v.GetBool(settings.Flags.SkipConfirmation.Name),
Confidential: v.GetBool("confidential"),
Secrets: v.GetStringSlice("secret"),
}, nil
}

func (h *handler) ValidateInputs() error {
if len(h.inputs.Secrets) > 0 && !h.inputs.Confidential {
return fmt.Errorf("--secret requires --confidential flag")
}

for _, s := range h.inputs.Secrets {
key, _, _ := strings.Cut(s, ":")
if strings.TrimSpace(key) == "" {
return fmt.Errorf("--secret value %q has empty key", s)
}
}

validate, err := validation.NewValidator()
if err != nil {
return fmt.Errorf("failed to initialize validator: %w", err)
Expand Down Expand Up @@ -299,5 +318,11 @@ func (h *handler) displayWorkflowDetails() {
ui.Title(fmt.Sprintf("Deploying Workflow: %s", h.inputs.WorkflowName))
ui.Dim(fmt.Sprintf("Target: %s", h.settings.User.TargetName))
ui.Dim(fmt.Sprintf("Owner Address: %s", h.settings.Workflow.UserWorkflowSettings.WorkflowOwnerAddress))
if h.inputs.Confidential {
ui.Dim("Confidential: yes")
if len(h.inputs.Secrets) > 0 {
ui.Dim(fmt.Sprintf("Secrets: %s", strings.Join(h.inputs.Secrets, ", ")))
}
}
ui.Line()
}
25 changes: 25 additions & 0 deletions cmd/workflow/deploy/deploy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,31 @@ func TestResolveInputs_TagTruncation(t *testing.T) {
}
}

func TestValidateInputs_SecretRequiresConfidential(t *testing.T) {
t.Parallel()
simulatedEnvironment := chainsim.NewSimulatedEnvironment(t)
defer simulatedEnvironment.Close()

ctx, buf := simulatedEnvironment.NewRuntimeContextWithBufferedOutput()
h := newHandler(ctx, buf)

h.inputs = Inputs{
WorkflowName: "test_workflow",
WorkflowOwner: chainsim.TestAddress,
WorkflowPath: "testdata/basic_workflow/main.go",
DonFamily: "zone-a",
WorkflowRegistryContractChainName: "ethereum-testnet-sepolia",
WorkflowRegistryContractAddress: simulatedEnvironment.Contracts.WorkflowRegistry.Contract.Hex(),
Secrets: []string{"API_KEY"},
Confidential: false,
}

err := h.ValidateInputs()
require.Error(t, err)
assert.Contains(t, err.Error(), "--secret requires --confidential flag")
assert.False(t, h.validated)
}

func stringPtr(s string) *string {
return &s
}
Expand Down
45 changes: 44 additions & 1 deletion cmd/workflow/deploy/register.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package deploy

import (
"encoding/hex"
"encoding/json"
"fmt"
"strings"
"time"

"github.com/ethereum/go-ethereum/common"
Expand Down Expand Up @@ -43,6 +45,11 @@ func (h *handler) prepareUpsertParams() (client.RegisterWorkflowV2Parameters, er
status = *h.existingWorkflowStatus
}

attrs, err := h.buildAttributes()
if err != nil {
return client.RegisterWorkflowV2Parameters{}, err
}

ui.Dim(fmt.Sprintf("Preparing transaction for workflowID: %s", workflowID))
return client.RegisterWorkflowV2Parameters{
WorkflowName: workflowName,
Expand All @@ -52,7 +59,7 @@ func (h *handler) prepareUpsertParams() (client.RegisterWorkflowV2Parameters, er
DonFamily: h.inputs.DonFamily,
BinaryURL: binaryURL,
ConfigURL: configURL,
Attributes: []byte{}, // optional
Attributes: attrs,
KeepAlive: h.inputs.KeepAlive,
}, nil
}
Expand Down Expand Up @@ -145,3 +152,39 @@ func (h *handler) handleUpsert(params client.RegisterWorkflowV2Parameters) error
}
return nil
}

func (h *handler) buildAttributes() ([]byte, error) {
if !h.inputs.Confidential {
return []byte{}, nil
}

secrets := make([]secretIdentifier, 0, len(h.inputs.Secrets))
for _, s := range h.inputs.Secrets {
key, ns, _ := strings.Cut(s, ":")
secrets = append(secrets, secretIdentifier{
Key: key,
Namespace: ns,
})
}

attrs := workflowAttributes{
Confidential: true,
VaultDonSecrets: secrets,
}

data, err := json.Marshal(attrs)
if err != nil {
return nil, fmt.Errorf("marshalling workflow attributes: %w", err)
}
return data, nil
}

type workflowAttributes struct {
Confidential bool `json:"confidential"`
VaultDonSecrets []secretIdentifier `json:"vault_don_secrets,omitempty"`
}

type secretIdentifier struct {
Key string `json:"key"`
Namespace string `json:"namespace,omitempty"`
}
62 changes: 62 additions & 0 deletions cmd/workflow/deploy/register_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package deploy

import (
"encoding/json"
"path/filepath"
"testing"

Expand Down Expand Up @@ -171,3 +172,64 @@ func TestPrepareUpsertParams_StatusPreservation(t *testing.T) {
assert.Equal(t, uint8(0), params.Status, "updating active workflow should preserve active status (0)")
})
}

func TestBuildAttributes(t *testing.T) {
t.Parallel()

t.Run("non-confidential workflow returns empty attributes", func(t *testing.T) {
t.Parallel()
h := &handler{inputs: Inputs{Confidential: false}}
attrs, err := h.buildAttributes()
require.NoError(t, err)
assert.Equal(t, []byte{}, attrs)
})

t.Run("confidential workflow with no secrets", func(t *testing.T) {
t.Parallel()
h := &handler{inputs: Inputs{Confidential: true}}
attrs, err := h.buildAttributes()
require.NoError(t, err)

var parsed workflowAttributes
require.NoError(t, json.Unmarshal(attrs, &parsed))
assert.True(t, parsed.Confidential)
assert.Empty(t, parsed.VaultDonSecrets)
})

t.Run("confidential workflow with secrets", func(t *testing.T) {
t.Parallel()
h := &handler{inputs: Inputs{
Confidential: true,
Secrets: []string{"API_KEY", "DB_PASS"},
}}
attrs, err := h.buildAttributes()
require.NoError(t, err)

var parsed workflowAttributes
require.NoError(t, json.Unmarshal(attrs, &parsed))
assert.True(t, parsed.Confidential)
require.Len(t, parsed.VaultDonSecrets, 2)
assert.Equal(t, "API_KEY", parsed.VaultDonSecrets[0].Key)
assert.Empty(t, parsed.VaultDonSecrets[0].Namespace)
assert.Equal(t, "DB_PASS", parsed.VaultDonSecrets[1].Key)
assert.Empty(t, parsed.VaultDonSecrets[1].Namespace)
})

t.Run("secret with namespace parsed correctly", func(t *testing.T) {
t.Parallel()
h := &handler{inputs: Inputs{
Confidential: true,
Secrets: []string{"API_KEY:prod", "DB_PASS"},
}}
attrs, err := h.buildAttributes()
require.NoError(t, err)

var parsed workflowAttributes
require.NoError(t, json.Unmarshal(attrs, &parsed))
require.Len(t, parsed.VaultDonSecrets, 2)
assert.Equal(t, "API_KEY", parsed.VaultDonSecrets[0].Key)
assert.Equal(t, "prod", parsed.VaultDonSecrets[0].Namespace)
assert.Equal(t, "DB_PASS", parsed.VaultDonSecrets[1].Key)
assert.Empty(t, parsed.VaultDonSecrets[1].Namespace)
})
}
Loading