cluster/admitomatic: finish up service

This turns admitomatic into a self-standing service that can be used as
an admission controller.

I've tested this E2E on a local k3s server, and have some early test
code for that - but that'll land up in a follow up CR, as it first needs
to be cleaned up.

Change-Id: I46da0fc49f9d1a3a1a96700a36deb82e5057249b
diff --git a/cluster/admitomatic/service.go b/cluster/admitomatic/service.go
new file mode 100644
index 0000000..8fa2698
--- /dev/null
+++ b/cluster/admitomatic/service.go
@@ -0,0 +1,105 @@
+package main
+
+import (
+	"encoding/json"
+	"fmt"
+	"io/ioutil"
+	"net/http"
+
+	"github.com/golang/glog"
+	"google.golang.org/protobuf/encoding/prototext"
+	admission "k8s.io/api/admission/v1beta1"
+	meta "k8s.io/apimachinery/pkg/apis/meta/v1"
+
+	pb "code.hackerspace.pl/hscloud/cluster/admitomatic/config"
+)
+
+type service struct {
+	ingress ingressFilter
+}
+
+// newService creates an admitomatic service from a given prototext config.
+func newService(configuration []byte) (*service, error) {
+	var cfg pb.Config
+	if err := prototext.Unmarshal(configuration, &cfg); err != nil {
+		return nil, fmt.Errorf("parsing config: %v", err)
+	}
+
+	s := service{}
+
+	for i, ad := range cfg.AllowDomain {
+		if ad.Namespace == "" {
+			ad.Namespace = "default"
+		}
+		if ad.Dns == "" {
+			return nil, fmt.Errorf("config entry %d: dns must be set", i)
+		}
+		if err := s.ingress.allow(ad.Namespace, ad.Dns); err != nil {
+			return nil, fmt.Errorf("config entry %d: %v", i, err)
+		}
+		glog.Infof("Ingress: allowing %s in %s", ad.Dns, ad.Namespace)
+	}
+	return &s, nil
+}
+
+// handler is the main HTTP handler of the admitomatic service. It servers the
+// AdmissionReview API, and is called by the Kubernetes API server to
+// permit/deny creation/updating of resources.
+func (s *service) handler(w http.ResponseWriter, r *http.Request) {
+	var body []byte
+	if r.Body != nil {
+		if data, err := ioutil.ReadAll(r.Body); err == nil {
+			body = data
+		}
+	}
+
+	if r.Method != "POST" {
+		glog.Errorf("%s %s: invalid method", r.Method, r.URL)
+		return
+	}
+
+	contentType := r.Header.Get("Content-Type")
+	if contentType != "application/json" {
+		glog.Errorf("%s %s: invalid content-type", r.Method, r.URL)
+		return
+	}
+
+	var review admission.AdmissionReview
+	if err := json.Unmarshal(body, &review); err != nil {
+		glog.Errorf("%s %s: cannot decode: %v", r.Method, r.URL, err)
+		return
+	}
+
+	if review.Kind != "AdmissionReview" {
+		glog.Errorf("%s %s: invalid Kind (%q)", r.Method, r.URL, review.Kind)
+		return
+	}
+
+	var err error
+	req := review.Request
+	resp := &admission.AdmissionResponse{
+		UID:     req.UID,
+		Allowed: true,
+	}
+	switch {
+	case req.Kind.Group == "networking.k8s.io" && req.Kind.Kind == "Ingress":
+		resp, err = s.ingress.admit(req)
+		if err != nil {
+			glog.Errorf("%s %s %s: %v", req.Operation, req.Name, req.Namespace, err)
+			// Fail safe.
+			// TODO(q3k): monitor this?
+			resp = &admission.AdmissionResponse{
+				UID:     req.UID,
+				Allowed: false,
+				Result: &meta.Status{
+					Code:    500,
+					Message: "admitomatic: internal server error",
+				},
+			}
+		}
+	}
+
+	glog.Infof("%s %s %s in %s: %v (%v)", req.Operation, req.Kind.Kind, req.Name, req.Namespace, resp.Allowed, resp.Result)
+	review.Response = resp
+	json.NewEncoder(w).Encode(review)
+}