diff --git a/chain/evm/provider/ctf_anvil_provider.go b/chain/evm/provider/ctf_anvil_provider.go index 24a1b597..26435d4a 100644 --- a/chain/evm/provider/ctf_anvil_provider.go +++ b/chain/evm/provider/ctf_anvil_provider.go @@ -208,6 +208,9 @@ import ( "fmt" "math/big" "net/http" + "os" + "path" + "slices" "strconv" "strings" "sync" @@ -217,9 +220,11 @@ import ( "github.com/avast/retry-go/v4" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/crypto" + "github.com/rs/zerolog" chainsel "github.com/smartcontractkit/chain-selectors" "github.com/smartcontractkit/chainlink-testing-framework/framework" "github.com/smartcontractkit/chainlink-testing-framework/framework/components/blockchain" + ctfdocker "github.com/smartcontractkit/chainlink-testing-framework/lib/docker" "github.com/smartcontractkit/freeport" "github.com/testcontainers/testcontainers-go" @@ -516,7 +521,18 @@ func (p *CTFAnvilChainProvider) GetNodeHTTPURL() string { // Returns an error if the container termination fails. func (p *CTFAnvilChainProvider) Cleanup(ctx context.Context) error { if p.container != nil { - err := p.container.Terminate(ctx) + shouldSave, path, err := shouldSaveCtfContainerLogs(os.Getenv(saveContainerLogsEnvVar)) + if err != nil { + return fmt.Errorf("failed to check if we should save the container logs: %w", err) + } + + if shouldSave { + zlogger := zerolog.New(os.Stdout) + ctfdocker.WriteAllContainersLogs(zlogger, path) + zlogger.Info().Msgf("container logs saved to %q", path) + } + + err = p.container.Terminate(ctx) if err != nil { return fmt.Errorf("failed to terminate Anvil container: %w", err) } @@ -714,3 +730,41 @@ func (p *CTFAnvilChainProvider) waitForAnvilReady(ctx context.Context, httpURL s retry.DelayType(retry.FixedDelay), ) } + +const saveContainerLogsEnvVar = "CLDF_CONTAINER_LOGS" + +func shouldSaveCtfContainerLogs(envVarValue string) (bool, string, error) { + if envVarValue == "" { + return false, "", nil + } + + if path.IsAbs(envVarValue) { + if isDirectory(envVarValue) { + return true, envVarValue, nil + } + + err := os.MkdirAll(envVarValue, 0o700) + if err != nil { + return false, "", fmt.Errorf("failed to create container logs dir: %w", err) + } + + return true, envVarValue, nil + } + + trueValues := []string{"1", "true", "on", "enabled"} + if slices.Contains(trueValues, strings.ToLower(envVarValue)) { + tmpDirPath, err := os.MkdirTemp("", "cldf-container-logs-*") + if err != nil { + return false, "", fmt.Errorf("failed to create temporary dir: %w", err) + } + + return true, tmpDirPath, nil + } + + return false, "", fmt.Errorf("invalid value for %s variable: %v", saveContainerLogsEnvVar, envVarValue) +} + +func isDirectory(path string) bool { + fileInfo, err := os.Stat(path) + return err == nil && fileInfo.IsDir() +} diff --git a/chain/evm/provider/ctf_anvil_provider_test.go b/chain/evm/provider/ctf_anvil_provider_test.go index 39ef7b0c..6e2649fc 100644 --- a/chain/evm/provider/ctf_anvil_provider_test.go +++ b/chain/evm/provider/ctf_anvil_provider_test.go @@ -7,12 +7,11 @@ import ( "testing" "time" + "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm" + "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm/provider/rpcclient" "github.com/smartcontractkit/freeport" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - - "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm" - "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm/provider/rpcclient" ) func TestCTFAnvilChainProvider_Initialize(t *testing.T) { @@ -517,3 +516,32 @@ func TestCTFAnvilChainProvider_Cleanup(t *testing.T) { assert.Nil(t, provider.container, "Container reference should remain nil after second cleanup") }) } + +func Test_shouldSaveCtfContainerLogs(t *testing.T) { + tests := []struct { + name string + envvar string + wantShouldSave bool + wantPath string + assertion assert.ErrorAssertionFunc + }{ + { + name: "environment variable not set", + envvar: "", + wantShouldSave: false, + wantPath: "", + assertion: assert.NoError, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Setenv(saveContainerLogsEnvVar, tt.envvar) + + gotShouldSave, gotPath, err := shouldSaveCtfContainerLogs() + + tt.assertion(t, err) + assert.Equal(t, tt.wantShouldSave, gotShouldSave) + assert.Equal(t, tt.wantPath, gotPath) + }) + } +} diff --git a/engine/cld/environment/anvil.go b/engine/cld/environment/anvil.go index 08c2b03c..0954672a 100644 --- a/engine/cld/environment/anvil.go +++ b/engine/cld/environment/anvil.go @@ -117,6 +117,7 @@ type RPCs struct { type ChainConfig struct { ChainID string // chain id as per EIP-155 HTTPRPCs []RPCs // http rpcs to connect to the chain + Provider *evmprov.CTFAnvilChainProvider } // AnvilChainsOutput represents the output of the newAnvilChains function. @@ -203,7 +204,6 @@ func newAnvilChains( } network.Metadata = cfgnet.EVMMetadata{ AnvilConfig: &cfgnet.AnvilConfig{ - Image: "f4hrenh9it/foundry:latest", Port: uint64(ports[0]), //nolint:gosec // G115: int to uint64 conversion is safe here (port numbers are always in valid range) ArchiveHTTPURL: network.RPCs[0].HTTPURL, }, @@ -287,6 +287,7 @@ func newAnvilChains( chainConfigsBySelector[chainSelector] = ChainConfig{ ChainID: chainIDStr, + Provider: provider, HTTPRPCs: []RPCs{ { External: provider.GetNodeHTTPURL(), diff --git a/engine/cld/legacy/cli/mcmsv2/mcms_v2.go b/engine/cld/legacy/cli/mcmsv2/mcms_v2.go index 2a59d65f..08d1166d 100644 --- a/engine/cld/legacy/cli/mcmsv2/mcms_v2.go +++ b/engine/cld/legacy/cli/mcmsv2/mcms_v2.go @@ -619,6 +619,13 @@ func buildExecuteForkCommand(lggr logger.Logger, domain cldf_domain.Domain, prop if err != nil { return fmt.Errorf("error creating config: %w", err) } + defer func() { + for _, chainConfig := range cfg.forkedEnv.ChainConfigs { + if chainConfig.Provider != nil { + chainConfig.Provider.Cleanup(cmd.Context()) + } + } + }() // get the chain URL, chain ID and MCM contract address url := cfg.forkedEnv.ChainConfigs[cfg.chainSelector].HTTPRPCs[0].External diff --git a/go.mod b/go.mod index 4127c555..0d7d75e5 100644 --- a/go.mod +++ b/go.mod @@ -34,6 +34,7 @@ require ( github.com/smartcontractkit/chainlink-protos/job-distributor v0.17.0 github.com/smartcontractkit/chainlink-protos/op-catalog v0.0.4 github.com/smartcontractkit/chainlink-testing-framework/framework v0.11.3 + github.com/smartcontractkit/chainlink-testing-framework/lib v1.54.6 github.com/smartcontractkit/chainlink-testing-framework/seth v1.51.2 github.com/smartcontractkit/chainlink-tron/relayer v0.0.11-0.20250815105909-75499abc4335 github.com/smartcontractkit/freeport v0.1.2 @@ -227,7 +228,7 @@ require ( github.com/rivo/uniseg v0.4.7 // indirect github.com/rogpeppe/go-internal v1.13.1 // indirect github.com/rs/cors v1.11.1 // indirect - github.com/rs/zerolog v1.33.0 // indirect + github.com/rs/zerolog v1.33.0 github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sagikazarmark/locafero v0.11.0 // indirect github.com/samber/lo v1.49.1 // indirect @@ -284,7 +285,6 @@ require ( go.opentelemetry.io/otel/sdk/metric v1.37.0 // indirect go.opentelemetry.io/otel/trace v1.37.0 // indirect go.opentelemetry.io/proto/otlp v1.5.0 // indirect - go.uber.org/atomic v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/ratelimit v0.3.1 // indirect golang.org/x/net v0.43.0 // indirect diff --git a/go.sum b/go.sum index 9de7c4d4..b37f67be 100644 --- a/go.sum +++ b/go.sum @@ -703,6 +703,8 @@ github.com/smartcontractkit/chainlink-sui v0.0.0-20251013155034-5f85c5f450ab h1: github.com/smartcontractkit/chainlink-sui v0.0.0-20251013155034-5f85c5f450ab/go.mod h1:VlyZhVw+a93Sk8rVHOIH6tpiXrMzuWLZrjs1eTIExW8= github.com/smartcontractkit/chainlink-testing-framework/framework v0.11.3 h1:crYKFHTxxt1TNYuPOptjVyJdW4wO15aV5vVuEeLhICY= github.com/smartcontractkit/chainlink-testing-framework/framework v0.11.3/go.mod h1:ssfyl4ynbxSyASGztjuAxhsum5i6uZSHM7Dd0v2p8sc= +github.com/smartcontractkit/chainlink-testing-framework/lib v1.54.6 h1:t1db5+sEbmw/26xQVDqyeY448V4RW8xtAl4eNGpd0uw= +github.com/smartcontractkit/chainlink-testing-framework/lib v1.54.6/go.mod h1:dgwtcefGr+0i+C2S6V/Xgntzm7E5CPxXMyi2OnQvnHI= github.com/smartcontractkit/chainlink-testing-framework/seth v1.51.2 h1:ZJ/8Jx6Be5//TyjPi1pS1uotnmcYq5vVkSyISIymSj8= github.com/smartcontractkit/chainlink-testing-framework/seth v1.51.2/go.mod h1:kHYJnZUqiPF7/xN5273prV+srrLJkS77GbBXHLKQpx0= github.com/smartcontractkit/chainlink-tron/relayer v0.0.11-0.20250815105909-75499abc4335 h1:7bxYNrPpygn8PUSBiEKn8riMd7CXMi/4bjTy0fHhcrY=