agones.dev/agones@v1.53.0/cmd/processor/main.go (about)

     1  // Copyright 2025 Google LLC All Rights Reserved.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  // Processor
    16  package main
    17  
    18  import (
    19  	"context"
    20  	"os"
    21  	"strings"
    22  	"time"
    23  
    24  	"agones.dev/agones/pkg"
    25  	"agones.dev/agones/pkg/util/httpserver"
    26  	"agones.dev/agones/pkg/util/runtime"
    27  	"agones.dev/agones/pkg/util/signals"
    28  
    29  	"github.com/google/uuid"
    30  	"github.com/heptiolabs/healthcheck"
    31  	"github.com/sirupsen/logrus"
    32  	"github.com/spf13/pflag"
    33  	"github.com/spf13/viper"
    34  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    35  	"k8s.io/client-go/kubernetes"
    36  	"k8s.io/client-go/rest"
    37  	leaderelection "k8s.io/client-go/tools/leaderelection"
    38  	"k8s.io/client-go/tools/leaderelection/resourcelock"
    39  )
    40  
    41  const (
    42  	logLevelFlag       = "log-level"
    43  	leaderElectionFlag = "leader-election"
    44  	podNamespace       = "pod-namespace"
    45  	leaseDurationFlag  = "lease-duration"
    46  	renewDeadlineFlag  = "renew-deadline"
    47  	retryPeriodFlag    = "retry-period"
    48  )
    49  
    50  var (
    51  	logger = runtime.NewLoggerWithSource("main")
    52  )
    53  
    54  type processorConfig struct {
    55  	LogLevel       string
    56  	LeaderElection bool
    57  	PodNamespace   string
    58  	LeaseDuration  time.Duration
    59  	RenewDeadline  time.Duration
    60  	RetryPeriod    time.Duration
    61  }
    62  
    63  func parseEnvFlags() processorConfig {
    64  	viper.SetDefault(logLevelFlag, "Info")
    65  	viper.SetDefault(leaderElectionFlag, false)
    66  	viper.SetDefault(podNamespace, "")
    67  	viper.SetDefault(leaseDurationFlag, 15*time.Second)
    68  	viper.SetDefault(renewDeadlineFlag, 10*time.Second)
    69  	viper.SetDefault(retryPeriodFlag, 2*time.Second)
    70  
    71  	pflag.String(logLevelFlag, viper.GetString(logLevelFlag), "Log level")
    72  	pflag.Bool(leaderElectionFlag, viper.GetBool(leaderElectionFlag), "Enable leader election")
    73  	pflag.String(podNamespace, viper.GetString(podNamespace), "Pod namespace")
    74  	pflag.Duration(leaseDurationFlag, viper.GetDuration(leaseDurationFlag), "Leader election lease duration")
    75  	pflag.Duration(renewDeadlineFlag, viper.GetDuration(renewDeadlineFlag), "Leader election renew deadline")
    76  	pflag.Duration(retryPeriodFlag, viper.GetDuration(retryPeriodFlag), "Leader election retry period")
    77  	pflag.Parse()
    78  
    79  	viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_"))
    80  	viper.AutomaticEnv()
    81  	_ = viper.BindPFlags(pflag.CommandLine)
    82  
    83  	return processorConfig{
    84  		LogLevel:       viper.GetString(logLevelFlag),
    85  		LeaderElection: viper.GetBool(leaderElectionFlag),
    86  		PodNamespace:   viper.GetString(podNamespace),
    87  		LeaseDuration:  viper.GetDuration(leaseDurationFlag),
    88  		RenewDeadline:  viper.GetDuration(renewDeadlineFlag),
    89  		RetryPeriod:    viper.GetDuration(retryPeriodFlag),
    90  	}
    91  }
    92  
    93  func main() {
    94  	ctx, cancelCtx := context.WithCancel(context.Background())
    95  	defer cancelCtx()
    96  
    97  	conf := parseEnvFlags()
    98  
    99  	logger.WithField("version", pkg.Version).WithField("processorConf", conf).
   100  		WithField("featureGates", runtime.EncodeFeatures()).
   101  		Info("Starting agones-processor")
   102  
   103  	logger.WithField("logLevel", conf.LogLevel).Info("Setting LogLevel configuration")
   104  	level, err := logrus.ParseLevel(strings.ToLower(conf.LogLevel))
   105  	if err == nil {
   106  		runtime.SetLevel(level)
   107  	} else {
   108  		logger.WithError(err).Info("Specified wrong Logging. Setting default loglevel - Info")
   109  		runtime.SetLevel(logrus.InfoLevel)
   110  	}
   111  
   112  	healthserver := &httpserver.Server{Logger: logger}
   113  	health := healthcheck.NewHandler()
   114  
   115  	config, err := rest.InClusterConfig()
   116  	if err != nil {
   117  		logger.WithError(err).Fatal("Failed to create in-cluster config")
   118  		panic("Failed to create in-cluster config: " + err.Error())
   119  	}
   120  	kubeClient, err := kubernetes.NewForConfig(config)
   121  	if err != nil {
   122  		logger.WithError(err).Fatal("Failed to create Kubernetes client")
   123  		panic("Failed to create Kubernetes client: " + err.Error())
   124  	}
   125  
   126  	go func() {
   127  		healthserver.Handle("/", health)
   128  		_ = healthserver.Run(context.Background(), 0)
   129  	}()
   130  
   131  	signals.NewSigTermHandler(func() {
   132  		logger.Info("Pod shutdown has been requested, failing readiness check")
   133  		cancelCtx()
   134  		os.Exit(0)
   135  	})
   136  
   137  	whenLeader(ctx, cancelCtx, logger, conf, kubeClient, func(ctx context.Context) {
   138  		logger.Info("Starting processor work as leader")
   139  
   140  		// Simulate processor work (to ensure the leader is working)
   141  		// TODO: implement processor work
   142  		ticker := time.NewTicker(5 * time.Second)
   143  		defer ticker.Stop()
   144  
   145  		for {
   146  			select {
   147  			case <-ctx.Done():
   148  				logger.Info("Processor work completed")
   149  				return
   150  			case <-ticker.C:
   151  				logger.Info("Processor is active as leader")
   152  			}
   153  		}
   154  	})
   155  
   156  	logger.Info("Processor exited gracefully.")
   157  }
   158  
   159  func whenLeader(ctx context.Context, cancel context.CancelFunc, logger *logrus.Entry,
   160  	conf processorConfig, kubeClient *kubernetes.Clientset, start func(_ context.Context)) {
   161  	if !conf.LeaderElection {
   162  		start(ctx)
   163  		return
   164  	}
   165  
   166  	id := uuid.New().String()
   167  
   168  	lock := &resourcelock.LeaseLock{
   169  		LeaseMeta: metav1.ObjectMeta{
   170  			Name:      "agones-processor-lock",
   171  			Namespace: conf.PodNamespace,
   172  		},
   173  		Client: kubeClient.CoordinationV1(),
   174  		LockConfig: resourcelock.ResourceLockConfig{
   175  			Identity: id,
   176  		},
   177  	}
   178  
   179  	logger.WithField("id", id).Info("Leader Election ID")
   180  
   181  	leaderelection.RunOrDie(ctx, leaderelection.LeaderElectionConfig{
   182  		Lock: lock,
   183  		// IMPORTANT: you MUST ensure that any code you have that
   184  		// is protected by the lease must terminate **before**
   185  		// you call cancel. Otherwise, you could have a background
   186  		// loop still running and another process could
   187  		// get elected before your background loop finished, violating
   188  		// the stated goal of the lease.
   189  		ReleaseOnCancel: true,
   190  		LeaseDuration:   conf.LeaseDuration,
   191  		RenewDeadline:   conf.RenewDeadline,
   192  		RetryPeriod:     conf.RetryPeriod,
   193  		Callbacks: leaderelection.LeaderCallbacks{
   194  			OnStartedLeading: start,
   195  			OnStoppedLeading: func() {
   196  				logger.WithField("id", id).Info("Leader Lost")
   197  				cancel()
   198  				os.Exit(0)
   199  			},
   200  			OnNewLeader: func(identity string) {
   201  				if identity == id {
   202  					return
   203  				}
   204  				logger.WithField("id", id).Info("New Leader Elected")
   205  			},
   206  		},
   207  	})
   208  }