gokuberneteskubernetes-operator

Golang K8s operator - what is causing a nil pointer Error?


I'm developing a custom k8s operator that utilizes this go-client library: https://github.com/newrelic/newrelic-client-go/tree/main to makes API calls to New Relic (3rd party service). I've created a shared interface that I want to reuse between controllers that allows to interact with that API, but I'm receiving the following error when testing the operator:

2024-09-11T11:43:33-04:00   ERROR   Reconciler error    {"controller": "nrqlcondition", "controllerGroup": "alerts.k8s.newrelic.com", "controllerKind": "NrqlCondition", "NrqlCondition": {"name":"nrqlcondition-example","namespace":"default"}, "namespace": "default", "name": "nrqlcondition-example", "reconcileID": "04f43b77-c342-46ed-acbd-d066e059a4c2", "error": "panic: runtime error: invalid memory address or nil pointer dereference [recovered]"}

My question (being new to developing custom K8s operators) is - do operators support concurrent patterns like this during the reconciliation process? The exact implementation of the shared interface works fine in Controller #1 (Policy), but throws above error on Controller #2 (NrqlCondition). The stack trace points to the AlertClient being nil within the nrqlcondition_controller.go - specifically line

alertClient, errAlertClient := r.AlertClient(r.apiKey, condition.Spec.Region)

go.mod:

go 1.22.0

require (
    github.com/go-logr/logr v1.4.2
    github.com/newrelic/newrelic-client-go/v2 v2.44.0
    github.com/onsi/ginkgo/v2 v2.19.0
    github.com/onsi/gomega v1.33.1
    k8s.io/apimachinery v0.31.0
    k8s.io/client-go v0.31.0
    sigs.k8s.io/controller-runtime v0.19.0
)

Code snippets below:

interfaces/client.go:

package interfaces

import (
    "fmt"

    "github.com/newrelic/newrelic-client-go/v2/newrelic"
    "github.com/newrelic/newrelic-client-go/v2/pkg/alerts"
    "github.com/newrelic/newrelic-client-go/v2/pkg/config"
)

// NewRelicClientInterface defines the methods for interacting with the NR API
type NewRelicClientInterface interface {
    Alerts() *alerts.Alerts
}

// NewRelicClientWrapper wraps the New Relic client and implements NewRelicClientInterface
type NewRelicClientWrapper struct {
    client *newrelic.NewRelic
}

// Alerts returns the Alerts client
func (n *NewRelicClientWrapper) Alerts() *alerts.Alerts {
    return &n.client.Alerts
}

// NewClient initializes a new instance of NR Client
func NewClient(apiKey string, regionVal string) (*newrelic.NewRelic, error) {
    cfg := config.New()

    client, err := newrelic.New(
        newrelic.ConfigPersonalAPIKey(apiKey),
        newrelic.ConfigLogLevel(cfg.LogLevel),
        newrelic.ConfigRegion(regionVal),
    )

    if err != nil {
        return nil, err
    }

    return client, nil
}

// InitNewClient initalizes all Alerts CRUD functionality
func InitNewClient(apiKey string, regionName string) (NewRelicClientInterface, error) {
    client, err := NewClient(apiKey, regionName)
    if err != nil {
        return nil, fmt.Errorf("unable to create New Relic client with error: %s", err)
    }

    return &NewRelicClientWrapper{client: client}, nil
}

controller/alertpolicy_controller.go:

// AlertPolicyReconciler reconciles a AlertPolicy object
type AlertPolicyReconciler struct {
    client.Client
    Scheme      *runtime.Scheme
    Log         logr.Logger
    Alerts      interfaces.NewRelicClientInterface
    AlertClient func(string, string) (interfaces.NewRelicClientInterface, error)
    apiKey      string
}

func (r *AlertPolicyReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
    _ = log.FromContext(ctx)

    //Fetch AlertPolicy instance
    var policy alertsv1.AlertPolicy
    err := r.Get(ctx, req.NamespacedName, &policy)
    if err != nil {
        if errors.IsNotFound(err) {
            r.Log.Info("Policy 'not found' after being deleted. This is expected and no cause for alarm", "error", err)
            return ctrl.Result{}, nil
        }
        r.Log.Error(err, "Failed to GET policy", "name", req.NamespacedName.String())
        return ctrl.Result{}, err
    }

    r.Log.Info("Starting policy reconcile")

    //get API key
    r.apiKey, err = r.getAPIKeyOrSecret(policy)
    if err != nil {
        return ctrl.Result{}, err
    }

    if r.apiKey == "" {
        return ctrl.Result{}, err
    }

    //init client
    alertClient, errAlertClient := r.AlertClient(r.apiKey, policy.Spec.Region)
    if errAlertClient != nil {
        r.Log.Error(errAlertClient, "Failed to create Alert Client")
        return ctrl.Result{}, errAlertClient
    }
    r.Alerts = alertClient

    ...

}

controllers/nrqlcondition_controller.go:

// NrqlConditionReconciler reconciles a NrqlCondition object
type NrqlConditionReconciler struct {
    client.Client
    Scheme      *runtime.Scheme
    Log         logr.Logger
    Alerts      interfaces.NewRelicClientInterface
    AlertClient func(string, string) (interfaces.NewRelicClientInterface, error)
    apiKey      string
}

func (r *NrqlConditionReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
    _ = log.FromContext(ctx)

    //Fetch NrqlCondition instance
    var condition alertsv1.NrqlCondition
    err := r.Get(ctx, req.NamespacedName, &condition)
    if err != nil {
        if errors.IsNotFound(err) {
            r.Log.Info("Condition 'not found' after being deleted. This is expected and no cause for alarm", "error", err)
            return ctrl.Result{}, nil
        }
        r.Log.Error(err, "Failed to GET nrql condition", "name", req.NamespacedName.String())
        return ctrl.Result{}, err
    }

    r.Log.Info("Starting condition reconcile")

    //get API key
    r.apiKey, err = r.getAPIKeyOrSecret(condition)
    if err != nil {
        return ctrl.Result{}, err
    }

    if r.apiKey == "" {
        return ctrl.Result{}, err
    }

    //init client
    alertClient, errAlertClient := r.AlertClient(r.apiKey, condition.Spec.Region)
    if errAlertClient != nil {
        r.Log.Error(errAlertClient, "Failed to create Alert Client")
        return ctrl.Result{}, errAlertClient
    }
    r.Alerts = alertClient

    ...

}

Solution

  • Figured it out finally - The problem was that the AlertClient was not registered in main.go:

    Before:

        if err = (&controller.NrqlConditionReconciler{
            Client:      mgr.GetClient(),
            Scheme:      mgr.GetScheme(),
        }).SetupWithManager(mgr); err != nil {
            setupLog.Error(err, "unable to create controller", "controller", "NrqlCondition")
            os.Exit(1)
        }
    

    After:

        if err = (&controller.NrqlConditionReconciler{
            Client:      mgr.GetClient(),
            Scheme:      mgr.GetScheme(),
            AlertClient: interfaces.InitNewClient,
        }).SetupWithManager(mgr); err != nil {
            setupLog.Error(err, "unable to create controller", "controller", "NrqlCondition")
            os.Exit(1)
        }