blob: 6d1a8e74e2d3eda5b92edeb2f72c1fe0fb796227 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 eventmesh_runtime
import (
"context"
"fmt"
eventmeshoperatorv1 "github.com/apache/eventmesh/eventmesh-operator/api/v1"
"github.com/apache/eventmesh/eventmesh-operator/share"
"github.com/go-logr/logr"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/intstr"
"reflect"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
"sigs.k8s.io/controller-runtime/pkg/handler"
"sigs.k8s.io/controller-runtime/pkg/manager"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"sigs.k8s.io/controller-runtime/pkg/source"
"strconv"
"time"
)
// RuntimeReconciler reconciles a Runtime object
type RuntimeReconciler struct {
Client client.Client
Scheme *runtime.Scheme
Logger logr.Logger
}
// SetupWithManager creates a new Runtime Controller and adds it to the Manager. The Manager will set fields on the Controller
// and Start it when the Manager is Started.
func SetupWithManager(mgr manager.Manager) error {
return add(mgr, newReconciler(mgr))
}
// newReconciler returns a new reconcile.Reconciler
func newReconciler(mgr manager.Manager) reconcile.Reconciler {
return &RuntimeReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
Logger: mgr.GetLogger().WithName("runtime"),
}
}
// add adds a new Controller to mgr with r as the reconcile.Reconciler
func add(mgr manager.Manager, r reconcile.Reconciler) error {
// Create a new controller
c, err := controller.New("runtime-controller", mgr, controller.Options{Reconciler: r})
if err != nil {
return err
}
// Watch for changes to primary resource runtime
err = c.Watch(&source.Kind{Type: &eventmeshoperatorv1.Runtime{}}, &handler.EnqueueRequestForObject{})
if err != nil {
return err
}
// Watch for changes to secondary resource Pods and requeue the owner runtime
err = c.Watch(&source.Kind{Type: &corev1.Pod{}}, &handler.EnqueueRequestForOwner{
IsController: true,
OwnerType: &eventmeshoperatorv1.Runtime{},
})
if err != nil {
return err
}
return nil
}
//+kubebuilder:rbac:groups=eventmesh-operator.eventmesh,resources=runtimes,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=eventmesh-operator.eventmesh,resources=runtimes/status,verbs=get;update;patch
//+kubebuilder:rbac:groups=eventmesh-operator.eventmesh,resources=runtimes/finalizers,verbs=update
//+kubebuilder:rbac:groups="",resources=pods,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups="apps",resources=statefulsets,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="",resources=services,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.
func (r *RuntimeReconciler) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) {
r.Logger.Info("eventMeshRuntime start reconciling",
"Namespace", req.Namespace, "Name", req.Name)
eventMeshRuntime := &eventmeshoperatorv1.Runtime{}
err := r.Client.Get(context.TODO(), req.NamespacedName, eventMeshRuntime)
if err != nil {
if errors.IsNotFound(err) {
r.Logger.Info("eventMeshRuntime resource not found. Ignoring since object must be deleted.")
return reconcile.Result{}, nil
}
r.Logger.Error(err, "Failed to get eventMeshRuntime")
return reconcile.Result{}, err
}
r.Logger.Info("get eventMeshRuntime object", "name", eventMeshRuntime.Name)
var groupNum int
if eventMeshRuntime.Status.Size == 0 {
groupNum = eventMeshRuntime.Spec.Size
} else {
groupNum = eventMeshRuntime.Status.Size
}
replicaPerGroup := eventMeshRuntime.Spec.ReplicaPerGroup
r.Logger.Info("GroupNum=" + strconv.Itoa(groupNum) + ", replicaPerGroup=" + strconv.Itoa(replicaPerGroup))
for groupIndex := 0; groupIndex < groupNum; groupIndex++ {
r.Logger.Info("Check eventMeshRuntime cluster " + strconv.Itoa(groupIndex+1) + "/" + strconv.Itoa(groupNum))
// 1. Reconcile Service
service := r.getEventMeshRuntimeService(eventMeshRuntime, groupIndex)
foundService := &corev1.Service{}
err = r.Client.Get(context.TODO(), types.NamespacedName{Name: service.Name, Namespace: service.Namespace}, foundService)
if err != nil && errors.IsNotFound(err) {
r.Logger.Info("Creating a new eventMeshRuntime Service.", "Service.Namespace", service.Namespace, "Service.Name", service.Name)
err = r.Client.Create(context.TODO(), service)
if err != nil {
r.Logger.Error(err, "Failed to create new Service", "Service.Namespace", service.Namespace, "Service.Name", service.Name)
return reconcile.Result{}, err
}
} else if err != nil {
r.Logger.Error(err, "Failed to get eventMeshRuntime Service.")
return reconcile.Result{}, err
}
// 2. Reconcile StatefulSet
runtimeSts := r.getEventMeshRuntimeStatefulSet(eventMeshRuntime, groupIndex)
foundSts := &appsv1.StatefulSet{}
err = r.Client.Get(context.TODO(), types.NamespacedName{
Name: runtimeSts.Name,
Namespace: runtimeSts.Namespace,
}, foundSts)
if err != nil && errors.IsNotFound(err) {
r.Logger.Info("Creating a new eventMeshRuntime StatefulSet.",
"StatefulSet.Namespace", runtimeSts.Namespace,
"StatefulSet.Name", runtimeSts.Name)
err = r.Client.Create(context.TODO(), runtimeSts)
if err != nil {
r.Logger.Error(err, "Failed to create new StatefulSet",
"StatefulSet.Namespace", runtimeSts.Namespace,
"StatefulSet.Name", runtimeSts.Name)
return reconcile.Result{}, err
}
} else if err != nil {
r.Logger.Error(err, "Failed to get eventMeshRuntime StatefulSet.")
return reconcile.Result{}, err
} else {
// Update if needed
if eventMeshRuntime.Spec.AllowRestart {
// Simple update logic: overwrite spec
r.Logger.Info("Updating eventMeshRuntime StatefulSet", "Name", foundSts.Name)
runtimeSts.ResourceVersion = foundSts.ResourceVersion
err = r.Client.Update(context.TODO(), runtimeSts)
if err != nil {
r.Logger.Error(err, "Failed to update eventMeshRuntime StatefulSet", "Name", foundSts.Name)
return reconcile.Result{}, err
}
}
}
}
podList := &corev1.PodList{}
labelSelector := labels.SelectorFromSet(getLabels(eventMeshRuntime.Name))
listOps := &client.ListOptions{
Namespace: eventMeshRuntime.Namespace,
LabelSelector: labelSelector,
}
err = r.Client.List(context.TODO(), podList, listOps)
if err != nil {
r.Logger.Error(err, "Failed to list pods.",
"eventMeshRuntime.Namespace", eventMeshRuntime.Namespace, "eventMeshRuntime.Name", eventMeshRuntime.Name)
return reconcile.Result{}, err
}
podNames := getRuntimePodNames(podList.Items)
// Update Status
var needsUpdate bool
if eventMeshRuntime.Spec.Size != eventMeshRuntime.Status.Size {
eventMeshRuntime.Status.Size = eventMeshRuntime.Spec.Size
needsUpdate = true
}
if !reflect.DeepEqual(podNames, eventMeshRuntime.Status.Nodes) {
eventMeshRuntime.Status.Nodes = podNames
needsUpdate = true
}
if needsUpdate {
r.Logger.Info("Updating eventMeshRuntime status")
err = r.Client.Status().Update(context.TODO(), eventMeshRuntime)
if err != nil {
r.Logger.Error(err, "Failed to update eventMeshRuntime status.")
return reconcile.Result{}, err
}
}
// Update global state
runningEventMeshRuntimeNum := getRunningRuntimeNum(podList.Items)
// We check if total running pods match expected total replicas
totalExpectedReplicas := groupNum * replicaPerGroup
if runningEventMeshRuntimeNum == totalExpectedReplicas {
// share.IsEventMeshRuntimeInitialized = true (Removed as per refactor)
}
r.Logger.Info("Successful reconciliation!")
return reconcile.Result{RequeueAfter: time.Duration(share.RequeueAfterSecond) * time.Second}, nil
}
func getRunningRuntimeNum(pods []corev1.Pod) int {
var num = 0
for _, pod := range pods {
if reflect.DeepEqual(pod.Status.Phase, corev1.PodRunning) {
num++
}
}
return num
}
func getRuntimePodNames(pods []corev1.Pod) []string {
var podNames []string
for _, pod := range pods {
podNames = append(podNames, pod.Name)
}
return podNames
}
func (r *RuntimeReconciler) getEventMeshRuntimeStatefulSet(runtime *eventmeshoperatorv1.Runtime, groupIndex int) *appsv1.StatefulSet {
// Naming: <runtimeName>-<groupIndex>
statefulSetName := fmt.Sprintf("%s-%d", runtime.Name, groupIndex)
serviceName := fmt.Sprintf("%s-%d-headless", runtime.Name, groupIndex)
replicas := int32(runtime.Spec.ReplicaPerGroup)
label := getLabels(runtime.Name)
deployment := &appsv1.StatefulSet{
ObjectMeta: metav1.ObjectMeta{
Name: statefulSetName,
Namespace: runtime.Namespace,
Labels: label,
},
Spec: appsv1.StatefulSetSpec{
ServiceName: serviceName,
Replicas: &replicas,
Selector: &metav1.LabelSelector{
MatchLabels: label,
},
UpdateStrategy: appsv1.StatefulSetUpdateStrategy{
Type: appsv1.RollingUpdateStatefulSetStrategyType,
},
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: label,
},
Spec: corev1.PodSpec{
DNSPolicy: runtime.Spec.RuntimePodTemplate.Template.Spec.DNSPolicy,
Affinity: runtime.Spec.RuntimePodTemplate.Template.Spec.Affinity,
Tolerations: runtime.Spec.RuntimePodTemplate.Template.Spec.Tolerations,
NodeSelector: runtime.Spec.RuntimePodTemplate.Template.Spec.NodeSelector,
PriorityClassName: runtime.Spec.RuntimePodTemplate.Template.Spec.PriorityClassName,
HostNetwork: runtime.Spec.RuntimePodTemplate.Template.Spec.HostNetwork,
Containers: runtime.Spec.RuntimePodTemplate.Template.Spec.Containers, // Use all containers
Volumes: runtime.Spec.RuntimePodTemplate.Template.Spec.Volumes,
SecurityContext: getRuntimePodSecurityContext(runtime),
},
},
},
}
// Manually set security context for the first container if not set, for backward compatibility or strict override
if len(deployment.Spec.Template.Spec.Containers) > 0 {
if deployment.Spec.Template.Spec.Containers[0].SecurityContext == nil {
deployment.Spec.Template.Spec.Containers[0].SecurityContext = getContainerSecurityContext(runtime)
}
}
_ = controllerutil.SetControllerReference(runtime, deployment, r.Scheme)
return deployment
}
func (r *RuntimeReconciler) getEventMeshRuntimeService(runtime *eventmeshoperatorv1.Runtime, groupIndex int) *corev1.Service {
serviceName := fmt.Sprintf("%s-%d-headless", runtime.Name, groupIndex)
label := getLabels(runtime.Name)
var ports []corev1.ServicePort
// Extract ports from the first container
if len(runtime.Spec.RuntimePodTemplate.Template.Spec.Containers) > 0 {
for _, port := range runtime.Spec.RuntimePodTemplate.Template.Spec.Containers[0].Ports {
ports = append(ports, corev1.ServicePort{
Name: port.Name,
Port: port.ContainerPort,
TargetPort: intstr.FromInt(int(port.ContainerPort)),
})
}
}
// Fallback if no ports defined, though ideally CR should have them
if len(ports) == 0 {
ports = append(ports, corev1.ServicePort{
Name: "grpc",
Port: 10000,
TargetPort: intstr.FromInt(10000),
})
}
svc := &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: serviceName,
Namespace: runtime.Namespace,
Labels: label,
},
Spec: corev1.ServiceSpec{
ClusterIP: "None", // Headless Service
Selector: label,
Ports: ports,
},
}
_ = controllerutil.SetControllerReference(runtime, svc, r.Scheme)
return svc
}
func getRuntimePodSecurityContext(runtime *eventmeshoperatorv1.Runtime) *corev1.PodSecurityContext {
var securityContext = corev1.PodSecurityContext{}
if runtime.Spec.PodSecurityContext != nil {
securityContext = *runtime.Spec.PodSecurityContext
}
return &securityContext
}
func getContainerSecurityContext(runtime *eventmeshoperatorv1.Runtime) *corev1.SecurityContext {
var securityContext = corev1.SecurityContext{}
if runtime.Spec.ContainerSecurityContext != nil {
securityContext = *runtime.Spec.ContainerSecurityContext
}
return &securityContext
}
func getLabels(name string) map[string]string {
return map[string]string{
"app": "eventmesh-runtime",
"instance": name,
}
}