Search code examples
kuberneteskubernetes-go-client

Resource not found error performing SSA create using dynamic client


I was following @ymmt2005 excellent dynamic client guide. All is good until the final step when I make the actual PATCH call, and I get a the server could not find the requested resource error. Just about everything seems right, except I'm unsure about the 'FieldManager' field in the PathOptions struct. I'm not sure what "the actor or entity that is making these changes" refers to. Does this need to match something in my code or system? Any other ideas?

package main

import (
...
)

const resourceYAML = `
apiVersion: apps/v1
kind: Deployment
metadata:
  name: mike-nginx
spec:
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
        - name: nginx
          image: 'nginx:latest'
          ports:
            - containerPort: 80
`

func main() {
    ctx := context.Background()

    // Create dynamic discovery client from local kubeconfig file
    kubePath := filepath.Join(homedir.HomeDir(), ".kube", "config")
    cfg, err := clientcmd.BuildConfigFromFlags("", kubePath)
    if err != nil {
        log.Fatalf("error building config, %v\n", err)
    }
    dynClient, err := dynamic.NewForConfig(cfg)
    if err != nil {
        log.Fatalf("error creating client, %v\n", err)
    }
    disClient, err := discovery.NewDiscoveryClientForConfig(cfg)
    if err != nil {
        log.Fatalf("error creating discovery client, %v\n", err)
    }

    // Decode YAML manifest & get GVK
    decodeUnstr := yaml.NewDecodingSerializer(unstructured.UnstructuredJSONScheme)
    obj := &unstructured.Unstructured{}
    _, gvk, err := decodeUnstr.Decode([]byte(resourceYAML), nil, obj)
    if err != nil {
        log.Fatalf("error decoding manifest, %v\n", err)
    }
    jsonObj, err := json.Marshal(obj)
    if err != nil {
        log.Fatalf("error marshaling object, %v\n", err)
    }

    // Find GVR using GVK
    mapper := restmapper.NewDeferredDiscoveryRESTMapper(memory.NewMemCacheClient(disClient))
    mapping, err := mapper.RESTMapping(gvk.GroupKind(), gvk.Version)
    if err != nil {
        log.Fatalf("error finding GVR, %v\n", err)
    }

    // Get REST interface for the GVR, checking for namespace or cluster-wide
    var dr dynamic.ResourceInterface
    if mapping.Scope.Name() == meta.RESTScopeNameNamespace {
        // Namespaced resource
        dr = dynClient.Resource(mapping.Resource).Namespace(obj.GetNamespace())
    } else {
        // Cluster-wide resource
        dr = dynClient.Resource(mapping.Resource)
    }

    // Create or Update the object with SSA
    options := metav1.PatchOptions{FieldManager: "sample-controller"}
    _, err = dr.Patch(ctx, obj.GetName(), types.ApplyPatchType, jsonObj, options)
    if err != nil {
        log.Fatalf("error patching, %v\n", err)
    }
}

[edit] I confirmed that I was only able to use 'Patch' on a resource that already existed. I tweaked the code to use 'Create' to create the resource, then I was able to successfully do a 'Patch' against it to change. To overcome the FieldManager inconsistencies I added Force=true to the PatchOptions which is recommended in the docs anyway. I'd still like to know how I can create if resource doesn't exist and update if it does--maybe just test for exist?


Solution

  • The answer is really trivial. The original code assumes that namespace is provided in the manifest. The deployment endpoint does not automatically set namespace to default if the provided namespace is "", and errors out because "" is not a valid namespace. Therefore, I added logic to set namespace to default if not provided and presto, the server side apply will create the resource if it doesn't exist and update if it does exist. Thanks again @ymmt2005 .

    package main
    
    import (
    ...
    )
    
    const resourceYAML = `
    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: mike-nginx
    spec:
      selector:
        matchLabels:
          app: nginx
      template:
        metadata:
          labels:
            app: nginx
        spec:
          containers:
            - name: nginx
              image: 'nginx:latest'
              ports:
                - containerPort: 80
    `
    
    func main() {
        ctx := context.Background()
    
        // Create dynamic discovery client from local kubeconfig file
        kubePath := filepath.Join(homedir.HomeDir(), ".kube", "config")
        cfg, err := clientcmd.BuildConfigFromFlags("", kubePath)
        if err != nil {
            log.Fatalf("error building config, %v\n", err)
        }
        dynClient, err := dynamic.NewForConfig(cfg)
        if err != nil {
            log.Fatalf("error creating client, %v\n", err)
        }
        disClient, err := discovery.NewDiscoveryClientForConfig(cfg)
        if err != nil {
            log.Fatalf("error creating discovery client, %v\n", err)
        }
    
        // Decode YAML manifest & get GVK
        decodeUnstr := yaml.NewDecodingSerializer(unstructured.UnstructuredJSONScheme)
        obj := &unstructured.Unstructured{}
        _, gvk, err := decodeUnstr.Decode([]byte(resourceYAML), nil, obj)
        if err != nil {
            log.Fatalf("error decoding manifest, %v\n", err)
        }
        jsonObj, err := json.Marshal(obj)
        if err != nil {
            log.Fatalf("error marshaling object, %v\n", err)
        }
    
        // Find GVR using GVK
        mapper := restmapper.NewDeferredDiscoveryRESTMapper(memory.NewMemCacheClient(disClient))
        mapping, err := mapper.RESTMapping(gvk.GroupKind(), gvk.Version)
        if err != nil {
            log.Fatalf("error finding GVR, %v\n", err)
        }
    
        // Set Namespace to default if not provided in manifest
        var ns string
        if ns = obj.GetNamespace(); ns == "" {
            ns = "default"
        }
    
        // Get REST interface for the GVR, checking for namespace or cluster-wide
        var dr dynamic.ResourceInterface
        if mapping.Scope.Name() == meta.RESTScopeNameNamespace {
            // Namespaced resource
            dr = dynClient.Resource(mapping.Resource).Namespace(ns)
        } else {
            // Cluster-wide resource
            dr = dynClient.Resource(mapping.Resource)
        }
    
        // Create or Update the object with SSA
        options := metav1.PatchOptions{FieldManager: "sample-controller"}
        _, err = dr.Patch(ctx, obj.GetName(), types.ApplyPatchType, jsonObj, options)
        if err != nil {
            log.Fatalf("error patching, %v\n", err)
        }
    }