why i can't change value from cr in kubernetes operator

Viewed 759

I had such operator code

clock_types.go

package v1

import (
    metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// EDIT THIS FILE!  THIS IS SCAFFOLDING FOR YOU TO OWN!
// NOTE: json tags are required.  Any new fields you add must have json tags for the fields to be serialized.

// ClockSpec defines the desired state of Clock
type ClockSpec struct {
    // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
    // Important: Run "operator-sdk generate k8s" to regenerate code after modifying this file
    // Add custom validation using kubebuilder tags: https://book-v1.book.kubebuilder.io/beyond_basics/generating_crd.html
    Reset       bool   `json:"reset"`
    CurrentDay  string `json:"current_day"`
    CurrentTime string `json:"current_time"`
}

// ClockStatus defines the observed state of Clock
type ClockStatus struct {
    // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
    // Important: Run "operator-sdk generate k8s" to regenerate code after modifying this file
    // Add custom validation using kubebuilder tags: https://book-v1.book.kubebuilder.io/beyond_basics/generating_crd.html
    Reset       bool   `json:"reset"`
    CurrentDay  string `json:"current_day"`
    CurrentTime string `json:"current_time"`
}

// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object

// Clock is the Schema for the clocks API
// +kubebuilder:subresource:status
// +kubebuilder:resource:path=clocks,scope=Namespaced
type Clock struct {
    metav1.TypeMeta   `json:",inline"`
    metav1.ObjectMeta `json:"metadata,omitempty"`

    Spec   ClockSpec   `json:"spec,omitempty"`
    Status ClockStatus `json:"status,omitempty"`
}

// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object

// ClockList contains a list of Clock
type ClockList struct {
    metav1.TypeMeta `json:",inline"`
    metav1.ListMeta `json:"metadata,omitempty"`
    Items           []Clock `json:"items"`
}

func init() {
    SchemeBuilder.Register(&Clock{}, &ClockList{})
}

clock_controller.go

package clock

import (
    "context"
    "strconv"

    "fmt"

    clockv1 "github.com/iamgabrielwu/clock/pkg/apis/clock/v1"
    corev1 "k8s.io/api/core/v1"
    "k8s.io/apimachinery/pkg/api/errors"
    metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    "k8s.io/apimachinery/pkg/runtime"
    "k8s.io/apimachinery/pkg/types"
    "sigs.k8s.io/controller-runtime/pkg/client"
    "sigs.k8s.io/controller-runtime/pkg/controller"
    "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
    "sigs.k8s.io/controller-runtime/pkg/handler"
    logf "sigs.k8s.io/controller-runtime/pkg/log"
    "sigs.k8s.io/controller-runtime/pkg/manager"
    "sigs.k8s.io/controller-runtime/pkg/reconcile"
    "sigs.k8s.io/controller-runtime/pkg/source"
)

var log = logf.Log.WithName("controller_clock")

/**
* USER ACTION REQUIRED: This is a scaffold file intended for the user to modify with their own Controller
* business logic.  Delete these comments after modifying this file.*
 */

// Add creates a new Clock Controller and adds it to the Manager. The Manager will set fields on the Controller
// and Start it when the Manager is Started.
func Add(mgr manager.Manager) error {
    return add(mgr, newReconciler(mgr))
}

// newReconciler returns a new reconcile.Reconciler
func newReconciler(mgr manager.Manager) reconcile.Reconciler {
    return &ReconcileClock{client: mgr.GetClient(), scheme: mgr.GetScheme()}
}

// add adds a new Controller to mgr with r as the reconcile.Reconciler
func add(mgr manager.Manager, r reconcile.Reconciler) error {
    // Create a new controller
    c, err := controller.New("clock-controller", mgr, controller.Options{Reconciler: r})
    if err != nil {
        return err
    }

    // Watch for changes to primary resource Clock
    err = c.Watch(&source.Kind{Type: &clockv1.Clock{}}, &handler.EnqueueRequestForObject{})
    if err != nil {
        return err
    }

    // TODO(user): Modify this to be the types you create that are owned by the primary resource
    // Watch for changes to secondary resource Pods and requeue the owner Clock
    err = c.Watch(&source.Kind{Type: &corev1.Pod{}}, &handler.EnqueueRequestForOwner{
        IsController: true,
        OwnerType:    &clockv1.Clock{},
    })
    if err != nil {
        return err
    }

    return nil
}

// blank assignment to verify that ReconcileClock implements reconcile.Reconciler
var _ reconcile.Reconciler = &ReconcileClock{}

// ReconcileClock reconciles a Clock object
type ReconcileClock struct {
    // This client, initialized using mgr.Client() above, is a split client
    // that reads objects from the cache and writes to the apiserver
    client client.Client
    scheme *runtime.Scheme
}

// Reconcile reads that state of the cluster for a Clock object and makes changes based on the state read
// and what is in the Clock.Spec
// TODO(user): Modify this Reconcile function to implement your Controller logic.  This example creates
// a Pod as an example
// Note:
// The Controller will requeue the Request to be processed again if the returned error is non-nil or
// Result.Requeue is true, otherwise upon completion it will remove the work from the queue.
func (r *ReconcileClock) Reconcile(request reconcile.Request) (reconcile.Result, error) {
    reqLogger := log.WithValues("Request.Namespace", request.Namespace, "Request.Name", request.Name)
    reqLogger.Info("Reconciling Clock")

    // Fetch the Clock instance
    instance := &clockv1.Clock{}
    err := r.client.Get(context.TODO(), request.NamespacedName, instance)
    if err != nil {
        if errors.IsNotFound(err) {
            // Request object not found, could have been deleted after reconcile request.
            // Owned objects are automatically garbage collected. For additional cleanup logic use finalizers.
            // Return and don't requeue
            reqLogger.Info("object not found or garbage collected ")

            return reconcile.Result{}, nil
        }
        // Error reading the object - requeue the request.
        fmt.Println(err)
        reqLogger.Info("object found with err ")
        return reconcile.Result{}, err
    }

    // Define a new Pod object
    ck := newClockForCR(instance)

    // Set Clock instance as the owner and controller
    if err := controllerutil.SetControllerReference(instance, ck, r.scheme); err != nil {
        fmt.Println("Set Clock instance as the owner and controller")
        reqLogger.Info("Set Clock instance as the owner and controller ")
        return reconcile.Result{}, err
    }

    // Check if this Pod already exists
    found := &clockv1.Clock{}
    err = r.client.Get(context.TODO(), types.NamespacedName{Name: ck.Name, Namespace: ck.Namespace}, found)
    if err != nil && errors.IsNotFound(err) {
        reqLogger.Info("Creating a new Clock", "Clock.Namespace", ck.Namespace, "Clock.Name", ck.Name)
        err = r.client.Create(context.TODO(), ck)
        if err != nil {
            return reconcile.Result{}, err
        }

        // Pod created successfully - don't requeue
        fmt.Println("Set Clock created successfully - don't requeue")
        return reconcile.Result{}, nil
    } else if err != nil {
        return reconcile.Result{}, err
    }

    // Pod already exists - don't requeue
    reqLogger.Info("Skip reconcile: Clock already exists", "Clock.Namespace", found.Namespace, "Clock.Name", found.Name)
    return reconcile.Result{}, nil
}

// newClockForCR returns a busybox pod with the same name/namespace as the cr
func newClockForCR(cr *clockv1.Clock) *clockv1.Clock {
    labels := map[string]string{
        "app":   cr.Name,
        "reset": strconv.FormatBool(cr.Spec.Reset),
    }
    fmt.Println("cr Reset", cr.Spec.Reset)
    if cr.Spec.Reset == true {
        fmt.Println("Resetting Clock, not matter what current date/time is")
        cr.Spec.CurrentDay = "1970-01-01"
        cr.Spec.CurrentTime = "00:00"
    }
    return &clockv1.Clock{
        ObjectMeta: metav1.ObjectMeta{
            Name:      cr.Name,
            Namespace: cr.Namespace,
            Labels:    labels,
        },
        Spec: clockv1.ClockSpec{
            Reset:       cr.Spec.Reset,
            CurrentDay:  cr.Spec.CurrentDay,
            CurrentTime: cr.Spec.CurrentTime,
        },
    }
}

// newPodForCR returns a busybox pod with the same name/namespace as the cr
// func newPodForCR(cr *clockv1.Clock) *corev1.Pod {
//  labels := map[string]string{
//      "app": cr.Name,
//  }
//  return &corev1.Pod{
//      ObjectMeta: metav1.ObjectMeta{
//          Name:      cr.Name + "-pod",
//          Namespace: cr.Namespace,
//          Labels:    labels,
//      },
//      Spec: corev1.PodSpec{
//          Containers: []corev1.Container{
//              {
//                  Name:    "busybox",
//                  Image:   "busybox",
//                  Command: []string{"sleep", "3600"},
//              },
//          },
//      },
//  }
// }

My logic is if value rest == true, reset current time and day to be 1970-01-01. But it turned out doesn't work this way.

my cr file

apiVersion: clock.iamthat.com/v1
kind: Clock
metadata:
  name: example-clock
spec:
  # Add fields here
  reset: true
  # current_day: "2020-01-01"
  # current_time: "01:01"

and output:

> kubectl get clock example-clock -o yaml
apiVersion: clock.iamthat.com/v1
kind: Clock
metadata:
  annotations:
    kubectl.kubernetes.io/last-applied-configuration: |
      {"apiVersion":"clock.iamthat.com/v1","kind":"Clock","metadata":{"annotations":{},"name":"example-clock","namespace":"default"},"spec":{"reset":true}}
  creationTimestamp: 2020-03-15T09:25:20Z
  generation: 3
  name: example-clock
  namespace: default
  resourceVersion: "24731366"
  selfLink: /apis/clock.iamthat.com/v1/namespaces/default/clocks/example-clock
  uid: 6c851efc-2d72-468a-ae92-b2cb633c3cb4
spec:
  reset: true

what i expect is like below. I don't know what's going wrong in my controller code. my direct assignment doesn't work, e.g, cr.Spec.CurrentDay = "1970-01-01"

> kubectl get clock example-clock -o yaml
apiVersion: clock.iamthat.com/v1
kind: Clock
metadata:
......
spec:
  reset: true
  current_day: "1970-01-01"
  current_time: "00:00"
2 Answers

You would have to save your modified root object back to the API using Update() or Patch(). You only changed a local, in-memory copy.

There's a lot going on here but I'll try to explain every step as best I can:

  1. newClockForCR is being passed a pointer so you are already modifying the input - no need to return a pointer.

Try the following change:

+     instance.NewClockForCR()
-     ck := newClockForCR(instance)

and change the function as such:

func (cr *Clock) NewClockForCR() {
    labels := map[string]string{
        "app":   cr.Name,
        "reset": strconv.FormatBool(cr.Spec.Reset),
    }
    fmt.Println("cr Reset", cr.Spec.Reset)
    if cr.Spec.Reset == true {
        fmt.Println("Resetting Clock, not matter what current date/time is")
        cr.Spec.CurrentDay = "1970-01-01"
        cr.Spec.CurrentTime = "00:00"
    }
    cr.ObjectMeta.Labels = labels
}

and move it to clock_types.go

  1. I don't see where pods are actually being created in your code. Setting owner references on the CR itself (so it owns itself) doesn't make sense. Normally you would create these ownerReferences on the Pod resource if it were created.
    // Define a new Pod object
    ck := newClockForCR(instance)

This is not creating a pod but changing the clock CR that the reconciler is triggered on.

So all this code can be removed

    // Set Clock instance as the owner and controller
    if err := controllerutil.SetControllerReference(instance, ck, r.scheme); err != nil {
        fmt.Println("Set Clock instance as the owner and controller")
        reqLogger.Info("Set Clock instance as the owner and controller ")
        return reconcile.Result{}, err
    }

And this code can also be removed because you already checked above that the clock CR was found:

    // Check if this Pod already exists
    found := &clockv1.Clock{}
    err = r.client.Get(context.TODO(), types.NamespacedName{Name: ck.Name, Namespace: ck.Namespace}, found)
    if err != nil && errors.IsNotFound(err) {
        reqLogger.Info("Creating a new Clock", "Clock.Namespace", ck.Namespace, "Clock.Name", ck.Name)
        err = r.client.Create(context.TODO(), ck)
        if err != nil {
            return reconcile.Result{}, err
        }

        // Pod created successfully - don't requeue
        fmt.Println("Set Clock created successfully - don't requeue")
        return reconcile.Result{}, nil
    } else if err != nil {
        return reconcile.Result{}, err
    }

    // Pod already exists - don't requeue
    reqLogger.Info("Skip reconcile: Clock already exists", "Clock.Namespace", found.Namespace, "Clock.Name", found.Name)
    return reconcile.Result{}, nil
  1. Instead of the above you want to just Update the CR
    oldSpec := instance.DeepCopy().Spec
    instance.newClockForCR()

    // check if mutation changed anything
    if !reflect.DeepEqual(oldSpec, instance.Spec) {
        if err := r.client.Update(context.TODO(), instance); err != nil {
            return reconcile.Result{}, err
        }
    }

But we should check that our update actually changes things (that's the DeepEqual part). This is important because otherwise you'll always be updating the CR which will trigger the reconciler and re-update etc. in an infinite loop.

Putting it all together here is what your reconciler code could look like:

func (r *ReconcileClock) Reconcile(request reconcile.Request) (reconcile.Result, error) {
    reqLogger := log.WithValues("Request.Namespace", request.Namespace, "Request.Name", request.Name)
    reqLogger.Info("Reconciling Clock")

    // Fetch the Clock instance
    instance := &clockv1.Clock{}
    err := r.client.Get(context.TODO(), request.NamespacedName, instance)
    if err != nil {
        if errors.IsNotFound(err) {
            // Request object not found, could have been deleted after reconcile request.
            // Owned objects are automatically garbage collected. For additional cleanup logic use finalizers.
            // Return and don't requeue
            reqLogger.Info("object not found or garbage collected ")

            return reconcile.Result{}, nil
        }
        // Error reading the object - requeue the request.
        fmt.Println(err)
        reqLogger.Info("object found with err ")
        return reconcile.Result{}, err
    }

    oldSpec := instance.DeepCopy().Spec
    instance.NewClockForCR()

    // check if mutation changed anything
    if !reflect.DeepEqual(oldSpec, instance.Spec) {
        if err := r.client.Update(context.TODO(), instance); err != nil {
            return reconcile.Result{}, err
        }
    }
    return reconcile.Result{}, nil
}

and in clock_types.go:

func (cr *Clock) NewClockForCR() {
    labels := map[string]string{
        "app":   cr.Name,
        "reset": strconv.FormatBool(cr.Spec.Reset),
    }
    fmt.Println("cr Reset", cr.Spec.Reset)
    if cr.Spec.Reset == true {
        fmt.Println("Resetting Clock, not matter what current date/time is")
        cr.Spec.CurrentDay = "1970-01-01"
        cr.Spec.CurrentTime = "00:00"
    }
    cr.ObjectMeta.Labels = labels
}
Related