You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

182 lines
4.2 KiB

// 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()
}