Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

spike: create warning events for parsing errors #3088

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 26 additions & 6 deletions internal/dataplane/kong_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import (
"github.com/kong/go-kong/kong"
"github.com/prometheus/client_golang/prometheus"
"github.com/sirupsen/logrus"
corev1 "k8s.io/api/core/v1"
"k8s.io/client-go/tools/record"
"sigs.k8s.io/controller-runtime/pkg/client"

"github.com/kong/kubernetes-ingress-controller/v2/internal/dataplane/deckgen"
Expand Down Expand Up @@ -106,6 +108,7 @@ type KongClient struct {
// whether a Kubernetes object has corresponding data-plane configuration that
// is actively configured (e.g. to know how to set the object status).
kubernetesObjectReportsFilter k8sobj.Set
eventsRecorder record.EventRecorder
}

// NewKongClient provides a new KongClient object after connecting to the
Expand All @@ -118,6 +121,7 @@ func NewKongClient(
skipCACertificates bool,
diagnostic util.ConfigDumpDiagnostic,
kongConfig sendconfig.Kong,
eventsRecorder record.EventRecorder,
) (*KongClient, error) {
// build the client object
cache := store.NewCacheStores()
Expand All @@ -131,6 +135,7 @@ func NewKongClient(
prometheusMetrics: metrics.NewCtrlFuncMetrics(),
cache: &cache,
kongConfig: kongConfig,
eventsRecorder: eventsRecorder,
}

// download the kong root configuration (and validate connectivity to the proxy API)
Expand Down Expand Up @@ -312,12 +317,18 @@ func (c *KongClient) Update(ctx context.Context) error {

// parse the Kubernetes objects from the storer into Kong configuration
kongstate := p.Build()
// todo: does it still make sense to report TranslationCount when Build no longer returns an error?
// https://github.com/Kong/kubernetes-ingress-controller/issues/1892
c.prometheusMetrics.TranslationCount.With(prometheus.Labels{
metrics.SuccessKey: metrics.SuccessTrue,
}).Inc()
c.logger.Debug("successfully built data-plane configuration")
if errors := p.PopParsingErrors(); errors != nil {
c.createParsingErrorsEvents(errors)
c.prometheusMetrics.TranslationCount.With(prometheus.Labels{
metrics.SuccessKey: metrics.SuccessFalse,
}).Inc()
c.logger.Debugf("%d translation errors occurred when building data-plane configuration", len(errors))
} else {
c.prometheusMetrics.TranslationCount.With(prometheus.Labels{
metrics.SuccessKey: metrics.SuccessTrue,
}).Inc()
c.logger.Debug("successfully built data-plane configuration")
}

// generate the deck configuration to be applied to the admin API
c.logger.Debug("converting configuration to deck config")
Expand Down Expand Up @@ -438,3 +449,12 @@ func (c *KongClient) updateKubernetesObjectReportFilter(set k8sobj.Set) {
defer c.kubernetesObjectReportLock.Unlock()
c.kubernetesObjectReportsFilter = set
}

func (c *KongClient) createParsingErrorsEvents(errors []parser.ParsingError) {
const reason = "TranslationToKongConfigurationFailed"
for _, err := range errors {
for _, obj := range err.RelatedObjects() {
c.eventsRecorder.Event(obj, corev1.EventTypeWarning, reason, err.Reason())
}
}
}
Comment on lines +453 to +460
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

eventsRecorder creates Kubernetes Event object associated with the obj.

50 changes: 47 additions & 3 deletions internal/dataplane/parser/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,41 @@ const (
// Parser - Public Types
// -----------------------------------------------------------------------------

type ParsingError struct {
relatedObjects []client.Object
reason string
}
Comment on lines +42 to +45
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Every parsing error has a human-readable reason string and related kubernetes objects.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apart from this deserving its own file I feel this is a good approach.


func NewParsingError(reason string, relatedObjects ...client.Object) ParsingError {
if reason == "" {
reason = "unknown"
}
return ParsingError{
relatedObjects: relatedObjects,
reason: reason,
}
}

func (p ParsingError) RelatedObjects() []client.Object {
return p.relatedObjects
}

func (p ParsingError) Reason() string {
return p.reason
}

type parsingErrorsCollector struct {
errors []ParsingError
}

func newParsingErrorsCollector() *parsingErrorsCollector {
return &parsingErrorsCollector{}
}

func (c *parsingErrorsCollector) ParsingError(reason string, relatedObjects ...client.Object) {
c.errors = append(c.errors, NewParsingError(reason, relatedObjects...))
}
Comment on lines +73 to +75
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ParsingError is used to gather parsing errors during the translation. It accepts related/affected objects regarding the error.


// Parser parses Kubernetes objects and configurations into their
// equivalent Kong objects and configurations, producing a complete
// state configuration for the Kong Admin API.
Expand All @@ -51,6 +86,7 @@ type Parser struct {
featureEnabledCombinedServiceRoutes bool

flagEnabledRegexPathPrefix bool
errorsCollector *parsingErrorsCollector
}

// NewParser produces a new Parser object provided a logging mechanism
Expand All @@ -60,8 +96,9 @@ func NewParser(
storer store.Storer,
) *Parser {
return &Parser{
logger: logger,
storer: storer,
logger: logger,
storer: storer,
errorsCollector: newParsingErrorsCollector(),
}
}

Expand Down Expand Up @@ -118,7 +155,7 @@ func (p *Parser) Build() *kongstate.KongState {
result.Certificates = mergeCerts(p.logger, ingressCerts, gatewayCerts)

// populate CA certificates in Kong
result.CACertificates = getCACerts(p.logger, p.storer, result.Plugins)
result.CACertificates = p.getCACerts(p.logger, p.storer)

return &result
}
Expand Down Expand Up @@ -154,6 +191,13 @@ func (p *Parser) GenerateKubernetesObjectReport() []client.Object {
return report
}

// PopParsingErrors pops all the parsing errors collected during the last parsing round.
func (p *Parser) PopParsingErrors() []ParsingError {
errors := p.errorsCollector.errors
p.errorsCollector.errors = nil
Comment on lines +196 to +197
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would ideally be placed in parsingErrorsCollector

return errors
}

// -----------------------------------------------------------------------------
// Parser - Public Methods - Other Optional Features
// -----------------------------------------------------------------------------
Expand Down
54 changes: 28 additions & 26 deletions internal/dataplane/parser/translate_secrets.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,46 +2,45 @@ package parser

import (
"crypto/x509"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"time"

"github.com/kong/go-kong/kong"
"github.com/sirupsen/logrus"
corev1 "k8s.io/api/core/v1"
v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
"sigs.k8s.io/controller-runtime/pkg/client"

"github.com/kong/kubernetes-ingress-controller/v2/internal/dataplane/kongstate"
"github.com/kong/kubernetes-ingress-controller/v2/internal/store"
)

// getCACerts translates CA certificates Secrets to kong.CACertificates. It ensures every certificate's structure and
// validity. In case of violation of any validation rule, a secret gets skipped in a result and error message is logged
// with affected plugins for context.
func getCACerts(log logrus.FieldLogger, storer store.Storer, plugins []kongstate.Plugin) []kong.CACertificate {
caCertSecrets, err := storer.ListCACerts()
func (p *Parser) getCACerts() []kong.CACertificate {
log := p.logger
caCertSecrets, err := p.storer.ListCACerts()
if err != nil {
log.WithError(err).Error("failed to list CA certs")
return nil
}

var caCerts []kong.CACertificate
for _, certSecret := range caCertSecrets {
log := log.WithFields(logrus.Fields{
"secret_name": certSecret.Name,
"secret_namespace": certSecret.Namespace,
})

idBytes, ok := certSecret.Data["id"]
if !ok {
log.Error("skipping synchronisation, invalid CA certificate: missing 'id' field in data")
p.errorsCollector.ParsingError("invalid CA certificate: missing 'id' field in data", certSecret)
continue
}
secretID := string(idBytes)

caCert, err := toKongCACertificate(certSecret, secretID)
if err != nil {
logWithAffectedPlugins(log, plugins, secretID).WithError(err).
Error("skipping synchronisation, invalid CA certificate")
affectedObjects := getPluginsAssociatedWithCACertSecret(secretID, p.storer)
affectedObjects = append(affectedObjects, certSecret)
p.errorsCollector.ParsingError(fmt.Sprintf("invalid CA certificate: %s", err), affectedObjects...)
continue
}

Expand Down Expand Up @@ -77,30 +76,33 @@ func toKongCACertificate(certSecret *corev1.Secret, secretID string) (kong.CACer
}, nil
}

func logWithAffectedPlugins(log logrus.FieldLogger, plugins []kongstate.Plugin, secretID string) logrus.FieldLogger {
affectedPlugins := getPluginsAssociatedWithCACertSecret(plugins, secretID)
return log.WithField("affected_plugins", affectedPlugins)
}

func getPluginsAssociatedWithCACertSecret(plugins []kongstate.Plugin, secretID string) []string {
refersToSecret := func(pluginConfig map[string]interface{}) bool {
caCertReferences, ok := pluginConfig["ca_certificates"].([]string)
if !ok {
func getPluginsAssociatedWithCACertSecret(secretID string, storer store.Storer) []client.Object {
refersToSecret := func(pluginConfig v1.JSON) bool {
cfg := struct {
CACertificates []string `json:"ca_certificates,omitempty"`
}{}
err := json.Unmarshal(pluginConfig.Raw, &cfg)
if err != nil {
return false
}

for _, reference := range caCertReferences {
for _, reference := range cfg.CACertificates {
if reference == secretID {
return true
}
}
return false
}

var affectedPlugins []string
for _, p := range plugins {
if refersToSecret(p.Config) && p.Name != nil {
affectedPlugins = append(affectedPlugins, *p.Name)
var affectedPlugins []client.Object
for _, p := range storer.ListKongPlugins() {
if refersToSecret(p.Config) {
affectedPlugins = append(affectedPlugins, p.DeepCopy())
}
}
for _, p := range storer.ListKongClusterPlugins() {
if refersToSecret(p.Config) {
affectedPlugins = append(affectedPlugins, p.DeepCopy())
}
}

Expand Down
8 changes: 4 additions & 4 deletions internal/dataplane/parser/translate_secrets_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ import (
"testing"

"github.com/kong/go-kong/kong"
"github.com/stretchr/testify/require"

"github.com/kong/kubernetes-ingress-controller/v2/internal/dataplane/kongstate"
)

func TestGetPluginsAssociatedWithCACertSecret(t *testing.T) {
secretID := "8a3753e0-093b-43d9-9d39-27985c987d92" //nolint:gosec
plugins := []kongstate.Plugin{
// todo: adapt to implementation
_ = []kongstate.Plugin{
Comment on lines +13 to +14
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given the PR is already marked as ready to review, I believe this needs addressing.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please note it's marked as ready for review (as Michał has suggested doing), but still is not meant to be merged to main in this form. I can adapt the tests but IMO it's not worth it for now until we decide that we wanna go with this approach. The key output I wanted to get out of reviewing this is whether the team thinks it's the way we wanna go regarding events publishing (mainly, if it's ok to do that directly from Parser).

{
Plugin: kong.Plugin{
Name: kong.String("associated-plugin"),
Expand All @@ -35,6 +35,6 @@ func TestGetPluginsAssociatedWithCACertSecret(t *testing.T) {
},
}

associatedPlugins := getPluginsAssociatedWithCACertSecret(plugins, secretID)
require.ElementsMatch(t, []string{"associated-plugin", "another-associated-plugin"}, associatedPlugins)
// associatedPlugins := getPluginsAssociatedWithCACertSecret(plugins, secretID)
// require.ElementsMatch(t, []string{"associated-plugin", "another-associated-plugin"}, associatedPlugins)
}
13 changes: 12 additions & 1 deletion internal/manager/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,18 @@ func Run(ctx context.Context, c *Config, diagnostic util.ConfigDumpDiagnostic) e
if err != nil {
return fmt.Errorf("%f is not a valid number of seconds to the timeout config for the kong client: %w", c.ProxyTimeoutSeconds, err)
}
dataplaneClient, err := dataplane.NewKongClient(deprecatedLogger, timeoutDuration, c.IngressClassName, c.EnableReverseSync, c.SkipCACertificates, diagnostic, kongConfig)

dataplaneEventRecorder := mgr.GetEventRecorderFor("kubernetes-ingress-controller-data-plane")
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's straightforward to create an event recorder from the controller manager.

dataplaneClient, err := dataplane.NewKongClient(
deprecatedLogger,
timeoutDuration,
c.IngressClassName,
c.EnableReverseSync,
c.SkipCACertificates,
diagnostic,
kongConfig,
dataplaneEventRecorder,
)
if err != nil {
return fmt.Errorf("failed to initialize kong data-plane client: %w", err)
}
Expand Down
24 changes: 24 additions & 0 deletions internal/store/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,8 @@ type Storer interface {
ListTCPIngresses() ([]*kongv1beta1.TCPIngress, error)
ListUDPIngresses() ([]*kongv1beta1.UDPIngress, error)
ListKnativeIngresses() ([]*knative.Ingress, error)
ListKongPlugins() []*kongv1.KongPlugin
ListKongClusterPlugins() []*kongv1.KongClusterPlugin
ListGlobalKongPlugins() ([]*kongv1.KongPlugin, error)
ListGlobalKongClusterPlugins() ([]*kongv1.KongClusterPlugin, error)
ListKongConsumers() []*kongv1.KongConsumer
Expand Down Expand Up @@ -961,6 +963,28 @@ func (s Store) ListGlobalKongClusterPlugins() ([]*kongv1.KongClusterPlugin, erro
return plugins, nil
}

func (s Store) ListKongClusterPlugins() []*kongv1.KongClusterPlugin {
var plugins []*kongv1.KongClusterPlugin
for _, item := range s.stores.ClusterPlugin.List() {
p, ok := item.(*kongv1.KongClusterPlugin)
if ok && s.isValidIngressClass(&p.ObjectMeta, annotations.IngressClassKey, s.getIngressClassHandling()) {
plugins = append(plugins, p)
}
}
return plugins
}

func (s Store) ListKongPlugins() []*kongv1.KongPlugin {
var plugins []*kongv1.KongPlugin
for _, item := range s.stores.Plugin.List() {
p, ok := item.(*kongv1.KongPlugin)
if ok && s.isValidIngressClass(&p.ObjectMeta, annotations.IngressClassKey, s.getIngressClassHandling()) {
plugins = append(plugins, p)
}
}
return plugins
}

// ListCACerts returns all Secrets containing the label
// "konghq.com/ca-cert"="true".
func (s Store) ListCACerts() ([]*corev1.Secret, error) {
Expand Down