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
...
}
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)
}