Skip to content

Commit

Permalink
Execute yaml examples via go tests
Browse files Browse the repository at this point in the history
In tektoncd#2540 we are seeing that some yaml tests are timing out, but it's
hard to see what yaml tests are failing. This commit moves the logic out
of bash and into individual go tests - now we will run an individual go
test for each yaml example, completing all v1alpha1 before all v1beta1
and cleaning up in between. The output will still be challenging to read
since it will be interleaved, however the failures should at least
be associated with a specific yaml file.

This also makes it easier to run all tests locally, though if you
interrupt the tests you end up with your cluster in a bad state and it
might be good to update these to execute each example in a separate
namespace (in which case we could run all of v1alpha1 and v1beta1 at the
same time as well!)
  • Loading branch information
bobcatfish committed May 21, 2020
1 parent e83aa5f commit 2fb6e49
Show file tree
Hide file tree
Showing 3 changed files with 243 additions and 83 deletions.
241 changes: 241 additions & 0 deletions examples/yaml_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
/*
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"

"os"
"os/exec"
)

const (
timeoutSeconds = 600
sleepBetween = 10
)

// cmd will run the command c with args and if input is provided, that will be piped
// into the process as input
func cmd(t *testing.T, c string, args []string, input string) (string, error) {
t.Helper()
binary, err := exec.LookPath(c)
if err != nil {
t.Fatalf("couldn't find %s, plz install", c)
}

t.Logf("Executing %s %v", binary, args)

cmd := exec.Command(binary, 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 {
t.Fatalf("couldn't run command %s %v: %v, %s", c, args, err, stderr.String())
return "", err
}

return stdout.String(), nil
}

// resetState will erase all types that the yaml examples could have concievably created.
func resetState(t *testing.T) {
t.Helper()

crdTypes := []string{
"conditions.tekton.dev",
"pipelineresources.tekton.dev",
"tasks.tekton.dev",
"clustertasks.tekton.dev",
"pipelines.tekton.dev",
"taskruns.tekton.dev",
"pipelineruns.tekton.dev",
"services",
"pods",
"configmaps",
"secrets",
"serviceaccounts",
"persistentvolumeclaims",
}
for _, c := range crdTypes {
cmd(t, "kubectl", []string{"delete", "--ignore-not-found=true", c, "--all"}, "")
}
}

// 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 {
t.Helper()
_, filename, _, _ := runtime.Caller(0)

// 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")
}
read, err := ioutil.ReadFile(f)
if err != nil {
t.Fatalf("couldnt read contents of %s: %v", f, err)
}
return strings.Replace(string(read), "gcr.io/christiewilson-catfactory", r, -1)
}

// logRun will retrieve the entire yaml of run and log it
func logRun(t *testing.T, run string) {
t.Helper()
yaml, err := cmd(t, "kubectl", []string{"get", run, "-o", "yaml"}, "")
if err == nil {
t.Logf(yaml)
}
}

// pollRun will use kubectl to query the specified run to see if it
// has completed. It will timeout after timeoutSeconds.
func pollRun(t *testing.T, run string, wg *sync.WaitGroup) {
t.Helper()
for i := 0; i < (timeoutSeconds / sleepBetween); i++ {
status, err := cmd(t, "kubectl", []string{"get", run, "--output=jsonpath={.status.conditions[*].status}"}, "")
if err != nil {
t.Fatalf("couldnt get status of %s: %v", run, err)
wg.Done()
return
}

switch status {
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, run)

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

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

var wg sync.WaitGroup
for _, run := range runs {
wg.Add(1)
go pollRun(t, 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{}
for _, s := range strings.Split(k, "\n") {
name := strings.TrimSuffix(s, " created")
if strings.Contains(name, "run") {
runs = append(runs, name)
}
}
return runs
}

// 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()

t.Logf("Applying %s", y)
content := replaceDockerRepo(t, fmt.Sprintf("%s/%s/%s", version, run, y))
output, err := cmd(t, "ko", []string{"create", "-f", "-"}, content)
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, runs)
}
})
}
}

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

t.Cleanup(func() { resetState(t) })
for i, version := range versions {
t.Run(version, func(t *testing.T) {
for _, run := range runs {
runTests(t, version, run)
}
})
// Since the v1alpha1 and v1beta1 tests are mostly copies of each other,
// we need to wipe them out between invocations
if i == 0 {
t.Logf("deleting all %s types between tests", version)
resetState(t)
}
}
}
65 changes: 0 additions & 65 deletions test/e2e-common.sh
Original file line number Diff line number Diff line change
Expand Up @@ -62,71 +62,6 @@ function dump_extra_cluster_state() {
kubectl -n tekton-pipelines logs $(get_app_pod tekton-pipelines-webhook tekton-pipelines)
}

function validate_run() {
local tests_finished=0
for i in {1..90}; do
local finished="$(kubectl get $1.tekton.dev --output=jsonpath='{.items[*].status.conditions[*].status}')"
if [[ ! "$finished" == *"Unknown"* ]]; then
tests_finished=1
break
fi
sleep 10
done

return ${tests_finished}
}

function check_results() {
local failed=0
results="$(kubectl get $1.tekton.dev --output=jsonpath='{range .items[*]}{.metadata.name}={.status.conditions[*].type}{.status.conditions[*].status}{" "}{end}')"
for result in ${results}; do
if [[ ! "${result,,}" == *"=succeededtrue" ]]; then
echo "ERROR: test ${result} but should be succeededtrue"
failed=1
fi
done

return ${failed}
}

function create_resources() {
local resource=$1
echo ">> Creating resources ${resource}"

# Applying the resources, either *taskruns or * *pipelineruns except those
# in the no-ci directory
for file in $(find ${REPO_ROOT_DIR}/examples/${resource}s/ -name '*.yaml' -not -path '*/no-ci/*' | sort); do
perl -p -e 's/gcr.io\/christiewilson-catfactory/$ENV{KO_DOCKER_REPO}/g' ${file} | ko create -f - || return 1
done
}

function run_tests() {
local resource=$1

# Wait for tests to finish.
echo ">> Waiting for tests to finish for ${resource}"
if validate_run $resource; then
echo "ERROR: tests timed out"
fi

# Check that tests passed.
echo ">> Checking test results for ${resource}"
if check_results $resource; then
echo ">> All YAML tests passed"
return 0
fi
return 1
}

function run_yaml_tests() {
echo ">> Starting tests for the resource ${1}/${2}"
create_resources ${1}/${2} || fail_test "Could not create ${2}/${1} from the examples"
if ! run_tests ${2}; then
return 1
fi
return 0
}

function install_pipeline_crd() {
echo ">> Deploying Tekton Pipelines"
ko resolve -f config/ \
Expand Down
20 changes: 2 additions & 18 deletions test/e2e-tests-yaml.sh
Original file line number Diff line number Diff line change
Expand Up @@ -32,24 +32,8 @@ set +o pipefail
install_pipeline_crd

# Run the tests
failed=0
for version in v1alpha1 v1beta1; do
for test in taskrun pipelinerun; do
header "Running YAML e2e tests for ${version} ${test}s"
if ! run_yaml_tests ${version} ${test}; then
echo "ERROR: one or more YAML tests failed"
output_yaml_test_results ${test}
output_pods_logs ${test}
failed=1
fi
done
# Clean resources
delete_pipeline_resources
for res in services pods configmaps secrets serviceaccounts persistentvolumeclaims; do
kubectl delete --ignore-not-found=true ${res} --all
done
done

set -x
failed=$(go test -timeout 15m ./examples)
(( failed )) && fail_test

success

0 comments on commit 2fb6e49

Please sign in to comment.