Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 5 additions & 8 deletions config/tests/base/kustomization.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,10 @@ apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

helmCharts:
- repo: oci://registry-1.docker.io/bitnamicharts
name: redis
version: 20.5.0
releaseName: redis
valuesInline:
auth:
enabled: false
- repo: oci://ghcr.io/dragonflydb/dragonfly/helm
name: dragonfly
version: v1.37.0
releaseName: dragonfly

images:
- name: ghcr.io/doodlescheduling/ratelimit-controller
Expand All @@ -17,4 +14,4 @@ images:

resources:
- ../../default
- verify-pod.yaml
- verify-pod.yaml
2 changes: 1 addition & 1 deletion config/tests/base/verify-pod.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,4 @@ spec:
runAsNonRoot: true
runAsUser: 1000
terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File
terminationMessagePolicy: File
4 changes: 2 additions & 2 deletions config/tests/cases/default/ratelimitservice.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ spec:
- name: REDIS_SOCKET_TYPE
value: tcp
- name: REDIS_URL
value: redis-master:6379
value: dragonfly:6379
- name: REDIS_TYPE
value: single
- name: REDIS_TLS
value: "false"
value: "false"
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ go 1.25.0
require (
github.com/fluxcd/pkg/runtime v0.80.0
github.com/go-logr/logr v1.4.3
github.com/goccy/go-yaml v1.19.2
github.com/kylelemons/godebug v1.1.0
github.com/onsi/ginkgo/v2 v2.25.3
github.com/onsi/gomega v1.38.2
github.com/spf13/pflag v1.0.10
github.com/stretchr/testify v1.11.1
gopkg.in/yaml.v3 v3.0.1
k8s.io/api v0.34.1
k8s.io/apimachinery v0.34.1
k8s.io/client-go v0.34.1
Expand Down Expand Up @@ -90,6 +90,7 @@ require (
google.golang.org/protobuf v1.36.7 // indirect
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/apiextensions-apiserver v0.34.0 // indirect
k8s.io/cli-runtime v0.33.2 // indirect
k8s.io/component-base v0.34.0 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZ
github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
Expand Down
190 changes: 61 additions & 129 deletions internal/controllers/ratelimitservice_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,9 @@ import (
"crypto/sha256"
"fmt"
"slices"
"strings"

"github.com/go-logr/logr"
"gopkg.in/yaml.v3"
"github.com/goccy/go-yaml"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
Expand Down Expand Up @@ -318,7 +317,6 @@ func (r *RateLimitServiceReconciler) reconcile(ctx context.Context, service infr
gid int64 = 10000
uid int64 = 10000
runAsNonRoot = true
replicas int32 = 1
controllerOwner = true
labels = map[string]string{
"app.kubernetes.io/instance": "ratelimit",
Expand Down Expand Up @@ -363,33 +361,11 @@ func (r *RateLimitServiceReconciler) reconcile(ctx context.Context, service infr
}

checksum := fmt.Sprintf("%x", checksumSha.Sum(nil))

var cm corev1.ConfigMap
err = r.Get(ctx, client.ObjectKey{
Namespace: cmTemplate.Namespace,
Name: cmTemplate.Name,
}, &cm)

if err != nil && !apierrors.IsNotFound(err) {
if err := r.createOrUpdateWithOwnershipValidation(ctx, &service, cmTemplate); err != nil {
return service, ctrl.Result{}, err
}

if apierrors.IsNotFound(err) {
if err := r.Create(ctx, cmTemplate); err != nil {
return service, ctrl.Result{}, err
}
} else {
if !isOwner(&service, &cm) {
return service, ctrl.Result{}, fmt.Errorf("can not take ownership of existing configmap: %s", cm.Name)
}

mergeMetadata(&cmTemplate.ObjectMeta, cm.ObjectMeta)
if err := r.Update(ctx, cmTemplate); err != nil {
return service, ctrl.Result{}, err
}
}

template := &appsv1.Deployment{
deploymentTemplate := &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: fmt.Sprintf("ratelimit-%s", service.Name),
Namespace: service.Namespace,
Expand Down Expand Up @@ -417,49 +393,44 @@ func (r *RateLimitServiceReconciler) reconcile(ctx context.Context, service infr
}

if service.Spec.DeploymentTemplate != nil {
template.Labels = service.Spec.DeploymentTemplate.Labels
template.Annotations = service.Spec.DeploymentTemplate.Annotations
service.Spec.DeploymentTemplate.Spec.Template.DeepCopyInto(&template.Spec.Template)
template.Spec.MinReadySeconds = service.Spec.DeploymentTemplate.Spec.MinReadySeconds
template.Spec.Paused = service.Spec.DeploymentTemplate.Spec.Paused
template.Spec.ProgressDeadlineSeconds = service.Spec.DeploymentTemplate.Spec.ProgressDeadlineSeconds
template.Spec.Replicas = service.Spec.DeploymentTemplate.Spec.Replicas
template.Spec.RevisionHistoryLimit = service.Spec.DeploymentTemplate.Spec.RevisionHistoryLimit
template.Spec.Strategy = service.Spec.DeploymentTemplate.Spec.Strategy
deploymentTemplate.Labels = service.Spec.DeploymentTemplate.Labels
deploymentTemplate.Annotations = service.Spec.DeploymentTemplate.Annotations
service.Spec.DeploymentTemplate.Spec.Template.DeepCopyInto(&deploymentTemplate.Spec.Template)
deploymentTemplate.Spec.MinReadySeconds = service.Spec.DeploymentTemplate.Spec.MinReadySeconds
deploymentTemplate.Spec.Paused = service.Spec.DeploymentTemplate.Spec.Paused
deploymentTemplate.Spec.ProgressDeadlineSeconds = service.Spec.DeploymentTemplate.Spec.ProgressDeadlineSeconds
deploymentTemplate.Spec.Replicas = service.Spec.DeploymentTemplate.Spec.Replicas
deploymentTemplate.Spec.RevisionHistoryLimit = service.Spec.DeploymentTemplate.Spec.RevisionHistoryLimit
deploymentTemplate.Spec.Strategy = service.Spec.DeploymentTemplate.Spec.Strategy
}

if template.Labels == nil {
template.Labels = make(map[string]string)
if deploymentTemplate.Labels == nil {
deploymentTemplate.Labels = make(map[string]string)
}

if template.Spec.Template.Labels == nil {
template.Spec.Template.Labels = make(map[string]string)
if deploymentTemplate.Spec.Template.Labels == nil {
deploymentTemplate.Spec.Template.Labels = make(map[string]string)
}

template.Spec.Selector = &metav1.LabelSelector{
MatchLabels: labels,
if deploymentTemplate.Annotations == nil {
deploymentTemplate.Annotations = make(map[string]string)
}

if template.Spec.Replicas == nil {
template.Spec.Replicas = &replicas
if deploymentTemplate.Spec.Template.Annotations == nil {
deploymentTemplate.Spec.Template.Annotations = make(map[string]string)
}

template.Spec.Template.Labels["app.kubernetes.io/instance"] = "ratelimit"
template.Spec.Template.Labels["app.kubernetes.io/name"] = "ratelimit"
template.Spec.Template.Labels["ratelimit-controller/service"] = service.Name
template.Labels["app.kubernetes.io/instance"] = "ratelimit"
template.Labels["app.kubernetes.io/name"] = "ratelimit"
template.Labels["ratelimit-controller/service"] = service.Name

if template.Annotations == nil {
template.Annotations = make(map[string]string)
}

if template.Spec.Template.Annotations == nil {
template.Spec.Template.Annotations = make(map[string]string)
deploymentTemplate.Spec.Selector = &metav1.LabelSelector{
MatchLabels: labels,
}

template.Spec.Template.Annotations["ratelimit-controller/sha256-checksum"] = checksum
deploymentTemplate.Spec.Template.Labels["app.kubernetes.io/instance"] = "ratelimit"
deploymentTemplate.Spec.Template.Labels["app.kubernetes.io/name"] = "ratelimit"
deploymentTemplate.Spec.Template.Labels["ratelimit-controller/service"] = service.Name
deploymentTemplate.Labels["app.kubernetes.io/instance"] = "ratelimit"
deploymentTemplate.Labels["app.kubernetes.io/name"] = "ratelimit"
deploymentTemplate.Labels["ratelimit-controller/service"] = service.Name
deploymentTemplate.Spec.Template.Annotations["ratelimit-controller/sha256-checksum"] = checksum

containers := []corev1.Container{
{
Expand Down Expand Up @@ -521,12 +492,12 @@ func (r *RateLimitServiceReconciler) reconcile(ctx context.Context, service infr
},
}

containers, err = merge.MergePatchContainers(containers, template.Spec.Template.Spec.Containers)
containers, err = merge.MergePatchContainers(containers, deploymentTemplate.Spec.Template.Spec.Containers)
if err != nil {
return service, ctrl.Result{}, err
}

template.Spec.Template.Spec.Volumes = append(template.Spec.Template.Spec.Volumes, corev1.Volume{
deploymentTemplate.Spec.Template.Spec.Volumes = append(deploymentTemplate.Spec.Template.Spec.Volumes, corev1.Volume{
Name: "config",
VolumeSource: corev1.VolumeSource{
ConfigMap: &corev1.ConfigMapVolumeSource{
Expand All @@ -537,7 +508,7 @@ func (r *RateLimitServiceReconciler) reconcile(ctx context.Context, service infr
},
})

template.Spec.Template.Spec.Containers = containers
deploymentTemplate.Spec.Template.Spec.Containers = containers

svcTemplate := &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Expand Down Expand Up @@ -571,92 +542,53 @@ func (r *RateLimitServiceReconciler) reconcile(ctx context.Context, service infr
},
}

var svc corev1.Service
err = r.Get(ctx, client.ObjectKey{
Namespace: svcTemplate.Namespace,
Name: svcTemplate.Name,
}, &svc)

if err != nil && !apierrors.IsNotFound(err) {
if err := r.createOrUpdateWithOwnershipValidation(ctx, &service, svcTemplate); err != nil {
return service, ctrl.Result{}, err
}

if apierrors.IsNotFound(err) {
if err := r.Create(ctx, svcTemplate); err != nil {
return service, ctrl.Result{}, err
}
} else {
if !isOwner(&service, &cm) {
return service, ctrl.Result{}, fmt.Errorf("can not take ownership of existing service: %s", svc.Name)
}

mergeMetadata(&svcTemplate.ObjectMeta, svc.ObjectMeta)
if err := r.Update(ctx, svcTemplate); err != nil {
return service, ctrl.Result{}, err
}
if err := r.createOrUpdateWithOwnershipValidation(ctx, &service, deploymentTemplate); err != nil {
return service, ctrl.Result{}, err
}

var deployment appsv1.Deployment
err = r.Get(ctx, client.ObjectKey{
Namespace: template.Namespace,
Name: template.Name,
}, &deployment)
service = infrav1beta1.RateLimitServiceReady(service, metav1.ConditionTrue, "ReconciliationSuccessful", fmt.Sprintf("deployment/%s created", deploymentTemplate.Name))
return service, ctrl.Result{}, nil
}

func (r *RateLimitServiceReconciler) createOrUpdateWithOwnershipValidation(ctx context.Context, owner client.Object, obj client.Object) error {
existing := obj.DeepCopyObject().(client.Object)
err := r.Get(ctx, client.ObjectKey{
Namespace: obj.GetNamespace(),
Name: obj.GetName(),
}, existing)

if err != nil && !apierrors.IsNotFound(err) {
return service, ctrl.Result{}, err
return err
}

if apierrors.IsNotFound(err) {
if err := r.Create(ctx, template); err != nil {
return service, ctrl.Result{}, err
if err := r.Create(ctx, obj); err != nil {
return err
}

} else {
if !isOwner(&service, &deployment) {
return service, ctrl.Result{}, fmt.Errorf("can not take ownership of existing deployment: %s", deployment.Name)
}

mergeMetadata(&template.ObjectMeta, deployment.ObjectMeta)
if err := r.Update(ctx, template); err != nil {
return service, ctrl.Result{}, err
if !isOwner(owner, existing) {
return fmt.Errorf("can not take ownership of existing resource: %s", obj.GetName())
}
}

service = infrav1beta1.RateLimitServiceReady(service, metav1.ConditionTrue, "ReconciliationSuccessful", fmt.Sprintf("deployment/%s created", template.Name))
return service, ctrl.Result{}, nil
}

// mergeMetadata takes labels and annotations from the old resource and merges
// them into the new resource. If a key is present in both resources, the new
// resource wins. It also copies the ResourceVersion from the old resource to
// the new resource to prevent update conflicts.
func mergeMetadata(new *metav1.ObjectMeta, old metav1.ObjectMeta) {
new.ResourceVersion = old.ResourceVersion

new.SetLabels(mergeMaps(new.Labels, old.Labels))
new.SetAnnotations(mergeMaps(new.Annotations, old.Annotations))
}

func mergeMaps(new map[string]string, old map[string]string) map[string]string {
return mergeMapsByPrefix(new, old, "")
}

func mergeMapsByPrefix(from map[string]string, to map[string]string, prefix string) map[string]string {
if to == nil {
to = make(map[string]string)
}

if from == nil {
from = make(map[string]string)
}
obj.GetObjectKind().SetGroupVersionKind(existing.GetObjectKind().GroupVersionKind())
err := r.Patch(
ctx,
obj,
client.Apply,
client.FieldOwner("ratelimit-controller"),
client.ForceOwnership,
)

for k, v := range from {
if strings.HasPrefix(k, prefix) {
to[k] = v
if err != nil {
return fmt.Errorf("can not patch resource: %w", err)
}
}

return to
return nil
}

func (r *RateLimitServiceReconciler) extendserviceWithRateLimitRules(ctx context.Context, service infrav1beta1.RateLimitService) (infrav1beta1.RateLimitService, []infrav1beta1.RateLimitRule, error) {
Expand Down
Loading
Loading