Skip to content

Commit

Permalink
Feature: Golden Versions (#24055)
Browse files Browse the repository at this point in the history
* TaggedVersion information in structs, rather than job_endpoint (#23841)

* TaggedVersion information in structs, rather than job_endpoint

* Test for taggedVersion description length

* Some API plumbing

* Tag and Untag job versions (#23863)

* Tag and Untag at API level on down, but am I unblocking the wrong thing?

* Code and comment cleanup

* Unset methods generally now I stare long into the namespace abyss

* Namespace passes through with QueryOptions removed from a write requesting struct

* Comment and PR review cleanup

* Version back to VersionStr

* Generally consolidate unset logic into apply for version tagging

* Addressed some PR comments

* Auth check and RPC forwarding

* uint64 instead of pointer for job version after api layer and renamed copy

* job tag command split into apply and unset

* latest-version convenience handling moved to CLI command level

* CLI tests for tagging/untagging

* UI parts removed

* Add to job table when unsetting job tag on latest version

* Vestigial no more

* Compare versions by name and version number with the nomad history command (#23889)

* First pass at passing a tagname and/or diff version to plan/versions requests

* versions API now takes compare_to flags

* Job history command output can have tag names and descriptions

* compare_to to diff-tag and diff-version, plus adding flags to history command

* 0th version now shows a diff if a specific diff target is requested

* Addressing some PR comments

* Simplify the diff-appending part of jobVersions and hide None-type diffs from CLI

* Remove the diff-tag and diff-version parts of nomad job plan, with an eye toward making them a new top-level CLI command soon

* Version diff tests

* re-implement JobVersionByTagName

* Test mods and simplification

* Documentation for nomad job history additions

* Prevent pruning and reaping of TaggedVersion jobs (#23983)

tagged versions should not count against JobTrackedVersions
i.e. new job versions being inserted should not evict tagged versions

and GC should not delete a job if any of its versions are tagged

Co-authored-by: Daniel Bennett <[email protected]>

---------

Co-authored-by: Daniel Bennett <[email protected]>

* [ui] Version Tags on the job versions page (#24013)

* Timeline styles and their buttons modernized, and tags added

* styled but not yet functional version blocks

* Rough pass at edit/unedit UX

* Styles consolidated

* better UX around version tag crud, plus adapter and serializers

* Mirage and acceptance tests

* Modify percy to not show time-based things

---------

Co-authored-by: Daniel Bennett <[email protected]>

* Job revert command and API endpoint can take a string version tag name (#24059)

* Job revert command and API endpoint can take a string version tag name

* RevertOpts as a signature-modified alternative to Revert()

* job revert CLI test

* Version pointers in endpoint tests

* Dont copy over the tag when a job is reverted to a version with a tag

* Convert tag name to version number at CLI level

* Client method for version lookup by tag

* No longer double-declaring client

* [ui] Add tag filter to the job versions page (#24064)

* Rough pass at the UI for version diff dropdown

* Cleanup and diff fetching via adapter method

* TaggedVersion now VersionTag (#24066)

---------

Co-authored-by: Daniel Bennett <[email protected]>
  • Loading branch information
philrenaud and gulducat authored Sep 25, 2024
1 parent a3a2028 commit e206993
Show file tree
Hide file tree
Showing 43 changed files with 2,363 additions and 124 deletions.
82 changes: 81 additions & 1 deletion api/jobs.go
Original file line number Diff line number Diff line change
Expand Up @@ -264,8 +264,50 @@ func (j *Jobs) ScaleStatus(jobID string, q *QueryOptions) (*JobScaleStatusRespon
// Versions is used to retrieve all versions of a particular job given its
// unique ID.
func (j *Jobs) Versions(jobID string, diffs bool, q *QueryOptions) ([]*Job, []*JobDiff, *QueryMeta, error) {
opts := &VersionsOptions{
Diffs: diffs,
}
return j.VersionsOpts(jobID, opts, q)
}

// VersionByTag is used to retrieve a job version by its VersionTag name.
func (j *Jobs) VersionByTag(jobID, tag string, q *QueryOptions) (*Job, *QueryMeta, error) {
versions, _, qm, err := j.Versions(jobID, false, q)
if err != nil {
return nil, nil, err
}

// Find the version with the matching tag
for _, version := range versions {
if version.VersionTag != nil && version.VersionTag.Name == tag {
return version, qm, nil
}
}

return nil, nil, fmt.Errorf("version tag %s not found for job %s", tag, jobID)
}

type VersionsOptions struct {
Diffs bool
DiffTag string
DiffVersion *uint64
}

func (j *Jobs) VersionsOpts(jobID string, opts *VersionsOptions, q *QueryOptions) ([]*Job, []*JobDiff, *QueryMeta, error) {
var resp JobVersionsResponse
qm, err := j.client.query(fmt.Sprintf("/v1/job/%s/versions?diffs=%v", url.PathEscape(jobID), diffs), &resp, q)

qp := url.Values{}
if opts != nil {
qp.Add("diffs", strconv.FormatBool(opts.Diffs))
if opts.DiffTag != "" {
qp.Add("diff_tag", opts.DiffTag)
}
if opts.DiffVersion != nil {
qp.Add("diff_version", strconv.FormatUint(*opts.DiffVersion, 10))
}
}

qm, err := j.client.query(fmt.Sprintf("/v1/job/%s/versions?%s", url.PathEscape(jobID), qp.Encode()), &resp, q)
if err != nil {
return nil, nil, nil, err
}
Expand Down Expand Up @@ -988,6 +1030,24 @@ func (j *JobUILink) Copy() *JobUILink {
}
}

type JobVersionTag struct {
Name string
Description string
TaggedTime int64
}

func (j *JobVersionTag) Copy() *JobVersionTag {
if j == nil {
return nil
}

return &JobVersionTag{
Name: j.Name,
Description: j.Description,
TaggedTime: j.TaggedTime,
}
}

func (js *JobSubmission) Canonicalize() {
if js == nil {
return
Expand Down Expand Up @@ -1066,6 +1126,7 @@ type Job struct {
CreateIndex *uint64
ModifyIndex *uint64
JobModifyIndex *uint64
VersionTag *JobVersionTag
}

// IsPeriodic returns whether a job is periodic.
Expand Down Expand Up @@ -1622,3 +1683,22 @@ type JobStatusesRequest struct {
// IncludeChildren will include child (batch) jobs in the response.
IncludeChildren bool
}

type TagVersionRequest struct {
Version uint64
Description string
WriteRequest
}

func (j *Jobs) TagVersion(jobID string, version uint64, name string, description string, q *WriteOptions) (*WriteMeta, error) {
var tagRequest = &TagVersionRequest{
Version: version,
Description: description,
}

return j.client.put("/v1/job/"+url.PathEscape(jobID)+"/versions/"+name+"/tag", tagRequest, nil, q)
}

func (j *Jobs) UntagVersion(jobID string, name string, q *WriteOptions) (*WriteMeta, error) {
return j.client.delete("/v1/job/"+url.PathEscape(jobID)+"/versions/"+name+"/tag", nil, nil, q)
}
96 changes: 94 additions & 2 deletions command/agent/job_endpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,14 @@ func (s *HTTPServer) JobSpecificRequest(resp http.ResponseWriter, req *http.Requ
case strings.HasSuffix(path, "/action"):
jobID := strings.TrimSuffix(path, "/action")
return s.jobRunAction(resp, req, jobID)
case strings.HasSuffix(path, "/tag"):
parts := strings.Split(path, "/")
if len(parts) != 4 {
return nil, CodedError(404, "invalid job tag endpoint")
}
jobID := parts[0]
name := parts[2] // job/<jobID>/tag/<name>
return s.jobTagVersion(resp, req, jobID, name)
default:
return s.jobCRUD(resp, req, path)
}
Expand Down Expand Up @@ -400,6 +408,62 @@ func (s *HTTPServer) jobRunAction(resp http.ResponseWriter, req *http.Request, j
return s.execStream(conn, &args)
}

func (s *HTTPServer) jobTagVersion(resp http.ResponseWriter, req *http.Request, jobID string, name string) (interface{}, error) {
switch req.Method {
case http.MethodPut, http.MethodPost:
return s.jobVersionApplyTag(resp, req, jobID, name)
case http.MethodDelete:
return s.jobVersionUnsetTag(resp, req, jobID, name)
default:
return nil, CodedError(405, ErrInvalidMethod)
}
}

func (s *HTTPServer) jobVersionApplyTag(resp http.ResponseWriter, req *http.Request, jobID string, name string) (interface{}, error) {
var args api.TagVersionRequest

if err := decodeBody(req, &args); err != nil {
return nil, CodedError(400, err.Error())
}

rpcArgs := structs.JobApplyTagRequest{
JobID: jobID,
Version: args.Version,
Name: name,
Tag: &structs.JobVersionTag{
Name: name,
Description: args.Description,
},
}

// parseWriteRequest overrides Namespace, Region and AuthToken
// based on values from the original http request
s.parseWriteRequest(req, &rpcArgs.WriteRequest)

var out structs.JobTagResponse
if err := s.agent.RPC("Job.TagVersion", &rpcArgs, &out); err != nil {
return nil, err
}
return out, nil
}

func (s *HTTPServer) jobVersionUnsetTag(resp http.ResponseWriter, req *http.Request, jobID string, name string) (interface{}, error) {
rpcArgs := structs.JobApplyTagRequest{
JobID: jobID,
Name: name,
}

// parseWriteRequest overrides Namespace, Region and AuthToken
// based on values from the original http request
s.parseWriteRequest(req, &rpcArgs.WriteRequest)

var out structs.JobTagResponse
if err := s.agent.RPC("Job.TagVersion", &rpcArgs, &out); err != nil {
return nil, err
}
return out, nil
}

func (s *HTTPServer) jobSubmissionCRUD(resp http.ResponseWriter, req *http.Request, jobID string) (*structs.JobSubmission, error) {
version, err := strconv.ParseUint(req.URL.Query().Get("version"), 10, 64)
if err != nil {
Expand Down Expand Up @@ -684,6 +748,9 @@ func (s *HTTPServer) jobScaleAction(resp http.ResponseWriter, req *http.Request,
func (s *HTTPServer) jobVersions(resp http.ResponseWriter, req *http.Request, jobID string) (interface{}, error) {

diffsStr := req.URL.Query().Get("diffs")
diffTagName := req.URL.Query().Get("diff_tag")
diffVersion := req.URL.Query().Get("diff_version")

var diffsBool bool
if diffsStr != "" {
var err error
Expand All @@ -693,9 +760,21 @@ func (s *HTTPServer) jobVersions(resp http.ResponseWriter, req *http.Request, jo
}
}

var diffVersionInt *uint64

if diffVersion != "" {
parsedDiffVersion, err := strconv.ParseUint(diffVersion, 10, 64)
if err != nil {
return nil, fmt.Errorf("Failed to parse value of %q (%v) as a uint64: %v", "diff_version", diffVersion, err)
}
diffVersionInt = &parsedDiffVersion
}

args := structs.JobVersionsRequest{
JobID: jobID,
Diffs: diffsBool,
JobID: jobID,
Diffs: diffsBool,
DiffVersion: diffVersionInt,
DiffTagName: diffTagName,
}
if s.parse(resp, req, &args.Region, &args.QueryOptions) {
return nil, nil
Expand Down Expand Up @@ -1034,6 +1113,7 @@ func ApiJobToStructJob(job *api.Job) *structs.Job {
Constraints: ApiConstraintsToStructs(job.Constraints),
Affinities: ApiAffinitiesToStructs(job.Affinities),
UI: ApiJobUIConfigToStructs(job.UI),
VersionTag: ApiJobVersionTagToStructs(job.VersionTag),
}

// Update has been pushed into the task groups. stagger and max_parallel are
Expand Down Expand Up @@ -2138,6 +2218,18 @@ func ApiJobUIConfigToStructs(jobUI *api.JobUIConfig) *structs.JobUIConfig {
}
}

func ApiJobVersionTagToStructs(jobVersionTag *api.JobVersionTag) *structs.JobVersionTag {
if jobVersionTag == nil {
return nil
}

return &structs.JobVersionTag{
Name: jobVersionTag.Name,
Description: jobVersionTag.Description,
TaggedTime: jobVersionTag.TaggedTime,
}
}

func ApiAffinityToStructs(a1 *api.Affinity) *structs.Affinity {
return &structs.Affinity{
LTarget: a1.LTarget,
Expand Down
32 changes: 32 additions & 0 deletions command/agent/job_endpoint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4448,3 +4448,35 @@ func TestConversion_ApiJobUIConfigToStructs(t *testing.T) {
must.Eq(t, expected, result)
})
}

func TestConversion_ApiJobVersionTagToStructs(t *testing.T) {
t.Run("nil tagged version", func(t *testing.T) {
must.Nil(t, ApiJobVersionTagToStructs(nil))
})

t.Run("empty tagged version", func(t *testing.T) {
versionTag := &api.JobVersionTag{}
expected := &structs.JobVersionTag{
Name: "",
Description: "",
TaggedTime: 0,
}
result := ApiJobVersionTagToStructs(versionTag)
must.Eq(t, expected, result)
})

t.Run("tagged version with tag and version", func(t *testing.T) {
versionTag := &api.JobVersionTag{
Name: "low-latency",
Description: "Low latency version",
TaggedTime: 1234567890,
}
expected := &structs.JobVersionTag{
Name: "low-latency",
Description: "Low latency version",
TaggedTime: 1234567890,
}
result := ApiJobVersionTagToStructs(versionTag)
must.Eq(t, expected, result)
})
}
15 changes: 15 additions & 0 deletions command/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,21 @@ func Commands(metaPtr *Meta, agentUi cli.Ui) map[string]cli.CommandFactory {
Meta: meta,
}, nil
},
"job tag": func() (cli.Command, error) {
return &JobTagCommand{
Meta: meta,
}, nil
},
"job tag apply": func() (cli.Command, error) {
return &JobTagApplyCommand{
Meta: meta,
}, nil
},
"job tag unset": func() (cli.Command, error) {
return &JobTagUnsetCommand{
Meta: meta,
}, nil
},
"job validate": func() (cli.Command, error) {
return &JobValidateCommand{
Meta: meta,
Expand Down
Loading

0 comments on commit e206993

Please sign in to comment.