Initial commit
Some checks failed
Tests / Run on Ubuntu (push) Has been cancelled
E2E Tests / Run on Ubuntu (push) Has been cancelled
Lint / Run on Ubuntu (push) Failing after 23m29s

This commit is contained in:
Jonas Kaninda
2024-11-27 08:29:51 +01:00
commit 59e2f0164b
73 changed files with 8334 additions and 0 deletions

View 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
}

View 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)
}

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

View 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
View 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
}

View 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)
}

View 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.
})
})
})

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

View 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
}

View 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"`
}

View 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,
}
}

View 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
)