From 9bfa2dd194c38de06cd3cf02afb9f9e668f782ef Mon Sep 17 00:00:00 2001 From: Dan Duvall Date: Tue, 14 Aug 2018 16:33:37 -0700 Subject: 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 --- Makefile | 8 ++-- cmd/blubber/main.go | 84 ++++++++++++++++++++++++++++++++++ cmd/blubberoid/main.go | 119 +++++++++++++++++++++++++++++++++++++++++++++++++ main.go | 84 ---------------------------------- 4 files changed, 207 insertions(+), 88 deletions(-) create mode 100644 cmd/blubber/main.go create mode 100644 cmd/blubberoid/main.go delete mode 100644 main.go 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/cmd/blubber/main.go b/cmd/blubber/main.go new file mode 100644 index 0000000..53009ea --- /dev/null +++ b/cmd/blubber/main.go @@ -0,0 +1,84 @@ +// Package main provides the command line interface. +// +package main + +import ( + "fmt" + "log" + "os" + + "github.com/pborman/getopt/v2" + + "gerrit.wikimedia.org/r/blubber/config" + "gerrit.wikimedia.org/r/blubber/docker" + "gerrit.wikimedia.org/r/blubber/meta" +) + +const parameters = "config.yaml variant" + +var ( + showHelp *bool = getopt.BoolLong("help", 'h', "show help/usage") + showVersion *bool = getopt.BoolLong("version", 'v', "show version information") + policyURI *string = getopt.StringLong("policy", 'p', "", "policy file URI", "uri") +) + +func main() { + getopt.SetParameters(parameters) + getopt.Parse() + + if *showHelp { + getopt.Usage() + os.Exit(1) + } + + if *showVersion { + fmt.Println(meta.FullVersion()) + os.Exit(0) + } + + args := getopt.Args() + + if len(args) < 2 { + getopt.Usage() + os.Exit(1) + } + + cfgPath, variant := args[0], args[1] + + cfg, err := config.ReadConfigFile(cfgPath) + + if err != nil { + if config.IsValidationError(err) { + log.Printf("%s is invalid:\n%v", cfgPath, config.HumanizeValidationError(err)) + os.Exit(4) + } else { + log.Printf("Error reading %s: %v\n", cfgPath, err) + os.Exit(2) + } + } + + if *policyURI != "" { + policy, err := config.ReadPolicyFromURI(*policyURI) + + if err != nil { + log.Printf("Error loading policy from %s: %v\n", *policyURI, err) + os.Exit(5) + } + + err = policy.Validate(*cfg) + + if err != nil { + log.Printf("Configuration fails policy check against:\npolicy: %s\nviolation: %v\n", *policyURI, err) + os.Exit(6) + } + } + + dockerFile, err := docker.Compile(cfg, variant) + + if err != nil { + log.Printf("Error compiling config: %v\n", err) + os.Exit(3) + } + + dockerFile.WriteTo(os.Stdout) +} 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...)) +} diff --git a/main.go b/main.go deleted file mode 100644 index 53009ea..0000000 --- a/main.go +++ /dev/null @@ -1,84 +0,0 @@ -// Package main provides the command line interface. -// -package main - -import ( - "fmt" - "log" - "os" - - "github.com/pborman/getopt/v2" - - "gerrit.wikimedia.org/r/blubber/config" - "gerrit.wikimedia.org/r/blubber/docker" - "gerrit.wikimedia.org/r/blubber/meta" -) - -const parameters = "config.yaml variant" - -var ( - showHelp *bool = getopt.BoolLong("help", 'h', "show help/usage") - showVersion *bool = getopt.BoolLong("version", 'v', "show version information") - policyURI *string = getopt.StringLong("policy", 'p', "", "policy file URI", "uri") -) - -func main() { - getopt.SetParameters(parameters) - getopt.Parse() - - if *showHelp { - getopt.Usage() - os.Exit(1) - } - - if *showVersion { - fmt.Println(meta.FullVersion()) - os.Exit(0) - } - - args := getopt.Args() - - if len(args) < 2 { - getopt.Usage() - os.Exit(1) - } - - cfgPath, variant := args[0], args[1] - - cfg, err := config.ReadConfigFile(cfgPath) - - if err != nil { - if config.IsValidationError(err) { - log.Printf("%s is invalid:\n%v", cfgPath, config.HumanizeValidationError(err)) - os.Exit(4) - } else { - log.Printf("Error reading %s: %v\n", cfgPath, err) - os.Exit(2) - } - } - - if *policyURI != "" { - policy, err := config.ReadPolicyFromURI(*policyURI) - - if err != nil { - log.Printf("Error loading policy from %s: %v\n", *policyURI, err) - os.Exit(5) - } - - err = policy.Validate(*cfg) - - if err != nil { - log.Printf("Configuration fails policy check against:\npolicy: %s\nviolation: %v\n", *policyURI, err) - os.Exit(6) - } - } - - dockerFile, err := docker.Compile(cfg, variant) - - if err != nil { - log.Printf("Error compiling config: %v\n", err) - os.Exit(3) - } - - dockerFile.WriteTo(os.Stdout) -} -- cgit v1.2.1