Commit 38aa19f3 authored by Yann Hodique's avatar Yann Hodique Committed by GitHub

feat(kurtosis-devnet): expose jwt tokens in output (#13738)

* chore(kurtosis-devnet): split artifact management to its own package

* feat(kurtosis-devnet): add jwt tokens to output
parent 6c44b0b5
......@@ -6,6 +6,8 @@ import (
"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/interfaces"
"github.com/ethereum-optimism/optimism/kurtosis-devnet/pkg/kurtosis/sources/jwt"
"github.com/ethereum-optimism/optimism/kurtosis-devnet/pkg/kurtosis/sources/spec"
)
......@@ -15,7 +17,7 @@ func (a *enclaveSpecAdapter) EnclaveSpec(r io.Reader) (*spec.EnclaveSpec, error)
return spec.NewSpec().ExtractData(r)
}
var _ EnclaveSpecifier = (*enclaveSpecAdapter)(nil)
var _ interfaces.EnclaveSpecifier = (*enclaveSpecAdapter)(nil)
type enclaveInspectAdapter struct{}
......@@ -23,7 +25,7 @@ func (a *enclaveInspectAdapter) EnclaveInspect(ctx context.Context, enclave stri
return inspect.NewInspector(enclave).ExtractData(ctx)
}
var _ EnclaveInspecter = (*enclaveInspectAdapter)(nil)
var _ interfaces.EnclaveInspecter = (*enclaveInspectAdapter)(nil)
type enclaveDeployerAdapter struct{}
......@@ -31,4 +33,12 @@ func (a *enclaveDeployerAdapter) EnclaveObserve(ctx context.Context, enclave str
return deployer.NewDeployer(enclave).ExtractData(ctx)
}
var _ EnclaveObserver = (*enclaveDeployerAdapter)(nil)
var _ interfaces.EnclaveObserver = (*enclaveDeployerAdapter)(nil)
type enclaveJWTAdapter struct{}
func (a *enclaveJWTAdapter) ExtractData(ctx context.Context, enclave string) (*jwt.Data, error) {
return jwt.NewExtractor(enclave).ExtractData(ctx)
}
var _ interfaces.JWTExtractor = (*enclaveJWTAdapter)(nil)
......@@ -6,11 +6,12 @@ import (
"fmt"
"io"
"github.com/ethereum-optimism/optimism/kurtosis-devnet/pkg/kurtosis/api/interfaces"
apiInterfaces "github.com/ethereum-optimism/optimism/kurtosis-devnet/pkg/kurtosis/api/interfaces"
"github.com/ethereum-optimism/optimism/kurtosis-devnet/pkg/kurtosis/api/run"
"github.com/ethereum-optimism/optimism/kurtosis-devnet/pkg/kurtosis/api/wrappers"
"github.com/ethereum-optimism/optimism/kurtosis-devnet/pkg/kurtosis/sources/deployer"
"github.com/ethereum-optimism/optimism/kurtosis-devnet/pkg/kurtosis/sources/inspect"
srcInterfaces "github.com/ethereum-optimism/optimism/kurtosis-devnet/pkg/kurtosis/sources/interfaces"
"github.com/ethereum-optimism/optimism/kurtosis-devnet/pkg/kurtosis/sources/spec"
)
......@@ -39,6 +40,7 @@ type Chain struct {
Nodes []Node `json:"nodes"`
Addresses deployer.DeploymentAddresses `json:"addresses,omitempty"`
Wallets WalletMap `json:"wallets,omitempty"`
JWT string `json:"jwt,omitempty"`
}
type Wallet struct {
......@@ -65,10 +67,14 @@ type KurtosisDeployer struct {
// Enclave name
enclave string
enclaveSpec EnclaveSpecifier
enclaveInspecter EnclaveInspecter
enclaveObserver EnclaveObserver
kurtosisCtx interfaces.KurtosisContextInterface
// interfaces for kurtosis sources
enclaveSpec srcInterfaces.EnclaveSpecifier
enclaveInspecter srcInterfaces.EnclaveInspecter
enclaveObserver srcInterfaces.EnclaveObserver
jwtExtractor srcInterfaces.JWTExtractor
// interface for kurtosis interactions
kurtosisCtx apiInterfaces.KurtosisContextInterface
}
type KurtosisDeployerOptions func(*KurtosisDeployer)
......@@ -97,25 +103,31 @@ func WithKurtosisEnclave(enclave string) KurtosisDeployerOptions {
}
}
func WithKurtosisEnclaveSpec(enclaveSpec EnclaveSpecifier) KurtosisDeployerOptions {
func WithKurtosisEnclaveSpec(enclaveSpec srcInterfaces.EnclaveSpecifier) KurtosisDeployerOptions {
return func(d *KurtosisDeployer) {
d.enclaveSpec = enclaveSpec
}
}
func WithKurtosisEnclaveInspecter(enclaveInspecter EnclaveInspecter) KurtosisDeployerOptions {
func WithKurtosisEnclaveInspecter(enclaveInspecter srcInterfaces.EnclaveInspecter) KurtosisDeployerOptions {
return func(d *KurtosisDeployer) {
d.enclaveInspecter = enclaveInspecter
}
}
func WithKurtosisEnclaveObserver(enclaveObserver EnclaveObserver) KurtosisDeployerOptions {
func WithKurtosisEnclaveObserver(enclaveObserver srcInterfaces.EnclaveObserver) KurtosisDeployerOptions {
return func(d *KurtosisDeployer) {
d.enclaveObserver = enclaveObserver
}
}
func WithKurtosisKurtosisContext(kurtosisCtx interfaces.KurtosisContextInterface) KurtosisDeployerOptions {
func WithKurtosisJWTExtractor(extractor srcInterfaces.JWTExtractor) KurtosisDeployerOptions {
return func(d *KurtosisDeployer) {
d.jwtExtractor = extractor
}
}
func WithKurtosisKurtosisContext(kurtosisCtx apiInterfaces.KurtosisContextInterface) KurtosisDeployerOptions {
return func(d *KurtosisDeployer) {
d.kurtosisCtx = kurtosisCtx
}
......@@ -132,6 +144,7 @@ func NewKurtosisDeployer(opts ...KurtosisDeployerOptions) (*KurtosisDeployer, er
enclaveSpec: &enclaveSpecAdapter{},
enclaveInspecter: &enclaveInspectAdapter{},
enclaveObserver: &enclaveDeployerAdapter{},
jwtExtractor: &enclaveJWTAdapter{},
}
for _, opt := range opts {
......@@ -173,6 +186,12 @@ func (d *KurtosisDeployer) GetEnvironmentInfo(ctx context.Context, spec *spec.En
return nil, fmt.Errorf("failed to parse deployer state: %w", err)
}
// Get JWT data
jwtData, err := d.jwtExtractor.ExtractData(ctx, d.enclave)
if err != nil {
return nil, fmt.Errorf("failed to extract JWT data: %w", err)
}
env := &KurtosisEnvironment{
L2: make([]*Chain, 0, len(spec.Chains)),
}
......@@ -184,6 +203,7 @@ func (d *KurtosisDeployer) GetEnvironmentInfo(ctx context.Context, spec *spec.En
Name: "Ethereum",
Services: services,
Nodes: nodes,
JWT: jwtData.L1JWT,
}
if deployerState.State != nil {
chain.Addresses = deployerState.State.Addresses
......@@ -201,6 +221,7 @@ func (d *KurtosisDeployer) GetEnvironmentInfo(ctx context.Context, spec *spec.En
ID: chainSpec.NetworkID,
Services: services,
Nodes: nodes,
JWT: jwtData.L2JWT,
}
// Add contract addresses if available
......
......@@ -11,6 +11,7 @@ import (
"github.com/ethereum-optimism/optimism/kurtosis-devnet/pkg/kurtosis/api/interfaces"
"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/jwt"
"github.com/ethereum-optimism/optimism/kurtosis-devnet/pkg/kurtosis/sources/spec"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
......@@ -104,6 +105,16 @@ func (f *fakeEnclaveSpecifier) EnclaveSpec(r io.Reader) (*spec.EnclaveSpec, erro
return f.spec, f.err
}
// fakeJWTExtractor implements interfaces.JWTExtractor for testing
type fakeJWTExtractor struct {
data *jwt.Data
err error
}
func (f *fakeJWTExtractor) ExtractData(ctx context.Context, enclave string) (*jwt.Data, error) {
return f.data, f.err
}
func TestDeploy(t *testing.T) {
testSpec := &spec.EnclaveSpec{
Chains: []spec.ChainSpec{
......@@ -206,3 +217,131 @@ func TestDeploy(t *testing.T) {
})
}
}
func TestGetEnvironmentInfo(t *testing.T) {
testSpec := &spec.EnclaveSpec{
Chains: []spec.ChainSpec{
{
Name: "op-kurtosis",
NetworkID: "1234",
},
},
}
// Create test services map with the expected structure
testServices := make(inspect.ServiceMap)
testServices["el-1-geth-lighthouse"] = inspect.PortMap{
"rpc": {Port: 52645},
}
testWallets := deployer.WalletList{
{
Name: "test-wallet",
Address: "0x123",
PrivateKey: "0xabc",
},
}
testJWTs := &jwt.Data{
L1JWT: "test-l1-jwt",
L2JWT: "test-l2-jwt",
}
// Create expected L1 services
l1Services := make(ServiceMap)
l1Services["el"] = Service{
Name: "el-1-geth-lighthouse",
Endpoints: EndpointMap{
"rpc": inspect.PortInfo{Port: 52645},
},
}
tests := []struct {
name string
spec *spec.EnclaveSpec
inspect *inspect.InspectData
deploy *deployer.DeployerData
jwt *jwt.Data
want *KurtosisEnvironment
wantErr bool
err error
}{
{
name: "successful environment info with JWT",
spec: testSpec,
inspect: &inspect.InspectData{UserServices: testServices},
deploy: &deployer.DeployerData{Wallets: testWallets},
jwt: testJWTs,
want: &KurtosisEnvironment{
L1: &Chain{
Name: "Ethereum",
Services: make(ServiceMap),
Nodes: []Node{
{
Services: l1Services,
},
},
JWT: testJWTs.L1JWT,
},
L2: []*Chain{
{
Name: "op-kurtosis",
ID: "1234",
Services: make(ServiceMap),
JWT: testJWTs.L2JWT,
},
},
},
},
{
name: "inspect error",
spec: testSpec,
err: fmt.Errorf("inspect failed"),
wantErr: true,
},
{
name: "deploy error",
spec: testSpec,
inspect: &inspect.InspectData{UserServices: testServices},
err: fmt.Errorf("deploy failed"),
wantErr: true,
},
{
name: "jwt error",
spec: testSpec,
inspect: &inspect.InspectData{UserServices: testServices},
deploy: &deployer.DeployerData{},
err: fmt.Errorf("jwt failed"),
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
deployer, err := NewKurtosisDeployer(
WithKurtosisKurtosisContext(&fake.KurtosisContext{}),
WithKurtosisEnclaveInspecter(&fakeEnclaveInspecter{
result: tt.inspect,
err: tt.err,
}),
WithKurtosisEnclaveObserver(&fakeEnclaveObserver{
state: tt.deploy,
err: tt.err,
}),
WithKurtosisJWTExtractor(&fakeJWTExtractor{
data: tt.jwt,
err: tt.err,
}),
)
require.NoError(t, err)
got, err := deployer.GetEnvironmentInfo(context.Background(), tt.spec)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
require.Equal(t, tt.want, got)
})
}
}
package deployer
package artifact
import (
"archive/tar"
"bytes"
"compress/gzip"
"context"
"fmt"
"io"
"path/filepath"
"github.com/kurtosis-tech/kurtosis/api/golang/core/lib/enclaves"
"github.com/kurtosis-tech/kurtosis/api/golang/engine/lib/kurtosis_context"
)
// EnclaveContextIface abstracts the EnclaveContext for testing
type EnclaveContextIface interface {
DownloadFilesArtifact(ctx context.Context, name string) ([]byte, error)
}
type EnclaveFS struct {
enclaveCtx *enclaves.EnclaveContext
enclaveCtx EnclaveContextIface
}
func NewEnclaveFS(ctx context.Context, enclave string) (*EnclaveFS, error) {
......@@ -31,6 +34,11 @@ func NewEnclaveFS(ctx context.Context, enclave string) (*EnclaveFS, error) {
return &EnclaveFS{enclaveCtx: enclaveCtx}, nil
}
// NewEnclaveFSWithContext creates an EnclaveFS with a provided context (useful for testing)
func NewEnclaveFSWithContext(ctx EnclaveContextIface) *EnclaveFS {
return &EnclaveFS{enclaveCtx: ctx}
}
type Artifact struct {
reader *tar.Reader
}
......@@ -55,11 +63,17 @@ type ArtifactFileWriter struct {
writer io.Writer
}
func NewArtifactFileWriter(path string, writer io.Writer) *ArtifactFileWriter {
return &ArtifactFileWriter{
path: path,
writer: writer,
}
}
func (a *Artifact) ExtractFiles(writers ...*ArtifactFileWriter) error {
paths := make(map[string]io.Writer)
for _, writer := range writers {
canonicalPath := filepath.Clean(writer.path)
canonicalPath = fmt.Sprintf("./%s", canonicalPath)
paths[canonicalPath] = writer.writer
}
......@@ -69,11 +83,12 @@ func (a *Artifact) ExtractFiles(writers ...*ArtifactFileWriter) error {
break
}
if _, ok := paths[header.Name]; !ok {
headerPath := filepath.Clean(header.Name)
if _, ok := paths[headerPath]; !ok {
continue
}
writer := paths[header.Name]
writer := paths[headerPath]
_, err = io.Copy(writer, a.reader)
if err != nil {
return err
......
package artifact
import (
"archive/tar"
"bytes"
"compress/gzip"
"context"
"testing"
"github.com/stretchr/testify/require"
)
type mockEnclaveContext struct {
artifacts map[string][]byte
}
func (m *mockEnclaveContext) DownloadFilesArtifact(_ context.Context, name string) ([]byte, error) {
return m.artifacts[name], nil
}
func createTarGzArtifact(t *testing.T, files map[string]string) []byte {
var buf bytes.Buffer
gzWriter := gzip.NewWriter(&buf)
tarWriter := tar.NewWriter(gzWriter)
for name, content := range files {
err := tarWriter.WriteHeader(&tar.Header{
Name: name,
Mode: 0600,
Size: int64(len(content)),
})
require.NoError(t, err)
_, err = tarWriter.Write([]byte(content))
require.NoError(t, err)
}
require.NoError(t, tarWriter.Close())
require.NoError(t, gzWriter.Close())
return buf.Bytes()
}
func TestArtifactExtraction(t *testing.T) {
tests := []struct {
name string
files map[string]string
requests map[string]string
wantErr bool
}{
{
name: "simple path",
files: map[string]string{
"file1.txt": "content1",
},
requests: map[string]string{
"file1.txt": "content1",
},
},
{
name: "path with dot prefix",
files: map[string]string{
"./file1.txt": "content1",
},
requests: map[string]string{
"file1.txt": "content1",
},
},
{
name: "mixed paths",
files: map[string]string{
"./file1.txt": "content1",
"file2.txt": "content2",
"./dir/f3.txt": "content3",
},
requests: map[string]string{
"file1.txt": "content1",
"file2.txt": "content2",
"dir/f3.txt": "content3",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Create mock context with artifact
mockCtx := &mockEnclaveContext{
artifacts: map[string][]byte{
"test-artifact": createTarGzArtifact(t, tt.files),
},
}
fs := NewEnclaveFSWithContext(mockCtx)
artifact, err := fs.GetArtifact(context.Background(), "test-artifact")
require.NoError(t, err)
// Create writers for all requested files
writers := make([]*ArtifactFileWriter, 0, len(tt.requests))
buffers := make(map[string]*bytes.Buffer, len(tt.requests))
for reqPath := range tt.requests {
buf := &bytes.Buffer{}
buffers[reqPath] = buf
writers = append(writers, NewArtifactFileWriter(reqPath, buf))
}
// Extract all files at once
err = artifact.ExtractFiles(writers...)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
// Verify contents
for reqPath, wantContent := range tt.requests {
require.Equal(t, wantContent, buffers[reqPath].String(), "content mismatch for %s", reqPath)
}
})
}
}
......@@ -8,6 +8,8 @@ import (
"io"
"math/big"
"strings"
"github.com/ethereum-optimism/optimism/kurtosis-devnet/pkg/kurtosis/sources/artifact"
)
const (
......@@ -242,21 +244,21 @@ func parseStateFile(r io.Reader) (*DeployerState, error) {
// ExtractData downloads and parses the op-deployer state
func (d *Deployer) ExtractData(ctx context.Context) (*DeployerData, error) {
fs, err := NewEnclaveFS(ctx, d.enclave)
fs, err := artifact.NewEnclaveFS(ctx, d.enclave)
if err != nil {
return nil, err
}
artifact, err := fs.GetArtifact(ctx, d.deployerArtifactName)
a, err := fs.GetArtifact(ctx, d.deployerArtifactName)
if err != nil {
return nil, err
}
stateBuffer := bytes.NewBuffer(nil)
walletsBuffer := bytes.NewBuffer(nil)
if err := artifact.ExtractFiles(
&ArtifactFileWriter{path: d.stateName, writer: stateBuffer},
&ArtifactFileWriter{path: d.walletsName, writer: walletsBuffer},
if err := a.ExtractFiles(
artifact.NewArtifactFileWriter(d.stateName, stateBuffer),
artifact.NewArtifactFileWriter(d.walletsName, walletsBuffer),
); err != nil {
return nil, err
}
......@@ -283,5 +285,8 @@ func (d *Deployer) ExtractData(ctx context.Context) (*DeployerData, error) {
return nil, err
}
return &DeployerData{State: state, Wallets: knownWallets}, nil
return &DeployerData{
State: state,
Wallets: knownWallets,
}, nil
}
......@@ -6,6 +6,7 @@ import (
"fmt"
"io"
"github.com/ethereum-optimism/optimism/kurtosis-devnet/pkg/kurtosis/sources/artifact"
"github.com/ethereum-optimism/optimism/op-chain-ops/devkeys"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/crypto"
......@@ -33,15 +34,15 @@ func getMnemonics(r io.Reader) (string, error) {
return config[0].Mnemonic, nil
}
func (d *Deployer) getKnownWallets(ctx context.Context, fs *EnclaveFS) ([]*Wallet, error) {
artifact, err := fs.GetArtifact(ctx, d.genesisArtifactName)
func (d *Deployer) getKnownWallets(ctx context.Context, fs *artifact.EnclaveFS) ([]*Wallet, error) {
a, err := fs.GetArtifact(ctx, d.genesisArtifactName)
if err != nil {
return nil, err
}
mnemonicsBuffer := bytes.NewBuffer(nil)
if err := artifact.ExtractFiles(
&ArtifactFileWriter{path: d.mnemonicsName, writer: mnemonicsBuffer},
if err := a.ExtractFiles(
artifact.NewArtifactFileWriter(d.mnemonicsName, mnemonicsBuffer),
); err != nil {
return nil, err
}
......
package kurtosis
package interfaces
import (
"context"
......@@ -6,6 +6,7 @@ import (
"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/jwt"
"github.com/ethereum-optimism/optimism/kurtosis-devnet/pkg/kurtosis/sources/spec"
)
......@@ -20,3 +21,7 @@ type EnclaveInspecter interface {
type EnclaveObserver interface {
EnclaveObserve(context.Context, string) (*deployer.DeployerData, error)
}
type JWTExtractor interface {
ExtractData(context.Context, string) (*jwt.Data, error)
}
package main
import (
"fmt"
"os"
"github.com/ethereum-optimism/optimism/kurtosis-devnet/pkg/kurtosis/sources/jwt"
"github.com/ethereum-optimism/optimism/op-service/cliapp"
"github.com/urfave/cli/v2"
)
var (
GitCommit = ""
GitDate = ""
)
func main() {
app := cli.NewApp()
app.Version = fmt.Sprintf("%s-%s", GitCommit, GitDate)
app.Name = "jwt"
app.Usage = "Tool to extract JWT secrets from Kurtosis enclaves"
app.Flags = cliapp.ProtectFlags([]cli.Flag{
&cli.StringFlag{
Name: "enclave",
Usage: "Name of the Kurtosis enclave",
Required: true,
},
})
app.Action = runJWT
app.Writer = os.Stdout
app.ErrWriter = os.Stderr
err := app.Run(os.Args)
if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "Application failed: %v\n", err)
os.Exit(1)
}
}
func runJWT(ctx *cli.Context) error {
enclave := ctx.String("enclave")
extractor := jwt.NewExtractor(enclave)
data, err := extractor.ExtractData(ctx.Context)
if err != nil {
return fmt.Errorf("failed to extract JWT data: %w", err)
}
// Print the JWT secrets
fmt.Printf("L1 JWT Secret: %s\n", data.L1JWT)
fmt.Printf("L2 JWT Secret: %s\n", data.L2JWT)
return nil
}
package jwt
import (
"bytes"
"context"
"fmt"
"io"
"github.com/ethereum-optimism/optimism/kurtosis-devnet/pkg/kurtosis/sources/artifact"
)
const (
jwtSecretFileName = "jwtsecret"
)
// Data holds the JWT secrets for L1 and L2
type Data struct {
L1JWT string
L2JWT string
}
// extractor implements the interfaces.JWTExtractor interface
type extractor struct {
enclave string
}
// NewExtractor creates a new JWT extractor
func NewExtractor(enclave string) *extractor {
return &extractor{
enclave: enclave,
}
}
// ExtractData extracts JWT secrets from their respective artifacts
func (e *extractor) ExtractData(ctx context.Context) (*Data, error) {
fs, err := artifact.NewEnclaveFS(ctx, e.enclave)
if err != nil {
return nil, err
}
// Get L1 JWT
l1JWT, err := extractJWTFromArtifact(ctx, fs, "jwt_file")
if err != nil {
return nil, fmt.Errorf("failed to get L1 JWT: %w", err)
}
// Get L2 JWT
l2JWT, err := extractJWTFromArtifact(ctx, fs, "op_jwt_file")
if err != nil {
return nil, fmt.Errorf("failed to get L2 JWT: %w", err)
}
return &Data{
L1JWT: l1JWT,
L2JWT: l2JWT,
}, nil
}
func extractJWTFromArtifact(ctx context.Context, fs *artifact.EnclaveFS, artifactName string) (string, error) {
a, err := fs.GetArtifact(ctx, artifactName)
if err != nil {
return "", fmt.Errorf("failed to get artifact: %w", err)
}
buffer := &bytes.Buffer{}
if err := a.ExtractFiles(artifact.NewArtifactFileWriter(jwtSecretFileName, buffer)); err != nil {
return "", fmt.Errorf("failed to extract JWT: %w", err)
}
return parseJWT(buffer)
}
func parseJWT(r io.Reader) (string, error) {
data, err := io.ReadAll(r)
if err != nil {
return "", fmt.Errorf("failed to read JWT file: %w", err)
}
return string(data), nil
}
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