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
47 changes: 44 additions & 3 deletions pkg/provision/config/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,18 +146,59 @@ type GatewayConfig struct {
//
// Ignored when Skip is true.
ClassName string `yaml:"className,omitempty" json:"className,omitempty" jsonschema:"default=y-cluster,description=GatewayClass name. Consumer Gateway resources reference this via gatewayClassName. Ignored when skip is true."`

// Resources tunes resource requests on the EG controller pod
// and the per-Gateway envoy proxy pod. Defaults target a
// single-user/dev cluster; bump for production-shaped load.
// Upstream defaults are 100m/256Mi (controller) and
// 100m/512Mi (proxy), which oversubscribe a 2GB-RAM
// appliance node.
Resources GatewayResources `yaml:"resources,omitempty" json:"resources,omitempty" jsonschema:"description=Resource requests for the bundled EG install. Defaults: controller 10m/64Mi, proxy 10m/128Mi. Limits are left as upstream sets them."`
}

// GatewayResources groups the two pods whose resource requests
// y-cluster manages: the EG controller (Deployment in
// envoy-gateway-system) and the per-Gateway envoy proxy
// (spawned by EG via the EnvoyProxy CR our default GatewayClass
// references).
type GatewayResources struct {
Controller ResourceRequests `yaml:"controller,omitempty" json:"controller,omitempty" jsonschema:"description=EG controller container requests. Default cpu 10m, memory 64Mi."`
Proxy ResourceRequests `yaml:"proxy,omitempty" json:"proxy,omitempty" jsonschema:"description=Per-Gateway envoy proxy container requests. Default cpu 10m, memory 128Mi."`
}

// ResourceRequests is a minimal Kubernetes-resource-style
// shape covering CPU + memory requests only. Limits are not
// modelled here on purpose: y-cluster's stance is that bursty
// idle workloads are healthier under upstream's existing
// limits than under tighter ones we'd have to guess at.
type ResourceRequests struct {
CPU string `yaml:"cpu,omitempty" json:"cpu,omitempty" jsonschema:"description=CPU request in Kubernetes notation (e.g. 10m, 0.5, 1)."`
Memory string `yaml:"memory,omitempty" json:"memory,omitempty" jsonschema:"description=Memory request in Kubernetes notation (e.g. 64Mi, 256Mi, 1Gi)."`
}

// applyGatewayDefaults fills ClassName when the install is
// enabled. When Skip is set, ClassName is left as the user
// supplied it so debug logs make the operator's intent obvious.
// applyGatewayDefaults fills ClassName + Resources when the
// install is enabled. When Skip is set everything is left as
// the user supplied it so debug logs make the operator's
// intent obvious.
func (c *CommonConfig) applyGatewayDefaults() {
if c.Gateway.Skip {
return
}
if c.Gateway.ClassName == "" {
c.Gateway.ClassName = "y-cluster"
}
if c.Gateway.Resources.Controller.CPU == "" {
c.Gateway.Resources.Controller.CPU = "10m"
}
if c.Gateway.Resources.Controller.Memory == "" {
c.Gateway.Resources.Controller.Memory = "64Mi"
}
if c.Gateway.Resources.Proxy.CPU == "" {
c.Gateway.Resources.Proxy.CPU = "10m"
}
if c.Gateway.Resources.Proxy.Memory == "" {
c.Gateway.Resources.Proxy.Memory = "128Mi"
}
}

// EffectiveGatewayClassName returns the GatewayClass name the
Expand Down
53 changes: 53 additions & 0 deletions pkg/provision/config/gateway_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,59 @@ func TestGateway_SkipLeavesClassNameAlone(t *testing.T) {
if c.Gateway.ClassName != "" {
t.Fatalf("Skip:true should leave ClassName empty, got %q", c.Gateway.ClassName)
}
// Skip also keeps Resources empty so a downstream consumer
// reading the rendered config can tell "operator didn't ask
// for an install" from "operator asked, defaults applied".
if c.Gateway.Resources != (GatewayResources{}) {
t.Fatalf("Skip:true should leave Resources zero, got %+v", c.Gateway.Resources)
}
}

// TestGateway_DefaultResources pins the lower-than-upstream
// defaults that single-user dev clusters benefit from. Upstream
// EG ships 100m/256Mi controller and 100m/512Mi proxy, which
// oversubscribe a 2GB appliance node. Changing these values is
// a contract change for anyone running on those budgets.
func TestGateway_DefaultResources(t *testing.T) {
c := &CommonConfig{}
c.applyCommonDefaults()
if c.Gateway.Resources.Controller.CPU != "10m" {
t.Errorf("Controller.CPU: got %q, want 10m", c.Gateway.Resources.Controller.CPU)
}
if c.Gateway.Resources.Controller.Memory != "64Mi" {
t.Errorf("Controller.Memory: got %q, want 64Mi", c.Gateway.Resources.Controller.Memory)
}
if c.Gateway.Resources.Proxy.CPU != "10m" {
t.Errorf("Proxy.CPU: got %q, want 10m", c.Gateway.Resources.Proxy.CPU)
}
if c.Gateway.Resources.Proxy.Memory != "128Mi" {
t.Errorf("Proxy.Memory: got %q, want 128Mi", c.Gateway.Resources.Proxy.Memory)
}
}

// TestGateway_PreservesExplicitResources: an operator setting
// any subset of fields keeps their explicit values; only
// unset fields default.
func TestGateway_PreservesExplicitResources(t *testing.T) {
c := &CommonConfig{Gateway: GatewayConfig{
Resources: GatewayResources{
Controller: ResourceRequests{CPU: "200m"},
Proxy: ResourceRequests{Memory: "1Gi"},
},
}}
c.applyCommonDefaults()
if c.Gateway.Resources.Controller.CPU != "200m" {
t.Errorf("explicit Controller.CPU lost: %q", c.Gateway.Resources.Controller.CPU)
}
if c.Gateway.Resources.Controller.Memory != "64Mi" {
t.Errorf("unset Controller.Memory should default, got %q", c.Gateway.Resources.Controller.Memory)
}
if c.Gateway.Resources.Proxy.CPU != "10m" {
t.Errorf("unset Proxy.CPU should default, got %q", c.Gateway.Resources.Proxy.CPU)
}
if c.Gateway.Resources.Proxy.Memory != "1Gi" {
t.Errorf("explicit Proxy.Memory lost: %q", c.Gateway.Resources.Proxy.Memory)
}
}

// TestEffectiveGatewayClassName covers the helper Provision uses
Expand Down
12 changes: 8 additions & 4 deletions pkg/provision/docker/docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -280,10 +280,14 @@ func Provision(ctx context.Context, cfg config.DockerConfig, logger *zap.Logger)
logger.Info("envoy gateway install skipped (gateway.skip)")
} else {
if err := envoygateway.Install(ctx, envoygateway.Options{
ContextName: cfg.Context,
GatewayClassName: cfg.Gateway.ClassName,
DNSHintIP: cfg.HostRoutableIP(),
Logger: logger,
ContextName: cfg.Context,
GatewayClassName: cfg.Gateway.ClassName,
DNSHintIP: cfg.HostRoutableIP(),
Logger: logger,
ControllerCPURequest: cfg.Gateway.Resources.Controller.CPU,
ControllerMemRequest: cfg.Gateway.Resources.Controller.Memory,
ProxyCPURequest: cfg.Gateway.Resources.Proxy.CPU,
ProxyMemRequest: cfg.Gateway.Resources.Proxy.Memory,
}); err != nil {
return nil, fmt.Errorf("install envoy gateway: %w", err)
}
Expand Down
84 changes: 81 additions & 3 deletions pkg/provision/envoygateway/embed.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,27 @@ const DNSHintIPAnnotation = "yolean.se/dns-hint-ip"
// the configured class name. dnsHintIP is the value the provisioner
// stamps under the DNSHintIPAnnotation; empty string omits the
// annotations block entirely so an absent hint is distinguishable
// from a present-but-empty one.
// from a present-but-empty one. envoyProxyName, when non-empty,
// adds a parametersRef pointing at the EnvoyProxy CR of that name
// in the envoy-gateway-system namespace -- the upstream-blessed
// extension point for tuning the data-plane proxy's resources.
//
// Pure function so unit tests can pin the rendered shape against
// a known-good baseline.
func GatewayClassYAML(name, dnsHintIP string) []byte {
func GatewayClassYAML(name, dnsHintIP, envoyProxyName string) []byte {
var annotations string
if dnsHintIP != "" {
annotations = fmt.Sprintf(" annotations:\n %s: %s\n", DNSHintIPAnnotation, dnsHintIP)
}
var parametersRef string
if envoyProxyName != "" {
parametersRef = fmt.Sprintf(` parametersRef:
group: gateway.envoyproxy.io
kind: EnvoyProxy
name: %s
namespace: %s
`, envoyProxyName, Namespace)
}
return []byte(fmt.Sprintf(`---
# y-cluster default GatewayClass for the bundled Envoy Gateway
# install. Consumer Gateway resources reference this name via
Expand All @@ -47,6 +59,72 @@ metadata:
name: %s
%sspec:
controllerName: %s
`, name, name, annotations, EGControllerName))
%s`, name, name, annotations, EGControllerName, parametersRef))
}

// EnvoyProxyName is the metadata.name of the EnvoyProxy CR
// y-cluster applies in the envoy-gateway-system namespace. The
// default GatewayClass references it via parametersRef so
// Gateways under that class inherit the tuned resources without
// any per-Gateway boilerplate.
const EnvoyProxyName = "y-cluster"

// EnvoyProxyYAML renders the EnvoyProxy CR that tunes the
// data-plane envoy proxy pod's resource requests. cpuRequest /
// memRequest land under spec.provider.kubernetes.envoyDeployment
// .container.resources.requests. Limits are left for EG's
// defaults (and the cluster's LimitRange, if any).
//
// The CR lives in envoy-gateway-system because that's the only
// namespace EG looks at for parametersRef of GatewayClass.
//
// Pure function for unit-test pinning.
func EnvoyProxyYAML(cpuRequest, memRequest string) []byte {
return []byte(fmt.Sprintf(`---
# y-cluster's tuning for the per-Gateway envoy proxy pod.
# Referenced by the GatewayClass via parametersRef.
apiVersion: gateway.envoyproxy.io/v1alpha1
kind: EnvoyProxy
metadata:
name: %s
namespace: %s
spec:
provider:
type: Kubernetes
kubernetes:
envoyDeployment:
container:
resources:
requests:
cpu: %s
memory: %s
`, EnvoyProxyName, Namespace, cpuRequest, memRequest))
}

// ControllerResourcesYAML is a partial Deployment manifest
// declaring ownership over the envoy-gateway controller
// container's resources.requests under server-side apply. The
// apply uses field-manager y-cluster; existing fields (image,
// env, replicas, container args) stay with their original
// owners. Limits are not declared -- intentional, so the
// upstream limit (currently 1Gi memory, no CPU cap) stays in
// effect.
func ControllerResourcesYAML(cpuRequest, memRequest string) []byte {
return []byte(fmt.Sprintf(`---
apiVersion: apps/v1
kind: Deployment
metadata:
name: %s
namespace: %s
spec:
template:
spec:
containers:
- name: envoy-gateway
resources:
requests:
cpu: %s
memory: %s
`, DeploymentName, Namespace, cpuRequest, memRequest))
}

75 changes: 72 additions & 3 deletions pkg/provision/envoygateway/embed_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (
// entirely, so an absent hint is distinguishable from
// "annotation present with empty value".
func TestGatewayClassYAML_NoHintIP(t *testing.T) {
got := string(GatewayClassYAML("y-cluster", ""))
got := string(GatewayClassYAML("y-cluster", "", ""))
if strings.Contains(got, "annotations") {
t.Fatalf("expected no annotations block:\n%s", got)
}
Expand All @@ -23,13 +23,16 @@ func TestGatewayClassYAML_NoHintIP(t *testing.T) {
if !strings.Contains(got, "controllerName: "+EGControllerName) {
t.Fatalf("missing controller name:\n%s", got)
}
if strings.Contains(got, "parametersRef") {
t.Fatalf("empty envoyProxyName should omit parametersRef:\n%s", got)
}
}

// TestGatewayClassYAML_WithHintIP guards the qemu/docker
// host-loopback shape: the dnsHintIP value lands as a single
// annotation under the GatewayClass metadata.
func TestGatewayClassYAML_WithHintIP(t *testing.T) {
got := string(GatewayClassYAML("y-cluster", "127.0.0.1"))
got := string(GatewayClassYAML("y-cluster", "127.0.0.1", ""))
if !strings.Contains(got, "annotations:") {
t.Fatalf("missing annotations block:\n%s", got)
}
Expand All @@ -51,11 +54,77 @@ func TestGatewayClassYAML_WithHintIP(t *testing.T) {
// both metadata.name and the doc comment. The comment header line
// is part of the contract -- consumers grep for it during debug.
func TestGatewayClassYAML_RespectsCustomName(t *testing.T) {
got := string(GatewayClassYAML("eg", ""))
got := string(GatewayClassYAML("eg", "", ""))
if !strings.Contains(got, "name: eg") {
t.Fatalf("missing custom name:\n%s", got)
}
if !strings.Contains(got, "gatewayClassName: eg") {
t.Fatalf("comment should reference the configured name:\n%s", got)
}
}

// TestGatewayClassYAML_WithEnvoyProxyRef pins the parametersRef
// shape EG expects: group / kind / name / namespace under
// spec.parametersRef, namespace fixed to the EG namespace.
func TestGatewayClassYAML_WithEnvoyProxyRef(t *testing.T) {
got := string(GatewayClassYAML("y-cluster", "", EnvoyProxyName))
for _, want := range []string{
"parametersRef:",
"group: gateway.envoyproxy.io",
"kind: EnvoyProxy",
"name: " + EnvoyProxyName,
"namespace: " + Namespace,
} {
if !strings.Contains(got, want) {
t.Errorf("missing %q:\n%s", want, got)
}
}
}

// TestEnvoyProxyYAML_ShapesResources pins the EnvoyProxy CR
// fields y-cluster actually owns: requests under provider.
// kubernetes.envoyDeployment.container.resources.
func TestEnvoyProxyYAML_ShapesResources(t *testing.T) {
got := string(EnvoyProxyYAML("10m", "128Mi"))
for _, want := range []string{
"apiVersion: gateway.envoyproxy.io/v1alpha1",
"kind: EnvoyProxy",
"name: " + EnvoyProxyName,
"namespace: " + Namespace,
"cpu: 10m",
"memory: 128Mi",
} {
if !strings.Contains(got, want) {
t.Errorf("missing %q:\n%s", want, got)
}
}
if strings.Contains(got, "limits:") {
t.Errorf("CR should declare requests only, not limits:\n%s", got)
}
}

// TestControllerResourcesYAML_RequestsOnly pins the partial
// Deployment shape: requests-only, container matched by name so
// SSA targets the right container, no limits/image/env claimed
// (so upstream owners keep them).
func TestControllerResourcesYAML_RequestsOnly(t *testing.T) {
got := string(ControllerResourcesYAML("10m", "64Mi"))
for _, want := range []string{
"kind: Deployment",
"name: " + DeploymentName,
"namespace: " + Namespace,
"- name: envoy-gateway",
"cpu: 10m",
"memory: 64Mi",
} {
if !strings.Contains(got, want) {
t.Errorf("missing %q:\n%s", want, got)
}
}
if strings.Contains(got, "limits:") {
t.Errorf("patch should declare requests only:\n%s", got)
}
if strings.Contains(got, "image:") {
t.Errorf("patch must not claim image (would fight upstream owner):\n%s", got)
}
}
Loading