diff options
author | Dan Duvall <dduvall@wikimedia.org> | 2018-08-14 16:33:37 -0700 |
---|---|---|
committer | Dan Duvall <dduvall@wikimedia.org> | 2018-08-29 15:35:40 -0700 |
commit | 9bfa2dd194c38de06cd3cf02afb9f9e668f782ef (patch) | |
tree | 8071381a4d00a136a1c132ef33cf193526ef63d2 | |
parent | b6c9e31fc5e7965e52614b2fc82dda7403476061 (diff) | |
download | blubber-9bfa2dd194c38de06cd3cf02afb9f9e668f782ef.tar.gz |
Provide a stateless blubberoid microservice
The `blubber` command already gets everything it needs from explicit
inputs, which makes it an easy candidate for running as a simple
microservice. This patch provides exactly that in the form of
`blubberoid`, an HTTP server that processes Blubber configuration.
To start the daemon:
make && blubberoid
To use it:
curl -i -X POST --data-binary @blubber.example.yaml http://:8748/[variant]
Change-Id: Ieea73048d092b974da424ba40ddc90eaf693af0b
-rw-r--r-- | Makefile | 8 | ||||
-rw-r--r-- | cmd/blubber/main.go (renamed from main.go) | 0 | ||||
-rw-r--r-- | cmd/blubberoid/main.go | 119 |
3 files changed, 123 insertions, 4 deletions
@@ -15,18 +15,18 @@ install: # workaround bug in case CURDIR is a symlink # see https://github.com/golang/go/issues/24359 cd "$(REAL_CURDIR)" && \ - go install -v -ldflags "$(GO_LDFLAGS)" + go install -v -ldflags "$(GO_LDFLAGS)" $(GO_PACKAGES) release: - gox -output="$(RELEASE_DIR)/{{.OS}}-{{.Arch}}/{{.Dir}}" -osarch='$(TARGETS)' -ldflags '$(GO_LDFLAGS)' $(PACKAGE) + gox -output="$(RELEASE_DIR)/{{.OS}}-{{.Arch}}/{{.Dir}}" -osarch='$(TARGETS)' -ldflags '$(GO_LDFLAGS)' $(GO_PACKAGES) cp LICENSE "$(RELEASE_DIR)" - for f in "$(RELEASE_DIR)"/*/blubber; do \ + for f in "$(RELEASE_DIR)"/*/{blubber,blubberd}; do \ shasum -a 256 "$${f}" | awk '{print $$1}' > "$${f}.sha256"; \ done lint: @echo > .lint-gofmt.diff - @go list -f $(GO_LIST_GOFILES) ./... | while read f; do \ + @go list -f $(GO_LIST_GOFILES) $(GO_PACKAGES) | while read f; do \ gofmt -e -d "$${f}" >> .lint-gofmt.diff; \ done @test -z "$(grep '[^[:blank:]]' .lint-gofmt.diff)" || (echo "gofmt found errors:"; cat .lint-gofmt.diff; exit 1) diff --git a/main.go b/cmd/blubber/main.go index 53009ea..53009ea 100644 --- a/main.go +++ b/cmd/blubber/main.go diff --git a/cmd/blubberoid/main.go b/cmd/blubberoid/main.go new file mode 100644 index 0000000..b15f603 --- /dev/null +++ b/cmd/blubberoid/main.go @@ -0,0 +1,119 @@ +// Package main provides the blubberoid server. +// +package main + +import ( + "fmt" + "io/ioutil" + "log" + "net/http" + "os" + "path" + + "github.com/pborman/getopt/v2" + + "gerrit.wikimedia.org/r/blubber/config" + "gerrit.wikimedia.org/r/blubber/docker" +) + +var ( + showHelp *bool = getopt.BoolLong("help", 'h', "show help/usage") + address *string = getopt.StringLong("address", 'a', ":8748", "socket address/port to listen on (default ':8748')", "address:port") + endpoint *string = getopt.StringLong("endpoint", 'e', "/", "server endpoint (default '/')", "path") + policyURI *string = getopt.StringLong("policy", 'p', "", "policy file URI", "uri") + policy *config.Policy +) + +func main() { + getopt.Parse() + + if *showHelp { + getopt.Usage() + os.Exit(1) + } + + if *policyURI != "" { + var err error + + policy, err = config.ReadPolicyFromURI(*policyURI) + + if err != nil { + log.Fatalf("Error loading policy from %s: %v\n", *policyURI, err) + } + } + + // Ensure endpoint is always an absolute path starting and ending with "/" + *endpoint = path.Clean("/" + *endpoint) + + if *endpoint != "/" { + *endpoint += "/" + } + + log.Printf("listening on %s for requests to %s[variant]\n", *address, *endpoint) + + http.HandleFunc(*endpoint, blubberoid) + log.Fatal(http.ListenAndServe(*address, nil)) +} + +func blubberoid(res http.ResponseWriter, req *http.Request) { + if len(req.URL.Path) <= len(*endpoint) { + res.WriteHeader(http.StatusNotFound) + res.Write(responseBody("request a variant at %s[variant]", *endpoint)) + return + } + + variant := req.URL.Path[len(*endpoint):] + body, err := ioutil.ReadAll(req.Body) + + if err != nil { + res.WriteHeader(http.StatusInternalServerError) + log.Printf("failed to read request body: %s\n", err) + return + } + + cfg, err := config.ReadConfig(body) + + if err != nil { + if config.IsValidationError(err) { + res.WriteHeader(http.StatusUnprocessableEntity) + res.Write(responseBody(config.HumanizeValidationError(err))) + return + } + + res.WriteHeader(http.StatusBadRequest) + res.Write(responseBody( + "Failed to read config YAML from request body. "+ + "Was it formatted correctly and encoded as binary data?\nerror: %s", + err.Error(), + )) + return + } + + if policy != nil { + err = policy.Validate(*cfg) + + if err != nil { + res.WriteHeader(http.StatusUnprocessableEntity) + res.Write(responseBody( + "Configuration fails policy check against:\npolicy: %s\nviolation: %v", + *policyURI, err, + )) + return + } + } + + dockerFile, err := docker.Compile(cfg, variant) + + if err != nil { + res.WriteHeader(http.StatusNotFound) + res.Write(responseBody(err.Error())) + return + } + + res.Header().Set("Content-Type", "text/plain") + res.Write(dockerFile.Bytes()) +} + +func responseBody(msg string, a ...interface{}) []byte { + return []byte(fmt.Sprintf(msg+"\n", a...)) +} |