Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit c65dbce

Browse files
committedMar 20, 2024
add raycluster controller to CFO
Signed-off-by: Kevin <[email protected]>
1 parent 7419586 commit c65dbce

12 files changed

+1000
-172
lines changed
 

‎Dockerfile

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ RUN go mod download
1010
# Copy the Go sources
1111
COPY main.go main.go
1212
COPY pkg/ pkg/
13+
COPY controllers/ controllers/
1314

1415
# Build
1516
USER root

‎PROJECT

+10
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
# Code generated by tool. DO NOT EDIT.
2+
# This file is used to track the info used to scaffold your project
3+
# and allow the plugins properly work.
4+
# More info: https://book.kubebuilder.io/reference/project-config.html
15
domain: codeflare.dev
26
layout:
37
- go.kubebuilder.io/v3
@@ -6,4 +10,10 @@ plugins:
610
scorecard.sdk.operatorframework.io/v2: {}
711
projectName: codeflare-operator
812
repo: github.com/project-codeflare/codeflare-operator
13+
resources:
14+
- controller: true
15+
domain: codeflare.dev
16+
group: ray
17+
kind: RayCluster
18+
version: v1
919
version: "3"

‎config/rbac/kustomization.yaml

+2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ resources:
77
- admin_role.yaml
88
- editor_role.yaml
99
- service_account.yaml
10+
- mcad_manager_role.yaml
11+
- mcad_manager_role_binding.yaml
1012
- role.yaml
1113
- role_binding.yaml
1214
- instascale_role.yaml

‎config/rbac/mcad_manager_role.yaml

+223
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
---
2+
apiVersion: rbac.authorization.k8s.io/v1
3+
kind: ClusterRole
4+
metadata:
5+
creationTimestamp: null
6+
name: manual-manager-role
7+
rules:
8+
- apiGroups:
9+
- '*'
10+
resources:
11+
- deployments
12+
- services
13+
verbs:
14+
- create
15+
- delete
16+
- get
17+
- list
18+
- patch
19+
- update
20+
- watch
21+
- apiGroups:
22+
- batch
23+
resources:
24+
- jobs
25+
verbs:
26+
- create
27+
- delete
28+
- list
29+
- patch
30+
- update
31+
- watch
32+
- apiGroups:
33+
- apps
34+
resources:
35+
- deployments
36+
- replicasets
37+
- statefulsets
38+
verbs:
39+
- create
40+
- delete
41+
- get
42+
- list
43+
- patch
44+
- update
45+
- watch
46+
- apiGroups:
47+
- authentication.k8s.io
48+
resources:
49+
- tokenreviews
50+
verbs:
51+
- create
52+
- apiGroups:
53+
- authorization.k8s.io
54+
resources:
55+
- subjectaccessreviews
56+
verbs:
57+
- create
58+
- apiGroups:
59+
- config.openshift.io
60+
resources:
61+
- clusterversions
62+
verbs:
63+
- get
64+
- list
65+
- apiGroups:
66+
- coordination.k8s.io
67+
resources:
68+
- kube-scheduler
69+
- leases
70+
verbs:
71+
- create
72+
- get
73+
- update
74+
- apiGroups:
75+
- ""
76+
resources:
77+
- bindings
78+
- pods/binding
79+
verbs:
80+
- create
81+
- apiGroups:
82+
- ""
83+
resources:
84+
- configmaps
85+
- nodes
86+
- persistentvolumeclaims
87+
- persistentvolumes
88+
- secrets
89+
- serviceaccounts
90+
- services
91+
verbs:
92+
- create
93+
- delete
94+
- get
95+
- list
96+
- patch
97+
- update
98+
- watch
99+
- apiGroups:
100+
- ""
101+
resources:
102+
- endpoints
103+
- kube-scheduler
104+
verbs:
105+
- create
106+
- get
107+
- update
108+
- apiGroups:
109+
- ""
110+
resources:
111+
- events
112+
verbs:
113+
- create
114+
- patch
115+
- update
116+
- apiGroups:
117+
- ""
118+
resources:
119+
- kube-scheduler
120+
verbs:
121+
- get
122+
- update
123+
- apiGroups:
124+
- ""
125+
resources:
126+
- pods
127+
verbs:
128+
- create
129+
- delete
130+
- deletecollection
131+
- get
132+
- list
133+
- patch
134+
- update
135+
- watch
136+
- apiGroups:
137+
- ""
138+
resources:
139+
- pods/status
140+
verbs:
141+
- patch
142+
- update
143+
- apiGroups:
144+
- ""
145+
resources:
146+
- replicationcontrollers
147+
verbs:
148+
- get
149+
- list
150+
- watch
151+
- apiGroups:
152+
- events.k8s.io
153+
resources:
154+
- events
155+
- kube-scheduler
156+
verbs:
157+
- create
158+
- patch
159+
- update
160+
- apiGroups:
161+
- machine.openshift.io
162+
resources:
163+
- '*'
164+
verbs:
165+
- create
166+
- delete
167+
- get
168+
- list
169+
- patch
170+
- update
171+
- watch
172+
- apiGroups:
173+
- scheduling.sigs.k8s.io
174+
resources:
175+
- podgroups
176+
verbs:
177+
- create
178+
- delete
179+
- deletecollection
180+
- get
181+
- list
182+
- patch
183+
- update
184+
- watch
185+
- apiGroups:
186+
- storage.k8s.io
187+
resources:
188+
- csidrivers
189+
- csinodes
190+
- csistoragecapacities
191+
verbs:
192+
- get
193+
- list
194+
- watch
195+
- apiGroups:
196+
- workload.codeflare.dev
197+
resources:
198+
- appwrappers
199+
- appwrappers/finalizers
200+
- appwrappers/status
201+
- schedulingspecs
202+
verbs:
203+
- create
204+
- delete
205+
- deletecollection
206+
- get
207+
- list
208+
- patch
209+
- update
210+
- watch
211+
- apiGroups:
212+
- quota.codeflare.dev
213+
resources:
214+
- quotasubtrees
215+
verbs:
216+
- create
217+
- delete
218+
- deletecollection
219+
- get
220+
- list
221+
- patch
222+
- update
223+
- watch
+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
apiVersion: rbac.authorization.k8s.io/v1
2+
kind: ClusterRoleBinding
3+
metadata:
4+
name: manual-manager-rolebinding
5+
roleRef:
6+
apiGroup: rbac.authorization.k8s.io
7+
kind: ClusterRole
8+
name: manual-manager-role
9+
subjects:
10+
- kind: ServiceAccount
11+
name: controller-manager
12+
namespace: system

‎config/rbac/role.yaml

+11-168
Original file line numberDiff line numberDiff line change
@@ -5,219 +5,62 @@ metadata:
55
creationTimestamp: null
66
name: manager-role
77
rules:
8-
- apiGroups:
9-
- '*'
10-
resources:
11-
- deployments
12-
- services
13-
verbs:
14-
- create
15-
- delete
16-
- get
17-
- list
18-
- patch
19-
- update
20-
- watch
21-
- apiGroups:
22-
- batch
23-
resources:
24-
- jobs
25-
verbs:
26-
- create
27-
- delete
28-
- list
29-
- patch
30-
- update
31-
- watch
32-
- apiGroups:
33-
- apps
34-
resources:
35-
- deployments
36-
- replicasets
37-
- statefulsets
38-
verbs:
39-
- create
40-
- delete
41-
- get
42-
- list
43-
- patch
44-
- update
45-
- watch
46-
- apiGroups:
47-
- authentication.k8s.io
48-
resources:
49-
- tokenreviews
50-
verbs:
51-
- create
52-
- apiGroups:
53-
- authorization.k8s.io
54-
resources:
55-
- subjectaccessreviews
56-
verbs:
57-
- create
58-
- apiGroups:
59-
- config.openshift.io
60-
resources:
61-
- clusterversions
62-
verbs:
63-
- get
64-
- list
65-
- apiGroups:
66-
- coordination.k8s.io
67-
resources:
68-
- kube-scheduler
69-
- leases
70-
verbs:
71-
- create
72-
- get
73-
- update
748
- apiGroups:
759
- ""
7610
resources:
77-
- bindings
78-
- pods/binding
79-
verbs:
80-
- create
81-
- apiGroups:
82-
- ""
83-
resources:
84-
- configmaps
85-
- nodes
86-
- persistentvolumeclaims
87-
- persistentvolumes
8811
- secrets
89-
- serviceaccounts
90-
- services
9112
verbs:
9213
- create
9314
- delete
9415
- get
95-
- list
9616
- patch
97-
- update
98-
- watch
9917
- apiGroups:
10018
- ""
10119
resources:
102-
- endpoints
103-
- kube-scheduler
104-
verbs:
105-
- create
106-
- get
107-
- update
108-
- apiGroups:
109-
- ""
110-
resources:
111-
- events
112-
verbs:
113-
- create
114-
- patch
115-
- update
116-
- apiGroups:
117-
- ""
118-
resources:
119-
- kube-scheduler
120-
verbs:
121-
- get
122-
- update
123-
- apiGroups:
124-
- ""
125-
resources:
126-
- pods
20+
- serviceaccounts
12721
verbs:
128-
- create
12922
- delete
130-
- deletecollection
13123
- get
132-
- list
13324
- patch
134-
- update
135-
- watch
136-
- apiGroups:
137-
- ""
138-
resources:
139-
- pods/status
140-
verbs:
141-
- patch
142-
- update
14325
- apiGroups:
14426
- ""
14527
resources:
146-
- replicationcontrollers
147-
verbs:
148-
- get
149-
- list
150-
- watch
151-
- apiGroups:
152-
- events.k8s.io
153-
resources:
154-
- events
155-
- kube-scheduler
156-
verbs:
157-
- create
158-
- patch
159-
- update
160-
- apiGroups:
161-
- machine.openshift.io
162-
resources:
163-
- '*'
28+
- services
16429
verbs:
165-
- create
16630
- delete
16731
- get
168-
- list
16932
- patch
170-
- update
171-
- watch
17233
- apiGroups:
173-
- scheduling.sigs.k8s.io
34+
- ray.io
17435
resources:
175-
- podgroups
36+
- rayclusters
17637
verbs:
17738
- create
17839
- delete
179-
- deletecollection
18040
- get
18141
- list
18242
- patch
18343
- update
18444
- watch
18545
- apiGroups:
186-
- storage.k8s.io
46+
- ray.io
18747
resources:
188-
- csidrivers
189-
- csinodes
190-
- csistoragecapacities
48+
- rayclusters/finalizers
19149
verbs:
192-
- get
193-
- list
194-
- watch
50+
- update
19551
- apiGroups:
196-
- workload.codeflare.dev
52+
- ray.io
19753
resources:
198-
- appwrappers
199-
- appwrappers/finalizers
200-
- appwrappers/status
201-
- schedulingspecs
54+
- rayclusters/status
20255
verbs:
203-
- create
204-
- delete
205-
- deletecollection
20656
- get
207-
- list
20857
- patch
20958
- update
210-
- watch
21159
- apiGroups:
212-
- quota.codeflare.dev
60+
- route.openshift.io
21361
resources:
214-
- quotasubtrees
62+
- routes
21563
verbs:
216-
- create
21764
- delete
218-
- deletecollection
21965
- get
220-
- list
22166
- patch
222-
- update
223-
- watch

‎controllers/raycluster_controller.go

+337
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,337 @@
1+
/*
2+
Copyright 2023.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package controllers
18+
19+
import (
20+
"context"
21+
"crypto/rand"
22+
"crypto/sha1"
23+
"encoding/base64"
24+
"strconv"
25+
26+
rayv1 "github.com/ray-project/kuberay/ray-operator/apis/ray/v1"
27+
28+
corev1 "k8s.io/api/core/v1"
29+
rbacv1 "k8s.io/api/rbac/v1"
30+
"k8s.io/apimachinery/pkg/api/errors"
31+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
32+
"k8s.io/apimachinery/pkg/runtime"
33+
"k8s.io/apimachinery/pkg/types"
34+
"k8s.io/apimachinery/pkg/util/intstr"
35+
coreapply "k8s.io/client-go/applyconfigurations/core/v1"
36+
v1 "k8s.io/client-go/applyconfigurations/meta/v1"
37+
rbacapply "k8s.io/client-go/applyconfigurations/rbac/v1"
38+
"k8s.io/client-go/kubernetes"
39+
ctrl "sigs.k8s.io/controller-runtime"
40+
"sigs.k8s.io/controller-runtime/pkg/client"
41+
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
42+
43+
routev1 "github.com/openshift/api/route/v1"
44+
routeapply "github.com/openshift/client-go/route/applyconfigurations/route/v1"
45+
routev1client "github.com/openshift/client-go/route/clientset/versioned/typed/route/v1"
46+
)
47+
48+
// RayClusterReconciler reconciles a RayCluster object
49+
type RayClusterReconciler struct {
50+
client.Client
51+
kubeClient *kubernetes.Clientset
52+
routeClient *routev1client.RouteV1Client
53+
Scheme *runtime.Scheme
54+
CookieSalt string
55+
}
56+
57+
const (
58+
requeueTime = 10
59+
controllerName = "codeflare-raycluster-controller"
60+
oauthAnnotation = "codeflare.dev/oauth=true"
61+
CodeflareOAuthFinalizer = "codeflare.dev/oauth-finalizer"
62+
OAuthServicePort = 443
63+
OAuthServicePortName = "oauth-proxy"
64+
strTrue = "true"
65+
strFalse = "false"
66+
logRequeueing = "requeueing"
67+
)
68+
69+
var (
70+
deletePolicy = metav1.DeletePropagationForeground
71+
deleteOptions = client.DeleteOptions{PropagationPolicy: &deletePolicy}
72+
)
73+
74+
//+kubebuilder:rbac:groups=ray.io,resources=rayclusters,verbs=get;list;watch;create;update;patch;delete
75+
//+kubebuilder:rbac:groups=ray.io,resources=rayclusters/status,verbs=get;update;patch
76+
//+kubebuilder:rbac:groups=ray.io,resources=rayclusters/finalizers,verbs=update
77+
//+kubebuilder:rbac:groups=route.openshift.io,resources=routes,verbs=patch;delete;get
78+
//+kubebuilder:rbac:groups=core,resources=secrets,verbs=get;create;patch;delete;get
79+
//+kubebuilder:rbac:groups=core,resources=services,verbs=patch;delete;get
80+
//+kubebuilder:rbac:groups=core,resources=serviceaccounts,verbs=patch;delete;get
81+
82+
// Reconcile is part of the main kubernetes reconciliation loop which aims to
83+
// move the current state of the cluster closer to the desired state.
84+
// TODO(user): Modify the Reconcile function to compare the state specified by
85+
// the RayCluster object against the actual cluster state, and then
86+
// perform operations to make the cluster state reflect the state specified by
87+
// the user.
88+
//
89+
// For more details, check Reconcile and its Result here:
90+
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.15.3/pkg/reconcile
91+
92+
func (r *RayClusterReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
93+
logger := ctrl.LoggerFrom(ctx)
94+
95+
var cluster rayv1.RayCluster
96+
97+
if err := r.Get(ctx, req.NamespacedName, &cluster); err != nil {
98+
if !errors.IsNotFound(err) {
99+
logger.Error(err, "Error getting RayCluster resource")
100+
}
101+
return ctrl.Result{}, client.IgnoreNotFound(err)
102+
}
103+
104+
if cluster.ObjectMeta.DeletionTimestamp.IsZero() {
105+
if !controllerutil.ContainsFinalizer(&cluster, CodeflareOAuthFinalizer) {
106+
logger.Info("Add a finalizer", "finalizer", CodeflareOAuthFinalizer)
107+
controllerutil.AddFinalizer(&cluster, CodeflareOAuthFinalizer)
108+
if err := r.Update(ctx, &cluster); err != nil {
109+
logger.Error(err, "Failed to update RayCluster with finalizer", logRequeueing, strTrue)
110+
return ctrl.Result{RequeueAfter: requeueTime}, err
111+
}
112+
}
113+
} else if controllerutil.ContainsFinalizer(&cluster, CodeflareOAuthFinalizer) {
114+
err := r.deleteIfNotExist(
115+
ctx, types.NamespacedName{Namespace: cluster.Namespace, Name: crbNameFromCluster(&cluster)}, &rbacv1.ClusterRoleBinding{},
116+
)
117+
if err != nil {
118+
logger.Error(err, "Failed to remove OAuth ClusterRoleBinding.", logRequeueing, strTrue)
119+
return ctrl.Result{RequeueAfter: requeueTime}, err
120+
}
121+
controllerutil.RemoveFinalizer(&cluster, CodeflareOAuthFinalizer)
122+
if err := r.Update(ctx, &cluster); err != nil {
123+
logger.Error(err, "Failed to remove finalizer from RayCluster", logRequeueing, strTrue)
124+
return ctrl.Result{RequeueAfter: requeueTime}, err
125+
}
126+
logger.Info("Successfully removed finalizer.", logRequeueing, strFalse)
127+
return ctrl.Result{}, nil
128+
}
129+
130+
val, ok := cluster.ObjectMeta.Annotations["codeflare.dev/oauth"]
131+
boolVal, err := strconv.ParseBool(val)
132+
if err != nil {
133+
logger.Error(err, "Could not convert codeflare.dev/oauth value to bool", "codeflare.dev/oauth", val)
134+
}
135+
if !ok || err != nil || !boolVal {
136+
logger.Info("Removing all OAuth Objects")
137+
err := r.deleteIfNotExist(
138+
ctx, types.NamespacedName{Namespace: cluster.Namespace, Name: oauthSecretNameFromCluster(&cluster)}, &corev1.Secret{},
139+
)
140+
if err != nil {
141+
logger.Error(err, "Error deleting OAuth Secret, retrying", logRequeueing, strTrue)
142+
return ctrl.Result{RequeueAfter: requeueTime}, nil
143+
}
144+
err = r.deleteIfNotExist(
145+
ctx, types.NamespacedName{Namespace: cluster.Namespace, Name: oauthServiceNameFromCluster(&cluster)}, &corev1.Service{},
146+
)
147+
if err != nil {
148+
logger.Error(err, "Error deleting OAuth Service, retrying", logRequeueing, strTrue)
149+
return ctrl.Result{RequeueAfter: requeueTime}, nil
150+
}
151+
err = r.deleteIfNotExist(
152+
ctx, types.NamespacedName{Namespace: cluster.Namespace, Name: oauthServiceAccountNameFromCluster(&cluster)}, &corev1.ServiceAccount{},
153+
)
154+
if err != nil {
155+
logger.Error(err, "Error deleting OAuth ServiceAccount, retrying", logRequeueing, strTrue)
156+
return ctrl.Result{RequeueAfter: requeueTime}, nil
157+
}
158+
err = r.deleteIfNotExist(
159+
ctx, types.NamespacedName{Namespace: cluster.Namespace, Name: crbNameFromCluster(&cluster)}, &rbacv1.ClusterRoleBinding{},
160+
)
161+
if err != nil {
162+
logger.Error(err, "Error deleting OAuth CRB, retrying", logRequeueing, strTrue)
163+
return ctrl.Result{RequeueAfter: requeueTime}, nil
164+
}
165+
err = r.deleteIfNotExist(
166+
ctx, types.NamespacedName{Namespace: cluster.Namespace, Name: routeNameFromCluster(&cluster)}, &routev1.Route{},
167+
)
168+
if err != nil {
169+
logger.Error(err, "Error deleting OAuth Route, retrying", logRequeueing, strTrue)
170+
return ctrl.Result{RequeueAfter: requeueTime}, nil
171+
}
172+
return ctrl.Result{}, nil
173+
}
174+
175+
_, err = r.routeClient.Routes(cluster.Namespace).Apply(ctx, desiredClusterRoute(&cluster), metav1.ApplyOptions{FieldManager: controllerName, Force: true})
176+
if err != nil {
177+
logger.Error(err, "Failed to update OAuth Route")
178+
}
179+
180+
_, err = r.kubeClient.CoreV1().Secrets(cluster.Namespace).Apply(ctx, desiredOAuthSecret(&cluster, r), metav1.ApplyOptions{FieldManager: controllerName, Force: true})
181+
if err != nil {
182+
logger.Error(err, "Failed to create OAuth Secret")
183+
}
184+
185+
_, err = r.kubeClient.CoreV1().Services(cluster.Namespace).Apply(ctx, desiredOAuthService(&cluster), metav1.ApplyOptions{FieldManager: controllerName, Force: true})
186+
if err != nil {
187+
logger.Error(err, "Failed to update OAuth Service")
188+
}
189+
190+
_, err = r.kubeClient.CoreV1().ServiceAccounts(cluster.Namespace).Apply(ctx, desiredServiceAccount(&cluster), metav1.ApplyOptions{FieldManager: controllerName, Force: true})
191+
if err != nil {
192+
logger.Error(err, "Failed to update OAuth ServiceAccount")
193+
}
194+
195+
_, err = r.kubeClient.RbacV1().ClusterRoleBindings().Apply(ctx, desiredOAuthClusterRoleBinding(&cluster), metav1.ApplyOptions{FieldManager: controllerName, Force: true})
196+
if err != nil {
197+
logger.Error(err, "Failed to update OAuth ClusterRoleBinding")
198+
}
199+
200+
return ctrl.Result{}, nil
201+
}
202+
203+
func crbNameFromCluster(cluster *rayv1.RayCluster) string {
204+
return cluster.Name + "-" + cluster.Namespace + "-auth" // NOTE: potential naming conflicts ie {name: foo, ns: bar-baz} and {name: foo-bar, ns: baz}
205+
}
206+
207+
func (r *RayClusterReconciler) deleteIfNotExist(ctx context.Context, namespacedName types.NamespacedName, obj client.Object) error {
208+
err := r.Client.Get(ctx, namespacedName, obj)
209+
if err != nil && errors.IsNotFound(err) {
210+
return nil
211+
} else if err != nil {
212+
return err
213+
}
214+
return r.Client.Delete(ctx, obj, &deleteOptions)
215+
}
216+
217+
func desiredOAuthClusterRoleBinding(cluster *rayv1.RayCluster) *rbacapply.ClusterRoleBindingApplyConfiguration {
218+
return rbacapply.ClusterRoleBinding(
219+
crbNameFromCluster(cluster)).
220+
WithLabels(map[string]string{"ray.io/cluster-name": cluster.Name}).
221+
WithSubjects(
222+
rbacapply.Subject().
223+
WithKind("ServiceAccount").
224+
WithName(oauthServiceAccountNameFromCluster(cluster)).
225+
WithNamespace(cluster.Namespace),
226+
).
227+
WithRoleRef(
228+
rbacapply.RoleRef().
229+
WithAPIGroup("rbac.authorization.k8s.io").
230+
WithKind("ClusterRole").
231+
WithName("system:auth-delegator"),
232+
).
233+
WithOwnerReferences(
234+
v1.OwnerReference().WithUID(cluster.UID).WithName(cluster.Name).WithKind(cluster.Kind).WithAPIVersion(cluster.APIVersion),
235+
)
236+
}
237+
238+
func oauthServiceAccountNameFromCluster(cluster *rayv1.RayCluster) string {
239+
return cluster.Name + "-oauth-proxy"
240+
}
241+
242+
func desiredServiceAccount(cluster *rayv1.RayCluster) *coreapply.ServiceAccountApplyConfiguration {
243+
return coreapply.ServiceAccount(oauthServiceAccountNameFromCluster(cluster), cluster.Namespace).
244+
WithLabels(map[string]string{"ray.io/cluster-name": cluster.Name}).
245+
WithAnnotations(map[string]string{
246+
"serviceaccounts.openshift.io/oauth-redirectreference.first": "" +
247+
`{"kind":"OAuthRedirectReference","apiVersion":"v1",` +
248+
`"reference":{"kind":"Route","name":"` + routeNameFromCluster(cluster) + `"}}`,
249+
}).
250+
WithOwnerReferences(
251+
v1.OwnerReference().WithUID(cluster.UID).WithName(cluster.Name).WithKind(cluster.Kind).WithAPIVersion(cluster.APIVersion),
252+
)
253+
}
254+
255+
func routeNameFromCluster(cluster *rayv1.RayCluster) string {
256+
return "ray-dashboard-" + cluster.Name
257+
}
258+
259+
func desiredClusterRoute(cluster *rayv1.RayCluster) *routeapply.RouteApplyConfiguration {
260+
return routeapply.Route(routeNameFromCluster(cluster), cluster.Namespace).
261+
WithLabels(map[string]string{"ray.io/cluster-name": cluster.Name}).
262+
WithSpec(routeapply.RouteSpec().
263+
WithTo(routeapply.RouteTargetReference().WithKind("Service").WithName(oauthServiceNameFromCluster(cluster))).
264+
WithPort(routeapply.RoutePort().WithTargetPort(intstr.FromString((OAuthServicePortName)))).
265+
WithTLS(routeapply.TLSConfig().
266+
WithInsecureEdgeTerminationPolicy(routev1.InsecureEdgeTerminationPolicyRedirect).
267+
WithTermination(routev1.TLSTerminationReencrypt),
268+
),
269+
).
270+
WithOwnerReferences(
271+
v1.OwnerReference().WithUID(cluster.UID).WithName(cluster.Name).WithKind(cluster.Kind).WithAPIVersion(cluster.APIVersion),
272+
)
273+
}
274+
275+
func oauthServiceNameFromCluster(cluster *rayv1.RayCluster) string {
276+
return cluster.Name + "-oauth"
277+
}
278+
279+
func oauthServiceTLSSecretName(cluster *rayv1.RayCluster) string {
280+
return cluster.Name + "-proxy-tls-secret"
281+
}
282+
283+
func desiredOAuthService(cluster *rayv1.RayCluster) *coreapply.ServiceApplyConfiguration {
284+
return coreapply.Service(oauthServiceNameFromCluster(cluster), cluster.Namespace).
285+
WithLabels(map[string]string{"ray.io/cluster-name": cluster.Name}).
286+
WithAnnotations(map[string]string{"service.beta.openshift.io/serving-cert-secret-name": oauthServiceTLSSecretName(cluster)}).
287+
WithSpec(
288+
coreapply.ServiceSpec().
289+
WithPorts(
290+
coreapply.ServicePort().
291+
WithName(OAuthServicePortName).
292+
WithPort(OAuthServicePort).
293+
WithTargetPort(intstr.FromString(OAuthServicePortName)).
294+
WithProtocol(corev1.ProtocolTCP),
295+
).
296+
WithSelector(map[string]string{"ray.io/cluster": cluster.Name, "ray.io/node-type": "head"}),
297+
).
298+
WithOwnerReferences(
299+
v1.OwnerReference().WithUID(cluster.UID).WithName(cluster.Name).WithKind(cluster.Kind).WithAPIVersion(cluster.APIVersion),
300+
)
301+
}
302+
303+
func oauthSecretNameFromCluster(cluster *rayv1.RayCluster) string {
304+
return cluster.Name + "-oauth-config"
305+
}
306+
307+
// desiredOAuthSecret defines the desired OAuth secret object
308+
func desiredOAuthSecret(cluster *rayv1.RayCluster, r *RayClusterReconciler) *coreapply.SecretApplyConfiguration {
309+
// Generate the cookie secret for the OAuth proxy
310+
hasher := sha1.New() // REVIEW is SHA1 okay here?
311+
hasher.Write([]byte(cluster.Name + r.CookieSalt))
312+
cookieSecret := base64.StdEncoding.EncodeToString(hasher.Sum(nil))
313+
314+
return coreapply.Secret(oauthSecretNameFromCluster(cluster), cluster.Namespace).
315+
WithLabels(map[string]string{"ray.io/cluster-name": cluster.Name}).
316+
WithStringData(map[string]string{"cookie_secret": cookieSecret}).
317+
WithOwnerReferences(
318+
v1.OwnerReference().WithUID(cluster.UID).WithName(cluster.Name).WithKind(cluster.Kind).WithAPIVersion(cluster.APIVersion),
319+
)
320+
// Create a Kubernetes secret to store the cookie secret
321+
}
322+
323+
// SetupWithManager sets up the controller with the Manager.
324+
func (r *RayClusterReconciler) SetupWithManager(mgr ctrl.Manager) error {
325+
r.kubeClient = kubernetes.NewForConfigOrDie(mgr.GetConfig())
326+
r.routeClient = routev1client.NewForConfigOrDie(mgr.GetConfig())
327+
b := make([]byte, 16)
328+
_, err := rand.Read(b)
329+
if err != nil {
330+
return err
331+
}
332+
r.CookieSalt = string(b)
333+
return ctrl.NewControllerManagedBy(mgr).
334+
Named(controllerName).
335+
For(&rayv1.RayCluster{}).
336+
Complete(r)
337+
}
+222
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
/*
2+
Copyright 2024.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package controllers
18+
19+
import (
20+
"context"
21+
"math/rand"
22+
"time"
23+
24+
. "github.com/onsi/ginkgo/v2"
25+
. "github.com/onsi/gomega"
26+
rayv1 "github.com/ray-project/kuberay/ray-operator/apis/ray/v1"
27+
28+
corev1 "k8s.io/api/core/v1"
29+
rbacv1 "k8s.io/api/rbac/v1"
30+
"k8s.io/apimachinery/pkg/api/errors"
31+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
32+
"k8s.io/apimachinery/pkg/types"
33+
34+
routev1 "github.com/openshift/api/route/v1"
35+
)
36+
37+
func stringInList(l []string, s string) bool {
38+
for _, i := range l {
39+
if i == s {
40+
return true
41+
}
42+
}
43+
return false
44+
}
45+
46+
var letters = []rune("abcdefghijklmnopqrstuvwxyz")
47+
var r = rand.New(rand.NewSource(time.Now().UnixNano()))
48+
49+
func randSeq(n int) string {
50+
b := make([]rune, n)
51+
for i := range b {
52+
b[i] = letters[r.Intn(len(letters))]
53+
}
54+
return string(b)
55+
}
56+
57+
var _ = Describe("RayCluster controller", func() {
58+
Context("RayCluster controller test", func() {
59+
var rayClusterName = "test-raycluster"
60+
var typeNamespaceName types.NamespacedName
61+
ctx := context.Background()
62+
BeforeEach(func() {
63+
By("Generate random number so each run is creating unique")
64+
rString := randSeq(10)
65+
rayClusterName = rayClusterName + "-" + rString
66+
typeNamespaceName = types.NamespacedName{Name: rayClusterName, Namespace: rayClusterName}
67+
By("Creating a namespace for running the tests.")
68+
namespace := &corev1.Namespace{
69+
ObjectMeta: metav1.ObjectMeta{
70+
Name: rayClusterName,
71+
},
72+
}
73+
var err error
74+
err = k8sClient.Create(ctx, namespace)
75+
Expect(err).To(Not(HaveOccurred()))
76+
77+
By("creating a basic instance of the RayCluster CR")
78+
raycluster := &rayv1.RayCluster{
79+
ObjectMeta: metav1.ObjectMeta{
80+
Name: rayClusterName,
81+
Namespace: rayClusterName,
82+
},
83+
Spec: rayv1.RayClusterSpec{
84+
HeadGroupSpec: rayv1.HeadGroupSpec{
85+
Template: corev1.PodTemplateSpec{
86+
Spec: corev1.PodSpec{
87+
Containers: []corev1.Container{
88+
corev1.Container{},
89+
},
90+
},
91+
},
92+
RayStartParams: map[string]string{},
93+
},
94+
},
95+
}
96+
err = k8sClient.Get(ctx, typeNamespaceName, &rayv1.RayCluster{})
97+
Expect(errors.IsNotFound(err)).To(Equal(true))
98+
err = k8sClient.Create(ctx, raycluster)
99+
Expect(err).To(Not(HaveOccurred()))
100+
})
101+
102+
AfterEach(func() {
103+
By("removing the instance of the RayCluster used")
104+
// err := clientSet.CoreV1().Namespaces().Delete(ctx, RayClusterName, metav1.DeleteOptions{})
105+
foundRayCluster := rayv1.RayCluster{}
106+
err := k8sClient.Get(ctx, typeNamespaceName, &foundRayCluster)
107+
if err != nil {
108+
Expect(errors.IsNotFound(err)).To(Equal(true))
109+
} else {
110+
Expect(err).To(Not(HaveOccurred()))
111+
_ = k8sClient.Delete(ctx, &foundRayCluster)
112+
}
113+
Eventually(func() bool {
114+
err := k8sClient.Get(ctx, typeNamespaceName, &foundRayCluster)
115+
return errors.IsNotFound(err)
116+
}, SpecTimeout(time.Second*10)).Should(Equal(true))
117+
})
118+
119+
It("should have oauth finalizer set", func() {
120+
foundRayCluster := rayv1.RayCluster{}
121+
Eventually(func() bool {
122+
err := k8sClient.Get(ctx, typeNamespaceName, &foundRayCluster)
123+
Expect(err).To(Not(HaveOccurred()))
124+
return stringInList(foundRayCluster.Finalizers, CodeflareOAuthFinalizer)
125+
}, SpecTimeout(time.Second*10)).Should(Equal(true))
126+
})
127+
128+
Context("Cluster has OAuth annotation", func() {
129+
BeforeEach(func() {
130+
By("adding OAuth annotation to RayCluster")
131+
Eventually(func() error {
132+
foundRayCluster := rayv1.RayCluster{}
133+
err := k8sClient.Get(ctx, typeNamespaceName, &foundRayCluster)
134+
Expect(err).To(Not(HaveOccurred()))
135+
if foundRayCluster.Annotations == nil {
136+
foundRayCluster.Annotations = map[string]string{"codeflare.dev/oauth": "true"}
137+
} else {
138+
foundRayCluster.Annotations["codeflare.dev/oauth"] = "'true'"
139+
}
140+
return k8sClient.Update(ctx, &foundRayCluster)
141+
}, SpecTimeout(time.Second*10)).Should(Not(HaveOccurred()))
142+
By("waiting for dependent resources to be created")
143+
Eventually(func() error {
144+
foundRayCluster := rayv1.RayCluster{}
145+
err := k8sClient.Get(ctx, typeNamespaceName, &foundRayCluster)
146+
if err != nil {
147+
return err
148+
}
149+
err = k8sClient.Get(ctx, types.NamespacedName{Name: oauthSecretNameFromCluster(&foundRayCluster), Namespace: foundRayCluster.Namespace}, &corev1.Secret{})
150+
if err != nil {
151+
return err
152+
}
153+
err = k8sClient.Get(ctx, types.NamespacedName{Name: oauthServiceNameFromCluster(&foundRayCluster), Namespace: foundRayCluster.Namespace}, &corev1.Service{})
154+
if err != nil {
155+
return err
156+
}
157+
err = k8sClient.Get(ctx, types.NamespacedName{Name: foundRayCluster.Name, Namespace: foundRayCluster.Namespace}, &corev1.ServiceAccount{})
158+
if err != nil {
159+
return err
160+
}
161+
err = k8sClient.Get(ctx, types.NamespacedName{Name: crbNameFromCluster(&foundRayCluster)}, &rbacv1.ClusterRoleBinding{})
162+
if err != nil {
163+
return err
164+
}
165+
err = k8sClient.Get(ctx, types.NamespacedName{Name: foundRayCluster.Name, Namespace: foundRayCluster.Namespace}, &routev1.Route{})
166+
if err != nil {
167+
return err
168+
}
169+
return nil
170+
}, SpecTimeout(time.Second*10)).Should(Not(HaveOccurred()))
171+
})
172+
173+
It("should set owner references for all resources", func() {
174+
foundRayCluster := rayv1.RayCluster{}
175+
err := k8sClient.Get(ctx, typeNamespaceName, &foundRayCluster)
176+
Expect(err).ToNot(HaveOccurred())
177+
err = k8sClient.Get(ctx, types.NamespacedName{Name: oauthSecretNameFromCluster(&foundRayCluster), Namespace: foundRayCluster.Namespace}, &corev1.Secret{})
178+
Expect(err).To(Not(HaveOccurred()))
179+
err = k8sClient.Get(ctx, types.NamespacedName{Name: oauthServiceNameFromCluster(&foundRayCluster), Namespace: foundRayCluster.Namespace}, &corev1.Service{})
180+
Expect(err).To(Not(HaveOccurred()))
181+
err = k8sClient.Get(ctx, types.NamespacedName{Name: foundRayCluster.Name, Namespace: foundRayCluster.Namespace}, &corev1.ServiceAccount{})
182+
Expect(err).To(Not(HaveOccurred()))
183+
err = k8sClient.Get(ctx, types.NamespacedName{Name: crbNameFromCluster(&foundRayCluster)}, &rbacv1.ClusterRoleBinding{})
184+
Expect(err).To(Not(HaveOccurred()))
185+
err = k8sClient.Get(ctx, types.NamespacedName{Name: foundRayCluster.Name, Namespace: foundRayCluster.Namespace}, &routev1.Route{})
186+
Expect(err).To(Not(HaveOccurred()))
187+
})
188+
189+
It("should delete OAuth resources when annotation is removed", func() {
190+
foundRayCluster := rayv1.RayCluster{}
191+
err := k8sClient.Get(ctx, typeNamespaceName, &foundRayCluster)
192+
Expect(err).To(Not(HaveOccurred()))
193+
delete(foundRayCluster.Annotations, "codeflare.dev/oauth")
194+
err = k8sClient.Update(ctx, &foundRayCluster)
195+
Expect(err).To(Not(HaveOccurred()))
196+
Eventually(func() bool {
197+
return errors.IsNotFound(k8sClient.Get(ctx, types.NamespacedName{Name: oauthSecretNameFromCluster(&foundRayCluster), Namespace: foundRayCluster.Namespace}, &corev1.Secret{}))
198+
}, SpecTimeout(time.Second*10)).Should(Equal(true))
199+
Eventually(func() bool {
200+
return errors.IsNotFound(k8sClient.Get(ctx, types.NamespacedName{Name: oauthServiceNameFromCluster(&foundRayCluster), Namespace: foundRayCluster.Namespace}, &corev1.Service{}))
201+
}, SpecTimeout(time.Second*10)).Should(Equal(true))
202+
Eventually(func() bool {
203+
return errors.IsNotFound(k8sClient.Get(ctx, types.NamespacedName{Name: foundRayCluster.Name, Namespace: foundRayCluster.Namespace}, &corev1.ServiceAccount{}))
204+
}, SpecTimeout(time.Second*10)).Should(Equal(true))
205+
Eventually(func() bool {
206+
return errors.IsNotFound(k8sClient.Get(ctx, types.NamespacedName{Name: crbNameFromCluster(&foundRayCluster)}, &rbacv1.ClusterRoleBinding{}))
207+
}, SpecTimeout(time.Second*10)).Should(Equal(true))
208+
})
209+
210+
It("should remove CRB when the RayCluster is deleted", func() {
211+
foundRayCluster := rayv1.RayCluster{}
212+
err := k8sClient.Get(ctx, typeNamespaceName, &foundRayCluster)
213+
Expect(err).To(Not(HaveOccurred()))
214+
err = k8sClient.Delete(ctx, &foundRayCluster)
215+
Expect(err).To(Not(HaveOccurred()))
216+
Eventually(func() bool {
217+
return errors.IsNotFound(k8sClient.Get(ctx, types.NamespacedName{Name: crbNameFromCluster(&foundRayCluster)}, &rbacv1.ClusterRoleBinding{}))
218+
}, SpecTimeout(time.Second*10)).Should(Equal(true))
219+
})
220+
})
221+
})
222+
})

‎controllers/suite_test.go

+135
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
/*
2+
Copyright 2023.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package controllers
18+
19+
import (
20+
"context"
21+
"io"
22+
"net/http"
23+
"os"
24+
"path/filepath"
25+
"testing"
26+
27+
. "github.com/onsi/ginkgo/v2"
28+
. "github.com/onsi/gomega"
29+
rayv1 "github.com/ray-project/kuberay/ray-operator/apis/ray/v1"
30+
31+
"k8s.io/client-go/kubernetes"
32+
"k8s.io/client-go/kubernetes/scheme"
33+
"k8s.io/client-go/rest"
34+
ctrl "sigs.k8s.io/controller-runtime"
35+
"sigs.k8s.io/controller-runtime/pkg/client"
36+
"sigs.k8s.io/controller-runtime/pkg/envtest"
37+
logf "sigs.k8s.io/controller-runtime/pkg/log"
38+
"sigs.k8s.io/controller-runtime/pkg/log/zap"
39+
40+
routev1 "github.com/openshift/api/route/v1"
41+
//+kubebuilder:scaffold:imports
42+
)
43+
44+
// These tests use Ginkgo (BDD-style Go testing framework). Refer to
45+
// http://onsi.github.io/ginkgo/ to learn more about Ginkgo.
46+
47+
var cfg *rest.Config
48+
var k8sClient client.Client
49+
var testEnv *envtest.Environment
50+
51+
func TestAPIs(t *testing.T) {
52+
RegisterFailHandler(Fail)
53+
54+
RunSpecs(t, "Controller Suite")
55+
}
56+
57+
const (
58+
RayClusterCRDFileDownload = "https://raw.githubusercontent.com/ray-project/kuberay/master/ray-operator/config/crd/bases/ray.io_rayclusters.yaml"
59+
RouteCRDFileDownload = "https://raw.githubusercontent.com/openshift/api/master/route/v1/route.crd.yaml"
60+
)
61+
62+
var _ = BeforeSuite(func() {
63+
logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true)))
64+
65+
var err error
66+
var fRoute, fRaycluster *os.File
67+
68+
By("Creating and downloading necessary crds")
69+
err = os.Mkdir("./test-crds", os.ModePerm)
70+
Expect(err).ToNot(HaveOccurred())
71+
fRoute, err = os.Create("./test-crds/route.yaml")
72+
Expect(err).ToNot(HaveOccurred())
73+
defer fRoute.Close()
74+
resp, err := http.Get(RouteCRDFileDownload)
75+
Expect(err).ToNot(HaveOccurred())
76+
_, err = io.Copy(fRoute, resp.Body)
77+
Expect(err).ToNot(HaveOccurred())
78+
fRaycluster, err = os.Create("./test-crds/raycluster.yaml")
79+
Expect(err).ToNot(HaveOccurred())
80+
defer fRaycluster.Close()
81+
resp, err = http.Get(RayClusterCRDFileDownload)
82+
Expect(err).ToNot(HaveOccurred())
83+
_, err = io.Copy(fRaycluster, resp.Body)
84+
Expect(err).ToNot(HaveOccurred())
85+
86+
By("bootstrapping test environment")
87+
testEnv = &envtest.Environment{
88+
CRDDirectoryPaths: []string{
89+
filepath.Join("..", "config", "crd"),
90+
filepath.Join(".", "test-crds"),
91+
},
92+
ErrorIfCRDPathMissing: true,
93+
}
94+
95+
// cfg is defined in this file globally.
96+
cfg, err = testEnv.Start()
97+
Expect(err).NotTo(HaveOccurred())
98+
Expect(cfg).NotTo(BeNil())
99+
100+
//+kubebuilder:scaffold:scheme
101+
102+
k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})
103+
Expect(err).NotTo(HaveOccurred())
104+
clientSet, err := kubernetes.NewForConfig(cfg)
105+
Expect(err).NotTo(HaveOccurred())
106+
Expect(k8sClient).NotTo(BeNil())
107+
err = rayv1.AddToScheme(scheme.Scheme)
108+
Expect(err).To(Not(HaveOccurred()))
109+
err = routev1.AddToScheme(scheme.Scheme)
110+
Expect(err).NotTo(HaveOccurred())
111+
k8sManager, err := ctrl.NewManager(cfg, ctrl.Options{
112+
Scheme: scheme.Scheme,
113+
})
114+
Expect(err).NotTo(HaveOccurred())
115+
err = (&RayClusterReconciler{
116+
Client: k8sManager.GetClient(),
117+
Scheme: k8sManager.GetScheme(),
118+
kubeClient: clientSet,
119+
CookieSalt: "foo",
120+
}).SetupWithManager(k8sManager)
121+
Expect(err).NotTo(HaveOccurred())
122+
go func() {
123+
defer GinkgoRecover()
124+
err = k8sManager.Start(context.Background())
125+
Expect(err).ToNot(HaveOccurred(), "failed to run manager")
126+
}()
127+
})
128+
129+
var _ = AfterSuite(func() {
130+
By("tearing down the test environment")
131+
err := os.RemoveAll("./test-crds")
132+
Expect(err).NotTo(HaveOccurred())
133+
err = testEnv.Stop()
134+
Expect(err).NotTo(HaveOccurred())
135+
})

‎go.mod

+6-2
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@ module github.com/project-codeflare/codeflare-operator
33
go 1.20
44

55
require (
6+
github.com/onsi/ginkgo/v2 v2.11.0
67
github.com/onsi/gomega v1.27.10
78
github.com/openshift/api v0.0.0-20230213134911-7ba313770556
8-
github.com/project-codeflare/codeflare-common v0.0.0-20240201153809-2e7292120303
9+
github.com/openshift/client-go v0.0.0-20221019143426-16aed247da5c
10+
github.com/project-codeflare/codeflare-common v0.0.0-20240207083912-d7a229270a0a
911
github.com/project-codeflare/instascale v0.4.0
1012
github.com/project-codeflare/multi-cluster-app-dispatcher v1.40.0
1113
github.com/ray-project/kuberay/ray-operator v1.0.0
@@ -46,6 +48,7 @@ require (
4648
github.com/go-openapi/jsonpointer v0.19.6 // indirect
4749
github.com/go-openapi/jsonreference v0.20.1 // indirect
4850
github.com/go-openapi/swag v0.22.3 // indirect
51+
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
4952
github.com/gogo/protobuf v1.3.2 // indirect
5053
github.com/golang-jwt/jwt/v4 v4.4.3 // indirect
5154
github.com/golang/glog v1.1.2 // indirect
@@ -55,6 +58,7 @@ require (
5558
github.com/google/gnostic v0.6.9 // indirect
5659
github.com/google/go-cmp v0.5.9 // indirect
5760
github.com/google/gofuzz v1.2.0 // indirect
61+
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 // indirect
5862
github.com/google/uuid v1.3.0 // indirect
5963
github.com/gorilla/css v1.0.0 // indirect
6064
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect
@@ -72,7 +76,6 @@ require (
7276
github.com/modern-go/reflect2 v1.0.2 // indirect
7377
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
7478
github.com/openshift-online/ocm-sdk-go v0.1.368 // indirect
75-
github.com/openshift/client-go v0.0.0-20221019143426-16aed247da5c // indirect
7679
github.com/pkg/errors v0.9.1 // indirect
7780
github.com/prometheus/client_golang v1.18.0 // indirect
7881
github.com/prometheus/client_model v0.5.0 // indirect
@@ -102,6 +105,7 @@ require (
102105
golang.org/x/term v0.16.0 // indirect
103106
golang.org/x/text v0.14.0 // indirect
104107
golang.org/x/time v0.3.0 // indirect
108+
golang.org/x/tools v0.12.0 // indirect
105109
gomodules.xyz/jsonpatch/v2 v2.3.0 // indirect
106110
google.golang.org/appengine v1.6.7 // indirect
107111
google.golang.org/genproto v0.0.0-20230711160842-782d3b101e98 // indirect

‎go.sum

+8-2
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+
142142
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
143143
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
144144
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
145+
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
145146
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
146147
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
147148
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
@@ -216,6 +217,7 @@ github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hf
216217
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
217218
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
218219
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec=
220+
github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
219221
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
220222
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
221223
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
@@ -366,6 +368,7 @@ github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
366368
github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c=
367369
github.com/onsi/ginkgo/v2 v2.1.4/go.mod h1:um6tUpWM/cxCK3/FK8BXqEiUMUwRgSM4JXG47RKZmLU=
368370
github.com/onsi/ginkgo/v2 v2.11.0 h1:WgqUCUt/lT6yXoQ8Wef0fsNn5cAuMK7+KT9UFRz2tcU=
371+
github.com/onsi/ginkgo/v2 v2.11.0/go.mod h1:ZhrRA5XmEE3x3rhlzamx/JJvujdZoJ2uvgI7kR0iZvM=
369372
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
370373
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
371374
github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
@@ -384,8 +387,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
384387
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
385388
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
386389
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
387-
github.com/project-codeflare/codeflare-common v0.0.0-20240201153809-2e7292120303 h1:30LG8751WElZmWA3mVS8l23l2oZnUCqbDkLCyy0U/p0=
388-
github.com/project-codeflare/codeflare-common v0.0.0-20240201153809-2e7292120303/go.mod h1:2Ck9LC+6Xi4jTDSlCJoP00tCzSrxek0roLsjvUgL2gY=
390+
github.com/project-codeflare/codeflare-common v0.0.0-20240207083912-d7a229270a0a h1:Yk9J5qXjp+yfSRCzS0EElrhpTgfYJ+S+W/z84cmlmX4=
391+
github.com/project-codeflare/codeflare-common v0.0.0-20240207083912-d7a229270a0a/go.mod h1:2Ck9LC+6Xi4jTDSlCJoP00tCzSrxek0roLsjvUgL2gY=
389392
github.com/project-codeflare/instascale v0.4.0 h1:l/cb+x4FrJ2bN9wXjv1mCngy77tVw0CLMiqJovTAflo=
390393
github.com/project-codeflare/instascale v0.4.0/go.mod h1:CpduFXKeuqYW4Ph1CPOJV6dpAdpebOxhbU4CmccZWSo=
391394
github.com/project-codeflare/multi-cluster-app-dispatcher v1.40.0 h1:IkTmd/W/zxcsC5s4EbnW74PFpkQVEiTc/8rWWwFw0Ok=
@@ -450,6 +453,7 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf
450453
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
451454
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
452455
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
456+
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
453457
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
454458
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
455459
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
@@ -569,6 +573,7 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
569573
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
570574
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
571575
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
576+
golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
572577
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
573578
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
574579
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -764,6 +769,7 @@ golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4f
764769
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
765770
golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E=
766771
golang.org/x/tools v0.12.0 h1:YW6HUoUmYBpwSgyaGaZq1fHjrBjX1rlpZ54T6mu2kss=
772+
golang.org/x/tools v0.12.0/go.mod h1:Sc0INKfu04TlqNoRA1hgpFZbhYXHPr4V5DzpSBTPqQM=
767773
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
768774
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
769775
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

‎main.go

+33
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,16 @@ import (
3030
quotasubtreev1alpha1 "github.com/project-codeflare/multi-cluster-app-dispatcher/pkg/apis/quotaplugins/quotasubtree/v1alpha1"
3131
mcadconfig "github.com/project-codeflare/multi-cluster-app-dispatcher/pkg/config"
3232
mcad "github.com/project-codeflare/multi-cluster-app-dispatcher/pkg/controller/queuejob"
33+
rayv1 "github.com/ray-project/kuberay/ray-operator/apis/ray/v1"
3334
"go.uber.org/zap/zapcore"
3435

3536
corev1 "k8s.io/api/core/v1"
3637
apierrors "k8s.io/apimachinery/pkg/api/errors"
3738
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3839
"k8s.io/apimachinery/pkg/runtime"
40+
"k8s.io/apimachinery/pkg/runtime/schema"
3941
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
42+
"k8s.io/client-go/discovery"
4043
"k8s.io/client-go/kubernetes"
4144
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
4245
_ "k8s.io/client-go/plugin/pkg/client/auth"
@@ -51,7 +54,9 @@ import (
5154

5255
configv1 "github.com/openshift/api/config/v1"
5356
machinev1beta1 "github.com/openshift/api/machine/v1beta1"
57+
routev1 "github.com/openshift/api/route/v1"
5458

59+
cfoControllers "github.com/project-codeflare/codeflare-operator/controllers"
5560
"github.com/project-codeflare/codeflare-operator/pkg/config"
5661
// Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.)
5762
// to ensure that exec-entrypoint and run can make use of them.
@@ -75,6 +80,10 @@ func init() {
7580
// InstaScale
7681
utilruntime.Must(configv1.Install(scheme))
7782
utilruntime.Must(machinev1beta1.Install(scheme))
83+
// Ray
84+
utilruntime.Must(rayv1.AddToScheme(scheme))
85+
// OpenShift Route
86+
utilruntime.Must(routev1.Install(scheme))
7887
}
7988

8089
func main() {
@@ -171,6 +180,13 @@ func main() {
171180
exitOnError(instaScaleController.SetupWithManager(context.Background(), mgr), "Error setting up InstaScale controller")
172181
}
173182

183+
if v, err := HasAPIResourceForGVK(kubeClient.DiscoveryClient, rayv1.GroupVersion.WithKind("RayCluster")); v {
184+
rayClusterController := cfoControllers.RayClusterReconciler{Client: mgr.GetClient(), Scheme: mgr.GetScheme()}
185+
exitOnError(rayClusterController.SetupWithManager(mgr), "Error setting up RayCluster controller")
186+
} else if err != nil {
187+
exitOnError(err, "Could not determine if RayCluster CR present on cluster.")
188+
}
189+
174190
exitOnError(mgr.AddHealthzCheck(cfg.Health.LivenessEndpointName, healthz.Ping), "unable to set up health check")
175191
exitOnError(mgr.AddReadyzCheck(cfg.Health.ReadinessEndpointName, healthz.Ping), "unable to set up ready check")
176192

@@ -221,6 +237,23 @@ func createConfigMap(ctx context.Context, client kubernetes.Interface, ns, name
221237
return err
222238
}
223239

240+
func HasAPIResourceForGVK(dc discovery.DiscoveryInterface, gvk schema.GroupVersionKind) (bool, error) {
241+
gv, kind := gvk.ToAPIVersionAndKind()
242+
if resources, err := dc.ServerResourcesForGroupVersion(gv); err != nil {
243+
if apierrors.IsNotFound(err) {
244+
return false, nil
245+
}
246+
return false, err
247+
} else {
248+
for _, res := range resources.APIResources {
249+
if res.Kind == kind {
250+
return true, nil
251+
}
252+
}
253+
}
254+
return false, nil
255+
}
256+
224257
func namespaceOrDie() string {
225258
// This way assumes you've set the NAMESPACE environment variable either manually, when running
226259
// the operator standalone, or using the downward API, when running the operator in-cluster.

0 commit comments

Comments
 (0)
Please sign in to comment.