diff --git a/conformance/conformance_suite.go b/conformance/conformance_suite.go index 40fe67f..17b5caf 100644 --- a/conformance/conformance_suite.go +++ b/conformance/conformance_suite.go @@ -146,6 +146,11 @@ func (t *testDriver) createServiceExport(c *clusterClients) { Expect(err).ToNot(HaveOccurred()) } +func (t *testDriver) deleteServiceExport(c *clusterClients) { + Expect(c.mcs.MulticlusterV1alpha1().ServiceExports(t.namespace).Delete(ctx, helloServiceName, + metav1.DeleteOptions{})).ToNot(HaveOccurred()) +} + func (t *testDriver) deployHelloService(c *clusterClients, service *corev1.Service) { _, err := c.k8s.AppsV1().Deployments(t.namespace).Create(ctx, newHelloDeployment(), metav1.CreateOptions{}) Expect(err).ToNot(HaveOccurred()) diff --git a/conformance/endpoint_slice.go b/conformance/endpoint_slice.go new file mode 100644 index 0000000..e749882 --- /dev/null +++ b/conformance/endpoint_slice.go @@ -0,0 +1,117 @@ +/* +Copyright 2024 The Kubernetes 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 conformance + +import ( + "context" + "fmt" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + discoveryv1 "k8s.io/api/discovery/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/wait" + "sigs.k8s.io/mcs-api/pkg/apis/v1alpha1" +) + +const K8sEndpointSliceManagedByName = "endpointslice-controller.k8s.io" + +var _ = Describe("", Label(OptionalLabel, EndpointSliceLabel), func() { + t := newTestDriver() + + JustBeforeEach(func() { + t.createServiceExport(&clients[0]) + }) + + Specify("Exporting a service should create an MCS EndpointSlice in the service's namespace in each cluster with the "+ + "required MCS labels. Unexporting should delete the EndpointSlice.", func() { + AddReportEntry(SpecRefReportEntry, "https://github.com/kubernetes/enhancements/tree/master/keps/sig-multicluster/1645-multi-cluster-services-api#using-endpointslice-objects-to-track-endpoints") + + endpointSlices := make([]*discoveryv1.EndpointSlice, len(clients)) + + for i, client := range clients { + eps := t.awaitMCSEndpointSlice(&client) + Expect(eps).NotTo(BeNil(), reportNonConformant(fmt.Sprintf( + "an MCS EndpointSlice was not found on cluster %d. A K8s EndpointSlice is distinguished from an MCS EndpointSlice "+ + "via the %q label where the former has the value set to %q. If the MCS implementation does not use EndpointSlices, "+ + "you can specify a Ginkgo label filter using the %q label where appropriate to skip this test.", + i+1, discoveryv1.LabelManagedBy, K8sEndpointSliceManagedByName, EndpointSliceLabel))) + + endpointSlices[i] = eps + + Expect(eps.Labels).To(HaveKeyWithValue(v1alpha1.LabelServiceName, t.helloService.Name), + reportNonConformant(fmt.Sprintf("the MCS EndpointSlice %q does not contain the %q label referencing the service name", + eps.Name, v1alpha1.LabelServiceName))) + + Expect(eps.Labels).To(HaveKey(v1alpha1.LabelSourceCluster), + reportNonConformant(fmt.Sprintf("the MCS EndpointSlice %q does not contain the %q label", + eps.Name, v1alpha1.LabelSourceCluster))) + + Expect(eps.Labels).To(HaveKey(discoveryv1.LabelManagedBy), + reportNonConformant(fmt.Sprintf("the MCS EndpointSlice %q does not contain the %q label", + eps.Name, discoveryv1.LabelManagedBy))) + Expect(eps.Labels[discoveryv1.LabelManagedBy]).ToNot(Equal(K8sEndpointSliceManagedByName), + reportNonConformant(fmt.Sprintf("the MCS EndpointSlice's %q label must not reference %q", + discoveryv1.LabelManagedBy, K8sEndpointSliceManagedByName))) + } + + By("Unexporting the service") + + t.deleteServiceExport(&clients[0]) + + for i, client := range clients { + Eventually(func() bool { + _, err := client.k8s.DiscoveryV1().EndpointSlices(t.namespace).Get(ctx, endpointSlices[i].Name, metav1.GetOptions{}) + return apierrors.IsNotFound(err) + }, 20*time.Second, 100*time.Millisecond).Should(BeTrue(), + reportNonConformant(fmt.Sprintf("the EndpointSlice was not deleted on unexport from cluster %d", i+1))) + } + }) +}) + +func (t *testDriver) awaitMCSEndpointSlice(c *clusterClients) *discoveryv1.EndpointSlice { + var endpointSlice *discoveryv1.EndpointSlice + + hasLabel := func(eps *discoveryv1.EndpointSlice, label string) bool { + _, exists := eps.Labels[label] + return exists + } + + _ = wait.PollUntilContextTimeout(ctx, 100*time.Millisecond, + 20*time.Second, true, func(ctx context.Context) (bool, error) { + defer GinkgoRecover() + + list, err := c.k8s.DiscoveryV1().EndpointSlices(t.namespace).List(ctx, metav1.ListOptions{}) + Expect(err).ToNot(HaveOccurred(), "Error retrieving EndpointSlices") + + for i := range list.Items { + eps := &list.Items[i] + + if eps.Labels[discoveryv1.LabelManagedBy] != K8sEndpointSliceManagedByName || + hasLabel(eps, v1alpha1.LabelServiceName) || hasLabel(eps, v1alpha1.LabelSourceCluster) { + endpointSlice = eps + return true, nil + } + } + + return false, nil + }) + + return endpointSlice +} diff --git a/conformance/report.go b/conformance/report.go index 406491d..3364b19 100644 --- a/conformance/report.go +++ b/conformance/report.go @@ -37,6 +37,7 @@ const ( RequiredLabel = "Required" DNSLabel = "DNS" ClusterIPLabel = "ClusterIP" + EndpointSliceLabel = "EndpointSlice" SpecRefReportEntry = "spec-ref" NonConformantReportEntry = "non-conformant" )