test: oncluster build initial e2e set of tests (#1193)

This commit is contained in:
Jefferson Ramos 2022-09-05 10:08:22 -03:00 committed by GitHub
parent ae75e5e803
commit 4041d609dd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 1319 additions and 33 deletions

View File

@ -0,0 +1,29 @@
name: Func E2E OnCluster RT Test
on: [pull_request]
jobs:
test:
name: On Cluster RT Test
strategy:
matrix:
go: [1.17.x]
os: ["ubuntu-latest"]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
- uses: actions/setup-go@v2
with:
go-version: ${{ matrix.go }}
- name: Install Binaries
run: ./hack/binaries.sh
- name: Allocate Cluster
run: ./hack/allocate.sh
- name: Deploy Tekton
run: ./hack/tekton.sh
- name: Deploy Test Git Server
run: ./test/gitserver.sh
- name: E2E On Cluster Test (Runtimes)
env:
TEST_TAGS: runtime
run: make && make test-e2e-on-cluster

View File

@ -0,0 +1,29 @@
name: Func E2E OnCluster Test
on: [pull_request]
jobs:
test:
name: On Cluster Test
strategy:
matrix:
go: [1.17.x]
os: ["ubuntu-latest"]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
- uses: actions/setup-go@v2
with:
go-version: ${{ matrix.go }}
- name: Install Binaries
run: ./hack/binaries.sh
- name: Allocate Cluster
run: ./hack/allocate.sh
- name: Deploy Tekton
run: ./hack/tekton.sh
- name: Deploy Test Git Server
run: ./test/gitserver.sh
- name: E2E On Cluster Test
env:
E2E_RUNTIMES: ""
run: make && make test-e2e-on-cluster

View File

@ -149,6 +149,9 @@ test-e2e: ## Run end-to-end tests using an available cluster.
test-e2e-runtime: ## Run end-to-end lifecycle tests using an available cluster for a single runtime.
./test/e2e_lifecycle_tests.sh $(runtime)
test-e2e-on-cluster: ## Run end-to-end on-cluster build tests using an available cluster.
./test/e2e_oncluster_tests.sh
######################
##@ Release Artifacts
######################

50
hack/tekton.sh Executable file
View File

@ -0,0 +1,50 @@
#!/usr/bin/env bash
# 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
#
# https://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.
#
# Install Tekton and required tasks in the cluster
#
set -o errexit
set -o nounset
set -o pipefail
export TERM="${TERM:-dumb}"
tekton_release="previous/v0.38.3"
git_clone_release="0.4"
source_path="https://raw.githubusercontent.com/knative-sandbox/kn-plugin-func/main"
namespace="${NAMESPACE:-default}"
tekton() {
echo "Installing Tekton..."
kubectl apply -f https://storage.googleapis.com/tekton-releases/pipeline/${tekton_release}/release.yaml
sleep 10
kubectl wait pod --for=condition=Ready --timeout=180s -n tekton-pipelines -l "app=tekton-pipelines-controller"
kubectl create clusterrolebinding ${namespace}:knative-serving-namespaced-admin \
--clusterrole=knative-serving-namespaced-admin --serviceaccount=${namespace}:default
}
tekton_tasks() {
echo "Creating Pipeline tasks..."
kubectl apply -f https://raw.githubusercontent.com/tektoncd/catalog/master/task/git-clone/${git_clone_release}/git-clone.yaml
kubectl apply -f ${source_path}/pipelines/resources/tekton/task/func-buildpacks/0.1/func-buildpacks.yaml
kubectl apply -f ${source_path}/pipelines/resources/tekton/task/func-deploy/0.1/func-deploy.yaml
}
tekton
tekton_tasks
echo Done

View File

@ -17,7 +17,7 @@ podman_pid=$!
DOCKER_HOST="unix://$(podman info -f '{{.Host.RemoteSocket.Path}}' 2> /dev/null)"
export DOCKER_HOST
go test -tags integration ./... -v
go test -test.timeout=15m -tags integration ./... -v
e=$?
kill -TERM "$podman_pid" > /dev/null 2>&1

68
test/README.md Normal file
View File

@ -0,0 +1,68 @@
# Functions E2E Test
## Lifecycle tests
Lifecycle tests exercises the most important phases of a function lifecycle starting from
creation, going thru to build, deployment, execution and then deletion (CRUD operations).
It runs func commands such as `create`, `deploy`, `list` and `delete` for a language
runtime using both default `http` and `cloudevents` templates.
## Extended tests
Extended tests performs additional tests on `func` such as templates, config envs, volumes, labels and
other scenarios.
## On Cluster Builds tests
On cluster builds e2e tests exercises functions built directly on cluster.
The tests are organized per scenarios under `./_oncluster` folder.
### Pre-requisites
Prior to run On Cluster builds e2e tests ensure you are connected to
a Kubernetes Cluster with the following deployed:
- Knative Serving
- Tekton
- Tekton Tasks listed [here](../docs/reference/on_cluster_build.md)
- Embedded Git Server (`func-git`) used by tests
For your convenience you can run the following script to setup Tekton and required Tasks:
```
$ ../hack/tekton.sh
```
To install the Git Server required by tests, run:
```
$ ./gitserver.sh
```
#### Running all the Tests on KinD
The below instructions will run all the tests on KinD using an **ephemeral** container registry.
```
# Pre-Reqs
./hack/allocate.sh
./hack/tekton.sh
./test/gitserver.sh
make build
# Run tests
./test/e2e_oncluter_tests.sh
```
#### Running "runtime" only scenario
You can run only e2e tests to exercise a given language/runtime, for example *python*
```
env E2E_RUNTIMES=python TEST_TAGS=runtime ./test/e2e_oncluster_test.sh
```
#### Running tests except "runtime" ones
You can run most of on cluster builds e2e scenarios, except the language/runtime specific
ones, by running:
```
env E2E_RUNTIMES="" ./test/e2e_oncluster_test.sh
```

115
test/_common/cmd.go Normal file
View File

@ -0,0 +1,115 @@
package common
import (
"bytes"
"os"
"os/exec"
"strings"
"testing"
)
type TestExecCmd struct {
// binary to invoke
// Example: "func", "kn", "kubectl", "/usr/bin/sh"
Binary string
// Binary args to append before actual args. Examples:
// when 'kn' binary binaryArgs should be ["func"]
BinaryArgs []string
// Run commands from Dir
SourceDir string
// Indicates shell should dump command line args during execution
ShouldDumpCmdLine bool
// Indicates shell should dump
ShouldDumpOnSuccess bool
// Fail Test on Error
ShouldFailOnError bool
// Environment variable to be used with the command
Env []string
// Optional function to be used to dump stdout command results
DumpLogger func(out string)
// Boolean
T *testing.T
}
// TestExecCmdResult stored command result
type TestExecCmdResult struct {
Stdout string
Stderr string
Error error
}
func (f *TestExecCmd) WithEnv(envKey string, envValue string) *TestExecCmd {
env := envKey + "=" + envValue
f.Env = append(f.Env, env)
return f
}
func (f *TestExecCmd) FromDir(dir string) *TestExecCmd {
f.SourceDir = dir
return f
}
func (f *TestExecCmd) Run(oneArgs string) TestExecCmdResult {
args := strings.Split(oneArgs, " ")
return f.Exec(args...)
}
// Exec invokes go exec library and runs a shell command combining the binary args with args from method signature
func (f *TestExecCmd) Exec(args ...string) TestExecCmdResult {
finalArgs := f.BinaryArgs
if finalArgs == nil {
finalArgs = args
} else if args != nil {
finalArgs = append(finalArgs, args...)
}
if f.ShouldDumpCmdLine {
f.T.Log(f.Binary, strings.Join(finalArgs, " "))
}
var stderr bytes.Buffer
var stdout bytes.Buffer
cmd := exec.Command(f.Binary, finalArgs...)
cmd.Stderr = &stderr
cmd.Stdout = &stdout
if f.SourceDir != "" {
cmd.Dir = f.SourceDir
}
cmd.Env = append(os.Environ(), f.Env...)
err := cmd.Run()
result := TestExecCmdResult{
Stdout: stdout.String(),
Stderr: stderr.String(),
Error: err,
}
if err == nil && f.ShouldDumpOnSuccess {
if result.Stdout != "" {
if f.DumpLogger != nil {
f.DumpLogger(result.Stdout)
} else {
f.T.Logf("%v", result.Stdout)
}
}
}
if err != nil {
f.T.Log(err.Error())
f.T.Log(result.Stderr)
if f.ShouldFailOnError {
f.T.Fail()
}
}
return result
}

106
test/_common/gitserver.go Normal file
View File

@ -0,0 +1,106 @@
package common
import (
"context"
"knative.dev/kn-plugin-func/k8s"
e2e "knative.dev/kn-plugin-func/test/_e2e"
"strings"
"testing"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
)
func GetGitServer(T *testing.T) GitProvider {
gitTestServer := GitTestServerProvider{}
gitTestServer.Init(T)
return &gitTestServer
}
type GitRemoteRepo struct {
RepoName string
ExternalCloneURL string
ClusterCloneURL string
}
type GitProvider interface {
Init(T *testing.T)
CreateRepository(repoName string) *GitRemoteRepo
DeleteRepository(repoName string)
}
// ------------------------------------------------------
// Git Server on Kubernetes as Knative Service (func-git)
// ------------------------------------------------------
type GitTestServerProvider struct {
PodName string
ServiceUrl string
Kubectl *TestExecCmd
t *testing.T
}
func (g *GitTestServerProvider) Init(T *testing.T) {
g.t = T
if g.PodName == "" {
config, err := k8s.GetClientConfig().ClientConfig()
if err != nil {
T.Fatal(err.Error())
}
clientSet, err := kubernetes.NewForConfig(config)
if err != nil {
T.Fatal(err.Error())
}
ctx := context.Background()
namespace, _, _ := k8s.GetClientConfig().Namespace()
podList, err := clientSet.CoreV1().Pods(namespace).List(ctx, metav1.ListOptions{
LabelSelector: "serving.knative.dev/service=func-git",
})
if err != nil {
T.Fatal(err.Error())
}
for _, pod := range podList.Items {
g.PodName = pod.Name
}
}
if g.ServiceUrl == "" {
// Get Route Name
g.ServiceUrl = e2e.GetKnativeServiceUrl(T, "func-git")
}
if g.Kubectl == nil {
g.Kubectl = &TestExecCmd{
Binary: "kubectl",
ShouldDumpCmdLine: true,
ShouldDumpOnSuccess: true,
T: T,
}
}
T.Logf("Initialized HTTP Func Git Server: Server URL = %v Pod Name = %v\n", g.ServiceUrl, g.PodName)
}
func (g *GitTestServerProvider) CreateRepository(repoName string) *GitRemoteRepo {
// kubectl exec $podname -c user-container -- git-repo create $reponame
cmdResult := g.Kubectl.Exec("exec", g.PodName, "-c", "user-container", "--", "git-repo", "create", repoName)
if !strings.Contains(cmdResult.Stdout, "created") {
g.t.Fatal("unable to create git bare repository " + repoName)
}
gitRepo := &GitRemoteRepo{
RepoName: repoName,
ExternalCloneURL: g.ServiceUrl + "/" + repoName + ".git",
ClusterCloneURL: "http://func-git.default.svc.cluster.local/" + repoName + ".git",
}
return gitRepo
}
func (g *GitTestServerProvider) DeleteRepository(repoName string) {
cmdResult := g.Kubectl.Exec("exec", g.PodName, "-c", "user-container", "--", "git-repo", "delete", repoName)
if !strings.Contains(cmdResult.Stdout, "deleted") {
g.t.Fatal("unable to delete git bare repository " + repoName)
}
}

30
test/_common/knfunc.go Normal file
View File

@ -0,0 +1,30 @@
package common
import (
"testing"
e2e "knative.dev/kn-plugin-func/test/_e2e"
)
func NewKnFuncShellCli(t *testing.T) *TestExecCmd {
knFunc := TestExecCmd{}
knFunc.T = t
if e2e.IsUseKnFunc() {
knFunc.Binary = "kn"
knFunc.BinaryArgs = []string{"func"}
} else {
knFunc.Binary = e2e.GetFuncBinaryPath()
if knFunc.Binary == "" {
t.Log("'func' binary not defined. Please set E2E_FUNC_BIN_PATH environment variable prior to running tests")
t.FailNow()
}
}
cmd := knFunc.Exec()
if cmd.Error != nil {
t.FailNow()
}
knFunc.ShouldDumpCmdLine = true
knFunc.ShouldFailOnError = true
return &knFunc
}

18
test/_common/shell.go Normal file
View File

@ -0,0 +1,18 @@
package common
import (
"testing"
)
func NewShellCmd(t *testing.T, fromDirectory string) *TestExecCmd {
shellCmd := TestExecCmd{
Binary: "sh",
BinaryArgs: []string{"-c"},
SourceDir: fromDirectory,
ShouldDumpCmdLine: true,
ShouldDumpOnSuccess: true,
T: t,
}
return &shellCmd
}

29
test/_e2e/cleaner.go Normal file
View File

@ -0,0 +1,29 @@
package e2e
import "strings"
// CleanOutput Some commands, such as deploy command, spans spinner chars and cursor shifts at output which are captured and merged
// regular output messages. This functions is meant to remove these chars in order to facilitate tests assertions and data extraction from output
func CleanOutput(stdOutput string) string {
toRemove := []string{
"🕛 ",
"🕐 ",
"🕑 ",
"🕒 ",
"🕓 ",
"🕔 ",
"🕕 ",
"🕖 ",
"🕗 ",
"🕘 ",
"🕙 ",
"🕚 ",
"\033[1A",
"\033[1B",
"\033[K",
}
for _, c := range toRemove {
stdOutput = strings.ReplaceAll(stdOutput, c, "")
}
return stdOutput
}

View File

@ -41,29 +41,3 @@ func Deploy(t *testing.T, knFunc *TestShellCmdRunner, project *FunctionTestProje
project.IsDeployed = true
}
// CleanOutput Some commands, such as deploy command, spans spinner chars and cursor shifts at output which are captured and merged
// regular output messages. This functions is meant to remove these chars in order to facilitate tests assertions and data extraction from output
func CleanOutput(deployOutput string) string {
toRemove := []string{
"🕛 ",
"🕐 ",
"🕑 ",
"🕒 ",
"🕓 ",
"🕔 ",
"🕕 ",
"🕖 ",
"🕗 ",
"🕘 ",
"🕙 ",
"🕚 ",
"\033[1A",
"\033[1B",
"\033[K",
}
for _, c := range toRemove {
deployOutput = strings.ReplaceAll(deployOutput, c, "")
}
return deployOutput
}

View File

@ -39,10 +39,18 @@ func RetrieveKnativeServiceResource(t *testing.T, serviceName string) *unstructu
}
// GetCurrentServiceRevision retrieves current revision name for the deployed function
func GetCurrentServiceRevision(t *testing.T, project *FunctionTestProject) string {
resource := RetrieveKnativeServiceResource(t, project.FunctionName)
func GetCurrentServiceRevision(t *testing.T, serviceName string) string {
resource := RetrieveKnativeServiceResource(t, serviceName)
rootMap := resource.UnstructuredContent()
statusMap := rootMap["status"].(map[string]interface{})
latestReadyRevision := statusMap["latestReadyRevisionName"].(string)
return latestReadyRevision
}
func GetKnativeServiceUrl(t *testing.T, functionName string) string {
resource := RetrieveKnativeServiceResource(t, functionName)
rootMap := resource.UnstructuredContent()
statusMap := rootMap["status"].(map[string]interface{})
url := statusMap["url"].(string)
return url
}

View File

@ -22,9 +22,9 @@ func ReadyCheck(t *testing.T, knFunc *TestShellCmdRunner, project FunctionTestPr
}
// NewRevisionCheck waits for a new revision to report as ready
func NewRevisionCheck(t *testing.T, previousRevision string, project *FunctionTestProject) (newRevision string) {
func NewRevisionCheck(t *testing.T, previousRevision string, serviceName string) (newRevision string) {
err := wait.PollImmediate(5*time.Second, 1*time.Minute, func() (done bool, err error) {
newRevision = GetCurrentServiceRevision(t, project)
newRevision = GetCurrentServiceRevision(t, serviceName)
t.Logf("Waiting for new revision deployment (previous revision [%v], current revision [%v])", previousRevision, newRevision)
return newRevision != "" && newRevision != previousRevision, nil
})

View File

@ -33,13 +33,13 @@ func Update(t *testing.T, knFunc *TestShellCmdRunner, project *FunctionTestProje
t.Fatal("an error has occurred while updating project folder with new sources.", err.Error())
}
previousRevision := GetCurrentServiceRevision(t, project)
previousRevision := GetCurrentServiceRevision(t, project.FunctionName)
// Redeploy function
Deploy(t, knFunc, project)
// Waits New Revision to become ready
NewRevisionCheck(t, previousRevision, project)
NewRevisionCheck(t, previousRevision, project.FunctionName)
// Indicates new project (from update templates) is in use
project.IsNewRevision = true

View File

@ -0,0 +1,35 @@
package oncluster
import (
"testing"
"gotest.tools/v3/assert"
)
// AssertNoError ensure err is nil otherwise fails testing
func AssertNoError(t *testing.T, err error) {
if err != nil {
t.Error(err.Error())
t.FailNow()
}
}
// AssertThatTektonPipelineRunSucceed verifies the pipeline and pipelinerun were actually created
// on the cluster and ensure all the Tasks of the pipelinerun executed successfully
// Also it logs a brief summary of execution of the pipeline for potential debug purposes
func AssertThatTektonPipelineRunSucceed(t *testing.T, functionName string) {
assert.Assert(t, TektonPipelineExists(t, functionName), "tekton pipeline not found on cluster")
RunSummary := TektonPipelineLastRunSummary(t, functionName)
t.Logf("Tekton Run Summary:\n %v", RunSummary.ToString())
assert.Assert(t, RunSummary.IsSucceed(), "expected pipeline run was not succeeded")
}
// AssertThatTektonPipelineResourcesNotExists is intended to check the pipeline and pipelinerun resources
// do not exists. This is meant to be called after a `func delete` to ensure everything is cleaned
func AssertThatTektonPipelineResourcesNotExists(t *testing.T, functionName string) {
if !t.Failed() {
t.Log("Checking resources got cleaned")
assert.Assert(t, !TektonPipelineExists(t, functionName), "tekton pipeline was found but it should not exist")
assert.Assert(t, !TektonPipelineRunExists(t, functionName), "tekton pipelinerun was found but it should not exist")
}
}

View File

@ -0,0 +1,67 @@
package oncluster
import (
"fmt"
"os"
"testing"
yaml "gopkg.in/yaml.v2"
common "knative.dev/kn-plugin-func/test/_common"
)
type Git struct {
URL string
Revision string
ContextDir string
}
// UpdateFuncYamlGit update func.yaml file by setting build to git as well as git fields.
func UpdateFuncYamlGit(t *testing.T, projectDir string, git Git) {
funcYamlPath := projectDir + "/func.yaml"
data, err := os.ReadFile(funcYamlPath)
AssertNoError(t, err)
m := make(map[interface{}]interface{})
err = yaml.Unmarshal([]byte(data), &m)
AssertNoError(t, err)
gitMap := make(map[interface{}]interface{})
m["build"] = "git"
m["git"] = gitMap
changeLog := fmt.Sprintln("build:", "git")
updateGitField := func(targetField string, targetValue string) {
if targetValue != "" {
gitMap[targetField] = targetValue
changeLog += fmt.Sprintln("git.", targetField, ":", targetValue)
}
}
updateGitField("url", git.URL)
updateGitField("revision", git.Revision)
updateGitField("contextDir", git.ContextDir)
outData, _ := yaml.Marshal(m)
err = os.WriteFile(funcYamlPath, outData, 0644)
AssertNoError(t, err)
t.Logf("func.yaml changed:\n%v", changeLog)
}
// GitInitialCommitAndPush Runs repeatable git commands used on every initial repository setup
// such as `git init`, `git config user`, `git add .`, `git remote add ...` and `git push`
func GitInitialCommitAndPush(t *testing.T, gitProjectPath string, originCloneURL string) (sh *common.TestExecCmd) {
sh = common.NewShellCmd(t, gitProjectPath)
sh.ShouldFailOnError = true
sh.ShouldDumpOnSuccess = true
sh.Exec(`git init`)
sh.Exec(`git branch -M main`)
sh.Exec(`git add .`)
sh.Exec(`git config user.name "John Smith"`)
sh.Exec(`git config user.email "john.smith@example.com"`)
sh.Exec(`git commit -m "initial commit"`)
sh.Exec(`git remote add origin ` + originCloneURL)
sh.Exec(`git push -u origin main`)
return sh
}

View File

@ -0,0 +1,23 @@
package oncluster
import (
"fmt"
"os"
"path/filepath"
"testing"
)
// WriteNewSimpleIndexJS is used to replace the content of "index.js" of a Node JS function created by a test case.
// File content will cause the deployed function to, when invoked, return the value specified on `withBodyReturning`
// params, which is handy for test assertions.
func WriteNewSimpleIndexJS(t *testing.T, nodeJsFuncProjectDir string, withBodyReturning string) {
indexJsContent := fmt.Sprintf(`
function invoke(context) {
return { body: '%v' }
}
module.exports = invoke;
`, withBodyReturning)
err := os.WriteFile(filepath.Join(nodeJsFuncProjectDir, "index.js"), []byte(indexJsContent), 0644)
AssertNoError(t, err)
}

View File

@ -0,0 +1,71 @@
//go:build oncluster
// +build oncluster
package oncluster
import (
"os"
"path/filepath"
"strings"
"testing"
"gotest.tools/v3/assert"
common "knative.dev/kn-plugin-func/test/_common"
e2e "knative.dev/kn-plugin-func/test/_e2e"
)
// TestDefault covers basic test scenario that ensure on cluster build from a "default branch" and
// code changes (new commits) will be properly built and deployed on new revision
func TestBasicDefault(t *testing.T) {
var funcName = "test-func-basic"
var funcPath = filepath.Join(os.TempDir(), funcName)
func() {
gitServer := common.GitTestServerProvider{}
gitServer.Init(t)
remoteRepo := gitServer.CreateRepository(funcName)
defer gitServer.DeleteRepository(funcName)
knFunc := common.NewKnFuncShellCli(t)
knFunc.Exec("create", "-l", "node", funcPath)
defer os.RemoveAll(funcPath)
// Write an `index.js` that make node func to return 'first revision'
WriteNewSimpleIndexJS(t, funcPath, "first revision")
sh := GitInitialCommitAndPush(t, funcPath, remoteRepo.ExternalCloneURL)
// Update func.yaml build as git + url + context-dir
UpdateFuncYamlGit(t, funcPath, Git{URL: remoteRepo.ClusterCloneURL})
// Deploy it
knFunc.Exec("deploy", "-r", e2e.GetRegistry(), "-p", funcPath)
defer knFunc.Exec("delete", "-p", funcPath)
// Assert "first revision" is returned
result := knFunc.Exec("invoke", "-p", funcPath)
assert.Assert(t, strings.Contains(result.Stdout, "first revision"), "Func body does not contain 'first revision'")
previousServiceRevision := e2e.GetCurrentServiceRevision(t, funcName)
// Update index.js to force node func to return 'new revision'
WriteNewSimpleIndexJS(t, funcPath, "new revision")
sh.Exec(`git add index.js`)
sh.Exec(`git commit -m "revision 2"`)
sh.Exec(`git push`)
// Re-Deploy Func
knFunc.Exec("deploy", "-r", e2e.GetRegistry(), "-p", funcPath)
e2e.NewRevisionCheck(t, previousServiceRevision, funcName) // Wait New Service Revision
// -- Assertions --
result = knFunc.Exec("invoke", "-p", funcPath)
assert.Assert(t, strings.Contains(result.Stdout, "new revision"), "Func body does not contain 'new revision'")
AssertThatTektonPipelineRunSucceed(t, funcName)
}()
AssertThatTektonPipelineResourcesNotExists(t, funcName)
}

View File

@ -0,0 +1,59 @@
//go:build oncluster
// +build oncluster
package oncluster
import (
"os"
"path/filepath"
"strings"
"testing"
"gotest.tools/v3/assert"
common "knative.dev/kn-plugin-func/test/_common"
e2e "knative.dev/kn-plugin-func/test/_e2e"
)
// TestContextDirFunc tests the following use case:
// - As a Developer I want my function located in a specific directory on my project, hosted on my
// public git repository from the main branch, to get deployed on my cluster
func TestContextDirFunc(t *testing.T) {
var gitProjectName = "test-project"
var gitProjectPath = filepath.Join(os.TempDir(), gitProjectName)
var funcName = "test-func-context-dir"
var funcContextDir = filepath.Join("functions", funcName)
var funcPath = filepath.Join(gitProjectPath, funcContextDir)
func() {
gitServer := common.GitTestServerProvider{}
gitServer.Init(t)
remoteRepo := gitServer.CreateRepository(gitProjectName)
defer gitServer.DeleteRepository(gitProjectName)
knFunc := common.NewKnFuncShellCli(t)
knFunc.Exec("create", "-l", "node", funcPath)
WriteNewSimpleIndexJS(t, funcPath, "hello dir")
defer os.RemoveAll(gitProjectPath)
// Initial commit to repository: git init + commit + push
GitInitialCommitAndPush(t, gitProjectPath, remoteRepo.ExternalCloneURL)
// Update func.yaml build as git + url + context-dir
UpdateFuncYamlGit(t, funcPath, Git{URL: remoteRepo.ClusterCloneURL, ContextDir: funcContextDir})
knFunc.Exec("deploy", "-r", e2e.GetRegistry(), "-p", funcPath)
defer knFunc.Exec("delete", "-p", funcPath)
// -- Assertions --
result := knFunc.Exec("invoke", "-p", funcPath)
assert.Assert(t, strings.Contains(result.Stdout, "hello dir"), "Func body does not contain 'hello dir'")
AssertThatTektonPipelineRunSucceed(t, funcName)
}()
AssertThatTektonPipelineResourcesNotExists(t, funcName)
}

View File

@ -0,0 +1,36 @@
//go:build oncluster
// +build oncluster
package oncluster
import (
"os"
"path/filepath"
"testing"
common "knative.dev/kn-plugin-func/test/_common"
e2e "knative.dev/kn-plugin-func/test/_e2e"
)
// TestFromCliBuildLocal tests the scenario which func.yaml indicates that builds should be on cluster
// but users wants to run a local build on its machine
func TestFromCliBuildLocal(t *testing.T) {
var funcName = "test-func-cli-local"
var funcPath = filepath.Join(os.TempDir(), funcName)
knFunc := common.NewKnFuncShellCli(t)
knFunc.Exec("create", "-l", "node", funcPath)
defer os.RemoveAll(funcPath)
// Update func.yaml build as local + some fake url (it should not call it anyway)
UpdateFuncYamlGit(t, funcPath, Git{URL: "http://fake-repo/repo.git"})
knFunc.Exec("deploy", "-r", e2e.GetRegistry(), "-p", funcPath, "--build", "local")
defer knFunc.Exec("delete", "-p", funcPath)
// -- Assertions --
knFunc.Exec("invoke", "-p", funcPath)
AssertThatTektonPipelineResourcesNotExists(t, funcName)
}

View File

@ -0,0 +1,143 @@
//go:build oncluster
// +build oncluster
package oncluster
/*
Tests on this file covers the scenarios when func.yaml is not modified (build: local)
and git build strategy is specified thru CLI.
A) Default Branch Test
func deploy --build=git --git-url=http://gitserver/myfunc.git
b) Feature Branch Test
func deploy --build=git --git-url=http://gitserver/myfunc.git --git-branch=feature/my-branch
c) Context Dir test
func deploy --build=git --git-url=http://gitserver/myfunc.git --git-dir=functions/myfunc
*/
import (
"os"
"path/filepath"
"strings"
"testing"
"gotest.tools/v3/assert"
common "knative.dev/kn-plugin-func/test/_common"
e2e "knative.dev/kn-plugin-func/test/_e2e"
)
// TestFromCliDefaultBranch triggers a default branch test by using CLI flags
func TestFromCliDefaultBranch(t *testing.T) {
var gitProjectName = "test-func-yaml-build-local"
var gitProjectPath = filepath.Join(os.TempDir(), gitProjectName)
var funcName = gitProjectName
var funcPath = gitProjectPath
gitServer := common.GitTestServerProvider{}
gitServer.Init(t)
remoteRepo := gitServer.CreateRepository(gitProjectName)
defer gitServer.DeleteRepository(gitProjectName)
knFunc := common.NewKnFuncShellCli(t)
knFunc.Exec("create", "-l", "node", funcPath)
defer os.RemoveAll(gitProjectPath)
GitInitialCommitAndPush(t, gitProjectPath, remoteRepo.ExternalCloneURL)
knFunc.Exec("deploy",
"-r", e2e.GetRegistry(),
"-p", funcPath,
"--build", "git",
"--git-url", remoteRepo.ClusterCloneURL)
defer knFunc.Exec("delete", "-p", funcPath)
// ## ASSERTIONS
result := knFunc.Exec("invoke", "-p", funcPath)
assert.Assert(t, strings.Contains(result.Stdout, "Hello"), "Func body does not contain 'Hello'")
AssertThatTektonPipelineRunSucceed(t, funcName)
}
// TestFromCliFeatureBranch trigger a feature branch test by using CLI flags
func TestFromCliFeatureBranch(t *testing.T) {
var funcName = "test-func-cli-feature-branch"
var funcPath = filepath.Join(os.TempDir(), funcName)
gitServer := common.GitTestServerProvider{}
gitServer.Init(t)
remoteRepo := gitServer.CreateRepository(funcName)
defer gitServer.DeleteRepository(funcName)
knFunc := common.NewKnFuncShellCli(t)
knFunc.Exec("create", "-l", "node", funcPath)
defer os.RemoveAll(funcPath)
GitInitialCommitAndPush(t, funcPath, remoteRepo.ExternalCloneURL)
WriteNewSimpleIndexJS(t, funcPath, "hello branch")
sh := common.NewShellCmd(t, funcPath)
sh.ShouldFailOnError = true
sh.Exec("git checkout -b feature/branch")
sh.Exec("git add index.js")
sh.Exec(`git commit -m "feature branch change"`)
sh.Exec("git push -u origin feature/branch")
knFunc.Exec("deploy",
"-r", e2e.GetRegistry(),
"-p", funcPath,
"--build", "git",
"--git-url", remoteRepo.ClusterCloneURL,
"--git-branch", "feature/branch")
defer knFunc.Exec("delete", "-p", funcPath)
// ## ASSERTIONS
result := knFunc.Exec("invoke", "-p", funcPath)
assert.Assert(t, strings.Contains(result.Stdout, "hello branch"), "Func body does not contain 'hello branch'")
AssertThatTektonPipelineRunSucceed(t, funcName)
}
// TestFromCliContextDirFunc triggers a context dir test by using CLI flags
func TestFromCliContextDirFunc(t *testing.T) {
var gitProjectName = "test-project"
var gitProjectPath = filepath.Join(os.TempDir(), gitProjectName)
var funcName = "test-func-context-dir"
var funcContextDir = filepath.Join("functions", funcName)
var funcPath = filepath.Join(gitProjectPath, funcContextDir)
gitServer := common.GitTestServerProvider{}
gitServer.Init(t)
remoteRepo := gitServer.CreateRepository(gitProjectName)
defer gitServer.DeleteRepository(gitProjectName)
knFunc := common.NewKnFuncShellCli(t)
knFunc.Exec("create", "-l", "node", funcPath)
defer os.RemoveAll(gitProjectPath)
WriteNewSimpleIndexJS(t, funcPath, "hello dir")
GitInitialCommitAndPush(t, gitProjectPath, remoteRepo.ExternalCloneURL)
knFunc.Exec("deploy",
"-r", e2e.GetRegistry(),
"-p", funcPath,
"--build", "git",
"--git-url", remoteRepo.ClusterCloneURL,
"--git-dir", funcContextDir)
defer knFunc.Exec("delete", "-p", funcPath)
// -- Assertions --
result := knFunc.Exec("invoke", "-p", funcPath)
assert.Assert(t, strings.Contains(result.Stdout, "hello dir"), "Func body does not contain 'hello dir'")
AssertThatTektonPipelineRunSucceed(t, funcName)
}

View File

@ -0,0 +1,121 @@
//go:build oncluster
// +build oncluster
package oncluster
/*
Tests on this file covers "on cluster build" use cases:
A) I want my function hosted on my public git repository from a FEATURE BRANCH to get built deployed
b) I want my function hosted on my public git repository from a specific GIT TAG to get built and deployed
c) I want my function hosted on my public git repository from a specific COMMIT HASH to get built and deployed
*/
import (
"os"
"path/filepath"
"strings"
"testing"
common "knative.dev/kn-plugin-func/test/_common"
e2e "knative.dev/kn-plugin-func/test/_e2e"
)
func TestFromFeatureBranch(t *testing.T) {
setupCodeFn := func(sh *common.TestExecCmd, funcProjectPath string, clusterCloneUrl string) {
WriteNewSimpleIndexJS(t, funcProjectPath, "hello branch")
sh.Exec("git checkout -b feature/branch")
sh.Exec("git add index.js")
sh.Exec(`git commit -m "feature branch change"`)
sh.Exec("git push -u origin feature/branch")
UpdateFuncYamlGit(t, funcProjectPath, Git{URL: clusterCloneUrl, Revision: "feature/branch"})
}
assertBodyFn := func(response string) bool {
return strings.Contains(response, "hello branch")
}
GitRevisionCheck(t, "test-func-feature-branch", setupCodeFn, assertBodyFn)
}
func TestFromRevisionTag(t *testing.T) {
setupCodeFn := func(sh *common.TestExecCmd, funcProjectPath string, clusterCloneUrl string) {
WriteNewSimpleIndexJS(t, funcProjectPath, "hello v1")
sh.Exec("git add index.js")
sh.Exec(`git commit -m "version 1"`)
sh.Exec("git push origin main")
sh.Exec("git tag tag-v1")
sh.Exec("git push origin tag-v1")
WriteNewSimpleIndexJS(t, funcProjectPath, "hello v2")
sh.Exec("git add index.js")
sh.Exec(`git commit -m "version 2"`)
sh.Exec("git push origin main")
UpdateFuncYamlGit(t, funcProjectPath, Git{URL: clusterCloneUrl, Revision: "tag-v1"})
}
assertBodyFn := func(response string) bool {
return strings.Contains(response, "hello v1")
}
GitRevisionCheck(t, "test-func-tag", setupCodeFn, assertBodyFn)
}
func TestFromCommitHash(t *testing.T) {
setupCodeFn := func(sh *common.TestExecCmd, funcProjectPath string, clusterCloneUrl string) {
WriteNewSimpleIndexJS(t, funcProjectPath, "hello v1")
sh.Exec("git add index.js")
sh.Exec(`git commit -m "version 1"`)
sh.Exec("git push origin main")
gitRevParse := sh.Exec("git rev-parse HEAD")
WriteNewSimpleIndexJS(t, funcProjectPath, "hello v2")
sh.Exec("git add index.js")
sh.Exec(`git commit -m "version 2"`)
sh.Exec("git push origin main")
commitHash := strings.TrimSpace(gitRevParse.Stdout)
UpdateFuncYamlGit(t, funcProjectPath, Git{URL: clusterCloneUrl, Revision: commitHash})
t.Logf("Revision Check: commit hash resolved to [%v]", commitHash)
}
assertBodyFn := func(response string) bool {
return strings.Contains(response, "hello v1")
}
GitRevisionCheck(t, "test-func-commit", setupCodeFn, assertBodyFn)
}
func GitRevisionCheck(
t *testing.T,
funcName string,
setupCodeFn func(shell *common.TestExecCmd, funcProjectPath string, clusterCloneUrl string),
assertBodyFn func(response string) bool) {
var funcPath = filepath.Join(os.TempDir(), funcName)
gitServer := common.GitTestServerProvider{}
gitServer.Init(t)
remoteRepo := gitServer.CreateRepository(funcName)
defer gitServer.DeleteRepository(funcName)
knFunc := common.NewKnFuncShellCli(t)
knFunc.Exec("create", "-l", "node", funcPath)
defer os.RemoveAll(funcPath)
sh := GitInitialCommitAndPush(t, funcPath, remoteRepo.ExternalCloneURL)
// Setup specific code
setupCodeFn(sh, funcPath, remoteRepo.ClusterCloneURL)
knFunc.Exec("deploy", "-r", e2e.GetRegistry(), "-p", funcPath)
defer knFunc.Exec("delete", "-p", funcPath)
// -- Assertions --
result := knFunc.Exec("invoke", "-p", funcPath)
if !assertBodyFn(result.Stdout) {
t.Error("Func Body does not contains expected expression")
}
AssertThatTektonPipelineRunSucceed(t, funcName)
}

View File

@ -0,0 +1,69 @@
//go:build oncluster || runtime
// +build oncluster runtime
package oncluster
import (
"os"
"path/filepath"
"strings"
"testing"
common "knative.dev/kn-plugin-func/test/_common"
e2e "knative.dev/kn-plugin-func/test/_e2e"
)
// TestRuntime will invoke a language runtime test against (by default) to all runtimes.
// The Environment Variable E2E_RUNTIMES can be used to select the languages/runtimes to be tested
func TestRuntime(t *testing.T) {
var runtimeList = []string{}
runtimes, present := os.LookupEnv("E2E_RUNTIMES")
if present {
if runtimes != "" {
runtimeList = strings.Split(runtimes, " ")
}
} else {
runtimeList = []string{"node", "python", "quarkus", "springboot", "typescript"} // "go" and "rust" pending support
}
for _, lang := range runtimeList {
t.Run(lang+"_test", func(t *testing.T) {
runtimeImpl(t, lang)
})
}
}
func runtimeImpl(t *testing.T, lang string) {
var gitProjectName = "test-func-lang-" + lang
var gitProjectPath = filepath.Join(os.TempDir(), gitProjectName)
var funcName = gitProjectName
var funcPath = gitProjectPath
gitServer := common.GitTestServerProvider{}
gitServer.Init(t)
remoteRepo := gitServer.CreateRepository(gitProjectName)
defer gitServer.DeleteRepository(gitProjectName)
knFunc := common.NewKnFuncShellCli(t)
knFunc.Exec("create", "-l", lang, funcPath)
defer os.RemoveAll(gitProjectPath)
GitInitialCommitAndPush(t, gitProjectPath, remoteRepo.ExternalCloneURL)
knFunc.Exec("deploy",
"-r", e2e.GetRegistry(),
"-p", funcPath,
"--build", "git",
"--git-url", remoteRepo.ClusterCloneURL)
defer knFunc.Exec("delete", "-p", funcPath)
// -- Assertions --
result := knFunc.Exec("invoke", "-p", funcPath)
t.Log(result)
AssertThatTektonPipelineRunSucceed(t, funcName)
}

96
test/_oncluster/tekton.go Normal file
View File

@ -0,0 +1,96 @@
package oncluster
import (
"context"
"fmt"
"strings"
"testing"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"knative.dev/kn-plugin-func/k8s"
"knative.dev/kn-plugin-func/pipelines/tekton"
)
// TektonPipelineExists verifies pipeline with a given prefix exists on cluster
func TektonPipelineExists(t *testing.T, pipelinePrefix string) bool {
namespace, _, _ := k8s.GetClientConfig().Namespace()
client, ns, _ := tekton.NewTektonClientAndResolvedNamespace(namespace)
pipelines, err := client.Pipelines(ns).List(context.Background(), v1.ListOptions{})
if err != nil {
t.Error(err.Error())
}
for _, pipeline := range pipelines.Items {
if strings.HasPrefix(pipeline.Name, pipelinePrefix) && strings.HasSuffix(pipeline.Name, "-pipeline") {
return true
}
}
return false
}
// TektonPipelineRunExists verifies pipelinerun with a given prefix exists on cluster
func TektonPipelineRunExists(t *testing.T, pipelineRunPrefix string) bool {
namespace, _, _ := k8s.GetClientConfig().Namespace()
client, ns, _ := tekton.NewTektonClientAndResolvedNamespace(namespace)
pipelineRuns, err := client.PipelineRuns(ns).List(context.Background(), v1.ListOptions{})
if err != nil {
t.Error(err.Error())
}
for _, run := range pipelineRuns.Items {
if strings.HasPrefix(run.Name, pipelineRunPrefix) {
return true
}
}
return false
}
type PipelineRunSummary struct {
PipelineRunName string
PipelineRunStatus string
TasksRunSummary []PipelineTaskRunSummary
}
type PipelineTaskRunSummary struct {
TaskName string
TaskStatus string
}
func (p *PipelineRunSummary) ToString() string {
r := fmt.Sprintf("run: %-42v, status: %v\n", p.PipelineRunName, p.PipelineRunStatus)
for _, t := range p.TasksRunSummary {
r = r + fmt.Sprintf(" task: %-15v, status: %v\n", t.TaskName, t.TaskStatus)
}
return r
}
func (p *PipelineRunSummary) IsSucceed() bool {
return p.PipelineRunStatus == "Succeeded"
}
// TektonPipelTektonPipelineLastRunSummary gather information about a pipeline run such as
// list of tasks executed and status of each task execution. It is meant to be used on assertions
func TektonPipelineLastRunSummary(t *testing.T, pipelinePrefix string) *PipelineRunSummary {
namespace, _, _ := k8s.GetClientConfig().Namespace()
client, ns, _ := tekton.NewTektonClientAndResolvedNamespace(namespace)
pipelineRuns, err := client.PipelineRuns(ns).List(context.Background(), v1.ListOptions{})
if err != nil {
t.Error(err.Error())
}
lr := PipelineRunSummary{}
for _, run := range pipelineRuns.Items {
if strings.HasPrefix(run.Name, pipelinePrefix) {
lr.PipelineRunName = run.Name
if len(run.Status.Conditions) > 0 {
lr.PipelineRunStatus = run.Status.Conditions[0].Reason
}
lr.TasksRunSummary = []PipelineTaskRunSummary{}
for _, taskRun := range run.Status.TaskRuns {
trun := PipelineTaskRunSummary{}
trun.TaskName = taskRun.PipelineTaskName
if len(taskRun.Status.Conditions) > 0 {
trun.TaskStatus = taskRun.Status.Conditions[0].Reason
}
lr.TasksRunSummary = append(lr.TasksRunSummary, trun)
}
}
}
return &lr
}

58
test/e2e_oncluster_tests.sh Executable file
View File

@ -0,0 +1,58 @@
#!/usr/bin/env bash
# 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
#
# https://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.
#
# Runs basic lifecycle E2E tests against kn func cli for a given language/runtime.
# By default it will run e2e tests against 'func' binary, but you can change it to use 'kn func' instead
#
# The following environment variable can be set in order to customize e2e execution:
#
# E2E_USE_KN_FUNC When set to "true" indicates e2e to issue func command using kn cli.
#
# E2E_REGISTRY_URL Indicates a specific registry (i.e: "quay.io/user") should be used. Make sure
# to authenticate to the registry (i.e: docker login ...) prior to execute the script
# By default it uses "ttl.sh" registry
#
# E2E_FUNC_BIN_PATH Path to func binary. Derived by this script when not set
#
# E2E_RUNTIMES List of runtimes (space separated) to execute TestRuntime.
#
set -o errexit
set -o nounset
set -o pipefail
runtime=${1:-}
use_kn_func=${E2E_USE_KN_FUNC:-}
curdir=$(pwd)
cd $(dirname $0)
cd ../
REGISTRY_PROJ=knfunc$(head -c 128 </dev/urandom | LC_CTYPE=C tr -dc 'a-z0-9' | fold -w 8 | head -n 1)
export E2E_REGISTRY_URL=${E2E_REGISTRY_URL:-ttl.sh/$REGISTRY_PROJ}
export E2E_FUNC_BIN_PATH=${E2E_FUNC_BIN_PATH:-$(pwd)/func}
# Make sure 'func' binary is built in case KN FUNC was not required for testing
if [[ ! -f "$E2E_FUNC_BIN_PATH" && "$use_kn_func" != "true" ]]; then
echo "func binary not found. Please run 'make build' prior to run e2e."
exit 1
fi
go clean -testcache
go test -v -test.v -test.timeout=90m -tags="${TEST_TAGS:-oncluster}" ./test/_oncluster/
ret=$?
cd $curdir
exit $ret

49
test/gitserver.sh Executable file
View File

@ -0,0 +1,49 @@
#!/usr/bin/env bash
# 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
#
# https://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.
set -o errexit
set -o nounset
set -o pipefail
git_server() {
echo "Creating Git Server Knative service..."
cat << EOF | kubectl apply -f -
apiVersion: serving.knative.dev/v1
kind: Service
metadata:
name: func-git
labels:
app: git
spec:
template:
metadata:
annotations:
autoscaling.knative.dev/max-scale: "1"
autoscaling.knative.dev/min-scale: "1"
client.knative.dev/user-image: ghcr.io/jrangelramos/gitserver
spec:
containers:
- image: ghcr.io/jrangelramos/gitserver
ports:
- containerPort: 80
resources: {}
status: {}
EOF
kubectl wait ksvc --for=condition=RoutesReady --timeout=30s -l "app=git"
}
git_server
echo Done