summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDan Duvall <dduvall@wikimedia.org>2018-08-14 16:33:37 -0700
committerDan Duvall <dduvall@wikimedia.org>2018-08-29 15:35:40 -0700
commit9bfa2dd194c38de06cd3cf02afb9f9e668f782ef (patch)
tree8071381a4d00a136a1c132ef33cf193526ef63d2
parentb6c9e31fc5e7965e52614b2fc82dda7403476061 (diff)
downloadblubber-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--Makefile8
-rw-r--r--cmd/blubber/main.go (renamed from main.go)0
-rw-r--r--cmd/blubberoid/main.go119
3 files changed, 123 insertions, 4 deletions
diff --git a/Makefile b/Makefile
index 3fbc19d..decdf84 100644
--- a/Makefile
+++ b/Makefile
@@ -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...))
+}