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
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.