Kubernetes Operator that creates Service Endpoints from Secrets
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 209 lines 6.6 kB view raw
1/* 2Copyright 2025. 3 4Licensed under the Apache License, Version 2.0 (the "License"); 5you may not use this file except in compliance with the License. 6You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10Unless required by applicable law or agreed to in writing, software 11distributed under the License is distributed on an "AS IS" BASIS, 12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13See the License for the specific language governing permissions and 14limitations under the License. 15*/ 16 17package controller 18 19import ( 20 "context" 21 "net" 22 "strconv" 23 "strings" 24 "time" 25 26 "k8s.io/apimachinery/pkg/api/errors" 27 "k8s.io/apimachinery/pkg/runtime" 28 "k8s.io/apimachinery/pkg/types" 29 "k8s.io/apimachinery/pkg/util/intstr" 30 ctrl "sigs.k8s.io/controller-runtime" 31 "sigs.k8s.io/controller-runtime/pkg/client" 32 "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" 33 "sigs.k8s.io/controller-runtime/pkg/log" 34 35 appsv1 "github.com/evanjarrett/secret-service-operator/api/v1" 36 corev1 "k8s.io/api/core/v1" 37 discoveryv1 "k8s.io/api/discovery/v1" 38 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 39) 40 41// SecretServiceReconciler reconciles a SecretService object 42type SecretServiceReconciler struct { 43 client.Client 44 Scheme *runtime.Scheme 45} 46 47// +kubebuilder:rbac:groups=apps.j5t.io,resources=secretservices,verbs=get;list;watch;create;update;patch;delete 48// +kubebuilder:rbac:groups=apps.j5t.io,resources=secretservices/status,verbs=get;update;patch 49// +kubebuilder:rbac:groups=apps.j5t.io,resources=secretservices/finalizers,verbs=update 50 51// Reconcile is part of the main kubernetes reconciliation loop which aims to 52// move the current state of the cluster closer to the desired state. 53// TODO(user): Modify the Reconcile function to compare the state specified by 54// the SecretService object against the actual cluster state, and then 55// perform operations to make the cluster state reflect the state specified by 56// the user. 57// 58// For more details, check Reconcile and its Result here: 59// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.19.0/pkg/reconcile 60func (r *SecretServiceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { 61 logger := log.FromContext(ctx) 62 63 // Get the SecretService object 64 instance := &appsv1.SecretService{} 65 if err := r.Get(ctx, req.NamespacedName, instance); err != nil { 66 if errors.IsNotFound(err) { 67 return ctrl.Result{}, nil // Don't requeue if the object is gone 68 } 69 return ctrl.Result{}, err 70 } 71 72 // Get the Secret 73 secret := &corev1.Secret{} 74 if err := r.Get(ctx, types.NamespacedName{Name: instance.Spec.SecretName, Namespace: instance.Namespace}, secret); err != nil { 75 if errors.IsNotFound(err) { 76 // If the secret doesn't exist, requeue after a delay 77 logger.Error(err, "Secret not found", "secretName", instance.Spec.SecretName) 78 return ctrl.Result{RequeueAfter: 30 * time.Second}, nil 79 } 80 return ctrl.Result{}, err 81 } 82 83 for serviceName, value := range secret.Data { 84 85 endpointPort := string(value) 86 parts := strings.Split(endpointPort, ":") 87 if len(parts) != 2 { 88 // Log a warning or skip if it doesn't match the expected format, but don't return an error 89 logger.Info("Skipping secret key with invalid format:", "key", serviceName, "value", endpointPort) 90 continue // Skip this key and process the next one 91 } 92 93 endpointIP := parts[0] 94 portStr := parts[1] 95 96 // Validate IP address format 97 if ip := net.ParseIP(endpointIP); ip == nil { 98 logger.Info("Skipping secret key with invalid IP address:", "key", serviceName, "ip", endpointIP) 99 continue 100 } 101 102 port, err := strconv.Atoi(portStr) 103 if err != nil || port < 1 || port > 65535 { 104 logger.Info("Skipping secret key with invalid port:", "key", serviceName, "port", portStr) 105 continue 106 } 107 108 // Create or update the Service (Example: ClusterIP service) 109 service := &corev1.Service{ 110 ObjectMeta: metav1.ObjectMeta{ 111 Name: serviceName, 112 Namespace: instance.Namespace, 113 }, 114 Spec: corev1.ServiceSpec{ 115 Type: corev1.ServiceTypeClusterIP, 116 Ports: []corev1.ServicePort{ 117 { 118 Name: "http", 119 Port: int32(port), 120 TargetPort: intstr.FromInt(port), // Or string target port if needed 121 Protocol: corev1.ProtocolTCP, 122 }, 123 }, 124 }, 125 } 126 127 if err := r.CreateOrUpdate(ctx, service, instance); err != nil { 128 return ctrl.Result{}, err 129 } 130 131 httpName := "http" 132 tcpProtocol := corev1.ProtocolTCP 133 readyTrue := true 134 portInt32 := int32(port) 135 136 // Create or update EndpointSlice (modern replacement for Endpoints) 137 endpointSlice := &discoveryv1.EndpointSlice{ 138 ObjectMeta: metav1.ObjectMeta{ 139 Name: serviceName, 140 Namespace: instance.Namespace, 141 Labels: map[string]string{ 142 discoveryv1.LabelServiceName: serviceName, // Required label to associate with Service 143 }, 144 }, 145 AddressType: discoveryv1.AddressTypeIPv4, 146 Endpoints: []discoveryv1.Endpoint{ 147 { 148 Addresses: []string{endpointIP}, 149 Conditions: discoveryv1.EndpointConditions{ 150 Ready: &readyTrue, 151 }, 152 }, 153 }, 154 Ports: []discoveryv1.EndpointPort{ 155 { 156 Name: &httpName, 157 Port: &portInt32, 158 Protocol: &tcpProtocol, 159 }, 160 }, 161 } 162 163 if err := r.CreateOrUpdate(ctx, endpointSlice, instance); err != nil { 164 return ctrl.Result{}, err 165 } 166 } 167 168 return ctrl.Result{}, nil 169} 170 171// CreateOrUpdate helper function 172func (r *SecretServiceReconciler) CreateOrUpdate(ctx context.Context, obj client.Object, owner metav1.Object) error { 173 logger := log.FromContext(ctx) 174 key := client.ObjectKeyFromObject(obj) 175 176 // 1. Attempt to get the object. This checks if it already exists. 177 if err := r.Get(ctx, key, obj); err != nil { 178 // 2. If the object is not found, create it. 179 if !errors.IsNotFound(err) { 180 return err // Return any error other than NotFound 181 } 182 183 // Use Controllerutil to manage the obj 184 if err := controllerutil.SetControllerReference(owner, obj, r.Scheme); err != nil { 185 return err 186 } 187 188 logger.Info("Creating resource", "object", key) 189 return r.Create(ctx, obj) 190 } 191 192 // 3. If the object exists, update it. 193 existing := obj.DeepCopyObject().(client.Object) // Deep copy to avoid modifying the cache 194 195 // Use Patch to apply only the changes, improving efficiency 196 if err := r.Patch(ctx, obj, client.MergeFrom(existing)); err != nil { 197 return err 198 } 199 200 logger.Info("Updated resource", "object", key) 201 return nil 202} 203 204// SetupWithManager sets up the controller with the Manager. 205func (r *SecretServiceReconciler) SetupWithManager(mgr ctrl.Manager) error { 206 return ctrl.NewControllerManagedBy(mgr). 207 For(&appsv1.SecretService{}). 208 Complete(r) 209}