Files
cert-manager/pkg/controller/certificate-shim/gateways/controller_test.go
T
Maël ValaisandJake Sanders 30f9c123d3 gateway-shim: add the gateway-shim controller
Note that the gateway-shim is only half the work for supporting the
Gateway API in cert-manager. The other half is the HTTP01 solver
support, which is still worked on.

The Gateway API in cert-manager is releases as an experimental feature
and needs to be enabled manually with the following flag:

  --controllers=*,gateway-shim

All the annotations supported by ingress-shim are also supported by
gateway-shim, with some exceptions:

  "acme.cert-manager.io/http01-ingress-class"

This annotation is not supported on the Gateway resource. Although the
Gateway resource also has a "gatewayClass" field, we will need to add
another field instead of "ingress-class" to avoid confusion with the
ingress-shim.

  "acme.cert-manager.io/http01-edit-in-place"

This annotation is not supported because it is specific to some ingress
controllers like ingress-gce.

  "kubernetes.io/tls-acme"

This annotation is not supported because it is a behavior inherited from
kube-lego and we chose not to keep this behavior with the Gateway API.

Unlike the ingress-shim, you can reuse the same Secret name in multiple
TLS configurations on the same Gateway resource.

The ingress-shim now shows the exact location of the duplicate
secretName when the user gives the same secretName in two separate TLS
blocks.

Signed-off-by: Maël Valais <mael@vls.dev>
Co-authored-by: Jake Sanders <i@am.so-aweso.me>
2021-07-15 20:34:55 +02:00

229 lines
8.9 KiB
Go

/*
Copyright 2020 The cert-manager Authors.
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 controller
import (
"context"
"testing"
"time"
testpkg "github.com/jetstack/cert-manager/pkg/controller/test"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
gwapi "sigs.k8s.io/gateway-api/apis/v1alpha1"
gwclient "sigs.k8s.io/gateway-api/pkg/client/clientset/versioned"
cmapi "github.com/jetstack/cert-manager/pkg/apis/certmanager/v1"
cmclient "github.com/jetstack/cert-manager/pkg/client/clientset/versioned"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
)
var gatewayGVK = gwapi.SchemeGroupVersion.WithKind("Gateway")
func Test_controller_Register(t *testing.T) {
tests := []struct {
name string
existingCert *cmapi.Certificate
givenCall func(*testing.T, cmclient.Interface, gwclient.Interface)
expectAddCalls []interface{}
}{
{
name: "gateway is re-queued when an 'Added' event is received for this gateway",
givenCall: func(t *testing.T, _ cmclient.Interface, c gwclient.Interface) {
_, err := c.NetworkingV1alpha1().Gateways("namespace-1").Create(context.Background(), &gwapi.Gateway{ObjectMeta: metav1.ObjectMeta{
Namespace: "namespace-1", Name: "gateway-1",
}}, metav1.CreateOptions{})
require.NoError(t, err)
},
expectAddCalls: []interface{}{"namespace-1/gateway-1"},
},
{
name: "gateway is re-queued when an 'Updated' event is received for this gateway",
givenCall: func(t *testing.T, _ cmclient.Interface, c gwclient.Interface) {
// We can't use the gateway-api fake.NewSimpleClientset due to
// Gateway being pluralized as "gatewaies" instead of
// "gateways". The trick is thus to use Create instead.
_, err := c.NetworkingV1alpha1().Gateways("namespace-1").Create(context.Background(), &gwapi.Gateway{ObjectMeta: metav1.ObjectMeta{
Namespace: "namespace-1", Name: "gateway-1",
}}, metav1.CreateOptions{})
require.NoError(t, err)
_, err = c.NetworkingV1alpha1().Gateways("namespace-1").Update(context.Background(), &gwapi.Gateway{ObjectMeta: metav1.ObjectMeta{
Namespace: "namespace-1", Name: "gateway-1", Labels: map[string]string{"foo": "bar"},
}}, metav1.UpdateOptions{})
require.NoError(t, err)
},
expectAddCalls: []interface{}{"namespace-1/gateway-1", "namespace-1/gateway-1"},
// <----- Create ------> <------ Update ----->
},
{
name: "gateway is re-queued when a 'Deleted' event is received for this gateway",
givenCall: func(t *testing.T, _ cmclient.Interface, c gwclient.Interface) {
_, err := c.NetworkingV1alpha1().Gateways("namespace-1").Create(context.Background(), &gwapi.Gateway{ObjectMeta: metav1.ObjectMeta{
Namespace: "namespace-1", Name: "gateway-1",
}}, metav1.CreateOptions{})
require.NoError(t, err)
err = c.NetworkingV1alpha1().Gateways("namespace-1").Delete(context.Background(), "gateway-1", metav1.DeleteOptions{})
require.NoError(t, err)
},
expectAddCalls: []interface{}{"namespace-1/gateway-1", "namespace-1/gateway-1"},
// <----- Create ------> <------ Delete ----->
},
{
name: "gateway is re-queued when an 'Added' event is received for its child Certificate",
givenCall: func(t *testing.T, c cmclient.Interface, _ gwclient.Interface) {
_, err := c.CertmanagerV1().Certificates("namespace-1").Create(context.Background(), &cmapi.Certificate{ObjectMeta: metav1.ObjectMeta{
Namespace: "namespace-1", Name: "cert-1",
OwnerReferences: []metav1.OwnerReference{*metav1.NewControllerRef(&gwapi.Gateway{ObjectMeta: metav1.ObjectMeta{
Namespace: "namespace-1", Name: "gateway-2",
}}, gatewayGVK)},
}}, metav1.CreateOptions{})
require.NoError(t, err)
},
expectAddCalls: []interface{}{"namespace-1/gateway-2"},
},
{
name: "gateway is re-queued when an 'Updated' event is received for its child Certificate",
existingCert: &cmapi.Certificate{ObjectMeta: metav1.ObjectMeta{
Namespace: "namespace-1", Name: "cert-1",
OwnerReferences: []metav1.OwnerReference{*metav1.NewControllerRef(&gwapi.Gateway{ObjectMeta: metav1.ObjectMeta{
Namespace: "namespace-1", Name: "gateway-2",
}}, gatewayGVK)},
}},
givenCall: func(t *testing.T, c cmclient.Interface, _ gwclient.Interface) {
_, err := c.CertmanagerV1().Certificates("namespace-1").Update(context.Background(), &cmapi.Certificate{ObjectMeta: metav1.ObjectMeta{
Namespace: "namespace-1", Name: "cert-1",
OwnerReferences: []metav1.OwnerReference{*metav1.NewControllerRef(&gwapi.Gateway{ObjectMeta: metav1.ObjectMeta{
Namespace: "namespace-1", Name: "gateway-2",
}}, gatewayGVK)},
}}, metav1.UpdateOptions{})
require.NoError(t, err)
},
expectAddCalls: []interface{}{"namespace-1/gateway-2"},
},
{
name: "gateway is re-queued when a 'Deleted' event is received for its child Certificate",
existingCert: &cmapi.Certificate{ObjectMeta: metav1.ObjectMeta{
Namespace: "namespace-1", Name: "cert-1",
OwnerReferences: []metav1.OwnerReference{*metav1.NewControllerRef(&gwapi.Gateway{ObjectMeta: metav1.ObjectMeta{
Namespace: "namespace-1", Name: "gateway-2",
}}, gatewayGVK)},
}},
givenCall: func(t *testing.T, c cmclient.Interface, _ gwclient.Interface) {
// err := c.CertmanagerV1().Certificates("namespace-1").Delete(context.Background(), "cert-1", metav1.DeleteOptions{})
// require.NoError(t, err)
},
expectAddCalls: []interface{}{"namespace-1/gateway-2"},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
var o []runtime.Object
if test.existingCert != nil {
o = append(o, test.existingCert)
}
// NOTE(mael): we can't use Gateway with GWObjects because of a
// limitation in client-go's NewSimpleClientset. It uses a heuristic
// that wrongly guesses the resource from the Gateway kind
// ("gatewaies" instead of "gateways"). To work around this, the
// only way is to either use a real apiserver or to use call Create
// instead of setting existing objects with NewSimpleClientset. See:
// https://github.com/kubernetes/client-go/blob/7a90b0858/testing/fixture.go#L326-L331
b := &testpkg.Builder{T: t, CertManagerObjects: o}
b.Init()
// We don't care about the HasSynced functions since we already know
// whether they have been properly "used": if no Gateway or
// Certificate event is received then HasSynced has not been setup
// properly.
mock := &mockWorkqueue{t: t}
_, _, err := (&controller{queue: mock}).Register(b.Context)
require.NoError(t, err)
b.Start()
defer b.Stop()
test.givenCall(t, b.CMClient, b.GWClient)
// We have no way of knowing when the informers will be done adding
// items to the queue due to the "shared informer" architecture:
// Start(stop) does not allow you to wait for the informers to be
// done.
time.Sleep(50 * time.Millisecond)
// We only expect 0 or 1 keys received in the queue, or 2 keys when
// we have to create a Gateway before deleting or updating it.
assert.Equal(t, test.expectAddCalls, mock.callsToAdd)
})
}
}
type mockWorkqueue struct {
t *testing.T
callsToAdd []interface{}
}
func (m *mockWorkqueue) Add(arg0 interface{}) {
m.callsToAdd = append(m.callsToAdd, arg0)
}
func (m *mockWorkqueue) AddAfter(arg0 interface{}, arg1 time.Duration) {
m.t.Error("workqueue.AddAfter was called but was not expected to be called")
}
func (m *mockWorkqueue) AddRateLimited(arg0 interface{}) {
m.t.Error("workqueue.AddRateLimited was called but was not expected to be called")
}
func (m *mockWorkqueue) Done(arg0 interface{}) {
m.t.Error("workqueue.Done was called but was not expected to be called")
}
func (m *mockWorkqueue) Forget(arg0 interface{}) {
m.t.Error("workqueue.Forget was called but was not expected to be called")
}
func (m *mockWorkqueue) Get() (interface{}, bool) {
m.t.Error("workqueue.Get was called but was not expected to be called")
return nil, false
}
func (m *mockWorkqueue) Len() int {
m.t.Error("workqueue.Len was called but was not expected to be called")
return 0
}
func (m *mockWorkqueue) NumRequeues(arg0 interface{}) int {
m.t.Error("workqueue.NumRequeues was called but was not expected to be called")
return 0
}
func (m *mockWorkqueue) ShutDown() {
m.t.Error("workqueue.NumRequeues was called but was not expected to be called")
}
func (m *mockWorkqueue) ShuttingDown() bool {
m.t.Error("workqueue.NumRequeues was called but was not expected to be called")
return false
}