Skip to content

Commit

Permalink
[RS-2260] Add GatewayAPI field to configure CRD management
Browse files Browse the repository at this point in the history
The default behaviour - which is used when the new field is not set, or is set
to "Reconcile" - is to keep the cluster's Gateway API CRDs aligned with those
that the Tigera operator would install on a cluster that does not yet have any
version of those CRDs.

Alternatively, if this field is set to "PreferExisting", the Tigera operator
will create the Gateway API CRDs if they do not already exist, but will not
overwrite any existing Gateway API CRDs.  This setting may be preferable if the
customer is using other implementations of the Gateway API concurrently with
the Gateway API support in Calico Enterprise.  It is then the customer's
responsibility to ensure that CRDs are installed that meet the needs of all the
Gateway API implementations in their cluster.
  • Loading branch information
nelljerram committed Feb 7, 2025
1 parent 57cdab7 commit 135e1be
Show file tree
Hide file tree
Showing 6 changed files with 221 additions and 3 deletions.
23 changes: 22 additions & 1 deletion api/v1/gatewayapi_types.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) 2024 Tigera, Inc. All rights reserved.
// Copyright (c) 2024-2025 Tigera, Inc. All rights reserved.
/*
Licensed under the Apache License, Version 2.0 (the "License");
Expand All @@ -22,6 +22,14 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// +kubebuilder:validation:Enum=Reconcile;PreferExisting
type GatewayCRDManagement string

const (
GatewayCRDManagementReconcile GatewayCRDManagement = "Reconcile"
GatewayCRDManagementPreferExisting GatewayCRDManagement = "PreferExisting"
)

// GatewayAPISpec has fields that can be used to customize our GatewayAPI support.
type GatewayAPISpec struct {
// Allow optional customization of the gateway controller deployment.
Expand All @@ -32,6 +40,19 @@ type GatewayAPISpec struct {

// Allow optional customization of gateway deployments.
GatewayDeployment *GatewayDeployment `json:"gatewayDeployment,omitempty"`

// Configure how to manage and update Gateway API CRDs. The default behaviour - which is
// used when this field is not set, or is set to "Reconcile" - is to keep the cluster's
// Gateway API CRDs aligned with those that the Tigera operator would install on a cluster
// that does not yet have any version of those CRDs.
//
// Alternatively, if this field is set to "PreferExisting", the Tigera operator will create
// the Gateway API CRDs if they do not already exist, but will not overwrite any existing
// Gateway API CRDs. This setting may be preferable if the customer is using other
// implementations of the Gateway API concurrently with the Gateway API support in Calico
// Enterprise. It is then the customer's responsibility to ensure that CRDs are installed
// that meet the needs of all the Gateway API implementations in their cluster.
CRDManagement *GatewayCRDManagement `json:"crdManagement,omitempty"`
}

//+kubebuilder:object:root=true
Expand Down
5 changes: 5 additions & 0 deletions api/v1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 4 additions & 2 deletions pkg/controller/gatewayapi/gatewayapi_controller.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) 2024 Tigera, Inc. All rights reserved.
// Copyright (c) 2024-2025 Tigera, Inc. All rights reserved.

// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -156,7 +156,9 @@ func (r *ReconcileGatewayAPI) Reconcile(ctx context.Context, request reconcile.R
// one that we are providing here.
crdComponent := render.NewPassthrough(render.GatewayAPICRDs(log)...)
handler := utils.NewComponentHandler(log, r.client, r.scheme, nil)
handler.SetCreateOnly()
if gatewayAPI.Spec.CRDManagement != nil && *gatewayAPI.Spec.CRDManagement == operatorv1.GatewayCRDManagementPreferExisting {
handler.SetCreateOnly()
}
err = handler.CreateOrUpdateOrDelete(ctx, crdComponent, nil)
if err != nil {
r.status.SetDegraded(operatorv1.ResourceCreateError, "Error rendering GatewayAPI CRDs", err, log)
Expand Down
29 changes: 29 additions & 0 deletions pkg/controller/gatewayapi/gatewayapi_controller_suite_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Copyright (c) 2025 Tigera, Inc. All rights reserved.

// Licensed 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 gatewayapi

import (
"testing"

. "github.com/onsi/ginkgo"
"github.com/onsi/ginkgo/reporters"
. "github.com/onsi/gomega"
)

func TestStatus(t *testing.T) {
RegisterFailHandler(Fail)
junitReporter := reporters.NewJUnitReporter("../../../report/ut/gatewayapi_controller_suite.xml")
RunSpecsWithDefaultAndCustomReporters(t, "pkg/controller/gatewayapi Suite", []Reporter{junitReporter})
}
145 changes: 145 additions & 0 deletions pkg/controller/gatewayapi/gatewayapi_controller_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
// Copyright (c) 2025 Tigera, Inc. All rights reserved.

// Licensed 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 gatewayapi

import (
"context"

. "github.com/onsi/ginkgo"
. "github.com/onsi/ginkgo/extensions/table"
. "github.com/onsi/gomega"
. "github.com/onsi/gomega/gstruct"
"github.com/stretchr/testify/mock"

admregv1 "k8s.io/api/admissionregistration/v1"
appsv1 "k8s.io/api/apps/v1"
batchv1 "k8s.io/api/batch/v1"
rbacv1 "k8s.io/api/rbac/v1"
apiextenv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/reconcile"

operatorv1 "github.com/tigera/operator/api/v1"
"github.com/tigera/operator/pkg/apis"
"github.com/tigera/operator/pkg/controller/status"
ctrlrfake "github.com/tigera/operator/pkg/ctrlruntime/client/fake"
)

var _ = Describe("Gateway API controller tests", func() {
var c client.Client
var ctx context.Context
var r *ReconcileGatewayAPI
var scheme *runtime.Scheme
var mockStatus *status.MockStatus
var installation *operatorv1.Installation

BeforeEach(func() {
// The schema contains all objects that should be known to the fake client when the test runs.
scheme = runtime.NewScheme()
Expect(apis.AddToScheme(scheme)).NotTo(HaveOccurred())
Expect(appsv1.SchemeBuilder.AddToScheme(scheme)).ShouldNot(HaveOccurred())
Expect(rbacv1.SchemeBuilder.AddToScheme(scheme)).ShouldNot(HaveOccurred())
Expect(batchv1.SchemeBuilder.AddToScheme(scheme)).ShouldNot(HaveOccurred())
Expect(operatorv1.SchemeBuilder.AddToScheme(scheme)).NotTo(HaveOccurred())
Expect(admregv1.SchemeBuilder.AddToScheme(scheme)).NotTo(HaveOccurred())

// Create a client that will have a CRUD interface of k8s objects.
c = ctrlrfake.DefaultFakeClientBuilder(scheme).Build()
ctx = context.Background()
installation = &operatorv1.Installation{
ObjectMeta: metav1.ObjectMeta{Name: "default"},
Spec: operatorv1.InstallationSpec{
Variant: operatorv1.TigeraSecureEnterprise,
Registry: "some.registry.org/",
},
Status: operatorv1.InstallationStatus{
Variant: operatorv1.TigeraSecureEnterprise,
Computed: &operatorv1.InstallationSpec{
Registry: "my-reg",
// The test is provider agnostic.
KubernetesProvider: operatorv1.ProviderNone,
},
},
}
mockStatus = &status.MockStatus{}
mockStatus.On("OnCRFound").Return()
mockStatus.On("AddDaemonsets", mock.Anything).Return()
mockStatus.On("AddDeployments", mock.Anything).Return()
mockStatus.On("IsAvailable").Return(true)
mockStatus.On("AddStatefulSets", mock.Anything).Return()
mockStatus.On("AddCronJobs", mock.Anything)
mockStatus.On("OnCRNotFound").Return()
mockStatus.On("ClearDegraded")
mockStatus.On("SetDegraded", "Waiting for LicenseKeyAPI to be ready", "").Return().Maybe()
mockStatus.On("ReadyToMonitor")
mockStatus.On("SetMetaData", mock.Anything).Return()

r = &ReconcileGatewayAPI{
client: c,
scheme: scheme,
status: mockStatus,
}
})

DescribeTable("CRD management",
func(gwapiMod func(*operatorv1.GatewayAPI), expectReplace bool) {
Expect(c.Create(ctx, installation)).NotTo(HaveOccurred())

By("installing a pre-existing Gateway CRD with an improbable version")
crdName := "gateways.gateway.networking.k8s.io"
existingCRD := &apiextenv1.CustomResourceDefinition{
ObjectMeta: metav1.ObjectMeta{Name: crdName},
Spec: apiextenv1.CustomResourceDefinitionSpec{
Versions: []apiextenv1.CustomResourceDefinitionVersion{{
Name: "v0123456789",
}},
},
}
Expect(c.Create(ctx, existingCRD)).NotTo(HaveOccurred())

By("applying the GatewayAPI CR to the fake cluster")
gwapi := &operatorv1.GatewayAPI{
ObjectMeta: metav1.ObjectMeta{Name: "tigera-secure"},
Spec: operatorv1.GatewayAPISpec{},
}
gwapiMod(gwapi)
Expect(c.Create(ctx, gwapi)).NotTo(HaveOccurred())

By("triggering a reconcile")
_, err := r.Reconcile(ctx, reconcile.Request{})
Expect(err).ShouldNot(HaveOccurred())

By("examining the Gateway CRD that is now present")
gatewayCRD := &apiextenv1.CustomResourceDefinition{}
Expect(c.Get(ctx, client.ObjectKey{Name: crdName}, gatewayCRD)).NotTo(HaveOccurred())
if expectReplace {
Expect(gatewayCRD.Spec.Versions).NotTo(ContainElement(MatchFields(IgnoreExtras, Fields{"Name": Equal("v0123456789")})))
} else {
Expect(gatewayCRD.Spec.Versions).To(ContainElement(MatchFields(IgnoreExtras, Fields{"Name": Equal("v0123456789")})))
}
},
Entry("default", func(_ *operatorv1.GatewayAPI) {}, true),
Entry("Reconcile", func(gwapi *operatorv1.GatewayAPI) {
setting := operatorv1.GatewayCRDManagementReconcile
gwapi.Spec.CRDManagement = &setting
}, true),
Entry("PreferExisting", func(gwapi *operatorv1.GatewayAPI) {
setting := operatorv1.GatewayCRDManagementPreferExisting
gwapi.Spec.CRDManagement = &setting
}, false),
)
})
16 changes: 16 additions & 0 deletions pkg/crds/operator/operator.tigera.io_gatewayapis.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,22 @@ spec:
description: GatewayAPISpec has fields that can be used to customize our
GatewayAPI support.
properties:
crdManagement:
description: |-
Configure how to manage and update Gateway API CRDs. The default behaviour - which is
used when this field is not set, or is set to "Reconcile" - is to keep the cluster's
Gateway API CRDs aligned with those that the Tigera operator would install on a cluster
that does not yet have any version of those CRDs.
Alternatively, if this field is set to "PreferExisting", the Tigera operator will create
the Gateway API CRDs if they do not already exist, but will not overwrite any existing
Gateway API CRDs. This setting may be preferable if the customer is using other
implementations of the Gateway API concurrently with the Gateway API support in Calico
Enterprise. It is then the customer's responsibility to ensure that CRDs are installed
that meet the needs of all the Gateway API implementations in their cluster.
enum:
- Reconcile
- PreferExisting
type: string
gatewayCertgenJob:
description: Allow optional customization of the gateway certgen job.
properties:
Expand Down

0 comments on commit 135e1be

Please sign in to comment.