Skip to content

Commit fad940e

Browse files
committed
running dpl in k8s vis ECS
1 parent 8f5f828 commit fad940e

6 files changed

Lines changed: 170 additions & 12 deletions

File tree

‎control-operator/api/v1alpha1/environment_types.go‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ type EnvironmentStatus struct {
8181
type TaskReference struct {
8282
Name string `json:"name"`
8383
TaskID string `json:"taskID,omitempty"`
84+
NameSuffix string `json:"nameSuffix,omitempty"`
8485
Env []v1.EnvVar `json:"env"`
8586
ArgsCLI []string `json:"argsCLI"`
8687
ArgsTransition map[string]string `json:"argsTransition,omitempty"`

‎control-operator/config/crd/bases/aliecs.alice.cern_environments.yaml‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4100,6 +4100,8 @@ spec:
41004100
type: array
41014101
name:
41024102
type: string
4103+
nameSuffix:
4104+
type: string
41034105
taskID:
41044106
type: string
41054107
required:
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
apiVersion: aliecs.alice.cern/v1alpha1
2+
kind: TaskTemplate
3+
metadata:
4+
name: dpl
5+
namespace: alice-tasks
6+
spec:
7+
envVars:
8+
- O2_DETECTOR
9+
- O2_PARTITION
10+
- OCC_CONTROL_PORT
11+
- O2_SYSTEM
12+
- O2_ROLE
13+
pod:
14+
hostNetwork: true
15+
hostIPC: true
16+
securityContext:
17+
fsGroup: 1100
18+
supplementalGroups: [10, 1105]
19+
containers:
20+
- name: dpl
21+
image: gitlab-registry.cern.ch/pkonopka/dockerfiles/flp-sw:20260729-1
22+
command: ["bash", "-c"]
23+
securityContext:
24+
privileged: true
25+
runAsUser: 1100
26+
runAsGroup: 1100
27+
volumeMounts:
28+
- name: host-shm
29+
mountPath: /dev/shm
30+
- name: group
31+
mountPath: /etc/group
32+
readOnly: true
33+
- name: passwd
34+
mountPath: /etc/passwd
35+
readOnly: true
36+
- name: tmp
37+
mountPath: /tmp
38+
- name: modules
39+
mountPath: /lib/modules
40+
imagePullPolicy: IfNotPresent
41+
volumes:
42+
- name: host-shm
43+
hostPath:
44+
path: /dev/shm
45+
type: Directory
46+
- name: group
47+
hostPath:
48+
path: /etc/group
49+
- name: passwd
50+
hostPath:
51+
path: /etc/passwd
52+
- name: tmp
53+
hostPath:
54+
path: /tmp
55+
- name: modules
56+
hostPath:
57+
path: /lib/modules
58+
imagePullSecrets:
59+
- name: gitlab-registry-secret
60+
control:
61+
mode: "fairmq"
62+
# port: ${OCC_CONTROL_PORT} # to be filled in

‎control-operator/internal/controller/environment_controller.go‎

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,16 +57,27 @@ func (r *EnvironmentReconciler) runTasksFromReferenceOnNode(ctx context.Context,
5757
nodename string, resolvedNodename string, req ctrl.Request, environment *aliecsv1alpha1.Environment, log logr.Logger,
5858
) (*ctrl.Result, error) {
5959
for _, taskReference := range taskReferences {
60-
log.Info("geting stored template for task", "task", taskReference.Name)
60+
61+
log.Info("getting stored template for task", "task", taskReference.Name)
62+
taskTemplateName := taskReference.Name
63+
if strings.HasPrefix(taskTemplateName, "jit-") {
64+
log.Info("getting dpl TaskTemplate for task", "task", taskReference.Name)
65+
taskTemplateName = "dpl"
66+
}
6167
template := &aliecsv1alpha1.TaskTemplate{}
62-
if err := r.Get(ctx, types.NamespacedName{Namespace: req.Namespace, Name: taskReference.Name}, template); err != nil {
68+
if err := r.Get(ctx, types.NamespacedName{Namespace: req.Namespace, Name: taskTemplateName}, template); err != nil {
6369
log.Error(err, "failed to get template for task", "task", taskReference.Name)
6470
return &ctrl.Result{}, nil
6571
}
6672

6773
task := &aliecsv1alpha1.Task{}
6874
task.Namespace = req.Namespace
6975
task.Name = fmt.Sprintf("%s-%s", nodename, template.Name)
76+
if taskReference.NameSuffix != "" {
77+
task.Name = fmt.Sprintf("%s-%s", task.Name, taskReference.NameSuffix)
78+
}
79+
task.Name = strings.ToLower(task.Name)
80+
7081
if err := r.Get(ctx, types.NamespacedName{Name: task.Name, Namespace: task.Namespace}, task); err == nil {
7182
continue
7283
}

‎core/config.go‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ func setDefaults() error {
129129
viper.SetDefault("enableKafka", true)
130130
viper.SetDefault("logAllIL", false)
131131
viper.SetDefault("metricsEndpoint", "8088/ecsmetrics")
132+
viper.SetDefault("jitK8sBasePort", uint16(32000))
132133
return nil
133134
}
134135

@@ -200,6 +201,7 @@ func setFlags() error {
200201
pflag.Bool("enableKafka", viper.GetBool("enableKafka"), "Turn on the kafka messaging")
201202
pflag.Bool("logAllIL", viper.GetBool("logAllIL"), "Send all the logs into IL, including Debug and Trace messages")
202203
pflag.String("metricsEndpoint", viper.GetString("metricsEndpoint"), "Http endpoint from which metrics can be scraped: [port/endpoint]")
204+
pflag.Uint16("jitK8sBasePort", viper.GetUint16("jitK8sBasePort"), "First control port to allocate for JIT tasks on Kubernetes, incremented per-node")
203205

204206
pflag.Parse()
205207
return viper.BindPFlags(pflag.CommandLine)

‎core/task/managerk8s.go‎

Lines changed: 90 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ import (
2828
"context"
2929
"fmt"
3030
"maps"
31+
"regexp"
32+
"strconv"
3133
"strings"
3234
"sync"
3335
"time"
@@ -52,8 +54,29 @@ const (
5254
k8sDeployTimeout = 80 * time.Second
5355
k8sTransitionTimeout = 80 * time.Second
5456
k8sWatchRetryDelay = 5 * time.Second
57+
58+
// k8sJitTaskTemplateName is the shared TaskTemplate used to run JIT/DPL
59+
// pipeline tasks, since their task class name is unique per generated
60+
// workflow and can't be pre-registered as its own TaskTemplate.
61+
k8sJitTaskTemplateName = "dpl"
5562
)
5663

64+
// jitClassNameRe matches JIT-generated task class identifiers of the form
65+
// "jit-<40-hex-char-sha1>-<devicename>" (see configuration/template/dplutil.go)
66+
// and captures the devicename, e.g. "readout-proxy" or "Dispatcher".
67+
var jitClassNameRe = regexp.MustCompile(`^jit-[0-9a-f]{40}-(.+)$`)
68+
69+
func isJitClassName(name string) bool {
70+
return jitClassNameRe.MatchString(name)
71+
}
72+
73+
// jitOnK8sEnabled reports whether ECS may route JIT/DPL task classes to the
74+
// K8s path. Missing/false means JIT keeps running through Mesos, same as
75+
// before this bridge existed.
76+
func jitOnK8sEnabled() bool {
77+
return viper.GetBool("jitOnK8s")
78+
}
79+
5780
// k8sEnvRegistry maps ECS environment IDs to K8s custom Environment names.
5881
type k8sEnvRegistry struct {
5982
mu sync.RWMutex
@@ -83,6 +106,27 @@ func (r *k8sEnvRegistry) delete(envId uid.ID) {
83106
r.mu.Unlock()
84107
}
85108

109+
// k8sPortAllocator hands out control ports for JIT tasks, one monotonically
110+
// increasing counter per node (hostNetwork is used, so ports must not collide
111+
// between JIT tasks scheduled to the same node)
112+
type k8sPortAllocator struct {
113+
portBase uint16
114+
next map[string]uint16
115+
}
116+
117+
func newK8sPortAllocator(port uint16) *k8sPortAllocator {
118+
return &k8sPortAllocator{portBase: port, next: make(map[string]uint16)}
119+
}
120+
121+
func (allocator *k8sPortAllocator) allocate(hostname string) uint16 {
122+
port, ok := allocator.next[hostname]
123+
if !ok {
124+
port = allocator.portBase
125+
}
126+
allocator.next[hostname] = port + 1
127+
return port
128+
}
129+
86130
// newK8sClientFromViper creates a K8s client from viper config.
87131
// Returns nil, nil if kubeNamespace is not configured (K8s disabled).
88132
func newK8sClientFromViper() (*k8sclient.Client, error) {
@@ -112,7 +156,7 @@ func (m *Manager) deployKubernetesTasks(ctx context.Context, envId uid.ID, descr
112156
return nil, err
113157
}
114158

115-
nodeToRefs, err := m.buildK8sNodeTaskRefs(entries)
159+
nodeToRefs, err := m.buildK8sNodeTaskRefs(envId, entries)
116160
if err != nil {
117161
log.WithField("partition", envId).WithError(err).Error("failed to build K8s node task refs")
118162
return nil, err
@@ -187,8 +231,12 @@ func (m *Manager) createK8sTaskEntries(envId uid.ID, descriptors Descriptors) ([
187231
return entries, nil
188232
}
189233

190-
func (m *Manager) buildK8sNodeTaskRefs(entries []k8sTaskEntry) (map[string][]v1alpha1.TaskReference, error) {
234+
const jitK8sBasePortStr = "jitK8sBasePort"
235+
236+
func (m *Manager) buildK8sNodeTaskRefs(envId uid.ID, entries []k8sTaskEntry) (map[string][]v1alpha1.TaskReference, error) {
191237
nodeToRefs := make(map[string][]v1alpha1.TaskReference)
238+
239+
portAllocator := newK8sPortAllocator(viper.GetUint16(jitK8sBasePortStr))
192240
for _, e := range entries {
193241
t := e.task
194242
desc := e.desc
@@ -217,18 +265,50 @@ func (m *Manager) buildK8sNodeTaskRefs(entries []k8sTaskEntry) (map[string][]v1a
217265
}
218266
}
219267

220-
argsCLI := make([]string, 0, len(cmd.Arguments))
221-
for _, arg := range cmd.Arguments {
222-
if strings.TrimSpace(arg) != "" {
223-
argsCLI = append(argsCLI, arg)
268+
refName := taskClass.Identifier.Name
269+
nameSuffix := ""
270+
isJit := false
271+
if match := jitClassNameRe.FindStringSubmatch(refName); match != nil {
272+
isJit = true
273+
refName = k8sJitTaskTemplateName
274+
nameSuffix = match[1]
275+
}
276+
277+
var argsCLI []string
278+
if isJit {
279+
// Unlike readout/stfbuilder/stfsender (one fixed port per task type,
280+
// baked into their TaskTemplate), JIT devices are dynamic and several
281+
// can run on the same node, so each needs its own control port. K8s has
282+
// no Mesos-style resource offer to claim a verified-free port from, so
283+
// ECS tracks per-node allocation itself (mirrors scheduler.go's
284+
// Mesos-offer-based control port claim for FAIRMQ tasks).
285+
controlPort := portAllocator.allocate(t.hostname)
286+
cmd.Env = append(cmd.Env, fmt.Sprintf("OCC_CONTROL_PORT=%d", controlPort))
287+
cmd.Arguments = append(cmd.Arguments, "--control-port", strconv.FormatUint(uint64(controlPort), 10))
288+
289+
// The "dpl" TaskTemplate runs `bash -c <script>`, so the whole
290+
// generated pipeline (driver command + OCC flags, joined the same
291+
// way the Mesos executor does it) must be a single Args entry.
292+
value := ""
293+
if cmd.Value != nil {
294+
value = *cmd.Value
295+
}
296+
argsCLI = []string{strings.Join(append([]string{value}, cmd.Arguments...), " ")}
297+
} else {
298+
argsCLI = make([]string, 0, len(cmd.Arguments))
299+
for _, arg := range cmd.Arguments {
300+
if strings.TrimSpace(arg) != "" {
301+
argsCLI = append(argsCLI, arg)
302+
}
224303
}
225304
}
226305

227306
ref := v1alpha1.TaskReference{
228-
Name: taskClass.Identifier.Name,
229-
TaskID: t.taskId,
230-
ArgsCLI: argsCLI,
231-
Env: cmdEnvToK8sEnvVars(cmd.Env),
307+
Name: refName,
308+
TaskID: t.taskId,
309+
NameSuffix: nameSuffix,
310+
ArgsCLI: argsCLI,
311+
Env: cmdEnvToK8sEnvVars(cmd.Env),
232312
}
233313
nodeToRefs[t.hostname] = append(nodeToRefs[t.hostname], ref)
234314
}

0 commit comments

Comments
 (0)