cluster/admitomatic: implement basic dns/ns filtering

This is the beginning of a validating admission controller which we will
use to permit end-users access to manage Ingresses.

This first pass implements an ingressFilter, which is the main structure
through which allowed namespace/dns combinations will be allowed. The
interface is currently via a test, but in the future this will likely be
configured via a command line, or via a serialized protobuf config.

Change-Id: I22dbed633ea8d8e1fa02c2a1598f37f02ea1b309
diff --git a/cluster/admitomatic/ingress.go b/cluster/admitomatic/ingress.go
new file mode 100644
index 0000000..42cab98
--- /dev/null
+++ b/cluster/admitomatic/ingress.go
@@ -0,0 +1,130 @@
+package main
+
+import (
+	"fmt"
+	"strings"
+
+	admission "k8s.io/api/admission/v1beta1"
+)
+
+// ingressFilter is a filter which allows or denies the creation of an ingress
+// backing a given domain with a namespace. It does so by operating on an
+// explicit list of allowed namespace/domain pairs, where each domain is either
+// a single domain or a DNS wildcard at a given root.
+// By default every domain is allowed in every namespace. However, the moment
+// an entry is added for a given domain (or wildcard that matches some
+// domains), this domain will only be allowed in that namespace.
+//
+// For example, with the given allowed domains:
+// -  ns: example, domain: one.example.com
+// -  ns: example, domain: *.google.com
+// The logic will be as follows:
+// -  one.example.com will be only allowed in the example namespace
+// -  any .google.com domain will be only allowed in the example namespace
+// -  all other domains will be allowed everywhere.
+//
+// This logic allows for the easy use of arbitrary domains by k8s users within
+// their personal namespaces, but allows critical domains to only be allowed in
+// trusted namespaces.
+//
+// ingressFilter can be used straight away after constructing it as an empty
+// type.
+type ingressFilter struct {
+	// allowed is a map from namespace to list of domain matchers.
+	allowed map[string][]*domain
+}
+
+// domain is a matcher for either a single given domain, or a domain wildcard.
+// If this is a wildcard matcher, any amount of dot-delimited levels under the
+// domain will be permitted.
+type domain struct {
+	// dns is either the domain name matched by this matcher (if wildcard ==
+	// false), or the root of a wildcard represented by this matcher (if
+	// wildcard == true).
+	dns      string
+	wildcard bool
+}
+
+// match returns whether this matcher matches a given domain.
+func (d *domain) match(dns string) bool {
+	if !d.wildcard {
+		return dns == d.dns
+	}
+	return strings.HasSuffix(dns, "."+d.dns)
+}
+
+// allow adds a given (namespace, dns) pair to the filter. The dns variable is
+// a string that is either a simple domain name, or a wildcard like
+// *.foo.example.com. An error is returned if the dns stirng could not be
+// parsed.
+func (i *ingressFilter) allow(ns, dns string) error {
+	// If the filter is brand new, initialize it.
+	if i.allowed == nil {
+		i.allowed = make(map[string][]*domain)
+	}
+
+	// Try to parse the name as a wildcard.
+	parts := strings.Split(dns, ".")
+	wildcard := false
+	for i, part := range parts {
+		if i == 0 && part == "*" {
+			wildcard = true
+			continue
+		}
+		// Do some basic validation of the name.
+		if part == "" || strings.Contains(part, "*") {
+			return fmt.Errorf("invalid domain")
+		}
+	}
+	if wildcard {
+		if len(parts) < 2 {
+			return fmt.Errorf("invalid domain")
+		}
+		dns = strings.Join(parts[1:], ".")
+	}
+	i.allowed[ns] = append(i.allowed[ns], &domain{
+		dns:      dns,
+		wildcard: wildcard,
+	})
+	return nil
+}
+
+// domainAllowed returns whether a given domain is allowed to be backed by an
+// ingress within a given namespace.
+func (i *ingressFilter) domainAllowed(ns, domain string) bool {
+	if i.allowed == nil {
+		return true
+	}
+
+	domainFound := false
+	// TODO(q3k): if this becomes too slow, build some inverted index for this.
+	for n, ds := range i.allowed {
+		for _, d := range ds {
+			if !d.match(domain) {
+				continue
+			}
+			// Domain matched, see if allowed in this namespace.
+			domainFound = true
+			if n == ns {
+				return true
+			}
+		}
+		// Otherwise, maybe it's allowed in another domain.
+	}
+	// No direct match found - if this domain has been at all matched before,
+	// it means that it's a restriected domain and the requested namespace is
+	// not one that's allowed to host it. Refuse.
+	if domainFound {
+		return false
+	}
+	// No direct match found, and this domain is not restricted. Allow.
+	return true
+}
+
+func (i *ingressFilter) admit(req *admission.AdmissionRequest) (*admission.AdmissionResponse, error) {
+	if req.Kind.Group != "networking.k8s.io" || req.Kind.Kind != "Ingress" {
+		return nil, fmt.Errorf("not an ingress")
+	}
+	// TODO(q3k); implement
+	return nil, fmt.Errorf("unimplemented")
+}