diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ba0cbc5..0e3df2d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Override `--latest` flag to not use the latest version of the image in the clients if image is specified +- JWT secret path set as relative path when not provided. ## [v1.7.2] - 2024-11-12 diff --git a/cli/actions/jwt_secrets.go b/cli/actions/jwt_secrets.go index 0b71d8fa..a786042b 100644 --- a/cli/actions/jwt_secrets.go +++ b/cli/actions/jwt_secrets.go @@ -40,10 +40,14 @@ func (s *sedgeActions) CreateJWTSecrets(options CreateJWTSecretOptions) (string, jwtPath := options.JWTPath if jwtPath == "" && !configs.NetworksConfigs()[options.Network].NoJWT { return handleJWTSecret(options.GenerationPath) - } else if filepath.IsAbs(jwtPath) { // Ensure jwtPath is absolute - if jwtPath, err = filepath.Abs(jwtPath); err != nil { - return jwtPath, err - } + } + // Handle relative paths by joining with generation path + if !filepath.IsAbs(jwtPath) { + jwtPath = filepath.Join(options.GenerationPath, jwtPath) + } + // Create parent directories if they don't exist + if err = os.MkdirAll(filepath.Dir(jwtPath), 0o755); err != nil { + return "", fmt.Errorf(configs.GenerateJWTSecretError, err) } return jwtPath, nil } @@ -71,5 +75,11 @@ func handleJWTSecret(generationPath string) (string, error) { } log.Info(configs.JWTSecretGenerated) - return jwtPath, nil + + // Convert the absolute path to a relative path + relPath, err := filepath.Rel(generationPath, jwtPath) + if err != nil { + return "", fmt.Errorf(configs.GenerateJWTSecretError, err) + } + return relPath, nil } diff --git a/cli/actions/jwt_secrets_test.go b/cli/actions/jwt_secrets_test.go index 1c48be20..907fcb66 100644 --- a/cli/actions/jwt_secrets_test.go +++ b/cli/actions/jwt_secrets_test.go @@ -78,7 +78,11 @@ func TestCreateJwtSecrets(t *testing.T) { if jwtPath == "" { return } - assert.FileExists(t, jwtPath) + if tc.options.JWTPath == "" { + assert.FileExists(t, filepath.Join(tempDir, "jwtsecret")) + } else { + assert.FileExists(t, jwtPath) + } }) } } diff --git a/cli/cli_test.go b/cli/cli_test.go index c2065bf1..51763364 100644 --- a/cli/cli_test.go +++ b/cli/cli_test.go @@ -68,6 +68,9 @@ func TestCli(t *testing.T) { name: "full node with validator mainnet", setup: func(t *testing.T, sedgeActions *sedge_mocks.MockSedgeActions, prompter *sedge_mocks.MockPrompter, depsMgr *sedge_mocks.MockDependenciesManager) { generationPath := t.TempDir() + jwtPathAbs, _ := filepath.Abs(filepath.Join(generationPath, "jwtsecret")) + jwtPath, _ := filepath.Rel(generationPath, jwtPathAbs) + relativeJWTPath := "." + string(filepath.Separator) + jwtPath genData := generate.GenData{ Services: []string{"execution", "consensus", "validator", "mev-boost"}, ExecutionClient: &clients.Client{ @@ -95,7 +98,7 @@ func TestCli(t *testing.T) { MevImage: "flashbots/mev-boost:latest", RelayURLs: configs.NetworksConfigs()[NetworkMainnet].RelayURLs, ContainerTag: "tag", - JWTSecretPath: filepath.Join(generationPath, "jwtsecret"), + JWTSecretPath: relativeJWTPath, } sedgeActions.EXPECT().GetCommandRunner().Return(&test.SimpleCMDRunner{}) gomock.InOrder( @@ -151,6 +154,9 @@ func TestCli(t *testing.T) { name: "full node without validator mainnet", setup: func(t *testing.T, sedgeActions *sedge_mocks.MockSedgeActions, prompter *sedge_mocks.MockPrompter, depsMgr *sedge_mocks.MockDependenciesManager) { generationPath := t.TempDir() + jwtPathAbs, _ := filepath.Abs(filepath.Join(generationPath, "jwtsecret")) + jwtPath, _ := filepath.Rel(generationPath, jwtPathAbs) + relativeJWTPath := "." + string(filepath.Separator) + jwtPath genData := generate.GenData{ Services: []string{"execution", "consensus"}, ExecutionClient: &clients.Client{ @@ -168,7 +174,7 @@ func TestCli(t *testing.T) { FeeRecipient: "0x2d07a21ebadde0c13e6b91022a7e5722eb6bf5d5", MapAllPorts: true, ContainerTag: "tag", - JWTSecretPath: filepath.Join(generationPath, "jwtsecret"), + JWTSecretPath: relativeJWTPath, } gomock.InOrder( prompter.EXPECT().Select("Select node setup", "", []string{sedgeOpts.EthereumNode, sedgeOpts.LidoNode}).Return(0, nil), @@ -196,6 +202,9 @@ func TestCli(t *testing.T) { name: "full node without validator holesky", setup: func(t *testing.T, sedgeActions *sedge_mocks.MockSedgeActions, prompter *sedge_mocks.MockPrompter, depsMgr *sedge_mocks.MockDependenciesManager) { generationPath := t.TempDir() + jwtPathAbs, _ := filepath.Abs(filepath.Join(generationPath, "jwtsecret")) + jwtPath, _ := filepath.Rel(generationPath, jwtPathAbs) + relativeJWTPath := "." + string(filepath.Separator) + jwtPath genData := generate.GenData{ Services: []string{"execution", "consensus"}, ExecutionClient: &clients.Client{ @@ -213,7 +222,7 @@ func TestCli(t *testing.T) { FeeRecipient: "0x2d07a21ebadde0c13e6b91022a7e5722eb6bf5d5", MapAllPorts: true, ContainerTag: "tag", - JWTSecretPath: filepath.Join(generationPath, "jwtsecret"), + JWTSecretPath: relativeJWTPath, } gomock.InOrder( prompter.EXPECT().Select("Select node setup", "", []string{sedgeOpts.EthereumNode, sedgeOpts.LidoNode}).Return(0, nil), @@ -331,6 +340,9 @@ func TestCli(t *testing.T) { name: "consensus node", setup: func(t *testing.T, sedgeActions *sedge_mocks.MockSedgeActions, prompter *sedge_mocks.MockPrompter, depsMgr *sedge_mocks.MockDependenciesManager) { generationPath := t.TempDir() + jwtPathAbs, _ := filepath.Abs(filepath.Join(generationPath, "jwtsecret")) + jwtPath, _ := filepath.Rel(generationPath, jwtPathAbs) + relativeJWTPath := "." + string(filepath.Separator) + jwtPath genData := generate.GenData{ Services: []string{"consensus"}, ConsensusClient: &clients.Client{ @@ -346,7 +358,7 @@ func TestCli(t *testing.T) { ExecutionAuthUrl: "http://execution:5051", MevBoostEndpoint: "http://mev-boost:3030", ContainerTag: "tag", - JWTSecretPath: filepath.Join(generationPath, "jwtsecret"), + JWTSecretPath: relativeJWTPath, } gomock.InOrder( @@ -376,6 +388,9 @@ func TestCli(t *testing.T) { name: "consensus node holesky", setup: func(t *testing.T, sedgeActions *sedge_mocks.MockSedgeActions, prompter *sedge_mocks.MockPrompter, depsMgr *sedge_mocks.MockDependenciesManager) { generationPath := t.TempDir() + jwtPathAbs, _ := filepath.Abs(filepath.Join(generationPath, "jwtsecret")) + jwtPath, _ := filepath.Rel(generationPath, jwtPathAbs) + relativeJWTPath := "." + string(filepath.Separator) + jwtPath genData := generate.GenData{ Services: []string{"consensus"}, ConsensusClient: &clients.Client{ @@ -391,7 +406,7 @@ func TestCli(t *testing.T) { ExecutionAuthUrl: "http://execution:5051", MevBoostEndpoint: "http://mev-boost:3030", ContainerTag: "tag", - JWTSecretPath: filepath.Join(generationPath, "jwtsecret"), + JWTSecretPath: relativeJWTPath, } gomock.InOrder( @@ -485,6 +500,9 @@ func TestCli(t *testing.T) { name: "consensus node", setup: func(t *testing.T, sedgeActions *sedge_mocks.MockSedgeActions, prompter *sedge_mocks.MockPrompter, depsMgr *sedge_mocks.MockDependenciesManager) { generationPath := t.TempDir() + jwtPathAbs, _ := filepath.Abs(filepath.Join(generationPath, "jwtsecret")) + jwtPath, _ := filepath.Rel(generationPath, jwtPathAbs) + relativeJWTPath := "." + string(filepath.Separator) + jwtPath genData := generate.GenData{ Services: []string{"consensus"}, ConsensusClient: &clients.Client{ @@ -500,7 +518,7 @@ func TestCli(t *testing.T) { ExecutionAuthUrl: "http://execution:5051", MevBoostEndpoint: "http://mev-boost:3030", ContainerTag: "tag", - JWTSecretPath: filepath.Join(generationPath, "jwtsecret"), + JWTSecretPath: relativeJWTPath, } gomock.InOrder( @@ -530,6 +548,9 @@ func TestCli(t *testing.T) { name: "full node with Lido, mainnet", setup: func(t *testing.T, sedgeActions *sedge_mocks.MockSedgeActions, prompter *sedge_mocks.MockPrompter, depsMgr *sedge_mocks.MockDependenciesManager) { generationPath := t.TempDir() + jwtPathAbs, _ := filepath.Abs(filepath.Join(generationPath, "jwtsecret")) + jwtPath, _ := filepath.Rel(generationPath, jwtPathAbs) + relativeJWTPath := "." + string(filepath.Separator) + jwtPath genData := generate.GenData{ Services: []string{"execution", "consensus", "validator", "mev-boost"}, ExecutionClient: &clients.Client{ @@ -557,7 +578,7 @@ func TestCli(t *testing.T) { MevImage: "flashbots/mev-boost:latest", RelayURLs: mainnetMevboostRelayListUris, ContainerTag: "tag", - JWTSecretPath: filepath.Join(generationPath, "jwtsecret"), + JWTSecretPath: relativeJWTPath, } sedgeActions.EXPECT().GetCommandRunner().Return(&test.SimpleCMDRunner{}) gomock.InOrder( @@ -609,6 +630,9 @@ func TestCli(t *testing.T) { name: "full node with Lido, holesky", setup: func(t *testing.T, sedgeActions *sedge_mocks.MockSedgeActions, prompter *sedge_mocks.MockPrompter, depsMgr *sedge_mocks.MockDependenciesManager) { generationPath := t.TempDir() + jwtPathAbs, _ := filepath.Abs(filepath.Join(generationPath, "jwtsecret")) + jwtPath, _ := filepath.Rel(generationPath, jwtPathAbs) + relativeJWTPath := "." + string(filepath.Separator) + jwtPath genData := generate.GenData{ Services: []string{"execution", "consensus", "validator", "mev-boost"}, ExecutionClient: &clients.Client{ @@ -636,7 +660,7 @@ func TestCli(t *testing.T) { MevImage: "flashbots/mev-boost:latest", RelayURLs: holeskyMevboostRelayListUris, ContainerTag: "tag", - JWTSecretPath: filepath.Join(generationPath, "jwtsecret"), + JWTSecretPath: relativeJWTPath, } sedgeActions.EXPECT().GetCommandRunner().Return(&test.SimpleCMDRunner{}) gomock.InOrder( diff --git a/cli/generate.go b/cli/generate.go index cc83bb64..b9f9b00e 100644 --- a/cli/generate.go +++ b/cli/generate.go @@ -580,10 +580,21 @@ func handleJWTSecret(generationPath, name string) (string, error) { } log.Info(configs.JWTSecretGenerated) - return jwtPath, nil + + // Convert the absolute path to a relative path + relativeJWTPath, err := filepath.Rel(generationPath, jwtPath) + if err != nil { + return "", fmt.Errorf(configs.GenerateJWTSecretError, err) + } + + // Prepend "./" to ensure compatibility with relative paths + if !filepath.IsAbs(relativeJWTPath) && !filepath.HasPrefix(relativeJWTPath, ".") { + relativeJWTPath = "." + string(filepath.Separator) + relativeJWTPath + } + + return relativeJWTPath, nil } -// TODO: Add unit tests func loadJWTSecret(from string) (absFrom string, err error) { // Ensure from is absolute absFrom, err = filepath.Abs(from) diff --git a/e2e/sedge/jwt_test.go b/e2e/sedge/jwt_test.go new file mode 100644 index 00000000..eb2fac43 --- /dev/null +++ b/e2e/sedge/jwt_test.go @@ -0,0 +1,150 @@ +/* +Copyright 2022 Nethermind + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +package e2e + +import ( + "os" + "path/filepath" + "testing" + + base "github.com/NethermindEth/sedge/e2e" + "github.com/stretchr/testify/assert" +) + +const ( + // validJWTSecret is a 32-byte hex string used as a valid JWT secret + validJWTSecret = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" + // jwtSecretPath is the default path for JWT secrets + jwtSecretPath = "jwtsecret" +) + +func TestE2E_Generate_JWTSecret_RelativePath(t *testing.T) { + // Test context + var ( + runErr error + jwtPath = "custom/path/jwtsecret" + ) + + // Build test case + e2eTest := newE2ESedgeTestCase( + t, + // Arrange + func(t *testing.T, sedgePath string) error { + // Create the JWT secret file in the data directory + dataDir := filepath.Join(filepath.Dir(sedgePath), "sedge-data") + fullJWTPath := filepath.Join(dataDir, jwtPath) + if err := os.MkdirAll(filepath.Dir(fullJWTPath), 0o755); err != nil { + return err + } + if err := os.WriteFile(fullJWTPath, []byte(validJWTSecret), 0o644); err != nil { + return err + } + return nil + }, + // Act + func(t *testing.T, binaryPath, dataDirPath string) { + // Use the full path when running the generate command + fullJWTPath := filepath.Join(dataDirPath, jwtPath) + runErr = base.RunSedge(t, binaryPath, "generate", "full-node", "--network", "mainnet", "--jwt-secret-path", fullJWTPath) + }, + // Assert + func(t *testing.T, dataDirPath string) { + assert.NoError(t, runErr) + // Check if JWT file exists in the relative path + fullJWTPath := filepath.Join(dataDirPath, jwtPath) + _, err := os.Stat(fullJWTPath) + assert.NoError(t, err, "JWT secret file should exist at relative path") + + // Verify the JWT secret content + content, err := os.ReadFile(fullJWTPath) + assert.NoError(t, err) + assert.Equal(t, validJWTSecret, string(content), "JWT secret content should match") + }, + ) + e2eTest.run() +} + +func TestE2E_Generate_JWTSecret_AbsolutePath(t *testing.T) { + // Test context + var ( + runErr error + tempJWTDir string + ) + + // Build test case + e2eTest := newE2ESedgeTestCase( + t, + // Arrange + func(t *testing.T, sedgePath string) error { + // Create a temporary directory for JWT secret + tempJWTDir = t.TempDir() + // Create the JWT secret file + jwtPath := filepath.Join(tempJWTDir, "jwtsecret") + return os.WriteFile(jwtPath, []byte(validJWTSecret), 0o644) + }, + // Act + func(t *testing.T, binaryPath, dataDirPath string) { + jwtPath := filepath.Join(tempJWTDir, "jwtsecret") + runErr = base.RunSedge(t, binaryPath, "generate", "full-node", "--network", "mainnet", "--jwt-secret-path", jwtPath) + }, + // Assert + func(t *testing.T, dataDirPath string) { + assert.NoError(t, runErr) + // Check if JWT file exists in the absolute path + jwtPath := filepath.Join(tempJWTDir, "jwtsecret") + _, err := os.Stat(jwtPath) + assert.NoError(t, err, "JWT secret file should exist at absolute path") + + // Verify the JWT secret content + content, err := os.ReadFile(jwtPath) + assert.NoError(t, err) + assert.Equal(t, validJWTSecret, string(content), "JWT secret content should match") + }, + ) + e2eTest.run() +} + +func TestE2E_Generate_JWTSecret_DefaultPath(t *testing.T) { + // Test context + var ( + runErr error + ) + + // Build test case + e2eTest := newE2ESedgeTestCase( + t, + // Arrange + nil, + // Act + func(t *testing.T, binaryPath, dataDirPath string) { + runErr = base.RunSedge(t, binaryPath, "generate", "full-node", "--network", "mainnet") + }, + // Assert + func(t *testing.T, dataDirPath string) { + assert.NoError(t, runErr) + // Check if JWT file exists in the default path + defaultJWTPath := filepath.Join(dataDirPath, "jwtsecret") + _, err := os.Stat(defaultJWTPath) + assert.NoError(t, err, "JWT secret file should exist at default path") + + // Read the file to ensure it's not empty + content, err := os.ReadFile(defaultJWTPath) + assert.NoError(t, err) + assert.NotEmpty(t, content, "JWT secret file should not be empty") + }, + ) + e2eTest.run() +}