diff --git a/CHANGELOG.md b/CHANGELOG.md index 016253de7f..35e2e4c551 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -115,6 +115,11 @@ Adding a new version? You'll need three changes: that all time series for those metrics will get a new label designating the address of the dataplane that the configuration push has been targeted for. [#3521](https://github.com/Kong/kubernetes-ingress-controller/pull/3521) +- In DB-less mode, failure to push a config will now generate Kubernetes Events + with the reason `KongConfigurationApplyFailed` and an `InvolvedObject` + indicating which Kubernetes resource was responsible for the broken Kong + configuration. + [#3446](https://github.com/Kong/kubernetes-ingress-controller/pull/3446) - Leader election is enabled by default then kong admin service discovery is enabled. [#3529](https://github.com/Kong/kubernetes-ingress-controller/pull/3529) diff --git a/internal/dataplane/kong_client.go b/internal/dataplane/kong_client.go index 1dac5c6dc3..cd91f09eb4 100644 --- a/internal/dataplane/kong_client.go +++ b/internal/dataplane/kong_client.go @@ -487,7 +487,7 @@ func (c *KongClient) sendToClient( // apply the configuration update in Kong timedCtx, cancel := context.WithTimeout(ctx, c.requestTimeout) defer cancel() - newConfigSHA, err := sendconfig.PerformUpdate( + newConfigSHA, entityErrors, err := sendconfig.PerformUpdate( timedCtx, logger, client, @@ -495,7 +495,10 @@ func (c *KongClient) sendToClient( targetConfig, c.prometheusMetrics, ) + + c.recordResourceFailureEvents(entityErrors, KongConfigurationApplyFailedEventReason) sendDiagnostic(err != nil) + if err != nil { if expired, ok := timedCtx.Deadline(); ok && time.Now().After(expired) { logger.Warn("exceeded Kong API timeout, consider increasing --proxy-timeout-seconds") diff --git a/internal/dataplane/kongstate/consumer.go b/internal/dataplane/kongstate/consumer.go index 99c5273f0b..a4ea757a19 100644 --- a/internal/dataplane/kongstate/consumer.go +++ b/internal/dataplane/kongstate/consumer.go @@ -66,43 +66,49 @@ func (c *Consumer) SanitizedCopy() *Consumer { } } -func (c *Consumer) SetCredential(credType string, credConfig interface{}) error { +func (c *Consumer) SetCredential(credType string, credConfig interface{}, tags []*string) error { switch credType { case "key-auth", "keyauth_credential": cred, err := NewKeyAuth(credConfig) if err != nil { return err } + cred.Tags = tags c.KeyAuths = append(c.KeyAuths, cred) case "basic-auth", "basicauth_credential": cred, err := NewBasicAuth(credConfig) if err != nil { return err } + cred.Tags = tags c.BasicAuths = append(c.BasicAuths, cred) case "hmac-auth", "hmacauth_credential": cred, err := NewHMACAuth(credConfig) if err != nil { return err } + cred.Tags = tags c.HMACAuths = append(c.HMACAuths, cred) case "oauth2": cred, err := NewOauth2Credential(credConfig) if err != nil { return err } + cred.Tags = tags c.Oauth2Creds = append(c.Oauth2Creds, cred) case "jwt", "jwt_secret": cred, err := NewJWTAuth(credConfig) if err != nil { return err } + cred.Tags = tags c.JWTAuths = append(c.JWTAuths, cred) case "acl": cred, err := NewACLGroup(credConfig) if err != nil { return err } + cred.Tags = tags c.ACLGroups = append(c.ACLGroups, cred) case "mtls-auth": if !versions.GetKongVersion().MajorMinorPatchOnly().GTE(versions.MTLSCredentialVersionCutoff) { @@ -112,6 +118,7 @@ func (c *Consumer) SetCredential(credType string, credConfig interface{}) error if err != nil { return err } + cred.Tags = tags c.MTLSAuths = append(c.MTLSAuths, cred) default: return fmt.Errorf("invalid credential type: '%v'", credType) diff --git a/internal/dataplane/kongstate/consumer_test.go b/internal/dataplane/kongstate/consumer_test.go index f134933ee3..ce4c11d12b 100644 --- a/internal/dataplane/kongstate/consumer_test.go +++ b/internal/dataplane/kongstate/consumer_test.go @@ -106,7 +106,8 @@ func TestConsumer_SetCredential(t *testing.T) { result: &Consumer{ KeyAuths: []*KeyAuth{ {kong.KeyAuth{ - Key: kong.String("foo"), + Key: kong.String("foo"), + Tags: []*string{}, }}, }, }, @@ -116,20 +117,20 @@ func TestConsumer_SetCredential(t *testing.T) { name: "key-auth without key", args: args{ credType: "key-auth", - consumer: &Consumer{Consumer: kong.Consumer{Username: &username}}, + consumer: &Consumer{Consumer: kong.Consumer{Username: &username, Tags: []*string{}}}, credConfig: map[string]string{}, }, - result: &Consumer{Consumer: kong.Consumer{Username: &username}}, + result: &Consumer{Consumer: kong.Consumer{Username: &username, Tags: []*string{}}}, wantErr: true, }, { name: "key-auth with invalid key type", args: args{ credType: "key-auth", - consumer: &Consumer{Consumer: kong.Consumer{Username: &username}}, + consumer: &Consumer{Consumer: kong.Consumer{Username: &username, Tags: []*string{}}}, credConfig: map[string]interface{}{"key": true}, }, - result: &Consumer{Consumer: kong.Consumer{Username: &username}}, + result: &Consumer{Consumer: kong.Consumer{Username: &username, Tags: []*string{}}}, wantErr: true, }, { @@ -142,7 +143,8 @@ func TestConsumer_SetCredential(t *testing.T) { result: &Consumer{ KeyAuths: []*KeyAuth{ {kong.KeyAuth{ - Key: kong.String("foo"), + Key: kong.String("foo"), + Tags: []*string{}, }}, }, }, @@ -163,6 +165,7 @@ func TestConsumer_SetCredential(t *testing.T) { {kong.BasicAuth{ Username: kong.String("foo"), Password: kong.String("bar"), + Tags: []*string{}, }}, }, }, @@ -172,20 +175,20 @@ func TestConsumer_SetCredential(t *testing.T) { name: "basic-auth without username", args: args{ credType: "basic-auth", - consumer: &Consumer{Consumer: kong.Consumer{Username: &username}}, + consumer: &Consumer{Consumer: kong.Consumer{Username: &username, Tags: []*string{}}}, credConfig: map[string]string{}, }, - result: &Consumer{Consumer: kong.Consumer{Username: &username}}, + result: &Consumer{Consumer: kong.Consumer{Username: &username, Tags: []*string{}}}, wantErr: true, }, { name: "basic-auth with invalid username type", args: args{ credType: "basic-auth", - consumer: &Consumer{Consumer: kong.Consumer{Username: &username}}, + consumer: &Consumer{Consumer: kong.Consumer{Username: &username, Tags: []*string{}}}, credConfig: map[string]interface{}{"username": true}, }, - result: &Consumer{Consumer: kong.Consumer{Username: &username}}, + result: &Consumer{Consumer: kong.Consumer{Username: &username, Tags: []*string{}}}, wantErr: true, }, { @@ -203,6 +206,7 @@ func TestConsumer_SetCredential(t *testing.T) { {kong.BasicAuth{ Username: kong.String("foo"), Password: kong.String("bar"), + Tags: []*string{}, }}, }, }, @@ -223,6 +227,7 @@ func TestConsumer_SetCredential(t *testing.T) { {kong.HMACAuth{ Username: kong.String("foo"), Secret: kong.String("bar"), + Tags: []*string{}, }}, }, }, @@ -232,20 +237,20 @@ func TestConsumer_SetCredential(t *testing.T) { name: "hmac-auth without username", args: args{ credType: "hmac-auth", - consumer: &Consumer{Consumer: kong.Consumer{Username: &username}}, + consumer: &Consumer{Consumer: kong.Consumer{Username: &username, Tags: []*string{}}}, credConfig: map[string]string{}, }, - result: &Consumer{Consumer: kong.Consumer{Username: &username}}, + result: &Consumer{Consumer: kong.Consumer{Username: &username, Tags: []*string{}}}, wantErr: true, }, { name: "hmac-auth with invalid username type", args: args{ credType: "hmac-auth", - consumer: &Consumer{Consumer: kong.Consumer{Username: &username}}, + consumer: &Consumer{Consumer: kong.Consumer{Username: &username, Tags: []*string{}}}, credConfig: map[string]interface{}{"username": true}, }, - result: &Consumer{Consumer: kong.Consumer{Username: &username}}, + result: &Consumer{Consumer: kong.Consumer{Username: &username, Tags: []*string{}}}, wantErr: true, }, { @@ -263,6 +268,7 @@ func TestConsumer_SetCredential(t *testing.T) { {kong.HMACAuth{ Username: kong.String("foo"), Secret: kong.String("bar"), + Tags: []*string{}, }}, }, }, @@ -287,6 +293,7 @@ func TestConsumer_SetCredential(t *testing.T) { ClientID: kong.String("bar"), ClientSecret: kong.String("baz"), RedirectURIs: kong.StringSlice("example.com"), + Tags: []*string{}, }}, }, }, @@ -296,34 +303,34 @@ func TestConsumer_SetCredential(t *testing.T) { name: "oauth2 without name", args: args{ credType: "oauth2", - consumer: &Consumer{Consumer: kong.Consumer{Username: &username}}, + consumer: &Consumer{Consumer: kong.Consumer{Username: &username, Tags: []*string{}}}, credConfig: map[string]interface{}{ "client_id": "bar", }, }, - result: &Consumer{Consumer: kong.Consumer{Username: &username}}, + result: &Consumer{Consumer: kong.Consumer{Username: &username, Tags: []*string{}}}, wantErr: true, }, { name: "oauth2 without client_id", args: args{ credType: "oauth2", - consumer: &Consumer{Consumer: kong.Consumer{Username: &username}}, + consumer: &Consumer{Consumer: kong.Consumer{Username: &username, Tags: []*string{}}}, credConfig: map[string]interface{}{ "name": "bar", }, }, - result: &Consumer{Consumer: kong.Consumer{Username: &username}}, + result: &Consumer{Consumer: kong.Consumer{Username: &username, Tags: []*string{}}}, wantErr: true, }, { name: "oauth2 with invalid client_id type", args: args{ credType: "oauth2", - consumer: &Consumer{Consumer: kong.Consumer{Username: &username}}, + consumer: &Consumer{Consumer: kong.Consumer{Username: &username, Tags: []*string{}}}, credConfig: map[string]interface{}{"client_id": true}, }, - result: &Consumer{Consumer: kong.Consumer{Username: &username}}, + result: &Consumer{Consumer: kong.Consumer{Username: &username, Tags: []*string{}}}, wantErr: true, }, { @@ -345,6 +352,7 @@ func TestConsumer_SetCredential(t *testing.T) { Secret: kong.String("baz"), // set by default Algorithm: kong.String("HS256"), + Tags: []*string{}, }}, }, }, @@ -354,20 +362,20 @@ func TestConsumer_SetCredential(t *testing.T) { name: "jwt without key", args: args{ credType: "jwt", - consumer: &Consumer{Consumer: kong.Consumer{Username: &username}}, + consumer: &Consumer{Consumer: kong.Consumer{Username: &username, Tags: []*string{}}}, credConfig: map[string]string{}, }, - result: &Consumer{Consumer: kong.Consumer{Username: &username}}, + result: &Consumer{Consumer: kong.Consumer{Username: &username, Tags: []*string{}}}, wantErr: true, }, { name: "jwt with invald key type", args: args{ credType: "jwt", - consumer: &Consumer{Consumer: kong.Consumer{Username: &username}}, + consumer: &Consumer{Consumer: kong.Consumer{Username: &username, Tags: []*string{}}}, credConfig: map[string]interface{}{"key": true}, }, - result: &Consumer{Consumer: kong.Consumer{Username: &username}}, + result: &Consumer{Consumer: kong.Consumer{Username: &username, Tags: []*string{}}}, wantErr: true, }, { @@ -389,6 +397,7 @@ func TestConsumer_SetCredential(t *testing.T) { Secret: kong.String("baz"), // set by default Algorithm: kong.String("HS256"), + Tags: []*string{}, }}, }, }, @@ -405,6 +414,7 @@ func TestConsumer_SetCredential(t *testing.T) { ACLGroups: []*ACLGroup{ {kong.ACLGroup{ Group: kong.String("group-foo"), + Tags: []*string{}, }}, }, }, @@ -414,41 +424,41 @@ func TestConsumer_SetCredential(t *testing.T) { name: "acl without group", args: args{ credType: "acl", - consumer: &Consumer{Consumer: kong.Consumer{Username: &username}}, + consumer: &Consumer{Consumer: kong.Consumer{Username: &username, Tags: []*string{}}}, credConfig: map[string]string{}, }, - result: &Consumer{Consumer: kong.Consumer{Username: &username}}, + result: &Consumer{Consumer: kong.Consumer{Username: &username, Tags: []*string{}}}, wantErr: true, }, { name: "acl with invalid group type", args: args{ credType: "acl", - consumer: &Consumer{Consumer: kong.Consumer{Username: &username}}, + consumer: &Consumer{Consumer: kong.Consumer{Username: &username, Tags: []*string{}}}, credConfig: map[string]interface{}{"group": true}, }, - result: &Consumer{Consumer: kong.Consumer{Username: &username}}, + result: &Consumer{Consumer: kong.Consumer{Username: &username, Tags: []*string{}}}, wantErr: true, }, { name: "mtls-auth on unsupported version", args: args{ credType: "mtls-auth", - consumer: &Consumer{Consumer: kong.Consumer{Username: &username}}, + consumer: &Consumer{Consumer: kong.Consumer{Username: &username, Tags: []*string{}}}, credConfig: map[string]string{"subject_name": "foo@example.com"}, }, - result: &Consumer{Consumer: kong.Consumer{Username: &username}}, + result: &Consumer{Consumer: kong.Consumer{Username: &username, Tags: []*string{}}}, wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if err := tt.args.consumer.SetCredential(tt.args.credType, - tt.args.credConfig); (err != nil) != tt.wantErr { + tt.args.credConfig, []*string{}); (err != nil) != tt.wantErr { t.Errorf("processCredential() error = %v, wantErr %v", err, tt.wantErr) } - assert.Equal(t, tt.result, tt.args.consumer) + assert.Equal(t, tt.args.consumer, tt.result) }) } @@ -458,14 +468,15 @@ func TestConsumer_SetCredential(t *testing.T) { name: "mtls-auth", args: args{ credType: "mtls-auth", - consumer: &Consumer{Consumer: kong.Consumer{Username: &username}}, + consumer: &Consumer{Consumer: kong.Consumer{Username: &username, Tags: []*string{}}}, credConfig: map[string]string{"subject_name": "foo@example.com"}, }, result: &Consumer{ - Consumer: kong.Consumer{Username: &username}, + Consumer: kong.Consumer{Username: &username, Tags: []*string{}}, MTLSAuths: []*MTLSAuth{ {kong.MTLSAuth{ SubjectName: kong.String("foo@example.com"), + Tags: []*string{}, }}, }, }, @@ -475,20 +486,20 @@ func TestConsumer_SetCredential(t *testing.T) { name: "mtls-auth without subject_name", args: args{ credType: "mtls-auth", - consumer: &Consumer{Consumer: kong.Consumer{Username: &username}}, + consumer: &Consumer{Consumer: kong.Consumer{Username: &username, Tags: []*string{}}}, credConfig: map[string]string{}, }, - result: &Consumer{Consumer: kong.Consumer{Username: &username}}, + result: &Consumer{Consumer: kong.Consumer{Username: &username, Tags: []*string{}}}, wantErr: true, }, { name: "mtls-auth with invalid subject_name type", args: args{ credType: "mtls-auth", - consumer: &Consumer{Consumer: kong.Consumer{Username: &username}}, + consumer: &Consumer{Consumer: kong.Consumer{Username: &username, Tags: []*string{}}}, credConfig: map[string]interface{}{"subject_name": true}, }, - result: &Consumer{Consumer: kong.Consumer{Username: &username}}, + result: &Consumer{Consumer: kong.Consumer{Username: &username, Tags: []*string{}}}, wantErr: true, }, } @@ -496,11 +507,11 @@ func TestConsumer_SetCredential(t *testing.T) { for _, tt := range mtlsSupportedTests { t.Run(tt.name, func(t *testing.T) { if err := tt.args.consumer.SetCredential(tt.args.credType, - tt.args.credConfig); (err != nil) != tt.wantErr { + tt.args.credConfig, []*string{}); (err != nil) != tt.wantErr { t.Errorf("processCredential() error = %v, wantErr %v", err, tt.wantErr) } - assert.Equal(t, tt.result, tt.args.consumer) + assert.Equal(t, tt.args.consumer, tt.result) }) } } diff --git a/internal/dataplane/kongstate/kongstate.go b/internal/dataplane/kongstate/kongstate.go index b50d4b70ea..2a5c1c5fd8 100644 --- a/internal/dataplane/kongstate/kongstate.go +++ b/internal/dataplane/kongstate/kongstate.go @@ -64,6 +64,7 @@ func (ks *KongState) FillConsumersAndCredentials(log logrus.FieldLogger, s store c.CustomID = kong.String(consumer.CustomID) } c.K8sKongConsumer = *consumer + c.Tags = util.GenerateTagsForObject(consumer) log = log.WithFields(logrus.Fields{ "kongconsumer_name": consumer.Name, @@ -127,7 +128,8 @@ func (ks *KongState) FillConsumersAndCredentials(log logrus.FieldLogger, s store log.Error("failed to provision credential: empty secret") continue } - err = c.SetCredential(credType, credConfig) + credTags := util.GenerateTagsForObject(secret) + err = c.SetCredential(credType, credConfig, credTags) if err != nil { log.WithError(err).Errorf("failed to provision credential") continue diff --git a/internal/dataplane/kongstate/util.go b/internal/dataplane/kongstate/util.go index 57ea298e1c..1db801a4a7 100644 --- a/internal/dataplane/kongstate/util.go +++ b/internal/dataplane/kongstate/util.go @@ -145,6 +145,7 @@ func kongPluginFromK8SClusterPlugin( Ordering: k8sPlugin.Ordering, Disabled: k8sPlugin.Disabled, Protocols: protocolsToStrings(k8sPlugin.Protocols), + Tags: util.GenerateTagsForObject(&k8sPlugin), }.toKongPlugin() return kongPlugin, nil } @@ -197,6 +198,7 @@ func kongPluginFromK8SPlugin( Ordering: k8sPlugin.Ordering, Disabled: k8sPlugin.Disabled, Protocols: protocolsToStrings(k8sPlugin.Protocols), + Tags: util.GenerateTagsForObject(&k8sPlugin), }.toKongPlugin() return kongPlugin, nil } @@ -284,12 +286,14 @@ type plugin struct { Ordering *kong.PluginOrdering Disabled bool Protocols []string + Tags []*string } func (p plugin) toKongPlugin() kong.Plugin { result := kong.Plugin{ Name: kong.String(p.Name), Config: p.Config.DeepCopy(), + Tags: p.Tags, } if p.RunOn != "" { result.RunOn = kong.String(p.RunOn) diff --git a/internal/dataplane/kongstate/util_test.go b/internal/dataplane/kongstate/util_test.go index 886fca3739..1c2d2e3e82 100644 --- a/internal/dataplane/kongstate/util_test.go +++ b/internal/dataplane/kongstate/util_test.go @@ -288,6 +288,8 @@ func TestKongPluginFromK8SPlugin(t *testing.T) { t.Errorf("kongPluginFromK8SPlugin error = %v, wantErr %v", err, tt.wantErr) return } + // don't care about tags in this test + got.Tags = nil assert.Equal(tt.want, got) }) } diff --git a/internal/dataplane/parser/ingressrules.go b/internal/dataplane/parser/ingressrules.go index ccda7a7623..8fcda6865c 100644 --- a/internal/dataplane/parser/ingressrules.go +++ b/internal/dataplane/parser/ingressrules.go @@ -16,17 +16,20 @@ import ( "github.com/kong/kubernetes-ingress-controller/v2/internal/dataplane/failures" "github.com/kong/kubernetes-ingress-controller/v2/internal/dataplane/kongstate" "github.com/kong/kubernetes-ingress-controller/v2/internal/store" + "github.com/kong/kubernetes-ingress-controller/v2/internal/util" ) type ingressRules struct { SecretNameToSNIs SecretNameToSNIs ServiceNameToServices map[string]kongstate.Service + ServiceNameToParent map[string]client.Object } func newIngressRules() ingressRules { return ingressRules{ SecretNameToSNIs: newSecretNameToSNIs(), ServiceNameToServices: make(map[string]kongstate.Service), + ServiceNameToParent: make(map[string]client.Object), } } @@ -90,6 +93,33 @@ func (ir *ingressRules) populateServices(log logrus.FieldLogger, s store.Storer, } } } + if len(k8sServices) > 1 { + if parent, ok := ir.ServiceNameToParent[*service.Name]; ok { + service.Tags = util.GenerateTagsForObject(parent) + } else { + log.WithFields(logrus.Fields{ + "service": *service.Name, + }).Error("multi-service backend lacks parent info, cannot generate tags") + } + } else if len(k8sServices) > 0 { + service.Tags = util.GenerateTagsForObject(k8sServices[0]) + } else { + // TODO https://github.com/Kong/kubernetes-ingress-controller/issues/3484 + // somehow https://gist.github.com/rainest/8d5a067e9c8b93c98100559fcbe75631 results in _ZERO_ + // k8sServices, causing a panic here without this if clause. + // That shouldn't happen and requires further investigation. + log.WithFields(logrus.Fields{ + "service": *service.Name, + }).Error("service has zero k8sServices backends, cannot generate tags for it properly") + service.Tags = kong.StringSlice( + util.K8sNameTagPrefix+"UNKNOWN", + util.K8sNamespaceTagPrefix+"UNKNOWN", + util.K8sKindTagPrefix+"Service", + util.K8sUIDTagPrefix+"00000000-0000-0000-0000-000000000000", + util.K8sGroupTagPrefix+"core", + util.K8sVersionTagPrefix+"v1", + ) + } // Kubernetes Services have been populated for this Kong Service, so it can // now be cached. diff --git a/internal/dataplane/parser/ingressrules_test.go b/internal/dataplane/parser/ingressrules_test.go index 5b55b26137..998033b692 100644 --- a/internal/dataplane/parser/ingressrules_test.go +++ b/internal/dataplane/parser/ingressrules_test.go @@ -54,6 +54,7 @@ func TestMergeIngressRules(t *testing.T) { wantOutput: &ingressRules{ SecretNameToSNIs: newSecretNameToSNIs(), ServiceNameToServices: map[string]kongstate.Service{}, + ServiceNameToParent: map[string]client.Object{}, }, }, { @@ -65,6 +66,7 @@ func TestMergeIngressRules(t *testing.T) { wantOutput: &ingressRules{ SecretNameToSNIs: newSecretNameToSNIs(), ServiceNameToServices: map[string]kongstate.Service{}, + ServiceNameToParent: map[string]client.Object{}, }, }, { @@ -78,6 +80,8 @@ func TestMergeIngressRules(t *testing.T) { wantOutput: &ingressRules{ SecretNameToSNIs: makeSecretToSNIs(map[string]testSNIs{"a": {hosts: []string{"b", "c"}}, "d": {hosts: []string{"e", "f"}}}), ServiceNameToServices: map[string]kongstate.Service{"1": {Namespace: "potato"}}, + // not all of the inputs have proper parents, so while they're not empty, this still is in the unit + ServiceNameToParent: map[string]client.Object{}, }, }, { @@ -92,11 +96,13 @@ func TestMergeIngressRules(t *testing.T) { }, { ServiceNameToServices: map[string]kongstate.Service{"2": {Namespace: "carrot"}}, + ServiceNameToParent: map[string]client.Object{}, }, }, wantOutput: &ingressRules{ SecretNameToSNIs: makeSecretToSNIs(map[string]testSNIs{"a": {hosts: []string{"b", "c"}}, "d": {hosts: []string{"e", "f"}}, "g": {hosts: []string{"h"}}}), ServiceNameToServices: map[string]kongstate.Service{"1": {Namespace: "potato"}, "2": {Namespace: "carrot"}}, + ServiceNameToParent: map[string]client.Object{}, }, }, { @@ -108,6 +114,7 @@ func TestMergeIngressRules(t *testing.T) { wantOutput: &ingressRules{ SecretNameToSNIs: makeSecretToSNIs(map[string]testSNIs{"a": {hosts: []string{"b", "c", "d", "e"}}}), ServiceNameToServices: map[string]kongstate.Service{}, + ServiceNameToParent: map[string]client.Object{}, }, }, { @@ -119,6 +126,7 @@ func TestMergeIngressRules(t *testing.T) { wantOutput: &ingressRules{ SecretNameToSNIs: makeSecretToSNIs(map[string]testSNIs{"a": {parents: []client.Object{parent1, parent2}, hosts: []string{"b", "c", "d", "e"}}}), ServiceNameToServices: map[string]kongstate.Service{}, + ServiceNameToParent: map[string]client.Object{}, }, }, { @@ -130,6 +138,7 @@ func TestMergeIngressRules(t *testing.T) { wantOutput: &ingressRules{ SecretNameToSNIs: makeSecretToSNIs(map[string]testSNIs{"a": {parents: []client.Object{parent1, parent2, parent1, parent2}, hosts: []string{"b", "c", "d", "e"}}}), ServiceNameToServices: map[string]kongstate.Service{}, + ServiceNameToParent: map[string]client.Object{}, }, }, { @@ -145,6 +154,7 @@ func TestMergeIngressRules(t *testing.T) { wantOutput: &ingressRules{ SecretNameToSNIs: newSecretNameToSNIs(), ServiceNameToServices: map[string]kongstate.Service{"svc-name": {Namespace: "new"}}, + ServiceNameToParent: map[string]client.Object{}, }, }, } { diff --git a/internal/dataplane/parser/parser.go b/internal/dataplane/parser/parser.go index 3242505c0e..bd43cf58bb 100644 --- a/internal/dataplane/parser/parser.go +++ b/internal/dataplane/parser/parser.go @@ -347,6 +347,7 @@ func (p *Parser) getUpstreams(serviceMap map[string]kongstate.Service) []kongsta upstream := kongstate.Upstream{ Upstream: kong.Upstream{ Name: kong.String(name), + Tags: service.Tags, // populated by populateServices already }, Service: service, Targets: targets, @@ -470,6 +471,7 @@ func (p *Parser) getGatewayCerts() []certWrapper { ID: kong.String(string(secret.UID)), Cert: kong.String(cert), Key: kong.String(key), + Tags: util.GenerateTagsForObject(secret), }, CreationTimestamp: secret.CreationTimestamp, snis: []string{hostname}, @@ -503,6 +505,7 @@ func (p *Parser) getCerts(secretsToSNIs SecretNameToSNIs) []certWrapper { ID: kong.String(string(secret.UID)), Cert: kong.String(cert), Key: kong.String(key), + Tags: util.GenerateTagsForObject(secret), }, CreationTimestamp: secret.CreationTimestamp, snis: SNIs.Hosts(), diff --git a/internal/dataplane/parser/parser_test.go b/internal/dataplane/parser/parser_test.go index df8307cbc8..75cc7750f2 100644 --- a/internal/dataplane/parser/parser_test.go +++ b/internal/dataplane/parser/parser_test.go @@ -865,6 +865,8 @@ func TestCACertificate(t *testing.T) { assert.NotNil(state) assert.Equal(1, len(state.CACertificates)) + // parser tests do not check tags, these are tested independently + state.CACertificates[0].Tags = nil assert.Equal(kong.CACertificate{ ID: kong.String("8214a145-a328-4c56-ab72-2973a56d4eae"), Cert: kong.String(caCert1), @@ -1001,6 +1003,8 @@ func TestCACertificate(t *testing.T) { assert.NotNil(state) assert.Equal(1, len(state.CACertificates)) + // parser tests do not check tags, these are tested independently + state.CACertificates[0].Tags = nil assert.Equal(kong.CACertificate{ ID: kong.String("8214a145-a328-4c56-ab72-2973a56d4eae"), Cert: kong.String(caCert1), @@ -1216,6 +1220,8 @@ func TestKongRouteAnnotations(t *testing.T) { assert.Equal(1, len(state.Services), "expected one service to be rendered") + // parser tests do not check tags, these are tested independently + state.Services[0].Service.Tags = nil assert.Equal(kong.Service{ Name: kong.String("default.foo-svc.80"), Host: kong.String("foo-svc.default.80.svc"), @@ -1230,6 +1236,8 @@ func TestKongRouteAnnotations(t *testing.T) { assert.Equal(1, len(state.Services[0].Routes), "expected one route to be rendered") + // parser tests do not check tags, these are tested independently + state.Services[0].Routes[0].Route.Tags = nil assert.Equal(kong.Route{ Name: kong.String("default.bar.00"), StripPath: kong.Bool(true), @@ -1296,6 +1304,8 @@ func TestKongRouteAnnotations(t *testing.T) { assert.Equal(1, len(state.Services), "expected one service to be rendered") + // parser tests do not check tags, these are tested independently + state.Services[0].Service.Tags = nil assert.Equal(kong.Service{ Name: kong.String("default.foo-svc.80"), Host: kong.String("foo-svc.default.80.svc"), @@ -1310,6 +1320,8 @@ func TestKongRouteAnnotations(t *testing.T) { assert.Equal(1, len(state.Services[0].Routes), "expected one route to be rendered") + // parser tests do not check tags, these are tested independently + state.Services[0].Routes[0].Route.Tags = nil assert.Equal(kong.Route{ Name: kong.String("default.bar.00"), StripPath: kong.Bool(false), @@ -1377,6 +1389,8 @@ func TestKongRouteAnnotations(t *testing.T) { assert.Equal(1, len(state.Services), "expected one service to be rendered") + // parser tests do not check tags, these are tested independently + state.Services[0].Service.Tags = nil assert.Equal(kong.Service{ Name: kong.String("default.foo-svc.80"), Host: kong.String("foo-svc.default.80.svc"), @@ -1389,6 +1403,8 @@ func TestKongRouteAnnotations(t *testing.T) { Protocol: kong.String("http"), }, state.Services[0].Service) + // parser tests do not check tags, these are tested independently + state.Services[0].Routes[0].Route.Tags = nil assert.Equal(1, len(state.Services[0].Routes), "expected one route to be rendered") assert.Equal(kong.Route{ @@ -1459,6 +1475,8 @@ func TestKongRouteAnnotations(t *testing.T) { assert.Equal(1, len(state.Services), "expected one service to be rendered") + // parser tests do not check tags, these are tested independently + state.Services[0].Service.Tags = nil assert.Equal(kong.Service{ Name: kong.String("default.foo-svc.80"), Host: kong.String("foo-svc.default.80.svc"), @@ -1473,6 +1491,8 @@ func TestKongRouteAnnotations(t *testing.T) { assert.Equal(1, len(state.Services[0].Routes), "expected one route to be rendered") + // parser tests do not check tags, these are tested independently + state.Services[0].Routes[0].Route.Tags = nil assert.Equal(kong.Route{ Name: kong.String("default.bar.00"), StripPath: kong.Bool(false), @@ -1540,6 +1560,8 @@ func TestKongRouteAnnotations(t *testing.T) { assert.Equal(1, len(state.Services), "expected one service to be rendered") + // parser tests do not check tags, these are tested independently + state.Services[0].Service.Tags = nil assert.Equal(kong.Service{ Name: kong.String("default.foo-svc.80"), Host: kong.String("foo-svc.default.80.svc"), @@ -1554,6 +1576,8 @@ func TestKongRouteAnnotations(t *testing.T) { assert.Equal(1, len(state.Services[0].Routes), "expected one route to be rendered") + // parser tests do not check tags, these are tested independently + state.Services[0].Routes[0].Route.Tags = nil assert.Equal(kong.Route{ Name: kong.String("default.bar.00"), StripPath: kong.Bool(false), @@ -1621,6 +1645,8 @@ func TestKongRouteAnnotations(t *testing.T) { assert.Equal(1, len(state.Services), "expected one service to be rendered") + // parser tests do not check tags, these are tested independently + state.Services[0].Service.Tags = nil assert.Equal(kong.Service{ Name: kong.String("default.foo-svc.80"), Host: kong.String("foo-svc.default.80.svc"), @@ -1635,6 +1661,8 @@ func TestKongRouteAnnotations(t *testing.T) { assert.Equal(1, len(state.Services[0].Routes), "expected one route to be rendered") + // parser tests do not check tags, these are tested independently + state.Services[0].Routes[0].Route.Tags = nil assert.Equal(kong.Route{ Name: kong.String("default.bar.00"), StripPath: kong.Bool(false), @@ -1702,6 +1730,8 @@ func TestKongRouteAnnotations(t *testing.T) { assert.Equal(1, len(state.Services), "expected one service to be rendered") + // parser tests do not check tags, these are tested independently + state.Services[0].Service.Tags = nil assert.Equal(kong.Service{ Name: kong.String("default.foo-svc.80"), Host: kong.String("foo-svc.default.80.svc"), @@ -1716,6 +1746,8 @@ func TestKongRouteAnnotations(t *testing.T) { assert.Equal(1, len(state.Services[0].Routes), "expected one route to be rendered") + // parser tests do not check tags, these are tested independently + state.Services[0].Routes[0].Route.Tags = nil assert.Equal(kong.Route{ Name: kong.String("default.bar.00"), StripPath: kong.Bool(false), @@ -1783,6 +1815,8 @@ func TestKongRouteAnnotations(t *testing.T) { assert.Equal(1, len(state.Services), "expected one service to be rendered") + // parser tests do not check tags, these are tested independently + state.Services[0].Service.Tags = nil assert.Equal(kong.Service{ Name: kong.String("default.foo-svc.80"), Host: kong.String("foo-svc.default.80.svc"), @@ -1797,6 +1831,8 @@ func TestKongRouteAnnotations(t *testing.T) { assert.Equal(1, len(state.Services[0].Routes), "expected one route to be rendered") + // parser tests do not check tags, these are tested independently + state.Services[0].Routes[0].Route.Tags = nil assert.Equal(kong.Route{ Name: kong.String("default.bar.00"), StripPath: kong.Bool(false), @@ -1863,6 +1899,8 @@ func TestKongRouteAnnotations(t *testing.T) { assert.NotNil(state) assert.Equal(1, len(state.Services), "expected one service to be rendered") + // parser tests do not check tags, these are tested independently + state.Services[0].Service.Tags = nil assert.Equal(kong.Service{ Name: kong.String("default.foo-svc.80"), Host: kong.String("foo-svc.default.80.svc"), @@ -1876,6 +1914,8 @@ func TestKongRouteAnnotations(t *testing.T) { }, state.Services[0].Service) assert.Equal(1, len(state.Services[0].Routes), "expected one route to be rendered") + // parser tests do not check tags, these are tested independently + state.Services[0].Routes[0].Route.Tags = nil assert.Equal(kong.Route{ Name: kong.String("default.route-buffering-test.00"), StripPath: kong.Bool(false), @@ -1942,6 +1982,8 @@ func TestKongRouteAnnotations(t *testing.T) { assert.NotNil(state) assert.Equal(1, len(state.Services), "expected one service to be rendered") + // parser tests do not check tags, these are tested independently + state.Services[0].Service.Tags = nil assert.Equal(kong.Service{ Name: kong.String("default.foo-svc.80"), Host: kong.String("foo-svc.default.80.svc"), @@ -1955,6 +1997,8 @@ func TestKongRouteAnnotations(t *testing.T) { }, state.Services[0].Service) assert.Equal(1, len(state.Services[0].Routes), "expected one route to be rendered") + // parser tests do not check tags, these are tested independently + state.Services[0].Routes[0].Route.Tags = nil assert.Equal(kong.Route{ Name: kong.String("default.route-buffering-test.00"), StripPath: kong.Bool(false), @@ -2447,6 +2491,8 @@ func TestKnativeIngressAndPlugins(t *testing.T) { assert.Equal(1, len(state.Services), "expected one service to be rendered") + // parser tests do not check tags, these are tested independently + state.Services[0].Service.Tags = nil svc := state.Services[0] assert.Equal(kong.Service{ @@ -2473,6 +2519,8 @@ func TestKnativeIngressAndPlugins(t *testing.T) { assert.Equal(1, len(svc.Routes), "expected one route to be rendered") + // parser tests do not check tags, these are tested independently + svc.Routes[0].Route.Tags = nil assert.Equal(kong.Route{ Name: kong.String("foo-ns.knative-ingress.00"), StripPath: kong.Bool(false), @@ -2486,6 +2534,8 @@ func TestKnativeIngressAndPlugins(t *testing.T) { }, svc.Routes[0].Route) assert.Equal(1, len(state.Plugins), "expected one key-auth plugin") + // parser tests do not check tags, these are tested independently + state.Plugins[0].Plugin.Tags = nil assert.Equal(kong.Plugin{ Name: kong.String("key-auth"), Config: kong.Configuration{ @@ -2558,6 +2608,8 @@ func TestKongServiceAnnotations(t *testing.T) { assert.Equal(1, len(state.Services), "expected one service to be rendered") + // parser tests do not check tags, these are tested independently + state.Services[0].Service.Tags = nil assert.Equal(kong.Service{ Name: kong.String("default.foo-svc.80"), Host: kong.String("foo-svc.default.80.svc"), @@ -2572,6 +2624,8 @@ func TestKongServiceAnnotations(t *testing.T) { assert.Equal(1, len(state.Services[0].Routes), "expected one route to be rendered") + // parser tests do not check tags, these are tested independently + state.Services[0].Routes[0].Route.Tags = nil assert.Equal(kong.Route{ Name: kong.String("default.bar.00"), StripPath: kong.Bool(false), @@ -2641,6 +2695,8 @@ func TestKongServiceAnnotations(t *testing.T) { assert.Equal(1, len(state.Services), "expected one service to be rendered") + // parser tests do not check tags, these are tested independently + state.Services[0].Service.Tags = nil assert.Equal(kong.Service{ Name: kong.String("default.foo-svc.80"), Host: kong.String("foo-svc.default.80.svc"), @@ -2655,6 +2711,8 @@ func TestKongServiceAnnotations(t *testing.T) { assert.Equal(1, len(state.Upstreams), "expected one upstream to be rendered") + // parser tests do not check tags, these are tested independently + state.Upstreams[0].Upstream.Tags = nil assert.Equal(kong.Upstream{ Name: kong.String("foo-svc.default.80.svc"), HostHeader: kong.String("example.com"), @@ -2662,6 +2720,8 @@ func TestKongServiceAnnotations(t *testing.T) { assert.Equal(1, len(state.Services[0].Routes), "expected one route to be rendered") + // parser tests do not check tags, these are tested independently + state.Services[0].Routes[0].Route.Tags = nil assert.Equal(kong.Route{ Name: kong.String("default.bar.00"), StripPath: kong.Bool(false), @@ -2730,6 +2790,8 @@ func TestKongServiceAnnotations(t *testing.T) { assert.Equal(1, len(state.Services), "expected one service to be rendered") + // parser tests do not check tags, these are tested independently + state.Services[0].Service.Tags = nil assert.Equal(kong.Service{ Name: kong.String("default.foo-svc.80"), Host: kong.String("foo-svc.default.80.svc"), @@ -2744,6 +2806,8 @@ func TestKongServiceAnnotations(t *testing.T) { assert.Equal(1, len(state.Services[0].Routes), "expected one route to be rendered") + // parser tests do not check tags, these are tested independently + state.Services[0].Routes[0].Route.Tags = nil assert.Equal(kong.Route{ Name: kong.String("default.bar.00"), StripPath: kong.Bool(false), @@ -3022,6 +3086,8 @@ func TestParserSecret(t *testing.T) { return strings.Compare(*state.Certificates[0].SNIs[i], *state.Certificates[0].SNIs[j]) > 0 }) + // parser tests do not check tags, these are tested independently + state.Certificates[0].Tags = nil assert.Equal(kongstate.Certificate{ Certificate: kong.Certificate{ ID: kong.String("3e8edeca-7d23-4e02-84c9-437d11b746a6"), @@ -3149,6 +3215,8 @@ func TestParserSecret(t *testing.T) { return strings.Compare(*state.Certificates[0].SNIs[i], *state.Certificates[0].SNIs[j]) > 0 }) + // parser tests do not check tags, these are tested independently + state.Certificates[0].Tags = nil assert.Equal(kongstate.Certificate{ Certificate: kong.Certificate{ ID: kong.String("2c28a22c-41e1-4cd6-9099-fd7756ffe58e"), @@ -3307,6 +3375,9 @@ func TestParserSNI(t *testing.T) { state, translationFailures := p.Build() require.Empty(t, translationFailures) assert.NotNil(state) + // parser tests do not check tags, these are tested independently + state.Services[0].Routes[0].Route.Tags = nil + state.Services[0].Routes[1].Route.Tags = nil assert.Equal(kong.Route{ Name: kong.String("default.foo.00"), StripPath: kong.Bool(false), @@ -3372,6 +3443,8 @@ func TestParserSNI(t *testing.T) { state, translationFailures := p.Build() require.Empty(t, translationFailures) assert.NotNil(state) + // parser tests do not check tags, these are tested independently + state.Services[0].Routes[0].Route.Tags = nil assert.Equal(kong.Route{ Name: kong.String("default.foo.00"), StripPath: kong.Bool(false), @@ -3432,6 +3505,8 @@ func TestParserHostAliases(t *testing.T) { state, translationFailures := p.Build() require.Empty(t, translationFailures) assert.NotNil(state) + // parser tests do not check tags, these are tested independently + state.Services[0].Routes[0].Route.Tags = nil assert.Equal(kong.Route{ Name: kong.String("default.foo.00"), StripPath: kong.Bool(false), @@ -3485,6 +3560,8 @@ func TestParserHostAliases(t *testing.T) { state, translationFailures := p.Build() require.Empty(t, translationFailures) assert.NotNil(state) + // parser tests do not check tags, these are tested independently + state.Services[0].Routes[0].Route.Tags = nil assert.Equal(kong.Route{ Name: kong.String("default.foo.00"), StripPath: kong.Bool(false), @@ -3539,6 +3616,8 @@ func TestParserHostAliases(t *testing.T) { state, translationFailures := p.Build() require.Empty(t, translationFailures) assert.NotNil(state) + // parser tests do not check tags, these are tested independently + state.Services[0].Routes[0].Route.Tags = nil assert.Equal(kong.Route{ Name: kong.String("default.foo.00"), StripPath: kong.Bool(false), @@ -3633,6 +3712,8 @@ func TestPluginAnnotations(t *testing.T) { "expected no plugins to be rendered with missing plugin") pl := state.Plugins[0].Plugin pl.Route = nil + // parser tests do not check tags, these are tested independently + pl.Tags = nil assert.Equal(pl, kong.Plugin{ Name: kong.String("key-auth"), Protocols: kong.StringSlice("grpc"), @@ -4731,6 +4812,11 @@ func TestCertificate(t *testing.T) { Cert: kong.String(tlsPairs[0].Cert), Key: kong.String(tlsPairs[0].Key), SNIs: []*string{kong.String("foo.com")}, + Tags: []*string{ + kong.String("k8s-name:secret1"), + kong.String("k8s-namespace:ns1"), + kong.String("k8s-uid:7428fb98-180b-4702-a91f-61351a33c6e4"), + }, }, } store, err := store.NewFakeStore(store.FakeObjects{ @@ -4836,6 +4922,8 @@ func TestCertificate(t *testing.T) { require.Empty(t, translationFailures) assert.NotNil(state) assert.Equal(1, len(state.Certificates)) + // parser tests do not check tags, these are tested independently + state.Certificates[0].Tags = nil assert.Equal(state.Certificates[0], fooCertificate) }) } diff --git a/internal/dataplane/parser/translate_httproute.go b/internal/dataplane/parser/translate_httproute.go index 076d0d2724..515204f1b1 100644 --- a/internal/dataplane/parser/translate_httproute.go +++ b/internal/dataplane/parser/translate_httproute.go @@ -96,6 +96,7 @@ func (p *Parser) ingressRulesFromHTTPRouteWithCombinedServiceRoutes(httproute *g // cache the service to avoid duplicates in further loop iterations result.ServiceNameToServices[*service.Service.Name] = service + result.ServiceNameToParent[serviceName] = httproute } return nil @@ -126,6 +127,7 @@ func (p *Parser) ingressRulesFromHTTPRouteLegacyFallback(httproute *gatewayv1bet // cache the service to avoid duplicates in further loop iterations result.ServiceNameToServices[*service.Service.Name] = service + result.ServiceNameToParent[*service.Service.Name] = httproute } return nil } @@ -161,6 +163,7 @@ func generateKongRoutesFromHTTPRouteRule( // gather the k8s object information and hostnames from the httproute objectInfo := util.FromK8sObject(httproute) hostnames := getHTTPRouteHostnamesAsSliceOfStringPointers(httproute) + tags := util.GenerateTagsForObject(httproute) // the HTTPRoute specification upstream specifically defines matches as // independent (e.g. each match is an OR with other matches, not an AND). @@ -169,7 +172,7 @@ func generateKongRoutesFromHTTPRouteRule( var routes []kongstate.Route // generate kong plugins from rule.filters - plugins := generatePluginsFromHTTPRouteFilters(rule.Filters) + plugins := generatePluginsFromHTTPRouteFilters(rule.Filters, tags) if len(rule.Matches) > 0 { for matchNumber := range rule.Matches { @@ -190,6 +193,7 @@ func generateKongRoutesFromHTTPRouteRule( hostnames, plugins, addRegexPrefix, + tags, ) if err != nil { return nil, err @@ -200,7 +204,8 @@ func generateKongRoutesFromHTTPRouteRule( } } else { routeName := fmt.Sprintf("httproute.%s.%s.0.0", httproute.Namespace, httproute.Name) - r, err := generateKongRouteFromHTTPRouteMatches(routeName, rule.Matches, objectInfo, hostnames, plugins, addRegexPrefix) + r, err := generateKongRouteFromHTTPRouteMatches(routeName, rule.Matches, objectInfo, hostnames, plugins, + addRegexPrefix, tags) if err != nil { return nil, err } @@ -219,12 +224,13 @@ func generateKongRouteFromTranslation( ) (kongstate.Route, error) { // gather the k8s object information and hostnames from the httproute objectInfo := util.FromK8sObject(httproute) + tags := util.GenerateTagsForObject(httproute) // get the hostnames from the HTTPRoute hostnames := getHTTPRouteHostnamesAsSliceOfStringPointers(httproute) // generate kong plugins from rule.filters - plugins := generatePluginsFromHTTPRouteFilters(translation.Filters) + plugins := generatePluginsFromHTTPRouteFilters(translation.Filters, tags) return generateKongRouteFromHTTPRouteMatches( translation.Name, @@ -233,6 +239,7 @@ func generateKongRouteFromTranslation( hostnames, plugins, addRegexPrefix, + tags, ) } @@ -245,6 +252,7 @@ func generateKongRouteFromHTTPRouteMatches( hostnames []*string, plugins []kong.Plugin, addRegexPrefix bool, + tags []*string, ) (kongstate.Route, error) { if len(matches) == 0 { // it's acceptable for an HTTPRoute to have no matches in the rulesets, @@ -257,6 +265,7 @@ func generateKongRouteFromHTTPRouteMatches( Name: kong.String(routeName), Protocols: kong.StringSlice("http", "https"), PreserveHost: kong.Bool(true), + Tags: tags, }, } @@ -281,6 +290,7 @@ func generateKongRouteFromHTTPRouteMatches( } r := generateKongstateRoute(routeName, ingressObjectInfo, hostnames) + r.Tags = tags // convert header matching from HTTPRoute to Route format headers, err := convertGatewayMatchHeadersToKongRouteMatchHeaders(matches[0].Headers) @@ -360,6 +370,7 @@ func generateKongstateRoute(routeName string, ingressObjectInfo util.K8sObjectIn Name: kong.String(routeName), Protocols: kong.StringSlice("http", "https"), PreserveHost: kong.Bool(true), + // metadata tags aren't added here, they're added by the caller }, } @@ -372,7 +383,7 @@ func generateKongstateRoute(routeName string, ingressObjectInfo util.K8sObjectIn } // generatePluginsFromHTTPRouteFilters converts HTTPRouteFilter into Kong filters. -func generatePluginsFromHTTPRouteFilters(filters []gatewayv1beta1.HTTPRouteFilter) []kong.Plugin { +func generatePluginsFromHTTPRouteFilters(filters []gatewayv1beta1.HTTPRouteFilter, tags []*string) []kong.Plugin { kongPlugins := make([]kong.Plugin, 0) if len(filters) == 0 { return kongPlugins @@ -384,6 +395,9 @@ func generatePluginsFromHTTPRouteFilters(filters []gatewayv1beta1.HTTPRouteFilte } // TODO: https://github.com/Kong/kubernetes-ingress-controller/issues/2793 } + for _, p := range kongPlugins { + p.Tags = tags + } return kongPlugins } diff --git a/internal/dataplane/parser/translate_httproute_test.go b/internal/dataplane/parser/translate_httproute_test.go index 055d871398..1687efdc40 100644 --- a/internal/dataplane/parser/translate_httproute_test.go +++ b/internal/dataplane/parser/translate_httproute_test.go @@ -10,6 +10,7 @@ import ( corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/client" gatewayv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1" "github.com/kong/kubernetes-ingress-controller/v2/internal/dataplane/kongstate" @@ -42,6 +43,7 @@ func getIngressRulesFromHTTPRoutesCommonTestCases() []testCaseIngressRulesFromHT expected: func(routes []*gatewayv1beta1.HTTPRoute) ingressRules { return ingressRules{ SecretNameToSNIs: newSecretNameToSNIs(), + ServiceNameToParent: map[string]client.Object{}, ServiceNameToServices: make(map[string]kongstate.Service), } }, @@ -69,6 +71,9 @@ func getIngressRulesFromHTTPRoutesCommonTestCases() []testCaseIngressRulesFromHT expected: func(routes []*gatewayv1beta1.HTTPRoute) ingressRules { return ingressRules{ SecretNameToSNIs: newSecretNameToSNIs(), + ServiceNameToParent: map[string]client.Object{ + "httproute.default.basic-httproute.0": routes[0], + }, ServiceNameToServices: map[string]kongstate.Service{ "httproute.default.basic-httproute.0": { Service: kong.Service{ // only 1 service should be created @@ -96,6 +101,13 @@ func getIngressRulesFromHTTPRoutesCommonTestCases() []testCaseIngressRulesFromHT kong.String("konghq.com"), kong.String("www.konghq.com"), }, + Tags: []*string{ + kong.String("k8s-name:basic-httproute"), + kong.String("k8s-namespace:default"), + kong.String("k8s-kind:HTTPRoute"), + kong.String("k8s-group:gateway.networking.k8s.io"), + kong.String("k8s-version:v1beta1"), + }, }, Ingress: k8sObjectInfoOfHTTPRoute(routes[0]), }}, @@ -126,6 +138,7 @@ func getIngressRulesFromHTTPRoutesCommonTestCases() []testCaseIngressRulesFromHT expected: func(routes []*gatewayv1beta1.HTTPRoute) ingressRules { return ingressRules{ SecretNameToSNIs: newSecretNameToSNIs(), + ServiceNameToParent: map[string]client.Object{}, ServiceNameToServices: make(map[string]kongstate.Service), } }, @@ -155,6 +168,9 @@ func getIngressRulesFromHTTPRoutesCommonTestCases() []testCaseIngressRulesFromHT expected: func(routes []*gatewayv1beta1.HTTPRoute) ingressRules { return ingressRules{ SecretNameToSNIs: newSecretNameToSNIs(), + ServiceNameToParent: map[string]client.Object{ + "httproute.default.basic-httproute.0": routes[0], + }, ServiceNameToServices: map[string]kongstate.Service{ "httproute.default.basic-httproute.0": { Service: kong.Service{ // only 1 service should be created @@ -182,6 +198,13 @@ func getIngressRulesFromHTTPRoutesCommonTestCases() []testCaseIngressRulesFromHT kong.String("https"), }, StripPath: lo.ToPtr(false), + Tags: []*string{ + kong.String("k8s-name:basic-httproute"), + kong.String("k8s-namespace:default"), + kong.String("k8s-kind:HTTPRoute"), + kong.String("k8s-group:gateway.networking.k8s.io"), + kong.String("k8s-version:v1beta1"), + }, }, Ingress: k8sObjectInfoOfHTTPRoute(routes[0]), }}, @@ -205,6 +228,7 @@ func getIngressRulesFromHTTPRoutesCommonTestCases() []testCaseIngressRulesFromHT expected: func(routes []*gatewayv1beta1.HTTPRoute) ingressRules { return ingressRules{ SecretNameToSNIs: newSecretNameToSNIs(), + ServiceNameToParent: map[string]client.Object{}, ServiceNameToServices: make(map[string]kongstate.Service), } }, @@ -234,6 +258,7 @@ func getIngressRulesFromHTTPRoutesCommonTestCases() []testCaseIngressRulesFromHT expected: func(routes []*gatewayv1beta1.HTTPRoute) ingressRules { return ingressRules{ SecretNameToSNIs: newSecretNameToSNIs(), + ServiceNameToParent: map[string]client.Object{}, ServiceNameToServices: make(map[string]kongstate.Service), } }, @@ -263,6 +288,9 @@ func getIngressRulesFromHTTPRoutesCommonTestCases() []testCaseIngressRulesFromHT expected: func(routes []*gatewayv1beta1.HTTPRoute) ingressRules { return ingressRules{ SecretNameToSNIs: newSecretNameToSNIs(), + ServiceNameToParent: map[string]client.Object{ + "httproute.default.basic-httproute.0": routes[0], + }, ServiceNameToServices: map[string]kongstate.Service{ "httproute.default.basic-httproute.0": { Service: kong.Service{ // only 1 service should be created @@ -290,6 +318,13 @@ func getIngressRulesFromHTTPRoutesCommonTestCases() []testCaseIngressRulesFromHT kong.String("https"), }, StripPath: lo.ToPtr(false), + Tags: []*string{ + kong.String("k8s-name:basic-httproute"), + kong.String("k8s-namespace:default"), + kong.String("k8s-kind:HTTPRoute"), + kong.String("k8s-group:gateway.networking.k8s.io"), + kong.String("k8s-version:v1beta1"), + }, }, Ingress: k8sObjectInfoOfHTTPRoute(routes[0]), }}, @@ -323,6 +358,9 @@ func getIngressRulesFromHTTPRoutesCommonTestCases() []testCaseIngressRulesFromHT expected: func(routes []*gatewayv1beta1.HTTPRoute) ingressRules { return ingressRules{ SecretNameToSNIs: newSecretNameToSNIs(), + ServiceNameToParent: map[string]client.Object{ + "httproute.default.basic-httproute.0": routes[0], + }, ServiceNameToServices: map[string]kongstate.Service{ "httproute.default.basic-httproute.0": { Service: kong.Service{ // only 1 service should be created @@ -350,6 +388,13 @@ func getIngressRulesFromHTTPRoutesCommonTestCases() []testCaseIngressRulesFromHT kong.String("https"), }, StripPath: lo.ToPtr(false), + Tags: []*string{ + kong.String("k8s-name:basic-httproute"), + kong.String("k8s-namespace:default"), + kong.String("k8s-kind:HTTPRoute"), + kong.String("k8s-group:gateway.networking.k8s.io"), + kong.String("k8s-version:v1beta1"), + }, }, Ingress: k8sObjectInfoOfHTTPRoute(routes[0]), }}, @@ -393,6 +438,9 @@ func getIngressRulesFromHTTPRoutesCombinedRoutesTestCases() []testCaseIngressRul expected: func(routes []*gatewayv1beta1.HTTPRoute) ingressRules { return ingressRules{ SecretNameToSNIs: newSecretNameToSNIs(), + ServiceNameToParent: map[string]client.Object{ + "httproute.default.basic-httproute.0": routes[0], + }, ServiceNameToServices: map[string]kongstate.Service{ "httproute.default.basic-httproute.0": { Service: kong.Service{ // only 1 service should be created @@ -423,6 +471,13 @@ func getIngressRulesFromHTTPRoutesCombinedRoutesTestCases() []testCaseIngressRul kong.String("https"), }, StripPath: lo.ToPtr(false), + Tags: []*string{ + kong.String("k8s-name:basic-httproute"), + kong.String("k8s-namespace:default"), + kong.String("k8s-kind:HTTPRoute"), + kong.String("k8s-group:gateway.networking.k8s.io"), + kong.String("k8s-version:v1beta1"), + }, }, Ingress: k8sObjectInfoOfHTTPRoute(routes[0]), }, @@ -468,6 +523,10 @@ func getIngressRulesFromHTTPRoutesCombinedRoutesTestCases() []testCaseIngressRul expected: func(routes []*gatewayv1beta1.HTTPRoute) ingressRules { return ingressRules{ SecretNameToSNIs: newSecretNameToSNIs(), + ServiceNameToParent: map[string]client.Object{ + "httproute.default.basic-httproute.0": routes[0], + "httproute.default.basic-httproute.1": routes[0], + }, ServiceNameToServices: map[string]kongstate.Service{ "httproute.default.basic-httproute.0": { Service: kong.Service{ // 1 service per route should be created @@ -495,6 +554,13 @@ func getIngressRulesFromHTTPRoutesCombinedRoutesTestCases() []testCaseIngressRul kong.String("https"), }, StripPath: lo.ToPtr(false), + Tags: []*string{ + kong.String("k8s-name:basic-httproute"), + kong.String("k8s-namespace:default"), + kong.String("k8s-kind:HTTPRoute"), + kong.String("k8s-group:gateway.networking.k8s.io"), + kong.String("k8s-version:v1beta1"), + }, }, Ingress: k8sObjectInfoOfHTTPRoute(routes[0]), }}, @@ -527,6 +593,13 @@ func getIngressRulesFromHTTPRoutesCombinedRoutesTestCases() []testCaseIngressRul kong.String("https"), }, StripPath: lo.ToPtr(false), + Tags: []*string{ + kong.String("k8s-name:basic-httproute"), + kong.String("k8s-namespace:default"), + kong.String("k8s-kind:HTTPRoute"), + kong.String("k8s-group:gateway.networking.k8s.io"), + kong.String("k8s-version:v1beta1"), + }, }, Ingress: k8sObjectInfoOfHTTPRoute(routes[0]), }}, @@ -582,6 +655,10 @@ func getIngressRulesFromHTTPRoutesCombinedRoutesTestCases() []testCaseIngressRul expected: func(routes []*gatewayv1beta1.HTTPRoute) ingressRules { return ingressRules{ SecretNameToSNIs: newSecretNameToSNIs(), + ServiceNameToParent: map[string]client.Object{ + "httproute.default.basic-httproute.0": routes[0], + "httproute.default.basic-httproute.2": routes[0], + }, ServiceNameToServices: map[string]kongstate.Service{ "httproute.default.basic-httproute.0": { Service: kong.Service{ @@ -612,6 +689,13 @@ func getIngressRulesFromHTTPRoutesCombinedRoutesTestCases() []testCaseIngressRul kong.String("https"), }, StripPath: lo.ToPtr(false), + Tags: []*string{ + kong.String("k8s-name:basic-httproute"), + kong.String("k8s-namespace:default"), + kong.String("k8s-kind:HTTPRoute"), + kong.String("k8s-group:gateway.networking.k8s.io"), + kong.String("k8s-version:v1beta1"), + }, }, Ingress: k8sObjectInfoOfHTTPRoute(routes[0]), }, @@ -647,6 +731,13 @@ func getIngressRulesFromHTTPRoutesCombinedRoutesTestCases() []testCaseIngressRul kong.String("https"), }, StripPath: lo.ToPtr(false), + Tags: []*string{ + kong.String("k8s-name:basic-httproute"), + kong.String("k8s-namespace:default"), + kong.String("k8s-kind:HTTPRoute"), + kong.String("k8s-group:gateway.networking.k8s.io"), + kong.String("k8s-version:v1beta1"), + }, }, Ingress: k8sObjectInfoOfHTTPRoute(routes[0]), }, @@ -712,6 +803,9 @@ func getIngressRulesFromHTTPRoutesCombinedRoutesTestCases() []testCaseIngressRul expected: func(routes []*gatewayv1beta1.HTTPRoute) ingressRules { return ingressRules{ SecretNameToSNIs: newSecretNameToSNIs(), + ServiceNameToParent: map[string]client.Object{ + "httproute.default.basic-httproute.0": routes[0], + }, ServiceNameToServices: map[string]kongstate.Service{ "httproute.default.basic-httproute.0": { Service: kong.Service{ // only 1 service should be created @@ -741,6 +835,13 @@ func getIngressRulesFromHTTPRoutesCombinedRoutesTestCases() []testCaseIngressRul kong.String("https"), }, StripPath: lo.ToPtr(false), + Tags: []*string{ + kong.String("k8s-name:basic-httproute"), + kong.String("k8s-namespace:default"), + kong.String("k8s-kind:HTTPRoute"), + kong.String("k8s-group:gateway.networking.k8s.io"), + kong.String("k8s-version:v1beta1"), + }, }, Ingress: k8sObjectInfoOfHTTPRoute(routes[0]), Plugins: []kong.Plugin{ @@ -766,6 +867,13 @@ func getIngressRulesFromHTTPRoutesCombinedRoutesTestCases() []testCaseIngressRul kong.String("https"), }, StripPath: lo.ToPtr(false), + Tags: []*string{ + kong.String("k8s-name:basic-httproute"), + kong.String("k8s-namespace:default"), + kong.String("k8s-kind:HTTPRoute"), + kong.String("k8s-group:gateway.networking.k8s.io"), + kong.String("k8s-version:v1beta1"), + }, }, Ingress: k8sObjectInfoOfHTTPRoute(routes[0]), Plugins: []kong.Plugin{ @@ -829,6 +937,9 @@ func getIngressRulesFromHTTPRoutesCombinedRoutesTestCases() []testCaseIngressRul expected: func(routes []*gatewayv1beta1.HTTPRoute) ingressRules { return ingressRules{ SecretNameToSNIs: newSecretNameToSNIs(), + ServiceNameToParent: map[string]client.Object{ + "httproute.default.basic-httproute.0": routes[0], + }, ServiceNameToServices: map[string]kongstate.Service{ "httproute.default.basic-httproute.0": { Service: kong.Service{ @@ -861,6 +972,13 @@ func getIngressRulesFromHTTPRoutesCombinedRoutesTestCases() []testCaseIngressRul kong.String("https"), }, StripPath: lo.ToPtr(false), + Tags: []*string{ + kong.String("k8s-name:basic-httproute"), + kong.String("k8s-namespace:default"), + kong.String("k8s-kind:HTTPRoute"), + kong.String("k8s-group:gateway.networking.k8s.io"), + kong.String("k8s-version:v1beta1"), + }, }, Ingress: k8sObjectInfoOfHTTPRoute(routes[0]), }, @@ -879,6 +997,13 @@ func getIngressRulesFromHTTPRoutesCombinedRoutesTestCases() []testCaseIngressRul }, StripPath: lo.ToPtr(false), Methods: []*string{kong.String("DELETE")}, + Tags: []*string{ + kong.String("k8s-name:basic-httproute"), + kong.String("k8s-namespace:default"), + kong.String("k8s-kind:HTTPRoute"), + kong.String("k8s-group:gateway.networking.k8s.io"), + kong.String("k8s-version:v1beta1"), + }, }, Ingress: k8sObjectInfoOfHTTPRoute(routes[0]), }, @@ -900,6 +1025,13 @@ func getIngressRulesFromHTTPRoutesCombinedRoutesTestCases() []testCaseIngressRul "x-header-1": {"x-value-1"}, "x-header-2": {"x-value-2"}, }, + Tags: []*string{ + kong.String("k8s-name:basic-httproute"), + kong.String("k8s-namespace:default"), + kong.String("k8s-kind:HTTPRoute"), + kong.String("k8s-group:gateway.networking.k8s.io"), + kong.String("k8s-version:v1beta1"), + }, }, Ingress: k8sObjectInfoOfHTTPRoute(routes[0]), }, @@ -982,6 +1114,9 @@ func getIngressRulesFromHTTPRoutesCombinedRoutesTestCases() []testCaseIngressRul expected: func(routes []*gatewayv1beta1.HTTPRoute) ingressRules { return ingressRules{ SecretNameToSNIs: newSecretNameToSNIs(), + ServiceNameToParent: map[string]client.Object{ + "httproute.default.basic-httproute.0": routes[0], + }, ServiceNameToServices: map[string]kongstate.Service{ "httproute.default.basic-httproute.0": { Service: kong.Service{ @@ -1015,6 +1150,13 @@ func getIngressRulesFromHTTPRoutesCombinedRoutesTestCases() []testCaseIngressRul kong.String("https"), }, StripPath: lo.ToPtr(false), + Tags: []*string{ + kong.String("k8s-name:basic-httproute"), + kong.String("k8s-namespace:default"), + kong.String("k8s-kind:HTTPRoute"), + kong.String("k8s-group:gateway.networking.k8s.io"), + kong.String("k8s-version:v1beta1"), + }, }, Ingress: k8sObjectInfoOfHTTPRoute(routes[0]), }, @@ -1033,6 +1175,13 @@ func getIngressRulesFromHTTPRoutesCombinedRoutesTestCases() []testCaseIngressRul }, StripPath: lo.ToPtr(false), Methods: []*string{kong.String("DELETE")}, + Tags: []*string{ + kong.String("k8s-name:basic-httproute"), + kong.String("k8s-namespace:default"), + kong.String("k8s-kind:HTTPRoute"), + kong.String("k8s-group:gateway.networking.k8s.io"), + kong.String("k8s-version:v1beta1"), + }, }, Ingress: k8sObjectInfoOfHTTPRoute(routes[0]), }, @@ -1051,6 +1200,13 @@ func getIngressRulesFromHTTPRoutesCombinedRoutesTestCases() []testCaseIngressRul kong.String("https"), }, StripPath: lo.ToPtr(false), + Tags: []*string{ + kong.String("k8s-name:basic-httproute"), + kong.String("k8s-namespace:default"), + kong.String("k8s-kind:HTTPRoute"), + kong.String("k8s-group:gateway.networking.k8s.io"), + kong.String("k8s-version:v1beta1"), + }, }, Ingress: k8sObjectInfoOfHTTPRoute(routes[0]), Plugins: []kong.Plugin{ @@ -1236,6 +1392,9 @@ func TestIngressRulesFromHTTPRoutes_RegexPrefix(t *testing.T) { expected: func(routes []*gatewayv1beta1.HTTPRoute) ingressRules { return ingressRules{ SecretNameToSNIs: newSecretNameToSNIs(), + ServiceNameToParent: map[string]client.Object{ + "httproute.default.basic-httproute.0": routes[0], + }, ServiceNameToServices: map[string]kongstate.Service{ "httproute.default.basic-httproute.0": { Service: kong.Service{ // only 1 service should be created @@ -1263,6 +1422,13 @@ func TestIngressRulesFromHTTPRoutes_RegexPrefix(t *testing.T) { kong.String("https"), }, StripPath: lo.ToPtr(false), + Tags: []*string{ + kong.String("k8s-name:basic-httproute"), + kong.String("k8s-namespace:default"), + kong.String("k8s-kind:HTTPRoute"), + kong.String("k8s-group:gateway.networking.k8s.io"), + kong.String("k8s-version:v1beta1"), + }, }, Ingress: k8sObjectInfoOfHTTPRoute(routes[0]), }}, diff --git a/internal/dataplane/parser/translate_ingress.go b/internal/dataplane/parser/translate_ingress.go index f0a57c1719..e9d901e9fc 100644 --- a/internal/dataplane/parser/translate_ingress.go +++ b/internal/dataplane/parser/translate_ingress.go @@ -83,6 +83,7 @@ func (p *Parser) ingressRulesFromIngressV1beta1() ingressRules { RegexPriority: kong.Int(0), RequestBuffering: kong.Bool(true), ResponseBuffering: kong.Bool(true), + Tags: util.GenerateTagsForObject(ingress), }, } if host != "" { @@ -119,6 +120,7 @@ func (p *Parser) ingressRulesFromIngressV1beta1() ingressRules { } service.Routes = append(service.Routes, r) result.ServiceNameToServices[serviceName] = service + result.ServiceNameToParent[serviceName] = ingress objectSuccessfullyParsed = true } } @@ -153,6 +155,7 @@ func (p *Parser) ingressRulesFromIngressV1beta1() ingressRules { ReadTimeout: kong.Int(DefaultServiceTimeout), WriteTimeout: kong.Int(DefaultServiceTimeout), Retries: kong.Int(DefaultRetries), + Tags: util.GenerateTagsForObject(result.ServiceNameToParent[serviceName]), }, Namespace: ingress.Namespace, Backends: []kongstate.ServiceBackend{{ @@ -173,10 +176,12 @@ func (p *Parser) ingressRulesFromIngressV1beta1() ingressRules { RegexPriority: kong.Int(0), RequestBuffering: kong.Bool(true), ResponseBuffering: kong.Bool(true), + Tags: util.GenerateTagsForObject(result.ServiceNameToParent[serviceName]), }, } service.Routes = append(service.Routes, r) result.ServiceNameToServices[serviceName] = service + result.ServiceNameToParent[serviceName] = &ingress } return result @@ -224,6 +229,7 @@ func (p *Parser) ingressRulesFromIngressV1() ingressRules { } } result.ServiceNameToServices[*kongStateService.Service.Name] = *kongStateService + result.ServiceNameToParent[*kongStateService.Service.Name] = ingress objectSuccessfullyParsed = true } } else { @@ -262,6 +268,7 @@ func (p *Parser) ingressRulesFromIngressV1() ingressRules { RegexPriority: kong.Int(priorityForPath[*rulePath.PathType]), RequestBuffering: kong.Bool(true), ResponseBuffering: kong.Bool(true), + Tags: util.GenerateTagsForObject(ingress), }, } if rule.Host != "" { @@ -296,6 +303,7 @@ func (p *Parser) ingressRulesFromIngressV1() ingressRules { } service.Routes = append(service.Routes, r) result.ServiceNameToServices[serviceName] = service + result.ServiceNameToParent[serviceName] = ingress objectSuccessfullyParsed = true } } @@ -330,6 +338,7 @@ func (p *Parser) ingressRulesFromIngressV1() ingressRules { ReadTimeout: kong.Int(DefaultServiceTimeout), WriteTimeout: kong.Int(DefaultServiceTimeout), Retries: kong.Int(DefaultRetries), + Tags: util.GenerateTagsForObject(result.ServiceNameToParent[serviceName]), }, Namespace: ingress.Namespace, Backends: []kongstate.ServiceBackend{{ @@ -350,10 +359,12 @@ func (p *Parser) ingressRulesFromIngressV1() ingressRules { RegexPriority: kong.Int(0), RequestBuffering: kong.Bool(true), ResponseBuffering: kong.Bool(true), + Tags: util.GenerateTagsForObject(result.ServiceNameToParent[serviceName]), }, } service.Routes = append(service.Routes, r) result.ServiceNameToServices[serviceName] = service + result.ServiceNameToParent[serviceName] = &ingress } return result diff --git a/internal/dataplane/parser/translate_ingress_test.go b/internal/dataplane/parser/translate_ingress_test.go index 30ab2d1168..f5a11ce9eb 100644 --- a/internal/dataplane/parser/translate_ingress_test.go +++ b/internal/dataplane/parser/translate_ingress_test.go @@ -9,6 +9,7 @@ import ( netv1beta1 "k8s.io/api/networking/v1beta1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/intstr" + "sigs.k8s.io/controller-runtime/pkg/client" "github.com/kong/kubernetes-ingress-controller/v2/internal/annotations" "github.com/kong/kubernetes-ingress-controller/v2/internal/dataplane/kongstate" @@ -274,6 +275,7 @@ func TestFromIngressV1beta1(t *testing.T) { parsedInfo := p.ingressRulesFromIngressV1beta1() assert.Equal(ingressRules{ ServiceNameToServices: make(map[string]kongstate.Service), + ServiceNameToParent: make(map[string]client.Object), SecretNameToSNIs: newSecretNameToSNIs(), }, parsedInfo) }) @@ -747,6 +749,7 @@ func TestFromIngressV1(t *testing.T) { parsedInfo := p.ingressRulesFromIngressV1() assert.Equal(ingressRules{ ServiceNameToServices: make(map[string]kongstate.Service), + ServiceNameToParent: make(map[string]client.Object), SecretNameToSNIs: newSecretNameToSNIs(), }, parsedInfo) }) diff --git a/internal/dataplane/parser/translate_knative.go b/internal/dataplane/parser/translate_knative.go index d60a92930e..94e7dca418 100644 --- a/internal/dataplane/parser/translate_knative.go +++ b/internal/dataplane/parser/translate_knative.go @@ -76,6 +76,7 @@ func (p *Parser) ingressRulesFromKnativeIngress() ingressRules { RegexPriority: kong.Int(0), RequestBuffering: kong.Bool(true), ResponseBuffering: kong.Bool(true), + Tags: util.GenerateTagsForObject(ingress), }, } r.Hosts = kong.StringSlice(hosts...) @@ -136,6 +137,9 @@ func (p *Parser) ingressRulesFromKnativeIngress() ingressRules { } } + // Knative handling is odd and doesn't update SNTS like other translators, and it does not get parent info as such. + // It shouldn't need parent info since it doesn't have any of the special cases (multi-service backends, default + // backend) that require parent info to populate Kubernetes resource tags on Kong services result.ServiceNameToServices = services result.SecretNameToSNIs = secretToSNIs return result diff --git a/internal/dataplane/parser/translate_knative_test.go b/internal/dataplane/parser/translate_knative_test.go index 3f7023f00b..aeeb41c823 100644 --- a/internal/dataplane/parser/translate_knative_test.go +++ b/internal/dataplane/parser/translate_knative_test.go @@ -272,6 +272,10 @@ func TestFromKnativeIngress(t *testing.T) { Hosts: kong.StringSlice("my-func.example.com"), ResponseBuffering: kong.Bool(true), RequestBuffering: kong.Bool(true), + Tags: []*string{ + kong.String("k8s-name:foo"), + kong.String("k8s-namespace:foo-namespace"), + }, }, svc.Routes[0].Route) assert.Equal(kong.Plugin{ Name: kong.String("request-transformer"), @@ -332,6 +336,10 @@ func TestFromKnativeIngress(t *testing.T) { Hosts: kong.StringSlice("my-func.example.com"), ResponseBuffering: kong.Bool(true), RequestBuffering: kong.Bool(true), + Tags: []*string{ + kong.String("k8s-name:foo"), + kong.String("k8s-namespace:foo-namespace"), + }, }, svc.Routes[0].Route) assert.Equal(kong.Plugin{ Name: kong.String("request-transformer"), diff --git a/internal/dataplane/parser/translate_kong_l4.go b/internal/dataplane/parser/translate_kong_l4.go index 205088d377..67b01e0690 100644 --- a/internal/dataplane/parser/translate_kong_l4.go +++ b/internal/dataplane/parser/translate_kong_l4.go @@ -42,6 +42,7 @@ func (p *Parser) ingressRulesFromTCPIngressV1beta1() ingressRules { Port: kong.Int(rule.Port), }, }, + Tags: util.GenerateTagsForObject(ingress), }, } host := rule.Host @@ -74,6 +75,7 @@ func (p *Parser) ingressRulesFromTCPIngressV1beta1() ingressRules { } service.Routes = append(service.Routes, r) result.ServiceNameToServices[serviceName] = service + result.ServiceNameToParent[serviceName] = ingress objectSuccessfullyParsed = true } @@ -110,6 +112,7 @@ func (p *Parser) ingressRulesFromUDPIngressV1beta1() ingressRules { Name: kong.String(ingress.Namespace + "." + ingress.Name + "." + strconv.Itoa(i) + ".udp"), Protocols: kong.StringSlice("udp"), Destinations: []*kong.CIDRPort{{Port: kong.Int(rule.Port)}}, + Tags: util.GenerateTagsForObject(ingress), }, } @@ -135,6 +138,7 @@ func (p *Parser) ingressRulesFromUDPIngressV1beta1() ingressRules { } service.Routes = append(service.Routes, route) result.ServiceNameToServices[serviceName] = service + result.ServiceNameToParent[serviceName] = ingress objectSuccessfullyParsed = true } diff --git a/internal/dataplane/parser/translate_kong_l4_test.go b/internal/dataplane/parser/translate_kong_l4_test.go index edcb3ff5bf..8f9e327d8d 100644 --- a/internal/dataplane/parser/translate_kong_l4_test.go +++ b/internal/dataplane/parser/translate_kong_l4_test.go @@ -6,6 +6,7 @@ import ( "github.com/kong/go-kong/kong" "github.com/stretchr/testify/assert" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" "github.com/kong/kubernetes-ingress-controller/v2/internal/annotations" "github.com/kong/kubernetes-ingress-controller/v2/internal/dataplane/kongstate" @@ -108,6 +109,7 @@ func TestFromTCPIngressV1beta1(t *testing.T) { parsedInfo := p.ingressRulesFromTCPIngressV1beta1() assert.Equal(ingressRules{ ServiceNameToServices: make(map[string]kongstate.Service), + ServiceNameToParent: make(map[string]client.Object), SecretNameToSNIs: newSecretNameToSNIs(), }, parsedInfo) }) @@ -123,6 +125,7 @@ func TestFromTCPIngressV1beta1(t *testing.T) { parsedInfo := p.ingressRulesFromTCPIngressV1beta1() assert.Equal(ingressRules{ ServiceNameToServices: make(map[string]kongstate.Service), + ServiceNameToParent: make(map[string]client.Object), SecretNameToSNIs: newSecretNameToSNIs(), }, parsedInfo) }) @@ -152,6 +155,10 @@ func TestFromTCPIngressV1beta1(t *testing.T) { Port: kong.Int(9000), }, }, + Tags: []*string{ + kong.String("k8s-name:foo"), + kong.String("k8s-namespace:default"), + }, }, route.Route) }) t.Run("TCPIngress rule with host is parsed", func(t *testing.T) { @@ -181,6 +188,10 @@ func TestFromTCPIngressV1beta1(t *testing.T) { Port: kong.Int(9000), }, }, + Tags: []*string{ + kong.String("k8s-name:foo"), + kong.String("k8s-namespace:default"), + }, }, route.Route) }) t.Run("TCPIngress with TLS", func(t *testing.T) { diff --git a/internal/dataplane/parser/translate_routes_helpers.go b/internal/dataplane/parser/translate_routes_helpers.go index 9e0ebc3bb9..b920b2fd74 100644 --- a/internal/dataplane/parser/translate_routes_helpers.go +++ b/internal/dataplane/parser/translate_routes_helpers.go @@ -70,10 +70,11 @@ func generateKongRoutesFromRouteRule[T tRoute, TRule tRouteRule]( return []kongstate.Route{}, err } + tags := util.GenerateTagsForObject(route) return []kongstate.Route{ { Ingress: util.FromK8sObject(route), - Route: routeToKongRoute(route, backendRefs, ruleNumber), + Route: routeToKongRoute(route, backendRefs, ruleNumber, tags), }, }, nil } @@ -83,17 +84,22 @@ func routeToKongRoute[TRoute tTCPorUDPorTLSRoute]( r TRoute, backendRefs []gatewayv1alpha2.BackendRef, ruleNumber int, + tags []*string, ) kong.Route { + var kr kong.Route switch rr := any(r).(type) { case *gatewayv1alpha2.UDPRoute: - return udpRouteToKongRoute(rr, backendRefs, ruleNumber) + kr = udpRouteToKongRoute(rr, backendRefs, ruleNumber) case *gatewayv1alpha2.TCPRoute: - return tcpRouteToKongRoute(rr, backendRefs, ruleNumber) + kr = tcpRouteToKongRoute(rr, backendRefs, ruleNumber) case *gatewayv1alpha2.TLSRoute: - return tlsRouteToKongRoute(rr, ruleNumber) + kr = tlsRouteToKongRoute(rr, ruleNumber) + default: + kr = kong.Route{} } - return kong.Route{} + kr.Tags = tags + return kr } func udpRouteToKongRoute( diff --git a/internal/dataplane/parser/translate_routes_helpers_test.go b/internal/dataplane/parser/translate_routes_helpers_test.go index f90480544d..90c1ba6362 100644 --- a/internal/dataplane/parser/translate_routes_helpers_test.go +++ b/internal/dataplane/parser/translate_routes_helpers_test.go @@ -55,6 +55,10 @@ func TestGenerateKongRoutesFromRouteRule_TCP(t *testing.T) { Protocols: []*string{ lo.ToPtr("tcp"), }, + Tags: []*string{ + kong.String("k8s-name:mytcproute-name"), + kong.String("k8s-namespace:mynamespace"), + }, }, }, }, @@ -116,6 +120,10 @@ func TestGenerateKongRoutesFromRouteRule_UDP(t *testing.T) { Protocols: []*string{ lo.ToPtr("udp"), }, + Tags: []*string{ + kong.String("k8s-name:myudproute-name"), + kong.String("k8s-namespace:mynamespace"), + }, }, }, }, @@ -174,6 +182,10 @@ func TestGenerateKongRoutesFromRouteRule_TLS(t *testing.T) { Protocols: []*string{ lo.ToPtr("tls"), }, + Tags: []*string{ + kong.String("k8s-name:mytlsroute-name"), + kong.String("k8s-namespace:mynamespace"), + }, }, }, }, @@ -201,6 +213,10 @@ func TestGenerateKongRoutesFromRouteRule_TLS(t *testing.T) { Protocols: []*string{ lo.ToPtr("tls"), }, + Tags: []*string{ + kong.String("k8s-name:mytlsroute-name"), + kong.String("k8s-namespace:mynamespace"), + }, }, }, }, diff --git a/internal/dataplane/parser/translate_secrets.go b/internal/dataplane/parser/translate_secrets.go index 8b558a98ad..23841fb9f1 100644 --- a/internal/dataplane/parser/translate_secrets.go +++ b/internal/dataplane/parser/translate_secrets.go @@ -14,6 +14,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "github.com/kong/kubernetes-ingress-controller/v2/internal/store" + "github.com/kong/kubernetes-ingress-controller/v2/internal/util" ) // getCACerts translates CA certificates Secrets to kong.CACertificates. It ensures every certificate's structure and @@ -71,6 +72,7 @@ func toKongCACertificate(certSecret *corev1.Secret, secretID string) (kong.CACer return kong.CACertificate{ ID: kong.String(secretID), Cert: kong.String(string(caCertbytes)), + Tags: util.GenerateTagsForObject(certSecret), }, nil } diff --git a/internal/dataplane/parser/translate_tcproute.go b/internal/dataplane/parser/translate_tcproute.go index 2f8589a046..b3a12c7f51 100644 --- a/internal/dataplane/parser/translate_tcproute.go +++ b/internal/dataplane/parser/translate_tcproute.go @@ -79,6 +79,7 @@ func (p *Parser) ingressRulesFromTCPRoute(result *ingressRules, tcproute *gatewa // cache the service to avoid duplicates in further loop iterations result.ServiceNameToServices[*service.Service.Name] = service + result.ServiceNameToParent[*service.Service.Name] = tcproute } return nil diff --git a/internal/dataplane/parser/translate_tlsroute.go b/internal/dataplane/parser/translate_tlsroute.go index e42322339b..fa701bc583 100644 --- a/internal/dataplane/parser/translate_tlsroute.go +++ b/internal/dataplane/parser/translate_tlsroute.go @@ -87,6 +87,7 @@ func (p *Parser) ingressRulesFromTLSRoute(result *ingressRules, tlsroute *gatewa // cache the service to avoid duplicates in further loop iterations result.ServiceNameToServices[*service.Service.Name] = service + result.ServiceNameToParent[*service.Service.Name] = tlsroute } return nil diff --git a/internal/dataplane/parser/translate_udproute.go b/internal/dataplane/parser/translate_udproute.go index 818d609804..9216611012 100644 --- a/internal/dataplane/parser/translate_udproute.go +++ b/internal/dataplane/parser/translate_udproute.go @@ -79,6 +79,7 @@ func (p *Parser) ingressRulesFromUDPRoute(result *ingressRules, udproute *gatewa // cache the service to avoid duplicates in further loop iterations result.ServiceNameToServices[*service.Service.Name] = service + result.ServiceNameToParent[*service.Service.Name] = udproute } return nil diff --git a/internal/dataplane/parser/translators/ingress.go b/internal/dataplane/parser/translators/ingress.go index 5016ee3918..b3eb96a30b 100644 --- a/internal/dataplane/parser/translators/ingress.go +++ b/internal/dataplane/parser/translators/ingress.go @@ -97,16 +97,21 @@ func (i *ingressTranslationIndex) add(ingress *netv1.Ingress) { } serviceName := httpIngressPath.Backend.Service.Name + // TODO KIC#3484 this does something screwy where you can get zero backends port := PortDefFromServiceBackendPort(&httpIngressPath.Backend.Service.Port) cacheKey := fmt.Sprintf("%s.%s.%s.%s.%s", ingress.Namespace, ingress.Name, ingressRule.Host, serviceName, port.CanonicalString()) meta, ok := i.cache[cacheKey] if !ok { meta = &ingressTranslationMeta{ - ingressHost: ingressRule.Host, - serviceName: serviceName, - servicePort: port, - addRegexPrefix: i.addRegexPrefix, + ingressNamespace: ingress.Namespace, + ingressName: ingress.Name, + ingressUID: string(ingress.UID), + ingressHost: ingressRule.Host, + ingressTags: util.GenerateTagsForObject(ingress), + serviceName: serviceName, + servicePort: port, + addRegexPrefix: i.addRegexPrefix, } } @@ -145,12 +150,16 @@ func (i *ingressTranslationIndex) translate() []*kongstate.Service { // ----------------------------------------------------------------------------- type ingressTranslationMeta struct { - parentIngress client.Object - ingressHost string - serviceName string - servicePort kongstate.PortDef - paths []netv1.HTTPIngressPath - addRegexPrefix bool + parentIngress client.Object + ingressNamespace string + ingressName string + ingressUID string + ingressHost string + ingressTags []*string + serviceName string + servicePort kongstate.PortDef + paths []netv1.HTTPIngressPath + addRegexPrefix bool } func (m *ingressTranslationMeta) translateIntoKongStateService(kongServiceName string, portDef kongstate.PortDef) *kongstate.Service { @@ -197,6 +206,7 @@ func (m *ingressTranslationMeta) translateIntoKongRoutes() *kongstate.Route { RegexPriority: kong.Int(0), RequestBuffering: kong.Bool(true), ResponseBuffering: kong.Bool(true), + Tags: m.ingressTags, }, } diff --git a/internal/dataplane/parser/translators/ingress_test.go b/internal/dataplane/parser/translators/ingress_test.go index ce99ab95a9..d8ed4bbd43 100644 --- a/internal/dataplane/parser/translators/ingress_test.go +++ b/internal/dataplane/parser/translators/ingress_test.go @@ -94,6 +94,7 @@ func TestTranslateIngress(t *testing.T) { StripPath: kong.Bool(false), ResponseBuffering: kong.Bool(true), RequestBuffering: kong.Bool(true), + Tags: kong.StringSlice("k8s-name:test-ingress", "k8s-namespace:default"), }, }}, Backends: []kongstate.ServiceBackend{{ @@ -164,6 +165,7 @@ func TestTranslateIngress(t *testing.T) { StripPath: kong.Bool(false), ResponseBuffering: kong.Bool(true), RequestBuffering: kong.Bool(true), + Tags: kong.StringSlice("k8s-name:test-ingress", "k8s-namespace:default"), }, }}, Backends: []kongstate.ServiceBackend{{ @@ -235,6 +237,7 @@ func TestTranslateIngress(t *testing.T) { StripPath: kong.Bool(false), ResponseBuffering: kong.Bool(true), RequestBuffering: kong.Bool(true), + Tags: kong.StringSlice("k8s-name:test-ingress", "k8s-namespace:default"), }, }}, Backends: []kongstate.ServiceBackend{{ @@ -306,6 +309,7 @@ func TestTranslateIngress(t *testing.T) { StripPath: kong.Bool(false), ResponseBuffering: kong.Bool(true), RequestBuffering: kong.Bool(true), + Tags: kong.StringSlice("k8s-name:test-ingress", "k8s-namespace:default"), }, }}, Backends: []kongstate.ServiceBackend{{ @@ -377,6 +381,7 @@ func TestTranslateIngress(t *testing.T) { StripPath: kong.Bool(false), ResponseBuffering: kong.Bool(true), RequestBuffering: kong.Bool(true), + Tags: kong.StringSlice("k8s-name:test-ingress", "k8s-namespace:default"), }, }}, Backends: []kongstate.ServiceBackend{{ @@ -447,6 +452,7 @@ func TestTranslateIngress(t *testing.T) { StripPath: kong.Bool(false), ResponseBuffering: kong.Bool(true), RequestBuffering: kong.Bool(true), + Tags: kong.StringSlice("k8s-name:test-ingress", "k8s-namespace:default"), }, }}, Backends: []kongstate.ServiceBackend{{ @@ -517,6 +523,7 @@ func TestTranslateIngress(t *testing.T) { StripPath: kong.Bool(false), ResponseBuffering: kong.Bool(true), RequestBuffering: kong.Bool(true), + Tags: kong.StringSlice("k8s-name:test-ingress", "k8s-namespace:default"), }, }}, Backends: []kongstate.ServiceBackend{{ @@ -659,6 +666,7 @@ func TestTranslateIngress(t *testing.T) { StripPath: kong.Bool(false), ResponseBuffering: kong.Bool(true), RequestBuffering: kong.Bool(true), + Tags: kong.StringSlice("k8s-name:test-ingress", "k8s-namespace:default"), }, }}, Backends: []kongstate.ServiceBackend{{ @@ -799,6 +807,7 @@ func TestTranslateIngress(t *testing.T) { StripPath: kong.Bool(false), ResponseBuffering: kong.Bool(true), RequestBuffering: kong.Bool(true), + Tags: kong.StringSlice("k8s-name:test-ingress", "k8s-namespace:default"), }, }}, Backends: []kongstate.ServiceBackend{{ @@ -885,6 +894,7 @@ func TestTranslateIngress(t *testing.T) { StripPath: kong.Bool(false), ResponseBuffering: kong.Bool(true), RequestBuffering: kong.Bool(true), + Tags: kong.StringSlice("k8s-name:test-ingress", "k8s-namespace:default"), }, }, }, @@ -927,6 +937,7 @@ func TestTranslateIngress(t *testing.T) { StripPath: kong.Bool(false), ResponseBuffering: kong.Bool(true), RequestBuffering: kong.Bool(true), + Tags: kong.StringSlice("k8s-name:test-ingress", "k8s-namespace:default"), }, }, }, @@ -1026,6 +1037,7 @@ func TestTranslateIngress(t *testing.T) { StripPath: kong.Bool(false), ResponseBuffering: kong.Bool(true), RequestBuffering: kong.Bool(true), + Tags: kong.StringSlice("k8s-name:test-ingress", "k8s-namespace:default"), }, }, }, @@ -1068,6 +1080,7 @@ func TestTranslateIngress(t *testing.T) { StripPath: kong.Bool(false), ResponseBuffering: kong.Bool(true), RequestBuffering: kong.Bool(true), + Tags: kong.StringSlice("k8s-name:test-ingress", "k8s-namespace:default"), }, }, }, @@ -1140,6 +1153,7 @@ func TestTranslateIngress(t *testing.T) { StripPath: kong.Bool(false), ResponseBuffering: kong.Bool(true), RequestBuffering: kong.Bool(true), + Tags: kong.StringSlice("k8s-name:test-ingress", "k8s-namespace:default"), }, }}, Backends: []kongstate.ServiceBackend{{ @@ -1213,6 +1227,13 @@ func TestTranslateIngress(t *testing.T) { StripPath: kong.Bool(false), ResponseBuffering: kong.Bool(true), RequestBuffering: kong.Bool(true), + Tags: kong.StringSlice( + "k8s-name:test-ingress", + "k8s-namespace:default", + "k8s-kind:Ingress", + "k8s-group:networking.k8s.io", + "k8s-version:v1", + ), }, }}, Backends: []kongstate.ServiceBackend{{ diff --git a/internal/dataplane/sendconfig/dbmode.go b/internal/dataplane/sendconfig/dbmode.go index 3a1c9991f7..d8a9b38044 100644 --- a/internal/dataplane/sendconfig/dbmode.go +++ b/internal/dataplane/sendconfig/dbmode.go @@ -39,15 +39,19 @@ func NewUpdateStrategyDBMode( } } -func (s UpdateStrategyDBMode) Update(ctx context.Context, targetContent *file.Content) error { +func (s UpdateStrategyDBMode) Update(ctx context.Context, targetContent *file.Content) ( + err error, + resourceErrors []ResourceError, + resourceErrorsParseErr error, +) { cs, err := s.currentState(ctx) if err != nil { - return fmt.Errorf("failed getting current state for %s: %w", s.client.BaseRootURL(), err) + return fmt.Errorf("failed getting current state for %s: %w", s.client.BaseRootURL(), err), nil, nil } ts, err := s.targetState(ctx, cs, targetContent) if err != nil { - return deckerrors.ConfigConflictError{Err: err} + return deckerrors.ConfigConflictError{Err: err}, nil, nil } syncer, err := diff.NewSyncer(diff.SyncerOpts{ @@ -57,15 +61,15 @@ func (s UpdateStrategyDBMode) Update(ctx context.Context, targetContent *file.Co SilenceWarnings: true, }) if err != nil { - return fmt.Errorf("creating a new syncer for %s: %w", s.client.BaseRootURL(), err) + return fmt.Errorf("creating a new syncer for %s: %w", s.client.BaseRootURL(), err), nil, nil } _, errs := syncer.Solve(ctx, s.concurrency, false) if errs != nil { - return deckutils.ErrArray{Errors: errs} + return deckutils.ErrArray{Errors: errs}, nil, nil } - return nil + return nil, nil, nil } func (s UpdateStrategyDBMode) MetricsProtocol() metrics.Protocol { diff --git a/internal/dataplane/sendconfig/error_handling_test.go b/internal/dataplane/sendconfig/error_handling_test.go new file mode 100644 index 0000000000..9b27952261 --- /dev/null +++ b/internal/dataplane/sendconfig/error_handling_test.go @@ -0,0 +1,161 @@ +package sendconfig + +import ( + "testing" + + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/require" +) + +func TestParseFlatEntityErrors(t *testing.T) { + log := logrus.New() + tests := []struct { + name string + body []byte + want []ResourceError + wantErr bool + }{ + { + name: "a route nested under a service, with two and one errors, respectively", + want: []ResourceError{ + { + Name: "httpbin", + Namespace: "67338dc2-31fd-47b6-85a9-9c11d347d090", + Kind: "Ingress", + APIVersion: "networking.k8s.io/v1", + UID: "ea569579-f7e9-4d4e-973b-b207bfb848d8", + Problems: map[string]string{ + "methods": "cannot set methods when protocols is grpc or grpcs", + }, + }, + { + Name: "httpbin", + Namespace: "67338dc2-31fd-47b6-85a9-9c11d347d090", + Kind: "Service", + APIVersion: "v1", + UID: "e7e5c93e-4d56-4cc3-8f4f-ff1fcbe95eb2", + Problems: map[string]string{ + "": "failed conditional validation given value of field protocol", + "path": "value must be null", + }, + }, + }, + wantErr: false, + body: []byte(`{ + "name": "invalid declarative configuration", + "fields": { + "services": [ + { + "@entity": [ + "failed conditional validation given value of field protocol" + ], + "path": "value must be null" + } + ] + }, + "flattened_errors": [ + { + "entity_type": "route", + "entity_name": "67338dc2-31fd-47b6-85a9-9c11d347d090.httpbin.httpbin..80", + "entity": { + "paths": [ + "/bar/", + "~/bar$" + ], + "methods": [ + "GET" + ], + "response_buffering": true, + "tags": [ + "k8s-name:httpbin", + "k8s-namespace:67338dc2-31fd-47b6-85a9-9c11d347d090", + "k8s-kind:Ingress", + "k8s-uid:ea569579-f7e9-4d4e-973b-b207bfb848d8", + "k8s-group:networking.k8s.io", + "k8s-version:v1" + ], + "name": "67338dc2-31fd-47b6-85a9-9c11d347d090.httpbin.httpbin..80", + "request_buffering": true, + "preserve_host": true, + "https_redirect_status_code": 426, + "path_handling": "v0", + "protocols": [ + "grpcs" + ], + "regex_priority": 0 + }, + "errors": [ + { + "message": "cannot set methods when protocols is grpc or grpcs", + "type": "field", + "field": "methods" + } + ], + "entity_tags": [ + "k8s-name:httpbin", + "k8s-namespace:67338dc2-31fd-47b6-85a9-9c11d347d090", + "k8s-kind:Ingress", + "k8s-uid:ea569579-f7e9-4d4e-973b-b207bfb848d8", + "k8s-group:networking.k8s.io", + "k8s-version:v1" + ] + }, + { + "entity_type": "service", + "entity_name": "67338dc2-31fd-47b6-85a9-9c11d347d090.httpbin.httpbin.80", + "entity": { + "protocol": "tcp", + "tags": [ + "k8s-name:httpbin", + "k8s-namespace:67338dc2-31fd-47b6-85a9-9c11d347d090", + "k8s-kind:Service", + "k8s-uid:e7e5c93e-4d56-4cc3-8f4f-ff1fcbe95eb2", + "k8s-group:", + "k8s-version:v1" + ], + "retries": 5, + "connect_timeout": 60000, + "path": "/aitmatov", + "name": "67338dc2-31fd-47b6-85a9-9c11d347d090.httpbin.httpbin.80", + "read_timeout": 60000, + "port": 80, + "host": "httpbin.67338dc2-31fd-47b6-85a9-9c11d347d090.80.svc", + "write_timeout": 60000 + }, + "errors": [ + { + "message": "failed conditional validation given value of field protocol", + "type": "entity" + }, + { + "message": "value must be null", + "type": "field", + "field": "path" + } + ], + "entity_tags": [ + "k8s-name:httpbin", + "k8s-namespace:67338dc2-31fd-47b6-85a9-9c11d347d090", + "k8s-kind:Service", + "k8s-uid:e7e5c93e-4d56-4cc3-8f4f-ff1fcbe95eb2", + "k8s-group:", + "k8s-version:v1" + ] + } + ], + "message": "declarative config is invalid: {services={{[\"@entity\"]={\"failed conditional validation given value of field protocol\"},path=\"value must be null\"}}}", + "code": 14 +}`), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := parseFlatEntityErrors(tt.body, log) + if (err != nil) != tt.wantErr { + t.Errorf("parseFlatEntityErrors() error = %v, wantErr %v", err, tt.wantErr) + return + } + require.Equal(t, tt.want, got) + }) + } +} diff --git a/internal/dataplane/sendconfig/inmemory.go b/internal/dataplane/sendconfig/inmemory.go index ebf42a65ab..1415c4e826 100644 --- a/internal/dataplane/sendconfig/inmemory.go +++ b/internal/dataplane/sendconfig/inmemory.go @@ -8,9 +8,11 @@ import ( "io" "github.com/kong/deck/file" + "github.com/sirupsen/logrus" "github.com/kong/kubernetes-ingress-controller/v2/internal/dataplane/deckgen" "github.com/kong/kubernetes-ingress-controller/v2/internal/metrics" + "github.com/kong/kubernetes-ingress-controller/v2/internal/versions" ) type ConfigService interface { @@ -26,20 +28,24 @@ type ConfigService interface { // configuration using its `POST /config` endpoint that is used by ConfigService.ReloadDeclarativeRawConfig. type UpdateStrategyInMemory struct { configService ConfigService + log logrus.FieldLogger } func NewUpdateStrategyInMemory( configService ConfigService, + log logrus.FieldLogger, ) UpdateStrategyInMemory { return UpdateStrategyInMemory{ configService: configService, + log: log, } } -func (s UpdateStrategyInMemory) Update( - ctx context.Context, - targetState *file.Content, -) error { +func (s UpdateStrategyInMemory) Update(ctx context.Context, targetState *file.Content) ( + err error, + resourceErrors []ResourceError, + resourceErrorsParseErr error, +) { // Kong will error out if this is set targetState.Info = nil @@ -48,16 +54,33 @@ func (s UpdateStrategyInMemory) Update( config, err := json.Marshal(targetState) if err != nil { - return fmt.Errorf("constructing kong configuration: %w", err) + return fmt.Errorf("constructing kong configuration: %w", err), nil, nil } - if _, err = s.configService.ReloadDeclarativeRawConfig(ctx, bytes.NewReader(config), true, false); err != nil { - return err + flattenErrors := shouldUseFlattenedErrors() + if errBody, err := s.configService.ReloadDeclarativeRawConfig(ctx, bytes.NewReader(config), true, flattenErrors); err != nil { + resourceErrors, parseErr := parseFlatEntityErrors(errBody, s.log) + return err, resourceErrors, parseErr } - return nil + return nil, nil, nil } func (s UpdateStrategyInMemory) MetricsProtocol() metrics.Protocol { return metrics.ProtocolDBLess } + +// shouldUseFlattenedErrors verifies whether we should pass flatten errors flag to ReloadDeclarativeRawConfig. +// Kong's API library combines KVs in the request body (the config) and query string (check hash, flattened) +// into a single set of parameters: https://github.com/Kong/go-kong/pull/271#issuecomment-1416212852 +// KIC therefore must _not_ request flattened errors on versions that do not support it, as otherwise Kong +// will interpret the query string toggle as part of the config, and will reject it, as "flattened_errors" is +// not a valid config key. KIC only sends this query parameter if Kong is 3.2 or higher. +func shouldUseFlattenedErrors() bool { + return !versions.GetKongVersion().MajorMinorOnly().LTE(versions.FlattenedErrorCutoff) +} + +type InMemoryClient interface { + BaseRootURL() string + ReloadDeclarativeRawConfig(ctx context.Context, config io.Reader, checkHash bool, flattenErrors bool) ([]byte, error) +} diff --git a/internal/dataplane/sendconfig/inmemory_error_handling.go b/internal/dataplane/sendconfig/inmemory_error_handling.go new file mode 100644 index 0000000000..7f82533c69 --- /dev/null +++ b/internal/dataplane/sendconfig/inmemory_error_handling.go @@ -0,0 +1,139 @@ +package sendconfig + +import ( + "encoding/json" + "fmt" + "strings" + + "github.com/sirupsen/logrus" + "k8s.io/apimachinery/pkg/runtime/schema" + + "github.com/kong/kubernetes-ingress-controller/v2/internal/util" +) + +// rawResourceError is a Kong configuration error associated with a Kubernetes resource with Kubernetes metadata stored +// in raw Kong entity tags. +type rawResourceError struct { + Name string + ID string + Tags []string + Problems map[string]string +} + +// ConfigError is an error response from Kong's DB-less /config endpoint. +type ConfigError struct { + Code int `json:"code,omitempty" yaml:"code,omitempty"` + Flattened []FlatEntityError `json:"flattened_errors,omitempty" yaml:"flattened_errors,omitempty"` + Message string `json:"message,omitempty" yaml:"message,omitempty"` + Name string `json:"name,omitempty" yaml:"name,omitempty"` +} + +// ConfigErrorFields is the structure under the "fields" key in a /config error response. +type ConfigErrorFields struct { +} + +// FlatEntityError represents a single Kong entity with one or more invalid fields. +type FlatEntityError struct { + Name string `json:"entity_name,omitempty" yaml:"entity_name,omitempty"` + ID string `json:"entity_id,omitempty" yaml:"entity_id,omitempty"` + Tags []string `json:"entity_tags,omitempty" yaml:"entity_tags,omitempty"` + Errors []FlatFieldError `json:"errors,omitempty" yaml:"errors,omitempty"` +} + +// FlatFieldError represents an error for a single field within a Kong entity. +type FlatFieldError struct { + Field string `json:"field,omitempty" yaml:"field,omitempty"` + // Message is the error associated with Field for single-value fields. + Message string `json:"message,omitempty" yaml:"message,omitempty"` + // Messages are the errors associated with Field for multi-value fields. The array index in Messages matches the + // array index in the input. + Messages []string `json:"messages,omitempty" yaml:"messages,omitempty"` +} + +// parseFlatEntityErrors takes a Kong /config error response body and parses its "fields.flattened_errors" value +// into errors associated with Kubernetes resources. +func parseFlatEntityErrors(body []byte, log logrus.FieldLogger) ([]ResourceError, error) { + var resourceErrors []ResourceError + var configError ConfigError + err := json.Unmarshal(body, &configError) + if err != nil { + return resourceErrors, fmt.Errorf("could not unmarshal config error: %w", err) + } + for _, ee := range configError.Flattened { + raw := rawResourceError{ + Name: ee.Name, + ID: ee.ID, + Tags: ee.Tags, + Problems: map[string]string{}, + } + for _, p := range ee.Errors { + if len(p.Message) > 0 && len(p.Messages) > 0 { + log.WithFields(logrus.Fields{ + "name": ee.Name, + "field": p.Field}).Error("entity has both single and array errors for field") + + continue + } + if len(p.Message) > 0 { + raw.Problems[p.Field] = p.Message + } + if len(p.Messages) > 0 { + for i, message := range p.Messages { + if len(message) > 0 { + raw.Problems[fmt.Sprintf("%s[%d]", p.Field, i)] = message + } + } + } + } + parsed, err := parseRawResourceError(raw) + if err != nil { + log.WithError(err).WithField("name", ee.Name).Error("entity tags missing fields") + continue + } + resourceErrors = append(resourceErrors, parsed) + } + return resourceErrors, nil +} + +// parseRawResourceError takes a raw resource error and parses its tags into Kubernetes metadata. If critical tags are +// missing, it returns an error indicating the missing tag. +func parseRawResourceError(raw rawResourceError) (ResourceError, error) { + re := ResourceError{} + re.Problems = raw.Problems + var gvk schema.GroupVersionKind + for _, tag := range raw.Tags { + if strings.HasPrefix(tag, util.K8sNameTagPrefix) { + re.Name = strings.TrimPrefix(tag, util.K8sNameTagPrefix) + } + if strings.HasPrefix(tag, util.K8sNamespaceTagPrefix) { + re.Namespace = strings.TrimPrefix(tag, util.K8sNamespaceTagPrefix) + } + if strings.HasPrefix(tag, util.K8sKindTagPrefix) { + gvk.Kind = strings.TrimPrefix(tag, util.K8sKindTagPrefix) + } + if strings.HasPrefix(tag, util.K8sVersionTagPrefix) { + gvk.Version = strings.TrimPrefix(tag, util.K8sVersionTagPrefix) + } + // this will not set anything for core resources + if strings.HasPrefix(tag, util.K8sGroupTagPrefix) { + gvk.Group = strings.TrimPrefix(tag, util.K8sGroupTagPrefix) + } + if strings.HasPrefix(tag, util.K8sUIDTagPrefix) { + re.UID = strings.TrimPrefix(tag, util.K8sUIDTagPrefix) + } + } + re.APIVersion, re.Kind = gvk.ToAPIVersionAndKind() + if re.Name == "" { + return re, fmt.Errorf("no name") + } + if re.Namespace == "" { + return re, fmt.Errorf("no namespace") + } + if re.Kind == "" { + return re, fmt.Errorf("no kind") + } + if re.UID == "" { + return re, fmt.Errorf("no uid") + } + return re, nil +} diff --git a/internal/dataplane/sendconfig/sendconfig.go b/internal/dataplane/sendconfig/sendconfig.go index 84e8ea711d..25397ff684 100644 --- a/internal/dataplane/sendconfig/sendconfig.go +++ b/internal/dataplane/sendconfig/sendconfig.go @@ -11,9 +11,12 @@ import ( "github.com/kong/deck/file" "github.com/kong/go-kong/kong" "github.com/sirupsen/logrus" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" "github.com/kong/kubernetes-ingress-controller/v2/internal/adminapi" "github.com/kong/kubernetes-ingress-controller/v2/internal/dataplane/deckgen" + "github.com/kong/kubernetes-ingress-controller/v2/internal/dataplane/failures" "github.com/kong/kubernetes-ingress-controller/v2/internal/metrics" ) @@ -22,46 +25,48 @@ import ( // ----------------------------------------------------------------------------- // PerformUpdate writes `targetContent` to Kong Admin API specified by `kongConfig`. -func PerformUpdate(ctx context.Context, +func PerformUpdate( + ctx context.Context, log logrus.FieldLogger, client *adminapi.Client, config Config, targetContent *file.Content, promMetrics *metrics.CtrlFuncMetrics, -) ([]byte, error) { +) ([]byte, []failures.ResourceFailure, error) { oldSHA := client.LastConfigSHA() newSHA, err := deckgen.GenerateSHA(targetContent) if err != nil { - return oldSHA, err + return oldSHA, []failures.ResourceFailure{}, err } // disable optimization if reverse sync is enabled if !config.EnableReverseSync { configurationChanged, err := hasConfigurationChanged(ctx, oldSHA, newSHA, client, client.AdminAPIClient(), log) if err != nil { - return nil, err + return nil, []failures.ResourceFailure{}, err } if !configurationChanged { log.Debug("no configuration change, skipping sync to Kong") - return oldSHA, nil + return oldSHA, []failures.ResourceFailure{}, nil } } - updateStrategy := ResolveUpdateStrategy(client, config) + updateStrategy := ResolveUpdateStrategy(client, config, log) timeStart := time.Now() - err = updateStrategy.Update(ctx, targetContent) + err, resourceErrors, resourceErrorsParseErr := updateStrategy.Update(ctx, targetContent) duration := time.Since(timeStart) metricsProtocol := updateStrategy.MetricsProtocol() if err != nil { + resourceFailures := resourceErrorsToResourceFailures(resourceErrors, resourceErrorsParseErr, log) promMetrics.RecordPushFailure(metricsProtocol, duration, client.BaseRootURL(), err) - return nil, err + return nil, resourceFailures, err } promMetrics.RecordPushSuccess(metricsProtocol, duration, client.BaseRootURL()) log.Info("successfully synced configuration to kong") - return newSHA, nil + return newSHA, nil, nil } // ----------------------------------------------------------------------------- @@ -156,3 +161,41 @@ func kongHasNoConfiguration(ctx context.Context, client StatusClient, log logrus return false, nil } + +// resourceErrorsToResourceFailures translates a slice of ResourceError to a slice of failures.ResourceFailure. +// In case of parseErr being not nil, it just returns a nil slice. +func resourceErrorsToResourceFailures(resourceErrors []ResourceError, parseErr error, log logrus.FieldLogger) []failures.ResourceFailure { + if parseErr != nil { + log.WithError(parseErr).Error("failed parsing resource errors") + return nil + } + + var out []failures.ResourceFailure + for _, ee := range resourceErrors { + obj := metav1.PartialObjectMetadata{ + TypeMeta: metav1.TypeMeta{ + Kind: ee.Kind, + APIVersion: ee.APIVersion, + }, + ObjectMeta: metav1.ObjectMeta{ + Namespace: ee.Namespace, + Name: ee.Name, + UID: types.UID(ee.UID), + }, + } + for field, problem := range ee.Problems { + log.Debug(fmt.Sprintf("adding failure for %s: %s = %s", ee.Name, field, problem)) + resourceFailure, failureCreateErr := failures.NewResourceFailure( + fmt.Sprintf("invalid %s: %s", field, problem), + &obj, + ) + if failureCreateErr != nil { + log.WithError(failureCreateErr).Error("could create resource failure event") + } else { + out = append(out, resourceFailure) + } + } + } + + return out +} diff --git a/internal/dataplane/sendconfig/strategy.go b/internal/dataplane/sendconfig/strategy.go index b5196057d0..8e48ea1837 100644 --- a/internal/dataplane/sendconfig/strategy.go +++ b/internal/dataplane/sendconfig/strategy.go @@ -6,6 +6,7 @@ import ( "github.com/kong/deck/dump" "github.com/kong/deck/file" "github.com/kong/go-kong/kong" + "github.com/sirupsen/logrus" "github.com/kong/kubernetes-ingress-controller/v2/internal/metrics" ) @@ -13,7 +14,11 @@ import ( // UpdateStrategy is the way we approach updating data-plane's configuration, depending on its type. type UpdateStrategy interface { // Update applies targetConfig to the data-plane. - Update(ctx context.Context, targetContent *file.Content) error + Update(ctx context.Context, targetContent *file.Content) ( + err error, + resourceErrors []ResourceError, + resourceErrorsParseErr error, + ) // MetricsProtocol returns a string describing the update strategy type to be used in metrics. MetricsProtocol() metrics.Protocol @@ -25,12 +30,23 @@ type UpdateClient interface { AdminAPIClient() *kong.Client } +// ResourceError is a Kong configuration error associated with a Kubernetes resource. +type ResourceError struct { + Name string + Namespace string + Kind string + APIVersion string + UID string + Problems map[string]string +} + // ResolveUpdateStrategy returns an UpdateStrategy based on the client and configuration. // The UpdateStrategy can be either UpdateStrategyDBMode or UpdateStrategyInMemory. Both // of them implement different ways to populate Kong instances with data-plane configuration. func ResolveUpdateStrategy( client UpdateClient, config Config, + log logrus.FieldLogger, ) UpdateStrategy { adminAPIClient := client.AdminAPIClient() @@ -60,5 +76,5 @@ func ResolveUpdateStrategy( ) } - return NewUpdateStrategyInMemory(adminAPIClient) + return NewUpdateStrategyInMemory(adminAPIClient, log) } diff --git a/internal/dataplane/sendconfig/strategy_test.go b/internal/dataplane/sendconfig/strategy_test.go index 8e0771b621..74bc55e4c0 100644 --- a/internal/dataplane/sendconfig/strategy_test.go +++ b/internal/dataplane/sendconfig/strategy_test.go @@ -6,6 +6,7 @@ import ( "github.com/google/uuid" "github.com/kong/go-kong/kong" + "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -72,7 +73,7 @@ func TestResolveUpdateStrategy(t *testing.T) { strategy := sendconfig.ResolveUpdateStrategy(client, sendconfig.Config{ InMemory: tc.inMemory, - }) + }, logrus.New()) require.IsType(t, tc.expectedStrategy, strategy) assert.True(t, client.adminAPIClientWasCalled) assert.Equal(t, tc.expectKonnectRuntimeGroupCall, client.konnectRuntimeGroupWasCalled) diff --git a/internal/util/k8s.go b/internal/util/k8s.go index 8174eb9ad3..ca69327f21 100644 --- a/internal/util/k8s.go +++ b/internal/util/k8s.go @@ -22,9 +22,11 @@ import ( "os" "strings" + "github.com/kong/go-kong/kong" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" clientset "k8s.io/client-go/kubernetes" + "sigs.k8s.io/controller-runtime/pkg/client" gatewayv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1" ) @@ -128,3 +130,42 @@ func IsBackendRefGroupKindSupported(gatewayAPIGroup *gatewayv1beta1.Group, gatew _, ok := backendRefSupportedGroupKinds[fmt.Sprintf("%s/%s", group, *gatewayAPIKind)] return ok } + +const ( + K8sNamespaceTagPrefix = "k8s-namespace:" + K8sNameTagPrefix = "k8s-name:" + K8sUIDTagPrefix = "k8s-uid:" + K8sKindTagPrefix = "k8s-kind:" + K8sGroupTagPrefix = "k8s-group:" + K8sVersionTagPrefix = "k8s-version:" +) + +// GenerateTagsForObject returns a subset of an object's metadata as a slice of prefixed string pointers. +func GenerateTagsForObject(obj client.Object) []*string { + if obj == nil { + // this should never happen in practice, but it happen in some unit tests + // in those cases, the nil object has no tags + return nil + } + gvk := obj.GetObjectKind().GroupVersionKind() + tags := []string{} + if obj.GetName() != "" { + tags = append(tags, K8sNameTagPrefix+obj.GetName()) + } + if obj.GetNamespace() != "" { + tags = append(tags, K8sNamespaceTagPrefix+obj.GetNamespace()) + } + if gvk.Kind != "" { + tags = append(tags, K8sKindTagPrefix+gvk.Kind) + } + if string(obj.GetUID()) != "" { + tags = append(tags, K8sUIDTagPrefix+string(obj.GetUID())) + } + if gvk.Group != "" { + tags = append(tags, K8sGroupTagPrefix+gvk.Group) + } + if gvk.Version != "" { + tags = append(tags, K8sVersionTagPrefix+gvk.Version) + } + return kong.StringSlice(tags...) +} diff --git a/internal/versions/versions.go b/internal/versions/versions.go index 1238165dd3..ef47482209 100644 --- a/internal/versions/versions.go +++ b/internal/versions/versions.go @@ -21,6 +21,9 @@ var ( // MTLSCredentialVersionCutoff is the minimum Kong version that support mTLS credentials. This is a patch version // because the original version of the mTLS credential was not compatible with KIC. MTLSCredentialVersionCutoff = semver.Version{Major: 2, Minor: 3, Patch: 2} + + // FlattenedErrorCutoff is the Kong version prior to the addition of flattened errors. + FlattenedErrorCutoff = semver.Version{Major: 3, Minor: 1} ) var ( diff --git a/test/integration/config_error_event_test.go b/test/integration/config_error_event_test.go new file mode 100644 index 0000000000..3c7ade8972 --- /dev/null +++ b/test/integration/config_error_event_test.go @@ -0,0 +1,107 @@ +//go:build integration_tests +// +build integration_tests + +package integration + +import ( + "context" + "testing" + + "github.com/kong/kubernetes-testing-framework/pkg/clusters" + "github.com/kong/kubernetes-testing-framework/pkg/utils/kubernetes/generators" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/kong/kubernetes-ingress-controller/v2/internal/annotations" + "github.com/kong/kubernetes-ingress-controller/v2/internal/dataplane" + "github.com/kong/kubernetes-ingress-controller/v2/internal/versions" + "github.com/kong/kubernetes-ingress-controller/v2/test" + "github.com/kong/kubernetes-ingress-controller/v2/test/consts" + "github.com/kong/kubernetes-ingress-controller/v2/test/internal/helpers" + "github.com/kong/kubernetes-ingress-controller/v2/test/internal/testenv" +) + +func TestConfigErrorEventGeneration(t *testing.T) { + // this test is NOT parallel. the broken configuration prevents all updates and will break unrelated tests + if testenv.DBMode() != "off" { + t.Skip("config errors are only supported on DB-less mode") + } + if versions.GetKongVersion().MajorMinorOnly().LTE(versions.FlattenedErrorCutoff) { + t.Skipf("kong version is %s < 3.2, skipping testing config error parsing", versions.GetKongVersion().MajorMinorOnly().String()) + } else { + t.Logf("kong version is %s >= 3.2, testing config error parsing", versions.GetKongVersion().MajorMinorOnly().String()) + } + ctx := context.Background() + ns, cleaner := helpers.Setup(ctx, t, env) + + t.Log("deploying a minimal HTTP container deployment to test Ingress routes") + container := generators.NewContainer("httpbin", test.HTTPBinImage, 80) + deployment := generators.NewDeploymentForContainer(container) + deployment, err := env.Cluster().Client().AppsV1().Deployments(ns.Name).Create(ctx, deployment, metav1.CreateOptions{}) + assert.NoError(t, err) + cleaner.Add(deployment) + + t.Logf("exposing deployment %s via service", deployment.Name) + service := generators.NewServiceForDeployment(deployment, corev1.ServiceTypeLoadBalancer) + service.ObjectMeta.Annotations = map[string]string{} + // TCP services cannot have paths, and we don't catch this as a translation error + service.ObjectMeta.Annotations["konghq.com/protocol"] = "tcp" + service.ObjectMeta.Annotations["konghq.com/path"] = "/aitmatov" + _, err = env.Cluster().Client().CoreV1().Services(ns.Name).Create(ctx, service, metav1.CreateOptions{}) + assert.NoError(t, err) + cleaner.Add(service) + + t.Logf("creating an ingress for service %s with invalid configuration", service.Name) + kubernetesVersion, err := env.Cluster().Version() + require.NoError(t, err) + // GRPC routes cannot have methods, only HTTP, and we don't catch this as a translation error + ingress := generators.NewIngressForServiceWithClusterVersion(kubernetesVersion, "/bar", map[string]string{ + annotations.IngressClassKey: consts.IngressClass, + "konghq.com/strip-path": "true", + "konghq.com/protocols": "grpcs", + "konghq.com/methods": "GET", + }, service) + + t.Log("deploying ingress") + require.NoError(t, clusters.DeployIngress(ctx, env.Cluster(), ns.Name, ingress)) + helpers.AddIngressToCleaner(cleaner, ingress) + + t.Log("checking ingress event creation") + require.Eventually(t, func() bool { + events, err := env.Cluster().Client().CoreV1().Events(ns.Name).List(ctx, metav1.ListOptions{}) + if err != nil { + return false + } + for _, event := range events.Items { + if event.Reason == dataplane.KongConfigurationApplyFailedEventReason { + if event.InvolvedObject.Kind == "Ingress" { + // this is a runtime.Object because of v1/v1beta1 handling, so no ObjectMeta or other obvious way + // to get the name. we can reasonably assume it's the only Ingress in the namespace + return true + } + } + } + return false + }, statusWait, waitTick, true) + + t.Log("checking service event creation") + require.Eventually(t, func() bool { + events, err := env.Cluster().Client().CoreV1().Events(ns.Name).List(ctx, metav1.ListOptions{}) + if err != nil { + return false + } + for _, event := range events.Items { + if event.Reason == dataplane.KongConfigurationApplyFailedEventReason { + if event.InvolvedObject.Kind == "Service" { + if event.InvolvedObject.Name == service.ObjectMeta.Name { + return true + } + } + } + } + return false + }, statusWait, waitTick, true) + t.Log("push failure events recorded successfully") +} diff --git a/test/internal/testenv/testenv.go b/test/internal/testenv/testenv.go index 383598f951..024887142f 100644 --- a/test/internal/testenv/testenv.go +++ b/test/internal/testenv/testenv.go @@ -16,6 +16,11 @@ func DBMode() string { os.Exit(1) } + if dbmode == "" { + // if none explicitly set, the default is off + return "off" + } + return dbmode }