Skip to content

Commit

Permalink
Add support for end-users to create their own conformance profiles (#…
Browse files Browse the repository at this point in the history
…2864)

* Add support for end users to have their own conformance profiles

* gofumpt code

* fix copyright date

* allow multiple fs.FS to be provided

This lets downstreams to have their own tests with embedded yaml files

* add godoc
  • Loading branch information
dprotaso authored Apr 2, 2024
1 parent e1c2bf1 commit b150af2
Show file tree
Hide file tree
Showing 6 changed files with 215 additions and 173 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
}
152 changes: 152 additions & 0 deletions conformance/conformance.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
/*
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 (
"io/fs"
"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"

"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"
"sigs.k8s.io/yaml"
)

// DefaultOptions will parse command line flags to populate a
// ConformanceOptions struct. It will also initialize the various clients
// required by the tests.
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: []fs.FS{&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(),
}
}

// RunConformance will run the Gateway API Conformance tests
// using the default ConformanceOptions computed from command line flags.
func RunConformance(t *testing.T) {
RunConformanceWithOptions(t, DefaultOptions(t))
}

// RunConformanceWithOptions will run the Gateway API Conformance tests
// with the supplied options
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)
}
25 changes: 16 additions & 9 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,11 +337,18 @@ func getContentsFromPathOrURL(fs embed.FS, location string, timeoutConfig config
}
return manifests, nil
}
b, err := fs.ReadFile(location)
if err != nil {
return nil, err
var err error
var buf []byte
for _, mfs := range manifestFS {
buf, err = fs.ReadFile(mfs, location)
if err != nil && errors.Is(err, fs.ErrNotExist) {
continue
} else if err != nil {
return nil, err
}
return bytes.NewBuffer(buf), nil
}
return bytes.NewBuffer(b), nil
return nil, err
}

// convertGatewayAddrsToPrimitives converts a slice of Gateway addresses
Expand Down
Loading

0 comments on commit b150af2

Please sign in to comment.