summaryrefslogtreecommitdiff
path: root/cmd/blubberoid/main.go
blob: d436390cac98833fde478c4114c704f08682b781 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
// Package main provides the blubberoid server.
//
package main

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"log"
	"mime"
	"net/http"
	"os"
	"path"

	"github.com/pborman/getopt/v2"

	"gerrit.wikimedia.org/r/blubber/config"
	"gerrit.wikimedia.org/r/blubber/docker"
)

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
)

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 += "/"
	}

	log.Printf("listening on %s for requests to %s[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) {
		res.WriteHeader(http.StatusNotFound)
		res.Write(responseBody("request a variant at %s[variant]", *endpoint))
		return
	}

	variant := req.URL.Path[len(*endpoint):]
	body, err := ioutil.ReadAll(req.Body)

	if err != nil {
		res.WriteHeader(http.StatusInternalServerError)
		log.Printf("failed to read request body: %s\n", err)
		return
	}

	switch mt, _, _ := mime.ParseMediaType(req.Header.Get("content-type")); mt {
	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
		}
	case "application/yaml", "application/x-yaml":
		// Let the config parser validate YAML syntax
	default:
		res.WriteHeader(http.StatusUnsupportedMediaType)
		res.Write(responseBody("'%s' media type is not supported", mt))
		return
	}

	cfg, err := config.ReadYAMLConfig(body)

	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 config YAML from request body. "+
				"Was it formatted correctly and encoded as binary data?\nerror: %s",
			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...))
}