Kubernetes Operator that creates Service Endpoints from Secrets
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}