Merge pull request #1743 from allenporter/flux-envtest

Use shared envTest for unit tests
pull/1748/head
Stefan Prodan 3 years ago committed by GitHub
commit 6894f6f3bf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -77,6 +77,16 @@ You can run the unit tests by simply doing
make test
```
The e2e test suite uses [kind](https://kind.sigs.k8s.io/) for running kubernetes cluster inside docker containers. You can run the e2e tests by simply doing
```bash
make setup-kind
make e2e
# When done
make cleanup-kind
```
## Acceptance policy
These things will make a PR more likely to be accepted:

@ -27,8 +27,7 @@ func TestCheckPre(t *testing.T) {
serverVersion := strings.TrimPrefix(versions["serverVersion"].GitVersion, "v")
cmd := cmdTestCase{
args: "check --pre",
testClusterMode: ExistingClusterMode,
args: "check --pre",
assert: assertGoldenTemplateFile("testdata/check/check_pre.golden", map[string]string{
"clientVersion": clientVersion,
"serverVersion": serverVersion,

@ -39,7 +39,7 @@ func TestHelmReleaseFromGit(t *testing.T) {
},
}
namespace := "thrfg"
namespace := allocateNamespace("thrfg")
del, err := setupTestNamespace(namespace)
if err != nil {
t.Fatal(err)
@ -48,9 +48,8 @@ func TestHelmReleaseFromGit(t *testing.T) {
for _, tc := range cases {
cmd := cmdTestCase{
args: tc.args + " -n=" + namespace,
assert: assertGoldenFile(tc.goldenFile),
testClusterMode: ExistingClusterMode,
args: tc.args + " -n=" + namespace,
assert: assertGoldenTemplateFile(tc.goldenFile, map[string]string{"ns": namespace}),
}
cmd.runTestCmd(t)
}

@ -31,7 +31,7 @@ func TestImageScanning(t *testing.T) {
},
}
namespace := "tis"
namespace := allocateNamespace("tis")
del, err := setupTestNamespace(namespace)
if err != nil {
t.Fatal(err)
@ -40,9 +40,8 @@ func TestImageScanning(t *testing.T) {
for _, tc := range cases {
cmd := cmdTestCase{
args: tc.args + " -n=" + namespace,
assert: assertGoldenFile(tc.goldenFile),
testClusterMode: ExistingClusterMode,
args: tc.args + " -n=" + namespace,
assert: assertGoldenFile(tc.goldenFile),
}
cmd.runTestCmd(t)
}

@ -39,7 +39,7 @@ func TestKustomizationFromGit(t *testing.T) {
},
}
namespace := "tkfg"
namespace := allocateNamespace("tkfg")
del, err := setupTestNamespace(namespace)
if err != nil {
t.Fatal(err)
@ -48,9 +48,8 @@ func TestKustomizationFromGit(t *testing.T) {
for _, tc := range cases {
cmd := cmdTestCase{
args: tc.args + " -n=" + namespace,
assert: assertGoldenFile(tc.goldenFile),
testClusterMode: ExistingClusterMode,
args: tc.args + " -n=" + namespace,
assert: assertGoldenTemplateFile(tc.goldenFile, map[string]string{"ns": namespace}),
}
cmd.runTestCmd(t)
}

@ -15,12 +15,13 @@ func TestMain(m *testing.M) {
// Ensure tests print consistent timestamps regardless of timezone
os.Setenv("TZ", "UTC")
// Install Flux.
// Creating the test env manager sets rootArgs client flags
_, err := NewTestEnvKubeManager(ExistingClusterMode)
testEnv, err := NewTestEnvKubeManager(ExistingClusterMode)
if err != nil {
panic(fmt.Errorf("error creating kube manager: '%w'", err))
}
rootArgs.kubeconfig = testEnv.kubeConfigPath
// Install Flux.
output, err := executeCommand("install --components-extra=image-reflector-controller,image-automation-controller")
if err != nil {
panic(fmt.Errorf("install falied: %s error:'%w'", output, err))
@ -42,6 +43,8 @@ func TestMain(m *testing.M) {
panic(fmt.Errorf("delete namespace error:'%w'", err))
}
testEnv.Stop()
os.Exit(code)
}

@ -9,6 +9,8 @@ import (
"io/ioutil"
"os"
"path/filepath"
"strings"
"sync/atomic"
"testing"
"text/template"
"time"
@ -22,13 +24,18 @@ import (
"sigs.k8s.io/controller-runtime/pkg/envtest"
)
func readYamlObjects(objectFile string) ([]unstructured.Unstructured, error) {
obj, err := os.ReadFile(objectFile)
if err != nil {
return nil, err
}
var nextNamespaceId int64
// Return a unique namespace with the specified prefix, for tests to create
// objects that won't collide with each other.
func allocateNamespace(prefix string) string {
id := atomic.AddInt64(&nextNamespaceId, 1)
return fmt.Sprintf("%s-%d", prefix, id)
}
func readYamlObjects(rdr io.Reader) ([]unstructured.Unstructured, error) {
objects := []unstructured.Unstructured{}
reader := k8syaml.NewYAMLReader(bufio.NewReader(bytes.NewReader(obj)))
reader := k8syaml.NewYAMLReader(bufio.NewReader(rdr))
for {
doc, err := reader.Read()
if err != nil {
@ -49,15 +56,31 @@ func readYamlObjects(objectFile string) ([]unstructured.Unstructured, error) {
// A KubeManager that can create objects that are subject to a test.
type testEnvKubeManager struct {
client client.WithWatch
testEnv *envtest.Environment
client client.WithWatch
testEnv *envtest.Environment
kubeConfigPath string
}
func (m *testEnvKubeManager) NewClient(kubeconfig string, kubecontext string) (client.WithWatch, error) {
return m.client, nil
func (m *testEnvKubeManager) CreateObjectFile(objectFile string, templateValues map[string]string, t *testing.T) {
buf, err := os.ReadFile(objectFile)
if err != nil {
t.Fatalf("Error reading file '%s': %v", objectFile, err)
}
content, err := executeTemplate(string(buf), templateValues)
if err != nil {
t.Fatalf("Error evaluating template file '%s': '%v'", objectFile, err)
}
clientObjects, err := readYamlObjects(strings.NewReader(content))
if err != nil {
t.Fatalf("Error decoding yaml file '%s': %v", objectFile, err)
}
err = m.CreateObjects(clientObjects, t)
if err != nil {
t.Logf("Error creating test objects: '%v'", err)
}
}
func (m *testEnvKubeManager) CreateObjects(clientObjects []unstructured.Unstructured) error {
func (m *testEnvKubeManager) CreateObjects(clientObjects []unstructured.Unstructured, t *testing.T) error {
for _, obj := range clientObjects {
// First create the object then set its status if present in the
// yaml file. Make a copy first since creating an object may overwrite
@ -110,14 +133,14 @@ func NewTestEnvKubeManager(testClusterMode TestClusterMode) (*testEnvKubeManager
tmpFilename := filepath.Join("/tmp", "kubeconfig-"+time.Nanosecond.String())
ioutil.WriteFile(tmpFilename, kubeConfig, 0644)
rootArgs.kubeconfig = tmpFilename
k8sClient, err := client.NewWithWatch(cfg, client.Options{})
if err != nil {
return nil, err
}
return &testEnvKubeManager{
testEnv: testEnv,
client: k8sClient,
testEnv: testEnv,
client: k8sClient,
kubeConfigPath: tmpFilename,
}, nil
case ExistingClusterMode:
// TEST_KUBECONFIG is mandatory to prevent destroying a current cluster accidentally.
@ -125,7 +148,6 @@ func NewTestEnvKubeManager(testClusterMode TestClusterMode) (*testEnvKubeManager
if testKubeConfig == "" {
return nil, fmt.Errorf("environment variable TEST_KUBECONFIG is required to run tests against an existing cluster")
}
rootArgs.kubeconfig = testKubeConfig
useExistingCluster := true
config, err := clientcmd.BuildConfigFromFlags("", testKubeConfig)
@ -142,8 +164,9 @@ func NewTestEnvKubeManager(testClusterMode TestClusterMode) (*testEnvKubeManager
return nil, err
}
return &testEnvKubeManager{
testEnv: testEnv,
client: k8sClient,
testEnv: testEnv,
client: k8sClient,
kubeConfigPath: testKubeConfig,
}, nil
}
@ -217,7 +240,7 @@ func assertGoldenTemplateFile(goldenFile string, templateValues map[string]strin
}
var expectedOutput string
if len(templateValues) > 0 {
expectedOutput, err = executeGoldenTemplate(string(goldenFileContents), templateValues)
expectedOutput, err = executeTemplate(string(goldenFileContents), templateValues)
if err != nil {
return fmt.Errorf("Error executing golden template file '%s': %s", goldenFile, err)
}
@ -243,8 +266,6 @@ const (
type cmdTestCase struct {
// The command line arguments to test.
args string
// TestClusterMode to bootstrap and testing, default to Fake
testClusterMode TestClusterMode
// Tests use assertFunc to assert on an output, success or failure. This
// can be a function defined by the test or existing function above.
assert assertFunc
@ -253,34 +274,14 @@ type cmdTestCase struct {
}
func (cmd *cmdTestCase) runTestCmd(t *testing.T) {
km, err := NewTestEnvKubeManager(cmd.testClusterMode)
if err != nil {
t.Fatalf("Error creating kube manager: '%v'", err)
}
if km != nil {
defer km.Stop()
}
if cmd.objectFile != "" {
clientObjects, err := readYamlObjects(cmd.objectFile)
if err != nil {
t.Fatalf("Error loading yaml: '%v'", err)
}
err = km.CreateObjects(clientObjects)
if err != nil {
t.Fatalf("Error creating test objects: '%v'", err)
}
}
actual, testErr := executeCommand(cmd.args)
if assertErr := cmd.assert(actual, testErr); assertErr != nil {
t.Error(assertErr)
}
}
func executeGoldenTemplate(goldenValue string, templateValues map[string]string) (string, error) {
tmpl := template.Must(template.New("golden").Parse(goldenValue))
func executeTemplate(content string, templateValues map[string]string) (string, error) {
tmpl := template.Must(template.New("golden").Parse(content))
var out bytes.Buffer
if err := tmpl.Execute(&out, templateValues); err != nil {
return "", err

@ -3,12 +3,32 @@
package main
import (
"fmt"
"os"
"testing"
)
// The test environment is long running process shared between tests, initialized
// by a `TestMain` function depending on how the test is involved and which tests
// are a part of the build.
var testEnv *testEnvKubeManager
func TestMain(m *testing.M) {
// Ensure tests print consistent timestamps regardless of timezone
os.Setenv("TZ", "UTC")
os.Exit(m.Run())
// Creating the test env manager sets rootArgs client flags
km, err := NewTestEnvKubeManager(TestEnvClusterMode)
if err != nil {
panic(fmt.Errorf("error creating kube manager: '%w'", err))
}
testEnv = km
rootArgs.kubeconfig = testEnv.kubeConfigPath
// Run tests
code := m.Run()
km.Stop()
os.Exit(code)
}

@ -1,2 +1,2 @@
► deleting helmreleases thrfg in thrfg namespace
► deleting helmreleases thrfg in {{ .ns }} namespace
✔ helmreleases deleted

@ -1,9 +1,9 @@
► annotating GitRepository thrfg in thrfg namespace
► annotating GitRepository thrfg in {{ .ns }} namespace
✔ GitRepository annotated
◎ waiting for GitRepository reconciliation
✔ GitRepository reconciliation completed
✔ fetched revision 6.0.0/627d5c4bb67b77185f37e31d734b085019ff2951
► annotating HelmRelease thrfg in thrfg namespace
► annotating HelmRelease thrfg in {{ .ns }} namespace
✔ HelmRelease annotated
◎ waiting for HelmRelease reconciliation
✔ HelmRelease reconciliation completed

@ -1,4 +1,4 @@
► resuming helmreleases thrfg in thrfg namespace
► resuming helmreleases thrfg in {{ .ns }} namespace
✔ helmreleases resumed
◎ waiting for HelmRelease reconciliation
✔ HelmRelease reconciliation completed

@ -1,2 +1,2 @@
► suspending helmreleases thrfg in thrfg namespace
► suspending helmreleases thrfg in {{ .ns }} namespace
✔ helmreleases suspended

@ -1,2 +1,2 @@
► deleting kustomizations tkfg in tkfg namespace
► deleting kustomizations tkfg in {{ .ns }} namespace
✔ kustomizations deleted

@ -1,9 +1,9 @@
► annotating GitRepository tkfg in tkfg namespace
► annotating GitRepository tkfg in {{ .ns }} namespace
✔ GitRepository annotated
◎ waiting for GitRepository reconciliation
✔ GitRepository reconciliation completed
✔ fetched revision 6.0.0/627d5c4bb67b77185f37e31d734b085019ff2951
► annotating Kustomization tkfg in tkfg namespace
► annotating Kustomization tkfg in {{ .ns }} namespace
✔ Kustomization annotated
◎ waiting for Kustomization reconciliation
✔ Kustomization reconciliation completed

@ -1,4 +1,4 @@
► resuming kustomizations tkfg in tkfg namespace
► resuming kustomizations tkfg in {{ .ns }} namespace
✔ kustomizations resumed
◎ waiting for Kustomization reconciliation
✔ Kustomization reconciliation completed

@ -1,2 +1,2 @@
► suspending kustomizations tkfg in tkfg namespace
► suspending kustomizations tkfg in {{ .ns }} namespace
✔ kustomizations suspended

@ -1,16 +1,16 @@
Object: deployment/podinfo
Namespace: podinfo
Namespace: {{ .ns }}
Status: Managed by Flux
---
HelmRelease: podinfo
Namespace: podinfo
Namespace: {{ .ns }}
Revision: 6.0.0
Status: Last reconciled at 2021-07-16 15:42:20 +0000 UTC
Message: Release reconciliation succeeded
---
HelmChart: podinfo-podinfo
Namespace: flux-system
Namespace: {{ .fluxns }}
Chart: podinfo
Version: 6.0.0
Revision: 6.0.0
@ -18,7 +18,7 @@ Status: Last reconciled at 2021-07-16 15:32:09 +0000 UTC
Message: Fetched revision: 6.0.0
---
HelmRepository: podinfo
Namespace: flux-system
Namespace: {{ .fluxns }}
URL: https://stefanprodan.github.io/podinfo
Revision: 8411f23d07d3701f0e96e7d9e503b7936d7e1d56
Status: Last reconciled at 2021-07-11 00:25:46 +0000 UTC

@ -2,12 +2,12 @@
apiVersion: v1
kind: Namespace
metadata:
name: flux-system
name: {{ .fluxns }}
---
apiVersion: v1
kind: Namespace
metadata:
name: podinfo
name: {{ .ns }}
---
apiVersion: apps/v1
kind: Deployment
@ -15,11 +15,10 @@ metadata:
labels:
app.kubernetes.io/name: podinfo
app.kubernetes.io/managed-by: Helm
app.kubernetes.io/name: podinfo
helm.toolkit.fluxcd.io/name: podinfo
helm.toolkit.fluxcd.io/namespace: podinfo
helm.toolkit.fluxcd.io/namespace: {{ .ns }}
name: podinfo
namespace: podinfo
namespace: {{ .ns }}
spec:
replicas: 1
selector:
@ -40,9 +39,9 @@ kind: HelmRelease
metadata:
labels:
kustomize.toolkit.fluxcd.io/name: infrastructure
kustomize.toolkit.fluxcd.io/namespace: flux-system
kustomize.toolkit.fluxcd.io/namespace: {{ .fluxns }}
name: podinfo
namespace: podinfo
namespace: {{ .ns }}
spec:
chart:
spec:
@ -50,7 +49,7 @@ spec:
sourceRef:
kind: HelmRepository
name: podinfo
namespace: flux-system
namespace: {{ .fluxns }}
interval: 5m
status:
conditions:
@ -59,7 +58,7 @@ status:
reason: ReconciliationSucceeded
status: "True"
type: Ready
helmChart: flux-system/podinfo-podinfo
helmChart: {{ .fluxns }}/podinfo-podinfo
lastAppliedRevision: 6.0.0
lastAttemptedRevision: 6.0.0
lastAttemptedValuesChecksum: c31db75d05b7515eba2eef47bd71038c74b2e531
@ -68,7 +67,7 @@ apiVersion: source.toolkit.fluxcd.io/v1beta1
kind: HelmChart
metadata:
name: podinfo-podinfo
namespace: flux-system
namespace: {{ .fluxns }}
spec:
chart: podinfo
sourceRef:
@ -98,7 +97,7 @@ metadata:
kustomize.toolkit.fluxcd.io/name: infrastructure
kustomize.toolkit.fluxcd.io/namespace: flux-system
name: podinfo
namespace: flux-system
namespace: {{ .fluxns }}
spec:
interval: 5m
timeout: 1m0s
@ -121,7 +120,7 @@ apiVersion: kustomize.toolkit.fluxcd.io/v1beta1
kind: Kustomization
metadata:
name: infrastructure
namespace: flux-system
namespace: {{ .fluxns }}
spec:
path: ./infrastructure/
sourceRef:
@ -143,9 +142,9 @@ kind: GitRepository
metadata:
labels:
kustomize.toolkit.fluxcd.io/name: flux-system
kustomize.toolkit.fluxcd.io/namespace: flux-system
kustomize.toolkit.fluxcd.io/namespace: {{ .fluxns }}
name: flux-system
namespace: flux-system
namespace: {{ .fluxns }}
spec:
gitImplementation: go-git
ref:

@ -1,17 +1,17 @@
Object: HelmRelease/podinfo
Namespace: podinfo
Namespace: {{ .ns }}
Status: Managed by Flux
---
Kustomization: infrastructure
Namespace: flux-system
Namespace: {{ .fluxns }}
Path: ./infrastructure
Revision: main/696f056df216eea4f9401adbee0ff744d4df390f
Status: Last reconciled at 2021-08-01 04:52:56 +0000 UTC
Message: Applied revision: main/696f056df216eea4f9401adbee0ff744d4df390f
---
GitRepository: flux-system
Namespace: flux-system
Namespace: {{ .fluxns }}
URL: ssh://git@github.com/example/repo
Branch: main
Revision: main/696f056df216eea4f9401adbee0ff744d4df390f

@ -2,21 +2,21 @@
apiVersion: v1
kind: Namespace
metadata:
name: flux-system
name: {{ .fluxns }}
---
apiVersion: v1
kind: Namespace
metadata:
name: podinfo
name: {{ .ns }}
---
apiVersion: helm.toolkit.fluxcd.io/v2beta1
kind: HelmRelease
metadata:
labels:
kustomize.toolkit.fluxcd.io/name: infrastructure
kustomize.toolkit.fluxcd.io/namespace: flux-system
kustomize.toolkit.fluxcd.io/namespace: {{ .fluxns }}
name: podinfo
namespace: podinfo
namespace: {{ .ns }}
spec:
chart:
spec:
@ -24,7 +24,7 @@ spec:
sourceRef:
kind: HelmRepository
name: podinfo
namespace: flux-system
namespace: {{ .fluxns }}
interval: 5m
status:
conditions:
@ -33,7 +33,7 @@ status:
reason: ReconciliationSucceeded
status: "True"
type: Ready
helmChart: flux-system/podinfo-podinfo
helmChart: {{ .fluxns }}/podinfo-podinfo
lastAppliedRevision: 6.0.0
lastAttemptedRevision: 6.0.0
lastAttemptedValuesChecksum: c31db75d05b7515eba2eef47bd71038c74b2e531
@ -42,7 +42,7 @@ apiVersion: kustomize.toolkit.fluxcd.io/v1beta1
kind: Kustomization
metadata:
name: infrastructure
namespace: flux-system
namespace: {{ .fluxns }}
spec:
path: ./infrastructure
sourceRef:
@ -65,9 +65,9 @@ kind: GitRepository
metadata:
labels:
kustomize.toolkit.fluxcd.io/name: flux-system
kustomize.toolkit.fluxcd.io/namespace: flux-system
kustomize.toolkit.fluxcd.io/namespace: {{ .fluxns }}
name: flux-system
namespace: flux-system
namespace: {{ .fluxns }}
spec:
gitImplementation: go-git
ref:

@ -8,29 +8,44 @@ import (
func TestTraceNoArgs(t *testing.T) {
cmd := cmdTestCase{
args: "trace",
testClusterMode: TestEnvClusterMode,
assert: assertError("object name is required"),
args: "trace",
assert: assertError("object name is required"),
}
cmd.runTestCmd(t)
}
func TestTraceDeployment(t *testing.T) {
cmd := cmdTestCase{
args: "trace podinfo -n podinfo --kind deployment --api-version=apps/v1",
testClusterMode: TestEnvClusterMode,
assert: assertGoldenFile("testdata/trace/deployment.golden"),
objectFile: "testdata/trace/deployment.yaml",
func TestTrace(t *testing.T) {
cases := []struct {
name string
args string
objectFile string
goldenFile string
}{
{
"Deployment",
"trace podinfo --kind deployment --api-version=apps/v1",
"testdata/trace/deployment.yaml",
"testdata/trace/deployment.golden",
},
{
"HelmRelease",
"trace podinfo --kind HelmRelease --api-version=helm.toolkit.fluxcd.io/v2beta1",
"testdata/trace/helmrelease.yaml",
"testdata/trace/helmrelease.golden",
},
}
cmd.runTestCmd(t)
}
func TestTraceHelmRelease(t *testing.T) {
cmd := cmdTestCase{
args: "trace podinfo -n podinfo --kind HelmRelease --api-version=helm.toolkit.fluxcd.io/v2beta1",
testClusterMode: TestEnvClusterMode,
assert: assertGoldenFile("testdata/trace/helmrelease.golden"),
objectFile: "testdata/trace/helmrelease.yaml",
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
tmpl := map[string]string{
"ns": allocateNamespace("podinfo"),
"fluxns": allocateNamespace("flux-system"),
}
testEnv.CreateObjectFile(tc.objectFile, tmpl, t)
cmd := cmdTestCase{
args: tc.args + " -n=" + tmpl["ns"],
assert: assertGoldenTemplateFile(tc.goldenFile, tmpl),
}
cmd.runTestCmd(t)
})
}
cmd.runTestCmd(t)
}

@ -8,9 +8,8 @@ import (
func TestVersion(t *testing.T) {
cmd := cmdTestCase{
args: "--version",
testClusterMode: TestEnvClusterMode,
assert: assertGoldenValue("flux version 0.0.0-dev.0\n"),
args: "--version",
assert: assertGoldenValue("flux version 0.0.0-dev.0\n"),
}
cmd.runTestCmd(t)
}

Loading…
Cancel
Save