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

  1. // Package main provides the blubberoid server.
  2. //
  3. package main
  4. import (
  5. "bytes"
  6. "fmt"
  7. "io/ioutil"
  8. "log"
  9. "mime"
  10. "net/http"
  11. "net/url"
  12. "os"
  13. "path"
  14. "strings"
  15. "text/template"
  16. "github.com/pborman/getopt/v2"
  17. "gerrit.wikimedia.org/r/blubber/config"
  18. "gerrit.wikimedia.org/r/blubber/docker"
  19. "gerrit.wikimedia.org/r/blubber/meta"
  20. )
  21. var (
  22. showHelp = getopt.BoolLong("help", 'h', "show help/usage")
  23. address = getopt.StringLong("address", 'a', ":8748", "socket address/port to listen on (default ':8748')", "address:port")
  24. endpoint = getopt.StringLong("endpoint", 'e', "/", "server endpoint (default '/')", "path")
  25. policyURI = getopt.StringLong("policy", 'p', "", "policy file URI", "uri")
  26. policy *config.Policy
  27. openAPISpec []byte
  28. )
  29. func main() {
  30. getopt.Parse()
  31. if *showHelp {
  32. getopt.Usage()
  33. os.Exit(1)
  34. }
  35. if *policyURI != "" {
  36. var err error
  37. policy, err = config.ReadPolicyFromURI(*policyURI)
  38. if err != nil {
  39. log.Fatalf("Error loading policy from %s: %v\n", *policyURI, err)
  40. }
  41. }
  42. // Ensure endpoint is always an absolute path starting and ending with "/"
  43. *endpoint = path.Clean("/" + *endpoint)
  44. if *endpoint != "/" {
  45. *endpoint += "/"
  46. }
  47. // Evaluate OpenAPI spec template and store results for ?spec requests
  48. openAPISpec = readOpenAPISpec()
  49. log.Printf("listening on %s for requests to %sv1/[variant]\n", *address, *endpoint)
  50. http.HandleFunc(*endpoint, blubberoid)
  51. log.Fatal(http.ListenAndServe(*address, nil))
  52. }
  53. func blubberoid(res http.ResponseWriter, req *http.Request) {
  54. if len(req.URL.Path) <= len(*endpoint) {
  55. if req.URL.RawQuery == "spec" {
  56. res.Header().Set("Content-Type", "text/plain")
  57. res.Write(openAPISpec)
  58. return
  59. }
  60. res.WriteHeader(http.StatusNotFound)
  61. res.Write(responseBody("request a variant at %sv1/[variant]", *endpoint))
  62. return
  63. }
  64. requestPath := req.URL.Path[len(*endpoint):]
  65. pathSegments := strings.Split(requestPath, "/")
  66. // Request should have been to v1/[variant]
  67. if len(pathSegments) != 2 || pathSegments[0] != "v1" {
  68. res.WriteHeader(http.StatusNotFound)
  69. res.Write(responseBody("request a variant at %sv1/[variant]", *endpoint))
  70. return
  71. }
  72. variant, err := url.PathUnescape(pathSegments[1])
  73. if err != nil {
  74. res.WriteHeader(http.StatusInternalServerError)
  75. log.Printf("failed to unescape variant name '%s': %s\n", pathSegments[1], err)
  76. return
  77. }
  78. body, err := ioutil.ReadAll(req.Body)
  79. if err != nil {
  80. res.WriteHeader(http.StatusInternalServerError)
  81. log.Printf("failed to read request body: %s\n", err)
  82. return
  83. }
  84. var cfg *config.Config
  85. mediaType, _, _ := mime.ParseMediaType(req.Header.Get("content-type"))
  86. // Default to application/json
  87. if mediaType == "" {
  88. mediaType = "application/json"
  89. }
  90. switch mediaType {
  91. case "application/json":
  92. cfg, err = config.ReadConfig(body)
  93. case "application/yaml", "application/x-yaml":
  94. cfg, err = config.ReadYAMLConfig(body)
  95. default:
  96. res.WriteHeader(http.StatusUnsupportedMediaType)
  97. res.Write(responseBody("'%s' media type is not supported", mediaType))
  98. return
  99. }
  100. if err != nil {
  101. if config.IsValidationError(err) {
  102. res.WriteHeader(http.StatusUnprocessableEntity)
  103. res.Write(responseBody(config.HumanizeValidationError(err)))
  104. return
  105. }
  106. res.WriteHeader(http.StatusBadRequest)
  107. res.Write(responseBody(
  108. "Failed to read '%s' config from request body. Error: %s",
  109. mediaType,
  110. err.Error(),
  111. ))
  112. return
  113. }
  114. if policy != nil {
  115. err = policy.Validate(*cfg)
  116. if err != nil {
  117. res.WriteHeader(http.StatusUnprocessableEntity)
  118. res.Write(responseBody(
  119. "Configuration fails policy check against:\npolicy: %s\nviolation: %v",
  120. *policyURI, err,
  121. ))
  122. return
  123. }
  124. }
  125. dockerFile, err := docker.Compile(cfg, variant)
  126. if err != nil {
  127. res.WriteHeader(http.StatusNotFound)
  128. res.Write(responseBody(err.Error()))
  129. return
  130. }
  131. res.Header().Set("Content-Type", "text/plain")
  132. res.Write(dockerFile.Bytes())
  133. }
  134. func responseBody(msg string, a ...interface{}) []byte {
  135. return []byte(fmt.Sprintf(msg+"\n", a...))
  136. }
  137. func readOpenAPISpec() []byte {
  138. var buffer bytes.Buffer
  139. tmpl, _ := template.New("spec").Parse(openAPISpecTemplate)
  140. tmpl.Execute(&buffer, struct {
  141. Version string
  142. }{
  143. Version: meta.FullVersion(),
  144. })
  145. return buffer.Bytes()
  146. }