Creating HTTP/S Routing Rules Programmatically

We understand the structure of the Ingress resource and the necessity of an Ingress Controller. Now, let's use client-go to define the core routing logic – the rules that map incoming hostnames and paths to backend Kubernetes Services.

This involves constructing the networkingv1.Ingress Go struct, paying close attention to the spec.rules and spec.tls fields, and then using the client-go clientset to create this resource in the cluster.

Constructing the Ingress Struct in Go

Let's build an example where we want to achieve the following routing:

  1. Requests to app.example.com/login should go to login-service on its named port http.

  2. Requests to app.example.com/api/users should go to user-api-service on port 8080.

  3. Requests to metrics.example.com/ should go to metrics-service on port 9090.

  4. Enable TLS for app.example.com using a Secret named app-tls-secret.

First, we need to import the necessary packages, including k8s.io/api/networking/v1. Then, we construct the Ingress object in Go:

package main

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

	// Kubernetes API imports
	networkingv1 "k8s.io/api/networking/v1" // Import networking v1 package
	k8serrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

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

func main() {
	// --- Setup Kubeconfig and Flags ---
	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 to create the ingress in")
	ingressName := flag.String("ingress-name", "example-ingress", "name for the new ingress")
	ingressClassName := flag.String("ingress-class", "", "(required) name of the IngressClass") // Make class name required

	flag.Parse()

	if *ingressClassName == "" {
		log.Fatal("Error: --ingress-class flag is required")
	}

	// --- 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())
	}

	// --- Define the Ingress Object ---
	pathTypePrefix := networkingv1.PathTypePrefix // Define path type constant for clarity

	ingressSpec := &networkingv1.Ingress{
		ObjectMeta: metav1.ObjectMeta{
			Name:      *ingressName,
			Namespace: *namespace,
			Annotations: map[string]string{ // Optional: Add annotations if needed by controller
				// "nginx.ingress.kubernetes.io/rewrite-target": "/",
			},
		},
		Spec: networkingv1.IngressSpec{
			// Link to the specific Ingress Controller
			IngressClassName: ingressClassName,

			// --- Define Routing Rules ---
			Rules: []networkingv1.IngressRule{
				// Rule 1: Target app.example.com
				{
					Host: "app.example.com",
					HTTP: &networkingv1.HTTPIngressRuleValue{
						Paths: []networkingv1.HTTPIngressPath{
							// Path 1.1: /login
							{
								Path:     "/login",
								PathType: &pathTypePrefix, // Use Prefix match
								Backend: networkingv1.IngressBackend{
									Service: &networkingv1.IngressServiceBackend{
										Name: "login-service", // Target Service name
										Port: networkingv1.ServiceBackendPort{
											Name: "http", // Target Service port name
										},
									},
								},
							},
							// Path 1.2: /api/users
							{
								Path:     "/api/users",
								PathType: &pathTypePrefix,
								Backend: networkingv1.IngressBackend{
									Service: &networkingv1.IngressServiceBackend{
										Name: "user-api-service", // Target Service name
										Port: networkingv1.ServiceBackendPort{
											Number: 8080, // Target Service port number
										},
									},
								},
							},
						},
					},
				},
				// Rule 2: Target metrics.example.com
				{
					Host: "metrics.example.com",
					HTTP: &networkingv1.HTTPIngressRuleValue{
						Paths: []networkingv1.HTTPIngressPath{
							// Path 2.1: / (root path)
							{
								Path:     "/",
								PathType: &pathTypePrefix,
								Backend: networkingv1.IngressBackend{
									Service: &networkingv1.IngressServiceBackend{
										Name: "metrics-service",
										Port: networkingv1.ServiceBackendPort{
											Number: 9090,
										},
									},
								},
							},
						},
					},
				},
			}, // End of Rules

			// --- Define TLS Configuration ---
			TLS: []networkingv1.IngressTLS{
				{
					Hosts: []string{ // Hosts covered by this TLS cert
						"app.example.com",
					},
					SecretName: "app-tls-secret", // Name of the Secret containing TLS cert/key
				},
				// Could add another entry for metrics.example.com if it needed TLS
				// { Hosts: []string{"metrics.example.com"}, SecretName: "metrics-tls-secret",},
			}, // End of TLS
		}, // End of Spec
	} // End of Ingress definition

	// --- Create the Ingress Resource ---
	fmt.Printf("Creating Ingress '%s' in namespace '%s'...\n", *ingressName, *namespace)
	// Access the NetworkingV1 client
	ingressClient := clientset.NetworkingV1().Ingresses(*namespace)

	createdIngress, err := ingressClient.Create(context.TODO(), ingressSpec, metav1.CreateOptions{})

	// --- Handle Errors ---
	if err != nil {
		if k8serrors.IsAlreadyExists(err) {
			log.Printf("Ingress '%s' already exists in namespace '%s'.\n", *ingressName, *namespace)
			// Optionally Get/Update existing Ingress
		} else {
			log.Fatalf("Error creating Ingress '%s': %s\n", *ingressName, err.Error())
		}
	} else {
		fmt.Printf("Successfully created Ingress '%s'.\n", createdIngress.Name)
		// Note: It might take a moment for the Ingress Controller to process this
		// and for the external endpoint (if using type: LoadBalancer for the controller)
		// to become available and reflect in `kubectl get ingress`.
		fmt.Printf("Run `kubectl describe ingress %s -n %s` to see details.\n", *ingressName, *namespace)
	}
	fmt.Println("---------------")
}

Key Go Constructs:

  • Import networking/v1: We use k8s.io/api/networking/v1.

  • IngressClassName: Crucial for directing the resource to the correct controller.

  • Rules Slice: We create a slice of networkingv1.IngressRule.

  • Host: Set the target hostname within each rule.

  • HTTP and Paths: We define the HTTPIngressRuleValue and its Paths slice ([]networkingv1.HTTPIngressPath).

  • Path and PathType: Specify the URL path and how it should be matched (e.g., Prefix). Note that PathType requires a pointer, so we assign &pathTypePrefix.

  • Backend and Service: Define the target Service using networkingv1.IngressBackend and networkingv1.IngressServiceBackend.

  • Service Port (Name vs. Number): We specify the target port on the Service using either Port.Name or Port.Number.

  • TLS Slice: We define a slice of networkingv1.IngressTLS.

  • Hosts and SecretName: Link the hostnames to the Kubernetes Secret containing the certificate and key. Prerequisite: The Secret (app-tls-secret in this case) must exist in the same namespace and be of type kubernetes.io/tls.

  • Create Call: We use clientset.NetworkingV1().Ingresses(namespace).Create(...) to submit the defined Ingress object to the API server.

By constructing these Ingress objects programmatically, you gain the ability to automate the exposure of your applications, potentially integrating with CI/CD pipelines, service discovery systems, or custom dashboards to manage external access dynamically based on application lifecycle events or other triggers.

Last updated

Was this helpful?