Skip to content

Commit

Permalink
Job revert command and API endpoint can take a string version tag name (
Browse files Browse the repository at this point in the history
#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
  • Loading branch information
philrenaud authored Sep 25, 2024
1 parent f13273e commit cd3f47c
Show file tree
Hide file tree
Showing 4 changed files with 136 additions and 12 deletions.
17 changes: 17 additions & 0 deletions api/jobs.go
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,23 @@ func (j *Jobs) Versions(jobID string, diffs bool, q *QueryOptions) ([]*Job, []*J
return j.VersionsOpts(jobID, opts, q)
}

// VersionByTag is used to retrieve a job version by its TaggedVersion 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.TaggedVersion != nil && version.TaggedVersion.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
Expand Down
30 changes: 19 additions & 11 deletions command/job_revert.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ type JobRevertCommand struct {

func (c *JobRevertCommand) Help() string {
helpText := `
Usage: nomad job revert [options] <job> <version>
Usage: nomad job revert [options] <job> <version|tag>
Revert is used to revert a job to a prior version of the job. The available
versions to revert to can be found using "nomad job history" command.
Expand All @@ -30,6 +30,10 @@ Usage: nomad job revert [options] <job> <version>
capability is required to monitor the resulting evaluation when -detach is
not used.
If the version number is specified, the job will be reverted to the exact
version number. If a version tag is specified, the job will be reverted to
the version with the given tag.
General Options:
` + generalOptionsUsage(usageOptsDefault) + `
Expand Down Expand Up @@ -108,7 +112,7 @@ func (c *JobRevertCommand) Run(args []string) int {
// Check that we got two args
args = flags.Args()
if l := len(args); l != 2 {
c.Ui.Error("This command takes two arguments: <job> <version>")
c.Ui.Error("This command takes two arguments: <job> <version|tag>")
c.Ui.Error(commandErrorText(c))
return 1
}
Expand All @@ -132,15 +136,19 @@ func (c *JobRevertCommand) Run(args []string) int {
vaultToken = os.Getenv("VAULT_TOKEN")
}

// Parse the job version
revertVersion, ok, err := parseVersion(args[1])
if !ok {
c.Ui.Error("The job version to revert to must be specified using the -job-version flag")
return 1
}
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to parse job-version flag: %v", err))
return 1
// Parse the job version or version tag
var revertVersion uint64

parsedVersion, ok, err := parseVersion(args[1])
if ok && err == nil {
revertVersion = parsedVersion
} else {
foundTaggedVersion, _, err := client.Jobs().VersionByTag(args[0], args[1], nil)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error retrieving job versions: %s", err))
return 1
}
revertVersion = *foundTaggedVersion.Version
}

// Check if the job exists
Expand Down
96 changes: 96 additions & 0 deletions command/job_revert_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -200,3 +200,99 @@ namespace "default" {
})
}
}
func TestJobRevertCommand_VersionTag(t *testing.T) {
ci.Parallel(t)

// Start test server
srv, _, url := testServer(t, true, nil)
defer srv.Shutdown()
state := srv.Agent.Server().State()

// Create a job with multiple versions
v0 := mock.Job()
v0.ID = "test-job-revert"
v0.TaskGroups[0].Count = 1
must.NoError(t, state.UpsertJob(structs.MsgTypeTestSetup, 1000, nil, v0))

v1 := v0.Copy()
v1.TaskGroups[0].Count = 2
v1.TaggedVersion = &structs.JobTaggedVersion{
Name: "v1-tag",
Description: "Version 1 tag",
}
must.NoError(t, state.UpsertJob(structs.MsgTypeTestSetup, 1001, nil, v1))

v2 := v0.Copy()
v2.TaskGroups[0].Count = 3
must.NoError(t, state.UpsertJob(structs.MsgTypeTestSetup, 1002, nil, v2))

t.Run("Revert to version tag", func(t *testing.T) {
ui := cli.NewMockUi()
cmd := &JobRevertCommand{Meta: Meta{Ui: ui}}

code := cmd.Run([]string{"-address", url, "-detach", "test-job-revert", "v1-tag"})
must.Zero(t, code)
})

t.Run("Revert to non-existent version tag", func(t *testing.T) {
ui := cli.NewMockUi()
cmd := &JobRevertCommand{Meta: Meta{Ui: ui}}

code := cmd.Run([]string{"-address", url, "-detach", "test-job-revert", "non-existent-tag"})
must.One(t, code)
must.StrContains(t, ui.ErrorWriter.String(), "Error retrieving job versions")
must.StrContains(t, ui.ErrorWriter.String(), "tag non-existent-tag not found")
})

t.Run("Revert to version number", func(t *testing.T) {
ui := cli.NewMockUi()
cmd := &JobRevertCommand{Meta: Meta{Ui: ui}}

code := cmd.Run([]string{"-address", url, "-detach", "test-job-revert", "0"})
must.Zero(t, code)
})

t.Run("Throws errors with incorrect number of args", func(t *testing.T) {
ui := cli.NewMockUi()
cmd := &JobRevertCommand{Meta: Meta{Ui: ui}}

code := cmd.Run([]string{"-address", url, "test-job-revert", "v1-tag", "0"})
must.One(t, code)
must.StrContains(t, ui.ErrorWriter.String(), "This command takes two arguments")

code2 := cmd.Run([]string{"-address", url, "test-job-revert"})
must.One(t, code2)
must.StrContains(t, ui.ErrorWriter.String(), "This command takes two arguments")
})

t.Run("Revert to tagged version doesn't duplicate tag", func(t *testing.T) {
ui := cli.NewMockUi()
cmd := &JobRevertCommand{Meta: Meta{Ui: ui}}

// First, revert to the tagged version
code := cmd.Run([]string{"-address", url, "-detach", "test-job-revert", "v1-tag"})
must.Zero(t, code)

// Now, fetch the job versions
historyCmd := &JobHistoryCommand{Meta: Meta{Ui: ui}}
historyCode := historyCmd.Run([]string{"-address", url, "-version=4", v0.ID})
must.Zero(t, historyCode)

// Check the output for the expected version and no tag
output := ui.OutputWriter.String()
must.StrContains(t, output, "Version = 4")
must.StrNotContains(t, output, "Tag Name")
must.StrNotContains(t, output, "Tag Description")

ui.OutputWriter.Reset()

// Make sure the old version of the tag is still tagged
historyCmd = &JobHistoryCommand{Meta: Meta{Ui: ui}}
historyCode = historyCmd.Run([]string{"-address", url, "-version=1", v0.ID})
must.Zero(t, historyCode)
output = ui.OutputWriter.String()
must.StrContains(t, output, "Version = 1")
must.StrContains(t, output, "Tag Name = v1-tag")
must.StrContains(t, output, "Tag Description = Version 1 tag")
})
}
5 changes: 4 additions & 1 deletion nomad/job_endpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -631,7 +631,6 @@ func (j *Job) Revert(args *structs.JobRevertRequest, reply *structs.JobRegisterR
if err != nil {
return err
}

ws := memdb.NewWatchSet()
cur, err := snap.JobByID(ws, args.RequestNamespace(), args.JobID)
if err != nil {
Expand All @@ -656,6 +655,10 @@ func (j *Job) Revert(args *structs.JobRevertRequest, reply *structs.JobRegisterR
revJob := jobV.Copy()
revJob.VaultToken = args.VaultToken // use vault token from revert to perform (re)registration
revJob.ConsulToken = args.ConsulToken // use consul token from revert to perform (re)registration

// Clear out the TaggedVersion to prevent tag duplication
revJob.TaggedVersion = nil

reg := &structs.JobRegisterRequest{
Job: revJob,
WriteRequest: args.WriteRequest,
Expand Down

0 comments on commit cd3f47c

Please sign in to comment.