Commit dedf3452 authored by Yann Hodique's avatar Yann Hodique Committed by GitHub

feat(kurtosis-devnet): refactor run logic (#13554)

This change reimplements run over the kurtosis SDK instead of shelling
out to the CLI tool, which fixes potential consistency problems with
other uses of the SDK.

Additionally, we split the endpoint detection logic into a separate
entity (will make it a separate package a bit later), and improve the
host handling part by no longer assuming reachability on localhost
(implicitly assuming Docker backend). This is a step toward supporting
k8s backend transparently.
parent dd7f0fd2
......@@ -17,6 +17,7 @@ require (
github.com/ethereum-optimism/go-ethereum-hdwallet v0.1.3
github.com/ethereum-optimism/superchain-registry/superchain v0.0.0-20241213092551-33a63fce8214
github.com/ethereum/go-ethereum v1.14.11
github.com/fatih/color v1.16.0
github.com/fsnotify/fsnotify v1.8.0
github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb
github.com/google/go-cmp v0.6.0
......@@ -96,7 +97,6 @@ require (
github.com/elastic/gosigar v0.14.3 // indirect
github.com/ethereum/c-kzg-4844 v1.0.0 // indirect
github.com/ethereum/go-verkle v0.1.1-0.20240829091221-dffa7562dbe9 // indirect
github.com/fatih/color v1.16.0 // indirect
github.com/felixge/fgprof v0.9.5 // indirect
github.com/ferranbt/fastssz v0.1.2 // indirect
github.com/flynn/noise v1.1.0 // indirect
......
package kurtosis
import (
"fmt"
"strconv"
"strings"
"github.com/ethereum-optimism/optimism/kurtosis-devnet/pkg/kurtosis/sources/inspect"
)
// ServiceFinder is the main entry point for finding service endpoints
type ServiceFinder struct {
services inspect.ServiceMap
nodeServices []string
interestingPorts []string
l2ServicePrefix string
}
// ServiceFinderOption configures a ServiceFinder
type ServiceFinderOption func(*ServiceFinder)
// WithNodeServices sets the node service identifiers
func WithNodeServices(services []string) ServiceFinderOption {
return func(f *ServiceFinder) {
f.nodeServices = services
}
}
// WithInterestingPorts sets the ports to look for
func WithInterestingPorts(ports []string) ServiceFinderOption {
return func(f *ServiceFinder) {
f.interestingPorts = ports
}
}
// WithL2ServicePrefix sets the prefix used to identify L2 services
func WithL2ServicePrefix(prefix string) ServiceFinderOption {
return func(f *ServiceFinder) {
f.l2ServicePrefix = prefix
}
}
// NewServiceFinder creates a new ServiceFinder with the given options
func NewServiceFinder(services inspect.ServiceMap, opts ...ServiceFinderOption) *ServiceFinder {
f := &ServiceFinder{
services: services,
nodeServices: []string{"cl", "el"},
interestingPorts: []string{"rpc", "http"},
l2ServicePrefix: "op-",
}
for _, opt := range opts {
opt(f)
}
return f
}
// FindL1Endpoints finds L1 nodes. Currently returns empty endpoints as specified.
func (f *ServiceFinder) FindL1Endpoints() ([]Node, EndpointMap) {
return f.findRPCEndpoints(func(serviceName string) (string, int, bool) {
// Only match services that start with one of the node service identifiers.
// We might have to change this if we need to support L1 services beyond nodes.
for _, service := range f.nodeServices {
if strings.HasPrefix(serviceName, service) {
tag, idx := f.serviceTag(serviceName)
return tag, idx, true
}
}
return "", 0, false
})
}
// FindL2Endpoints finds L2 nodes and endpoints for a specific network
func (f *ServiceFinder) FindL2Endpoints(network string) ([]Node, EndpointMap) {
networkSuffix := "-" + network
return f.findRPCEndpoints(func(serviceName string) (string, int, bool) {
if strings.HasSuffix(serviceName, networkSuffix) {
name := strings.TrimSuffix(serviceName, networkSuffix)
tag, idx := f.serviceTag(strings.TrimPrefix(name, f.l2ServicePrefix))
return tag, idx, true
}
return "", 0, false
})
}
// findRPCEndpoints looks for services matching the given predicate that have an RPC port
func (f *ServiceFinder) findRPCEndpoints(matchService func(string) (string, int, bool)) ([]Node, EndpointMap) {
endpointMap := make(EndpointMap)
var nodes []Node
for serviceName, ports := range f.services {
var portInfo *inspect.PortInfo
for _, interestingPort := range f.interestingPorts {
if p, ok := ports[interestingPort]; ok {
portInfo = &p
break
}
}
if portInfo == nil {
continue
}
if serviceIdentifier, num, ok := matchService(serviceName); ok {
var allocated bool
for _, service := range f.nodeServices {
if serviceIdentifier == service {
if num > len(nodes) {
nodes = append(nodes, make(Node))
}
host := portInfo.Host
if host == "" {
host = "localhost"
}
nodes[num-1][serviceIdentifier] = fmt.Sprintf("http://%s:%d", host, portInfo.Port)
allocated = true
}
}
if !allocated {
host := portInfo.Host
if host == "" {
host = "localhost"
}
endpointMap[serviceIdentifier] = fmt.Sprintf("http://%s:%d", host, portInfo.Port)
}
}
}
return nodes, endpointMap
}
// serviceTag returns the shorthand service tag and index if it's a service with multiple instances
func (f *ServiceFinder) serviceTag(serviceName string) (string, int) {
// Find start of number sequence
start := strings.IndexFunc(serviceName, func(r rune) bool {
return r >= '0' && r <= '9'
})
if start == -1 {
return serviceName, 0
}
// Find end of number sequence
end := start + 1
for end < len(serviceName) && serviceName[end] >= '0' && serviceName[end] <= '9' {
end++
}
idx, err := strconv.Atoi(serviceName[start:end])
if err != nil {
return serviceName, 0
}
return serviceName[:start-1], idx
}
package kurtosis
import (
"testing"
"github.com/ethereum-optimism/optimism/kurtosis-devnet/pkg/kurtosis/sources/inspect"
"github.com/stretchr/testify/assert"
)
func TestFindRPCEndpoints(t *testing.T) {
testServices := make(inspect.ServiceMap)
testServices["el-1-geth-lighthouse"] = inspect.PortMap{
"metrics": {Port: 52643},
"tcp-discovery": {Port: 52644},
"udp-discovery": {Port: 51936},
"engine-rpc": {Port: 52642},
"rpc": {Port: 52645},
"ws": {Port: 52646},
}
testServices["op-batcher-op-kurtosis"] = inspect.PortMap{
"http": {Port: 53572},
}
testServices["op-cl-1-op-node-op-geth-op-kurtosis"] = inspect.PortMap{
"udp-discovery": {Port: 50990},
"http": {Port: 53503},
"tcp-discovery": {Port: 53504},
}
testServices["op-el-1-op-geth-op-node-op-kurtosis"] = inspect.PortMap{
"udp-discovery": {Port: 53233},
"engine-rpc": {Port: 53399},
"metrics": {Port: 53400},
"rpc": {Port: 53402},
"ws": {Port: 53403},
"tcp-discovery": {Port: 53401},
}
testServices["vc-1-geth-lighthouse"] = inspect.PortMap{
"metrics": {Port: 53149},
}
testServices["cl-1-lighthouse-geth"] = inspect.PortMap{
"metrics": {Port: 52691},
"tcp-discovery": {Port: 52692},
"udp-discovery": {Port: 58275},
"http": {Port: 52693},
}
tests := []struct {
name string
services inspect.ServiceMap
findFn func(*ServiceFinder) ([]Node, EndpointMap)
wantNodes []Node
wantEndpoints EndpointMap
}{
{
name: "find L1 endpoints",
services: testServices,
findFn: func(f *ServiceFinder) ([]Node, EndpointMap) {
return f.FindL1Endpoints()
},
wantNodes: []Node{
{
"cl": "http://localhost:52693",
"el": "http://localhost:52645",
},
},
wantEndpoints: EndpointMap{},
},
{
name: "find op-kurtosis L2 endpoints",
services: testServices,
findFn: func(f *ServiceFinder) ([]Node, EndpointMap) {
return f.FindL2Endpoints("op-kurtosis")
},
wantNodes: []Node{
{
"cl": "http://localhost:53503",
"el": "http://localhost:53402",
},
},
wantEndpoints: EndpointMap{
"batcher": "http://localhost:53572",
},
},
{
name: "custom host in endpoint",
services: inspect.ServiceMap{
"op-batcher-custom-host": inspect.PortMap{
"http": {Host: "custom.host", Port: 8080},
},
},
findFn: func(f *ServiceFinder) ([]Node, EndpointMap) {
return f.FindL2Endpoints("custom-host")
},
wantNodes: nil,
wantEndpoints: EndpointMap{
"batcher": "http://custom.host:8080",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
finder := NewServiceFinder(tt.services)
gotNodes, gotEndpoints := tt.findFn(finder)
assert.Equal(t, tt.wantNodes, gotNodes)
assert.Equal(t, tt.wantEndpoints, gotEndpoints)
})
}
}
......@@ -6,13 +6,8 @@ import (
"fmt"
"io"
"os"
"os/exec"
"strconv"
"strings"
"text/template"
"github.com/ethereum-optimism/optimism/kurtosis-devnet/pkg/kurtosis/sources/deployer"
"github.com/ethereum-optimism/optimism/kurtosis-devnet/pkg/kurtosis/sources/inspect"
"github.com/ethereum-optimism/optimism/kurtosis-devnet/pkg/kurtosis/sources/spec"
)
......@@ -51,8 +46,6 @@ type KurtosisEnvironment struct {
type KurtosisDeployer struct {
// Base directory where the deployment commands should be executed
baseDir string
// Template for the deployment command
cmdTemplate *template.Template
// Package name to deploy
packageName string
// Dry run mode
......@@ -63,14 +56,8 @@ type KurtosisDeployer struct {
enclaveSpec EnclaveSpecifier
enclaveInspecter EnclaveInspecter
enclaveObserver EnclaveObserver
}
const cmdTemplateStr = "just _kurtosis-run {{.PackageName}} {{.ArgFile}} {{.Enclave}}"
var defaultCmdTemplate *template.Template
func init() {
defaultCmdTemplate = template.Must(template.New("kurtosis_deploy_cmd").Parse(cmdTemplateStr))
kurtosisCtx kurtosisContextInterface
runHandlers []MessageHandler
}
type KurtosisDeployerOptions func(*KurtosisDeployer)
......@@ -81,12 +68,6 @@ func WithKurtosisBaseDir(baseDir string) KurtosisDeployerOptions {
}
}
func WithKurtosisCmdTemplate(cmdTemplate *template.Template) KurtosisDeployerOptions {
return func(d *KurtosisDeployer) {
d.cmdTemplate = cmdTemplate
}
}
func WithKurtosisPackageName(packageName string) KurtosisDeployerOptions {
return func(d *KurtosisDeployer) {
d.packageName = packageName
......@@ -123,14 +104,19 @@ func WithKurtosisEnclaveObserver(enclaveObserver EnclaveObserver) KurtosisDeploy
}
}
func WithKurtosisRunHandlers(runHandlers []MessageHandler) KurtosisDeployerOptions {
return func(d *KurtosisDeployer) {
d.runHandlers = runHandlers
}
}
// NewKurtosisDeployer creates a new KurtosisDeployer instance
func NewKurtosisDeployer(opts ...KurtosisDeployerOptions) *KurtosisDeployer {
d := &KurtosisDeployer{
baseDir: ".",
cmdTemplate: defaultCmdTemplate,
packageName: DefaultPackageName,
dryRun: false,
enclave: "devnet",
enclave: DefaultEnclave,
enclaveSpec: &enclaveSpecAdapter{},
enclaveInspecter: &enclaveInspectAdapter{},
......@@ -144,99 +130,6 @@ func NewKurtosisDeployer(opts ...KurtosisDeployerOptions) *KurtosisDeployer {
return d
}
// templateData holds the data for the command template
type templateData struct {
PackageName string
ArgFile string
Enclave string
}
// TODO: the following functions follow closely the naming convensions in place
// with optimism-package. We should probably make them a bit more generic, to
// make the whole thing less fragile.
// findRPCEndpoint looks for a service matching the given predicate that has an RPC port
func findRPCEndpoints(services inspect.ServiceMap, matchService func(string) (string, int, bool)) ([]Node, EndpointMap) {
interestingPorts := []string{"rpc", "http"}
nodeServices := []string{"cl", "el"}
endpointMap := make(EndpointMap)
var nodes []Node
for serviceName, ports := range services {
var port int
for _, interestingPort := range interestingPorts {
if p, ok := ports[interestingPort]; ok {
port = p
break
}
}
if port == 0 { // nothing to see here
continue
}
if serviceIdentifier, num, ok := matchService(serviceName); ok {
var allocated bool
for _, service := range nodeServices {
if serviceIdentifier == service { // this is a node
if num > len(nodes) {
nodes = append(nodes, make(Node))
}
nodes[num-1][serviceIdentifier] = fmt.Sprintf("http://localhost:%d", port)
allocated = true
}
}
if !allocated {
endpointMap[serviceIdentifier] = fmt.Sprintf("http://localhost:%d", port)
}
}
}
return nodes, endpointMap
}
// return the shorthand service tag (used as key in the final output) and the
// index if that's a service with multiple instances.
func serviceTag(serviceName string) (string, int) {
// Find index of first number
i := strings.IndexFunc(serviceName, func(r rune) bool {
return r >= '0' && r <= '9'
})
if i == -1 {
return serviceName, 0
}
idx, err := strconv.Atoi(serviceName[i : i+1])
if err != nil {
return serviceName, 0
}
return serviceName[:i-1], idx
}
const l2ServiceTagPrefix = "op-"
func findL2Endpoints(services inspect.ServiceMap, suffix string) ([]Node, EndpointMap) {
return findRPCEndpoints(services, func(serviceName string) (string, int, bool) {
if strings.HasSuffix(serviceName, suffix) {
name := strings.TrimSuffix(serviceName, suffix)
tag, idx := serviceTag(strings.TrimPrefix(name, l2ServiceTagPrefix))
return tag, idx, true
}
return "", 0, false
})
}
// TODO: L1 services are detected as "non-L2" right now. That might need to change
// in the future, but for now it's good enough.
func findL1Endpoints(services inspect.ServiceMap) ([]Node, EndpointMap) {
return findRPCEndpoints(services, func(serviceName string) (string, int, bool) {
match := !strings.HasPrefix(serviceName, l2ServiceTagPrefix)
if match {
tag, idx := serviceTag(serviceName)
return tag, idx, true
}
return "", 0, false
})
}
// prepareArgFile creates a temporary file with the input content and returns its path
// The caller is responsible for deleting the file.
func (d *KurtosisDeployer) prepareArgFile(input io.Reader) (string, error) {
......@@ -254,38 +147,6 @@ func (d *KurtosisDeployer) prepareArgFile(input io.Reader) (string, error) {
return argFile.Name(), nil
}
// runKurtosisCommand executes the kurtosis command with the given arguments
// TODO: reimplement this with the kurtosis SDK, it'll be cleaner.
func (d *KurtosisDeployer) runKurtosisCommand(argFile string) error {
data := templateData{
PackageName: d.packageName,
ArgFile: argFile,
Enclave: d.enclave,
}
var cmdBuf bytes.Buffer
if err := d.cmdTemplate.Execute(&cmdBuf, data); err != nil {
return fmt.Errorf("failed to execute command template: %w", err)
}
if d.dryRun {
fmt.Println("Dry run mode enabled, kurtosis would run the following command:")
fmt.Println(cmdBuf.String())
return nil
}
cmd := exec.Command("sh", "-c", cmdBuf.String())
cmd.Dir = d.baseDir
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("kurtosis deployment failed: %w", err)
}
return nil
}
func (d *KurtosisDeployer) getWallets(wallets deployer.WalletList) WalletMap {
walletMap := make(WalletMap)
for _, wallet := range wallets {
......@@ -316,7 +177,8 @@ func (d *KurtosisDeployer) getEnvironmentInfo(ctx context.Context, spec *spec.En
}
// Find L1 endpoint
if nodes, endpoints := findL1Endpoints(inspectResult.UserServices); len(nodes) > 0 {
finder := NewServiceFinder(inspectResult.UserServices)
if nodes, endpoints := finder.FindL1Endpoints(); len(nodes) > 0 {
env.L1 = &Chain{
Name: "Ethereum",
Services: endpoints,
......@@ -326,7 +188,7 @@ func (d *KurtosisDeployer) getEnvironmentInfo(ctx context.Context, spec *spec.En
// Find L2 endpoints
for _, chainSpec := range spec.Chains {
nodes, endpoints := findL2Endpoints(inspectResult.UserServices, fmt.Sprintf("-%s", chainSpec.Name))
nodes, endpoints := finder.FindL2Endpoints(chainSpec.Name)
chain := &Chain{
Name: chainSpec.Name,
......@@ -365,7 +227,7 @@ func (d *KurtosisDeployer) Deploy(ctx context.Context, input io.Reader) (*Kurtos
defer os.Remove(argFile)
// Run kurtosis command
if err := d.runKurtosisCommand(argFile); err != nil {
if err := d.runKurtosis(ctx, argFile); err != nil {
return nil, err
}
......
package kurtosis
import (
"context"
"errors"
"fmt"
"os"
"github.com/fatih/color"
"github.com/kurtosis-tech/kurtosis/api/golang/core/kurtosis_core_rpc_api_bindings"
"github.com/kurtosis-tech/kurtosis/api/golang/core/lib/enclaves"
"github.com/kurtosis-tech/kurtosis/api/golang/core/lib/starlark_run_config"
"github.com/kurtosis-tech/kurtosis/api/golang/engine/lib/kurtosis_context"
)
// Color printers
var (
printCyan = color.New(color.FgCyan).SprintFunc()
printYellow = color.New(color.FgYellow).SprintFunc()
printRed = color.New(color.FgRed).SprintFunc()
printBlue = color.New(color.FgBlue).SprintFunc()
)
// MessageHandler defines the interface for handling different types of messages
type MessageHandler interface {
// Handle processes the message if applicable and returns:
// - bool: whether the message was handled
// - error: any error that occurred during handling
Handle(context.Context, StarlarkResponse) (bool, error)
}
// MessageHandlerFunc is a function type that implements MessageHandler
type MessageHandlerFunc func(context.Context, StarlarkResponse) (bool, error)
func (f MessageHandlerFunc) Handle(ctx context.Context, resp StarlarkResponse) (bool, error) {
return f(ctx, resp)
}
// FirstMatchHandler returns a handler that applies the first matching handler from the given handlers
func FirstMatchHandler(handlers ...MessageHandler) MessageHandler {
return MessageHandlerFunc(func(ctx context.Context, resp StarlarkResponse) (bool, error) {
for _, h := range handlers {
handled, err := h.Handle(ctx, resp)
if err != nil {
return true, err
}
if handled {
return true, nil
}
}
return false, nil
})
}
// AllHandlers returns a handler that applies all the given handlers in order
func AllHandlers(handlers ...MessageHandler) MessageHandler {
return MessageHandlerFunc(func(ctx context.Context, resp StarlarkResponse) (bool, error) {
anyHandled := false
for _, h := range handlers {
handled, err := h.Handle(ctx, resp)
if err != nil {
return true, err
}
anyHandled = anyHandled || handled
}
return anyHandled, nil
})
}
// defaultHandler is the default message handler that provides standard Kurtosis output
var defaultHandler = FirstMatchHandler(
MessageHandlerFunc(handleProgress),
MessageHandlerFunc(handleInstruction),
MessageHandlerFunc(handleWarning),
MessageHandlerFunc(handleInfo),
MessageHandlerFunc(handleResult),
MessageHandlerFunc(handleError),
)
// handleProgress handles progress info messages
func handleProgress(ctx context.Context, resp StarlarkResponse) (bool, error) {
if progressInfo := resp.GetProgressInfo(); progressInfo != nil {
// ignore progress messages, same as kurtosis run does
return true, nil
}
return false, nil
}
// handleInstruction handles instruction messages
func handleInstruction(ctx context.Context, resp StarlarkResponse) (bool, error) {
if instruction := resp.GetInstruction(); instruction != nil {
desc := instruction.GetDescription()
fmt.Println(printCyan(desc))
return true, nil
}
return false, nil
}
// handleWarning handles warning messages
func handleWarning(ctx context.Context, resp StarlarkResponse) (bool, error) {
if warning := resp.GetWarning(); warning != nil {
fmt.Println(printYellow(warning.GetMessage()))
return true, nil
}
return false, nil
}
// handleInfo handles info messages
func handleInfo(ctx context.Context, resp StarlarkResponse) (bool, error) {
if info := resp.GetInfo(); info != nil {
fmt.Println(printBlue(info.GetMessage()))
return true, nil
}
return false, nil
}
// handleResult handles instruction result messages
func handleResult(ctx context.Context, resp StarlarkResponse) (bool, error) {
if result := resp.GetInstructionResult(); result != nil {
if result.GetSerializedInstructionResult() != "" {
fmt.Printf("%s\n\n", result.GetSerializedInstructionResult())
}
return true, nil
}
return false, nil
}
// handleError handles error messages
func handleError(ctx context.Context, resp StarlarkResponse) (bool, error) {
if err := resp.GetError(); err != nil {
if interpretErr := err.GetInterpretationError(); interpretErr != nil {
return true, fmt.Errorf(printRed("interpretation error: %v"), interpretErr)
}
if validationErr := err.GetValidationError(); validationErr != nil {
return true, fmt.Errorf(printRed("validation error: %v"), validationErr)
}
if executionErr := err.GetExecutionError(); executionErr != nil {
return true, fmt.Errorf(printRed("execution error: %v"), executionErr)
}
return true, nil
}
return false, nil
}
// makeRunFinishedHandler creates a handler for run finished events
func makeRunFinishedHandler(isSuccessful *bool) MessageHandlerFunc {
return func(ctx context.Context, resp StarlarkResponse) (bool, error) {
if event := resp.GetRunFinishedEvent(); event != nil {
*isSuccessful = event.GetIsRunSuccessful()
return true, nil
}
return false, nil
}
}
// Interfaces for Kurtosis SDK types to make testing easier
type StarlarkError interface {
GetInterpretationError() error
GetValidationError() error
GetExecutionError() error
}
type ProgressInfo interface {
GetCurrentStepInfo() []string
}
type Instruction interface {
GetDescription() string
}
type RunFinishedEvent interface {
GetIsRunSuccessful() bool
}
type Warning interface {
GetMessage() string
}
type Info interface {
GetMessage() string
}
type InstructionResult interface {
GetSerializedInstructionResult() string
}
type StarlarkResponse interface {
GetError() StarlarkError
GetProgressInfo() ProgressInfo
GetInstruction() Instruction
GetRunFinishedEvent() RunFinishedEvent
GetWarning() Warning
GetInfo() Info
GetInstructionResult() InstructionResult
}
type enclaveContext interface {
RunStarlarkRemotePackage(context.Context, string, *starlark_run_config.StarlarkRunConfig) (<-chan StarlarkResponse, string, error)
}
type kurtosisContextInterface interface {
CreateEnclave(context.Context, string) (enclaveContext, error)
GetEnclave(context.Context, string) (enclaveContext, error)
}
// Wrapper types to implement our interfaces
type kurtosisContextWrapper struct {
*kurtosis_context.KurtosisContext
}
type enclaveContextWrapper struct {
*enclaves.EnclaveContext
}
type starlarkResponseWrapper struct {
*kurtosis_core_rpc_api_bindings.StarlarkRunResponseLine
}
type starlarkErrorWrapper struct {
*kurtosis_core_rpc_api_bindings.StarlarkError
}
type progressInfoWrapper struct {
*kurtosis_core_rpc_api_bindings.StarlarkRunProgress
}
type instructionWrapper struct {
*kurtosis_core_rpc_api_bindings.StarlarkInstruction
}
type runFinishedEventWrapper struct {
*kurtosis_core_rpc_api_bindings.StarlarkRunFinishedEvent
}
type warningWrapper struct {
*kurtosis_core_rpc_api_bindings.StarlarkWarning
}
type infoWrapper struct {
*kurtosis_core_rpc_api_bindings.StarlarkInfo
}
type instructionResultWrapper struct {
*kurtosis_core_rpc_api_bindings.StarlarkInstructionResult
}
func (w kurtosisContextWrapper) CreateEnclave(ctx context.Context, name string) (enclaveContext, error) {
enclaveCtx, err := w.KurtosisContext.CreateEnclave(ctx, name)
if err != nil {
return nil, err
}
return &enclaveContextWrapper{enclaveCtx}, nil
}
func (w kurtosisContextWrapper) GetEnclave(ctx context.Context, name string) (enclaveContext, error) {
enclaveCtx, err := w.KurtosisContext.GetEnclaveContext(ctx, name)
if err != nil {
return nil, err
}
return &enclaveContextWrapper{enclaveCtx}, nil
}
func (w *enclaveContextWrapper) RunStarlarkRemotePackage(ctx context.Context, packageId string, serializedParams *starlark_run_config.StarlarkRunConfig) (<-chan StarlarkResponse, string, error) {
stream, cancel, err := w.EnclaveContext.RunStarlarkRemotePackage(ctx, packageId, serializedParams)
if err != nil {
return nil, "", err
}
// Convert the stream
wrappedStream := make(chan StarlarkResponse)
go func() {
defer close(wrappedStream)
defer cancel()
for line := range stream {
wrappedStream <- &starlarkResponseWrapper{line}
}
}()
return wrappedStream, "", nil
}
func (w *starlarkResponseWrapper) GetError() StarlarkError {
if err := w.StarlarkRunResponseLine.GetError(); err != nil {
return &starlarkErrorWrapper{err}
}
return nil
}
func (w *starlarkResponseWrapper) GetProgressInfo() ProgressInfo {
if progress := w.StarlarkRunResponseLine.GetProgressInfo(); progress != nil {
return &progressInfoWrapper{progress}
}
return nil
}
func (w *starlarkResponseWrapper) GetInstruction() Instruction {
if instruction := w.StarlarkRunResponseLine.GetInstruction(); instruction != nil {
return &instructionWrapper{instruction}
}
return nil
}
func (w *starlarkResponseWrapper) GetRunFinishedEvent() RunFinishedEvent {
if event := w.StarlarkRunResponseLine.GetRunFinishedEvent(); event != nil {
return &runFinishedEventWrapper{event}
}
return nil
}
func (w *starlarkResponseWrapper) GetWarning() Warning {
if warning := w.StarlarkRunResponseLine.GetWarning(); warning != nil {
return &warningWrapper{warning}
}
return nil
}
func (w *starlarkResponseWrapper) GetInfo() Info {
if info := w.StarlarkRunResponseLine.GetInfo(); info != nil {
return &infoWrapper{info}
}
return nil
}
func (w *starlarkResponseWrapper) GetInstructionResult() InstructionResult {
if result := w.StarlarkRunResponseLine.GetInstructionResult(); result != nil {
return &instructionResultWrapper{result}
}
return nil
}
func (w *progressInfoWrapper) GetCurrentStepInfo() []string {
return w.StarlarkRunProgress.CurrentStepInfo
}
func (w *instructionWrapper) GetDescription() string {
return w.StarlarkInstruction.Description
}
func (w *runFinishedEventWrapper) GetIsRunSuccessful() bool {
return w.StarlarkRunFinishedEvent.IsRunSuccessful
}
func (w *starlarkErrorWrapper) GetInterpretationError() error {
if err := w.StarlarkError.GetInterpretationError(); err != nil {
return fmt.Errorf("%v", err)
}
return nil
}
func (w *starlarkErrorWrapper) GetValidationError() error {
if err := w.StarlarkError.GetValidationError(); err != nil {
return fmt.Errorf("%v", err)
}
return nil
}
func (w *starlarkErrorWrapper) GetExecutionError() error {
if err := w.StarlarkError.GetExecutionError(); err != nil {
return fmt.Errorf("%v", err)
}
return nil
}
func (w *warningWrapper) GetMessage() string {
return w.StarlarkWarning.WarningMessage
}
func (w *infoWrapper) GetMessage() string {
return w.StarlarkInfo.InfoMessage
}
func (w *instructionResultWrapper) GetSerializedInstructionResult() string {
return w.StarlarkInstructionResult.SerializedInstructionResult
}
// runKurtosis executes the kurtosis package using the SDK
func (d *KurtosisDeployer) runKurtosis(ctx context.Context, argFile string) error {
if d.dryRun {
fmt.Printf("Dry run mode enabled, would run kurtosis package %s with args file %s in enclave %s\n",
d.packageName, argFile, d.enclave)
return nil
}
// Create Kurtosis context if not already set (for testing)
if d.kurtosisCtx == nil {
var err error
kCtx, err := kurtosis_context.NewKurtosisContextFromLocalEngine()
if err != nil {
return fmt.Errorf("failed to create Kurtosis context: %w", err)
}
d.kurtosisCtx = kurtosisContextWrapper{kCtx}
}
// Try to get existing enclave first
enclaveCtx, err := d.kurtosisCtx.GetEnclave(ctx, d.enclave)
if err != nil {
// If enclave doesn't exist, create a new one
fmt.Printf("Creating a new enclave for Starlark to run inside...\n")
enclaveCtx, err = d.kurtosisCtx.CreateEnclave(ctx, d.enclave)
if err != nil {
return fmt.Errorf("failed to create enclave: %w", err)
}
fmt.Printf("Enclave '%s' created successfully\n\n", d.enclave)
} else {
fmt.Printf("Using existing enclave '%s'\n\n", d.enclave)
}
var serializedParams string
if argFile != "" {
argsBytes, err := os.ReadFile(argFile)
if err != nil {
return fmt.Errorf("failed to read args file: %w", err)
}
serializedParams = string(argsBytes)
}
runConfig := &starlark_run_config.StarlarkRunConfig{
SerializedParams: serializedParams,
}
stream, _, err := enclaveCtx.RunStarlarkRemotePackage(ctx, d.packageName, runConfig)
if err != nil {
return fmt.Errorf("failed to run Kurtosis package: %w", err)
}
// Set up message handlers
var isRunSuccessful bool
runFinishedHandler := makeRunFinishedHandler(&isRunSuccessful)
// Combine custom handlers with default handler and run finished handler
handler := AllHandlers(append(d.runHandlers, defaultHandler, runFinishedHandler)...)
// Process the output stream
for responseLine := range stream {
if _, err := handler.Handle(ctx, responseLine); err != nil {
return err
}
}
if !isRunSuccessful {
return errors.New(printRed("kurtosis package execution failed"))
}
return nil
}
package kurtosis
import (
"context"
"fmt"
"testing"
"github.com/kurtosis-tech/kurtosis/api/golang/core/lib/starlark_run_config"
"github.com/stretchr/testify/assert"
)
// fakeStarlarkResponse implements StarlarkResponse for testing
type fakeStarlarkResponse struct {
err StarlarkError
progressMsg []string
instruction string
isSuccessful bool
warning string
info string
result string
hasResult bool // tracks whether result was explicitly set
}
func (f *fakeStarlarkResponse) GetError() StarlarkError {
return f.err
}
func (f *fakeStarlarkResponse) GetProgressInfo() ProgressInfo {
if f.progressMsg != nil {
return &fakeProgressInfo{info: f.progressMsg}
}
return nil
}
func (f *fakeStarlarkResponse) GetInstruction() Instruction {
if f.instruction != "" {
return &fakeInstruction{desc: f.instruction}
}
return nil
}
func (f *fakeStarlarkResponse) GetRunFinishedEvent() RunFinishedEvent {
return &fakeRunFinishedEvent{isSuccessful: f.isSuccessful}
}
func (f *fakeStarlarkResponse) GetWarning() Warning {
if f.warning != "" {
return &fakeWarning{msg: f.warning}
}
return nil
}
func (f *fakeStarlarkResponse) GetInfo() Info {
if f.info != "" {
return &fakeInfo{msg: f.info}
}
return nil
}
func (f *fakeStarlarkResponse) GetInstructionResult() InstructionResult {
if !f.hasResult {
return nil
}
return &fakeInstructionResult{result: f.result}
}
// fakeProgressInfo implements ProgressInfo for testing
type fakeProgressInfo struct {
info []string
}
func (f *fakeProgressInfo) GetCurrentStepInfo() []string {
return f.info
}
// fakeInstruction implements Instruction for testing
type fakeInstruction struct {
desc string
}
func (f *fakeInstruction) GetDescription() string {
return f.desc
}
// fakeStarlarkError implements StarlarkError for testing
type fakeStarlarkError struct {
interpretationErr error
validationErr error
executionErr error
}
func (f *fakeStarlarkError) GetInterpretationError() error {
return f.interpretationErr
}
func (f *fakeStarlarkError) GetValidationError() error {
return f.validationErr
}
func (f *fakeStarlarkError) GetExecutionError() error {
return f.executionErr
}
// fakeRunFinishedEvent implements RunFinishedEvent for testing
type fakeRunFinishedEvent struct {
isSuccessful bool
}
func (f *fakeRunFinishedEvent) GetIsRunSuccessful() bool {
return f.isSuccessful
}
// fakeWarning implements Warning for testing
type fakeWarning struct {
msg string
}
func (f *fakeWarning) GetMessage() string {
return f.msg
}
// fakeInfo implements Info for testing
type fakeInfo struct {
msg string
}
func (f *fakeInfo) GetMessage() string {
return f.msg
}
// fakeInstructionResult implements InstructionResult for testing
type fakeInstructionResult struct {
result string
}
func (f *fakeInstructionResult) GetSerializedInstructionResult() string {
return f.result
}
// fakeKurtosisContext implements a fake KurtosisContext for testing
type fakeKurtosisContext struct {
enclaveCtx *fakeEnclaveContext
createErr error
getErr error
}
func (f *fakeKurtosisContext) CreateEnclave(ctx context.Context, name string) (enclaveContext, error) {
if f.createErr != nil {
return nil, f.createErr
}
return f.enclaveCtx, nil
}
func (f *fakeKurtosisContext) GetEnclave(ctx context.Context, name string) (enclaveContext, error) {
if f.getErr != nil {
return nil, f.getErr
}
return f.enclaveCtx, nil
}
// fakeEnclaveContext implements a fake EnclaveContext for testing
type fakeEnclaveContext struct {
runErr error
responses []fakeStarlarkResponse
}
func (f *fakeEnclaveContext) RunStarlarkRemotePackage(ctx context.Context, packageId string, serializedParams *starlark_run_config.StarlarkRunConfig) (<-chan StarlarkResponse, string, error) {
if f.runErr != nil {
return nil, "", f.runErr
}
responseChan := make(chan StarlarkResponse)
go func() {
defer close(responseChan)
// Send all test responses
for _, resp := range f.responses {
responseChan <- &resp
}
}()
return responseChan, "", nil
}
func TestRunKurtosis(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
testErr := fmt.Errorf("test error")
tests := []struct {
name string
responses []fakeStarlarkResponse
kurtosisErr error
getErr error
wantErr bool
}{
{
name: "successful run with all message types",
responses: []fakeStarlarkResponse{
{progressMsg: []string{"Starting deployment..."}},
{info: "Preparing environment"},
{instruction: "Executing package"},
{warning: "Using default config"},
{result: "Service started", hasResult: true},
{progressMsg: []string{"Deployment complete"}},
{isSuccessful: true},
},
wantErr: false,
},
{
name: "run with error",
responses: []fakeStarlarkResponse{
{progressMsg: []string{"Starting deployment..."}},
{err: &fakeStarlarkError{executionErr: testErr}},
},
wantErr: true,
},
{
name: "run with unsuccessful completion",
responses: []fakeStarlarkResponse{
{progressMsg: []string{"Starting deployment..."}},
{isSuccessful: false},
},
wantErr: true,
},
{
name: "kurtosis error",
kurtosisErr: fmt.Errorf("kurtosis failed"),
wantErr: true,
},
{
name: "uses existing enclave",
responses: []fakeStarlarkResponse{
{progressMsg: []string{"Using existing enclave"}},
{isSuccessful: true},
},
getErr: nil,
wantErr: false,
},
{
name: "creates new enclave when get fails",
responses: []fakeStarlarkResponse{
{progressMsg: []string{"Creating new enclave"}},
{isSuccessful: true},
},
getErr: fmt.Errorf("enclave not found"),
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Create a fake enclave context that will return our test responses
fakeCtx := &fakeKurtosisContext{
enclaveCtx: &fakeEnclaveContext{
runErr: tt.kurtosisErr,
responses: tt.responses,
},
getErr: tt.getErr,
}
d := NewKurtosisDeployer()
d.kurtosisCtx = fakeCtx
err := d.runKurtosis(ctx, "")
if tt.wantErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
})
}
}
func TestHandleProgress(t *testing.T) {
ctx := context.Background()
tests := []struct {
name string
response StarlarkResponse
want bool
}{
{
name: "handles progress message",
response: &fakeStarlarkResponse{
progressMsg: []string{"Step 1", "Step 2"},
},
want: true,
},
{
name: "ignores non-progress message",
response: &fakeStarlarkResponse{},
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
handled, err := handleProgress(ctx, tt.response)
assert.NoError(t, err)
assert.Equal(t, tt.want, handled)
})
}
}
func TestHandleInstruction(t *testing.T) {
ctx := context.Background()
tests := []struct {
name string
response StarlarkResponse
want bool
}{
{
name: "handles instruction message",
response: &fakeStarlarkResponse{
instruction: "Execute command",
},
want: true,
},
{
name: "ignores non-instruction message",
response: &fakeStarlarkResponse{},
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
handled, err := handleInstruction(ctx, tt.response)
assert.NoError(t, err)
assert.Equal(t, tt.want, handled)
})
}
}
func TestHandleWarning(t *testing.T) {
ctx := context.Background()
tests := []struct {
name string
response StarlarkResponse
want bool
}{
{
name: "handles warning message",
response: &fakeStarlarkResponse{
warning: "Warning: deprecated feature",
},
want: true,
},
{
name: "ignores non-warning message",
response: &fakeStarlarkResponse{},
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
handled, err := handleWarning(ctx, tt.response)
assert.NoError(t, err)
assert.Equal(t, tt.want, handled)
})
}
}
func TestHandleInfo(t *testing.T) {
ctx := context.Background()
tests := []struct {
name string
response StarlarkResponse
want bool
}{
{
name: "handles info message",
response: &fakeStarlarkResponse{
info: "System info",
},
want: true,
},
{
name: "ignores non-info message",
response: &fakeStarlarkResponse{},
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
handled, err := handleInfo(ctx, tt.response)
assert.NoError(t, err)
assert.Equal(t, tt.want, handled)
})
}
}
func TestHandleResult(t *testing.T) {
ctx := context.Background()
tests := []struct {
name string
response StarlarkResponse
want bool
}{
{
name: "handles result message",
response: &fakeStarlarkResponse{
result: "Operation completed",
hasResult: true,
},
want: true,
},
{
name: "handles empty result message",
response: &fakeStarlarkResponse{
result: "",
hasResult: true,
},
want: true,
},
{
name: "ignores non-result message",
response: &fakeStarlarkResponse{},
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
handled, err := handleResult(ctx, tt.response)
assert.NoError(t, err)
assert.Equal(t, tt.want, handled)
})
}
}
func TestHandleError(t *testing.T) {
ctx := context.Background()
testErr := fmt.Errorf("test error")
tests := []struct {
name string
response StarlarkResponse
want bool
wantError bool
}{
{
name: "handles interpretation error",
response: &fakeStarlarkResponse{
err: &fakeStarlarkError{interpretationErr: testErr},
},
want: true,
wantError: true,
},
{
name: "handles validation error",
response: &fakeStarlarkResponse{
err: &fakeStarlarkError{validationErr: testErr},
},
want: true,
wantError: true,
},
{
name: "handles execution error",
response: &fakeStarlarkResponse{
err: &fakeStarlarkError{executionErr: testErr},
},
want: true,
wantError: true,
},
{
name: "ignores non-error message",
response: &fakeStarlarkResponse{},
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
handled, err := handleError(ctx, tt.response)
if tt.wantError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
assert.Equal(t, tt.want, handled)
})
}
}
func TestFirstMatchHandler(t *testing.T) {
ctx := context.Background()
testErr := fmt.Errorf("test error")
tests := []struct {
name string
handlers []MessageHandler
response StarlarkResponse
want bool
wantError bool
}{
{
name: "first handler matches",
handlers: []MessageHandler{
MessageHandlerFunc(handleInfo),
MessageHandlerFunc(handleWarning),
},
response: &fakeStarlarkResponse{
info: "test info",
},
want: true,
},
{
name: "second handler matches",
handlers: []MessageHandler{
MessageHandlerFunc(handleInfo),
MessageHandlerFunc(handleWarning),
},
response: &fakeStarlarkResponse{
warning: "test warning",
},
want: true,
},
{
name: "no handlers match",
handlers: []MessageHandler{
MessageHandlerFunc(handleInfo),
MessageHandlerFunc(handleWarning),
},
response: &fakeStarlarkResponse{
result: "test result", hasResult: true,
},
want: false,
},
{
name: "handler returns error",
handlers: []MessageHandler{
MessageHandlerFunc(handleError),
},
response: &fakeStarlarkResponse{
err: &fakeStarlarkError{interpretationErr: testErr},
},
want: true,
wantError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
handler := FirstMatchHandler(tt.handlers...)
handled, err := handler.Handle(ctx, tt.response)
if tt.wantError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
assert.Equal(t, tt.want, handled)
})
}
}
func TestAllHandlers(t *testing.T) {
ctx := context.Background()
testErr := fmt.Errorf("test error")
tests := []struct {
name string
handlers []MessageHandler
response StarlarkResponse
want bool
wantError bool
}{
{
name: "multiple handlers match",
handlers: []MessageHandler{
MessageHandlerFunc(func(ctx context.Context, resp StarlarkResponse) (bool, error) {
return true, nil
}),
MessageHandlerFunc(func(ctx context.Context, resp StarlarkResponse) (bool, error) {
return true, nil
}),
},
response: &fakeStarlarkResponse{},
want: true,
},
{
name: "some handlers match",
handlers: []MessageHandler{
MessageHandlerFunc(handleInfo),
MessageHandlerFunc(handleWarning),
},
response: &fakeStarlarkResponse{
info: "test info",
},
want: true,
},
{
name: "no handlers match",
handlers: []MessageHandler{
MessageHandlerFunc(handleInfo),
MessageHandlerFunc(handleWarning),
},
response: &fakeStarlarkResponse{
result: "test result", hasResult: true,
},
want: false,
},
{
name: "handler returns error",
handlers: []MessageHandler{
MessageHandlerFunc(handleInfo),
MessageHandlerFunc(handleError),
},
response: &fakeStarlarkResponse{
err: &fakeStarlarkError{interpretationErr: testErr},
},
want: true,
wantError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
handler := AllHandlers(tt.handlers...)
handled, err := handler.Handle(ctx, tt.response)
if tt.wantError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
assert.Equal(t, tt.want, handled)
})
}
}
......@@ -7,7 +7,6 @@ import (
"os"
"strings"
"testing"
"text/template"
"github.com/ethereum-optimism/optimism/kurtosis-devnet/pkg/kurtosis/sources/deployer"
"github.com/ethereum-optimism/optimism/kurtosis-devnet/pkg/kurtosis/sources/inspect"
......@@ -16,89 +15,6 @@ import (
"github.com/stretchr/testify/require"
)
func TestFindRPCEndpoints(t *testing.T) {
testServices := inspect.ServiceMap{
"el-1-geth-lighthouse": {
"metrics": 52643,
"tcp-discovery": 52644,
"udp-discovery": 51936,
"engine-rpc": 52642,
"rpc": 52645,
"ws": 52646,
},
"op-batcher-op-kurtosis": {
"http": 53572,
},
"op-cl-1-op-node-op-geth-op-kurtosis": {
"udp-discovery": 50990,
"http": 53503,
"tcp-discovery": 53504,
},
"op-el-1-op-geth-op-node-op-kurtosis": {
"udp-discovery": 53233,
"engine-rpc": 53399,
"metrics": 53400,
"rpc": 53402,
"ws": 53403,
"tcp-discovery": 53401,
},
"vc-1-geth-lighthouse": {
"metrics": 53149,
},
"cl-1-lighthouse-geth": {
"metrics": 52691,
"tcp-discovery": 52692,
"udp-discovery": 58275,
"http": 52693,
},
}
tests := []struct {
name string
services inspect.ServiceMap
lookupFn func(inspect.ServiceMap) ([]Node, EndpointMap)
wantNodes []Node
wantEndpoints EndpointMap
}{
{
name: "find L1 endpoints",
services: testServices,
lookupFn: findL1Endpoints,
wantNodes: []Node{
{
"cl": "http://localhost:52693",
"el": "http://localhost:52645",
},
},
wantEndpoints: EndpointMap{},
},
{
name: "find op-kurtosis L2 endpoints",
services: testServices,
lookupFn: func(services inspect.ServiceMap) ([]Node, EndpointMap) {
return findL2Endpoints(services, "-op-kurtosis")
},
wantNodes: []Node{
{
"cl": "http://localhost:53503",
"el": "http://localhost:53402",
},
},
wantEndpoints: EndpointMap{
"batcher": "http://localhost:53572",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotNodes, gotEndpoints := tt.lookupFn(tt.services)
assert.Equal(t, tt.wantNodes, gotNodes)
assert.Equal(t, tt.wantEndpoints, gotEndpoints)
})
}
}
func TestKurtosisDeployer(t *testing.T) {
tests := []struct {
name string
......@@ -158,53 +74,6 @@ func TestPrepareArgFile(t *testing.T) {
assert.Equal(t, "test content", string(content))
}
func TestRunKurtosisCommand(t *testing.T) {
fakeCmdTemplate := template.Must(template.New("fake_cmd").Parse("echo 'would run: {{.PackageName}} {{.ArgFile}} {{.Enclave}}'"))
tests := []struct {
name string
dryRun bool
wantError bool
wantOutput bool
cmdTemplate *template.Template
}{
{
name: "dry run",
dryRun: true,
wantError: false,
cmdTemplate: fakeCmdTemplate,
},
{
name: "successful run",
dryRun: false,
wantError: false,
wantOutput: true,
cmdTemplate: fakeCmdTemplate,
},
{
name: "template error",
dryRun: false,
wantError: true,
cmdTemplate: template.Must(template.New("bad_cmd").Parse("{{.NonExistentField}}")),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
d := NewKurtosisDeployer(
WithKurtosisDryRun(tt.dryRun),
WithKurtosisCmdTemplate(tt.cmdTemplate),
)
err := d.runKurtosisCommand("test.yaml")
if tt.wantError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
})
}
}
// fakeEnclaveInspecter implements EnclaveInspecter for testing
type fakeEnclaveInspecter struct {
result *inspect.InspectData
......@@ -236,9 +105,6 @@ func (f *fakeEnclaveSpecifier) EnclaveSpec(r io.Reader) (*spec.EnclaveSpec, erro
}
func TestDeploy(t *testing.T) {
// Create a template that just echoes the command that would be run
fakeCmdTemplate := template.Must(template.New("fake_cmd").Parse("echo 'would run: {{.PackageName}} {{.ArgFile}} {{.Enclave}}'"))
testSpecWithL2 := &spec.EnclaveSpec{
Chains: []spec.ChainSpec{
{
......@@ -252,19 +118,27 @@ func TestDeploy(t *testing.T) {
Chains: []spec.ChainSpec{},
}
testServices := inspect.ServiceMap{
"el-1-geth-lighthouse": {
"rpc": 52645,
},
"op-el-1-op-geth-op-node-op-kurtosis": {
"rpc": 53402,
},
"op-cl-1-op-node-op-geth-op-kurtosis": {
"http": 53503,
},
"op-batcher-op-kurtosis": {
"http": 53572,
},
// Define successful responses that will be used in multiple test cases
successResponses := []fakeStarlarkResponse{
{progressMsg: []string{"Starting deployment..."}},
{info: "Preparing environment"},
{instruction: "Executing package"},
{progressMsg: []string{"Deployment complete"}},
{isSuccessful: true},
}
testServices := make(inspect.ServiceMap)
testServices["el-1-geth-lighthouse"] = inspect.PortMap{
"rpc": {Port: 52645},
}
testServices["op-el-1-op-geth-op-node-op-kurtosis"] = inspect.PortMap{
"rpc": {Port: 53402},
}
testServices["op-cl-1-op-node-op-geth-op-kurtosis"] = inspect.PortMap{
"http": {Port: 53503},
}
testServices["op-batcher-op-kurtosis"] = inspect.PortMap{
"http": {Port: 53572},
}
testWallets := deployer.WalletList{
......@@ -288,7 +162,8 @@ func TestDeploy(t *testing.T) {
inspectErr error
deployerState *deployer.DeployerData
deployerErr error
dryRun bool
kurtosisErr error
responses []fakeStarlarkResponse
wantL1Nodes []Node
wantL2Nodes []Node
wantL2Services EndpointMap
......@@ -308,6 +183,7 @@ func TestDeploy(t *testing.T) {
"1234": testAddresses,
},
},
responses: successResponses,
wantL1Nodes: []Node{
{
"el": "http://localhost:52645",
......@@ -336,12 +212,6 @@ func TestDeploy(t *testing.T) {
specErr: fmt.Errorf("spec failed"),
wantErr: true,
},
{
name: "dry run",
input: "test input",
spec: testSpecWithL2,
dryRun: true,
},
{
name: "inspect error",
input: "test input",
......@@ -349,6 +219,13 @@ func TestDeploy(t *testing.T) {
inspectErr: fmt.Errorf("inspect failed"),
wantErr: true,
},
{
name: "kurtosis error",
input: "test input",
spec: testSpecWithL2,
kurtosisErr: fmt.Errorf("kurtosis failed"),
wantErr: true,
},
{
name: "deployer error",
input: "test input",
......@@ -365,11 +242,11 @@ func TestDeploy(t *testing.T) {
spec: testSpecWithL2,
inspectResult: &inspect.InspectData{
UserServices: inspect.ServiceMap{
"op-el-1-op-geth-op-node-op-kurtosis": {
"rpc": 53402,
"op-el-1-op-geth-op-node-op-kurtosis": inspect.PortMap{
"rpc": {Port: 53402},
},
"op-cl-1-op-node-op-geth-op-kurtosis": {
"http": 53503,
"op-cl-1-op-node-op-geth-op-kurtosis": inspect.PortMap{
"http": {Port: 53503},
},
},
},
......@@ -379,6 +256,7 @@ func TestDeploy(t *testing.T) {
"1234": testAddresses,
},
},
responses: successResponses,
wantL2Nodes: []Node{
{
"el": "http://localhost:53402",
......@@ -398,14 +276,15 @@ func TestDeploy(t *testing.T) {
spec: testSpecNoL2,
inspectResult: &inspect.InspectData{
UserServices: inspect.ServiceMap{
"el-1-geth-lighthouse": {
"rpc": 52645,
"el-1-geth-lighthouse": inspect.PortMap{
"rpc": {Port: 52645},
},
},
},
deployerState: &deployer.DeployerData{
Wallets: testWallets,
},
responses: successResponses,
wantL1Nodes: []Node{
{
"el": "http://localhost:52645",
......@@ -422,9 +301,15 @@ func TestDeploy(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Create a fake Kurtosis context
fakeCtx := &fakeKurtosisContext{
enclaveCtx: &fakeEnclaveContext{
runErr: tt.kurtosisErr,
responses: tt.responses,
},
}
d := NewKurtosisDeployer(
WithKurtosisDryRun(tt.dryRun),
WithKurtosisCmdTemplate(fakeCmdTemplate),
WithKurtosisEnclaveSpec(&fakeEnclaveSpecifier{
spec: tt.spec,
err: tt.specErr,
......@@ -439,6 +324,9 @@ func TestDeploy(t *testing.T) {
}),
)
// Set the fake Kurtosis context
d.kurtosisCtx = fakeCtx
env, err := d.Deploy(context.Background(), strings.NewReader(tt.input))
if tt.wantErr {
assert.Error(t, err)
......@@ -446,30 +334,25 @@ func TestDeploy(t *testing.T) {
}
require.NoError(t, err)
if tt.dryRun {
assert.NotNil(t, env)
assert.Empty(t, env.L1)
assert.Empty(t, env.L2)
assert.Empty(t, env.Wallets)
return
}
assert.NotNil(t, env)
if tt.wantL1Nodes != nil {
require.NotNil(t, env.L1)
assert.Equal(t, tt.wantL1Nodes, env.L1.Nodes)
} else {
assert.Nil(t, env.L1)
}
if len(tt.wantL2Nodes) > 0 {
if tt.wantL2Nodes != nil {
require.Len(t, env.L2, 1)
assert.Equal(t, tt.wantL2Nodes, env.L2[0].Nodes)
if tt.wantL2Services != nil {
assert.Equal(t, tt.wantL2Services, env.L2[0].Services)
}
if addresses, ok := tt.deployerState.State["1234"]; ok {
assert.Equal(t, addresses, env.L2[0].Addresses)
}
} else {
assert.Empty(t, env.L2)
}
assert.Equal(t, tt.wantWallets, env.Wallets)
})
}
......
......@@ -38,8 +38,12 @@ func main() {
fmt.Println("\nServices:")
for svc, ports := range data.UserServices {
fmt.Printf(" %s:\n", svc)
for portName, portNum := range ports {
fmt.Printf(" %s: %d\n", portName, portNum)
for portName, portInfo := range ports {
host := portInfo.Host
if host == "" {
host = "localhost"
}
fmt.Printf(" %s: %s:%d\n", portName, host, portInfo.Port)
}
}
}
......@@ -6,7 +6,13 @@ import (
"github.com/kurtosis-tech/kurtosis/api/golang/engine/lib/kurtosis_context"
)
type PortMap map[string]int
// PortInfo contains the host and port number for a service port
type PortInfo struct {
Host string
Port int
}
type PortMap map[string]PortInfo
type ServiceMap map[string]PortMap
......@@ -64,7 +70,10 @@ func (e *Inspector) ExtractData(ctx context.Context) (*InspectData, error) {
portMap := make(PortMap)
for port, portSpec := range svcCtx.GetPublicPorts() {
portMap[port] = int(portSpec.GetNumber())
portMap[port] = PortInfo{
Host: svcCtx.GetMaybePublicIPAddress(),
Port: int(portSpec.GetNumber()),
}
}
if len(portMap) != 0 {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment