Skip to content
Open
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
26 changes: 9 additions & 17 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -248,21 +248,13 @@ $(ISTIO_RESOURCES_DIR)/%.tgz:
# Envoy Gateway" in docs/common_tasks.md.
ENVOY_GATEWAY_HELM_CHART ?= oci://docker.io/envoyproxy/gateway-helm
ENVOY_GATEWAY_VERSION ?= v1.7.2
ENVOY_GATEWAY_PREFIX ?= tigera-gateway-api
ENVOY_GATEWAY_NAMESPACE ?= tigera-gateway
ENVOY_GATEWAY_RESOURCES = pkg/render/gatewayapi/gateway_api_resources.yaml

$(ENVOY_GATEWAY_RESOURCES): $(HACK_BIN)/helm-$(BUILDARCH)
echo "---" > $@
echo "apiVersion: v1" >> $@
echo "kind: Namespace" >> $@
echo "metadata:" >> $@
echo " name: $(ENVOY_GATEWAY_NAMESPACE)" >> $@
$(HELM_BUILDARCH_BINARY) template $(ENVOY_GATEWAY_PREFIX) $(ENVOY_GATEWAY_HELM_CHART) \
ENVOY_GATEWAY_CHART = pkg/render/gatewayapi/gateway-helm.tgz

$(ENVOY_GATEWAY_CHART): $(HACK_BIN)/helm-$(BUILDARCH)
$(HELM_BUILDARCH_BINARY) pull $(ENVOY_GATEWAY_HELM_CHART) \
--version $(ENVOY_GATEWAY_VERSION) \
-n $(ENVOY_GATEWAY_NAMESPACE) \
--include-crds \
>> $@
--destination pkg/render/gatewayapi/
@mv pkg/render/gatewayapi/gateway-helm-$(ENVOY_GATEWAY_VERSION).tgz $@

$(HELM_BUILDARCH_BINARY): $(HELM_BUILDARCH_VERSIONED_BINARY)
$(info ░▒▓ symlink $(HELM_BUILDARCH_VERSIONED_BINARY) -> $(HELM_BUILDARCH_BINARY))
Expand All @@ -276,7 +268,7 @@ $(HELM_BUILDARCH_VERSIONED_BINARY): | $(HACK_BIN)


build: $(BINDIR)/operator-$(ARCH)
$(BINDIR)/operator-$(ARCH): $(SRC_FILES) $(ENVOY_GATEWAY_RESOURCES) $(ISTIO_CHART_FILES)
$(BINDIR)/operator-$(ARCH): $(SRC_FILES) $(ENVOY_GATEWAY_CHART) $(ISTIO_CHART_FILES)
mkdir -p $(BINDIR)
$(CONTAINERIZED) -e CGO_ENABLED=$(CGO_ENABLED) -e GOEXPERIMENT=$(GOEXPERIMENT) $(CALICO_BUILD) \
sh -c '$(GIT_CONFIG_SSH) \
Expand Down Expand Up @@ -339,7 +331,7 @@ GINKGO_FOCUS?=.*
ENVTEST_K8S_VERSION?=1.34.x

.PHONY: ut
ut: $(ENVOY_GATEWAY_RESOURCES) $(ISTIO_CHART_FILES)
ut: $(ENVOY_GATEWAY_CHART) $(ISTIO_CHART_FILES)
-mkdir -p .go-pkg-cache report
$(CONTAINERIZED) $(CALICO_BUILD) sh -c '$(GIT_CONFIG_SSH) \
go install sigs.k8s.io/controller-runtime/tools/setup-envtest@release-0.22 && \
Expand All @@ -348,7 +340,7 @@ ut: $(ENVOY_GATEWAY_RESOURCES) $(ISTIO_CHART_FILES)

## Run the functional tests
fv: cluster-create load-container-images run-fvs cluster-destroy
run-fvs: $(ENVOY_GATEWAY_RESOURCES) $(ISTIO_CHART_FILES)
run-fvs: $(ENVOY_GATEWAY_CHART) $(ISTIO_CHART_FILES)
-mkdir -p .go-pkg-cache report
$(CONTAINERIZED) $(CALICO_BUILD) sh -c '$(GIT_CONFIG_SSH) \
ginkgo -focus="$(GINKGO_FOCUS)" $(GINKGO_ARGS) "$(FV_DIR)"'
Expand Down
14 changes: 4 additions & 10 deletions docs/common_tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,15 +182,9 @@ spec:

1. In `Makefile`, update `ENVOY_GATEWAY_VERSION`.

1. Delete `pkg/render/gatewayapi/gateway_api_resources.yaml`.
1. Delete `pkg/render/gatewayapi/gateway-helm.tgz`.

1. Run `make build`. This will generate a new version of `pkg/render/gateway_api_resources.yaml` and then build the operator image.

1. Review diffs between the old and new versions of `pkg/render/gateway_api_resources.yaml` (e.g. using `git diff`) to identify:

- any incompatible changes that would need changes in our Gateway-related operator coding or image building

- any entirely new CRDs or resources, that would need changes in `pkg/render/gateway_api.go`.
1. Run `make build`. This will download the new version of the Envoy Gateway helm chart and build the operator image. The chart is embedded in the binary and rendered at runtime using the Helm SDK.

1. Address build issues if there are any.

Expand All @@ -200,11 +194,11 @@ spec:

1. Identify the corresponding new versions of the `gateway`, `proxy` and `ratelimit` images.

- The `gateway` version can be found by looking for "envoyproxy/gateway" in `pkg/render/gateway_api_resources.yaml`, and probably also in the Envoy Gateway release notes ([for example](https://github.com/envoyproxy/gateway/releases/tag/v1.3.2)). It should be the same as the nominal Envoy Gateway version that you're updating to.
- The `gateway` version can be found in the Envoy Gateway release notes ([for example](https://github.com/envoyproxy/gateway/releases/tag/v1.3.2)). It should be the same as the nominal Envoy Gateway version that you're updating to.

- The `proxy` version can be found in the Envoy Gateway release notes, or by referring to [this compatibility matrix](https://gateway.envoyproxy.io/news/releases/matrix/).

- The `ratelimit` version can be found by looking for "envoyproxy/ratelimit" in `pkg/render/gateway_api_resources.yaml`, and probably also in the Envoy Gateway release notes.
- The `ratelimit` version can be found in the Envoy Gateway release notes.

1. Switching to the `projectcalico/calico` repo, update the code under `third_party/envoy-{gateway,proxy,ratelimit}` to build those new image versions. In each case:

Expand Down
116 changes: 107 additions & 9 deletions pkg/controller/gatewayapi/gatewayapi_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ import (
v3 "github.com/tigera/api/pkg/apis/projectcalico/v3"
v1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
rbacv1 "k8s.io/api/rbac/v1"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
Expand All @@ -40,12 +42,14 @@ import (
envoyapi "github.com/envoyproxy/gateway/api/v1alpha1"
"github.com/go-logr/logr"
operatorv1 "github.com/tigera/operator/api/v1"
"github.com/tigera/operator/pkg/common"
"github.com/tigera/operator/pkg/controller/options"
"github.com/tigera/operator/pkg/controller/status"
"github.com/tigera/operator/pkg/controller/utils"
"github.com/tigera/operator/pkg/controller/utils/imageset"
"github.com/tigera/operator/pkg/ctrlruntime"
"github.com/tigera/operator/pkg/render"
"github.com/tigera/operator/pkg/render/common/networkpolicy"
"github.com/tigera/operator/pkg/render/gatewayapi"
)

Expand All @@ -65,6 +69,7 @@ func Add(mgr manager.Manager, opts options.ControllerOptions) error {
client: mgr.GetClient(),
scheme: mgr.GetScheme(),
enterpriseCRDsExist: opts.EnterpriseCRDExists,
tierWatchReady: &utils.ReadyFlag{},
status: status.New(mgr.GetClient(), "gatewayapi", opts.KubernetesVersion),
clusterDomain: opts.ClusterDomain,
multiTenant: opts.MultiTenant,
Expand All @@ -77,6 +82,9 @@ func Add(mgr manager.Manager, opts options.ControllerOptions) error {
return fmt.Errorf("failed to create gatewayapi-controller: %w", err)
}

// Lazy tier watch; policies only render when the calico-system Tier exists.
go utils.WaitToAddTierWatch(networkpolicy.CalicoTierName, c, opts.K8sClientset, log, r.tierWatchReady)

// Watch for changes to primary resource GatewayAPI
err = c.WatchObject(&operatorv1.GatewayAPI{}, &handler.EnqueueRequestForObject{})
if err != nil {
Expand Down Expand Up @@ -131,6 +139,21 @@ func Add(mgr manager.Manager, opts options.ControllerOptions) error {
return nil
}

// Watch Gateway resources lazily — the CRD is created by this controller, so we can
// only start watching after it exists. Called from Reconcile once CRDs are in place.
gatewaysWatched := false
r.watchGateways = func() error {
if gatewaysWatched {
return nil
}
log.V(1).Info("Adding watch for Gateway resources")
if err = c.WatchObject(&gapi.Gateway{}, &handler.EnqueueRequestForObject{}); err != nil {
return fmt.Errorf("gatewayapi-controller failed to watch Gateway resource: %w", err)
}
gatewaysWatched = true
return nil
}

return nil
}

Expand All @@ -142,12 +165,14 @@ type ReconcileGatewayAPI struct {
client client.Client
scheme *runtime.Scheme
enterpriseCRDsExist bool
tierWatchReady *utils.ReadyFlag
status status.StatusManager
clusterDomain string
multiTenant bool
newComponentHandler func(log logr.Logger, client client.Client, scheme *runtime.Scheme, cr metav1.Object) utils.ComponentHandler
watchEnvoyProxy func(namespacedName operatorv1.NamespacedName) error
watchEnvoyGateway func(namespacedName operatorv1.NamespacedName) error
watchGateways func() error
}

// Reconcile reads that state of the cluster for a GatewayAPI object and makes changes based on the state read
Expand Down Expand Up @@ -213,7 +238,11 @@ func (r *ReconcileGatewayAPI) Reconcile(ctx context.Context, request reconcile.R
// not already exist and cannot be installed. The "optional" set is everything else that we
// would ideally install, to provide more options to our users; but this controller will
// only warn if any of those cannot be installed (and do not already exist).
essentialCRDs, optionalCRDs := gatewayapi.GatewayAPICRDs(installationSpec.KubernetesProvider)
essentialCRDs, optionalCRDs, err := gatewayapi.GatewayAPICRDs(installationSpec.KubernetesProvider, r.scheme)
if err != nil {
r.status.SetDegraded(operatorv1.ResourceReadError, "Error rendering gateway API CRDs", err, log)
return reconcile.Result{}, err
}
handler := r.newComponentHandler(log, r.client, r.scheme, nil)
if gatewayAPI.Spec.CRDManagement == nil || *gatewayAPI.Spec.CRDManagement == operatorv1.CRDManagementPreferExisting {
handler.SetCreateOnly()
Expand Down Expand Up @@ -274,12 +303,28 @@ func (r *ReconcileGatewayAPI) Reconcile(ctx context.Context, request reconcile.R
return reconcile.Result{}, err
}

// Render v3 NetworkPolicies only when the calico-system Tier exists — same pattern
// as the other controllers; tolerates clusters without Calico installed.
includeV3NetworkPolicy := false
if r.tierWatchReady.IsReady() {
if err := r.client.Get(ctx, types.NamespacedName{Name: networkpolicy.CalicoTierName}, &v3.Tier{}); err != nil {
if !errors.IsNotFound(err) && !meta.IsNoMatchError(err) {
r.status.SetDegraded(operatorv1.ResourceReadError, "Error querying calico-system tier", err, reqLogger)
return reconcile.Result{}, err
}
} else {
includeV3NetworkPolicy = true
}
}

gatewayConfig := &gatewayapi.GatewayAPIImplementationConfig{
Installation: installationSpec,
PullSecrets: pullSecrets,
GatewayAPI: gatewayAPI,
CustomEnvoyProxies: make(map[string]*envoyapi.EnvoyProxy),
CurrentGatewayClasses: set.New[string](),
Scheme: r.scheme,
Installation: installationSpec,
PullSecrets: pullSecrets,
GatewayAPI: gatewayAPI,
CustomEnvoyProxies: make(map[string]*envoyapi.EnvoyProxy),
CurrentGatewayClasses: set.New[string](),
IncludeV3NetworkPolicy: includeV3NetworkPolicy,
}

if gatewayAPI.Spec.EnvoyGatewayConfigRef != nil {
Expand Down Expand Up @@ -401,10 +446,64 @@ func (r *ReconcileGatewayAPI) Reconcile(ctx context.Context, request reconcile.R
}
}

// Start watching Gateway resources now that the CRDs are in place, so future
// Gateway changes trigger reconciliation.
if err = r.watchGateways(); err != nil {
r.status.SetDegraded(operatorv1.ResourceReadError, "Error watching Gateway resources", err, log)
return reconcile.Result{}, err
}

var gwList gapi.GatewayList
if err = r.client.List(ctx, &gwList); err != nil {
r.status.SetDegraded(operatorv1.ResourceReadError, "Error listing Gateway resources", err, log)
return reconcile.Result{}, err
}

// Collect namespaces hosting a Gateway whose class is ours (controllerName
// matches, or it's declared in Spec.GatewayClasses).
ownedClass := make(map[string]bool, len(gatewayAPI.Spec.GatewayClasses)+1)
ownedClass[gatewayapi.GatewayClassName] = true
for _, c := range gatewayAPI.Spec.GatewayClasses {
ownedClass[c.Name] = true
}
for i := range gcList.Items {
if string(gcList.Items[i].Spec.ControllerName) == gatewayapi.ControllerName {
ownedClass[gcList.Items[i].Name] = true
}
}
nsSet := set.New[string]()
for i := range gwList.Items {
if ownedClass[string(gwList.Items[i].Spec.GatewayClassName)] {
nsSet.Insert(gwList.Items[i].Namespace)
}
}
gatewayConfig.GatewayNamespaces = nsSet.SortedList()

// Enterprise: read previously-provisioned namespaces from the shared CRB's
// Subjects so we can clean them up when their Gateway is gone.
if variant.IsEnterprise() {
gatewayConfig.CurrentGatewayNamespaces = set.New[string]()
existingCRB := &rbacv1.ClusterRoleBinding{}
if err = r.client.Get(ctx, types.NamespacedName{Name: gatewayapi.GatewayNamespacesCRBName}, existingCRB); err == nil {
for _, s := range existingCRB.Subjects {
if s.Kind == "ServiceAccount" {
gatewayConfig.CurrentGatewayNamespaces.Insert(s.Namespace)
}
}
} else if !errors.IsNotFound(err) {
r.status.SetDegraded(operatorv1.ResourceReadError, "Error reading gateway namespaces ClusterRoleBinding", err, log)
return reconcile.Result{}, err
}
}

// Render non-CRD resources for Gateway API support, i.e. for our specific bundled
// implementation of the Gateway API. For these we specify the GatewayAPI CR as the owner,
// so that they all get automatically cleaned up if the GatewayAPI CR is removed again.
nonCRDComponent := gatewayapi.GatewayAPIImplementationComponent(gatewayConfig)
nonCRDComponent, err := gatewayapi.GatewayAPIImplementationComponent(gatewayConfig)
if err != nil {
r.status.SetDegraded(operatorv1.ResourceCreateError, "Error rendering Gateway API resources", err, log)
return reconcile.Result{}, err
}
err = imageset.ApplyImageSet(ctx, r.client, variant, nonCRDComponent)
if err != nil {
r.status.SetDegraded(operatorv1.ResourceCreateError, "Error with images from ImageSet", err, log)
Expand All @@ -425,7 +524,6 @@ func (r *ReconcileGatewayAPI) Reconcile(ctx context.Context, request reconcile.R
// Clear the degraded bit if we've reached this far.
r.status.ClearDegraded()

// Update the status of the GatewayAPI instance and StatusManager.
return reconcile.Result{}, nil
}

Expand Down Expand Up @@ -496,6 +594,6 @@ func (r *ReconcileGatewayAPI) getPolicySyncPathPrefix(fcSpec *v3.FelixConfigurat
// The bool return value indicates if the finalizer is Set
func (r *ReconcileGatewayAPI) maintainFinalizer(ctx context.Context, gatewayAPI client.Object) (bool, error) {
// These objects require graceful termination before the CNI plugin is torn down.
gatewayAPIDeployment := v1.Deployment{ObjectMeta: metav1.ObjectMeta{Name: "envoy-gateway", Namespace: "tigera-gateway"}}
gatewayAPIDeployment := v1.Deployment{ObjectMeta: metav1.ObjectMeta{Name: "envoy-gateway", Namespace: common.CalicoNamespace}}
return utils.MaintainInstallationFinalizer(ctx, r.client, gatewayAPI, render.GatewayAPIFinalizer, &gatewayAPIDeployment)
}
3 changes: 3 additions & 0 deletions pkg/controller/gatewayapi/gatewayapi_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,15 +93,18 @@ var _ = Describe("Gateway API controller tests", func() {
mockStatus.On("ClearDegraded")
mockStatus.On("ReadyToMonitor")
mockStatus.On("SetMetaData", mock.Anything).Return()
mockStatus.On("RemoveDeployments", mock.Anything).Return()

fakeComponentHandlers = nil
r = &ReconcileGatewayAPI{
client: c,
scheme: scheme,
status: mockStatus,
tierWatchReady: &utils.ReadyFlag{},
newComponentHandler: FakeComponentHandler,
watchEnvoyProxy: func(namespacedName operatorv1.NamespacedName) error { return nil },
watchEnvoyGateway: func(namespacedName operatorv1.NamespacedName) error { return nil },
watchGateways: func() error { return nil },
}
})

Expand Down
6 changes: 5 additions & 1 deletion pkg/controller/istio/istio_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,11 @@ func (r *ReconcileIstio) Reconcile(ctx context.Context, request reconcile.Reques
}

// Get the Kubernetes Gateway API CRDs.
essentialCRDs, optionalCRDs := gatewayapi.K8SGatewayAPICRDs(installationSpec.KubernetesProvider)
essentialCRDs, optionalCRDs, err := gatewayapi.K8SGatewayAPICRDs(installationSpec.KubernetesProvider, r.scheme)
if err != nil {
r.status.SetDegraded(operatorv1.ResourceReadError, "Error rendering gateway API CRDs", err, log)
return reconcile.Result{}, err
}

// Check CRDs are present and only create it if not
handler := utils.NewComponentHandler(log, r, r.scheme, nil)
Expand Down
Loading
Loading