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")
+}