Updating Ingress Resources

Creating an Ingress resource is just the first step. Applications evolve, routing needs change, and TLS certificates get renewed. Therefore, knowing how to update existing Ingress resources programmatically is just as important as creating them.

Common reasons to update an Ingress resource include:

  • Adding a new path rule to route traffic for a new feature or microservice.

  • Changing the backend Service for an existing path (e.g., during a blue-green deployment).

  • Adding or modifying host rules.

  • Updating the secretName in the tls section when a certificate is renewed.

  • Modifying annotations to change the behavior of the Ingress Controller.

  • Removing obsolete rules or hosts.

The Get-Modify-Update Pattern

You cannot directly modify just a part of an existing Kubernetes resource via a simple API call (except through Patch, which is more complex). The standard pattern for updating resources using client-go's typed clients (like the Ingress client) is:

  1. Get: Retrieve the current version of the Ingress resource from the API server using ingressClient.Get(...).

  2. Modify: Make the desired changes to the Go struct object you received in memory. For example, append a new HTTPIngressPath to the Paths slice within a specific rule, or add a new IngressRule to the Rules slice.

  3. Update: Call ingressClient.Update(...), passing the entire modified Go struct.

Crucial Concept: Optimistic Concurrency and resourceVersion

Kubernetes uses a mechanism called optimistic concurrency control to prevent conflicting updates. Every Kubernetes object has a metadata.resourceVersion field. This field is updated by the API server every time the object is changed.

When you call Update, the API server compares the resourceVersion of the object you're submitting with the resourceVersion of the object currently stored in etcd (the cluster's database).

  • If they match: The update is allowed, the object is changed, and a new resourceVersion is assigned.

  • If they don't match: This means someone else (or another process) modified the object between the time you performed your Get and when you submitted your Update. The API server rejects your update with an HTTP 409 Conflict error. client-go's k8serrors.IsConflict(err) function will return true in this case.

Handling Conflicts:

The standard way to handle a conflict error during an update is to:

  1. Catch the conflict error (k8serrors.IsConflict).

  2. Re-Get: Fetch the latest version of the resource from the API server again.

  3. Re-Apply Changes: Apply your intended modifications to this newly fetched object. (This might require some care if the conflicting change affected the part you wanted to modify).

  4. Retry Update: Call Update again with the re-modified object.

This process might need to be repeated a few times if conflicts are frequent. Libraries like k8s.io/client-go/util/retry provide helpers (like retry.RetryOnConflict) to automate this retry loop.

Example: Adding a Path Rule to an Existing Ingress

Let's extend our previous example. Assume the Ingress example-ingress already exists (created in the previous step). We now want to add a new path rule under the app.example.com host to route /admin traffic to an admin-service on port 8888.

package main

import (
	"context"
	"flag"
	"fmt"
	"log"
	"path/filepath"

	// Kubernetes API imports
	networkingv1 "k8s.io/api/networking/v1"
	k8serrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/util/wait" // For retry backoff

	// client-go imports
	"k8s.io/client-go/kubernetes"
	"k8s.io/client-go/tools/clientcmd"
	"k8s.io/client-go/util/homedir"
	"k8s.io/client-go/util/retry" // For RetryOnConflict
)

func main() {
	// --- Setup Kubeconfig and Flags (similar to create example) ---
	var kubeconfig *string
	if home := homedir.HomeDir(); home != "" {
		kubeconfig = flag.String("kubeconfig", filepath.Join(home, ".kube", "config"), "(optional) kubeconfig path")
	} else {
		kubeconfig = flag.String("kubeconfig", "", "kubeconfig path")
	}
	namespace := flag.String("namespace", "default", "namespace of the ingress")
	ingressName := flag.String("ingress-name", "example-ingress", "name of the ingress to update")
	// Flags for the new rule
	newPath := flag.String("new-path", "/admin", "new path prefix to add")
	newServiceName := flag.String("new-service", "admin-service", "service name for the new path")
	newServicePort := flag.Int("new-port", 8888, "service port number for the new path")
	targetHost := flag.String("target-host", "app.example.com", "host rule to add the path under")


	flag.Parse()

	// --- Load Config and Create Clientset ---
	config, err := clientcmd.BuildConfigFromFlags("", *kubeconfig)
	if err != nil {
		log.Fatalf("Error building kubeconfig: %s", err.Error())
	}
	clientset, err := kubernetes.NewForConfig(config)
	if err != nil {
		log.Fatalf("Error creating clientset: %s", err.Error())
	}

	ingressClient := clientset.NetworkingV1().Ingresses(*namespace)

	fmt.Printf("Attempting to update Ingress '%s' in namespace '%s' to add path '%s'...\n", *ingressName, *namespace, *newPath)

	// --- Use RetryOnConflict for Robust Updates ---
	// Define the update logic within the RetryOnConflict function
	retryErr := retry.RetryOnConflict(retry.DefaultRetry, func() error {
		// Step 1: Get the latest version of the Ingress
		currentIngress, getErr := ingressClient.Get(context.TODO(), *ingressName, metav1.GetOptions{})
		if k8serrors.IsNotFound(getErr) {
			log.Fatalf("Ingress '%s' not found in namespace '%s'. Cannot update.", *ingressName, *namespace)
			return nil // Or return the error if preferred
		}
		if getErr != nil {
			log.Printf("Failed to get Ingress '%s': %v. Retrying...", *ingressName, getErr)
			return getErr // Return error to trigger retry
		}
		log.Printf("Retrieved Ingress '%s' with resourceVersion %s\n", currentIngress.Name, currentIngress.ResourceVersion)


		// Step 2: Modify the retrieved object in memory
		pathTypePrefix := networkingv1.PathTypePrefix
		newRuleAdded := false

		// Find the rule for the target host and add the path
		ruleUpdated := false
		for i, rule := range currentIngress.Spec.Rules {
			if rule.Host == *targetHost {
				// Check if path already exists (optional, prevents duplicates)
				pathExists := false
				if rule.HTTP != nil {
					for _, path := range rule.HTTP.Paths {
						if path.Path == *newPath {
							pathExists = true
							log.Printf("Path '%s' already exists for host '%s'. No update needed for this path.", *newPath, *targetHost)
							break
						}
					}
				} else {
					// Initialize HTTP if it's nil
					 currentIngress.Spec.Rules[i].HTTP = &networkingv1.HTTPIngressRuleValue{ Paths: []networkingv1.HTTPIngressPath{} }
				}


				if !pathExists {
					// Append the new path
					newPathRule := networkingv1.HTTPIngressPath{
						Path:     *newPath,
						PathType: &pathTypePrefix,
						Backend: networkingv1.IngressBackend{
							Service: &networkingv1.IngressServiceBackend{
								Name: *newServiceName,
								Port: networkingv1.ServiceBackendPort{Number: int32(*newServicePort)},
							},
						},
					}
					currentIngress.Spec.Rules[i].HTTP.Paths = append(currentIngress.Spec.Rules[i].HTTP.Paths, newPathRule)
					ruleUpdated = true
					log.Printf("Path '%s' will be added to host '%s'.\n", *newPath, *targetHost)
				}
				break // Found the host rule, stop searching rules
			}
		}

		// If the host rule didn't exist, you might want to add a new IngressRule entirely (more complex logic)
		if !ruleUpdated {
             // Example: Add a whole new rule if host not found (adjust as needed)
             /*
			 newHostRule := networkingv1.IngressRule{ ... }
			 currentIngress.Spec.Rules = append(currentIngress.Spec.Rules, newHostRule)
             ruleUpdated = true
             */
             log.Printf("Host '%s' not found in existing rules. No path added.", *targetHost)
             // If no modifications were made, return nil to stop retrying
             return nil
        }


		// Step 3: Call Update with the modified object
		log.Printf("Attempting to update Ingress '%s' (ResourceVersion: %s)...\n", currentIngress.Name, currentIngress.ResourceVersion)
		_, updateErr := ingressClient.Update(context.TODO(), currentIngress, metav1.UpdateOptions{})
		if updateErr == nil {
			log.Println("Update successful!")
		} else {
			log.Printf("Update failed: %v. Retrying...\n", updateErr)
		}
		return updateErr // retry.RetryOnConflict will retry if this is a conflict error
	})

	// Check if the retry loop itself failed (e.g., after max retries)
	if retryErr != nil {
		log.Fatalf("Failed to update Ingress '%s' after retries: %s", *ingressName, retryErr.Error())
	}

	fmt.Println("---------------")
	fmt.Printf("Ingress '%s' update process complete.\n", *ingressName)

}

Explanation:

  1. retry.RetryOnConflict: We wrap the Get-Modify-Update logic inside this helper function. It automatically retries the inner function if ingressClient.Update returns a conflict error (errors.IsConflict). It uses default backoff settings between retries.

  2. Get Latest: Inside the function, the first step is always to Get the most current version of the Ingress.

  3. Modify In Memory: We find the specific rule for the targetHost and append our new HTTPIngressPath struct to its HTTP.Paths slice. We added a check to avoid adding duplicates. (More complex logic would be needed to add a new host rule if it didn't exist).

  4. Call Update: We attempt the Update using the modified currentIngress object.

  5. Return Error: The function passed to RetryOnConflict must return the error from the Update call. RetryOnConflict checks if this error is a conflict; if so, it waits and retries the function. If it's another error, or if the update succeeds (nil error), the retry loop stops.

Patch vs Update:

While Update replaces the entire object, Kubernetes also supports Patch operations (JSON Patch, Strategic Merge Patch, Server-Side Apply) for partial updates. Patching can be more efficient network-wise and less prone to certain types of conflicts, but constructing the patch requests can be more complex than modifying the Go struct for Update. For many common modifications like adding items to lists within the spec, the Get-Modify-Update pattern with RetryOnConflict is a robust and widely used approach.

Mastering the Get-Modify-Update pattern (especially with conflict handling) is essential for building reliable Go applications that manage the lifecycle of Kubernetes resources like Ingress.

Last updated

Was this helpful?