Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions go/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,23 @@ if err != nil {
fmt.Println(job.Name(), job.Status(), job.Machine())
```

## List jobs by tag

```go
jobs, err := teamspace.Jobs(lit.WithJobTags("prod", "nightly"))
if err != nil {
log.Fatal(err)
}

for _, job := range jobs {
fmt.Println(job.Name(), job.Tags())
}
```

Jobs carrying at least one of the tags are returned. `teamspace.MMTs(...)`
accepts the same option for multi-machine jobs, and `teamspace.Tags()` lists the tags
defined in the teamspace.

# API shape

| Area | Entry point |
Expand Down
9 changes: 9 additions & 0 deletions go/job.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"net/http"
"path"
"regexp"
"slices"
"strconv"
"strings"
"time"
Expand Down Expand Up @@ -42,6 +43,7 @@ type Job struct {
artifactsDestination string
maxRunAttempts int64
currentRunAttempt int64
tags []string
}

// JobDict is the JSON-friendly public representation of a job.
Expand Down Expand Up @@ -330,6 +332,12 @@ func (j *Job) CurrentRunAttempt() int64 {
return j.currentRunAttempt
}

// Tags returns the teamspace tags applied to this job, in the order the
// platform returns them.
func (j *Job) Tags() []string {
return slices.Clone(j.tags)
}

// GetJob returns an existing job by name or ID.
func GetJob(name string, opts ...JobOptions) (*Job, error) {
resolved := applyJobOptions(opts...)
Expand Down Expand Up @@ -752,6 +760,7 @@ func jobFromModel(model *models.V1Job, opts jobOptions) *Job {
totalCost: model.TotalCost,
startedAt: time.Time(model.StartedAt),
stoppedAt: time.Time(model.StoppedAt),
tags: tagNames(model.Tags),
}
if model.Spec != nil {
result.machine = model.Spec.InstanceName
Expand Down
9 changes: 9 additions & 0 deletions go/mmt.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"encoding/json"
"errors"
"fmt"
"slices"
"strings"
"time"

Expand Down Expand Up @@ -32,6 +33,7 @@ type MMT struct {

maxRunAttempts int64
currentRunAttempt int64
tags []string
}

// MachineDict is the JSON-friendly public representation of one MMT machine.
Expand Down Expand Up @@ -297,6 +299,12 @@ func (m *MMT) CurrentRunAttempt() int64 {
return m.currentRunAttempt
}

// 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...)
Expand Down Expand Up @@ -691,6 +699,7 @@ func mmtFromModel(model *models.V1MultiMachineJob, opts mmtOptions) *MMT {

maxRunAttempts: model.MaxRunAttempts,
currentRunAttempt: model.CurrentRunAttempt,
tags: tagNames(model.Tags),
}
if model.State != nil {
result.status = string(*model.State)
Expand Down
102 changes: 102 additions & 0 deletions go/owner_teamspace_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,108 @@ func TestTeamspaceListsStudiosJobsAndMMTs(t *testing.T) {
}
}

func TestTeamspaceListsJobsAndMMTsByTag(t *testing.T) {
var seen []string

server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
seen = append(seen, r.Method+" "+r.URL.Path)
w.Header().Set("Content-Type", "application/json")
switch r.Method + " " + r.URL.Path {
case "GET /v1/projects/project-1/workload-tags":
_ = json.NewEncoder(w).Encode(map[string]any{
"tags": []map[string]any{
{"id": "tag-1", "name": "prod"},
{"id": "tag-2", "name": "team a"},
},
})
case "GET /v1/projects/project-1/jobs":
assert.Equal(t, "true", r.URL.Query().Get("standalone"))
assert.Equal(t, []string{"tag-2", "tag-1"}, r.URL.Query()["tagIds"])
_ = json.NewEncoder(w).Encode(map[string]any{
"jobs": []map[string]any{
{"id": "job-1", "name": "train", "projectId": "project-1", "tags": []map[string]any{{"id": "tag-2", "name": "team a"}}},
},
})
case "GET /v1/projects/project-1/multi-machine-jobs":
_ = json.NewEncoder(w).Encode(map[string]any{
"multiMachineJobs": []map[string]any{
{"id": "mmt-1", "name": "tagged", "projectId": "project-1", "tags": []map[string]any{{"id": "tag-1", "name": "prod"}}},
{"id": "mmt-2", "name": "other-tag", "projectId": "project-1", "tags": []map[string]any{{"id": "tag-3", "name": "staging"}}},
{"id": "mmt-3", "name": "untagged", "projectId": "project-1"},
},
})
default:
assert.Fail(t, fmt.Sprintf("unexpected request: %s %s", r.Method, r.URL.RequestURI()))
}
}))
defer server.Close()
t.Setenv("LIGHTNING_CLOUD_URL", server.URL)

ts := mustTeamspace(t, "project-1", "default", "alice")

jobs, err := ts.Jobs(lit.WithJobTags(" Team A ", "PROD"))
require.NoError(t, err)
require.Len(t, jobs, 1)
assert.Equal(t, "job-1", jobs[0].ID())
assert.Equal(t, []string{"team a"}, jobs[0].Tags())

mmts, err := ts.MMTs(lit.WithJobTags("prod"))
require.NoError(t, err)
require.Len(t, mmts, 1)
assert.Equal(t, "mmt-1", mmts[0].ID())
assert.Equal(t, []string{"prod"}, mmts[0].Tags())

tags, err := ts.Tags()
require.NoError(t, err)
assert.Equal(t, []string{"prod", "team a"}, tags)

assert.Equal(t, []string{
"GET /v1/projects/project-1/workload-tags",
"GET /v1/projects/project-1/jobs",
"GET /v1/projects/project-1/workload-tags",
"GET /v1/projects/project-1/multi-machine-jobs",
"GET /v1/projects/project-1/workload-tags",
}, seen)
}

func TestTeamspaceJobsRejectsUnknownTagBeforeListing(t *testing.T) {
tags := []map[string]any{
{"id": "tag-2", "name": "staging"},
{"id": "tag-1", "name": "prod"},
}
var seen []string

server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
seen = append(seen, r.Method+" "+r.URL.Path)
w.Header().Set("Content-Type", "application/json")
if r.Method+" "+r.URL.Path != "GET /v1/projects/project-1/workload-tags" {
assert.Fail(t, fmt.Sprintf("unexpected request: %s %s", r.Method, r.URL.RequestURI()))
return
}
_ = json.NewEncoder(w).Encode(map[string]any{"tags": tags})
}))
defer server.Close()
t.Setenv("LIGHTNING_CLOUD_URL", server.URL)

ts := mustTeamspace(t, "project-1", "default", "alice")

_, err := ts.Jobs(lit.WithJobTags("nightly"))
assert.EqualError(t, err, `teamspace has no tag named "nightly"; tags in this teamspace: prod, staging`)

_, err = ts.MMTs(lit.WithJobTags("nightly"))
assert.EqualError(t, err, `teamspace has no tag named "nightly"; tags in this teamspace: prod, staging`)

tags = nil
_, err = ts.Jobs(lit.WithJobTags("prod"))
assert.EqualError(t, err, `teamspace has no tag named "prod"; tags in this teamspace: none`)

assert.Equal(t, []string{
"GET /v1/projects/project-1/workload-tags",
"GET /v1/projects/project-1/workload-tags",
"GET /v1/projects/project-1/workload-tags",
}, seen)
}

func TestTeamspaceRefreshAndCloudAccountsUseGeneratedRoutes(t *testing.T) {
var seen []string

Expand Down
Loading
Loading