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
4 changes: 4 additions & 0 deletions charts/gateway-helm/templates/_rbac.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,7 @@ verbs:
- delete
- deletecollection
- patch
- watch
- apiGroups:
- apps
resources:
Expand All @@ -223,9 +224,11 @@ verbs:
verbs:
- create
- get
- list
- delete
- deletecollection
- patch
- watch
Comment thread
zhaohuabing marked this conversation as resolved.
- apiGroups:
- autoscaling
- policy
Expand All @@ -239,6 +242,7 @@ verbs:
- delete
- deletecollection
- patch
- watch
- apiGroups:
- certificates.k8s.io
resources:
Expand Down
7 changes: 7 additions & 0 deletions internal/cmd/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,9 @@ func startRunners(ctx context.Context, cfg *config.Server, runnerErrors *message
// The Elected channel is used to block the tasks that are waiting for the leader to be elected.
// It will be closed once the leader is elected in the controller manager.
cfg.Elected = make(chan struct{})
// ProviderReady is used to block consumers of the provider's cached client until the provider has
// synced its cache.
cfg.ProviderReady = make(chan struct{})

// Setup the Extension Manager
var extMgr types.Manager
Expand All @@ -197,6 +200,10 @@ func startRunners(ctx context.Context, cfg *config.Server, runnerErrors *message
// and publishes it.
// It also subscribes to status resources and once it receives
// a status resource back, it writes it out.
//
// Important: order matters here: the provider runner must be started before the infra manager runner because
// the provider runner creates the Kubernetes client that is needed by the infra manager runner to reconcile
// the Envoy Proxy and the rate limit infra resources.
runner: providerrunner.New(&providerrunner.Config{
Server: *cfg,
ProviderResources: channels.pResources,
Expand Down
30 changes: 30 additions & 0 deletions internal/envoygateway/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import (
"errors"
"io"

"sigs.k8s.io/controller-runtime/pkg/client"

egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1"
"github.com/envoyproxy/gateway/api/v1alpha1/validation"
"github.com/envoyproxy/gateway/internal/logging"
Expand Down Expand Up @@ -39,10 +41,36 @@ type Server struct {
Logger logging.Logger
// Elected chan is used to signal when an EG instance is elected as leader.
Elected chan struct{}
// ProviderReady is closed once the Kubernetes provider cache is synced and the cached client is ready for consumers.
ProviderReady chan struct{}
// Stdout is the writer for standard output.
Stdout io.Writer
// Stderr is the writer for error output.
Stderr io.Writer
// KubernetesClient holds the controller-runtime client created by the Kubernetes provider.
// This is used by the infrastructure runner to create the envoy proxy and rate limit infra resources.
KubernetesClient *KubernetesClientHolder

@zhaohuabing zhaohuabing Apr 22, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A KubernetesClientHolder is used to hold the shared kube client from the controller runtime.
config.Server is passed by value to runners, and changing it to reference is not safe since runners have modified the members of config.Server, like the logger.

}

type KubernetesClientHolder struct {
client client.Client
}

func NewKubernetesClientHolder() *KubernetesClientHolder {
return &KubernetesClientHolder{}
}

func (h *KubernetesClientHolder) Set(cli client.Client) {
if h != nil {
h.client = cli
}
}

func (h *KubernetesClientHolder) Get() client.Client {
if h == nil {
return nil
}
return h.client
}

// New returns a Server with default parameters.
Expand All @@ -55,6 +83,8 @@ func New(stdout, stderr io.Writer) (*Server, error) {
Stdout: stdout,
Stderr: stderr,
Elected: make(chan struct{}),
ProviderReady: make(chan struct{}),
KubernetesClient: NewKubernetesClientHolder(),
}, nil
}

Expand Down
96 changes: 44 additions & 52 deletions internal/infrastructure/kubernetes/infra_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -455,8 +455,9 @@ func (i *Infra) deleteServiceAccount(ctx context.Context, r ResourceRender) (err
// deleteDeployment deletes the Envoy Deployment in the kube api server, if it exists.
func (i *Infra) deleteDeployment(ctx context.Context, r ResourceRender) (err error) {
var (
name, ns = r.Name(), r.Namespace()
deployment = &appsv1.Deployment{
name, ns = r.Name(), r.Namespace()
recordDeleteMetric = true
deployment = &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Namespace: ns,
Name: name,
Expand All @@ -470,25 +471,22 @@ func (i *Infra) deleteDeployment(ctx context.Context, r ResourceRender) (err err
}
)

// Check if any Deployments exist before attempting deletion to avoid
// incrementing delete metrics on no-op reconciles.
// DeleteAllOf always runs because this cached read may miss live objects and
// must not decide whether deletion is skipped. It only suppresses success
// metrics for likely no-op reconciles.
deployList := &appsv1.DeploymentList{}
if err = i.Client.List(ctx, deployList, &client.ListOptions{
if listErr := i.Client.List(ctx, deployList, &client.ListOptions{
Namespace: ns,
LabelSelector: r.LabelSelector(),
}); err != nil {
resourceDeleteTotal.WithFailure(metrics.ReasonError, labels...).Increment()
return err
}
if len(deployList.Items) == 0 {
return nil
}); listErr == nil && len(deployList.Items) == 0 {
recordDeleteMetric = false
}

defer func() {
if err == nil {
if err == nil && recordDeleteMetric {
resourceDeleteDurationSeconds.With(labels...).Record(time.Since(startTime).Seconds())
resourceDeleteTotal.WithSuccess(labels...).Increment()
} else {
} else if err != nil {
resourceDeleteTotal.WithFailure(metrics.ReasonError, labels...).Increment()
}
}()
Expand All @@ -504,8 +502,9 @@ func (i *Infra) deleteDeployment(ctx context.Context, r ResourceRender) (err err
// deleteDaemonSet deletes the Envoy DaemonSet in the kube api server, if it exists.
func (i *Infra) deleteDaemonSet(ctx context.Context, r ResourceRender) (err error) {
var (
name, ns = r.Name(), r.Namespace()
daemonSet = &appsv1.DaemonSet{
name, ns = r.Name(), r.Namespace()
recordDeleteMetric = true
daemonSet = &appsv1.DaemonSet{
ObjectMeta: metav1.ObjectMeta{
Namespace: ns,
Name: name,
Expand All @@ -519,25 +518,22 @@ func (i *Infra) deleteDaemonSet(ctx context.Context, r ResourceRender) (err erro
}
)

// Check if any DaemonSets exist before attempting deletion to avoid
// incrementing delete metrics on no-op reconciles.
// DeleteAllOf always runs because this cached read may miss live objects and
// must not decide whether deletion is skipped. It only suppresses success
// metrics for likely no-op reconciles.
dsList := &appsv1.DaemonSetList{}
if err = i.Client.List(ctx, dsList, &client.ListOptions{
if listErr := i.Client.List(ctx, dsList, &client.ListOptions{
Namespace: ns,
LabelSelector: r.LabelSelector(),
}); err != nil {
resourceDeleteTotal.WithFailure(metrics.ReasonError, labels...).Increment()
return err
}
if len(dsList.Items) == 0 {
return nil
}); listErr == nil && len(dsList.Items) == 0 {
recordDeleteMetric = false
}

defer func() {
if err == nil {
if err == nil && recordDeleteMetric {
resourceDeleteDurationSeconds.With(labels...).Record(time.Since(startTime).Seconds())
resourceDeleteTotal.WithSuccess(labels...).Increment()
} else {
} else if err != nil {
resourceDeleteTotal.WithFailure(metrics.ReasonError, labels...).Increment()
}
}()
Expand Down Expand Up @@ -623,8 +619,9 @@ func (i *Infra) deleteService(ctx context.Context, r ResourceRender) (err error)
// deleteHpa deletes the Horizontal Pod Autoscaler associated to its renderer, if it exists.
func (i *Infra) deleteHPA(ctx context.Context, r ResourceRender) (err error) {
var (
name, ns = r.Name(), r.Namespace()
hpa = &autoscalingv2.HorizontalPodAutoscaler{
name, ns = r.Name(), r.Namespace()
recordDeleteMetric = true
hpa = &autoscalingv2.HorizontalPodAutoscaler{
ObjectMeta: metav1.ObjectMeta{
Namespace: ns,
Name: name,
Expand All @@ -638,25 +635,22 @@ func (i *Infra) deleteHPA(ctx context.Context, r ResourceRender) (err error) {
}
)

// Check if any HPAs exist before attempting deletion to avoid
// incrementing delete metrics on no-op reconciles.
// DeleteAllOf always runs because this cached read may miss live objects and
// must not decide whether deletion is skipped. It only suppresses success
// metrics for likely no-op reconciles.
hpaList := &autoscalingv2.HorizontalPodAutoscalerList{}
if err = i.Client.List(ctx, hpaList, &client.ListOptions{
if listErr := i.Client.List(ctx, hpaList, &client.ListOptions{
Namespace: ns,
LabelSelector: r.LabelSelector(),
}); err != nil {
resourceDeleteTotal.WithFailure(metrics.ReasonError, labels...).Increment()
return err
}
if len(hpaList.Items) == 0 {
return nil
}); listErr == nil && len(hpaList.Items) == 0 {
recordDeleteMetric = false
}

defer func() {
if err == nil {
if err == nil && recordDeleteMetric {
resourceDeleteDurationSeconds.With(labels...).Record(time.Since(startTime).Seconds())
resourceDeleteTotal.WithSuccess(labels...).Increment()
} else {
} else if err != nil {
resourceDeleteTotal.WithFailure(metrics.ReasonError, labels...).Increment()
}
}()
Expand All @@ -672,8 +666,9 @@ func (i *Infra) deleteHPA(ctx context.Context, r ResourceRender) (err error) {
// deletePDB deletes the PodDistribution budget associated to its renderer, if it exists.
func (i *Infra) deletePDB(ctx context.Context, r ResourceRender) (err error) {
var (
name, ns = r.Name(), r.Namespace()
pdb = &policyv1.PodDisruptionBudget{
name, ns = r.Name(), r.Namespace()
recordDeleteMetric = true
pdb = &policyv1.PodDisruptionBudget{
ObjectMeta: metav1.ObjectMeta{
Namespace: ns,
Name: name,
Expand All @@ -687,25 +682,22 @@ func (i *Infra) deletePDB(ctx context.Context, r ResourceRender) (err error) {
}
)

// Check if any PDBs exist before attempting deletion to avoid
// incrementing delete metrics on no-op reconciles.
// DeleteAllOf always runs because this cached read may miss live objects and
// must not decide whether deletion is skipped. It only suppresses success
// metrics for likely no-op reconciles.
pdbList := &policyv1.PodDisruptionBudgetList{}
if err = i.Client.List(ctx, pdbList, &client.ListOptions{
if listErr := i.Client.List(ctx, pdbList, &client.ListOptions{
Namespace: ns,
LabelSelector: r.LabelSelector(),
}); err != nil {
resourceDeleteTotal.WithFailure(metrics.ReasonError, labels...).Increment()
return err
}
if len(pdbList.Items) == 0 {
return nil
}); listErr == nil && len(pdbList.Items) == 0 {
recordDeleteMetric = false
}

defer func() {
if err == nil {
if err == nil && recordDeleteMetric {
resourceDeleteDurationSeconds.With(labels...).Record(time.Since(startTime).Seconds())
resourceDeleteTotal.WithSuccess(labels...).Increment()
} else {
} else if err != nil {
resourceDeleteTotal.WithFailure(metrics.ReasonError, labels...).Increment()
}
}()
Expand Down
Loading
Loading