e2e: Install and run workflow and verify the result (#661)

This enhances the E2E test suite introduced in #658 to also include the following steps:

- Install GitHub Actions workflow
- Trigger a workflow run via a git commit
- Verify the workflow run result

In the workflow, we use `kubectl create cm --from-literal` to create a configmap that contains an unique test ID. In the last step we obtain the configmap from within the E2E test and check the test ID to match the expected one.

To install a GitHub Actions workflow, we clone a GitHub repository denoted by the TEST_REPO envvar, progmatically generate a few files with some Go code, run `git-add`, `git-commit`, and then `git-push` to actually push the files to the repository. A single commit containing an updated workflow definition and an updated file seems to run a workflow derived to the definition introduced in the commit, which was a bit surpirising and useful behaviour.

At this point, the E2E test fully covers all the steps for a GitHub token based installation. We need to add scenarios for more deployment options, like GitHub App, RunnerDeployment, HRA, and so on. But each of them would worth another pull request.
This commit is contained in:
Yusuke Kuoka
2021-06-28 08:30:32 +09:00
committed by GitHub
parent 927d6f03ce
commit 7a305d2892
9 changed files with 427 additions and 54 deletions

112
testing/git.go Normal file
View File

@@ -0,0 +1,112 @@
package testing
import (
"context"
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
)
type GitRepo struct {
Dir string
Name string
CommitMessage string
Contents map[string][]byte
}
func (g *GitRepo) Sync(ctx context.Context) error {
repoName := g.Name
if repoName == "" {
return errors.New("missing git repo name")
}
repoURL := fmt.Sprintf("git@github.com:%s.git", repoName)
if g.Dir == "" {
return errors.New("missing git dir")
}
dir, err := filepath.Abs(g.Dir)
if err != nil {
return fmt.Errorf("error getting abs path for %q: %w", g.Dir, err)
}
if _, err := g.combinedOutput(g.gitCloneCmd(ctx, repoURL, dir)); err != nil {
return err
}
for path, content := range g.Contents {
absPath := filepath.Join(dir, path)
if err := os.WriteFile(absPath, content, 0755); err != nil {
return fmt.Errorf("error writing %s: %w", path, err)
}
if _, err := g.combinedOutput(g.gitAddCmd(ctx, dir, path)); err != nil {
return err
}
}
if _, err := g.combinedOutput(g.gitDiffCmd(ctx, dir)); err != nil {
if _, err := g.combinedOutput(g.gitCommitCmd(ctx, dir, g.CommitMessage)); err != nil {
return err
}
if _, err := g.combinedOutput(g.gitPushCmd(ctx, dir)); err != nil {
return err
}
}
return nil
}
func (g *GitRepo) gitCloneCmd(ctx context.Context, repo, dir string) *exec.Cmd {
return exec.CommandContext(ctx, "git", "clone", repo, dir)
}
func (g *GitRepo) gitDiffCmd(ctx context.Context, dir string) *exec.Cmd {
cmd := exec.CommandContext(ctx, "git", "diff", "--exit-code", "--cached")
cmd.Dir = dir
return cmd
}
func (g *GitRepo) gitAddCmd(ctx context.Context, dir, path string) *exec.Cmd {
cmd := exec.CommandContext(ctx, "git", "add", path)
cmd.Dir = dir
return cmd
}
func (g *GitRepo) gitCommitCmd(ctx context.Context, dir, msg string) *exec.Cmd {
cmd := exec.CommandContext(ctx, "git", "commit", "-m", msg)
cmd.Dir = dir
return cmd
}
func (g *GitRepo) gitPushCmd(ctx context.Context, dir string) *exec.Cmd {
cmd := exec.CommandContext(ctx, "git", "push", "origin", "main")
cmd.Dir = dir
return cmd
}
func (g *GitRepo) combinedOutput(cmd *exec.Cmd) (string, error) {
o, err := cmd.CombinedOutput()
if err != nil {
args := append([]string{}, cmd.Args...)
args[0] = cmd.Path
cs := strings.Join(args, " ")
s := string(o)
g.errorf("%s failed with output:\n%s", cs, s)
return s, err
}
return string(o), nil
}
func (g *GitRepo) errorf(f string, args ...interface{}) {
fmt.Fprintf(os.Stderr, f+"\n", args...)
}

View File

@@ -2,6 +2,7 @@ package testing
import (
"context"
"encoding/json"
"fmt"
"os"
"os/exec"
@@ -342,6 +343,55 @@ func (k *Cluster) RunKubectlEnsureNS(ctx context.Context, name string, cfg Kubec
return nil
}
func (k *Cluster) GetClusterRoleBinding(ctx context.Context, name string, cfg KubectlConfig) (string, error) {
o, err := k.combinedOutput(k.kubectlCmd(ctx, "get", []string{"clusterrolebinding", name}, cfg))
if err != nil {
return "", err
}
return o, nil
}
func (k *Cluster) CreateClusterRoleBindingServiceAccount(ctx context.Context, name string, clusterrole string, sa string, cfg KubectlConfig) error {
_, err := k.combinedOutput(k.kubectlCmd(ctx, "create", []string{"clusterrolebinding", name, "--clusterrole=" + clusterrole, "--serviceaccount=" + sa}, cfg))
if err != nil {
return err
}
return nil
}
func (k *Cluster) GetCMLiterals(ctx context.Context, name string, cfg KubectlConfig) (map[string]string, error) {
o, err := k.combinedOutput(k.kubectlCmd(ctx, "get", []string{"cm", name, "-o=json"}, cfg))
if err != nil {
return nil, err
}
var cm struct {
Data map[string]string `json:"data"`
}
if err := json.Unmarshal([]byte(o), &cm); err != nil {
k.errorf("Failed unmarshalling this data to JSON:\n%s\n", o)
return nil, fmt.Errorf("unmarshalling json: %w", err)
}
return cm.Data, nil
}
func (k *Cluster) CreateCMLiterals(ctx context.Context, name string, literals map[string]string, cfg KubectlConfig) error {
args := []string{"cm", name}
for k, v := range literals {
args = append(args, fmt.Sprintf("--from-literal=%s=%s", k, v))
}
if _, err := k.combinedOutput(k.kubectlCmd(ctx, "create", args, cfg)); err != nil {
return err
}
return nil
}
func (k *Cluster) Apply(ctx context.Context, path string, cfg KubectlConfig) error {
if _, err := k.combinedOutput(k.kubectlCmd(ctx, "apply", []string{"-f", path}, cfg)); err != nil {
return err

46
testing/workflow.go Normal file
View File

@@ -0,0 +1,46 @@
package testing
const (
ActionsCheckoutV2 = "actions/checkout@v2"
)
type Workflow struct {
Name string `json:"name"`
On On `json:"on"`
Jobs map[string]Job `json:"jobs"`
}
type On struct {
Push *Push `json:"push,omitempty"`
WorkflowDispatch *WorkflowDispatch `json:"workflow_dispatch,omitempty"`
}
type Push struct {
Branches []string `json:"branches,omitempty"`
}
type WorkflowDispatch struct {
Inputs map[string]InputSpec `json:"inputs,omitempty"`
}
type InputSpec struct {
Description string `json:"description,omitempty"`
Required bool `json:"required,omitempty"`
Default string `json:"default,omitempty"`
}
type Job struct {
RunsOn string `json:"runs-on"`
Steps []Step `json:"steps"`
}
type Step struct {
Name string `json:"name,omitempty"`
Uses string `json:"uses,omitempty"`
With *With `json:"with,omitempty"`
Run string `json:"run,omitempty"`
}
type With struct {
Version string `json:"version,omitempty"`
}