diff --git a/internal/ingress/controller/controller.go b/internal/ingress/controller/controller.go index 468d994a63..cf0794fef7 100644 --- a/internal/ingress/controller/controller.go +++ b/internal/ingress/controller/controller.go @@ -121,7 +121,7 @@ func (n *KongController) syncIngress(interface{}) error { }) n.Logger.Infof("syncing configuration") - state, err := n.parser.Build() + state, err := parser.Build(n.Logger.WithField("component", "store"), n.store) if err != nil { return fmt.Errorf("error building kong state: %w", err) } @@ -160,7 +160,6 @@ func NewKongController(ctx context.Context, } n.store = store - n.parser = parser.New(n.store, n.Logger.WithField("component", "store")) n.syncQueue = task.NewTaskQueue(n.syncIngress, config.Logger.WithField("component", "sync-queue")) @@ -240,8 +239,6 @@ type KongController struct { store store.Storer - parser parser.Parser - PluginSchemaStore PluginSchemaStore Logger logrus.FieldLogger diff --git a/internal/ingress/controller/kong.go b/internal/ingress/controller/kong.go index 57d53cf08a..abcdd5e51c 100644 --- a/internal/ingress/controller/kong.go +++ b/internal/ingress/controller/kong.go @@ -35,14 +35,14 @@ import ( "github.com/kong/deck/state" deckutils "github.com/kong/deck/utils" "github.com/kong/go-kong/kong" - "github.com/kong/kubernetes-ingress-controller/internal/ingress/controller/parser" + "github.com/kong/kubernetes-ingress-controller/internal/ingress/controller/parser/kongstate" "github.com/kong/kubernetes-ingress-controller/internal/ingress/utils" ) // OnUpdate is called periodically by syncQueue to keep the configuration in sync. // returning nil implies the synchronization finished correctly. // Returning an error means requeue the update. -func (n *KongController) OnUpdate(ctx context.Context, state *parser.KongState) error { +func (n *KongController) OnUpdate(ctx context.Context, state *kongstate.KongState) error { targetContent := n.toDeckContent(ctx, state) var customEntities []byte @@ -295,7 +295,7 @@ func (n *KongController) getIngressControllerTags() []string { func (n *KongController) toDeckContent( ctx context.Context, - k8sState *parser.KongState) *file.Content { + k8sState *kongstate.KongState) *file.Content { var content file.Content content.FormatVersion = "1.1" var err error diff --git a/internal/ingress/controller/parser/ingressrules.go b/internal/ingress/controller/parser/ingressrules.go new file mode 100644 index 0000000000..5ed32a32a7 --- /dev/null +++ b/internal/ingress/controller/parser/ingressrules.go @@ -0,0 +1,114 @@ +package parser + +import ( + "github.com/kong/go-kong/kong" + "github.com/kong/kubernetes-ingress-controller/internal/ingress/annotations" + "github.com/kong/kubernetes-ingress-controller/internal/ingress/controller/parser/kongstate" + "github.com/kong/kubernetes-ingress-controller/internal/ingress/store" + "github.com/sirupsen/logrus" + networking "k8s.io/api/networking/v1beta1" +) + +type ingressRules struct { + SecretNameToSNIs SecretNameToSNIs + ServiceNameToServices map[string]kongstate.Service +} + +func newIngressRules() ingressRules { + return ingressRules{ + SecretNameToSNIs: newSecretNameToSNIs(), + ServiceNameToServices: make(map[string]kongstate.Service), + } +} + +func mergeIngressRules(objs ...ingressRules) ingressRules { + result := newIngressRules() + + for _, obj := range objs { + for k, v := range obj.SecretNameToSNIs { + result.SecretNameToSNIs[k] = append(result.SecretNameToSNIs[k], v...) + } + for k, v := range obj.ServiceNameToServices { + result.ServiceNameToServices[k] = v + } + } + return result +} + +func (ir *ingressRules) populateServices(log logrus.FieldLogger, s store.Storer) { + // populate Kubernetes Service + for key, service := range ir.ServiceNameToServices { + k8sSvc, err := s.GetService(service.Namespace, service.Backend.Name) + if err != nil { + log.WithFields(logrus.Fields{ + "service_name": service.Backend.Name, + "service_namespace": service.Namespace, + }).Errorf("failed to fetch service: %v", err) + } + if k8sSvc != nil { + service.K8sService = *k8sSvc + } + secretName := annotations.ExtractClientCertificate( + service.K8sService.GetAnnotations()) + if secretName != "" { + secret, err := s.GetSecret(service.K8sService.Namespace, + secretName) + secretKey := service.K8sService.Namespace + "/" + secretName + // ensure that the cert is loaded into Kong + if _, ok := ir.SecretNameToSNIs[secretKey]; !ok { + ir.SecretNameToSNIs[secretKey] = []string{} + } + if err == nil { + service.ClientCertificate = &kong.Certificate{ + ID: kong.String(string(secret.UID)), + } + } else { + log.WithFields(logrus.Fields{ + "secret_name": secretName, + "secret_namespace": service.K8sService.Namespace, + }).Errorf("failed to fetch secret: %v", err) + } + } + ir.ServiceNameToServices[key] = service + } +} + +type SecretNameToSNIs map[string][]string + +func newSecretNameToSNIs() SecretNameToSNIs { + return SecretNameToSNIs(map[string][]string{}) +} + +func (m SecretNameToSNIs) addFromIngressTLS(tlsSections []networking.IngressTLS, namespace string) { + for _, tls := range tlsSections { + if len(tls.Hosts) == 0 { + continue + } + if tls.SecretName == "" { + continue + } + hosts := tls.Hosts + secretName := namespace + "/" + tls.SecretName + hosts = m.filterHosts(hosts) + if m[secretName] != nil { + hosts = append(hosts, m[secretName]...) + } + m[secretName] = hosts + } +} + +func (m SecretNameToSNIs) filterHosts(hosts []string) []string { + hostsToAdd := []string{} + seenHosts := map[string]bool{} + for _, hosts := range m { + for _, host := range hosts { + seenHosts[host] = true + } + } + for _, host := range hosts { + if !seenHosts[host] { + hostsToAdd = append(hostsToAdd, host) + } + } + return hostsToAdd +} diff --git a/internal/ingress/controller/parser/ingressrules_test.go b/internal/ingress/controller/parser/ingressrules_test.go new file mode 100644 index 0000000000..628e7be160 --- /dev/null +++ b/internal/ingress/controller/parser/ingressrules_test.go @@ -0,0 +1,172 @@ +package parser + +import ( + "testing" + + "github.com/kong/kubernetes-ingress-controller/internal/ingress/controller/parser/kongstate" + "github.com/stretchr/testify/assert" + networking "k8s.io/api/networking/v1beta1" +) + +func TestMergeIngressRules(t *testing.T) { + for _, tt := range []struct { + name string + inputs []ingressRules + wantOutput *ingressRules + }{ + { + name: "empty list", + wantOutput: &ingressRules{ + SecretNameToSNIs: map[string][]string{}, + ServiceNameToServices: map[string]kongstate.Service{}, + }, + }, + { + name: "nil maps", + inputs: []ingressRules{ + {}, {}, {}, + }, + wantOutput: &ingressRules{ + SecretNameToSNIs: map[string][]string{}, + ServiceNameToServices: map[string]kongstate.Service{}, + }, + }, + { + name: "one input", + inputs: []ingressRules{ + { + SecretNameToSNIs: map[string][]string{"a": {"b", "c"}, "d": {"e", "f"}}, + ServiceNameToServices: map[string]kongstate.Service{"1": {Namespace: "potato"}}, + }, + }, + wantOutput: &ingressRules{ + SecretNameToSNIs: map[string][]string{"a": {"b", "c"}, "d": {"e", "f"}}, + ServiceNameToServices: map[string]kongstate.Service{"1": {Namespace: "potato"}}, + }, + }, + { + name: "three inputs", + inputs: []ingressRules{ + { + SecretNameToSNIs: map[string][]string{"a": {"b", "c"}, "d": {"e", "f"}}, + ServiceNameToServices: map[string]kongstate.Service{"1": {Namespace: "potato"}}, + }, + { + SecretNameToSNIs: map[string][]string{"g": {"h"}}, + }, + { + ServiceNameToServices: map[string]kongstate.Service{"2": {Namespace: "carrot"}}, + }, + }, + wantOutput: &ingressRules{ + SecretNameToSNIs: map[string][]string{"a": {"b", "c"}, "d": {"e", "f"}, "g": {"h"}}, + ServiceNameToServices: map[string]kongstate.Service{"1": {Namespace: "potato"}, "2": {Namespace: "carrot"}}, + }, + }, + { + name: "can merge SNI arrays", + inputs: []ingressRules{ + { + SecretNameToSNIs: map[string][]string{"a": {"b", "c"}}, + }, + { + SecretNameToSNIs: map[string][]string{"a": {"d", "e"}}, + }, + }, + wantOutput: &ingressRules{ + SecretNameToSNIs: map[string][]string{"a": {"b", "c", "d", "e"}}, + ServiceNameToServices: map[string]kongstate.Service{}, + }, + }, + { + name: "overwrites services", + inputs: []ingressRules{ + { + ServiceNameToServices: map[string]kongstate.Service{"svc-name": {Namespace: "old"}}, + }, + { + ServiceNameToServices: map[string]kongstate.Service{"svc-name": {Namespace: "new"}}, + }, + }, + wantOutput: &ingressRules{ + SecretNameToSNIs: map[string][]string{}, + ServiceNameToServices: map[string]kongstate.Service{"svc-name": {Namespace: "new"}}, + }, + }, + } { + t.Run(tt.name, func(t *testing.T) { + gotOutput := mergeIngressRules(tt.inputs...) + assert.Equal(t, &gotOutput, tt.wantOutput) + }) + } +} + +func Test_addFromIngressTLS(t *testing.T) { + type args struct { + tlsSections []networking.IngressTLS + namespace string + } + tests := []struct { + name string + args args + want SecretNameToSNIs + }{ + { + args: args{ + tlsSections: []networking.IngressTLS{ + { + Hosts: []string{ + "1.example.com", + "2.example.com", + }, + SecretName: "sooper-secret", + }, + { + Hosts: []string{ + "3.example.com", + "4.example.com", + }, + SecretName: "sooper-secret2", + }, + }, + namespace: "foo", + }, + want: SecretNameToSNIs{ + "foo/sooper-secret": {"1.example.com", "2.example.com"}, + "foo/sooper-secret2": {"3.example.com", "4.example.com"}, + }, + }, + { + args: args{ + tlsSections: []networking.IngressTLS{ + { + Hosts: []string{ + "1.example.com", + }, + SecretName: "sooper-secret", + }, + { + Hosts: []string{ + "3.example.com", + "1.example.com", + "4.example.com", + }, + SecretName: "sooper-secret2", + }, + }, + namespace: "foo", + }, + want: SecretNameToSNIs{ + "foo/sooper-secret": {"1.example.com"}, + "foo/sooper-secret2": {"3.example.com", "4.example.com"}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m := newSecretNameToSNIs() + m.addFromIngressTLS(tt.args.tlsSections, tt.args.namespace) + assert.Equal(t, m, tt.want) + }) + } +} diff --git a/internal/ingress/controller/parser/kongstate/consumer.go b/internal/ingress/controller/parser/kongstate/consumer.go new file mode 100644 index 0000000000..55f26172e9 --- /dev/null +++ b/internal/ingress/controller/parser/kongstate/consumer.go @@ -0,0 +1,101 @@ +package kongstate + +import ( + "fmt" + + "github.com/kong/go-kong/kong" + configurationv1 "github.com/kong/kubernetes-ingress-controller/pkg/apis/configuration/v1" + "github.com/mitchellh/mapstructure" + "github.com/sirupsen/logrus" +) + +// Consumer holds a Kong consumer and its plugins and credentials. +type Consumer struct { + kong.Consumer + Plugins []kong.Plugin + KeyAuths []*kong.KeyAuth + HMACAuths []*kong.HMACAuth + JWTAuths []*kong.JWTAuth + BasicAuths []*kong.BasicAuth + ACLGroups []*kong.ACLGroup + + Oauth2Creds []*kong.Oauth2Credential + + K8sKongConsumer configurationv1.KongConsumer +} + +func (c *Consumer) SetCredential(log logrus.FieldLogger, credType string, credConfig interface{}) error { + switch credType { + case "key-auth", "keyauth_credential": + var cred kong.KeyAuth + err := decodeCredential(credConfig, &cred) + if err != nil { + return fmt.Errorf("failed to decode key-auth credential: %w", err) + + } + c.KeyAuths = append(c.KeyAuths, &cred) + case "basic-auth", "basicauth_credential": + var cred kong.BasicAuth + err := decodeCredential(credConfig, &cred) + if err != nil { + return fmt.Errorf("failed to decode basic-auth credential: %w", err) + } + c.BasicAuths = append(c.BasicAuths, &cred) + case "hmac-auth", "hmacauth_credential": + var cred kong.HMACAuth + err := decodeCredential(credConfig, &cred) + if err != nil { + return fmt.Errorf("failed to decode hmac-auth credential: %w", err) + } + c.HMACAuths = append(c.HMACAuths, &cred) + case "oauth2": + var cred kong.Oauth2Credential + err := decodeCredential(credConfig, &cred) + if err != nil { + return fmt.Errorf("failed to decode oauth2 credential: %w", err) + } + c.Oauth2Creds = append(c.Oauth2Creds, &cred) + case "jwt", "jwt_secret": + var cred kong.JWTAuth + err := decodeCredential(credConfig, &cred) + if err != nil { + log.Errorf("failed to process JWT credential: %v", err) + } + // This is treated specially because only this + // field might be omitted by user under the expectation + // that Kong will insert the default. + // If we don't set it, decK will detect a diff and PUT this + // credential everytime it performs a sync operation, which + // leads to unnecessary cache invalidations in Kong. + if cred.Algorithm == nil || *cred.Algorithm == "" { + cred.Algorithm = kong.String("HS256") + } + c.JWTAuths = append(c.JWTAuths, &cred) + case "acl": + var cred kong.ACLGroup + err := decodeCredential(credConfig, &cred) + if err != nil { + log.Errorf("failed to process ACL group: %v", err) + } + c.ACLGroups = append(c.ACLGroups, &cred) + default: + return fmt.Errorf("invalid credential type: '%v'", credType) + } + return nil +} + +func decodeCredential(credConfig interface{}, + credStructPointer interface{}) error { + decoder, err := mapstructure.NewDecoder( + &mapstructure.DecoderConfig{TagName: "json", + Result: credStructPointer, + }) + if err != nil { + return fmt.Errorf("failed to create a decoder: %w", err) + } + err = decoder.Decode(credConfig) + if err != nil { + return fmt.Errorf("failed to decode credential: %w", err) + } + return nil +} diff --git a/internal/ingress/controller/parser/kongstate/consumer_test.go b/internal/ingress/controller/parser/kongstate/consumer_test.go new file mode 100644 index 0000000000..88b5a6c867 --- /dev/null +++ b/internal/ingress/controller/parser/kongstate/consumer_test.go @@ -0,0 +1,244 @@ +package kongstate + +import ( + "testing" + + "github.com/kong/go-kong/kong" + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" +) + +func TestConsumer_SetCredential(t *testing.T) { + type args struct { + credType string + consumer *Consumer + credConfig interface{} + } + tests := []struct { + name string + args args + result *Consumer + wantErr bool + }{ + { + name: "invalid cred type errors", + args: args{ + credType: "invalid-type", + consumer: &Consumer{}, + credConfig: nil, + }, + result: &Consumer{}, + wantErr: true, + }, + { + name: "key-auth", + args: args{ + credType: "key-auth", + consumer: &Consumer{}, + credConfig: map[string]string{"key": "foo"}, + }, + result: &Consumer{ + KeyAuths: []*kong.KeyAuth{ + { + Key: kong.String("foo"), + }, + }, + }, + wantErr: false, + }, + { + name: "keyauth_credential", + args: args{ + credType: "keyauth_credential", + consumer: &Consumer{}, + credConfig: map[string]string{"key": "foo"}, + }, + result: &Consumer{ + KeyAuths: []*kong.KeyAuth{ + { + Key: kong.String("foo"), + }, + }, + }, + wantErr: false, + }, + { + name: "basic-auth", + args: args{ + credType: "basic-auth", + consumer: &Consumer{}, + credConfig: map[string]string{ + "username": "foo", + "password": "bar", + }, + }, + result: &Consumer{ + BasicAuths: []*kong.BasicAuth{ + { + Username: kong.String("foo"), + Password: kong.String("bar"), + }, + }, + }, + wantErr: false, + }, + { + name: "basicauth_credential", + args: args{ + credType: "basicauth_credential", + consumer: &Consumer{}, + credConfig: map[string]string{ + "username": "foo", + "password": "bar", + }, + }, + result: &Consumer{ + BasicAuths: []*kong.BasicAuth{ + { + Username: kong.String("foo"), + Password: kong.String("bar"), + }, + }, + }, + wantErr: false, + }, + { + name: "hmac-auth", + args: args{ + credType: "hmac-auth", + consumer: &Consumer{}, + credConfig: map[string]string{ + "username": "foo", + "secret": "bar", + }, + }, + result: &Consumer{ + HMACAuths: []*kong.HMACAuth{ + { + Username: kong.String("foo"), + Secret: kong.String("bar"), + }, + }, + }, + wantErr: false, + }, + { + name: "hmacauth_credential", + args: args{ + credType: "hmacauth_credential", + consumer: &Consumer{}, + credConfig: map[string]string{ + "username": "foo", + "secret": "bar", + }, + }, + result: &Consumer{ + HMACAuths: []*kong.HMACAuth{ + { + Username: kong.String("foo"), + Secret: kong.String("bar"), + }, + }, + }, + wantErr: false, + }, + { + name: "oauth2", + args: args{ + credType: "oauth2", + consumer: &Consumer{}, + credConfig: map[string]interface{}{ + "name": "foo", + "client_id": "bar", + "client_secret": "baz", + "redirect_uris": []string{"example.com"}, + }, + }, + result: &Consumer{ + Oauth2Creds: []*kong.Oauth2Credential{ + { + Name: kong.String("foo"), + ClientID: kong.String("bar"), + ClientSecret: kong.String("baz"), + RedirectURIs: kong.StringSlice("example.com"), + }, + }, + }, + wantErr: false, + }, + { + name: "jwt", + args: args{ + credType: "jwt", + consumer: &Consumer{}, + credConfig: map[string]string{ + "key": "foo", + "rsa_public_key": "bar", + "secret": "baz", + }, + }, + result: &Consumer{ + JWTAuths: []*kong.JWTAuth{ + { + Key: kong.String("foo"), + RSAPublicKey: kong.String("bar"), + Secret: kong.String("baz"), + // set by default + Algorithm: kong.String("HS256"), + }, + }, + }, + wantErr: false, + }, + { + name: "jwt_secret", + args: args{ + credType: "jwt_secret", + consumer: &Consumer{}, + credConfig: map[string]string{ + "key": "foo", + "rsa_public_key": "bar", + "secret": "baz", + }, + }, + result: &Consumer{ + JWTAuths: []*kong.JWTAuth{ + { + Key: kong.String("foo"), + RSAPublicKey: kong.String("bar"), + Secret: kong.String("baz"), + // set by default + Algorithm: kong.String("HS256"), + }, + }, + }, + wantErr: false, + }, + { + name: "acl", + args: args{ + credType: "acl", + consumer: &Consumer{}, + credConfig: map[string]string{"group": "group-foo"}, + }, + result: &Consumer{ + ACLGroups: []*kong.ACLGroup{ + { + Group: kong.String("group-foo"), + }, + }, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := tt.args.consumer.SetCredential(logrus.New(), tt.args.credType, + tt.args.credConfig); (err != nil) != tt.wantErr { + t.Errorf("processCredential() error = %v, wantErr %v", + err, tt.wantErr) + } + assert.Equal(t, tt.result, tt.args.consumer) + }) + } +} diff --git a/internal/ingress/controller/parser/kongstate/kongstate.go b/internal/ingress/controller/parser/kongstate/kongstate.go new file mode 100644 index 0000000000..97baa587ac --- /dev/null +++ b/internal/ingress/controller/parser/kongstate/kongstate.go @@ -0,0 +1,362 @@ +package kongstate + +import ( + "fmt" + "strings" + + "github.com/kong/go-kong/kong" + "github.com/kong/kubernetes-ingress-controller/internal/ingress/annotations" + "github.com/kong/kubernetes-ingress-controller/internal/ingress/controller/parser/util" + "github.com/kong/kubernetes-ingress-controller/internal/ingress/store" + configurationv1 "github.com/kong/kubernetes-ingress-controller/pkg/apis/configuration/v1" + "github.com/sirupsen/logrus" + "k8s.io/apimachinery/pkg/util/sets" +) + +// KongState holds the configuration that should be applied to Kong. +type KongState struct { + Services []Service + Upstreams []Upstream + Certificates []Certificate + CACertificates []kong.CACertificate + Plugins []Plugin + Consumers []Consumer +} + +func (ks *KongState) FillConsumersAndCredentials(log logrus.FieldLogger, s store.Storer) { + consumerIndex := make(map[string]Consumer) + + // build consumer index + for _, kConsumer := range s.ListKongConsumers() { + var c Consumer + if kConsumer.Username == "" && kConsumer.CustomID == "" { + continue + } + if kConsumer.Username != "" { + c.Username = kong.String(kConsumer.Username) + } + if kConsumer.CustomID != "" { + c.CustomID = kong.String(kConsumer.CustomID) + } + c.K8sKongConsumer = *kConsumer + + log = log.WithFields(logrus.Fields{ + "kongconsumer_name": kConsumer.Name, + "kongconsumer_namespace": kConsumer.Namespace, + }) + for _, cred := range kConsumer.Credentials { + log = log.WithFields(logrus.Fields{ + "secret_name": cred, + "secret_namespace": kConsumer.Namespace, + }) + secret, err := s.GetSecret(kConsumer.Namespace, cred) + if err != nil { + log.Errorf("failed to fetch secret: %v", err) + continue + } + credConfig := map[string]interface{}{} + for k, v := range secret.Data { + // TODO populate these based on schema from Kong + // and remove this workaround + if k == "redirect_uris" { + credConfig[k] = strings.Split(string(v), ",") + continue + } + credConfig[k] = string(v) + } + credType, ok := credConfig["kongCredType"].(string) + if !ok { + log.Errorf("failed to provision credential: invalid credType: %v", credType) + } + if !supportedCreds.Has(credType) { + log.Errorf("failed to provision credential: invalid credType: %v", credType) + continue + } + if len(credConfig) <= 1 { // 1 key of credType itself + log.Errorf("failed to provision credential: empty secret") + continue + } + err = c.SetCredential(log, credType, credConfig) + if err != nil { + log.Errorf("failed to provision credential: %v", err) + continue + } + } + + consumerIndex[kConsumer.Namespace+"/"+kConsumer.Name] = c + } + + // legacy attach credentials + credentials := s.ListKongCredentials() + if len(credentials) > 0 { + log.Warnf("deprecated KongCredential resource in use; " + + "please use secret-based credentials, " + + "KongCredential resource will be removed in future") + } + for _, credential := range credentials { + log = log.WithFields(logrus.Fields{ + "kongcredential_name": credential.Name, + "kongcredential_namespace": credential.Namespace, + "consumerRef": credential.ConsumerRef, + }) + cons, ok := consumerIndex[credential.Namespace+"/"+ + credential.ConsumerRef] + if !ok { + continue + } + if credential.Type == "" { + log.Errorf("invalid KongCredential: no Type provided") + continue + } + if !supportedCreds.Has(credential.Type) { + log.Errorf("invalid KongCredential: invalid Type provided") + continue + } + if credential.Config == nil { + log.Errorf("invalid KongCredential: empty config") + continue + } + err := cons.SetCredential(log, credential.Type, credential.Config) + if err != nil { + log.Errorf("failed to provision credential: %v", err) + continue + } + consumerIndex[credential.Namespace+"/"+credential.ConsumerRef] = cons + } + + // populate the consumer in the state + for _, c := range consumerIndex { + ks.Consumers = append(ks.Consumers, c) + } +} + +func (ks *KongState) FillOverrides(log logrus.FieldLogger, s store.Storer) { + for i := 0; i < len(ks.Services); i++ { + // Services + anns := ks.Services[i].K8sService.Annotations + kongIngress, err := getKongIngressForService(s, ks.Services[i].K8sService) + if err != nil { + log.WithFields(logrus.Fields{ + "service_name": ks.Services[i].K8sService.Name, + "service_namespace": ks.Services[i].K8sService.Namespace, + }).Errorf("failed to fetch KongIngress resource for Service: %v", err) + } + ks.Services[i].override(kongIngress, anns) + + // Routes + for j := 0; j < len(ks.Services[i].Routes); j++ { + var kongIngress *configurationv1.KongIngress + var err error + if ks.Services[i].Routes[j].IsTCP { + kongIngress, err = getKongIngressFromTCPIngress(s, + &ks.Services[i].Routes[j].TCPIngress) + if err != nil { + log.WithFields(logrus.Fields{ + "tcpingress_name": ks.Services[i].Routes[j].TCPIngress.Name, + "tcpingress_namespace": ks.Services[i].Routes[j].TCPIngress.Namespace, + }).Errorf("failed to fetch KongIngress resource for Ingress: %v", err) + } + } else { + kongIngress, err = getKongIngressFromIngress(s, + &ks.Services[i].Routes[j].Ingress) + if err != nil { + log.WithFields(logrus.Fields{ + "ingress_name": ks.Services[i].Routes[j].Ingress.Name, + "ingress_namespace": ks.Services[i].Routes[j].Ingress.Namespace, + }).Errorf("failed to fetch KongIngress resource for Ingress: %v", err) + } + } + + ks.Services[i].Routes[j].override(log, kongIngress) + } + } + + // Upstreams + for i := 0; i < len(ks.Upstreams); i++ { + kongIngress, err := getKongIngressForService(s, + ks.Upstreams[i].Service.K8sService) + anns := ks.Upstreams[i].Service.K8sService.Annotations + if err != nil { + log.WithFields(logrus.Fields{ + "service_name": ks.Upstreams[i].Service.K8sService.Name, + "service_namespace": ks.Upstreams[i].Service.K8sService.Namespace, + }).Errorf("failed to fetch KongIngress resource for Service: %v", err) + continue + } + ks.Upstreams[i].override(kongIngress, anns) + } +} + +func (ks *KongState) getPluginRelations() map[string]util.ForeignRelations { + // KongPlugin key (KongPlugin's name:namespace) to corresponding associations + pluginRels := map[string]util.ForeignRelations{} + addConsumerRelation := func(namespace, pluginName, identifier string) { + pluginKey := namespace + ":" + pluginName + relations, ok := pluginRels[pluginKey] + if !ok { + relations = util.ForeignRelations{} + } + relations.Consumer = append(relations.Consumer, identifier) + pluginRels[pluginKey] = relations + } + addRouteRelation := func(namespace, pluginName, identifier string) { + pluginKey := namespace + ":" + pluginName + relations, ok := pluginRels[pluginKey] + if !ok { + relations = util.ForeignRelations{} + } + relations.Route = append(relations.Route, identifier) + pluginRels[pluginKey] = relations + } + addServiceRelation := func(namespace, pluginName, identifier string) { + pluginKey := namespace + ":" + pluginName + relations, ok := pluginRels[pluginKey] + if !ok { + relations = util.ForeignRelations{} + } + relations.Service = append(relations.Service, identifier) + pluginRels[pluginKey] = relations + } + + for i := range ks.Services { + // service + svc := ks.Services[i].K8sService + pluginList := annotations.ExtractKongPluginsFromAnnotations( + svc.GetAnnotations()) + for _, pluginName := range pluginList { + addServiceRelation(svc.Namespace, pluginName, + *ks.Services[i].Name) + } + // route + for j := range ks.Services[i].Routes { + ingress := ks.Services[i].Routes[j].Ingress + pluginList := annotations.ExtractKongPluginsFromAnnotations(ingress.GetAnnotations()) + for _, pluginName := range pluginList { + addRouteRelation(ingress.Namespace, pluginName, *ks.Services[i].Routes[j].Name) + } + } + } + // consumer + for _, c := range ks.Consumers { + pluginList := annotations.ExtractKongPluginsFromAnnotations(c.K8sKongConsumer.GetAnnotations()) + for _, pluginName := range pluginList { + addConsumerRelation(c.K8sKongConsumer.Namespace, pluginName, *c.Username) + } + } + return pluginRels +} + +func buildPlugins(log logrus.FieldLogger, s store.Storer, pluginRels map[string]util.ForeignRelations) []Plugin { + var plugins []Plugin + + for pluginIdentifier, relations := range pluginRels { + identifier := strings.Split(pluginIdentifier, ":") + namespace, kongPluginName := identifier[0], identifier[1] + plugin, err := getPlugin(s, namespace, kongPluginName) + if err != nil { + log.WithFields(logrus.Fields{ + "kongplugin_name": kongPluginName, + "kongplugin_namespace": namespace, + }).Logger.Errorf("failed to fetch KongPlugin: %v", err) + continue + } + + for _, rel := range relations.GetCombinations() { + plugin := *plugin.DeepCopy() + // ID is populated because that is read by decK and in_memory + // translator too + if rel.Service != "" { + plugin.Service = &kong.Service{ID: kong.String(rel.Service)} + } + if rel.Route != "" { + plugin.Route = &kong.Route{ID: kong.String(rel.Route)} + } + if rel.Consumer != "" { + plugin.Consumer = &kong.Consumer{ID: kong.String(rel.Consumer)} + } + plugins = append(plugins, Plugin{plugin}) + } + } + + globalPlugins, err := globalPlugins(log, s) + if err != nil { + log.Errorf("failed to fetch global plugins: %v", err) + } + plugins = append(plugins, globalPlugins...) + + return plugins +} + +func globalPlugins(log logrus.FieldLogger, s store.Storer) ([]Plugin, error) { + // removed as of 0.10.0 + // only retrieved now to warn users + globalPlugins, err := s.ListGlobalKongPlugins() + if err != nil { + return nil, fmt.Errorf("error listing global KongPlugins: %w", err) + } + if len(globalPlugins) > 0 { + log.Warning("global KongPlugins found. These are no longer applied and", + " must be replaced with KongClusterPlugins.", + " Please run \"kubectl get kongplugin -l global=true --all-namespaces\" to list existing plugins") + } + res := make(map[string]Plugin) + var duplicates []string // keep track of duplicate + // TODO respect the oldest CRD + // Current behavior is to skip creating the plugin but in case + // of duplicate plugin definitions, we should respect the oldest one + // This is important since if a user comes in to k8s and creates a new + // CRD, the user now deleted an older plugin + + globalClusterPlugins, err := s.ListGlobalKongClusterPlugins() + if err != nil { + return nil, fmt.Errorf("error listing global KongClusterPlugins: %w", err) + } + for i := 0; i < len(globalClusterPlugins); i++ { + k8sPlugin := *globalClusterPlugins[i] + pluginName := k8sPlugin.PluginName + // empty pluginName skip it + if pluginName == "" { + log.WithFields(logrus.Fields{ + "kongclusterplugin_name": k8sPlugin.Name, + }).Errorf("invalid KongClusterPlugin: empty plugin property") + continue + } + if _, ok := res[pluginName]; ok { + log.Error("multiple KongPlugin definitions found with"+ + " 'global' label for '", pluginName, + "', the plugin will not be applied") + duplicates = append(duplicates, pluginName) + continue + } + if plugin, err := kongPluginFromK8SClusterPlugin(s, k8sPlugin); err == nil { + res[pluginName] = Plugin{ + Plugin: plugin, + } + } else { + log.WithFields(logrus.Fields{ + "kongclusterplugin_name": k8sPlugin.Name, + }).Errorf("failed to generate configuration from KongClusterPlugin: %v ", err) + } + } + for _, plugin := range duplicates { + delete(res, plugin) + } + var plugins []Plugin + for _, p := range res { + plugins = append(plugins, p) + } + return plugins, nil +} + +func (ks *KongState) FillPlugins(log logrus.FieldLogger, s store.Storer) { + ks.Plugins = buildPlugins(log, s, ks.getPluginRelations()) +} + +var supportedCreds = sets.NewString( + "acl", + "basic-auth", + "hmac-auth", + "jwt", + "key-auth", + "oauth2", +) diff --git a/internal/ingress/controller/parser/kongstate/kongstate_test.go b/internal/ingress/controller/parser/kongstate/kongstate_test.go new file mode 100644 index 0000000000..ed4f8de96d --- /dev/null +++ b/internal/ingress/controller/parser/kongstate/kongstate_test.go @@ -0,0 +1,273 @@ +package kongstate + +import ( + "reflect" + "testing" + + "github.com/kong/go-kong/kong" + "github.com/kong/kubernetes-ingress-controller/internal/ingress/annotations" + "github.com/kong/kubernetes-ingress-controller/internal/ingress/controller/parser/util" + configurationv1 "github.com/kong/kubernetes-ingress-controller/pkg/apis/configuration/v1" + corev1 "k8s.io/api/core/v1" + networking "k8s.io/api/networking/v1beta1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func Test_getPluginRelations(t *testing.T) { + type args struct { + state KongState + } + tests := []struct { + name string + args args + want map[string]util.ForeignRelations + }{ + { + name: "empty state", + want: map[string]util.ForeignRelations{}, + }, + { + name: "single consumer annotation", + args: args{ + state: KongState{ + Consumers: []Consumer{ + { + Consumer: kong.Consumer{ + Username: kong.String("foo-consumer"), + }, + K8sKongConsumer: configurationv1.KongConsumer{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "ns1", + Annotations: map[string]string{ + annotations.DeprecatedPluginsKey: "foo,bar", + }, + }, + }, + }, + }, + }, + }, + want: map[string]util.ForeignRelations{ + "ns1:foo": {Consumer: []string{"foo-consumer"}}, + "ns1:bar": {Consumer: []string{"foo-consumer"}}, + }, + }, + { + name: "single service annotation", + args: args{ + state: KongState{ + Services: []Service{ + { + Service: kong.Service{ + Name: kong.String("foo-service"), + }, + K8sService: corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "ns1", + Annotations: map[string]string{ + annotations.DeprecatedPluginsKey: "foo,bar", + }, + }, + }, + }, + }, + }, + }, + want: map[string]util.ForeignRelations{ + "ns1:foo": {Service: []string{"foo-service"}}, + "ns1:bar": {Service: []string{"foo-service"}}, + }, + }, + { + name: "single Ingress annotation", + args: args{ + state: KongState{ + Services: []Service{ + { + Service: kong.Service{ + Name: kong.String("foo-service"), + }, + Routes: []Route{ + { + Route: kong.Route{ + Name: kong.String("foo-route"), + }, + Ingress: networking.Ingress{ + ObjectMeta: metav1.ObjectMeta{ + Name: "some-ingress", + Namespace: "ns2", + Annotations: map[string]string{ + annotations.DeprecatedPluginsKey: "foo,bar", + }, + }, + }, + }, + }, + }, + }, + }, + }, + want: map[string]util.ForeignRelations{ + "ns2:foo": {Route: []string{"foo-route"}}, + "ns2:bar": {Route: []string{"foo-route"}}, + }, + }, + { + name: "multiple routes with annotation", + args: args{ + state: KongState{ + Services: []Service{ + { + Service: kong.Service{ + Name: kong.String("foo-service"), + }, + Routes: []Route{ + { + Route: kong.Route{ + Name: kong.String("foo-route"), + }, + Ingress: networking.Ingress{ + ObjectMeta: metav1.ObjectMeta{ + Name: "some-ingress", + Namespace: "ns2", + Annotations: map[string]string{ + annotations.DeprecatedPluginsKey: "foo,bar", + }, + }, + }, + }, + { + Route: kong.Route{ + Name: kong.String("bar-route"), + }, + Ingress: networking.Ingress{ + ObjectMeta: metav1.ObjectMeta{ + Name: "some-ingress", + Namespace: "ns2", + Annotations: map[string]string{ + annotations.DeprecatedPluginsKey: "bar,baz", + }, + }, + }, + }, + }, + }, + }, + }, + }, + want: map[string]util.ForeignRelations{ + "ns2:foo": {Route: []string{"foo-route"}}, + "ns2:bar": {Route: []string{"foo-route", "bar-route"}}, + "ns2:baz": {Route: []string{"bar-route"}}, + }, + }, + { + name: "multiple consumers, routes and services", + args: args{ + state: KongState{ + Consumers: []Consumer{ + { + Consumer: kong.Consumer{ + Username: kong.String("foo-consumer"), + }, + K8sKongConsumer: configurationv1.KongConsumer{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "ns1", + Annotations: map[string]string{ + annotations.DeprecatedPluginsKey: "foo,bar", + }, + }, + }, + }, + { + Consumer: kong.Consumer{ + Username: kong.String("foo-consumer"), + }, + K8sKongConsumer: configurationv1.KongConsumer{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "ns2", + Annotations: map[string]string{ + annotations.DeprecatedPluginsKey: "foo,bar", + }, + }, + }, + }, + { + Consumer: kong.Consumer{ + Username: kong.String("bar-consumer"), + }, + K8sKongConsumer: configurationv1.KongConsumer{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "ns1", + Annotations: map[string]string{ + annotations.DeprecatedPluginsKey: "foobar", + }, + }, + }, + }, + }, + Services: []Service{ + { + Service: kong.Service{ + Name: kong.String("foo-service"), + }, + K8sService: corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "ns1", + Annotations: map[string]string{ + annotations.DeprecatedPluginsKey: "foo,bar", + }, + }, + }, + Routes: []Route{ + { + Route: kong.Route{ + Name: kong.String("foo-route"), + }, + Ingress: networking.Ingress{ + ObjectMeta: metav1.ObjectMeta{ + Name: "some-ingress", + Namespace: "ns2", + Annotations: map[string]string{ + annotations.DeprecatedPluginsKey: "foo,bar", + }, + }, + }, + }, + { + Route: kong.Route{ + Name: kong.String("bar-route"), + }, + Ingress: networking.Ingress{ + ObjectMeta: metav1.ObjectMeta{ + Name: "some-ingress", + Namespace: "ns2", + Annotations: map[string]string{ + annotations.DeprecatedPluginsKey: "bar,baz", + }, + }, + }, + }, + }, + }, + }, + }, + }, + want: map[string]util.ForeignRelations{ + "ns1:foo": {Consumer: []string{"foo-consumer"}, Service: []string{"foo-service"}}, + "ns1:bar": {Consumer: []string{"foo-consumer"}, Service: []string{"foo-service"}}, + "ns1:foobar": {Consumer: []string{"bar-consumer"}}, + "ns2:foo": {Consumer: []string{"foo-consumer"}, Route: []string{"foo-route"}}, + "ns2:bar": {Consumer: []string{"foo-consumer"}, Route: []string{"foo-route", "bar-route"}}, + "ns2:baz": {Route: []string{"bar-route"}}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.args.state.getPluginRelations(); !reflect.DeepEqual(got, tt.want) { + t.Errorf("getPluginRelations() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/internal/ingress/controller/parser/kongstate/route.go b/internal/ingress/controller/parser/kongstate/route.go new file mode 100644 index 0000000000..955cd33319 --- /dev/null +++ b/internal/ingress/controller/parser/kongstate/route.go @@ -0,0 +1,274 @@ +package kongstate + +import ( + "regexp" + "strconv" + "strings" + + "github.com/kong/go-kong/kong" + "github.com/kong/kubernetes-ingress-controller/internal/ingress/annotations" + "github.com/kong/kubernetes-ingress-controller/internal/ingress/controller/parser/util" + configurationv1 "github.com/kong/kubernetes-ingress-controller/pkg/apis/configuration/v1" + configurationv1beta1 "github.com/kong/kubernetes-ingress-controller/pkg/apis/configuration/v1beta1" + "github.com/sirupsen/logrus" + networking "k8s.io/api/networking/v1beta1" +) + +// Route represents a Kong Route and holds a reference to the Ingress +// rule. +type Route struct { + kong.Route + + // Ingress object associated with this route + Ingress networking.Ingress + // TCPIngress object associated with this route + TCPIngress configurationv1beta1.TCPIngress + // Is this route coming from TCPIngress or networking.Ingress? + IsTCP bool + Plugins []kong.Plugin +} + +var validMethods = regexp.MustCompile(`\A[A-Z]+$`) + +// normalizeProtocols prevents users from mismatching grpc/http +func (r *Route) normalizeProtocols() { + protocols := r.Protocols + var http, grpc bool + + for _, protocol := range protocols { + if strings.Contains(*protocol, "grpc") { + grpc = true + } + if strings.Contains(*protocol, "http") { + http = true + } + if !util.ValidateProtocol(*protocol) { + http = true + } + } + + if grpc && http { + r.Protocols = kong.StringSlice("http", "https") + } +} + +// useSSLProtocol updates the protocol of the route to either https or grpcs, or https and grpcs +func (r *Route) useSSLProtocol() { + var http, grpc bool + var prots []*string + + for _, val := range r.Protocols { + + if strings.Contains(*val, "grpc") { + grpc = true + } + + if strings.Contains(*val, "http") { + http = true + } + } + + if grpc { + prots = append(prots, kong.String("grpcs")) + } + if http { + prots = append(prots, kong.String("https")) + } + + if !grpc && !http { + prots = append(prots, kong.String("https")) + } + + r.Protocols = prots +} + +func (r *Route) overrideStripPath(anns map[string]string) { + if r == nil { + return + } + + stripPathValue := annotations.ExtractStripPath(anns) + if stripPathValue == "" { + return + } + stripPathValue = strings.ToLower(stripPathValue) + switch stripPathValue { + case "true": + r.StripPath = kong.Bool(true) + case "false": + r.StripPath = kong.Bool(false) + default: + return + } +} + +func (r *Route) overrideProtocols(anns map[string]string) { + protocols := annotations.ExtractProtocolNames(anns) + var prots []*string + for _, prot := range protocols { + if !util.ValidateProtocol(prot) { + return + } + prots = append(prots, kong.String(prot)) + } + + r.Protocols = prots +} + +func (r *Route) overrideHTTPSRedirectCode(anns map[string]string) { + + if annotations.HasForceSSLRedirectAnnotation(anns) { + r.HTTPSRedirectStatusCode = kong.Int(302) + r.useSSLProtocol() + } + + code := annotations.ExtractHTTPSRedirectStatusCode(anns) + if code == "" { + return + } + statusCode, err := strconv.Atoi(code) + if err != nil { + return + } + if statusCode != 426 && + statusCode != 301 && + statusCode != 302 && + statusCode != 307 && + statusCode != 308 { + return + } + + r.HTTPSRedirectStatusCode = kong.Int(statusCode) +} + +func (r *Route) overridePreserveHost(anns map[string]string) { + preserveHostValue := annotations.ExtractPreserveHost(anns) + if preserveHostValue == "" { + return + } + preserveHostValue = strings.ToLower(preserveHostValue) + switch preserveHostValue { + case "true": + r.PreserveHost = kong.Bool(true) + case "false": + r.PreserveHost = kong.Bool(false) + default: + return + } +} + +func (r *Route) overrideRegexPriority(anns map[string]string) { + priority := annotations.ExtractRegexPriority(anns) + if priority == "" { + return + } + regexPriority, err := strconv.Atoi(priority) + if err != nil { + return + } + + r.RegexPriority = kong.Int(regexPriority) +} + +func (r *Route) overrideMethods(log logrus.FieldLogger, anns map[string]string) { + annMethods := annotations.ExtractMethods(anns) + if len(annMethods) == 0 { + return + } + var methods []*string + for _, method := range annMethods { + sanitizedMethod := strings.TrimSpace(strings.ToUpper(method)) + if validMethods.MatchString(sanitizedMethod) { + methods = append(methods, kong.String(sanitizedMethod)) + } else { + // if any method is invalid (not an uppercase alpha string), + // discard everything + log.WithField("kongroute", r.Name).Errorf("invalid method: %v", method) + return + } + } + + r.Methods = methods +} + +// overrideByAnnotation sets Route protocols via annotation +func (r *Route) overrideByAnnotation(log logrus.FieldLogger) { + anns := r.Ingress.Annotations + if r.IsTCP { + anns = r.TCPIngress.Annotations + } + r.overrideProtocols(anns) + r.overrideStripPath(anns) + r.overrideHTTPSRedirectCode(anns) + r.overridePreserveHost(anns) + r.overrideRegexPriority(anns) + r.overrideMethods(log, anns) +} + +// override sets Route fields by KongIngress first, then by annotation +func (r *Route) override(log logrus.FieldLogger, kongIngress *configurationv1.KongIngress) { + if r == nil { + return + } + r.overrideByKongIngress(log, kongIngress) + r.overrideByAnnotation(log) + r.normalizeProtocols() + for _, val := range r.Protocols { + if *val == "grpc" || *val == "grpcs" { + // grpc(s) doesn't accept strip_path + r.StripPath = nil + break + } + } +} + +// overrideByKongIngress sets Route fields by KongIngress +func (r *Route) overrideByKongIngress(log logrus.FieldLogger, kongIngress *configurationv1.KongIngress) { + if kongIngress == nil || kongIngress.Route == nil { + return + } + + ir := kongIngress.Route + if len(ir.Methods) != 0 { + invalid := false + var methods []*string + for _, method := range ir.Methods { + sanitizedMethod := strings.TrimSpace(strings.ToUpper(*method)) + if validMethods.MatchString(sanitizedMethod) { + methods = append(methods, kong.String(sanitizedMethod)) + } else { + // if any method is invalid (not an uppercase alpha string), + // discard everything + log.WithFields(logrus.Fields{ + "ingress_namespace": r.Ingress.Namespace, + "ingress_name": r.Ingress.Name, + }).Errorf("ingress contains invalid method: '%v'", *method) + invalid = true + } + } + if !invalid { + r.Methods = methods + } + } + if len(ir.Headers) != 0 { + r.Headers = ir.Headers + } + if len(ir.Protocols) != 0 { + r.Protocols = cloneStringPointerSlice(ir.Protocols...) + } + if ir.RegexPriority != nil { + r.RegexPriority = kong.Int(*ir.RegexPriority) + } + if ir.StripPath != nil { + r.StripPath = kong.Bool(*ir.StripPath) + } + if ir.PreserveHost != nil { + r.PreserveHost = kong.Bool(*ir.PreserveHost) + } + if ir.HTTPSRedirectStatusCode != nil { + r.HTTPSRedirectStatusCode = kong.Int(*ir.HTTPSRedirectStatusCode) + } + if ir.PathHandling != nil { + r.PathHandling = kong.String(*ir.PathHandling) + } +} diff --git a/internal/ingress/controller/parser/kongstate/route_test.go b/internal/ingress/controller/parser/kongstate/route_test.go new file mode 100644 index 0000000000..c1934a680f --- /dev/null +++ b/internal/ingress/controller/parser/kongstate/route_test.go @@ -0,0 +1,778 @@ +package kongstate + +import ( + "reflect" + "testing" + + "github.com/kong/go-kong/kong" + configurationv1 "github.com/kong/kubernetes-ingress-controller/pkg/apis/configuration/v1" + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" + networking "k8s.io/api/networking/v1beta1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestOverrideRoute(t *testing.T) { + assert := assert.New(t) + + testTable := []struct { + inRoute Route + inKongIngresss configurationv1.KongIngress + outRoute Route + }{ + { + Route{ + Route: kong.Route{ + Hosts: kong.StringSlice("foo.com", "bar.com"), + }, + }, + configurationv1.KongIngress{}, + Route{ + Route: kong.Route{ + Hosts: kong.StringSlice("foo.com", "bar.com"), + }, + }, + }, + { + Route{ + Route: kong.Route{ + Hosts: kong.StringSlice("foo.com", "bar.com"), + }, + }, + configurationv1.KongIngress{ + Route: &kong.Route{ + Methods: kong.StringSlice("GET", "POST"), + }, + }, + Route{ + Route: kong.Route{ + Hosts: kong.StringSlice("foo.com", "bar.com"), + Methods: kong.StringSlice("GET", "POST"), + }, + }, + }, + { + Route{ + Route: kong.Route{ + Hosts: kong.StringSlice("foo.com", "bar.com"), + }, + }, + configurationv1.KongIngress{ + Route: &kong.Route{ + Methods: kong.StringSlice("GET ", "post"), + }, + }, + Route{ + Route: kong.Route{ + Hosts: kong.StringSlice("foo.com", "bar.com"), + Methods: kong.StringSlice("GET", "POST"), + }, + }, + }, + { + Route{ + Route: kong.Route{ + Hosts: kong.StringSlice("foo.com", "bar.com"), + }, + }, + configurationv1.KongIngress{ + Route: &kong.Route{ + Methods: kong.StringSlice("GET", "-1"), + }, + }, + Route{ + Route: kong.Route{ + Hosts: kong.StringSlice("foo.com", "bar.com"), + }, + }, + }, + { + Route{ + Route: kong.Route{ + Hosts: kong.StringSlice("foo.com", "bar.com"), + }, + }, + configurationv1.KongIngress{ + Route: &kong.Route{ + HTTPSRedirectStatusCode: kong.Int(302), + }, + }, + Route{ + Route: kong.Route{ + Hosts: kong.StringSlice("foo.com", "bar.com"), + HTTPSRedirectStatusCode: kong.Int(302), + }, + }, + }, + { + Route{ + Route: kong.Route{ + Hosts: kong.StringSlice("foo.com", "bar.com"), + PreserveHost: kong.Bool(true), + StripPath: kong.Bool(true), + }, + }, + configurationv1.KongIngress{ + Route: &kong.Route{ + Protocols: kong.StringSlice("http"), + PreserveHost: kong.Bool(false), + StripPath: kong.Bool(false), + RegexPriority: kong.Int(10), + }, + }, + Route{ + Route: kong.Route{ + Hosts: kong.StringSlice("foo.com", "bar.com"), + Protocols: kong.StringSlice("http"), + PreserveHost: kong.Bool(false), + StripPath: kong.Bool(false), + RegexPriority: kong.Int(10), + }, + }, + }, + { + Route{ + Route: kong.Route{ + Hosts: kong.StringSlice("foo.com"), + Protocols: kong.StringSlice("http", "https"), + }, + }, + configurationv1.KongIngress{ + Route: &kong.Route{ + Headers: map[string][]string{ + "foo-header": {"bar-value"}, + }, + }, + }, + Route{ + Route: kong.Route{ + Hosts: kong.StringSlice("foo.com"), + Protocols: kong.StringSlice("http", "https"), + Headers: map[string][]string{ + "foo-header": {"bar-value"}, + }, + }, + }, + }, + { + Route{ + Route: kong.Route{ + Hosts: kong.StringSlice("foo.com"), + }, + }, + configurationv1.KongIngress{ + Route: &kong.Route{ + Protocols: kong.StringSlice("grpc", "grpcs"), + }, + }, + Route{ + Route: kong.Route{ + Hosts: kong.StringSlice("foo.com"), + Protocols: kong.StringSlice("grpc", "grpcs"), + StripPath: nil, + }, + }, + }, + { + Route{ + Route: kong.Route{ + Hosts: kong.StringSlice("foo.com"), + }, + }, + configurationv1.KongIngress{ + Route: &kong.Route{ + PathHandling: kong.String("v1"), + }, + }, + Route{ + Route: kong.Route{ + Hosts: kong.StringSlice("foo.com"), + PathHandling: kong.String("v1"), + }, + }, + }, + } + + for _, testcase := range testTable { + testcase.inRoute.override(logrus.New(), &testcase.inKongIngresss) + assert.Equal(testcase.inRoute, testcase.outRoute) + } + + assert.NotPanics(func() { + var nilRoute *Route + nilRoute.override(logrus.New(), nil) + }) +} + +func TestOverrideRoutePriority(t *testing.T) { + assert := assert.New(t) + var route Route + route = Route{ + Route: kong.Route{ + Hosts: kong.StringSlice("foo.com", "bar.com"), + }, + } + kongIngress := configurationv1.KongIngress{ + Route: &kong.Route{ + Hosts: kong.StringSlice("foo.com", "bar.com"), + }, + } + + netIngress := networking.Ingress{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + "configuration.konghq.com/protocols": "grpc,grpcs", + }, + }, + } + + route = Route{ + Route: kong.Route{ + Hosts: kong.StringSlice("foo.com", "bar.com"), + }, + Ingress: netIngress, + } + route.override(logrus.New(), &kongIngress) + assert.Equal(route.Hosts, kong.StringSlice("foo.com", "bar.com")) + assert.Equal(route.Protocols, kong.StringSlice("grpc", "grpcs")) +} + +func TestOverrideRouteByKongIngress(t *testing.T) { + assert := assert.New(t) + route := Route{ + Route: kong.Route{ + Hosts: kong.StringSlice("foo.com", "bar.com"), + }, + } + kongIngress := configurationv1.KongIngress{ + Route: &kong.Route{ + Hosts: kong.StringSlice("foo.com", "bar.com"), + }, + } + + route.overrideByKongIngress(logrus.New(), &kongIngress) + assert.Equal(route.Hosts, kong.StringSlice("foo.com", "bar.com")) + assert.NotPanics(func() { + var nilRoute *Route + nilRoute.override(logrus.New(), nil) + }) +} +func TestOverrideRouteByAnnotation(t *testing.T) { + assert := assert.New(t) + var route Route + route = Route{ + Route: kong.Route{ + Hosts: kong.StringSlice("foo.com", "bar.com"), + }, + } + + netIngress := networking.Ingress{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + "configuration.konghq.com/protocols": "grpc,grpcs", + }, + }, + } + + route = Route{ + Route: kong.Route{ + Hosts: kong.StringSlice("foo.com", "bar.com"), + }, + Ingress: netIngress, + } + route.overrideByAnnotation(logrus.New()) + assert.Equal(route.Hosts, kong.StringSlice("foo.com", "bar.com")) + assert.Equal(route.Protocols, kong.StringSlice("grpc", "grpcs")) + + assert.NotPanics(func() { + var nilRoute *Route + nilRoute.override(logrus.New(), nil) + }) +} + +func TestNormalizeProtocols(t *testing.T) { + assert := assert.New(t) + testTable := []struct { + inRoute Route + outRoute Route + }{ + { + Route{ + Route: kong.Route{ + Protocols: kong.StringSlice("grpc", "grpcs"), + }, + }, + Route{ + Route: kong.Route{ + Protocols: kong.StringSlice("grpc", "grpcs"), + }, + }, + }, + { + Route{ + Route: kong.Route{ + Protocols: kong.StringSlice("http", "https"), + }, + }, + Route{ + Route: kong.Route{ + Protocols: kong.StringSlice("http", "https"), + }, + }, + }, + { + Route{ + Route: kong.Route{ + Protocols: kong.StringSlice("grpc", "https"), + }, + }, + Route{ + Route: kong.Route{ + Protocols: kong.StringSlice("http", "https"), + }, + }, + }, + } + + for _, testcase := range testTable { + testcase.inRoute.normalizeProtocols() + assert.Equal(testcase.inRoute.Protocols, testcase.outRoute.Protocols) + } + + assert.NotPanics(func() { + var nilUpstream *Upstream + nilUpstream.override(nil, make(map[string]string)) + }) +} + +func TestUseSSLProtocol(t *testing.T) { + assert := assert.New(t) + testTable := []struct { + inRoute Route + outRoute kong.Route + }{ + { + Route{ + Route: kong.Route{ + Protocols: kong.StringSlice("grpc", "grpcs"), + }, + }, + kong.Route{ + Protocols: kong.StringSlice("grpcs"), + }, + }, + { + Route{ + Route: kong.Route{ + Protocols: kong.StringSlice("http", "https"), + }, + }, + kong.Route{ + Protocols: kong.StringSlice("https"), + }, + }, + { + Route{ + Route: kong.Route{ + Protocols: kong.StringSlice("grpcs", "https"), + }, + }, + + kong.Route{ + Protocols: kong.StringSlice("grpcs", "https"), + }, + }, + { + Route{ + Route: kong.Route{ + Protocols: kong.StringSlice("grpc", "http"), + }, + }, + kong.Route{ + Protocols: kong.StringSlice("grpcs", "https"), + }, + }, + { + Route{ + Route: kong.Route{ + Protocols: []*string{}, + }, + }, + kong.Route{ + Protocols: kong.StringSlice("https"), + }, + }, + } + + for _, testcase := range testTable { + testcase.inRoute.useSSLProtocol() + assert.Equal(testcase.inRoute.Protocols, testcase.outRoute.Protocols) + } +} + +func Test_overrideRouteStripPath(t *testing.T) { + type args struct { + route Route + anns map[string]string + } + tests := []struct { + name string + args args + want kong.Route + }{ + {}, + { + name: "basic empty route", + args: args{ + route: Route{Route: kong.Route{}}, + }, + want: kong.Route{}, + }, + { + name: "set to false", + args: args{ + route: Route{ + Route: kong.Route{}}, + anns: map[string]string{ + "configuration.konghq.com/strip-path": "false", + }, + }, + want: kong.Route{ + StripPath: kong.Bool(false), + }, + }, + { + name: "set to true and case insensitive", + args: args{ + route: Route{ + Route: kong.Route{}, + }, + anns: map[string]string{ + "configuration.konghq.com/strip-path": "truE", + }, + }, + want: kong.Route{ + StripPath: kong.Bool(true), + }, + }, + { + name: "overrides any other value", + args: args{ + route: Route{ + Route: kong.Route{ + StripPath: kong.Bool(false), + }, + }, + anns: map[string]string{ + "configuration.konghq.com/strip-path": "truE", + }, + }, + want: kong.Route{ + StripPath: kong.Bool(true), + }, + }, + { + name: "random value", + args: args{ + route: Route{ + Route: kong.Route{ + StripPath: kong.Bool(false), + }, + }, + anns: map[string]string{ + "configuration.konghq.com/strip-path": "42", + }, + }, + want: kong.Route{ + StripPath: kong.Bool(false), + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.args.route.overrideStripPath(tt.args.anns) + if !reflect.DeepEqual(tt.args.route.Route, tt.want) { + t.Errorf("overrideRouteStripPath() got = %v, want %v", &tt.args.route.Route, tt.want) + } + }) + } +} + +func Test_overrideRouteHTTPSRedirectCode(t *testing.T) { + type args struct { + route Route + anns map[string]string + } + tests := []struct { + name string + args args + want Route + }{ + {name: "basic empty route"}, + { + name: "basic sanity", + args: args{ + anns: map[string]string{ + "konghq.com/https-redirect-status-code": "301", + }, + }, + want: Route{ + Route: kong.Route{ + HTTPSRedirectStatusCode: kong.Int(301), + }, + }, + }, + { + name: "random integer value", + args: args{ + anns: map[string]string{ + "konghq.com/https-redirect-status-code": "42", + }, + }, + }, + { + name: "random string", + args: args{ + anns: map[string]string{ + "konghq.com/https-redirect-status-code": "foo", + }, + }, + }, + { + name: "force ssl annotation set to true and protocols is not set", + args: args{ + anns: map[string]string{ + "ingress.kubernetes.io/force-ssl-redirect": "true", + }, + }, + want: Route{ + Route: kong.Route{ + HTTPSRedirectStatusCode: kong.Int(302), + Protocols: []*string{kong.String("https")}, + }, + }, + }, + { + name: "force ssl annotation set to true and protocol is set to grpc", + args: args{ + route: Route{ + Route: kong.Route{ + Protocols: []*string{kong.String("grpc")}, + }, + }, + anns: map[string]string{ + "ingress.kubernetes.io/force-ssl-redirect": "true", + "konghq.com/protocols": "grpc", + }, + }, + want: Route{ + Route: kong.Route{ + HTTPSRedirectStatusCode: kong.Int(302), + Protocols: []*string{kong.String("grpcs")}, + }, + }, + }, + { + name: "force ssl annotation set to false", + args: args{ + anns: map[string]string{ + "ingress.kubernetes.io/force-ssl-redirect": "false", + }, + }, + }, + { + name: "force ssl annotation set to true and HTTPS redirect code set to 307", + args: args{ + anns: map[string]string{ + "ingress.kubernetes.io/force-ssl-redirect": "true", + "konghq.com/https-redirect-status-code": "307", + }, + }, + want: Route{ + Route: kong.Route{ + HTTPSRedirectStatusCode: kong.Int(307), + Protocols: []*string{kong.String("https")}, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.args.route.overrideHTTPSRedirectCode(tt.args.anns) + if !reflect.DeepEqual(tt.args.route, tt.want) { + t.Errorf("overrideRouteHTTPSRedirectCode() got = %v, want %v", tt.args.route, tt.want) + } + }) + } +} + +func Test_overrideRoutePreserveHost(t *testing.T) { + type args struct { + route Route + anns map[string]string + } + tests := []struct { + name string + args args + want Route + }{ + {name: "basic empty route"}, + { + name: "basic sanity", + args: args{ + anns: map[string]string{ + "konghq.com/preserve-host": "true", + }, + }, + want: Route{ + Route: kong.Route{ + PreserveHost: kong.Bool(true), + }, + }, + }, + { + name: "case insensitive", + args: args{ + anns: map[string]string{ + "konghq.com/preserve-host": "faLSe", + }, + }, + want: Route{ + Route: kong.Route{ + PreserveHost: kong.Bool(false), + }, + }, + }, + { + name: "random integer value", + args: args{ + anns: map[string]string{ + "konghq.com/https-redirect-status-code": "42", + }, + }, + }, + { + name: "random string", + args: args{ + anns: map[string]string{ + "konghq.com/https-redirect-status-code": "foo", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.args.route.overridePreserveHost(tt.args.anns) + if !reflect.DeepEqual(tt.args.route, tt.want) { + t.Errorf("overrideRoutePreserveHost() got = %v, want %v", tt.args.route, tt.want) + } + }) + } +} + +func Test_overrideRouteRegexPriority(t *testing.T) { + type args struct { + route Route + anns map[string]string + } + tests := []struct { + name string + args args + want Route + }{ + {name: "basic empty route"}, + { + name: "basic sanity", + args: args{ + anns: map[string]string{ + "konghq.com/regex-priority": "10", + }, + }, + want: Route{ + Route: kong.Route{ + RegexPriority: kong.Int(10), + }, + }, + }, + { + name: "negative integer", + args: args{ + anns: map[string]string{ + "konghq.com/regex-priority": "-10", + }, + }, + want: Route{ + Route: kong.Route{ + RegexPriority: kong.Int(-10), + }, + }, + }, + { + name: "random float value", + args: args{ + anns: map[string]string{ + "konghq.com/regex-priority": "42.42", + }, + }, + }, + { + name: "random string", + args: args{ + anns: map[string]string{ + "konghq.com/regex-priority": "foo", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.args.route.overrideRegexPriority(tt.args.anns) + if !reflect.DeepEqual(tt.args.route, tt.want) { + t.Errorf("overrideRouteRegexPriority() got = %v, want %v", tt.args.route, tt.want) + } + }) + } +} + +func Test_overrideRouteMethods(t *testing.T) { + type args struct { + route Route + anns map[string]string + } + tests := []struct { + name string + args args + want Route + }{ + {name: "basic empty route"}, + { + name: "basic sanity", + args: args{ + anns: map[string]string{ + "konghq.com/methods": "POST,GET", + }, + }, + want: Route{ + Route: kong.Route{ + Methods: kong.StringSlice("POST", "GET"), + }, + }, + }, + { + name: "non-string", + args: args{ + anns: map[string]string{ + "konghq.com/methods": "-10,GET", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.args.route.overrideMethods(logrus.New(), tt.args.anns) + if !reflect.DeepEqual(tt.args.route, tt.want) { + t.Errorf("overrideRouteMethods() got = %v, want %v", tt.args.route, tt.want) + } + }) + } +} diff --git a/internal/ingress/controller/parser/kongstate/service.go b/internal/ingress/controller/parser/kongstate/service.go new file mode 100644 index 0000000000..551ac5c97f --- /dev/null +++ b/internal/ingress/controller/parser/kongstate/service.go @@ -0,0 +1,100 @@ +package kongstate + +import ( + "strings" + + "github.com/kong/go-kong/kong" + "github.com/kong/kubernetes-ingress-controller/internal/ingress/annotations" + "github.com/kong/kubernetes-ingress-controller/internal/ingress/controller/parser/util" + configurationv1 "github.com/kong/kubernetes-ingress-controller/pkg/apis/configuration/v1" + corev1 "k8s.io/api/core/v1" +) + +// Service represents a service in Kong and holds routes associated with the +// service and other k8s metadata. +type Service struct { + kong.Service + Backend ServiceBackend + Namespace string + Routes []Route + Plugins []kong.Plugin + K8sService corev1.Service +} + +// overrideByKongIngress sets Service fields by KongIngress +func (s *Service) overrideByKongIngress(kongIngress *configurationv1.KongIngress) { + if kongIngress == nil || kongIngress.Proxy == nil { + return + } + p := kongIngress.Proxy + if p.Protocol != nil { + s.Protocol = kong.String(*p.Protocol) + } + if p.Path != nil { + s.Path = kong.String(*p.Path) + } + if p.Retries != nil { + s.Retries = kong.Int(*p.Retries) + } + if p.ConnectTimeout != nil { + s.ConnectTimeout = kong.Int(*p.ConnectTimeout) + } + if p.ReadTimeout != nil { + s.ReadTimeout = kong.Int(*p.ReadTimeout) + } + if p.WriteTimeout != nil { + s.WriteTimeout = kong.Int(*p.WriteTimeout) + } +} + +func (s *Service) overridePath(anns map[string]string) { + if s == nil { + return + } + path := annotations.ExtractPath(anns) + if path == "" { + return + } + // kong errors if path doesn't start with `/` + if !strings.HasPrefix(path, "/") { + return + } + s.Path = kong.String(path) +} + +func (s *Service) overrideProtocol(anns map[string]string) { + if s == nil { + return + } + protocol := annotations.ExtractProtocolName(anns) + if protocol == "" || !util.ValidateProtocol(protocol) { + return + } + s.Protocol = kong.String(protocol) +} + +// overrideByAnnotation modifies the Kong service based on annotations +// on the Kubernetes service. +func (s *Service) overrideByAnnotation(anns map[string]string) { + if s == nil { + return + } + s.overrideProtocol(anns) + s.overridePath(anns) +} + +// override sets Service fields by KongIngress first, then by annotation +func (s *Service) override(kongIngress *configurationv1.KongIngress, + anns map[string]string) { + if s == nil { + return + } + + s.overrideByKongIngress(kongIngress) + s.overrideByAnnotation(anns) + + if *s.Protocol == "grpc" || *s.Protocol == "grpcs" { + // grpc(s) doesn't accept a path + s.Path = nil + } +} diff --git a/internal/ingress/controller/parser/kongstate/service_test.go b/internal/ingress/controller/parser/kongstate/service_test.go new file mode 100644 index 0000000000..de83cc3b02 --- /dev/null +++ b/internal/ingress/controller/parser/kongstate/service_test.go @@ -0,0 +1,434 @@ +package kongstate + +import ( + "reflect" + "testing" + + "github.com/kong/go-kong/kong" + configurationv1 "github.com/kong/kubernetes-ingress-controller/pkg/apis/configuration/v1" + "github.com/stretchr/testify/assert" +) + +func TestOverrideService(t *testing.T) { + assert := assert.New(t) + + testTable := []struct { + inService Service + inKongIngresss configurationv1.KongIngress + outService Service + inAnnotation map[string]string + }{ + { + Service{ + Service: kong.Service{ + Host: kong.String("foo.com"), + Port: kong.Int(80), + Name: kong.String("foo"), + Protocol: kong.String("http"), + Path: kong.String("/"), + }, + }, + configurationv1.KongIngress{ + Proxy: &kong.Service{}, + }, + Service{ + Service: kong.Service{ + Host: kong.String("foo.com"), + Port: kong.Int(80), + Name: kong.String("foo"), + Protocol: kong.String("http"), + Path: kong.String("/"), + }, + }, + map[string]string{}, + }, + { + Service{ + Service: kong.Service{ + Host: kong.String("foo.com"), + Port: kong.Int(80), + Name: kong.String("foo"), + Protocol: kong.String("http"), + Path: kong.String("/"), + }, + }, + configurationv1.KongIngress{ + Proxy: &kong.Service{ + Protocol: kong.String("https"), + }, + }, + Service{ + Service: kong.Service{ + Host: kong.String("foo.com"), + Port: kong.Int(80), + Name: kong.String("foo"), + Protocol: kong.String("https"), + Path: kong.String("/"), + }, + }, + map[string]string{}, + }, + { + Service{ + Service: kong.Service{ + Host: kong.String("foo.com"), + Port: kong.Int(80), + Name: kong.String("foo"), + Protocol: kong.String("http"), + Path: kong.String("/"), + }, + }, + configurationv1.KongIngress{ + Proxy: &kong.Service{ + Retries: kong.Int(0), + }, + }, + Service{ + Service: kong.Service{ + Host: kong.String("foo.com"), + Port: kong.Int(80), + Name: kong.String("foo"), + Protocol: kong.String("http"), + Path: kong.String("/"), + Retries: kong.Int(0), + }, + }, + map[string]string{}, + }, + { + Service{ + Service: kong.Service{ + Host: kong.String("foo.com"), + Port: kong.Int(80), + Name: kong.String("foo"), + Protocol: kong.String("http"), + Path: kong.String("/"), + }, + }, + configurationv1.KongIngress{ + Proxy: &kong.Service{ + Path: kong.String("/new-path"), + }, + }, + Service{ + Service: kong.Service{ + Host: kong.String("foo.com"), + Port: kong.Int(80), + Name: kong.String("foo"), + Protocol: kong.String("http"), + Path: kong.String("/new-path"), + }, + }, + map[string]string{}, + }, + { + Service{ + Service: kong.Service{ + Host: kong.String("foo.com"), + Port: kong.Int(80), + Name: kong.String("foo"), + Protocol: kong.String("http"), + Path: kong.String("/"), + }, + }, + configurationv1.KongIngress{ + Proxy: &kong.Service{ + Retries: kong.Int(1), + }, + }, + Service{ + Service: kong.Service{ + Host: kong.String("foo.com"), + Port: kong.Int(80), + Name: kong.String("foo"), + Protocol: kong.String("http"), + Path: kong.String("/"), + Retries: kong.Int(1), + }, + }, + map[string]string{}, + }, + { + Service{ + Service: kong.Service{ + Host: kong.String("foo.com"), + Port: kong.Int(80), + Name: kong.String("foo"), + Protocol: kong.String("http"), + Path: kong.String("/"), + }, + }, + configurationv1.KongIngress{ + Proxy: &kong.Service{ + ConnectTimeout: kong.Int(100), + ReadTimeout: kong.Int(100), + WriteTimeout: kong.Int(100), + }, + }, + Service{ + Service: kong.Service{ + Host: kong.String("foo.com"), + Port: kong.Int(80), + Name: kong.String("foo"), + Protocol: kong.String("http"), + Path: kong.String("/"), + ConnectTimeout: kong.Int(100), + ReadTimeout: kong.Int(100), + WriteTimeout: kong.Int(100), + }, + }, + map[string]string{}, + }, + { + Service{ + Service: kong.Service{ + Host: kong.String("foo.com"), + Port: kong.Int(80), + Name: kong.String("foo"), + Protocol: kong.String("grpc"), + Path: nil, + }, + }, + configurationv1.KongIngress{ + Proxy: &kong.Service{ + Protocol: kong.String("grpc"), + }, + }, + Service{ + Service: kong.Service{ + Host: kong.String("foo.com"), + Port: kong.Int(80), + Name: kong.String("foo"), + Protocol: kong.String("grpc"), + Path: nil, + }, + }, + map[string]string{}, + }, + { + Service{ + Service: kong.Service{ + Host: kong.String("foo.com"), + Port: kong.Int(80), + Name: kong.String("foo"), + Protocol: kong.String("https"), + Path: nil, + }, + }, + configurationv1.KongIngress{ + Proxy: &kong.Service{ + Protocol: kong.String("grpcs"), + }, + }, + Service{ + Service: kong.Service{ + Host: kong.String("foo.com"), + Port: kong.Int(80), + Name: kong.String("foo"), + Protocol: kong.String("grpcs"), + Path: nil, + }, + }, + map[string]string{}, + }, + { + Service{ + Service: kong.Service{ + Host: kong.String("foo.com"), + Port: kong.Int(80), + Name: kong.String("foo"), + Protocol: kong.String("https"), + Path: kong.String("/"), + }, + }, + configurationv1.KongIngress{ + Proxy: &kong.Service{ + Protocol: kong.String("grpcs"), + }, + }, + Service{ + Service: kong.Service{ + Host: kong.String("foo.com"), + Port: kong.Int(80), + Name: kong.String("foo"), + Protocol: kong.String("grpcs"), + Path: nil, + }, + }, + map[string]string{"configuration.konghq.com/protocol": "grpcs"}, + }, + { + Service{ + Service: kong.Service{ + Host: kong.String("foo.com"), + Port: kong.Int(80), + Name: kong.String("foo"), + Protocol: kong.String("https"), + Path: kong.String("/"), + }, + }, + configurationv1.KongIngress{ + Proxy: &kong.Service{ + Protocol: kong.String("grpcs"), + }, + }, + Service{ + Service: kong.Service{ + Host: kong.String("foo.com"), + Port: kong.Int(80), + Name: kong.String("foo"), + Protocol: kong.String("grpc"), + Path: nil, + }, + }, + map[string]string{"configuration.konghq.com/protocol": "grpc"}, + }, + { + Service{ + Service: kong.Service{ + Host: kong.String("foo.com"), + Port: kong.Int(80), + Name: kong.String("foo"), + Protocol: kong.String("https"), + Path: kong.String("/"), + }, + }, + configurationv1.KongIngress{ + Proxy: &kong.Service{}, + }, + Service{ + Service: kong.Service{ + Host: kong.String("foo.com"), + Port: kong.Int(80), + Name: kong.String("foo"), + Protocol: kong.String("grpcs"), + Path: nil, + }, + }, + map[string]string{"configuration.konghq.com/protocol": "grpcs"}, + }, + { + Service{ + Service: kong.Service{ + Host: kong.String("foo.com"), + Port: kong.Int(80), + Name: kong.String("foo"), + Protocol: kong.String("https"), + Path: kong.String("/"), + }, + }, + configurationv1.KongIngress{ + Proxy: &kong.Service{ + Protocol: kong.String("grpcs"), + }, + }, + Service{ + Service: kong.Service{ + Host: kong.String("foo.com"), + Port: kong.Int(80), + Name: kong.String("foo"), + Protocol: kong.String("https"), + Path: kong.String("/"), + }, + }, + map[string]string{"configuration.konghq.com/protocol": "https"}, + }, + { + Service{ + Service: kong.Service{ + Host: kong.String("foo.com"), + Port: kong.Int(80), + Name: kong.String("foo"), + Protocol: kong.String("https"), + Path: kong.String("/"), + }, + }, + configurationv1.KongIngress{ + Proxy: &kong.Service{}, + }, + Service{ + Service: kong.Service{ + Host: kong.String("foo.com"), + Port: kong.Int(80), + Name: kong.String("foo"), + Protocol: kong.String("https"), + Path: kong.String("/"), + }, + }, + map[string]string{"configuration.konghq.com/protocol": "https"}, + }, + } + + for _, testcase := range testTable { + testcase.inService.override(&testcase.inKongIngresss, testcase.inAnnotation) + assert.Equal(testcase.inService, testcase.outService) + } + + assert.NotPanics(func() { + var nilService *Service + nilService.override(nil, nil) + }) +} + +func Test_overrideServicePath(t *testing.T) { + type args struct { + service Service + anns map[string]string + } + tests := []struct { + name string + args args + want Service + }{ + {}, + {name: "basic empty service"}, + { + name: "set to valid value", + args: args{ + anns: map[string]string{ + "configuration.konghq.com/path": "/foo", + }, + }, + want: Service{ + Service: kong.Service{ + Path: kong.String("/foo"), + }, + }, + }, + { + name: "does not set path if doesn't start with /", + args: args{ + anns: map[string]string{ + "configuration.konghq.com/path": "foo", + }, + }, + want: Service{}, + }, + { + name: "overrides any other value", + args: args{ + service: Service{ + Service: kong.Service{ + Path: kong.String("/foo"), + }, + }, + anns: map[string]string{ + "configuration.konghq.com/path": "/bar", + }, + }, + want: Service{ + Service: kong.Service{ + Path: kong.String("/bar"), + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.args.service.overridePath(tt.args.anns) + if !reflect.DeepEqual(tt.args.service, tt.want) { + t.Errorf("overrideServicePath() got = %v, want %v", tt.args.service, tt.want) + } + }) + } +} diff --git a/internal/ingress/controller/parser/kongstate/types.go b/internal/ingress/controller/parser/kongstate/types.go new file mode 100644 index 0000000000..10ca8dc254 --- /dev/null +++ b/internal/ingress/controller/parser/kongstate/types.go @@ -0,0 +1,26 @@ +package kongstate + +import ( + "github.com/kong/go-kong/kong" + "k8s.io/apimachinery/pkg/util/intstr" +) + +type ServiceBackend struct { + Name string + Port intstr.IntOrString +} + +// Target is a wrapper around Target object in Kong. +type Target struct { + kong.Target +} + +// Certificate represents the certificate object in Kong. +type Certificate struct { + kong.Certificate +} + +// Plugin represetns a plugin Object in Kong. +type Plugin struct { + kong.Plugin +} diff --git a/internal/ingress/controller/parser/kongstate/upstream.go b/internal/ingress/controller/parser/kongstate/upstream.go new file mode 100644 index 0000000000..7bdef7efaf --- /dev/null +++ b/internal/ingress/controller/parser/kongstate/upstream.go @@ -0,0 +1,64 @@ +package kongstate + +import ( + "github.com/kong/go-kong/kong" + "github.com/kong/kubernetes-ingress-controller/internal/ingress/annotations" + configurationv1 "github.com/kong/kubernetes-ingress-controller/pkg/apis/configuration/v1" +) + +// Upstream is a wrapper around Upstream object in Kong. +type Upstream struct { + kong.Upstream + Targets []Target + // Service this upstream is asosciated with. + Service Service +} + +func (u *Upstream) overrideHostHeader(anns map[string]string) { + if u == nil { + return + } + host := annotations.ExtractHostHeader(anns) + if host == "" { + return + } + u.HostHeader = kong.String(host) +} + +// overrideByAnnotation modifies the Kong upstream based on annotations +// on the Kubernetes service. +func (u *Upstream) overrideByAnnotation(anns map[string]string) { + if u == nil { + return + } + u.overrideHostHeader(anns) +} + +// overrideByKongIngress modifies the Kong upstream based on KongIngresses +// associated with the Kubernetes service. +func (u *Upstream) overrideByKongIngress(kongIngress *configurationv1.KongIngress) { + if u == nil { + return + } + + if kongIngress == nil || kongIngress.Upstream == nil { + return + } + + // The upstream within the KongIngress has no name. + // As this overwrites the entire upstream object, we must restore the + // original name after. + name := *u.Upstream.Name + u.Upstream = *kongIngress.Upstream.DeepCopy() + u.Name = &name +} + +// override sets Upstream fields by KongIngress first, then by annotation +func (u *Upstream) override(kongIngress *configurationv1.KongIngress, anns map[string]string) { + if u == nil { + return + } + + u.overrideByKongIngress(kongIngress) + u.overrideByAnnotation(anns) +} diff --git a/internal/ingress/controller/parser/kongstate/upstream_test.go b/internal/ingress/controller/parser/kongstate/upstream_test.go new file mode 100644 index 0000000000..be28294c2c --- /dev/null +++ b/internal/ingress/controller/parser/kongstate/upstream_test.go @@ -0,0 +1,76 @@ +package kongstate + +import ( + "testing" + + "github.com/kong/go-kong/kong" + configurationv1 "github.com/kong/kubernetes-ingress-controller/pkg/apis/configuration/v1" + "github.com/stretchr/testify/assert" +) + +func TestOverrideUpstream(t *testing.T) { + assert := assert.New(t) + + testTable := []struct { + inUpstream Upstream + inKongIngresss configurationv1.KongIngress + outUpstream Upstream + }{ + { + Upstream{ + Upstream: kong.Upstream{ + Name: kong.String("foo.com"), + }, + }, + configurationv1.KongIngress{ + Upstream: &kong.Upstream{}, + }, + Upstream{ + Upstream: kong.Upstream{ + Name: kong.String("foo.com"), + }, + }, + }, + { + Upstream{ + Upstream: kong.Upstream{ + Name: kong.String("foo.com"), + }, + }, + configurationv1.KongIngress{ + Upstream: &kong.Upstream{ + Name: kong.String("wrong.com"), + HashOn: kong.String("HashOn"), + HashOnCookie: kong.String("HashOnCookie"), + HashOnCookiePath: kong.String("HashOnCookiePath"), + HashOnHeader: kong.String("HashOnHeader"), + HashFallback: kong.String("HashFallback"), + HashFallbackHeader: kong.String("HashFallbackHeader"), + Slots: kong.Int(42), + }, + }, + Upstream{ + Upstream: kong.Upstream{ + Name: kong.String("foo.com"), + HashOn: kong.String("HashOn"), + HashOnCookie: kong.String("HashOnCookie"), + HashOnCookiePath: kong.String("HashOnCookiePath"), + HashOnHeader: kong.String("HashOnHeader"), + HashFallback: kong.String("HashFallback"), + HashFallbackHeader: kong.String("HashFallbackHeader"), + Slots: kong.Int(42), + }, + }, + }, + } + + for _, testcase := range testTable { + testcase.inUpstream.override(&testcase.inKongIngresss, make(map[string]string)) + assert.Equal(testcase.inUpstream, testcase.outUpstream) + } + + assert.NotPanics(func() { + var nilUpstream *Upstream + nilUpstream.override(nil, make(map[string]string)) + }) +} diff --git a/internal/ingress/controller/parser/kongstate/util.go b/internal/ingress/controller/parser/kongstate/util.go new file mode 100644 index 0000000000..62b15f568d --- /dev/null +++ b/internal/ingress/controller/parser/kongstate/util.go @@ -0,0 +1,232 @@ +package kongstate + +import ( + "encoding/json" + "errors" + "fmt" + + "github.com/ghodss/yaml" + "github.com/kong/go-kong/kong" + "github.com/kong/kubernetes-ingress-controller/internal/ingress/annotations" + "github.com/kong/kubernetes-ingress-controller/internal/ingress/store" + configurationv1 "github.com/kong/kubernetes-ingress-controller/pkg/apis/configuration/v1" + configurationv1beta1 "github.com/kong/kubernetes-ingress-controller/pkg/apis/configuration/v1beta1" + corev1 "k8s.io/api/core/v1" + networking "k8s.io/api/networking/v1beta1" +) + +func getKongIngressForService(s store.Storer, service corev1.Service) ( + *configurationv1.KongIngress, error) { + confName := annotations.ExtractConfigurationName(service.Annotations) + if confName == "" { + return nil, nil + } + return s.GetKongIngress(service.Namespace, confName) +} + +// getKongIngressFromIngress checks if the Ingress +// contains an annotation for configuration +// or if exists a KongIngress object with the same name than the Ingress +func getKongIngressFromIngress(s store.Storer, ing *networking.Ingress) ( + *configurationv1.KongIngress, error) { + return getKongIngressFromIngressAnnotations(s, ing.Namespace, + ing.Name, ing.Annotations) +} + +// getKongIngressFromTCPIngress checks if the TCPIngress contains an +// annotation for configuration +// or if exists a KongIngress object with the same name than the Ingress +func getKongIngressFromTCPIngress(s store.Storer, ing *configurationv1beta1.TCPIngress) ( + *configurationv1.KongIngress, error) { + return getKongIngressFromIngressAnnotations(s, ing.Namespace, + ing.Name, ing.Annotations) +} + +func getKongIngressFromIngressAnnotations(s store.Storer, namespace, name string, + anns map[string]string) ( + *configurationv1.KongIngress, error) { + confName := annotations.ExtractConfigurationName(anns) + if confName != "" { + ki, err := s.GetKongIngress(namespace, confName) + if err == nil { + return ki, nil + } + } + + ki, err := s.GetKongIngress(namespace, name) + if err == nil { + return ki, nil + } + return nil, nil +} + +// getPlugin constructs a plugins from a KongPlugin resource. +func getPlugin(s store.Storer, namespace, name string) (kong.Plugin, error) { + var plugin kong.Plugin + k8sPlugin, err := s.GetKongPlugin(namespace, name) + if err != nil { + // if no namespaced plugin definition, then + // search for cluster level-plugin definition + if errors.As(err, &store.ErrNotFound{}) { + clusterPlugin, err := s.GetKongClusterPlugin(name) + // not found + if errors.As(err, &store.ErrNotFound{}) { + return plugin, errors.New( + "no KongPlugin or KongClusterPlugin was found") + } + if err != nil { + return plugin, err + } + if clusterPlugin.PluginName == "" { + return plugin, fmt.Errorf("invalid empty 'plugin' property") + } + plugin, err = kongPluginFromK8SClusterPlugin(s, *clusterPlugin) + return plugin, err + } + } + // ignore plugins with no name + if k8sPlugin.PluginName == "" { + return plugin, fmt.Errorf("invalid empty 'plugin' property") + } + + plugin, err = kongPluginFromK8SPlugin(s, *k8sPlugin) + return plugin, err +} + +func kongPluginFromK8SClusterPlugin( + s store.Storer, + k8sPlugin configurationv1.KongClusterPlugin) (kong.Plugin, error) { + config := k8sPlugin.Config + if k8sPlugin.ConfigFrom.SecretValue != + (configurationv1.NamespacedSecretValueFromSource{}) && + len(k8sPlugin.Config) > 0 { + return kong.Plugin{}, + fmt.Errorf("KongClusterPlugin '/%v' has both "+ + "Config and ConfigFrom set", k8sPlugin.Name) + } + if k8sPlugin.ConfigFrom.SecretValue != (configurationv1. + NamespacedSecretValueFromSource{}) { + var err error + config, err = namespacedSecretToConfiguration( + s, + k8sPlugin.ConfigFrom.SecretValue) + if err != nil { + return kong.Plugin{}, + fmt.Errorf("error parsing config for KongClusterPlugin %v: %w", + k8sPlugin.Name, err) + } + } + kongPlugin := plugin{ + Name: k8sPlugin.PluginName, + Config: config, + + RunOn: k8sPlugin.RunOn, + Disabled: k8sPlugin.Disabled, + Protocols: k8sPlugin.Protocols, + }.toKongPlugin() + return kongPlugin, nil +} + +func cloneStringPointerSlice(array ...*string) (res []*string) { + res = append(res, array...) + return +} + +func kongPluginFromK8SPlugin( + s store.Storer, + k8sPlugin configurationv1.KongPlugin) (kong.Plugin, error) { + config := k8sPlugin.Config + if k8sPlugin.ConfigFrom.SecretValue != + (configurationv1.SecretValueFromSource{}) && + len(k8sPlugin.Config) > 0 { + return kong.Plugin{}, + fmt.Errorf("KongPlugin '%v/%v' has both "+ + "Config and ConfigFrom set", + k8sPlugin.Namespace, k8sPlugin.Name) + } + if k8sPlugin.ConfigFrom.SecretValue != + (configurationv1.SecretValueFromSource{}) { + var err error + config, err = SecretToConfiguration(s, + k8sPlugin.ConfigFrom.SecretValue, k8sPlugin.Namespace) + if err != nil { + return kong.Plugin{}, + fmt.Errorf("error parsing config for KongPlugin '%v/%v': %w", + k8sPlugin.Name, k8sPlugin.Namespace, err) + } + } + kongPlugin := plugin{ + Name: k8sPlugin.PluginName, + Config: config, + + RunOn: k8sPlugin.RunOn, + Disabled: k8sPlugin.Disabled, + Protocols: k8sPlugin.Protocols, + }.toKongPlugin() + return kongPlugin, nil +} + +func namespacedSecretToConfiguration( + s store.Storer, + reference configurationv1.NamespacedSecretValueFromSource) ( + configurationv1.Configuration, error) { + bareReference := configurationv1.SecretValueFromSource{ + Secret: reference.Secret, + Key: reference.Key} + return SecretToConfiguration(s, bareReference, reference.Namespace) +} + +func SecretToConfiguration( + s store.Storer, + reference configurationv1.SecretValueFromSource, namespace string) ( + configurationv1.Configuration, error) { + secret, err := s.GetSecret(namespace, reference.Secret) + if err != nil { + return configurationv1.Configuration{}, fmt.Errorf( + "error fetching plugin configuration secret '%v/%v': %v", + namespace, reference.Secret, err) + } + secretVal, ok := secret.Data[reference.Key] + if !ok { + return configurationv1.Configuration{}, + fmt.Errorf("no key '%v' in secret '%v/%v'", + reference.Key, namespace, reference.Secret) + } + var config configurationv1.Configuration + if err := json.Unmarshal(secretVal, &config); err != nil { + if err := yaml.Unmarshal(secretVal, &config); err != nil { + return configurationv1.Configuration{}, + fmt.Errorf("key '%v' in secret '%v/%v' contains neither "+ + "valid JSON nor valid YAML)", + reference.Key, namespace, reference.Secret) + } + } + return config, nil +} + +// plugin is a intermediate type to hold plugin related configuration +type plugin struct { + Name string + Config configurationv1.Configuration + + RunOn string + Disabled bool + Protocols []string +} + +func (p plugin) toKongPlugin() kong.Plugin { + result := kong.Plugin{ + Name: kong.String(p.Name), + Config: kong.Configuration(p.Config).DeepCopy(), + } + if p.RunOn != "" { + result.RunOn = kong.String(p.RunOn) + } + if p.Disabled { + result.Enabled = kong.Bool(false) + } + if len(p.Protocols) > 0 { + result.Protocols = kong.StringSlice(p.Protocols...) + } + return result +} diff --git a/internal/ingress/controller/parser/parser.go b/internal/ingress/controller/parser/parser.go index bc9aa3d115..f9c3d8b967 100644 --- a/internal/ingress/controller/parser/parser.go +++ b/internal/ingress/controller/parser/parser.go @@ -4,275 +4,116 @@ import ( "bytes" "crypto/tls" "crypto/x509" - "encoding/json" "encoding/pem" - "errors" "fmt" "reflect" - "regexp" "sort" "strconv" "strings" - "github.com/ghodss/yaml" "github.com/kong/go-kong/kong" "github.com/kong/kubernetes-ingress-controller/internal/ingress/annotations" + "github.com/kong/kubernetes-ingress-controller/internal/ingress/controller/parser/kongstate" "github.com/kong/kubernetes-ingress-controller/internal/ingress/store" "github.com/kong/kubernetes-ingress-controller/internal/ingress/utils" - configurationv1 "github.com/kong/kubernetes-ingress-controller/pkg/apis/configuration/v1" configurationv1beta1 "github.com/kong/kubernetes-ingress-controller/pkg/apis/configuration/v1beta1" - "github.com/mitchellh/mapstructure" "github.com/sirupsen/logrus" corev1 "k8s.io/api/core/v1" networking "k8s.io/api/networking/v1beta1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/intstr" - "k8s.io/apimachinery/pkg/util/sets" knative "knative.dev/networking/pkg/apis/networking/v1alpha1" ) -// Route represents a Kong Route and holds a reference to the Ingress -// rule. -type Route struct { - kong.Route - - // Ingress object associated with this route - Ingress networking.Ingress - // TCPIngress object associated with this route - TCPIngress configurationv1beta1.TCPIngress - // Is this route coming from TCPIngress or networking.Ingress? - IsTCP bool - Plugins []kong.Plugin -} - -type backend struct { - Name string - Port intstr.IntOrString -} - -// Service represents a service in Kong and holds routes associated with the -// service and other k8s metadata. -type Service struct { - kong.Service - Backend backend - Namespace string - Routes []Route - Plugins []kong.Plugin - K8sService corev1.Service -} - -// Upstream is a wrapper around Upstream object in Kong. -type Upstream struct { - kong.Upstream - Targets []Target - // Service this upstream is asosciated with. - Service Service -} - -// Target is a wrapper around Target object in Kong. -type Target struct { - kong.Target -} - -// Consumer holds a Kong consumer and its plugins and credentials. -type Consumer struct { - kong.Consumer - Plugins []kong.Plugin - KeyAuths []*kong.KeyAuth - HMACAuths []*kong.HMACAuth - JWTAuths []*kong.JWTAuth - BasicAuths []*kong.BasicAuth - ACLGroups []*kong.ACLGroup - - Oauth2Creds []*kong.Oauth2Credential - - k8sKongConsumer configurationv1.KongConsumer -} - -// KongState holds the configuration that should be applied to Kong. -type KongState struct { - Services []Service - Upstreams []Upstream - Certificates []Certificate - CACertificates []kong.CACertificate - Plugins []Plugin - Consumers []Consumer -} - -// Certificate represents the certificate object in Kong. -type Certificate struct { - kong.Certificate -} - -// Plugin represetns a plugin Object in Kong. -type Plugin struct { - kong.Plugin -} - -// Parser parses Kubernetes CRDs and Ingress rules and generates a -// Kong configuration. -type Parser struct { - store store.Storer - Logger logrus.FieldLogger -} - -type parsedIngressRules struct { - SecretNameToSNIs map[string][]string - ServiceNameToServices map[string]Service -} - -var supportedCreds = sets.NewString( - "acl", - "basic-auth", - "hmac-auth", - "jwt", - "key-auth", - "oauth2", -) - -var validProtocols = regexp.MustCompile(`\Ahttps$|\Ahttp$|\Agrpc$|\Agrpcs|\Atcp|\Atls$`) -var validMethods = regexp.MustCompile(`\A[A-Z]+$`) - -// New returns a new parser backed with store. -func New(store store.Storer, logger logrus.FieldLogger) Parser { - return Parser{store: store, Logger: logger} -} - -// Build creates a Kong configuration from Ingress and Custom resources -// defined in Kuberentes. -// It throws an error if there is an error returned from client-go. -func (p *Parser) Build() (*KongState, error) { - var state KongState - ings := p.store.ListIngresses() - tcpIngresses, err := p.store.ListTCPIngresses() +func parseAll(log logrus.FieldLogger, s store.Storer) ingressRules { + ings := s.ListIngresses() + tcpIngresses, err := s.ListTCPIngresses() if err != nil { - p.Logger.Errorf("failed to list TCPIngresses: %v", err) + log.Errorf("failed to list TCPIngresses: %v", err) } - // parse ingress rules - parsedInfo := p.parseIngressRules(ings, tcpIngresses) + parsedIngress := parseIngressRules(log, ings, tcpIngresses) - knativeIngresses, err := p.store.ListKnativeIngresses() + knativeIngresses, err := s.ListKnativeIngresses() if err != nil { - p.Logger.Errorf("failed to list Knative Ingresses: %v", err) + log.Errorf("failed to list Knative Ingresses: %v", err) } - servicesFromKnative, secretToSNIsFromKnative := p.parseKnativeIngressRules(knativeIngresses) + parsedKnative := parseKnativeIngressRules(knativeIngresses) - for name, service := range servicesFromKnative { - parsedInfo.ServiceNameToServices[name] = service - } - - for secret, snis := range secretToSNIsFromKnative { - var combinedSNIs []string - if snisFromIngress, ok := parsedInfo.SecretNameToSNIs[secret]; ok { - combinedSNIs = append(combinedSNIs, snisFromIngress...) - } - combinedSNIs = append(combinedSNIs, snis...) - parsedInfo.SecretNameToSNIs[secret] = combinedSNIs - } + return mergeIngressRules(parsedIngress, parsedKnative) +} - // populate Kubernetes Service - for key, service := range parsedInfo.ServiceNameToServices { - k8sSvc, err := p.store.GetService(service.Namespace, service.Backend.Name) - if err != nil { - p.Logger.WithFields(logrus.Fields{ - "service_name": service.Backend.Name, - "service_namespace": service.Namespace, - }).Errorf("failed to fetch service: %v", err) - } - if k8sSvc != nil { - service.K8sService = *k8sSvc - } - secretName := annotations.ExtractClientCertificate( - service.K8sService.GetAnnotations()) - if secretName != "" { - secret, err := p.store.GetSecret(service.K8sService.Namespace, - secretName) - secretKey := service.K8sService.Namespace + "/" + secretName - // ensure that the cert is loaded into Kong - if _, ok := parsedInfo.SecretNameToSNIs[secretKey]; !ok { - parsedInfo.SecretNameToSNIs[secretKey] = []string{} - } - if err == nil { - service.ClientCertificate = &kong.Certificate{ - ID: kong.String(string(secret.UID)), - } - } else { - p.Logger.WithFields(logrus.Fields{ - "secret_name": secretName, - "secret_namespace": service.K8sService.Namespace, - }).Errorf("failed to fetch secret: %v", err) - } - } - parsedInfo.ServiceNameToServices[key] = service - } +// Build creates a Kong configuration from Ingress and Custom resources +// defined in Kuberentes. +// It throws an error if there is an error returned from client-go. +func Build(log logrus.FieldLogger, s store.Storer) (*kongstate.KongState, error) { + parsedAll := parseAll(log, s) + parsedAll.populateServices(log, s) + var result kongstate.KongState // add the routes and services to the state - for _, service := range parsedInfo.ServiceNameToServices { - state.Services = append(state.Services, service) + for _, service := range parsedAll.ServiceNameToServices { + result.Services = append(result.Services, service) } // generate Upstreams and Targets from service defs - state.Upstreams = p.getUpstreams(parsedInfo.ServiceNameToServices) + result.Upstreams = getUpstreams(log, s, parsedAll.ServiceNameToServices) // merge KongIngress with Routes, Services and Upstream - p.fillOverrides(state) + result.FillOverrides(log, s) // generate consumers and credentials - p.fillConsumersAndCredentials(&state) + result.FillConsumersAndCredentials(log, s) // process annotation plugins - state.Plugins = p.fillPlugins(state) + result.FillPlugins(log, s) // generate Certificates and SNIs - state.Certificates = p.getCerts(parsedInfo.SecretNameToSNIs) + result.Certificates = getCerts(log, s, parsedAll.SecretNameToSNIs) // populate CA certificates in Kong - state.CACertificates, err = p.getCACerts() + var err error + caCertSecrets, err := s.ListCACerts() if err != nil { return nil, err } + result.CACertificates = toCACerts(log, caCertSecrets) - return &state, nil + return &result, nil } -func (p *Parser) getCACerts() ([]kong.CACertificate, error) { - caCertSecrets, err := p.store.ListCACerts() - if err != nil { - return nil, err - } - +func toCACerts(log logrus.FieldLogger, caCertSecrets []*corev1.Secret) []kong.CACertificate { var caCerts []kong.CACertificate for _, certSecret := range caCertSecrets { secretName := certSecret.Namespace + "/" + certSecret.Name idbytes, idExists := certSecret.Data["id"] - logger := p.Logger.WithFields(logrus.Fields{ + log = log.WithFields(logrus.Fields{ "secret_name": secretName, "secret_namespace": certSecret.Namespace, }) if !idExists { - logger.Errorf("invalid CA certificate: missing 'id' field in data") + log.Errorf("invalid CA certificate: missing 'id' field in data") continue } caCertbytes, certExists := certSecret.Data["cert"] if !certExists { - logger.Errorf("invalid CA certificate: missing 'cert' field in data") + log.Errorf("invalid CA certificate: missing 'cert' field in data") continue } pemBlock, _ := pem.Decode(caCertbytes) if pemBlock == nil { - logger.Errorf("invalid CA certificate: invalid PEM block") + log.Errorf("invalid CA certificate: invalid PEM block") continue } x509Cert, err := x509.ParseCertificate(pemBlock.Bytes) if err != nil { - logger.Errorf("invalid CA certificate: failed to parse certificate: %v", err) + log.Errorf("invalid CA certificate: failed to parse certificate: %v", err) continue } if !x509Cert.IsCA { - logger.Errorf("invalid CA certificate: certificate is missing the 'CA' basic constraint: %v", err) + log.Errorf("invalid CA certificate: certificate is missing the 'CA' basic constraint: %v", err) continue } @@ -282,229 +123,7 @@ func (p *Parser) getCACerts() ([]kong.CACertificate, error) { }) } - return caCerts, nil -} - -func (p *Parser) processCredential(credType string, consumer *Consumer, - credConfig interface{}) error { - switch credType { - case "key-auth", "keyauth_credential": - var cred kong.KeyAuth - err := decodeCredential(credConfig, &cred) - if err != nil { - return fmt.Errorf("failed to decode key-auth credential: %w", err) - - } - consumer.KeyAuths = append(consumer.KeyAuths, &cred) - case "basic-auth", "basicauth_credential": - var cred kong.BasicAuth - err := decodeCredential(credConfig, &cred) - if err != nil { - return fmt.Errorf("failed to decode basic-auth credential: %w", err) - } - consumer.BasicAuths = append(consumer.BasicAuths, &cred) - case "hmac-auth", "hmacauth_credential": - var cred kong.HMACAuth - err := decodeCredential(credConfig, &cred) - if err != nil { - return fmt.Errorf("failed to decode hmac-auth credential: %w", err) - } - consumer.HMACAuths = append(consumer.HMACAuths, &cred) - case "oauth2": - var cred kong.Oauth2Credential - err := decodeCredential(credConfig, &cred) - if err != nil { - return fmt.Errorf("failed to decode oauth2 credential: %w", err) - } - consumer.Oauth2Creds = append(consumer.Oauth2Creds, &cred) - case "jwt", "jwt_secret": - var cred kong.JWTAuth - err := decodeCredential(credConfig, &cred) - if err != nil { - p.Logger.Errorf("failed to process JWT credential: %v", err) - } - // This is treated specially because only this - // field might be omitted by user under the expectation - // that Kong will insert the default. - // If we don't set it, decK will detect a diff and PUT this - // credential everytime it performs a sync operation, which - // leads to unnecessary cache invalidations in Kong. - if cred.Algorithm == nil || *cred.Algorithm == "" { - cred.Algorithm = kong.String("HS256") - } - consumer.JWTAuths = append(consumer.JWTAuths, &cred) - case "acl": - var cred kong.ACLGroup - err := decodeCredential(credConfig, &cred) - if err != nil { - p.Logger.Errorf("failed to process ACL group: %v", err) - } - consumer.ACLGroups = append(consumer.ACLGroups, &cred) - default: - return fmt.Errorf("invalid credential type: '%v'", credType) - } - return nil -} - -func decodeCredential(credConfig interface{}, - credStructPointer interface{}) error { - decoder, err := mapstructure.NewDecoder( - &mapstructure.DecoderConfig{TagName: "json", - Result: credStructPointer, - }) - if err != nil { - return fmt.Errorf("failed to create a decoder: %w", err) - } - err = decoder.Decode(credConfig) - if err != nil { - return fmt.Errorf("failed to decode credential: %w", err) - } - return nil -} - -func (p *Parser) fillConsumersAndCredentials(state *KongState) { - consumerIndex := make(map[string]Consumer) - - // build consumer index - for _, consumer := range p.store.ListKongConsumers() { - var c Consumer - if consumer.Username == "" && consumer.CustomID == "" { - continue - } - if consumer.Username != "" { - c.Username = kong.String(consumer.Username) - } - if consumer.CustomID != "" { - c.CustomID = kong.String(consumer.CustomID) - } - c.k8sKongConsumer = *consumer - - logger := p.Logger.WithFields(logrus.Fields{ - "kongconsumer_name": consumer.Name, - "kongconsumer_namespace": consumer.Namespace, - }) - for _, cred := range consumer.Credentials { - logger = logger.WithFields(logrus.Fields{ - "secret_name": cred, - "secret_namespace": consumer.Namespace, - }) - secret, err := p.store.GetSecret(consumer.Namespace, cred) - if err != nil { - logger.Errorf("failed to fetch secret: %v", err) - continue - } - credConfig := map[string]interface{}{} - for k, v := range secret.Data { - // TODO populate these based on schema from Kong - // and remove this workaround - if k == "redirect_uris" { - credConfig[k] = strings.Split(string(v), ",") - continue - } - credConfig[k] = string(v) - } - credType, ok := credConfig["kongCredType"].(string) - if !ok { - logger.Errorf("failed to provision credential: invalid credType: %v", credType) - } - if !supportedCreds.Has(credType) { - logger.Errorf("failed to provision credential: invalid credType: %v", credType) - continue - } - if len(credConfig) <= 1 { // 1 key of credType itself - logger.Errorf("failed to provision credential: empty secret") - continue - } - err = p.processCredential(credType, &c, credConfig) - if err != nil { - logger.Errorf("failed to provision credential: %v", err) - continue - } - } - - consumerIndex[consumer.Namespace+"/"+consumer.Name] = c - } - - // legacy attach credentials - credentials := p.store.ListKongCredentials() - if len(credentials) > 0 { - p.Logger.Warnf("deprecated KongCredential resource in use; " + - "please use secret-based credentials, " + - "KongCredential resource will be removed in future") - } - for _, credential := range credentials { - logger := p.Logger.WithFields(logrus.Fields{ - "kongcredential_name": credential.Name, - "kongcredential_namespace": credential.Namespace, - "consumerRef": credential.ConsumerRef, - }) - consumer, ok := consumerIndex[credential.Namespace+"/"+ - credential.ConsumerRef] - if !ok { - continue - } - if credential.Type == "" { - logger.Errorf("invalid KongCredential: no Type provided") - continue - } - if !supportedCreds.Has(credential.Type) { - logger.Errorf("invalid KongCredential: invalid Type provided") - continue - } - if credential.Config == nil { - logger.Errorf("invalid KongCredential: empty config") - continue - } - err := p.processCredential(credential.Type, &consumer, credential.Config) - if err != nil { - logger.Errorf("failed to provision credential: %v", err) - continue - } - consumerIndex[credential.Namespace+"/"+credential.ConsumerRef] = consumer - } - - // populate the consumer in the state - for _, c := range consumerIndex { - state.Consumers = append(state.Consumers, c) - } -} - -func filterHosts(secretNameToSNIs map[string][]string, hosts []string) []string { - hostsToAdd := []string{} - seenHosts := map[string]bool{} - for _, hosts := range secretNameToSNIs { - for _, host := range hosts { - seenHosts[host] = true - } - } - for _, host := range hosts { - if !seenHosts[host] { - hostsToAdd = append(hostsToAdd, host) - } - } - return hostsToAdd -} - -func processTLSSections(tlsSections []networking.IngressTLS, - namespace string, secretNameToSNIs map[string][]string) { - // TODO: optmize: collect all TLS sections and process at the same - // time to avoid regenerating the seen map; or use a seen map in the - // parser struct itself. - for _, tls := range tlsSections { - if len(tls.Hosts) == 0 { - continue - } - if tls.SecretName == "" { - continue - } - hosts := tls.Hosts - secretName := namespace + "/" + tls.SecretName - hosts = filterHosts(secretNameToSNIs, hosts) - if secretNameToSNIs[secretName] != nil { - hosts = append(hosts, secretNameToSNIs[secretName]...) - } - secretNameToSNIs[secretName] = hosts - } + return caCerts } func knativeIngressToNetworkingTLS(tls []knative.IngressTLS) []networking.IngressTLS { @@ -531,23 +150,22 @@ func tcpIngressToNetworkingTLS(tls []configurationv1beta1.IngressTLS) []networki return result } -func (p *Parser) parseKnativeIngressRules( - ingressList []*knative.Ingress) (map[string]Service, map[string][]string) { +func parseKnativeIngressRules( + ingressList []*knative.Ingress) ingressRules { sort.SliceStable(ingressList, func(i, j int) bool { return ingressList[i].CreationTimestamp.Before( &ingressList[j].CreationTimestamp) }) - services := map[string]Service{} - secretToSNIs := map[string][]string{} + services := map[string]kongstate.Service{} + secretToSNIs := newSecretNameToSNIs() for i := 0; i < len(ingressList); i++ { ingress := *ingressList[i] ingressSpec := ingress.Spec - processTLSSections(knativeIngressToNetworkingTLS(ingress.Spec.TLS), - ingress.Namespace, secretToSNIs) + secretToSNIs.addFromIngressTLS(knativeIngressToNetworkingTLS(ingress.Spec.TLS), ingress.Namespace) for i, rule := range ingressSpec.Rules { hosts := rule.Hosts if rule.HTTP == nil { @@ -559,7 +177,7 @@ func (p *Parser) parseKnativeIngressRules( if path == "" { path = "/" } - r := Route{ + r := kongstate.Route{ Route: kong.Route{ // TODO Figure out a way to name the routes // This is not a stable scheme @@ -597,7 +215,7 @@ func (p *Parser) parseKnativeIngressRules( headers = append(headers, key+":"+value) } - service = Service{ + service = kongstate.Service{ Service: kong.Service{ Name: kong.String(serviceName), Host: kong.String(serviceHost), @@ -610,7 +228,7 @@ func (p *Parser) parseKnativeIngressRules( Retries: kong.Int(5), }, Namespace: ingress.Namespace, - Backend: backend{ + Backend: kongstate.ServiceBackend{ Name: knativeBackend.ServiceName, Port: knativeBackend.ServicePort, }, @@ -632,7 +250,10 @@ func (p *Parser) parseKnativeIngressRules( } } - return services, secretToSNIs + return ingressRules{ + ServiceNameToServices: services, + SecretNameToSNIs: secretToSNIs, + } } func knativeSelectSplit(splits []knative.IngressBackendSplit) knative.IngressBackendSplit { @@ -653,9 +274,10 @@ func knativeSelectSplit(splits []knative.IngressBackendSplit) knative.IngressBac return res } -func (p *Parser) parseIngressRules( +func parseIngressRules( + log logrus.FieldLogger, ingressList []*networking.Ingress, - tcpIngressList []*configurationv1beta1.TCPIngress) *parsedIngressRules { + tcpIngressList []*configurationv1beta1.TCPIngress) ingressRules { sort.SliceStable(ingressList, func(i, j int) bool { return ingressList[i].CreationTimestamp.Before( @@ -670,13 +292,13 @@ func (p *Parser) parseIngressRules( // generate the following: // Services and Routes var allDefaultBackends []networking.Ingress - secretNameToSNIs := make(map[string][]string) - serviceNameToServices := make(map[string]Service) + secretNameToSNIs := newSecretNameToSNIs() + serviceNameToServices := make(map[string]kongstate.Service) for i := 0; i < len(ingressList); i++ { ingress := *ingressList[i] ingressSpec := ingress.Spec - logger := p.Logger.WithFields(logrus.Fields{ + log = log.WithFields(logrus.Fields{ "ingress_namespace": ingress.Namespace, "ingress_name": ingress.Name, }) @@ -686,7 +308,7 @@ func (p *Parser) parseIngressRules( } - processTLSSections(ingressSpec.TLS, ingress.Namespace, secretNameToSNIs) + secretNameToSNIs.addFromIngressTLS(ingressSpec.TLS, ingress.Namespace) for i, rule := range ingressSpec.Rules { host := rule.Host @@ -697,13 +319,13 @@ func (p *Parser) parseIngressRules( path := rule.Path if strings.Contains(path, "//") { - logger.Errorf("rule skipped: invalid path: '%v'", path) + log.Errorf("rule skipped: invalid path: '%v'", path) continue } if path == "" { path = "/" } - r := Route{ + r := kongstate.Route{ Ingress: ingress, Route: kong.Route{ // TODO Figure out a way to name the routes @@ -731,7 +353,7 @@ func (p *Parser) parseIngressRules( rule.Backend.ServicePort.String() service, ok := serviceNameToServices[serviceName] if !ok { - service = Service{ + service = kongstate.Service{ Service: kong.Service{ Name: kong.String(serviceName), Host: kong.String(rule.Backend.ServiceName + @@ -746,7 +368,7 @@ func (p *Parser) parseIngressRules( Retries: kong.Int(5), }, Namespace: ingress.Namespace, - Backend: backend{ + Backend: kongstate.ServiceBackend{ Name: rule.Backend.ServiceName, Port: rule.Backend.ServicePort, }, @@ -762,21 +384,20 @@ func (p *Parser) parseIngressRules( ingress := *tcpIngressList[i] ingressSpec := ingress.Spec - logger := p.Logger.WithFields(logrus.Fields{ + log = log.WithFields(logrus.Fields{ "tcpingress_namespace": ingress.Namespace, "tcpingress_name": ingress.Name, }) - processTLSSections(tcpIngressToNetworkingTLS(ingressSpec.TLS), - ingress.Namespace, secretNameToSNIs) + secretNameToSNIs.addFromIngressTLS(tcpIngressToNetworkingTLS(ingressSpec.TLS), ingress.Namespace) for i, rule := range ingressSpec.Rules { if rule.Port <= 0 { - logger.Errorf("invalid TCPIngress: invalid port: %v", rule.Port) + log.Errorf("invalid TCPIngress: invalid port: %v", rule.Port) continue } - r := Route{ + r := kongstate.Route{ IsTCP: true, TCPIngress: ingress, Route: kong.Route{ @@ -802,11 +423,11 @@ func (p *Parser) parseIngressRules( r.SNIs = kong.StringSlice(host) } if rule.Backend.ServiceName == "" { - logger.Errorf("invalid TCPIngress: empty serviceName") + log.Errorf("invalid TCPIngress: empty serviceName") continue } if rule.Backend.ServicePort <= 0 { - logger.Errorf("invalid TCPIngress: invalid servicePort: %v", rule.Backend.ServicePort) + log.Errorf("invalid TCPIngress: invalid servicePort: %v", rule.Backend.ServicePort) continue } @@ -815,7 +436,7 @@ func (p *Parser) parseIngressRules( strconv.Itoa(rule.Backend.ServicePort) service, ok := serviceNameToServices[serviceName] if !ok { - service = Service{ + service = kongstate.Service{ Service: kong.Service{ Name: kong.String(serviceName), Host: kong.String(rule.Backend.ServiceName + @@ -829,7 +450,7 @@ func (p *Parser) parseIngressRules( Retries: kong.Int(5), }, Namespace: ingress.Namespace, - Backend: backend{ + Backend: kongstate.ServiceBackend{ Name: rule.Backend.ServiceName, Port: intstr.FromInt(rule.Backend.ServicePort), }, @@ -853,7 +474,7 @@ func (p *Parser) parseIngressRules( defaultBackend.ServicePort.String() service, ok := serviceNameToServices[serviceName] if !ok { - service = Service{ + service = kongstate.Service{ Service: kong.Service{ Name: kong.String(serviceName), Host: kong.String(defaultBackend.ServiceName + "." + @@ -867,13 +488,13 @@ func (p *Parser) parseIngressRules( Retries: kong.Int(5), }, Namespace: ingress.Namespace, - Backend: backend{ + Backend: kongstate.ServiceBackend{ Name: defaultBackend.ServiceName, Port: defaultBackend.ServicePort, }, } } - r := Route{ + r := kongstate.Route{ Ingress: ingress, Route: kong.Route{ Name: kong.String(ingress.Namespace + "." + ingress.Name), @@ -888,469 +509,24 @@ func (p *Parser) parseIngressRules( serviceNameToServices[serviceName] = service } - return &parsedIngressRules{ + return ingressRules{ SecretNameToSNIs: secretNameToSNIs, ServiceNameToServices: serviceNameToServices, } } -func (p *Parser) fillOverrides(state KongState) { - for i := 0; i < len(state.Services); i++ { - // Services - anns := state.Services[i].K8sService.Annotations - kongIngress, err := p.getKongIngressForService( - state.Services[i].K8sService) - if err != nil { - p.Logger.WithFields(logrus.Fields{ - "service_name": state.Services[i].K8sService.Name, - "service_namespace": state.Services[i].K8sService.Namespace, - }).Errorf("failed to fetch KongIngress resource for Service: %v", err) - } - overrideService(&state.Services[i], kongIngress, anns) - - // Routes - for j := 0; j < len(state.Services[i].Routes); j++ { - var kongIngress *configurationv1.KongIngress - var err error - if state.Services[i].Routes[j].IsTCP { - kongIngress, err = p.getKongIngressFromTCPIngress( - &state.Services[i].Routes[j].TCPIngress) - if err != nil { - p.Logger.WithFields(logrus.Fields{ - "tcpingress_name": state.Services[i].Routes[j].TCPIngress.Name, - "tcpingress_namespace": state.Services[i].Routes[j].TCPIngress.Namespace, - }).Errorf("failed to fetch KongIngress resource for Ingress: %v", err) - } - } else { - kongIngress, err = p.getKongIngressFromIngress( - &state.Services[i].Routes[j].Ingress) - if err != nil { - p.Logger.WithFields(logrus.Fields{ - "ingress_name": state.Services[i].Routes[j].Ingress.Name, - "ingress_namespace": state.Services[i].Routes[j].Ingress.Namespace, - }).Errorf("failed to fetch KongIngress resource for Ingress: %v", err) - } - } - - p.overrideRoute(&state.Services[i].Routes[j], kongIngress) - } - } - - // Upstreams - for i := 0; i < len(state.Upstreams); i++ { - kongIngress, err := p.getKongIngressForService( - state.Upstreams[i].Service.K8sService) - anns := state.Upstreams[i].Service.K8sService.Annotations - if err != nil { - p.Logger.WithFields(logrus.Fields{ - "service_name": state.Upstreams[i].Service.K8sService.Name, - "service_namespace": state.Upstreams[i].Service.K8sService.Namespace, - }).Errorf("failed to fetch KongIngress resource for Service: %v", err) - continue - } - overrideUpstream(&state.Upstreams[i], kongIngress, anns) - } -} - -// overrideServiceByKongIngress sets Service fields by KongIngress -func overrideServiceByKongIngress(service *Service, - kongIngress *configurationv1.KongIngress) { - if kongIngress == nil || kongIngress.Proxy == nil { - return - } - s := kongIngress.Proxy - if s.Protocol != nil { - service.Protocol = kong.String(*s.Protocol) - } - if s.Path != nil { - service.Path = kong.String(*s.Path) - } - if s.Retries != nil { - service.Retries = kong.Int(*s.Retries) - } - if s.ConnectTimeout != nil { - service.ConnectTimeout = kong.Int(*s.ConnectTimeout) - } - if s.ReadTimeout != nil { - service.ReadTimeout = kong.Int(*s.ReadTimeout) - } - if s.WriteTimeout != nil { - service.WriteTimeout = kong.Int(*s.WriteTimeout) - } -} - -func overrideServicePath(service *kong.Service, anns map[string]string) { - if service == nil { - return - } - path := annotations.ExtractPath(anns) - if path == "" { - return - } - // kong errors if path doesn't start with `/` - if !strings.HasPrefix(path, "/") { - return - } - service.Path = kong.String(path) -} - -func overrideServiceProtocol(service *kong.Service, anns map[string]string) { - if service == nil { - return - } - protocol := annotations.ExtractProtocolName(anns) - if protocol == "" || !validateProtocol(protocol) { - return - } - service.Protocol = kong.String(protocol) -} - -// overrideServiceByAnnotation modifies the Kong service based on annotations -// on the Kubernetes service. -func overrideServiceByAnnotation(service *kong.Service, - anns map[string]string) { - if service == nil { - return - } - overrideServiceProtocol(service, anns) - overrideServicePath(service, anns) -} - -// overrideService sets Service fields by KongIngress first, then by annotation -func overrideService(service *Service, - kongIngress *configurationv1.KongIngress, - anns map[string]string) { - if service == nil { - return - } - overrideServiceByKongIngress(service, kongIngress) - overrideServiceByAnnotation(&service.Service, anns) - - if *service.Protocol == "grpc" || *service.Protocol == "grpcs" { - // grpc(s) doesn't accept a path - service.Path = nil - } -} - -// overrideRouteByKongIngress sets Route fields by KongIngress -func (p *Parser) overrideRouteByKongIngress(route *Route, - kongIngress *configurationv1.KongIngress) { - if kongIngress == nil || kongIngress.Route == nil { - return - } - - r := kongIngress.Route - if len(r.Methods) != 0 { - invalid := false - var methods []*string - for _, method := range r.Methods { - sanitizedMethod := strings.TrimSpace(strings.ToUpper(*method)) - if validMethods.MatchString(sanitizedMethod) { - methods = append(methods, kong.String(sanitizedMethod)) - } else { - // if any method is invalid (not an uppercase alpha string), - // discard everything - p.Logger.WithFields(logrus.Fields{ - "ingress_namespace": route.Ingress.Namespace, - "ingress_name": route.Ingress.Name, - }).Errorf("ingress contains invalid method: '%v'", *method) - invalid = true - } - } - if !invalid { - route.Methods = methods - } - } - if len(r.Headers) != 0 { - route.Headers = r.Headers - } - if len(r.Protocols) != 0 { - route.Protocols = cloneStringPointerSlice(r.Protocols...) - } - if r.RegexPriority != nil { - route.RegexPriority = kong.Int(*r.RegexPriority) - } - if r.StripPath != nil { - route.StripPath = kong.Bool(*r.StripPath) - } - if r.PreserveHost != nil { - route.PreserveHost = kong.Bool(*r.PreserveHost) - } - if r.HTTPSRedirectStatusCode != nil { - route.HTTPSRedirectStatusCode = kong.Int(*r.HTTPSRedirectStatusCode) - } - if r.PathHandling != nil { - route.PathHandling = kong.String(*r.PathHandling) - } -} - -// normalizeProtocols prevents users from mismatching grpc/http -func normalizeProtocols(route *Route) { - protocols := route.Protocols - var http, grpc bool - - for _, protocol := range protocols { - if strings.Contains(*protocol, "grpc") { - grpc = true - } - if strings.Contains(*protocol, "http") { - http = true - } - if !validateProtocol(*protocol) { - http = true - } - } - - if grpc && http { - route.Protocols = kong.StringSlice("http", "https") - } -} - -// validateProtocol returns a bool of whether string is a valid protocol -func validateProtocol(protocol string) bool { - match := validProtocols.MatchString(protocol) - return match -} - -// useSSLProtocol updates the protocol of the route to either https or grpcs, or https and grpcs -func useSSLProtocol(route *kong.Route) { - var http, grpc bool - var prots []*string - - for _, val := range route.Protocols { - - if strings.Contains(*val, "grpc") { - grpc = true - } - - if strings.Contains(*val, "http") { - http = true - } - } - - if grpc { - prots = append(prots, kong.String("grpcs")) - } - if http { - prots = append(prots, kong.String("https")) - } - - if !grpc && !http { - prots = append(prots, kong.String("https")) - } - - route.Protocols = prots -} -func overrideRouteStripPath(route *kong.Route, anns map[string]string) { - if route == nil { - return - } - - stripPathValue := annotations.ExtractStripPath(anns) - if stripPathValue == "" { - return - } - stripPathValue = strings.ToLower(stripPathValue) - switch stripPathValue { - case "true": - route.StripPath = kong.Bool(true) - case "false": - route.StripPath = kong.Bool(false) - default: - return - } -} - -func overrideRouteProtocols(route *kong.Route, anns map[string]string) { - protocols := annotations.ExtractProtocolNames(anns) - var prots []*string - for _, prot := range protocols { - if !validateProtocol(prot) { - return - } - prots = append(prots, kong.String(prot)) - } - - route.Protocols = prots -} - -func overrideRouteHTTPSRedirectCode(route *kong.Route, anns map[string]string) { - - if annotations.HasForceSSLRedirectAnnotation(anns) { - route.HTTPSRedirectStatusCode = kong.Int(302) - useSSLProtocol(route) - } - - code := annotations.ExtractHTTPSRedirectStatusCode(anns) - if code == "" { - return - } - statusCode, err := strconv.Atoi(code) - if err != nil { - return - } - if statusCode != 426 && - statusCode != 301 && - statusCode != 302 && - statusCode != 307 && - statusCode != 308 { - return - } - - route.HTTPSRedirectStatusCode = kong.Int(statusCode) -} - -func overrideRoutePreserveHost(route *kong.Route, anns map[string]string) { - preserveHostValue := annotations.ExtractPreserveHost(anns) - if preserveHostValue == "" { - return - } - preserveHostValue = strings.ToLower(preserveHostValue) - switch preserveHostValue { - case "true": - route.PreserveHost = kong.Bool(true) - case "false": - route.PreserveHost = kong.Bool(false) - default: - return - } -} - -func overrideRouteRegexPriority(route *kong.Route, anns map[string]string) { - priority := annotations.ExtractRegexPriority(anns) - if priority == "" { - return - } - regexPriority, err := strconv.Atoi(priority) - if err != nil { - return - } - - route.RegexPriority = kong.Int(regexPriority) -} - -func (p *Parser) overrideRouteMethods(route *kong.Route, anns map[string]string) { - annMethods := annotations.ExtractMethods(anns) - if len(annMethods) == 0 { - return - } - var methods []*string - for _, method := range annMethods { - sanitizedMethod := strings.TrimSpace(strings.ToUpper(method)) - if validMethods.MatchString(sanitizedMethod) { - methods = append(methods, kong.String(sanitizedMethod)) - } else { - // if any method is invalid (not an uppercase alpha string), - // discard everything - p.Logger.WithField("kongroute", route.Name).Errorf("invalid method: %v", method) - return - } - } - - route.Methods = methods -} - -// overrideRouteByAnnotation sets Route protocols via annotation -func (p *Parser) overrideRouteByAnnotation(route *Route) { - anns := route.Ingress.Annotations - if route.IsTCP { - anns = route.TCPIngress.Annotations - } - overrideRouteProtocols(&route.Route, anns) - overrideRouteStripPath(&route.Route, anns) - overrideRouteHTTPSRedirectCode(&route.Route, anns) - overrideRoutePreserveHost(&route.Route, anns) - overrideRouteRegexPriority(&route.Route, anns) - p.overrideRouteMethods(&route.Route, anns) -} - -// overrideRoute sets Route fields by KongIngress first, then by annotation -func (p *Parser) overrideRoute(route *Route, - kongIngress *configurationv1.KongIngress) { - if route == nil { - return - } - p.overrideRouteByKongIngress(route, kongIngress) - p.overrideRouteByAnnotation(route) - normalizeProtocols(route) - for _, val := range route.Protocols { - if *val == "grpc" || *val == "grpcs" { - // grpc(s) doesn't accept strip_path - route.StripPath = nil - break - } - } -} - -func cloneStringPointerSlice(array ...*string) (res []*string) { - res = append(res, array...) - return -} - -func overrideUpstreamHostHeader(upstream *kong.Upstream, anns map[string]string) { - if upstream == nil { - return - } - host := annotations.ExtractHostHeader(anns) - if host == "" { - return - } - upstream.HostHeader = kong.String(host) -} - -// overrideUpstreamByAnnotation modifies the Kong upstream based on annotations -// on the Kubernetes service. -func overrideUpstreamByAnnotation(upstream *kong.Upstream, - anns map[string]string) { - if upstream == nil { - return - } - overrideUpstreamHostHeader(upstream, anns) -} - -// overrideUpstreamByKongIngress modifies the Kong upstream based on KongIngresses -// associated with the Kubernetes service. -func overrideUpstreamByKongIngress(upstream *Upstream, - kongIngress *configurationv1.KongIngress) { - if upstream == nil { - return - } - - if kongIngress == nil || kongIngress.Upstream == nil { - return - } - - // The upstream within the KongIngress has no name. - // As this overwrites the entire upstream object, we must restore the - // original name after. - name := *upstream.Upstream.Name - upstream.Upstream = *kongIngress.Upstream.DeepCopy() - upstream.Name = &name -} - -// overrideUpstream sets Upstream fields by KongIngress first, then by annotation -func overrideUpstream(upstream *Upstream, - kongIngress *configurationv1.KongIngress, - anns map[string]string) { - if upstream == nil { - return - } - - overrideUpstreamByKongIngress(upstream, kongIngress) - overrideUpstreamByAnnotation(&upstream.Upstream, anns) -} - -func (p *Parser) getUpstreams(serviceMap map[string]Service) []Upstream { - var upstreams []Upstream +func getUpstreams( + log logrus.FieldLogger, s store.Storer, serviceMap map[string]kongstate.Service) []kongstate.Upstream { + var upstreams []kongstate.Upstream for _, service := range serviceMap { upstreamName := service.Backend.Name + "." + service.Namespace + "." + service.Backend.Port.String() + ".svc" - upstream := Upstream{ + upstream := kongstate.Upstream{ Upstream: kong.Upstream{ Name: kong.String(upstreamName), }, Service: service, } - targets := p.getServiceEndpoints(service.K8sService, + targets := getServiceEndpoints(log, s, service.K8sService, service.Backend.Port.String()) upstream.Targets = targets upstreams = append(upstreams, upstream) @@ -1379,7 +555,7 @@ func getCertFromSecret(secret *corev1.Secret) (string, string, error) { return cert, key, nil } -func (p *Parser) getCerts(secretsToSNIs map[string][]string) []Certificate { +func getCerts(log logrus.FieldLogger, s store.Storer, secretsToSNIs map[string][]string) []kongstate.Certificate { snisAdded := make(map[string]bool) // map of cert public key + private key to certificate type certWrapper struct { @@ -1390,9 +566,9 @@ func (p *Parser) getCerts(secretsToSNIs map[string][]string) []Certificate { for secretKey, SNIs := range secretsToSNIs { namespaceName := strings.Split(secretKey, "/") - secret, err := p.store.GetSecret(namespaceName[0], namespaceName[1]) + secret, err := s.GetSecret(namespaceName[0], namespaceName[1]) if err != nil { - p.Logger.WithFields(logrus.Fields{ + log.WithFields(logrus.Fields{ "secret_name": namespaceName[1], "secret_namespace": namespaceName[0], }).Logger.Errorf("failed to fetch secret: %v", err) @@ -1400,7 +576,7 @@ func (p *Parser) getCerts(secretsToSNIs map[string][]string) []Certificate { } cert, key, err := getCertFromSecret(secret) if err != nil { - p.Logger.WithFields(logrus.Fields{ + log.WithFields(logrus.Fields{ "secret_name": namespaceName[1], "secret_namespace": namespaceName[0], }).Logger.Errorf("failed to construct certificate from secret: %v", err) @@ -1431,230 +607,20 @@ func (p *Parser) getCerts(secretsToSNIs map[string][]string) []Certificate { } certs[cert+key] = kongCert } - var res []Certificate + var res []kongstate.Certificate for _, cert := range certs { - res = append(res, Certificate{cert.cert}) + res = append(res, kongstate.Certificate{Certificate: cert.cert}) } return res } -type foreignRelations struct { - Consumer, Route, Service []string -} - -func getPluginRelations(state KongState) map[string]foreignRelations { - // KongPlugin key (KongPlugin's name:namespace) to corresponding associations - pluginRels := map[string]foreignRelations{} - addConsumerRelation := func(namespace, pluginName, identifier string) { - pluginKey := namespace + ":" + pluginName - relations, ok := pluginRels[pluginKey] - if !ok { - relations = foreignRelations{} - } - relations.Consumer = append(relations.Consumer, identifier) - pluginRels[pluginKey] = relations - } - addRouteRelation := func(namespace, pluginName, identifier string) { - pluginKey := namespace + ":" + pluginName - relations, ok := pluginRels[pluginKey] - if !ok { - relations = foreignRelations{} - } - relations.Route = append(relations.Route, identifier) - pluginRels[pluginKey] = relations - } - addServiceRelation := func(namespace, pluginName, identifier string) { - pluginKey := namespace + ":" + pluginName - relations, ok := pluginRels[pluginKey] - if !ok { - relations = foreignRelations{} - } - relations.Service = append(relations.Service, identifier) - pluginRels[pluginKey] = relations - } - - for i := range state.Services { - // service - svc := state.Services[i].K8sService - pluginList := annotations.ExtractKongPluginsFromAnnotations( - svc.GetAnnotations()) - for _, pluginName := range pluginList { - addServiceRelation(svc.Namespace, pluginName, - *state.Services[i].Name) - } - // route - for j := range state.Services[i].Routes { - ingress := state.Services[i].Routes[j].Ingress - pluginList := annotations.ExtractKongPluginsFromAnnotations(ingress.GetAnnotations()) - for _, pluginName := range pluginList { - addRouteRelation(ingress.Namespace, pluginName, *state.Services[i].Routes[j].Name) - } - } - } - // consumer - for _, c := range state.Consumers { - pluginList := annotations.ExtractKongPluginsFromAnnotations(c.k8sKongConsumer.GetAnnotations()) - for _, pluginName := range pluginList { - addConsumerRelation(c.k8sKongConsumer.Namespace, pluginName, *c.Username) - } - } - return pluginRels -} - -type rel struct { - Consumer, Route, Service string -} - -func getCombinations(relations foreignRelations) []rel { - - var cartesianProduct []rel - - if len(relations.Consumer) > 0 { - consumers := relations.Consumer - if len(relations.Route)+len(relations.Service) > 0 { - for _, service := range relations.Service { - for _, consumer := range consumers { - cartesianProduct = append(cartesianProduct, rel{ - Service: service, - Consumer: consumer, - }) - } - } - for _, route := range relations.Route { - for _, consumer := range consumers { - cartesianProduct = append(cartesianProduct, rel{ - Route: route, - Consumer: consumer, - }) - } - } - } else { - for _, consumer := range relations.Consumer { - cartesianProduct = append(cartesianProduct, rel{Consumer: consumer}) - } - } - } else { - for _, service := range relations.Service { - cartesianProduct = append(cartesianProduct, rel{Service: service}) - } - for _, route := range relations.Route { - cartesianProduct = append(cartesianProduct, rel{Route: route}) - } - } - - return cartesianProduct -} - -func (p *Parser) fillPlugins(state KongState) []Plugin { - var plugins []Plugin - pluginRels := getPluginRelations(state) - - for pluginIdentifier, relations := range pluginRels { - identifier := strings.Split(pluginIdentifier, ":") - namespace, kongPluginName := identifier[0], identifier[1] - plugin, err := p.getPlugin(namespace, kongPluginName) - if err != nil { - p.Logger.WithFields(logrus.Fields{ - "kongplugin_name": kongPluginName, - "kongplugin_namespace": namespace, - }).Logger.Errorf("failed to fetch KongPlugin: %v", err) - continue - } - - for _, rel := range getCombinations(relations) { - plugin := *plugin.DeepCopy() - // ID is populated because that is read by decK and in_memory - // translator too - if rel.Service != "" { - plugin.Service = &kong.Service{ID: kong.String(rel.Service)} - } - if rel.Route != "" { - plugin.Route = &kong.Route{ID: kong.String(rel.Route)} - } - if rel.Consumer != "" { - plugin.Consumer = &kong.Consumer{ID: kong.String(rel.Consumer)} - } - plugins = append(plugins, Plugin{plugin}) - } - } - - globalPlugins, err := p.globalPlugins() - if err != nil { - p.Logger.Errorf("failed to fetch global plugins: %v", err) - } - plugins = append(plugins, globalPlugins...) - - return plugins -} - -func (p *Parser) globalPlugins() ([]Plugin, error) { - // removed as of 0.10.0 - // only retrieved now to warn users - globalPlugins, err := p.store.ListGlobalKongPlugins() - if err != nil { - return nil, fmt.Errorf("error listing global KongPlugins: %w", err) - } - if len(globalPlugins) > 0 { - p.Logger.Warning("global KongPlugins found. These are no longer applied and", - " must be replaced with KongClusterPlugins.", - " Please run \"kubectl get kongplugin -l global=true --all-namespaces\" to list existing plugins") - } - res := make(map[string]Plugin) - var duplicates []string // keep track of duplicate - // TODO respect the oldest CRD - // Current behavior is to skip creating the plugin but in case - // of duplicate plugin definitions, we should respect the oldest one - // This is important since if a user comes in to k8s and creates a new - // CRD, the user now deleted an older plugin - - globalClusterPlugins, err := p.store.ListGlobalKongClusterPlugins() - if err != nil { - return nil, fmt.Errorf("error listing global KongClusterPlugins: %w", err) - } - for i := 0; i < len(globalClusterPlugins); i++ { - k8sPlugin := *globalClusterPlugins[i] - pluginName := k8sPlugin.PluginName - // empty pluginName skip it - if pluginName == "" { - p.Logger.WithFields(logrus.Fields{ - "kongclusterplugin_name": k8sPlugin.Name, - }).Errorf("invalid KongClusterPlugin: empty plugin property") - continue - } - if _, ok := res[pluginName]; ok { - p.Logger.Error("multiple KongPlugin definitions found with"+ - " 'global' label for '", pluginName, - "', the plugin will not be applied") - duplicates = append(duplicates, pluginName) - continue - } - if plugin, err := p.kongPluginFromK8SClusterPlugin(k8sPlugin); err == nil { - res[pluginName] = Plugin{ - Plugin: plugin, - } - } else { - p.Logger.WithFields(logrus.Fields{ - "kongclusterplugin_name": k8sPlugin.Name, - }).Errorf("failed to generate configuration from KongClusterPlugin: %v ", err) - } - } - for _, plugin := range duplicates { - delete(res, plugin) - } - var plugins []Plugin - for _, p := range res { - plugins = append(plugins, p) - } - return plugins, nil -} - -func (p *Parser) getServiceEndpoints(svc corev1.Service, - backendPort string) []Target { - var targets []Target +func getServiceEndpoints(log logrus.FieldLogger, s store.Storer, svc corev1.Service, + backendPort string) []kongstate.Target { + var targets []kongstate.Target var endpoints []utils.Endpoint var servicePort corev1.ServicePort - logger := p.Logger.WithFields(logrus.Fields{ + log = log.WithFields(logrus.Fields{ "service_name": svc.Name, "service_namespace": svc.Namespace, }) @@ -1675,7 +641,7 @@ func (p *Parser) getServiceEndpoints(svc corev1.Service, // nolint: gosec externalPort, err := strconv.Atoi(backendPort) if err != nil { - logger.Warningf("invalid ExternalName Service (only numeric ports allowed): %v", backendPort) + log.Warningf("invalid ExternalName Service (only numeric ports allowed): %v", backendPort) return targets } @@ -1686,13 +652,13 @@ func (p *Parser) getServiceEndpoints(svc corev1.Service, } } - endpoints = p.getEndpoints(&svc, &servicePort, - corev1.ProtocolTCP, p.store.GetEndpointsForService) + endpoints = getEndpoints(log, &svc, &servicePort, + corev1.ProtocolTCP, s.GetEndpointsForService) if len(endpoints) == 0 { - logger.Warningf("no active endpionts") + log.Warningf("no active endpionts") } for _, endpoint := range endpoints { - target := Target{ + target := kongstate.Target{ Target: kong.Target{ Target: kong.String(endpoint.Address + ":" + endpoint.Port), }, @@ -1702,214 +668,9 @@ func (p *Parser) getServiceEndpoints(svc corev1.Service, return targets } -func (p *Parser) getKongIngressForService(service corev1.Service) ( - *configurationv1.KongIngress, error) { - confName := annotations.ExtractConfigurationName(service.Annotations) - if confName == "" { - return nil, nil - } - return p.store.GetKongIngress(service.Namespace, confName) -} - -func (p *Parser) getKongIngressFromIngressAnnotations(namespace, name string, - anns map[string]string) ( - *configurationv1.KongIngress, error) { - confName := annotations.ExtractConfigurationName(anns) - if confName != "" { - ki, err := p.store.GetKongIngress(namespace, confName) - if err == nil { - return ki, nil - } - } - - ki, err := p.store.GetKongIngress(namespace, name) - if err == nil { - return ki, nil - } - return nil, nil -} - -// getKongIngressFromIngress checks if the Ingress -// contains an annotation for configuration -// or if exists a KongIngress object with the same name than the Ingress -func (p *Parser) getKongIngressFromIngress(ing *networking.Ingress) ( - *configurationv1.KongIngress, error) { - return p.getKongIngressFromIngressAnnotations(ing.Namespace, - ing.Name, ing.Annotations) -} - -// getKongIngressFromTCPIngress checks if the TCPIngress contains an -// annotation for configuration -// or if exists a KongIngress object with the same name than the Ingress -func (p *Parser) getKongIngressFromTCPIngress(ing *configurationv1beta1.TCPIngress) ( - *configurationv1.KongIngress, error) { - return p.getKongIngressFromIngressAnnotations(ing.Namespace, - ing.Name, ing.Annotations) -} - -// getPlugin constructs a plugins from a KongPlugin resource. -func (p *Parser) getPlugin(namespace, name string) (kong.Plugin, error) { - var plugin kong.Plugin - k8sPlugin, err := p.store.GetKongPlugin(namespace, name) - if err != nil { - // if no namespaced plugin definition, then - // search for cluster level-plugin definition - if errors.As(err, &store.ErrNotFound{}) { - clusterPlugin, err := p.store.GetKongClusterPlugin(name) - // not found - if errors.As(err, &store.ErrNotFound{}) { - return plugin, errors.New( - "no KongPlugin or KongClusterPlugin was found") - } - if err != nil { - return plugin, err - } - if clusterPlugin.PluginName == "" { - return plugin, fmt.Errorf("invalid empty 'plugin' property") - } - plugin, err = p.kongPluginFromK8SClusterPlugin(*clusterPlugin) - return plugin, err - } - } - // ignore plugins with no name - if k8sPlugin.PluginName == "" { - return plugin, fmt.Errorf("invalid empty 'plugin' property") - } - - plugin, err = p.kongPluginFromK8SPlugin(*k8sPlugin) - return plugin, err -} - -func (p *Parser) secretToConfiguration( - reference configurationv1.SecretValueFromSource, namespace string) ( - configurationv1.Configuration, error) { - secret, err := p.store.GetSecret(namespace, reference.Secret) - if err != nil { - return configurationv1.Configuration{}, fmt.Errorf( - "error fetching plugin configuration secret '%v/%v': %v", - namespace, reference.Secret, err) - } - secretVal, ok := secret.Data[reference.Key] - if !ok { - return configurationv1.Configuration{}, - fmt.Errorf("no key '%v' in secret '%v/%v'", - reference.Key, namespace, reference.Secret) - } - var config configurationv1.Configuration - if err := json.Unmarshal(secretVal, &config); err != nil { - if err := yaml.Unmarshal(secretVal, &config); err != nil { - return configurationv1.Configuration{}, - fmt.Errorf("key '%v' in secret '%v/%v' contains neither "+ - "valid JSON nor valid YAML)", - reference.Key, namespace, reference.Secret) - } - } - return config, nil -} - -func (p *Parser) namespacedSecretToConfiguration( - reference configurationv1.NamespacedSecretValueFromSource) ( - configurationv1.Configuration, error) { - bareReference := configurationv1.SecretValueFromSource{ - Secret: reference.Secret, - Key: reference.Key} - return p.secretToConfiguration(bareReference, reference.Namespace) -} - -// plugin is a intermediate type to hold plugin related configuration -type plugin struct { - Name string - Config configurationv1.Configuration - - RunOn string - Disabled bool - Protocols []string -} - -func toKongPlugin(plugin plugin) kong.Plugin { - result := kong.Plugin{ - Name: kong.String(plugin.Name), - Config: kong.Configuration(plugin.Config).DeepCopy(), - } - if plugin.RunOn != "" { - result.RunOn = kong.String(plugin.RunOn) - } - if plugin.Disabled { - result.Enabled = kong.Bool(false) - } - if len(plugin.Protocols) > 0 { - result.Protocols = kong.StringSlice(plugin.Protocols...) - } - return result -} - -func (p *Parser) kongPluginFromK8SClusterPlugin( - k8sPlugin configurationv1.KongClusterPlugin) (kong.Plugin, error) { - config := k8sPlugin.Config - if k8sPlugin.ConfigFrom.SecretValue != - (configurationv1.NamespacedSecretValueFromSource{}) && - len(k8sPlugin.Config) > 0 { - return kong.Plugin{}, - fmt.Errorf("KongClusterPlugin '/%v' has both "+ - "Config and ConfigFrom set", k8sPlugin.Name) - } - if k8sPlugin.ConfigFrom.SecretValue != (configurationv1. - NamespacedSecretValueFromSource{}) { - var err error - config, err = p.namespacedSecretToConfiguration( - k8sPlugin.ConfigFrom.SecretValue) - if err != nil { - return kong.Plugin{}, - fmt.Errorf("error parsing config for KongClusterPlugin %v: %w", - k8sPlugin.Name, err) - } - } - kongPlugin := toKongPlugin(plugin{ - Name: k8sPlugin.PluginName, - Config: config, - - RunOn: k8sPlugin.RunOn, - Disabled: k8sPlugin.Disabled, - Protocols: k8sPlugin.Protocols, - }) - return kongPlugin, nil -} - -func (p *Parser) kongPluginFromK8SPlugin( - k8sPlugin configurationv1.KongPlugin) (kong.Plugin, error) { - config := k8sPlugin.Config - if k8sPlugin.ConfigFrom.SecretValue != - (configurationv1.SecretValueFromSource{}) && - len(k8sPlugin.Config) > 0 { - return kong.Plugin{}, - fmt.Errorf("KongPlugin '%v/%v' has both "+ - "Config and ConfigFrom set", - k8sPlugin.Namespace, k8sPlugin.Name) - } - if k8sPlugin.ConfigFrom.SecretValue != - (configurationv1.SecretValueFromSource{}) { - var err error - config, err = p.secretToConfiguration( - k8sPlugin.ConfigFrom.SecretValue, k8sPlugin.Namespace) - if err != nil { - return kong.Plugin{}, - fmt.Errorf("error parsing config for KongPlugin '%v/%v': %w", - k8sPlugin.Name, k8sPlugin.Namespace, err) - } - } - kongPlugin := toKongPlugin(plugin{ - Name: k8sPlugin.PluginName, - Config: config, - - RunOn: k8sPlugin.RunOn, - Disabled: k8sPlugin.Disabled, - Protocols: k8sPlugin.Protocols, - }) - return kongPlugin, nil -} - // getEndpoints returns a list of : for a given service/target port combination. -func (p *Parser) getEndpoints( +func getEndpoints( + log logrus.FieldLogger, s *corev1.Service, port *corev1.ServicePort, proto corev1.Protocol, @@ -1922,7 +683,7 @@ func (p *Parser) getEndpoints( return upsServers } - logger := p.Logger.WithFields(logrus.Fields{ + log = log.WithFields(logrus.Fields{ "service_name": s.Name, "service_namespace": s.Namespace, "service_port": port.String(), @@ -1935,12 +696,12 @@ func (p *Parser) getEndpoints( // ExternalName services if s.Spec.Type == corev1.ServiceTypeExternalName { - logger.Debug("found service of type=ExternalName") + log.Debug("found service of type=ExternalName") targetPort := port.TargetPort.IntValue() // check for invalid port value if targetPort <= 0 { - logger.Errorf("invalid service: invalid port: %v", targetPort) + log.Errorf("invalid service: invalid port: %v", targetPort) return upsServers } @@ -1957,10 +718,10 @@ func (p *Parser) getEndpoints( } - logger.Debugf("fetching endpoints") + log.Debugf("fetching endpoints") ep, err := getEndpoints(s.Namespace, s.Name) if err != nil { - logger.Errorf("failed to fetch endpoints: %v", err) + log.Errorf("failed to fetch endpoints: %v", err) return upsServers } @@ -2000,6 +761,6 @@ func (p *Parser) getEndpoints( } } - logger.Debugf("found endpoints: %v", upsServers) + log.Debugf("found endpoints: %v", upsServers) return upsServers } diff --git a/internal/ingress/controller/parser/parser_test.go b/internal/ingress/controller/parser/parser_test.go index 4d29afc6d0..f95eb2e03b 100644 --- a/internal/ingress/controller/parser/parser_test.go +++ b/internal/ingress/controller/parser/parser_test.go @@ -10,6 +10,7 @@ import ( "github.com/kong/go-kong/kong" "github.com/kong/kubernetes-ingress-controller/internal/ingress/annotations" + "github.com/kong/kubernetes-ingress-controller/internal/ingress/controller/parser/kongstate" "github.com/kong/kubernetes-ingress-controller/internal/ingress/store" "github.com/kong/kubernetes-ingress-controller/internal/ingress/utils" configurationv1 "github.com/kong/kubernetes-ingress-controller/pkg/apis/configuration/v1" @@ -162,8 +163,7 @@ func TestGlobalPlugin(t *testing.T) { }, }) assert.Nil(err) - parser := New(store, logrus.New()) - state, err := parser.Build() + state, err := Build(logrus.New(), store) assert.Nil(err) assert.NotNil(state) assert.Equal(1, len(state.Plugins), @@ -343,8 +343,7 @@ func TestSecretConfigurationPlugin(t *testing.T) { } store, err := store.NewFakeStore(objects) assert.Nil(err) - parser := New(store, logrus.New()) - state, err := parser.Build() + state, err := Build(logrus.New(), store) assert.Nil(err) assert.NotNil(state) assert.Equal(3, len(state.Plugins), @@ -448,8 +447,7 @@ func TestSecretConfigurationPlugin(t *testing.T) { } store, err := store.NewFakeStore(objects) assert.Nil(err) - parser := New(store, logrus.New()) - state, err := parser.Build() + state, err := Build(logrus.New(), store) assert.Nil(err) assert.NotNil(state) assert.Equal(0, len(state.Plugins), @@ -542,8 +540,7 @@ func TestSecretConfigurationPlugin(t *testing.T) { } store, err := store.NewFakeStore(objects) assert.Nil(err) - parser := New(store, logrus.New()) - state, err := parser.Build() + state, err := Build(logrus.New(), store) assert.Nil(err) assert.NotNil(state) assert.Equal(0, len(state.Plugins), @@ -594,17 +591,16 @@ func TestSecretConfigurationPlugin(t *testing.T) { } store, err := store.NewFakeStore(objects) assert.Nil(err) - parser := New(store, logrus.New()) - state, err := parser.Build() + state, err := Build(logrus.New(), store) assert.Nil(err) assert.NotNil(state) for _, testcase := range references { - config, err := parser.secretToConfiguration(*testcase, "default") + config, err := kongstate.SecretToConfiguration(store, *testcase, "default") assert.NotEmpty(config) assert.Nil(err) } for _, testcase := range badReferences { - config, err := parser.secretToConfiguration(*testcase, "default") + config, err := kongstate.SecretToConfiguration(store, *testcase, "default") assert.Empty(config) assert.NotEmpty(err) } @@ -693,8 +689,7 @@ func TestSecretConfigurationPlugin(t *testing.T) { } store, err := store.NewFakeStore(objects) assert.Nil(err) - parser := New(store, logrus.New()) - state, err := parser.Build() + state, err := Build(logrus.New(), store) assert.Nil(err) assert.NotNil(state) assert.Equal(0, len(state.Plugins), @@ -728,8 +723,7 @@ func TestCACertificate(t *testing.T) { Secrets: secrets, }) assert.Nil(err) - parser := New(store, logrus.New()) - state, err := parser.Build() + state, err := Build(logrus.New(), store) assert.Nil(err) assert.NotNil(state) @@ -779,8 +773,7 @@ func TestCACertificate(t *testing.T) { Secrets: secrets, }) assert.Nil(err) - parser := New(store, logrus.New()) - state, err := parser.Build() + state, err := Build(logrus.New(), store) assert.Nil(err) assert.NotNil(state) @@ -842,8 +835,7 @@ func TestCACertificate(t *testing.T) { Secrets: secrets, }) assert.Nil(err) - parser := New(store, logrus.New()) - state, err := parser.Build() + state, err := Build(logrus.New(), store) assert.Nil(err) assert.NotNil(state) @@ -926,8 +918,7 @@ func TestServiceClientCertificate(t *testing.T) { Services: services, }) assert.Nil(err) - parser := New(store, logrus.New()) - state, err := parser.Build() + state, err := Build(logrus.New(), store) assert.Nil(err) assert.NotNil(state) assert.Equal(1, len(state.Certificates), @@ -994,8 +985,7 @@ func TestServiceClientCertificate(t *testing.T) { Services: services, }) assert.Nil(err) - parser := New(store, logrus.New()) - state, err := parser.Build() + state, err := Build(logrus.New(), store) assert.Nil(err) assert.NotNil(state) assert.Equal(0, len(state.Certificates), @@ -1055,8 +1045,7 @@ func TestKongRouteAnnotations(t *testing.T) { Services: services, }) assert.Nil(err) - parser := New(store, logrus.New()) - state, err := parser.Build() + state, err := Build(logrus.New(), store) assert.Nil(err) assert.NotNil(state) @@ -1133,8 +1122,7 @@ func TestKongRouteAnnotations(t *testing.T) { Services: services, }) assert.Nil(err) - parser := New(store, logrus.New()) - state, err := parser.Build() + state, err := Build(logrus.New(), store) assert.Nil(err) assert.NotNil(state) @@ -1212,8 +1200,7 @@ func TestKongRouteAnnotations(t *testing.T) { Services: services, }) assert.Nil(err) - parser := New(store, logrus.New()) - state, err := parser.Build() + state, err := Build(logrus.New(), store) assert.Nil(err) assert.NotNil(state) @@ -1292,8 +1279,7 @@ func TestKongRouteAnnotations(t *testing.T) { Services: services, }) assert.Nil(err) - parser := New(store, logrus.New()) - state, err := parser.Build() + state, err := Build(logrus.New(), store) assert.Nil(err) assert.NotNil(state) @@ -1371,8 +1357,7 @@ func TestKongRouteAnnotations(t *testing.T) { Services: services, }) assert.Nil(err) - parser := New(store, logrus.New()) - state, err := parser.Build() + state, err := Build(logrus.New(), store) assert.Nil(err) assert.NotNil(state) @@ -1450,8 +1435,7 @@ func TestKongRouteAnnotations(t *testing.T) { Services: services, }) assert.Nil(err) - parser := New(store, logrus.New()) - state, err := parser.Build() + state, err := Build(logrus.New(), store) assert.Nil(err) assert.NotNil(state) @@ -1529,8 +1513,7 @@ func TestKongRouteAnnotations(t *testing.T) { Services: services, }) assert.Nil(err) - parser := New(store, logrus.New()) - state, err := parser.Build() + state, err := Build(logrus.New(), store) assert.Nil(err) assert.NotNil(state) @@ -1608,8 +1591,7 @@ func TestKongRouteAnnotations(t *testing.T) { Services: services, }) assert.Nil(err) - parser := New(store, logrus.New()) - state, err := parser.Build() + state, err := Build(logrus.New(), store) assert.Nil(err) assert.NotNil(state) @@ -1689,8 +1671,7 @@ func TestKongProcessClasslessIngress(t *testing.T) { Services: services, }) assert.Nil(err) - parser := New(store, logrus.New()) - state, err := parser.Build() + state, err := Build(logrus.New(), store) assert.Nil(err) assert.NotNil(state) @@ -1740,8 +1721,7 @@ func TestKongProcessClasslessIngress(t *testing.T) { Services: services, }) assert.Nil(err) - parser := New(store, logrus.New()) - state, err := parser.Build() + state, err := Build(logrus.New(), store) assert.Nil(err) assert.NotNil(state) @@ -1823,8 +1803,7 @@ func TestKnativeIngressAndPlugins(t *testing.T) { KongPlugins: plugins, }) assert.Nil(err) - parser := New(store, logrus.New()) - state, err := parser.Build() + state, err := Build(logrus.New(), store) assert.Nil(err) assert.NotNil(state) @@ -1932,8 +1911,7 @@ func TestKongServiceAnnotations(t *testing.T) { Services: services, }) assert.Nil(err) - parser := New(store, logrus.New()) - state, err := parser.Build() + state, err := Build(logrus.New(), store) assert.Nil(err) assert.NotNil(state) @@ -2013,8 +1991,7 @@ func TestKongServiceAnnotations(t *testing.T) { Services: services, }) assert.Nil(err) - parser := New(store, logrus.New()) - state, err := parser.Build() + state, err := Build(logrus.New(), store) assert.Nil(err) assert.NotNil(state) @@ -2100,8 +2077,7 @@ func TestKongServiceAnnotations(t *testing.T) { Services: services, }) assert.Nil(err) - parser := New(store, logrus.New()) - state, err := parser.Build() + state, err := Build(logrus.New(), store) assert.Nil(err) assert.NotNil(state) @@ -2168,8 +2144,7 @@ func TestDefaultBackend(t *testing.T) { Services: services, }) assert.Nil(err) - parser := New(store, logrus.New()) - state, err := parser.Build() + state, err := Build(logrus.New(), store) assert.Nil(err) assert.NotNil(state) assert.Equal(1, len(state.Services), @@ -2237,8 +2212,7 @@ func TestDefaultBackend(t *testing.T) { Services: services, }) assert.Nil(err) - parser := New(store, logrus.New()) - state, err := parser.Build() + state, err := Build(logrus.New(), store) assert.Nil(err) assert.NotNil(state) assert.Equal(0, len(state.Certificates), @@ -2303,8 +2277,7 @@ func TestParserSecret(t *testing.T) { Secrets: secrets, }) assert.Nil(err) - parser := New(store, logrus.New()) - state, err := parser.Build() + state, err := Build(logrus.New(), store) assert.Nil(err) assert.NotNil(state) assert.Equal(0, len(state.Certificates), @@ -2385,8 +2358,7 @@ func TestParserSecret(t *testing.T) { Secrets: secrets, }) assert.Nil(err) - parser := New(store, logrus.New()) - state, err := parser.Build() + state, err := Build(logrus.New(), store) assert.Nil(err) assert.NotNil(state) assert.Equal(1, len(state.Certificates), @@ -2396,7 +2368,7 @@ func TestParserSecret(t *testing.T) { return strings.Compare(*state.Certificates[0].SNIs[i], *state.Certificates[0].SNIs[j]) > 0 }) - assert.Equal(Certificate{ + assert.Equal(kongstate.Certificate{ Certificate: kong.Certificate{ ID: kong.String("3e8edeca-7d23-4e02-84c9-437d11b746a6"), Cert: kong.String(tlsPairs[0].Cert), @@ -2467,8 +2439,7 @@ func TestParserSecret(t *testing.T) { Secrets: secrets, }) assert.Nil(err) - parser := New(store, logrus.New()) - state, err := parser.Build() + state, err := Build(logrus.New(), store) assert.Nil(err) assert.NotNil(state) assert.Equal(1, len(state.Certificates), @@ -2546,8 +2517,7 @@ func TestPluginAnnotations(t *testing.T) { KongPlugins: plugins, }) assert.Nil(err) - parser := New(store, logrus.New()) - state, err := parser.Build() + state, err := Build(logrus.New(), store) assert.Nil(err) assert.NotNil(state) assert.Equal(1, len(state.Plugins), @@ -2644,8 +2614,7 @@ func TestPluginAnnotations(t *testing.T) { KongClusterPlugins: clusterPlugins, }) assert.Nil(err) - parser := New(store, logrus.New()) - state, err := parser.Build() + state, err := Build(logrus.New(), store) assert.Nil(err) assert.NotNil(state) assert.Equal(1, len(state.Plugins), @@ -2715,8 +2684,7 @@ func TestPluginAnnotations(t *testing.T) { KongClusterPlugins: clusterPlugins, }) assert.Nil(err) - parser := New(store, logrus.New()) - state, err := parser.Build() + state, err := Build(logrus.New(), store) assert.Nil(err) assert.NotNil(state) assert.Equal(1, len(state.Plugins), @@ -2762,8 +2730,7 @@ func TestPluginAnnotations(t *testing.T) { Ingresses: ingresses, }) assert.Nil(err) - parser := New(store, logrus.New()) - state, err := parser.Build() + state, err := Build(logrus.New(), store) assert.Nil(err) assert.NotNil(state) assert.Equal(0, len(state.Plugins), @@ -2773,9 +2740,6 @@ func TestPluginAnnotations(t *testing.T) { func TestParseIngressRules(t *testing.T) { assert := assert.New(t) - p := Parser{ - Logger: logrus.New(), - } ingressList := []*networking.Ingress{ // 0 { @@ -3072,23 +3036,23 @@ func TestParseIngressRules(t *testing.T) { }, } t.Run("no ingress returns empty info", func(t *testing.T) { - parsedInfo := p.parseIngressRules([]*networking.Ingress{}, + parsedInfo := parseIngressRules(logrus.New(), []*networking.Ingress{}, []*configurationv1beta1.TCPIngress{}) - assert.Equal(&parsedIngressRules{ - ServiceNameToServices: make(map[string]Service), + assert.Equal(ingressRules{ + ServiceNameToServices: make(map[string]kongstate.Service), SecretNameToSNIs: make(map[string][]string), }, parsedInfo) }) t.Run("empty TCPIngress return empty info", func(t *testing.T) { - parsedInfo := p.parseIngressRules([]*networking.Ingress{}, + parsedInfo := parseIngressRules(logrus.New(), []*networking.Ingress{}, []*configurationv1beta1.TCPIngress{tcpIngressList[0]}) - assert.Equal(&parsedIngressRules{ - ServiceNameToServices: make(map[string]Service), + assert.Equal(ingressRules{ + ServiceNameToServices: make(map[string]kongstate.Service), SecretNameToSNIs: make(map[string][]string), }, parsedInfo) }) t.Run("simple ingress rule is parsed", func(t *testing.T) { - parsedInfo := p.parseIngressRules([]*networking.Ingress{ + parsedInfo := parseIngressRules(logrus.New(), []*networking.Ingress{ ingressList[0], }, []*configurationv1beta1.TCPIngress{}) assert.Equal(1, len(parsedInfo.ServiceNameToServices)) @@ -3099,7 +3063,7 @@ func TestParseIngressRules(t *testing.T) { assert.Equal("example.com", *parsedInfo.ServiceNameToServices["foo-namespace.foo-svc.80"].Routes[0].Hosts[0]) }) t.Run("simple TCPIngress rule is parsed", func(t *testing.T) { - parsedInfo := p.parseIngressRules([]*networking.Ingress{}, + parsedInfo := parseIngressRules(logrus.New(), []*networking.Ingress{}, []*configurationv1beta1.TCPIngress{tcpIngressList[1]}) assert.Equal(1, len(parsedInfo.ServiceNameToServices)) svc := parsedInfo.ServiceNameToServices["default.foo-svc.80"] @@ -3120,7 +3084,7 @@ func TestParseIngressRules(t *testing.T) { }, route.Route) }) t.Run("TCPIngress rule with host is parsed", func(t *testing.T) { - parsedInfo := p.parseIngressRules([]*networking.Ingress{}, + parsedInfo := parseIngressRules(logrus.New(), []*networking.Ingress{}, []*configurationv1beta1.TCPIngress{tcpIngressList[2]}) assert.Equal(1, len(parsedInfo.ServiceNameToServices)) svc := parsedInfo.ServiceNameToServices["default.foo-svc.80"] @@ -3142,7 +3106,7 @@ func TestParseIngressRules(t *testing.T) { }, route.Route) }) t.Run("ingress rule with default backend", func(t *testing.T) { - parsedInfo := p.parseIngressRules([]*networking.Ingress{ + parsedInfo := parseIngressRules(logrus.New(), []*networking.Ingress{ ingressList[0], ingressList[2], }, []*configurationv1beta1.TCPIngress{}) @@ -3158,7 +3122,7 @@ func TestParseIngressRules(t *testing.T) { assert.Equal(0, len(parsedInfo.ServiceNameToServices["bar-namespace.default-svc.80"].Routes[0].Hosts)) }) t.Run("ingress rule with TLS", func(t *testing.T) { - parsedInfo := p.parseIngressRules([]*networking.Ingress{ + parsedInfo := parseIngressRules(logrus.New(), []*networking.Ingress{ ingressList[1], }, []*configurationv1beta1.TCPIngress{}) assert.Equal(2, len(parsedInfo.SecretNameToSNIs)) @@ -3166,14 +3130,14 @@ func TestParseIngressRules(t *testing.T) { assert.Equal(2, len(parsedInfo.SecretNameToSNIs["bar-namespace/sooper-secret2"])) }) t.Run("TCPIngress with TLS", func(t *testing.T) { - parsedInfo := p.parseIngressRules([]*networking.Ingress{}, + parsedInfo := parseIngressRules(logrus.New(), []*networking.Ingress{}, []*configurationv1beta1.TCPIngress{tcpIngressList[3]}) assert.Equal(2, len(parsedInfo.SecretNameToSNIs)) assert.Equal(2, len(parsedInfo.SecretNameToSNIs["default/sooper-secret"])) assert.Equal(2, len(parsedInfo.SecretNameToSNIs["default/sooper-secret2"])) }) t.Run("ingress rule with ACME like path has strip_path set to false", func(t *testing.T) { - parsedInfo := p.parseIngressRules([]*networking.Ingress{ + parsedInfo := parseIngressRules(logrus.New(), []*networking.Ingress{ ingressList[3], }, []*configurationv1beta1.TCPIngress{}) assert.Equal(1, len(parsedInfo.ServiceNameToServices)) @@ -3188,7 +3152,7 @@ func TestParseIngressRules(t *testing.T) { assert.False(*parsedInfo.ServiceNameToServices["foo-namespace.cert-manager-solver-pod.80"].Routes[0].StripPath) }) t.Run("ingress with empty path is correctly parsed", func(t *testing.T) { - parsedInfo := p.parseIngressRules([]*networking.Ingress{ + parsedInfo := parseIngressRules(logrus.New(), []*networking.Ingress{ ingressList[4], }, []*configurationv1beta1.TCPIngress{}) assert.Equal("/", *parsedInfo.ServiceNameToServices["foo-namespace.foo-svc.80"].Routes[0].Paths[0]) @@ -3196,20 +3160,20 @@ func TestParseIngressRules(t *testing.T) { }) t.Run("empty Ingress rule doesn't cause a panic", func(t *testing.T) { assert.NotPanics(func() { - p.parseIngressRules([]*networking.Ingress{ + parseIngressRules(logrus.New(), []*networking.Ingress{ ingressList[5], }, []*configurationv1beta1.TCPIngress{}) }) }) t.Run("Ingress rules with multiple ports for one Service use separate hostnames for each port", func(t *testing.T) { - parsedInfo := p.parseIngressRules([]*networking.Ingress{ + parsedInfo := parseIngressRules(logrus.New(), []*networking.Ingress{ ingressList[6], }, []*configurationv1beta1.TCPIngress{}) assert.Equal("foo-svc.foo-namespace.80.svc", *parsedInfo.ServiceNameToServices["foo-namespace.foo-svc.80"].Host) assert.Equal("foo-svc.foo-namespace.8000.svc", *parsedInfo.ServiceNameToServices["foo-namespace.foo-svc.8000"].Host) }) t.Run("Ingress rule with path containing multiple slashes ('//') is skipped", func(t *testing.T) { - parsedInfo := p.parseIngressRules([]*networking.Ingress{ + parsedInfo := parseIngressRules(logrus.New(), []*networking.Ingress{ ingressList[7], }, []*configurationv1beta1.TCPIngress{}) assert.Empty(parsedInfo.ServiceNameToServices) @@ -3218,7 +3182,6 @@ func TestParseIngressRules(t *testing.T) { func TestParseKnativeIngressRules(t *testing.T) { assert := assert.New(t) - p := Parser{} ingressList := []*knative.Ingress{ // 0 { @@ -3368,23 +3331,23 @@ func TestParseKnativeIngressRules(t *testing.T) { }, } t.Run("no ingress returns empty info", func(t *testing.T) { - parsedInfo, secretToSNIs := p.parseKnativeIngressRules([]*knative.Ingress{}) - assert.Equal(map[string]Service{}, parsedInfo) - assert.Equal(map[string][]string{}, secretToSNIs) + parsedInfo := parseKnativeIngressRules([]*knative.Ingress{}) + assert.Equal(map[string]kongstate.Service{}, parsedInfo.ServiceNameToServices) + assert.Equal(newSecretNameToSNIs(), parsedInfo.SecretNameToSNIs) }) t.Run("empty ingress returns empty info", func(t *testing.T) { - parsedInfo, secretToSNIs := p.parseKnativeIngressRules([]*knative.Ingress{ + parsedInfo := parseKnativeIngressRules([]*knative.Ingress{ ingressList[0], }) - assert.Equal(map[string]Service{}, parsedInfo) - assert.Equal(map[string][]string{}, secretToSNIs) + assert.Equal(map[string]kongstate.Service{}, parsedInfo.ServiceNameToServices) + assert.Equal(newSecretNameToSNIs(), parsedInfo.SecretNameToSNIs) }) t.Run("basic knative Ingress resource is parsed", func(t *testing.T) { - parsedInfo, secretToSNIs := p.parseKnativeIngressRules([]*knative.Ingress{ + parsedInfo := parseKnativeIngressRules([]*knative.Ingress{ ingressList[1], }) - assert.Equal(1, len(parsedInfo)) - svc := parsedInfo["foo-ns.foo-svc.42"] + assert.Equal(1, len(parsedInfo.ServiceNameToServices)) + svc := parsedInfo.ServiceNameToServices["foo-ns.foo-svc.42"] assert.Equal(kong.Service{ Name: kong.String("foo-ns.foo-svc.42"), Port: kong.Int(80), @@ -3414,24 +3377,24 @@ func TestParseKnativeIngressRules(t *testing.T) { }, }, svc.Plugins[0]) - assert.Equal(map[string][]string{}, secretToSNIs) + assert.Equal(newSecretNameToSNIs(), parsedInfo.SecretNameToSNIs) }) t.Run("knative TLS section is correctly parsed", func(t *testing.T) { - _, secretToSNIs := p.parseKnativeIngressRules([]*knative.Ingress{ + parsedInfo := parseKnativeIngressRules([]*knative.Ingress{ ingressList[3], }) - assert.Equal(map[string][]string{ + assert.Equal(SecretNameToSNIs(map[string][]string{ "foo-namespace/bar-secret": {"bar.example.com", "bar1.example.com"}, "foo-namespace/foo-secret": {"foo.example.com", "foo1.example.com"}, - }, secretToSNIs) + }), parsedInfo.SecretNameToSNIs) }) t.Run("split knative Ingress resource chooses the highest split", func(t *testing.T) { - parsedInfo, secretToSNIs := p.parseKnativeIngressRules([]*knative.Ingress{ + parsedInfo := parseKnativeIngressRules([]*knative.Ingress{ ingressList[2], }) - assert.Equal(1, len(parsedInfo)) - svc := parsedInfo["foo-ns.foo-svc.42"] + assert.Equal(1, len(parsedInfo.ServiceNameToServices)) + svc := parsedInfo.ServiceNameToServices["foo-ns.foo-svc.42"] assert.Equal(kong.Service{ Name: kong.String("foo-ns.foo-svc.42"), Port: kong.Int(80), @@ -3461,2447 +3424,384 @@ func TestParseKnativeIngressRules(t *testing.T) { }, }, svc.Plugins[0]) - assert.Equal(map[string][]string{}, secretToSNIs) + assert.Equal(newSecretNameToSNIs(), parsedInfo.SecretNameToSNIs) }) } -func TestOverrideService(t *testing.T) { - assert := assert.New(t) - - testTable := []struct { - inService Service - inKongIngresss configurationv1.KongIngress - outService Service - inAnnotation map[string]string +func TestGetEndpoints(t *testing.T) { + tests := []struct { + name string + svc *corev1.Service + port *corev1.ServicePort + proto corev1.Protocol + fn func(string, string) (*corev1.Endpoints, error) + result []utils.Endpoint }{ { - Service{ - Service: kong.Service{ - Host: kong.String("foo.com"), - Port: kong.Int(80), - Name: kong.String("foo"), - Protocol: kong.String("http"), - Path: kong.String("/"), - }, - }, - configurationv1.KongIngress{ - Proxy: &kong.Service{}, - }, - Service{ - Service: kong.Service{ - Host: kong.String("foo.com"), - Port: kong.Int(80), - Name: kong.String("foo"), - Protocol: kong.String("http"), - Path: kong.String("/"), - }, + "no service should return 0 endpoints", + nil, + nil, + corev1.ProtocolTCP, + func(string, string) (*corev1.Endpoints, error) { + return nil, nil }, - map[string]string{}, + []utils.Endpoint{}, }, { - Service{ - Service: kong.Service{ - Host: kong.String("foo.com"), - Port: kong.Int(80), - Name: kong.String("foo"), - Protocol: kong.String("http"), - Path: kong.String("/"), - }, - }, - configurationv1.KongIngress{ - Proxy: &kong.Service{ - Protocol: kong.String("https"), - }, - }, - Service{ - Service: kong.Service{ - Host: kong.String("foo.com"), - Port: kong.Int(80), - Name: kong.String("foo"), - Protocol: kong.String("https"), - Path: kong.String("/"), - }, + "no service port should return 0 endpoints", + &corev1.Service{}, + nil, + corev1.ProtocolTCP, + func(string, string) (*corev1.Endpoints, error) { + return nil, nil }, - map[string]string{}, + []utils.Endpoint{}, }, { - Service{ - Service: kong.Service{ - Host: kong.String("foo.com"), - Port: kong.Int(80), - Name: kong.String("foo"), - Protocol: kong.String("http"), - Path: kong.String("/"), - }, - }, - configurationv1.KongIngress{ - Proxy: &kong.Service{ - Retries: kong.Int(0), - }, - }, - Service{ - Service: kong.Service{ - Host: kong.String("foo.com"), - Port: kong.Int(80), - Name: kong.String("foo"), - Protocol: kong.String("http"), - Path: kong.String("/"), - Retries: kong.Int(0), - }, + "a service without endpoints should return 0 endpoints", + &corev1.Service{}, + &corev1.ServicePort{Name: "default"}, + corev1.ProtocolTCP, + func(string, string) (*corev1.Endpoints, error) { + return &corev1.Endpoints{}, nil }, - map[string]string{}, + []utils.Endpoint{}, }, { - Service{ - Service: kong.Service{ - Host: kong.String("foo.com"), - Port: kong.Int(80), - Name: kong.String("foo"), - Protocol: kong.String("http"), - Path: kong.String("/"), - }, - }, - configurationv1.KongIngress{ - Proxy: &kong.Service{ - Path: kong.String("/new-path"), + "a service type ServiceTypeExternalName service with an invalid port should return 0 endpoints", + &corev1.Service{ + Spec: corev1.ServiceSpec{ + Type: corev1.ServiceTypeExternalName, }, }, - Service{ - Service: kong.Service{ - Host: kong.String("foo.com"), - Port: kong.Int(80), - Name: kong.String("foo"), - Protocol: kong.String("http"), - Path: kong.String("/new-path"), - }, + &corev1.ServicePort{Name: "default"}, + corev1.ProtocolTCP, + func(string, string) (*corev1.Endpoints, error) { + return &corev1.Endpoints{}, nil }, - map[string]string{}, + []utils.Endpoint{}, }, { - Service{ - Service: kong.Service{ - Host: kong.String("foo.com"), - Port: kong.Int(80), - Name: kong.String("foo"), - Protocol: kong.String("http"), - Path: kong.String("/"), - }, - }, - configurationv1.KongIngress{ - Proxy: &kong.Service{ - Retries: kong.Int(1), - }, - }, - Service{ - Service: kong.Service{ - Host: kong.String("foo.com"), - Port: kong.Int(80), - Name: kong.String("foo"), - Protocol: kong.String("http"), - Path: kong.String("/"), - Retries: kong.Int(1), + "a service type ServiceTypeExternalName with a valid port should return one endpoint", + &corev1.Service{ + Spec: corev1.ServiceSpec{ + Type: corev1.ServiceTypeExternalName, + ExternalName: "10.0.0.1.xip.io", + Ports: []corev1.ServicePort{ + { + Name: "default", + TargetPort: intstr.FromInt(80), + }, + }, }, }, - map[string]string{}, - }, - { - Service{ - Service: kong.Service{ - Host: kong.String("foo.com"), - Port: kong.Int(80), - Name: kong.String("foo"), - Protocol: kong.String("http"), - Path: kong.String("/"), - }, - }, - configurationv1.KongIngress{ - Proxy: &kong.Service{ - ConnectTimeout: kong.Int(100), - ReadTimeout: kong.Int(100), - WriteTimeout: kong.Int(100), - }, - }, - Service{ - Service: kong.Service{ - Host: kong.String("foo.com"), - Port: kong.Int(80), - Name: kong.String("foo"), - Protocol: kong.String("http"), - Path: kong.String("/"), - ConnectTimeout: kong.Int(100), - ReadTimeout: kong.Int(100), - WriteTimeout: kong.Int(100), - }, - }, - map[string]string{}, - }, - { - Service{ - Service: kong.Service{ - Host: kong.String("foo.com"), - Port: kong.Int(80), - Name: kong.String("foo"), - Protocol: kong.String("grpc"), - Path: nil, - }, + &corev1.ServicePort{ + Name: "default", + TargetPort: intstr.FromInt(80), }, - configurationv1.KongIngress{ - Proxy: &kong.Service{ - Protocol: kong.String("grpc"), - }, + corev1.ProtocolTCP, + func(string, string) (*corev1.Endpoints, error) { + return &corev1.Endpoints{}, nil }, - Service{ - Service: kong.Service{ - Host: kong.String("foo.com"), - Port: kong.Int(80), - Name: kong.String("foo"), - Protocol: kong.String("grpc"), - Path: nil, + []utils.Endpoint{ + { + Address: "10.0.0.1.xip.io", + Port: "80", }, }, - map[string]string{}, }, { - Service{ - Service: kong.Service{ - Host: kong.String("foo.com"), - Port: kong.Int(80), - Name: kong.String("foo"), - Protocol: kong.String("https"), - Path: nil, - }, - }, - configurationv1.KongIngress{ - Proxy: &kong.Service{ - Protocol: kong.String("grpcs"), + "a service with ingress.kubernetes.io/service-upstream annotation should return one endpoint", + &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + Namespace: "bar", + Annotations: map[string]string{ + "ingress.kubernetes.io/service-upstream": "true", + }, }, - }, - Service{ - Service: kong.Service{ - Host: kong.String("foo.com"), - Port: kong.Int(80), - Name: kong.String("foo"), - Protocol: kong.String("grpcs"), - Path: nil, + Spec: corev1.ServiceSpec{ + Type: corev1.ServiceTypeClusterIP, + Ports: []corev1.ServicePort{ + { + Name: "default", + TargetPort: intstr.FromInt(80), + }, + }, }, }, - map[string]string{}, - }, - { - Service{ - Service: kong.Service{ - Host: kong.String("foo.com"), - Port: kong.Int(80), - Name: kong.String("foo"), - Protocol: kong.String("https"), - Path: kong.String("/"), - }, + &corev1.ServicePort{ + Name: "default", + TargetPort: intstr.FromInt(2080), }, - configurationv1.KongIngress{ - Proxy: &kong.Service{ - Protocol: kong.String("grpcs"), - }, + corev1.ProtocolTCP, + func(string, string) (*corev1.Endpoints, error) { + return &corev1.Endpoints{}, nil }, - Service{ - Service: kong.Service{ - Host: kong.String("foo.com"), - Port: kong.Int(80), - Name: kong.String("foo"), - Protocol: kong.String("grpcs"), - Path: nil, + []utils.Endpoint{ + { + Address: "foo.bar.svc", + Port: "2080", }, }, - map[string]string{"configuration.konghq.com/protocol": "grpcs"}, }, { - Service{ - Service: kong.Service{ - Host: kong.String("foo.com"), - Port: kong.Int(80), - Name: kong.String("foo"), - Protocol: kong.String("https"), - Path: kong.String("/"), + "should return no endpoints when there is an error searching for endpoints", + &corev1.Service{ + Spec: corev1.ServiceSpec{ + Type: corev1.ServiceTypeClusterIP, + ClusterIP: "1.1.1.1", + Ports: []corev1.ServicePort{ + { + Name: "default", + TargetPort: intstr.FromInt(80), + }, + }, }, }, - configurationv1.KongIngress{ - Proxy: &kong.Service{ - Protocol: kong.String("grpcs"), - }, + &corev1.ServicePort{ + Name: "default", + TargetPort: intstr.FromInt(80), }, - Service{ - Service: kong.Service{ - Host: kong.String("foo.com"), - Port: kong.Int(80), - Name: kong.String("foo"), - Protocol: kong.String("grpc"), - Path: nil, - }, + corev1.ProtocolTCP, + func(string, string) (*corev1.Endpoints, error) { + return nil, fmt.Errorf("unexpected error") }, - map[string]string{"configuration.konghq.com/protocol": "grpc"}, + []utils.Endpoint{}, }, { - Service{ - Service: kong.Service{ - Host: kong.String("foo.com"), - Port: kong.Int(80), - Name: kong.String("foo"), - Protocol: kong.String("https"), - Path: kong.String("/"), + "should return no endpoints when the protocol does not match", + &corev1.Service{ + Spec: corev1.ServiceSpec{ + Type: corev1.ServiceTypeClusterIP, + ClusterIP: "1.1.1.1", + Ports: []corev1.ServicePort{ + { + Name: "default", + TargetPort: intstr.FromInt(80), + }, + }, }, }, - configurationv1.KongIngress{ - Proxy: &kong.Service{}, + &corev1.ServicePort{ + Name: "default", + TargetPort: intstr.FromInt(80), }, - Service{ - Service: kong.Service{ - Host: kong.String("foo.com"), - Port: kong.Int(80), - Name: kong.String("foo"), - Protocol: kong.String("grpcs"), - Path: nil, - }, + corev1.ProtocolTCP, + func(string, string) (*corev1.Endpoints, error) { + nodeName := "dummy" + return &corev1.Endpoints{ + Subsets: []corev1.EndpointSubset{ + { + Addresses: []corev1.EndpointAddress{ + { + IP: "1.1.1.1", + NodeName: &nodeName, + }, + }, + Ports: []corev1.EndpointPort{ + { + Protocol: corev1.ProtocolUDP, + }, + }, + }, + }, + }, nil }, - map[string]string{"configuration.konghq.com/protocol": "grpcs"}, + []utils.Endpoint{}, }, { - Service{ - Service: kong.Service{ - Host: kong.String("foo.com"), - Port: kong.Int(80), - Name: kong.String("foo"), - Protocol: kong.String("https"), - Path: kong.String("/"), + "should return no endpoints when there is no ready Addresses", + &corev1.Service{ + Spec: corev1.ServiceSpec{ + Type: corev1.ServiceTypeClusterIP, + ClusterIP: "1.1.1.1", + Ports: []corev1.ServicePort{ + { + Name: "default", + TargetPort: intstr.FromInt(80), + }, + }, }, }, - configurationv1.KongIngress{ - Proxy: &kong.Service{ - Protocol: kong.String("grpcs"), - }, + &corev1.ServicePort{ + Name: "default", + TargetPort: intstr.FromInt(80), }, - Service{ - Service: kong.Service{ - Host: kong.String("foo.com"), - Port: kong.Int(80), - Name: kong.String("foo"), - Protocol: kong.String("https"), - Path: kong.String("/"), - }, + corev1.ProtocolTCP, + func(string, string) (*corev1.Endpoints, error) { + nodeName := "dummy" + return &corev1.Endpoints{ + Subsets: []corev1.EndpointSubset{ + { + NotReadyAddresses: []corev1.EndpointAddress{ + { + IP: "1.1.1.1", + NodeName: &nodeName, + }, + }, + Ports: []corev1.EndpointPort{ + { + Protocol: corev1.ProtocolUDP, + }, + }, + }, + }, + }, nil }, - map[string]string{"configuration.konghq.com/protocol": "https"}, + []utils.Endpoint{}, }, { - Service{ - Service: kong.Service{ - Host: kong.String("foo.com"), - Port: kong.Int(80), - Name: kong.String("foo"), - Protocol: kong.String("https"), - Path: kong.String("/"), + "should return no endpoints when the name of the port name do not match any port in the endpoint Subsets", + &corev1.Service{ + Spec: corev1.ServiceSpec{ + Type: corev1.ServiceTypeClusterIP, + ClusterIP: "1.1.1.1", + Ports: []corev1.ServicePort{ + { + Name: "default", + TargetPort: intstr.FromInt(80), + }, + }, }, }, - configurationv1.KongIngress{ - Proxy: &kong.Service{}, + &corev1.ServicePort{ + Name: "default", + TargetPort: intstr.FromInt(80), }, - Service{ - Service: kong.Service{ - Host: kong.String("foo.com"), - Port: kong.Int(80), - Name: kong.String("foo"), - Protocol: kong.String("https"), - Path: kong.String("/"), - }, - }, - map[string]string{"configuration.konghq.com/protocol": "https"}, - }, - } - - for _, testcase := range testTable { - overrideService(&testcase.inService, &testcase.inKongIngresss, testcase.inAnnotation) - assert.Equal(testcase.inService, testcase.outService) - } - - assert.NotPanics(func() { - overrideService(nil, nil, nil) - }) -} - -func TestOverrideRoute(t *testing.T) { - assert := assert.New(t) - - testTable := []struct { - inRoute Route - inKongIngresss configurationv1.KongIngress - outRoute Route - }{ - { - Route{ - Route: kong.Route{ - Hosts: kong.StringSlice("foo.com", "bar.com"), - }, - }, - configurationv1.KongIngress{}, - Route{ - Route: kong.Route{ - Hosts: kong.StringSlice("foo.com", "bar.com"), - }, - }, - }, - { - Route{ - Route: kong.Route{ - Hosts: kong.StringSlice("foo.com", "bar.com"), - }, - }, - configurationv1.KongIngress{ - Route: &kong.Route{ - Methods: kong.StringSlice("GET", "POST"), - }, - }, - Route{ - Route: kong.Route{ - Hosts: kong.StringSlice("foo.com", "bar.com"), - Methods: kong.StringSlice("GET", "POST"), - }, - }, - }, - { - Route{ - Route: kong.Route{ - Hosts: kong.StringSlice("foo.com", "bar.com"), - }, - }, - configurationv1.KongIngress{ - Route: &kong.Route{ - Methods: kong.StringSlice("GET ", "post"), - }, - }, - Route{ - Route: kong.Route{ - Hosts: kong.StringSlice("foo.com", "bar.com"), - Methods: kong.StringSlice("GET", "POST"), - }, - }, - }, - { - Route{ - Route: kong.Route{ - Hosts: kong.StringSlice("foo.com", "bar.com"), - }, - }, - configurationv1.KongIngress{ - Route: &kong.Route{ - Methods: kong.StringSlice("GET", "-1"), - }, - }, - Route{ - Route: kong.Route{ - Hosts: kong.StringSlice("foo.com", "bar.com"), - }, - }, - }, - { - Route{ - Route: kong.Route{ - Hosts: kong.StringSlice("foo.com", "bar.com"), - }, - }, - configurationv1.KongIngress{ - Route: &kong.Route{ - HTTPSRedirectStatusCode: kong.Int(302), - }, - }, - Route{ - Route: kong.Route{ - Hosts: kong.StringSlice("foo.com", "bar.com"), - HTTPSRedirectStatusCode: kong.Int(302), - }, - }, - }, - { - Route{ - Route: kong.Route{ - Hosts: kong.StringSlice("foo.com", "bar.com"), - PreserveHost: kong.Bool(true), - StripPath: kong.Bool(true), - }, - }, - configurationv1.KongIngress{ - Route: &kong.Route{ - Protocols: kong.StringSlice("http"), - PreserveHost: kong.Bool(false), - StripPath: kong.Bool(false), - RegexPriority: kong.Int(10), - }, - }, - Route{ - Route: kong.Route{ - Hosts: kong.StringSlice("foo.com", "bar.com"), - Protocols: kong.StringSlice("http"), - PreserveHost: kong.Bool(false), - StripPath: kong.Bool(false), - RegexPriority: kong.Int(10), - }, + corev1.ProtocolTCP, + func(string, string) (*corev1.Endpoints, error) { + nodeName := "dummy" + return &corev1.Endpoints{ + Subsets: []corev1.EndpointSubset{ + { + Addresses: []corev1.EndpointAddress{ + { + IP: "1.1.1.1", + NodeName: &nodeName, + }, + }, + Ports: []corev1.EndpointPort{ + { + Protocol: corev1.ProtocolTCP, + Port: int32(80), + Name: "another-name", + }, + }, + }, + }, + }, nil }, + []utils.Endpoint{}, }, { - Route{ - Route: kong.Route{ - Hosts: kong.StringSlice("foo.com"), - Protocols: kong.StringSlice("http", "https"), - }, - }, - configurationv1.KongIngress{ - Route: &kong.Route{ - Headers: map[string][]string{ - "foo-header": {"bar-value"}, - }, - }, - }, - Route{ - Route: kong.Route{ - Hosts: kong.StringSlice("foo.com"), - Protocols: kong.StringSlice("http", "https"), - Headers: map[string][]string{ - "foo-header": {"bar-value"}, + "should return one endpoint when the name of the port name match a port in the endpoint Subsets", + &corev1.Service{ + Spec: corev1.ServiceSpec{ + Type: corev1.ServiceTypeClusterIP, + ClusterIP: "1.1.1.1", + Ports: []corev1.ServicePort{ + { + Name: "default", + TargetPort: intstr.FromInt(80), + }, }, }, }, - }, - { - Route{ - Route: kong.Route{ - Hosts: kong.StringSlice("foo.com"), - }, - }, - configurationv1.KongIngress{ - Route: &kong.Route{ - Protocols: kong.StringSlice("grpc", "grpcs"), - }, - }, - Route{ - Route: kong.Route{ - Hosts: kong.StringSlice("foo.com"), - Protocols: kong.StringSlice("grpc", "grpcs"), - StripPath: nil, - }, - }, - }, - { - Route{ - Route: kong.Route{ - Hosts: kong.StringSlice("foo.com"), - }, - }, - configurationv1.KongIngress{ - Route: &kong.Route{ - PathHandling: kong.String("v1"), - }, - }, - Route{ - Route: kong.Route{ - Hosts: kong.StringSlice("foo.com"), - PathHandling: kong.String("v1"), - }, - }, - }, - } - - for _, testcase := range testTable { - p := Parser{ - Logger: logrus.New(), - } - p.overrideRoute(&testcase.inRoute, &testcase.inKongIngresss) - assert.Equal(testcase.inRoute, testcase.outRoute) - } - - assert.NotPanics(func() { - var p Parser - p.overrideRoute(nil, nil) - }) -} - -func TestOverrideRoutePriority(t *testing.T) { - assert := assert.New(t) - var route Route - route = Route{ - Route: kong.Route{ - Hosts: kong.StringSlice("foo.com", "bar.com"), - }, - } - kongIngress := configurationv1.KongIngress{ - Route: &kong.Route{ - Hosts: kong.StringSlice("foo.com", "bar.com"), - }, - } - - netIngress := networking.Ingress{ - ObjectMeta: metav1.ObjectMeta{ - Annotations: map[string]string{ - "configuration.konghq.com/protocols": "grpc,grpcs", - }, - }, - } - - route = Route{ - Route: kong.Route{ - Hosts: kong.StringSlice("foo.com", "bar.com"), - }, - Ingress: netIngress, - } - var p Parser - p.overrideRoute(&route, &kongIngress) - assert.Equal(route.Hosts, kong.StringSlice("foo.com", "bar.com")) - assert.Equal(route.Protocols, kong.StringSlice("grpc", "grpcs")) -} - -func TestOverrideRouteByKongIngress(t *testing.T) { - assert := assert.New(t) - route := Route{ - Route: kong.Route{ - Hosts: kong.StringSlice("foo.com", "bar.com"), - }, - } - kongIngress := configurationv1.KongIngress{ - Route: &kong.Route{ - Hosts: kong.StringSlice("foo.com", "bar.com"), - }, - } - - var p Parser - p.overrideRouteByKongIngress(&route, &kongIngress) - assert.Equal(route.Hosts, kong.StringSlice("foo.com", "bar.com")) - assert.NotPanics(func() { - p.overrideRoute(nil, nil) - }) -} -func TestOverrideRouteByAnnotation(t *testing.T) { - assert := assert.New(t) - var route Route - route = Route{ - Route: kong.Route{ - Hosts: kong.StringSlice("foo.com", "bar.com"), - }, - } - - netIngress := networking.Ingress{ - ObjectMeta: metav1.ObjectMeta{ - Annotations: map[string]string{ - "configuration.konghq.com/protocols": "grpc,grpcs", + &corev1.ServicePort{ + Name: "default", + TargetPort: intstr.FromInt(80), }, - }, - } - - route = Route{ - Route: kong.Route{ - Hosts: kong.StringSlice("foo.com", "bar.com"), - }, - Ingress: netIngress, - } - var p Parser - p.overrideRouteByAnnotation(&route) - assert.Equal(route.Hosts, kong.StringSlice("foo.com", "bar.com")) - assert.Equal(route.Protocols, kong.StringSlice("grpc", "grpcs")) - - assert.NotPanics(func() { - p.overrideRoute(nil, nil) - }) -} - -func TestNormalizeProtocols(t *testing.T) { - assert := assert.New(t) - testTable := []struct { - inRoute Route - outRoute Route - }{ - { - Route{ - Route: kong.Route{ - Protocols: kong.StringSlice("grpc", "grpcs"), - }, + corev1.ProtocolTCP, + func(string, string) (*corev1.Endpoints, error) { + nodeName := "dummy" + return &corev1.Endpoints{ + Subsets: []corev1.EndpointSubset{ + { + Addresses: []corev1.EndpointAddress{ + { + IP: "1.1.1.1", + NodeName: &nodeName, + }, + }, + Ports: []corev1.EndpointPort{ + { + Protocol: corev1.ProtocolTCP, + Port: int32(80), + Name: "default", + }, + }, + }, + }, + }, nil }, - Route{ - Route: kong.Route{ - Protocols: kong.StringSlice("grpc", "grpcs"), + []utils.Endpoint{ + { + Address: "1.1.1.1", + Port: "80", }, }, }, { - Route{ - Route: kong.Route{ - Protocols: kong.StringSlice("http", "https"), + "should return one endpoint when the name of the port name match more than one port in the endpoint Subsets", + &corev1.Service{ + Spec: corev1.ServiceSpec{ + Type: corev1.ServiceTypeClusterIP, + ClusterIP: "1.1.1.1", + Ports: []corev1.ServicePort{ + { + Name: "default", + TargetPort: intstr.FromString("port-1"), + }, + }, }, }, - Route{ - Route: kong.Route{ - Protocols: kong.StringSlice("http", "https"), - }, + &corev1.ServicePort{ + Name: "port-1", + TargetPort: intstr.FromString("port-1"), }, - }, - { - Route{ - Route: kong.Route{ - Protocols: kong.StringSlice("grpc", "https"), - }, + corev1.ProtocolTCP, + func(string, string) (*corev1.Endpoints, error) { + nodeName := "dummy" + return &corev1.Endpoints{ + Subsets: []corev1.EndpointSubset{ + { + Addresses: []corev1.EndpointAddress{ + { + IP: "1.1.1.1", + NodeName: &nodeName, + }, + }, + Ports: []corev1.EndpointPort{ + { + Name: "port-1", + Protocol: corev1.ProtocolTCP, + Port: 80, + }, + { + Name: "port-1", + Protocol: corev1.ProtocolTCP, + Port: 80, + }, + }, + }, + }, + }, nil }, - Route{ - Route: kong.Route{ - Protocols: kong.StringSlice("http", "https"), + []utils.Endpoint{ + { + Address: "1.1.1.1", + Port: "80", }, }, }, } - for _, testcase := range testTable { - normalizeProtocols(&testcase.inRoute) - assert.Equal(testcase.inRoute.Protocols, testcase.outRoute.Protocols) - } - - assert.NotPanics(func() { - overrideUpstream(nil, nil, make(map[string]string)) - }) -} - -func TestValidateProtocol(t *testing.T) { - assert := assert.New(t) - testTable := []struct { - input string - result bool - }{ - {"http", true}, - {"https", true}, - {"grpc", true}, - {"grpcs", true}, - {"grcpsfdsafdsfafdshttp", false}, - } - for _, testcase := range testTable { - isMatch := validateProtocol(testcase.input) - assert.Equal(isMatch, testcase.result) - } - - assert.NotPanics(func() { - overrideUpstream(nil, nil, make(map[string]string)) - }) -} -func TestUseSSLProtocol(t *testing.T) { - assert := assert.New(t) - testTable := []struct { - inRoute kong.Route - outRoute kong.Route - }{ - { - kong.Route{ - Protocols: kong.StringSlice("grpc", "grpcs"), - }, - kong.Route{ - Protocols: kong.StringSlice("grpcs"), - }, - }, - { - kong.Route{ - Protocols: kong.StringSlice("http", "https"), - }, - kong.Route{ - Protocols: kong.StringSlice("https"), - }, - }, - { - kong.Route{ - Protocols: kong.StringSlice("grpcs", "https"), - }, - - kong.Route{ - Protocols: kong.StringSlice("grpcs", "https"), - }, - }, - { - kong.Route{ - Protocols: kong.StringSlice("grpc", "http"), - }, - kong.Route{ - Protocols: kong.StringSlice("grpcs", "https"), - }, - }, - { - kong.Route{ - Protocols: []*string{}, - }, - kong.Route{ - Protocols: kong.StringSlice("https"), - }, - }, - } - - for _, testcase := range testTable { - useSSLProtocol(&testcase.inRoute) - assert.Equal(testcase.inRoute.Protocols, testcase.outRoute.Protocols) - } -} - -func TestOverrideUpstream(t *testing.T) { - assert := assert.New(t) - - testTable := []struct { - inUpstream Upstream - inKongIngresss configurationv1.KongIngress - outUpstream Upstream - }{ - { - Upstream{ - Upstream: kong.Upstream{ - Name: kong.String("foo.com"), - }, - }, - configurationv1.KongIngress{ - Upstream: &kong.Upstream{}, - }, - Upstream{ - Upstream: kong.Upstream{ - Name: kong.String("foo.com"), - }, - }, - }, - { - Upstream{ - Upstream: kong.Upstream{ - Name: kong.String("foo.com"), - }, - }, - configurationv1.KongIngress{ - Upstream: &kong.Upstream{ - Name: kong.String("wrong.com"), - HashOn: kong.String("HashOn"), - HashOnCookie: kong.String("HashOnCookie"), - HashOnCookiePath: kong.String("HashOnCookiePath"), - HashOnHeader: kong.String("HashOnHeader"), - HashFallback: kong.String("HashFallback"), - HashFallbackHeader: kong.String("HashFallbackHeader"), - Slots: kong.Int(42), - }, - }, - Upstream{ - Upstream: kong.Upstream{ - Name: kong.String("foo.com"), - HashOn: kong.String("HashOn"), - HashOnCookie: kong.String("HashOnCookie"), - HashOnCookiePath: kong.String("HashOnCookiePath"), - HashOnHeader: kong.String("HashOnHeader"), - HashFallback: kong.String("HashFallback"), - HashFallbackHeader: kong.String("HashFallbackHeader"), - Slots: kong.Int(42), - }, - }, - }, - } - - for _, testcase := range testTable { - overrideUpstream(&testcase.inUpstream, &testcase.inKongIngresss, make(map[string]string)) - assert.Equal(testcase.inUpstream, testcase.outUpstream) - } - - assert.NotPanics(func() { - overrideUpstream(nil, nil, make(map[string]string)) - }) -} - -func TestGetEndpoints(t *testing.T) { - tests := []struct { - name string - svc *corev1.Service - port *corev1.ServicePort - proto corev1.Protocol - fn func(string, string) (*corev1.Endpoints, error) - result []utils.Endpoint - }{ - { - "no service should return 0 endpoints", - nil, - nil, - corev1.ProtocolTCP, - func(string, string) (*corev1.Endpoints, error) { - return nil, nil - }, - []utils.Endpoint{}, - }, - { - "no service port should return 0 endpoints", - &corev1.Service{}, - nil, - corev1.ProtocolTCP, - func(string, string) (*corev1.Endpoints, error) { - return nil, nil - }, - []utils.Endpoint{}, - }, - { - "a service without endpoints should return 0 endpoints", - &corev1.Service{}, - &corev1.ServicePort{Name: "default"}, - corev1.ProtocolTCP, - func(string, string) (*corev1.Endpoints, error) { - return &corev1.Endpoints{}, nil - }, - []utils.Endpoint{}, - }, - { - "a service type ServiceTypeExternalName service with an invalid port should return 0 endpoints", - &corev1.Service{ - Spec: corev1.ServiceSpec{ - Type: corev1.ServiceTypeExternalName, - }, - }, - &corev1.ServicePort{Name: "default"}, - corev1.ProtocolTCP, - func(string, string) (*corev1.Endpoints, error) { - return &corev1.Endpoints{}, nil - }, - []utils.Endpoint{}, - }, - { - "a service type ServiceTypeExternalName with a valid port should return one endpoint", - &corev1.Service{ - Spec: corev1.ServiceSpec{ - Type: corev1.ServiceTypeExternalName, - ExternalName: "10.0.0.1.xip.io", - Ports: []corev1.ServicePort{ - { - Name: "default", - TargetPort: intstr.FromInt(80), - }, - }, - }, - }, - &corev1.ServicePort{ - Name: "default", - TargetPort: intstr.FromInt(80), - }, - corev1.ProtocolTCP, - func(string, string) (*corev1.Endpoints, error) { - return &corev1.Endpoints{}, nil - }, - []utils.Endpoint{ - { - Address: "10.0.0.1.xip.io", - Port: "80", - }, - }, - }, - { - "a service with ingress.kubernetes.io/service-upstream annotation should return one endpoint", - &corev1.Service{ - ObjectMeta: metav1.ObjectMeta{ - Name: "foo", - Namespace: "bar", - Annotations: map[string]string{ - "ingress.kubernetes.io/service-upstream": "true", - }, - }, - Spec: corev1.ServiceSpec{ - Type: corev1.ServiceTypeClusterIP, - Ports: []corev1.ServicePort{ - { - Name: "default", - TargetPort: intstr.FromInt(80), - }, - }, - }, - }, - &corev1.ServicePort{ - Name: "default", - TargetPort: intstr.FromInt(2080), - }, - corev1.ProtocolTCP, - func(string, string) (*corev1.Endpoints, error) { - return &corev1.Endpoints{}, nil - }, - []utils.Endpoint{ - { - Address: "foo.bar.svc", - Port: "2080", - }, - }, - }, - { - "should return no endpoints when there is an error searching for endpoints", - &corev1.Service{ - Spec: corev1.ServiceSpec{ - Type: corev1.ServiceTypeClusterIP, - ClusterIP: "1.1.1.1", - Ports: []corev1.ServicePort{ - { - Name: "default", - TargetPort: intstr.FromInt(80), - }, - }, - }, - }, - &corev1.ServicePort{ - Name: "default", - TargetPort: intstr.FromInt(80), - }, - corev1.ProtocolTCP, - func(string, string) (*corev1.Endpoints, error) { - return nil, fmt.Errorf("unexpected error") - }, - []utils.Endpoint{}, - }, - { - "should return no endpoints when the protocol does not match", - &corev1.Service{ - Spec: corev1.ServiceSpec{ - Type: corev1.ServiceTypeClusterIP, - ClusterIP: "1.1.1.1", - Ports: []corev1.ServicePort{ - { - Name: "default", - TargetPort: intstr.FromInt(80), - }, - }, - }, - }, - &corev1.ServicePort{ - Name: "default", - TargetPort: intstr.FromInt(80), - }, - corev1.ProtocolTCP, - func(string, string) (*corev1.Endpoints, error) { - nodeName := "dummy" - return &corev1.Endpoints{ - Subsets: []corev1.EndpointSubset{ - { - Addresses: []corev1.EndpointAddress{ - { - IP: "1.1.1.1", - NodeName: &nodeName, - }, - }, - Ports: []corev1.EndpointPort{ - { - Protocol: corev1.ProtocolUDP, - }, - }, - }, - }, - }, nil - }, - []utils.Endpoint{}, - }, - { - "should return no endpoints when there is no ready Addresses", - &corev1.Service{ - Spec: corev1.ServiceSpec{ - Type: corev1.ServiceTypeClusterIP, - ClusterIP: "1.1.1.1", - Ports: []corev1.ServicePort{ - { - Name: "default", - TargetPort: intstr.FromInt(80), - }, - }, - }, - }, - &corev1.ServicePort{ - Name: "default", - TargetPort: intstr.FromInt(80), - }, - corev1.ProtocolTCP, - func(string, string) (*corev1.Endpoints, error) { - nodeName := "dummy" - return &corev1.Endpoints{ - Subsets: []corev1.EndpointSubset{ - { - NotReadyAddresses: []corev1.EndpointAddress{ - { - IP: "1.1.1.1", - NodeName: &nodeName, - }, - }, - Ports: []corev1.EndpointPort{ - { - Protocol: corev1.ProtocolUDP, - }, - }, - }, - }, - }, nil - }, - []utils.Endpoint{}, - }, - { - "should return no endpoints when the name of the port name do not match any port in the endpoint Subsets", - &corev1.Service{ - Spec: corev1.ServiceSpec{ - Type: corev1.ServiceTypeClusterIP, - ClusterIP: "1.1.1.1", - Ports: []corev1.ServicePort{ - { - Name: "default", - TargetPort: intstr.FromInt(80), - }, - }, - }, - }, - &corev1.ServicePort{ - Name: "default", - TargetPort: intstr.FromInt(80), - }, - corev1.ProtocolTCP, - func(string, string) (*corev1.Endpoints, error) { - nodeName := "dummy" - return &corev1.Endpoints{ - Subsets: []corev1.EndpointSubset{ - { - Addresses: []corev1.EndpointAddress{ - { - IP: "1.1.1.1", - NodeName: &nodeName, - }, - }, - Ports: []corev1.EndpointPort{ - { - Protocol: corev1.ProtocolTCP, - Port: int32(80), - Name: "another-name", - }, - }, - }, - }, - }, nil - }, - []utils.Endpoint{}, - }, - { - "should return one endpoint when the name of the port name match a port in the endpoint Subsets", - &corev1.Service{ - Spec: corev1.ServiceSpec{ - Type: corev1.ServiceTypeClusterIP, - ClusterIP: "1.1.1.1", - Ports: []corev1.ServicePort{ - { - Name: "default", - TargetPort: intstr.FromInt(80), - }, - }, - }, - }, - &corev1.ServicePort{ - Name: "default", - TargetPort: intstr.FromInt(80), - }, - corev1.ProtocolTCP, - func(string, string) (*corev1.Endpoints, error) { - nodeName := "dummy" - return &corev1.Endpoints{ - Subsets: []corev1.EndpointSubset{ - { - Addresses: []corev1.EndpointAddress{ - { - IP: "1.1.1.1", - NodeName: &nodeName, - }, - }, - Ports: []corev1.EndpointPort{ - { - Protocol: corev1.ProtocolTCP, - Port: int32(80), - Name: "default", - }, - }, - }, - }, - }, nil - }, - []utils.Endpoint{ - { - Address: "1.1.1.1", - Port: "80", - }, - }, - }, - { - "should return one endpoint when the name of the port name match more than one port in the endpoint Subsets", - &corev1.Service{ - Spec: corev1.ServiceSpec{ - Type: corev1.ServiceTypeClusterIP, - ClusterIP: "1.1.1.1", - Ports: []corev1.ServicePort{ - { - Name: "default", - TargetPort: intstr.FromString("port-1"), - }, - }, - }, - }, - &corev1.ServicePort{ - Name: "port-1", - TargetPort: intstr.FromString("port-1"), - }, - corev1.ProtocolTCP, - func(string, string) (*corev1.Endpoints, error) { - nodeName := "dummy" - return &corev1.Endpoints{ - Subsets: []corev1.EndpointSubset{ - { - Addresses: []corev1.EndpointAddress{ - { - IP: "1.1.1.1", - NodeName: &nodeName, - }, - }, - Ports: []corev1.EndpointPort{ - { - Name: "port-1", - Protocol: corev1.ProtocolTCP, - Port: 80, - }, - { - Name: "port-1", - Protocol: corev1.ProtocolTCP, - Port: 80, - }, - }, - }, - }, - }, nil - }, - []utils.Endpoint{ - { - Address: "1.1.1.1", - Port: "80", - }, - }, - }, - } - - for _, testCase := range tests { - t.Run(testCase.name, func(t *testing.T) { - p := Parser{ - Logger: logrus.New(), - } - result := p.getEndpoints(testCase.svc, testCase.port, testCase.proto, testCase.fn) - if len(testCase.result) != len(result) { - t.Errorf("expected %v Endpoints but got %v", testCase.result, len(result)) - } - }) - } -} - -func Test_processCredential(t *testing.T) { - type args struct { - credType string - consumer *Consumer - credConfig interface{} - } - tests := []struct { - name string - args args - result *Consumer - wantErr bool - }{ - { - name: "invalid cred type errors", - args: args{ - credType: "invalid-type", - consumer: &Consumer{}, - credConfig: nil, - }, - result: &Consumer{}, - wantErr: true, - }, - { - name: "key-auth", - args: args{ - credType: "key-auth", - consumer: &Consumer{}, - credConfig: map[string]string{"key": "foo"}, - }, - result: &Consumer{ - KeyAuths: []*kong.KeyAuth{ - { - Key: kong.String("foo"), - }, - }, - }, - wantErr: false, - }, - { - name: "keyauth_credential", - args: args{ - credType: "keyauth_credential", - consumer: &Consumer{}, - credConfig: map[string]string{"key": "foo"}, - }, - result: &Consumer{ - KeyAuths: []*kong.KeyAuth{ - { - Key: kong.String("foo"), - }, - }, - }, - wantErr: false, - }, - { - name: "basic-auth", - args: args{ - credType: "basic-auth", - consumer: &Consumer{}, - credConfig: map[string]string{ - "username": "foo", - "password": "bar", - }, - }, - result: &Consumer{ - BasicAuths: []*kong.BasicAuth{ - { - Username: kong.String("foo"), - Password: kong.String("bar"), - }, - }, - }, - wantErr: false, - }, - { - name: "basicauth_credential", - args: args{ - credType: "basicauth_credential", - consumer: &Consumer{}, - credConfig: map[string]string{ - "username": "foo", - "password": "bar", - }, - }, - result: &Consumer{ - BasicAuths: []*kong.BasicAuth{ - { - Username: kong.String("foo"), - Password: kong.String("bar"), - }, - }, - }, - wantErr: false, - }, - { - name: "hmac-auth", - args: args{ - credType: "hmac-auth", - consumer: &Consumer{}, - credConfig: map[string]string{ - "username": "foo", - "secret": "bar", - }, - }, - result: &Consumer{ - HMACAuths: []*kong.HMACAuth{ - { - Username: kong.String("foo"), - Secret: kong.String("bar"), - }, - }, - }, - wantErr: false, - }, - { - name: "hmacauth_credential", - args: args{ - credType: "hmacauth_credential", - consumer: &Consumer{}, - credConfig: map[string]string{ - "username": "foo", - "secret": "bar", - }, - }, - result: &Consumer{ - HMACAuths: []*kong.HMACAuth{ - { - Username: kong.String("foo"), - Secret: kong.String("bar"), - }, - }, - }, - wantErr: false, - }, - { - name: "oauth2", - args: args{ - credType: "oauth2", - consumer: &Consumer{}, - credConfig: map[string]interface{}{ - "name": "foo", - "client_id": "bar", - "client_secret": "baz", - "redirect_uris": []string{"example.com"}, - }, - }, - result: &Consumer{ - Oauth2Creds: []*kong.Oauth2Credential{ - { - Name: kong.String("foo"), - ClientID: kong.String("bar"), - ClientSecret: kong.String("baz"), - RedirectURIs: kong.StringSlice("example.com"), - }, - }, - }, - wantErr: false, - }, - { - name: "jwt", - args: args{ - credType: "jwt", - consumer: &Consumer{}, - credConfig: map[string]string{ - "key": "foo", - "rsa_public_key": "bar", - "secret": "baz", - }, - }, - result: &Consumer{ - JWTAuths: []*kong.JWTAuth{ - { - Key: kong.String("foo"), - RSAPublicKey: kong.String("bar"), - Secret: kong.String("baz"), - // set by default - Algorithm: kong.String("HS256"), - }, - }, - }, - wantErr: false, - }, - { - name: "jwt_secret", - args: args{ - credType: "jwt_secret", - consumer: &Consumer{}, - credConfig: map[string]string{ - "key": "foo", - "rsa_public_key": "bar", - "secret": "baz", - }, - }, - result: &Consumer{ - JWTAuths: []*kong.JWTAuth{ - { - Key: kong.String("foo"), - RSAPublicKey: kong.String("bar"), - Secret: kong.String("baz"), - // set by default - Algorithm: kong.String("HS256"), - }, - }, - }, - wantErr: false, - }, - { - name: "acl", - args: args{ - credType: "acl", - consumer: &Consumer{}, - credConfig: map[string]string{"group": "group-foo"}, - }, - result: &Consumer{ - ACLGroups: []*kong.ACLGroup{ - { - Group: kong.String("group-foo"), - }, - }, - }, - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - var p Parser - if err := p.processCredential(tt.args.credType, tt.args.consumer, - tt.args.credConfig); (err != nil) != tt.wantErr { - t.Errorf("processCredential() error = %v, wantErr %v", - err, tt.wantErr) - } - assert.Equal(t, tt.result, tt.args.consumer) - }) - } -} - -func Test_getPluginRelations(t *testing.T) { - type args struct { - state KongState - } - tests := []struct { - name string - args args - want map[string]foreignRelations - }{ - { - name: "empty state", - want: map[string]foreignRelations{}, - }, - { - name: "single consumer annotation", - args: args{ - state: KongState{ - Consumers: []Consumer{ - { - Consumer: kong.Consumer{ - Username: kong.String("foo-consumer"), - }, - k8sKongConsumer: configurationv1.KongConsumer{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "ns1", - Annotations: map[string]string{ - annotations.DeprecatedPluginsKey: "foo,bar", - }, - }, - }, - }, - }, - }, - }, - want: map[string]foreignRelations{ - "ns1:foo": {Consumer: []string{"foo-consumer"}}, - "ns1:bar": {Consumer: []string{"foo-consumer"}}, - }, - }, - { - name: "single service annotation", - args: args{ - state: KongState{ - Services: []Service{ - { - Service: kong.Service{ - Name: kong.String("foo-service"), - }, - K8sService: corev1.Service{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "ns1", - Annotations: map[string]string{ - annotations.DeprecatedPluginsKey: "foo,bar", - }, - }, - }, - }, - }, - }, - }, - want: map[string]foreignRelations{ - "ns1:foo": {Service: []string{"foo-service"}}, - "ns1:bar": {Service: []string{"foo-service"}}, - }, - }, - { - name: "single Ingress annotation", - args: args{ - state: KongState{ - Services: []Service{ - { - Service: kong.Service{ - Name: kong.String("foo-service"), - }, - Routes: []Route{ - { - Route: kong.Route{ - Name: kong.String("foo-route"), - }, - Ingress: networking.Ingress{ - ObjectMeta: metav1.ObjectMeta{ - Name: "some-ingress", - Namespace: "ns2", - Annotations: map[string]string{ - annotations.DeprecatedPluginsKey: "foo,bar", - }, - }, - }, - }, - }, - }, - }, - }, - }, - want: map[string]foreignRelations{ - "ns2:foo": {Route: []string{"foo-route"}}, - "ns2:bar": {Route: []string{"foo-route"}}, - }, - }, - { - name: "multiple routes with annotation", - args: args{ - state: KongState{ - Services: []Service{ - { - Service: kong.Service{ - Name: kong.String("foo-service"), - }, - Routes: []Route{ - { - Route: kong.Route{ - Name: kong.String("foo-route"), - }, - Ingress: networking.Ingress{ - ObjectMeta: metav1.ObjectMeta{ - Name: "some-ingress", - Namespace: "ns2", - Annotations: map[string]string{ - annotations.DeprecatedPluginsKey: "foo,bar", - }, - }, - }, - }, - { - Route: kong.Route{ - Name: kong.String("bar-route"), - }, - Ingress: networking.Ingress{ - ObjectMeta: metav1.ObjectMeta{ - Name: "some-ingress", - Namespace: "ns2", - Annotations: map[string]string{ - annotations.DeprecatedPluginsKey: "bar,baz", - }, - }, - }, - }, - }, - }, - }, - }, - }, - want: map[string]foreignRelations{ - "ns2:foo": {Route: []string{"foo-route"}}, - "ns2:bar": {Route: []string{"foo-route", "bar-route"}}, - "ns2:baz": {Route: []string{"bar-route"}}, - }, - }, - { - name: "multiple consumers, routes and services", - args: args{ - state: KongState{ - Consumers: []Consumer{ - { - Consumer: kong.Consumer{ - Username: kong.String("foo-consumer"), - }, - k8sKongConsumer: configurationv1.KongConsumer{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "ns1", - Annotations: map[string]string{ - annotations.DeprecatedPluginsKey: "foo,bar", - }, - }, - }, - }, - { - Consumer: kong.Consumer{ - Username: kong.String("foo-consumer"), - }, - k8sKongConsumer: configurationv1.KongConsumer{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "ns2", - Annotations: map[string]string{ - annotations.DeprecatedPluginsKey: "foo,bar", - }, - }, - }, - }, - { - Consumer: kong.Consumer{ - Username: kong.String("bar-consumer"), - }, - k8sKongConsumer: configurationv1.KongConsumer{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "ns1", - Annotations: map[string]string{ - annotations.DeprecatedPluginsKey: "foobar", - }, - }, - }, - }, - }, - Services: []Service{ - { - Service: kong.Service{ - Name: kong.String("foo-service"), - }, - K8sService: corev1.Service{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "ns1", - Annotations: map[string]string{ - annotations.DeprecatedPluginsKey: "foo,bar", - }, - }, - }, - Routes: []Route{ - { - Route: kong.Route{ - Name: kong.String("foo-route"), - }, - Ingress: networking.Ingress{ - ObjectMeta: metav1.ObjectMeta{ - Name: "some-ingress", - Namespace: "ns2", - Annotations: map[string]string{ - annotations.DeprecatedPluginsKey: "foo,bar", - }, - }, - }, - }, - { - Route: kong.Route{ - Name: kong.String("bar-route"), - }, - Ingress: networking.Ingress{ - ObjectMeta: metav1.ObjectMeta{ - Name: "some-ingress", - Namespace: "ns2", - Annotations: map[string]string{ - annotations.DeprecatedPluginsKey: "bar,baz", - }, - }, - }, - }, - }, - }, - }, - }, - }, - want: map[string]foreignRelations{ - "ns1:foo": {Consumer: []string{"foo-consumer"}, Service: []string{"foo-service"}}, - "ns1:bar": {Consumer: []string{"foo-consumer"}, Service: []string{"foo-service"}}, - "ns1:foobar": {Consumer: []string{"bar-consumer"}}, - "ns2:foo": {Consumer: []string{"foo-consumer"}, Route: []string{"foo-route"}}, - "ns2:bar": {Consumer: []string{"foo-consumer"}, Route: []string{"foo-route", "bar-route"}}, - "ns2:baz": {Route: []string{"bar-route"}}, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := getPluginRelations(tt.args.state); !reflect.DeepEqual(got, tt.want) { - t.Errorf("getPluginRelations() = %v, want %v", got, tt.want) - } - }) - } -} - -func Test_getCombinations(t *testing.T) { - type args struct { - relations foreignRelations - } - tests := []struct { - name string - args args - want []rel - }{ - { - name: "empty", - args: args{ - relations: foreignRelations{}, - }, - want: nil, - }, - { - name: "plugins on consumer only", - args: args{ - relations: foreignRelations{ - Consumer: []string{"foo", "bar"}, - }, - }, - want: []rel{ - { - Consumer: "foo", - }, - { - Consumer: "bar", - }, - }, - }, - { - name: "plugins on service only", - args: args{ - relations: foreignRelations{ - Service: []string{"foo", "bar"}, - }, - }, - want: []rel{ - { - Service: "foo", - }, - { - Service: "bar", - }, - }, - }, - { - name: "plugins on routes only", - args: args{ - relations: foreignRelations{ - Route: []string{"foo", "bar"}, - }, - }, - want: []rel{ - { - Route: "foo", - }, - { - Route: "bar", - }, - }, - }, - { - name: "plugins on service and routes only", - args: args{ - relations: foreignRelations{ - Route: []string{"foo", "bar"}, - Service: []string{"foo", "bar"}, - }, - }, - want: []rel{ - { - Service: "foo", - }, - { - Service: "bar", - }, - { - Route: "foo", - }, - { - Route: "bar", - }, - }, - }, - { - name: "plugins on combination of route and consumer", - args: args{ - relations: foreignRelations{ - Route: []string{"foo", "bar"}, - Consumer: []string{"foo", "bar"}, - }, - }, - want: []rel{ - { - Consumer: "foo", - Route: "foo", - }, - { - Consumer: "bar", - Route: "foo", - }, - { - Consumer: "foo", - Route: "bar", - }, - { - Consumer: "bar", - Route: "bar", - }, - }, - }, - { - name: "plugins on combination of service and consumer", - args: args{ - relations: foreignRelations{ - Service: []string{"foo", "bar"}, - Consumer: []string{"foo", "bar"}, - }, - }, - want: []rel{ - { - Consumer: "foo", - Service: "foo", - }, - { - Consumer: "bar", - Service: "foo", - }, - { - Consumer: "foo", - Service: "bar", - }, - { - Consumer: "bar", - Service: "bar", - }, - }, - }, - { - name: "plugins on combination of service,route and consumer", - args: args{ - relations: foreignRelations{ - Consumer: []string{"c1", "c2"}, - Route: []string{"r1", "r2"}, - Service: []string{"s1", "s2"}, - }, - }, - want: []rel{ - { - Consumer: "c1", - Service: "s1", - }, - { - Consumer: "c2", - Service: "s1", - }, - { - Consumer: "c1", - Service: "s2", - }, - { - Consumer: "c2", - Service: "s2", - }, - { - Consumer: "c1", - Route: "r1", - }, - { - Consumer: "c2", - Route: "r1", - }, - { - Consumer: "c1", - Route: "r2", - }, - { - Consumer: "c2", - Route: "r2", - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := getCombinations(tt.args.relations); !reflect.DeepEqual(got, tt.want) { - t.Errorf("getCombinations() = %v, want %v", got, tt.want) - } - }) - } -} - -func Test_processTLSSections(t *testing.T) { - type args struct { - tlsSections []networking.IngressTLS - namespace string - } - tests := []struct { - name string - args args - want map[string][]string - }{ - { - args: args{ - tlsSections: []networking.IngressTLS{ - { - Hosts: []string{ - "1.example.com", - "2.example.com", - }, - SecretName: "sooper-secret", - }, - { - Hosts: []string{ - "3.example.com", - "4.example.com", - }, - SecretName: "sooper-secret2", - }, - }, - namespace: "foo", - }, - want: map[string][]string{ - "foo/sooper-secret": {"1.example.com", "2.example.com"}, - "foo/sooper-secret2": {"3.example.com", "4.example.com"}, - }, - }, - { - args: args{ - tlsSections: []networking.IngressTLS{ - { - Hosts: []string{ - "1.example.com", - }, - SecretName: "sooper-secret", - }, - { - Hosts: []string{ - "3.example.com", - "1.example.com", - "4.example.com", - }, - SecretName: "sooper-secret2", - }, - }, - namespace: "foo", - }, - want: map[string][]string{ - "foo/sooper-secret": {"1.example.com"}, - "foo/sooper-secret2": {"3.example.com", "4.example.com"}, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := map[string][]string{} - processTLSSections(tt.args.tlsSections, tt.args.namespace, got) - if !reflect.DeepEqual(got, tt.want) { - t.Errorf("processTLSSections() = %v, want %v", got, tt.want) - } - }) - } -} - -func Test_overrideRouteStripPath(t *testing.T) { - type args struct { - route *kong.Route - anns map[string]string - } - tests := []struct { - name string - args args - want *kong.Route - }{ - {}, - { - name: "basic empty route", - args: args{ - route: &kong.Route{}, - }, - want: &kong.Route{}, - }, - { - name: "set to false", - args: args{ - route: &kong.Route{}, - anns: map[string]string{ - "configuration.konghq.com/strip-path": "false", - }, - }, - want: &kong.Route{ - StripPath: kong.Bool(false), - }, - }, - { - name: "set to true and case insensitive", - args: args{ - route: &kong.Route{}, - anns: map[string]string{ - "configuration.konghq.com/strip-path": "truE", - }, - }, - want: &kong.Route{ - StripPath: kong.Bool(true), - }, - }, - { - name: "overrides any other value", - args: args{ - route: &kong.Route{ - StripPath: kong.Bool(false), - }, - anns: map[string]string{ - "configuration.konghq.com/strip-path": "truE", - }, - }, - want: &kong.Route{ - StripPath: kong.Bool(true), - }, - }, - { - name: "random value", - args: args{ - route: &kong.Route{ - StripPath: kong.Bool(false), - }, - anns: map[string]string{ - "configuration.konghq.com/strip-path": "42", - }, - }, - want: &kong.Route{ - StripPath: kong.Bool(false), - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - overrideRouteStripPath(tt.args.route, tt.args.anns) - if !reflect.DeepEqual(tt.args.route, tt.want) { - t.Errorf("overrideRouteStripPath() got = %v, want %v", tt.args.route, tt.want) - } - }) - } -} - -func Test_overrideServicePath(t *testing.T) { - type args struct { - service *kong.Service - anns map[string]string - } - tests := []struct { - name string - args args - want *kong.Service - }{ - {}, - { - name: "basic empty service", - args: args{ - service: &kong.Service{}, - }, - want: &kong.Service{}, - }, - { - name: "set to valid value", - args: args{ - service: &kong.Service{}, - anns: map[string]string{ - "configuration.konghq.com/path": "/foo", - }, - }, - want: &kong.Service{ - Path: kong.String("/foo"), - }, - }, - { - name: "does not set path if doesn't start with /", - args: args{ - service: &kong.Service{}, - anns: map[string]string{ - "configuration.konghq.com/path": "foo", - }, - }, - want: &kong.Service{}, - }, - { - name: "overrides any other value", - args: args{ - service: &kong.Service{ - Path: kong.String("/foo"), - }, - anns: map[string]string{ - "configuration.konghq.com/path": "/bar", - }, - }, - want: &kong.Service{ - Path: kong.String("/bar"), - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - overrideServicePath(tt.args.service, tt.args.anns) - if !reflect.DeepEqual(tt.args.service, tt.want) { - t.Errorf("overrideServicePath() got = %v, want %v", tt.args.service, tt.want) - } - }) - } -} - -func Test_overrideRouteHTTPSRedirectCode(t *testing.T) { - type args struct { - route *kong.Route - anns map[string]string - } - tests := []struct { - name string - args args - want *kong.Route - }{ - {}, - { - name: "basic empty route", - args: args{ - route: &kong.Route{}, - }, - want: &kong.Route{}, - }, - { - name: "basic sanity", - args: args{ - route: &kong.Route{}, - anns: map[string]string{ - "konghq.com/https-redirect-status-code": "301", - }, - }, - want: &kong.Route{ - HTTPSRedirectStatusCode: kong.Int(301), - }, - }, - { - name: "random integer value", - args: args{ - route: &kong.Route{}, - anns: map[string]string{ - "konghq.com/https-redirect-status-code": "42", - }, - }, - want: &kong.Route{}, - }, - { - name: "random string", - args: args{ - route: &kong.Route{}, - anns: map[string]string{ - "konghq.com/https-redirect-status-code": "foo", - }, - }, - want: &kong.Route{}, - }, - { - name: "force ssl annotation set to true and protocols is not set", - args: args{ - route: &kong.Route{}, - anns: map[string]string{ - "ingress.kubernetes.io/force-ssl-redirect": "true", - }, - }, - want: &kong.Route{ - HTTPSRedirectStatusCode: kong.Int(302), - Protocols: []*string{kong.String("https")}, - }, - }, - { - name: "force ssl annotation set to true and protocol is set to grpc", - args: args{ - route: &kong.Route{ - Protocols: []*string{kong.String("grpc")}, - }, - anns: map[string]string{ - "ingress.kubernetes.io/force-ssl-redirect": "true", - "konghq.com/protocols": "grpc", - }, - }, - want: &kong.Route{ - HTTPSRedirectStatusCode: kong.Int(302), - Protocols: []*string{kong.String("grpcs")}, - }, - }, - { - name: "force ssl annotation set to false", - args: args{ - route: &kong.Route{}, - anns: map[string]string{ - "ingress.kubernetes.io/force-ssl-redirect": "false", - }, - }, - want: &kong.Route{}, - }, - { - name: "force ssl annotation set to true and HTTPS redirect code set to 307", - args: args{ - route: &kong.Route{}, - anns: map[string]string{ - "ingress.kubernetes.io/force-ssl-redirect": "true", - "konghq.com/https-redirect-status-code": "307", - }, - }, - want: &kong.Route{ - HTTPSRedirectStatusCode: kong.Int(307), - Protocols: []*string{kong.String("https")}, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - overrideRouteHTTPSRedirectCode(tt.args.route, tt.args.anns) - if !reflect.DeepEqual(tt.args.route, tt.want) { - t.Errorf("overrideRouteHTTPSRedirectCode() got = %v, want %v", tt.args.route, tt.want) - } - }) - } -} - -func Test_overrideRoutePreserveHost(t *testing.T) { - type args struct { - route *kong.Route - anns map[string]string - } - tests := []struct { - name string - args args - want *kong.Route - }{ - {}, - { - name: "basic empty route", - args: args{ - route: &kong.Route{}, - }, - want: &kong.Route{}, - }, - { - name: "basic sanity", - args: args{ - route: &kong.Route{}, - anns: map[string]string{ - "konghq.com/preserve-host": "true", - }, - }, - want: &kong.Route{ - PreserveHost: kong.Bool(true), - }, - }, - { - name: "case insensitive", - args: args{ - route: &kong.Route{}, - anns: map[string]string{ - "konghq.com/preserve-host": "faLSe", - }, - }, - want: &kong.Route{ - PreserveHost: kong.Bool(false), - }, - }, - { - name: "random integer value", - args: args{ - route: &kong.Route{}, - anns: map[string]string{ - "konghq.com/https-redirect-status-code": "42", - }, - }, - want: &kong.Route{}, - }, - { - name: "random string", - args: args{ - route: &kong.Route{}, - anns: map[string]string{ - "konghq.com/https-redirect-status-code": "foo", - }, - }, - want: &kong.Route{}, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - overrideRoutePreserveHost(tt.args.route, tt.args.anns) - if !reflect.DeepEqual(tt.args.route, tt.want) { - t.Errorf("overrideRoutePreserveHost() got = %v, want %v", tt.args.route, tt.want) - } - }) - } -} - -func Test_overrideRouteRegexPriority(t *testing.T) { - type args struct { - route *kong.Route - anns map[string]string - } - tests := []struct { - name string - args args - want *kong.Route - }{ - {}, - { - name: "basic empty route", - args: args{ - route: &kong.Route{}, - }, - want: &kong.Route{}, - }, - { - name: "basic sanity", - args: args{ - route: &kong.Route{}, - anns: map[string]string{ - "konghq.com/regex-priority": "10", - }, - }, - want: &kong.Route{ - RegexPriority: kong.Int(10), - }, - }, - { - name: "negative integer", - args: args{ - route: &kong.Route{}, - anns: map[string]string{ - "konghq.com/regex-priority": "-10", - }, - }, - want: &kong.Route{ - RegexPriority: kong.Int(-10), - }, - }, - { - name: "random float value", - args: args{ - route: &kong.Route{}, - anns: map[string]string{ - "konghq.com/regex-priority": "42.42", - }, - }, - want: &kong.Route{}, - }, - { - name: "random string", - args: args{ - route: &kong.Route{}, - anns: map[string]string{ - "konghq.com/regex-priority": "foo", - }, - }, - want: &kong.Route{}, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - overrideRouteRegexPriority(tt.args.route, tt.args.anns) - if !reflect.DeepEqual(tt.args.route, tt.want) { - t.Errorf("overrideRouteRegexPriority() got = %v, want %v", tt.args.route, tt.want) - } - }) - } -} - -func Test_overrideRouteMethods(t *testing.T) { - type args struct { - route *kong.Route - anns map[string]string - } - tests := []struct { - name string - args args - want *kong.Route - }{ - {}, - { - name: "basic empty route", - args: args{ - route: &kong.Route{}, - }, - want: &kong.Route{}, - }, - { - name: "basic sanity", - args: args{ - route: &kong.Route{}, - anns: map[string]string{ - "konghq.com/methods": "POST,GET", - }, - }, - want: &kong.Route{ - Methods: kong.StringSlice("POST", "GET"), - }, - }, - { - name: "non-string", - args: args{ - route: &kong.Route{}, - anns: map[string]string{ - "konghq.com/methods": "-10,GET", - }, - }, - want: &kong.Route{}, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - p := Parser{ - Logger: logrus.New(), - } - p.overrideRouteMethods(tt.args.route, tt.args.anns) - if !reflect.DeepEqual(tt.args.route, tt.want) { - t.Errorf("overrideRouteMethods() got = %v, want %v", tt.args.route, tt.want) + for _, testCase := range tests { + t.Run(testCase.name, func(t *testing.T) { + result := getEndpoints(logrus.New(), testCase.svc, testCase.port, testCase.proto, testCase.fn) + if len(testCase.result) != len(result) { + t.Errorf("expected %v Endpoints but got %v", testCase.result, len(result)) } }) } diff --git a/internal/ingress/controller/parser/util/protocol.go b/internal/ingress/controller/parser/util/protocol.go new file mode 100644 index 0000000000..9c36a05986 --- /dev/null +++ b/internal/ingress/controller/parser/util/protocol.go @@ -0,0 +1,11 @@ +package util + +import "regexp" + +// ValidateProtocol returns a bool of whether string is a valid protocol +func ValidateProtocol(protocol string) bool { + match := validProtocols.MatchString(protocol) + return match +} + +var validProtocols = regexp.MustCompile(`\Ahttps$|\Ahttp$|\Agrpc$|\Agrpcs|\Atcp|\Atls$`) diff --git a/internal/ingress/controller/parser/util/protocol_test.go b/internal/ingress/controller/parser/util/protocol_test.go new file mode 100644 index 0000000000..a69102324e --- /dev/null +++ b/internal/ingress/controller/parser/util/protocol_test.go @@ -0,0 +1,25 @@ +package util + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestValidateProtocol(t *testing.T) { + assert := assert.New(t) + testTable := []struct { + input string + result bool + }{ + {"http", true}, + {"https", true}, + {"grpc", true}, + {"grpcs", true}, + {"grcpsfdsafdsfafdshttp", false}, + } + for _, testcase := range testTable { + isMatch := ValidateProtocol(testcase.input) + assert.Equal(isMatch, testcase.result) + } +} diff --git a/internal/ingress/controller/parser/util/relations.go b/internal/ingress/controller/parser/util/relations.go new file mode 100644 index 0000000000..021b7ed7ef --- /dev/null +++ b/internal/ingress/controller/parser/util/relations.go @@ -0,0 +1,49 @@ +package util + +type ForeignRelations struct { + Consumer, Route, Service []string +} + +type Rel struct { + Consumer, Route, Service string +} + +func (relations *ForeignRelations) GetCombinations() []Rel { + + var cartesianProduct []Rel + + if len(relations.Consumer) > 0 { + consumers := relations.Consumer + if len(relations.Route)+len(relations.Service) > 0 { + for _, service := range relations.Service { + for _, consumer := range consumers { + cartesianProduct = append(cartesianProduct, Rel{ + Service: service, + Consumer: consumer, + }) + } + } + for _, route := range relations.Route { + for _, consumer := range consumers { + cartesianProduct = append(cartesianProduct, Rel{ + Route: route, + Consumer: consumer, + }) + } + } + } else { + for _, consumer := range relations.Consumer { + cartesianProduct = append(cartesianProduct, Rel{Consumer: consumer}) + } + } + } else { + for _, service := range relations.Service { + cartesianProduct = append(cartesianProduct, Rel{Service: service}) + } + for _, route := range relations.Route { + cartesianProduct = append(cartesianProduct, Rel{Route: route}) + } + } + + return cartesianProduct +} diff --git a/internal/ingress/controller/parser/util/relations_test.go b/internal/ingress/controller/parser/util/relations_test.go new file mode 100644 index 0000000000..a273ec4b50 --- /dev/null +++ b/internal/ingress/controller/parser/util/relations_test.go @@ -0,0 +1,201 @@ +package util + +import ( + "reflect" + "testing" +) + +func Test_GetCombinations(t *testing.T) { + type args struct { + relations ForeignRelations + } + tests := []struct { + name string + args args + want []Rel + }{ + { + name: "empty", + args: args{ + relations: ForeignRelations{}, + }, + want: nil, + }, + { + name: "plugins on consumer only", + args: args{ + relations: ForeignRelations{ + Consumer: []string{"foo", "bar"}, + }, + }, + want: []Rel{ + { + Consumer: "foo", + }, + { + Consumer: "bar", + }, + }, + }, + { + name: "plugins on service only", + args: args{ + relations: ForeignRelations{ + Service: []string{"foo", "bar"}, + }, + }, + want: []Rel{ + { + Service: "foo", + }, + { + Service: "bar", + }, + }, + }, + { + name: "plugins on routes only", + args: args{ + relations: ForeignRelations{ + Route: []string{"foo", "bar"}, + }, + }, + want: []Rel{ + { + Route: "foo", + }, + { + Route: "bar", + }, + }, + }, + { + name: "plugins on service and routes only", + args: args{ + relations: ForeignRelations{ + Route: []string{"foo", "bar"}, + Service: []string{"foo", "bar"}, + }, + }, + want: []Rel{ + { + Service: "foo", + }, + { + Service: "bar", + }, + { + Route: "foo", + }, + { + Route: "bar", + }, + }, + }, + { + name: "plugins on combination of route and consumer", + args: args{ + relations: ForeignRelations{ + Route: []string{"foo", "bar"}, + Consumer: []string{"foo", "bar"}, + }, + }, + want: []Rel{ + { + Consumer: "foo", + Route: "foo", + }, + { + Consumer: "bar", + Route: "foo", + }, + { + Consumer: "foo", + Route: "bar", + }, + { + Consumer: "bar", + Route: "bar", + }, + }, + }, + { + name: "plugins on combination of service and consumer", + args: args{ + relations: ForeignRelations{ + Service: []string{"foo", "bar"}, + Consumer: []string{"foo", "bar"}, + }, + }, + want: []Rel{ + { + Consumer: "foo", + Service: "foo", + }, + { + Consumer: "bar", + Service: "foo", + }, + { + Consumer: "foo", + Service: "bar", + }, + { + Consumer: "bar", + Service: "bar", + }, + }, + }, + { + name: "plugins on combination of service,route and consumer", + args: args{ + relations: ForeignRelations{ + Consumer: []string{"c1", "c2"}, + Route: []string{"r1", "r2"}, + Service: []string{"s1", "s2"}, + }, + }, + want: []Rel{ + { + Consumer: "c1", + Service: "s1", + }, + { + Consumer: "c2", + Service: "s1", + }, + { + Consumer: "c1", + Service: "s2", + }, + { + Consumer: "c2", + Service: "s2", + }, + { + Consumer: "c1", + Route: "r1", + }, + { + Consumer: "c2", + Route: "r1", + }, + { + Consumer: "c1", + Route: "r2", + }, + { + Consumer: "c2", + Route: "r2", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.args.relations.GetCombinations(); !reflect.DeepEqual(got, tt.want) { + t.Errorf("GetCombinations() = %v, want %v", got, tt.want) + } + }) + } +}