Skip to content

Commit

Permalink
Self-execute the same path
Browse files Browse the repository at this point in the history
  • Loading branch information
DrJosh9000 committed Nov 3, 2022
1 parent 1ea1a5b commit 4c73867
Show file tree
Hide file tree
Showing 5 changed files with 99 additions and 88 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,5 @@
packaging/docker/*/buildkite-agent
packaging/docker/*/hooks/

.idea
.idea
.DS_Store
6 changes: 3 additions & 3 deletions bootstrap/bootstrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -1445,14 +1445,14 @@ func (b *Bootstrap) defaultCheckoutPhase(ctx context.Context) error {
// we'll check to see if someone else has done
// it first.
b.shell.Commentf("Checking to see if Git data needs to be sent to Buildkite")
if err := b.shell.Run("buildkite-agent", "meta-data", "exists", "buildkite:git:commit"); err != nil {
if err := b.shell.Run(utils.BuildkiteAgentPath(), "meta-data", "exists", "buildkite:git:commit"); err != nil {
b.shell.Commentf("Sending Git commit information back to Buildkite")
out, err := b.shell.RunAndCapture("git", "--no-pager", "show", "HEAD", "-s", "--format=fuller", "--no-color", "--")
if err != nil {
return err
}
stdin := strings.NewReader(out)
if err := b.shell.WithStdin(stdin).Run("buildkite-agent", "meta-data", "set", "buildkite:git:commit"); err != nil {
if err := b.shell.WithStdin(stdin).Run(utils.BuildkiteAgentPath(), "meta-data", "set", "buildkite:git:commit"); err != nil {
return err
}
}
Expand Down Expand Up @@ -1863,7 +1863,7 @@ func (b *Bootstrap) uploadArtifacts(ctx context.Context) error {
args = append(args, b.ArtifactUploadDestination)
}

if err = b.shell.Run("buildkite-agent", args...); err != nil {
if err = b.shell.Run(utils.BuildkiteAgentPath(), args...); err != nil {
return err
}

Expand Down
62 changes: 36 additions & 26 deletions hook/scriptwrapper.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
package hook

import (
"bytes"
"encoding/json"
"fmt"
"os"
"path/filepath"
"runtime"
"strings"
"text/template"

"github.com/buildkite/agent/v3/bootstrap/shell"
Expand Down Expand Up @@ -35,11 +35,11 @@ $Env:BUILDKITE_HOOK_WORKING_DIR = $PWD | Select-Object -ExpandProperty Path
Get-ChildItem Env: | Foreach-Object {"$($_.Name)=$($_.Value)"} | Set-Content "{{.AfterEnvFileName}}"
exit $Env:BUILDKITE_HOOK_EXIT_STATUS`

bashScript = `buildkite-agent env > "{{.BeforeEnvFileName}}"
bashScript = `{{.BuildkiteAgentPath}} env > "{{.BeforeEnvFileName}}"
. "{{.PathToHook}}"
export BUILDKITE_HOOK_EXIT_STATUS=$?
export BUILDKITE_HOOK_WORKING_DIR=$PWD
buildkite-agent env > "{{.AfterEnvFileName}}"
{{.BuildkiteAgentPath}} env > "{{.AfterEnvFileName}}"
exit $BUILDKITE_HOOK_EXIT_STATUS`
)

Expand All @@ -50,9 +50,10 @@ var (
)

type scriptTemplateInput struct {
BeforeEnvFileName string
AfterEnvFileName string
PathToHook string
BuildkiteAgentPath string
BeforeEnvFileName string
AfterEnvFileName string
PathToHook string
}

type HookScriptChanges struct {
Expand Down Expand Up @@ -92,11 +93,18 @@ type scriptWrapperOpt func(*ScriptWrapper)
// a way to get the difference between the environment before the hook is run and
// after it
type ScriptWrapper struct {
hookPath string
os string
scriptFile *os.File
beforeEnvFile *os.File
afterEnvFile *os.File
buildkiteAgentPath string
hookPath string
os string
scriptFile *os.File
beforeEnvFile *os.File
afterEnvFile *os.File
}

func WithBuildkiteAgentPath(path string) scriptWrapperOpt {
return func(wrap *ScriptWrapper) {
wrap.buildkiteAgentPath = path
}
}

func WithHookPath(path string) scriptWrapperOpt {
Expand All @@ -115,7 +123,8 @@ func WithOS(os string) scriptWrapperOpt {
// Writes temporary files to the filesystem.
func NewScriptWrapper(opts ...scriptWrapperOpt) (*ScriptWrapper, error) {
wrap := &ScriptWrapper{
os: runtime.GOOS,
buildkiteAgentPath: utils.BuildkiteAgentPath(),
os: runtime.GOOS,
}

for _, o := range opts {
Expand All @@ -135,12 +144,13 @@ func NewScriptWrapper(opts ...scriptWrapperOpt) (*ScriptWrapper, error) {

// we use bash hooks for scripts with no extension, otherwise on windows
// we probably need a .bat extension
if filepath.Ext(wrap.hookPath) == ".ps1" {
switch {
case filepath.Ext(wrap.hookPath) == ".ps1":
isPwshHook = true
scriptFileName += ".ps1"
} else if filepath.Ext(wrap.hookPath) == "" {
case filepath.Ext(wrap.hookPath) == "":
isBashHook = true
} else if isWindows {
case isWindows:
scriptFileName += ".bat"
}

Expand Down Expand Up @@ -175,31 +185,31 @@ func NewScriptWrapper(opts ...scriptWrapperOpt) (*ScriptWrapper, error) {
}

tmplInput := scriptTemplateInput{
BeforeEnvFileName: wrap.beforeEnvFile.Name(),
AfterEnvFileName: wrap.afterEnvFile.Name(),
PathToHook: absolutePathToHook,
BuildkiteAgentPath: wrap.buildkiteAgentPath,
BeforeEnvFileName: wrap.beforeEnvFile.Name(),
AfterEnvFileName: wrap.afterEnvFile.Name(),
PathToHook: absolutePathToHook,
}

// Create the hook runner code
buf := &bytes.Buffer{}
if isWindows && !isBashHook && !isPwshHook {
buf := &strings.Builder{}
switch {
case isWindows && !isBashHook && !isPwshHook:
batchScriptTmpl.Execute(buf, tmplInput)
} else if isWindows && isPwshHook {
case isWindows && isPwshHook:
powershellScriptTmpl.Execute(buf, tmplInput)
} else {
default:
bashScriptTmpl.Execute(buf, tmplInput)
}
script := buf.String()

// Write the hook script to the runner then close the file so we can run it
_, err = wrap.scriptFile.WriteString(script)
if err != nil {
if _, err := wrap.scriptFile.WriteString(script); err != nil {
return nil, err
}

// Make script executable
err = utils.ChmodExecutable(wrap.scriptFile.Name())
if err != nil {
if err := utils.ChmodExecutable(wrap.scriptFile.Name()); err != nil {
return wrap, err
}

Expand Down
95 changes: 37 additions & 58 deletions hook/scriptwrapper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,9 @@ import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"runtime"
"strings"
"testing"

"github.com/buildkite/agent/v3/bootstrap/shell"
Expand All @@ -20,16 +18,14 @@ import (

func TestRunningHookDetectsChangedEnvironment(t *testing.T) {
ctx := context.Background()
var script []string

if runtime.GOOS != "windows" {
script = []string{
"#!/bin/bash",
"export LLAMAS=rock",
"export Alpacas=\"are ok\"",
"echo hello world",
}
} else {
script := []string{
"#!/bin/bash",
"export LLAMAS=rock",
"export Alpacas=\"are ok\"",
"echo hello world",
}
if runtime.GOOS == "windows" {
script = []string{
"@echo off",
"set LLAMAS=rock",
Expand Down Expand Up @@ -106,18 +102,13 @@ func TestHookScriptsAreGeneratedCorrectlyOnWindowsBatch(t *testing.T) {

defer wrapper.Close()

// The double percent signs %% are sprintf-escaped literal percent signs. Escaping hell is impossible to get out of.
// See: https://pkg.go.dev/fmt > ctrl-f for "%%"
scriptTemplate := `@echo off
SETLOCAL ENABLEDELAYEDEXPANSION
SET > "%s"
CALL "%s"
SET BUILDKITE_HOOK_EXIT_STATUS=!ERRORLEVEL!
SET BUILDKITE_HOOK_WORKING_DIR=%%CD%%
SET > "%s"
EXIT %%BUILDKITE_HOOK_EXIT_STATUS%%`

assertScriptLike(t, scriptTemplate, hookFile.Name(), wrapper)
fi, err := os.Stat(hookFile.Name())
if err != nil {
t.Errorf("os.Stat(hookFile.Name()) error = %v", err)
}
if fi.Size() == 0 {
t.Error("hookFile is empty")
}
}

func TestHookScriptsAreGeneratedCorrectlyOnWindowsPowershell(t *testing.T) {
Expand All @@ -139,15 +130,13 @@ func TestHookScriptsAreGeneratedCorrectlyOnWindowsPowershell(t *testing.T) {

defer wrapper.Close()

scriptTemplate := `$ErrorActionPreference = "STOP"
Get-ChildItem Env: | Foreach-Object {"$($_.Name)=$($_.Value)"} | Set-Content "%s"
%s
if ($LASTEXITCODE -eq $null) {$Env:BUILDKITE_HOOK_EXIT_STATUS = 0} else {$Env:BUILDKITE_HOOK_EXIT_STATUS = $LASTEXITCODE}
$Env:BUILDKITE_HOOK_WORKING_DIR = $PWD | Select-Object -ExpandProperty Path
Get-ChildItem Env: | Foreach-Object {"$($_.Name)=$($_.Value)"} | Set-Content "%s"
exit $Env:BUILDKITE_HOOK_EXIT_STATUS`

assertScriptLike(t, scriptTemplate, hookFile.Name(), wrapper)
fi, err := os.Stat(hookFile.Name())
if err != nil {
t.Errorf("os.Stat(hookFile.Name()) error = %v", err)
}
if fi.Size() == 0 {
t.Error("hookFile is empty")
}
}

func TestHookScriptsAreGeneratedCorrectlyOnUnix(t *testing.T) {
Expand All @@ -169,14 +158,13 @@ func TestHookScriptsAreGeneratedCorrectlyOnUnix(t *testing.T) {

defer wrapper.Close()

scriptTemplate := `buildkite-agent env > "%s"
. "%s"
export BUILDKITE_HOOK_EXIT_STATUS=$?
export BUILDKITE_HOOK_WORKING_DIR=$PWD
buildkite-agent env > "%s"
exit $BUILDKITE_HOOK_EXIT_STATUS`

assertScriptLike(t, scriptTemplate, hookFile.Name(), wrapper)
fi, err := os.Stat(hookFile.Name())
if err != nil {
t.Errorf("os.Stat(hookFile.Name()) error = %v", err)
}
if fi.Size() == 0 {
t.Error("hookFile is empty")
}
}

func TestRunningHookDetectsChangedWorkingDirectory(t *testing.T) {
Expand All @@ -190,7 +178,7 @@ func TestRunningHookDetectsChangedWorkingDirectory(t *testing.T) {
defer cleanup()
}

tempDir, err := ioutil.TempDir("", "hookwrapperdir")
tempDir, err := os.MkdirTemp("", "hookwrapperdir")
if err != nil {
t.Fatal(err)
}
Expand Down Expand Up @@ -273,26 +261,16 @@ func newTestScriptWrapper(t *testing.T, script []string) *ScriptWrapper {

hookFile.Close()

wrapper, err := NewScriptWrapper(WithHookPath(hookFile.Name()))
wrapper, err := NewScriptWrapper(
// The test binary is not a substitute for the whole agent.
WithBuildkiteAgentPath("buildkite-agent"),
WithHookPath(hookFile.Name()),
)
assert.NoError(t, err)

return wrapper
}

func assertScriptLike(t *testing.T, scriptTemplate, hookFileName string, wrapper *ScriptWrapper) {
file, err := os.Open(wrapper.scriptFile.Name())
assert.NoError(t, err)

defer file.Close()

wrapperScriptContents, err := ioutil.ReadAll(file)
assert.NoError(t, err)

expected := fmt.Sprintf(scriptTemplate, wrapper.beforeEnvFile.Name(), hookFileName, wrapper.afterEnvFile.Name())

assert.Equal(t, expected, string(wrapperScriptContents))
}

func mockAgent() (*bintest.Mock, func(), error) {
tmpPathDir, err := os.MkdirTemp("", "scriptwrapper-path")
if err != nil {
Expand Down Expand Up @@ -325,8 +303,9 @@ func mockAgent() (*bintest.Mock, func(), error) {
envMap := map[string]string{}

for _, e := range c.Env { // The env from the call
k, v, _ := strings.Cut(e, "=")
envMap[k] = v
if k, v, ok := env.Split(e); ok {
envMap[k] = v
}
}

envJSON, err := json.Marshal(envMap)
Expand Down
21 changes: 21 additions & 0 deletions utils/selfpath.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package utils

import "os"

var pathToSelf = "buildkite-agent"

func init() {
p, err := os.Executable()
if err != nil {
return
}
pathToSelf = p
}

// BuildkiteAgentPath returns a file path to buildkite-agent. If an absolute
// path cannot be found, it defaults to "buildkite-agent" assuming it is in
// $PATH. Self-executing with this path can fail if someone is playing games
// (e.g. unlinking the binary after starting it).
func BuildkiteAgentPath() string {
return pathToSelf
}

0 comments on commit 4c73867

Please sign in to comment.