refactor: provide a base reconciler to provide common functionnalities

- singleton
- finalizer

Signed-off-by: Luca Burgazzoli <lburgazzoli@gmail.com>
This commit is contained in:
Luca Burgazzoli 2024-06-18 16:55:46 +02:00
parent 4fad550e78
commit 025596d9d3
No known key found for this signature in database
GPG Key ID: 714C91BFF2D29141
19 changed files with 313 additions and 188 deletions

View File

@ -19,6 +19,8 @@ import (
"encoding/json"
"errors"
"fmt"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
var ErrUnmarshalOnNil = errors.New("UnmarshalJSON on nil pointer")
@ -94,3 +96,9 @@ type ChartMeta struct {
Name string `json:"name,omitempty"`
Version string `json:"version,omitempty"`
}
type Status struct {
Phase string `json:"phase"`
Conditions []metav1.Condition `json:"conditions,omitempty"`
ObservedGeneration int64 `json:"observedGeneration,omitempty"`
}

View File

@ -27,10 +27,8 @@ type DaprControlPlaneSpec struct {
}
type DaprControlPlaneStatus struct {
Phase string `json:"phase"`
Conditions []metav1.Condition `json:"conditions,omitempty"`
ObservedGeneration int64 `json:"observedGeneration,omitempty"`
Chart *ChartMeta `json:"chart,omitempty"`
Status `json:",inline"`
Chart *ChartMeta `json:"chart,omitempty"`
}
// +genclient
@ -53,6 +51,10 @@ type DaprControlPlane struct {
Status DaprControlPlaneStatus `json:"status,omitempty"`
}
func (in *DaprControlPlane) GetStatus() *Status {
return &in.Status.Status
}
func (in *DaprControlPlane) GetConditions() conditions.Conditions {
return in.Status.Conditions
}

View File

@ -27,10 +27,8 @@ type DaprCruiseControlSpec struct {
// DaprCruiseControlStatus defines the observed state of DaprCruiseControl.
type DaprCruiseControlStatus struct {
Phase string `json:"phase"`
Conditions []metav1.Condition `json:"conditions,omitempty"`
ObservedGeneration int64 `json:"observedGeneration,omitempty"`
Chart *ChartMeta `json:"chart,omitempty"`
Status `json:",inline"`
Chart *ChartMeta `json:"chart,omitempty"`
}
// +genclient
@ -53,6 +51,10 @@ type DaprCruiseControl struct {
Status DaprCruiseControlStatus `json:"status,omitempty"`
}
func (in *DaprCruiseControl) GetStatus() *Status {
return &in.Status.Status
}
func (in *DaprCruiseControl) GetConditions() conditions.Conditions {
return in.Status.Conditions
}

View File

@ -32,10 +32,8 @@ type DaprInstanceSpec struct {
// DaprInstanceStatus defines the observed state of DaprInstance.
type DaprInstanceStatus struct {
Phase string `json:"phase"`
Conditions []metav1.Condition `json:"conditions,omitempty"`
ObservedGeneration int64 `json:"observedGeneration,omitempty"`
Chart *ChartMeta `json:"chart,omitempty"`
Status `json:",inline"`
Chart *ChartMeta `json:"chart,omitempty"`
}
// +genclient
@ -58,6 +56,10 @@ type DaprInstance struct {
Status DaprInstanceStatus `json:"status,omitempty"`
}
func (in *DaprInstance) GetStatus() *Status {
return &in.Status.Status
}
func (in *DaprInstance) GetConditions() conditions.Conditions {
return in.Status.Conditions
}

View File

@ -137,13 +137,7 @@ func (in *DaprControlPlaneSpec) DeepCopy() *DaprControlPlaneSpec {
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *DaprControlPlaneStatus) DeepCopyInto(out *DaprControlPlaneStatus) {
*out = *in
if in.Conditions != nil {
in, out := &in.Conditions, &out.Conditions
*out = make([]v1.Condition, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
in.Status.DeepCopyInto(&out.Status)
if in.Chart != nil {
in, out := &in.Chart, &out.Chart
*out = new(ChartMeta)
@ -238,13 +232,7 @@ func (in *DaprCruiseControlSpec) DeepCopy() *DaprCruiseControlSpec {
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *DaprCruiseControlStatus) DeepCopyInto(out *DaprCruiseControlStatus) {
*out = *in
if in.Conditions != nil {
in, out := &in.Conditions, &out.Conditions
*out = make([]v1.Condition, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
in.Status.DeepCopyInto(&out.Status)
if in.Chart != nil {
in, out := &in.Chart, &out.Chart
*out = new(ChartMeta)
@ -349,13 +337,7 @@ func (in *DaprInstanceSpec) DeepCopy() *DaprInstanceSpec {
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *DaprInstanceStatus) DeepCopyInto(out *DaprInstanceStatus) {
*out = *in
if in.Conditions != nil {
in, out := &in.Conditions, &out.Conditions
*out = make([]v1.Condition, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
in.Status.DeepCopyInto(&out.Status)
if in.Chart != nil {
in, out := &in.Chart, &out.Chart
*out = new(ChartMeta)
@ -411,3 +393,25 @@ func (in RawMessage) DeepCopy() RawMessage {
in.DeepCopyInto(out)
return *out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Status) DeepCopyInto(out *Status) {
*out = *in
if in.Conditions != nil {
in, out := &in.Conditions, &out.Conditions
*out = make([]v1.Condition, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Status.
func (in *Status) DeepCopy() *Status {
if in == nil {
return nil
}
out := new(Status)
in.DeepCopyInto(out)
return out
}

View File

@ -8,5 +8,5 @@ apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
images:
- name: controller
newName: ghcr.io/dapr-sandbox/dapr-kubernetes-operator
newName: ghcr.io/dapr/kubernetes-operator
newTag: 0.0.8

View File

@ -5,7 +5,7 @@ metadata:
namespace: olm
spec:
sourceType: grpc
image: ghcr.io/dapr-sandbox/dapr-kubernetes-operator-catalog:latest
image: ghcr.io/dapr/kubernetes-operator-catalog:latest
displayName: dapr.io catalog
grpcPodConfig:
securityContextConfig: restricted

View File

@ -20,23 +20,24 @@ import (
"context"
"fmt"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"github.com/dapr/kubernetes-operator/pkg/openshift"
"github.com/dapr/kubernetes-operator/pkg/helm"
"k8s.io/client-go/tools/record"
"helm.sh/helm/v3/pkg/chart"
"helm.sh/helm/v3/pkg/chart/loader"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/controller-runtime/pkg/builder"
"sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/predicate"
daprApi "github.com/dapr/kubernetes-operator/api/operator/v1alpha1"
"github.com/dapr/kubernetes-operator/pkg/controller"
"github.com/dapr/kubernetes-operator/pkg/controller/client"
"github.com/dapr/kubernetes-operator/pkg/controller/reconciler"
"github.com/dapr/kubernetes-operator/pkg/helm"
"github.com/dapr/kubernetes-operator/pkg/openshift"
"github.com/go-logr/logr"
ctrlRt "sigs.k8s.io/controller-runtime"
@ -129,9 +130,19 @@ func (r *Reconciler) init(ctx context.Context) error {
c = b
}
objRec := reconcile.AsReconciler[*daprApi.DaprControlPlane](r.manager.GetClient(), r)
// by default, the controller expect the DaprControlPlane resource to be created
// in the same namespace where it runs, if not fallback to the default namespace
rec := reconciler.BaseReconciler[*daprApi.DaprControlPlane]{
Delegate: r,
Client: r.client,
Log: log.FromContext(ctx),
Name: DaprControlPlaneResourceName,
Namespace: controller.OperatorNamespace(),
FinalizerName: DaprControlPlaneFinalizerName,
FinalizerAction: r.Cleanup,
}
ct, err := c.Build(objRec)
ct, err := c.Build(&rec)
if err != nil {
return fmt.Errorf("failure building the application controller for DaprControlPlane resource: %w", err)
}

View File

@ -20,13 +20,8 @@ import (
"context"
"errors"
"fmt"
"os"
"sort"
"github.com/dapr/kubernetes-operator/pkg/controller/reconciler"
"github.com/dapr/kubernetes-operator/pkg/controller"
"github.com/dapr/kubernetes-operator/pkg/conditions"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
@ -39,10 +34,8 @@ import (
daprApi "github.com/dapr/kubernetes-operator/api/operator/v1alpha1"
)
func (r *Reconciler) Reconcile(ctx context.Context, res *daprApi.DaprControlPlane) (ctrl.Result, error) {
l := log.FromContext(ctx)
rr := ReconciliationRequest{
func (r *Reconciler) reconciliationRequest(res *daprApi.DaprControlPlane) ReconciliationRequest {
return ReconciliationRequest{
Client: r.Client(),
NamespacedName: types.NamespacedName{
Name: res.Name,
@ -52,62 +45,14 @@ func (r *Reconciler) Reconcile(ctx context.Context, res *daprApi.DaprControlPlan
Reconciler: r,
Resource: res,
}
}
func (r *Reconciler) Reconcile(ctx context.Context, res *daprApi.DaprControlPlane) (ctrl.Result, error) {
rr := r.reconciliationRequest(res)
l := log.FromContext(ctx)
l.Info("Reconciling", "resource", rr.NamespacedName.String())
// by default, the controller expect the DaprControlPlane resource to be created
// in the same namespace where it runs, if not fallback to the default namespace
// dapr-system
ns := os.Getenv(controller.NamespaceEnv)
if ns == "" {
ns = controller.NamespaceDefault
}
if res.Name != DaprControlPlaneResourceName || res.Namespace != ns {
rr.Resource.Status.Phase = conditions.TypeError
meta.SetStatusCondition(&rr.Resource.Status.Conditions, metav1.Condition{
Type: conditions.TypeReconciled,
Status: metav1.ConditionFalse,
Reason: conditions.ReasonUnsupportedConfiguration,
Message: fmt.Sprintf(
"Unsupported resource, the operator handles a single DaprControlPlane resource named %s in namespace %s",
DaprControlPlaneResourceName,
ns),
})
err := r.Client().Status().Update(ctx, rr.Resource)
if err != nil && k8serrors.IsConflict(err) {
l.Info(err.Error())
return ctrl.Result{Requeue: true}, nil
}
return ctrl.Result{}, fmt.Errorf("error updating DaprControlPlane resource: %w", err)
}
//nolint:wrapcheck
if rr.Resource.ObjectMeta.DeletionTimestamp.IsZero() {
err := reconciler.AddFinalizer(ctx, r.Client(), rr.Resource, DaprControlPlaneFinalizerName)
if err != nil {
return ctrl.Result{}, err
}
} else {
// Cleanup leftovers if needed
for i := len(r.actions) - 1; i >= 0; i-- {
if err := r.actions[i].Cleanup(ctx, &rr); err != nil {
return ctrl.Result{}, err
}
}
err := reconciler.RemoveFinalizer(ctx, r.Client(), rr.Resource, DaprControlPlaneFinalizerName)
if err != nil {
return ctrl.Result{}, err
}
return ctrl.Result{}, nil
}
//
// Reconcile
//
@ -159,3 +104,19 @@ func (r *Reconciler) Reconcile(ctx context.Context, res *daprApi.DaprControlPlan
return ctrl.Result{}, errors.Join(errs...)
}
func (r *Reconciler) Cleanup(ctx context.Context, res *daprApi.DaprControlPlane) error {
rr := r.reconciliationRequest(res)
l := log.FromContext(ctx)
l.Info("Cleanup", "resource", rr.NamespacedName.String())
// Cleanup leftovers if needed
for i := len(r.actions) - 1; i >= 0; i-- {
if err := r.actions[i].Cleanup(ctx, &rr); err != nil {
return fmt.Errorf("failure running cleanup action: %w", err)
}
}
return nil
}

View File

@ -20,15 +20,11 @@ import (
"context"
"fmt"
"github.com/dapr/kubernetes-operator/pkg/openshift"
"github.com/dapr/kubernetes-operator/pkg/helm"
"k8s.io/client-go/tools/record"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"sigs.k8s.io/controller-runtime/pkg/handler"
"sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"sigs.k8s.io/controller-runtime/pkg/source"
"helm.sh/helm/v3/pkg/chart"
@ -38,8 +34,12 @@ import (
"sigs.k8s.io/controller-runtime/pkg/predicate"
daprApi "github.com/dapr/kubernetes-operator/api/operator/v1alpha1"
"github.com/dapr/kubernetes-operator/pkg/controller"
"github.com/dapr/kubernetes-operator/pkg/controller/client"
"github.com/dapr/kubernetes-operator/pkg/controller/reconciler"
"github.com/dapr/kubernetes-operator/pkg/helm"
"github.com/dapr/kubernetes-operator/pkg/openshift"
"github.com/go-logr/logr"
ctrlRt "sigs.k8s.io/controller-runtime"
@ -159,9 +159,19 @@ func (r *Reconciler) init(ctx context.Context) error {
c = b
}
objRec := reconcile.AsReconciler[*daprApi.DaprInstance](r.manager.GetClient(), r)
// by default, the controller expect the DaprControlPlane resource to be created
// in the same namespace where it runs, if not fallback to the default namespace
rec := reconciler.BaseReconciler[*daprApi.DaprInstance]{
Delegate: r,
Client: r.client,
Log: log.FromContext(ctx),
Name: DaprInstanceResourceName,
Namespace: controller.OperatorNamespace(),
FinalizerName: DaprInstanceFinalizerName,
FinalizerAction: r.Cleanup,
}
ct, err := c.Build(objRec)
ct, err := c.Build(&rec)
if err != nil {
return fmt.Errorf("failure building the application controller for DaprInstance resource: %w", err)
}

View File

@ -20,13 +20,8 @@ import (
"context"
"errors"
"fmt"
"os"
"sort"
"github.com/dapr/kubernetes-operator/pkg/controller/reconciler"
"github.com/dapr/kubernetes-operator/pkg/controller"
"github.com/dapr/kubernetes-operator/pkg/conditions"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
@ -39,10 +34,8 @@ import (
daprApi "github.com/dapr/kubernetes-operator/api/operator/v1alpha1"
)
func (r *Reconciler) Reconcile(ctx context.Context, res *daprApi.DaprInstance) (ctrl.Result, error) {
l := log.FromContext(ctx)
rr := ReconciliationRequest{
func (r *Reconciler) reconciliationRequest(res *daprApi.DaprInstance) ReconciliationRequest {
return ReconciliationRequest{
Client: r.Client(),
NamespacedName: types.NamespacedName{
Name: res.Name,
@ -59,62 +52,14 @@ func (r *Reconciler) Reconcile(ctx context.Context, res *daprApi.DaprInstance) (
"dapr_dashboard": map[string]interface{}{"runAsNonRoot": "true"},
},
}
}
func (r *Reconciler) Reconcile(ctx context.Context, res *daprApi.DaprInstance) (ctrl.Result, error) {
rr := r.reconciliationRequest(res)
l := log.FromContext(ctx)
l.Info("Reconciling", "resource", rr.NamespacedName.String())
// by default, the controller expect the DaprInstance resource to be created
// in the same namespace where it runs, if not fallback to the default namespace
// dapr-system
ns := os.Getenv(controller.NamespaceEnv)
if ns == "" {
ns = controller.NamespaceDefault
}
if res.Name != DaprInstanceResourceName || res.Namespace != ns {
rr.Resource.Status.Phase = conditions.TypeError
meta.SetStatusCondition(&rr.Resource.Status.Conditions, metav1.Condition{
Type: conditions.TypeReconciled,
Status: metav1.ConditionFalse,
Reason: conditions.ReasonUnsupportedConfiguration,
Message: fmt.Sprintf(
"Unsupported resource, the operator handles a single DaprInstance resource named %s in namespace %s",
DaprInstanceResourceName,
ns),
})
err := r.Client().Status().Update(ctx, rr.Resource)
if err != nil && k8serrors.IsConflict(err) {
l.Info(err.Error())
return ctrl.Result{Requeue: true}, nil
}
return ctrl.Result{}, fmt.Errorf("error updating DaprInstance resource: %w", err)
}
//nolint:wrapcheck
if rr.Resource.ObjectMeta.DeletionTimestamp.IsZero() {
err := reconciler.AddFinalizer(ctx, r.Client(), rr.Resource, DaprInstanceFinalizerName)
if err != nil {
return ctrl.Result{}, err
}
} else {
// Cleanup leftovers if needed
for i := len(r.actions) - 1; i >= 0; i-- {
if err := r.actions[i].Cleanup(ctx, &rr); err != nil {
return ctrl.Result{}, err
}
}
err := reconciler.RemoveFinalizer(ctx, r.Client(), rr.Resource, DaprInstanceFinalizerName)
if err != nil {
return ctrl.Result{}, err
}
return ctrl.Result{}, nil
}
//
// Reconcile
//
@ -167,3 +112,19 @@ func (r *Reconciler) Reconcile(ctx context.Context, res *daprApi.DaprInstance) (
return ctrl.Result{}, errors.Join(errs...)
}
func (r *Reconciler) Cleanup(ctx context.Context, res *daprApi.DaprInstance) error {
rr := r.reconciliationRequest(res)
l := log.FromContext(ctx)
l.Info("Cleanup", "resource", rr.NamespacedName.String())
// Cleanup leftovers if needed
for i := len(r.actions) - 1; i >= 0; i-- {
if err := r.actions[i].Cleanup(ctx, &rr); err != nil {
return fmt.Errorf("failure running cleanup action: %w", err)
}
}
return nil
}

View File

@ -24,10 +24,8 @@ import (
// DaprControlPlaneStatusApplyConfiguration represents an declarative configuration of the DaprControlPlaneStatus type for use
// with apply.
type DaprControlPlaneStatusApplyConfiguration struct {
Phase *string `json:"phase,omitempty"`
Conditions []v1.ConditionApplyConfiguration `json:"conditions,omitempty"`
ObservedGeneration *int64 `json:"observedGeneration,omitempty"`
Chart *ChartMetaApplyConfiguration `json:"chart,omitempty"`
StatusApplyConfiguration `json:",inline"`
Chart *ChartMetaApplyConfiguration `json:"chart,omitempty"`
}
// DaprControlPlaneStatusApplyConfiguration constructs an declarative configuration of the DaprControlPlaneStatus type for use with

View File

@ -24,10 +24,8 @@ import (
// DaprCruiseControlStatusApplyConfiguration represents an declarative configuration of the DaprCruiseControlStatus type for use
// with apply.
type DaprCruiseControlStatusApplyConfiguration struct {
Phase *string `json:"phase,omitempty"`
Conditions []v1.ConditionApplyConfiguration `json:"conditions,omitempty"`
ObservedGeneration *int64 `json:"observedGeneration,omitempty"`
Chart *ChartMetaApplyConfiguration `json:"chart,omitempty"`
StatusApplyConfiguration `json:",inline"`
Chart *ChartMetaApplyConfiguration `json:"chart,omitempty"`
}
// DaprCruiseControlStatusApplyConfiguration constructs an declarative configuration of the DaprCruiseControlStatus type for use with

View File

@ -24,10 +24,8 @@ import (
// DaprInstanceStatusApplyConfiguration represents an declarative configuration of the DaprInstanceStatus type for use
// with apply.
type DaprInstanceStatusApplyConfiguration struct {
Phase *string `json:"phase,omitempty"`
Conditions []v1.ConditionApplyConfiguration `json:"conditions,omitempty"`
ObservedGeneration *int64 `json:"observedGeneration,omitempty"`
Chart *ChartMetaApplyConfiguration `json:"chart,omitempty"`
StatusApplyConfiguration `json:",inline"`
Chart *ChartMetaApplyConfiguration `json:"chart,omitempty"`
}
// DaprInstanceStatusApplyConfiguration constructs an declarative configuration of the DaprInstanceStatus type for use with

View File

@ -0,0 +1,65 @@
/*
Copyright 2023.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Code generated by applyconfiguration-gen. DO NOT EDIT.
package v1alpha1
import (
v1 "k8s.io/client-go/applyconfigurations/meta/v1"
)
// StatusApplyConfiguration represents an declarative configuration of the Status type for use
// with apply.
type StatusApplyConfiguration struct {
Phase *string `json:"phase,omitempty"`
Conditions []v1.ConditionApplyConfiguration `json:"conditions,omitempty"`
ObservedGeneration *int64 `json:"observedGeneration,omitempty"`
}
// StatusApplyConfiguration constructs an declarative configuration of the Status type for use with
// apply.
func Status() *StatusApplyConfiguration {
return &StatusApplyConfiguration{}
}
// WithPhase sets the Phase field in the declarative configuration to the given value
// and returns the receiver, so that objects can be built by chaining "With" function invocations.
// If called multiple times, the Phase field is set to the value of the last call.
func (b *StatusApplyConfiguration) WithPhase(value string) *StatusApplyConfiguration {
b.Phase = &value
return b
}
// WithConditions adds the given value to the Conditions field in the declarative configuration
// and returns the receiver, so that objects can be build by chaining "With" function invocations.
// If called multiple times, values provided by each call will be appended to the Conditions field.
func (b *StatusApplyConfiguration) WithConditions(values ...*v1.ConditionApplyConfiguration) *StatusApplyConfiguration {
for i := range values {
if values[i] == nil {
panic("nil value passed to WithConditions")
}
b.Conditions = append(b.Conditions, *values[i])
}
return b
}
// WithObservedGeneration sets the ObservedGeneration field in the declarative configuration to the given value
// and returns the receiver, so that objects can be built by chaining "With" function invocations.
// If called multiple times, the ObservedGeneration field is set to the value of the last call.
func (b *StatusApplyConfiguration) WithObservedGeneration(value int64) *StatusApplyConfiguration {
b.ObservedGeneration = &value
return b
}

View File

@ -50,6 +50,8 @@ func ForKind(kind schema.GroupVersionKind) interface{} {
return &operatorv1alpha1.DaprInstanceStatusApplyConfiguration{}
case v1alpha1.SchemeGroupVersion.WithKind("JSON"):
return &operatorv1alpha1.JSONApplyConfiguration{}
case v1alpha1.SchemeGroupVersion.WithKind("Status"):
return &operatorv1alpha1.StatusApplyConfiguration{}
}
return nil

View File

@ -4,6 +4,7 @@ import (
"fmt"
"net/http"
"net/http/pprof"
"os"
"time"
// Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.)
@ -105,3 +106,14 @@ func Start(options Options, setup func(manager.Manager, Options) error) error {
return nil
}
func OperatorNamespace() string {
// by default, the controller expect singleton resources to be created in the same
// namespace where it runs, if not fallback to the default namespace
ns := os.Getenv(NamespaceEnv)
if ns == "" {
ns = NamespaceDefault
}
return ns
}

View File

@ -3,12 +3,23 @@ package reconciler
import (
"context"
"fmt"
"reflect"
"github.com/dapr/kubernetes-operator/pkg/conditions"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/go-logr/logr"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"github.com/dapr/kubernetes-operator/pkg/controller/client"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/builder"
ctrlClient "sigs.k8s.io/controller-runtime/pkg/client"
ctrlutil "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
"github.com/dapr/kubernetes-operator/pkg/controller"
"github.com/dapr/kubernetes-operator/pkg/controller/client"
)
type Reconciler interface {
@ -54,3 +65,73 @@ func RemoveFinalizer(ctx context.Context, client ctrlClient.Client, o ctrlClient
return nil
}
type BaseReconciler[T controller.ResourceObject] struct {
Log logr.Logger
Name string
Namespace string
FinalizerName string
FinalizerAction func(ctx context.Context, res T) error
Delegate reconcile.ObjectReconciler[T]
Client ctrlClient.Client
}
//nolint:forcetypeassert,wrapcheck,nestif
func (s *BaseReconciler[T]) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
res := reflect.New(reflect.TypeOf(*new(T)).Elem()).Interface().(T)
if err := s.Client.Get(ctx, req.NamespacedName, res); err != nil {
return ctrl.Result{}, ctrlClient.IgnoreNotFound(err)
}
if res.GetName() != s.Name || res.GetNamespace() != s.Namespace {
res.GetStatus().Phase = conditions.TypeError
meta.SetStatusCondition(&res.GetStatus().Conditions, metav1.Condition{
Type: conditions.TypeReconciled,
Status: metav1.ConditionFalse,
Reason: conditions.ReasonUnsupportedConfiguration,
Message: fmt.Sprintf(
"Unsupported resource, the operator handles a single %s resource named %s in namespace %s",
res.GetObjectKind().GroupVersionKind().String(),
s.Name,
s.Namespace),
})
err := s.Client.Status().Update(ctx, res)
if err != nil && k8serrors.IsConflict(err) {
s.Log.Info(err.Error())
return ctrl.Result{Requeue: true}, nil
}
if err != nil {
return ctrl.Result{}, fmt.Errorf("error updating %s resource: %w", res.GetObjectKind().GroupVersionKind().String(), err)
}
return ctrl.Result{}, nil
}
//nolint:wrapcheck
if res.GetDeletionTimestamp().IsZero() {
err := AddFinalizer(ctx, s.Client, res, s.FinalizerName)
if err != nil {
return ctrl.Result{}, err
}
} else {
if s.FinalizerAction != nil {
err := s.FinalizerAction(ctx, res)
if err != nil {
return ctrl.Result{}, err
}
}
err := RemoveFinalizer(ctx, s.Client, res, s.FinalizerName)
if err != nil {
return ctrl.Result{}, err
}
return ctrl.Result{}, nil
}
return s.Delegate.Reconcile(ctx, res)
}

View File

@ -1,6 +1,7 @@
package controller
import (
daptCtrlApi "github.com/dapr/kubernetes-operator/api/operator/v1alpha1"
rtcache "sigs.k8s.io/controller-runtime/pkg/cache"
rtclient "sigs.k8s.io/controller-runtime/pkg/client"
)
@ -26,3 +27,12 @@ type Options struct {
ReleaseLeaderElectionOnCancel bool
WatchSelectors map[rtclient.Object]rtcache.ByObject
}
type WithStatus interface {
GetStatus() *daptCtrlApi.Status
}
type ResourceObject interface {
rtclient.Object
WithStatus
}