Skip to content

Commit

Permalink
[certmanager] Add EnsureCertForServicesWithSelector() and EnsureCertF…
Browse files Browse the repository at this point in the history
…orServiceWithSelector()

Adds function EnsureCertForServicesWithSelector() to create certs
for a k8s services identified by a label selector and
EnsureCertForServicesWithSelector() where the expectation is that
the selector maps to a single service.
  • Loading branch information
stuggi committed Dec 19, 2023
1 parent 113a404 commit 1c48f64
Show file tree
Hide file tree
Showing 6 changed files with 293 additions and 12 deletions.
83 changes: 83 additions & 0 deletions modules/certmanager/certificate.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ import (
certmgrmetav1 "github.com/cert-manager/cert-manager/pkg/apis/meta/v1"
"github.com/openstack-k8s-operators/lib-common/modules/common/helper"
"github.com/openstack-k8s-operators/lib-common/modules/common/secret"
"github.com/openstack-k8s-operators/lib-common/modules/common/service"
"github.com/openstack-k8s-operators/lib-common/modules/common/util"
"golang.org/x/exp/maps"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"

Expand Down Expand Up @@ -231,3 +233,84 @@ func EnsureCert(

return certSecret, ctrl.Result{}, nil
}

// EnsureCertForServicesWithSelector - creates certificate for k8s services identified
// by a label selector
func EnsureCertForServicesWithSelector(
ctx context.Context,
helper *helper.Helper,
namespace string,
selector map[string]string,
issuer string,
) (map[string]string, ctrl.Result, error) {
certs := map[string]string{}
svcs, err := service.GetServicesListWithLabel(
ctx,
helper,
namespace,
selector,
)
if err != nil {
return certs, ctrl.Result{}, err
}

for _, svc := range svcs.Items {
hostname := fmt.Sprintf("%s.%s.svc", svc.Name, namespace)
// create cert for the service
certRequest := CertificateRequest{
IssuerName: issuer,
CertName: fmt.Sprintf("%s-svc", svc.Name),
Hostnames: []string{hostname},
Labels: svc.Labels,
}
certSecret, ctrlResult, err := EnsureCert(
ctx,
helper,
certRequest)
if err != nil {
return certs, ctrlResult, err
} else if (ctrlResult != ctrl.Result{}) {
return certs, ctrlResult, nil
}

certs[hostname] = certSecret.Name
}

return certs, ctrl.Result{}, nil
}

// EnsureCertForServiceWithSelector - creates certificate for a k8s service identified
// by a label selector. The label selector must match a single service
func EnsureCertForServiceWithSelector(
ctx context.Context,
helper *helper.Helper,
namespace string,
selector map[string]string,
issuer string,
) (string, ctrl.Result, error) {
var cert string
svcs, err := service.GetServicesListWithLabel(
ctx,
helper,
namespace,
selector,
)
if err != nil {
return cert, ctrl.Result{}, err
}
if len(svcs.Items) != 1 {
return cert, ctrl.Result{}, fmt.Errorf("multiple services identified by selector: %+v", selector)
}

certs, ctrlResult, err := EnsureCertForServicesWithSelector(
ctx, helper, namespace, selector, issuer)
if err != nil {
return cert, ctrlResult, err
} else if (ctrlResult != ctrl.Result{}) {
return cert, ctrlResult, nil
}

hostname := maps.Keys(certs)

return certs[hostname[0]], ctrl.Result{}, nil
}
1 change: 1 addition & 0 deletions modules/certmanager/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ require (
github.com/onsi/gomega v1.30.0
github.com/openstack-k8s-operators/lib-common/modules/common v0.3.1-0.20231215134849-9acca0025036
go.uber.org/zap v1.26.0
golang.org/x/exp v0.0.0-20231214170342-aacd6d4b4611
k8s.io/api v0.26.11
k8s.io/apimachinery v0.26.11
k8s.io/client-go v0.26.11
Expand Down
1 change: 1 addition & 0 deletions modules/certmanager/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,7 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/exp v0.0.0-20231214170342-aacd6d4b4611 h1:qCEDpW1G+vcj3Y7Fy52pEM1AWm3abj8WimGYejI3SC4=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
Expand Down
170 changes: 170 additions & 0 deletions modules/certmanager/test/functional/certmanager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,13 @@ limitations under the License.
package functional

import (
"fmt"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/openstack-k8s-operators/lib-common/modules/certmanager"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/types"

certmgrv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1"
certmgrmetav1 "github.com/cert-manager/cert-manager/pkg/apis/meta/v1"
Expand Down Expand Up @@ -147,4 +151,170 @@ var _ = Describe("certmanager module", func() {
Expect(err).ShouldNot(HaveOccurred())
th.AssertIssuerDoesNotExist(names.CertName)
})

It("creates certificates for k8s services with label selector", func() {
i := certmanager.NewIssuer(
certmanager.CAIssuer(
"ca",
names.Namespace,
map[string]string{"f": "l"},
"secret",
),
timeout,
)

_, err := i.CreateOrPatch(ctx, h)
Expect(err).ShouldNot(HaveOccurred())
issuer := th.GetIssuer(names.CAName)
Expect(issuer.Spec.CA).NotTo(BeNil())

svc1Name := types.NamespacedName{Name: "svc1", Namespace: names.Namespace}
svc2Name := types.NamespacedName{Name: "svc2", Namespace: names.Namespace}
svc3Name := types.NamespacedName{Name: "svc3", Namespace: names.Namespace}
th.CreateService(svc1Name, map[string]string{"foo": ""}, corev1.ServiceSpec{
Ports: []corev1.ServicePort{
{
Name: svc1Name.Name,
Port: int32(1111),
Protocol: corev1.ProtocolTCP,
},
},
})
th.CreateService(svc2Name, map[string]string{"foo": ""}, corev1.ServiceSpec{
Ports: []corev1.ServicePort{
{
Name: svc2Name.Name,
Port: int32(2222),
Protocol: corev1.ProtocolTCP,
},
},
})
th.CreateService(svc3Name, map[string]string{}, corev1.ServiceSpec{
Ports: []corev1.ServicePort{
{
Name: svc2Name.Name,
Port: int32(3333),
Protocol: corev1.ProtocolTCP,
},
},
})
// simulate underlying cert secerts exist
th.CreateCertSecret(types.NamespacedName{Name: "cert-svc1-svc", Namespace: names.Namespace})
th.CreateCertSecret(types.NamespacedName{Name: "cert-svc2-svc", Namespace: names.Namespace})

certs, _, err := certmanager.EnsureCertForServicesWithSelector(
th.Ctx, h, names.Namespace, map[string]string{"foo": ""}, names.CAName.Name)
Expect(err).ShouldNot(HaveOccurred())
Expect(certs).To(HaveLen(2))
Expect(certs).To(HaveKey(fmt.Sprintf("svc1.%s.svc", names.Namespace)))
Expect(certs).To(HaveKey(fmt.Sprintf("svc2.%s.svc", names.Namespace)))
})

It("creates a certificate for a specific k8s service matching label selector", func() {
i := certmanager.NewIssuer(
certmanager.CAIssuer(
"ca",
names.Namespace,
map[string]string{"f": "l"},
"secret",
),
timeout,
)

_, err := i.CreateOrPatch(ctx, h)
Expect(err).ShouldNot(HaveOccurred())
issuer := th.GetIssuer(names.CAName)
Expect(issuer.Spec.CA).NotTo(BeNil())

svc1Name := types.NamespacedName{Name: "svc1", Namespace: names.Namespace}
svc2Name := types.NamespacedName{Name: "svc2", Namespace: names.Namespace}
svc3Name := types.NamespacedName{Name: "svc3", Namespace: names.Namespace}
th.CreateService(svc1Name, map[string]string{"foo": "1"}, corev1.ServiceSpec{
Ports: []corev1.ServicePort{
{
Name: svc1Name.Name,
Port: int32(1111),
Protocol: corev1.ProtocolTCP,
},
},
})
th.CreateService(svc2Name, map[string]string{"foo": "2"}, corev1.ServiceSpec{
Ports: []corev1.ServicePort{
{
Name: svc2Name.Name,
Port: int32(2222),
Protocol: corev1.ProtocolTCP,
},
},
})
th.CreateService(svc3Name, map[string]string{}, corev1.ServiceSpec{
Ports: []corev1.ServicePort{
{
Name: svc2Name.Name,
Port: int32(3333),
Protocol: corev1.ProtocolTCP,
},
},
})
// simulate underlying cert secert exist
th.CreateCertSecret(types.NamespacedName{Name: "cert-svc2-svc", Namespace: names.Namespace})

cert, _, err := certmanager.EnsureCertForServiceWithSelector(
th.Ctx, h, names.Namespace, map[string]string{"foo": "2"}, names.CAName.Name)
Expect(err).ShouldNot(HaveOccurred())
Expect(cert).To(Equal("cert-svc2-svc"))

})

It("fails to create a certificate for a specific k8s service if the label selector returns not a single service", func() {
i := certmanager.NewIssuer(
certmanager.CAIssuer(
"ca",
names.Namespace,
map[string]string{"f": "l"},
"secret",
),
timeout,
)

_, err := i.CreateOrPatch(ctx, h)
Expect(err).ShouldNot(HaveOccurred())
issuer := th.GetIssuer(names.CAName)
Expect(issuer.Spec.CA).NotTo(BeNil())

svc1Name := types.NamespacedName{Name: "svc1", Namespace: names.Namespace}
svc2Name := types.NamespacedName{Name: "svc2", Namespace: names.Namespace}
svc3Name := types.NamespacedName{Name: "svc3", Namespace: names.Namespace}
th.CreateService(svc1Name, map[string]string{"foo": ""}, corev1.ServiceSpec{
Ports: []corev1.ServicePort{
{
Name: svc1Name.Name,
Port: int32(1111),
Protocol: corev1.ProtocolTCP,
},
},
})
th.CreateService(svc2Name, map[string]string{"foo": ""}, corev1.ServiceSpec{
Ports: []corev1.ServicePort{
{
Name: svc2Name.Name,
Port: int32(2222),
Protocol: corev1.ProtocolTCP,
},
},
})
th.CreateService(svc3Name, map[string]string{}, corev1.ServiceSpec{
Ports: []corev1.ServicePort{
{
Name: svc2Name.Name,
Port: int32(3333),
Protocol: corev1.ProtocolTCP,
},
},
})

_, _, err = certmanager.EnsureCertForServiceWithSelector(
th.Ctx, h, names.Namespace, map[string]string{"foo": ""}, names.CAName.Name)
Expect(err).To(HaveOccurred())
})
})
28 changes: 16 additions & 12 deletions modules/certmanager/test/functional/suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import (

"go.uber.org/zap/zapcore"

"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/rest"
Expand Down Expand Up @@ -117,18 +118,6 @@ var _ = BeforeSuite(func() {
th = certmanager_test.NewTestHelper(ctx, k8sClient, timeout, interval, logger)
Expect(th).NotTo(BeNil())

kclient, err := kubernetes.NewForConfig(cfg)
Expect(err).ToNot(HaveOccurred(), "failed to create kclient")

// NOTE(gibi): helper.Helper needs an object that is being reconciled
// we are not really doing reconciliation in this test but still we need to
// provide a valid object. It is used as controller reference for certain
// objects created in the test. So we provide a simple one, a Namespace.
genericObject := th.CreateNamespace("generic-object")
h, err = helper.NewHelper(genericObject, k8sClient, kclient, testEnv.Scheme, ctrl.Log)
Expect(err).NotTo(HaveOccurred())
Expect(h).NotTo(BeNil())

go func() {
defer GinkgoRecover()
}()
Expand All @@ -148,6 +137,21 @@ var _ = BeforeEach(func() {
// https://book.kubebuilder.io/reference/envtest.html#namespace-usage-limitation
namespace = uuid.New().String()
th.CreateNamespace(namespace)

kclient, err := kubernetes.NewForConfig(cfg)
Expect(err).ToNot(HaveOccurred(), "failed to create kclient")

// NOTE(gibi): helper.Helper needs an object that is being reconciled
// we are not really doing reconciliation in this test but still we need to
// provide a valid object. It is used as controller reference for certain
// objects created in the test. So we provide a simple one, a Namespace.
// Note(mschuppert) using a Secret as a Namespace object does not have
// metadata with namespace and some functions use the BeforeObject.GetNamespace()
genericObject := th.CreateSecret(types.NamespacedName{Name: "generic", Namespace: namespace}, map[string][]byte{})
h, err = helper.NewHelper(genericObject, k8sClient, kclient, testEnv.Scheme, ctrl.Log)
Expect(err).NotTo(HaveOccurred())
Expect(h).NotTo(BeNil())

// We still request the delete of the Namespace to properly cleanup if
// we run the test in an existing cluster.
DeferCleanup(th.DeleteNamespace, namespace)
Expand Down
22 changes: 22 additions & 0 deletions modules/common/test/helpers/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (

corev1 "k8s.io/api/core/v1"
k8s_errors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
)

Expand All @@ -35,6 +36,27 @@ func (tc *TestHelper) GetService(name types.NamespacedName) *corev1.Service {
return instance
}

// CreateService creates a new k8s service resource with provided data.
//
// Example usage:
//
// secret := th.CreateService(types.NamespacedName{Name: "test-secret", Namespace: "test-namespace"}, map[string]string{}, corev1.ServiceSpec{...})
func (tc *TestHelper) CreateService(name types.NamespacedName, labels map[string]string, svcSpec corev1.ServiceSpec) *corev1.Service {
svc := &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: name.Name,
Namespace: name.Namespace,
Labels: labels,
},
Spec: svcSpec,
}
gomega.Eventually(func(g gomega.Gomega) {
g.Expect(tc.K8sClient.Create(tc.Ctx, svc)).Should(gomega.Succeed())
}, tc.Timeout, tc.Interval).Should(gomega.Succeed())

return svc
}

// AssertServiceExists - asserts the existence of a Service resource in the Kubernetes cluster.
//
// Example usage:
Expand Down

0 comments on commit 1c48f64

Please sign in to comment.