15
15
package test
16
16
17
17
import (
18
+ "bytes"
18
19
goctx "context"
20
+ "flag"
19
21
"fmt"
22
+ "io/ioutil"
20
23
"os"
24
+ "os/exec"
25
+ "path/filepath"
26
+ "strings"
21
27
"sync"
28
+ "testing"
22
29
"time"
23
30
24
31
// Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.)
25
32
_ "k8s.io/client-go/plugin/pkg/client/auth"
26
33
34
+ "github.com/operator-framework/operator-sdk/internal/scaffold"
27
35
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"
28
38
39
+ log "github.com/sirupsen/logrus"
29
40
extscheme "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/scheme"
30
41
"k8s.io/apimachinery/pkg/runtime"
31
- "k8s.io/apimachinery/pkg/runtime/serializer"
32
42
"k8s.io/apimachinery/pkg/util/wait"
33
43
cached "k8s.io/client-go/discovery/cached"
34
44
"k8s.io/client-go/kubernetes"
35
45
cgoscheme "k8s.io/client-go/kubernetes/scheme"
36
46
"k8s.io/client-go/rest"
37
47
"k8s.io/client-go/restmapper"
48
+ "k8s.io/client-go/tools/clientcmd"
38
49
dynclient "sigs.k8s.io/controller-runtime/pkg/client"
39
50
)
40
51
41
52
var (
42
53
// Global framework struct
43
54
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
52
55
)
53
56
54
57
type Framework struct {
@@ -59,52 +62,102 @@ type Framework struct {
59
62
NamespacedManPath * string
60
63
Namespace string
61
64
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
62
74
}
63
75
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 )
76
111
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
+ }
78
121
}
122
+
79
123
kubeclient , err := kubernetes .NewForConfig (kubeconfig )
80
124
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 )
82
126
}
127
+
83
128
scheme := runtime .NewScheme ()
84
129
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 )
86
131
}
87
132
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 )
89
134
}
135
+
90
136
cachedDiscoveryClient := cached .NewMemCacheClient (kubeclient .Discovery ())
91
- restMapper = restmapper .NewDeferredDiscoveryRESTMapper (cachedDiscoveryClient )
137
+ restMapper : = restmapper .NewDeferredDiscoveryRESTMapper (cachedDiscoveryClient )
92
138
restMapper .Reset ()
139
+
93
140
dynClient , err := dynclient .New (kubeconfig , dynclient.Options {Scheme : scheme , Mapper : restMapper })
94
141
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 )
96
143
}
97
- dynamicDecoder = serializer .NewCodecFactory (scheme ).UniversalDeserializer ()
98
- Global = & Framework {
144
+ framework := & Framework {
99
145
Client : & frameworkClient {Client : dynClient },
100
146
KubeConfig : kubeconfig ,
101
147
KubeClient : kubeclient ,
102
148
Scheme : scheme ,
103
- NamespacedManPath : namespacedManPath ,
149
+ NamespacedManPath : & opts . namespacedManPath ,
104
150
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 ,
106
159
}
107
- return nil
160
+ return framework , nil
108
161
}
109
162
110
163
type addToSchemeFunc func (* runtime.Scheme ) error
@@ -119,33 +172,118 @@ type addToSchemeFunc func(*runtime.Scheme) error
119
172
// by the time this function is called. If the CRD takes more than 5 seconds to
120
173
// become ready, this function throws an error
121
174
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 )
125
183
if err != nil {
126
184
return err
127
185
}
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 })
130
188
if err != nil {
131
189
return fmt .Errorf ("failed to initialize new dynamic client: (%v)" , err )
132
190
}
133
191
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 ))
136
194
} else {
137
195
err = dynClient .List (goctx .TODO (), obj , dynclient .InNamespace ("default" ))
138
196
}
139
197
if err != nil {
140
- restMapper .Reset ()
198
+ f . restMapper .Reset ()
141
199
return false , nil
142
200
}
143
- Global .Client = & frameworkClient {Client : dynClient }
201
+ f .Client = & frameworkClient {Client : dynClient }
144
202
return true , nil
145
203
})
146
204
if err != nil {
147
205
return fmt .Errorf ("failed to build the dynamic client: %v" , err )
148
206
}
149
- dynamicDecoder = serializer .NewCodecFactory (Global .Scheme ).UniversalDeserializer ()
150
207
return nil
151
208
}
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