Skip to content

Commit e03cde6

Browse files
joelanfordjmrodri
authored andcommitted
Refactor e2e framework to remove globals (#2037)
* refactor e2e framework to remove globals * pkg/test/context.go: initialize TestCtx kubeclient * cmd/operator-sdk/test/local.go: use go1.13 error wrapping to check test failure reason
1 parent 460022f commit e03cde6

File tree

6 files changed

+229
-166
lines changed

6 files changed

+229
-166
lines changed

cmd/operator-sdk/test/local.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
package test
1616

1717
import (
18+
"errors"
1819
"fmt"
1920
"io/ioutil"
2021
"os"
@@ -225,6 +226,10 @@ func testLocalGoFunc(cmd *cobra.Command, args []string) error {
225226
TestBinaryArgs: testArgs,
226227
}
227228
if err := projutil.GoTest(opts); err != nil {
229+
var exitErr *exec.ExitError
230+
if errors.As(err, &exitErr) {
231+
os.Exit(exitErr.ExitCode())
232+
}
228233
return fmt.Errorf("failed to build test binary: (%v)", err)
229234
}
230235
log.Info("Local operator test successfully completed.")

internal/util/projutil/exec.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ func ExecCmd(cmd *exec.Cmd) error {
2929
cmd.Stderr = os.Stderr
3030
log.Debugf("Running %#v", cmd.Args)
3131
if err := cmd.Run(); err != nil {
32-
return fmt.Errorf("failed to exec %#v: %v", cmd.Args, err)
32+
return fmt.Errorf("failed to exec %#v: %w", cmd.Args, err)
3333
}
3434
return nil
3535
}

pkg/test/context.go

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,20 @@ import (
2121
"time"
2222

2323
log "github.com/sirupsen/logrus"
24+
"k8s.io/client-go/kubernetes"
25+
"k8s.io/client-go/restmapper"
2426
)
2527

2628
type TestCtx struct {
2729
id string
2830
cleanupFns []cleanupFn
2931
namespace string
3032
t *testing.T
33+
34+
namespacedManPath string
35+
client *frameworkClient
36+
kubeclient kubernetes.Interface
37+
restMapper *restmapper.DeferredDiscoveryRESTMapper
3138
}
3239

3340
type CleanupOptions struct {
@@ -38,7 +45,7 @@ type CleanupOptions struct {
3845

3946
type cleanupFn func() error
4047

41-
func NewTestCtx(t *testing.T) *TestCtx {
48+
func (f *Framework) newTestCtx(t *testing.T) *TestCtx {
4249
var prefix string
4350
if t != nil {
4451
// TestCtx is used among others for namespace names where '/' is forbidden
@@ -56,12 +63,26 @@ func NewTestCtx(t *testing.T) *TestCtx {
5663
}
5764

5865
id := prefix + "-" + strconv.FormatInt(time.Now().Unix(), 10)
66+
67+
var namespace string
68+
if f.singleNamespaceMode {
69+
namespace = f.Namespace
70+
}
5971
return &TestCtx{
60-
id: id,
61-
t: t,
72+
id: id,
73+
t: t,
74+
namespace: namespace,
75+
namespacedManPath: *f.NamespacedManPath,
76+
client: f.Client,
77+
kubeclient: f.KubeClient,
78+
restMapper: f.restMapper,
6279
}
6380
}
6481

82+
func NewTestCtx(t *testing.T) *TestCtx {
83+
return Global.newTestCtx(t)
84+
}
85+
6586
func (ctx *TestCtx) GetID() string {
6687
return ctx.id
6788
}

pkg/test/framework.go

Lines changed: 180 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -15,40 +15,43 @@
1515
package test
1616

1717
import (
18+
"bytes"
1819
goctx "context"
20+
"flag"
1921
"fmt"
22+
"io/ioutil"
2023
"os"
24+
"os/exec"
25+
"path/filepath"
26+
"strings"
2127
"sync"
28+
"testing"
2229
"time"
2330

2431
// Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.)
2532
_ "k8s.io/client-go/plugin/pkg/client/auth"
2633

34+
"github.com/operator-framework/operator-sdk/internal/scaffold"
2735
k8sInternal "github.com/operator-framework/operator-sdk/internal/util/k8sutil"
36+
"github.com/operator-framework/operator-sdk/internal/util/projutil"
37+
"github.com/operator-framework/operator-sdk/pkg/k8sutil"
2838

39+
log "github.com/sirupsen/logrus"
2940
extscheme "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/scheme"
3041
"k8s.io/apimachinery/pkg/runtime"
31-
"k8s.io/apimachinery/pkg/runtime/serializer"
3242
"k8s.io/apimachinery/pkg/util/wait"
3343
cached "k8s.io/client-go/discovery/cached"
3444
"k8s.io/client-go/kubernetes"
3545
cgoscheme "k8s.io/client-go/kubernetes/scheme"
3646
"k8s.io/client-go/rest"
3747
"k8s.io/client-go/restmapper"
48+
"k8s.io/client-go/tools/clientcmd"
3849
dynclient "sigs.k8s.io/controller-runtime/pkg/client"
3950
)
4051

4152
var (
4253
// Global framework struct
4354
Global *Framework
44-
// mutex for AddToFrameworkScheme
45-
mutex = sync.Mutex{}
46-
// whether to run tests in a single namespace
47-
singleNamespace *bool
48-
// decoder used by createFromYaml
49-
dynamicDecoder runtime.Decoder
50-
// restMapper for the dynamic client
51-
restMapper *restmapper.DeferredDiscoveryRESTMapper
5255
)
5356

5457
type Framework struct {
@@ -59,52 +62,102 @@ type Framework struct {
5962
NamespacedManPath *string
6063
Namespace string
6164
LocalOperator bool
65+
66+
projectRoot string
67+
singleNamespaceMode bool
68+
globalManPath string
69+
localOperatorArgs string
70+
kubeconfigPath string
71+
restMapper *restmapper.DeferredDiscoveryRESTMapper
72+
73+
schemeMutex sync.Mutex
6274
}
6375

64-
func setup(kubeconfigPath, namespacedManPath *string, localOperator bool) error {
65-
namespace := ""
66-
if *singleNamespace {
67-
namespace = os.Getenv(TestNamespaceEnv)
68-
}
69-
var err error
70-
var kubeconfig *rest.Config
71-
var kcNamespace string
72-
kubeconfig, kcNamespace, err = k8sInternal.GetKubeconfigAndNamespace(*kubeconfigPath)
73-
if *singleNamespace && namespace == "" {
74-
namespace = kcNamespace
75-
}
76+
type frameworkOpts struct {
77+
projectRoot string
78+
kubeconfigPath string
79+
globalManPath string
80+
namespacedManPath string
81+
localOperator bool
82+
singleNamespaceMode bool
83+
isLocalOperator bool
84+
localOperatorArgs string
85+
}
86+
87+
const (
88+
ProjRootFlag = "root"
89+
KubeConfigFlag = "kubeconfig"
90+
NamespacedManPathFlag = "namespacedMan"
91+
GlobalManPathFlag = "globalMan"
92+
SingleNamespaceFlag = "singleNamespace"
93+
LocalOperatorFlag = "localOperator"
94+
LocalOperatorArgs = "localOperatorArgs"
95+
96+
TestNamespaceEnv = "TEST_NAMESPACE"
97+
)
98+
99+
func (opts *frameworkOpts) addToFlagSet(flagset *flag.FlagSet) {
100+
flagset.StringVar(&opts.projectRoot, ProjRootFlag, "", "path to project root")
101+
flagset.StringVar(&opts.namespacedManPath, NamespacedManPathFlag, "", "path to rbac manifest")
102+
flagset.BoolVar(&opts.isLocalOperator, LocalOperatorFlag, false, "enable if operator is running locally (not in cluster)")
103+
flagset.StringVar(&opts.kubeconfigPath, KubeConfigFlag, "", "path to kubeconfig")
104+
flagset.StringVar(&opts.globalManPath, GlobalManPathFlag, "", "path to operator manifest")
105+
flagset.BoolVar(&opts.singleNamespaceMode, SingleNamespaceFlag, false, "enable single namespace mode")
106+
flagset.StringVar(&opts.localOperatorArgs, LocalOperatorArgs, "", "flags that the operator needs (while using --up-local). example: \"--flag1 value1 --flag2=value2\"")
107+
}
108+
109+
func newFramework(opts *frameworkOpts) (*Framework, error) {
110+
kubeconfig, kcNamespace, err := k8sInternal.GetKubeconfigAndNamespace(opts.kubeconfigPath)
76111
if err != nil {
77-
return fmt.Errorf("failed to build the kubeconfig: %v", err)
112+
return nil, fmt.Errorf("failed to build the kubeconfig: %v", err)
113+
}
114+
115+
namespace := kcNamespace
116+
if opts.singleNamespaceMode {
117+
testNamespace := os.Getenv(TestNamespaceEnv)
118+
if testNamespace != "" {
119+
namespace = testNamespace
120+
}
78121
}
122+
79123
kubeclient, err := kubernetes.NewForConfig(kubeconfig)
80124
if err != nil {
81-
return fmt.Errorf("failed to build the kubeclient: %v", err)
125+
return nil, fmt.Errorf("failed to build the kubeclient: %v", err)
82126
}
127+
83128
scheme := runtime.NewScheme()
84129
if err := cgoscheme.AddToScheme(scheme); err != nil {
85-
return fmt.Errorf("failed to add cgo scheme to runtime scheme: (%v)", err)
130+
return nil, fmt.Errorf("failed to add cgo scheme to runtime scheme: (%v)", err)
86131
}
87132
if err := extscheme.AddToScheme(scheme); err != nil {
88-
return fmt.Errorf("failed to add api extensions scheme to runtime scheme: (%v)", err)
133+
return nil, fmt.Errorf("failed to add api extensions scheme to runtime scheme: (%v)", err)
89134
}
135+
90136
cachedDiscoveryClient := cached.NewMemCacheClient(kubeclient.Discovery())
91-
restMapper = restmapper.NewDeferredDiscoveryRESTMapper(cachedDiscoveryClient)
137+
restMapper := restmapper.NewDeferredDiscoveryRESTMapper(cachedDiscoveryClient)
92138
restMapper.Reset()
139+
93140
dynClient, err := dynclient.New(kubeconfig, dynclient.Options{Scheme: scheme, Mapper: restMapper})
94141
if err != nil {
95-
return fmt.Errorf("failed to build the dynamic client: %v", err)
142+
return nil, fmt.Errorf("failed to build the dynamic client: %v", err)
96143
}
97-
dynamicDecoder = serializer.NewCodecFactory(scheme).UniversalDeserializer()
98-
Global = &Framework{
144+
framework := &Framework{
99145
Client: &frameworkClient{Client: dynClient},
100146
KubeConfig: kubeconfig,
101147
KubeClient: kubeclient,
102148
Scheme: scheme,
103-
NamespacedManPath: namespacedManPath,
149+
NamespacedManPath: &opts.namespacedManPath,
104150
Namespace: namespace,
105-
LocalOperator: localOperator,
151+
LocalOperator: opts.isLocalOperator,
152+
153+
projectRoot: opts.projectRoot,
154+
singleNamespaceMode: opts.singleNamespaceMode,
155+
globalManPath: opts.globalManPath,
156+
localOperatorArgs: opts.localOperatorArgs,
157+
kubeconfigPath: opts.kubeconfigPath,
158+
restMapper: restMapper,
106159
}
107-
return nil
160+
return framework, nil
108161
}
109162

110163
type addToSchemeFunc func(*runtime.Scheme) error
@@ -119,33 +172,118 @@ type addToSchemeFunc func(*runtime.Scheme) error
119172
// by the time this function is called. If the CRD takes more than 5 seconds to
120173
// become ready, this function throws an error
121174
func AddToFrameworkScheme(addToScheme addToSchemeFunc, obj runtime.Object) error {
122-
mutex.Lock()
123-
defer mutex.Unlock()
124-
err := addToScheme(Global.Scheme)
175+
return Global.addToScheme(addToScheme, obj)
176+
}
177+
178+
func (f *Framework) addToScheme(addToScheme addToSchemeFunc, obj runtime.Object) error {
179+
f.schemeMutex.Lock()
180+
defer f.schemeMutex.Unlock()
181+
182+
err := addToScheme(f.Scheme)
125183
if err != nil {
126184
return err
127185
}
128-
restMapper.Reset()
129-
dynClient, err := dynclient.New(Global.KubeConfig, dynclient.Options{Scheme: Global.Scheme, Mapper: restMapper})
186+
f.restMapper.Reset()
187+
dynClient, err := dynclient.New(f.KubeConfig, dynclient.Options{Scheme: f.Scheme, Mapper: f.restMapper})
130188
if err != nil {
131189
return fmt.Errorf("failed to initialize new dynamic client: (%v)", err)
132190
}
133191
err = wait.PollImmediate(time.Second, time.Second*10, func() (done bool, err error) {
134-
if *singleNamespace {
135-
err = dynClient.List(goctx.TODO(), obj, dynclient.InNamespace(Global.Namespace))
192+
if f.singleNamespaceMode {
193+
err = dynClient.List(goctx.TODO(), obj, dynclient.InNamespace(f.Namespace))
136194
} else {
137195
err = dynClient.List(goctx.TODO(), obj, dynclient.InNamespace("default"))
138196
}
139197
if err != nil {
140-
restMapper.Reset()
198+
f.restMapper.Reset()
141199
return false, nil
142200
}
143-
Global.Client = &frameworkClient{Client: dynClient}
201+
f.Client = &frameworkClient{Client: dynClient}
144202
return true, nil
145203
})
146204
if err != nil {
147205
return fmt.Errorf("failed to build the dynamic client: %v", err)
148206
}
149-
dynamicDecoder = serializer.NewCodecFactory(Global.Scheme).UniversalDeserializer()
150207
return nil
151208
}
209+
210+
func (f *Framework) runM(m *testing.M) (int, error) {
211+
// setup context to use when setting up crd
212+
ctx := f.newTestCtx(nil)
213+
defer ctx.Cleanup()
214+
215+
// go test always runs from the test directory; change to project root
216+
err := os.Chdir(f.projectRoot)
217+
if err != nil {
218+
return 0, fmt.Errorf("failed to change directory to project root: %v", err)
219+
}
220+
221+
// create crd
222+
globalYAML, err := ioutil.ReadFile(f.globalManPath)
223+
if err != nil {
224+
return 0, fmt.Errorf("failed to read global resource manifest: %v", err)
225+
}
226+
err = ctx.createFromYAML(globalYAML, true, &CleanupOptions{TestContext: ctx})
227+
if err != nil {
228+
return 0, fmt.Errorf("failed to create resource(s) in global resource manifest: %v", err)
229+
}
230+
231+
if !f.LocalOperator {
232+
return m.Run(), nil
233+
}
234+
235+
// start local operator before running tests
236+
outBuf := &bytes.Buffer{}
237+
localCmd, err := f.setupLocalCommand()
238+
if err != nil {
239+
return 0, fmt.Errorf("failed to setup local command: %v", err)
240+
}
241+
localCmd.Stdout = outBuf
242+
localCmd.Stderr = outBuf
243+
244+
err = localCmd.Start()
245+
if err != nil {
246+
return 0, fmt.Errorf("failed to run operator locally: %v", err)
247+
}
248+
log.Info("Started local operator")
249+
250+
// run the tests
251+
exitCode := m.Run()
252+
253+
// kill the local operator and print its logs
254+
err = localCmd.Process.Kill()
255+
if err != nil {
256+
log.Warn("Failed to stop local operator process")
257+
}
258+
fmt.Printf("\n------ Local operator output ------\n%s\n", outBuf.String())
259+
return exitCode, nil
260+
}
261+
262+
func (f *Framework) setupLocalCommand() (*exec.Cmd, error) {
263+
projectName := filepath.Base(projutil.MustGetwd())
264+
outputBinName := filepath.Join(scaffold.BuildBinDir, projectName+"-local")
265+
opts := projutil.GoCmdOptions{
266+
BinName: outputBinName,
267+
PackagePath: filepath.Join(scaffold.ManagerDir, scaffold.CmdFile),
268+
}
269+
if err := projutil.GoBuild(opts); err != nil {
270+
return nil, fmt.Errorf("failed to build local operator binary: %s", err)
271+
}
272+
273+
args := []string{}
274+
if f.localOperatorArgs != "" {
275+
args = append(args, strings.Split(f.localOperatorArgs, " ")...)
276+
}
277+
278+
localCmd := exec.Command(outputBinName, args...)
279+
280+
if f.kubeconfigPath != "" {
281+
localCmd.Env = append(os.Environ(), fmt.Sprintf("%v=%v", k8sutil.KubeConfigEnvVar, f.kubeconfigPath))
282+
} else {
283+
// we can hardcode index 0 as that is the highest priority kubeconfig to be loaded and will always
284+
// be populated by NewDefaultClientConfigLoadingRules()
285+
localCmd.Env = append(os.Environ(), fmt.Sprintf("%v=%v", k8sutil.KubeConfigEnvVar, clientcmd.NewDefaultClientConfigLoadingRules().Precedence[0]))
286+
}
287+
localCmd.Env = append(localCmd.Env, fmt.Sprintf("%v=%v", k8sutil.WatchNamespaceEnvVar, f.Namespace))
288+
return localCmd, nil
289+
}

0 commit comments

Comments
 (0)