From 7dca18f47004850a407823406823f299bb63dcf6 Mon Sep 17 00:00:00 2001 From: Stephen Benjamin Date: Tue, 18 Nov 2025 09:11:40 -0500 Subject: [PATCH] Implement config verbs for OTE --- cmd/example-tests/main.go | 16 +++++ pkg/cmd/cmd.go | 2 + pkg/cmd/cmdconfig/apply.go | 65 ++++++++++++++++++ pkg/cmd/cmdconfig/config.go | 23 +++++++ pkg/cmd/cmdconfig/remove.go | 65 ++++++++++++++++++ pkg/cmd/cmdrun/runsuite.go | 130 ++++++++++++++++++++++++++++++++++-- pkg/extension/extension.go | 5 ++ pkg/extension/types.go | 17 +++++ pkg/ginkgo/util.go | 4 ++ test/example/example.go | 4 ++ 10 files changed, 327 insertions(+), 4 deletions(-) create mode 100644 pkg/cmd/cmdconfig/apply.go create mode 100644 pkg/cmd/cmdconfig/config.go create mode 100644 pkg/cmd/cmdconfig/remove.go diff --git a/cmd/example-tests/main.go b/cmd/example-tests/main.go index 7187656..15182e1 100644 --- a/cmd/example-tests/main.go +++ b/cmd/example-tests/main.go @@ -1,6 +1,7 @@ package main import ( + "context" "fmt" "os" @@ -9,6 +10,7 @@ import ( "github.com/openshift-eng/openshift-tests-extension/pkg/cmd" e "github.com/openshift-eng/openshift-tests-extension/pkg/extension" + "github.com/openshift-eng/openshift-tests-extension/pkg/flags" g "github.com/openshift-eng/openshift-tests-extension/pkg/ginkgo" // If using ginkgo, import your tests here @@ -48,6 +50,20 @@ func main() { }, }) + // Register configs that tests may require + ext.RegisterConfig(e.Config{ + Name: "example-config", + Description: "An example configuration demonstrating the config feature", + Apply: func(ctx context.Context, envFlags flags.EnvironmentalFlags) error { + fmt.Fprintf(os.Stderr, "Example config applied (platform: %s)\n", envFlags.Platform) + return nil + }, + Remove: func(ctx context.Context, envFlags flags.EnvironmentalFlags) error { + fmt.Fprintf(os.Stderr, "Example config removed\n") + return nil + }, + }) + // If using Ginkgo, build test specs automatically specs, err := g.BuildExtensionTestSpecsFromOpenShiftGinkgoSuite() if err != nil { diff --git a/pkg/cmd/cmd.go b/pkg/cmd/cmd.go index 2db8cfa..873d5a0 100644 --- a/pkg/cmd/cmd.go +++ b/pkg/cmd/cmd.go @@ -3,6 +3,7 @@ package cmd import ( "github.com/spf13/cobra" + "github.com/openshift-eng/openshift-tests-extension/pkg/cmd/cmdconfig" "github.com/openshift-eng/openshift-tests-extension/pkg/cmd/cmdimages" "github.com/openshift-eng/openshift-tests-extension/pkg/cmd/cmdinfo" "github.com/openshift-eng/openshift-tests-extension/pkg/cmd/cmdlist" @@ -19,5 +20,6 @@ func DefaultExtensionCommands(registry *extension.Registry) []*cobra.Command { cmdinfo.NewInfoCommand(registry), cmdupdate.NewUpdateCommand(registry), cmdimages.NewImagesCommand(registry), + cmdconfig.NewConfigCommand(registry), } } diff --git a/pkg/cmd/cmdconfig/apply.go b/pkg/cmd/cmdconfig/apply.go new file mode 100644 index 0000000..06dd682 --- /dev/null +++ b/pkg/cmd/cmdconfig/apply.go @@ -0,0 +1,65 @@ +package cmdconfig + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + + "github.com/openshift-eng/openshift-tests-extension/pkg/extension" + "github.com/openshift-eng/openshift-tests-extension/pkg/flags" +) + +func NewApplyCommand(registry *extension.Registry) *cobra.Command { + componentFlags := flags.NewComponentFlags() + envFlags := flags.NewEnvironmentalFlags() + var configName string + + cmd := &cobra.Command{ + Use: "apply", + Short: "Apply a configuration", + SilenceUsage: true, + RunE: func(cmd *cobra.Command, args []string) error { + ext := registry.Get(componentFlags.Component) + if ext == nil { + return fmt.Errorf("couldn't find the component %q", componentFlags.Component) + } + + if configName == "" { + return fmt.Errorf("--config flag is required") + } + + // Find the config by name + var config *extension.Config + for i := range ext.Configs { + if ext.Configs[i].Name == configName { + config = &ext.Configs[i] + break + } + } + + if config == nil { + return fmt.Errorf("config %q not found", configName) + } + + if config.Apply == nil { + return fmt.Errorf("config %q does not have an Apply function", configName) + } + + ctx := context.Background() + if err := config.Apply(ctx, *envFlags); err != nil { + return fmt.Errorf("failed to apply config %q: %w", configName, err) + } + + fmt.Printf("Successfully applied config %q\n", configName) + return nil + }, + } + + componentFlags.BindFlags(cmd.Flags()) + envFlags.BindFlags(cmd.Flags()) + cmd.Flags().StringVar(&configName, "config", "", "Name of the configuration to apply") + + return cmd +} + diff --git a/pkg/cmd/cmdconfig/config.go b/pkg/cmd/cmdconfig/config.go new file mode 100644 index 0000000..a076f75 --- /dev/null +++ b/pkg/cmd/cmdconfig/config.go @@ -0,0 +1,23 @@ +package cmdconfig + +import ( + "github.com/spf13/cobra" + + "github.com/openshift-eng/openshift-tests-extension/pkg/extension" +) + +func NewConfigCommand(registry *extension.Registry) *cobra.Command { + cmd := &cobra.Command{ + Use: "config", + Short: "Manage extension configurations", + SilenceUsage: true, + } + + cmd.AddCommand( + NewApplyCommand(registry), + NewRemoveCommand(registry), + ) + + return cmd +} + diff --git a/pkg/cmd/cmdconfig/remove.go b/pkg/cmd/cmdconfig/remove.go new file mode 100644 index 0000000..85fa0cb --- /dev/null +++ b/pkg/cmd/cmdconfig/remove.go @@ -0,0 +1,65 @@ +package cmdconfig + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + + "github.com/openshift-eng/openshift-tests-extension/pkg/extension" + "github.com/openshift-eng/openshift-tests-extension/pkg/flags" +) + +func NewRemoveCommand(registry *extension.Registry) *cobra.Command { + componentFlags := flags.NewComponentFlags() + envFlags := flags.NewEnvironmentalFlags() + var configName string + + cmd := &cobra.Command{ + Use: "remove", + Short: "Remove a configuration", + SilenceUsage: true, + RunE: func(cmd *cobra.Command, args []string) error { + ext := registry.Get(componentFlags.Component) + if ext == nil { + return fmt.Errorf("couldn't find the component %q", componentFlags.Component) + } + + if configName == "" { + return fmt.Errorf("--config flag is required") + } + + // Find the config by name + var config *extension.Config + for i := range ext.Configs { + if ext.Configs[i].Name == configName { + config = &ext.Configs[i] + break + } + } + + if config == nil { + return fmt.Errorf("config %q not found", configName) + } + + if config.Remove == nil { + return fmt.Errorf("config %q does not have a Remove function", configName) + } + + ctx := context.Background() + if err := config.Remove(ctx, *envFlags); err != nil { + return fmt.Errorf("failed to remove config %q: %w", configName, err) + } + + fmt.Printf("Successfully removed config %q\n", configName) + return nil + }, + } + + componentFlags.BindFlags(cmd.Flags()) + envFlags.BindFlags(cmd.Flags()) + cmd.Flags().StringVar(&configName, "config", "", "Name of the configuration to remove") + + return cmd +} + diff --git a/pkg/cmd/cmdrun/runsuite.go b/pkg/cmd/cmdrun/runsuite.go index d81d07c..1e3f47e 100644 --- a/pkg/cmd/cmdrun/runsuite.go +++ b/pkg/cmd/cmdrun/runsuite.go @@ -5,6 +5,7 @@ import ( "fmt" "os" "os/signal" + "strings" "syscall" "time" @@ -14,6 +15,7 @@ import ( "github.com/openshift-eng/openshift-tests-extension/pkg/extension" "github.com/openshift-eng/openshift-tests-extension/pkg/extension/extensiontests" "github.com/openshift-eng/openshift-tests-extension/pkg/flags" + "github.com/openshift-eng/openshift-tests-extension/pkg/util/sets" ) func NewRunSuiteCommand(registry *extension.Registry) *cobra.Command { @@ -21,11 +23,13 @@ func NewRunSuiteCommand(registry *extension.Registry) *cobra.Command { componentFlags *flags.ComponentFlags outputFlags *flags.OutputFlags concurrencyFlags *flags.ConcurrencyFlags + envFlags *flags.EnvironmentalFlags junitPath string }{ componentFlags: flags.NewComponentFlags(), outputFlags: flags.NewOutputFlags(), concurrencyFlags: flags.NewConcurrencyFlags(), + envFlags: flags.NewEnvironmentalFlags(), junitPath: "", } @@ -97,18 +101,136 @@ func NewRunSuiteCommand(registry *extension.Registry) *cobra.Command { } compositeWriter.AddWriter(jsonWriter) - specs, err := ext.GetSpecs().Filter(suite.Qualifiers) - if err != nil { - return errors.Wrap(err, "couldn't filter specs") + specs, err := ext.GetSpecs().Filter(suite.Qualifiers) + if err != nil { + return errors.Wrap(err, "couldn't filter specs") + } + + // Group specs by required configs + specsByConfig := groupSpecsByConfig(specs) + + // Track overall errors + var runErrors []error + + // First, run specs that don't require any config + if noConfigSpecs, ok := specsByConfig[""]; ok && len(noConfigSpecs) > 0 { + fmt.Fprintf(os.Stderr, "Running %d test(s) without config requirements...\n", len(noConfigSpecs)) + if err := noConfigSpecs.Run(ctx, compositeWriter, opts.concurrencyFlags.MaxConcurency); err != nil { + runErrors = append(runErrors, err) } + delete(specsByConfig, "") + } + + // Then run specs grouped by config + for configName, configSpecs := range specsByConfig { + if err := runSpecsWithConfig( + ctx, + ext, + configName, + configSpecs, + compositeWriter, + opts.concurrencyFlags.MaxConcurency, + *opts.envFlags, + ); err != nil { + runErrors = append(runErrors, err) + } + } + + // Return combined errors if any + if len(runErrors) > 0 { + return fmt.Errorf("%d test group(s) failed", len(runErrors)) + } - return specs.Run(ctx, compositeWriter, opts.concurrencyFlags.MaxConcurency) + return nil }, } opts.componentFlags.BindFlags(cmd.Flags()) opts.outputFlags.BindFlags(cmd.Flags()) opts.concurrencyFlags.BindFlags(cmd.Flags()) + opts.envFlags.BindFlags(cmd.Flags()) cmd.Flags().StringVarP(&opts.junitPath, "junit-path", "j", opts.junitPath, "write results to junit XML") return cmd } + +// extractRequiredConfigs extracts the set of config names required by the given specs +func extractRequiredConfigs(specs extensiontests.ExtensionTestSpecs) sets.Set[string] { + configs := sets.New[string]() + for _, spec := range specs { + for label := range spec.Labels { + if strings.HasPrefix(label, "Config:") { + configName := strings.TrimPrefix(label, "Config:") + configs.Insert(configName) + } + } + } + return configs +} + +// groupSpecsByConfig groups specs by the configs they require. Specs with no config requirement +// are grouped under an empty string key. +func groupSpecsByConfig(specs extensiontests.ExtensionTestSpecs) map[string]extensiontests.ExtensionTestSpecs { + groups := make(map[string]extensiontests.ExtensionTestSpecs) + + for _, spec := range specs { + var configName string + // Find the config requirement for this spec + for label := range spec.Labels { + if strings.HasPrefix(label, "Config:") { + configName = strings.TrimPrefix(label, "Config:") + break + } + } + + // Group by config name (empty string for no config) + groups[configName] = append(groups[configName], spec) + } + + return groups +} + +// runSpecsWithConfig applies a config, runs the specs, then removes the config +func runSpecsWithConfig( + ctx context.Context, + ext *extension.Extension, + configName string, + specs extensiontests.ExtensionTestSpecs, + writer extensiontests.ResultWriter, + maxConcurrency int, + envFlags flags.EnvironmentalFlags, +) error { + // Find the config + var config *extension.Config + for i := range ext.Configs { + if ext.Configs[i].Name == configName { + config = &ext.Configs[i] + break + } + } + + if config == nil { + return fmt.Errorf("config %q not found but required by tests", configName) + } + + // Apply the config + if config.Apply != nil { + fmt.Fprintf(os.Stderr, "Applying config %q for %d test(s)...\n", configName, len(specs)) + if err := config.Apply(ctx, envFlags); err != nil { + return fmt.Errorf("failed to apply config %q: %w", configName, err) + } + } + + // Run the specs + err := specs.Run(ctx, writer, maxConcurrency) + + // Remove the config + if config.Remove != nil { + fmt.Fprintf(os.Stderr, "Removing config %q...\n", configName) + if removeErr := config.Remove(ctx, envFlags); removeErr != nil { + // Log warning but don't fail the overall run + fmt.Fprintf(os.Stderr, "Warning: failed to remove config %q: %v\n", configName, removeErr) + } + } + + return err +} diff --git a/pkg/extension/extension.go b/pkg/extension/extension.go index b9fbfb2..eb70c54 100644 --- a/pkg/extension/extension.go +++ b/pkg/extension/extension.go @@ -135,6 +135,11 @@ func (e *Extension) RegisterImage(image Image) *Extension { return e } +func (e *Extension) RegisterConfig(config Config) *Extension { + e.Configs = append(e.Configs, config) + return e +} + func (e *Extension) FindSpecsByName(names ...string) (et.ExtensionTestSpecs, error) { var specs et.ExtensionTestSpecs var notFound []string diff --git a/pkg/extension/types.go b/pkg/extension/types.go index 00d2d9d..ddfe5f0 100644 --- a/pkg/extension/types.go +++ b/pkg/extension/types.go @@ -1,9 +1,11 @@ package extension import ( + "context" "time" "github.com/openshift-eng/openshift-tests-extension/pkg/extension/extensiontests" + "github.com/openshift-eng/openshift-tests-extension/pkg/flags" "github.com/openshift-eng/openshift-tests-extension/pkg/util/sets" ) @@ -20,6 +22,8 @@ type Extension struct { Images []Image `json:"images"` + Configs []Config `json:"configs"` + // Private data specs extensiontests.ExtensionTestSpecs obsoleteTests sets.Set[string] @@ -92,3 +96,16 @@ type Image struct { // This field should be populated if the mirrored image reference is predetermined by the test extensions. Mapped *Image `json:"mapped,omitempty"` } + +// Config represents a configuration that can be applied and removed by an extension. +// Configs can be required by tests and will be automatically managed during test execution. +type Config struct { + Name string `json:"name"` + Description string `json:"description"` + + // Apply is called to apply the configuration. It receives context and environmental flags. + Apply func(context.Context, flags.EnvironmentalFlags) error `json:"-"` + + // Remove is called to remove the configuration. It receives context and environmental flags. + Remove func(context.Context, flags.EnvironmentalFlags) error `json:"-"` +} diff --git a/pkg/ginkgo/util.go b/pkg/ginkgo/util.go index e970d46..3cad299 100644 --- a/pkg/ginkgo/util.go +++ b/pkg/ginkgo/util.go @@ -179,6 +179,10 @@ func Blocking() ginkgo.Labels { return ginkgo.Label(fmt.Sprintf("Lifecycle:%s", ext.LifecycleBlocking)) } +func RequiresConfig(name string) ginkgo.Labels { + return ginkgo.Label(fmt.Sprintf("Config:%s", name)) +} + func GetLifecycle(labels ginkgo.Labels) ext.Lifecycle { for _, label := range labels { res := strings.Split(label, ":") diff --git a/test/example/example.go b/test/example/example.go index c3bba5d..2608789 100644 --- a/test/example/example.go +++ b/test/example/example.go @@ -55,4 +55,8 @@ var _ = Describe("[sig-testing] openshift-tests-extension", func() { It("should support test-skips via environment flags", func() { Expect(true).To(BeTrue()) //This doesn't need to do anything special, just exist }) + + It("should support tests requiring config", g.RequiresConfig("example-config"), func() { + Expect(true).To(BeTrue()) + }) })