// Package main provides the blubberoid server.
|
|
//
|
|
package main
|
|
|
|
import (
|
|
"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
|
|
openAPISpec []byte
|
|
)
|
|
|
|
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 += "/"
|
|
}
|
|
|
|
// 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))
|
|
}
|
|
|
|
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 %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
|
|
}
|
|
|
|
body, err := ioutil.ReadAll(req.Body)
|
|
|
|
if err != nil {
|
|
res.WriteHeader(http.StatusInternalServerError)
|
|
log.Printf("failed to read request body: %s\n", err)
|
|
return
|
|
}
|
|
|
|
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":
|
|
cfg, err = config.ReadConfig(body)
|
|
case "application/yaml", "application/x-yaml":
|
|
cfg, err = config.ReadYAMLConfig(body)
|
|
default:
|
|
res.WriteHeader(http.StatusUnsupportedMediaType)
|
|
res.Write(responseBody("'%s' media type is not supported", mediaType))
|
|
return
|
|
}
|
|
|
|
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 '%s' config from request body. Error: %s",
|
|
mediaType,
|
|
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...))
|
|
}
|
|
|
|
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()
|
|
}
|