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

Experiment: Polyglot hooks #2040

Merged
merged 11 commits into from
May 22, 2023
Merged

Experiment: Polyglot hooks #2040

merged 11 commits into from
May 22, 2023

Conversation

moskyb
Copy link
Contributor

@moskyb moskyb commented Apr 3, 2023

This PR: Removes the requirement that hooks for the buildkite-agent be written in a shell scripting language a la bash. With the use of this branch, hooks can also either be compiled executable binaries (ie those produced by languages like Golang or Rust), or any interpreted scripting language (ie python, ruby, lua), so long as an appropriate shebang is specified at the top of the file.

note that neither binary hooks nor non-shell script hooks have environment variable changes captured as bash script hooks do; they must use the Job API to modify the environment of the job.

@moskyb moskyb requested a review from a team April 3, 2023 04:00
Comment on lines +285 to +308
func (s *Shell) RunWithEnv(ctx context.Context, environ *env.Environment, command string, arg ...string) error {
formatted := process.FormatCommand(command, arg)
if s.stdin == nil {
s.Promptf("%s", formatted)
} else {
// bash-syntax-compatible indication that input is coming from somewhere
s.Promptf("%s < /dev/stdin", formatted)
}

cmd, err := s.buildCommand(command, arg...)
if err != nil {
s.Errorf("Error building command: %v", err)
return err
}

cmd.Env = append(cmd.Env, environ.ToSlice()...)

return s.executeCommand(ctx, cmd, s.Writer, executeFlags{
Stdout: true,
Stderr: true,
PTY: s.PTY,
})
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this function is just the Run method and the RunWithPrompt methods garglemeshed together with an env-y thing going on in the middle. earlier on, i tried to make a nice refactor so that you could compose bits and bobs of the different Run* methods together, but it wasn't super important to make this change. i'd like to clean it up at some point though

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes please

Comment on lines +200 to +203
if e == nil {
return New()
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

gotta catch 'em all ('em being nil pointer dereference panics)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I sort of expect a copy of nil to be nil, but it's probably more pragmatic for it to be New().

Comment on lines 148 to 150
if shebang != "" && !shellscript.IsPOSIXShell(shebang) {
return nil, fmt.Errorf("hook starts with an unsupported shebang line %q", shebang)
}
// if shebang != "" && !shellscript.IsPOSIXShell(shebang) {
// return nil, fmt.Errorf("hook starts with an unsupported shebang line %q", shebang)
// }
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this doesn't technically have to be commented out anymore - it's still an error if we try to scriptwrap a non-shell hook, but we've kind of lifted this logic out to here. Maybe it makes sense to keep this in as a defensive programming thing?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can also defend ourselves with a test. I think that's more robust, so would prefer to delete the comments.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh totally, if this gets out of WIP i'd either de-comment it or remove these lines entirely.

when you say we can defend ourselves with a test, do you mean adding a test that asserts that if we try to wrap a non-shell-script, it complains?

Copy link
Contributor

@triarius triarius Apr 3, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, exactly. I'd imagine, we would want to test a whole table of binaries and scripts and verify that they run. We should be able to tease out a test that fails when the scriptwrapper is run on the wrong type of binary from this.

You might have to think a bit about making an interface. I think that's a good thing to do, though.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yep, that sounds like a good idea. when we get to properly testing this, i'll do that.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have we gotten to properly testing this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

very good call! when i said:

oh totally, if this gets out of WIP i'd either de-comment it or remove these lines entirely.

apparently i was lying :P

i'll bang out some tests for this now

)

// Magic numbers for various types of executable files
var (
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i'd like to thank the peanut gallery in the #serious-engineering channel for helping me with all of this binary nonsense

bootstrap/bootstrap.go Outdated Show resolved Hide resolved
bootstrap/bootstrap.go Outdated Show resolved Hide resolved
ELFMagic = []byte{0x7F, 0x45, 0x4C, 0x46}

// Windows uses MS-DOS MZ-style executables. This includes PE32 and PE32+ (which is 64-bit 🙃) binaries,
// which a are what modern windowses tend to use.
Copy link
Member

@pda pda Apr 3, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It hates the windowses, precious.

shellscript/shellscript.go Outdated Show resolved Hide resolved
bootstrap/bootstrap.go Outdated Show resolved Hide resolved
shellscript/shellscript.go Outdated Show resolved Hide resolved
@lox
Copy link
Contributor

lox commented Apr 7, 2023

Love this!

note that neither binary hooks nor non-shell script hooks have environment variable changes captured as bash script hooks do; they must use the Job API to modify the environment of the job.

Yeah, this is my only concern. I wonder if this will be an unexpected sharp edge. I guess that folks assume that bash scripts are simply sourced (vs the magic of wrapped hooks) and they won't expect that behaviour for non-shell scripts.

I assume the future is all Job API and no more magical environment capture?

@moskyb
Copy link
Contributor Author

moskyb commented Apr 17, 2023

@lox

I assume the future is all Job API and no more magical environment capture?

in an "as the limit tends to infinity" sense, i think so. i doubt that we'd ever fully remove the environment functionality anytime in the forseeable future; it's just too useful and easy to set up.

i suspect that if we merged this, we'd try to make non-bash hooks the golden path, and try to build some nicer tooling around them, and encourage it that way, but keep env capture around as a simple mode? i'm not 100% sure, i may or may or may not have thought that far ahead 😅

@moskyb moskyb force-pushed the polyglot-hooks branch 2 times, most recently from 2527108 to 81976a2 Compare May 4, 2023 00:22
Copy link
Contributor

@DrJosh9000 DrJosh9000 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking pretty good 🥞

experiments/experiments.go Outdated Show resolved Hide resolved
hook/binary.go Outdated Show resolved Hide resolved
bootstrap/integration/hooks_integration_test.go Outdated Show resolved Hide resolved
hook/type_test.go Outdated Show resolved Hide resolved
hook/type_test.go Show resolved Hide resolved
@moskyb moskyb force-pushed the polyglot-hooks branch 4 times, most recently from 2b791bc to 2f7e950 Compare May 18, 2023 02:57
@moskyb moskyb marked this pull request as ready for review May 18, 2023 03:45
@moskyb moskyb requested review from pda, DrJosh9000 and triarius May 18, 2023 03:45
@moskyb moskyb force-pushed the polyglot-hooks branch 2 times, most recently from a73416c to 6b534b1 Compare May 18, 2023 04:35
Copy link
Contributor

@triarius triarius left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for your service @moskyb 🫡

I think my only blocking comment is for the scriptwrapper tests. It may no longer be necessary to write them, though because of some combination of other tests. If so, I think you should delete the commented code. If not, you should still delete it. We are all pretty good at git archaeology.

// Potentially there's something that we could do with file extensions, but that's a bit of a minefield, and would
// probably mean that we have to have a list of blessed hook runtimes on windows only... or something.

// Regardless: not supported right now, or potentially ever.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💯% reasonable to cut this from scope.

experiments/experiments.go Outdated Show resolved Hide resolved
Comment on lines +200 to +203
if e == nil {
return New()
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I sort of expect a copy of nil to be nil, but it's probably more pragmatic for it to be New().

Env: map[string]string{
"OCEAN": "Pacífico",
"MOUNTAIN": "chimborazo",
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good thinking to test the jobapi as well 🧠

Comment on lines +285 to +308
func (s *Shell) RunWithEnv(ctx context.Context, environ *env.Environment, command string, arg ...string) error {
formatted := process.FormatCommand(command, arg)
if s.stdin == nil {
s.Promptf("%s", formatted)
} else {
// bash-syntax-compatible indication that input is coming from somewhere
s.Promptf("%s < /dev/stdin", formatted)
}

cmd, err := s.buildCommand(command, arg...)
if err != nil {
s.Errorf("Error building command: %v", err)
return err
}

cmd.Env = append(cmd.Env, environ.ToSlice()...)

return s.executeCommand(ctx, cmd, s.Writer, executeFlags{
Stdout: true,
Stderr: true,
PTY: s.PTY,
})
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes please

scripts/generate-test-binaries.sh Show resolved Hide resolved
hook/type.go Show resolved Hide resolved
Comment on lines 148 to 150
if shebang != "" && !shellscript.IsPOSIXShell(shebang) {
return nil, fmt.Errorf("hook starts with an unsupported shebang line %q", shebang)
}
// if shebang != "" && !shellscript.IsPOSIXShell(shebang) {
// return nil, fmt.Errorf("hook starts with an unsupported shebang line %q", shebang)
// }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have we gotten to properly testing this?

moskyb added 8 commits May 19, 2023 16:09
The server needs to be able to handle JSON nulls as values, so it should use a map[string]*string, but the client library should just not allow anyone to send nulls, so it should use a map[string]string. This has the side-benefit of making the DX of actually using the library much better
bootstrap/bootstrap.go Outdated Show resolved Hide resolved
bootstrap/integration/hooks_integration_test.go Outdated Show resolved Hide resolved
bootstrap/integration/hooks_integration_test.go Outdated Show resolved Hide resolved
bootstrap/integration/hooks_integration_test.go Outdated Show resolved Hide resolved
Copy link
Contributor

@DrJosh9000 DrJosh9000 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome! Ship it 🚢

@moskyb moskyb merged commit 01542e0 into main May 22, 2023
@moskyb moskyb deleted the polyglot-hooks branch May 22, 2023 03:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants