diff --git a/examples/examples_test.go b/examples/examples_test.go new file mode 100644 index 00000000000..9f72e181064 --- /dev/null +++ b/examples/examples_test.go @@ -0,0 +1,243 @@ +// +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" + + "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) + } + } +} diff --git a/test/e2e-common.sh b/test/e2e-common.sh index e0a6328bc33..ee6261c42e9 100755 --- a/test/e2e-common.sh +++ b/test/e2e-common.sh @@ -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/ \ diff --git a/test/e2e-tests-yaml.sh b/test/e2e-tests-yaml.sh index 6d2061dca9f..e83637982d9 100755 --- a/test/e2e-tests-yaml.sh +++ b/test/e2e-tests-yaml.sh @@ -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 -v -count=1 -tags=examples -timeout 15m ./examples) (( failed )) && fail_test success