func/client_int_test.go

345 lines
8.5 KiB
Go

//go:build integration
// +build integration
package function_test
import (
"context"
"os"
"reflect"
"testing"
"time"
fn "knative.dev/kn-plugin-func"
"knative.dev/kn-plugin-func/buildpacks"
"knative.dev/kn-plugin-func/docker"
"knative.dev/kn-plugin-func/knative"
)
/*
NOTE: Running integration tests locally requires a configured test cluster.
Test failures may require manual removal of dangling resources.
## Integration Cluster
These integration tests require a properly configured cluster,
such as that which is setup and configured in CI (see .github/workflows).
A local KinD cluster can be started via:
./hack/allocate.sh && ./hack/configure.sh
## Integration Testing
These tests can be run via the make target:
make test-integration
or manually by specifying the tag
go test -v -tags integration ./...
## Teardown and Cleanup
Tests should clean up after themselves. In the event of failures, one may
need to manually remove files:
rm -rf ./testdata/example.com
The test cluster is not automatically removed, as it can be reused. To remove:
./hack/delete.sh
*/
const (
// DefaultRegistry must contain both the registry host and
// registry namespace at this time. This will likely be
// split and defaulted to the forthcoming in-cluster registry.
DefaultRegistry = "localhost:5000/func"
// DefaultNamespace for the underlying deployments. Must be the same
// as is set up and configured (see hack/configure.sh)
DefaultNamespace = "func"
)
func TestList(t *testing.T) {
verbose := true
// Assemble
lister, err := knative.NewLister(DefaultNamespace)
if err != nil {
t.Fatal(err)
}
client := fn.New(
fn.WithLister(lister),
fn.WithVerbose(verbose))
// Act
names, err := client.List(context.Background())
if err != nil {
t.Fatal(err)
}
// Assert
if len(names) != 0 {
t.Fatalf("Expected no Functions, got %v", names)
}
}
// TestNew creates
func TestNew(t *testing.T) {
defer within(t, "testdata/example.com/testnew")()
verbose := true
client := newClient(verbose)
// Act
if err := client.New(context.Background(), fn.Function{Name: "testnew", Root: ".", Runtime: "go"}); err != nil {
t.Fatal(err)
}
defer del(t, client, "testnew")
// Assert
items, err := client.List(context.Background())
names := []string{}
for _, item := range items {
names = append(names, item.Name)
}
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(names, []string{"testnew"}) {
t.Fatalf("Expected function list ['testnew'], got %v", names)
}
}
// TestDeploy updates
func TestDeploy(t *testing.T) {
defer within(t, "testdata/example.com/deploy")()
verbose := true
client := newClient(verbose)
if err := client.New(context.Background(), fn.Function{Name: "deploy", Root: ".", Runtime: "go"}); err != nil {
t.Fatal(err)
}
defer del(t, client, "deploy")
if err := client.Deploy(context.Background(), "."); err != nil {
t.Fatal(err)
}
}
// TestRemove deletes
func TestRemove(t *testing.T) {
defer within(t, "testdata/example.com/remove")()
verbose := true
client := newClient(verbose)
if err := client.New(context.Background(), fn.Function{Name: "remove", Root: ".", Runtime: "go"}); err != nil {
t.Fatal(err)
}
waitFor(t, client, "remove")
if err := client.Remove(context.Background(), fn.Function{Name: "remove"}); err != nil {
t.Fatal(err)
}
names, err := client.List(context.Background())
if err != nil {
t.Fatal(err)
}
if len(names) != 0 {
t.Fatalf("Expected empty Functions list, got %v", names)
}
}
// TestRemoteRepositories ensures that initializing a Function
// defined in a remote repository finds the template, writes
// the expected files, and retains the expected modes.
// NOTE: this test only succeeds due to an override in
// templates' copyNode which forces mode 755 for directories.
// See https://github.com/go-git/go-git/issues/364
func TestRemoteRepositories(t *testing.T) {
defer within(t, "testdata/example.com/remote")()
// Write the test template from the remote onto root
client := fn.New(
fn.WithRegistry(DefaultRegistry),
fn.WithRepository("https://github.com/boson-project/test-templates"),
)
err := client.Create(fn.Function{
Root: ".",
Runtime: "runtime",
Template: "template",
})
if err != nil {
t.Fatal(err)
}
tests := []struct {
Path string
Perm uint32
Dir bool
}{
{Path: "file", Perm: 0644},
{Path: "dir-a/file", Perm: 0644},
{Path: "dir-b/file", Perm: 0644},
{Path: "dir-b/executable", Perm: 0755},
{Path: "dir-b", Perm: 0755},
{Path: "dir-a", Perm: 0755},
}
// Note that .Perm() are used to only consider the least-signifigant 9 and
// thus not have to consider the directory bit.
for _, test := range tests {
file, err := os.Stat(test.Path)
if err != nil {
t.Fatal(err)
}
t.Logf("%04o repository/%v", file.Mode().Perm(), test.Path)
if file.Mode().Perm() != os.FileMode(test.Perm) {
t.Fatalf("expected 'repository/%v' to have mode %04o, got %04o", test.Path, test.Perm, file.Mode().Perm())
}
}
}
// ***********
// Helpers
// ***********
// newClient creates an instance of the func client whose concrete impls
// match those created by the kn func plugin CLI.
func newClient(verbose bool) *fn.Client {
builder := buildpacks.NewBuilder()
builder.Verbose = verbose
pusher, err := docker.NewPusher()
if err != nil {
panic(err)
}
pusher.Verbose = verbose
deployer, err := knative.NewDeployer(DefaultNamespace)
if err != nil {
panic(err) // TODO: remove error from deployer constructor
}
deployer.Verbose = verbose
remover, err := knative.NewRemover(DefaultNamespace)
if err != nil {
panic(err) // TODO: remove error from remover constructor
}
remover.Verbose = verbose
lister, err := knative.NewLister(DefaultNamespace)
if err != nil {
panic(err) // TODO: remove error from lister constructor
}
lister.Verbose = verbose
return fn.New(
fn.WithRegistry(DefaultRegistry),
fn.WithVerbose(verbose),
fn.WithBuilder(builder),
fn.WithPusher(pusher),
fn.WithDeployer(deployer),
fn.WithRemover(remover),
fn.WithLister(lister),
)
}
// Del cleans up after a test by removing a function by name.
// (test fails if the named function does not exist)
//
// Intended to be run in a defer statement immediately after creation, del
// works around the asynchronicity of the underlying platform's creation
// step by polling the provider until the names function becomes available
// (or the test times out), before firing off a deletion request.
// Of course, ideally this would be replaced by the use of a synchronous
// method, or at a minimum a way to register a callback/listener for the
// creation event. This is what we have for now, and the show must go on.
func del(t *testing.T, c *fn.Client, name string) {
t.Helper()
waitFor(t, c, name)
if err := c.Remove(context.Background(), fn.Function{Name: name}); err != nil {
t.Fatal(err)
}
}
// waitFor the named Function to become available in List output.
// TODO: the API should be synchronous, but that depends first on
// Create returning the derived name such that we can bake polling in.
// Ideally the Boson provider's Creaet would be made syncrhonous.
func waitFor(t *testing.T, c *fn.Client, name string) {
t.Helper()
var pollInterval = 2 * time.Second
for { // ever (i.e. defer to global test timeout)
nn, err := c.List(context.Background())
if err != nil {
t.Fatal(err)
}
for _, n := range nn {
if n.Name == name {
return
}
}
time.Sleep(pollInterval)
}
}
// Create the given directory, CD to it, and return a function which can be
// run in a defer statement to return to the original directory and cleanup.
// Note must be executed, not deferred itself
// NO: defer within(t, "somedir")
// YES: defer within(t, "somedir")()
func within(t *testing.T, root string) func() {
t.Helper()
cwd := pwd(t)
mkdir(t, root)
cd(t, root)
return func() {
cd(t, cwd)
rm(t, root)
}
}
func pwd(t *testing.T) string {
t.Helper()
dir, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
return dir
}
func mkdir(t *testing.T, dir string) {
t.Helper()
if err := os.MkdirAll(dir, 0700); err != nil {
t.Fatal(err)
}
}
func cd(t *testing.T, dir string) {
t.Helper()
if err := os.Chdir(dir); err != nil {
t.Fatal(err)
}
}
func rm(t *testing.T, dir string) {
t.Helper()
if err := os.RemoveAll(dir); err != nil {
t.Fatal(err)
}
}
func touch(file string) {
_, err := os.Stat(file)
if os.IsNotExist(err) {
f, err := os.Create(file)
if err != nil {
panic(err)
}
defer f.Close()
}
t := time.Now().Local()
if err := os.Chtimes(file, t, t); err != nil {
panic(err)
}
}