Skip to content
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

Execute yaml examples via go tests #2541

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
261 changes: 261 additions & 0 deletions examples/examples_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,261 @@
// +build examples

/*
Copyright 2020 The Tekton 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 test

import (
"bytes"
"fmt"
"io/ioutil"
"path"
"path/filepath"
"runtime"
"strings"
"sync"
"testing"
"time"

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: extra space not needed 😝

"os"
"os/exec"

"github.com/tektoncd/pipeline/pkg/names"
knativetest "knative.dev/pkg/test"
)

const (
timeoutSeconds = 600
sleepBetween = 10

// we may want to consider either not running examples that require registry access
// or doing something more sophisticated to inject the right registry in when folks
// are executing the examples
horribleHardCodedRegistry = "gcr.io/christiewilson-catfactory"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Why not running a local registry (in the test namespace) ? (in any case, it would be a follow-up)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could be a good follow up! unless the test is deploying to a different namespace 🤔

)

// formatLogger is a printf style function for logging in tests.
type formatLogger func(template string, args ...interface{})

// cmd will run the command c with args and if input is provided, that will be piped
// into the process as input
func cmd(logf formatLogger, c string, args []string, input string) (string, error) {
// Remove braces from args when logging so users can see the complete call
logf("Executing %s %v", c, strings.Trim(fmt.Sprint(args), "[]"))

cmd := exec.Command(c, args...)
cmd.Env = os.Environ()

if input != "" {
cmd.Stdin = strings.NewReader(input)
}

var stderr, stdout bytes.Buffer
cmd.Stderr = &stderr
cmd.Stdout = &stdout
if err := cmd.Run(); err != nil {
logf("couldn't run command %s %v: %v, %s", c, args, err, stderr.String())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks to me like in case of error we do not get to see the stdout at all.
I think we should print out both out and err, perhaps in two different log commands.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tend to agree, if the command failed, I tend to want to see stderr and stdout.
gotest.tools/v3/icmd would come handy there 😝

return "", err
}

return stdout.String(), nil
}

// eraseClusterTasks will erase all cluster tasks which do not get cleaned up by namespace.
func eraseClusterTasks(logf formatLogger) {
if _, err := cmd(logf, "kubectl", []string{"delete", "--ignore-not-found=true", "clustertasks.tekton.dev", "--all"}, ""); err != nil {
logf("couldn't delete cluster tasks: %v", err)
}
}

// getYamls will look in the directory in examples indicated by version and run for yaml files
func getYamls(t *testing.T, version, run string) []string {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if some of this helpers could use a unit test... we do run them as part of the tests anyways, so they most likely all do that they are expected to :)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

haha probably!!!

t.Helper()
_, filename, _, _ := runtime.Caller(0)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason to make the test dependent on the test file ? 🤔 (and use runtime.Caller)


// Don't read recursively; the only dir within these dirs is no-ci which doesn't
// want any tests run against it
files, err := ioutil.ReadDir(path.Join(path.Dir(filename), version, run))
if err != nil {
t.Fatalf("Couldn't read yaml files from %s/%s/%s: %v", path.Dir(filename), version, run, err)
}
yamls := []string{}
for _, f := range files {
if matches, _ := filepath.Match("*.yaml", f.Name()); matches {
yamls = append(yamls, f.Name())
}
}
return yamls
}

// replaceDockerRepo will look in the content f and replace the hard coded docker
// repo with the on provided via the KO_DOCKER_REPO environment variable
func replaceDockerRepo(t *testing.T, f string) string {
t.Helper()
r := os.Getenv("KO_DOCKER_REPO")
if r == "" {
t.Fatalf("KO_DOCKER_REPO must be set")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we error out or skip ? (like we do on the e2e go tests — mainly to not break openshift-pipelines CI 😝 )

}
read, err := ioutil.ReadFile(f)
if err != nil {
t.Fatalf("couldnt read contents of %s: %v", f, err)
}
return strings.Replace(string(read), horribleHardCodedRegistry, r, -1)
}

// logRun will retrieve the entire yaml of run in namespace n and log it
func logRun(t *testing.T, n, run string) {
t.Helper()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

t.Helper might no be needed here (as it is called by other func that are already calling t.Helper) — see stretchr/testify#933

yaml, err := cmd(t.Logf, "kubectl", []string{"--namespace", n, "get", run, "-o", "yaml"}, "")
if err == nil {
t.Logf(yaml)
}
}

// pollRun will use kubectl to query the specified run in namespace n
// to see if it has completed. It will timeout after timeoutSeconds.
func pollRun(t *testing.T, n, run string, wg *sync.WaitGroup) {
t.Helper()
// instead of polling it might be faster to explore using --watch and parsing
// the output as it returns
for i := 0; i < (timeoutSeconds / sleepBetween); i++ {
status, err := cmd(t.Logf, "kubectl", []string{"--namespace", n, "get", run, "--output=jsonpath={.status.conditions[*].status}"}, "")
if err != nil {
t.Fatalf("couldnt get status of %s: %v", run, err)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

t.Fatalf does t.FailNow which calls runtime.Goexit, so wg.Done() seems unecessary 🙃

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This means it will quit the test on this error… If that's not what we want, we need to use t.Errorf.

wg.Done()
return
}

switch status {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In future we might want to allow for example metadata of some kind (annotations?) to specify an expected target status and more details about it.

case "", "Unknown":
// Not finished running yet
time.Sleep(sleepBetween * time.Second)
case "True":
t.Logf("%s completed successfully", run)
wg.Done()
return
default:
t.Errorf("%s has failed with status %s", run, status)
logRun(t, n, run)

wg.Done()
return
}
}
t.Errorf("%s did not complete within %d seconds", run, timeoutSeconds)
logRun(t, n, run)
wg.Done()
}

// waitForAllRuns will use kubectl to poll all runs in runs in namespace n
// until completed, failed, or timed out
func waitForAllRuns(t *testing.T, n string, runs []string) {
t.Helper()

var wg sync.WaitGroup
for _, run := range runs {
wg.Add(1)
go pollRun(t, n, run, &wg)
}
wg.Wait()
}

// getRuns will look for "run" in the provided ko output to determine the names
// of any runs created.
func getRuns(k string) []string {
runs := []string{}
// this a pretty naive way of looking for these run names, it would be an
// improvement to be more aware of the output format or to parse the yamls and
// use a client to apply them so we can get the name of the created runs via
// the client
for _, s := range strings.Split(k, "\n") {
name := strings.TrimSuffix(s, " created")
if strings.Contains(name, "run") {
runs = append(runs, name)
}
}
return runs
}

// createRandomNamespace will create a namespace with a randomized name so that anything
// happening within this namespace will not conflict with other namespaces. It will return
// the name of the namespace.
func createRandomNamespace(t *testing.T) string {
t.Helper()

n := names.SimpleNameGenerator.RestrictLengthWithRandomSuffix("irkalla")
_, err := cmd(t.Logf, "kubectl", []string{"create", "namespace", n}, "")
if err != nil {
t.Fatalf("could not create namespace %s for example: %v", n, err)
}
return n
}

// deleteNamespace will delete the namespace with kubectl.
func deleteNamespace(logf formatLogger, n string) {
_, err := cmd(logf, "kubectl", []string{"delete", "namespace", n}, "")
if err != nil {
logf("could not delete namespace %s for example: %v", n, err)
}
}

// runTests will use ko to create all yamls in the directory indicated by version
// and run, and wait for all runs (PipelineRuns and TaskRuns) created
func runTests(t *testing.T, version, run string) {
yamls := getYamls(t, version, run)
for _, yaml := range yamls {
y := yaml

t.Run(fmt.Sprintf("%s/%s", run, y), func(t *testing.T) {
t.Parallel()

// apply the yaml in its own namespace so it can run in parallel
// with similar tests (e.g. v1alpha1 + v1beta1) without conflicting with each other
// and we can easily cleanup.
n := createRandomNamespace(t)
knativetest.CleanupOnInterrupt(func() { deleteNamespace(t.Logf, n) }, t.Logf)
defer deleteNamespace(t.Logf, n)

t.Logf("Applying %s to namespace %s", y, n)
content := replaceDockerRepo(t, fmt.Sprintf("%s/%s/%s", version, run, y))
output, err := cmd(t.Logf, "ko", []string{"create", "--namespace", n, "-f", "-"}, content)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/ko/kubectl 🤔 ⁉️

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the existing scripts are using ko :O

if err == nil {
runs := getRuns(output)

if len(runs) == 0 {
t.Fatalf("no runs were created for %s, output %s", y, output)
}

t.Logf("Waiting for created runs %v", runs)
waitForAllRuns(t, n, runs)
}
})
}
}

func TestYaml(t *testing.T) {
versions := []string{"v1alpha1", "v1beta1"}
runs := []string{"taskruns", "pipelineruns"}

knativetest.CleanupOnInterrupt(func() { eraseClusterTasks(t.Logf) }, t.Logf)
defer eraseClusterTasks(t.Logf)

for _, version := range versions {
for _, run := range runs {
runTests(t, version, run)
}
}
}
4 changes: 2 additions & 2 deletions examples/v1alpha1/pipelineruns/clustertask-pipelinerun.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
apiVersion: tekton.dev/v1alpha1
kind: ClusterTask
metadata:
name: cluster-task-pipeline-4
name: cluster-task-pipeline-4-v1alpha1
spec:
steps:
- name: task-two-step-one
Expand All @@ -17,7 +17,7 @@ spec:
tasks:
- name: cluster-task-pipeline-4
taskRef:
name: cluster-task-pipeline-4
name: cluster-task-pipeline-4-v1alpha1
kind: ClusterTask
---
apiVersion: tekton.dev/v1alpha1
Expand Down
4 changes: 2 additions & 2 deletions examples/v1alpha1/taskruns/clustertask.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
apiVersion: tekton.dev/v1alpha1
kind: ClusterTask
metadata:
name: clustertask
name: clustertask-v1alpha1
spec:
steps:
- image: ubuntu
Expand All @@ -13,5 +13,5 @@ metadata:
generateName: clustertask-
spec:
taskRef:
name: clustertask
name: clustertask-v1alpha1
kind: ClusterTask
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
apiVersion: tekton.dev/v1alpha1
kind: ClusterTask
metadata:
name: clustertask-with-optional-resources
name: clustertask-with-optional-resources-v1alpha1
spec:
inputs:
resources:
Expand Down Expand Up @@ -31,5 +31,5 @@ metadata:
name: clustertask-without-resources
spec:
taskRef:
name: clustertask-with-optional-resources
name: clustertask-with-optional-resources-v1alpha1
kind: ClusterTask
4 changes: 2 additions & 2 deletions examples/v1beta1/pipelineruns/clustertask-pipelinerun.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
apiVersion: tekton.dev/v1beta1
kind: ClusterTask
metadata:
name: cluster-task-pipeline-4
name: cluster-task-pipeline-4-v1beta1
spec:
steps:
- name: task-two-step-one
Expand All @@ -17,7 +17,7 @@ spec:
tasks:
- name: cluster-task-pipeline-4
taskRef:
name: cluster-task-pipeline-4
name: cluster-task-pipeline-4-v1beta1
kind: ClusterTask
---
apiVersion: tekton.dev/v1beta1
Expand Down
4 changes: 2 additions & 2 deletions examples/v1beta1/taskruns/clustertask.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
apiVersion: tekton.dev/v1beta1
kind: ClusterTask
metadata:
name: clustertask
name: clustertask-v1beta1
spec:
steps:
- image: ubuntu
Expand All @@ -13,5 +13,5 @@ metadata:
generateName: clustertask-
spec:
taskRef:
name: clustertask
name: clustertask-v1beta1
kind: ClusterTask
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
apiVersion: tekton.dev/v1beta1
kind: ClusterTask
metadata:
name: clustertask-with-optional-resources
name: clustertask-with-optional-resources-v1beta1
spec:
params:
- name: filename
Expand Down Expand Up @@ -30,5 +30,5 @@ metadata:
name: clustertask-without-resources
spec:
taskRef:
name: clustertask-with-optional-resources
name: clustertask-with-optional-resources-v1beta1
kind: ClusterTask
Loading