gokubernetesclient-go

Field conflict with kubernetes dynamic client and server-side apply


How do I correctly use the kubernetes client-go library with a dynamic client and server-side apply?

I am creating and then trying to update a Kubernetes resource using the dynamic client, with FieldManager set (to enable server-side apply). Despite setting the same value for FieldManager for both operations, the update fails with the error below:

panic: Apply failed with 1 conflict: conflict with "test" using cert-manager.io/v1: .spec.secretName

My code is below, as you can see it is using the same FieldManager for both operations, so I don't see why there is a conflict:

package main

import (
    "context"
    "flag"

    metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    "k8s.io/apimachinery/pkg/runtime/schema"
    "k8s.io/client-go/dynamic"
    "k8s.io/client-go/tools/clientcmd"
)

func main() {
    kubeconfig := flag.String("kubeconfig", "", "absolute path to the kubeconfig file")
    flag.Parse()

    config, err := clientcmd.BuildConfigFromFlags("", *kubeconfig)
    if err != nil {
        panic(err)
    }
    client, err := dynamic.NewForConfig(config)
    if err != nil {
        panic(err)
    }

    fieldManager := "test"
    name := "foo"
    namespace := "default"
    certificate := &unstructured.Unstructured{
        Object: map[string]interface{}{
            "apiVersion": "cert-manager.io/v1",
            "kind":       "Certificate",
            "metadata":   map[string]interface{}{"name": name, "namespace": namespace},
            "spec": map[string]interface{}{
                "dnsNames": []string{"example.com"},
                "issuerRef": map[string]interface{}{
                    "group": "cert-manager.io",
                    "kind":  "ClusterIssuer",
                    "name":  "foo",
                },
                "secretName": "foo",
            },
        },
    }

    certificateGvr := schema.GroupVersionResource{Group: "cert-manager.io", Version: "v1", Resource: "certificates"}

    _, err = client.Resource(certificateGvr).
        Namespace(namespace).
        Create(context.TODO(), certificate, metav1.CreateOptions{FieldManager: fieldManager})
    if err != nil {
        panic(err)
    }

    unstructured.SetNestedField(certificate.UnstructuredContent(), "bar", "spec", "secretName")
    _, err = client.Resource(certificateGvr).
        Namespace(namespace).
        Apply(context.TODO(), name, certificate, metav1.ApplyOptions{FieldManager: fieldManager, Force: true})
    if err != nil {
        panic(err)
    }
}

I have noticed that if I force the update, and view the managed fields, there are two sets of managed fields (one for each operation). But I'd prefer to avoid a force, given that it might result in accidentally overwriting a field.

apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  creationTimestamp: "2024-05-24T10:01:09Z"
  generation: 2
  managedFields:
  - apiVersion: cert-manager.io/v1
    fieldsType: FieldsV1
    fieldsV1:
      f:spec:
        f:dnsNames: {}
        f:issuerRef:
          f:group: {}
          f:kind: {}
          f:name: {}
        f:secretName: {}
    manager: test
    operation: Apply
    time: "2024-05-24T10:01:09Z"
  - apiVersion: cert-manager.io/v1
    fieldsType: FieldsV1
    fieldsV1:
      f:spec:
        .: {}
        f:dnsNames: {}
        f:issuerRef:
          .: {}
          f:group: {}
          f:kind: {}
          f:name: {}
    manager: test
    operation: Update
    time: "2024-05-24T10:01:09Z"
  name: foo
  namespace: default
  resourceVersion: "56441"
  uid: d4b97ef5-3314-498b-bb13-786967f1fcf8
spec:
  dnsNames:
  - example.com
  issuerRef:
    group: cert-manager.io
    kind: ClusterIssuer
    name: foo
  secretName: bar

Solution

  • So far, the best solution I've found is to use Apply for both creation and update of objects, like so:

        // Create the object
        _, err = client.Resource(certificateGvr).
            Namespace(namespace).
            Apply(context.TODO(), name, certificate, metav1.ApplyOptions{FieldManager: fieldManager})
        if err != nil {
            panic(err)
        }
    
        // Modify and update it
        unstructured.SetNestedField(certificate.UnstructuredContent(), "bar", "spec", "secretName")
        _, err = client.Resource(certificateGvr).
            Namespace(namespace).
            Apply(context.TODO(), name, certificate, metav1.ApplyOptions{FieldManager: fieldManager})
        if err != nil {
            panic(err)
        }
    

    The downside is that unlike Create, the first operation does not error if the object already exists (as long as it is managed by the same fieldManager). So if you want to be sure that you are creating an object using this approach, you need to generate and keep track of a unique fieldManager for this object.

    There might be a better way, so I'm leaving this question open for now.

    Update: I've confirmed with the developers of client-go in the #sig-api-machinery Slack channel - using Apply() for creation and updates is the intended way to use this.