Skip to content

Commit

Permalink
Tag and Untag job versions (#23863)
Browse files Browse the repository at this point in the history
* 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 <dbennett@hashicorp.com>

---------

Co-authored-by: Daniel Bennett <dbennett@hashicorp.com>

* [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 <dbennett@hashicorp.com>
  • Loading branch information
philrenaud and gulducat committed Sep 25, 2024
1 parent a5a02e7 commit f13273e
Show file tree
Hide file tree
Showing 33 changed files with 2,013 additions and 106 deletions.
46 changes: 45 additions & 1 deletion api/jobs.go
Original file line number Diff line number Diff line change
Expand Up @@ -264,8 +264,33 @@ 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)
}

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 @@ -1641,3 +1666,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)
}
83 changes: 81 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.JobTaggedVersion{
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
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
79 changes: 59 additions & 20 deletions command/job_history.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,18 @@ General Options:
History Options:
-p
Display the difference between each job and its predecessor.
Display the difference between each version of the job and a reference
version. The reference version can be specified using the -diff-tag or
-diff-version flags. If neither flag is set, the most recent version is used.
-diff-tag
Specifies the version of the job to compare against, referenced by
tag name (defaults to latest). Mutually exclusive with -diff-version.
This tag can be set using the "nomad job tag" command.
-diff-version
Specifies the version number of the job to compare against.
Mutually exclusive with -diff-tag.
-full
Display the full job definition for each version.
Expand All @@ -64,11 +75,13 @@ func (c *JobHistoryCommand) Synopsis() string {
func (c *JobHistoryCommand) AutocompleteFlags() complete.Flags {
return mergeAutocompleteFlags(c.Meta.AutocompleteFlags(FlagSetClient),
complete.Flags{
"-p": complete.PredictNothing,
"-full": complete.PredictNothing,
"-version": complete.PredictAnything,
"-json": complete.PredictNothing,
"-t": complete.PredictAnything,
"-p": complete.PredictNothing,
"-full": complete.PredictNothing,
"-version": complete.PredictAnything,
"-json": complete.PredictNothing,
"-t": complete.PredictAnything,
"-diff-tag": complete.PredictNothing,
"-diff-version": complete.PredictNothing,
})
}

Expand All @@ -91,7 +104,8 @@ func (c *JobHistoryCommand) Name() string { return "job history" }

func (c *JobHistoryCommand) Run(args []string) int {
var json, diff, full bool
var tmpl, versionStr string
var tmpl, versionStr, diffTag, diffVersionFlag string
var diffVersion *uint64

flags := c.Meta.FlagSet(c.Name(), FlagSetClient)
flags.Usage = func() { c.Ui.Output(c.Help()) }
Expand All @@ -100,6 +114,8 @@ func (c *JobHistoryCommand) Run(args []string) int {
flags.BoolVar(&json, "json", false, "")
flags.StringVar(&versionStr, "version", "", "")
flags.StringVar(&tmpl, "t", "", "")
flags.StringVar(&diffTag, "diff-tag", "", "")
flags.StringVar(&diffVersionFlag, "diff-version", "", "")

if err := flags.Parse(args); err != nil {
return 1
Expand All @@ -118,6 +134,25 @@ func (c *JobHistoryCommand) Run(args []string) int {
return 1
}

if (diffTag != "" && !diff) || (diffVersionFlag != "" && !diff) {
c.Ui.Error("-diff-tag and -diff-version can only be used with -p")
return 1
}

if diffTag != "" && diffVersionFlag != "" {
c.Ui.Error("-diff-tag and -diff-version are mutually exclusive")
return 1
}

if diffVersionFlag != "" {
parsedDiffVersion, err := strconv.ParseUint(diffVersionFlag, 10, 64)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error parsing -diff-version: %s", err))
return 1
}
diffVersion = &parsedDiffVersion
}

// Get the HTTP client
client, err := c.Meta.Client()
if err != nil {
Expand All @@ -136,7 +171,12 @@ func (c *JobHistoryCommand) Run(args []string) int {
q := &api.QueryOptions{Namespace: namespace}

// Prefix lookup matched a single job
versions, diffs, _, err := client.Jobs().Versions(jobID, diff, q)
versionOptions := &api.VersionsOptions{
Diffs: diff,
DiffTag: diffTag,
DiffVersion: diffVersion,
}
versions, diffs, _, err := client.Jobs().VersionsOpts(jobID, versionOptions, q)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error retrieving job versions: %s", err))
return 1
Expand All @@ -158,7 +198,6 @@ func (c *JobHistoryCommand) Run(args []string) int {

var job *api.Job
var diff *api.JobDiff
var nextVersion uint64
for i, v := range versions {
if *v.Version != version {
continue
Expand All @@ -167,7 +206,6 @@ func (c *JobHistoryCommand) Run(args []string) int {
job = v
if i+1 <= len(diffs) {
diff = diffs[i]
nextVersion = *versions[i+1].Version
}
}

Expand All @@ -182,7 +220,7 @@ func (c *JobHistoryCommand) Run(args []string) int {
return 0
}

if err := c.formatJobVersion(job, diff, nextVersion, full); err != nil {
if err := c.formatJobVersion(job, diff, full); err != nil {
c.Ui.Error(err.Error())
return 1
}
Expand Down Expand Up @@ -222,19 +260,14 @@ func parseVersion(input string) (uint64, bool, error) {
func (c *JobHistoryCommand) formatJobVersions(versions []*api.Job, diffs []*api.JobDiff, full bool) error {
vLen := len(versions)
dLen := len(diffs)
if dLen != 0 && vLen != dLen+1 {
return fmt.Errorf("Number of job versions %d doesn't match number of diffs %d", vLen, dLen)
}

for i, version := range versions {
var diff *api.JobDiff
var nextVersion uint64
if i+1 <= dLen {
diff = diffs[i]
nextVersion = *versions[i+1].Version
}

if err := c.formatJobVersion(version, diff, nextVersion, full); err != nil {
if err := c.formatJobVersion(version, diff, full); err != nil {
return err
}

Expand All @@ -247,7 +280,7 @@ func (c *JobHistoryCommand) formatJobVersions(versions []*api.Job, diffs []*api.
return nil
}

func (c *JobHistoryCommand) formatJobVersion(job *api.Job, diff *api.JobDiff, nextVersion uint64, full bool) error {
func (c *JobHistoryCommand) formatJobVersion(job *api.Job, diff *api.JobDiff, full bool) error {
if job == nil {
return fmt.Errorf("Error printing job history for non-existing job or job version")
}
Expand All @@ -257,9 +290,15 @@ func (c *JobHistoryCommand) formatJobVersion(job *api.Job, diff *api.JobDiff, ne
fmt.Sprintf("Stable|%v", *job.Stable),
fmt.Sprintf("Submit Date|%v", formatTime(time.Unix(0, *job.SubmitTime))),
}
// if tagged version is not nil
if job.TaggedVersion != nil {
basic = append(basic, fmt.Sprintf("Tag Name|%v", *&job.TaggedVersion.Name))
if job.TaggedVersion.Description != "" {
basic = append(basic, fmt.Sprintf("Tag Description|%v", *&job.TaggedVersion.Description))
}
}

if diff != nil {
//diffStr := fmt.Sprintf("Difference between version %d and %d:", *job.Version, nextVersion)
if diff != nil && diff.Type != "None" {
basic = append(basic, fmt.Sprintf("Diff|\n%s", strings.TrimSpace(formatJobDiff(diff, false))))
}

Expand Down
Loading

0 comments on commit f13273e

Please sign in to comment.