diff --git a/pkg/docker/image.go b/pkg/docker/image.go index c5627be1f..105346083 100644 --- a/pkg/docker/image.go +++ b/pkg/docker/image.go @@ -5,10 +5,13 @@ package docker import ( "fmt" + "os" + "path/filepath" "strings" "github.com/ava-labs/avalanche-cli/pkg/constants" "github.com/ava-labs/avalanche-cli/pkg/models" + "github.com/ava-labs/avalanche-cli/pkg/utils" "github.com/ava-labs/avalanche-cli/pkg/ux" ) @@ -38,10 +41,34 @@ func parseDockerImageListOutput(output []byte) []string { return strings.Split(string(output), "\n") } +func parseRemoteGoModFile(path string, host *models.Host) (string, error) { + goMod := filepath.Join(path, "go.mod") + // download and parse go.mod + tmpFile, err := os.CreateTemp("", "go-mod-*.txt") + if err != nil { + return "", err + } + defer os.Remove(tmpFile.Name()) + if err := host.Download(goMod, tmpFile.Name(), constants.SSHFileOpsTimeout); err != nil { + return "", err + } + return utils.ReadGoVersion(tmpFile.Name()) +} + // BuildDockerImage builds a docker image on a remote host. func BuildDockerImage(host *models.Host, image string, path string, dockerfile string) error { - _, err := host.Command(fmt.Sprintf("cd %s && docker build -q --build-arg GO_VERSION=%s -t %s -f %s .", path, constants.BuildEnvGolangVersion, image, dockerfile), nil, constants.SSHLongRunningScriptTimeout) - return err + goVersion, err := parseRemoteGoModFile(path, host) + if err != nil { + // fall back to default + ux.Logger.Info("failed to read go version from go.mod: %s. falling back to default", err) + goVersion = constants.BuildEnvGolangVersion + } + cmd := fmt.Sprintf("cd %s && docker build -q --build-arg GO_VERSION=%s -t %s -f %s .", path, goVersion, image, dockerfile) + _, err = host.Command(cmd, nil, constants.SSHLongRunningScriptTimeout) + if err != nil { + return fmt.Errorf("failed to build docker image %s: %w", image, err) + } + return nil } // BuildDockerImageFromGitRepo builds a docker image from a git repo on a remote host. @@ -59,7 +86,7 @@ func BuildDockerImageFromGitRepo(host *models.Host, image string, gitRepo string } }() // clone the repo and checkout commit - if _, err := host.Command(fmt.Sprintf("git clone %s %s && cd %s && git checkout %s ", gitRepo, tmpDir, tmpDir, commit), nil, constants.SSHLongRunningScriptTimeout); err != nil { + if _, err := host.Command(fmt.Sprintf("git clone %s %s && cd %s && git checkout %s", gitRepo, tmpDir, tmpDir, commit), nil, constants.SSHLongRunningScriptTimeout); err != nil { return err } // build the image diff --git a/pkg/utils/file.go b/pkg/utils/file.go index 3898b3d00..c276f71bd 100644 --- a/pkg/utils/file.go +++ b/pkg/utils/file.go @@ -8,6 +8,7 @@ import ( "path/filepath" "github.com/ava-labs/avalanche-cli/pkg/constants" + "golang.org/x/mod/modfile" ) func NonEmptyDirectory(dirName string) (bool, error) { @@ -127,3 +128,19 @@ func GetRemoteComposeServicePath(serviceName string, dirs ...string) string { servicePrefix := filepath.Join(constants.CloudNodeCLIConfigBasePath, "services", serviceName) return filepath.Join(append([]string{servicePrefix}, dirs...)...) } + +// ReadGoVersion reads the Go version from the go.mod file +func ReadGoVersion(filePath string) (string, error) { + data, err := os.ReadFile(filePath) + if err != nil { + return "", err + } + modFile, err := modfile.Parse(filePath, data, nil) + if err != nil { + return "", err + } + if modFile.Go != nil { + return modFile.Go.Version, nil + } + return "", fmt.Errorf("go version not found in %s", filePath) +} diff --git a/pkg/utils/file_test.go b/pkg/utils/file_test.go index de9c42ccb..f500cd82d 100644 --- a/pkg/utils/file_test.go +++ b/pkg/utils/file_test.go @@ -44,3 +44,57 @@ func TestExpandHome(t *testing.T) { t.Errorf("ExpandHome failed for empty path: expected %s, got %s", expectedEmptyPath, expandedEmptyPath) } } + +// createTempGoMod creates a temporary go.mod file with the provided content. +func createTempGoMod(t *testing.T, content string) string { + t.Helper() + file, err := os.CreateTemp("", "go.mod") + if err != nil { + t.Fatal(err) + } + + if _, err := file.Write([]byte(content)); err != nil { + t.Fatal(err) + } + + if err := file.Close(); err != nil { + t.Fatal(err) + } + + return file.Name() +} + +// TestReadGoVersion tests all scenarios in one function using sub-tests. +func TestReadGoVersion(t *testing.T) { + t.Run("Success", func(t *testing.T) { + tempFile := createTempGoMod(t, "module example.com/test\n\ngo 1.23\n") + defer os.Remove(tempFile) // Clean up the temp file + + version, err := ReadGoVersion(tempFile) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + expectedVersion := "1.23" + if version != expectedVersion { + t.Errorf("expected version %s, got %s", expectedVersion, version) + } + }) + + t.Run("NoVersion", func(t *testing.T) { + tempFile := createTempGoMod(t, "module example.com/test\n") + defer os.Remove(tempFile) + + _, err := ReadGoVersion(tempFile) + if err == nil { + t.Fatalf("expected an error, but got none") + } + }) + + t.Run("InvalidFile", func(t *testing.T) { + _, err := ReadGoVersion("nonexistent-go.mod") + if err == nil { + t.Fatalf("expected an error for nonexistent file, but got none") + } + }) +}