Skip to content
This repository has been archived by the owner on Sep 19, 2018. It is now read-only.

Commit

Permalink
Merge pull request #10 from utilitywarehouse/multi-target
Browse files Browse the repository at this point in the history
Enable setting multiple targets and specifying them via a label
  • Loading branch information
alkar committed Mar 6, 2017
2 parents 4e3ed81 + 532cf64 commit fd7e734
Show file tree
Hide file tree
Showing 5 changed files with 254 additions and 138 deletions.
25 changes: 15 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

ingress53 is a service designed to run in kubernetes and maintain DNS records for the cluster's ingress resources in AWS Route53.

It will watch the kubernetes API (using the service token) for any Ingress resource changes and try to apply those records to route53 in Amazon, mapping the record to the "target name", which is the dns name of the ingress endpoint for your cluster.
It will watch the kubernetes API (using the service token) for any Ingress resource changes and try to apply those records to route53 in Amazon, mapping the record to the "target", which is the dns name of the ingress endpoint for your cluster.

# Requirements

Expand Down Expand Up @@ -42,15 +42,15 @@ The minimum AWS policy you can use:

# Usage

ingress53 is slightly opinionated in that it assumes there are two kinds of ingress endpoints: public and private. A kubernetes selector is used to select public ingresses, while all others default to being private.
A kubernetes selector is used to specify the target (entry point of the cluster).

You will need to create a dns record that points to your ingress endpoint[s]. We will use this to CNAME all ingress resource entries to that "target".

Your set up might look like this:

- A ingress controller (nginx/traefik) kubernetes service running on a nodePort (:8080)
- ELB that serves all worker nodes on :8080
- A CNAME for the elb `private.example.com` > `my-loadbalancer-1234567890.us-west-2.elb.amazonaws.com`
- A CNAME for the elb `private.cluster-entrypoint.com` > `my-loadbalancer-1234567890.us-west-2.elb.amazonaws.com`
- ingress53 service running inside the cluster

Now, if you were to create an ingress kubernetes resource:
Expand All @@ -60,6 +60,8 @@ apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: my-app
labels:
ingress53.target: private.cluster-entrypoint.com
spec:
rules:
- host: my-app.example.com
Expand All @@ -71,15 +73,17 @@ spec:
servicePort: 80
```

ingress53 will create a CNAME record in route53: `my-app.example.com` > `private.example.com`
ingress53 will create a CNAME record in route53: `my-app.example.com` > `private.cluster-entrypoint.com`

You can test it locally (please refer to the command line help for more options):

```sh
./ingress53 \
-route53-zone-id=XXXXXXXXXXXXXX \
-target-private=private.example.com \
-target-public=public.example.com \
-label-name=ingress53.target \
-target=private.cluster-entrypoint.com \
-target=public.cluster-entrypoint.com \
-default-target=private.cluster-entrypoint.com \
-kubernetes-config=$HOME/.kube/config \
-dry-run
```
Expand Down Expand Up @@ -129,10 +133,11 @@ spec:
- name: ingress53
image: utilitywarehouse/ingress53:v1.0.0
args:
- -route53-zone-id=XXXXXX
- -target-private=private.example.com
- -target-public=public.example.com
- -public-ingress-selector=ingress-tag-name:ingress-tag-value
- -route53-zone-id=XXXXXXXXXXXXXX
- -label-name=ingress53.target
- -target=private.cluster-entrypoint.com
- -target=public.cluster-entrypoint.com
- -default-target=private.cluster-entrypoint.com
resources:
requests:
cpu: 10m
Expand Down
96 changes: 65 additions & 31 deletions ingress_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,87 +15,121 @@ import (
)

var (
testIngressA = &v1beta1.Ingress{
privateIngressHostsAB = &v1beta1.Ingress{
ObjectMeta: v1.ObjectMeta{
Name: "exampleA",
Name: "privateIngressHostsAB",
Namespace: api.NamespaceDefault,
Labels: map[string]string{},
Labels: map[string]string{
LabelName: PrivateTarget,
},
},
Spec: v1beta1.IngressSpec{
Rules: []v1beta1.IngressRule{
{Host: "foo1.example.com"},
{Host: "foo2.example.com"},
{Host: "a.example.com"},
{Host: "b.example.com"},
},
},
}

testIngressB = &v1beta1.Ingress{
publicIngressHostC = &v1beta1.Ingress{
ObjectMeta: v1.ObjectMeta{
Name: "exampleB",
Name: "publicIngressHostCD",
Namespace: api.NamespaceDefault,
Labels: map[string]string{
"public": "true",
LabelName: PublicTarget,
},
},
Spec: v1beta1.IngressSpec{
Rules: []v1beta1.IngressRule{
{Host: "bar.example.com"},
{Host: "c.example.com"},
},
},
}

testIngressB2 = &v1beta1.Ingress{
publicIngressHostD = &v1beta1.Ingress{
ObjectMeta: v1.ObjectMeta{
Name: "exampleB",
Name: "publicIngressHostCD",
Namespace: api.NamespaceDefault,
Labels: map[string]string{
"public": "true",
LabelName: PublicTarget,
},
},
Spec: v1beta1.IngressSpec{
Rules: []v1beta1.IngressRule{
{Host: "bar2.example.com"},
{Host: "d.example.com"},
},
},
}

testIngressC1 = &v1beta1.Ingress{
privateIngressHostE = &v1beta1.Ingress{
ObjectMeta: v1.ObjectMeta{
Name: "exampleC1",
Name: "ingressHostE",
Namespace: api.NamespaceDefault,
Labels: map[string]string{},
Labels: map[string]string{
LabelName: PrivateTarget,
},
},
Spec: v1beta1.IngressSpec{
Rules: []v1beta1.IngressRule{
{Host: "e.example.com"},
},
},
}

privateIngressHostEDup = &v1beta1.Ingress{
ObjectMeta: v1.ObjectMeta{
Name: "ingressHostE",
Namespace: api.NamespaceDefault,
Labels: map[string]string{
LabelName: PrivateTarget,
},
},
Spec: v1beta1.IngressSpec{
Rules: []v1beta1.IngressRule{
{Host: "e.example.com"},
},
},
}

publicIngressHostEDup = &v1beta1.Ingress{
ObjectMeta: v1.ObjectMeta{
Name: "ingressHostE",
Namespace: api.NamespaceDefault,
Labels: map[string]string{
LabelName: PublicTarget,
},
},
Spec: v1beta1.IngressSpec{
Rules: []v1beta1.IngressRule{
{Host: "baz.example.com"},
{Host: "e.example.com"},
},
},
}

testIngressC2 = &v1beta1.Ingress{
ingressNoLabels = &v1beta1.Ingress{
ObjectMeta: v1.ObjectMeta{
Name: "exampleC2",
Name: "ingressNoLabels",
Namespace: api.NamespaceDefault,
Labels: map[string]string{},
},
Spec: v1beta1.IngressSpec{
Rules: []v1beta1.IngressRule{
{Host: "baz.example.com"},
{Host: "no-labels.example.com"},
},
},
}

testIngressC3 = &v1beta1.Ingress{
nonRegisteredIngress = &v1beta1.Ingress{
ObjectMeta: v1.ObjectMeta{
Name: "exampleC3",
Name: "nonRegisteredIngress",
Namespace: api.NamespaceDefault,
Labels: map[string]string{
"public": "true",
LabelName: "non-registered-target.aws.com",
},
},
Spec: v1beta1.IngressSpec{
Rules: []v1beta1.IngressRule{
{Host: "baz.example.com"},
{Host: "non-registered-target.example.com"},
},
},
}
Expand Down Expand Up @@ -180,13 +214,13 @@ func waitForTrue(test func() bool, timeout time.Duration) error {

func TestIngressWatcher(t *testing.T) {
expected := []testIngressEvent{
{watch.Added, nil, testIngressA},
{watch.Added, nil, testIngressB},
{watch.Deleted, testIngressA, nil},
{watch.Modified, testIngressB, testIngressB2},
{watch.Added, nil, privateIngressHostsAB},
{watch.Added, nil, publicIngressHostC},
{watch.Deleted, privateIngressHostsAB, nil},
{watch.Modified, publicIngressHostC, publicIngressHostD},
}

client, watcher := newTestIngressWatcherClient(*testIngressA, *testIngressB)
client, watcher := newTestIngressWatcherClient(*privateIngressHostsAB, *publicIngressHostC)

pM := &sync.Mutex{}
processed := []testIngressEvent{}
Expand Down Expand Up @@ -219,11 +253,11 @@ func TestIngressWatcher(t *testing.T) {
if err := waitForTrue(pLenIs(2), 10*time.Second); err != nil {
t.Fatalf("timed out waiting for ingressWatcher to process events")
}
watcher.Delete(testIngressA)
watcher.Delete(privateIngressHostsAB)
if err := waitForTrue(pLenIs(3), 10*time.Second); err != nil {
t.Fatalf("timed out waiting for ingressWatcher to process events")
}
watcher.Modify(testIngressB2)
watcher.Modify(publicIngressHostD)
if err := waitForTrue(pLenIs(4), 10*time.Second); err != nil {
t.Fatalf("timed out waiting for ingressWatcher to process events")
}
Expand Down
65 changes: 44 additions & 21 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ package main

import (
"flag"
"fmt"
"log"
"net/http"
"os"
"os/signal"
"strings"

"k8s.io/client-go/1.5/tools/clientcmd"

Expand All @@ -15,20 +17,45 @@ import (
"github.com/utilitywarehouse/go-operational/op"
)

// Define a type named "strslice" as a slice of strings
type strslice []string

// Now, for our new type, implement the two methods of
// the flag.Value interface...
// The first method is String() string
func (s *strslice) String() string {
return fmt.Sprint(*s)
}

// Set is the method to set the flag value, part of the flag.Value interface.
// Set's argument is a string to be parsed to set the flag.
// It's a comma-separated list, so we split it.
func (s *strslice) Set(value string) error {
for _, target := range strings.Split(value, ",") {
*s = append(*s, target)
}
return nil
}

var (
appGitHash = "master"

kubeConfig = flag.String("kubernetes-config", "", "path to the kubeconfig file, if unspecified then in-cluster config will be used")
publicSelectorString = flag.String("public-ingress-selector", "", "selector for ingresses that should point to the public target")
targetPublic = flag.String("target-public", "", "target hostname for public ingresses")
targetPrivate = flag.String("target-private", "", "target hostnam for private ingresses")
r53ZoneID = flag.String("route53-zone-id", "", "route53 hosted DNS zone id")
debugLogs = flag.Bool("debug", false, "enables debug logs")
dryRun = flag.Bool("dry-run", false, "if set, ingress53 will not make any Route53 changes")
// Define a flag to accumulate durations. Because it has a special type,
// we need to use the Var function and therefore create the flag during
// init.
targets strslice

kubeConfig = flag.String("kubernetes-config", "", "path to the kubeconfig file, if unspecified then in-cluster config will be used")
labelName = flag.String("label-name", "", "Kubernetes key of the label that specifies the target type")
defaultTarget = flag.String("default-target", "", "Default target to use in the absense of matching labels")
r53ZoneID = flag.String("route53-zone-id", "", "route53 hosted DNS zone id")
debugLogs = flag.Bool("debug", false, "enables debug logs")
dryRun = flag.Bool("dry-run", false, "if set, ingress53 will not make any Route53 changes")

metricUpdatesApplied = prometheus.NewCounterVec(
prometheus.CounterOpts{
Namespace: "ingress53",

Subsystem: "route53",
Name: "updates_applied",
Help: "number of route53 updates",
Expand Down Expand Up @@ -56,34 +83,30 @@ var (
)
)

func init() {
func main() {
prometheus.MustRegister(metricUpdatesApplied)
prometheus.MustRegister(metricUpdatesReceived)
prometheus.MustRegister(metricUpdatesRejected)

flag.Var(&targets, "target", "List of endpoints (ELB) targets to map ingress records to")
flag.Parse()

luf := &logutils.LevelFilter{
Levels: []logutils.LogLevel{"DEBUG", "INFO", "ERROR"},
MinLevel: logutils.LogLevel("INFO"),
Writer: os.Stdout,
}

if *debugLogs {
luf.MinLevel = logutils.LogLevel("DEBUG")
}

log.SetOutput(luf)

prometheus.MustRegister(metricUpdatesApplied)
prometheus.MustRegister(metricUpdatesReceived)
prometheus.MustRegister(metricUpdatesRejected)
}

func main() {
ro := registratorOptions{
PrivateHostname: *targetPrivate,
PublicHostname: *targetPublic,
PublicResourceSelector: *publicSelectorString,
Route53ZoneID: *r53ZoneID,
Targets: targets,
LabelName: *labelName,
DefaultTarget: *defaultTarget,
Route53ZoneID: *r53ZoneID,
}

if *kubeConfig != "" {
config, err := clientcmd.BuildConfigFromFlags("", *kubeConfig)
if err != nil {
Expand Down
Loading

0 comments on commit fd7e734

Please sign in to comment.