Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[cmd/builder] add ocb feature to install go binary to temp dir #11386

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions .chloggen/jackgopack4_add-ocb-go-binary-download.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Use this changelog template to create an entry for release notes.

# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix'
change_type: enhancement

# The name of the component, or a single word describing the area of concern, (e.g. otlpreceiver)
component: cmd/builder

# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
note: Removes requirement to have a go binary installed on the host machine. The builder will now download the go binary from the official source if not found.

# One or more tracking issues or pull requests related to the change
issues: [11382]

# (Optional) One or more lines of additional information to render under the primary note.
# These lines will be padded with 2 spaces and then inserted directly into the document.
# Use pipe (|) for multiline entries.
subtext:

# Optional: The change log or logs in which this entry should be included.
# e.g. '[user]' or '[user, api]'
# Include 'user' if the change is relevant to end users.
# Include 'api' if there is a change to a library API.
# Default: '[user]'
change_logs: [user]
132 changes: 130 additions & 2 deletions cmd/builder/internal/builder/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,17 @@
package builder // import "go.opentelemetry.io/collector/cmd/builder/internal/builder"

import (
"archive/tar"
"compress/gzip"
"errors"
"fmt"
"io"
"net/http"
"os"
"os/exec"
"path/filepath"
"runtime"
"runtime/debug"
"strings"
"time"

Expand All @@ -19,9 +25,13 @@

const defaultOtelColVersion = "0.111.0"

var goVersionPackageURL = "https://golang.org/dl/"

// ErrMissingGoMod indicates an empty gomod field
var ErrMissingGoMod = errors.New("missing gomod specification for module")

var readBuildInfo = debug.ReadBuildInfo

// Config holds the builder's configuration
type Config struct {
Logger *zap.Logger
Expand Down Expand Up @@ -128,14 +138,132 @@
)
}

// SetGoPath sets go path
func getGoVersionFromBuildInfo() (string, error) {
info, ok := readBuildInfo()
if !ok {
return "", fmt.Errorf("failed to read Go build info")
}
version := strings.TrimPrefix(info.GoVersion, "go")
return version, nil
}
func sanitizeExtractPath(filePath string, destination string) error {
// to avoid zip slip (writing outside of the destination), we resolve
// the target path, and make sure it's nested in the intended
// destination, or bail otherwise.
destpath := filepath.Join(destination, filePath)
if !strings.HasPrefix(destpath, destination) {
return fmt.Errorf("%s: illegal file path", filePath)

Check warning on line 155 in cmd/builder/internal/builder/config.go

View check run for this annotation

Codecov / codecov/patch

cmd/builder/internal/builder/config.go#L155

Added line #L155 was not covered by tests
}
return nil
}

func downloadGoBinary(version string) error {
platform := runtime.GOOS
arch := runtime.GOARCH
originalGoURL := goVersionPackageURL
goVersionPackageURL = fmt.Sprintf(goVersionPackageURL+"/go%s.%s-%s.tar.gz", version, platform, arch)
defer func() {
goVersionPackageURL = originalGoURL
}()
client := http.Client{Transport: &http.Transport{DisableKeepAlives: true}}
request, err := http.NewRequest(http.MethodGet, goVersionPackageURL, nil)
if err != nil {
return err

Check warning on line 171 in cmd/builder/internal/builder/config.go

View check run for this annotation

Codecov / codecov/patch

cmd/builder/internal/builder/config.go#L171

Added line #L171 was not covered by tests
}

resp, err := client.Do(request)
if err != nil {
return err

Check warning on line 176 in cmd/builder/internal/builder/config.go

View check run for this annotation

Codecov / codecov/patch

cmd/builder/internal/builder/config.go#L176

Added line #L176 was not covered by tests
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
return fmt.Errorf("failed to download Go binary: %s", resp.Status)
}

gzr, err := gzip.NewReader(resp.Body)
if err != nil {
return err
}
defer gzr.Close()

tr := tar.NewReader(gzr)
if err := os.MkdirAll(filepath.Join(os.TempDir(), "go"), 0750); err != nil {
return err

Check warning on line 192 in cmd/builder/internal/builder/config.go

View check run for this annotation

Codecov / codecov/patch

cmd/builder/internal/builder/config.go#L192

Added line #L192 was not covered by tests
}

for {
header, err := tr.Next()
Fixed Show fixed Hide fixed
if errors.Is(err, io.EOF) {
break
}
if err != nil {
return err

Check warning on line 201 in cmd/builder/internal/builder/config.go

View check run for this annotation

Codecov / codecov/patch

cmd/builder/internal/builder/config.go#L201

Added line #L201 was not covered by tests
}
err = sanitizeExtractPath(header.Name, os.TempDir())
if err == nil {
target := filepath.Join(os.TempDir(), header.Name)
switch header.Typeflag {
case tar.TypeDir:
if err := os.MkdirAll(target, 0750); err != nil {
return err

Check warning on line 209 in cmd/builder/internal/builder/config.go

View check run for this annotation

Codecov / codecov/patch

cmd/builder/internal/builder/config.go#L207-L209

Added lines #L207 - L209 were not covered by tests
}
case tar.TypeReg:
f, err := os.OpenFile(target, os.O_CREATE|os.O_WRONLY, os.FileMode(header.Mode))
if err != nil {
return err

Check warning on line 214 in cmd/builder/internal/builder/config.go

View check run for this annotation

Codecov / codecov/patch

cmd/builder/internal/builder/config.go#L214

Added line #L214 was not covered by tests
}
// Copy up to 500MB of data to avoid gosec warning; current Go distributions are arount 250MB
if _, err := io.CopyN(f, tr, 500000000); err != nil {
f.Close()
if !errors.Is(err, io.EOF) {
return err

Check warning on line 220 in cmd/builder/internal/builder/config.go

View check run for this annotation

Codecov / codecov/patch

cmd/builder/internal/builder/config.go#L220

Added line #L220 was not covered by tests
}
}
f.Close()
}

}
}

return nil

}

func removeGoTempDir() error {
goTempDir := filepath.Join(os.TempDir(), "go")
var err error
if _, err = os.Stat(goTempDir); err == nil {
if err = os.RemoveAll(goTempDir); err != nil {
return fmt.Errorf("failed to remove go temp directory: %w", err)

Check warning on line 238 in cmd/builder/internal/builder/config.go

View check run for this annotation

Codecov / codecov/patch

cmd/builder/internal/builder/config.go#L238

Added line #L238 was not covered by tests
}
} else if !os.IsNotExist(err) {
return fmt.Errorf("failed to check go temp directory: %w", err)

Check warning on line 241 in cmd/builder/internal/builder/config.go

View check run for this annotation

Codecov / codecov/patch

cmd/builder/internal/builder/config.go#L241

Added line #L241 was not covered by tests
}
return nil
}

// SetGoPath sets go path, and if go binary not found on path, downloads it
func (c *Config) SetGoPath() error {
if !c.SkipCompilation || !c.SkipGetModules {
// #nosec G204
if _, err := exec.Command(c.Distribution.Go, "env").CombinedOutput(); err != nil { // nolint G204
path, err := exec.LookPath("go")
if err != nil {
return ErrGoNotFound
if runtime.GOOS == "windows" {
return fmt.Errorf("failed to find go executable in PATH, please install Go from %s", goVersionPackageURL)

Check warning on line 254 in cmd/builder/internal/builder/config.go

View check run for this annotation

Codecov / codecov/patch

cmd/builder/internal/builder/config.go#L253-L254

Added lines #L253 - L254 were not covered by tests
}
c.Logger.Info("Failed to find go executable in PATH, downloading Go binary")
goVersion, err := getGoVersionFromBuildInfo()
if err != nil {
return err

Check warning on line 259 in cmd/builder/internal/builder/config.go

View check run for this annotation

Codecov / codecov/patch

cmd/builder/internal/builder/config.go#L256-L259

Added lines #L256 - L259 were not covered by tests
}
c.Logger.Info(fmt.Sprintf("Downloading Go version %s from "+filepath.Join(goVersionPackageURL, fmt.Sprintf("go%s.%s-%s.tar.gz", goVersion, runtime.GOOS, runtime.GOARCH)), goVersion))
if err := downloadGoBinary(goVersion); err != nil {
return err

Check warning on line 263 in cmd/builder/internal/builder/config.go

View check run for this annotation

Codecov / codecov/patch

cmd/builder/internal/builder/config.go#L261-L263

Added lines #L261 - L263 were not covered by tests
}
path = filepath.Join(os.TempDir(), "go", "bin", "go")
c.Logger.Info(fmt.Sprintf("Installed go at temporary path: %s", path))

Check warning on line 266 in cmd/builder/internal/builder/config.go

View check run for this annotation

Codecov / codecov/patch

cmd/builder/internal/builder/config.go#L265-L266

Added lines #L265 - L266 were not covered by tests
}
c.Distribution.Go = path
}
Expand Down
166 changes: 166 additions & 0 deletions cmd/builder/internal/builder/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@
package builder

import (
"fmt"
"net/http"
"net/http/httptest"
"os"
"runtime"
"runtime/debug"
"strings"
"testing"

Expand Down Expand Up @@ -336,3 +341,164 @@ func TestSkipsNilFieldValidation(t *testing.T) {
cfg.Providers = nil
assert.NoError(t, cfg.Validate())
}

func TestGetGoVersionFromBuildInfo(t *testing.T) {
tests := []struct {
name string
mockBuildInfo *debug.BuildInfo
expected string
expectError bool
}{
{
name: "successful read",
mockBuildInfo: &debug.BuildInfo{
GoVersion: "go1.16",
},
expected: "1.16",
expectError: false,
},
{
name: "failed read",
mockBuildInfo: nil,
expected: "",
expectError: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Mock debug.ReadBuildInfo
mockReadBuildInfo := func() (*debug.BuildInfo, bool) {
if tt.mockBuildInfo == nil {
return nil, false
}
return tt.mockBuildInfo, true
}
old := readBuildInfo
readBuildInfo = mockReadBuildInfo
defer func() { readBuildInfo = old }()

version, err := getGoVersionFromBuildInfo()
if tt.expectError {
assert.Error(t, err)
} else {
require.NoError(t, err)
assert.Equal(t, tt.expected, version)
}
})
}
}

const readmePath = "../../README.md"
const tarballPath = "../../test/test-tarball.tar.gz"

func TestDownloadGoBinary(t *testing.T) {
tests := []struct {
name string
version string
serverResponse string
serverStatus int
expectError bool
}{
{
name: "successful download",
version: "1.16.5",
serverResponse: "fake go binary content",
serverStatus: http.StatusOK,
expectError: false,
},
{
name: "failed download with 404",
version: "1.16.5",
serverResponse: "not found",
serverStatus: http.StatusNotFound,
expectError: true,
},
{
name: "failed to create gzip reader",
version: "1.16.5",
serverResponse: "invalid gzip content",
serverStatus: http.StatusOK,
expectError: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tarballLocation := fmt.Sprintf("/go%s.%s-%s.tar.gz", tt.version, runtime.GOOS, runtime.GOARCH)
// Create a mock HTTP server
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
tmp := r.URL.Path
if tmp == tarballLocation {
w.WriteHeader(tt.serverStatus)
if tt.serverStatus == http.StatusOK {
var fileContent []byte
var err error
if tt.serverResponse == "invalid gzip content" {
fileContent, err = os.ReadFile(readmePath)
} else {
fileContent, err = os.ReadFile(tarballPath)
}
if err != nil {
t.Fatalf("failed to read test tarball: %v", err)
}
if _, err := w.Write(fileContent); err != nil {
t.Fatalf("failed to write server response: %v", err)
}
} else {
if _, err := w.Write([]byte(tt.serverResponse)); err != nil {
t.Fatalf("failed to write server response: %v", err)
}
}
} else {
w.WriteHeader(http.StatusNotFound)
}
}))
defer server.Close()

// Override the goVersionPackageURL with the mock server URL
goVersionPackageURL = server.URL

// Call the function under test
err := downloadGoBinary(tt.version)

// Check for expected error
if (err != nil) != tt.expectError {
t.Fatalf("expected error: %v, got: %v", tt.expectError, err)
}

// If no error is expected, check if the file was downloaded correctly
if !tt.expectError {
tempDir := os.TempDir()
if _, err := os.Stat(tempDir); os.IsNotExist(err) {
t.Fatalf("expected directory %s to exist", tempDir)
}
}
})
}
}

func TestSetGoPathWithDownload(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("skipping test on windows")
}
cfg := NewDefaultConfig()
require.NoError(t, cfg.ParseModules())
assert.NoError(t, cfg.Validate())
// Save the original PATH
originalPath := os.Getenv("PATH")

// Mock the PATH to exclude any references to go
paths := strings.Split(originalPath, string(os.PathListSeparator))
var newPaths []string
for _, p := range paths {
if !strings.Contains(p, "go") {
newPaths = append(newPaths, p)
}
}
mockedPath := strings.Join(newPaths, string(os.PathListSeparator))
t.Setenv("PATH", mockedPath)

assert.NoError(t, cfg.SetGoPath())
assert.NoError(t, removeGoTempDir())
}
4 changes: 4 additions & 0 deletions cmd/builder/internal/builder/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,10 @@
}
cfg.Logger.Info("Compiled", zap.String("binary", fmt.Sprintf("%s/%s", cfg.Distribution.OutputPath, cfg.Distribution.Name)))

if err := removeGoTempDir(); err != nil {
cfg.Logger.Info("failed to remove temporary directory used for Go compilation", zap.String("error", err.Error()))

Check warning on line 134 in cmd/builder/internal/builder/main.go

View check run for this annotation

Codecov / codecov/patch

cmd/builder/internal/builder/main.go#L134

Added line #L134 was not covered by tests
}

return nil
}

Expand Down
Binary file added cmd/builder/test/test-tarball.tar.gz
Binary file not shown.
Loading