@@ -19,17 +19,23 @@ import (
19
19
"context"
20
20
"crypto/rand"
21
21
"encoding/base64"
22
+ "encoding/json"
23
+ "fmt"
24
+ "math/big"
22
25
"reflect"
23
26
24
27
"k8s.io/apimachinery/pkg/util/intstr"
25
28
26
29
nbv1 "github.com/kubeflow/kubeflow/components/notebook-controller/api/v1"
30
+ oauthv1 "github.com/openshift/api/oauth/v1"
27
31
routev1 "github.com/openshift/api/route/v1"
28
32
corev1 "k8s.io/api/core/v1"
29
33
apierrs "k8s.io/apimachinery/pkg/api/errors"
30
34
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
31
35
"k8s.io/apimachinery/pkg/types"
32
36
ctrl "sigs.k8s.io/controller-runtime"
37
+ "sigs.k8s.io/controller-runtime/pkg/client"
38
+ logf "sigs.k8s.io/controller-runtime/pkg/log"
33
39
)
34
40
35
41
const (
@@ -39,12 +45,24 @@ const (
39
45
// taken from https://catalog.redhat.com/software/containers/openshift4/ose-oauth-proxy/5cdb2133bed8bd5717d5ae64?image=66cefc14401df6ff4664ec43&architecture=amd64&container-tabs=overview
40
46
// and kept in sync with the manifests here and in ClusterServiceVersion metadata of opendatahub operator
41
47
OAuthProxyImage = "registry.redhat.io/openshift4/ose-oauth-proxy@sha256:4f8d66597feeb32bb18699326029f9a71a5aca4a57679d636b876377c2e95695"
48
+
49
+ // Strings used in secret generation
50
+ letterRunes = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
51
+
52
+ // Complexity of generated secrets, this will be stored in a const for now, but in the future
53
+ // there is the possibility of creating a specific file to manage secrets, change the size of
54
+ // the complexity if required, etc. For now, a complexity of 16 will suffice.
55
+ SECRET_DEFAULT_COMPLEXITY = 16
42
56
)
43
57
44
58
type OAuthConfig struct {
45
59
ProxyImage string
46
60
}
47
61
62
+ type OAuthClientConfig struct {
63
+ Name string
64
+ }
65
+
48
66
// NewNotebookServiceAccount defines the desired service account object
49
67
func NewNotebookServiceAccount (notebook * nbv1.Notebook ) * corev1.ServiceAccount {
50
68
return & corev1.ServiceAccount {
@@ -209,39 +227,106 @@ func NewNotebookOAuthSecret(notebook *nbv1.Notebook) *corev1.Secret {
209
227
}
210
228
}
211
229
230
+ // The NewNotebookOAuthClientSecret will generate a random secret that will be used
231
+ // by the OAuth proxy sidecar container to automatically accept the required permissions
232
+ // for the user to access a notebook instead of showing up a page where the user needs to
233
+ // aggree with content he might not fully understand
234
+ // More info: https://issues.redhat.com/browse/RHOAIENG-11155
235
+ func NewNotebookOAuthClientSecret (notebook * nbv1.Notebook ) * corev1.Secret {
236
+ // Generate the client secret for the OAuth proxy
237
+ randomValue := make ([]byte , SECRET_DEFAULT_COMPLEXITY )
238
+ for i := 0 ; i < SECRET_DEFAULT_COMPLEXITY ; i ++ {
239
+ num , err := rand .Int (rand .Reader , big .NewInt (int64 (len (letterRunes ))))
240
+ if err != nil {
241
+ fmt .Printf ("Error generating secret: %v\n " , err )
242
+ }
243
+ randomValue [i ] = letterRunes [num .Int64 ()]
244
+ }
245
+
246
+ // Create a Kubernetes secret to store the cookie secret
247
+ return & corev1.Secret {
248
+ ObjectMeta : metav1.ObjectMeta {
249
+ Name : notebook .Name + "-oauth-client" ,
250
+ Namespace : notebook .Namespace ,
251
+ Labels : map [string ]string {
252
+ "notebook-name" : notebook .Name ,
253
+ },
254
+ },
255
+ StringData : map [string ]string {
256
+ "secret" : string (randomValue ),
257
+ },
258
+ }
259
+ }
260
+
212
261
// ReconcileOAuthSecret will manage the OAuth secret reconciliation required by
213
262
// the notebook OAuth proxy
214
263
func (r * OpenshiftNotebookReconciler ) ReconcileOAuthSecret (notebook * nbv1.Notebook , ctx context.Context ) error {
215
- // Initialize logger format
216
- log := r .Log .WithValues ("notebook" , notebook .Name , "namespace" , notebook .Namespace )
217
-
218
264
// Generate the desired OAuth secret
219
- desiredSecret := NewNotebookOAuthSecret (notebook )
265
+ desiredOAuthSecret := NewNotebookOAuthSecret (notebook )
266
+ _ = r .createSecret (notebook , ctx , desiredOAuthSecret )
267
+
268
+ // Generate the desired OAuthClientSecret
269
+ desiredOAuthClientSecret := NewNotebookOAuthClientSecret (notebook )
270
+ _ = r .createSecret (notebook , ctx , desiredOAuthClientSecret )
271
+
272
+ return nil
273
+ }
274
+
275
+ func (r * OpenshiftNotebookReconciler ) ReconcileOAuthClient (notebook * nbv1.Notebook , ctx context.Context ) error {
276
+ log := logf .FromContext (ctx )
220
277
221
- // Create the OAuth secret if it does not already exist
222
- foundSecret := & corev1.Secret {}
278
+ route := & routev1.Route {}
279
+ err := r .Get (ctx , types.NamespacedName {
280
+ Name : notebook .Name ,
281
+ Namespace : notebook .Namespace ,
282
+ }, route )
283
+ if err != nil {
284
+ if apierrs .IsNotFound (err ) {
285
+ log .Info ("Route not found, cannot create OAuthClient yet" , "route" , notebook .Name )
286
+ return nil
287
+ }
288
+ log .Error (err , "Failed to get Route for OAuthClient" )
289
+ return err
290
+ }
291
+
292
+ err = r .createOAuthClient (notebook , ctx )
293
+ if err != nil {
294
+ log .Error (err , "Unable to handle OAuthClient creation / update" )
295
+ return err
296
+ }
297
+
298
+ return nil
299
+ }
300
+
301
+ func (r * OpenshiftNotebookReconciler ) createSecret (notebook * nbv1.Notebook , ctx context.Context , desiredSecret * corev1.Secret ) error {
302
+ log := logf .FromContext (ctx )
303
+
304
+ secret := & corev1.Secret {}
223
305
err := r .Get (ctx , types.NamespacedName {
224
306
Name : desiredSecret .Name ,
225
307
Namespace : notebook .Namespace ,
226
- }, foundSecret )
308
+ }, secret )
309
+
227
310
if err != nil {
228
311
if apierrs .IsNotFound (err ) {
229
- log .Info ("Creating OAuth Secret" )
230
- // Add .metatada.ownerReferences to the OAuth secret to be deleted by
312
+ log .Info ("Creating " , "secret" , desiredSecret .Name )
313
+
314
+ // Add .metatada.ownerReferences to the OAuth client secret to be deleted by
231
315
// the Kubernetes garbage collector if the notebook is deleted
232
316
err = ctrl .SetControllerReference (notebook , desiredSecret , r .Scheme )
233
317
if err != nil {
234
- log .Error (err , "Unable to add OwnerReference to the OAuth Secret" )
318
+ log .Error (err , "Unable to add OwnerReference to secret " , desiredSecret . Name )
235
319
return err
236
320
}
237
- // Create the OAuth secret in the Openshift cluster
321
+
322
+ // Create the OAuth client secret in the Openshift cluster
238
323
err = r .Create (ctx , desiredSecret )
239
324
if err != nil && ! apierrs .IsAlreadyExists (err ) {
240
- log .Error (err , "Unable to create the OAuth Secret" )
325
+ log .Error (err , "Unable to create secret " , desiredSecret . Name )
241
326
return err
242
327
}
243
328
} else {
244
- log .Error (err , "Unable to fetch the OAuth Secret " )
329
+ log .Error (err , "Unable to fetch secret " )
245
330
return err
246
331
}
247
332
}
@@ -264,3 +349,64 @@ func (r *OpenshiftNotebookReconciler) ReconcileOAuthRoute(
264
349
notebook * nbv1.Notebook , ctx context.Context ) error {
265
350
return r .reconcileRoute (notebook , ctx , NewNotebookOAuthRoute )
266
351
}
352
+
353
+ func (r * OpenshiftNotebookReconciler ) createOAuthClient (notebook * nbv1.Notebook , ctx context.Context ) error {
354
+ log := logf .FromContext (ctx )
355
+
356
+ // Get the route that will be used in the OAuthClient
357
+ oauthClientRoute := & routev1.Route {}
358
+ err := r .Get (ctx , types.NamespacedName {
359
+ Name : notebook .Name ,
360
+ Namespace : notebook .Namespace ,
361
+ }, oauthClientRoute )
362
+ if err != nil {
363
+ log .Error (err , "Unable to retrieve route" , "route" , notebook .Name )
364
+ return err
365
+ }
366
+
367
+ // Get the secret that will be used in the OAuthClient
368
+ secret := & corev1.Secret {}
369
+ err = r .Get (ctx , types.NamespacedName {
370
+ Name : notebook .Name + "-oauth-client" ,
371
+ Namespace : notebook .Namespace ,
372
+ }, secret )
373
+ if err != nil {
374
+ log .Error (err , "Unable to retrieve secret" , "secret" , notebook .Name )
375
+ return err
376
+ }
377
+
378
+ stringData := string (secret .Data ["secret" ])
379
+
380
+ oauthClient := & oauthv1.OAuthClient {
381
+ TypeMeta : metav1.TypeMeta {
382
+ Kind : "OAuthClient" ,
383
+ APIVersion : "oauth.openshift.io/v1" ,
384
+ },
385
+ ObjectMeta : metav1.ObjectMeta {
386
+ Name : notebook .Name + "-" + notebook .Namespace + "-oauth-client" ,
387
+ Labels : map [string ]string {
388
+ "notebook-owner" : notebook .Name ,
389
+ },
390
+ },
391
+ Secret : stringData ,
392
+ RedirectURIs : []string {"https://" + oauthClientRoute .Spec .Host },
393
+ GrantMethod : oauthv1 .GrantHandlerAuto ,
394
+ }
395
+
396
+ err = r .Create (ctx , oauthClient )
397
+ if err != nil {
398
+ if apierrs .IsAlreadyExists (err ) {
399
+ data , err := json .Marshal (oauthClient )
400
+ if err != nil {
401
+ return fmt .Errorf ("failed to create OAuth Client: %w" , err )
402
+ }
403
+ if err = r .Patch (ctx , oauthClient , client .RawPatch (types .ApplyPatchType , data ),
404
+ client .ForceOwnership , client .FieldOwner ("odh-notebook-controller" )); err != nil {
405
+ return fmt .Errorf ("failed to patch existing OAuthClient CR: %w" , err )
406
+ }
407
+ return nil
408
+ }
409
+ }
410
+
411
+ return nil
412
+ }
0 commit comments