From 4bc30939246b49396b6766f84a9bcc0155c791a4 Mon Sep 17 00:00:00 2001 From: skudasov Date: Mon, 3 Nov 2025 19:17:03 +0100 Subject: [PATCH 01/16] change test names, add runtime go version in generated mod --- wasp/tmpl_generator.go | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/wasp/tmpl_generator.go b/wasp/tmpl_generator.go index 40b217540..07a8eff8a 100644 --- a/wasp/tmpl_generator.go +++ b/wasp/tmpl_generator.go @@ -7,6 +7,7 @@ import ( "os" "os/exec" "path/filepath" + "runtime" "strings" "text/template" @@ -51,7 +52,7 @@ const ( const ( GoModTemplate = `module {{.ModuleName}} -go 1.25 +go {{.RuntimeVersion}} ` // TableTestTmpl is a load/chaos table test template @@ -235,7 +236,8 @@ require.NoError(t, err) // GoModParams params for generating go.mod file type GoModParams struct { - ModuleName string + ModuleName string + RuntimeVersion string } // TableTestParams params for generating a table test @@ -480,7 +482,8 @@ func (g *LoadTestCodegen) Write() error { // GenerateGoMod generates a go.mod file func (g *LoadTestCodegen) GenerateGoMod() (string, error) { data := GoModParams{ - ModuleName: g.cfg.moduleName, + ModuleName: g.cfg.moduleName, + RuntimeVersion: strings.Replace(runtime.Version(), "go", "", -1), } return render(GoModTemplate, data) } @@ -533,7 +536,7 @@ func (g *LoadTestCodegen) GenerateTestCases() ([]TestCaseParams, error) { return nil, err } testCases = append(testCases, TestCaseParams{ - Name: fmt.Sprintf("Fail pod %s", pod.Name), + Name: fmt.Sprintf("Fail pod %s=%s", g.cfg.uniqPodLabelKey, pod.Labels[g.cfg.uniqPodLabelKey]), RunFunc: r, }) } @@ -551,7 +554,7 @@ func (g *LoadTestCodegen) GenerateTestCases() ([]TestCaseParams, error) { } testCases = append(testCases, TestCaseParams{ - Name: fmt.Sprintf("Network delay for %s", pod.Name), + Name: fmt.Sprintf("Delay network for pod %s=%s", g.cfg.uniqPodLabelKey, pod.Labels[g.cfg.uniqPodLabelKey]), RunFunc: r, }) } From aed505cb7a907d5197d73a0a5dcc362ee116147a Mon Sep 17 00:00:00 2001 From: skudasov Date: Mon, 17 Nov 2025 10:57:32 +0100 Subject: [PATCH 02/16] devenv codegen --- framework/.gitignore | 4 + framework/cmd/main.go | 51 + framework/tmpl_gen.go | 1826 ++++++++++++++++++++++++++++++++++++ framework/tmpl_gen_test.go | 119 +++ 4 files changed, 2000 insertions(+) create mode 100644 framework/.gitignore create mode 100644 framework/tmpl_gen.go create mode 100644 framework/tmpl_gen_test.go diff --git a/framework/.gitignore b/framework/.gitignore new file mode 100644 index 000000000..0e121751f --- /dev/null +++ b/framework/.gitignore @@ -0,0 +1,4 @@ +# dev environment generated by manually testing CLI +cmd/devenv/ +# dev environments generated by automated tests +test-env*/ \ No newline at end of file diff --git a/framework/cmd/main.go b/framework/cmd/main.go index c380f2179..aed3423ec 100644 --- a/framework/cmd/main.go +++ b/framework/cmd/main.go @@ -25,6 +25,57 @@ func main() { Aliases: []string{"g"}, Usage: "Generates various test templates", Subcommands: []*cli.Command{ + { + Name: "env", + Aliases: []string{"e"}, + Usage: "Generate a Chainlink Node developer environment", + Description: `This utility generate a Chainlink Node Cluster (DON) environment. +Default setup includes: +- Anvil (EVM emulator from Foundry) or Geth blockchain nodes +- Chainlink Nodes +- Fake server (for 3rd party dependencies) +- Test boilerplates, CI and documentation + +Usage: + + Generate a basic environment: + ctf gen env --cli myenv --nodes 4 +`, + ArgsUsage: "", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "cli", + Aliases: []string{"c"}, + Usage: "Your devenv CLI binary name", + }, + &cli.IntFlag{ + Name: "nodes", + Aliases: []string{"n"}, + Value: 4, + Usage: "Chainlink Nodes", + }, + }, + Action: func(c *cli.Context) error { + nodes := c.Int("nodes") + cliName := c.String("cli") + if cliName == "" { + return fmt.Errorf("CLI name can't be empty, choose your CLI name") + } + framework.L.Info(). + Str("Name", cliName). + Int("CLNodes", nodes). + Msg("Generating developer environment") + + cg, err := framework.NewEnvBuilder(cliName, nodes).Build() + if err != nil { + return fmt.Errorf("failed to create codegen: %w", err) + } + if err := cg.Write(); err != nil { + return fmt.Errorf("failed to generate module: %w", err) + } + return nil + }, + }, { Name: "load", Aliases: []string{"l"}, diff --git a/framework/tmpl_gen.go b/framework/tmpl_gen.go new file mode 100644 index 000000000..2a0c74363 --- /dev/null +++ b/framework/tmpl_gen.go @@ -0,0 +1,1826 @@ +package framework + +import ( + "bytes" + "fmt" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" + "text/template" + + "github.com/rs/zerolog/log" +) + +const ( + // DefaultTestSuiteName default table test name + DefaultTestSuiteName = "TestSmokeE2E" +) + +/* Templates */ + +const ( + // GoModTemplate go module template + GoModTemplate = `module {{.ModuleName}} + +go {{.RuntimeVersion}} + +require ( + github.com/smartcontractkit/chainlink-evm v0.0.0-20250709215002-07f34ab867df + github.com/smartcontractkit/chainlink-deployments-framework v0.17.0 + github.com/smartcontractkit/chainlink-testing-framework/framework v0.11.10 +) + +replace github.com/fbsobreira/gotron-sdk => github.com/smartcontractkit/chainlink-tron/relayer/gotron-sdk v0.0.5-0.20250528121202-292529af39df + +replace ( + github.com/gogo/protobuf => github.com/regen-network/protobuf v1.3.3-alpha.regen.1 + github.com/prometheus/common => github.com/prometheus/common v0.62.0 + github.com/smartcontractkit/chainlink-testing-framework/lib => github.com/smartcontractkit/chainlink-testing-framework/lib v1.54.4 +) +` + // ConfigTOMLTmpl is a default env.toml template for devenv describind components configuration + ConfigTOMLTmpl = `[on_chain] + link_contract_address = "0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9" + cl_nodes_funding_eth = 50 + cl_nodes_funding_link = 50 + verification_timeout_sec = 400 + contracts_configuration_timeout_sec = 60 + verify = false + + [on_chain.gas_settings] + fee_cap_multiplier = 2 + tip_cap_multiplier = 2 + + +[[blockchains]] + chain_id = "1337" + docker_cmd_params = ["-b", "1", "--mixed-mining", "--slots-in-an-epoch", "1"] + image = "ghcr.io/foundry-rs/foundry:stable" + port = "8545" + type = "anvil" + +[[nodesets]] + name = "don" + nodes = {{ .Nodes }} + override_mode = "each" + + [nodesets.db] + image = "postgres:15.0" + + {{- range .NodeIndices }} + [[nodesets.node_specs]] + [nodesets.node_specs.node] + image = "public.ecr.aws/chainlink/chainlink:2.26.0" + {{- end }} +` + // CompletionTmpl is a go-prompt library completion template providing interactive prompt + CompletionTmpl = `package main + +import ( + "fmt" + "os" + "os/exec" + "strings" + + "github.com/c-bata/go-prompt" +) + +func getCommands() []prompt.Suggest { + return []prompt.Suggest{ + {Text: "", Description: "Choose command, press for more options after selecting command"}, + {Text: "up", Description: "Spin up the development environment"}, + {Text: "down", Description: "Tear down the development environment"}, + {Text: "restart", Description: "Restart the development environment"}, + {Text: "test", Description: "Perform smoke or load/chaos testing"}, + {Text: "bs", Description: "Manage the Blockscout EVM block explorer"}, + {Text: "obs", Description: "Manage the observability stack"}, + {Text: "db", Description: "Inspect Databases"}, + {Text: "exit", Description: "Exit the interactive shell"}, + } +} + +func getSubCommands(parent string) []prompt.Suggest { + switch parent { + case "test": + return []prompt.Suggest{ + {Text: "soak", Description: "Run {{ .CLIName }} soak test"}, + {Text: "rpc-latency", Description: "Default soak test + 400ms RPC latency (all chains)"}, + {Text: "gas-spikes", Description: "Default soak test + slow and fast gas spikes"}, + {Text: "reorgs", Description: "Default soak test + reorgs (Requires 'up env.toml,env-geth.toml' environment"}, + {Text: "chaos", Description: "Default soak test + chaos (restarts, latency, data loss between services)"}, + } + case "bs": + return []prompt.Suggest{ + {Text: "up", Description: "Spin up Blockscout and listen to dst chain (8555)"}, + {Text: "up -u http://host.docker.internal:8545 -c 1337", Description: "Spin up Blockscout and listen to src chain (8545)"}, + {Text: "down", Description: "Remove Blockscout stack"}, + {Text: "restart", Description: "Restart Blockscout and listen to dst chain (8555)"}, + {Text: "restart -u http://host.docker.internal:8545 -c 1337", Description: "Restart Blockscout and listen to src chain (8545)"}, + } + case "obs": + return []prompt.Suggest{ + {Text: "up", Description: "Spin up observability stack (Loki/Prometheus/Grafana)"}, + {Text: "up -f", Description: "Spin up full observability stack (Pyroscope, cadvisor, postgres exporter)"}, + {Text: "down", Description: "Spin down observability stack"}, + {Text: "restart", Description: "Restart observability stack"}, + {Text: "restart -f", Description: "Restart full observability stack"}, + } + case "u": + fallthrough + case "up": + fallthrough + case "r": + fallthrough + case "restart": + return []prompt.Suggest{ + {Text: "env.toml", Description: "Spin up Anvil <> Anvil local chains, all services, 4 CL nodes"}, + {Text: "env.toml,env-cl-rebuild.toml", Description: "Spin up Anvil <> Anvil local chains, all services, 4 CL nodes (custom build)"}, + {Text: "env.toml,env-geth.toml", Description: "Spin up Geth <> Geth local chains (clique), all services, 4 CL nodes"}, + {Text: "env.toml,env-fuji-fantom.toml", Description: "Spin up testnets: Fuji <> Fantom, all services, 4 CL nodes"}, + } + default: + return []prompt.Suggest{} + } +} + +func executor(in string) { + checkDockerIsRunning() + in = strings.TrimSpace(in) + if in == "" { + return + } + if in == "exit" { + fmt.Println("Goodbye!") + os.Exit(0) + } + + args := strings.Fields(in) + os.Args = append([]string{"{{ .CLIName }}"}, args...) + if err := rootCmd.Execute(); err != nil { + fmt.Printf("Error: %v\n", err) + } +} + +// completer provides autocomplete suggestions for multi-word commands. +func completer(in prompt.Document) []prompt.Suggest { + text := in.TextBeforeCursor() + words := strings.Fields(text) + lastCharIsSpace := len(text) > 0 && text[len(text)-1] == ' ' + + switch { + case len(words) == 0: + return getCommands() + case len(words) == 1: + if lastCharIsSpace { + return getSubCommands(words[0]) + } else { + return prompt.FilterHasPrefix(getCommands(), words[0], true) + } + + case len(words) >= 2: + if lastCharIsSpace { + return []prompt.Suggest{} + } else { + parent := words[0] + currentWord := words[len(words)-1] + return prompt.FilterHasPrefix(getSubCommands(parent), currentWord, true) + } + default: + return []prompt.Suggest{} + } +} + +// resetTerm resets terminal settings to Unix defaults. +func resetTerm() { + cmd := exec.Command("stty", "sane") + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Run() +} + +func StartShell() { + defer resetTerm() + p := prompt.New( + executor, + completer, + prompt.OptionPrefix("{{ .CLIName }}> "), + prompt.OptionPrefixTextColor(prompt.Yellow), + prompt.OptionTitle("CCIP Interactive Shell"), + prompt.OptionMaxSuggestion(15), + prompt.OptionShowCompletionAtStart(), + prompt.OptionCompletionWordSeparator(" "), + prompt.OptionSelectedSuggestionBGColor(prompt.LightGray), + prompt.OptionSelectedSuggestionTextColor(prompt.Black), + prompt.OptionDescriptionBGColor(prompt.DarkGray), + prompt.OptionDescriptionTextColor(prompt.White), + prompt.OptionSuggestionBGColor(prompt.Black), + prompt.OptionSuggestionTextColor(prompt.Green), + prompt.OptionScrollbarThumbColor(prompt.DarkGray), + prompt.OptionScrollbarBGColor(prompt.Black), + prompt.OptionAddKeyBind(prompt.KeyBind{ + Key: prompt.ControlC, + Fn: func(buf *prompt.Buffer) { + fmt.Println("Interrupted, exiting...") + resetTerm() + os.Exit(0) + }, + }), + ) + p.Run() +} +` + // CLITmpl is a Cobra library CLI template with basic devenv commands + CLITmpl = `package main + +import ( + "context" + "fmt" + "os" + "os/exec" + "syscall" + + "github.com/docker/docker/client" + "github.com/spf13/cobra" + + "github.com/smartcontractkit/chainlink-testing-framework/framework" + "{{ .DevEnvPkgImport }}" +) + +const ( + LocalWASPLoadDashboard = "http://localhost:3000/d/WASPLoadTests/wasp-load-test?orgId=1&from=now-5m&to=now&refresh=5s" + Local{{ .CLIName}}Dashboard = "http://localhost:3000/d/f8a04cef-653f-46d3-86df-87c532300672/svr-soak-test?orgId=1&refresh=5s" +) + +var rootCmd = &cobra.Command{ + Use: "{{ .CLIName }}", + Short: "A {{ .CLIName }} local environment tool", + PersistentPreRunE: func(cmd *cobra.Command, args []string) error { + debug, err := cmd.Flags().GetBool("debug") + if err != nil { + return err + } + if debug { + framework.L.Info().Msg("Debug mode enabled, setting CTF_CLNODE_DLV=true") + os.Setenv("CTF_CLNODE_DLV", "true") + } + return nil + }, +} + +var restartCmd = &cobra.Command{ + Use: "restart", + Aliases: []string{"r"}, + Args: cobra.RangeArgs(0, 1), + Short: "Restart development environment, remove apps and apply default configuration again", + RunE: func(cmd *cobra.Command, args []string) error { + var configFile string + if len(args) > 0 { + configFile = args[0] + } else { + configFile = "env.toml" + } + framework.L.Info().Str("Config", configFile).Msg("Reconfiguring development environment") + _ = os.Setenv("CTF_CONFIGS", configFile) + _ = os.Setenv("TESTCONTAINERS_RYUK_DISABLED", "true") + framework.L.Info().Msg("Tearing down the development environment") + err := framework.RemoveTestContainers() + if err != nil { + return fmt.Errorf("failed to clean Docker resources: %w", err) + } + _, err = devenv.NewEnvironment() + return err + }, +} + +var upCmd = &cobra.Command{ + Use: "up", + Aliases: []string{"u"}, + Short: "Spin up the development environment", + Args: cobra.RangeArgs(0, 1), + RunE: func(cmd *cobra.Command, args []string) error { + var configFile string + if len(args) > 0 { + configFile = args[0] + } else { + configFile = "env.toml" + } + framework.L.Info().Str("Config", configFile).Msg("Creating development environment") + _ = os.Setenv("CTF_CONFIGS", configFile) + _ = os.Setenv("TESTCONTAINERS_RYUK_DISABLED", "true") + _, err := devenv.NewEnvironment() + if err != nil { + return err + } + return nil + }, +} + +var downCmd = &cobra.Command{ + Use: "down", + Aliases: []string{"d"}, + Short: "Tear down the development environment", + RunE: func(cmd *cobra.Command, args []string) error { + framework.L.Info().Msg("Tearing down the development environment") + err := framework.RemoveTestContainers() + if err != nil { + return fmt.Errorf("failed to clean Docker resources: %w", err) + } + return nil + }, +} + +var bsCmd = &cobra.Command{ + Use: "bs", + Short: "Manage the Blockscout EVM block explorer", + Long: "Spin up or down the Blockscout EVM block explorer", +} + +var bsUpCmd = &cobra.Command{ + Use: "up", + Aliases: []string{"u"}, + Short: "Spin up Blockscout EVM block explorer", + RunE: func(cmd *cobra.Command, args []string) error { + url, _ := bsCmd.Flags().GetString("url") + chainID, _ := bsCmd.Flags().GetString("chain-id") + return framework.BlockScoutUp(url, chainID) + }, +} + +var bsDownCmd = &cobra.Command{ + Use: "down", + Aliases: []string{"d"}, + Short: "Spin down Blockscout EVM block explorer", + RunE: func(cmd *cobra.Command, args []string) error { + url, _ := bsCmd.Flags().GetString("url") + return framework.BlockScoutDown(url) + }, +} + +var bsRestartCmd = &cobra.Command{ + Use: "restart", + Aliases: []string{"r"}, + Short: "Restart the Blockscout EVM block explorer", + RunE: func(cmd *cobra.Command, args []string) error { + url, _ := bsCmd.Flags().GetString("url") + chainID, _ := bsCmd.Flags().GetString("chain-id") + if err := framework.BlockScoutDown(url); err != nil { + return err + } + return framework.BlockScoutUp(url, chainID) + }, +} + +var obsCmd = &cobra.Command{ + Use: "obs", + Short: "Manage the observability stack", + Long: "Spin up or down the observability stack with subcommands 'up' and 'down'", +} + +var obsUpCmd = &cobra.Command{ + Use: "up", + Aliases: []string{"u"}, + Short: "Spin up the observability stack", + RunE: func(cmd *cobra.Command, args []string) error { + full, _ := cmd.Flags().GetBool("full") + var err error + if full { + err = framework.ObservabilityUpFull() + } else { + err = framework.ObservabilityUp() + } + if err != nil { + return fmt.Errorf("observability up failed: %w", err) + } + devenv.Plog.Info().Msgf("{{ .CLIName }} Dashboard: %s", Local{{ .CLIName }}Dashboard) + devenv.Plog.Info().Msgf("{{ .CLIName }} Load Test Dashboard: %s", LocalWASPLoadDashboard) + return nil + }, +} + +var obsDownCmd = &cobra.Command{ + Use: "down", + Aliases: []string{"d"}, + Short: "Spin down the observability stack", + RunE: func(cmd *cobra.Command, args []string) error { + return framework.ObservabilityDown() + }, +} + +var obsRestartCmd = &cobra.Command{ + Use: "restart", + Aliases: []string{"r"}, + Short: "Restart the observability stack (data wipe)", + RunE: func(cmd *cobra.Command, args []string) error { + if err := framework.ObservabilityDown(); err != nil { + return fmt.Errorf("observability down failed: %w", err) + } + full, _ := cmd.Flags().GetBool("full") + var err error + if full { + err = framework.ObservabilityUpFull() + } else { + err = framework.ObservabilityUp() + } + if err != nil { + return fmt.Errorf("observability up failed: %w", err) + } + devenv.Plog.Info().Msgf("{{ .CLIName }} Dashboard: %s", Local{{ .CLIName }}Dashboard) + devenv.Plog.Info().Msgf("{{ .CLIName }} Load Test Dashboard: %s", LocalWASPLoadDashboard) + return nil + }, +} + +var testCmd = &cobra.Command{ + Use: "test", + Aliases: []string{"t"}, + Short: "Run the tests", + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) != 1 { + return fmt.Errorf("specify the test suite: smoke or load") + } + var testPattern string + switch args[0] { + case "soak": + testPattern = "TestSoak" + case "rpc-latency": + testPattern = "TestSoak/rpc_latency" + case "gas-spikes": + testPattern = "TestSoak/gas" + case "reorg": + testPattern = "TestSoak/reorg" + case "chaos": + testPattern = "TestSoak/chaos" + default: + return fmt.Errorf("test suite %s is unknown, choose between smoke or load", args[0]) + } + + testCmd := exec.Command("go", "test", "-v", "-run", testPattern) + testCmd.Dir = "./tests" + testCmd.Stdout = os.Stdout + testCmd.Stderr = os.Stderr + testCmd.Stdin = os.Stdin + + if err := testCmd.Run(); err != nil { + if exitError, ok := err.(*exec.ExitError); ok { + if status, ok := exitError.Sys().(syscall.WaitStatus); ok { + os.Exit(status.ExitStatus()) + } + os.Exit(1) + } + return fmt.Errorf("failed to run test command: %w", err) + } + return nil + }, +} + +func init() { + rootCmd.PersistentFlags().BoolP("debug", "d", false, "Enable running services with dlv to allow remote debugging.") + + rootCmd.AddCommand(testCmd) + + // Blockscout, on-chain debug + bsCmd.PersistentFlags().StringP("url", "u", "http://host.docker.internal:8555", "EVM RPC node URL (default to dst chain on 8555") + bsCmd.PersistentFlags().StringP("chain-id", "c", "2337", "RPC's Chain ID") + bsCmd.AddCommand(bsUpCmd) + bsCmd.AddCommand(bsDownCmd) + bsCmd.AddCommand(bsRestartCmd) + rootCmd.AddCommand(bsCmd) + + // observability + obsCmd.PersistentFlags().BoolP("full", "f", false, "Enable full observability stack with additional components") + obsCmd.AddCommand(obsRestartCmd) + obsCmd.AddCommand(obsUpCmd) + obsCmd.AddCommand(obsDownCmd) + rootCmd.AddCommand(obsCmd) + + // main env commands + rootCmd.AddCommand(upCmd) + rootCmd.AddCommand(restartCmd) + rootCmd.AddCommand(downCmd) +} + +func checkDockerIsRunning() { + cli, err := client.NewClientWithOpts(client.FromEnv) + if err != nil { + fmt.Println("Can't create Docker client, please check if Docker daemon is running!") + os.Exit(1) + } + defer cli.Close() + _, err = cli.Ping(context.Background()) + if err != nil { + fmt.Println("Docker is not running, please start Docker daemon first!") + os.Exit(1) + } +} + +func main() { + checkDockerIsRunning() + if len(os.Args) == 2 && (os.Args[1] == "shell" || os.Args[1] == "sh") { + _ = os.Setenv("CTF_CONFIGS", "env.toml") // Set default config for shell + + StartShell() + return + } + if err := rootCmd.Execute(); err != nil { + devenv.Plog.Err(err).Send() + os.Exit(1) + } +}` + // TestsTmpl is a an e2e table test template + TestsTmpl = `` + // CLDFTmpl is a Chainlink Deployments Framework template + CLDFTmpl = `package {{ .PackageName }} + +import ( + "context" + "crypto/ecdsa" + "errors" + "fmt" + "math/big" + "strings" + "time" + + "github.com/Masterminds/semver/v3" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/ethclient" + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-deployments-framework/datastore" + "github.com/smartcontractkit/chainlink-deployments-framework/operations" + "github.com/smartcontractkit/chainlink-evm/gethwrappers/shared/generated/link_token" + "github.com/smartcontractkit/chainlink-testing-framework/framework/components/blockchain" + "go.uber.org/zap" + "golang.org/x/sync/errgroup" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + + chainsel "github.com/smartcontractkit/chain-selectors" + cldfchain "github.com/smartcontractkit/chainlink-deployments-framework/chain" + cldfevm "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm" + cldfevmprovider "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm/provider" + cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" + csav1 "github.com/smartcontractkit/chainlink-protos/job-distributor/v1/csa" + jobv1 "github.com/smartcontractkit/chainlink-protos/job-distributor/v1/job" + nodev1 "github.com/smartcontractkit/chainlink-protos/job-distributor/v1/node" +) + +const ( + AnvilKey0 = "ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" + DefaultNativeTransferGasPrice = 21000 +) + +const LinkToken cldf.ContractType = "LinkToken" + +var _ cldf.ChangeSet[[]uint64] = DeployLinkToken + +type JobDistributor struct { + nodev1.NodeServiceClient + jobv1.JobServiceClient + csav1.CSAServiceClient + WSRPC string +} + +type JDConfig struct { + GRPC string + WSRPC string +} + +// DeployLinkToken deploys a link token contract to the chain identified by the ChainSelector. +func DeployLinkToken(e cldf.Environment, chains []uint64) (cldf.ChangesetOutput, error) { //nolint:gocritic + newAddresses := cldf.NewMemoryAddressBook() + deployGrp := errgroup.Group{} + for _, chain := range chains { + family, err := chainsel.GetSelectorFamily(chain) + if err != nil { + return cldf.ChangesetOutput{AddressBook: newAddresses}, err + } + var deployFn func() error + switch family { + case chainsel.FamilyEVM: + // Deploy EVM LINK token + deployFn = func() error { + _, err := deployLinkTokenContractEVM( + e.Logger, e.BlockChains.EVMChains()[chain], newAddresses, + ) + return err + } + default: + return cldf.ChangesetOutput{}, fmt.Errorf("unsupported chain family %s", family) + } + deployGrp.Go(func() error { + err := deployFn() + if err != nil { + e.Logger.Errorw("Failed to deploy link token", "chain", chain, "err", err) + return fmt.Errorf("failed to deploy link token for chain %d: %w", chain, err) + } + return nil + }) + } + return cldf.ChangesetOutput{AddressBook: newAddresses}, deployGrp.Wait() +} + +func deployLinkTokenContractEVM( + lggr logger.Logger, + chain cldfevm.Chain, //nolint:gocritic + ab cldf.AddressBook, +) (*cldf.ContractDeploy[*link_token.LinkToken], error) { + linkToken, err := cldf.DeployContract[*link_token.LinkToken](lggr, chain, ab, + func(chain cldfevm.Chain) cldf.ContractDeploy[*link_token.LinkToken] { + var ( + linkTokenAddr common.Address + tx *types.Transaction + linkToken *link_token.LinkToken + err2 error + ) + if !chain.IsZkSyncVM { + linkTokenAddr, tx, linkToken, err2 = link_token.DeployLinkToken( + chain.DeployerKey, + chain.Client, + ) + } else { + linkTokenAddr, _, linkToken, err2 = link_token.DeployLinkTokenZk( + nil, + chain.ClientZkSyncVM, + chain.DeployerKeyZkSyncVM, + chain.Client, + ) + } + return cldf.ContractDeploy[*link_token.LinkToken]{ + Address: linkTokenAddr, + Contract: linkToken, + Tx: tx, + Tv: cldf.NewTypeAndVersion(LinkToken, *semver.MustParse("1.0.0")), + Err: err2, + } + }) + if err != nil { + lggr.Errorw("Failed to deploy link token", "chain", chain.String(), "err", err) + return linkToken, err + } + return linkToken, nil +} + +// LoadCLDFEnvironment loads CLDF environment with a memory data store and JD client. +func LoadCLDFEnvironment(in *Cfg) (cldf.Environment, error) { + ctx := context.Background() + + getCtx := func() context.Context { + return ctx + } + + // This only generates a brand new datastore and does not load any existing data. + // We will need to figure out how data will be persisted and loaded in the future. + ds := datastore.NewMemoryDataStore().Seal() + + lggr, err := logger.NewWith(func(config *zap.Config) { + config.Development = true + config.Encoding = "console" + }) + if err != nil { + return cldf.Environment{}, fmt.Errorf("failed to create logger: %w", err) + } + + blockchains, err := loadCLDFChains(in.Blockchains) + if err != nil { + return cldf.Environment{}, fmt.Errorf("failed to load CLDF chains: %w", err) + } + + jd, err := NewJDClient(ctx, JDConfig{ + GRPC: in.JD.Out.ExternalGRPCUrl, + WSRPC: in.JD.Out.ExternalWSRPCUrl, + }) + if err != nil { + return cldf.Environment{}, + fmt.Errorf("failed to load offchain client: %w", err) + } + + opBundle := operations.NewBundle( + getCtx, + lggr, + operations.NewMemoryReporter(), + operations.WithOperationRegistry(operations.NewOperationRegistry()), + ) + + return cldf.Environment{ + Name: "local", + Logger: lggr, + ExistingAddresses: cldf.NewMemoryAddressBook(), + DataStore: ds, + Offchain: jd, + GetContext: getCtx, + OperationsBundle: opBundle, + BlockChains: cldfchain.NewBlockChainsFromSlice(blockchains), + }, nil +} + +func loadCLDFChains(bcis []*blockchain.Input) ([]cldfchain.BlockChain, error) { + blockchains := make([]cldfchain.BlockChain, 0) + for _, bci := range bcis { + switch bci.Type { + case "anvil": + bc, err := loadEVMChain(bci) + if err != nil { + return blockchains, fmt.Errorf("failed to load EVM chain %s: %w", bci.ChainID, err) + } + + blockchains = append(blockchains, bc) + default: + return blockchains, fmt.Errorf("unsupported chain type %s", bci.Type) + } + } + + return blockchains, nil +} + +func loadEVMChain(bci *blockchain.Input) (cldfchain.BlockChain, error) { + if bci.Out == nil { + return nil, fmt.Errorf("output configuration for %s blockchain %s is not set", bci.Type, bci.ChainID) + } + + chainDetails, err := chainsel.GetChainDetailsByChainIDAndFamily(bci.ChainID, chainsel.FamilyEVM) + if err != nil { + return nil, fmt.Errorf("failed to get chain details for %s: %w", bci.ChainID, err) + } + + chain, err := cldfevmprovider.NewRPCChainProvider( + chainDetails.ChainSelector, + cldfevmprovider.RPCChainProviderConfig{ + DeployerTransactorGen: cldfevmprovider.TransactorFromRaw( + // TODO: we need to figure out a reliable way to get secrets here that is + // TODO: - easy for developers + // TODO: - works the same way locally, in K8s and in CI + // TODO: - do not require specific AWS access like AWSSecretsManager + // TODO: for now it's just an Anvil 0 key + AnvilKey0, + ), + RPCs: []cldf.RPC{ + { + Name: "default", + WSURL: bci.Out.Nodes[0].ExternalWSUrl, + HTTPURL: bci.Out.Nodes[0].ExternalHTTPUrl, + PreferredURLScheme: cldf.URLSchemePreferenceHTTP, + }, + }, + ConfirmFunctor: cldfevmprovider.ConfirmFuncGeth(1 * time.Minute), + }, + ).Initialize(context.Background()) + if err != nil { + return nil, fmt.Errorf("failed to initialize EVM chain %s: %w", bci.ChainID, err) + } + + return chain, nil +} + +// NewJDClient creates a new JobDistributor client. +func NewJDClient(ctx context.Context, cfg JDConfig) (cldf.OffchainClient, error) { + conn, err := NewJDConnection(cfg) + if err != nil { + return nil, fmt.Errorf("failed to connect Job Distributor service. Err: %w", err) + } + jd := &JobDistributor{ + WSRPC: cfg.WSRPC, + NodeServiceClient: nodev1.NewNodeServiceClient(conn), + JobServiceClient: jobv1.NewJobServiceClient(conn), + CSAServiceClient: csav1.NewCSAServiceClient(conn), + } + + return jd, err +} + +func (jd JobDistributor) GetCSAPublicKey(ctx context.Context) (string, error) { + keypairs, err := jd.ListKeypairs(ctx, &csav1.ListKeypairsRequest{}) + if err != nil { + return "", err + } + if keypairs == nil || len(keypairs.Keypairs) == 0 { + return "", errors.New("no keypairs found") + } + csakey := keypairs.Keypairs[0].PublicKey + return csakey, nil +} + +// ProposeJob proposes jobs through the jobService and accepts the proposed job on selected node based on ProposeJobRequest.NodeId. +func (jd JobDistributor) ProposeJob(ctx context.Context, in *jobv1.ProposeJobRequest, opts ...grpc.CallOption) (*jobv1.ProposeJobResponse, error) { + res, err := jd.JobServiceClient.ProposeJob(ctx, in, opts...) + if err != nil { + return nil, fmt.Errorf("failed to propose job. err: %w", err) + } + if res.Proposal == nil { + return nil, errors.New("failed to propose job. err: proposal is nil") + } + + return res, nil +} + +// NewJDConnection creates new gRPC connection with JobDistributor. +func NewJDConnection(cfg JDConfig) (*grpc.ClientConn, error) { + opts := []grpc.DialOption{ + grpc.WithTransportCredentials(insecure.NewCredentials()), + } + interceptors := []grpc.UnaryClientInterceptor{} + + if len(interceptors) > 0 { + opts = append(opts, grpc.WithChainUnaryInterceptor(interceptors...)) + } + + conn, err := grpc.NewClient(cfg.GRPC, opts...) + if err != nil { + return nil, fmt.Errorf("failed to connect Job Distributor service. Err: %w", err) + } + + return conn, nil +} + +// FundNodeEIP1559 funds CL node using RPC URL, recipient address and amount of funds to send (ETH). +// Uses EIP-1559 transaction type. +func FundNodeEIP1559(c *ethclient.Client, pkey, recipientAddress string, amountOfFundsInETH float64) error { + amount := new(big.Float).Mul(big.NewFloat(amountOfFundsInETH), big.NewFloat(1e18)) + amountWei, _ := amount.Int(nil) + Plog.Info().Str("Addr", recipientAddress).Str("Wei", amountWei.String()).Msg("Funding Node") + + chainID, err := c.NetworkID(context.Background()) + if err != nil { + return err + } + privateKeyStr := strings.TrimPrefix(pkey, "0x") + privateKey, err := crypto.HexToECDSA(privateKeyStr) + if err != nil { + return err + } + publicKey := privateKey.Public() + publicKeyECDSA, ok := publicKey.(*ecdsa.PublicKey) + if !ok { + return errors.New("error casting public key to ECDSA") + } + fromAddress := crypto.PubkeyToAddress(*publicKeyECDSA) + + nonce, err := c.PendingNonceAt(context.Background(), fromAddress) + if err != nil { + return err + } + feeCap, err := c.SuggestGasPrice(context.Background()) + if err != nil { + return err + } + tipCap, err := c.SuggestGasTipCap(context.Background()) + if err != nil { + return err + } + recipient := common.HexToAddress(recipientAddress) + tx := types.NewTx(&types.DynamicFeeTx{ + ChainID: chainID, + Nonce: nonce, + To: &recipient, + Value: amountWei, + Gas: DefaultNativeTransferGasPrice, + GasFeeCap: feeCap, + GasTipCap: tipCap, + }) + signedTx, err := types.SignTx(tx, types.NewLondonSigner(chainID), privateKey) + if err != nil { + return err + } + err = c.SendTransaction(context.Background(), signedTx) + if err != nil { + return err + } + if _, err := bind.WaitMined(context.Background(), c, signedTx); err != nil { + return err + } + Plog.Info().Str("Wei", amountWei.String()).Msg("Funded with ETH") + return nil +} + +/* +This is just a basic ETH client, CLDF should provide something like this +*/ + +// ETHClient creates a basic Ethereum client using PRIVATE_KEY env var and tip/cap gas settings +func ETHClient(in *Cfg) (*ethclient.Client, *bind.TransactOpts, string, error) { + rpcURL := in.Blockchains[0].Out.Nodes[0].ExternalWSUrl + client, err := ethclient.Dial(rpcURL) + if err != nil { + return nil, nil, "", fmt.Errorf("could not connect to eth client: %w", err) + } + privateKey, err := crypto.HexToECDSA(GetNetworkPrivateKey()) + if err != nil { + return nil, nil, "", fmt.Errorf("could not parse private key: %w", err) + } + publicKey := privateKey.PublicKey + address := crypto.PubkeyToAddress(publicKey).String() + chainID, err := client.ChainID(context.Background()) + if err != nil { + return nil, nil, "", fmt.Errorf("could not get chain ID: %w", err) + } + auth, err := bind.NewKeyedTransactorWithChainID(privateKey, chainID) + if err != nil { + return nil, nil, "", fmt.Errorf("could not create transactor: %w", err) + } + gasSettings := in.OnChain.GasSettings + fc, tc, err := MultiplyEIP1559GasPrices(client, gasSettings.FeeCapMultiplier, gasSettings.TipCapMultiplier) + if err != nil { + return nil, nil, "", fmt.Errorf("could not get bumped gas price: %w", err) + } + auth.GasFeeCap = fc + auth.GasTipCap = tc + Plog.Info(). + Str("GasFeeCap", fc.String()). + Str("GasTipCap", tc.String()). + Msg("Default gas prices set") + return client, auth, address, nil +} + +// MultiplyEIP1559GasPrices returns bumped EIP1159 gas prices increased by multiplier +func MultiplyEIP1559GasPrices(client *ethclient.Client, fcMult, tcMult int64) (*big.Int, *big.Int, error) { + feeCap, err := client.SuggestGasPrice(context.Background()) + if err != nil { + return nil, nil, err + } + tipCap, err := client.SuggestGasTipCap(context.Background()) + if err != nil { + return nil, nil, err + } + + return new(big.Int).Mul(feeCap, big.NewInt(fcMult)), new(big.Int).Mul(tipCap, big.NewInt(tcMult)), nil +} +` + // DebugToolsTmpl is a template for various debug tools, tracing, tx debug, etc + DebugToolsTmpl = `package {{ .PackageName }} + +import ( + "os" + "runtime/trace" +) + + +func tracing() func() { + f, err := os.Create("trace.out") + if err != nil { + panic("can't create trace.out file") + } + if err := trace.Start(f); err != nil { + panic("can't start tracing") + } + return func() { + trace.Stop() + f.Close() + } +} +` + // ConfigTmpl is a template for reading and writing devenv configuration (env.toml, env-out.toml) + ConfigTmpl = `package {{ .PackageName }} + +/* +This file provides a simple boilerplate for TOML configuration with overrides +It has 4 functions: Load[T], Store[T], LoadCache[T] and GetNetworkPrivateKey + +To configure the environment we use a set of files we read from the env var CTF_CONFIGS=env.toml,overrides.toml (can be more than 2) in Load[T] +To store infra or product component outputs we use Store[T] that creates env-cache.toml file. +This file can be used in tests or in any other code that integrated with dev environment. +LoadCache[T] is used if you need to write outputs the second time. + +GetNetworkPrivateKey is used to get your network private key from the env var we are using across all our environments, or fallback to default Anvil's key. +*/ + +import ( + "errors" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/davecgh/go-spew/spew" + "github.com/pelletier/go-toml/v2" + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" +) + +const ( + // DefaultConfigDir is the default directory we are expecting TOML config to be. + DefaultConfigDir = "." + // EnvVarTestConfigs is the environment variable name to read config paths from, ex.: CTF_CONFIGS=env.toml,overrides.toml. + EnvVarTestConfigs = "CTF_CONFIGS" + // DefaultOverridesFilePath is the default overrides.toml file path. + DefaultOverridesFilePath = "overrides.toml" + // DefaultAnvilKey is a default, well-known Anvil first key + DefaultAnvilKey = "ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" +) + +var L = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}).Level(zerolog.InfoLevel) + +// Load loads TOML configurations from environment variable, ex.: CTF_CONFIGS=env.toml,overrides.toml +// and unmarshalls the files from left to right overriding keys. +func Load[T any]() (*T, error) { + var config T + paths := strings.Split(os.Getenv(EnvVarTestConfigs), ",") + for _, path := range paths { + L.Info().Str("Path", path).Msg("Loading configuration input") + data, err := os.ReadFile(filepath.Join(DefaultConfigDir, path)) //nolint:gosec + if err != nil { + if path == DefaultOverridesFilePath { + L.Info().Str("Path", path).Msg("Overrides file not found or empty") + continue + } + return nil, fmt.Errorf("error reading config file %s: %w", path, err) + } + if L.GetLevel() == zerolog.TraceLevel { + fmt.Println(string(data)) //nolint:forbidigo + } + + decoder := toml.NewDecoder(strings.NewReader(string(data))) + decoder.DisallowUnknownFields() + + if err := decoder.Decode(&config); err != nil { + var details *toml.StrictMissingError + if errors.As(err, &details) { + fmt.Println(details.String()) //nolint:forbidigo + } + return nil, fmt.Errorf("failed to decode TOML config, strict mode: %s", err) + } + } + if L.GetLevel() == zerolog.TraceLevel { + L.Trace().Msg("Merged inputs") + spew.Dump(config) //nolint:forbidigo + } + return &config, nil +} + +// Store writes config to a file, adds -cache.toml suffix if it's an initial configuration. +func Store[T any](cfg *T) error { + baseConfigPath, err := BaseConfigPath() + if err != nil { + return err + } + newCacheName := strings.ReplaceAll(baseConfigPath, ".toml", "") + var outCacheName string + if strings.Contains(newCacheName, "cache") { + L.Info().Str("Cache", baseConfigPath).Msg("Cache file already exists, overriding") + outCacheName = baseConfigPath + } else { + outCacheName = fmt.Sprintf("%s-out.toml", strings.ReplaceAll(baseConfigPath, ".toml", "")) + } + L.Info().Str("OutputFile", outCacheName).Msg("Storing configuration output") + d, err := toml.Marshal(cfg) + if err != nil { + return err + } + return os.WriteFile(filepath.Join(DefaultConfigDir, outCacheName), d, 0o600) +} + +// LoadOutput loads config output file from path. +func LoadOutput[T any](path string) (*T, error) { + _ = os.Setenv(EnvVarTestConfigs, path) + return Load[T]() +} + +// BaseConfigPath returns base config path, ex. env.toml,overrides.toml -> env.toml. +func BaseConfigPath() (string, error) { + configs := os.Getenv(EnvVarTestConfigs) + if configs == "" { + return "", fmt.Errorf("no %s env var is provided, you should provide at least one test config in TOML", EnvVarTestConfigs) + } + L.Debug().Str("Configs", configs).Msg("Getting base config path") + return strings.Split(configs, ",")[0], nil +} + +// GetNetworkPrivateKey gets network private key or fallback to default simulator key (Anvil's first key) +func GetNetworkPrivateKey() string { + pk := os.Getenv("PRIVATE_KEY") + if pk == "" { + // that's the first Anvil and Geth private key, serves as a fallback for local testing if not overridden + return DefaultAnvilKey + } + return pk +} +` + // EnvironmentTmpl is an environment.go template - main file for environment composition + EnvironmentTmpl = `package {{ .PackageName }} + +import ( + "fmt" + + "github.com/smartcontractkit/chainlink-testing-framework/framework" + "github.com/smartcontractkit/chainlink-testing-framework/framework/components/blockchain" + "github.com/smartcontractkit/chainlink-testing-framework/framework/components/jd" + + ns "github.com/smartcontractkit/chainlink-testing-framework/framework/components/simple_node_set" +) + +type Cfg struct { + OnChain *OnChain ` + "`" + `toml:"on_chain"` + "`" + ` + Blockchains []*blockchain.Input ` + "`" + `toml:"blockchains" validate:"required"` + "`" + ` + NodeSets []*ns.Input ` + "`" + `toml:"nodesets" validate:"required"` + "`" + ` + JD *jd.Input ` + "`" + `toml:"jd"` + "`" + ` +} + +func NewEnvironment() (*Cfg, error) { + endTracing := tracing() + defer endTracing() + + if err := framework.DefaultNetwork(nil); err != nil { + return nil, err + } + in, err := Load[Cfg]() + if err != nil { + return nil, fmt.Errorf("failed to load configuration: %w", err) + } + _, err = blockchain.NewBlockchainNetwork(in.Blockchains[0]) + if err != nil { + return nil, fmt.Errorf("failed to create blockchain network 1337: %w", err) + } + if err := DefaultProductConfiguration(in, ConfigureNodesNetwork); err != nil { + return nil, fmt.Errorf("failed to setup default CLDF orchestration: %w", err) + } + _, err = ns.NewSharedDBNodeSet(in.NodeSets[0], nil) + if err != nil { + return nil, fmt.Errorf("failed to create new shared db node set: %w", err) + } + if err := DefaultProductConfiguration(in, ConfigureProductContractsJobs); err != nil { + return nil, fmt.Errorf("failed to setup default CLDF orchestration: %w", err) + } + return in, Store[Cfg](in) +} +` + // SingleNetworkProductConfigurationTmpl is an single-network product configuration template + SingleNetworkProductConfigurationTmpl = `package {{ .PackageName }} + +import ( + "context" + "fmt" + "math/big" + "os" + "time" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/ethclient" + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" + "github.com/smartcontractkit/chainlink-evm/gethwrappers/shared/generated/link_token" + "github.com/smartcontractkit/chainlink-testing-framework/framework/clclient" +) + +const ( + ConfigureNodesNetwork ConfigPhase = iota + ConfigureProductContractsJobs +) + +var Plog = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}).Level(zerolog.DebugLevel).With().Fields(map[string]any{"component": "on_chain"}).Logger() + +type OnChain struct { + LinkContractAddress string ` + "`" + `toml:"link_contract_address"` + "`" + ` + CLNodesFundingETH float64 ` + "`" + `toml:"cl_nodes_funding_eth"` + "`" + ` + CLNodesFundingLink float64 ` + "`" + `toml:"cl_nodes_funding_link"` + "`" + ` + VerificationTimeoutSec time.Duration ` + "`" + `toml:"verification_timeout_sec"` + "`" + ` + ContractsConfigurationTimeoutSec time.Duration ` + "`" + `toml:"contracts_configuration_timeout_sec"` + "`" + ` + GasSettings *GasSettings ` + "`" + `toml:"gas_settings"` + "`" + ` + Verify bool ` + "`" + `toml:"verify"` + "`" + ` + DeployedContracts *DeployedContracts ` + "`" + `toml:"deployed_contracts"` + "`" + ` +} + +type DeployedContracts struct { + SomeContractAddr string ` + "`" + `toml:"some_contract_addr"` + "`" + ` +} + + +type GasSettings struct { + FeeCapMultiplier int64 ` + "`" + `toml:"fee_cap_multiplier"` + "`" + ` + TipCapMultiplier int64 ` + "`" + `toml:"tip_cap_multiplier"` + "`" + ` +} + +type Jobs struct { + ConfigPollIntervalSeconds time.Duration ` + "`" + `toml:"config_poll_interval_sec"` + "`" + ` + MaxTaskDurationSec time.Duration ` + "`" + `toml:"max_task_duration_sec"` + "`" + ` +} + +type ConfigPhase int + +// deployLinkAndMint is a universal action that deploys link token and mints required amount of LINK token for all the nodes. +func deployLinkAndMint(ctx context.Context, in *Cfg, c *ethclient.Client, auth *bind.TransactOpts, rootAddr string, transmitters []common.Address) (*link_token.LinkToken, error) { + addr, tx, lt, err := link_token.DeployLinkToken(auth, c) + if err != nil { + return nil, fmt.Errorf("could not create link token contract: %w", err) + } + _, err = bind.WaitDeployed(ctx, c, tx) + if err != nil { + return nil, err + } + Plog.Info().Str("Address", addr.Hex()).Msg("Deployed link token contract") + tx, err = lt.GrantMintRole(auth, common.HexToAddress(rootAddr)) + if err != nil { + return nil, fmt.Errorf("could not grant mint role: %w", err) + } + _, err = bind.WaitMined(ctx, c, tx) + if err != nil { + return nil, err + } + // mint for public keys of nodes directly instead of transferring + for _, transmitter := range transmitters { + amount := new(big.Float).Mul(big.NewFloat(in.OnChain.CLNodesFundingLink), big.NewFloat(1e18)) + amountWei, _ := amount.Int(nil) + Plog.Info().Msgf("Minting LINK for transmitter address: %s", transmitter.Hex()) + tx, err = lt.Mint(auth, transmitter, amountWei) + if err != nil { + return nil, fmt.Errorf("could not transfer link token contract: %w", err) + } + _, err = bind.WaitMined(ctx, c, tx) + if err != nil { + return nil, err + } + } + return lt, nil +} + + +func configureContracts(in *Cfg, c *ethclient.Client, auth *bind.TransactOpts, cl []*clclient.ChainlinkClient, rootAddr string, transmitters []common.Address) (*DeployedContracts, error) { + ctx, cancel := context.WithTimeout(context.Background(), in.OnChain.ContractsConfigurationTimeoutSec*time.Second) + defer cancel() + Plog.Info().Msg("Deploying LINK token contract") + _, err := deployLinkAndMint(ctx, in, c, auth, rootAddr, transmitters) + if err != nil { + return nil, fmt.Errorf("could not create link token contract and mint: %w", err) + } + // TODO: use client and deploy your contracts + return &DeployedContracts{ + SomeContractAddr: "", + }, nil +} + +func configureJobs(in *Cfg, clNodes []*clclient.ChainlinkClient, contracts *DeployedContracts) error { + bootstrapNode := clNodes[0] + workerNodes := clNodes[1:] + // TODO: define your jobs + job := "" + _, _, err := bootstrapNode.CreateJobRaw(job) + if err != nil { + return fmt.Errorf("creating bootstrap job have failed: %w", err) + } + + for _, chainlinkNode := range workerNodes { + // TODO: define your job for nodes here + job := "" + _, _, err = chainlinkNode.CreateJobRaw(job) + if err != nil { + return fmt.Errorf("creating job on node have failed: %w", err) + } + } + return nil +} + +// DefaultProductConfiguration is default product configuration that includes: +// - Deploying required prerequisites (LINK token, shared contracts) +// - Applying product-specific changesets +// - Creating cldf.Environment, connecting to components, see *Cfg fields +// - Generating CL nodes configs +// All the data can be added *Cfg struct like and is synced between local machine and remote environment +// so later both local and remote tests can use it. +func DefaultProductConfiguration(in *Cfg, phase ConfigPhase) error { + pkey := GetNetworkPrivateKey() + if pkey == "" { + return fmt.Errorf("PRIVATE_KEY environment variable not set") + } + switch phase { + case ConfigureNodesNetwork: + Plog.Info().Msg("Applying default CL nodes configuration") + node := in.Blockchains[0].Out.Nodes[0] + chainID := in.Blockchains[0].ChainID + // configure node set and generate CL nodes configs + netConfig := fmt.Sprintf(` + "`" + ` + [[EVM]] + LogPollInterval = '1s' + BlockBackfillDepth = 100 + LinkContractAddress = '%s' + ChainID = '%s' + MinIncomingConfirmations = 1 + MinContractPayment = '0.0000001 link' + FinalityDepth = %d + + [[EVM.Nodes]] + Name = 'default' + WsUrl = '%s' + HttpUrl = '%s' + + [Feature] + FeedsManager = true + LogPoller = true + UICSAKeys = true + [OCR2] + Enabled = true + SimulateTransactions = false + DefaultTransactionQueueDepth = 1 + [P2P.V2] + Enabled = true + ListenAddresses = ['0.0.0.0:6690'] + + [Log] + JSONConsole = true + Level = 'debug' + [Pyroscope] + ServerAddress = 'http://host.docker.internal:4040' + Environment = 'local' + [WebServer] + SessionTimeout = '999h0m0s' + HTTPWriteTimeout = '3m' + SecureCookies = false + HTTPPort = 6688 + [WebServer.TLS] + HTTPSPort = 0 + [WebServer.RateLimit] + Authenticated = 5000 + Unauthenticated = 5000 + [JobPipeline] + [JobPipeline.HTTPRequest] + DefaultTimeout = '1m' + [Log.File] + MaxSize = '0b' +` + "`" + `, in.OnChain.LinkContractAddress, chainID, 5, node.InternalWSUrl, node.InternalHTTPUrl) + for _, nodeSpec := range in.NodeSets[0].NodeSpecs { + nodeSpec.Node.TestConfigOverrides = netConfig + } + Plog.Info().Msg("Nodes network configuration is finished") + case ConfigureProductContractsJobs: + Plog.Info().Msg("Connecting to CL nodes") + nodeClients, err := clclient.New(in.NodeSets[0].Out.CLNodes) + if err != nil { + return err + } + transmitters := make([]common.Address, 0) + ethKeyAddresses := make([]string, 0) + for i, nc := range nodeClients { + addr, err := nc.ReadPrimaryETHKey(in.Blockchains[0].ChainID) + if err != nil { + return err + } + ethKeyAddresses = append(ethKeyAddresses, addr.Attributes.Address) + transmitters = append(transmitters, common.HexToAddress(addr.Attributes.Address)) + Plog.Info(). + Int("Idx", i). + Str("ETH", addr.Attributes.Address). + Msg("Node info") + } + // ETH examples + c, auth, rootAddr, err := ETHClient(in) + if err != nil { + return fmt.Errorf("could not create basic eth client: %w", err) + } + for _, addr := range ethKeyAddresses { + if err := FundNodeEIP1559(c, pkey, addr, in.OnChain.CLNodesFundingETH); err != nil { + return err + } + } + contracts, err := configureContracts(in, c, auth, nodeClients, rootAddr, transmitters) + if err != nil { + return err + } + if err := configureJobs(in, nodeClients, contracts); err != nil { + return err + } + Plog.Info().Str("BootstrapNode", in.NodeSets[0].Out.CLNodes[0].Node.ExternalURL).Send() + for _, n := range in.NodeSets[0].Out.CLNodes[1:] { + Plog.Info().Str("Node", n.Node.ExternalURL).Send() + } + in.OnChain.DeployedContracts = contracts + } + return nil +} +` + // JustFileTmpl is a Justfile template used for building and publishing Docker images + JustFileTmpl = `set fallback + +# Default: show available recipes +default: + just --list + +clean: + rm -rf compose/ blockscout/ + +build-fakes: + just fakes/build + +push-fakes: + just fakes/push + +# Rebuild CLI +cli: + pushd cmd/{{ .CLIName }} > /dev/null && go install -ldflags="-X main.Version=1.0.0" . && popd > /dev/null` +) + +/* Template params in heirarchical order, module -> file(table test) -> test */ + +// GoModParams params for generating go.mod file +type GoModParams struct { + ModuleName string + RuntimeVersion string +} + +// ConfigTOMLParams default env.toml params +type ConfigTOMLParams struct { + PackageName string + Nodes int + NodeIndices []int +} + +// JustfileParams Justfile params +type JustfileParams struct { + PackageName string + CLIName string +} + +// CLICompletionParams cli.go file params +type CLICompletionParams struct { + PackageName string + CLIName string +} + +// CLIParams cli.go file params +type CLIParams struct { + PackageName string + CLIName string + DevEnvPkgImport string +} + +// CLDFParams cldf.go file params +type CLDFParams struct { + PackageName string +} + +// ToolsParams tools.go file params +type ToolsParams struct { + PackageName string +} + +// ConfigParams config.go file params +type ConfigParams struct { + PackageName string +} + +// EnvParams environment.go file params +type EnvParams struct { + PackageName string +} + +// ProductConfigurationSimple product_configuration.go file params +type ProductConfigurationSimple struct { + PackageName string +} + +// TableTestParams params for generating a table test +type TableTestParams struct { + Package string + Namespace string + TableTestName string + TestCases []TestCaseParams + WorkloadCode string + GunCode string +} + +// TestCaseParams params for generating a test case +type TestCaseParams struct { + Name string + RunFunc string +} + +/* Codegen logic */ + +// EnvBuilder builder for load test codegen +type EnvBuilder struct { + nodes int + namespace string + outputDir string + packageName string + cliName string + moduleName string +} + +// EnvCodegen is a load test code generator that creates workload and chaos experiments +type EnvCodegen struct { + cfg *EnvBuilder +} + +// NewEnvBuilder creates a new Chainlink Cluster developer environment +func NewEnvBuilder(cliName string, nodes int) *EnvBuilder { + return &EnvBuilder{ + cliName: cliName, + nodes: nodes, + packageName: "devenv", + outputDir: "devenv", + moduleName: fmt.Sprintf("github.com/smartcontractkit/%s/devenv", cliName), + } +} + +// OutputDir sets the output directory for generated files +func (g *EnvBuilder) OutputDir(dir string) *EnvBuilder { + g.outputDir = dir + return g +} + +// GoModName sets the Go module name for the generated project +func (g *EnvBuilder) GoModName(name string) *EnvBuilder { + g.moduleName = name + return g +} + +// validate verifier that we can build codegen with provided params +// empty for now, add validation here if later it'd become more complex +func (g *EnvBuilder) validate() error { + return nil +} + +// Validate validate generation params +// for now it's empty but for more complex mutually exclusive cases we should +// add validation here +func (g *EnvBuilder) Build() (*EnvCodegen, error) { + if err := g.validate(); err != nil { + return nil, err + } + return &EnvCodegen{g}, nil +} + +// Read read K8s namespace and find all the pods +// some pods may be crashing but it doesn't matter for code generation +func (g *EnvCodegen) Read() error { + return nil +} + +// Write generates a complete boilerplate, can be multiple files +func (g *EnvCodegen) Write() error { + // Create output directory + if err := os.MkdirAll(g.cfg.outputDir, os.ModePerm); err != nil { + return fmt.Errorf("failed to create output directory: %w", err) + } + + // Generate go.mod file + goModContent, err := g.GenerateGoMod() + if err != nil { + return err + } + if err := os.WriteFile( + filepath.Join(g.cfg.outputDir, "go.mod"), + []byte(goModContent), + os.ModePerm, + ); err != nil { + return fmt.Errorf("failed to write go.mod: %w", err) + } + + // Generate Justfile + justContents, err := g.GenerateJustfile() + if err != nil { + return err + } + if err := os.WriteFile( + filepath.Join(g.cfg.outputDir, "Justfile"), + []byte(justContents), + os.ModePerm, + ); err != nil { + return fmt.Errorf("failed to write CLI completion file: %w", err) + } + + // Generate default env.toml file + tomlContents, err := g.GenerateDefaultTOMLConfig() + if err != nil { + return err + } + if err := os.WriteFile( + filepath.Join(g.cfg.outputDir, "env.toml"), + []byte(tomlContents), + os.ModePerm, + ); err != nil { + return fmt.Errorf("failed to write default env.toml file: %w", err) + } + + cliDir := filepath.Join(g.cfg.outputDir, "cmd", g.cfg.cliName) + if err := os.MkdirAll(cliDir, os.ModePerm); err != nil { + return fmt.Errorf("failed to create CLI directory: %w", err) + } + + // Generate CLI file + cliContents, err := g.GenerateCLI() + if err != nil { + return err + } + if err := os.WriteFile( + filepath.Join(cliDir, fmt.Sprintf("%s.go", g.cfg.cliName)), + []byte(cliContents), + os.ModePerm, + ); err != nil { + return fmt.Errorf("failed to write CLI file: %w", err) + } + + // Generate completion file + completionContents, err := g.GenerateCLICompletion() + if err != nil { + return err + } + if err := os.WriteFile( + filepath.Join(cliDir, "completion.go"), + []byte(completionContents), + os.ModePerm, + ); err != nil { + return fmt.Errorf("failed to write CLI completion file: %w", err) + } + + // Generate tools.go + toolsContents, err := g.GenerateDebugTools() + if err != nil { + return err + } + if err := os.WriteFile( + filepath.Join(g.cfg.outputDir, "tools.go"), + []byte(toolsContents), + os.ModePerm, + ); err != nil { + return fmt.Errorf("failed to write config file: %w", err) + } + + // Generate config.go + configFileContents, err := g.GenerateConfig() + if err != nil { + return err + } + if err := os.WriteFile( + filepath.Join(g.cfg.outputDir, "config.go"), + []byte(configFileContents), + os.ModePerm, + ); err != nil { + return fmt.Errorf("failed to write config file: %w", err) + } + + // Generate cldf.go + cldfContents, err := g.GenerateCLDF() + if err != nil { + return err + } + if err := os.WriteFile( + filepath.Join(g.cfg.outputDir, "cldf.go"), + []byte(cldfContents), + os.ModePerm, + ); err != nil { + return fmt.Errorf("failed to write config file: %w", err) + } + + // Generate environment.go + envFileContents, err := g.GenerateEnvironment() + if err != nil { + return err + } + if err := os.WriteFile( + filepath.Join(g.cfg.outputDir, "environment.go"), + []byte(envFileContents), + os.ModePerm, + ); err != nil { + return fmt.Errorf("failed to write environment file: %w", err) + } + + // Generate product_configuration.go + prodConfigFileContents, err := g.GenerateSingleNetworkProductConfiguration() + if err != nil { + return err + } + if err := os.WriteFile( + filepath.Join(g.cfg.outputDir, "product_configuration.go"), + []byte(prodConfigFileContents), + os.ModePerm, + ); err != nil { + return fmt.Errorf("failed to write product configration file: %w", err) + } + + // tidy and finalize + currentDir, err := os.Getwd() + if err != nil { + return err + } + + // nolint + defer os.Chdir(currentDir) + if err := os.Chdir(g.cfg.outputDir); err != nil { + return err + } + log.Info().Msg("Downloading dependencies and running 'go mod tidy' ..") + _, err = exec.Command("go", "mod", "tidy").CombinedOutput() + if err != nil { + return fmt.Errorf("failed to tidy generated module: %w", err) + } + log.Info(). + Str("OutputDir", g.cfg.outputDir). + Str("Module", g.cfg.moduleName). + Msg("Developer environment generated") + return nil +} + +// GenerateGoMod generates a go.mod file +func (g *EnvCodegen) GenerateGoMod() (string, error) { + data := GoModParams{ + ModuleName: g.cfg.moduleName, + RuntimeVersion: strings.Replace(runtime.Version(), "go", "", -1), + } + return render(GoModTemplate, data) +} + +// GenerateDefaultTOMLConfig generate default env.toml config +func (g *EnvCodegen) GenerateDefaultTOMLConfig() (string, error) { + p := ConfigTOMLParams{ + PackageName: g.cfg.packageName, + Nodes: g.cfg.nodes, + NodeIndices: make([]int, g.cfg.nodes), + } + return render(ConfigTOMLTmpl, p) +} + +// GenerateJustfile generate Justfile to build and publish Docker images +func (g *EnvCodegen) GenerateJustfile() (string, error) { + p := JustfileParams{ + PackageName: g.cfg.packageName, + CLIName: g.cfg.cliName, + } + return render(JustFileTmpl, p) +} + +// GenerateCLICompletion generate CLI completion for "go-prompt" library +func (g *EnvCodegen) GenerateCLICompletion() (string, error) { + p := CLICompletionParams{ + PackageName: g.cfg.packageName, + CLIName: g.cfg.cliName, + } + return render(CompletionTmpl, p) +} + +// GenerateCLI generate Cobra CLI +func (g *EnvCodegen) GenerateCLI() (string, error) { + p := CLIParams{ + PackageName: g.cfg.packageName, + CLIName: g.cfg.cliName, + DevEnvPkgImport: g.cfg.moduleName, + } + return render(CLITmpl, p) +} + +// GenerateSingleNetworkProductConfiguration generate a single-network product configuration +func (g *EnvCodegen) GenerateSingleNetworkProductConfiguration() (string, error) { + p := ProductConfigurationSimple{ + PackageName: g.cfg.packageName, + } + return render(SingleNetworkProductConfigurationTmpl, p) +} + +// GenerateEnvironment generate environment.go, our environment composition function +func (g *EnvCodegen) GenerateEnvironment() (string, error) { + p := EnvParams{ + PackageName: g.cfg.packageName, + } + return render(EnvironmentTmpl, p) +} + +// GenerateCLDF generate CLDF helpers +func (g *EnvCodegen) GenerateCLDF() (string, error) { + p := CLDFParams{ + PackageName: g.cfg.packageName, + } + return render(CLDFTmpl, p) +} + +// GenerateDebugTools generate debug tools (tracing) +func (g *EnvCodegen) GenerateDebugTools() (string, error) { + p := ToolsParams{ + PackageName: g.cfg.packageName, + } + return render(DebugToolsTmpl, p) +} + +// GenerateConfig generate read/write utilities for TOML configs +func (g *EnvCodegen) GenerateConfig() (string, error) { + p := ConfigParams{ + PackageName: g.cfg.packageName, + } + return render(ConfigTmpl, p) +} + +// GenerateTableTest generates all possible experiments for a namespace +// first generate all small pieces then insert into a table test template +func (g *EnvCodegen) GenerateTableTest() (string, error) { + // TODO: generate a table test when we'll have chain-specific interface solidified + return "", nil +} + +// GenerateTestCases generates table test cases +func (g *EnvCodegen) GenerateTestCases() ([]TestCaseParams, error) { + // TODO: generate test cases when we'll have chain-specific interface solidified + return []TestCaseParams{}, nil +} + +// render is just an internal function to parse and render template +func render(tmpl string, data any) (string, error) { + parsed, err := template.New("").Parse(tmpl) + if err != nil { + return "", fmt.Errorf("failed to parse table test template: %w", err) + } + var buf bytes.Buffer + if err := parsed.Execute(&buf, data); err != nil { + return "", fmt.Errorf("failed to generate table test: %w", err) + } + return buf.String(), err +} diff --git a/framework/tmpl_gen_test.go b/framework/tmpl_gen_test.go new file mode 100644 index 000000000..44ee3e69d --- /dev/null +++ b/framework/tmpl_gen_test.go @@ -0,0 +1,119 @@ +package framework_test + +import ( + "os" + "os/exec" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/pelletier/go-toml/v2" + "github.com/smartcontractkit/chainlink-testing-framework/framework" + "github.com/smartcontractkit/chainlink-testing-framework/framework/clclient" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink-testing-framework/framework/components/blockchain" + ns "github.com/smartcontractkit/chainlink-testing-framework/framework/components/simple_node_set" +) + +func runCmd(t *testing.T, dir string, name string, args ...string) string { + t.Helper() + cmd := exec.Command(name, args...) + cmd.Env = os.Environ() + cmd.Dir = dir + t.Logf("Executing: %s %v in %s", name, args, dir) + output, err := cmd.CombinedOutput() + if err != nil { + t.Logf("Command output:\n%s", string(output)) + t.Fatalf("Command failed: %v", err) + } + t.Logf("Command output:\n%s", string(output)) + return string(output) +} + +type TestCfg struct { + Blockchains []*blockchain.Input `toml:"blockchains" validate:"required"` + NodeSets []*ns.Input `toml:"nodesets" validate:"required"` +} + +// TestSmokeGenerateEnvPositive top-down approach tests until all the environment variations aren't stable +func TestSmokeGenerateEnvPositive(t *testing.T) { + tests := []struct { + name string + goModName string + cliName string + outputDir string + nodes int + }{ + // all variations of env, test they can be generated and create valid services + // that can pass healthchecks + { + name: "basic 2 nodes env", + goModName: "devenv", + cliName: "tcli", + outputDir: "test-env", + nodes: 2, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + defer os.RemoveAll(tt.outputDir) + defer runCmd(t, tt.outputDir, tt.cliName, `down`) + cg, err := framework.NewEnvBuilder(tt.cliName, tt.nodes). + GoModName(tt.goModName). + OutputDir(tt.outputDir). + Build() + require.NoError(t, err) + err = cg.Write() + require.NoError(t, err) + runCmd(t, filepath.Join(tt.outputDir, "cmd", tt.cliName), `go`, `install`, `.`) + runCmd(t, tt.outputDir, tt.cliName, `up`) + + data, err := os.ReadFile(filepath.Join(tt.outputDir, "env-out.toml")) + require.NoError(t, err) + + decoder := toml.NewDecoder(strings.NewReader(string(data))) + + var cfg TestCfg + err = decoder.Decode(&cfg) + require.NoError(t, err) + + clClients, err := clclient.New(cfg.NodeSets[0].Out.CLNodes) + require.NoError(t, err) + + // verify that all the services are healthy + require.Eventually(t, func() bool { + failedServices := false + for _, c := range clClients { + r, _, err := c.Health() + require.NoError(t, err) + t.Log(r) + for _, d := range r.Data { + if d.Attributes.Status != "passing" { + t.Logf("CL service %s is not healthy: %s", d.Attributes.Name, d.Attributes.Output) + failedServices = true + } + } + } + return !failedServices + }, 2*time.Minute, 2*time.Second) + }) + } +} + +func TestSmokeGenerateDataNegative(t *testing.T) { + tests := []struct { + name string + }{ + { + name: "single pod but incorrect instance annotation", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + }) + } +} From 0269f544d83ff12ade8d148cb6654226a9790aa9 Mon Sep 17 00:00:00 2001 From: skudasov Date: Mon, 17 Nov 2025 11:12:01 +0100 Subject: [PATCH 03/16] fix gosum --- framework/components/dockercompose/go.sum | 44 ++++++++++++++++++++ framework/components/fake/go.sum | 50 +++++++++++++++++++++++ 2 files changed, 94 insertions(+) diff --git a/framework/components/dockercompose/go.sum b/framework/components/dockercompose/go.sum index 648c42395..850415da8 100644 --- a/framework/components/dockercompose/go.sum +++ b/framework/components/dockercompose/go.sum @@ -26,6 +26,8 @@ github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDe github.com/Shopify/logrus-bugsnag v0.0.0-20170309145241-6dbc35f2c30d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ= github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d h1:UrqY+r/OJnIp5u0s1SbQ8dVfLCZJsnvazdBP5hS4iRs= github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ= +github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDOSA= +github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8= github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8= github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo= github.com/actgardner/gogen-avro/v10 v10.1.0/go.mod h1:o+ybmVjEa27AAr35FRqU98DJu1fXES56uXniYFv4yDA= @@ -75,6 +77,10 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bitly/go-hostpool v0.1.0/go.mod h1:4gOCgp6+NZnVqlKyZ/iBZFTAJKembaVENUpMkpg42fw= github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA= +github.com/bits-and-blooms/bitset v1.17.0 h1:1X2TS7aHz1ELcC0yU1y2stUs/0ig5oMU6STFZGrhvHI= +github.com/bits-and-blooms/bitset v1.17.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= +github.com/block-vision/sui-go-sdk v1.0.6 h1:FysCc4TJC8v4BEBbCjJPDR4iR5eKqJT1dxGwsT67etg= +github.com/block-vision/sui-go-sdk v1.0.6/go.mod h1:FyK1vGE8lWm9QA1fdQpf1agfXQSMbPT8AV1BICgx6d8= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= github.com/buger/goterm v1.0.4 h1:Z9YvGmOih81P0FbVtEYTFF6YsSgxSUKEhf/f9bTMXbY= github.com/buger/goterm v1.0.4/go.mod h1:HiFWV3xnkolgrBV3mY8m0X0Pumt4zg4QhbdOzQtB8tE= @@ -108,6 +114,10 @@ github.com/compose-spec/compose-go/v2 v2.6.0 h1:/+oBD2ixSENOeN/TlJqWZmUak0xM8A7J github.com/compose-spec/compose-go/v2 v2.6.0/go.mod h1:vPlkN0i+0LjLf9rv52lodNMUTJF5YHVfHVGLLIP67NA= github.com/confluentinc/confluent-kafka-go v1.9.2 h1:gV/GxhMBUb03tFWkN+7kdhg+zf+QUM+wVkI9zwh770Q= github.com/confluentinc/confluent-kafka-go v1.9.2/go.mod h1:ptXNqsuDfYbAE/LBW6pnwWZElUoWxHoV8E43DCrliyo= +github.com/consensys/bavard v0.1.22 h1:Uw2CGvbXSZWhqK59X0VG/zOjpTFuOMcPLStrp1ihI0A= +github.com/consensys/bavard v0.1.22/go.mod h1:k/zVjHHC4B+PQy1Pg7fgvG3ALicQw540Crag8qx+dZs= +github.com/consensys/gnark-crypto v0.14.0 h1:DDBdl4HaBtdQsq/wfMwJvZNE80sHidrK3Nfrefatm0E= +github.com/consensys/gnark-crypto v0.14.0/go.mod h1:CU4UijNPsHawiVGNxe9co07FkzCeWHHrb1li/n1XoU0= github.com/containerd/cgroups/v3 v3.0.5 h1:44na7Ud+VwyE7LIoJ8JTNQOa549a8543BmzaJHo6Bzo= github.com/containerd/cgroups/v3 v3.0.5/go.mod h1:SA5DLYnXO8pTGYiAHXz94qvLQTKfVM5GEVisn4jpins= github.com/containerd/console v1.0.4 h1:F2g4+oChYvBTsASRTz8NP6iIAi97J3TtSAsLbIFn4ro= @@ -143,6 +153,10 @@ github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSV github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a h1:W8mUrRp6NOVl3J+MYp5kPMoUZPp7aOYHtaua31lwRHg= +github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a/go.mod h1:sTwzHBvIzm2RfVCGNEBZgRyjwK40bVoun3ZnGOCafNM= +github.com/crate-crypto/go-kzg-4844 v1.1.0 h1:EN/u9k2TF6OWSHrCCDBBU6GLNMq88OspHHlMnHfoyU4= +github.com/crate-crypto/go-kzg-4844 v1.1.0/go.mod h1:JolLjpSff1tCCJKaJx4psrlEdlXuJEC996PL3tTAFks= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= @@ -151,6 +165,10 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/deckarep/golang-set/v2 v2.6.0 h1:XfcQbWM1LlMB8BsJ8N9vW5ehnnPVIw0je80NsVHagjM= +github.com/deckarep/golang-set/v2 v2.6.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= github.com/denisenkom/go-mssqldb v0.0.0-20191128021309-1d7a30a10f73/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= @@ -196,6 +214,12 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.m github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0= +github.com/ethereum/c-kzg-4844 v1.0.0 h1:0X1LBXxaEtYD9xsyj9B9ctQEZIpnvVDeoBx8aHEwTNA= +github.com/ethereum/c-kzg-4844 v1.0.0/go.mod h1:VewdlzQmpT5QSrVhbBuGoCdFJkpaJlO1aQputP83wc0= +github.com/ethereum/go-ethereum v1.15.0 h1:LLb2jCPsbJZcB4INw+E/MgzUX5wlR6SdwXcv09/1ME4= +github.com/ethereum/go-ethereum v1.15.0/go.mod h1:4q+4t48P2C03sjqGvTXix5lEOplf5dz4CTosbjt5tGs= +github.com/ethereum/go-verkle v0.2.2 h1:I2W0WjnrFUIzzVPwm8ykY+7pL2d4VhlsePn4j7cnFk8= +github.com/ethereum/go-verkle v0.2.2/go.mod h1:M3b90YRnzqKyyzBEWJGqj8Qff4IDeXnzFw0P9bFw3uk= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/frankban/quicktest v1.2.2/go.mod h1:Qh/WofXFeiAFII1aEBu529AtJo6Zg2VHscnEsbBnJ20= @@ -205,6 +229,8 @@ github.com/frankban/quicktest v1.14.0/go.mod h1:NeW+ay9A/U67EYXNFA1nPE8e/tnQv/09 github.com/fsnotify/fsevents v0.2.0 h1:BRlvlqjvNTfogHfeBOFvSC9N0Ddy+wzQCQukyoD7o/c= github.com/fsnotify/fsevents v0.2.0/go.mod h1:B3eEk39i4hz8y1zaWS/wPrAP4O6wkIl7HQwKBr1qH/w= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= +github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/fvbommel/sortorder v1.1.0 h1:fUmoe+HLsBTctBDoaBwpQo5N+nrCp8g/BjKb/6ZQmYw= github.com/fvbommel/sortorder v1.1.0/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0= github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= @@ -333,6 +359,8 @@ github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09 github.com/heetch/avro v0.3.1/go.mod h1:4xn38Oz/+hiEUTpbVfGVLfvOg0yKLlRP7Q9+gJJILgA= github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog= github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68= +github.com/holiman/uint256 v1.3.2 h1:a9EgMPSC1AAaj1SZL5zIQD3WbwTuHrMGOerLjGmM/TA= +github.com/holiman/uint256 v1.3.2/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/iancoleman/orderedmap v0.0.0-20190318233801-ac98e3ecb4b0/go.mod h1:N0Wam8K1arqPXNWjMo21EXnBPOPp36vB07FNRdD2geA= github.com/ianlancetaylor/demangle v0.0.0-20210905161508-09a460cdf81d/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w= @@ -425,6 +453,8 @@ github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/z github.com/mitchellh/mapstructure v0.0.0-20150613213606-2caf8efc9366/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.5.1-0.20220423185008-bf980b35cac4 h1:BpfhmLKZf+SjVanKKhCgf3bg+511DmU9eDQTen7LLbY= github.com/mitchellh/mapstructure v1.5.1-0.20220423185008-bf980b35cac4/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mmcloughlin/addchain v0.4.0 h1:SobOdjm2xLj1KkXN5/n0xTIWyZA2+s99UCY1iPfkHRY= +github.com/mmcloughlin/addchain v0.4.0/go.mod h1:A86O+tHqZLMNO4w6ZZ4FlVQEadcoqkyU72HC5wJ4RlU= github.com/moby/buildkit v0.20.1 h1:sT0ZXhhNo5rVbMcYfgttma3TdUHfO5JjFA0UAL8p9fY= github.com/moby/buildkit v0.20.1/go.mod h1:Rq9nB/fJImdk6QeM0niKtOHJqwKeYMrK847hTTDVuA4= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= @@ -556,6 +586,8 @@ github.com/serialx/hashring v0.0.0-20200727003509-22c0c7ab6b1b h1:h+3JX2VoWTFuyQ github.com/serialx/hashring v0.0.0-20200727003509-22c0c7ab6b1b/go.mod h1:/yeG0My1xr/u+HZrFQ1tOQQQQrOawfyMUH13ai5brBc= github.com/shibumi/go-pathspec v1.3.0 h1:QUyMZhFo0Md5B8zV8x2tesohbb5kfbpTi9rBnKh5dkI= github.com/shibumi/go-pathspec v1.3.0/go.mod h1:Xutfslp817l2I1cZvgcfeMQJG5QnU2lh5tVaaMCl3jE= +github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible h1:Bn1aCHHRnjv4Bl16T8rcaFjYSrGrIZvpiGO6P3Q4GpU= +github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= github.com/shirou/gopsutil/v4 v4.25.1 h1:QSWkTc+fu9LTAWfkZwZ6j8MSUk4A2LV7rbH0ZqmLjXs= github.com/shirou/gopsutil/v4 v4.25.1/go.mod h1:RoUCUpndaJFtT+2zsZzzmhvbfGoDCJ7nFXKJf8GqJbI= github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= @@ -597,12 +629,20 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/supranational/blst v0.3.13 h1:AYeSxdOMacwu7FBmpfloBz5pbFXDmJL33RuwnKtmTjk= +github.com/supranational/blst v0.3.13/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= github.com/testcontainers/testcontainers-go v0.37.0 h1:L2Qc0vkTw2EHWQ08djon0D2uw7Z/PtHS/QzZZ5Ra/hg= github.com/testcontainers/testcontainers-go v0.37.0/go.mod h1:QPzbxZhQ6Bclip9igjLFj6z0hs01bU8lrl2dHQmgFGM= github.com/testcontainers/testcontainers-go/modules/compose v0.37.0 h1:AE6XYnyUMkiyuo8GZ3B36d0i4L/HMSjaQ6QtAffkD4k= github.com/testcontainers/testcontainers-go/modules/compose v0.37.0/go.mod h1:fgzGeGw5iVyzS6qWOAYDbvv3iWp/wCtqWNSH4Aev8hs= github.com/theupdateframework/notary v0.7.0 h1:QyagRZ7wlSpjT5N2qQAh/pN+DVqgekv4DzbAiAiEL3c= github.com/theupdateframework/notary v0.7.0/go.mod h1:c9DRxcmhHmVLDay4/2fUYdISnHqbFDGRSlXPO0AhYWw= +github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM= +github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tilt-dev/fsnotify v1.4.8-0.20220602155310-fff9c274a375 h1:QB54BJwA6x8QU9nHY3xJSZR2kX9bgpZekRKGkLTmEXA= github.com/tilt-dev/fsnotify v1.4.8-0.20220602155310-fff9c274a375/go.mod h1:xRroudyp5iVtxKqZCrA6n2TLFRBf8bmnjr1UD4x+z7g= github.com/tklauser/go-sysconf v0.3.13 h1:GBUpcahXSpR2xN01jhkNAbTLRk2Yzgggk8IM08lq3r4= @@ -856,6 +896,8 @@ gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSP gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= +gopkg.in/guregu/null.v4 v4.0.0 h1:1Wm3S1WEA2I26Kq+6vcW+w0gcDo44YKYD7YIEJNHDjg= +gopkg.in/guregu/null.v4 v4.0.0/go.mod h1:YoQhUrADuG3i9WqesrCmpNRwm1ypAgSHYqoOcTu/JrI= gopkg.in/httprequest.v1 v1.2.1/go.mod h1:x2Otw96yda5+8+6ZeWwHIJTFkEHWP/qP8pJOzqEtWPM= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= @@ -894,6 +936,8 @@ k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJ k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f/go.mod h1:R/HEjbvWI0qdfb8viZUeVZm0X6IZnxAydC7YU42CMw4= k8s.io/utils v0.0.0-20241104163129-6fe5fd82f078 h1:jGnCPejIetjiy2gqaJ5V0NLwTpF4wbQ6cZIItJCSHno= k8s.io/utils v0.0.0-20241104163129-6fe5fd82f078/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +rsc.io/tmplfunc v0.0.3 h1:53XFQh69AfOa8Tw0Jm7t+GV7KZhOi6jzsCzTtKbMvzU= +rsc.io/tmplfunc v0.0.3/go.mod h1:AG3sTPzElb1Io3Yg4voV9AGZJuleGAwaVRxL9M49PhA= sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= sigs.k8s.io/structured-merge-diff/v4 v4.4.2 h1:MdmvkGuXi/8io6ixD5wud3vOLwc1rj0aNqRlpuvjmwA= diff --git a/framework/components/fake/go.sum b/framework/components/fake/go.sum index 1d2adffeb..76ec655ce 100644 --- a/framework/components/fake/go.sum +++ b/framework/components/fake/go.sum @@ -6,6 +6,14 @@ github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEK github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDOSA= +github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8= +github.com/avast/retry-go/v4 v4.6.1 h1:VkOLRubHdisGrHnTu89g08aQEWEgRU7LVEop3GbIcMk= +github.com/avast/retry-go/v4 v4.6.1/go.mod h1:V6oF8njAwxJ5gRo1Q7Cxab24xs5NCWZBeaHHBklR8mA= +github.com/bits-and-blooms/bitset v1.17.0 h1:1X2TS7aHz1ELcC0yU1y2stUs/0ig5oMU6STFZGrhvHI= +github.com/bits-and-blooms/bitset v1.17.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= +github.com/block-vision/sui-go-sdk v1.0.6 h1:FysCc4TJC8v4BEBbCjJPDR4iR5eKqJT1dxGwsT67etg= +github.com/block-vision/sui-go-sdk v1.0.6/go.mod h1:FyK1vGE8lWm9QA1fdQpf1agfXQSMbPT8AV1BICgx6d8= github.com/bytedance/sonic v1.12.3 h1:W2MGa7RCU1QTeYRTPE3+88mVC0yXmsRQRChiyVocVjU= github.com/bytedance/sonic v1.12.3/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk= github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= @@ -17,6 +25,10 @@ github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/ github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= +github.com/consensys/bavard v0.1.22 h1:Uw2CGvbXSZWhqK59X0VG/zOjpTFuOMcPLStrp1ihI0A= +github.com/consensys/bavard v0.1.22/go.mod h1:k/zVjHHC4B+PQy1Pg7fgvG3ALicQw540Crag8qx+dZs= +github.com/consensys/gnark-crypto v0.14.0 h1:DDBdl4HaBtdQsq/wfMwJvZNE80sHidrK3Nfrefatm0E= +github.com/consensys/gnark-crypto v0.14.0/go.mod h1:CU4UijNPsHawiVGNxe9co07FkzCeWHHrb1li/n1XoU0= github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= @@ -28,12 +40,20 @@ github.com/containerd/platforms v1.0.0-rc.1/go.mod h1:J71L7B+aiM5SdIEqmd9wp6THLV github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a h1:W8mUrRp6NOVl3J+MYp5kPMoUZPp7aOYHtaua31lwRHg= +github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a/go.mod h1:sTwzHBvIzm2RfVCGNEBZgRyjwK40bVoun3ZnGOCafNM= +github.com/crate-crypto/go-kzg-4844 v1.1.0 h1:EN/u9k2TF6OWSHrCCDBBU6GLNMq88OspHHlMnHfoyU4= +github.com/crate-crypto/go-kzg-4844 v1.1.0/go.mod h1:JolLjpSff1tCCJKaJx4psrlEdlXuJEC996PL3tTAFks= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/deckarep/golang-set/v2 v2.6.0 h1:XfcQbWM1LlMB8BsJ8N9vW5ehnnPVIw0je80NsVHagjM= +github.com/deckarep/golang-set/v2 v2.6.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/docker/docker v28.3.3+incompatible h1:Dypm25kh4rmk49v1eiVbsAtpAsYURjYkaKubwuBdxEI= @@ -44,8 +64,16 @@ github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4 github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I= github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/ethereum/c-kzg-4844 v1.0.0 h1:0X1LBXxaEtYD9xsyj9B9ctQEZIpnvVDeoBx8aHEwTNA= +github.com/ethereum/c-kzg-4844 v1.0.0/go.mod h1:VewdlzQmpT5QSrVhbBuGoCdFJkpaJlO1aQputP83wc0= +github.com/ethereum/go-ethereum v1.15.0 h1:LLb2jCPsbJZcB4INw+E/MgzUX5wlR6SdwXcv09/1ME4= +github.com/ethereum/go-ethereum v1.15.0/go.mod h1:4q+4t48P2C03sjqGvTXix5lEOplf5dz4CTosbjt5tGs= +github.com/ethereum/go-verkle v0.2.2 h1:I2W0WjnrFUIzzVPwm8ykY+7pL2d4VhlsePn4j7cnFk8= +github.com/ethereum/go-verkle v0.2.2/go.mod h1:M3b90YRnzqKyyzBEWJGqj8Qff4IDeXnzFw0P9bFw3uk= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= +github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/gabriel-vasile/mimetype v1.4.6 h1:3+PzJTKLkvgjeTbts6msPJt4DixhT4YtFNf1gtGe3zc= github.com/gabriel-vasile/mimetype v1.4.6/go.mod h1:JX1qVKqZd40hUPpAfiNTe0Sne7hdfKSbOqqmkq8GCXc= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= @@ -81,8 +109,12 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo= github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI= +github.com/holiman/uint256 v1.3.2 h1:a9EgMPSC1AAaj1SZL5zIQD3WbwTuHrMGOerLjGmM/TA= +github.com/holiman/uint256 v1.3.2/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= @@ -109,6 +141,8 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/ github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mmcloughlin/addchain v0.4.0 h1:SobOdjm2xLj1KkXN5/n0xTIWyZA2+s99UCY1iPfkHRY= +github.com/mmcloughlin/addchain v0.4.0/go.mod h1:A86O+tHqZLMNO4w6ZZ4FlVQEadcoqkyU72HC5wJ4RlU= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ= @@ -150,6 +184,8 @@ github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWN github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8= github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= +github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible h1:Bn1aCHHRnjv4Bl16T8rcaFjYSrGrIZvpiGO6P3Q4GpU= +github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= github.com/shirou/gopsutil/v4 v4.25.1 h1:QSWkTc+fu9LTAWfkZwZ6j8MSUk4A2LV7rbH0ZqmLjXs= github.com/shirou/gopsutil/v4 v4.25.1/go.mod h1:RoUCUpndaJFtT+2zsZzzmhvbfGoDCJ7nFXKJf8GqJbI= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= @@ -166,8 +202,16 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/supranational/blst v0.3.13 h1:AYeSxdOMacwu7FBmpfloBz5pbFXDmJL33RuwnKtmTjk= +github.com/supranational/blst v0.3.13/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= github.com/testcontainers/testcontainers-go v0.37.0 h1:L2Qc0vkTw2EHWQ08djon0D2uw7Z/PtHS/QzZZ5Ra/hg= github.com/testcontainers/testcontainers-go v0.37.0/go.mod h1:QPzbxZhQ6Bclip9igjLFj6z0hs01bU8lrl2dHQmgFGM= +github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM= +github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tklauser/go-sysconf v0.3.13 h1:GBUpcahXSpR2xN01jhkNAbTLRk2Yzgggk8IM08lq3r4= github.com/tklauser/go-sysconf v0.3.13/go.mod h1:zwleP4Q4OehZHGn4CYZDipCgg9usW5IJePewFCGVEa0= github.com/tklauser/numcpus v0.7.0 h1:yjuerZP127QG9m5Zh/mSO4wqurYil27tHrqwRoRjpr4= @@ -207,6 +251,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= +golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0kKxwaAIqN/il7x4voA= +golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -260,9 +306,13 @@ google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/guregu/null.v4 v4.0.0 h1:1Wm3S1WEA2I26Kq+6vcW+w0gcDo44YKYD7YIEJNHDjg= +gopkg.in/guregu/null.v4 v4.0.0/go.mod h1:YoQhUrADuG3i9WqesrCmpNRwm1ypAgSHYqoOcTu/JrI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= +rsc.io/tmplfunc v0.0.3 h1:53XFQh69AfOa8Tw0Jm7t+GV7KZhOi6jzsCzTtKbMvzU= +rsc.io/tmplfunc v0.0.3/go.mod h1:AG3sTPzElb1Io3Yg4voV9AGZJuleGAwaVRxL9M49PhA= From c30216433fdaf3d9a64547af7c659a9d288f16b0 Mon Sep 17 00:00:00 2001 From: skudasov Date: Mon, 17 Nov 2025 11:44:47 +0100 Subject: [PATCH 04/16] fix lint --- .github/workflows/framework-codegen.yml | 63 +++++++++++++++++++++++++ framework/config.go | 10 ++-- framework/tmpl_gen.go | 36 +++++++------- framework/tmpl_gen_test.go | 3 +- 4 files changed, 90 insertions(+), 22 deletions(-) create mode 100644 .github/workflows/framework-codegen.yml diff --git a/.github/workflows/framework-codegen.yml b/.github/workflows/framework-codegen.yml new file mode 100644 index 000000000..cf728020f --- /dev/null +++ b/.github/workflows/framework-codegen.yml @@ -0,0 +1,63 @@ +name: Framework Code Generation +on: + push: + +jobs: + test: + defaults: + run: + working-directory: framework/ + env: + LOKI_TENANT_ID: promtail + LOKI_URL: http://localhost:3030/loki/api/v1/push + runs-on: ubuntu-latest + permissions: + id-token: write + contents: read + strategy: + fail-fast: false + matrix: + test: + - name: TestSmokeGenerate + count: 1 + timeout: 10m + name: ${{ matrix.test.name }} + steps: + - name: Checkout repo + uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 + - name: Configure AWS credentials using OIDC + uses: aws-actions/configure-aws-credentials@e3dd6a429d7300a6a4c196c26e071d42e0343502 # v4.0.2 + with: + role-to-assume: ${{ secrets.PUBLIC_AWS_ECR_ROLE }} + aws-region: us-east-1 + - name: Authenticate to ECR Public + id: login-ecr-public + uses: aws-actions/amazon-ecr-login@062b18b96a7aff071d4dc91bc00c4c1a7945b076 # v2.0.1 + with: + registry-type: public + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + - name: Cache Go modules + uses: actions/cache@v4 + with: + path: | + ~/.cache/go-build + ~/go/pkg/mod + key: go-modules-${{ hashFiles('framework/go.sum') }}-${{ runner.os }}-framework-codegen + restore-keys: | + go-modules-${{ runner.os }}-framework-codegen + go-modules-${{ runner.os }} + - name: Install dependencies + run: go mod download + - name: Run Codegen Tests + run: | + go test -timeout ${{ matrix.test.timeout }} -v -count ${{ matrix.test.count }} -run ${{ matrix.test.name }} + - name: Upload Logs + if: always() + uses: actions/upload-artifact@v4 + with: + name: container-logs-${{ matrix.test.name }} + path: framework/logs + retention-days: 1 diff --git a/framework/config.go b/framework/config.go index f8d6efbce..938b1e7a6 100644 --- a/framework/config.go +++ b/framework/config.go @@ -237,7 +237,7 @@ func BaseConfigName() (string, error) { if err != nil { return "", err } - return strings.Replace(cp, ".toml", "", -1), nil + return strings.ReplaceAll(cp, ".toml", ""), nil } // BaseCacheName returns base cache file name, ex.: env.toml -> env-cache.toml @@ -246,7 +246,7 @@ func BaseCacheName() (string, error) { if err != nil { return "", err } - name := strings.Replace(cp, ".toml", "", -1) + name := strings.ReplaceAll(cp, ".toml", "") return fmt.Sprintf("%s-cache.toml", name), nil } @@ -282,12 +282,12 @@ func Store[T any](cfg *T) error { if err != nil { return err } - newCacheName := strings.Replace(baseConfigPath, ".toml", "", -1) + newCacheName := strings.ReplaceAll(baseConfigPath, ".toml", "") if strings.Contains(newCacheName, "cache") { L.Info().Str("Cache", baseConfigPath).Msg("Cache file already exists, skipping") return nil } - cachedOutName := fmt.Sprintf("%s-cache.toml", strings.Replace(baseConfigPath, ".toml", "", -1)) + cachedOutName := fmt.Sprintf("%s-cache.toml", strings.ReplaceAll(baseConfigPath, ".toml", "")) L.Info().Str("OutputFile", cachedOutName).Msg("Storing configuration output") d, err := toml.Marshal(cfg) if err != nil { @@ -303,7 +303,7 @@ func Store[T any](cfg *T) error { } } - return os.WriteFile(writePath, d, 0600) + return os.WriteFile(writePath, d, 0o600) } // JSONStrDuration is JSON friendly duration that can be parsed from "1h2m0s" Go format diff --git a/framework/tmpl_gen.go b/framework/tmpl_gen.go index 2a0c74363..2ce02596d 100644 --- a/framework/tmpl_gen.go +++ b/framework/tmpl_gen.go @@ -1472,7 +1472,6 @@ type ProductConfigurationSimple struct { // TableTestParams params for generating a table test type TableTestParams struct { Package string - Namespace string TableTestName string TestCases []TestCaseParams WorkloadCode string @@ -1490,7 +1489,6 @@ type TestCaseParams struct { // EnvBuilder builder for load test codegen type EnvBuilder struct { nodes int - namespace string outputDir string packageName string cliName string @@ -1550,7 +1548,10 @@ func (g *EnvCodegen) Read() error { // Write generates a complete boilerplate, can be multiple files func (g *EnvCodegen) Write() error { // Create output directory - if err := os.MkdirAll(g.cfg.outputDir, os.ModePerm); err != nil { + if err := os.MkdirAll( //nolint:gosec + g.cfg.outputDir, + os.ModePerm, + ); err != nil { return fmt.Errorf("failed to create output directory: %w", err) } @@ -1559,7 +1560,7 @@ func (g *EnvCodegen) Write() error { if err != nil { return err } - if err := os.WriteFile( + if err := os.WriteFile( //nolint:gosec filepath.Join(g.cfg.outputDir, "go.mod"), []byte(goModContent), os.ModePerm, @@ -1572,7 +1573,7 @@ func (g *EnvCodegen) Write() error { if err != nil { return err } - if err := os.WriteFile( + if err := os.WriteFile( //nolint:gosec filepath.Join(g.cfg.outputDir, "Justfile"), []byte(justContents), os.ModePerm, @@ -1585,7 +1586,7 @@ func (g *EnvCodegen) Write() error { if err != nil { return err } - if err := os.WriteFile( + if err := os.WriteFile( //nolint:gosec filepath.Join(g.cfg.outputDir, "env.toml"), []byte(tomlContents), os.ModePerm, @@ -1594,7 +1595,10 @@ func (g *EnvCodegen) Write() error { } cliDir := filepath.Join(g.cfg.outputDir, "cmd", g.cfg.cliName) - if err := os.MkdirAll(cliDir, os.ModePerm); err != nil { + if err := os.MkdirAll( //nolint:gosec + cliDir, + os.ModePerm, + ); err != nil { return fmt.Errorf("failed to create CLI directory: %w", err) } @@ -1603,7 +1607,7 @@ func (g *EnvCodegen) Write() error { if err != nil { return err } - if err := os.WriteFile( + if err := os.WriteFile( //nolint:gosec filepath.Join(cliDir, fmt.Sprintf("%s.go", g.cfg.cliName)), []byte(cliContents), os.ModePerm, @@ -1616,7 +1620,7 @@ func (g *EnvCodegen) Write() error { if err != nil { return err } - if err := os.WriteFile( + if err := os.WriteFile( //nolint:gosec filepath.Join(cliDir, "completion.go"), []byte(completionContents), os.ModePerm, @@ -1629,7 +1633,7 @@ func (g *EnvCodegen) Write() error { if err != nil { return err } - if err := os.WriteFile( + if err := os.WriteFile( //nolint:gosec filepath.Join(g.cfg.outputDir, "tools.go"), []byte(toolsContents), os.ModePerm, @@ -1642,7 +1646,7 @@ func (g *EnvCodegen) Write() error { if err != nil { return err } - if err := os.WriteFile( + if err := os.WriteFile( //nolint:gosec filepath.Join(g.cfg.outputDir, "config.go"), []byte(configFileContents), os.ModePerm, @@ -1655,7 +1659,7 @@ func (g *EnvCodegen) Write() error { if err != nil { return err } - if err := os.WriteFile( + if err := os.WriteFile( //nolint:gosec filepath.Join(g.cfg.outputDir, "cldf.go"), []byte(cldfContents), os.ModePerm, @@ -1668,7 +1672,7 @@ func (g *EnvCodegen) Write() error { if err != nil { return err } - if err := os.WriteFile( + if err := os.WriteFile( //nolint:gosec filepath.Join(g.cfg.outputDir, "environment.go"), []byte(envFileContents), os.ModePerm, @@ -1681,12 +1685,12 @@ func (g *EnvCodegen) Write() error { if err != nil { return err } - if err := os.WriteFile( + if err := os.WriteFile( //nolint:gosec filepath.Join(g.cfg.outputDir, "product_configuration.go"), []byte(prodConfigFileContents), os.ModePerm, ); err != nil { - return fmt.Errorf("failed to write product configration file: %w", err) + return fmt.Errorf("failed to write product configuration file: %w", err) } // tidy and finalize @@ -1716,7 +1720,7 @@ func (g *EnvCodegen) Write() error { func (g *EnvCodegen) GenerateGoMod() (string, error) { data := GoModParams{ ModuleName: g.cfg.moduleName, - RuntimeVersion: strings.Replace(runtime.Version(), "go", "", -1), + RuntimeVersion: strings.ReplaceAll(runtime.Version(), "go", ""), } return render(GoModTemplate, data) } diff --git a/framework/tmpl_gen_test.go b/framework/tmpl_gen_test.go index 44ee3e69d..3fe6ddc0a 100644 --- a/framework/tmpl_gen_test.go +++ b/framework/tmpl_gen_test.go @@ -9,9 +9,10 @@ import ( "time" "github.com/pelletier/go-toml/v2" + "github.com/stretchr/testify/require" + "github.com/smartcontractkit/chainlink-testing-framework/framework" "github.com/smartcontractkit/chainlink-testing-framework/framework/clclient" - "github.com/stretchr/testify/require" "github.com/smartcontractkit/chainlink-testing-framework/framework/components/blockchain" ns "github.com/smartcontractkit/chainlink-testing-framework/framework/components/simple_node_set" From a0fcd747cef7d05588304931206cb44c2fcaf7c8 Mon Sep 17 00:00:00 2001 From: skudasov Date: Mon, 17 Nov 2025 11:49:16 +0100 Subject: [PATCH 05/16] fix CI --- .github/workflows/framework-codegen.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/framework-codegen.yml b/.github/workflows/framework-codegen.yml index cf728020f..c7d771eb3 100644 --- a/.github/workflows/framework-codegen.yml +++ b/.github/workflows/framework-codegen.yml @@ -38,7 +38,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v5 with: - go-version-file: go.mod + go-version-file: framework/go.mod - name: Cache Go modules uses: actions/cache@v4 with: From 16e8b3902f6d4caff14f340aadf7114e21d18378 Mon Sep 17 00:00:00 2001 From: skudasov Date: Mon, 17 Nov 2025 11:57:14 +0100 Subject: [PATCH 06/16] runner --- .github/workflows/framework-codegen.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/framework-codegen.yml b/.github/workflows/framework-codegen.yml index c7d771eb3..23c64bb60 100644 --- a/.github/workflows/framework-codegen.yml +++ b/.github/workflows/framework-codegen.yml @@ -10,7 +10,7 @@ jobs: env: LOKI_TENANT_ID: promtail LOKI_URL: http://localhost:3030/loki/api/v1/push - runs-on: ubuntu-latest + runs-on: ubuntu24.04-16cores-64GB permissions: id-token: write contents: read From 341de2bd9843d8beb2a88df8ce1b50a58339eb79 Mon Sep 17 00:00:00 2001 From: skudasov Date: Mon, 17 Nov 2025 15:25:07 +0100 Subject: [PATCH 07/16] slower runner for now --- .github/workflows/framework-codegen.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/framework-codegen.yml b/.github/workflows/framework-codegen.yml index 23c64bb60..c7d771eb3 100644 --- a/.github/workflows/framework-codegen.yml +++ b/.github/workflows/framework-codegen.yml @@ -10,7 +10,7 @@ jobs: env: LOKI_TENANT_ID: promtail LOKI_URL: http://localhost:3030/loki/api/v1/push - runs-on: ubuntu24.04-16cores-64GB + runs-on: ubuntu-latest permissions: id-token: write contents: read From 46f6bcc5bc90d66910bad870d18ea620d0e2c4a8 Mon Sep 17 00:00:00 2001 From: skudasov Date: Tue, 18 Nov 2025 18:07:34 +0100 Subject: [PATCH 08/16] extend --- framework/cmd/main.go | 46 +++- framework/tmpl_gen.go | 533 ++++++++++++++++++++++++++++++++++--- framework/tmpl_gen_test.go | 37 ++- 3 files changed, 553 insertions(+), 63 deletions(-) diff --git a/framework/cmd/main.go b/framework/cmd/main.go index aed3423ec..8be9242f7 100644 --- a/framework/cmd/main.go +++ b/framework/cmd/main.go @@ -29,17 +29,28 @@ func main() { Name: "env", Aliases: []string{"e"}, Usage: "Generate a Chainlink Node developer environment", - Description: `This utility generate a Chainlink Node Cluster (DON) environment. -Default setup includes: -- Anvil (EVM emulator from Foundry) or Geth blockchain nodes -- Chainlink Nodes -- Fake server (for 3rd party dependencies) -- Test boilerplates, CI and documentation + Description: `🔗 Chainlink's Developer Environment Generator 🔗 + +Prerequisites: + We are using Just, please install it first - https://github.com/casey/just Usage: - Generate a basic environment: - ctf gen env --cli myenv --nodes 4 + ⚙️ Generate basic environment: + ctf gen env --cli myenv --product-name Knilniahc --nodes 4 + + 🔧 Address all TODO comments and customize it if needed + + 💻 Enter the shell and spin up the environment: + cd devenv && just cli && myenv sh + + 🔍 Implement system-level smoke tests (tests/smoke_test.go) and run them: + myenv test smoke + + 📈 Implement load/chaos tests (tests/load_test.go) and run them: + myenv test load + + 🔄 Enforce quality standards in CI: copy .github/workflows to your CI folder and commit `, ArgsUsage: "", Flags: []cli.Flag{ @@ -48,6 +59,17 @@ Usage: Aliases: []string{"c"}, Usage: "Your devenv CLI binary name", }, + &cli.StringFlag{ + Name: "product-name", + Aliases: []string{"r"}, + Usage: "Your product name", + }, + &cli.StringFlag{ + Name: "product-configuration-type", + Aliases: []string{"p"}, + Value: "evm-single", + Usage: "Product configuration type/layout (single network, multi-network, etc)", + }, &cli.IntFlag{ Name: "nodes", Aliases: []string{"n"}, @@ -56,17 +78,23 @@ Usage: }, }, Action: func(c *cli.Context) error { + productConfType := c.String("product-configuration-type") nodes := c.Int("nodes") cliName := c.String("cli") if cliName == "" { return fmt.Errorf("CLI name can't be empty, choose your CLI name") } + productName := c.String("product-name") + if productName == "" { + return fmt.Errorf("Product name must be specified, call your product somehow, any name") + } framework.L.Info(). Str("Name", cliName). Int("CLNodes", nodes). + Str("ProductConfigurationType", productConfType). Msg("Generating developer environment") - cg, err := framework.NewEnvBuilder(cliName, nodes).Build() + cg, err := framework.NewEnvBuilder(cliName, nodes, productConfType, productName).Build() if err != nil { return fmt.Errorf("failed to create codegen: %w", err) } diff --git a/framework/tmpl_gen.go b/framework/tmpl_gen.go index 2ce02596d..51354d664 100644 --- a/framework/tmpl_gen.go +++ b/framework/tmpl_gen.go @@ -9,15 +9,11 @@ import ( "runtime" "strings" "text/template" + "unicode" "github.com/rs/zerolog/log" ) -const ( - // DefaultTestSuiteName default table test name - DefaultTestSuiteName = "TestSmokeE2E" -) - /* Templates */ const ( @@ -75,6 +71,96 @@ replace ( image = "public.ecr.aws/chainlink/chainlink:2.26.0" {{- end }} ` + + // CISmokeTmpl is a continuous integration template for end-to-end smoke tests + CISmokeTmpl = `name: End-to-end {{ .ProductName }} Tests + +on: + pull_request: + +defaults: + run: + working-directory: {{ .DevEnvRelPath }} + +concurrency: + group: {{"${{"}} github.workflow {{"}}"}}-{{"${{"}} github.ref {{"}}"}}-{{"${{"}} github.sha {{"}}"}} + cancel-in-progress: true + +jobs: + e2e-tests: + permissions: + id-token: write + contents: read + pull-requests: write + runs-on: ubuntu-latest + strategy: + matrix: + include: + - name: smoke + config: env.toml + # Add more test configurations as needed + steps: + - name: Checkout code + uses: actions/checkout@v5 + with: + fetch-depth: 0 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1 + + - name: Install Just + uses: extractions/setup-just@e33e0265a09d6d736e2ee1e0eb685ef1de4669ff # v3 + with: + just-version: '1.40.0' + + - name: Authenticate to AWS ECR + uses: ./.github/actions/aws-ecr-auth + with: + role-to-assume: {{"${{"}} secrets.CCV_IAM_ROLE {{"}}"}} + aws-region: us-east-1 + registry-type: public + + - name: Authenticate to AWS ECR (JD) + uses: ./.github/actions/aws-ecr-auth + with: + role-to-assume: {{"${{"}} secrets.CCV_IAM_ROLE {{"}}"}} + aws-region: us-west-2 + registry-type: private + registries: {{"${{"}} secrets.JD_REGISTRY {{"}}"}} + + - name: Set up Go + uses: actions/setup-go@v6 # v6 + with: + cache: true + go-version-file: {{ .DevEnvRelPath }}/go.mod + cache-dependency-path: {{ .DevEnvRelPath }}/go.sum + + - name: Download Go dependencies + run: | + go mod download + + - name: Run CCV environment + env: + JD_IMAGE: {{"${{"}} secrets.JD_IMAGE {{"$}}"}} + run: | + cd cmd/{{ .CLIName }} && go install . && cd - + ccv u {{"${{"}} matrix.config {{"}}"}} + + - name: Run tests + working-directory: build/devenv/tests + run: | + set -o pipefail + go test -v -count=1 -run 'Test{{"${{"}} matrix.name {{"}}"}}' + + - name: Upload Logs + if: always() + uses: actions/upload-artifact@v4 + with: + name: container-logs-{{"${{"}} matrix.name {{"}}"}} + path: {{ .DevEnvRelPath }}/tests/logs + retention-days: 1 +` + // CompletionTmpl is a go-prompt library completion template providing interactive prompt CompletionTmpl = `package main @@ -105,11 +191,8 @@ func getSubCommands(parent string) []prompt.Suggest { switch parent { case "test": return []prompt.Suggest{ - {Text: "soak", Description: "Run {{ .CLIName }} soak test"}, - {Text: "rpc-latency", Description: "Default soak test + 400ms RPC latency (all chains)"}, - {Text: "gas-spikes", Description: "Default soak test + slow and fast gas spikes"}, - {Text: "reorgs", Description: "Default soak test + reorgs (Requires 'up env.toml,env-geth.toml' environment"}, - {Text: "chaos", Description: "Default soak test + chaos (restarts, latency, data loss between services)"}, + {Text: "smoke", Description: "Run {{ .CLIName }} smoke test"}, + {Text: "load", Description: "Run {{ .CLIName }} load test"}, } case "bs": return []prompt.Suggest{ @@ -251,12 +334,12 @@ import ( const ( LocalWASPLoadDashboard = "http://localhost:3000/d/WASPLoadTests/wasp-load-test?orgId=1&from=now-5m&to=now&refresh=5s" - Local{{ .CLIName}}Dashboard = "http://localhost:3000/d/f8a04cef-653f-46d3-86df-87c532300672/svr-soak-test?orgId=1&refresh=5s" + Local{{ .ProductName }}Dashboard = "http://localhost:3000/d/f8a04cef-653f-46d3-86df-87c532300672/svr-soak-test?orgId=1&refresh=5s" ) var rootCmd = &cobra.Command{ Use: "{{ .CLIName }}", - Short: "A {{ .CLIName }} local environment tool", + Short: "A {{ .ProductName }} local environment tool", PersistentPreRunE: func(cmd *cobra.Command, args []string) error { debug, err := cmd.Flags().GetBool("debug") if err != nil { @@ -394,8 +477,8 @@ var obsUpCmd = &cobra.Command{ if err != nil { return fmt.Errorf("observability up failed: %w", err) } - devenv.Plog.Info().Msgf("{{ .CLIName }} Dashboard: %s", Local{{ .CLIName }}Dashboard) - devenv.Plog.Info().Msgf("{{ .CLIName }} Load Test Dashboard: %s", LocalWASPLoadDashboard) + devenv.Plog.Info().Msgf("{{ .ProductName }} Dashboard: %s", Local{{ .ProductName }}Dashboard) + devenv.Plog.Info().Msgf("{{ .ProductName }} Load Test Dashboard: %s", LocalWASPLoadDashboard) return nil }, } @@ -427,8 +510,8 @@ var obsRestartCmd = &cobra.Command{ if err != nil { return fmt.Errorf("observability up failed: %w", err) } - devenv.Plog.Info().Msgf("{{ .CLIName }} Dashboard: %s", Local{{ .CLIName }}Dashboard) - devenv.Plog.Info().Msgf("{{ .CLIName }} Load Test Dashboard: %s", LocalWASPLoadDashboard) + devenv.Plog.Info().Msgf("{{ .ProductName }} Dashboard: %s", Local{{ .ProductName }}Dashboard) + devenv.Plog.Info().Msgf("{{ .ProductName }} Load Test Dashboard: %s", LocalWASPLoadDashboard) return nil }, } @@ -443,16 +526,10 @@ var testCmd = &cobra.Command{ } var testPattern string switch args[0] { - case "soak": - testPattern = "TestSoak" - case "rpc-latency": - testPattern = "TestSoak/rpc_latency" - case "gas-spikes": - testPattern = "TestSoak/gas" - case "reorg": - testPattern = "TestSoak/reorg" - case "chaos": - testPattern = "TestSoak/chaos" + case "smoke": + testPattern = "TestSmoke" + case "load": + testPattern = "TestLoadChaos" default: return fmt.Errorf("test suite %s is unknown, choose between smoke or load", args[0]) } @@ -529,8 +606,200 @@ func main() { os.Exit(1) } }` - // TestsTmpl is a an e2e table test template - TestsTmpl = `` + // LoadTestTmpl is a load/chaos test template + LoadTestTmpl = `package devenv_test + +import ( + "testing" + "time" + + "github.com/go-resty/resty/v2" + "github.com/stretchr/testify/require" + + de "{{ .GoModName }}" + + "github.com/smartcontractkit/chainlink-testing-framework/framework/chaos" + "github.com/smartcontractkit/chainlink-testing-framework/framework/clclient" + "github.com/smartcontractkit/chainlink-testing-framework/wasp" +) + +type ExampleGun struct { + target string + client *resty.Client + Data []string +} + +func NewExampleHTTPGun(target string) *ExampleGun { + return &ExampleGun{ + client: resty.New(), + target: target, + Data: make([]string, 0), + } +} + +// Call implements example gun call, assertions on response bodies should be done here +func (m *ExampleGun) Call(l *wasp.Generator) *wasp.Response { + var result map[string]any + r, err := m.client.R(). + SetResult(&result). + Get(m.target) + if err != nil { + return &wasp.Response{Data: result, Error: err.Error()} + } + if r.Status() != "200 OK" { + return &wasp.Response{Data: result, Error: "not 200", Failed: true} + } + return &wasp.Response{Data: result} +} + +func TestLoadChaos(t *testing.T) { + in, err := de.LoadOutput[de.Cfg]("../env-out.toml") + require.NoError(t, err) + + clNodes, err := clclient.New(in.NodeSets[0].Out.CLNodes) + require.NoError(t, err) + + // use local observability stack for Docker load/chaos tests + t.Setenv("LOKI_URL", "http://localhost:3030/loki/api/v1/push") + + // define labels for differentiate one run from another + labels := map[string]string{ + "go_test_name": "generator_healthcheck", + "gen_name": "generator_healthcheck", + "branch": "test", + "commit": "test", + } + + // create a WASP generator + gen, err := wasp.NewGenerator(&wasp.Config{ + LoadType: wasp.RPS, + T: t, + // just use plain line profile - 1 RPS for 60s + Schedule: wasp.Plain(1, 60*time.Second), + Gun: NewExampleHTTPGun("https://example.com"), + Labels: labels, + LokiConfig: wasp.NewEnvLokiConfig(), + }) + require.NoError(t, err) + gen.Run(false) + + // define chaos test cases + testCases := []struct { + name string + command string + wait time.Duration + validate func(c []*clclient.ChainlinkClient) error + }{ + { + name: "Reboot the pods", + wait: 1 * time.Minute, + command: "stop --duration=20s --restart re2:don-node0", + validate: func(c []*clclient.ChainlinkClient) error { + _, _, err := c[0].ReadBridges() + return err + }, + }, + { + name: "Introduce network delay", + wait: 1 * time.Minute, + command: "netem --tc-image=gaiadocker/iproute2 --duration=1m delay --time=1000 re2:don-node.*", + validate: func(c []*clclient.ChainlinkClient) error { + _, _, err := c[0].ReadBridges() + return err + }, + }, + } + + // Run chaos test cases + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Log(tc.name) + _, err = chaos.ExecPumba(tc.command, tc.wait) + require.NoError(t, err) + err = tc.validate(clNodes) + require.NoError(t, err) + }) + } + // wait for the workload to finish + _, failed := gen.Wait() + require.False(t, failed) +} +` + // SmokeTestTmpl is a smoke test template + SmokeTestTmpl = `package devenv_test + +import ( + "fmt" + "strconv" + "testing" + "time" + + de "{{ .GoModName }}" + + "github.com/smartcontractkit/chainlink-testing-framework/framework/clclient" + "github.com/stretchr/testify/require" + + f "github.com/smartcontractkit/chainlink-testing-framework/framework" +) + +var L = de.Plog + +func TestSmoke(t *testing.T) { + in, err := de.LoadOutput[de.Cfg]("../env-out.toml") + require.NoError(t, err) + c, _, _, err := de.ETHClient(in) + require.NoError(t, err) + clNodes, err := clclient.New(in.NodeSets[0].Out.CLNodes) + require.NoError(t, err) + + tests := []struct { + name string + clNodes []*clclient.ChainlinkClient + }{ + { + name: "feature_1", + }, + { + name: "feature_2", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, _ = c, clNodes + }) + } +} + +// assertResources is a simple assertion on resources if you run with the observability stack (obs up) +func assertResources(t *testing.T, in *de.Cfg, start, end time.Time) { + pc := f.NewPrometheusQueryClient(f.LocalPrometheusBaseURL) + // no more than 10% CPU for this test + maxCPU := 10.0 + cpuResp, err := pc.Query("sum(rate(container_cpu_usage_seconds_total{name=~\".*don.*\"}[5m])) by (name) *100", end) + require.NoError(t, err) + cpu := f.ToLabelsMap(cpuResp) + for i := 0; i < in.NodeSets[0].Nodes; i++ { + nodeLabel := fmt.Sprintf("name:don-node%d", i) + nodeCpu, err := strconv.ParseFloat(cpu[nodeLabel][0].(string), 64) + L.Info().Int("Node", i).Float64("CPU", nodeCpu).Msg("CPU usage percentage") + require.NoError(t, err) + require.LessOrEqual(t, nodeCpu, maxCPU) + } + // no more than 200mb for this test + maxMem := int(200e6) // 200mb + memoryResp, err := pc.Query("sum(container_memory_rss{name=~\".*don.*\"}) by (name)", end) + require.NoError(t, err) + mem := f.ToLabelsMap(memoryResp) + for i := 0; i < in.NodeSets[0].Nodes; i++ { + nodeLabel := fmt.Sprintf("name:don-node%d", i) + nodeMem, err := strconv.Atoi(mem[nodeLabel][0].(string)) + L.Info().Int("Node", i).Int("Memory", nodeMem).Msg("Total memory") + require.NoError(t, err) + require.LessOrEqual(t, nodeMem, maxMem) + } +} +` // CLDFTmpl is a Chainlink Deployments Framework template CLDFTmpl = `package {{ .PackageName }} @@ -1146,8 +1415,8 @@ func NewEnvironment() (*Cfg, error) { return in, Store[Cfg](in) } ` - // SingleNetworkProductConfigurationTmpl is an single-network product configuration template - SingleNetworkProductConfigurationTmpl = `package {{ .PackageName }} + // SingleNetworkProductConfigurationTmpl is an single-network EVM product configuration template + SingleNetworkEVMProductConfigurationTmpl = `package {{ .PackageName }} import ( "context" @@ -1412,6 +1681,23 @@ cli: /* Template params in heirarchical order, module -> file(table test) -> test */ +// SmokeTestParams params for generating end-to-end test template +type SmokeTestParams struct { + GoModName string +} + +// LoadTestParams params for generating end-to-end test template +type LoadTestParams struct { + GoModName string +} + +// GoModParams params for generating go.mod file +type CISmokeParams struct { + ProductName string + DevEnvRelPath string + CLIName string +} + // GoModParams params for generating go.mod file type GoModParams struct { ModuleName string @@ -1442,6 +1728,7 @@ type CLIParams struct { PackageName string CLIName string DevEnvPkgImport string + ProductName string } // CLDFParams cldf.go file params @@ -1488,10 +1775,12 @@ type TestCaseParams struct { // EnvBuilder builder for load test codegen type EnvBuilder struct { + productName string nodes int outputDir string packageName string cliName string + productType string moduleName string } @@ -1501,13 +1790,15 @@ type EnvCodegen struct { } // NewEnvBuilder creates a new Chainlink Cluster developer environment -func NewEnvBuilder(cliName string, nodes int) *EnvBuilder { +func NewEnvBuilder(cliName string, nodes int, productType string, productName string) *EnvBuilder { return &EnvBuilder{ + productName: productName, cliName: cliName, nodes: nodes, packageName: "devenv", outputDir: "devenv", - moduleName: fmt.Sprintf("github.com/smartcontractkit/%s/devenv", cliName), + productType: productType, + moduleName: fmt.Sprintf("github.com/smartcontractkit/%s/devenv", productName), } } @@ -1524,11 +1815,25 @@ func (g *EnvBuilder) GoModName(name string) *EnvBuilder { } // validate verifier that we can build codegen with provided params -// empty for now, add validation here if later it'd become more complex +// fixing input params if possible func (g *EnvBuilder) validate() error { + // to avoid issues in different contexts we only allow lower-case letters, no special symbols or numerics + g.productName = onlyLetters(g.productName) + g.cliName = onlyLetters(g.cliName) + g.moduleName = onlyLetters(g.moduleName) return nil } +// onlyLetters strip any symbol except letters +func onlyLetters(name string) string { + return strings.Map(func(r rune) rune { + if unicode.IsLetter(r) && unicode.IsLower(r) { + return r + } + return -1 + }, name) +} + // Validate validate generation params // for now it's empty but for more complex mutually exclusive cases we should // add validation here @@ -1681,16 +1986,80 @@ func (g *EnvCodegen) Write() error { } // Generate product_configuration.go - prodConfigFileContents, err := g.GenerateSingleNetworkProductConfiguration() + switch g.cfg.productType { + case "evm-single": + prodConfigFileContents, err := g.GenerateSingleNetworkProductConfiguration() + if err != nil { + return err + } + if err := os.WriteFile( //nolint:gosec + filepath.Join(g.cfg.outputDir, "product_configuration.go"), + []byte(prodConfigFileContents), + os.ModePerm, + ); err != nil { + return fmt.Errorf("failed to write product configuration file: %w", err) + } + case "multi-network": + return fmt.Errorf("product configuration 'multi-network' is not supported yet") + default: + return fmt.Errorf("unknown product configuration type: %s, known types are 'evm-single' or 'multi-network'", g.cfg.productType) + } + + // create CI directory + ciDir := filepath.Join(g.cfg.outputDir, ".github", "workflows") + if err := os.MkdirAll( //nolint:gosec + ciDir, + os.ModePerm, + ); err != nil { + return fmt.Errorf("failed to create output directory: %w", err) + } + + // Generate GitHub CI smoke test workflow + ciSmokeContents, err := g.GenerateCISmoke() + if err != nil { + return err + } + if err := os.WriteFile( //nolint:gosec + filepath.Join(ciDir, "devenv-smoke-test.yml"), + []byte(ciSmokeContents), + os.ModePerm, + ); err != nil { + return fmt.Errorf("failed to write CI smoke workflow file: %w", err) + } + + // create e2e tests directory + e2eDir := filepath.Join(g.cfg.outputDir, "tests") + if err := os.MkdirAll( //nolint:gosec + e2eDir, + os.ModePerm, + ); err != nil { + return fmt.Errorf("failed to create output directory: %w", err) + } + + // generate smoke tests + smokeTestsContent, err := g.GenerateSmokeTests() + if err != nil { + return err + } + if err := os.WriteFile( //nolint:gosec + filepath.Join(e2eDir, "smoke_test.go"), + []byte(smokeTestsContent), + os.ModePerm, + ); err != nil { + return fmt.Errorf("failed to write smoke tests file: %w", err) + } + + // generate load/chaos tests + loadTestsContent, err := g.GenerateLoadTests() if err != nil { return err } if err := os.WriteFile( //nolint:gosec - filepath.Join(g.cfg.outputDir, "product_configuration.go"), - []byte(prodConfigFileContents), + filepath.Join(e2eDir, "load_test.go"), + []byte(loadTestsContent), os.ModePerm, ); err != nil { - return fmt.Errorf("failed to write product configuration file: %w", err) + return fmt.Errorf("failed to write load tests file: %w", err) } // tidy and finalize @@ -1716,8 +2085,42 @@ func (g *EnvCodegen) Write() error { return nil } +// GenerateSmokeTests generates a smoke test template +func (g *EnvCodegen) GenerateLoadTests() (string, error) { + log.Info().Msg("Generating load test template") + data := LoadTestParams{ + GoModName: g.cfg.moduleName, + } + return render(LoadTestTmpl, data) +} + +// GenerateSmokeTests generates a smoke test template +func (g *EnvCodegen) GenerateSmokeTests() (string, error) { + log.Info().Msg("Generating smoke test template") + data := SmokeTestParams{ + GoModName: g.cfg.moduleName, + } + return render(SmokeTestTmpl, data) +} + +// GenerateCISmoke generates a smoke test CI workflow +func (g *EnvCodegen) GenerateCISmoke() (string, error) { + log.Info().Msg("Generating GitHub CI smoke test") + p, err := dirPathRelFromGitRoot(g.cfg.outputDir) + if err != nil { + return "", fmt.Errorf("failed to find relative devenv path from Git root: %w", err) + } + data := CISmokeParams{ + DevEnvRelPath: p, + ProductName: g.cfg.productName, + CLIName: g.cfg.cliName, + } + return render(CISmokeTmpl, data) +} + // GenerateGoMod generates a go.mod file func (g *EnvCodegen) GenerateGoMod() (string, error) { + log.Info().Msg("Generating Go module") data := GoModParams{ ModuleName: g.cfg.moduleName, RuntimeVersion: strings.ReplaceAll(runtime.Version(), "go", ""), @@ -1727,6 +2130,7 @@ func (g *EnvCodegen) GenerateGoMod() (string, error) { // GenerateDefaultTOMLConfig generate default env.toml config func (g *EnvCodegen) GenerateDefaultTOMLConfig() (string, error) { + log.Info().Msg("Generating default environment config (env.toml)") p := ConfigTOMLParams{ PackageName: g.cfg.packageName, Nodes: g.cfg.nodes, @@ -1737,6 +2141,7 @@ func (g *EnvCodegen) GenerateDefaultTOMLConfig() (string, error) { // GenerateJustfile generate Justfile to build and publish Docker images func (g *EnvCodegen) GenerateJustfile() (string, error) { + log.Info().Msg("Generating Justfile") p := JustfileParams{ PackageName: g.cfg.packageName, CLIName: g.cfg.cliName, @@ -1746,6 +2151,7 @@ func (g *EnvCodegen) GenerateJustfile() (string, error) { // GenerateCLICompletion generate CLI completion for "go-prompt" library func (g *EnvCodegen) GenerateCLICompletion() (string, error) { + log.Info().Msg("Generating shell completion") p := CLICompletionParams{ PackageName: g.cfg.packageName, CLIName: g.cfg.cliName, @@ -1755,24 +2161,28 @@ func (g *EnvCodegen) GenerateCLICompletion() (string, error) { // GenerateCLI generate Cobra CLI func (g *EnvCodegen) GenerateCLI() (string, error) { + log.Info().Msg("Generating Cobra CLI") p := CLIParams{ PackageName: g.cfg.packageName, CLIName: g.cfg.cliName, + ProductName: g.cfg.productName, DevEnvPkgImport: g.cfg.moduleName, } return render(CLITmpl, p) } -// GenerateSingleNetworkProductConfiguration generate a single-network product configuration +// GenerateSingleNetworkProductConfiguration generate a single-network EVM product configuration func (g *EnvCodegen) GenerateSingleNetworkProductConfiguration() (string, error) { + log.Info().Msg("Configuring EVM network") p := ProductConfigurationSimple{ PackageName: g.cfg.packageName, } - return render(SingleNetworkProductConfigurationTmpl, p) + return render(SingleNetworkEVMProductConfigurationTmpl, p) } // GenerateEnvironment generate environment.go, our environment composition function func (g *EnvCodegen) GenerateEnvironment() (string, error) { + log.Info().Msg("Generating environment composition (environment.go)") p := EnvParams{ PackageName: g.cfg.packageName, } @@ -1781,6 +2191,7 @@ func (g *EnvCodegen) GenerateEnvironment() (string, error) { // GenerateCLDF generate CLDF helpers func (g *EnvCodegen) GenerateCLDF() (string, error) { + log.Info().Msg("Generating CLDF helpers") p := CLDFParams{ PackageName: g.cfg.packageName, } @@ -1789,6 +2200,7 @@ func (g *EnvCodegen) GenerateCLDF() (string, error) { // GenerateDebugTools generate debug tools (tracing) func (g *EnvCodegen) GenerateDebugTools() (string, error) { + log.Info().Msg("Generating debug tools") p := ToolsParams{ PackageName: g.cfg.packageName, } @@ -1797,6 +2209,7 @@ func (g *EnvCodegen) GenerateDebugTools() (string, error) { // GenerateConfig generate read/write utilities for TOML configs func (g *EnvCodegen) GenerateConfig() (string, error) { + log.Info().Msg("Generating config tools") p := ConfigParams{ PackageName: g.cfg.packageName, } @@ -1806,25 +2219,63 @@ func (g *EnvCodegen) GenerateConfig() (string, error) { // GenerateTableTest generates all possible experiments for a namespace // first generate all small pieces then insert into a table test template func (g *EnvCodegen) GenerateTableTest() (string, error) { + log.Info().Msg("Generating end-to-end table tests template") // TODO: generate a table test when we'll have chain-specific interface solidified return "", nil } // GenerateTestCases generates table test cases func (g *EnvCodegen) GenerateTestCases() ([]TestCaseParams, error) { + log.Info().Msg("Generating test cases") // TODO: generate test cases when we'll have chain-specific interface solidified return []TestCaseParams{}, nil } +/* Utility */ + +func getGitRoot() (string, error) { + cmd := exec.Command("git", "rev-parse", "--show-toplevel") + output, err := cmd.Output() + if err != nil { + return "", fmt.Errorf("not a git repository or git not installed: %w", err) + } + return strings.TrimSpace(string(output)), nil +} + +func dirPathRelFromGitRoot(name string) (string, error) { + gitRoot, err := getGitRoot() + if err != nil { + return "", err + } + var devenvPath string + err = filepath.Walk(gitRoot, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if info.IsDir() && info.Name() == name { + devenvPath, err = filepath.Rel(gitRoot, path) + if err != nil { + return err + } + return filepath.SkipAll + } + return nil + }) + if err != nil { + return "", fmt.Errorf("devenv directory not found: %w", err) + } + return devenvPath, nil +} + // render is just an internal function to parse and render template func render(tmpl string, data any) (string, error) { parsed, err := template.New("").Parse(tmpl) if err != nil { - return "", fmt.Errorf("failed to parse table test template: %w", err) + return "", fmt.Errorf("failed to parse text template: %w", err) } var buf bytes.Buffer if err := parsed.Execute(&buf, data); err != nil { - return "", fmt.Errorf("failed to generate table test: %w", err) + return "", fmt.Errorf("failed to generate text: %w", err) } return buf.String(), err } diff --git a/framework/tmpl_gen_test.go b/framework/tmpl_gen_test.go index 3fe6ddc0a..f2bbfb63a 100644 --- a/framework/tmpl_gen_test.go +++ b/framework/tmpl_gen_test.go @@ -41,28 +41,39 @@ type TestCfg struct { // TestSmokeGenerateEnvPositive top-down approach tests until all the environment variations aren't stable func TestSmokeGenerateEnvPositive(t *testing.T) { tests := []struct { - name string - goModName string - cliName string - outputDir string - nodes int + name string + goModName string + cliName string + productName string + productType string + outputDir string + nodes int }{ // all variations of env, test they can be generated and create valid services // that can pass healthchecks { - name: "basic 2 nodes env", - goModName: "devenv", - cliName: "tcli", - outputDir: "test-env", - nodes: 2, + name: "basic 2 nodes env", + goModName: "devenv", + cliName: "tcli", + productName: "myproduct", + productType: "evm-single", + outputDir: "test-env", + nodes: 2, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - defer os.RemoveAll(tt.outputDir) - defer runCmd(t, tt.outputDir, tt.cliName, `down`) - cg, err := framework.NewEnvBuilder(tt.cliName, tt.nodes). + t.Cleanup(func() { + runCmd(t, tt.outputDir, tt.cliName, `down`) + os.RemoveAll(tt.outputDir) + }) + cg, err := framework.NewEnvBuilder( + tt.cliName, + tt.nodes, + tt.productType, + tt.productName, + ). GoModName(tt.goModName). OutputDir(tt.outputDir). Build() From c42324377150a3b35eb5eba02b84acc362e421de Mon Sep 17 00:00:00 2001 From: skudasov Date: Thu, 20 Nov 2025 13:15:45 +0100 Subject: [PATCH 09/16] all product-agnostic boilerplate --- book/src/SUMMARY.md | 6 +- book/src/framework/generate.md | 22 +- framework/cmd/main.go | 58 +- framework/tmpl_gen.go | 2041 +++++++++++++++++++++++--------- framework/tmpl_gen_test.go | 24 +- 5 files changed, 1576 insertions(+), 575 deletions(-) diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md index ae2506ba5..f178fad7f 100644 --- a/book/src/SUMMARY.md +++ b/book/src/SUMMARY.md @@ -4,15 +4,11 @@ - [Framework](./framework/overview.md) - [Basic Usage](./framework/getting_started.md) - [Getting Started](./framework/getting_started.md) - - [Your First Test](./framework/first_test.md) - - [NodeSet Environment](./framework/nodeset_environment.md) - - [NodeSet (Capabilities)](./framework/nodeset_capabilities.md) - - [NodeSet (Local Docker builds)](./framework/nodeset_docker_rebuild.md) + - [Generating Developer Environment](./framework/generate.md) - [Advanced Usage](./framework/configuration.md) - [CLI](./framework/cli.md) - [Configuration](./framework/configuration.md) - [Debugging Tests](framework/components/debug.md) - - [Generating Tests](framework/generate.md) - [Creating your own components](./developing/developing_components.md) - [Exposing Components](framework/components/state.md) - [Asserting Logs](./developing/asserting_logs.md) diff --git a/book/src/framework/generate.md b/book/src/framework/generate.md index 5a8e55fa1..00246c36d 100644 --- a/book/src/framework/generate.md +++ b/book/src/framework/generate.md @@ -1,7 +1,21 @@ -# Generating Tests +# Generating Chainlink Environment -Some types of tests may require a lot of boilerplate and configurability, for example load and chaos tests. +Our code generation tools automatically build complete Chainlink environments and test templates, which minimizes documentation and provides a framework that is both structured and easily extensible. -We provide code generation tool that help you to start with best practes in such cases. +Let's read `help` first and then build an environment for a single EVM network: +```bash +ctf gen -h +# generate a new Chainlink environment in "devenv" directory with 4 Chainlink nodes and one EVM network. Generate CLI called "pcli" and enter the shell +ctf gen env --cli pcli --product-name MyProduct --output-dir devenv --nodes 4 +``` -All generators have `-h` or `--help` flag, please read the docs! \ No newline at end of file +Follow further instructions in `devenv/README.md` + +# Generating Infrastructure Testing Template + +If you have Chainlink infrastructure already deployed it's useful to generate a workload + chaos suite template. +```bash +ctf gen load -h +# generate test suite named ChaosGen, with workload + default chaos experiments (fail + latency) for all the pods that have app.kubernetes.io/instance annotation +ctf gen load -w -n ChaosGen default +``` \ No newline at end of file diff --git a/framework/cmd/main.go b/framework/cmd/main.go index 8be9242f7..aed66a17d 100644 --- a/framework/cmd/main.go +++ b/framework/cmd/main.go @@ -4,6 +4,7 @@ import ( "fmt" "log" "os" + "os/exec" "path/filepath" "strings" @@ -32,25 +33,16 @@ func main() { Description: `🔗 Chainlink's Developer Environment Generator 🔗 Prerequisites: - We are using Just, please install it first - https://github.com/casey/just + We are using Just, please install it first - https://github.com/casey/just Usage: ⚙️ Generate basic environment: - ctf gen env --cli myenv --product-name Knilniahc --nodes 4 + ctf gen env --cli myenv --output-dir devenv --product-name Knilniahc --nodes 4 + + 📜 Read the docs in devenv/README.md - 🔧 Address all TODO comments and customize it if needed - - 💻 Enter the shell and spin up the environment: - cd devenv && just cli && myenv sh - - 🔍 Implement system-level smoke tests (tests/smoke_test.go) and run them: - myenv test smoke - - 📈 Implement load/chaos tests (tests/load_test.go) and run them: - myenv test load - - 🔄 Enforce quality standards in CI: copy .github/workflows to your CI folder and commit + 🔧 Address all TODO comments and customize it `, ArgsUsage: "", Flags: []cli.Flag{ @@ -59,6 +51,12 @@ Usage: Aliases: []string{"c"}, Usage: "Your devenv CLI binary name", }, + &cli.StringFlag{ + Name: "output-dir", + Aliases: []string{"o"}, + Value: "devenv", + Usage: "Your devenv directory", + }, &cli.StringFlag{ Name: "product-name", Aliases: []string{"r"}, @@ -78,6 +76,7 @@ Usage: }, }, Action: func(c *cli.Context) error { + outputDir := c.String("output-dir") productConfType := c.String("product-configuration-type") nodes := c.Int("nodes") cliName := c.String("cli") @@ -89,18 +88,47 @@ Usage: return fmt.Errorf("Product name must be specified, call your product somehow, any name") } framework.L.Info(). + Str("OutputDir", outputDir). Str("Name", cliName). Int("CLNodes", nodes). Str("ProductConfigurationType", productConfType). Msg("Generating developer environment") - cg, err := framework.NewEnvBuilder(cliName, nodes, productConfType, productName).Build() + cg, err := framework.NewEnvBuilder(cliName, nodes, productConfType, productName). + OutputDir(outputDir). + Build() if err != nil { return fmt.Errorf("failed to create codegen: %w", err) } if err := cg.Write(); err != nil { return fmt.Errorf("failed to generate module: %w", err) } + + fmt.Println() + fmt.Printf("📁 Your environment directory is: %s\n", outputDir) + fmt.Printf("💻 Your CLI name is: %s\n", cliName) + fmt.Printf("📜 More docs can be found in %s/README.md\n", outputDir) + fmt.Println() + + cmd := exec.Command("just", "cli") + cmd.Env = os.Environ() + cmd.Dir = outputDir + out, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("failed to build CLI via Justfile: %w, output: %s", err, string(out)) + } + if err := os.Chdir(outputDir); err != nil { + return err + } + cmd = exec.Command(cliName, "sh") + cmd.Env = os.Environ() + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + err = cmd.Run() + if err != nil { + return fmt.Errorf("failed to enter devenv shell: %w", err) + } return nil }, }, diff --git a/framework/tmpl_gen.go b/framework/tmpl_gen.go index 51354d664..2a69973bd 100644 --- a/framework/tmpl_gen.go +++ b/framework/tmpl_gen.go @@ -17,6 +17,37 @@ import ( /* Templates */ const ( + // ProductDashboardUUID is a default product dashboard uuid, can be static since it's our local environment + ProductDashboardUUID = "f8a04cef-653f-46d3-86df-87c532300672" + + ReadmeTmpl = `## Chainlink Developer Environment + +This template provides a complete Chainlink development environment with pre-configured infrastructure and observability tools, enabling rapid development while maintaining high quality standards. + +🔧 Address all **TODO** comments and implement "product_configuration.go" + +💻 Enter the shell: +` + "```" + `bash +just cli && {{ .CLIName }} sh +` + "```" + ` + +🚀 Spin up the environment +` + "```" + `bash +up ↵ +` + "```" + ` + +🔍 Implement system-level smoke tests (tests/smoke_test.go) and run them: +` + "```" + `bash +test smoke ↵ +` + "```" + ` + +📈 Implement load/chaos tests (tests/load_test.go) and run them: +` + "```" + `bash +test load ↵ +` + "```" + ` + +🔄 **Enforce** quality standards in CI: copy .github/workflows to your CI folder, commit and make them pass +` // GoModTemplate go module template GoModTemplate = `module {{.ModuleName}} @@ -36,6 +67,746 @@ replace ( github.com/smartcontractkit/chainlink-testing-framework/lib => github.com/smartcontractkit/chainlink-testing-framework/lib v1.54.4 ) ` + // GitIgnoreTmpl default gitignore template + GitIgnoreTmpl = `compose/ +blockscout/ +env-out.toml` + + // GrafanaDashboardTmpl is a Grafana dashboard template for your product + GrafanaDashboardTmpl = `{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "liveNow": false, + "panels": [ + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 34, + "panels": [], + "title": "Container Resources", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "percent" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 0, + "y": 1 + }, + "id": 2, + "options": { + "legend": { + "calcs": [ + "mean", + "max" + ], + "displayMode": "table", + "placement": "right", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "10.1.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "expr": "sum(rate(container_cpu_usage_seconds_total{name=~\".*don.*|.*fake.*\"}[5m])) by (name) *100", + "hide": false, + "interval": "", + "legendFormat": "{{"{{"}}name{{"}}"}}", + "range": true, + "refId": "A" + } + ], + "title": "CPU Usage", + "transparent": true, + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "Bps" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 12, + "y": 1 + }, + "id": 4, + "options": { + "legend": { + "calcs": [ + "mean", + "max" + ], + "displayMode": "table", + "placement": "right", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "10.1.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "expr": "sum(rate(container_network_receive_bytes_total{name=~\".*don.*|.*fake.*\"}[5m])) by (name)", + "hide": false, + "interval": "", + "legendFormat": "{{"{{"}}name{{"}}"}}", + "range": true, + "refId": "A" + } + ], + "title": "Received Network Traffic", + "transparent": true, + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 0, + "y": 8 + }, + "id": 3, + "options": { + "legend": { + "calcs": [ + "mean", + "max" + ], + "displayMode": "table", + "placement": "right", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "10.1.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "expr": "sum(container_memory_rss{name=~\".*don.*|.*fake.*\"}) by (name)", + "hide": false, + "interval": "", + "legendFormat": "{{"{{"}}name{{"}}"}}", + "range": true, + "refId": "A" + } + ], + "title": "Memory Usage", + "transparent": true, + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "Bps" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 12, + "y": 8 + }, + "id": 5, + "options": { + "legend": { + "calcs": [ + "mean", + "max" + ], + "displayMode": "table", + "placement": "right", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "10.1.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "PBFA97CFB590B2093" + }, + "editorMode": "code", + "expr": "sum(rate(container_network_transmit_bytes_total{name=~\".*don.*|.*fake.*\"}[5m])) by (name)", + "interval": "", + "legendFormat": "{{"{{"}}name{{"}}"}}", + "range": true, + "refId": "A" + } + ], + "title": "Sent Network Traffic", + "transparent": true, + "type": "timeseries" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 15 + }, + "id": 11, + "panels": [ + { + "datasource": { + "type": "loki", + "uid": "P8E80F9AEF21F6940" + }, + "description": "CL Node Logs", + "gridPos": { + "h": 5, + "w": 24, + "x": 0, + "y": 16 + }, + "id": 6, + "options": { + "dedupStrategy": "none", + "enableLogDetails": true, + "prettifyLogMessage": false, + "showCommonLabels": false, + "showLabels": false, + "showTime": false, + "sortOrder": "Descending", + "wrapLogMessage": false + }, + "targets": [ + { + "datasource": { + "type": "loki", + "uid": "P8E80F9AEF21F6940" + }, + "editorMode": "code", + "expr": "{job=\"ctf\", container=~\".*don-node.*\"}", + "queryType": "range", + "refId": "A" + } + ], + "title": "CL Node Logs", + "transparent": true, + "type": "logs" + }, + { + "datasource": { + "type": "loki", + "uid": "P8E80F9AEF21F6940" + }, + "description": "P2P Discovery Report", + "gridPos": { + "h": 5, + "w": 24, + "x": 0, + "y": 21 + }, + "id": 8, + "options": { + "dedupStrategy": "none", + "enableLogDetails": true, + "prettifyLogMessage": false, + "showCommonLabels": false, + "showLabels": false, + "showTime": false, + "sortOrder": "Descending", + "wrapLogMessage": false + }, + "targets": [ + { + "datasource": { + "type": "loki", + "uid": "P8E80F9AEF21F6940" + }, + "editorMode": "code", + "expr": "{job=\"ctf\"} |= \"DiscoveryProtocol: Status report\"", + "queryType": "range", + "refId": "A" + } + ], + "title": "P2P Discovery Report", + "transparent": true, + "type": "logs" + } + ], + "title": "CL Node Stats", + "type": "row" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 16 + }, + "id": 35, + "panels": [ + { + "datasource": { + "type": "loki", + "uid": "P8E80F9AEF21F6940" + }, + "gridPos": { + "h": 4, + "w": 24, + "x": 0, + "y": 17 + }, + "id": 40, + "options": { + "code": { + "language": "plaintext", + "showLineNumbers": false, + "showMiniMap": false + }, + "content": "# Pprof profiling with Pyroscope\nSelect \"service_name\" variable to start.\n\nFor more info you can also use native [UI](http://localhost:4040)", + "mode": "markdown" + }, + "pluginVersion": "10.1.0", + "transparent": true, + "type": "text" + }, + { + "datasource": { + "type": "grafana-pyroscope-datasource", + "uid": "P02E4190217B50628" + }, + "description": "CPU", + "gridPos": { + "h": 9, + "w": 24, + "x": 0, + "y": 21 + }, + "id": 36, + "targets": [ + { + "datasource": { + "type": "grafana-pyroscope-datasource", + "uid": "P02E4190217B50628" + }, + "groupBy": [], + "labelSelector": "{service_name=\"${service_name}\"}", + "profileTypeId": "process_cpu:cpu:nanoseconds:cpu:nanoseconds", + "queryType": "profile", + "refId": "A" + } + ], + "title": "CPU", + "transparent": true, + "type": "flamegraph" + }, + { + "datasource": { + "type": "grafana-pyroscope-datasource", + "uid": "P02E4190217B50628" + }, + "description": "Memory (alloc_objects)", + "gridPos": { + "h": 9, + "w": 24, + "x": 0, + "y": 30 + }, + "id": 37, + "targets": [ + { + "datasource": { + "type": "grafana-pyroscope-datasource", + "uid": "P02E4190217B50628" + }, + "groupBy": [], + "labelSelector": "{service_name=\"${service_name}\"}", + "profileTypeId": "memory:alloc_objects:count:space:bytes", + "queryType": "profile", + "refId": "A" + } + ], + "title": "Memory (alloc_objects)", + "transparent": true, + "type": "flamegraph" + }, + { + "datasource": { + "type": "grafana-pyroscope-datasource", + "uid": "P02E4190217B50628" + }, + "description": "Goroutines", + "gridPos": { + "h": 9, + "w": 24, + "x": 0, + "y": 39 + }, + "id": 39, + "targets": [ + { + "datasource": { + "type": "grafana-pyroscope-datasource", + "uid": "P02E4190217B50628" + }, + "groupBy": [], + "labelSelector": "{service_name=\"${service_name}\"}", + "profileTypeId": "goroutines:goroutine:count:goroutine:count", + "queryType": "profile", + "refId": "A" + } + ], + "title": "Goroutines", + "transparent": true, + "type": "flamegraph" + }, + { + "datasource": { + "type": "grafana-pyroscope-datasource", + "uid": "P02E4190217B50628" + }, + "description": "Mutex", + "gridPos": { + "h": 9, + "w": 24, + "x": 0, + "y": 48 + }, + "id": 38, + "targets": [ + { + "datasource": { + "type": "grafana-pyroscope-datasource", + "uid": "P02E4190217B50628" + }, + "groupBy": [], + "labelSelector": "{service_name=\"${service_name}\"}", + "profileTypeId": "mutex:contentions:count:contentions:count", + "queryType": "profile", + "refId": "A" + } + ], + "title": "Mutex", + "transparent": true, + "type": "flamegraph" + } + ], + "title": "Pyroscope", + "type": "row" + } + ], + "refresh": "5s", + "schemaVersion": 38, + "style": "dark", + "tags": [], + "templating": { + "list": [ + { + "current": { + "selected": false, + "text": "chainlink-node", + "value": "chainlink-node" + }, + "description": "service_name", + "hide": 0, + "includeAll": false, + "label": "service_name", + "multi": false, + "name": "service_name", + "options": [ + { + "selected": true, + "text": "chainlink-node", + "value": "chainlink-node" + } + ], + "query": "chainlink-node", + "queryValue": "", + "skipUrlSync": false, + "type": "custom" + } + ] + }, + "time": { + "from": "now-1h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "{{ .ProductName }} Services", + "uid": "{{ .UUID }}", + "version": 1, + "weekStart": "" +}` // ConfigTOMLTmpl is a default env.toml template for devenv describind components configuration ConfigTOMLTmpl = `[on_chain] link_contract_address = "0xDc64a140Aa3E981100a9becA4E685f962f0cF6C9" @@ -72,6 +843,96 @@ replace ( {{- end }} ` + // CILoadChaosTemplate is a continuous integration template for end-to-end load/chaos tests + CILoadChaosTemplate = `name: End-to-end {{ .ProductName }} Load and Chaos Tests + +on: + pull_request: + +defaults: + run: + working-directory: {{ .DevEnvRelPath }} + +concurrency: + group: {{"${{"}} github.workflow {{"}}"}}-{{"${{"}} github.ref {{"}}"}} + cancel-in-progress: true + +jobs: + e2e-tests: + permissions: + id-token: write + contents: read + pull-requests: write + runs-on: ubuntu-latest + strategy: + matrix: + include: + - name: LoadChaos + config: env.toml + # TODO: Add more test and environment configurations as needed + steps: + - name: Checkout code + uses: actions/checkout@v5 + with: + fetch-depth: 0 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1 + + - name: Install Just + uses: extractions/setup-just@e33e0265a09d6d736e2ee1e0eb685ef1de4669ff # v3 + with: + just-version: '1.40.0' + + - name: Authenticate to AWS ECR + uses: ./.github/actions/aws-ecr-auth + with: + role-to-assume: {{"${{"}} secrets.CCV_IAM_ROLE {{"}}"}} + aws-region: us-east-1 + registry-type: public + + - name: Authenticate to AWS ECR (JD) + uses: ./.github/actions/aws-ecr-auth + with: + role-to-assume: {{"${{"}} secrets.CCV_IAM_ROLE {{"}}"}} + aws-region: us-west-2 + registry-type: private + registries: {{"${{"}} secrets.JD_REGISTRY {{"}}"}} + + - name: Set up Go + uses: actions/setup-go@v6 # v6 + with: + cache: true + go-version-file: {{ .DevEnvRelPath }}/go.mod + cache-dependency-path: {{ .DevEnvRelPath }}/go.sum + + - name: Download Go dependencies + run: | + go mod download + + - name: Run CCV environment + env: + JD_IMAGE: {{"${{"}} secrets.JD_IMAGE {{"$}}"}} + run: | + cd cmd/{{ .CLIName }} && go install . && cd - + {{ .CLIName }} u {{"${{"}} matrix.config {{"}}"}} + {{ .CLIName }} obs u -f + + - name: Run tests + working-directory: build/devenv/tests + run: | + set -o pipefail + go test -v -count=1 -run 'Test{{"${{"}} matrix.name {{"}}"}}' + + - name: Upload Logs + if: always() + uses: actions/upload-artifact@v4 + with: + name: container-logs-{{"${{"}} matrix.name {{"}}"}} + path: {{ .DevEnvRelPath }}/tests/logs + retention-days: 1 +` + // CISmokeTmpl is a continuous integration template for end-to-end smoke tests CISmokeTmpl = `name: End-to-end {{ .ProductName }} Tests @@ -83,7 +944,7 @@ defaults: working-directory: {{ .DevEnvRelPath }} concurrency: - group: {{"${{"}} github.workflow {{"}}"}}-{{"${{"}} github.ref {{"}}"}}-{{"${{"}} github.sha {{"}}"}} + group: {{"${{"}} github.workflow {{"}}"}}-{{"${{"}} github.ref {{"}}"}} cancel-in-progress: true jobs: @@ -96,9 +957,9 @@ jobs: strategy: matrix: include: - - name: smoke + - name: Smoke config: env.toml - # Add more test configurations as needed + # TODO: Add more test and environment configurations as needed steps: - name: Checkout code uses: actions/checkout@v5 @@ -144,7 +1005,7 @@ jobs: JD_IMAGE: {{"${{"}} secrets.JD_IMAGE {{"$}}"}} run: | cd cmd/{{ .CLIName }} && go install . && cd - - ccv u {{"${{"}} matrix.config {{"}}"}} + {{ .CLIName }} u {{"${{"}} matrix.config {{"}}"}} - name: Run tests working-directory: build/devenv/tests @@ -334,7 +1195,7 @@ import ( const ( LocalWASPLoadDashboard = "http://localhost:3000/d/WASPLoadTests/wasp-load-test?orgId=1&from=now-5m&to=now&refresh=5s" - Local{{ .ProductName }}Dashboard = "http://localhost:3000/d/f8a04cef-653f-46d3-86df-87c532300672/svr-soak-test?orgId=1&refresh=5s" + Local{{ .ProductName }}Dashboard = "http://localhost:3000/d/{{ .DashboardUUID }}/{{ .ProductName}}?orgId=1&refresh=5s" ) var rootCmd = &cobra.Command{ @@ -354,493 +1215,493 @@ var rootCmd = &cobra.Command{ } var restartCmd = &cobra.Command{ - Use: "restart", - Aliases: []string{"r"}, - Args: cobra.RangeArgs(0, 1), - Short: "Restart development environment, remove apps and apply default configuration again", - RunE: func(cmd *cobra.Command, args []string) error { - var configFile string - if len(args) > 0 { - configFile = args[0] - } else { - configFile = "env.toml" - } - framework.L.Info().Str("Config", configFile).Msg("Reconfiguring development environment") - _ = os.Setenv("CTF_CONFIGS", configFile) - _ = os.Setenv("TESTCONTAINERS_RYUK_DISABLED", "true") - framework.L.Info().Msg("Tearing down the development environment") - err := framework.RemoveTestContainers() - if err != nil { - return fmt.Errorf("failed to clean Docker resources: %w", err) - } - _, err = devenv.NewEnvironment() - return err - }, + Use: "restart", + Aliases: []string{"r"}, + Args: cobra.RangeArgs(0, 1), + Short: "Restart development environment, remove apps and apply default configuration again", + RunE: func(cmd *cobra.Command, args []string) error { + var configFile string + if len(args) > 0 { + configFile = args[0] + } else { + configFile = "env.toml" + } + framework.L.Info().Str("Config", configFile).Msg("Reconfiguring development environment") + _ = os.Setenv("CTF_CONFIGS", configFile) + _ = os.Setenv("TESTCONTAINERS_RYUK_DISABLED", "true") + framework.L.Info().Msg("Tearing down the development environment") + err := framework.RemoveTestContainers() + if err != nil { + return fmt.Errorf("failed to clean Docker resources: %w", err) + } + _, err = devenv.NewEnvironment() + return err + }, } var upCmd = &cobra.Command{ - Use: "up", - Aliases: []string{"u"}, - Short: "Spin up the development environment", - Args: cobra.RangeArgs(0, 1), - RunE: func(cmd *cobra.Command, args []string) error { - var configFile string - if len(args) > 0 { - configFile = args[0] - } else { - configFile = "env.toml" - } - framework.L.Info().Str("Config", configFile).Msg("Creating development environment") - _ = os.Setenv("CTF_CONFIGS", configFile) - _ = os.Setenv("TESTCONTAINERS_RYUK_DISABLED", "true") - _, err := devenv.NewEnvironment() - if err != nil { - return err - } - return nil - }, + Use: "up", + Aliases: []string{"u"}, + Short: "Spin up the development environment", + Args: cobra.RangeArgs(0, 1), + RunE: func(cmd *cobra.Command, args []string) error { + var configFile string + if len(args) > 0 { + configFile = args[0] + } else { + configFile = "env.toml" + } + framework.L.Info().Str("Config", configFile).Msg("Creating development environment") + _ = os.Setenv("CTF_CONFIGS", configFile) + _ = os.Setenv("TESTCONTAINERS_RYUK_DISABLED", "true") + _, err := devenv.NewEnvironment() + if err != nil { + return err + } + return nil + }, } var downCmd = &cobra.Command{ - Use: "down", - Aliases: []string{"d"}, - Short: "Tear down the development environment", - RunE: func(cmd *cobra.Command, args []string) error { - framework.L.Info().Msg("Tearing down the development environment") - err := framework.RemoveTestContainers() - if err != nil { - return fmt.Errorf("failed to clean Docker resources: %w", err) - } - return nil - }, + Use: "down", + Aliases: []string{"d"}, + Short: "Tear down the development environment", + RunE: func(cmd *cobra.Command, args []string) error { + framework.L.Info().Msg("Tearing down the development environment") + err := framework.RemoveTestContainers() + if err != nil { + return fmt.Errorf("failed to clean Docker resources: %w", err) + } + return nil + }, } var bsCmd = &cobra.Command{ - Use: "bs", - Short: "Manage the Blockscout EVM block explorer", - Long: "Spin up or down the Blockscout EVM block explorer", + Use: "bs", + Short: "Manage the Blockscout EVM block explorer", + Long: "Spin up or down the Blockscout EVM block explorer", } var bsUpCmd = &cobra.Command{ - Use: "up", - Aliases: []string{"u"}, - Short: "Spin up Blockscout EVM block explorer", - RunE: func(cmd *cobra.Command, args []string) error { - url, _ := bsCmd.Flags().GetString("url") - chainID, _ := bsCmd.Flags().GetString("chain-id") - return framework.BlockScoutUp(url, chainID) - }, + Use: "up", + Aliases: []string{"u"}, + Short: "Spin up Blockscout EVM block explorer", + RunE: func(cmd *cobra.Command, args []string) error { + url, _ := bsCmd.Flags().GetString("url") + chainID, _ := bsCmd.Flags().GetString("chain-id") + return framework.BlockScoutUp(url, chainID) + }, } var bsDownCmd = &cobra.Command{ - Use: "down", - Aliases: []string{"d"}, - Short: "Spin down Blockscout EVM block explorer", - RunE: func(cmd *cobra.Command, args []string) error { - url, _ := bsCmd.Flags().GetString("url") - return framework.BlockScoutDown(url) - }, + Use: "down", + Aliases: []string{"d"}, + Short: "Spin down Blockscout EVM block explorer", + RunE: func(cmd *cobra.Command, args []string) error { + url, _ := bsCmd.Flags().GetString("url") + return framework.BlockScoutDown(url) + }, } var bsRestartCmd = &cobra.Command{ - Use: "restart", - Aliases: []string{"r"}, - Short: "Restart the Blockscout EVM block explorer", - RunE: func(cmd *cobra.Command, args []string) error { - url, _ := bsCmd.Flags().GetString("url") - chainID, _ := bsCmd.Flags().GetString("chain-id") - if err := framework.BlockScoutDown(url); err != nil { - return err - } - return framework.BlockScoutUp(url, chainID) - }, + Use: "restart", + Aliases: []string{"r"}, + Short: "Restart the Blockscout EVM block explorer", + RunE: func(cmd *cobra.Command, args []string) error { + url, _ := bsCmd.Flags().GetString("url") + chainID, _ := bsCmd.Flags().GetString("chain-id") + if err := framework.BlockScoutDown(url); err != nil { + return err + } + return framework.BlockScoutUp(url, chainID) + }, } var obsCmd = &cobra.Command{ - Use: "obs", - Short: "Manage the observability stack", - Long: "Spin up or down the observability stack with subcommands 'up' and 'down'", + Use: "obs", + Short: "Manage the observability stack", + Long: "Spin up or down the observability stack with subcommands 'up' and 'down'", } var obsUpCmd = &cobra.Command{ - Use: "up", - Aliases: []string{"u"}, - Short: "Spin up the observability stack", - RunE: func(cmd *cobra.Command, args []string) error { - full, _ := cmd.Flags().GetBool("full") - var err error - if full { - err = framework.ObservabilityUpFull() - } else { - err = framework.ObservabilityUp() - } - if err != nil { - return fmt.Errorf("observability up failed: %w", err) - } - devenv.Plog.Info().Msgf("{{ .ProductName }} Dashboard: %s", Local{{ .ProductName }}Dashboard) - devenv.Plog.Info().Msgf("{{ .ProductName }} Load Test Dashboard: %s", LocalWASPLoadDashboard) - return nil - }, + Use: "up", + Aliases: []string{"u"}, + Short: "Spin up the observability stack", + RunE: func(cmd *cobra.Command, args []string) error { + full, _ := cmd.Flags().GetBool("full") + var err error + if full { + err = framework.ObservabilityUpFull() + } else { + err = framework.ObservabilityUp() + } + if err != nil { + return fmt.Errorf("observability up failed: %w", err) + } + devenv.Plog.Info().Msgf("{{ .ProductName }} Dashboard: %s", Local{{ .ProductName }}Dashboard) + devenv.Plog.Info().Msgf("{{ .ProductName }} Load Test Dashboard: %s", LocalWASPLoadDashboard) + return nil + }, } var obsDownCmd = &cobra.Command{ - Use: "down", - Aliases: []string{"d"}, - Short: "Spin down the observability stack", - RunE: func(cmd *cobra.Command, args []string) error { - return framework.ObservabilityDown() - }, + Use: "down", + Aliases: []string{"d"}, + Short: "Spin down the observability stack", + RunE: func(cmd *cobra.Command, args []string) error { + return framework.ObservabilityDown() + }, } var obsRestartCmd = &cobra.Command{ - Use: "restart", - Aliases: []string{"r"}, - Short: "Restart the observability stack (data wipe)", - RunE: func(cmd *cobra.Command, args []string) error { - if err := framework.ObservabilityDown(); err != nil { - return fmt.Errorf("observability down failed: %w", err) - } - full, _ := cmd.Flags().GetBool("full") - var err error - if full { - err = framework.ObservabilityUpFull() - } else { - err = framework.ObservabilityUp() - } - if err != nil { - return fmt.Errorf("observability up failed: %w", err) - } - devenv.Plog.Info().Msgf("{{ .ProductName }} Dashboard: %s", Local{{ .ProductName }}Dashboard) - devenv.Plog.Info().Msgf("{{ .ProductName }} Load Test Dashboard: %s", LocalWASPLoadDashboard) - return nil - }, + Use: "restart", + Aliases: []string{"r"}, + Short: "Restart the observability stack (data wipe)", + RunE: func(cmd *cobra.Command, args []string) error { + if err := framework.ObservabilityDown(); err != nil { + return fmt.Errorf("observability down failed: %w", err) + } + full, _ := cmd.Flags().GetBool("full") + var err error + if full { + err = framework.ObservabilityUpFull() + } else { + err = framework.ObservabilityUp() + } + if err != nil { + return fmt.Errorf("observability up failed: %w", err) + } + devenv.Plog.Info().Msgf("{{ .ProductName }} Dashboard: %s", Local{{ .ProductName }}Dashboard) + devenv.Plog.Info().Msgf("{{ .ProductName }} Load Test Dashboard: %s", LocalWASPLoadDashboard) + return nil + }, } var testCmd = &cobra.Command{ - Use: "test", - Aliases: []string{"t"}, - Short: "Run the tests", - RunE: func(cmd *cobra.Command, args []string) error { - if len(args) != 1 { - return fmt.Errorf("specify the test suite: smoke or load") - } - var testPattern string - switch args[0] { - case "smoke": - testPattern = "TestSmoke" - case "load": - testPattern = "TestLoadChaos" - default: - return fmt.Errorf("test suite %s is unknown, choose between smoke or load", args[0]) - } + Use: "test", + Aliases: []string{"t"}, + Short: "Run the tests", + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) != 1 { + return fmt.Errorf("specify the test suite: smoke or load") + } + var testPattern string + switch args[0] { + case "smoke": + testPattern = "TestSmoke" + case "load": + testPattern = "TestLoadChaos" + default: + return fmt.Errorf("test suite %s is unknown, choose between smoke or load", args[0]) + } + + testCmd := exec.Command("go", "test", "-v", "-run", testPattern) + testCmd.Dir = "./tests" + testCmd.Stdout = os.Stdout + testCmd.Stderr = os.Stderr + testCmd.Stdin = os.Stdin - testCmd := exec.Command("go", "test", "-v", "-run", testPattern) - testCmd.Dir = "./tests" - testCmd.Stdout = os.Stdout - testCmd.Stderr = os.Stderr - testCmd.Stdin = os.Stdin - - if err := testCmd.Run(); err != nil { - if exitError, ok := err.(*exec.ExitError); ok { - if status, ok := exitError.Sys().(syscall.WaitStatus); ok { - os.Exit(status.ExitStatus()) - } - os.Exit(1) + if err := testCmd.Run(); err != nil { + if exitError, ok := err.(*exec.ExitError); ok { + if status, ok := exitError.Sys().(syscall.WaitStatus); ok { + os.Exit(status.ExitStatus()) } - return fmt.Errorf("failed to run test command: %w", err) + os.Exit(1) } - return nil - }, + return fmt.Errorf("failed to run test command: %w", err) + } + return nil + }, } func init() { - rootCmd.PersistentFlags().BoolP("debug", "d", false, "Enable running services with dlv to allow remote debugging.") + rootCmd.PersistentFlags().BoolP("debug", "d", false, "Enable running services with dlv to allow remote debugging.") - rootCmd.AddCommand(testCmd) + rootCmd.AddCommand(testCmd) - // Blockscout, on-chain debug - bsCmd.PersistentFlags().StringP("url", "u", "http://host.docker.internal:8555", "EVM RPC node URL (default to dst chain on 8555") - bsCmd.PersistentFlags().StringP("chain-id", "c", "2337", "RPC's Chain ID") - bsCmd.AddCommand(bsUpCmd) - bsCmd.AddCommand(bsDownCmd) - bsCmd.AddCommand(bsRestartCmd) - rootCmd.AddCommand(bsCmd) + // Blockscout, on-chain debug + bsCmd.PersistentFlags().StringP("url", "u", "http://host.docker.internal:8555", "EVM RPC node URL (default to dst chain on 8555") + bsCmd.PersistentFlags().StringP("chain-id", "c", "2337", "RPC's Chain ID") + bsCmd.AddCommand(bsUpCmd) + bsCmd.AddCommand(bsDownCmd) + bsCmd.AddCommand(bsRestartCmd) + rootCmd.AddCommand(bsCmd) - // observability - obsCmd.PersistentFlags().BoolP("full", "f", false, "Enable full observability stack with additional components") - obsCmd.AddCommand(obsRestartCmd) - obsCmd.AddCommand(obsUpCmd) - obsCmd.AddCommand(obsDownCmd) - rootCmd.AddCommand(obsCmd) + // observability + obsCmd.PersistentFlags().BoolP("full", "f", false, "Enable full observability stack with additional components") + obsCmd.AddCommand(obsRestartCmd) + obsCmd.AddCommand(obsUpCmd) + obsCmd.AddCommand(obsDownCmd) + rootCmd.AddCommand(obsCmd) - // main env commands - rootCmd.AddCommand(upCmd) - rootCmd.AddCommand(restartCmd) - rootCmd.AddCommand(downCmd) + // main env commands + rootCmd.AddCommand(upCmd) + rootCmd.AddCommand(restartCmd) + rootCmd.AddCommand(downCmd) } func checkDockerIsRunning() { - cli, err := client.NewClientWithOpts(client.FromEnv) - if err != nil { - fmt.Println("Can't create Docker client, please check if Docker daemon is running!") - os.Exit(1) - } - defer cli.Close() - _, err = cli.Ping(context.Background()) - if err != nil { - fmt.Println("Docker is not running, please start Docker daemon first!") - os.Exit(1) - } + cli, err := client.NewClientWithOpts(client.FromEnv) + if err != nil { + fmt.Println("Can't create Docker client, please check if Docker daemon is running!") + os.Exit(1) + } + defer cli.Close() + _, err = cli.Ping(context.Background()) + if err != nil { + fmt.Println("Docker is not running, please start Docker daemon first!") + os.Exit(1) + } } func main() { - checkDockerIsRunning() - if len(os.Args) == 2 && (os.Args[1] == "shell" || os.Args[1] == "sh") { - _ = os.Setenv("CTF_CONFIGS", "env.toml") // Set default config for shell + checkDockerIsRunning() + if len(os.Args) == 2 && (os.Args[1] == "shell" || os.Args[1] == "sh") { + _ = os.Setenv("CTF_CONFIGS", "env.toml") // Set default config for shell - StartShell() - return - } - if err := rootCmd.Execute(); err != nil { - devenv.Plog.Err(err).Send() - os.Exit(1) - } + StartShell() + return + } + if err := rootCmd.Execute(); err != nil { + devenv.Plog.Err(err).Send() + os.Exit(1) + } }` // LoadTestTmpl is a load/chaos test template LoadTestTmpl = `package devenv_test import ( - "testing" - "time" + "testing" + "time" - "github.com/go-resty/resty/v2" - "github.com/stretchr/testify/require" + "github.com/go-resty/resty/v2" + "github.com/stretchr/testify/require" - de "{{ .GoModName }}" + de "{{ .GoModName }}" - "github.com/smartcontractkit/chainlink-testing-framework/framework/chaos" - "github.com/smartcontractkit/chainlink-testing-framework/framework/clclient" - "github.com/smartcontractkit/chainlink-testing-framework/wasp" + "github.com/smartcontractkit/chainlink-testing-framework/framework/chaos" + "github.com/smartcontractkit/chainlink-testing-framework/framework/clclient" + "github.com/smartcontractkit/chainlink-testing-framework/wasp" ) type ExampleGun struct { - target string - client *resty.Client - Data []string + target string + client *resty.Client + Data []string } func NewExampleHTTPGun(target string) *ExampleGun { - return &ExampleGun{ - client: resty.New(), - target: target, - Data: make([]string, 0), - } + return &ExampleGun{ + client: resty.New(), + target: target, + Data: make([]string, 0), + } } // Call implements example gun call, assertions on response bodies should be done here func (m *ExampleGun) Call(l *wasp.Generator) *wasp.Response { - var result map[string]any - r, err := m.client.R(). - SetResult(&result). - Get(m.target) - if err != nil { - return &wasp.Response{Data: result, Error: err.Error()} - } - if r.Status() != "200 OK" { - return &wasp.Response{Data: result, Error: "not 200", Failed: true} - } - return &wasp.Response{Data: result} + var result map[string]any + r, err := m.client.R(). + SetResult(&result). + Get(m.target) + if err != nil { + return &wasp.Response{Data: result, Error: err.Error()} + } + if r.Status() != "200 OK" { + return &wasp.Response{Data: result, Error: "not 200", Failed: true} + } + return &wasp.Response{Data: result} } func TestLoadChaos(t *testing.T) { - in, err := de.LoadOutput[de.Cfg]("../env-out.toml") - require.NoError(t, err) - - clNodes, err := clclient.New(in.NodeSets[0].Out.CLNodes) - require.NoError(t, err) - - // use local observability stack for Docker load/chaos tests - t.Setenv("LOKI_URL", "http://localhost:3030/loki/api/v1/push") - - // define labels for differentiate one run from another - labels := map[string]string{ - "go_test_name": "generator_healthcheck", - "gen_name": "generator_healthcheck", - "branch": "test", - "commit": "test", - } - - // create a WASP generator - gen, err := wasp.NewGenerator(&wasp.Config{ - LoadType: wasp.RPS, - T: t, - // just use plain line profile - 1 RPS for 60s - Schedule: wasp.Plain(1, 60*time.Second), - Gun: NewExampleHTTPGun("https://example.com"), - Labels: labels, - LokiConfig: wasp.NewEnvLokiConfig(), - }) - require.NoError(t, err) - gen.Run(false) - - // define chaos test cases - testCases := []struct { - name string - command string - wait time.Duration - validate func(c []*clclient.ChainlinkClient) error - }{ - { - name: "Reboot the pods", - wait: 1 * time.Minute, - command: "stop --duration=20s --restart re2:don-node0", - validate: func(c []*clclient.ChainlinkClient) error { - _, _, err := c[0].ReadBridges() - return err - }, + in, err := de.LoadOutput[de.Cfg]("../env-out.toml") + require.NoError(t, err) + + clNodes, err := clclient.New(in.NodeSets[0].Out.CLNodes) + require.NoError(t, err) + + // use local observability stack for Docker load/chaos tests + t.Setenv("LOKI_URL", "http://localhost:3030/loki/api/v1/push") + + // define labels for differentiate one run from another + labels := map[string]string{ + "go_test_name": "generator_healthcheck", + "gen_name": "generator_healthcheck", + "branch": "test", + "commit": "test", + } + + // create a WASP generator + gen, err := wasp.NewGenerator(&wasp.Config{ + LoadType: wasp.RPS, + T: t, + // just use plain line profile - 1 RPS for 60s + Schedule: wasp.Plain(1, 60*time.Second), + Gun: NewExampleHTTPGun("https://example.com"), + Labels: labels, + LokiConfig: wasp.NewEnvLokiConfig(), + }) + require.NoError(t, err) + gen.Run(false) + + // define chaos test cases + testCases := []struct { + name string + command string + wait time.Duration + validate func(c []*clclient.ChainlinkClient) error + }{ + { + name: "Reboot the pods", + wait: 20 * time.Second, + command: "stop --duration=20s --restart re2:don-node0", + validate: func(c []*clclient.ChainlinkClient) error { + _, _, err := c[0].ReadBridges() + return err }, - { - name: "Introduce network delay", - wait: 1 * time.Minute, - command: "netem --tc-image=gaiadocker/iproute2 --duration=1m delay --time=1000 re2:don-node.*", - validate: func(c []*clclient.ChainlinkClient) error { - _, _, err := c[0].ReadBridges() - return err - }, + }, + { + name: "Introduce network delay", + wait: 20 * time.Second, + command: "netem --tc-image=gaiadocker/iproute2 --duration=20s delay --time=1000 re2:don-node.*", + validate: func(c []*clclient.ChainlinkClient) error { + _, _, err := c[0].ReadBridges() + return err }, - } + }, + } - // Run chaos test cases - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - t.Log(tc.name) - _, err = chaos.ExecPumba(tc.command, tc.wait) - require.NoError(t, err) - err = tc.validate(clNodes) - require.NoError(t, err) - }) - } - // wait for the workload to finish - _, failed := gen.Wait() - require.False(t, failed) + // Run chaos test cases + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Log(tc.name) + _, err = chaos.ExecPumba(tc.command, tc.wait) + require.NoError(t, err) + err = tc.validate(clNodes) + require.NoError(t, err) + }) + } + // wait for the workload to finish + _, failed := gen.Wait() + require.False(t, failed) } ` // SmokeTestTmpl is a smoke test template SmokeTestTmpl = `package devenv_test import ( - "fmt" - "strconv" - "testing" - "time" + "fmt" + "strconv" + "testing" + "time" - de "{{ .GoModName }}" + de "{{ .GoModName }}" - "github.com/smartcontractkit/chainlink-testing-framework/framework/clclient" - "github.com/stretchr/testify/require" + "github.com/smartcontractkit/chainlink-testing-framework/framework/clclient" + "github.com/stretchr/testify/require" - f "github.com/smartcontractkit/chainlink-testing-framework/framework" + f "github.com/smartcontractkit/chainlink-testing-framework/framework" ) var L = de.Plog func TestSmoke(t *testing.T) { - in, err := de.LoadOutput[de.Cfg]("../env-out.toml") - require.NoError(t, err) - c, _, _, err := de.ETHClient(in) - require.NoError(t, err) - clNodes, err := clclient.New(in.NodeSets[0].Out.CLNodes) - require.NoError(t, err) - - tests := []struct { - name string - clNodes []*clclient.ChainlinkClient - }{ - { - name: "feature_1", - }, - { - name: "feature_2", - }, - } + in, err := de.LoadOutput[de.Cfg]("../env-out.toml") + require.NoError(t, err) + c, _, _, err := de.ETHClient(in) + require.NoError(t, err) + clNodes, err := clclient.New(in.NodeSets[0].Out.CLNodes) + require.NoError(t, err) + + tests := []struct { + name string + clNodes []*clclient.ChainlinkClient + }{ + { + name: "feature_1", + }, + { + name: "feature_2", + }, + } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - _, _ = c, clNodes - }) - } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, _ = c, clNodes + }) + } } // assertResources is a simple assertion on resources if you run with the observability stack (obs up) func assertResources(t *testing.T, in *de.Cfg, start, end time.Time) { - pc := f.NewPrometheusQueryClient(f.LocalPrometheusBaseURL) - // no more than 10% CPU for this test - maxCPU := 10.0 - cpuResp, err := pc.Query("sum(rate(container_cpu_usage_seconds_total{name=~\".*don.*\"}[5m])) by (name) *100", end) + pc := f.NewPrometheusQueryClient(f.LocalPrometheusBaseURL) + // no more than 10% CPU for this test + maxCPU := 10.0 + cpuResp, err := pc.Query("sum(rate(container_cpu_usage_seconds_total{name=~\".*don.*\"}[5m])) by (name) *100", end) + require.NoError(t, err) + cpu := f.ToLabelsMap(cpuResp) + for i := 0; i < in.NodeSets[0].Nodes; i++ { + nodeLabel := fmt.Sprintf("name:don-node%d", i) + nodeCpu, err := strconv.ParseFloat(cpu[nodeLabel][0].(string), 64) + L.Info().Int("Node", i).Float64("CPU", nodeCpu).Msg("CPU usage percentage") require.NoError(t, err) - cpu := f.ToLabelsMap(cpuResp) - for i := 0; i < in.NodeSets[0].Nodes; i++ { - nodeLabel := fmt.Sprintf("name:don-node%d", i) - nodeCpu, err := strconv.ParseFloat(cpu[nodeLabel][0].(string), 64) - L.Info().Int("Node", i).Float64("CPU", nodeCpu).Msg("CPU usage percentage") - require.NoError(t, err) - require.LessOrEqual(t, nodeCpu, maxCPU) - } - // no more than 200mb for this test - maxMem := int(200e6) // 200mb - memoryResp, err := pc.Query("sum(container_memory_rss{name=~\".*don.*\"}) by (name)", end) + require.LessOrEqual(t, nodeCpu, maxCPU) + } + // no more than 200mb for this test + maxMem := int(200e6) // 200mb + memoryResp, err := pc.Query("sum(container_memory_rss{name=~\".*don.*\"}) by (name)", end) + require.NoError(t, err) + mem := f.ToLabelsMap(memoryResp) + for i := 0; i < in.NodeSets[0].Nodes; i++ { + nodeLabel := fmt.Sprintf("name:don-node%d", i) + nodeMem, err := strconv.Atoi(mem[nodeLabel][0].(string)) + L.Info().Int("Node", i).Int("Memory", nodeMem).Msg("Total memory") require.NoError(t, err) - mem := f.ToLabelsMap(memoryResp) - for i := 0; i < in.NodeSets[0].Nodes; i++ { - nodeLabel := fmt.Sprintf("name:don-node%d", i) - nodeMem, err := strconv.Atoi(mem[nodeLabel][0].(string)) - L.Info().Int("Node", i).Int("Memory", nodeMem).Msg("Total memory") - require.NoError(t, err) - require.LessOrEqual(t, nodeMem, maxMem) - } + require.LessOrEqual(t, nodeMem, maxMem) + } } ` // CLDFTmpl is a Chainlink Deployments Framework template CLDFTmpl = `package {{ .PackageName }} import ( - "context" - "crypto/ecdsa" - "errors" - "fmt" - "math/big" - "strings" - "time" - - "github.com/Masterminds/semver/v3" - "github.com/ethereum/go-ethereum/accounts/abi/bind" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/crypto" - "github.com/ethereum/go-ethereum/ethclient" - "github.com/smartcontractkit/chainlink-common/pkg/logger" - "github.com/smartcontractkit/chainlink-deployments-framework/datastore" - "github.com/smartcontractkit/chainlink-deployments-framework/operations" - "github.com/smartcontractkit/chainlink-evm/gethwrappers/shared/generated/link_token" - "github.com/smartcontractkit/chainlink-testing-framework/framework/components/blockchain" - "go.uber.org/zap" - "golang.org/x/sync/errgroup" - "google.golang.org/grpc" - "google.golang.org/grpc/credentials/insecure" - - chainsel "github.com/smartcontractkit/chain-selectors" - cldfchain "github.com/smartcontractkit/chainlink-deployments-framework/chain" - cldfevm "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm" - cldfevmprovider "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm/provider" - cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" - csav1 "github.com/smartcontractkit/chainlink-protos/job-distributor/v1/csa" - jobv1 "github.com/smartcontractkit/chainlink-protos/job-distributor/v1/job" - nodev1 "github.com/smartcontractkit/chainlink-protos/job-distributor/v1/node" + "context" + "crypto/ecdsa" + "errors" + "fmt" + "math/big" + "strings" + "time" + + "github.com/Masterminds/semver/v3" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/ethclient" + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-deployments-framework/datastore" + "github.com/smartcontractkit/chainlink-deployments-framework/operations" + "github.com/smartcontractkit/chainlink-evm/gethwrappers/shared/generated/link_token" + "github.com/smartcontractkit/chainlink-testing-framework/framework/components/blockchain" + "go.uber.org/zap" + "golang.org/x/sync/errgroup" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + + chainsel "github.com/smartcontractkit/chain-selectors" + cldfchain "github.com/smartcontractkit/chainlink-deployments-framework/chain" + cldfevm "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm" + cldfevmprovider "github.com/smartcontractkit/chainlink-deployments-framework/chain/evm/provider" + cldf "github.com/smartcontractkit/chainlink-deployments-framework/deployment" + csav1 "github.com/smartcontractkit/chainlink-protos/job-distributor/v1/csa" + jobv1 "github.com/smartcontractkit/chainlink-protos/job-distributor/v1/job" + nodev1 "github.com/smartcontractkit/chainlink-protos/job-distributor/v1/node" ) const ( - AnvilKey0 = "ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" - DefaultNativeTransferGasPrice = 21000 + AnvilKey0 = "ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" + DefaultNativeTransferGasPrice = 21000 ) const LinkToken cldf.ContractType = "LinkToken" @@ -1222,23 +2083,23 @@ func MultiplyEIP1559GasPrices(client *ethclient.Client, fcMult, tcMult int64) (* DebugToolsTmpl = `package {{ .PackageName }} import ( - "os" - "runtime/trace" + "os" + "runtime/trace" ) func tracing() func() { - f, err := os.Create("trace.out") - if err != nil { - panic("can't create trace.out file") - } - if err := trace.Start(f); err != nil { - panic("can't start tracing") - } - return func() { - trace.Stop() - f.Close() - } + f, err := os.Create("trace.out") + if err != nil { + panic("can't create trace.out file") + } + if err := trace.Start(f); err != nil { + panic("can't start tracing") + } + return func() { + trace.Stop() + f.Close() + } } ` // ConfigTmpl is a template for reading and writing devenv configuration (env.toml, env-out.toml) @@ -1257,27 +2118,27 @@ GetNetworkPrivateKey is used to get your network private key from the env var we */ import ( - "errors" - "fmt" - "os" - "path/filepath" - "strings" + "errors" + "fmt" + "os" + "path/filepath" + "strings" - "github.com/davecgh/go-spew/spew" - "github.com/pelletier/go-toml/v2" - "github.com/rs/zerolog" - "github.com/rs/zerolog/log" + "github.com/davecgh/go-spew/spew" + "github.com/pelletier/go-toml/v2" + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" ) const ( - // DefaultConfigDir is the default directory we are expecting TOML config to be. - DefaultConfigDir = "." - // EnvVarTestConfigs is the environment variable name to read config paths from, ex.: CTF_CONFIGS=env.toml,overrides.toml. - EnvVarTestConfigs = "CTF_CONFIGS" - // DefaultOverridesFilePath is the default overrides.toml file path. - DefaultOverridesFilePath = "overrides.toml" - // DefaultAnvilKey is a default, well-known Anvil first key - DefaultAnvilKey = "ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" + // DefaultConfigDir is the default directory we are expecting TOML config to be. + DefaultConfigDir = "." + // EnvVarTestConfigs is the environment variable name to read config paths from, ex.: CTF_CONFIGS=env.toml,overrides.toml. + EnvVarTestConfigs = "CTF_CONFIGS" + // DefaultOverridesFilePath is the default overrides.toml file path. + DefaultOverridesFilePath = "overrides.toml" + // DefaultAnvilKey is a default, well-known Anvil first key + DefaultAnvilKey = "ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" ) var L = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}).Level(zerolog.InfoLevel) @@ -1285,86 +2146,86 @@ var L = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}).Level(zerolog.InfoLeve // Load loads TOML configurations from environment variable, ex.: CTF_CONFIGS=env.toml,overrides.toml // and unmarshalls the files from left to right overriding keys. func Load[T any]() (*T, error) { - var config T - paths := strings.Split(os.Getenv(EnvVarTestConfigs), ",") - for _, path := range paths { - L.Info().Str("Path", path).Msg("Loading configuration input") - data, err := os.ReadFile(filepath.Join(DefaultConfigDir, path)) //nolint:gosec - if err != nil { - if path == DefaultOverridesFilePath { - L.Info().Str("Path", path).Msg("Overrides file not found or empty") - continue - } - return nil, fmt.Errorf("error reading config file %s: %w", path, err) - } - if L.GetLevel() == zerolog.TraceLevel { - fmt.Println(string(data)) //nolint:forbidigo + var config T + paths := strings.Split(os.Getenv(EnvVarTestConfigs), ",") + for _, path := range paths { + L.Info().Str("Path", path).Msg("Loading configuration input") + data, err := os.ReadFile(filepath.Join(DefaultConfigDir, path)) //nolint:gosec + if err != nil { + if path == DefaultOverridesFilePath { + L.Info().Str("Path", path).Msg("Overrides file not found or empty") + continue } + return nil, fmt.Errorf("error reading config file %s: %w", path, err) + } + if L.GetLevel() == zerolog.TraceLevel { + fmt.Println(string(data)) //nolint:forbidigo + } - decoder := toml.NewDecoder(strings.NewReader(string(data))) - decoder.DisallowUnknownFields() + decoder := toml.NewDecoder(strings.NewReader(string(data))) + decoder.DisallowUnknownFields() - if err := decoder.Decode(&config); err != nil { - var details *toml.StrictMissingError - if errors.As(err, &details) { - fmt.Println(details.String()) //nolint:forbidigo - } - return nil, fmt.Errorf("failed to decode TOML config, strict mode: %s", err) + if err := decoder.Decode(&config); err != nil { + var details *toml.StrictMissingError + if errors.As(err, &details) { + fmt.Println(details.String()) //nolint:forbidigo } + return nil, fmt.Errorf("failed to decode TOML config, strict mode: %s", err) } - if L.GetLevel() == zerolog.TraceLevel { - L.Trace().Msg("Merged inputs") - spew.Dump(config) //nolint:forbidigo - } - return &config, nil + } + if L.GetLevel() == zerolog.TraceLevel { + L.Trace().Msg("Merged inputs") + spew.Dump(config) //nolint:forbidigo + } + return &config, nil } // Store writes config to a file, adds -cache.toml suffix if it's an initial configuration. func Store[T any](cfg *T) error { - baseConfigPath, err := BaseConfigPath() - if err != nil { - return err - } - newCacheName := strings.ReplaceAll(baseConfigPath, ".toml", "") - var outCacheName string - if strings.Contains(newCacheName, "cache") { - L.Info().Str("Cache", baseConfigPath).Msg("Cache file already exists, overriding") - outCacheName = baseConfigPath - } else { - outCacheName = fmt.Sprintf("%s-out.toml", strings.ReplaceAll(baseConfigPath, ".toml", "")) - } - L.Info().Str("OutputFile", outCacheName).Msg("Storing configuration output") - d, err := toml.Marshal(cfg) - if err != nil { - return err - } - return os.WriteFile(filepath.Join(DefaultConfigDir, outCacheName), d, 0o600) + baseConfigPath, err := BaseConfigPath() + if err != nil { + return err + } + newCacheName := strings.ReplaceAll(baseConfigPath, ".toml", "") + var outCacheName string + if strings.Contains(newCacheName, "cache") { + L.Info().Str("Cache", baseConfigPath).Msg("Cache file already exists, overriding") + outCacheName = baseConfigPath + } else { + outCacheName = fmt.Sprintf("%s-out.toml", strings.ReplaceAll(baseConfigPath, ".toml", "")) + } + L.Info().Str("OutputFile", outCacheName).Msg("Storing configuration output") + d, err := toml.Marshal(cfg) + if err != nil { + return err + } + return os.WriteFile(filepath.Join(DefaultConfigDir, outCacheName), d, 0o600) } // LoadOutput loads config output file from path. func LoadOutput[T any](path string) (*T, error) { - _ = os.Setenv(EnvVarTestConfigs, path) - return Load[T]() + _ = os.Setenv(EnvVarTestConfigs, path) + return Load[T]() } // BaseConfigPath returns base config path, ex. env.toml,overrides.toml -> env.toml. func BaseConfigPath() (string, error) { - configs := os.Getenv(EnvVarTestConfigs) - if configs == "" { - return "", fmt.Errorf("no %s env var is provided, you should provide at least one test config in TOML", EnvVarTestConfigs) - } - L.Debug().Str("Configs", configs).Msg("Getting base config path") - return strings.Split(configs, ",")[0], nil + configs := os.Getenv(EnvVarTestConfigs) + if configs == "" { + return "", fmt.Errorf("no %s env var is provided, you should provide at least one test config in TOML", EnvVarTestConfigs) + } + L.Debug().Str("Configs", configs).Msg("Getting base config path") + return strings.Split(configs, ",")[0], nil } // GetNetworkPrivateKey gets network private key or fallback to default simulator key (Anvil's first key) func GetNetworkPrivateKey() string { - pk := os.Getenv("PRIVATE_KEY") - if pk == "" { - // that's the first Anvil and Geth private key, serves as a fallback for local testing if not overridden - return DefaultAnvilKey - } - return pk + pk := os.Getenv("PRIVATE_KEY") + if pk == "" { + // that's the first Anvil and Geth private key, serves as a fallback for local testing if not overridden + return DefaultAnvilKey + } + return pk } ` // EnvironmentTmpl is an environment.go template - main file for environment composition @@ -1388,31 +2249,31 @@ type Cfg struct { } func NewEnvironment() (*Cfg, error) { - endTracing := tracing() - defer endTracing() + endTracing := tracing() + defer endTracing() - if err := framework.DefaultNetwork(nil); err != nil { - return nil, err - } - in, err := Load[Cfg]() - if err != nil { - return nil, fmt.Errorf("failed to load configuration: %w", err) - } - _, err = blockchain.NewBlockchainNetwork(in.Blockchains[0]) - if err != nil { - return nil, fmt.Errorf("failed to create blockchain network 1337: %w", err) - } - if err := DefaultProductConfiguration(in, ConfigureNodesNetwork); err != nil { - return nil, fmt.Errorf("failed to setup default CLDF orchestration: %w", err) - } - _, err = ns.NewSharedDBNodeSet(in.NodeSets[0], nil) - if err != nil { - return nil, fmt.Errorf("failed to create new shared db node set: %w", err) - } - if err := DefaultProductConfiguration(in, ConfigureProductContractsJobs); err != nil { - return nil, fmt.Errorf("failed to setup default CLDF orchestration: %w", err) - } - return in, Store[Cfg](in) + if err := framework.DefaultNetwork(nil); err != nil { + return nil, err + } + in, err := Load[Cfg]() + if err != nil { + return nil, fmt.Errorf("failed to load configuration: %w", err) + } + _, err = blockchain.NewBlockchainNetwork(in.Blockchains[0]) + if err != nil { + return nil, fmt.Errorf("failed to create blockchain network 1337: %w", err) + } + if err := DefaultProductConfiguration(in, ConfigureNodesNetwork); err != nil { + return nil, fmt.Errorf("failed to setup default CLDF orchestration: %w", err) + } + _, err = ns.NewSharedDBNodeSet(in.NodeSets[0], nil) + if err != nil { + return nil, fmt.Errorf("failed to create new shared db node set: %w", err) + } + if err := DefaultProductConfiguration(in, ConfigureProductContractsJobs); err != nil { + return nil, fmt.Errorf("failed to setup default CLDF orchestration: %w", err) + } + return in, Store[Cfg](in) } ` // SingleNetworkProductConfigurationTmpl is an single-network EVM product configuration template @@ -1442,14 +2303,14 @@ const ( var Plog = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}).Level(zerolog.DebugLevel).With().Fields(map[string]any{"component": "on_chain"}).Logger() type OnChain struct { - LinkContractAddress string ` + "`" + `toml:"link_contract_address"` + "`" + ` - CLNodesFundingETH float64 ` + "`" + `toml:"cl_nodes_funding_eth"` + "`" + ` - CLNodesFundingLink float64 ` + "`" + `toml:"cl_nodes_funding_link"` + "`" + ` - VerificationTimeoutSec time.Duration ` + "`" + `toml:"verification_timeout_sec"` + "`" + ` - ContractsConfigurationTimeoutSec time.Duration ` + "`" + `toml:"contracts_configuration_timeout_sec"` + "`" + ` - GasSettings *GasSettings ` + "`" + `toml:"gas_settings"` + "`" + ` - Verify bool ` + "`" + `toml:"verify"` + "`" + ` - DeployedContracts *DeployedContracts ` + "`" + `toml:"deployed_contracts"` + "`" + ` + LinkContractAddress string ` + "`" + `toml:"link_contract_address"` + "`" + ` + CLNodesFundingETH float64 ` + "`" + `toml:"cl_nodes_funding_eth"` + "`" + ` + CLNodesFundingLink float64 ` + "`" + `toml:"cl_nodes_funding_link"` + "`" + ` + VerificationTimeoutSec time.Duration ` + "`" + `toml:"verification_timeout_sec"` + "`" + ` + ContractsConfigurationTimeoutSec time.Duration ` + "`" + `toml:"contracts_configuration_timeout_sec"` + "`" + ` + GasSettings *GasSettings ` + "`" + `toml:"gas_settings"` + "`" + ` + Verify bool ` + "`" + `toml:"verify"` + "`" + ` + DeployedContracts *DeployedContracts ` + "`" + `toml:"deployed_contracts"` + "`" + ` } type DeployedContracts struct { @@ -1458,13 +2319,13 @@ type DeployedContracts struct { type GasSettings struct { - FeeCapMultiplier int64 ` + "`" + `toml:"fee_cap_multiplier"` + "`" + ` - TipCapMultiplier int64 ` + "`" + `toml:"tip_cap_multiplier"` + "`" + ` + FeeCapMultiplier int64 ` + "`" + `toml:"fee_cap_multiplier"` + "`" + ` + TipCapMultiplier int64 ` + "`" + `toml:"tip_cap_multiplier"` + "`" + ` } type Jobs struct { - ConfigPollIntervalSeconds time.Duration ` + "`" + `toml:"config_poll_interval_sec"` + "`" + ` - MaxTaskDurationSec time.Duration ` + "`" + `toml:"max_task_duration_sec"` + "`" + ` + ConfigPollIntervalSeconds time.Duration ` + "`" + `toml:"config_poll_interval_sec"` + "`" + ` + MaxTaskDurationSec time.Duration ` + "`" + `toml:"max_task_duration_sec"` + "`" + ` } type ConfigPhase int @@ -1675,7 +2536,7 @@ push-fakes: just fakes/push # Rebuild CLI -cli: +@cli: pushd cmd/{{ .CLIName }} > /dev/null && go install -ldflags="-X main.Version=1.0.0" . && popd > /dev/null` ) @@ -1691,19 +2552,40 @@ type LoadTestParams struct { GoModName string } -// GoModParams params for generating go.mod file +// CISmokeParams params for generating CI smoke tests file type CISmokeParams struct { ProductName string DevEnvRelPath string CLIName string } +// CILoadChaosParams params for generating CI load&chaos tests file +type CILoadChaosParams struct { + ProductName string + DevEnvRelPath string + CLIName string +} + // GoModParams params for generating go.mod file type GoModParams struct { ModuleName string RuntimeVersion string } +// ReadmeParams params for generating README.md file +type ReadmeParams struct { + CLIName string +} + +// GitIgnoreParams default .gitignore params +type GitIgnoreParams struct{} + +// GrafanaDashboardParams default Grafana dashboard params +type GrafanaDashboardParams struct { + ProductName string + UUID string +} + // ConfigTOMLParams default env.toml params type ConfigTOMLParams struct { PackageName string @@ -1729,6 +2611,7 @@ type CLIParams struct { CLIName string DevEnvPkgImport string ProductName string + DashboardUUID string } // CLDFParams cldf.go file params @@ -1798,7 +2681,6 @@ func NewEnvBuilder(cliName string, nodes int, productType string, productName st packageName: "devenv", outputDir: "devenv", productType: productType, - moduleName: fmt.Sprintf("github.com/smartcontractkit/%s/devenv", productName), } } @@ -1808,19 +2690,13 @@ func (g *EnvBuilder) OutputDir(dir string) *EnvBuilder { return g } -// GoModName sets the Go module name for the generated project -func (g *EnvBuilder) GoModName(name string) *EnvBuilder { - g.moduleName = name - return g -} - // validate verifier that we can build codegen with provided params // fixing input params if possible func (g *EnvBuilder) validate() error { - // to avoid issues in different contexts we only allow lower-case letters, no special symbols or numerics + // to avoid issues in different contexts (go.mod, CI, env variables) we only allow lower-case letters, no special symbols or numerics g.productName = onlyLetters(g.productName) g.cliName = onlyLetters(g.cliName) - g.moduleName = onlyLetters(g.moduleName) + g.moduleName = fmt.Sprintf("github.com/smartcontractkit/%s/devenv", g.productName) return nil } @@ -1908,7 +2784,7 @@ func (g *EnvCodegen) Write() error { } // Generate CLI file - cliContents, err := g.GenerateCLI() + cliContents, err := g.GenerateCLI(ProductDashboardUUID) if err != nil { return err } @@ -1933,6 +2809,19 @@ func (g *EnvCodegen) Write() error { return fmt.Errorf("failed to write CLI completion file: %w", err) } + // Generate README.md file + readmeContents, err := g.GenerateReadme() + if err != nil { + return err + } + if err := os.WriteFile( //nolint:gosec + filepath.Join(g.cfg.outputDir, "README.md"), + []byte(readmeContents), + os.ModePerm, + ); err != nil { + return fmt.Errorf("failed to write README.md file: %w", err) + } + // Generate tools.go toolsContents, err := g.GenerateDebugTools() if err != nil { @@ -1943,7 +2832,7 @@ func (g *EnvCodegen) Write() error { []byte(toolsContents), os.ModePerm, ); err != nil { - return fmt.Errorf("failed to write config file: %w", err) + return fmt.Errorf("failed to write tools file: %w", err) } // Generate config.go @@ -2027,13 +2916,26 @@ func (g *EnvCodegen) Write() error { return fmt.Errorf("failed to write CI smoke workflow file: %w", err) } + // Generate GitHub CI load&chaos test workflow + ciLoadChaosContents, err := g.GenerateCILoadChaos() + if err != nil { + return err + } + if err := os.WriteFile( //nolint:gosec + filepath.Join(ciDir, "devenv-load-chaos-test.yml"), + []byte(ciLoadChaosContents), + os.ModePerm, + ); err != nil { + return fmt.Errorf("failed to write CI load&chaos workflow file: %w", err) + } + // create e2e tests directory e2eDir := filepath.Join(g.cfg.outputDir, "tests") if err := os.MkdirAll( //nolint:gosec e2eDir, os.ModePerm, ); err != nil { - return fmt.Errorf("failed to create output directory: %w", err) + return fmt.Errorf("failed to create tests directory: %w", err) } // generate smoke tests @@ -2062,6 +2964,41 @@ func (g *EnvCodegen) Write() error { return fmt.Errorf("failed to write load tests file: %w", err) } + // create Grafana dashboards directory + dashboardsDir := filepath.Join(g.cfg.outputDir, "dashboards") + if err := os.MkdirAll( //nolint:gosec + dashboardsDir, + os.ModePerm, + ); err != nil { + return fmt.Errorf("failed to create dashboards directory: %w", err) + } + + // generate Grafana dashboard + grafanaDashboardContents, err := g.GenerateGrafanaDashboard(ProductDashboardUUID) + if err != nil { + return err + } + if err := os.WriteFile( //nolint:gosec + filepath.Join(dashboardsDir, "environment.json"), + []byte(grafanaDashboardContents), + os.ModePerm, + ); err != nil { + return fmt.Errorf("failed to write Grafana dashboard file: %w", err) + } + + // generate .gitignore file + gitIgnoreContents, err := g.GenerateGitIgnore() + if err != nil { + return err + } + if err := os.WriteFile( //nolint:gosec + filepath.Join(g.cfg.outputDir, ".gitignore"), + []byte(gitIgnoreContents), + os.ModePerm, + ); err != nil { + return fmt.Errorf("failed to write gitignore file: %w", err) + } + // tidy and finalize currentDir, err := os.Getwd() if err != nil { @@ -2103,6 +3040,21 @@ func (g *EnvCodegen) GenerateSmokeTests() (string, error) { return render(SmokeTestTmpl, data) } +// GenerateCILoadChaos generates a load&chaos test CI workflow +func (g *EnvCodegen) GenerateCILoadChaos() (string, error) { + log.Info().Msg("Generating GitHub CI load&chaos test") + p, err := dirPathRelFromGitRoot(g.cfg.outputDir) + if err != nil { + return "", fmt.Errorf("failed to find relative devenv path from Git root: %w", err) + } + data := CILoadChaosParams{ + DevEnvRelPath: p, + ProductName: g.cfg.productName, + CLIName: g.cfg.cliName, + } + return render(CILoadChaosTemplate, data) +} + // GenerateCISmoke generates a smoke test CI workflow func (g *EnvCodegen) GenerateCISmoke() (string, error) { log.Info().Msg("Generating GitHub CI smoke test") @@ -2128,6 +3080,32 @@ func (g *EnvCodegen) GenerateGoMod() (string, error) { return render(GoModTemplate, data) } +// GenerateReadme generates a readme file +func (g *EnvCodegen) GenerateReadme() (string, error) { + log.Info().Msg("Generating README file") + data := ReadmeParams{ + CLIName: g.cfg.cliName, + } + return render(ReadmeTmpl, data) +} + +// GenerateGitIgnore generate .gitignore file +func (g *EnvCodegen) GenerateGitIgnore() (string, error) { + log.Info().Msg("Generating .gitignore file") + p := GitIgnoreParams{} + return render(GitIgnoreTmpl, p) +} + +// GenerateGrafanaDashboard generate default Grafana dashboard +func (g *EnvCodegen) GenerateGrafanaDashboard(uuid string) (string, error) { + log.Info().Msg("Generating default environment dashboard for Grafana") + p := GrafanaDashboardParams{ + ProductName: g.cfg.productName, + UUID: uuid, + } + return render(GrafanaDashboardTmpl, p) +} + // GenerateDefaultTOMLConfig generate default env.toml config func (g *EnvCodegen) GenerateDefaultTOMLConfig() (string, error) { log.Info().Msg("Generating default environment config (env.toml)") @@ -2160,13 +3138,14 @@ func (g *EnvCodegen) GenerateCLICompletion() (string, error) { } // GenerateCLI generate Cobra CLI -func (g *EnvCodegen) GenerateCLI() (string, error) { +func (g *EnvCodegen) GenerateCLI(dashboardUUID string) (string, error) { log.Info().Msg("Generating Cobra CLI") p := CLIParams{ PackageName: g.cfg.packageName, CLIName: g.cfg.cliName, ProductName: g.cfg.productName, DevEnvPkgImport: g.cfg.moduleName, + DashboardUUID: dashboardUUID, } return render(CLITmpl, p) } @@ -2233,7 +3212,7 @@ func (g *EnvCodegen) GenerateTestCases() ([]TestCaseParams, error) { /* Utility */ -func getGitRoot() (string, error) { +func gitRoot() (string, error) { cmd := exec.Command("git", "rev-parse", "--show-toplevel") output, err := cmd.Output() if err != nil { @@ -2242,8 +3221,10 @@ func getGitRoot() (string, error) { return strings.TrimSpace(string(output)), nil } +// dirPathRelFromGitRoot gets directory path relative from Git root +// used in CI templates to set default execution directory func dirPathRelFromGitRoot(name string) (string, error) { - gitRoot, err := getGitRoot() + gitRoot, err := gitRoot() if err != nil { return "", err } diff --git a/framework/tmpl_gen_test.go b/framework/tmpl_gen_test.go index f2bbfb63a..6ef06915e 100644 --- a/framework/tmpl_gen_test.go +++ b/framework/tmpl_gen_test.go @@ -38,11 +38,10 @@ type TestCfg struct { NodeSets []*ns.Input `toml:"nodesets" validate:"required"` } -// TestSmokeGenerateEnvPositive top-down approach tests until all the environment variations aren't stable -func TestSmokeGenerateEnvPositive(t *testing.T) { +// TestSmokeGenerateDevEnv top-down approach tests until all the environment variations aren't stable +func TestSmokeGenerateDevEnv(t *testing.T) { tests := []struct { name string - goModName string cliName string productName string productType string @@ -53,7 +52,6 @@ func TestSmokeGenerateEnvPositive(t *testing.T) { // that can pass healthchecks { name: "basic 2 nodes env", - goModName: "devenv", cliName: "tcli", productName: "myproduct", productType: "evm-single", @@ -74,7 +72,6 @@ func TestSmokeGenerateEnvPositive(t *testing.T) { tt.productType, tt.productName, ). - GoModName(tt.goModName). OutputDir(tt.outputDir). Build() require.NoError(t, err) @@ -113,19 +110,4 @@ func TestSmokeGenerateEnvPositive(t *testing.T) { }, 2*time.Minute, 2*time.Second) }) } -} - -func TestSmokeGenerateDataNegative(t *testing.T) { - tests := []struct { - name string - }{ - { - name: "single pod but incorrect instance annotation", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - }) - } -} +} \ No newline at end of file From e07f4f1a75a2cc064e6afc734c5fe4b76926f1b9 Mon Sep 17 00:00:00 2001 From: skudasov Date: Thu, 20 Nov 2025 14:06:22 +0100 Subject: [PATCH 10/16] finalize --- framework/cmd/main.go | 1 + framework/tmpl_gen.go | 13 +++++++------ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/framework/cmd/main.go b/framework/cmd/main.go index aed66a17d..a8eab672f 100644 --- a/framework/cmd/main.go +++ b/framework/cmd/main.go @@ -108,6 +108,7 @@ Usage: fmt.Printf("📁 Your environment directory is: %s\n", outputDir) fmt.Printf("💻 Your CLI name is: %s\n", cliName) fmt.Printf("📜 More docs can be found in %s/README.md\n", outputDir) + fmt.Printf("⬛ Entering the shell..\n") fmt.Println() cmd := exec.Command("just", "cli") diff --git a/framework/tmpl_gen.go b/framework/tmpl_gen.go index 2a69973bd..8e143fad1 100644 --- a/framework/tmpl_gen.go +++ b/framework/tmpl_gen.go @@ -1043,7 +1043,6 @@ func getCommands() []prompt.Suggest { {Text: "test", Description: "Perform smoke or load/chaos testing"}, {Text: "bs", Description: "Manage the Blockscout EVM block explorer"}, {Text: "obs", Description: "Manage the observability stack"}, - {Text: "db", Description: "Inspect Databases"}, {Text: "exit", Description: "Exit the interactive shell"}, } } @@ -1556,8 +1555,7 @@ func TestLoadChaos(t *testing.T) { wait: 20 * time.Second, command: "stop --duration=20s --restart re2:don-node0", validate: func(c []*clclient.ChainlinkClient) error { - _, _, err := c[0].ReadBridges() - return err + return nil }, }, { @@ -1565,8 +1563,7 @@ func TestLoadChaos(t *testing.T) { wait: 20 * time.Second, command: "netem --tc-image=gaiadocker/iproute2 --duration=20s delay --time=1000 re2:don-node.*", validate: func(c []*clclient.ChainlinkClient) error { - _, _, err := c[0].ReadBridges() - return err + return nil }, }, } @@ -2703,7 +2700,11 @@ func (g *EnvBuilder) validate() error { // onlyLetters strip any symbol except letters func onlyLetters(name string) string { return strings.Map(func(r rune) rune { - if unicode.IsLetter(r) && unicode.IsLower(r) { + if unicode.IsUpper(r) { + r = []rune(strings.ToLower(string(r)))[0] + return r + } + if unicode.IsLetter(r) { return r } return -1 From 5c4212a06b4851a72bd02cddd37208c0a3fe9b46 Mon Sep 17 00:00:00 2001 From: skudasov Date: Thu, 20 Nov 2025 14:14:14 +0100 Subject: [PATCH 11/16] fix lint --- framework/cmd/main.go | 6 +++--- framework/tmpl_gen_test.go | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/framework/cmd/main.go b/framework/cmd/main.go index a8eab672f..4074adbf1 100644 --- a/framework/cmd/main.go +++ b/framework/cmd/main.go @@ -39,7 +39,7 @@ Usage: ⚙️ Generate basic environment: ctf gen env --cli myenv --output-dir devenv --product-name Knilniahc --nodes 4 - + 📜 Read the docs in devenv/README.md 🔧 Address all TODO comments and customize it @@ -103,14 +103,14 @@ Usage: if err := cg.Write(); err != nil { return fmt.Errorf("failed to generate module: %w", err) } - + fmt.Println() fmt.Printf("📁 Your environment directory is: %s\n", outputDir) fmt.Printf("💻 Your CLI name is: %s\n", cliName) fmt.Printf("📜 More docs can be found in %s/README.md\n", outputDir) fmt.Printf("⬛ Entering the shell..\n") fmt.Println() - + cmd := exec.Command("just", "cli") cmd.Env = os.Environ() cmd.Dir = outputDir diff --git a/framework/tmpl_gen_test.go b/framework/tmpl_gen_test.go index 6ef06915e..c6ea49465 100644 --- a/framework/tmpl_gen_test.go +++ b/framework/tmpl_gen_test.go @@ -110,4 +110,4 @@ func TestSmokeGenerateDevEnv(t *testing.T) { }, 2*time.Minute, 2*time.Second) }) } -} \ No newline at end of file +} From b7db9657db2f827fdb8d13ae1d86f263805b87bd Mon Sep 17 00:00:00 2001 From: skudasov Date: Thu, 20 Nov 2025 15:23:22 +0100 Subject: [PATCH 12/16] copilot review fixes --- framework/.changeset/v0.12.0.md | 1 + framework/tmpl_gen.go | 2 +- framework/tmpl_gen_test.go | 5 ++++- 3 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 framework/.changeset/v0.12.0.md diff --git a/framework/.changeset/v0.12.0.md b/framework/.changeset/v0.12.0.md new file mode 100644 index 000000000..4e4122bb8 --- /dev/null +++ b/framework/.changeset/v0.12.0.md @@ -0,0 +1 @@ +- Codegeneration for Devenv diff --git a/framework/tmpl_gen.go b/framework/tmpl_gen.go index 8e143fad1..c910a9ed1 100644 --- a/framework/tmpl_gen.go +++ b/framework/tmpl_gen.go @@ -2196,7 +2196,7 @@ func Store[T any](cfg *T) error { if err != nil { return err } - return os.WriteFile(filepath.Join(DefaultConfigDir, outCacheName), d, 0o600) + return os.WriteFile(filepath.Join(DefaultConfigDir, outCacheName), d, 0o644) } // LoadOutput loads config output file from path. diff --git a/framework/tmpl_gen_test.go b/framework/tmpl_gen_test.go index c6ea49465..1002fdc39 100644 --- a/framework/tmpl_gen_test.go +++ b/framework/tmpl_gen_test.go @@ -92,6 +92,9 @@ func TestSmokeGenerateDevEnv(t *testing.T) { clClients, err := clclient.New(cfg.NodeSets[0].Out.CLNodes) require.NoError(t, err) + pollEvery := 2 * time.Second + timeout := 2 * time.Minute + // verify that all the services are healthy require.Eventually(t, func() bool { failedServices := false @@ -107,7 +110,7 @@ func TestSmokeGenerateDevEnv(t *testing.T) { } } return !failedServices - }, 2*time.Minute, 2*time.Second) + }, timeout, pollEvery) }) } } From 895bd1abd81145388067d285b20c035526ed15fe Mon Sep 17 00:00:00 2001 From: skudasov Date: Thu, 20 Nov 2025 15:24:35 +0100 Subject: [PATCH 13/16] copilot fixes --- framework/config.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework/config.go b/framework/config.go index 938b1e7a6..bcf892028 100644 --- a/framework/config.go +++ b/framework/config.go @@ -303,7 +303,7 @@ func Store[T any](cfg *T) error { } } - return os.WriteFile(writePath, d, 0o600) + return os.WriteFile(writePath, d, 0o644) } // JSONStrDuration is JSON friendly duration that can be parsed from "1h2m0s" Go format From 97b9b0dac34d21bb5fc7d59dcacec7f078ca4a06 Mon Sep 17 00:00:00 2001 From: skudasov Date: Thu, 20 Nov 2025 18:09:04 +0100 Subject: [PATCH 14/16] fix conflict between linter and AI --- framework/config.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework/config.go b/framework/config.go index bcf892028..938b1e7a6 100644 --- a/framework/config.go +++ b/framework/config.go @@ -303,7 +303,7 @@ func Store[T any](cfg *T) error { } } - return os.WriteFile(writePath, d, 0o644) + return os.WriteFile(writePath, d, 0o600) } // JSONStrDuration is JSON friendly duration that can be parsed from "1h2m0s" Go format From b9e4448d4b2c181d114e9533cf221bb810d611c2 Mon Sep 17 00:00:00 2001 From: skudasov Date: Thu, 20 Nov 2025 18:15:33 +0100 Subject: [PATCH 15/16] retrigger --- framework/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework/README.md b/framework/README.md index 9c925a564..7bd3c928f 100644 --- a/framework/README.md +++ b/framework/README.md @@ -2,4 +2,4 @@ Modular and data-driven harness for Chainlink on-chain and off-chain components. -[![Documentation](https://img.shields.io/badge/Documentation-MDBook-blue?style=for-the-badge)](https://smartcontractkit.github.io/chainlink-testing-framework/framework/overview.html) \ No newline at end of file +[![Documentation](https://img.shields.io/badge/Documentation-MDBook-blue?style=for-the-badge)](https://smartcontractkit.github.io/chainlink-testing-framework/framework/overview.html) From 68a0b5ef0205a2a3edbe6b69661f3a0063ecdc31 Mon Sep 17 00:00:00 2001 From: sebawo Date: Thu, 20 Nov 2025 18:27:43 +0100 Subject: [PATCH 16/16] feat: auto-install just command if not available - Add ensureJustInstalled() function to check and install 'just' automatically - On macOS, automatically installs via Homebrew if available - Provides helpful error messages with installation instructions for other platforms - Update documentation to reflect automatic installation capability - Fixes issue where environment generation failed when 'just' was not in PATH --- framework/cmd/main.go | 42 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/framework/cmd/main.go b/framework/cmd/main.go index 4074adbf1..7afdefb2c 100644 --- a/framework/cmd/main.go +++ b/framework/cmd/main.go @@ -6,6 +6,7 @@ import ( "os" "os/exec" "path/filepath" + "runtime" "strings" "github.com/pelletier/go-toml" @@ -33,7 +34,8 @@ func main() { Description: `🔗 Chainlink's Developer Environment Generator 🔗 Prerequisites: - We are using Just, please install it first - https://github.com/casey/just + Just will be automatically installed if not available (via Homebrew on macOS). + For other platforms, please install it manually: https://github.com/casey/just Usage: @@ -111,6 +113,11 @@ Usage: fmt.Printf("⬛ Entering the shell..\n") fmt.Println() + // Ensure 'just' is installed before proceeding + if err := ensureJustInstalled(); err != nil { + return fmt.Errorf("failed to ensure 'just' is installed: %w", err) + } + cmd := exec.Command("just", "cli") cmd.Env = os.Environ() cmd.Dir = outputDir @@ -467,3 +474,36 @@ func RemoveCacheFiles() error { framework.L.Info().Msg("All cache files has been removed") return nil } + +// ensureJustInstalled checks if 'just' is available in PATH, and if not, attempts to install it. +// On macOS, it tries to install via Homebrew. On other platforms, it provides installation instructions. +func ensureJustInstalled() error { + // Check if just is already available + if _, err := exec.LookPath("just"); err == nil { + return nil + } + + fmt.Println("⚠️ 'just' command not found in PATH") + fmt.Println("📦 Attempting to install 'just'...") + + // Try to install via Homebrew on macOS + if runtime.GOOS == "darwin" { + // Check if Homebrew is available + if _, err := exec.LookPath("brew"); err == nil { + fmt.Println("🍺 Installing 'just' via Homebrew...") + cmd := exec.Command("brew", "install", "just") + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + return fmt.Errorf("failed to install 'just' via Homebrew: %w. Please install manually: brew install just", err) + } + fmt.Println("✅ Successfully installed 'just'") + return nil + } + // Homebrew not available, provide instructions + return fmt.Errorf("'just' is not installed and Homebrew is not available. Please install 'just' manually:\n brew install just\n Or visit: https://github.com/casey/just") + } + + // For non-macOS platforms, provide installation instructions + return fmt.Errorf("'just' is not installed. Please install it manually:\n Visit: https://github.com/casey/just\n Or use your package manager (e.g., apt install just, pacman -S just)") +}