Initial commit
This commit is contained in:
222
internal/controller/deployment.go
Normal file
222
internal/controller/deployment.go
Normal file
@@ -0,0 +1,222 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
gomaprojv1beta1 "github.com/jkaninda/goma-operator/api/v1beta1"
|
||||
v1 "k8s.io/api/apps/v1"
|
||||
av1 "k8s.io/api/autoscaling/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log"
|
||||
)
|
||||
|
||||
// createDeployment creates Kubernetes deployment
|
||||
func createDeployment(r GatewayReconciler, ctx context.Context, req ctrl.Request, gateway gomaprojv1beta1.Gateway, imageName string) error {
|
||||
logger := log.FromContext(ctx)
|
||||
// Define the desired Deployment
|
||||
deployment := &v1.Deployment{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: req.Name,
|
||||
Namespace: req.Namespace,
|
||||
Labels: gateway.Labels,
|
||||
},
|
||||
Spec: v1.DeploymentSpec{
|
||||
Replicas: int32Ptr(gateway.Spec.ReplicaCount), // Set desired replicas
|
||||
Selector: &metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{
|
||||
"app": req.Name,
|
||||
"belongs-to": BelongsTo,
|
||||
"managed-by": gateway.Name,
|
||||
},
|
||||
},
|
||||
Template: corev1.PodTemplateSpec{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: map[string]string{
|
||||
"app": req.Name,
|
||||
"belongs-to": BelongsTo,
|
||||
"managed-by": gateway.Name,
|
||||
},
|
||||
Annotations: map[string]string{
|
||||
"updated-at": time.Now().Format(time.RFC3339),
|
||||
},
|
||||
},
|
||||
Spec: corev1.PodSpec{
|
||||
Affinity: gateway.Spec.Affinity,
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "gateway",
|
||||
Image: imageName,
|
||||
ImagePullPolicy: corev1.PullIfNotPresent,
|
||||
Command: []string{"/usr/local/bin/goma", "server"},
|
||||
Ports: []corev1.ContainerPort{
|
||||
{
|
||||
ContainerPort: 8080,
|
||||
},
|
||||
},
|
||||
ReadinessProbe: &corev1.Probe{
|
||||
InitialDelaySeconds: 5,
|
||||
PeriodSeconds: 10,
|
||||
ProbeHandler: corev1.ProbeHandler{
|
||||
HTTPGet: &corev1.HTTPGetAction{
|
||||
Path: "/readyz",
|
||||
Port: intstr.FromInt32(8080),
|
||||
},
|
||||
},
|
||||
},
|
||||
LivenessProbe: &corev1.Probe{
|
||||
InitialDelaySeconds: 15,
|
||||
PeriodSeconds: 30,
|
||||
ProbeHandler: corev1.ProbeHandler{
|
||||
HTTPGet: &corev1.HTTPGetAction{
|
||||
Path: "/healthz",
|
||||
Port: intstr.FromInt32(8080),
|
||||
},
|
||||
},
|
||||
},
|
||||
Resources: gateway.Spec.Resources,
|
||||
VolumeMounts: []corev1.VolumeMount{
|
||||
{
|
||||
Name: "config",
|
||||
MountPath: "/etc/goma",
|
||||
ReadOnly: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Volumes: []corev1.Volume{
|
||||
{
|
||||
Name: "config",
|
||||
VolumeSource: corev1.VolumeSource{
|
||||
ConfigMap: &corev1.ConfigMapVolumeSource{
|
||||
LocalObjectReference: corev1.LocalObjectReference{
|
||||
Name: req.Name,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Check if the Deployment already exists
|
||||
var existingDeployment v1.Deployment
|
||||
err := r.Get(ctx, types.NamespacedName{Name: deployment.Name, Namespace: deployment.Namespace}, &existingDeployment)
|
||||
if err != nil && client.IgnoreNotFound(err) != nil {
|
||||
logger.Error(err, "Failed to get Deployment")
|
||||
return err
|
||||
}
|
||||
if err != nil && client.IgnoreNotFound(err) == nil {
|
||||
logger.Info("Creating a new Deployment")
|
||||
// Create the Deployment if it doesn't exist
|
||||
if err = controllerutil.SetControllerReference(&gateway, deployment, r.Scheme); err != nil {
|
||||
logger.Error(err, "Failed to set controller reference")
|
||||
return err
|
||||
}
|
||||
if err = r.Create(ctx, deployment); err != nil {
|
||||
logger.Error(err, "Failed to create Deployment")
|
||||
return err
|
||||
}
|
||||
logger.Info("Created Deployment", "Deployment.Name", deployment.Name)
|
||||
} else {
|
||||
logger.Info("Deployment already exists", "Deployment.Name", deployment.Name)
|
||||
// Update the Deployment if the spec has changed
|
||||
if !equalDeploymentSpec(existingDeployment.Spec, deployment.Spec, gateway.Spec.AutoScaling.Enabled) {
|
||||
logger.Info("Updating Deployment", "Deployment.Name", deployment.Name)
|
||||
existingDeployment.Spec = deployment.Spec
|
||||
if err = r.Update(ctx, &existingDeployment); err != nil {
|
||||
logger.Error(err, "Failed to update Deployment")
|
||||
return err
|
||||
}
|
||||
logger.Info("Updated Deployment", "Deployment.Name", deployment.Name)
|
||||
}
|
||||
}
|
||||
|
||||
// check if hpa is enabled
|
||||
if gateway.Spec.AutoScaling.Enabled {
|
||||
err = createHpa(r, ctx, req, &gateway)
|
||||
if err != nil {
|
||||
logger.Error(err, "Failed to create HorizontalPodAutoscaler")
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
// Check if the hpa already exists
|
||||
var existHpa av1.HorizontalPodAutoscaler
|
||||
err = r.Get(ctx, types.NamespacedName{Name: req.Name, Namespace: req.Namespace}, &existHpa)
|
||||
if err != nil && client.IgnoreNotFound(err) != nil {
|
||||
logger.Error(err, "Failed to get HorizontalPodAutoscaler")
|
||||
return err
|
||||
}
|
||||
if err == nil {
|
||||
// Delete the HorizontalPodAutoscaler
|
||||
if err = r.Delete(ctx, &existHpa); err != nil {
|
||||
logger.Error(err, "Failed to delete HorizontalPodAutoscaler")
|
||||
return err
|
||||
}
|
||||
logger.Info("Deleted HorizontalPodAutoscaler successfully", "HorizontalPodAutoscaler.Name", req.Name)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Helper function to compare Deployment specs
|
||||
func equalDeploymentSpec(existing, desired v1.DeploymentSpec, autoScalingEnabled bool) bool {
|
||||
if existing.Template.Spec.Containers[0].Image != desired.Template.Spec.Containers[0].Image {
|
||||
return false
|
||||
}
|
||||
|
||||
if !autoScalingEnabled {
|
||||
if existing.Replicas == nil || *existing.Replicas != *desired.Replicas {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
func (r *RouteReconciler) RestartDeployment(ctx context.Context, req ctrl.Request, gateway gomaprojv1beta1.Gateway) error {
|
||||
logger := log.FromContext(ctx)
|
||||
// Fetch the Deployment
|
||||
var deployment v1.Deployment
|
||||
if err := r.Get(ctx, types.NamespacedName{Name: gateway.Name, Namespace: req.Namespace}, &deployment); err != nil {
|
||||
logger.Error(err, "Failed to get Deployment", "name", gateway.Name, "namespace", req.Name)
|
||||
return client.IgnoreNotFound(err)
|
||||
}
|
||||
|
||||
// Add or update an annotation to trigger a rolling update
|
||||
if deployment.Spec.Template.ObjectMeta.Annotations == nil {
|
||||
deployment.Spec.Template.ObjectMeta.Annotations = make(map[string]string)
|
||||
}
|
||||
|
||||
deployment.Spec.Template.ObjectMeta.Annotations["restarted-at"] = time.Now().Format(time.RFC3339)
|
||||
deployment.Spec.Template.ObjectMeta.Annotations["updated-at"] = time.Now().Format(time.RFC3339)
|
||||
// Update the Deployment
|
||||
if err := r.Update(ctx, &deployment); err != nil {
|
||||
logger.Error(err, "Failed to update Deployment for restart", "name", gateway.Name, "namespace", req.Name)
|
||||
return err
|
||||
}
|
||||
|
||||
logger.Info("Successfully restarted Deployment", "name", gateway.Name, "namespace", req.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
// currentReplicas returns current replicas
|
||||
func currentReplicas(ctx context.Context, c client.Client, hpaName, namespace string) (int32, error) {
|
||||
hpa := &av1.HorizontalPodAutoscaler{}
|
||||
// Retrieve the HPA resource
|
||||
err := c.Get(ctx, types.NamespacedName{Name: hpaName, Namespace: namespace}, hpa)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to get HPA: %w", err)
|
||||
}
|
||||
// Access the current replicas in the status field
|
||||
replicas := hpa.Status.CurrentReplicas
|
||||
return replicas, nil
|
||||
}
|
||||
305
internal/controller/gateway_controller.go
Normal file
305
internal/controller/gateway_controller.go
Normal file
@@ -0,0 +1,305 @@
|
||||
/*
|
||||
Copyright 2024.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
v1 "k8s.io/api/apps/v1"
|
||||
autoscalingv1 "k8s.io/api/autoscaling/v1"
|
||||
"strings"
|
||||
|
||||
gomaprojv1beta1 "github.com/jkaninda/goma-operator/api/v1beta1"
|
||||
"gopkg.in/yaml.v3"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log"
|
||||
)
|
||||
|
||||
// GatewayReconciler reconciles a Gateway object
|
||||
type GatewayReconciler struct {
|
||||
client.Client
|
||||
Scheme *runtime.Scheme
|
||||
}
|
||||
|
||||
// +kubebuilder:rbac:groups=gomaproj.github.io,resources=gateways,verbs=get;list;watch;create;update;patch;delete
|
||||
// +kubebuilder:rbac:groups=gomaproj.github.io,resources=gateways/status,verbs=get;update;patch
|
||||
// +kubebuilder:rbac:groups=gomaproj.github.io,resources=gateways/finalizers,verbs=update
|
||||
// +kubebuilder:rbac:groups=gomaproj.github.io,resources=middlewares,verbs=get;list;watch;create;update;patch;delete
|
||||
// +kubebuilder:rbac:groups=gomaproj.github.io,resources=middlewares/status,verbs=get;update;patch
|
||||
// +kubebuilder:rbac:groups=gomaproj.github.io,resources=middlewares/finalizers,verbs=update
|
||||
// +kubebuilder:rbac:groups="",resources=configmaps,verbs=get;list;watch;create;update;patch;delete
|
||||
// +kubebuilder:rbac:groups="",resources=events,verbs=create;update;patch;
|
||||
// +kubebuilder:rbac:groups="",resources=services,verbs=get;list;watch;create;update;patch;delete
|
||||
// +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete
|
||||
// +kubebuilder:rbac:groups=autoscaling,resources=horizontalpodautoscalers,verbs=get;list;watch;create;update;patch;delete
|
||||
|
||||
// Reconcile is part of the main kubernetes reconciliation loop which aims to
|
||||
// move the current state of the cluster closer to the desired state.
|
||||
// the Gateway object against the actual cluster state, and then
|
||||
// perform operations to make the cluster state reflect the state specified by
|
||||
// the user.
|
||||
//
|
||||
// For more details, check Reconcile and its Result here:
|
||||
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.19.1/pkg/reconcile
|
||||
func (r *GatewayReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
|
||||
logger := log.FromContext(ctx)
|
||||
imageName := AppImageName
|
||||
// Fetch the custom resource
|
||||
gateway := &gomaprojv1beta1.Gateway{}
|
||||
if err := r.Get(ctx, req.NamespacedName, gateway); err != nil {
|
||||
logger.Error(err, "Unable to fetch Gateway")
|
||||
return ctrl.Result{}, client.IgnoreNotFound(err)
|
||||
}
|
||||
|
||||
// Check if the object is being deleted and if so, handle it
|
||||
if gateway.ObjectMeta.DeletionTimestamp.IsZero() {
|
||||
if !controllerutil.ContainsFinalizer(gateway, FinalizerName) {
|
||||
controllerutil.AddFinalizer(gateway, FinalizerName)
|
||||
err := r.Update(ctx, gateway)
|
||||
if err != nil {
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if controllerutil.ContainsFinalizer(gateway, FinalizerName) {
|
||||
// Once finalization is done, remove the finalizer
|
||||
if err := r.finalize(ctx, gateway); err != nil {
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
controllerutil.RemoveFinalizer(gateway, FinalizerName)
|
||||
err := r.Update(ctx, gateway)
|
||||
if err != nil {
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
}
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
if gateway.Spec.GatewayVersion != "" {
|
||||
imageName = fmt.Sprintf("%s:%s", AppImageName, gateway.Spec.GatewayVersion)
|
||||
}
|
||||
if gateway.Spec.ReplicaCount != 0 {
|
||||
ReplicaCount = gateway.Spec.ReplicaCount
|
||||
}
|
||||
gomaConfig := gatewayConfig(*r, ctx, req, gateway)
|
||||
yamlContent, err := yaml.Marshal(&gomaConfig)
|
||||
if err != nil {
|
||||
logger.Error(err, "Unable to marshal YAML")
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
// Define the desired ConfigMap
|
||||
configMap := &corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: req.Name,
|
||||
Namespace: req.Namespace,
|
||||
Labels: map[string]string{
|
||||
"belongs-to": BelongsTo,
|
||||
"gateway": gateway.Name,
|
||||
},
|
||||
},
|
||||
|
||||
Data: map[string]string{
|
||||
ConfigName: strings.TrimSpace(string(yamlContent)),
|
||||
},
|
||||
}
|
||||
// Check if the ConfigMap already exists
|
||||
var existingConfigMap corev1.ConfigMap
|
||||
err = r.Get(ctx, types.NamespacedName{Name: configMap.Name, Namespace: configMap.Namespace}, &existingConfigMap)
|
||||
if err != nil && client.IgnoreNotFound(err) != nil {
|
||||
logger.Error(err, "Failed to get ConfigMap")
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
if err != nil && client.IgnoreNotFound(err) == nil {
|
||||
// Create the ConfigMap if it doesn't exist
|
||||
if err := controllerutil.SetControllerReference(gateway, configMap, r.Scheme); err != nil {
|
||||
logger.Error(err, "Failed to set controller reference")
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
if err := r.Create(ctx, configMap); err != nil {
|
||||
addCondition(&gateway.Status, "ConfigMapNotReady", metav1.ConditionFalse, "ConfigMapNotReady", "Failed to add configMap for Gateway")
|
||||
logger.Error(err, "Failed to create ConfigMap")
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
logger.Info("Created ConfigMap", "ConfigMap.Name", configMap.Name)
|
||||
} else {
|
||||
// Optional: Update the ConfigMap if needed
|
||||
if !equalConfigMapData(existingConfigMap.Data, configMap.Data) {
|
||||
existingConfigMap.Data = configMap.Data
|
||||
if err := r.Update(ctx, &existingConfigMap); err != nil {
|
||||
logger.Error(err, "Failed to update ConfigMap")
|
||||
addCondition(&gateway.Status, "ConfigMapReady", metav1.ConditionFalse, "ConfigMapReady", "Failed to update ConfigMap for Gateway")
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
logger.Info("Updated ConfigMap", "ConfigMap.Name", configMap.Name)
|
||||
}
|
||||
|
||||
}
|
||||
err = createDeployment(*r, ctx, req, *gateway, imageName)
|
||||
if err != nil {
|
||||
addCondition(&gateway.Status, "DeploymentNotReady", metav1.ConditionFalse, "DeploymentNotReady", "Failed to created deployment for Gateway")
|
||||
logger.Error(err, "Failed to create Deployment")
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
err = createService(*r, ctx, req, gateway)
|
||||
if err != nil {
|
||||
addCondition(&gateway.Status, "ServiceNotReady", metav1.ConditionFalse, "ServiceNotReady", "Failed to create Service for Gateway")
|
||||
logger.Error(err, "Failed to create Service")
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
addCondition(&gateway.Status, "GatewayReady", metav1.ConditionTrue, "AllSubresourcesReady", "All subresources are ready")
|
||||
logger.Info("All Subresources ready")
|
||||
|
||||
// Update the Status
|
||||
gateway.Status.Replicas = gateway.Spec.ReplicaCount
|
||||
if gateway.Spec.AutoScaling.Enabled {
|
||||
replicas, err := currentReplicas(ctx, r.Client, gateway.Name, gateway.Namespace)
|
||||
if err != nil {
|
||||
logger.Error(err, "Failed to get current replicas")
|
||||
}
|
||||
gateway.Status.Replicas = replicas
|
||||
|
||||
}
|
||||
gateway.Status.Routes = int32(len(gomaConfig.Gateway.Routes))
|
||||
if err := r.updateStatus(ctx, gateway); err != nil {
|
||||
logger.Error(err, "Failed to update resource status")
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
logger.Info("Successfully updated resource status")
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
func (r *GatewayReconciler) updateStatus(ctx context.Context, gateway *gomaprojv1beta1.Gateway) error {
|
||||
return r.Client.Status().Update(ctx, gateway)
|
||||
}
|
||||
|
||||
// Helper function to return a pointer to an int32
|
||||
func int32Ptr(i int32) *int32 {
|
||||
return &i
|
||||
}
|
||||
|
||||
func addCondition(status *gomaprojv1beta1.GatewayStatus, condType string, statusType metav1.ConditionStatus, reason, message string) {
|
||||
for i, existingCondition := range status.Conditions {
|
||||
if existingCondition.Type == condType {
|
||||
// Condition already exists, update it
|
||||
status.Conditions[i].Status = statusType
|
||||
status.Conditions[i].Reason = reason
|
||||
status.Conditions[i].Message = message
|
||||
status.Conditions[i].LastTransitionTime = metav1.Now()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// The Condition does not exist, add it
|
||||
condition := metav1.Condition{
|
||||
Type: condType,
|
||||
Status: statusType,
|
||||
Reason: reason,
|
||||
Message: message,
|
||||
LastTransitionTime: metav1.Now(),
|
||||
}
|
||||
status.Conditions = append(status.Conditions, condition)
|
||||
}
|
||||
|
||||
func (r *GatewayReconciler) finalize(ctx context.Context, gateway *gomaprojv1beta1.Gateway) error {
|
||||
logger := log.FromContext(ctx)
|
||||
logger.Info("Finalizing Gateway", "Name", gateway.Name, "Namespace", gateway.Namespace)
|
||||
// Delete the ConfigMap
|
||||
configMap := &corev1.ConfigMap{}
|
||||
err := r.Get(ctx, client.ObjectKey{Namespace: gateway.Namespace, Name: gateway.Name}, configMap)
|
||||
if err != nil {
|
||||
logger.Error(err, "Failed to get Deployment")
|
||||
return err
|
||||
}
|
||||
logger.Info("Deleting ConfigMap...", "Name", configMap.Name)
|
||||
err = r.Delete(ctx, configMap)
|
||||
if err != nil {
|
||||
logger.Error(err, "Failed to delete Deployment")
|
||||
return err
|
||||
}
|
||||
|
||||
// Delete the Deployment
|
||||
deployment := &v1.Deployment{}
|
||||
err = r.Get(ctx, client.ObjectKey{Namespace: gateway.Namespace, Name: gateway.Name}, deployment)
|
||||
if err != nil {
|
||||
logger.Error(err, "Failed to get Deployment")
|
||||
return err
|
||||
}
|
||||
logger.Info("Deleting Deployment...", "Name", deployment.Name)
|
||||
err = r.Delete(ctx, deployment)
|
||||
if err != nil {
|
||||
logger.Error(err, "Failed to delete Deployment")
|
||||
return err
|
||||
}
|
||||
|
||||
if gateway.Spec.AutoScaling.Enabled {
|
||||
// Delete the HorizontalPodAutoscaler
|
||||
hpa := &autoscalingv1.HorizontalPodAutoscaler{}
|
||||
err = r.Get(ctx, client.ObjectKey{Namespace: gateway.Namespace, Name: gateway.Name}, hpa)
|
||||
if err != nil {
|
||||
logger.Error(err, "Failed to get HorizontalPodAutoscaler")
|
||||
return err
|
||||
}
|
||||
logger.Info("Deleting HorizontalPodAutoscaler...", "Name", hpa.Name)
|
||||
err = r.Delete(ctx, hpa)
|
||||
if err != nil {
|
||||
logger.Error(err, "Failed to delete HorizontalPodAutoscaler")
|
||||
return err
|
||||
}
|
||||
|
||||
}
|
||||
logger.Info("Deleted Deployment", "Name", deployment.Name, "Namespace", deployment.Namespace)
|
||||
|
||||
// Delete the Service
|
||||
service := &corev1.Service{}
|
||||
err = r.Get(ctx, client.ObjectKey{Namespace: gateway.Namespace, Name: gateway.Name}, service)
|
||||
if err != nil {
|
||||
logger.Error(err, "Failed to get Service")
|
||||
return err
|
||||
}
|
||||
logger.Info("Deleting Service...", "Name", service.Name)
|
||||
err = r.Delete(ctx, service)
|
||||
if err != nil {
|
||||
logger.Error(err, "Failed to delete Service")
|
||||
return err
|
||||
}
|
||||
|
||||
logger.Info("Deleted Service", "Name", service.Name, "Namespace", service.Namespace)
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetupWithManager sets up the controller with the Manager.
|
||||
func (r *GatewayReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||
return ctrl.NewControllerManagedBy(mgr).
|
||||
For(&gomaprojv1beta1.Gateway{}).
|
||||
Owns(&corev1.ConfigMap{}). // Watch ConfigMaps created by the controller
|
||||
Owns(&v1.Deployment{}). // Watch Deployments created by the controller
|
||||
Owns(&corev1.Service{}). // Watch Services created by the controller
|
||||
Owns(&autoscalingv1.HorizontalPodAutoscaler{}). // Watch HorizontalPodAutoscaler created by the controller
|
||||
Named("gateway").
|
||||
Complete(r)
|
||||
}
|
||||
89
internal/controller/gateway_controller_test.go
Normal file
89
internal/controller/gateway_controller_test.go
Normal file
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
Copyright 2024.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
gomaprojv1beta1 "github.com/jkaninda/goma-operator/api/v1beta1"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||
)
|
||||
|
||||
var _ = Describe("Gateway Controller", func() {
|
||||
Context("When reconciling a resource", func() {
|
||||
const resourceName = "test-gateway"
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
typeNamespacedName := types.NamespacedName{
|
||||
Name: resourceName,
|
||||
Namespace: "default",
|
||||
}
|
||||
gateway := &gomaprojv1beta1.Gateway{}
|
||||
|
||||
BeforeEach(func() {
|
||||
By("creating the custom resource for the Kind Gateway")
|
||||
err := k8sClient.Get(ctx, typeNamespacedName, gateway)
|
||||
if err != nil && errors.IsNotFound(err) {
|
||||
resource := &gomaprojv1beta1.Gateway{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: resourceName,
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: gomaprojv1beta1.GatewaySpec{
|
||||
GatewayVersion: "latest",
|
||||
Server: gomaprojv1beta1.Server{},
|
||||
ReplicaCount: 1,
|
||||
AutoScaling: gomaprojv1beta1.AutoScaling{
|
||||
Enabled: false,
|
||||
MinReplicas: 2,
|
||||
MaxReplicas: 5,
|
||||
TargetCPUUtilizationPercentage: 80,
|
||||
},
|
||||
},
|
||||
}
|
||||
Expect(k8sClient.Create(ctx, resource)).To(Succeed())
|
||||
}
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
resource := &gomaprojv1beta1.Gateway{}
|
||||
err := k8sClient.Get(ctx, typeNamespacedName, resource)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
By("Cleanup the specific resource instance Gateway")
|
||||
Expect(k8sClient.Delete(ctx, resource)).To(Succeed())
|
||||
})
|
||||
It("should successfully reconcile the resource", func() {
|
||||
By("Reconciling the created resource")
|
||||
controllerReconciler := &GatewayReconciler{
|
||||
Client: k8sClient,
|
||||
Scheme: k8sClient.Scheme(),
|
||||
}
|
||||
|
||||
_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{
|
||||
NamespacedName: typeNamespacedName,
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
})
|
||||
})
|
||||
})
|
||||
210
internal/controller/helpers.go
Normal file
210
internal/controller/helpers.go
Normal file
@@ -0,0 +1,210 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
gomaprojv1beta1 "github.com/jkaninda/goma-operator/api/v1beta1"
|
||||
"gopkg.in/yaml.v3"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log"
|
||||
)
|
||||
|
||||
func gatewayConfig(r GatewayReconciler, ctx context.Context, req ctrl.Request, gateway *gomaprojv1beta1.Gateway) GatewayConfig {
|
||||
logger := log.FromContext(ctx)
|
||||
gomaConfig := &GatewayConfig{}
|
||||
gomaConfig.Version = GatewayConfigVersion
|
||||
gomaConfig.Gateway = mapToGateway(gateway.Spec)
|
||||
labelSelector := client.MatchingLabels{}
|
||||
var middlewareNames []string
|
||||
// List ConfigMaps in the namespace with the matching label
|
||||
var routes gomaprojv1beta1.RouteList
|
||||
if err := r.List(ctx, &routes, labelSelector, client.InNamespace(req.Namespace)); err != nil {
|
||||
logger.Error(err, "Failed to list Routes")
|
||||
return *gomaConfig
|
||||
}
|
||||
var middlewares gomaprojv1beta1.MiddlewareList
|
||||
if err := r.List(ctx, &middlewares, labelSelector, client.InNamespace(req.Namespace)); err != nil {
|
||||
logger.Error(err, "Failed to list Middlewares")
|
||||
return *gomaConfig
|
||||
}
|
||||
logger.Info(fmt.Sprintf("Listing Routes: size: %d", len(routes.Items)))
|
||||
|
||||
for _, route := range routes.Items {
|
||||
logger.Info("Found Route", "Name", route.Name)
|
||||
if route.Spec.Gateway == gateway.Name {
|
||||
gomaConfig.Gateway.Routes = append(gomaConfig.Gateway.Routes, route.Spec.Routes...)
|
||||
for _, rt := range route.Spec.Routes {
|
||||
middlewareNames = append(middlewareNames, rt.Middlewares...)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
for _, mid := range middlewares.Items {
|
||||
middleware := *mapMid(mid)
|
||||
logger.Info("Adding Middleware", "Name", middleware.Name)
|
||||
if slices.Contains(middlewareNames, middleware.Name) {
|
||||
gomaConfig.Middlewares = append(gomaConfig.Middlewares, middleware)
|
||||
}
|
||||
|
||||
}
|
||||
return *gomaConfig
|
||||
}
|
||||
func updateGatewayConfig(r RouteReconciler, ctx context.Context, req ctrl.Request, gateway gomaprojv1beta1.Gateway) error {
|
||||
logger := log.FromContext(ctx)
|
||||
gomaConfig := &GatewayConfig{}
|
||||
gomaConfig.Version = GatewayConfigVersion
|
||||
gomaConfig.Gateway = mapToGateway(gateway.Spec)
|
||||
labelSelector := client.MatchingLabels{}
|
||||
var middlewareNames []string
|
||||
// List ConfigMaps in the namespace with the matching label
|
||||
var routes gomaprojv1beta1.RouteList
|
||||
if err := r.List(ctx, &routes, labelSelector, client.InNamespace(req.Namespace)); err != nil {
|
||||
logger.Error(err, "Failed to list Routes")
|
||||
return err
|
||||
}
|
||||
var middlewares gomaprojv1beta1.MiddlewareList
|
||||
if err := r.List(ctx, &middlewares, labelSelector, client.InNamespace(req.Namespace)); err != nil {
|
||||
logger.Error(err, "Failed to list Middlewares")
|
||||
return err
|
||||
}
|
||||
logger.Info(fmt.Sprintf("Listing Routes: size: %d", len(routes.Items)))
|
||||
|
||||
for _, route := range routes.Items {
|
||||
logger.Info("Found Route", "Name", route.Name)
|
||||
if route.Spec.Gateway == gateway.Name {
|
||||
gomaConfig.Gateway.Routes = append(gomaConfig.Gateway.Routes, route.Spec.Routes...)
|
||||
for _, rt := range route.Spec.Routes {
|
||||
middlewareNames = append(middlewareNames, rt.Middlewares...)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
for _, mid := range middlewares.Items {
|
||||
middleware := *mapMid(mid)
|
||||
logger.Info("Adding Middleware", "Name", middleware.Name)
|
||||
if slices.Contains(middlewareNames, middleware.Name) {
|
||||
gomaConfig.Middlewares = append(gomaConfig.Middlewares, middleware)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
yamlContent, err := yaml.Marshal(&gomaConfig)
|
||||
if err != nil {
|
||||
logger.Error(err, "Unable to marshal YAML")
|
||||
return err
|
||||
}
|
||||
// Define the desired ConfigMap
|
||||
configMap := &corev1.ConfigMap{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: gateway.Name,
|
||||
Namespace: req.Namespace,
|
||||
Labels: map[string]string{
|
||||
"belongs-to": BelongsTo,
|
||||
"gateway": gateway.Name,
|
||||
},
|
||||
},
|
||||
Data: map[string]string{
|
||||
ConfigName: strings.TrimSpace(string(yamlContent)),
|
||||
},
|
||||
}
|
||||
// Check if the ConfigMap already exists
|
||||
var existingConfigMap corev1.ConfigMap
|
||||
err = r.Get(ctx, types.NamespacedName{Name: configMap.Name, Namespace: configMap.Namespace}, &existingConfigMap)
|
||||
if err != nil && client.IgnoreNotFound(err) != nil {
|
||||
logger.Error(err, "Failed to get ConfigMap")
|
||||
return err
|
||||
}
|
||||
|
||||
if err != nil && client.IgnoreNotFound(err) == nil {
|
||||
// Create the ConfigMap if it doesn't exist
|
||||
if err = controllerutil.SetControllerReference(&gateway, configMap, r.Scheme); err != nil {
|
||||
logger.Error(err, "Failed to set controller reference")
|
||||
return err
|
||||
}
|
||||
if err = r.Create(ctx, configMap); err != nil {
|
||||
logger.Error(err, "Failed to create ConfigMap")
|
||||
return err
|
||||
}
|
||||
logger.Info("Created ConfigMap", "ConfigMap.Name", configMap.Name)
|
||||
} else {
|
||||
// Optional: Update the ConfigMap if needed
|
||||
if !equalConfigMapData(existingConfigMap.Data, configMap.Data) {
|
||||
existingConfigMap.Data = configMap.Data
|
||||
if err = r.Update(ctx, &existingConfigMap); err != nil {
|
||||
logger.Error(err, "Failed to update ConfigMap")
|
||||
return err
|
||||
}
|
||||
logger.Info("Updated ConfigMap", "ConfigMap.Name", configMap.Name)
|
||||
}
|
||||
|
||||
}
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
// Helper function to compare ConfigMap data
|
||||
func equalConfigMapData(existing, desired map[string]string) bool {
|
||||
if len(existing) != len(desired) {
|
||||
return false
|
||||
}
|
||||
for key, value := range desired {
|
||||
if existing[key] != value {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// mapMid converts RawExtensionT to struct
|
||||
func mapMid(middleware gomaprojv1beta1.Middleware) *Middleware {
|
||||
mid := &Middleware{
|
||||
Name: middleware.Name,
|
||||
Type: middleware.Spec.Type,
|
||||
Paths: middleware.Spec.Paths,
|
||||
}
|
||||
switch middleware.Spec.Type {
|
||||
case BasicAuth:
|
||||
var basicAuth BasicRuleMiddleware
|
||||
err := ConvertRawExtensionToStruct(middleware.Spec.Rule, &basicAuth)
|
||||
if err != nil {
|
||||
return mid
|
||||
}
|
||||
mid.Rule = basicAuth
|
||||
return mid
|
||||
case OAuth:
|
||||
var oauthRulerMiddleware OauthRulerMiddleware
|
||||
err := ConvertRawExtensionToStruct(middleware.Spec.Rule, &oauthRulerMiddleware)
|
||||
if err != nil {
|
||||
return mid
|
||||
}
|
||||
mid.Rule = oauthRulerMiddleware
|
||||
return mid
|
||||
case JWTAuth:
|
||||
var jwtAuth JWTRuleMiddleware
|
||||
err := ConvertRawExtensionToStruct(middleware.Spec.Rule, &jwtAuth)
|
||||
if err != nil {
|
||||
return mid
|
||||
}
|
||||
mid.Rule = jwtAuth
|
||||
return mid
|
||||
case ratelimit, RateLimit:
|
||||
var rateLimitRuleMiddleware RateLimitRuleMiddleware
|
||||
err := ConvertRawExtensionToStruct(middleware.Spec.Rule, &rateLimitRuleMiddleware)
|
||||
if err != nil {
|
||||
return mid
|
||||
}
|
||||
mid.Rule = rateLimitRuleMiddleware
|
||||
return mid
|
||||
}
|
||||
return mid
|
||||
}
|
||||
122
internal/controller/hpa.go
Normal file
122
internal/controller/hpa.go
Normal file
@@ -0,0 +1,122 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
|
||||
gomaprojv1beta1 "github.com/jkaninda/goma-operator/api/v1beta1"
|
||||
autoscalingv2 "k8s.io/api/autoscaling/v2"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log"
|
||||
)
|
||||
|
||||
// createHpa creates HPA
|
||||
func createHpa(r GatewayReconciler, ctx context.Context, req ctrl.Request, gateway *gomaprojv1beta1.Gateway) error {
|
||||
logger := log.FromContext(ctx)
|
||||
var metrics []autoscalingv2.MetricSpec
|
||||
targetCPUUtilizationPercentage := gateway.Spec.AutoScaling.TargetCPUUtilizationPercentage
|
||||
targetMemoryUtilizationPercentage := gateway.Spec.AutoScaling.TargetMemoryUtilizationPercentage
|
||||
// Add CPU metric if targetCPUUtilizationPercentage is set
|
||||
if targetCPUUtilizationPercentage != 0 {
|
||||
metrics = append(metrics, autoscalingv2.MetricSpec{
|
||||
Type: autoscalingv2.ResourceMetricSourceType,
|
||||
Resource: &autoscalingv2.ResourceMetricSource{
|
||||
Name: "cpu",
|
||||
Target: autoscalingv2.MetricTarget{
|
||||
Type: autoscalingv2.UtilizationMetricType,
|
||||
AverageUtilization: int32Ptr(targetCPUUtilizationPercentage),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
// Add Memory metric if targetMemoryUtilizationPercentage is set
|
||||
if targetMemoryUtilizationPercentage != 0 {
|
||||
metrics = append(metrics, autoscalingv2.MetricSpec{
|
||||
Type: autoscalingv2.ResourceMetricSourceType,
|
||||
Resource: &autoscalingv2.ResourceMetricSource{
|
||||
Name: "memory",
|
||||
Target: autoscalingv2.MetricTarget{
|
||||
Type: autoscalingv2.UtilizationMetricType,
|
||||
AverageUtilization: int32Ptr(targetMemoryUtilizationPercentage),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
// Create HPA
|
||||
hpa := &autoscalingv2.HorizontalPodAutoscaler{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: req.Name,
|
||||
Namespace: req.Namespace,
|
||||
},
|
||||
Spec: autoscalingv2.HorizontalPodAutoscalerSpec{
|
||||
MinReplicas: int32Ptr(gateway.Spec.AutoScaling.MinReplicas),
|
||||
MaxReplicas: gateway.Spec.AutoScaling.MaxReplicas,
|
||||
Metrics: metrics,
|
||||
ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{
|
||||
APIVersion: "apps/v1",
|
||||
Kind: "Deployment",
|
||||
Name: req.Name,
|
||||
},
|
||||
},
|
||||
}
|
||||
// Check if the hpa already exists
|
||||
var existHpa autoscalingv2.HorizontalPodAutoscaler
|
||||
err := r.Get(ctx, types.NamespacedName{Name: req.Name, Namespace: req.Namespace}, &existHpa)
|
||||
if err != nil && client.IgnoreNotFound(err) != nil {
|
||||
logger.Error(err, "Failed to get HorizontalPodAutoscaler")
|
||||
return err
|
||||
}
|
||||
if err != nil && client.IgnoreNotFound(err) == nil {
|
||||
// Create the HPA if it doesn't exist
|
||||
if err = controllerutil.SetControllerReference(gateway, hpa, r.Scheme); err != nil {
|
||||
logger.Error(err, "Failed to set controller reference")
|
||||
return err
|
||||
}
|
||||
if err = r.Create(ctx, hpa); err != nil {
|
||||
logger.Error(err, "Failed to create HorizontalPodAutoscaler")
|
||||
return err
|
||||
}
|
||||
logger.Info("Created HorizontalPodAutoscaler", "HorizontalPodAutoscaler.Name", hpa.Name)
|
||||
} else {
|
||||
// Update the Deployment if the spec has changed
|
||||
if !equalHpaSpec(existHpa, *hpa) {
|
||||
existHpa.Spec = hpa.Spec
|
||||
if err = r.Update(ctx, &existHpa); err != nil {
|
||||
logger.Error(err, "Failed to update Deployment")
|
||||
return err
|
||||
}
|
||||
logger.Info("Updated HorizontalPodAutoscaler", "HorizontalPodAutoscaler.Name", hpa.Name)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Helper function to compare Deployment specs
|
||||
func equalHpaSpec(existing, desired autoscalingv2.HorizontalPodAutoscaler) bool {
|
||||
// A deep equality check or field-by-field comparison would be more accurate
|
||||
if existing.Spec.MinReplicas != desired.Spec.MinReplicas {
|
||||
return false
|
||||
}
|
||||
if existing.Spec.MaxReplicas != desired.Spec.MaxReplicas {
|
||||
return false
|
||||
}
|
||||
if existing.Spec.Metrics[0].Resource.Target.AverageUtilization != desired.Spec.Metrics[0].Resource.Target.AverageUtilization {
|
||||
return false
|
||||
}
|
||||
if existing.Spec.Metrics[1].Resource.Target.AverageUtilization != desired.Spec.Metrics[1].Resource.Target.AverageUtilization {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
func ConvertRawExtensionToStruct(raw runtime.RawExtension, out interface{}) error {
|
||||
// Unmarshal the raw JSON into the provided struct
|
||||
if err := json.Unmarshal(raw.Raw, out); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
87
internal/controller/route_controller.go
Normal file
87
internal/controller/route_controller.go
Normal file
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
Copyright 2024.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
gomaprojv1beta1 "github.com/jkaninda/goma-operator/api/v1beta1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log"
|
||||
)
|
||||
|
||||
// RouteReconciler reconciles a Route object
|
||||
type RouteReconciler struct {
|
||||
client.Client
|
||||
Scheme *runtime.Scheme
|
||||
}
|
||||
|
||||
// +kubebuilder:rbac:groups=gomaproj.github.io,resources=routes,verbs=get;list;watch;create;update;patch;delete
|
||||
// +kubebuilder:rbac:groups=gomaproj.github.io,resources=routes/status,verbs=get;update;patch
|
||||
// +kubebuilder:rbac:groups=gomaproj.github.io,resources=routes/finalizers,verbs=update
|
||||
// +kubebuilder:rbac:groups=gomaproj.github.io,resources=middlewares,verbs=get;list;watch;create;update;patch;delete
|
||||
// +kubebuilder:rbac:groups=gomaproj.github.io,resources=middlewares/status,verbs=get;update;patch
|
||||
// +kubebuilder:rbac:groups=gomaproj.github.io,resources=middlewares/finalizers,verbs=update
|
||||
// +kubebuilder:rbac:groups="",resources=configmaps,verbs=get;list;watch;create;update;patch;delete
|
||||
// +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete
|
||||
|
||||
// Reconcile is part of the main kubernetes reconciliation loop which aims to
|
||||
// move the current state of the cluster closer to the desired state.
|
||||
// TODO(user): Modify the Reconcile function to compare the state specified by
|
||||
// the Route object against the actual cluster state, and then
|
||||
// perform operations to make the cluster state reflect the state specified by
|
||||
// the user.
|
||||
//
|
||||
// For more details, check Reconcile and its Result here:
|
||||
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.19.1/pkg/reconcile
|
||||
func (r *RouteReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
|
||||
logger := log.FromContext(ctx)
|
||||
// Fetch the custom resource
|
||||
var route gomaprojv1beta1.Route
|
||||
if err := r.Get(ctx, req.NamespacedName, &route); err != nil {
|
||||
logger.Error(err, "Unable to fetch CustomResource")
|
||||
return ctrl.Result{}, client.IgnoreNotFound(err)
|
||||
}
|
||||
var gateway gomaprojv1beta1.Gateway
|
||||
if err := r.Get(ctx, types.NamespacedName{Name: route.Spec.Gateway, Namespace: route.Namespace}, &gateway); err != nil {
|
||||
logger.Error(err, "Failed to fetch Gateway")
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
err := updateGatewayConfig(*r, ctx, req, gateway)
|
||||
if err != nil {
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
//
|
||||
if err = r.RestartDeployment(ctx, req, gateway); err != nil {
|
||||
logger.Error(err, "Failed to restart Deployment")
|
||||
return ctrl.Result{}, err
|
||||
|
||||
}
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
// SetupWithManager sets up the controller with the Manager.
|
||||
func (r *RouteReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||
return ctrl.NewControllerManagedBy(mgr).
|
||||
For(&gomaprojv1beta1.Route{}).
|
||||
Named("route").
|
||||
Complete(r)
|
||||
}
|
||||
119
internal/controller/route_controller_test.go
Normal file
119
internal/controller/route_controller_test.go
Normal file
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
Copyright 2024.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
gomaprojv1beta1 "github.com/jkaninda/goma-operator/api/v1beta1"
|
||||
)
|
||||
|
||||
var _ = Describe("Route Controller", func() {
|
||||
Context("When reconciling a resource", func() {
|
||||
const resourceName = "test-resource"
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
typeNamespacedName := types.NamespacedName{
|
||||
Name: resourceName,
|
||||
Namespace: "default", // TODO(user):Modify as needed
|
||||
}
|
||||
gateway := &gomaprojv1beta1.Gateway{}
|
||||
|
||||
route := &gomaprojv1beta1.Route{}
|
||||
|
||||
BeforeEach(func() {
|
||||
By("creating the custom resource for the Kind Gateway")
|
||||
err := k8sClient.Get(ctx, typeNamespacedName, gateway)
|
||||
if err != nil && errors.IsNotFound(err) {
|
||||
resource := &gomaprojv1beta1.Gateway{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: resourceName,
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: gomaprojv1beta1.GatewaySpec{
|
||||
GatewayVersion: "latest",
|
||||
Server: gomaprojv1beta1.Server{},
|
||||
ReplicaCount: 1,
|
||||
AutoScaling: gomaprojv1beta1.AutoScaling{
|
||||
Enabled: false,
|
||||
MinReplicas: 2,
|
||||
MaxReplicas: 5,
|
||||
TargetCPUUtilizationPercentage: 80,
|
||||
},
|
||||
},
|
||||
}
|
||||
Expect(k8sClient.Create(ctx, resource)).To(Succeed())
|
||||
}
|
||||
By("creating the custom resource for the Kind Route")
|
||||
err = k8sClient.Get(ctx, typeNamespacedName, route)
|
||||
if err != nil && errors.IsNotFound(err) {
|
||||
resource := &gomaprojv1beta1.Route{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: resourceName,
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: gomaprojv1beta1.RouteSpec{
|
||||
Gateway: resourceName,
|
||||
Routes: []gomaprojv1beta1.RouteConfig{
|
||||
{
|
||||
Path: "/",
|
||||
Name: resourceName,
|
||||
Rewrite: "/",
|
||||
Destination: "https://example.com",
|
||||
Methods: []string{"GET", "POST"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
Expect(k8sClient.Create(ctx, resource)).To(Succeed())
|
||||
}
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
// TODO(user): Cleanup logic after each test, like removing the resource instance.
|
||||
resource := &gomaprojv1beta1.Route{}
|
||||
err := k8sClient.Get(ctx, typeNamespacedName, resource)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
By("Cleanup the specific resource instance Route")
|
||||
Expect(k8sClient.Delete(ctx, resource)).To(Succeed())
|
||||
})
|
||||
It("should successfully reconcile the resource", func() {
|
||||
By("Reconciling the created resource")
|
||||
controllerReconciler := &RouteReconciler{
|
||||
Client: k8sClient,
|
||||
Scheme: k8sClient.Scheme(),
|
||||
}
|
||||
|
||||
_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{
|
||||
NamespacedName: typeNamespacedName,
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
// TODO(user): Add more specific assertions depending on your controller's reconciliation logic.
|
||||
// Example: If you expect a certain status condition after reconciliation, verify it here.
|
||||
})
|
||||
})
|
||||
})
|
||||
96
internal/controller/suite_test.go
Normal file
96
internal/controller/suite_test.go
Normal file
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
Copyright 2024.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
"k8s.io/client-go/kubernetes/scheme"
|
||||
"k8s.io/client-go/rest"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/envtest"
|
||||
logf "sigs.k8s.io/controller-runtime/pkg/log"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log/zap"
|
||||
|
||||
gomaprojv1beta1 "github.com/jkaninda/goma-operator/api/v1beta1"
|
||||
// +kubebuilder:scaffold:imports
|
||||
)
|
||||
|
||||
// These tests use Ginkgo (BDD-style Go testing framework). Refer to
|
||||
// http://onsi.github.io/ginkgo/ to learn more about Ginkgo.
|
||||
|
||||
var cfg *rest.Config
|
||||
var k8sClient client.Client
|
||||
var testEnv *envtest.Environment
|
||||
var ctx context.Context
|
||||
var cancel context.CancelFunc
|
||||
|
||||
func TestControllers(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
|
||||
RunSpecs(t, "Controller Suite")
|
||||
}
|
||||
|
||||
var _ = BeforeSuite(func() {
|
||||
logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true)))
|
||||
|
||||
ctx, cancel = context.WithCancel(context.TODO())
|
||||
|
||||
By("bootstrapping test environment")
|
||||
testEnv = &envtest.Environment{
|
||||
CRDDirectoryPaths: []string{filepath.Join("..", "..", "config", "crd", "bases")},
|
||||
ErrorIfCRDPathMissing: true,
|
||||
|
||||
// The BinaryAssetsDirectory is only required if you want to run the tests directly
|
||||
// without call the makefile target test. If not informed it will look for the
|
||||
// default path defined in controller-runtime which is /usr/local/kubebuilder/.
|
||||
// Note that you must have the required binaries setup under the bin directory to perform
|
||||
// the tests directly. When we run make test it will be setup and used automatically.
|
||||
BinaryAssetsDirectory: filepath.Join("..", "..", "bin", "k8s",
|
||||
fmt.Sprintf("1.31.0-%s-%s", runtime.GOOS, runtime.GOARCH)),
|
||||
}
|
||||
|
||||
var err error
|
||||
// cfg is defined in this file globally.
|
||||
cfg, err = testEnv.Start()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(cfg).NotTo(BeNil())
|
||||
|
||||
err = gomaprojv1beta1.AddToScheme(scheme.Scheme)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// +kubebuilder:scaffold:scheme
|
||||
|
||||
k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(k8sClient).NotTo(BeNil())
|
||||
|
||||
})
|
||||
|
||||
var _ = AfterSuite(func() {
|
||||
By("tearing down the test environment")
|
||||
cancel()
|
||||
err := testEnv.Stop()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
})
|
||||
71
internal/controller/svc.go
Normal file
71
internal/controller/svc.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
gomaprojv1beta1 "github.com/jkaninda/goma-operator/api/v1beta1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log"
|
||||
)
|
||||
|
||||
// createService create K8s service
|
||||
func createService(r GatewayReconciler, ctx context.Context, req ctrl.Request, gateway *gomaprojv1beta1.Gateway) error {
|
||||
l := log.FromContext(ctx)
|
||||
k8sService := &corev1.Service{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: req.Name,
|
||||
Namespace: req.Namespace,
|
||||
Labels: map[string]string{
|
||||
"app": req.Name,
|
||||
"belongs-to": BelongsTo,
|
||||
"managed-by": req.Name,
|
||||
},
|
||||
},
|
||||
Spec: corev1.ServiceSpec{
|
||||
Selector: map[string]string{
|
||||
"app": req.Name,
|
||||
},
|
||||
Ports: []corev1.ServicePort{
|
||||
{
|
||||
Name: "http",
|
||||
Protocol: corev1.ProtocolTCP,
|
||||
Port: 8080,
|
||||
TargetPort: intstr.IntOrString{Type: intstr.Int, IntVal: 8080},
|
||||
}, {
|
||||
Name: "https",
|
||||
Protocol: corev1.ProtocolTCP,
|
||||
Port: 8443,
|
||||
TargetPort: intstr.IntOrString{Type: intstr.Int, IntVal: 8443},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Set Gateway instance as the owner and controller
|
||||
if err := controllerutil.SetControllerReference(gateway, k8sService, r.Scheme); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
found := &corev1.Service{}
|
||||
err := r.Get(ctx, client.ObjectKey{Namespace: req.Namespace, Name: req.Name}, found)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "not found") {
|
||||
l.Info("Creating a new Service", "Service.Namespace", k8sService.Namespace, "Service.Name", k8sService.Name)
|
||||
err = r.Create(ctx, k8sService)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
l.Info("Failed to get Service", "Service.Namespace", k8sService.Namespace, "Service.Name", k8sService.Name)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
105
internal/controller/types.go
Normal file
105
internal/controller/types.go
Normal file
@@ -0,0 +1,105 @@
|
||||
package controller
|
||||
|
||||
import gomaprojv1beta1 "github.com/jkaninda/goma-operator/api/v1beta1"
|
||||
|
||||
// Gateway contains Goma Proxy Gateway's configs
|
||||
type Gateway struct {
|
||||
// SSLCertFile SSL Certificate file
|
||||
SSLCertFile string `yaml:"sslCertFile"`
|
||||
// SSLKeyFile SSL Private key file
|
||||
SSLKeyFile string `yaml:"sslKeyFile"`
|
||||
// Redis contains redis database details
|
||||
Redis Redis `yaml:"redis"`
|
||||
// WriteTimeout defines proxy write timeout
|
||||
WriteTimeout int `yaml:"writeTimeout"`
|
||||
// ReadTimeout defines proxy read timeout
|
||||
ReadTimeout int `yaml:"readTimeout"`
|
||||
// IdleTimeout defines proxy idle timeout
|
||||
IdleTimeout int `yaml:"idleTimeout"`
|
||||
LogLevel string `yaml:"logLevel"`
|
||||
Cors gomaprojv1beta1.Cors `yaml:"cors"`
|
||||
// DisableHealthCheckStatus enable and disable routes health check
|
||||
DisableHealthCheckStatus bool `yaml:"disableHealthCheckStatus"`
|
||||
// DisableRouteHealthCheckError allows enabling and disabling backend healthcheck errors
|
||||
DisableRouteHealthCheckError bool `yaml:"disableRouteHealthCheckError"`
|
||||
// Disable allows enabling and disabling displaying routes on start
|
||||
DisableDisplayRouteOnStart bool `yaml:"disableDisplayRouteOnStart"`
|
||||
// DisableKeepAlive allows enabling and disabling KeepALive server
|
||||
DisableKeepAlive bool `yaml:"disableKeepAlive"`
|
||||
EnableMetrics bool `yaml:"enableMetrics"`
|
||||
// InterceptErrors holds the status codes to intercept the error from backend
|
||||
InterceptErrors []int `yaml:"interceptErrors,omitempty"`
|
||||
Routes []gomaprojv1beta1.RouteConfig `json:"routes,omitempty" yaml:"routes,omitempty"`
|
||||
}
|
||||
|
||||
type Redis struct {
|
||||
// Addr redis hostname and port number :
|
||||
Addr string `yaml:"addr"`
|
||||
// Password redis password
|
||||
Password string `yaml:"password"`
|
||||
}
|
||||
type Middleware struct {
|
||||
// Path contains the name of middlewares and must be unique
|
||||
Name string `json:"name" yaml:"name"`
|
||||
// Type contains authentication types
|
||||
//
|
||||
// basic, jwt, auth0, rateLimit, access
|
||||
Type string `json:"type" yaml:"type"` // Middleware type [basic, jwt, auth0, rateLimit, access]
|
||||
Paths []string `json:"paths" yaml:"paths"` // Protected paths
|
||||
// Rule contains route middleware rule
|
||||
Rule interface{} `json:"rule" yaml:"rule"`
|
||||
}
|
||||
|
||||
type Middlewares struct {
|
||||
Middlewares []Middleware `json:"middlewares,omitempty" yaml:"middlewares,omitempty"`
|
||||
}
|
||||
|
||||
type GatewayConfig struct {
|
||||
Version string `json:"version" yaml:"version"`
|
||||
Gateway Gateway `json:"gateway" yaml:"gateway"`
|
||||
Middlewares []Middleware `json:"middlewares,omitempty" yaml:"middlewares,omitempty"`
|
||||
}
|
||||
type BasicRuleMiddleware struct {
|
||||
Username string `yaml:"username" json:"username"`
|
||||
Password string `yaml:"password" json:"password"`
|
||||
}
|
||||
type JWTRuleMiddleware struct {
|
||||
URL string `yaml:"url" json:"url"`
|
||||
RequiredHeaders []string `yaml:"requiredHeaders" json:"requiredHeaders"`
|
||||
Headers map[string]string `yaml:"headers" json:"headers"`
|
||||
Params map[string]string `yaml:"params" json:"params"`
|
||||
}
|
||||
type RateLimitRuleMiddleware struct {
|
||||
Unit string `yaml:"unit" json:"unit"`
|
||||
RequestsPerUnit int `yaml:"requestsPerUnit" json:"requestsPerUnit"`
|
||||
}
|
||||
type OauthRulerMiddleware struct {
|
||||
// ClientID is the application's ID.
|
||||
ClientID string `yaml:"clientId"`
|
||||
|
||||
// ClientSecret is the application's secret.
|
||||
ClientSecret string `yaml:"clientSecret"`
|
||||
// oauth provider google, gitlab, github, amazon, facebook, custom
|
||||
Provider string `yaml:"provider"`
|
||||
// Endpoint contains the resource server's token endpoint
|
||||
Endpoint OauthEndpoint `yaml:"endpoint"`
|
||||
|
||||
// RedirectURL is the URL to redirect users going through
|
||||
// the OAuth flow, after the resource owner's URLs.
|
||||
RedirectURL string `yaml:"redirectUrl"`
|
||||
// RedirectPath is the PATH to redirect users after authentication, e.g: /my-protected-path/dashboard
|
||||
RedirectPath string `yaml:"redirectPath"`
|
||||
// CookiePath e.g: /my-protected-path or / || by default is applied on a route path
|
||||
CookiePath string `yaml:"cookiePath"`
|
||||
|
||||
// Scope specifies optional requested permissions.
|
||||
Scopes []string `yaml:"scopes"`
|
||||
// contains filtered or unexported fields
|
||||
State string `yaml:"state"`
|
||||
JWTSecret string `yaml:"jwtSecret"`
|
||||
}
|
||||
type OauthEndpoint struct {
|
||||
AuthURL string `yaml:"authUrl"`
|
||||
TokenURL string `yaml:"tokenUrl"`
|
||||
UserInfoURL string `yaml:"userInfoUrl"`
|
||||
}
|
||||
22
internal/controller/util.go
Normal file
22
internal/controller/util.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package controller
|
||||
|
||||
import gomaprojv1beta1 "github.com/jkaninda/goma-operator/api/v1beta1"
|
||||
|
||||
func mapToGateway(g gomaprojv1beta1.GatewaySpec) Gateway {
|
||||
return Gateway{
|
||||
SSLKeyFile: "",
|
||||
SSLCertFile: "",
|
||||
Redis: Redis{},
|
||||
WriteTimeout: g.Server.WriteTimeout,
|
||||
ReadTimeout: g.Server.ReadTimeout,
|
||||
IdleTimeout: g.Server.IdleTimeout,
|
||||
LogLevel: g.Server.LogLevel,
|
||||
Cors: g.Server.Cors,
|
||||
DisableHealthCheckStatus: g.Server.DisableHealthCheckStatus,
|
||||
DisableRouteHealthCheckError: g.Server.DisableHealthCheckStatus,
|
||||
DisableKeepAlive: g.Server.DisableKeepAlive,
|
||||
InterceptErrors: g.Server.InterceptErrors,
|
||||
EnableMetrics: g.Server.EnableMetrics,
|
||||
}
|
||||
|
||||
}
|
||||
19
internal/controller/var.go
Normal file
19
internal/controller/var.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package controller
|
||||
|
||||
const (
|
||||
AppImageName = "jkaninda/goma-gateway"
|
||||
ExtraConfigPath = "/etc/goma/extra/"
|
||||
BasicAuth = "basic" // basic authentication middlewares
|
||||
JWTAuth = "jwt" // JWT authentication middlewares
|
||||
OAuth = "oauth"
|
||||
ratelimit = "ratelimit"
|
||||
RateLimit = "rateLimit"
|
||||
BelongsTo = "goma-gateway"
|
||||
GatewayConfigVersion = "1.0"
|
||||
FinalizerName = "finalizer.gomaproj.jonaskaninda.com"
|
||||
ConfigName = "goma.yml"
|
||||
)
|
||||
|
||||
var (
|
||||
ReplicaCount int32 = 1
|
||||
)
|
||||
Reference in New Issue
Block a user