-
Notifications
You must be signed in to change notification settings - Fork 1.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Tag and Untag job versions #23863
base: epic/23794-golden-versions
Are you sure you want to change the base?
Tag and Untag job versions #23863
Conversation
Ember Test Audit comparison
|
86153aa
to
233f051
Compare
a3b6b5e
to
bb9ef81
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
looks pretty good! few comments for ye
s.parseWriteRequest(req, &rpcArgs.WriteRequest) | ||
|
||
var out structs.JobTagResponse | ||
if err := s.agent.RPC("Job.TagVersion", &rpcArgs, &out); err != nil { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
these two httpserver methods are almost identical except for the RPC that they call, so you could save a decent little bit of duplicate code by moving the logic into the main jobTagVersion
method instead.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I ended up splitting these up a bit more — the differences started compounding (unset
doesn't get version
passed in now that we go by tag name; they both get their own structs for req/resp, etc.)
Not opposed to having a single method to handle these but they look to me like they should be separated currently
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(This did in fact end up being a fence I straddled and continue to straddle — see "request for reviewers" code comment)
// If the version is not provided, get the "active" version of the job | ||
// TODO: Request for reviewers: is this the right place to do this? I'd given some thought to doing it at state store level instead. | ||
// And if this is the right place: does this invalidate my need for JobApplyTagRequest.Version to be a pointer / it could just be a uint64? | ||
if version == "" { | ||
job, _, err := j.Info(jobID, nil) | ||
if err != nil { | ||
return nil, err | ||
} | ||
version = strconv.FormatUint(*job.Version, 10) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, if you require the argument in the request, then the JobApplyTagRequest.Version
can just be uint64 and not *uint64
. The only thing that you'd need to do to make that work with your untag logic in the state store is to ignore Version
unless .Name == nil
. Generally I think this would clean up a bunch of annoying logic and checking along the way, so that sounds like a good idea. That would be roughly in line with how arguments are handled most other places in the code base.
Either way I would definitely require this function take in a uint64 though. Consider that callers outside the CLI (ex. the Terraform provider) will likely already have a uint64 from the Job
object so forcing them to use a string here just makes for unnecessary conversions. Use the type system to enforce correctness here rather than parsing.
case strings.HasSuffix(path, "/tag"): | ||
parts := strings.Split(path, "/") | ||
jobID := parts[0] | ||
name := parts[2] // job/<jobID>/tag/<name> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You need a length check here, otherwise job/:jobid/tag
will panic rather than 404.
@@ -2391,3 +2398,19 @@ func (j *Job) GetServiceRegistrations( | |||
}, | |||
}) | |||
} | |||
|
|||
func (j *Job) TagVersion(args *structs.JobApplyTagRequest, reply *structs.JobTagResponse) error { | |||
// Apply time to the tag if it isn't null |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nitpick: this kind of comment doesn't really add anything to the code
@@ -2391,3 +2398,19 @@ func (j *Job) GetServiceRegistrations( | |||
}, | |||
}) | |||
} | |||
|
|||
func (j *Job) TagVersion(args *structs.JobApplyTagRequest, reply *structs.JobTagResponse) error { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you just haven't implemented it yet, but don't forget you've still got to implement auth, metrics, and RPC forwarding at the top of this handler.
} | ||
} | ||
|
||
// TODO: this is a copy of the function in command/job_history.go; any way to import it here? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That'd be a circular reference as command
imports command/agent
. You could make this function exported and then have command
call it as agent.ParseVersion
, but this code seems tiny enough that it's not worth bothering.
// TODO: Request for reviewers: I am splitting by method here, but you'll notice both | ||
// methods now call the same RPC method, which later splits them based on presence of args.Tag. | ||
// So far the benefit of a method-based split here is that I can more easily create a slimmed-down | ||
// TagVersionRequest for the RPC method, but I'm not sure if that's enough to justify the split. | ||
// ...is this the best and most obvious place to set the .Tag or not? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These HTTP endpoints are definitely the best place to set the .Tag
, because this is the first point where we go from 2 APIs (PUT and DELETE) to a single API (the RPC handler).
Splitting the work into two methods seems fine to me. But if you didn't want to do that you could have each case here construct the args and then have the RPC call at the end of this jobTagVersion
function after the switch statement. It makes it a little easier to read and to write tight little tests if you keep it the way you've got it here IMO, but it's a matter of taste.
|
||
Example usage: | ||
|
||
nomad job tag apply -name "My Golden Version" -description "The version of the job we can roll back to in the future if needed" <jobname> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wrap command line help text at 80 cols if you can. For an example I'd add a \
to move the description into the line below (or just shorten the text)
for _, version := range versions { | ||
// Allow for a tag to be updated (new description, for example) but otherwise don't allow a same-tagname to a different version. | ||
if version.TaggedVersion != nil && version.TaggedVersion.Name == tag.Name && version.Version != *jobVersion { | ||
duplicateVersionName = true |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's no more useful work to do at this point. So rather than setting a bool you check later, you can just bail out early and return the error here.
return fmt.Errorf("job %q version %d not found", jobID, *jobVersion) | ||
} | ||
|
||
copy := job.Copy() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
copy
is a Go built-in function, which you're shadowing here. That's not wrong but we try to avoid it because it looks funny.
versions, err := s.JobVersionsByID(ws, namespace, jobID) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
var job *structs.Job | ||
|
||
for _, version := range versions { | ||
if version.TaggedVersion != nil && version.TaggedVersion.Name == name { | ||
job = version | ||
break | ||
} | ||
} | ||
|
||
if job == nil { | ||
return fmt.Errorf("tag %q not found on job %q", name, jobID) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't this JobVersionByTagName
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is, thanks for catching. I think I copied over the updateTag
code here instead because it does duplicate-name checking (and this won't have to)
via
curl
:Adds a tag to Version 3:
Removes the tag:
via Nomad CLI:
Adds a tag to Version 0:
Adds a tag to the latest/current version:
Removes a tag: