diff --git a/go.mod b/go.mod index c81d049..872848c 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,14 @@ module github.com/monder/service-target-group require ( + 4d63.com/gochecknoglobals v0.0.0-20180908201037-5090db600a84 // indirect + 4d63.com/gochecknoinits v0.0.0-20180528051558-14d5915061e5 // indirect + github.com/BurntSushi/toml v0.3.1 // indirect + github.com/alecthomas/gocyclo v0.0.0-20150208221726-aa8f8b160214 // indirect + github.com/alexflint/go-arg v1.0.0 // indirect + github.com/alexkohler/nakedret v0.0.0-20171106223215-c0e305a4f690 // indirect github.com/aws/aws-sdk-go v1.16.11 - github.com/davecgh/go-spew v1.1.1 // indirect + github.com/client9/misspell v0.3.4 // indirect github.com/ghodss/yaml v1.0.0 // indirect github.com/go-ini/ini v1.40.0 // indirect github.com/go-logr/logr v0.1.0 // indirect @@ -10,47 +16,63 @@ require ( github.com/gogo/protobuf v1.2.0 // indirect github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b // indirect github.com/golang/groupcache v0.0.0-20181024230925-c65c006176ff // indirect + github.com/golang/lint v0.0.0-20181217174547-8f45f776aaf1 // indirect github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c // indirect github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf // indirect github.com/googleapis/gnostic v0.2.0 // indirect github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e // indirect + github.com/gordonklaus/ineffassign v0.0.0-20180909121442-1003c8bd00dc // indirect github.com/gregjones/httpcache v0.0.0-20181110185634-c63ab54fda8f // indirect github.com/hashicorp/golang-lru v0.5.0 // indirect github.com/imdario/mergo v0.3.6 // indirect + github.com/jgautheron/goconst v0.0.0-20170703170152-9740945f5dcb // indirect github.com/json-iterator/go v1.1.5 // indirect github.com/jtolds/gls v4.2.1+incompatible // indirect + github.com/kisielk/errcheck v1.2.0 // indirect github.com/mattbaird/jsonpatch v0.0.0-20171005235357-81af80346b1a // indirect + github.com/mdempsky/maligned v0.0.0-20180708014732-6e39bd26a8c8 // indirect + github.com/mdempsky/unconvert v0.0.0-20180703203632-1a9a0a0a3594 // indirect + github.com/mibk/dupl v1.0.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.1 // indirect github.com/onsi/ginkgo v1.7.0 // indirect github.com/onsi/gomega v1.4.3 // indirect + github.com/opennota/check v0.0.0-20180911053232-0c771f5545ff // indirect github.com/pborman/uuid v1.2.0 // indirect github.com/peterbourgon/diskv v2.0.1+incompatible // indirect github.com/pkg/errors v0.8.0 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_golang v0.9.2 // indirect + github.com/securego/gosec v0.0.0-20181211171558-12400f9a1ca7 // indirect github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d // indirect github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c // indirect github.com/spf13/pflag v1.0.3 // indirect - github.com/stretchr/testify v1.2.2 // indirect + github.com/stripe/safesql v0.0.0-20171221195208-cddf355596fe // indirect + github.com/tsenart/deadcode v0.0.0-20160724212837-210d2dc333e9 // indirect + github.com/walle/lll v0.0.0-20160702150637-8b13b3fbf731 // indirect go.uber.org/atomic v1.3.2 // indirect go.uber.org/multierr v1.1.0 // indirect go.uber.org/zap v1.9.1 // indirect golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9 // indirect + golang.org/x/lint v0.0.0-20181217174547-8f45f776aaf1 // indirect golang.org/x/net v0.0.0-20181220203305-927f97764cc3 // indirect golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890 // indirect golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 // indirect golang.org/x/sys v0.0.0-20181221143128-b4a75ba826a6 // indirect golang.org/x/time v0.0.0-20181108054448-85acf8d2951c // indirect - gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect + golang.org/x/tools v0.0.0-20190106171756-3ef68632349c // indirect + google.golang.org/appengine v1.4.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.2.2 // indirect + honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a // indirect k8s.io/api v0.0.0-20181221193117-173ce66c1e39 k8s.io/apiextensions-apiserver v0.0.0-20181221201254-261a947e2c38 // indirect k8s.io/apimachinery v0.0.0-20181222072933-b814ad55d7c5 k8s.io/client-go v10.0.0+incompatible // indirect k8s.io/klog v0.1.0 // indirect k8s.io/kube-openapi v0.0.0-20181114233023-0317810137be // indirect + mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed // indirect + mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b // indirect + mvdan.cc/unparam v0.0.0-20181201214637-68701730a1d7 // indirect sigs.k8s.io/controller-runtime v0.1.9 sigs.k8s.io/testing_frameworks v0.1.1 // indirect sigs.k8s.io/yaml v1.1.0 // indirect diff --git a/main.go b/main.go index d031837..6bdcbbc 100644 --- a/main.go +++ b/main.go @@ -1,40 +1,29 @@ package main import ( - "context" - "fmt" "log" "os" - "reflect" - - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/aws/arn" - "github.com/aws/aws-sdk-go/aws/session" - "github.com/aws/aws-sdk-go/service/elbv2" corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/errors" "sigs.k8s.io/controller-runtime/pkg/builder" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/reconcile" "sigs.k8s.io/controller-runtime/pkg/runtime/signals" + + "github.com/monder/service-target-group/reconciler" ) func main() { - reconciler := &endpointReconciler{ - managedResources: make(map[string]map[string]*elbv2.TargetDescription, 0), - } + r := reconciler.New() + manager, err := builder.SimpleController(). ForType(&corev1.Service{}). ForType(&corev1.Endpoints{}). - Build(reconciler) + Build(r) if err != nil { log.Println("Unable to build controller:", err) os.Exit(1) } - - reconciler.client = manager.GetClient() + r.SetClient(manager.GetClient()) if err := manager.Start(signals.SetupSignalHandler()); err != nil { log.Println("Unable to run controller:", err) @@ -42,118 +31,3 @@ func main() { } } - -type endpointReconciler struct { - client client.Client - managedResources map[string]map[string]*elbv2.TargetDescription -} - -func (r *endpointReconciler) Reconcile(request reconcile.Request) (reconcile.Result, error) { - rss := &corev1.Service{} - err := r.client.Get(context.TODO(), request.NamespacedName, rss) - if errors.IsNotFound(err) { - delete(r.managedResources, request.NamespacedName.String()) - // TODO deregister everything? - return reconcile.Result{}, nil - } - - targetGroupARN := rss.Annotations["stg.monder.cc/target-group"] - if targetGroupARN == "" { // Skip services that we do not need to register - return reconcile.Result{}, nil - } - parsedARN, err := arn.Parse(targetGroupARN) - if err != nil { - fmt.Println(err.Error()) - return reconcile.Result{}, nil - } - - rse := &corev1.Endpoints{} - err = r.client.Get(context.TODO(), request.NamespacedName, rse) - if errors.IsNotFound(err) { - delete(r.managedResources, request.NamespacedName.String()) - // TODO deregister everything? - return reconcile.Result{}, nil - } - - newState := make(map[string]*elbv2.TargetDescription, 0) - - for _, s := range rse.Subsets { - for _, p := range s.Ports { - for _, a := range s.Addresses { - newState[fmt.Sprintf("%s:%d", a.IP, p.Port)] = &elbv2.TargetDescription{ - Id: aws.String(a.IP), - Port: aws.Int64(int64(p.Port)), - } - } - } - } - - if reflect.DeepEqual(newState, r.managedResources[request.NamespacedName.String()]) { - return reconcile.Result{}, nil - } - - targetsToDeregister := make([]*elbv2.TargetDescription, 0) - targetsToRegister := make([]*elbv2.TargetDescription, 0) - - svc := elbv2.New(session.Must(session.NewSession(&aws.Config{ - Region: aws.String(parsedARN.Region), - }))) - result, err := svc.DescribeTargetHealth(&elbv2.DescribeTargetHealthInput{ - TargetGroupArn: aws.String(targetGroupARN), - }) - if err != nil { - fmt.Println(err.Error()) - return reconcile.Result{}, nil - } - - for _, th := range result.TargetHealthDescriptions { - _, keep := newState[fmt.Sprintf("%s:%d", *th.Target.Id, *th.Target.Port)] - if !keep { - targetsToDeregister = append(targetsToDeregister, th.Target) - } - } - - for _, td := range newState { - found := false - for _, th := range result.TargetHealthDescriptions { - if *th.Target.Id == *td.Id && *th.Target.Port == *td.Port && *th.TargetHealth.State != elbv2.TargetHealthStateEnumDraining { - found = true - break - } - } - if !found { - targetsToRegister = append(targetsToRegister, td) - } - } - - fmt.Println("dereg:") - fmt.Println(targetsToDeregister) - fmt.Println("reg:") - fmt.Println(targetsToRegister) - - // Register - if len(targetsToRegister) > 0 { - _, err = svc.RegisterTargets(&elbv2.RegisterTargetsInput{ - TargetGroupArn: aws.String(targetGroupARN), - Targets: targetsToRegister, - }) - if err != nil { - fmt.Println(err.Error()) - } - } - - // Deregister - if len(targetsToDeregister) > 0 { - _, err = svc.DeregisterTargets(&elbv2.DeregisterTargetsInput{ - TargetGroupArn: aws.String(targetGroupARN), - Targets: targetsToDeregister, - }) - if err != nil { - fmt.Println(err.Error()) - } - } - - fmt.Println("---") - r.managedResources[request.NamespacedName.String()] = newState - return reconcile.Result{}, nil -} diff --git a/reconciler/new.go b/reconciler/new.go new file mode 100644 index 0000000..12834cd --- /dev/null +++ b/reconciler/new.go @@ -0,0 +1,53 @@ +package reconciler + +import ( + "context" + "fmt" + + "github.com/aws/aws-sdk-go/service/elbv2" + "github.com/aws/aws-sdk-go/service/route53" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +func New() Reconciler { + return &endpointReconciler{ + elbResources: make(map[string]map[string]*elbv2.TargetDescription, 0), + route53Resources: make(map[string]*route53.ResourceRecordSet, 0), + } +} + +func (r *endpointReconciler) SetClient(client client.Client) { + r.client = client +} + +func (r *endpointReconciler) Reconcile(request reconcile.Request) (reconcile.Result, error) { + rss := &corev1.Service{} + err := r.client.Get(context.TODO(), request.NamespacedName, rss) + if errors.IsNotFound(err) { + delete(r.elbResources, request.NamespacedName.String()) + delete(r.route53Resources, request.NamespacedName.String()) + // TODO deregister everything? + return reconcile.Result{}, nil + } + + targetGroupARN := rss.Annotations["stg.monder.cc/target-group"] + if targetGroupARN != "" { + err = r.ReconcileTargetGroup(request, targetGroupARN) + if err != nil { + fmt.Println(err.Error()) + } + } + route53Domain := rss.Annotations["route53.monder.cc/domain-name"] + route53Zone := rss.Annotations["route53.monder.cc/zone"] + if route53Domain != "" && route53Zone != "" { + err = r.ReconcileRoute53(request, route53Zone, route53Domain) + if err != nil { + fmt.Println(err.Error()) + } + } + return reconcile.Result{}, nil +} diff --git a/reconciler/route53.go b/reconciler/route53.go new file mode 100644 index 0000000..d0d34e5 --- /dev/null +++ b/reconciler/route53.go @@ -0,0 +1,66 @@ +package reconciler + +import ( + "context" + "fmt" + "reflect" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/route53" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +func (r *endpointReconciler) ReconcileRoute53(request reconcile.Request, zone string, domain string) error { + + rse := &corev1.Endpoints{} + err := r.client.Get(context.TODO(), request.NamespacedName, rse) + if errors.IsNotFound(err) { + delete(r.route53Resources, request.NamespacedName.String()) + // TODO deregister everything? + return nil + } + + newRecordSet := &route53.ResourceRecordSet{ + Name: aws.String(domain), + Type: aws.String(route53.RRTypeA), + TTL: aws.Int64(1), + ResourceRecords: []*route53.ResourceRecord{}, + } + + for _, s := range rse.Subsets { + for _, a := range s.Addresses { + newRecordSet.ResourceRecords = append(newRecordSet.ResourceRecords, &route53.ResourceRecord{ + Value: aws.String(a.IP), + }) + } + } + + if reflect.DeepEqual(newRecordSet, r.route53Resources[request.NamespacedName.String()]) { + return nil + } + + fmt.Printf("updating route53: %s\n", domain) + svc := route53.New(session.Must(session.NewSession(&aws.Config{}))) + _, err = svc.ChangeResourceRecordSets(&route53.ChangeResourceRecordSetsInput{ + ChangeBatch: &route53.ChangeBatch{ + Changes: []*route53.Change{ + { + Action: aws.String(route53.ChangeActionUpsert), + ResourceRecordSet: newRecordSet, + }, + }, + }, + HostedZoneId: aws.String(zone), + }) + + if err != nil { + fmt.Println(err.Error()) + } + + r.route53Resources[request.NamespacedName.String()] = newRecordSet + return nil +} diff --git a/reconciler/targetGroup.go b/reconciler/targetGroup.go new file mode 100644 index 0000000..ec66b78 --- /dev/null +++ b/reconciler/targetGroup.go @@ -0,0 +1,112 @@ +package reconciler + +import ( + "context" + "fmt" + "reflect" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/arn" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/elbv2" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +func (r *endpointReconciler) ReconcileTargetGroup(request reconcile.Request, targetGroupARN string) error { + parsedARN, err := arn.Parse(targetGroupARN) + if err != nil { + return err + } + + rse := &corev1.Endpoints{} + err = r.client.Get(context.TODO(), request.NamespacedName, rse) + if errors.IsNotFound(err) { + delete(r.elbResources, request.NamespacedName.String()) + // TODO deregister everything? + return nil + } + + newState := make(map[string]*elbv2.TargetDescription, 0) + + for _, s := range rse.Subsets { + for _, p := range s.Ports { + for _, a := range s.Addresses { + newState[fmt.Sprintf("%s:%d", a.IP, p.Port)] = &elbv2.TargetDescription{ + Id: aws.String(a.IP), + Port: aws.Int64(int64(p.Port)), + } + } + } + } + + if reflect.DeepEqual(newState, r.elbResources[request.NamespacedName.String()]) { + return nil + } + + targetsToDeregister := make([]*elbv2.TargetDescription, 0) + targetsToRegister := make([]*elbv2.TargetDescription, 0) + + svc := elbv2.New(session.Must(session.NewSession(&aws.Config{ + Region: aws.String(parsedARN.Region), + }))) + result, err := svc.DescribeTargetHealth(&elbv2.DescribeTargetHealthInput{ + TargetGroupArn: aws.String(targetGroupARN), + }) + if err != nil { + return err + } + + for _, th := range result.TargetHealthDescriptions { + _, keep := newState[fmt.Sprintf("%s:%d", *th.Target.Id, *th.Target.Port)] + if !keep { + targetsToDeregister = append(targetsToDeregister, th.Target) + } + } + + for _, td := range newState { + found := false + for _, th := range result.TargetHealthDescriptions { + if *th.Target.Id == *td.Id && *th.Target.Port == *td.Port && *th.TargetHealth.State != elbv2.TargetHealthStateEnumDraining { + found = true + break + } + } + if !found { + targetsToRegister = append(targetsToRegister, td) + } + } + + fmt.Println("dereg:") + fmt.Println(targetsToDeregister) + fmt.Println("reg:") + fmt.Println(targetsToRegister) + + // Register + if len(targetsToRegister) > 0 { + _, err = svc.RegisterTargets(&elbv2.RegisterTargetsInput{ + TargetGroupArn: aws.String(targetGroupARN), + Targets: targetsToRegister, + }) + if err != nil { + fmt.Println(err.Error()) + } + } + + // Deregister + if len(targetsToDeregister) > 0 { + _, err = svc.DeregisterTargets(&elbv2.DeregisterTargetsInput{ + TargetGroupArn: aws.String(targetGroupARN), + Targets: targetsToDeregister, + }) + if err != nil { + fmt.Println(err.Error()) + } + } + + fmt.Println("---") + r.elbResources[request.NamespacedName.String()] = newState + return nil +} diff --git a/reconciler/type.go b/reconciler/type.go new file mode 100644 index 0000000..dfe210c --- /dev/null +++ b/reconciler/type.go @@ -0,0 +1,20 @@ +package reconciler + +import ( + "github.com/aws/aws-sdk-go/service/elbv2" + "github.com/aws/aws-sdk-go/service/route53" + + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +type Reconciler interface { + Reconcile(reconcile.Request) (reconcile.Result, error) + SetClient(client.Client) +} + +type endpointReconciler struct { + client client.Client + elbResources map[string]map[string]*elbv2.TargetDescription + route53Resources map[string]*route53.ResourceRecordSet +}