Skip to content

Commit df05974

Browse files
committed
commands/.../test,pkg/test: add --up-local flag to test local
1 parent bf56b44 commit df05974

File tree

11 files changed

+132
-29
lines changed

11 files changed

+132
-29
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
### Added
88

99
- A new command [`operator-sdk print-deps`](https://github.com/operator-framework/operator-sdk/blob/master/doc/sdk-cli-reference.md#print-deps) which prints Golang packages and versions expected by the current Operator SDK version. Supplying `--as-file` prints packages and versions in Gopkg.toml format. ([#772](https://github.com/operator-framework/operator-sdk/pull/772))
10+
- Add [`up-local`](https://github.com/operator-framework/operator-sdk/blob/master/doc/sdk-cli-reference.md#flags-9) flag to `test local` subcommand ([#XXX](https://github.com/operator-framework/operator-sdk/pull/XXX))
1011

1112
### Bug fixes
1213

commands/operator-sdk/cmd/test/local.go

Lines changed: 31 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ type testLocalConfig struct {
3939
namespacedManPath string
4040
goTestFlags string
4141
namespace string
42+
upLocal bool
4243
}
4344

4445
var tlConfig testLocalConfig
@@ -54,6 +55,7 @@ func NewTestLocalCmd() *cobra.Command {
5455
testCmd.Flags().StringVar(&tlConfig.namespacedManPath, "namespaced-manifest", "", "Path to manifest for per-test, namespaced resources (e.g. RBAC and Operator manifest)")
5556
testCmd.Flags().StringVar(&tlConfig.goTestFlags, "go-test-flags", "", "Additional flags to pass to go test")
5657
testCmd.Flags().StringVar(&tlConfig.namespace, "namespace", "", "If non-empty, single namespace to run tests in")
58+
testCmd.Flags().BoolVar(&tlConfig.upLocal, "up-local", false, "Enable running operator locally with go run instead of as an image in the cluster")
5759

5860
return testCmd
5961
}
@@ -63,6 +65,10 @@ func testLocalFunc(cmd *cobra.Command, args []string) {
6365
log.Fatal("operator-sdk test local requires exactly 1 argument")
6466
}
6567

68+
if tlConfig.upLocal && tlConfig.namespace == "" {
69+
log.Fatal("must specify a namespace to run in when -up-local flag is set")
70+
}
71+
6672
log.Info("Testing operator locally.")
6773

6874
// if no namespaced manifest path is given, combine deploy/service_account.yaml, deploy/role.yaml, deploy/role_binding.yaml and deploy/operator.yaml
@@ -72,28 +78,29 @@ func testLocalFunc(cmd *cobra.Command, args []string) {
7278
log.Fatalf("could not create %s: (%v)", deployTestDir, err)
7379
}
7480
tlConfig.namespacedManPath = filepath.Join(deployTestDir, "namespace-manifests.yaml")
75-
76-
sa, err := ioutil.ReadFile(filepath.Join(scaffold.DeployDir, scaffold.ServiceAccountYamlFile))
77-
if err != nil {
78-
log.Warnf("could not find the serviceaccount manifest: (%v)", err)
79-
}
80-
role, err := ioutil.ReadFile(filepath.Join(scaffold.DeployDir, scaffold.RoleYamlFile))
81-
if err != nil {
82-
log.Warnf("could not find role manifest: (%v)", err)
83-
}
84-
roleBinding, err := ioutil.ReadFile(filepath.Join(scaffold.DeployDir, scaffold.RoleBindingYamlFile))
85-
if err != nil {
86-
log.Warnf("could not find role_binding manifest: (%v)", err)
87-
}
88-
operator, err := ioutil.ReadFile(filepath.Join(scaffold.DeployDir, scaffold.OperatorYamlFile))
89-
if err != nil {
90-
log.Fatalf("could not find operator manifest: (%v)", err)
91-
}
9281
combined := []byte{}
93-
combined = combineManifests(combined, sa)
94-
combined = combineManifests(combined, role)
95-
combined = combineManifests(combined, roleBinding)
96-
combined = append(combined, operator...)
82+
if !tlConfig.upLocal {
83+
sa, err := ioutil.ReadFile(filepath.Join(scaffold.DeployDir, scaffold.ServiceAccountYamlFile))
84+
if err != nil {
85+
log.Warnf("could not find the serviceaccount manifest: (%v)", err)
86+
}
87+
role, err := ioutil.ReadFile(filepath.Join(scaffold.DeployDir, scaffold.RoleYamlFile))
88+
if err != nil {
89+
log.Warnf("could not find role manifest: (%v)", err)
90+
}
91+
roleBinding, err := ioutil.ReadFile(filepath.Join(scaffold.DeployDir, scaffold.RoleBindingYamlFile))
92+
if err != nil {
93+
log.Warnf("could not find role_binding manifest: (%v)", err)
94+
}
95+
operator, err := ioutil.ReadFile(filepath.Join(scaffold.DeployDir, scaffold.OperatorYamlFile))
96+
if err != nil {
97+
log.Fatalf("could not find operator manifest: (%v)", err)
98+
}
99+
combined = combineManifests(combined, sa)
100+
combined = combineManifests(combined, role)
101+
combined = combineManifests(combined, roleBinding)
102+
combined = append(combined, operator...)
103+
}
97104
err = ioutil.WriteFile(tlConfig.namespacedManPath, combined, os.FileMode(fileutil.DefaultFileMode))
98105
if err != nil {
99106
log.Fatalf("could not create temporary namespaced manifest file: (%v)", err)
@@ -154,6 +161,9 @@ func testLocalFunc(cmd *cobra.Command, args []string) {
154161
if tlConfig.namespace != "" {
155162
testArgs = append(testArgs, "-"+test.SingleNamespaceFlag, "-parallel=1")
156163
}
164+
if tlConfig.upLocal {
165+
testArgs = append(testArgs, "-"+test.LocalOperatorFlag)
166+
}
157167
dc := exec.Command("go", testArgs...)
158168
dc.Env = append(os.Environ(), fmt.Sprintf("%v=%v", test.TestNamespaceEnv, tlConfig.namespace))
159169
dc.Dir = projutil.MustGetwd()

doc/sdk-cli-reference.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,7 @@ Runs the tests locally
257257
* `--namespaced-manifest` string - path to manifest for per-test, namespaced resources (default: combines deploy/service_account.yaml, deploy/rbac.yaml, and deploy/operator.yaml)
258258
* `--namespace` string - if non-empty, single namespace to run tests in (e.g. "operator-test") (default: "")
259259
* `--go-test-flags` string - extra arguments to pass to `go test` (e.g. -f "-v -parallel=2")
260+
* `--up-local` - Enable running operator locally with go run instead of as an image in the cluster
260261
* `-h, --help` - help for local
261262

262263
##### Use

doc/test-framework/writing-e2e-tests.md

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -121,10 +121,9 @@ in your namespaced manifest. The custom `Create` function use the controller-run
121121
creates a cleanup function that is called by `ctx.Cleanup` which deletes the resource and then waits for the resource to be
122122
fully deleted before returning. This is configurable with `CleanupOptions`. For info on how to use `CleanupOptions` see
123123
[this section](#how-to-use-cleanup).
124-
125124

126125
If you want to make sure the operator's deployment is fully ready before moving onto the next part of the
127-
test, the `WaitForDeployment` function from [e2eutil][e2eutil-link] (in the sdk under `pkg/test/e2eutil`) can be used:
126+
test, the `WaitForOperatorDeployment` function from [e2eutil][e2eutil-link] (in the sdk under `pkg/test/e2eutil`) can be used:
128127

129128
```go
130129
// get namespace
@@ -135,7 +134,7 @@ if err != nil {
135134
// get global framework variables
136135
f := framework.Global
137136
// wait for memcached-operator to be ready
138-
err = e2eutil.WaitForDeployment(t, f.KubeClient, namespace, "memcached-operator", 1, time.Second*5, time.Second*30)
137+
err = e2eutil.WaitForOperatorDeployment(t, f.KubeClient, namespace, "memcached-operator", 1, time.Second*5, time.Second*30)
139138
if err != nil {
140139
t.Fatal(err)
141140
}
@@ -188,7 +187,10 @@ if err != nil {
188187
```
189188

190189
Now we can check if the operator successfully worked. In the case of the memcached operator, it should have
191-
created a deployment called "example-memcached" with 3 replicas:
190+
created a deployment called "example-memcached" with 3 replicas. To check, we use the `WaitForDeployment` function, which
191+
is the same as `WaitForOperatorDeployment` with the exception that `WaitForOperatorDeployment` will skip waiting
192+
for the deployment if the test is run locally and the `--up-local` flag is set; the `WaitForDeployment` function always
193+
waits for the deployment:
192194

193195
```go
194196
// wait for example-memcached to reach 3 replicas
@@ -243,6 +245,16 @@ $ kubectl create namespace operator-test
243245
$ operator-sdk test local ./test/e2e --namespace operator-test
244246
```
245247

248+
To run the operator itself locally during the tests instead of starting a deployment in the cluster, you can use the
249+
`--up-local` flag. This mode will still create global resources, but by default will not create any in-cluster namespaced
250+
resources unless the user specifies one through the `--namespaced-manifest` flag. (Note: the `--up-local` flag requires
251+
the `--namespace` flag):
252+
253+
```shell
254+
$ kubectl create namespace operator-test
255+
$ operator-sdk test local ./test/e2e --namespace operator-test --up-local
256+
```
257+
246258
For more documentation on the `operator-sdk test local` command, see the [SDK CLI Reference][sdk-cli-ref] doc.
247259

248260
For advanced use cases, it is possible to run the tests via `go test` directly. As long as all flags defined

hack/tests/test-subcommand.sh

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,7 @@ operator-sdk test local . --global-manifest deploy/crds/cache_v1alpha1_memcached
1010
kubectl create namespace test-memcached
1111
operator-sdk test local . --namespace=test-memcached
1212
kubectl delete namespace test-memcached
13+
# test operator in up local mode
14+
kubectl create namespace test-memcached
15+
operator-sdk test local . --up-local --namespace=test-memcached
16+
kubectl delete namespace test-memcached

pkg/test/e2eutil/wait_util.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import (
1818
"testing"
1919
"time"
2020

21+
"github.com/operator-framework/operator-sdk/pkg/test"
22+
2123
apierrors "k8s.io/apimachinery/pkg/api/errors"
2224
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2325
"k8s.io/apimachinery/pkg/util/wait"
@@ -29,6 +31,20 @@ import (
2931
// This can be used in multiple ways, like verifying that a required resource is ready before trying to use it, or to test
3032
// failure handling, like simulated in SimulatePodFail.
3133
func WaitForDeployment(t *testing.T, kubeclient kubernetes.Interface, namespace, name string, replicas int, retryInterval, timeout time.Duration) error {
34+
return waitForDeployment(t, kubeclient, namespace, name, replicas, retryInterval, timeout, false)
35+
}
36+
37+
// WaitForOperatorDeployment has the same functionality as WaitForDeployment but will no wait for the deployment if the
38+
// test was run with a locally run operator (--up-local flag)
39+
func WaitForOperatorDeployment(t *testing.T, kubeclient kubernetes.Interface, namespace, name string, replicas int, retryInterval, timeout time.Duration) error {
40+
return waitForDeployment(t, kubeclient, namespace, name, replicas, retryInterval, timeout, true)
41+
}
42+
43+
func waitForDeployment(t *testing.T, kubeclient kubernetes.Interface, namespace, name string, replicas int, retryInterval, timeout time.Duration, isOperator bool) error {
44+
if isOperator && test.Global.LocalOperator {
45+
t.Logf("Operator is running locally; skip waitForDeployment\n")
46+
return nil
47+
}
3248
err := wait.Poll(retryInterval, timeout, func() (done bool, err error) {
3349
deployment, err := kubeclient.AppsV1().Deployments(namespace).Get(name, metav1.GetOptions{IncludeUninitialized: true})
3450
if err != nil {

pkg/test/framework.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,10 @@ type Framework struct {
5454
Scheme *runtime.Scheme
5555
NamespacedManPath *string
5656
Namespace string
57+
LocalOperator bool
5758
}
5859

59-
func setup(kubeconfigPath, namespacedManPath *string) error {
60+
func setup(kubeconfigPath, namespacedManPath *string, localOperator bool) error {
6061
var err error
6162
var kubeconfig *rest.Config
6263
if *kubeconfigPath == "incluster" {
@@ -105,6 +106,7 @@ func setup(kubeconfigPath, namespacedManPath *string) error {
105106
Scheme: scheme,
106107
NamespacedManPath: namespacedManPath,
107108
Namespace: namespace,
109+
LocalOperator: localOperator,
108110
}
109111
return nil
110112
}

pkg/test/main_entry.go

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,21 @@
1515
package test
1616

1717
import (
18+
"bytes"
1819
"flag"
20+
"fmt"
1921
"io/ioutil"
2022
"os"
23+
"os/exec"
24+
"os/signal"
25+
"path/filepath"
26+
"syscall"
2127
"testing"
2228

29+
"k8s.io/client-go/tools/clientcmd"
30+
31+
"github.com/operator-framework/operator-sdk/pkg/k8sutil"
32+
"github.com/operator-framework/operator-sdk/pkg/scaffold"
2333
log "github.com/sirupsen/logrus"
2434
)
2535

@@ -30,6 +40,7 @@ const (
3040
GlobalManPathFlag = "globalMan"
3141
SingleNamespaceFlag = "singleNamespace"
3242
TestNamespaceEnv = "TEST_NAMESPACE"
43+
LocalOperatorFlag = "localOperator"
3344
)
3445

3546
func MainEntry(m *testing.M) {
@@ -38,21 +49,63 @@ func MainEntry(m *testing.M) {
3849
globalManPath := flag.String(GlobalManPathFlag, "", "path to operator manifest")
3950
namespacedManPath := flag.String(NamespacedManPathFlag, "", "path to rbac manifest")
4051
singleNamespace = flag.Bool(SingleNamespaceFlag, false, "enable single namespace mode")
52+
localOperator := flag.Bool(LocalOperatorFlag, false, "enable if operator is running locally (not in cluster)")
4153
flag.Parse()
4254
// go test always runs from the test directory; change to project root
4355
err := os.Chdir(*projRoot)
4456
if err != nil {
4557
log.Fatalf("failed to change directory to project root: %v", err)
4658
}
47-
if err := setup(kubeconfigPath, namespacedManPath); err != nil {
59+
if err := setup(kubeconfigPath, namespacedManPath, *localOperator); err != nil {
4860
log.Fatalf("failed to set up framework: %v", err)
4961
}
62+
// setup local operator command, but don't start it yet
63+
var localCmd *exec.Cmd
64+
var localCmdOutBuf, localCmdErrBuf bytes.Buffer
65+
if *localOperator {
66+
// taken from commands/operator-sdk/cmd/up/local.go
67+
runArgs := append([]string{"run"}, []string{filepath.Join(scaffold.ManagerDir, scaffold.CmdFile)}...)
68+
localCmd = exec.Command("go", runArgs...)
69+
localCmd.Stdout = &localCmdOutBuf
70+
localCmd.Stderr = &localCmdErrBuf
71+
c := make(chan os.Signal)
72+
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
73+
go func() {
74+
<-c
75+
err := localCmd.Process.Signal(os.Interrupt)
76+
if err != nil {
77+
log.Fatalf("failed to terminate the operator: (%v)", err)
78+
}
79+
os.Exit(0)
80+
}()
81+
if *kubeconfigPath != "" {
82+
localCmd.Env = append(os.Environ(), fmt.Sprintf("%v=%v", k8sutil.KubeConfigEnvVar, kubeconfigPath))
83+
} else {
84+
// we can hardcode index 0 as that is the highest priority kubeconfig to be loaded and will always
85+
// be populated by NewDefaultClientConfigLoadingRules()
86+
localCmd.Env = append(os.Environ(), fmt.Sprintf("%v=%v", k8sutil.KubeConfigEnvVar, clientcmd.NewDefaultClientConfigLoadingRules().Precedence[0]))
87+
}
88+
localCmd.Env = append(localCmd.Env, fmt.Sprintf("%v=%v", k8sutil.WatchNamespaceEnvVar, Global.Namespace))
89+
}
5090
// setup context to use when setting up crd
5191
ctx := NewTestCtx(nil)
5292
// os.Exit stops the program before the deferred functions run
5393
// to fix this, we put the exit in the defer as well
5494
defer func() {
95+
// start local operator before running tests
96+
if *localOperator {
97+
err := localCmd.Start()
98+
if err != nil {
99+
log.Fatalf("failed to run operator locally: (%v)", err)
100+
}
101+
log.Info("started local operator")
102+
}
55103
exitCode := m.Run()
104+
if *localOperator {
105+
localCmd.Process.Kill()
106+
log.Infof("local operator stdout: %s", string(localCmdOutBuf.Bytes()))
107+
log.Infof("local operator stderr: %s", string(localCmdErrBuf.Bytes()))
108+
}
56109
ctx.CleanupNoT()
57110
os.Exit(exitCode)
58111
}()

pkg/test/resource_creator.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,10 @@ func (ctx *TestCtx) createFromYAML(yamlFile []byte, skipIfExists bool, cleanupOp
6666
}
6767
yamlSplit := bytes.Split(yamlFile, []byte("\n---\n"))
6868
for _, yamlSpec := range yamlSplit {
69+
// some autogenerated files may include an extra `---` at the end of the file or be empty
70+
if string(yamlSpec) == "" {
71+
continue
72+
}
6973
yamlSpec, err = setNamespaceYAML(yamlSpec, namespace)
7074
if err != nil {
7175
return err

test/e2e/incluster-test-code/memcached_test.go.tmpl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ func MemcachedCluster(t *testing.T) {
114114
// get global framework variables
115115
f := framework.Global
116116
// wait for memcached-operator to be ready
117-
err = e2eutil.WaitForDeployment(t, f.KubeClient, namespace, "memcached-operator", 1, retryInterval, timeout)
117+
err = e2eutil.WaitForOperatorDeployment(t, f.KubeClient, namespace, "memcached-operator", 1, retryInterval, timeout)
118118
if err != nil {
119119
t.Fatal(err)
120120
}

test/test-framework/memcached_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ func MemcachedCluster(t *testing.T) {
114114
// get global framework variables
115115
f := framework.Global
116116
// wait for memcached-operator to be ready
117-
err = e2eutil.WaitForDeployment(t, f.KubeClient, namespace, "memcached-operator", 1, retryInterval, timeout)
117+
err = e2eutil.WaitForOperatorDeployment(t, f.KubeClient, namespace, "memcached-operator", 1, retryInterval, timeout)
118118
if err != nil {
119119
t.Fatal(err)
120120
}

0 commit comments

Comments
 (0)