Skip to content

Commit

Permalink
Add support for end users to have their own conformance profiles
Browse files Browse the repository at this point in the history
  • Loading branch information
dprotaso committed Mar 29, 2024
1 parent 18ad4ba commit 3b67c89
Show file tree
Hide file tree
Showing 6 changed files with 196 additions and 170 deletions.
23 changes: 23 additions & 0 deletions conformance/apis/v1/conformancereport.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ limitations under the License.
package v1

import (
"errors"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

Expand Down Expand Up @@ -79,3 +81,24 @@ type Implementation struct {
// page URL can be provided.
Contact []string `json:"contact"`
}

// Validate ensures that the Implementation struct has valid fields set
func (i *Implementation) Validate() error {
// TODO: add data validation https://github.com/kubernetes-sigs/gateway-api/issues/2178
if i.Organization == "" {
return errors.New("implementation's organization can not be empty")
}
if i.Project == "" {
return errors.New("implementation's project can not be empty")
}
if i.URL == "" {
return errors.New("implementation's url can not be empty")
}
if i.Version == "" {
return errors.New("implementation's version can not be empty")
}
if len(i.Contact) == 0 {
return errors.New("implementation's contact can not be empty")
}
return nil
}
143 changes: 143 additions & 0 deletions conformance/conformance.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
/*
Copyright 2022 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 (
"os"
"testing"

v1 "sigs.k8s.io/gateway-api/apis/v1"
"sigs.k8s.io/gateway-api/apis/v1alpha2"
"sigs.k8s.io/gateway-api/apis/v1beta1"
confv1 "sigs.k8s.io/gateway-api/conformance/apis/v1"
"sigs.k8s.io/gateway-api/conformance/tests"
conformanceconfig "sigs.k8s.io/gateway-api/conformance/utils/config"
"sigs.k8s.io/gateway-api/conformance/utils/flags"
"sigs.k8s.io/gateway-api/conformance/utils/suite"
"sigs.k8s.io/yaml"

"github.com/stretchr/testify/require"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
clientset "k8s.io/client-go/kubernetes"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/config"
)

func DefaultOptions(t *testing.T) suite.ConformanceOptions {
cfg, err := config.GetConfig()
require.NoError(t, err, "error loading Kubernetes config")
client, err := client.New(cfg, client.Options{})
require.NoError(t, err, "error initializing Kubernetes client")

// This clientset is needed in addition to the client only because
// controller-runtime client doesn't support non CRUD sub-resources yet (https://github.com/kubernetes-sigs/controller-runtime/issues/452).
clientset, err := clientset.NewForConfig(cfg)
require.NoError(t, err, "error initializing Kubernetes clientset")

require.NoError(t, v1alpha2.AddToScheme(client.Scheme()))
require.NoError(t, v1beta1.AddToScheme(client.Scheme()))
require.NoError(t, v1.AddToScheme(client.Scheme()))
require.NoError(t, apiextensionsv1.AddToScheme(client.Scheme()))

supportedFeatures := suite.ParseSupportedFeatures(*flags.SupportedFeatures)
exemptFeatures := suite.ParseSupportedFeatures(*flags.ExemptFeatures)
skipTests := suite.ParseSkipTests(*flags.SkipTests)
namespaceLabels := suite.ParseKeyValuePairs(*flags.NamespaceLabels)
namespaceAnnotations := suite.ParseKeyValuePairs(*flags.NamespaceAnnotations)
conformanceProfiles := suite.ParseConformanceProfiles(*flags.ConformanceProfiles)

implementation := suite.ParseImplementation(
*flags.ImplementationOrganization,
*flags.ImplementationProject,
*flags.ImplementationURL,
*flags.ImplementationVersion,
*flags.ImplementationContact,
)

return suite.ConformanceOptions{
AllowCRDsMismatch: *flags.AllowCRDsMismatch,
CleanupBaseResources: *flags.CleanupBaseResources,
Client: client,
Clientset: clientset,
ConformanceProfiles: conformanceProfiles,
Debug: *flags.ShowDebug,
EnableAllSupportedFeatures: *flags.EnableAllSupportedFeatures,
ExemptFeatures: exemptFeatures,
ManifestFS: &Manifests,
GatewayClassName: *flags.GatewayClassName,
Implementation: implementation,
Mode: *flags.Mode,
NamespaceAnnotations: namespaceAnnotations,
NamespaceLabels: namespaceLabels,
ReportOutputPath: *flags.ReportOutput,
RestConfig: cfg,
RunTest: *flags.RunTest,
SkipTests: skipTests,
SupportedFeatures: supportedFeatures,
TimeoutConfig: conformanceconfig.DefaultTimeoutConfig(),
}
}

func RunConformance(t *testing.T) {
RunConformanceWithOptions(t, DefaultOptions(t))
}

func RunConformanceWithOptions(t *testing.T, opts suite.ConformanceOptions) {
if err := opts.Implementation.Validate(); err != nil && opts.ReportOutputPath != "" {
require.NoError(t, err, "supplied Implementation details are not valid")
}

t.Log("Running conformance tests with:")
logOptions(t, opts)

cSuite, err := suite.NewConformanceTestSuite(opts)
require.NoError(t, err, "error initializing conformance suite")

cSuite.Setup(t)
err = cSuite.Run(t, tests.ConformanceTests)
require.NoError(t, err)

if opts.ReportOutputPath != "" {
report, err := cSuite.Report()
require.NoError(t, err, "error generating conformance profile report")
require.NoError(t, writeReport(t.Logf, *report, opts.ReportOutputPath), "error writing report")
}
}

func logOptions(t *testing.T, opts suite.ConformanceOptions) {
t.Logf(" GatewayClass: %s", opts.GatewayClassName)
t.Logf(" Cleanup Resources: %t", opts.CleanupBaseResources)
t.Logf(" Debug: %t", opts.Debug)
t.Logf(" Enable All Features: %t", opts.EnableAllSupportedFeatures)
t.Logf(" Supported Features: %v", opts.SupportedFeatures.UnsortedList())
t.Logf(" ExemptFeatures: %v", opts.ExemptFeatures.UnsortedList())
}

func writeReport(logf func(string, ...any), report confv1.ConformanceReport, output string) error {
rawReport, err := yaml.Marshal(report)
if err != nil {
return err
}

if output != "" {
if err = os.WriteFile(output, rawReport, 0o600); err != nil {
return err
}
}
logf("Conformance report:\n%s", string(rawReport))
return nil
}
131 changes: 2 additions & 129 deletions conformance/conformance_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,138 +17,11 @@ limitations under the License.
package conformance_test

import (
"os"
"testing"

apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/config"
"sigs.k8s.io/yaml"

gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
gatewayv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1"
confv1 "sigs.k8s.io/gateway-api/conformance/apis/v1"
"sigs.k8s.io/gateway-api/conformance/tests"
"sigs.k8s.io/gateway-api/conformance/utils/flags"
"sigs.k8s.io/gateway-api/conformance/utils/suite"
)

var (
cfg *rest.Config
k8sClientset *kubernetes.Clientset
mgrClient client.Client
supportedFeatures sets.Set[suite.SupportedFeature]
exemptFeatures sets.Set[suite.SupportedFeature]
namespaceLabels map[string]string
namespaceAnnotations map[string]string
implementation *confv1.Implementation
mode string
allowCRDsMismatch bool
conformanceProfiles sets.Set[suite.ConformanceProfileName]
skipTests []string
"sigs.k8s.io/gateway-api/conformance"
)

func TestConformance(t *testing.T) {
var err error
cfg, err = config.GetConfig()
if err != nil {
t.Fatalf("Error loading Kubernetes config: %v", err)
}
mgrClient, err = client.New(cfg, client.Options{})
if err != nil {
t.Fatalf("Error initializing Kubernetes client: %v", err)
}
k8sClientset, err = kubernetes.NewForConfig(cfg)
if err != nil {
t.Fatalf("Error initializing Kubernetes REST client: %v", err)
}

gatewayv1alpha2.AddToScheme(mgrClient.Scheme())
gatewayv1beta1.AddToScheme(mgrClient.Scheme())
gatewayv1.AddToScheme(mgrClient.Scheme())
apiextensionsv1.AddToScheme(mgrClient.Scheme())

// conformance flags
supportedFeatures = suite.ParseSupportedFeatures(*flags.SupportedFeatures)
exemptFeatures = suite.ParseSupportedFeatures(*flags.ExemptFeatures)
skipTests = suite.ParseSkipTests(*flags.SkipTests)
namespaceLabels = suite.ParseKeyValuePairs(*flags.NamespaceLabels)
namespaceAnnotations = suite.ParseKeyValuePairs(*flags.NamespaceAnnotations)
conformanceProfiles = suite.ParseConformanceProfiles(*flags.ConformanceProfiles)
if len(conformanceProfiles) == 0 {
t.Fatal("conformance profiles need to be given")
}
mode = *flags.Mode
allowCRDsMismatch = *flags.AllowCRDsMismatch

implementation, err = suite.ParseImplementation(
*flags.ImplementationOrganization,
*flags.ImplementationProject,
*flags.ImplementationURL,
*flags.ImplementationVersion,
*flags.ImplementationContact,
)
if err != nil {
t.Fatalf("Error parsing implementation's details: %v", err)
}
testConformance(t)
}

func testConformance(t *testing.T) {
t.Logf("Running conformance tests with %s GatewayClass\n cleanup: %t\n debug: %t\n enable all features: %t \n supported features: [%v]\n exempt features: [%v]",
*flags.GatewayClassName, *flags.CleanupBaseResources, *flags.ShowDebug, *flags.EnableAllSupportedFeatures, *flags.SupportedFeatures, *flags.ExemptFeatures)

cSuite, err := suite.NewConformanceTestSuite(
suite.ConformanceOptions{
Client: mgrClient,
RestConfig: cfg,
// This clientset is needed in addition to the client only because
// controller-runtime client doesn't support non CRUD sub-resources yet (https://github.com/kubernetes-sigs/controller-runtime/issues/452).
Clientset: k8sClientset,
GatewayClassName: *flags.GatewayClassName,
Debug: *flags.ShowDebug,
CleanupBaseResources: *flags.CleanupBaseResources,
SupportedFeatures: supportedFeatures,
ExemptFeatures: exemptFeatures,
EnableAllSupportedFeatures: *flags.EnableAllSupportedFeatures,
NamespaceLabels: namespaceLabels,
NamespaceAnnotations: namespaceAnnotations,
SkipTests: skipTests,
RunTest: *flags.RunTest,
Mode: mode,
AllowCRDsMismatch: allowCRDsMismatch,
Implementation: *implementation,
ConformanceProfiles: conformanceProfiles,
})
if err != nil {
t.Fatalf("error creating conformance test suite: %v", err)
}

cSuite.Setup(t)
cSuite.Run(t, tests.ConformanceTests)
report, err := cSuite.Report()
if err != nil {
t.Fatalf("error generating conformance profile report: %v", err)
}
writeReport(t.Logf, *report, *flags.ReportOutput)
}

func writeReport(logf func(string, ...any), report confv1.ConformanceReport, output string) error {
rawReport, err := yaml.Marshal(report)
if err != nil {
return err
}

if output != "" {
if err = os.WriteFile(output, rawReport, 0o600); err != nil {
return err
}
}
logf("Conformance report:\n%s", string(rawReport))

return nil
conformance.RunConformance(t)
}
12 changes: 6 additions & 6 deletions conformance/utils/kubernetes/apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@ package kubernetes
import (
"bytes"
"context"
"embed"
"errors"
"fmt"
"io"
"io/fs"
"net/http"
"strings"
"testing"
Expand Down Expand Up @@ -51,8 +51,8 @@ type Applier struct {
// ControllerName will be used as the spec.controllerName when applying GatewayClass resources
ControllerName string

// FS is the filesystem to use when reading manifests.
FS embed.FS
// ManifestFS is the filesystem to use when reading manifests.
ManifestFS fs.FS

// UsableNetworkAddresses is a list of addresses that are expected to be
// supported AND usable for Gateways in the underlying implementation.
Expand Down Expand Up @@ -245,7 +245,7 @@ func (a Applier) MustApplyObjectsWithCleanup(t *testing.T, c client.Client, time
// provided YAML file and registers a cleanup function for resources it created.
// Note that this does not remove resources that already existed in the cluster.
func (a Applier) MustApplyWithCleanup(t *testing.T, c client.Client, timeoutConfig config.TimeoutConfig, location string, cleanup bool) {
data, err := getContentsFromPathOrURL(a.FS, location, timeoutConfig)
data, err := getContentsFromPathOrURL(a.ManifestFS, location, timeoutConfig)
require.NoError(t, err)

decoder := yaml.NewYAMLOrJSONDecoder(data, 4096)
Expand Down Expand Up @@ -308,7 +308,7 @@ func (a Applier) MustApplyWithCleanup(t *testing.T, c client.Client, timeoutConf

// getContentsFromPathOrURL takes a string that can either be a local file
// path or an https:// URL to YAML manifests and provides the contents.
func getContentsFromPathOrURL(fs embed.FS, location string, timeoutConfig config.TimeoutConfig) (*bytes.Buffer, error) {
func getContentsFromPathOrURL(manifestFS fs.FS, location string, timeoutConfig config.TimeoutConfig) (*bytes.Buffer, error) {
if strings.HasPrefix(location, "http://") {
return nil, fmt.Errorf("data can't be retrieved from %s: http is not supported, use https", location)
} else if strings.HasPrefix(location, "https://") {
Expand Down Expand Up @@ -337,7 +337,7 @@ func getContentsFromPathOrURL(fs embed.FS, location string, timeoutConfig config
}
return manifests, nil
}
b, err := fs.ReadFile(location)
b, err := fs.ReadFile(manifestFS, location)
if err != nil {
return nil, err
}
Expand Down
10 changes: 10 additions & 0 deletions conformance/utils/suite/profiles.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,16 @@ var (
}
)

// RegisterConformanceProfile allows downstream tests to register unique profiles that
// define their own set of features
func RegisterConformanceProfile(p ConformanceProfile) {
_, ok := conformanceProfileMap[p.Name]
if ok {
panic(fmt.Sprintf("ConformanceProfile named %q is already registered", p.Name))
}
conformanceProfileMap[p.Name] = p
}

// -----------------------------------------------------------------------------
// Conformance Profiles - Private Profile Mapping Helpers
// -----------------------------------------------------------------------------
Expand Down
Loading

0 comments on commit 3b67c89

Please sign in to comment.