Skip to content

Commit

Permalink
fix: cleanup status controller
Browse files Browse the repository at this point in the history
  • Loading branch information
Ajpantuso committed May 17, 2023
1 parent 7c75408 commit 208c2ca
Show file tree
Hide file tree
Showing 3 changed files with 389 additions and 0 deletions.
121 changes: 121 additions & 0 deletions integration/status_controller_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package integration

import (
"context"
"fmt"
"os/exec"
"time"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
. "github.com/onsi/gomega/gexec"
"github.com/onsi/gomega/types"
av1alpha1 "github.com/openshift/addon-operator/apis/addons/v1alpha1"
addoninstance "github.com/openshift/addon-operator/pkg/client"
internaltesting "github.com/openshift/reference-addon/internal/testing"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

var _ = Describe("Status Controller", func() {
var (
ctx context.Context
cancel context.CancelFunc
deleteLabel string
deleteLabelGen = nameGenerator("ref-test-label")
addonInstanceName string
addonInstanceNameGen = nameGenerator("ai-test-name")
namespace string
namespaceGen = nameGenerator("ref-test-namespace")
operatorName string
operatorNameGen = nameGenerator("ref-test-name")
heartbeatInterval = 1 * time.Second
)

BeforeEach(func() {
ctx, cancel = context.WithCancel(context.Background())

addonInstanceName = addonInstanceNameGen()
deleteLabel = deleteLabelGen()
namespace = namespaceGen()
operatorName = operatorNameGen()

By("Starting manager")

manager := exec.Command(_binPath,
"-addon-instance-name", addonInstanceName,
"-addon-instance-namespace", namespace,
"-namespace", namespace,
"-delete-label", deleteLabel,
"-operator-name", operatorName,
"-kubeconfig", _kubeConfigPath,
"-heartbeat-interval", heartbeatInterval.String(),
)

session, err := Start(manager, GinkgoWriter, GinkgoWriter)
Expect(err).ToNot(HaveOccurred())

By("Creating the addon namespace")

ns := addonNamespace(namespace)
addonInstance := addonInstanceObject(addonInstanceName, namespace)

_client.Create(ctx, &ns)
_client.Create(ctx, &addonInstance)

rbac, err := getRBAC(namespace, managerGroup)
Expect(err).ToNot(HaveOccurred())

for _, obj := range rbac {
_client.Create(ctx, obj)
}

DeferCleanup(func() {
cancel()

By("Stopping the managers")

session.Interrupt()

if usingExistingCluster() {
By("Deleting test namspace")

_client.Delete(ctx, &ns)
}
})
})

When("Addon Instance Object Exists", func() {
Context("Reference Addon Status Available'", func() {
It("Addon Instance should report Availalbe condition", func() {
addonInstance := addonInstanceObject(addonInstanceName, namespace)
_client.EventuallyObjectExists(ctx, &addonInstance, internaltesting.WithTimeout(10*time.Second))

expectedCondition := addoninstance.NewAddonInstanceConditionInstalled(
"True",
av1alpha1.AddonInstanceInstalledReasonSetupComplete,
"All Components Available",
)

Eventually(func() []metav1.Condition {
_client.Get(ctx, &addonInstance)

return addonInstance.Status.Conditions
}, 10*time.Second).Should(ContainElements(EqualCondition(expectedCondition)))

fmt.Printf("%+v\n", addonInstance)

Expect(addonInstance.Spec.HeartbeatUpdatePeriod.Duration).To(Equal(heartbeatInterval))
})
})
})
})

// Check if conditions match
func EqualCondition(expected metav1.Condition) types.GomegaMatcher {
return And(
HaveField("Type", expected.Type),
HaveField("Status", expected.Status),
HaveField("Reason", expected.Reason),
HaveField("Message", expected.Message),
)
}
43 changes: 43 additions & 0 deletions internal/controllers/status/options.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package status

import (
"time"

"github.com/go-logr/logr"
)

type WithLog struct{ Log logr.Logger }

func (w WithLog) ConfigureStatusControllerReconciler(c *StatusControllerReconcilerConfig) {
c.Log = w.Log
}

type WithAddonInstanceNamespace string

func (w WithAddonInstanceNamespace) ConfigureStatusControllerReconciler(c *StatusControllerReconcilerConfig) {
c.AddonInstanceNamespace = string(w)
}

type WithAddonInstanceName string

func (w WithAddonInstanceName) ConfigureStatusControllerReconciler(c *StatusControllerReconcilerConfig) {
c.AddonInstanceName = string(w)
}

type WithReferenceAddonNamespace string

func (w WithReferenceAddonNamespace) ConfigureStatusControllerReconciler(c *StatusControllerReconcilerConfig) {
c.ReferenceAddonNamespace = string(w)
}

type WithReferenceAddonName string

func (w WithReferenceAddonName) ConfigureStatusControllerReconciler(c *StatusControllerReconcilerConfig) {
c.ReferenceAddonName = string(w)
}

type WithHeartbeatInterval time.Duration

func (w WithHeartbeatInterval) ConfigureStatusControllerReconciler(c *StatusControllerReconcilerConfig) {
c.HeartBeatInterval = time.Duration(w)
}
225 changes: 225 additions & 0 deletions internal/controllers/status/status_controller.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
package status

import (
"context"
"encoding/json"
"fmt"
"time"

"github.com/go-logr/logr"

av1alpha1 "github.com/openshift/addon-operator/apis/addons/v1alpha1"
addoninstance "github.com/openshift/addon-operator/pkg/client"
rv1alpha1 "github.com/openshift/reference-addon/apis/reference/v1alpha1"
"github.com/openshift/reference-addon/internal/controllers"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"

ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/builder"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/handler"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"sigs.k8s.io/controller-runtime/pkg/source"
)

type StatusControllerReconciler struct {
cfg StatusControllerReconcilerConfig
client client.Client
addonInstanceClient *addoninstance.AddonInstanceClientImpl
}

// Grabbing namespace/name needs to be an option
func NewStatusControllerReconciler(client client.Client, opts ...StatusControllerReconcilerOption) (*StatusControllerReconciler, error) {
var cfg StatusControllerReconcilerConfig

cfg.Option(opts...)
cfg.Default()

return &StatusControllerReconciler{
cfg: cfg,
client: client,
addonInstanceClient: addoninstance.NewAddonInstanceClient(client),
}, nil
}

type StatusControllerReconcilerConfig struct {
Log logr.Logger

AddonInstanceNamespace string
AddonInstanceName string
ReferenceAddonNamespace string
ReferenceAddonName string
HeartBeatInterval time.Duration
}

type StatusControllerReconcilerOption interface {
ConfigureStatusControllerReconciler(*StatusControllerReconcilerConfig)
}

// Status controller option
func (c *StatusControllerReconcilerConfig) Option(opts ...StatusControllerReconcilerOption) {
for _, opt := range opts {
opt.ConfigureStatusControllerReconciler(c)
}
}

func (c *StatusControllerReconcilerConfig) Default() {
if c.Log.GetSink() == nil {
c.Log = logr.Discard()
}
if c.HeartBeatInterval == 0 {
c.HeartBeatInterval = 10 * time.Second
}
}

// Watch reference addon actions to trigger addon instance
func (r *StatusControllerReconciler) SetupWithManager(mgr ctrl.Manager) error {
//desired := r.desiredReferenceAddon()
requestObject := types.NamespacedName{
Name: r.cfg.AddonInstanceName,
Namespace: r.cfg.AddonInstanceNamespace,
}

statusControllerHandler := handler.EnqueueRequestsFromMapFunc(func(_ client.Object) []reconcile.Request {
return []reconcile.Request{
{
NamespacedName: requestObject,
},
}
})
return ctrl.NewControllerManagedBy(mgr).
For(&av1alpha1.AddonInstance{}).
Watches(
&source.Kind{Type: &rv1alpha1.ReferenceAddon{}},
statusControllerHandler,
builder.WithPredicates(controllers.HasNamePrefix(r.cfg.ReferenceAddonName)),
).
Complete(r)
}

// Utilize info gathered from SetupWithManager to perform logic against
func (r *StatusControllerReconciler) Reconcile(ctx context.Context, req reconcile.Request) (ctrl.Result, error) {
ai, err := r.getAddonInstance(ctx)
if err != nil {
r.cfg.Log.Error(err, "getting addon instance")

return ctrl.Result{RequeueAfter: r.cfg.HeartBeatInterval}, nil
}

if ai.Spec.HeartbeatUpdatePeriod.Duration != r.cfg.HeartBeatInterval {
r.cfg.Log.Info("patching heartbeat interval")

if err := r.patchHeartbeatInterval(ctx, ai); err != nil {
r.cfg.Log.Error(err, "patching heartbeat interval")

return ctrl.Result{RequeueAfter: r.cfg.HeartBeatInterval}, nil
}
}

refAddon, err := r.getReferenceAddon(ctx)
if err != nil {
r.cfg.Log.Error(err, "getting reference addon")

return ctrl.Result{RequeueAfter: r.cfg.HeartBeatInterval}, nil
}

conditions := r.getConditions(refAddon)

if err := r.addonInstanceClient.SendPulse(ctx, ai, addoninstance.WithConditions(conditions)); err != nil {
r.cfg.Log.Error(err, "sending pulse to addon instance")

return ctrl.Result{}, err
}

r.cfg.Log.Info("successfully reconciled AddonInstance")

return ctrl.Result{RequeueAfter: r.cfg.HeartBeatInterval}, nil
}

func (r *StatusControllerReconciler) getAddonInstance(ctx context.Context) (av1alpha1.AddonInstance, error) {
log := r.cfg.Log.WithValues(
"namespace", r.cfg.AddonInstanceNamespace,
"name", r.cfg.AddonInstanceName,
)

addonInstanceKey := client.ObjectKey{
Namespace: r.cfg.AddonInstanceNamespace,
Name: r.cfg.AddonInstanceName,
}

var addonInstance av1alpha1.AddonInstance
if err := r.client.Get(ctx, addonInstanceKey, &addonInstance); err != nil {
return addonInstance, fmt.Errorf("getting addon instance: %w", err)
}

log.Info("found addon instance")

return addonInstance, nil
}

func (r *StatusControllerReconciler) patchHeartbeatInterval(ctx context.Context, ai av1alpha1.AddonInstance) error {
patch := map[string]interface{}{
"metadata": map[string]interface{}{
"resourceVersion": ai.GetResourceVersion(),
},
"spec": map[string]interface{}{
"heartbeatUpdatePeriod": metav1.Duration{
Duration: r.cfg.HeartBeatInterval,
},
},
}

patchJson, err := json.Marshal(&patch)
if err != nil {
return fmt.Errorf("marshalling raw patch: %w", err)
}

return r.client.Patch(ctx, &ai, client.RawPatch(types.MergePatchType, patchJson))
}

func (r *StatusControllerReconciler) getReferenceAddon(ctx context.Context) (rv1alpha1.ReferenceAddon, error) {
log := r.cfg.Log.WithValues(
"namespace", r.cfg.ReferenceAddonNamespace,
"name", r.cfg.ReferenceAddonName,
)

log.Info("getting reference addon")

referenceAddonKey := client.ObjectKey{
Namespace: r.cfg.ReferenceAddonNamespace,
Name: r.cfg.ReferenceAddonName,
}

var referenceAddon rv1alpha1.ReferenceAddon
if err := r.client.Get(ctx, referenceAddonKey, &referenceAddon); err != nil {
return referenceAddon, fmt.Errorf("getting reference addon: %w", err)
}

log.Info("found reference addon")

return referenceAddon, nil
}

func (r *StatusControllerReconciler) getConditions(ra rv1alpha1.ReferenceAddon) []metav1.Condition {
var conditions []metav1.Condition

isAvailable := meta.IsStatusConditionTrue(
ra.Status.Conditions,
rv1alpha1.ReferenceAddonConditionAvailable.String(),
)

// Check if Reference addon is available for the first time
if isAvailable {
r.cfg.Log.Info("Reference Addon Successfully Installed")

conditions = append(conditions, addoninstance.NewAddonInstanceConditionInstalled(
"True",
av1alpha1.AddonInstanceInstalledReasonSetupComplete,
"All Components Available",
))
}

return conditions
}

0 comments on commit 208c2ca

Please sign in to comment.