diff options
author | Dan Duvall <dduvall@wikimedia.org> | 2018-09-19 11:06:46 -0700 |
---|---|---|
committer | Dan Duvall <dduvall@wikimedia.org> | 2018-12-12 15:02:22 -0800 |
commit | 4df63a83c93ba0f591eb78110e184bfef0f76535 (patch) | |
tree | 9a0c97f54432cf7ecb30ff910d5f514b51f02ed9 /cmd/blubberoid/main.go | |
parent | 3da9f201cad3b09b4e7d70dcaf023c70b4bcc026 (diff) | |
download | blubber-4df63a83c93ba0f591eb78110e184bfef0f76535.tar.gz |
Provide OpenAPI spec for Blubberoid
Wrote an OpenAPI 3.0 spec for Blubberoid that provides `x-amples`
entries compatible with service-checker.
The written spec includes basic schema for Blubber config objects that
may be later factored out for use in validation.
Note that OpenAPI 3.0 supports only the v4 draft of the JSON Schema
standard, so some parts of the configuration could not be fully
described. Specifically, v4 does not include the `patternProperties`
definition introduced in the JSON Schema v6 draft that would allow us to
describe `variants` and `runs.environment` and everything beneath.
Blubberoid was refactored slightly to incorporate the new spec as well
as assume JSON as the canonical and default configuration format. It was
also refactored to include a versioned namespace ("v1") after the server
endpoint.
Bug: T205920
Change-Id: I28a341aa503b8920d802715660d4c4d62be45475
Diffstat (limited to 'cmd/blubberoid/main.go')
-rw-r--r-- | cmd/blubberoid/main.go | 90 |
1 files changed, 67 insertions, 23 deletions
diff --git a/cmd/blubberoid/main.go b/cmd/blubberoid/main.go index d436390..19604c1 100644 --- a/cmd/blubberoid/main.go +++ b/cmd/blubberoid/main.go @@ -3,27 +3,32 @@ package main import ( - "encoding/json" + "bytes" "fmt" "io/ioutil" "log" "mime" "net/http" + "net/url" "os" "path" + "strings" + "text/template" "github.com/pborman/getopt/v2" "gerrit.wikimedia.org/r/blubber/config" "gerrit.wikimedia.org/r/blubber/docker" + "gerrit.wikimedia.org/r/blubber/meta" ) var ( - showHelp = getopt.BoolLong("help", 'h', "show help/usage") - address = getopt.StringLong("address", 'a', ":8748", "socket address/port to listen on (default ':8748')", "address:port") - endpoint = getopt.StringLong("endpoint", 'e', "/", "server endpoint (default '/')", "path") - policyURI = getopt.StringLong("policy", 'p', "", "policy file URI", "uri") - policy *config.Policy + showHelp = getopt.BoolLong("help", 'h', "show help/usage") + address = getopt.StringLong("address", 'a', ":8748", "socket address/port to listen on (default ':8748')", "address:port") + endpoint = getopt.StringLong("endpoint", 'e', "/", "server endpoint (default '/')", "path") + policyURI = getopt.StringLong("policy", 'p', "", "policy file URI", "uri") + policy *config.Policy + openAPISpec []byte ) func main() { @@ -51,7 +56,10 @@ func main() { *endpoint += "/" } - log.Printf("listening on %s for requests to %s[variant]\n", *address, *endpoint) + // Evaluate OpenAPI spec template and store results for ?spec requests + openAPISpec = readOpenAPISpec() + + log.Printf("listening on %s for requests to %sv1/[variant]\n", *address, *endpoint) http.HandleFunc(*endpoint, blubberoid) log.Fatal(http.ListenAndServe(*address, nil)) @@ -59,12 +67,35 @@ func main() { func blubberoid(res http.ResponseWriter, req *http.Request) { if len(req.URL.Path) <= len(*endpoint) { + if req.URL.RawQuery == "spec" { + res.Header().Set("Content-Type", "text/plain") + res.Write(openAPISpec) + return + } + res.WriteHeader(http.StatusNotFound) - res.Write(responseBody("request a variant at %s[variant]", *endpoint)) + res.Write(responseBody("request a variant at %sv1/[variant]", *endpoint)) + return + } + + requestPath := req.URL.Path[len(*endpoint):] + pathSegments := strings.Split(requestPath, "/") + + // Request should have been to v1/[variant] + if len(pathSegments) != 2 || pathSegments[0] != "v1" { + res.WriteHeader(http.StatusNotFound) + res.Write(responseBody("request a variant at %sv1/[variant]", *endpoint)) + return + } + + variant, err := url.PathUnescape(pathSegments[1]) + + if err != nil { + res.WriteHeader(http.StatusInternalServerError) + log.Printf("failed to unescape variant name '%s': %s\n", pathSegments[1], err) return } - variant := req.URL.Path[len(*endpoint):] body, err := ioutil.ReadAll(req.Body) if err != nil { @@ -73,25 +104,25 @@ func blubberoid(res http.ResponseWriter, req *http.Request) { return } - switch mt, _, _ := mime.ParseMediaType(req.Header.Get("content-type")); mt { + var cfg *config.Config + mediaType, _, _ := mime.ParseMediaType(req.Header.Get("content-type")) + + // Default to application/json + if mediaType == "" { + mediaType = "application/json" + } + + switch mediaType { case "application/json": - // Enforce strict JSON syntax if specified, even though the config parser - // would technically handle anything that's at least valid YAML - if !json.Valid(body) { - res.WriteHeader(http.StatusBadRequest) - res.Write(responseBody("'%s' media type given but request contains invalid JSON", mt)) - return - } + cfg, err = config.ReadConfig(body) case "application/yaml", "application/x-yaml": - // Let the config parser validate YAML syntax + cfg, err = config.ReadYAMLConfig(body) default: res.WriteHeader(http.StatusUnsupportedMediaType) - res.Write(responseBody("'%s' media type is not supported", mt)) + res.Write(responseBody("'%s' media type is not supported", mediaType)) return } - cfg, err := config.ReadYAMLConfig(body) - if err != nil { if config.IsValidationError(err) { res.WriteHeader(http.StatusUnprocessableEntity) @@ -101,8 +132,8 @@ func blubberoid(res http.ResponseWriter, req *http.Request) { 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", + "Failed to read '%s' config from request body. Error: %s", + mediaType, err.Error(), )) return @@ -136,3 +167,16 @@ func blubberoid(res http.ResponseWriter, req *http.Request) { func responseBody(msg string, a ...interface{}) []byte { return []byte(fmt.Sprintf(msg+"\n", a...)) } + +func readOpenAPISpec() []byte { + var buffer bytes.Buffer + tmpl, _ := template.New("spec").Parse(openAPISpecTemplate) + + tmpl.Execute(&buffer, struct { + Version string + }{ + Version: meta.FullVersion(), + }) + + return buffer.Bytes() +} |