Repository navigation
Expand file tree
/
Copy pathmmt.go
More file actions
875 lines (823 loc) · 25.1 KB
/
Copy pathmmt.go
File metadata and controls
875 lines (823 loc) · 25.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
package lit
import (
"context"
"encoding/json"
"errors"
"fmt"
"path/filepath"
"slices"
"strings"
"time"
"github.com/lightning-ai/sdk/go/internal/lightningcloud/openapi/generated/client/jobs_service"
"github.com/lightning-ai/sdk/go/internal/lightningcloud/openapi/generated/models"
"github.com/lightning-ai/sdk/go/internal/sdkclient"
)
// MMT represents a Lightning multi-machine job.
type MMT struct {
id string
name string
teamspaceID string
teamspace string
ownerName string
status string
numMachines int64
machine string
command string
image string
studioID string
totalCost float32
startedAt time.Time
stoppedAt time.Time
maxRunAttempts int64
currentRunAttempt int64
keepMinutes int64
tags []string
}
// MachineDict is the JSON-friendly public representation of one MMT machine.
type MachineDict struct {
// Name is the machine job name.
Name string `json:"name"`
// Status is the latest known machine job state.
Status string `json:"status"`
// Machine is the assigned machine type.
Machine string `json:"machine"`
}
// MMTDict is the JSON-friendly public representation of an MMT.
type MMTDict struct {
// Name is the multi-machine job display name.
Name string `json:"name"`
// Teamspace is the owning teamspace name.
Teamspace string `json:"teamspace"`
// Studio is the source studio name when the MMT was launched from a studio.
Studio *string `json:"studio"`
// Image is the container image used by the MMT.
Image *string `json:"image"`
// Command is the shell command run by each machine.
Command string `json:"command"`
// Status is the latest known MMT state.
Status string `json:"status"`
// Machine is the machine type assigned to each MMT worker.
Machine string `json:"machine"`
// Machines lists the worker jobs that belong to the MMT.
Machines []MachineDict `json:"machines"`
// TotalCost is the accumulated MMT cost reported by Lightning.
TotalCost float32 `json:"total_cost"`
}
// MMTPathMapping maps a teamspace connection path into an MMT container.
type MMTPathMapping struct {
// ContainerPath is the mount path inside each worker container.
ContainerPath string
// ConnectionName is the teamspace connection to mount.
ConnectionName string
// ConnectionPath is the path within the teamspace connection.
ConnectionPath string
}
type mmtOptions struct {
id string
teamspaceID string
teamspaceName string
ownerName string
studioID string
status string
numMachines int64
machine string
command string
image string
totalCost float32
env map[string]string
cloud string
interruptible bool
imageCredentials string
cloudAccountAuth bool
entrypoint *string
pathMappings []MMTPathMapping
artifactsSource string
artifactsDest string
maxRuntime int
maxRunAttempts int64
keepMinutes int64
}
type mmtWaitOptions struct {
interval time.Duration
timeout time.Duration
stopOnTimeout bool
}
// MMTOptions configures MMT lookup and creation.
type MMTOptions struct {
// ID resolves an existing MMT directly without looking it up by name.
ID string
// Teamspace scopes MMT lookup or creation to a specific teamspace.
Teamspace *Teamspace
// Studio launches the MMT from a specific studio environment.
Studio *Studio
// Status seeds the latest known state for ID-based handles.
Status string
// NumMachines is the number of worker machines to start.
NumMachines int64
// Machine selects the compute type used for each worker.
Machine Machine
// Command is the command executed by each worker.
Command string
// Image is the container image used when creating an MMT.
Image string
// TotalCost seeds the latest known cost for ID-based handles.
TotalCost float32
// Env sets environment variables for each worker.
Env map[string]string
// Cloud selects a cloud account or provider for the MMT.
Cloud string
// Interruptible overrides whether workers run on interruptible capacity.
Interruptible *bool
// ImageCredentials names credentials used to pull the container image.
ImageCredentials string
// CloudAccountAuth enables cloud account authentication for workers.
CloudAccountAuth bool
// Entrypoint overrides the container entrypoint.
Entrypoint string
// PathMappings mounts teamspace connection paths into each worker container.
PathMappings []MMTPathMapping
// ArtifactsSource is the local path copied from workers after completion.
ArtifactsSource string
// ArtifactsDestination is the remote teamspace connection destination.
ArtifactsDestination string
// MaxRuntime limits the MMT runtime in seconds.
MaxRuntime int
// MaxRunAttempts is the max number of run attempts for this multi-machine
// job. 0 means unset (legacy, no retries); 1 means a single attempt (no
// retries). When greater than 1 the RECREATE_ALL_NODES fault tolerance
// strategy is set automatically on the multi-machine job body. Set at the
// multi-machine job level, not on the per-machine JobSpec.
MaxRunAttempts int64
// KeepMinutes is minutes to keep the machines after the job stops.
// 0 means the machines are released immediately.
KeepMinutes int64
}
// MMTWaitOptions configures polling behavior while waiting for an MMT.
type MMTWaitOptions struct {
// Interval controls how often MMT status is polled.
Interval time.Duration
// Timeout limits how long Wait polls before returning.
Timeout time.Duration
// StopOnTimeout stops the remote MMT when the timeout expires.
StopOnTimeout bool
}
// ID returns the MMT ID.
func (m *MMT) ID() string {
if m == nil {
return ""
}
return m.id
}
// TeamspaceID returns the owning teamspace ID.
func (m *MMT) TeamspaceID() string {
if m == nil {
return ""
}
return m.teamspaceID
}
// StudioID returns the source studio ID when the MMT uses a studio.
func (m *MMT) StudioID() string {
if m == nil {
return ""
}
return m.studioID
}
// Name returns the MMT name.
func (m *MMT) Name() string {
if m == nil {
return ""
}
return m.name
}
// Teamspace returns the owning teamspace name.
func (m *MMT) Teamspace() string {
if m == nil {
return ""
}
return m.teamspace
}
// OwnerName returns the owning user or organization name.
func (m *MMT) OwnerName() string {
if m == nil {
return ""
}
return m.ownerName
}
// Status returns the latest known MMT state.
func (m *MMT) Status() string {
if m == nil {
return ""
}
return m.status
}
// NumMachines returns the requested number of machines.
func (m *MMT) NumMachines() int64 {
if m == nil {
return 0
}
return m.numMachines
}
// Machine returns the configured machine.
func (m *MMT) Machine() string {
if m == nil {
return ""
}
return m.machine
}
// Command returns the command run by the MMT.
func (m *MMT) Command() string {
if m == nil {
return ""
}
return m.command
}
// Image returns the image used by the MMT.
func (m *MMT) Image() string {
if m == nil {
return ""
}
return m.image
}
// TotalCost returns the latest known total cost.
func (m *MMT) TotalCost() float32 {
if m == nil {
return 0
}
return m.totalCost
}
// StartedAt returns when the MMT's machines started running, or the zero time
// if they haven't started yet.
func (m *MMT) StartedAt() time.Time {
if m == nil {
return time.Time{}
}
return m.startedAt
}
// StoppedAt returns when the MMT stopped running, or the zero time if it
// hasn't stopped yet.
func (m *MMT) StoppedAt() time.Time {
if m == nil {
return time.Time{}
}
return m.stoppedAt
}
// MaxRunAttempts returns the max number of run attempts for this multi-machine
// job, read from the multi-machine job body. 0 means unset.
func (m *MMT) MaxRunAttempts() int64 {
if m == nil {
return 0
}
return m.maxRunAttempts
}
// CurrentRunAttempt returns the current run attempt for this multi-machine job,
// read from the multi-machine job body. 0 means unset.
func (m *MMT) CurrentRunAttempt() int64 {
if m == nil {
return 0
}
return m.currentRunAttempt
}
// KeepMinutes returns minutes the machines are kept after the job stops.
// 0 means unset (the machines are released immediately).
func (m *MMT) KeepMinutes() int64 {
if m == nil {
return 0
}
return m.keepMinutes
}
// Tags returns the teamspace tags applied to this multi-machine job, in the
// order the platform returns them.
func (m *MMT) Tags() []string {
return slices.Clone(m.tags)
}
// GetMMT returns an existing MMT by name or ID.
func GetMMT(name string, opts ...MMTOptions) (*MMT, error) {
resolved := applyMMTOptions(opts...)
if name == "" && resolved.id == "" {
return nil, errors.New("mmt requires name or ID")
}
if resolved.id != "" {
return &MMT{
id: resolved.id,
name: name,
teamspaceID: resolved.teamspaceID,
teamspace: resolved.teamspaceName,
ownerName: resolved.ownerName,
status: resolved.status,
numMachines: resolved.numMachines,
machine: resolved.machine,
command: resolved.command,
image: resolved.image,
studioID: resolved.studioID,
totalCost: resolved.totalCost,
}, nil
}
api, err := sdkclient.New()
if err != nil {
return nil, err
}
if resolved.teamspaceID == "" {
return nil, errors.New("mmt requires teamspace")
}
resp, err := api.JobsService.JobsServiceGetMultiMachineJobByName(
jobs_service.NewJobsServiceGetMultiMachineJobByNameParamsWithContext(context.Background()).
WithName(name).
WithProjectID(resolved.teamspaceID),
)
if err != nil {
return nil, err
}
return mmtFromModel(resp.Payload, resolved), nil
}
// RunMMT creates and starts a new MMT.
func RunMMT(name string, numMachines int64, machine Machine, command string, opts ...MMTOptions) (*MMT, error) {
resolved := applyMMTOptions(opts...)
if name == "" {
return nil, errors.New("mmt run requires name")
}
if numMachines <= 1 {
return nil, errors.New("mmt run cannot use less than 2 machines")
}
if err := validateMMTComputeEnvironment(command, resolved); err != nil {
return nil, err
}
if err := validateKeepMinutes(resolved.keepMinutes); err != nil {
return nil, err
}
api, err := sdkclient.New()
if err != nil {
return nil, err
}
if resolved.teamspaceID == "" {
return nil, errors.New("mmt run requires teamspace or studio")
}
// Fault tolerance is auto-derived: when the job is multi-machine and allows
// retries (max_run_attempts > 1) the only currently supported strategy
// (RECREATE_ALL_NODES) is set automatically; otherwise the field is left
// unset.
var faultTolerance *models.V1MultiMachineJobFaultTolerance
if numMachines > 1 && resolved.maxRunAttempts > 1 {
s := models.V1MultiMachineJobFaultToleranceStrategyMULTIMACHINEJOBFAULTTOLERANCESTRATEGYRECREATEALLNODES
faultTolerance = &models.V1MultiMachineJobFaultTolerance{Strategy: &s}
}
body := &models.JobsServiceCreateMultiMachineJobBody{
ClusterID: resolved.cloud,
Machines: numMachines,
Name: name,
Spec: mmtJobSpec(numMachines, string(machine), command, resolved),
MaxRunAttempts: resolved.maxRunAttempts,
FaultTolerance: faultTolerance,
}
resp, err := api.JobsService.JobsServiceCreateMultiMachineJob(
jobs_service.NewJobsServiceCreateMultiMachineJobParamsWithContext(context.Background()).WithProjectID(resolved.teamspaceID).WithBody(body),
)
if err != nil {
return nil, err
}
return mmtFromModel(resp.Payload, resolved), nil
}
// Stop stops the MMT.
func (m *MMT) Stop() error {
if m == nil || m.teamspaceID == "" || m.id == "" {
return errors.New("mmt stop requires teamspace ID and MMT ID")
}
api, err := sdkclient.New()
if err != nil {
return err
}
desiredState := models.V1MultiMachineJobStateMultiMachineJobSTATESTOP
resp, err := api.JobsService.JobsServiceUpdateMultiMachineJob(
jobs_service.NewJobsServiceUpdateMultiMachineJobParamsWithContext(context.Background()).
WithProjectID(m.teamspaceID).
WithID(m.id).
WithBody(&models.JobsServiceUpdateMultiMachineJobBody{DesiredState: &desiredState}),
)
if err != nil {
return err
}
updated := mmtFromModel(resp.Payload, mmtOptions{teamspaceName: m.teamspace, ownerName: m.ownerName})
if updated != nil {
*m = *updated
}
return nil
}
// SetTags replaces the MMT's tag set with the given tag names. Tags that don't
// exist in the teamspace yet are created. Pass zero names to remove all tags.
func (m *MMT) SetTags(tagNames ...string) error {
if m == nil || m.teamspaceID == "" || m.id == "" {
return errors.New("mmt set tags requires teamspace ID and MMT ID")
}
return setWorkloadTags(m.teamspaceID, "multi_machine_job", m.id, tagNames)
}
// Delete deletes the MMT.
func (m *MMT) Delete() error {
if m == nil || m.teamspaceID == "" || m.id == "" {
return errors.New("mmt delete requires teamspace ID and MMT ID")
}
api, err := sdkclient.New()
if err != nil {
return err
}
_, err = api.JobsService.JobsServiceDeleteMultiMachineJob(
jobs_service.NewJobsServiceDeleteMultiMachineJobParamsWithContext(context.Background()).
WithProjectID(m.teamspaceID).
WithID(m.id),
)
return err
}
// Wait polls until the MMT reaches a terminal state.
func (m *MMT) Wait(opts ...MMTWaitOptions) error {
if m == nil || m.teamspaceID == "" || m.id == "" {
return errors.New("mmt wait requires teamspace ID and MMT ID")
}
resolved := applyMMTWaitOptions(opts...)
api, err := sdkclient.New()
if err != nil {
return err
}
start := time.Now()
for {
resp, err := api.JobsService.JobsServiceGetMultiMachineJob(
jobs_service.NewJobsServiceGetMultiMachineJobParamsWithContext(context.Background()).
WithProjectID(m.teamspaceID).
WithID(m.id),
)
if err != nil {
return err
}
updated := mmtFromModel(resp.Payload, mmtOptions{teamspaceName: m.teamspace, ownerName: m.ownerName})
if updated == nil {
return fmt.Errorf("get mmt returned empty payload")
}
*m = *updated
if isMMTTerminalStatus(m.status) {
return nil
}
if resolved.timeout > 0 && time.Since(start) > resolved.timeout {
if resolved.stopOnTimeout {
_ = m.Stop()
}
return fmt.Errorf("mmt didn't finish within the provided timeout")
}
time.Sleep(resolved.interval)
}
}
// Machines lists the jobs backing the MMT.
func (m *MMT) Machines() ([]*Job, error) {
if m == nil || m.teamspaceID == "" || m.id == "" {
return nil, errors.New("mmt machines requires teamspace ID and MMT ID")
}
api, err := sdkclient.New()
if err != nil {
return nil, err
}
var result []*Job
var pageToken string
for {
params := jobs_service.NewJobsServiceListJobsParamsWithContext(context.Background()).
WithProjectID(m.teamspaceID).
WithMultiMachineJobID(&m.id)
if pageToken != "" {
params = params.WithPageToken(&pageToken)
}
resp, err := api.JobsService.JobsServiceListJobs(params)
if err != nil {
return nil, err
}
if resp.Payload == nil {
return nil, fmt.Errorf("list mmt machines returned empty payload")
}
for _, model := range resp.Payload.Jobs {
if model == nil {
continue
}
result = append(result, m.jobFromModel(model))
}
if resp.Payload.NextPageToken == "" {
return result, nil
}
pageToken = resp.Payload.NextPageToken
}
}
// Logs returns the merged logs of every machine in the MMT, each line prefixed
// with the bracketed name of the machine it came from.
func (m *MMT) Logs() (string, error) {
if m == nil || m.teamspaceID == "" || m.id == "" {
return "", errors.New("mmt logs requires teamspace ID and MMT ID")
}
machines, err := m.Machines()
if err != nil {
return "", err
}
names := make(map[string]string, len(machines))
for _, machine := range machines {
if machine != nil {
names[machine.ID()] = machine.Name()
}
}
lines, err := readLogs(m.teamspaceID, func(params *jobs_service.JobsServiceGetLogsParams) {
params.SetMmtID(&m.id)
}, names)
if err != nil {
return "", err
}
if len(lines) == 0 && isJobTerminalStatus(m.status) {
return "", errLogsMigrating
}
return strings.Join(lines, "\n"), nil
}
// Dict returns a JSON-friendly public representation of the MMT.
func (m *MMT) Dict() (MMTDict, error) {
if m == nil {
return MMTDict{}, nil
}
machines, err := m.Machines()
if err != nil {
return MMTDict{}, err
}
machineDicts := make([]MachineDict, 0, len(machines))
for _, machine := range machines {
if machine == nil {
continue
}
machineDicts = append(machineDicts, MachineDict{
Name: machine.Name(),
Status: machine.Status(),
Machine: machine.Machine(),
})
}
var studioName *string
if m.studioID != "" {
studioName = &m.studioID
}
var image *string
if m.image != "" {
image = &m.image
}
return MMTDict{
Name: m.name,
Teamspace: ownerTeamspace(m.ownerName, m.teamspace),
Studio: studioName,
Image: image,
Command: m.command,
Status: m.status,
Machine: m.machine,
Machines: machineDicts,
TotalCost: m.totalCost,
}, nil
}
// JSON returns an indented JSON representation of the MMT.
func (m *MMT) JSON() (string, error) {
dict, err := m.Dict()
if err != nil {
return "", err
}
body, err := json.MarshalIndent(dict, "", " ")
if err != nil {
return "", err
}
return string(body), nil
}
// ArtifactsURI is always empty for an MMT: each machine writes its own
// artifacts, so there is no single address for the run. Use Machines and each
// machine's ArtifactsURI.
func (m *MMT) ArtifactsURI() string {
return ""
}
// ListArtifacts lists what every machine of this MMT wrote to the teamspace
// drive. Each path is prefixed with the name of the machine that wrote it, so
// the listing matches the layout DownloadArtifacts writes.
func (m *MMT) ListArtifacts(path string, recursive bool) ([]FileEntry, error) {
machines, err := m.Machines()
if err != nil {
return nil, err
}
var entries []FileEntry
for _, machine := range machines {
machineEntries, err := machine.ListArtifacts(path, recursive)
if err != nil {
return nil, err
}
for _, entry := range machineEntries {
entry.Path = machine.Name() + "/" + entry.Path
entries = append(entries, entry)
}
}
return entries, nil
}
// DownloadArtifacts downloads what every machine of this MMT wrote, each into
// its own folder named after the machine under targetDir.
func (m *MMT) DownloadArtifacts(targetDir, path string) error {
machines, err := m.Machines()
if err != nil {
return err
}
for _, machine := range machines {
if err := machine.DownloadArtifacts(filepath.Join(targetDir, machine.Name()), path); err != nil {
return err
}
}
return nil
}
// Link returns the Lightning web URL for the MMT.
func (m *MMT) Link() string {
if m == nil || m.ownerName == "" || m.teamspace == "" || m.name == "" {
return ""
}
return fmt.Sprintf("https://platform.lightning.ai/%s/%s/jobs/%s?app_id=mmt", m.ownerName, m.teamspace, m.name)
}
func applyMMTOptions(opts ...MMTOptions) mmtOptions {
var resolved mmtOptions
if len(opts) > 0 {
resolved.id = opts[0].ID
if opts[0].Teamspace != nil {
resolved.teamspaceID = opts[0].Teamspace.ID()
resolved.teamspaceName = opts[0].Teamspace.Name()
if resolved.cloud == "" {
resolved.cloud = opts[0].Teamspace.DefaultCloudAccount()
}
if owner := opts[0].Teamspace.Owner(); owner != nil {
resolved.ownerName = owner.Name()
}
}
if opts[0].Studio != nil {
resolved.studioID = opts[0].Studio.ID()
resolved.cloud = firstNonEmpty(opts[0].Cloud, opts[0].Studio.Cloud(), resolved.cloud)
if resolved.teamspaceID == "" {
resolved.teamspaceID = opts[0].Studio.TeamspaceID()
resolved.teamspaceName = opts[0].Studio.Teamspace()
resolved.ownerName = opts[0].Studio.OwnerName()
}
}
resolved.status = opts[0].Status
resolved.numMachines = opts[0].NumMachines
resolved.machine = string(opts[0].Machine)
resolved.command = opts[0].Command
resolved.image = opts[0].Image
resolved.totalCost = opts[0].TotalCost
resolved.env = opts[0].Env
resolved.cloud = firstNonEmpty(opts[0].Cloud, resolved.cloud)
if opts[0].Interruptible != nil {
resolved.interruptible = *opts[0].Interruptible
}
resolved.imageCredentials = opts[0].ImageCredentials
resolved.cloudAccountAuth = opts[0].CloudAccountAuth
if opts[0].Entrypoint != "" {
resolved.entrypoint = &opts[0].Entrypoint
}
resolved.pathMappings = opts[0].PathMappings
resolved.artifactsSource = opts[0].ArtifactsSource
resolved.artifactsDest = opts[0].ArtifactsDestination
resolved.maxRuntime = opts[0].MaxRuntime
resolved.maxRunAttempts = opts[0].MaxRunAttempts
resolved.keepMinutes = opts[0].KeepMinutes
}
return resolved
}
func applyMMTWaitOptions(opts ...MMTWaitOptions) mmtWaitOptions {
resolved := mmtWaitOptions{interval: 5 * time.Second}
if len(opts) > 0 {
resolved.interval = opts[0].Interval
resolved.timeout = opts[0].Timeout
resolved.stopOnTimeout = opts[0].StopOnTimeout
if resolved.interval == 0 {
resolved.interval = 5 * time.Second
}
}
if resolved.interval < 0 {
resolved.interval = 0
}
return resolved
}
func validateMMTComputeEnvironment(command string, opts mmtOptions) error {
if opts.studioID != "" {
if opts.image != "" {
return errors.New("image and studio are mutually exclusive as both define the environment to run the job in")
}
if command == "" {
return errors.New("command is required when using a studio")
}
if opts.artifactsSource != "" || opts.artifactsDest != "" {
return errors.New("artifacts are only supported when using a custom image")
}
return nil
}
if opts.image == "" {
return errors.New("either image or studio must be provided")
}
if err := validateArtifacts(opts.artifactsSource, opts.artifactsDest); err != nil {
return err
}
return nil
}
func mmtFromModel(model *models.V1MultiMachineJob, opts mmtOptions) *MMT {
if model == nil {
return nil
}
result := &MMT{
id: model.ID,
name: model.Name,
teamspaceID: model.ProjectID,
teamspace: opts.teamspaceName,
ownerName: opts.ownerName,
numMachines: model.Machines,
studioID: model.CloudspaceID,
totalCost: model.TotalCost,
maxRunAttempts: model.MaxRunAttempts,
currentRunAttempt: model.CurrentRunAttempt,
tags: tagNames(model.Tags),
}
if model.State != nil {
result.status = string(*model.State)
}
if model.Status != nil {
result.startedAt = time.Time(model.Status.StartedAt)
result.stoppedAt = time.Time(model.Status.StoppedAt)
}
if model.Spec != nil {
result.machine = model.Spec.InstanceName
result.command = model.Spec.Command
result.image = model.Spec.Image
if result.studioID == "" {
result.studioID = model.Spec.CloudspaceID
}
result.keepMinutes = model.Spec.ReserveMachinesTimeoutMinutes
}
return result
}
// isMMTTerminalStatus reports whether an observed MMT state is terminal. The
// API reports either the bare state or the MultiMachineJob_STATE_* enum name,
// so both spellings are matched. Mirrors the backend's terminal set:
// completed, stopped, failed, deleted.
func isMMTTerminalStatus(status string) bool {
switch strings.ToLower(status) {
case "completed", "stopped", "failed", "deleted",
"multimachinejob_state_completed", "multimachinejob_state_stopped", "multimachinejob_state_failed", "multimachinejob_state_deleted":
return true
default:
return false
}
}
func (m *MMT) jobFromModel(model *models.V1Job) *Job {
if model == nil {
return nil
}
opts := jobOptions{
id: model.ID,
teamspaceID: model.ProjectID,
teamspaceName: m.teamspace,
ownerName: m.ownerName,
mmtID: model.MultiMachineJobID,
status: model.State,
publicIP: model.PublicIPAddress,
totalCost: model.TotalCost,
}
if model.Spec != nil {
opts.studioID = model.Spec.CloudspaceID
opts.machine = model.Spec.InstanceName
opts.command = model.Spec.Command
opts.image = model.Spec.Image
}
return jobFromModel(model, opts)
}
func mmtJobSpec(numMachines int64, machine, command string, opts mmtOptions) *models.V1JobSpec {
return &models.V1JobSpec{
CloudspaceID: opts.studioID,
ClusterID: opts.cloud,
Command: command,
Entrypoint: resolveEntrypoint(command, opts.entrypoint, opts.image),
Env: envVars(opts.env),
Image: opts.image,
ImageClusterCredentials: opts.cloudAccountAuth,
ImageSecretRef: opts.imageCredentials,
InstanceName: machine,
PathMappings: mmtPathMappings(opts),
Quantity: numMachines,
RequestedRunDurationSeconds: maxRuntime(opts.maxRuntime),
KeepMachineAfterStop: opts.keepMinutes > 0,
ReserveMachinesTimeoutMinutes: opts.keepMinutes,
Spot: opts.interruptible,
}
}
func mmtPathMappings(opts mmtOptions) []*models.V1PathMapping {
if len(opts.pathMappings) == 0 && opts.artifactsSource == "" {
return nil
}
result := make([]*models.V1PathMapping, 0, len(opts.pathMappings)+1)
for _, mapping := range opts.pathMappings {
result = append(result, &models.V1PathMapping{
ConnectionName: mapping.ConnectionName,
ConnectionPath: mapping.ConnectionPath,
ContainerPath: mapping.ContainerPath,
})
}
if opts.artifactsSource != "" {
connectionName, connectionPath := artifactConnection(opts.artifactsDest)
result = append(result, &models.V1PathMapping{
ConnectionName: connectionName,
ConnectionPath: connectionPath,
ContainerPath: opts.artifactsSource,
})
}
return result
}