Skip to content
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

Draft
wants to merge 8 commits into
base: epic/23794-golden-versions
Choose a base branch
from

Conversation

philrenaud
Copy link
Contributor

@philrenaud philrenaud commented Aug 25, 2024

via curl:

Adds a tag to Version 3:

curl -X PUT localhost:4646/v1/job/difftester/versions/testabc/tag -H "Content-Type: application/json" -d '{"Version": "3", "Tag": {"Name": "Foo", "Description": "Bar"}}'

Removes the tag:

curl -X DELETE localhost:4646/v1/job/difftester/versions/testabc/tag -H "Content-Type: application/json"

via Nomad CLI:

Adds a tag to Version 0:

nomad job tag apply \
  -version=0 \
  -name="Foo" \
  -description="Bar baz" \
fails_every_10

Adds a tag to the latest/current version:

nomad job tag apply -name=foo -description=bar difftester

Removes a tag:

nomad job tag unset -name=foo fails_every_10

Copy link

github-actions bot commented Aug 25, 2024

Ember Test Audit comparison

epic/23794-golden-versions b93441a change
passes 1578 1578 0
failures 0 0 0
flaky 0 0 0
duration 11m 41s 887ms 11m 37s 459ms -04s 428ms

Base automatically changed from 23836-taggedversion-object-struct to epic/23794-golden-versions August 26, 2024 03:48
Copy link
Member

@gulducat gulducat left a 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

api/jobs.go Outdated Show resolved Hide resolved
s.parseWriteRequest(req, &rpcArgs.WriteRequest)

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

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.

Copy link
Contributor Author

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

Copy link
Contributor Author

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)

command/agent/job_endpoint.go Outdated Show resolved Hide resolved
command/agent/job_endpoint.go Outdated Show resolved Hide resolved
command/job_tag.go Show resolved Hide resolved
command/job_tag.go Outdated Show resolved Hide resolved
nomad/structs/structs.go Outdated Show resolved Hide resolved
website/content/docs/operations/metrics-reference.mdx Outdated Show resolved Hide resolved
nomad/structs/structs.go Outdated Show resolved Hide resolved
nomad/state/state_store.go Outdated Show resolved Hide resolved
Comment on lines +1654 to +1663
// 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)
}
Copy link
Member

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>
Copy link
Member

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
Copy link
Member

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 {
Copy link
Member

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?
Copy link
Member

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.

Comment on lines +412 to +416
// 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?
Copy link
Member

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>
Copy link
Member

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
Copy link
Member

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()
Copy link
Member

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.

Comment on lines +4978 to +4994
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)
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this JobVersionByTagName?

Copy link
Contributor Author

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)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants