-
Notifications
You must be signed in to change notification settings - Fork 218
[WIP][NE-2131] OTE - Implement initial framework and tests #1284
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
rikatz
wants to merge
11
commits into
openshift:master
Choose a base branch
from
rikatz:ote-local-utils
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+297,778
−509
Open
Changes from 3 commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
5abb6bc
[NE-2131] Add e2e framework
rikatz 92626cc
[NE-2131] Migrate GatewayAPI e2e test
rikatz 1913b15
[NE-2131] Migrate GatewayAPI Controller e2e test
rikatz fa45e54
[NE-2131] Add OTE binary
rikatz e886094
[NE-2131] Initial OTE work - vendor files
rikatz 2a3b89e
[NE-2131] Implement regular user client
rikatz 1ef54d9
[NE-2131] Fix specs to user the proper client construction
rikatz 54e4efc
[NE-2131] Add readme, implement test build
rikatz 0320130
[NE-2131] Vendor new dependencies
rikatz ce5e53c
[NE-2131] Remove gatewayapicontroller from serial tests
rikatz 9d664d3
test with ote names - should be dropped
rikatz File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,358 @@ | ||
| package framework | ||
|
|
||
| import ( | ||
| "bytes" | ||
| "context" | ||
| "fmt" | ||
| "io" | ||
| "os" | ||
| "os/exec" | ||
| "regexp" | ||
| "strings" | ||
|
|
||
| "github.com/onsi/ginkgo/v2" | ||
| configv1 "github.com/openshift/api/config/v1" | ||
| configv1client "github.com/openshift/client-go/config/clientset/versioned" | ||
| operatorv1client "github.com/openshift/client-go/operator/clientset/versioned" | ||
| ingressv1client "github.com/openshift/client-go/operatoringress/clientset/versioned" | ||
| corev1 "k8s.io/api/core/v1" | ||
| apiextensionsclient "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset" | ||
| metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||
| "k8s.io/apiserver/pkg/storage/names" | ||
| "k8s.io/client-go/kubernetes" | ||
| "k8s.io/client-go/rest" | ||
| admissionapi "k8s.io/pod-security-admission/api" | ||
| "sigs.k8s.io/controller-runtime/pkg/client" | ||
| gatewayapiv1client "sigs.k8s.io/gateway-api/pkg/client/clientset/versioned" | ||
| ) | ||
|
|
||
| type CLI struct { | ||
rikatz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| execPath string | ||
| adminConfigPath string | ||
| configPath string | ||
| commandArgs []string | ||
| globalArgs []string | ||
| finalArgs []string | ||
| env []string | ||
| addEnvVars map[string]string | ||
| verbose bool | ||
| verb string | ||
| username string | ||
| cleanupFunctions []cleanupFunc | ||
| namespace string | ||
| restCfg *rest.Config | ||
| hasError bool | ||
| kclient client.Client | ||
| openshiftVersion *configv1.ClusterVersion | ||
| stdin *bytes.Buffer | ||
| stdout io.Writer | ||
| stderr io.Writer | ||
| } | ||
|
|
||
| func NewCLI(project string, level admissionapi.Level, kclient client.Client, restcfg *rest.Config, isOpenshift bool) (*CLI, error) { | ||
| if project == "" { | ||
| return nil, fmt.Errorf("project name cannot be empty") | ||
| } | ||
| if kclient == nil { | ||
| return nil, fmt.Errorf("client cannot be empty") | ||
| } | ||
| if restcfg == nil { | ||
| return nil, fmt.Errorf("restcfg cannot be empty") | ||
| } | ||
|
|
||
| var cv *configv1.ClusterVersion | ||
| if isOpenshift { | ||
| c, err := configv1client.NewForConfig(restcfg) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| cv, err = c.ConfigV1().ClusterVersions().Get(context.Background(), "version", metav1.GetOptions{}) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| } | ||
|
|
||
| namespaceGeneratedName := names.SimpleNameGenerator.GenerateName(fmt.Sprintf("e2e-test-%s-", project)) | ||
| ns, nsCleanupFunc, err := CreateNamespace(context.Background(), namespaceGeneratedName, level, kclient) | ||
| if err != nil { | ||
| return &CLI{cleanupFunctions: []cleanupFunc{nsCleanupFunc}}, err | ||
| } | ||
|
|
||
| cli := &CLI{ | ||
| execPath: "oc", | ||
| username: "admin", | ||
| adminConfigPath: os.Getenv("KUBECONFIG"), | ||
| namespace: ns.GetName(), | ||
| cleanupFunctions: []cleanupFunc{nsCleanupFunc}, | ||
| restCfg: restcfg, | ||
| kclient: kclient, | ||
| openshiftVersion: cv, | ||
| } | ||
| return cli, nil | ||
|
|
||
| } | ||
|
|
||
| // Cleanup will execute the cleanup functions on a reverse order, from the latest | ||
| // function to the first one (Last in, first out) | ||
| // It should be added as an afterEach on each call | ||
| func (c *CLI) Cleanup(ctx context.Context, dumpNamespace bool) error { | ||
| if dumpNamespace { | ||
| Infof("Dumping events in namespace %q...", c.namespace) | ||
| eventList := &corev1.EventList{} | ||
| if err := c.kclient.List(context.TODO(), eventList, client.InNamespace(c.namespace)); err != nil { | ||
| WarnContextf("failed to list events for namespace %s: %v", c.namespace, err) | ||
| } else { | ||
| for _, e := range eventList.Items { | ||
| Infof("%s", fmt.Sprintln(e.FirstTimestamp, e.Source, e.InvolvedObject.Kind, e.InvolvedObject.Name, e.Reason, e.Message)) | ||
| } | ||
| } | ||
| } | ||
| for i := len(c.cleanupFunctions) - 1; i >= 0; i-- { | ||
| if err := c.cleanupFunctions[i](ctx); err != nil { | ||
| return err | ||
| } | ||
| } | ||
| return nil | ||
| } | ||
|
|
||
| // AllCapabilitiesEnabled returns true if all of the given capabilities are enabled on the cluster. | ||
| func (c *CLI) AllCapabilitiesEnabled(caps ...configv1.ClusterVersionCapability) (bool, error) { | ||
| cv, err := c.GetOpenshiftClusterVersion() | ||
| if err != nil { | ||
| return false, err | ||
| } | ||
|
|
||
| enabledCaps := make(map[configv1.ClusterVersionCapability]struct{}, len(cv.Status.Capabilities.EnabledCapabilities)) | ||
| for _, c := range cv.Status.Capabilities.EnabledCapabilities { | ||
| enabledCaps[c] = struct{}{} | ||
| } | ||
|
|
||
| for _, c := range caps { | ||
| if _, found := enabledCaps[c]; !found { | ||
| return false, nil | ||
| } | ||
| } | ||
|
|
||
| return true, nil | ||
| } | ||
|
|
||
| // Args sets the additional arguments for the OpenShift CLI command | ||
| func (c *CLI) Args(args ...string) *CLI { | ||
| c.commandArgs = args | ||
| return c | ||
| } | ||
|
|
||
| // AsAdmin changes current config file path to the admin config. | ||
| func (c *CLI) AsAdmin() *CLI { | ||
| nc := *c | ||
| nc.configPath = c.adminConfigPath | ||
| return &nc | ||
| } | ||
|
|
||
| func (c *CLI) AdminConfig() *rest.Config { | ||
| return c.restCfg | ||
| } | ||
|
|
||
| func (c *CLI) AdminApiextensionsClient() apiextensionsclient.Interface { | ||
| return apiextensionsclient.NewForConfigOrDie(c.restCfg) | ||
| } | ||
|
|
||
| func (c *CLI) AdminConfigClient() configv1client.Interface { | ||
| return configv1client.NewForConfigOrDie(c.restCfg) | ||
| } | ||
|
|
||
| // AdminGatewayApiClient provides a GatewayAPI client for the cluster admin user. | ||
| func (c *CLI) AdminGatewayApiClient() gatewayapiv1client.Interface { | ||
| return gatewayapiv1client.NewForConfigOrDie(c.restCfg) | ||
| } | ||
|
|
||
| func (c *CLI) AdminIngressClient() ingressv1client.Interface { | ||
| return ingressv1client.NewForConfigOrDie(c.AdminConfig()) | ||
| } | ||
|
|
||
| // AdminKubeClient provides a Kubernetes client for the cluster admin user. | ||
| func (c *CLI) AdminKubeClient() kubernetes.Interface { | ||
| return kubernetes.NewForConfigOrDie(c.restCfg) | ||
| } | ||
|
|
||
| func (c *CLI) AdminOperatorClient() operatorv1client.Interface { | ||
| return operatorv1client.NewForConfigOrDie(c.restCfg) | ||
| } | ||
|
|
||
| // TODO: this should be a user and not an admin, need to check! | ||
rikatz marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| // GatewayApiClient provides a GatewayAPI client for the current namespace user. | ||
| func (c *CLI) GatewayApiClient() gatewayapiv1client.Interface { | ||
| return gatewayapiv1client.NewForConfigOrDie(c.restCfg) | ||
| } | ||
|
|
||
| // KubeClient provides a Kubernetes client for the current namespace | ||
| func (c *CLI) KubeClient() kubernetes.Interface { | ||
| return kubernetes.NewForConfigOrDie(c.restCfg) | ||
| } | ||
|
|
||
| // END TODO | ||
|
|
||
| func (c *CLI) GetNamespace() string { | ||
| return c.namespace | ||
| } | ||
|
|
||
| func (c *CLI) GetOpenshiftClusterVersion() (*configv1.ClusterVersion, error) { | ||
| if c.openshiftVersion == nil { | ||
| return nil, fmt.Errorf("this is not an openshift cluster") | ||
| } | ||
| return c.openshiftVersion, nil | ||
| } | ||
|
|
||
| func (c *CLI) GetCurrentOpenshiftVersion() string { | ||
| if c.openshiftVersion == nil { | ||
| return "" | ||
| } | ||
| cv := c.openshiftVersion | ||
| for _, h := range c.openshiftVersion.Status.History { | ||
| if h.State == configv1.CompletedUpdate { | ||
| return h.Version | ||
| } | ||
| } | ||
| // Empty history should only occur if method is called early in startup before history is populated. | ||
| if len(cv.Status.History) != 0 { | ||
| return cv.Status.History[len(cv.Status.History)-1].Version | ||
| } | ||
| return "" | ||
| } | ||
|
|
||
| // Check for the existence of the okd-scos string in the version name to determine if it is OKD | ||
| func (c *CLI) IsOKD() bool { | ||
| current := c.GetCurrentOpenshiftVersion() | ||
| return strings.Contains(current, "okd-scos") | ||
| } | ||
|
|
||
| // Run executes given OpenShift CLI command verb (iow. "oc <verb>"). | ||
| // This function also override the default 'stdout' to redirect all output | ||
| // to a buffer and prepare the global flags such as namespace and config path. | ||
| func (c *CLI) Run(commands ...string) *CLI { | ||
| in, out, errout := &bytes.Buffer{}, &bytes.Buffer{}, &bytes.Buffer{} | ||
| nc := &CLI{ | ||
| execPath: c.execPath, | ||
| verb: commands[0], | ||
| adminConfigPath: c.adminConfigPath, | ||
| configPath: c.configPath, | ||
| username: c.username, | ||
| globalArgs: commands, | ||
| } | ||
| if len(c.configPath) > 0 { | ||
| nc.globalArgs = append([]string{fmt.Sprintf("--kubeconfig=%s", c.configPath)}, nc.globalArgs...) | ||
| } | ||
| /*if len(c.configPath) == 0 && len(c.token) > 0 { | ||
rikatz marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| nc.globalArgs = append([]string{fmt.Sprintf("--token=%s", c.token)}, nc.globalArgs...) | ||
| } | ||
| if !c.withoutNamespace { | ||
| nc.globalArgs = append([]string{fmt.Sprintf("--namespace=%s", c.Namespace())}, nc.globalArgs...) | ||
| }*/ | ||
| nc.stdin, nc.stdout, nc.stderr = in, out, errout | ||
| return nc.setOutput(c.stdout) | ||
| } | ||
|
|
||
| // Execute executes the current command and return error if the execution failed | ||
| // This function will set the default output to Ginkgo writer. | ||
| func (c *CLI) Execute() error { | ||
| out, err := c.Output() | ||
| if _, err := io.Copy(ginkgo.GinkgoWriter, strings.NewReader(out+"\n")); err != nil { | ||
| fmt.Fprintln(os.Stderr, "ERROR: Unable to copy the output to ginkgo writer") | ||
| } | ||
| os.Stdout.Sync() | ||
| return err | ||
| } | ||
|
|
||
| // Output executes the command and returns stdout/stderr combined into one string | ||
| func (c *CLI) Output() (string, error) { | ||
| var buff bytes.Buffer | ||
| _, _, err := c.outputs(&buff, &buff) | ||
| return strings.TrimSpace(string(buff.Bytes())), err | ||
| } | ||
|
|
||
| // setOutput allows to override the default command output | ||
| func (c *CLI) setOutput(out io.Writer) *CLI { | ||
| c.stdout = out | ||
| return c | ||
| } | ||
|
|
||
| func (c *CLI) outputs(stdOutBuff, stdErrBuff *bytes.Buffer) (string, string, error) { | ||
| cmd, err := c.start(stdOutBuff, stdErrBuff) | ||
| if err != nil { | ||
| return "", "", err | ||
| } | ||
| err = cmd.Wait() | ||
|
|
||
| stdOutBytes := stdOutBuff.Bytes() | ||
| stdErrBytes := stdErrBuff.Bytes() | ||
| stdOut := strings.TrimSpace(string(stdOutBytes)) | ||
| stdErr := strings.TrimSpace(string(stdErrBytes)) | ||
|
|
||
| switch err.(type) { | ||
| case nil: | ||
| c.stdout = bytes.NewBuffer(stdOutBytes) | ||
| c.stderr = bytes.NewBuffer(stdErrBytes) | ||
| return stdOut, stdErr, nil | ||
| case *exec.ExitError: | ||
| Infof("Error running %s %s:\nStdOut>\n%s\nStdErr>\n%s\n", c.execPath, RedactBearerToken(strings.Join(c.finalArgs, " ")), stdOut, stdErr) | ||
| wrappedErr := fmt.Errorf("Error running %s %s:\nStdOut>\n%s\nStdErr>\n%s\n%w\n", c.execPath, RedactBearerToken(strings.Join(c.finalArgs, " ")), stdOut[getStartingIndexForLastN(stdOutBytes, 4096):], stdErr[getStartingIndexForLastN(stdErrBytes, 4096):], err) | ||
| return stdOut, stdErr, wrappedErr | ||
| default: | ||
| FatalErr(fmt.Errorf("unable to execute %q: %v", c.execPath, err)) | ||
| // unreachable code | ||
| return "", "", nil | ||
| } | ||
| } | ||
|
|
||
| func (c *CLI) start(stdOutBuff, stdErrBuff *bytes.Buffer) (*exec.Cmd, error) { | ||
| c.finalArgs = append(c.globalArgs, c.commandArgs...) | ||
| if c.verbose { | ||
| fmt.Printf("DEBUG: oc %s\n", c.printCmd()) | ||
| } | ||
| cmd := exec.Command(c.execPath, c.finalArgs...) | ||
| cmd.Stdin = c.stdin | ||
| // Redact any bearer token information from the log. | ||
| Infof("Running '%s %s'", c.execPath, RedactBearerToken(strings.Join(c.finalArgs, " "))) | ||
|
|
||
| cmd.Env = c.env | ||
| if len(c.addEnvVars) > 0 { | ||
| // This is a nil check to allow setting empty environment with Env() | ||
| if cmd.Env == nil { | ||
| cmd.Env = os.Environ() | ||
| } | ||
| for name, value := range c.addEnvVars { | ||
| cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", name, value)) | ||
| } | ||
| } | ||
|
|
||
| cmd.Stdout = stdOutBuff | ||
| cmd.Stderr = stdErrBuff | ||
| err := cmd.Start() | ||
|
|
||
| return cmd, err | ||
| } | ||
|
|
||
| func (c *CLI) printCmd() string { | ||
| return strings.Join(c.finalArgs, " ") | ||
| } | ||
|
|
||
| func RedactBearerToken(args string) string { | ||
| if strings.Contains(args, "Authorization: Bearer") { | ||
| // redact bearer token | ||
| re := regexp.MustCompile(`Authorization:\s+Bearer.*\s+`) | ||
| args = re.ReplaceAllString(args, "Authorization: Bearer <redacted> ") | ||
| } | ||
| return args | ||
| } | ||
|
|
||
| // getStartingIndexForLastN calculates a byte offset in a byte slice such that when using | ||
| // that offset, we get the last N (size) bytes. | ||
| func getStartingIndexForLastN(byteString []byte, size int) int { | ||
| len := len(byteString) | ||
| if len < size { | ||
| // byte slice is less than size, so use all of it. | ||
| return 0 | ||
| } | ||
| return len - size | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.