Why does client.Update(...) ignore non-primitive values?

8/28/2019

I'm trying to modify the Spec of non-owned objects as part of the Reconcile of my Custom Resource, but it seems like it ignores any fields that are not primitives. I am using controller-runtime.

I figured since it was only working on primitives, maybe it's an issue related to DeepCopy. However, removing it did not solve the issue, and I read that any Updates on objects have to be on deep copies to avoid messing up the cache.

I also tried setting client.FieldOwner(...) since it says that that's required for Updates that are done server-side. I wasn't sure what to set it to, so I made it req.NamespacedName.String(). That did not work either.

Here is the Reconcile loop for my controller:

func (r *MyCustomObjectReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
    // ...

    var myCustomObject customv1.MyCustomObject
    if err := r.Get(ctx, req.NamespacedName, &myCustomObject); err != nil {
        log.Error(err, "unable to fetch ReleaseDefinition")
        return ctrl.Result{}, client.IgnoreNotFound(err)
    }

    // ...

    deployList := &kappsv1.DeploymentList{}
    labels := map[string]string{
        "mylabel": myCustomObject.Name,
    }
    if err := r.List(ctx, deployList, client.MatchingLabels(labels)); err != nil {
        log.Error(err, "unable to fetch Deployments")
        return ctrl.Result{}, err
    }

    // make a deep copy to avoid messing up the cache (used by other controllers)
    myCustomObjectSpec := myCustomObject.Spec.DeepCopy()

    // the two fields of my CRD that affect the Deployments
    port := myCustomObjectSpec.Port // type: *int32
    customenv := myCustomObjectSpec.CustomEnv // type: map[string]string

    for _, dep := range deployList.Items {
        newDeploy := dep.DeepCopy() // already returns a pointer

        // Do these things:
        // 1. replace first container's containerPort with myCustomObjectSpec.Port
        // 2. replace first container's Env with values from myCustomObjectSpec.CustomEnv
                // 3. Update the Deployment

        container := newDeploy.Spec.Template.Spec.Containers[0]

        // 1. Replace container's port
        container.Ports[0].ContainerPort = *port

        envVars := make([]kcorev1.EnvVar, 0, len(customenv))
        for key, val := range customenv {
            envVars = append(envVars, kcorev1.EnvVar{
                Name:  key,
                Value: val,
            })
        }

        // 2. Replace container's Env variables
        container.Env = envVars

        // 3. Perform update for deployment (port works, env gets ignored)
        if err := r.Update(ctx, newDeploy); err != nil {
            log.Error(err, "unable to update deployment", "deployment", dep.Name)
            return ctrl.Result{}, err
        }
    }
    return ctrl.Result{}, nil
}

The Spec for my CRD looks like:

// MyCustomObjectSpec defines the desired state of MyCustomObject
type MyCustomObjectSpec struct {
    // CustomEnv is a list of environment variables to set in the containers.
    // +optional
    CustomEnv map[string]string `json:"customEnv,omitempty"`

    // Port is the port that the backend container is listening on.
    // +optional
    Port *int32 `json:"port,omitempty"`
}

I expected that when I kubectl apply a new CR with changes to the Port and CustomEnv fields, it would modify the deployments as described in Reconcile. However, only the Port is updated, and the changes to the container's Env are ignored.

-- Andrew D.
kubebuilder
kubernetes
kubernetes-apiserver
kubernetes-custom-resources

1 Answer

8/28/2019

The problem was that I needed a pointer to the Container I was modifying.

Doing this instead worked:

container := &newDeploy.Spec.Template.Spec.Containers[0]
-- Andrew D.
Source: StackOverflow