Skip to content

Commit

Permalink
Allow passing payloads to rollout route (#8)
Browse files Browse the repository at this point in the history
minor
  • Loading branch information
joecorall committed Apr 18, 2024
1 parent 5425d12 commit 1091f6f
Show file tree
Hide file tree
Showing 3 changed files with 281 additions and 21 deletions.
37 changes: 34 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
Deploy your application from a CI/CD pipeline via `cURL` + JWT auth.

```
$ curl -s -H "Authorization: bearer abc..." https://example.com/your/rollout/path
$ curl -s -d '{"git-branch": "main"}' -H "Authorization: bearer abc..." https://example.com/your/rollout/path
Rollout complete
```

Expand All @@ -13,7 +13,7 @@ Instead of managing SSH keys in your CI/CD for accounts that have privileged acc

Requires creating a JWT from your CI provider, and sending that token to this service running in your deployment environment to trigger a deployment script.

Also requires a `rollout.sh` script that can handle all the command needing ran to rollout your software.
Also requires a `rollout.sh` script that can handle all the commands needing ran to rollout your software.

## Install

Expand All @@ -27,9 +27,11 @@ $ docker run \
rollout:latest
```

You should then proxy that port with some service that can handle TLS for you.

## OIDC Provider examples

This service requires two envionrment variables.
This service requires two environment variables.

- `JWKS_URI` - the URL of the OIDC Provider's [JSON Web Key (JWK) set document](https://www.rfc-editor.org/info/rfc7517). This is used to ensure the JWT was signed by the provider.
- `JWT_AUD` - the audience set in the JWT token.
Expand All @@ -40,6 +42,35 @@ This service requires two envionrment variables.
- `ROLLOUT_CMD` (default: `/bin/bash`) - the command to execute a rollout
- `ROLLOUT_ARGS` (default: `/rollout.sh` ) - the args to pass to `ROLLOUT_CMD`

## Dynamic environment variables for ROLLOUT_CMD

There are a few environment variables you can make available to your rollout command.

These environment variables can be passed to the cURL command when rolling out your changes.

For example, if you want your rollout script to have the git repo and branch that is being deployed you can pass that in the rollout cURL call as seen below. Doing so will make an environment variable `$GIT_REPO` and `$GIT_BRANCH` available in your rollout script.

```
$ curl -s \
-H "Authorization: bearer abc..." \
-d '{"git-repo": "[email protected]:lehigh-university-libraries/rollout.git", "git-branch": "main"}' \
https://example.com/your/rollout/path
```

These are the environment variables currently supported, keyed by their respective JSON key name:

| JSON Key | Env Var Name | Example JSON to send |
|----------------|---------------| -------------------------------------
| `docker-image` | `DOCKER_IMAGE`| `{"docker-image": "foo/bar:latest"}` |
| `docker-tag` | `DOCKER_TAG` | `{"docker-tag": "latest"}` |
| `git-repo` | `GIT_REPO` | `{"git-repo": "foo/bar:latest"}` |
| `git-branch` | `GIT_BRANCH` | `{"git-branch": "main"}` |
| `rollout-arg1` | `ROLLOUT_ARG1`| `{"rollout-arg1": "FOO"}` |
| `rollout-arg2` | `ROLLOUT_ARG2`| `{"rollout-arg2": "BAR"}` |
| `rollout-arg3` | `ROLLOUT_ARG3`| `{"rollout-arg3": "BAZ"}` |

If there is key/env var name that is generic enough that it warrants its own placeholder, it can be added by submitting an issue or a PR. Otherwise, use the general ARG variables.

### GitHub

```
Expand Down
80 changes: 75 additions & 5 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,27 @@ import (
"net/http"
"os"
"os/exec"
"reflect"
"regexp"

"github.com/golang-jwt/jwt/v5"
"github.com/google/shlex"
"github.com/lestrrat-go/jwx/jwk"
)

type RolloutPayload struct {
DockerImage string `json:"docker-image" env:"DOCKER_IMAGE"`
DockerTag string `json:"docker-tag" env:"DOCKER_TAG"`
GitRepo string `json:"git-repo" env:"GIT_REPO"`
GitBranch string `json:"git-branch" env:"GIT_BRANCH"`
Arg1 string `json:"rollout-arg1" env:"ROLLOUT_ARG1"`
Arg2 string `json:"rollout-arg2" env:"ROLLOUT_ARG2"`
Arg3 string `json:"rollout-arg3" env:"ROLLOUT_ARG3"`
}

func init() {
// call getArgs early to fail on a bad config
getArgs()
// call getRolloutCmdArgs early to fail on a bad config
getRolloutCmdArgs()
}

func main() {
Expand Down Expand Up @@ -88,11 +100,18 @@ func Rollout(w http.ResponseWriter, r *http.Request) {
return
}

err = setCustomArgs(r)
if err != nil {
slog.Error("Error setting custom args", "err", err)
http.Error(w, "Bad request", http.StatusBadRequest)
return
}

name := os.Getenv("ROLLOUT_CMD")
if name == "" {
name = "/bin/bash"
}
cmd := exec.Command(name, getArgs()...)
cmd := exec.Command(name, getRolloutCmdArgs()...)

var stdOut, stdErr bytes.Buffer
cmd.Stdout = &stdOut
Expand Down Expand Up @@ -132,7 +151,7 @@ func ParseToken(token *jwt.Token) (interface{}, error) {
jwksUri := os.Getenv("JWKS_URI")
jwksSet, err := jwk.Fetch(ctx, jwksUri)
if err != nil {
return nil, fmt.Errorf("Unable to fetch JWK set from %s: %v", jwksUri, err)
return nil, fmt.Errorf("unable to fetch JWK set from %s: %v", jwksUri, err)
}
// Find the appropriate key in JWKS
key, ok := jwksSet.LookupKeyID(kid)
Expand Down Expand Up @@ -166,7 +185,7 @@ func strInSlice(e string, s []string) bool {
return false
}

func getArgs() []string {
func getRolloutCmdArgs() []string {
args := os.Getenv("ROLLOUT_ARGS")
if args == "" {
args = "/rollout.sh"
Expand All @@ -179,3 +198,54 @@ func getArgs() []string {

return rolloutArgs
}

func setCustomArgs(r *http.Request) error {
if r.Method == "GET" {
return nil
}

var payload RolloutPayload
decoder := json.NewDecoder(r.Body)
err := decoder.Decode(&payload)
if err != nil {
return err
}

err = setEnvFromStruct(&payload)
if err != nil {
return err
}

return nil
}

func setEnvFromStruct(data interface{}) error {
regex, err := regexp.Compile(`^[a-zA-Z0-9._\-:\/@]+$`)
if err != nil {
return fmt.Errorf("failed to compile regex: %v", err)
}

v := reflect.ValueOf(data)
if v.Kind() == reflect.Ptr {
v = v.Elem()
}

t := v.Type()
for i := 0; i < v.NumField(); i++ {
field := t.Field(i)
if envTag, ok := field.Tag.Lookup("env"); ok {
// For now all fields are strings
value := v.Field(i).String()
if value == "" {
continue
}
if !regex.MatchString(value) {
return fmt.Errorf("invalid input for environment variable %s:%s", envTag, value)
}
if err := os.Setenv(envTag, value); err != nil {
return fmt.Errorf("could not set environment variable %s: %v", envTag, err)
}
}
}
return nil
}
Loading

0 comments on commit 1091f6f

Please sign in to comment.