Skip to content

Commit 67c4e22

Browse files
authored
Merge pull request #37 from buildkite/go
10–100x speed-up: s3secrets-helper written in Go
2 parents 48c2116 + ca69f16 commit 67c4e22

File tree

16 files changed

+820
-242
lines changed

16 files changed

+820
-242
lines changed

.buildkite/test_credentials.sh

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,21 @@ pre_exit() {
77
source hooks/pre-exit
88
}
99

10+
# hooks/environment assumes `s3secrets-helper` command is available, normally
11+
# by way of that binary existing in $PATH. But a shell function works too.
12+
# Rather than build/install s3secrets-helper onto the build agent, build and
13+
# run it inside docker.
14+
# Feel free to replace this terrible hack with something better.
15+
s3secrets-helper() {
16+
docker run \
17+
--rm \
18+
--volume $(pwd)/s3secrets-helper:/s3secrets-helper \
19+
--workdir /s3secrets-helper \
20+
--env-file <(env | egrep 'AWS|BUILDKITE') \
21+
golang:1.15 \
22+
bash -c 'go build && ./s3secrets-helper'
23+
}
24+
1025
trap pre_exit EXIT
1126
source hooks/environment
1227

README.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,12 @@ Different types of secrets are supported and exposed to your builds in appropria
1212

1313
The hooks needs to be installed directly in the agent so that secrets can be downloaded before jobs attempt checking out your repository. We are going to assume that buildkite has been installed at `/buildkite`, but this will vary depending on your operating system. Change the instructions accordingly.
1414

15+
The core of the hook is an `s3secrets-helper` binary, the result of `go build` in the `s3secrets-helper/` subdirectory of this repository. It must be placed in `$PATH` to be found by the `hooks/environment` wrapper script.
16+
1517
```bash
1618
# clone to a path your buildkite-agent can access
1719
git clone https://github.com/buildkite-plugins/s3-secrets-buildkite-plugin.git /buildkite/s3_secrets
20+
(cd /buildkite/s3_secrets/s3secrets-helper && go build -o /usr/local/bin/s3secrets-helper)
1821
```
1922

2023
Modify your agent's global hooks (see [https://buildkite.com/docs/agent/v3/hooks#global-hooks](https://buildkite.com/docs/agent/v3/hooks#global-hooks)):
@@ -50,7 +53,9 @@ When run via the agent environment and pre-exit hook, your builds will check in
5053
- `s3://{bucket_name}/environment` or `s3://{bucket_name}/env`
5154
- `s3://{bucket_name}/git-credentials`
5255

53-
The private key is exposed to both the checkout and the command as an ssh-agent instance. The secrets in the env file are exposed as environment variables.
56+
The private key is exposed to both the checkout and the command as an ssh-agent instance.
57+
The secrets in the env file are exposed as environment variables.
58+
The locations of git-credentials are passed via `GIT_CONFIG_PARAMETERS` environment to git.
5459

5560
## Uploading Secrets
5661

hooks/environment

Lines changed: 11 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -1,107 +1,20 @@
11
#!/bin/bash
2-
3-
set -eu -o pipefail
2+
set -e -o pipefail -u
43

54
basedir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && cd .. && pwd )"
5+
credhelper="$basedir/git-credential-s3-secrets"
66

7-
# shellcheck disable=SC1090
8-
. "$basedir/lib/shared.bash"
9-
10-
# For resiliency, increase the number of attempts to retrieve credentials for IAM roles
11-
# The default value is 1
12-
export AWS_METADATA_SERVICE_NUM_ATTEMPTS=3
13-
14-
TMPDIR=${TMPDIR:-/tmp}
15-
AWS_DEFAULT_REGION=${AWS_DEFAULT_REGION:-us-east-1}
16-
17-
s3_bucket="${BUILDKITE_PLUGIN_S3_SECRETS_BUCKET:-}"
18-
s3_bucket_prefix="${BUILDKITE_PLUGIN_S3_SECRETS_BUCKET_PREFIX:-$BUILDKITE_PIPELINE_SLUG}"
19-
20-
if [[ -z "$s3_bucket" ]] ; then
21-
exit 0
22-
fi
23-
24-
echo "~~~ Downloading secrets from :s3: $s3_bucket" >&2;
25-
26-
if ! s3_bucket_exists "$s3_bucket" ; then
27-
echo "+++ :warning: Bucket $s3_bucket doesn't exist" >&2;
28-
exit 1
29-
fi
30-
31-
ssh_key_paths=(
32-
"$s3_bucket_prefix/private_ssh_key"
33-
"$s3_bucket_prefix/id_rsa_github"
34-
"private_ssh_key"
35-
"id_rsa_github"
36-
)
37-
38-
for key in ${ssh_key_paths[*]} ; do
39-
echo "Checking ${key}" >&2
40-
if s3_exists "$s3_bucket" "$key" ; then
41-
echo "Found ${key}, downloading" >&2;
42-
if ! ssh_key=$(s3_download "${s3_bucket}" "$key") ; then
43-
echo "+++ :warning: Failed to download ssh-key $key" >&2;
44-
exit 1
45-
fi
46-
echo "Downloaded ${#ssh_key} bytes of ssh key"
47-
add_ssh_private_key_to_agent "$ssh_key"
48-
key_found=1
49-
elif [[ $? -eq 2 ]] ; then
50-
echo "+++ :warning: Failed to check if $key exists" >&2;
51-
exit 1
52-
fi
53-
done
54-
55-
if [[ -z "${key_found:-}" ]] && [[ "${BUILDKITE_REPO:-}" =~ ^git@ ]] ; then
56-
echo >&2 "+++ :warning: Failed to find an SSH key in secret bucket"
57-
echo >&2 "The repository '$BUILDKITE_REPO' appears to use SSH for transport, but the elastic-ci-stack-s3-secrets-hooks plugin did not find any SSH keys in the $s3_bucket S3 bucket."
58-
echo >&2 "See https://github.com/buildkite/elastic-ci-stack-for-aws#build-secrets for more information."
59-
fi
60-
61-
env_paths=(
62-
"env"
63-
"environment"
64-
"${s3_bucket_prefix}/env"
65-
"${s3_bucket_prefix}/environment"
66-
)
7+
# s3secrets-helper must be in PATH
8+
envscript="$(
9+
BUILDKITE_PLUGIN_S3_SECRETS_CREDHELPER="$credhelper" \
10+
s3secrets-helper
11+
)"
6712

6813
env_before="$(env | sort)"
69-
70-
for key in ${env_paths[*]} ; do
71-
echo "Checking ${key}" >&2
72-
if s3_exists "$s3_bucket" "$key" ; then
73-
echo "Downloading env file from ${key}" >&2;
74-
if ! envscript=$(s3_download "${s3_bucket}" "$key") ; then
75-
echo "+++ :warning: Failed to download env from $key" >&2;
76-
exit 1
77-
fi
78-
echo "Evaluating ${#envscript} bytes of env"
79-
set -o allexport
80-
eval "$envscript"
81-
set +o allexport
82-
elif [[ $? -eq 2 ]] ; then
83-
echo "Failed to check if $key exists" >&2;
84-
fi
85-
done
86-
87-
git_credentials_paths=(
88-
"git-credentials"
89-
"${s3_bucket_prefix}/git-credentials"
90-
)
91-
92-
git_credentials=()
93-
94-
for key in ${git_credentials_paths[*]} ; do
95-
if s3_exists "$s3_bucket" "$key" ; then
96-
echo "Adding git-credentials in $key as a credential helper" >&2;
97-
git_credentials+=("'credential.helper=$basedir/git-credential-s3-secrets ${s3_bucket} ${key}'")
98-
fi
99-
done
100-
101-
if [[ "${#git_credentials[@]}" -gt 0 ]] ; then
102-
export GIT_CONFIG_PARAMETERS
103-
GIT_CONFIG_PARAMETERS=$( IFS=' '; echo -n "${git_credentials[*]}" )
104-
fi
14+
echo "Evaluating ${#envscript} bytes of env"
15+
set -o allexport
16+
eval "$envscript"
17+
set +o allexport
10518

10619
if [[ "${BUILDKITE_PLUGIN_S3_SECRETS_DUMP_ENV:-}" =~ ^(true|1)$ ]] ; then
10720
echo "~~~ Environment variables that were set" >&2;

lib/shared.bash

Lines changed: 0 additions & 52 deletions
This file was deleted.

s3secrets-helper/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# default `go build` binary
2+
/s3secrets-helper

s3secrets-helper/go.mod

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
module github.com/buildkite/elastic-ci-stack-s3-secrets-hooks/s3secrets-helper/v2
2+
3+
go 1.15
4+
5+
require github.com/aws/aws-sdk-go v1.35.14

s3secrets-helper/go.sum

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
github.com/aws/aws-sdk-go v1.35.14 h1:nucVVXXjAr9UkmYCBWxQWRuYa5KOlaXjuJGg2ulW0K0=
2+
github.com/aws/aws-sdk-go v1.35.14/go.mod h1:tlPOdRjfxPBpNIwqDj61rmsnA85v9jc0Ps9+muhnW+k=
3+
github.com/buildkite/elastic-ci-stack-s3-secrets-hooks v1.3.0 h1:CRdPqLjYECJY0cgs24E4ZwUJ2qlmcavN7W1jo+5ujnQ=
4+
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
5+
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
6+
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
7+
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
8+
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
9+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
10+
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
11+
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
12+
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
13+
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
14+
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
15+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
16+
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

s3secrets-helper/main.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"log"
6+
"os"
7+
8+
"github.com/buildkite/elastic-ci-stack-s3-secrets-hooks/s3secrets-helper/v2/s3"
9+
"github.com/buildkite/elastic-ci-stack-s3-secrets-hooks/s3secrets-helper/v2/secrets"
10+
"github.com/buildkite/elastic-ci-stack-s3-secrets-hooks/s3secrets-helper/v2/sshagent"
11+
)
12+
13+
const (
14+
envBucket = "BUILDKITE_PLUGIN_S3_SECRETS_BUCKET"
15+
envPrefix = "BUILDKITE_PLUGIN_S3_SECRETS_BUCKET_PREFIX"
16+
envPipeline = "BUILDKITE_PIPELINE_SLUG"
17+
envRepo = "BUILDKITE_REPO"
18+
envCredHelper = "BUILDKITE_PLUGIN_S3_SECRETS_CREDHELPER"
19+
20+
envDefaultRegion = "AWS_DEFAULT_REGION"
21+
defaultRegion = "us-east-1"
22+
)
23+
24+
func main() {
25+
log := log.New(os.Stderr, "", log.Lmsgprefix)
26+
if err := mainWithError(log); err != nil {
27+
log.Fatalf("fatal error: %v", err)
28+
}
29+
}
30+
31+
func mainWithError(log *log.Logger) error {
32+
bucket := os.Getenv(envBucket)
33+
if bucket == "" {
34+
return nil
35+
}
36+
37+
prefix := os.Getenv(envPrefix)
38+
if prefix == "" {
39+
prefix = os.Getenv(envPipeline)
40+
}
41+
if prefix == "" {
42+
return fmt.Errorf("%s or %s required", envPrefix, envPipeline)
43+
}
44+
45+
region := os.Getenv(envDefaultRegion)
46+
if region == "" {
47+
region = defaultRegion
48+
}
49+
client, err := s3.New(region)
50+
if err != nil {
51+
return err
52+
}
53+
54+
agent := &sshagent.Agent{}
55+
56+
credHelper := os.Getenv(envCredHelper)
57+
if credHelper == "" {
58+
return fmt.Errorf("%s required", envCredHelper)
59+
}
60+
61+
return secrets.Run(secrets.Config{
62+
Repo: os.Getenv(envRepo),
63+
Bucket: bucket,
64+
Prefix: prefix,
65+
Client: client,
66+
Logger: log,
67+
SSHAgent: agent,
68+
EnvSink: os.Stdout,
69+
GitCredentialHelper: credHelper,
70+
})
71+
}

s3secrets-helper/s3/s3.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package s3
2+
3+
import (
4+
"io/ioutil"
5+
6+
"github.com/aws/aws-sdk-go/aws"
7+
"github.com/aws/aws-sdk-go/aws/awserr"
8+
"github.com/aws/aws-sdk-go/aws/session"
9+
"github.com/aws/aws-sdk-go/service/s3"
10+
"github.com/buildkite/elastic-ci-stack-s3-secrets-hooks/s3secrets-helper/v2/sentinel"
11+
)
12+
13+
type Client struct {
14+
s3 *s3.S3
15+
}
16+
17+
func New(region string) (*Client, error) {
18+
sess, err := session.NewSession(&aws.Config{
19+
Region: &region,
20+
})
21+
if err != nil {
22+
return nil, err
23+
}
24+
return &Client{
25+
s3: s3.New(sess),
26+
}, nil
27+
}
28+
29+
// Get downloads an object from S3.
30+
// Intended for small files; object is fully read into memory.
31+
// sentinel.ErrNotFound and sentinel.ErrForbidden are returned for those cases.
32+
// Other errors are returned verbatim.
33+
func (c *Client) Get(bucket, key string) ([]byte, error) {
34+
out, err := c.s3.GetObject(&s3.GetObjectInput{
35+
Bucket: &bucket,
36+
Key: &key,
37+
})
38+
if err != nil {
39+
if aerr, ok := err.(awserr.Error); ok {
40+
switch aerr.Code() {
41+
case "NoSuchKey":
42+
return nil, sentinel.ErrNotFound
43+
case "Forbidden":
44+
return nil, sentinel.ErrForbidden
45+
default:
46+
return nil, aerr
47+
}
48+
} else {
49+
return nil, err
50+
}
51+
}
52+
defer out.Body.Close()
53+
// we probably should return io.Reader or io.ReadCloser rather than []byte,
54+
// maybe somebody should refactor that (and all the tests etc) one day.
55+
return ioutil.ReadAll(out.Body)
56+
}
57+
58+
// BucketExists returns whether the bucket exists.
59+
// 200 OK returns true without error.
60+
// 404 Not Found and 403 Forbidden return false without error.
61+
// Other errors result in false with an error.
62+
func (c *Client) BucketExists(bucket string) (bool, error) {
63+
if _, err := c.s3.HeadBucket(&s3.HeadBucketInput{Bucket: &bucket}); err != nil {
64+
if aerr, ok := err.(awserr.Error); ok {
65+
switch aerr.Code() {
66+
// https://github.com/aws/aws-sdk-go/issues/2593#issuecomment-491436818
67+
case "NoSuchBucket", "NotFound":
68+
return false, nil
69+
default: // e.g. NoCredentialProviders, Forbidden
70+
return false, aerr
71+
}
72+
} else {
73+
return false, err
74+
}
75+
}
76+
return true, nil
77+
}

0 commit comments

Comments
 (0)