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