summaryrefslogtreecommitdiff
path: root/cmd
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 /cmd
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
Diffstat (limited to 'cmd')
-rw-r--r--cmd/blubber/main.go84
-rw-r--r--cmd/blubberoid/main.go119
2 files changed, 203 insertions, 0 deletions
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...))
+}