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

chore(kurtosis-devnet): add proper deploy package (#13869)

parent 8c456120
This diff is collapsed.
package main package main
import ( import (
"bytes"
"context"
"encoding/json"
"io"
"os" "os"
"path/filepath" "path/filepath"
"testing" "testing"
"github.com/ethereum-optimism/optimism/kurtosis-devnet/pkg/kurtosis" "github.com/ethereum-optimism/optimism/kurtosis-devnet/pkg/kurtosis"
"github.com/ethereum-optimism/optimism/kurtosis-devnet/pkg/kurtosis/sources/spec"
"github.com/ethereum-optimism/optimism/kurtosis-devnet/pkg/tmpl"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
"gopkg.in/yaml.v3"
) )
type mockDeployer struct {
dryRun bool
}
func (m *mockDeployer) Deploy(ctx context.Context, input io.Reader) (*spec.EnclaveSpec, error) {
return &spec.EnclaveSpec{}, nil
}
func (m *mockDeployer) GetEnvironmentInfo(ctx context.Context, spec *spec.EnclaveSpec) (*kurtosis.KurtosisEnvironment, error) {
return &kurtosis.KurtosisEnvironment{}, nil
}
func newMockDeployer(...kurtosis.KurtosisDeployerOptions) (deployer, error) {
return &mockDeployer{dryRun: true}, nil
}
type mockEngineManager struct{}
func (m *mockEngineManager) EnsureRunning() error {
return nil
}
func newTestMain(cfg *config) *Main {
return &Main{
cfg: cfg,
newDeployer: func(opts ...kurtosis.KurtosisDeployerOptions) (deployer, error) {
return newMockDeployer(opts...)
},
engineManager: &mockEngineManager{},
}
}
func TestParseFlags(t *testing.T) { func TestParseFlags(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
...@@ -124,100 +85,7 @@ func TestParseFlags(t *testing.T) { ...@@ -124,100 +85,7 @@ func TestParseFlags(t *testing.T) {
} }
} }
func TestRenderTemplate(t *testing.T) { func TestMainFuncValidatesConfig(t *testing.T) {
// Create a temporary directory for test files
tmpDir, err := os.MkdirTemp("", "template-test")
require.NoError(t, err)
defer os.RemoveAll(tmpDir)
// Create a test template file
templateContent := `
name: {{.name}}
image: {{localDockerImage "test-project"}}
artifacts: {{localContractArtifacts "l1"}}`
templatePath := filepath.Join(tmpDir, "template.yaml")
err = os.WriteFile(templatePath, []byte(templateContent), 0644)
require.NoError(t, err)
// Create a test data file
dataContent := `{"name": "test-deployment"}`
dataPath := filepath.Join(tmpDir, "data.json")
err = os.WriteFile(dataPath, []byte(dataContent), 0644)
require.NoError(t, err)
cfg := &config{
templateFile: templatePath,
dataFile: dataPath,
enclave: "test-enclave",
dryRun: true, // Important for tests
}
m := newTestMain(cfg)
buf, err := m.renderTemplate(tmpDir)
require.NoError(t, err)
// Verify template rendering
assert.Contains(t, buf.String(), "test-deployment")
assert.Contains(t, buf.String(), "test-project:test-enclave")
}
func TestDeploy(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// Create a temporary directory for the environment output
tmpDir, err := os.MkdirTemp("", "deploy-test")
require.NoError(t, err)
defer os.RemoveAll(tmpDir)
envPath := filepath.Join(tmpDir, "env.json")
cfg := &config{
environment: envPath,
dryRun: true,
}
// Create a simple deployment configuration
deployConfig := bytes.NewBufferString(`{"test": "config"}`)
m := newTestMain(cfg)
err = m.deploy(ctx, deployConfig)
require.NoError(t, err)
// Verify the environment file was created
assert.FileExists(t, envPath)
// Read and verify the content
content, err := os.ReadFile(envPath)
require.NoError(t, err)
var env map[string]interface{}
err = json.Unmarshal(content, &env)
require.NoError(t, err)
}
func TestDeployFileserver(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
tmpDir, err := os.MkdirTemp("", "deploy-fileserver-test")
require.NoError(t, err)
defer os.RemoveAll(tmpDir)
envPath := filepath.Join(tmpDir, "env.json")
cfg := &config{
baseDir: tmpDir,
environment: envPath,
dryRun: true,
}
m := newTestMain(cfg)
err = m.deployFileserver(ctx, filepath.Join(tmpDir, "fileserver"))
require.NoError(t, err)
}
func TestMainFunc(t *testing.T) {
// Create a temporary directory for test files // Create a temporary directory for test files
tmpDir, err := os.MkdirTemp("", "main-test") tmpDir, err := os.MkdirTemp("", "main-test")
require.NoError(t, err) require.NoError(t, err)
...@@ -231,111 +99,34 @@ func TestMainFunc(t *testing.T) { ...@@ -231,111 +99,34 @@ func TestMainFunc(t *testing.T) {
// Create environment output path // Create environment output path
envPath := filepath.Join(tmpDir, "env.json") envPath := filepath.Join(tmpDir, "env.json")
cfg := &config{ app := &cli.App{
templateFile: templatePath, Flags: getFlags(),
environment: envPath, Action: func(c *cli.Context) error {
dryRun: true, cfg, err := newConfig(c)
} if err != nil {
return err
m := newTestMain(cfg) }
err = m.run()
require.NoError(t, err)
// Verify the environment file was created
assert.FileExists(t, envPath)
}
func TestLocalPrestate(t *testing.T) {
// Create a temporary directory for test files
tmpDir, err := os.MkdirTemp("", "prestate-test")
require.NoError(t, err)
defer os.RemoveAll(tmpDir)
// Create a mock justfile // Verify config values
err = os.WriteFile(filepath.Join(tmpDir, "justfile"), []byte(` assert.Equal(t, templatePath, cfg.templateFile)
_prestate-build target: assert.Equal(t, envPath, cfg.environment)
@echo "Mock prestate build" assert.True(t, cfg.dryRun)
`), 0644)
require.NoError(t, err)
tests := []struct { // Create an empty environment file to simulate successful deployment
name string return os.WriteFile(envPath, []byte("{}"), 0644)
dryRun bool
wantErr bool
}{
{
name: "dry run mode",
dryRun: true,
wantErr: false,
},
{
name: "normal mode",
dryRun: false,
wantErr: false,
}, },
} }
for _, tt := range tests { args := []string{
t.Run(tt.name, func(t *testing.T) { "prog",
cfg := &config{ "--template", templatePath,
baseDir: tmpDir, "--environment", envPath,
dryRun: tt.dryRun, "--dry-run",
} }
m := newTestMain(cfg)
tmpDir, err := os.MkdirTemp("", "prestate-test")
require.NoError(t, err)
defer os.RemoveAll(tmpDir)
// Create template context with just the prestate function
tmplCtx := tmpl.NewTemplateContext(m.localPrestateOption(tmpDir))
// Test template with multiple calls to localPrestate
template := `first:
url: {{(localPrestate).URL}}
hashes:
game: {{index (localPrestate).Hashes "game"}}
proof: {{index (localPrestate).Hashes "proof"}}
second:
url: {{(localPrestate).URL}}
hashes:
game: {{index (localPrestate).Hashes "game"}}
proof: {{index (localPrestate).Hashes "proof"}}`
buf := bytes.NewBuffer(nil)
err = tmplCtx.InstantiateTemplate(bytes.NewBufferString(template), buf)
if tt.wantErr {
assert.Error(t, err)
return
}
require.NoError(t, err)
// Verify the output is valid YAML and contains the static path
output := buf.String()
assert.Contains(t, output, "url: http://fileserver/proofs/op-program/cannon")
// Verify both calls return the same values
var result struct {
First struct {
URL string `yaml:"url"`
Hashes map[string]string `yaml:"hashes"`
} `yaml:"first"`
Second struct {
URL string `yaml:"url"`
Hashes map[string]string `yaml:"hashes"`
} `yaml:"second"`
}
err = yaml.Unmarshal(buf.Bytes(), &result)
require.NoError(t, err)
// Check that both calls returned identical results err = app.Run(args)
assert.Equal(t, result.First.URL, result.Second.URL, "URLs should match") require.NoError(t, err)
assert.Equal(t, result.First.Hashes, result.Second.Hashes, "Hashes should match")
// Verify the directory was created only once // Verify the environment file was created
prestateDir := filepath.Join(tmpDir, "proofs", "op-program", "cannon") assert.FileExists(t, envPath)
assert.DirExists(t, prestateDir)
})
}
} }
package deploy
import (
"bytes"
"context"
"fmt"
"io"
"log"
"os"
"github.com/ethereum-optimism/optimism/kurtosis-devnet/pkg/kurtosis"
"github.com/ethereum-optimism/optimism/kurtosis-devnet/pkg/kurtosis/api/engine"
"github.com/ethereum-optimism/optimism/kurtosis-devnet/pkg/kurtosis/sources/spec"
)
type EngineManager interface {
EnsureRunning() error
}
type deployer interface {
Deploy(ctx context.Context, input io.Reader) (*spec.EnclaveSpec, error)
GetEnvironmentInfo(ctx context.Context, spec *spec.EnclaveSpec) (*kurtosis.KurtosisEnvironment, error)
}
type DeployerFunc func(opts ...kurtosis.KurtosisDeployerOptions) (deployer, error)
type DeployerOption func(*Deployer)
type Deployer struct {
baseDir string
dryRun bool
kurtosisPkg string
enclave string
kurtosisBinary string
ktDeployer DeployerFunc
engineManager EngineManager
templateFile string
dataFile string
}
func WithKurtosisDeployer(ktDeployer DeployerFunc) DeployerOption {
return func(d *Deployer) {
d.ktDeployer = ktDeployer
}
}
func WithEngineManager(engineManager EngineManager) DeployerOption {
return func(d *Deployer) {
d.engineManager = engineManager
}
}
func WithKurtosisBinary(kurtosisBinary string) DeployerOption {
return func(d *Deployer) {
d.kurtosisBinary = kurtosisBinary
}
}
func WithKurtosisPackage(kurtosisPkg string) DeployerOption {
return func(d *Deployer) {
d.kurtosisPkg = kurtosisPkg
}
}
func WithTemplateFile(templateFile string) DeployerOption {
return func(d *Deployer) {
d.templateFile = templateFile
}
}
func WithDataFile(dataFile string) DeployerOption {
return func(d *Deployer) {
d.dataFile = dataFile
}
}
func WithBaseDir(baseDir string) DeployerOption {
return func(d *Deployer) {
d.baseDir = baseDir
}
}
func WithDryRun(dryRun bool) DeployerOption {
return func(d *Deployer) {
d.dryRun = dryRun
}
}
func WithEnclave(enclave string) DeployerOption {
return func(d *Deployer) {
d.enclave = enclave
}
}
func NewDeployer(opts ...DeployerOption) *Deployer {
d := &Deployer{
kurtosisBinary: "kurtosis",
ktDeployer: func(opts ...kurtosis.KurtosisDeployerOptions) (deployer, error) {
return kurtosis.NewKurtosisDeployer(opts...)
},
}
for _, opt := range opts {
opt(d)
}
if d.engineManager == nil {
d.engineManager = engine.NewEngineManager(engine.WithKurtosisBinary(d.kurtosisBinary))
}
return d
}
func (d *Deployer) deployEnvironment(ctx context.Context, r io.Reader) (*kurtosis.KurtosisEnvironment, error) {
// Create a multi reader to output deployment input to stdout
buf := bytes.NewBuffer(nil)
tee := io.TeeReader(r, buf)
// Log the deployment input
log.Println("Deployment input:")
if _, err := io.Copy(os.Stdout, tee); err != nil {
return nil, fmt.Errorf("error copying deployment input: %w", err)
}
opts := []kurtosis.KurtosisDeployerOptions{
kurtosis.WithKurtosisBaseDir(d.baseDir),
kurtosis.WithKurtosisDryRun(d.dryRun),
kurtosis.WithKurtosisPackageName(d.kurtosisPkg),
kurtosis.WithKurtosisEnclave(d.enclave),
}
ktd, err := d.ktDeployer(opts...)
if err != nil {
return nil, fmt.Errorf("error creating kurtosis deployer: %w", err)
}
spec, err := ktd.Deploy(ctx, buf)
if err != nil {
return nil, fmt.Errorf("error deploying kurtosis package: %w", err)
}
return ktd.GetEnvironmentInfo(ctx, spec)
}
func (d *Deployer) renderTemplate(buildDir string, urlBuilder func(path ...string) string) (*bytes.Buffer, error) {
t := &Templater{
baseDir: d.baseDir,
dryRun: d.dryRun,
enclave: d.enclave,
templateFile: d.templateFile,
dataFile: d.dataFile,
buildDir: buildDir,
urlBuilder: urlBuilder,
}
return t.Render()
}
func (d *Deployer) Deploy(ctx context.Context, r io.Reader) (*kurtosis.KurtosisEnvironment, error) {
if !d.dryRun {
if err := d.engineManager.EnsureRunning(); err != nil {
return nil, fmt.Errorf("error ensuring kurtosis engine is running: %w", err)
}
}
tmpDir, err := os.MkdirTemp("", d.enclave)
if err != nil {
return nil, fmt.Errorf("error creating temporary directory: %w", err)
}
defer os.RemoveAll(tmpDir)
srv := &FileServer{
baseDir: d.baseDir,
dryRun: d.dryRun,
enclave: d.enclave,
deployer: d.ktDeployer,
}
buf, err := d.renderTemplate(tmpDir, srv.URL)
if err != nil {
return nil, fmt.Errorf("error rendering template: %w", err)
}
if err := srv.Deploy(ctx, tmpDir); err != nil {
return nil, fmt.Errorf("error deploying fileserver: %w", err)
}
return d.deployEnvironment(ctx, buf)
}
package deploy
import (
"bytes"
"context"
"encoding/json"
"io"
"os"
"path/filepath"
"testing"
"github.com/ethereum-optimism/optimism/kurtosis-devnet/pkg/kurtosis"
"github.com/ethereum-optimism/optimism/kurtosis-devnet/pkg/kurtosis/sources/spec"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// mockDeployerForTest implements the deployer interface for testing
type mockDeployerForTest struct {
baseDir string
}
func (m *mockDeployerForTest) Deploy(ctx context.Context, input io.Reader) (*spec.EnclaveSpec, error) {
// Create a mock env.json file
envPath := filepath.Join(m.baseDir, "env.json")
mockEnv := map[string]interface{}{
"test": "value",
}
data, err := json.Marshal(mockEnv)
if err != nil {
return nil, err
}
if err := os.WriteFile(envPath, data, 0644); err != nil {
return nil, err
}
return &spec.EnclaveSpec{}, nil
}
func (m *mockDeployerForTest) GetEnvironmentInfo(ctx context.Context, spec *spec.EnclaveSpec) (*kurtosis.KurtosisEnvironment, error) {
return &kurtosis.KurtosisEnvironment{}, nil
}
func TestDeploy(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// Create a temporary directory for the environment output
tmpDir, err := os.MkdirTemp("", "deploy-test")
require.NoError(t, err)
defer os.RemoveAll(tmpDir)
// Create a simple template file
templatePath := filepath.Join(tmpDir, "template.yaml")
err = os.WriteFile(templatePath, []byte("test: {{ .Config }}"), 0644)
require.NoError(t, err)
// Create a simple data file
dataPath := filepath.Join(tmpDir, "data.json")
err = os.WriteFile(dataPath, []byte(`{"Config": "value"}`), 0644)
require.NoError(t, err)
envPath := filepath.Join(tmpDir, "env.json")
// Create a simple deployment configuration
deployConfig := bytes.NewBufferString(`{"test": "config"}`)
// Create a mock deployer function
mockDeployerFunc := func(opts ...kurtosis.KurtosisDeployerOptions) (deployer, error) {
return &mockDeployerForTest{baseDir: tmpDir}, nil
}
d := NewDeployer(
WithBaseDir(tmpDir),
WithKurtosisDeployer(mockDeployerFunc),
WithDryRun(true),
WithTemplateFile(templatePath),
WithDataFile(dataPath),
)
env, err := d.Deploy(ctx, deployConfig)
require.NoError(t, err)
require.NotNil(t, env)
// Verify the environment file was created
assert.FileExists(t, envPath)
// Read and verify the content
content, err := os.ReadFile(envPath)
require.NoError(t, err)
var envData map[string]interface{}
err = json.Unmarshal(content, &envData)
require.NoError(t, err)
assert.Equal(t, "value", envData["test"])
}
package deploy
import (
"bytes"
"context"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/ethereum-optimism/optimism/kurtosis-devnet/pkg/kurtosis"
"github.com/ethereum-optimism/optimism/kurtosis-devnet/pkg/util"
)
const FILESERVER_PACKAGE = "fileserver"
type FileServer struct {
baseDir string
enclave string
dryRun bool
deployer DeployerFunc
}
func (f *FileServer) URL(path ...string) string {
return fmt.Sprintf("http://%s/%s", FILESERVER_PACKAGE, strings.Join(path, "/"))
}
func (f *FileServer) Deploy(ctx context.Context, sourceDir string) error {
// Create a temp dir in the fileserver package
baseDir := filepath.Join(f.baseDir, FILESERVER_PACKAGE)
if err := os.MkdirAll(baseDir, 0755); err != nil {
return fmt.Errorf("error creating base directory: %w", err)
}
tempDir, err := os.MkdirTemp(baseDir, "upload-content")
if err != nil {
return fmt.Errorf("error creating temporary directory: %w", err)
}
defer os.RemoveAll(tempDir)
// Copy build dir contents to tempDir
if err := util.CopyDir(sourceDir, tempDir); err != nil {
return fmt.Errorf("error copying directory: %w", err)
}
buf := bytes.NewBuffer(nil)
buf.WriteString(fmt.Sprintf("source_path: %s\n", filepath.Base(tempDir)))
opts := []kurtosis.KurtosisDeployerOptions{
kurtosis.WithKurtosisBaseDir(f.baseDir),
kurtosis.WithKurtosisDryRun(f.dryRun),
kurtosis.WithKurtosisPackageName(FILESERVER_PACKAGE),
kurtosis.WithKurtosisEnclave(f.enclave),
}
d, err := f.deployer(opts...)
if err != nil {
return fmt.Errorf("error creating kurtosis deployer: %w", err)
}
_, err = d.Deploy(ctx, buf)
if err != nil {
return fmt.Errorf("error deploying kurtosis package: %w", err)
}
return nil
}
package deploy
import (
"context"
"io"
"os"
"path/filepath"
"testing"
"github.com/ethereum-optimism/optimism/kurtosis-devnet/pkg/kurtosis"
"github.com/ethereum-optimism/optimism/kurtosis-devnet/pkg/kurtosis/sources/spec"
"github.com/stretchr/testify/require"
)
func TestDeployFileserver(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
tmpDir, err := os.MkdirTemp("", "deploy-fileserver-test")
require.NoError(t, err)
defer os.RemoveAll(tmpDir)
// Create a mock deployer function
mockDeployerFunc := func(opts ...kurtosis.KurtosisDeployerOptions) (deployer, error) {
return &mockDeployer{}, nil
}
testCases := []struct {
name string
fs *FileServer
shouldError bool
}{
{
name: "successful deployment",
fs: &FileServer{
baseDir: tmpDir,
enclave: "test-enclave",
dryRun: true,
deployer: mockDeployerFunc,
},
shouldError: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
err := tc.fs.Deploy(ctx, filepath.Join(tmpDir, "fileserver"))
if tc.shouldError {
require.Error(t, err)
} else {
require.NoError(t, err)
}
})
}
}
// mockDeployer implements the deployer interface for testing
type mockDeployer struct{}
func (m *mockDeployer) Deploy(ctx context.Context, input io.Reader) (*spec.EnclaveSpec, error) {
return &spec.EnclaveSpec{}, nil
}
func (m *mockDeployer) GetEnvironmentInfo(ctx context.Context, spec *spec.EnclaveSpec) (*kurtosis.KurtosisEnvironment, error) {
return &kurtosis.KurtosisEnvironment{}, nil
}
package deploy
import (
"encoding/json"
"fmt"
"log"
"os"
"path/filepath"
"strings"
"github.com/ethereum-optimism/optimism/kurtosis-devnet/pkg/build"
)
type PrestateInfo struct {
URL string `json:"url"`
Hashes map[string]string `json:"hashes"`
}
type localPrestateHolder struct {
info *PrestateInfo
baseDir string
buildDir string
dryRun bool
builder *build.PrestateBuilder
urlBuilder func(path ...string) string
}
func (h *localPrestateHolder) GetPrestateInfo() (*PrestateInfo, error) {
if h.info != nil {
return h.info, nil
}
prestatePath := []string{"proofs", "op-program", "cannon"}
prestateURL := h.urlBuilder(prestatePath...)
// Create build directory with the final path structure
buildDir := filepath.Join(append([]string{h.buildDir}, prestatePath...)...)
if err := os.MkdirAll(buildDir, 0755); err != nil {
return nil, fmt.Errorf("failed to create prestate build directory: %w", err)
}
info := &PrestateInfo{
URL: prestateURL,
Hashes: make(map[string]string),
}
if h.dryRun {
h.info = info
return info, nil
}
// Map of known file prefixes to their keys
fileToKey := map[string]string{
"prestate-proof.json": "prestate",
"prestate-proof-mt64.json": "prestate_mt64",
"prestate-proof-mt.json": "prestate_mt",
"prestate-proof-interop.json": "prestate_interop",
}
// Build all prestate files directly in the target directory
if err := h.builder.Build(buildDir); err != nil {
return nil, fmt.Errorf("failed to build prestates: %w", err)
}
// Find and process all prestate files
matches, err := filepath.Glob(filepath.Join(buildDir, "prestate-proof*.json"))
if err != nil {
return nil, fmt.Errorf("failed to find prestate files: %w", err)
}
// Process each file to rename it to its hash
for _, filePath := range matches {
content, err := os.ReadFile(filePath)
if err != nil {
return nil, fmt.Errorf("failed to read prestate %s: %w", filepath.Base(filePath), err)
}
var data struct {
Pre string `json:"pre"`
}
if err := json.Unmarshal(content, &data); err != nil {
return nil, fmt.Errorf("failed to parse prestate %s: %w", filepath.Base(filePath), err)
}
// Store hash with its corresponding key
if key, exists := fileToKey[filepath.Base(filePath)]; exists {
info.Hashes[key] = data.Pre
}
// Rename files to hash-based names
newFileName := data.Pre + ".json"
hashedPath := filepath.Join(buildDir, newFileName)
if err := os.Rename(filePath, hashedPath); err != nil {
return nil, fmt.Errorf("failed to rename prestate %s: %w", filepath.Base(filePath), err)
}
log.Printf("%s available at: %s/%s\n", filepath.Base(filePath), prestateURL, newFileName)
// Rename the corresponding binary file
binFilePath := strings.Replace(strings.TrimSuffix(filePath, ".json"), "-proof", "", 1) + ".bin.gz"
newBinFileName := data.Pre + ".bin.gz"
binHashedPath := filepath.Join(buildDir, newBinFileName)
if err := os.Rename(binFilePath, binHashedPath); err != nil {
return nil, fmt.Errorf("failed to rename prestate %s: %w", filepath.Base(binFilePath), err)
}
log.Printf("%s available at: %s/%s\n", filepath.Base(binFilePath), prestateURL, newBinFileName)
}
h.info = info
return info, nil
}
package deploy
import (
"bytes"
"os"
"path/filepath"
"strings"
"testing"
"github.com/ethereum-optimism/optimism/kurtosis-devnet/pkg/tmpl"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v2"
)
func TestLocalPrestate(t *testing.T) {
tests := []struct {
name string
dryRun bool
wantErr bool
}{
{
name: "dry run mode",
dryRun: true,
wantErr: false,
},
{
name: "normal mode",
dryRun: false,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "prestate-test")
require.NoError(t, err)
defer os.RemoveAll(tmpDir)
// Create a mock justfile for each test case
err = os.WriteFile(filepath.Join(tmpDir, "justfile"), []byte(`
_prestate-build target:
@echo "Mock prestate build"
`), 0644)
require.NoError(t, err)
templater := &Templater{
baseDir: tmpDir,
dryRun: tt.dryRun,
buildDir: tmpDir,
urlBuilder: func(path ...string) string {
return "http://fileserver/" + strings.Join(path, "/")
},
}
// Create template context with just the prestate function
tmplCtx := tmpl.NewTemplateContext(templater.localPrestateOption())
// Test template with multiple calls to localPrestate
template := `first:
url: {{(localPrestate).URL}}
hashes:
game: {{index (localPrestate).Hashes "game"}}
proof: {{index (localPrestate).Hashes "proof"}}
second:
url: {{(localPrestate).URL}}
hashes:
game: {{index (localPrestate).Hashes "game"}}
proof: {{index (localPrestate).Hashes "proof"}}`
buf := bytes.NewBuffer(nil)
err = tmplCtx.InstantiateTemplate(bytes.NewBufferString(template), buf)
if tt.wantErr {
assert.Error(t, err)
return
}
require.NoError(t, err)
// Verify the output is valid YAML and contains the static path
output := buf.String()
assert.Contains(t, output, "url: http://fileserver/proofs/op-program/cannon")
// Verify both calls return the same values
var result struct {
First struct {
URL string `yaml:"url"`
Hashes map[string]string `yaml:"hashes"`
} `yaml:"first"`
Second struct {
URL string `yaml:"url"`
Hashes map[string]string `yaml:"hashes"`
} `yaml:"second"`
}
err = yaml.Unmarshal(buf.Bytes(), &result)
require.NoError(t, err)
// Check that both calls returned identical results
assert.Equal(t, result.First.URL, result.Second.URL, "URLs should match")
assert.Equal(t, result.First.Hashes, result.Second.Hashes, "Hashes should match")
// Verify the directory was created only once
prestateDir := filepath.Join(tmpDir, "proofs", "op-program", "cannon")
assert.DirExists(t, prestateDir)
})
}
}
package deploy
import (
"bytes"
"encoding/json"
"fmt"
"log"
"os"
"path/filepath"
"github.com/ethereum-optimism/optimism/kurtosis-devnet/pkg/build"
"github.com/ethereum-optimism/optimism/kurtosis-devnet/pkg/tmpl"
)
type Templater struct {
enclave string
dryRun bool
baseDir string
templateFile string
dataFile string
buildDir string
urlBuilder func(path ...string) string
}
func (f *Templater) localDockerImageOption() tmpl.TemplateContextOptions {
dockerBuilder := build.NewDockerBuilder(
build.WithDockerBaseDir(f.baseDir),
build.WithDockerDryRun(f.dryRun),
)
imageTag := func(projectName string) string {
return fmt.Sprintf("%s:%s", projectName, f.enclave)
}
return tmpl.WithFunction("localDockerImage", func(projectName string) (string, error) {
return dockerBuilder.Build(projectName, imageTag(projectName))
})
}
func (f *Templater) localContractArtifactsOption() tmpl.TemplateContextOptions {
contractsBundle := fmt.Sprintf("contracts-bundle-%s.tar.gz", f.enclave)
contractsBundlePath := func(_ string) string {
return filepath.Join(f.buildDir, contractsBundle)
}
contractsURL := f.urlBuilder(contractsBundle)
contractBuilder := build.NewContractBuilder(
build.WithContractBaseDir(f.baseDir),
build.WithContractDryRun(f.dryRun),
)
return tmpl.WithFunction("localContractArtifacts", func(layer string) (string, error) {
bundlePath := contractsBundlePath(layer)
if err := contractBuilder.Build(layer, bundlePath); err != nil {
return "", err
}
log.Printf("%s: contract artifacts available at: %s\n", layer, contractsURL)
return contractsURL, nil
})
}
func (f *Templater) localPrestateOption() tmpl.TemplateContextOptions {
holder := &localPrestateHolder{
baseDir: f.baseDir,
buildDir: f.buildDir,
dryRun: f.dryRun,
builder: build.NewPrestateBuilder(
build.WithPrestateBaseDir(f.baseDir),
build.WithPrestateDryRun(f.dryRun),
),
urlBuilder: f.urlBuilder,
}
return tmpl.WithFunction("localPrestate", func() (*PrestateInfo, error) {
return holder.GetPrestateInfo()
})
}
func (f *Templater) Render() (*bytes.Buffer, error) {
opts := []tmpl.TemplateContextOptions{
f.localDockerImageOption(),
f.localContractArtifactsOption(),
f.localPrestateOption(),
tmpl.WithBaseDir(f.baseDir),
}
// Read and parse the data file if provided
if f.dataFile != "" {
data, err := os.ReadFile(f.dataFile)
if err != nil {
return nil, fmt.Errorf("error reading data file: %w", err)
}
var templateData map[string]interface{}
if err := json.Unmarshal(data, &templateData); err != nil {
return nil, fmt.Errorf("error parsing JSON data: %w", err)
}
opts = append(opts, tmpl.WithData(templateData))
}
// Open template file
tmplFile, err := os.Open(f.templateFile)
if err != nil {
return nil, fmt.Errorf("error opening template file: %w", err)
}
defer tmplFile.Close()
// Create template context
tmplCtx := tmpl.NewTemplateContext(opts...)
// Process template
buf := bytes.NewBuffer(nil)
if err := tmplCtx.InstantiateTemplate(tmplFile, buf); err != nil {
return nil, fmt.Errorf("error processing template: %w", err)
}
return buf, nil
}
package deploy
import (
"os"
"path/filepath"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestRenderTemplate(t *testing.T) {
// Create a temporary directory for test files
tmpDir, err := os.MkdirTemp("", "template-test")
require.NoError(t, err)
defer os.RemoveAll(tmpDir)
// Create a test template file
templateContent := `
name: {{.name}}
image: {{localDockerImage "test-project"}}
artifacts: {{localContractArtifacts "l1"}}`
templatePath := filepath.Join(tmpDir, "template.yaml")
err = os.WriteFile(templatePath, []byte(templateContent), 0644)
require.NoError(t, err)
// Create a test data file
dataContent := `{"name": "test-deployment"}`
dataPath := filepath.Join(tmpDir, "data.json")
err = os.WriteFile(dataPath, []byte(dataContent), 0644)
require.NoError(t, err)
// Create a Templater instance
templater := &Templater{
enclave: "test-enclave",
dryRun: true,
baseDir: tmpDir,
templateFile: templatePath,
dataFile: dataPath,
buildDir: tmpDir,
urlBuilder: func(path ...string) string {
return "http://localhost:8080/" + strings.Join(path, "/")
},
}
buf, err := templater.Render()
require.NoError(t, err)
// Verify template rendering
assert.Contains(t, buf.String(), "test-deployment")
assert.Contains(t, buf.String(), "test-project:test-enclave")
}
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