diff --git a/api/jobs.go b/api/jobs.go index a39fadd053b6..c188aa48d2ec 100644 --- a/api/jobs.go +++ b/api/jobs.go @@ -8,6 +8,7 @@ import ( "errors" "fmt" "io" + "log" "maps" "net/url" "sort" @@ -1643,3 +1644,31 @@ type JobStatusesRequest struct { // IncludeChildren will include child (batch) jobs in the response. IncludeChildren bool } + +// #region TaggedVersions + +// Function TagVersion to apply a JobTaggedVersion to a Job Version (which itself is a job) +// by POSTing to the /v1/job/:job_id/versions/:version/tag endpoint. + +type TagVersionRequest struct { + JobID string + Version string + Tag *JobTaggedVersion + WriteRequest +} + +func (j *Jobs) TagVersion(jobID string, version string, name string, description string, q *WriteOptions) (*WriteMeta, error) { + var tagRequest = &TagVersionRequest{ + JobID: jobID, + Version: version, + Tag: &JobTaggedVersion{ + Name: name, + Description: description, + }, + } + + log.Printf("TagVersionRequest: %+v ", tagRequest) + return j.client.put("/v1/job/"+url.PathEscape(jobID)+"/versions/"+version+"/tag", tagRequest, nil, q) +} + +// #endregion TaggedVersions diff --git a/command/agent/job_endpoint.go b/command/agent/job_endpoint.go index 7dec748e34b0..27bfe8d0a2e0 100644 --- a/command/agent/job_endpoint.go +++ b/command/agent/job_endpoint.go @@ -118,6 +118,18 @@ 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"): + // jobID := strings.TrimSuffix(path, "/tag") + // TrimSuffix isn't right, the route is actually /job/:job_id/versions/:version/tag + // So we need to split the path and get the jobID from the path + // Log that this method has been hit + jobID := strings.Split(path, "/")[0] + s.logger.Debug("=====================================") + s.logger.Debug("path to /tag hit, path: ", path) + s.logger.Debug("method: ", req.Method) + s.logger.Debug("jobID: ", jobID) + + return s.jobTagVersion(resp, req, jobID) default: return s.jobCRUD(resp, req, path) } @@ -401,6 +413,109 @@ func (s *HTTPServer) jobRunAction(resp http.ResponseWriter, req *http.Request, j return s.execStream(conn, &args) } +// jobTagVersion +func (s *HTTPServer) jobTagVersion(resp http.ResponseWriter, req *http.Request, jobID string) (interface{}, error) { + // Debug log that this method has been hit + + s.logger.Debug("+++++++++++++++++++++++++++++++++++++++++++++++") + s.logger.Debug("jobTagVersion method hit") + s.logger.Debug("req.Method: ", req.Method) + s.logger.Debug("jobID: ", jobID) + // s.logger.Debug("args: ", args) + s.logger.Debug("+++++++++++++++++++++++++++++++++++++++++++++++") + + // if err := decodeBody(req, &args); err != nil { + // return nil, CodedError(400, err.Error()) + // } + // if args.JobID == "" { + // return nil, CodedError(400, "Job must be specified") + // } + // TODO: check for Version and Name too + + // var args api.JobTagRequest + // if err := decodeBody(req, &args); err != nil { + // return nil, CodedError(400, err.Error()) + // } + + switch req.Method { + case http.MethodPut, http.MethodPost: + return s.jobVersionApplyTag(resp, req, jobID) + case http.MethodDelete: + return s.jobVersionUnsetTag(resp, req, jobID) + default: + return nil, CodedError(405, ErrInvalidMethod) + } + +} + +func (s *HTTPServer) jobVersionApplyTag(resp http.ResponseWriter, req *http.Request, jobID string) (interface{}, error) { + var args api.TagVersionRequest + // Decode req body + if err := decodeBody(req, &args); err != nil { + return nil, CodedError(400, err.Error()) + } + + s.logger.Debug("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx") + s.logger.Debug("jobVersionApplyTag method hit") + s.logger.Debug("jobID: ", jobID) + s.logger.Debug("req: ", req) + s.logger.Debug("args: ", args) + s.logger.Debug("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx") + + // var args structs.JobTagRequest + // var args convertToStructsStruct(api.JobTagRequest) //TODO: + // var args api.TagVersionRequest + // rpcArgs := ApiJobTaggedVersionToStructs(args) + rpcArgs := APIJobTagRequestToStructs(&args) + + // parseWriteRequest overrides Namespace, Region and AuthToken + // based on values from the original http request + s.parseWriteRequest(req, &rpcArgs.WriteRequest) + + s.logger.Debug("OK Ive now parsed write requests, what can we know?", rpcArgs.WriteRequest.Region) + + // if err := decodeBody(req, &args); err != nil { + // return nil, CodedError(400, err.Error()) + // } + + // if args.Name == "" { + // return nil, CodedError(400, "Name must be specified") + // } + + 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) (interface{}, error) { + var args api.TagVersionRequest + // Decode req body + if err := decodeBody(req, &args); err != nil { + return nil, CodedError(400, err.Error()) + } + + s.logger.Debug("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx") + s.logger.Debug("jobVersionUnsetTag method hit") + s.logger.Debug("jobID: ", jobID) + s.logger.Debug("req: ", req) + // s.logger.Debug("args: ", args) + s.logger.Debug("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx") + + rpcArgs := APIJobTagRequestToStructs(&args) + + // 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.UntagVersion", &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 { @@ -2158,6 +2273,25 @@ func ApiJobTaggedVersionToStructs(jobTaggedVersion *api.JobTaggedVersion) *struc } } +func APIJobTagRequestToStructs(jobTagRequest *api.TagVersionRequest) *structs.JobTagRequest { + if jobTagRequest == nil { + return nil + } + versionNumber, err := strconv.ParseUint(jobTagRequest.Version, 10, 64) + if err != nil { + // handle the error + } + return &structs.JobTagRequest{ + JobID: jobTagRequest.JobID, + Version: versionNumber, + Tag: ApiJobTaggedVersionToStructs(jobTagRequest.Tag), + // WriteRequest: structs.WriteRequest{ + // Region: jobTagRequest.Region, + // Namespace: jobTagRequest.Namespace, + // }, + } +} + func ApiAffinityToStructs(a1 *api.Affinity) *structs.Affinity { return &structs.Affinity{ LTarget: a1.LTarget, diff --git a/command/commands.go b/command/commands.go index 7a46c08d4844..e12805f122c2 100644 --- a/command/commands.go +++ b/command/commands.go @@ -521,6 +521,11 @@ 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 validate": func() (cli.Command, error) { return &JobValidateCommand{ Meta: meta, diff --git a/command/job_tag.go b/command/job_tag.go new file mode 100644 index 000000000000..c0477af3c102 --- /dev/null +++ b/command/job_tag.go @@ -0,0 +1,378 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package command + +import ( + "fmt" + "io" + "strings" + + "github.com/posener/complete" +) + +type JobTagCommand struct { + Meta + + Stdin io.Reader + Stdout io.WriteCloser + Stderr io.WriteCloser +} + +func (c *JobTagCommand) Help() string { + helpText := ` +Usage: nomad job tag [options] + + Save a job version to prevent it from being garbage-collected and allow it to + be diffed and reverted by name. + + Example usage: + + nomad job tag -name "My Golden Version" -description "The version of the job we can roll back to in the future if needed" + + nomad job tag -version 3 -name "My Golden Version" + + The first of the above will tag the latest version of the job, while the second + will specifically tag version 3 of the job. + +Tag Specific Options: + + -name + Specifies the name of the version to tag. This is a required field. + + -description + Specifies a description for the version. This is an optional field. + + -version + Specifies the version of the job to tag. If not provided, the latest version + of the job will be tagged. + + +General Options: + + ` + generalOptionsUsage(usageOptsNoNamespace) + ` +` + return strings.TrimSpace(helpText) +} + +func (c *JobTagCommand) Synopsis() string { + return "Save a job version to prevent it from being garbage-collected and allow it to be diffed and reverted by name." +} + +func (c *JobTagCommand) AutocompleteFlags() complete.Flags { + return mergeAutocompleteFlags(c.Meta.AutocompleteFlags(FlagSetClient), + complete.Flags{ + "-name": complete.PredictAnything, + "-description": complete.PredictAnything, + "-version": complete.PredictNothing, + }) +} + +func (c *JobTagCommand) AutocompleteArgs() complete.Predictor { + return complete.PredictNothing +} + +func (c *JobTagCommand) Name() string { return "job tag" } + +func (c *JobTagCommand) Run(args []string) int { + var name, description, versionStr string + + flags := c.Meta.FlagSet(c.Name(), FlagSetClient) + flags.Usage = func() { c.Ui.Output(c.Help()) } // TODO: what's this do? + flags.StringVar(&name, "name", "", "") + flags.StringVar(&description, "description", "", "") + flags.StringVar(&versionStr, "version", "", "") + + if err := flags.Parse(args); err != nil { + return 1 + } + + if len(flags.Args()) != 1 { + c.Ui.Error("This command takes one argument: ") + c.Ui.Error(commandErrorText(c)) + return 1 + } + + var job = flags.Args()[0] + + // Debugging: log out the name, description, versionStr, and args + fmt.Println("name: ", name) + fmt.Println("description: ", description) + fmt.Println("versionStr: ", versionStr) + fmt.Println("job: ", job) + fmt.Println("args: ", args) + + if job == "" { + c.Ui.Error("A job name is required") + c.Ui.Error(commandErrorText(c)) + return 1 + } + + if name == "" { + c.Ui.Error("A version name is required") + c.Ui.Error(commandErrorText(c)) + return 1 + } + + // // var versionID uint64 + // // if versionStr != "" { + // // TODO: handle when versionStr is empty, implying latest version should be tagged. + // // (Perhaps at API layer?) + // versionID, _, err := parseVersion(versionStr) + // if err != nil { + // c.Ui.Error(fmt.Sprintf("Error parsing version value %q: %v", versionStr, err)) + // return 1 + // } + // // } + + // Get the HTTP client + client, err := c.Meta.Client() + if err != nil { + c.Ui.Error(fmt.Sprintf("Error initializing client: %s", err)) + return 1 + } + + // Check if the job exists + jobIDPrefix := strings.TrimSpace(job) + jobID, namespace, err := c.JobIDByPrefix(client, jobIDPrefix, nil) + if err != nil { + c.Ui.Error(err.Error()) + return 1 + } + + // Log out the jobID, namespace, and err + fmt.Println("jobID: ", jobID) + fmt.Println("namespace: ", namespace) + fmt.Println("err: ", err) + + // Call API's Jobs.TagVersion + // Which has a signature like this: + // func (j *Jobs) TagVersion(jobID string, version uint64, tag *JobTaggedVersion, q *WriteOptions) (*WriteMeta, error) { + _, err = client.Jobs().TagVersion(jobID, versionStr, name, description, nil) // TODO: writeoptions nil??? + if err != nil { + c.Ui.Error(fmt.Sprintf("Error tagging job version: %s", err)) + return 1 + } + + // TODO: This stuff should all generally be implemented by routing through the API, and eventually be fun in something like nomad/job_endpoint.go + // ============== + // q := &api.QueryOptions{Namespace: namespace} + + // // Prefix lookup matched a single job + // versions, _, _, err := client.Jobs().Versions(jobID, false, q) + // if err != nil { + // c.Ui.Error(fmt.Sprintf("Error retrieving job versions: %s", err)) + // return 1 + // } + + // // Check to see if the version provided exists among versions + // if versionStr != "" { + // versionID, _, err := parseVersion(versionStr) + // if err != nil { + // c.Ui.Error(fmt.Sprintf("Error parsing version value %q: %v", versionStr, err)) + // return 1 + // } + + // var versionObject *api.Job + // for _, v := range versions { + // if *v.Version != versionID { + // // log that it's not this one + // fmt.Println("not version: ", versionID) + // continue + // } + + // // log that it is this one + // fmt.Println("version to tag has been found: ", versionID) + + // versionObject = v + // versionObject.TaggedVersion = &api.JobTaggedVersion{ + // Name: name, + // Description: description, + // TaggedTime: time.Now().Unix(), // TODO: nanos or millis? + // } + + // // // Do some server logs + // // c.Ui.Output(fmt.Sprintf("Tagged version %d of %e with name %q", versionID, jobID, name)) + + // // // Do I need to do something like versionObject (which is a job) .update()? + // // // Am I updating the wrong thing by updating the api object, and I need to do a struct conversion? + + // // // Handle if the version is not found + // // if versionObject == nil { + // // c.Ui.Error(fmt.Sprintf("Version %d not found", versionID)) + // // return 1 + // // } + + // // // Tag the version + // // // Log the versionObject's TaggedVersion and versionObject + // // fmt.Println("versionObject.TaggedVersion: ", versionObject.TaggedVersion) + // // fmt.Println("versionObject: ", versionObject) + // } + // } else { + // // Tag the latest + // panic("not implemented") + // } + + return 0 + + // First of all, accept the flags + // Then, check if the job name is provided and exists. + // If it doesn't exist, return an error + + // Then, check if the version is provided + // Then, check if the name is provided + // Then, check if the description is provided + // Then, tag the job + // Finally, return the result + +} + +// func (c *JobTagCommand) Run(args []string) int { + +// var stdinOpt, ttyOpt bool +// var task, allocation, job, group, escapeChar string + +// flags := c.Meta.FlagSet(c.Name(), FlagSetClient) +// flags.Usage = func() { c.Ui.Output(c.Help()) } +// flags.StringVar(&task, "task", "", "") +// flags.StringVar(&group, "group", "", "") +// flags.StringVar(&allocation, "alloc", "", "") +// flags.StringVar(&job, "job", "", "") +// flags.BoolVar(&stdinOpt, "i", true, "") +// flags.BoolVar(&ttyOpt, "t", isTty(), "") +// flags.StringVar(&escapeChar, "e", "~", "") + +// if err := flags.Parse(args); err != nil { +// c.Ui.Error(fmt.Sprintf("Error parsing flags: %s", err)) +// return 1 +// } + +// args = flags.Args() + +// if len(args) < 1 { +// c.Ui.Error("An action name is required") +// c.Ui.Error(commandErrorText(c)) +// return 1 +// } + +// if job == "" { +// c.Ui.Error("A job ID is required") +// c.Ui.Error(commandErrorText(c)) +// return 1 +// } + +// if ttyOpt && !stdinOpt { +// c.Ui.Error("-i must be enabled if running with tty") +// c.Ui.Error(commandErrorText(c)) +// return 1 +// } + +// if escapeChar == "none" { +// escapeChar = "" +// } + +// if len(escapeChar) > 1 { +// c.Ui.Error("-e requires 'none' or a single character") +// c.Ui.Error(commandErrorText(c)) +// return 1 +// } + +// client, err := c.Meta.Client() +// if err != nil { +// c.Ui.Error(fmt.Sprintf("Error initializing client: %v", err)) +// return 1 +// } + +// var allocStub *api.AllocationListStub +// // If no allocation provided, grab a random one from the job +// if allocation == "" { + +// // Group param cannot be empty if allocation is empty, +// // since we'll need to get a random allocation from the group +// if group == "" { +// c.Ui.Error("A group name is required if no allocation is provided") +// c.Ui.Error(commandErrorText(c)) +// return 1 +// } + +// if task == "" { +// c.Ui.Error("A task name is required if no allocation is provided") +// c.Ui.Error(commandErrorText(c)) +// return 1 +// } + +// jobID, ns, err := c.JobIDByPrefix(client, job, nil) +// if err != nil { +// c.Ui.Error(err.Error()) +// return 1 +// } + +// allocStub, err = getRandomJobAlloc(client, jobID, group, ns) +// if err != nil { +// c.Ui.Error(fmt.Sprintf("Error fetching allocations: %v", err)) +// return 1 +// } +// } else { +// allocs, _, err := client.Allocations().PrefixList(sanitizeUUIDPrefix(allocation)) +// if err != nil { +// c.Ui.Error(fmt.Sprintf("Error querying allocation: %v", err)) +// return 1 +// } + +// if len(allocs) == 0 { +// c.Ui.Error(fmt.Sprintf("No allocation(s) with prefix or id %q found", allocation)) +// return 1 +// } + +// if len(allocs) > 1 { +// out := formatAllocListStubs(allocs, false, shortId) +// c.Ui.Error(fmt.Sprintf("Prefix matched multiple allocations\n\n%s", out)) +// return 1 +// } + +// allocStub = allocs[0] +// } + +// q := &api.QueryOptions{Namespace: allocStub.Namespace} +// alloc, _, err := client.Allocations().Info(allocStub.ID, q) +// if err != nil { +// c.Ui.Error(fmt.Sprintf("Error querying allocation: %s", err)) +// return 1 +// } + +// if task != "" { +// err = validateTaskExistsInAllocation(task, alloc) +// } else { +// task, err = lookupAllocTask(alloc) +// } +// if err != nil { +// c.Ui.Error(err.Error()) +// return 1 +// } + +// if !stdinOpt { +// c.Stdin = bytes.NewReader(nil) +// } + +// if c.Stdin == nil { +// c.Stdin = os.Stdin +// } + +// if c.Stdout == nil { +// c.Stdout = os.Stdout +// } + +// if c.Stderr == nil { +// c.Stderr = os.Stderr +// } + +// action := args[0] + +// code, err := c.execImpl(client, alloc, task, job, action, ttyOpt, escapeChar, c.Stdin, c.Stdout, c.Stderr) +// if err != nil { +// c.Ui.Error(fmt.Sprintf("failed to exec into task: %v", err)) +// return 1 +// } + +// return code +// } diff --git a/nomad/fsm.go b/nomad/fsm.go index 3e7d99d7c86f..35f384f2bd11 100644 --- a/nomad/fsm.go +++ b/nomad/fsm.go @@ -385,6 +385,9 @@ func (n *nomadFSM) Apply(log *raft.Log) interface{} { return n.applyACLBindingRulesUpsert(buf[1:], log.Index) case structs.ACLBindingRulesDeleteRequestType: return n.applyACLBindingRulesDelete(buf[1:], log.Index) + case structs.JobVersionTagRequestType: + return n.applyJobVersionTag(buf[1:], log.Index) + // return n.applyUpsertJob(msgType, buf[1:], log.Index) // TODO: Does this make sense for version tagging, or should I make a new fsm method? } // Check enterprise only message types. @@ -1178,6 +1181,23 @@ func (n *nomadFSM) applyDeploymentDelete(buf []byte, index uint64) interface{} { return nil } +// Version Tag shenanigans +// TODO: consider a "Put job in state, no side-effects" method instead of this +func (n *nomadFSM) applyJobVersionTag(buf []byte, index uint64) interface{} { + defer metrics.MeasureSince([]string{"nomad", "fsm", "apply_job_version_tag"}, time.Now()) + var req structs.JobTagRequest + if err := structs.Decode(buf, &req); err != nil { + panic(fmt.Errorf("failed to decode request: %v", err)) + } + + if err := n.state.UpdateJobVersionTag(index, req.QueryOptions.Namespace, req.JobID, req.Version, req.Tag); err != nil { + n.logger.Error("UpdateJobVersionTag failed", "error", err) + return err + } + + return nil +} + // applyJobStability is used to set the stability of a job func (n *nomadFSM) applyJobStability(buf []byte, index uint64) interface{} { defer metrics.MeasureSince([]string{"nomad", "fsm", "apply_job_stability"}, time.Now()) diff --git a/nomad/job_endpoint.go b/nomad/job_endpoint.go index ec5258c733b4..5b41caf1c770 100644 --- a/nomad/job_endpoint.go +++ b/nomad/job_endpoint.go @@ -2391,3 +2391,32 @@ func (j *Job) GetServiceRegistrations( }, }) } + +// TagVersion +func (j *Job) TagVersion(args *structs.JobTagRequest, reply *structs.JobTagResponse) error { + // Log things out first + j.logger.Debug("TagVersion at server hit", "args", args) + + _, index, err := j.srv.raftApply(structs.JobVersionTagRequestType, args) + if err != nil { + j.logger.Error("tagging version failed", "error", err) + return err + } + + reply.Index = index + return nil +} + +func (j *Job) UntagVersion(args *structs.JobTagRequest, reply *structs.JobTagResponse) error { + // Log things out first + j.logger.Debug("UntagVersion at server hit", "args", args) + + _, index, err := j.srv.raftApply(structs.JobVersionTagRequestType, args) + if err != nil { + j.logger.Error("untagging version failed", "error", err) + return err + } + + reply.Index = index + return nil +} diff --git a/nomad/state/state_store.go b/nomad/state/state_store.go index 6c715ff28ec8..405ce495af17 100644 --- a/nomad/state/state_store.go +++ b/nomad/state/state_store.go @@ -1814,6 +1814,7 @@ func (s *StateStore) upsertJobImpl(index uint64, sub *structs.JobSubmission, job return fmt.Errorf("unable to create job summary: %v", err) } + // V---- TODO: This should be inserting an index entry for the job_versions table if err := s.upsertJobVersion(index, job, txn); err != nil { return fmt.Errorf("unable to upsert job into job_version table: %v", err) } @@ -4885,6 +4886,58 @@ func (s *StateStore) updateJobStabilityImpl(index uint64, namespace, jobID strin return s.upsertJobImpl(index, nil, copy, true, txn) } +func (s *StateStore) UpdateJobVersionTag(index uint64, namespace, jobID string, jobVersion uint64, tag *structs.JobTaggedVersion) error { + txn := s.db.WriteTxn(index) + defer txn.Abort() + + if err := s.updateJobVersionTagImpl(index, namespace, jobID, jobVersion, tag, txn); err != nil { + return err + } + + // TODO: I generally want updateJobVersionTagImpl to do the same kind of stuff that updateStabilityImpl does, but when it calls upsertJobImpl, I want to make sure keepversion is TRUE + + return txn.Commit() +} + +func (s *StateStore) updateJobVersionTagImpl(index uint64, namespace, jobID string, jobVersion uint64, tag *structs.JobTaggedVersion, txn *txn) error { + ws := memdb.NewWatchSet() + + // Note: could use JobByIDAndVersion to get the specific version I want here, + // but then I'd have to make a second lookup to make sure I'm not applying a duplicate tag name + + versions, err := s.JobVersionsByID(ws, namespace, jobID) + if err != nil { + return err + } + + duplicateVersionName := false + var job *structs.Job + + 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 tag != nil && version.TaggedVersion != nil && version.TaggedVersion.Name == tag.Name && version.Version != jobVersion { + duplicateVersionName = true + break + } + if version.Version == jobVersion { + job = version + } + } + + if duplicateVersionName { + return fmt.Errorf("Tag %q already exists on a different version of job %q", tag.Name, jobID) + } + + if job == nil { + return fmt.Errorf("Job %q version %d not found", jobID, jobVersion) + // TODO: set up a structs.NewErrUnknownVersion struct in errors.go and use that instead + } + + copy := job.Copy() + copy.TaggedVersion = tag + return s.upsertJobImpl(index, nil, copy, true, txn) +} + // UpdateDeploymentPromotion is used to promote canaries in a deployment and // potentially make a evaluation func (s *StateStore) UpdateDeploymentPromotion(msgType structs.MessageType, index uint64, req *structs.ApplyDeploymentPromoteRequest) error { diff --git a/nomad/structs/structs.go b/nomad/structs/structs.go index 51744f012674..e3ca6e87f2e8 100644 --- a/nomad/structs/structs.go +++ b/nomad/structs/structs.go @@ -127,6 +127,7 @@ const ( ACLBindingRulesDeleteRequestType MessageType = 58 NodePoolUpsertRequestType MessageType = 59 NodePoolDeleteRequestType MessageType = 60 + JobVersionTagRequestType MessageType = 61 // Namespace types were moved from enterprise and therefore start at 64 NamespaceUpsertRequestType MessageType = 64 @@ -4527,6 +4528,26 @@ type JobTaggedVersion struct { TaggedTime int64 } +// TODO: Probably don't need these json: marshalers. +type JobTagRequest struct { + // JobID string // TODO: JobID and Version dont really belong here I think. They should be URL params. + // Version string + // Name string + // Description string + JobID string + Version uint64 + Tag *JobTaggedVersion + QueryOptions + WriteRequest +} + +type JobTagResponse struct { + Name string + Description string + TaggedTime int64 + QueryMeta +} + func (tv *JobTaggedVersion) Copy() *JobTaggedVersion { if tv == nil { return nil diff --git a/ui/app/models/job-version.js b/ui/app/models/job-version.js index c4a0ef5f7291..866e7e427c3c 100644 --- a/ui/app/models/job-version.js +++ b/ui/app/models/job-version.js @@ -12,6 +12,7 @@ export default class JobVersion extends Model { @attr('date') submitTime; @attr('number') number; @attr() diff; + @attr() taggedVersion; revertTo() { return this.store.adapterFor('job-version').revertTo(this); diff --git a/ui/app/templates/components/job-version.hbs b/ui/app/templates/components/job-version.hbs index 19cceffe0ca5..bedcbea72752 100644 --- a/ui/app/templates/components/job-version.hbs +++ b/ui/app/templates/components/job-version.hbs @@ -4,7 +4,17 @@ ~}}
- Version #{{this.version.number}} + {{#if this.version.taggedVersion}} +
+ + (Version #{{this.version.number}}) + {{#if this.version.taggedVersion.Description}} +

{{this.version.taggedVersion.Description}}

+ {{/if}} +
+ {{else}} + Version #{{this.version.number}} + {{/if}} Stable {{this.version.stable}} @@ -42,6 +52,11 @@ {{/if}} {{/unless}} + {{#if this.version.taggedVersion}} + + {{else}} + + {{/if}} {{#if this.version.diff}} diff --git a/ui/app/templates/jobs/job/versions.hbs b/ui/app/templates/jobs/job/versions.hbs index d41d4d6eb19b..d0ea4d6f61c3 100644 --- a/ui/app/templates/jobs/job/versions.hbs +++ b/ui/app/templates/jobs/job/versions.hbs @@ -20,5 +20,32 @@
{{/if}} + + + + + Tagged Versions + + + Untagged Versions + + + + + + + + diff --git a/website/content/docs/operations/metrics-reference.mdx b/website/content/docs/operations/metrics-reference.mdx index c71839e981d7..f0521e9eafe9 100644 --- a/website/content/docs/operations/metrics-reference.mdx +++ b/website/content/docs/operations/metrics-reference.mdx @@ -356,6 +356,7 @@ those listed in [Key Metrics](#key-metrics) above. | `nomad.nomad.fsm.apply_deployment_promotion` | Time elapsed to apply `ApplyDeploymentPromotion` raft entry | Milliseconds | Timer | host | | `nomad.nomad.fsm.apply_deployment_status_update` | Time elapsed to apply `ApplyDeploymentStatusUpdate` raft entry | Milliseconds | Timer | host | | `nomad.nomad.fsm.apply_job_stability` | Time elapsed to apply `ApplyJobStability` raft entry | Milliseconds | Timer | host | +TODO: version tag! | `nomad.nomad.fsm.apply_namespace_delete` | Time elapsed to apply `ApplyNamespaceDelete` raft entry | Milliseconds | Timer | host | | `nomad.nomad.fsm.apply_namespace_upsert` | Time elapsed to apply `ApplyNamespaceUpsert` raft entry | Milliseconds | Timer | host | | `nomad.nomad.fsm.apply_node_pool_upsert` | Time elapsed to apply `ApplyNodePoolUpsert` raft entry | Milliseconds | Timer | host |