github.com/diptanu/nomad@v0.5.7-0.20170516172507-d72e86cbe3d9/command/agent/job_endpoint.go (about) 1 package agent 2 3 import ( 4 "fmt" 5 "net/http" 6 "strconv" 7 "strings" 8 9 "github.com/golang/snappy" 10 "github.com/hashicorp/nomad/api" 11 "github.com/hashicorp/nomad/nomad/structs" 12 ) 13 14 func (s *HTTPServer) JobsRequest(resp http.ResponseWriter, req *http.Request) (interface{}, error) { 15 switch req.Method { 16 case "GET": 17 return s.jobListRequest(resp, req) 18 case "PUT", "POST": 19 return s.jobUpdate(resp, req, "") 20 default: 21 return nil, CodedError(405, ErrInvalidMethod) 22 } 23 } 24 25 func (s *HTTPServer) jobListRequest(resp http.ResponseWriter, req *http.Request) (interface{}, error) { 26 args := structs.JobListRequest{} 27 if s.parse(resp, req, &args.Region, &args.QueryOptions) { 28 return nil, nil 29 } 30 31 var out structs.JobListResponse 32 if err := s.agent.RPC("Job.List", &args, &out); err != nil { 33 return nil, err 34 } 35 36 setMeta(resp, &out.QueryMeta) 37 if out.Jobs == nil { 38 out.Jobs = make([]*structs.JobListStub, 0) 39 } 40 return out.Jobs, nil 41 } 42 43 func (s *HTTPServer) JobSpecificRequest(resp http.ResponseWriter, req *http.Request) (interface{}, error) { 44 path := strings.TrimPrefix(req.URL.Path, "/v1/job/") 45 switch { 46 case strings.HasSuffix(path, "/evaluate"): 47 jobName := strings.TrimSuffix(path, "/evaluate") 48 return s.jobForceEvaluate(resp, req, jobName) 49 case strings.HasSuffix(path, "/allocations"): 50 jobName := strings.TrimSuffix(path, "/allocations") 51 return s.jobAllocations(resp, req, jobName) 52 case strings.HasSuffix(path, "/evaluations"): 53 jobName := strings.TrimSuffix(path, "/evaluations") 54 return s.jobEvaluations(resp, req, jobName) 55 case strings.HasSuffix(path, "/periodic/force"): 56 jobName := strings.TrimSuffix(path, "/periodic/force") 57 return s.periodicForceRequest(resp, req, jobName) 58 case strings.HasSuffix(path, "/plan"): 59 jobName := strings.TrimSuffix(path, "/plan") 60 return s.jobPlan(resp, req, jobName) 61 case strings.HasSuffix(path, "/summary"): 62 jobName := strings.TrimSuffix(path, "/summary") 63 return s.jobSummaryRequest(resp, req, jobName) 64 case strings.HasSuffix(path, "/dispatch"): 65 jobName := strings.TrimSuffix(path, "/dispatch") 66 return s.jobDispatchRequest(resp, req, jobName) 67 case strings.HasSuffix(path, "/versions"): 68 jobName := strings.TrimSuffix(path, "/versions") 69 return s.jobVersions(resp, req, jobName) 70 case strings.HasSuffix(path, "/revert"): 71 jobName := strings.TrimSuffix(path, "/revert") 72 return s.jobRevert(resp, req, jobName) 73 default: 74 return s.jobCRUD(resp, req, path) 75 } 76 } 77 78 func (s *HTTPServer) jobForceEvaluate(resp http.ResponseWriter, req *http.Request, 79 jobName string) (interface{}, error) { 80 if req.Method != "PUT" && req.Method != "POST" { 81 return nil, CodedError(405, ErrInvalidMethod) 82 } 83 args := structs.JobEvaluateRequest{ 84 JobID: jobName, 85 } 86 s.parseRegion(req, &args.Region) 87 88 var out structs.JobRegisterResponse 89 if err := s.agent.RPC("Job.Evaluate", &args, &out); err != nil { 90 return nil, err 91 } 92 setIndex(resp, out.Index) 93 return out, nil 94 } 95 96 func (s *HTTPServer) jobPlan(resp http.ResponseWriter, req *http.Request, 97 jobName string) (interface{}, error) { 98 if req.Method != "PUT" && req.Method != "POST" { 99 return nil, CodedError(405, ErrInvalidMethod) 100 } 101 102 var args api.JobPlanRequest 103 if err := decodeBody(req, &args); err != nil { 104 return nil, CodedError(400, err.Error()) 105 } 106 if args.Job == nil { 107 return nil, CodedError(400, "Job must be specified") 108 } 109 if args.Job.ID == nil { 110 return nil, CodedError(400, "Job must have a valid ID") 111 } 112 if jobName != "" && *args.Job.ID != jobName { 113 return nil, CodedError(400, "Job ID does not match") 114 } 115 s.parseRegion(req, &args.Region) 116 117 sJob := ApiJobToStructJob(args.Job) 118 planReq := structs.JobPlanRequest{ 119 Job: sJob, 120 Diff: args.Diff, 121 WriteRequest: structs.WriteRequest{ 122 Region: args.WriteRequest.Region, 123 }, 124 } 125 var out structs.JobPlanResponse 126 if err := s.agent.RPC("Job.Plan", &planReq, &out); err != nil { 127 return nil, err 128 } 129 setIndex(resp, out.Index) 130 return out, nil 131 } 132 133 func (s *HTTPServer) ValidateJobRequest(resp http.ResponseWriter, req *http.Request) (interface{}, error) { 134 // Ensure request method is POST or PUT 135 if !(req.Method == "POST" || req.Method == "PUT") { 136 return nil, CodedError(405, ErrInvalidMethod) 137 } 138 139 var validateRequest api.JobValidateRequest 140 if err := decodeBody(req, &validateRequest); err != nil { 141 return nil, CodedError(400, err.Error()) 142 } 143 if validateRequest.Job == nil { 144 return nil, CodedError(400, "Job must be specified") 145 } 146 147 job := ApiJobToStructJob(validateRequest.Job) 148 args := structs.JobValidateRequest{ 149 Job: job, 150 WriteRequest: structs.WriteRequest{ 151 Region: validateRequest.Region, 152 }, 153 } 154 s.parseRegion(req, &args.Region) 155 156 var out structs.JobValidateResponse 157 if err := s.agent.RPC("Job.Validate", &args, &out); err != nil { 158 return nil, err 159 } 160 161 return out, nil 162 } 163 164 func (s *HTTPServer) periodicForceRequest(resp http.ResponseWriter, req *http.Request, 165 jobName string) (interface{}, error) { 166 if req.Method != "PUT" && req.Method != "POST" { 167 return nil, CodedError(405, ErrInvalidMethod) 168 } 169 170 args := structs.PeriodicForceRequest{ 171 JobID: jobName, 172 } 173 s.parseRegion(req, &args.Region) 174 175 var out structs.PeriodicForceResponse 176 if err := s.agent.RPC("Periodic.Force", &args, &out); err != nil { 177 return nil, err 178 } 179 setIndex(resp, out.Index) 180 return out, nil 181 } 182 183 func (s *HTTPServer) jobAllocations(resp http.ResponseWriter, req *http.Request, 184 jobName string) (interface{}, error) { 185 if req.Method != "GET" { 186 return nil, CodedError(405, ErrInvalidMethod) 187 } 188 allAllocs, _ := strconv.ParseBool(req.URL.Query().Get("all")) 189 190 args := structs.JobSpecificRequest{ 191 JobID: jobName, 192 AllAllocs: allAllocs, 193 } 194 if s.parse(resp, req, &args.Region, &args.QueryOptions) { 195 return nil, nil 196 } 197 198 var out structs.JobAllocationsResponse 199 if err := s.agent.RPC("Job.Allocations", &args, &out); err != nil { 200 return nil, err 201 } 202 203 setMeta(resp, &out.QueryMeta) 204 if out.Allocations == nil { 205 out.Allocations = make([]*structs.AllocListStub, 0) 206 } 207 return out.Allocations, nil 208 } 209 210 func (s *HTTPServer) jobEvaluations(resp http.ResponseWriter, req *http.Request, 211 jobName string) (interface{}, error) { 212 if req.Method != "GET" { 213 return nil, CodedError(405, ErrInvalidMethod) 214 } 215 args := structs.JobSpecificRequest{ 216 JobID: jobName, 217 } 218 if s.parse(resp, req, &args.Region, &args.QueryOptions) { 219 return nil, nil 220 } 221 222 var out structs.JobEvaluationsResponse 223 if err := s.agent.RPC("Job.Evaluations", &args, &out); err != nil { 224 return nil, err 225 } 226 227 setMeta(resp, &out.QueryMeta) 228 if out.Evaluations == nil { 229 out.Evaluations = make([]*structs.Evaluation, 0) 230 } 231 return out.Evaluations, nil 232 } 233 234 func (s *HTTPServer) jobCRUD(resp http.ResponseWriter, req *http.Request, 235 jobName string) (interface{}, error) { 236 switch req.Method { 237 case "GET": 238 return s.jobQuery(resp, req, jobName) 239 case "PUT", "POST": 240 return s.jobUpdate(resp, req, jobName) 241 case "DELETE": 242 return s.jobDelete(resp, req, jobName) 243 default: 244 return nil, CodedError(405, ErrInvalidMethod) 245 } 246 } 247 248 func (s *HTTPServer) jobQuery(resp http.ResponseWriter, req *http.Request, 249 jobName string) (interface{}, error) { 250 args := structs.JobSpecificRequest{ 251 JobID: jobName, 252 } 253 if s.parse(resp, req, &args.Region, &args.QueryOptions) { 254 return nil, nil 255 } 256 257 var out structs.SingleJobResponse 258 if err := s.agent.RPC("Job.GetJob", &args, &out); err != nil { 259 return nil, err 260 } 261 262 setMeta(resp, &out.QueryMeta) 263 if out.Job == nil { 264 return nil, CodedError(404, "job not found") 265 } 266 267 // Decode the payload if there is any 268 job := out.Job 269 if len(job.Payload) != 0 { 270 decoded, err := snappy.Decode(nil, out.Job.Payload) 271 if err != nil { 272 return nil, err 273 } 274 job = job.Copy() 275 job.Payload = decoded 276 } 277 278 return job, nil 279 } 280 281 func (s *HTTPServer) jobUpdate(resp http.ResponseWriter, req *http.Request, 282 jobName string) (interface{}, error) { 283 var args api.JobRegisterRequest 284 if err := decodeBody(req, &args); err != nil { 285 return nil, CodedError(400, err.Error()) 286 } 287 if args.Job == nil { 288 return nil, CodedError(400, "Job must be specified") 289 } 290 291 if args.Job.ID == nil { 292 return nil, CodedError(400, "Job ID hasn't been provided") 293 } 294 if jobName != "" && *args.Job.ID != jobName { 295 return nil, CodedError(400, "Job ID does not match name") 296 } 297 s.parseRegion(req, &args.Region) 298 299 sJob := ApiJobToStructJob(args.Job) 300 301 regReq := structs.JobRegisterRequest{ 302 Job: sJob, 303 EnforceIndex: args.EnforceIndex, 304 JobModifyIndex: args.JobModifyIndex, 305 WriteRequest: structs.WriteRequest{ 306 Region: args.WriteRequest.Region, 307 }, 308 } 309 var out structs.JobRegisterResponse 310 if err := s.agent.RPC("Job.Register", ®Req, &out); err != nil { 311 return nil, err 312 } 313 setIndex(resp, out.Index) 314 return out, nil 315 } 316 317 func (s *HTTPServer) jobDelete(resp http.ResponseWriter, req *http.Request, 318 jobName string) (interface{}, error) { 319 320 purgeStr := req.URL.Query().Get("purge") 321 var purgeBool bool 322 if purgeStr != "" { 323 var err error 324 purgeBool, err = strconv.ParseBool(purgeStr) 325 if err != nil { 326 return nil, fmt.Errorf("Failed to parse value of %q (%v) as a bool: %v", "purge", purgeStr, err) 327 } 328 } 329 330 args := structs.JobDeregisterRequest{ 331 JobID: jobName, 332 Purge: purgeBool, 333 } 334 s.parseRegion(req, &args.Region) 335 336 var out structs.JobDeregisterResponse 337 if err := s.agent.RPC("Job.Deregister", &args, &out); err != nil { 338 return nil, err 339 } 340 setIndex(resp, out.Index) 341 return out, nil 342 } 343 344 func (s *HTTPServer) jobVersions(resp http.ResponseWriter, req *http.Request, 345 jobName string) (interface{}, error) { 346 args := structs.JobSpecificRequest{ 347 JobID: jobName, 348 } 349 if s.parse(resp, req, &args.Region, &args.QueryOptions) { 350 return nil, nil 351 } 352 353 var out structs.JobVersionsResponse 354 if err := s.agent.RPC("Job.GetJobVersions", &args, &out); err != nil { 355 return nil, err 356 } 357 358 setMeta(resp, &out.QueryMeta) 359 if len(out.Versions) == 0 { 360 return nil, CodedError(404, "job versions not found") 361 } 362 363 return out.Versions, nil 364 } 365 366 func (s *HTTPServer) jobRevert(resp http.ResponseWriter, req *http.Request, 367 jobName string) (interface{}, error) { 368 369 if req.Method != "PUT" && req.Method != "POST" { 370 return nil, CodedError(405, ErrInvalidMethod) 371 } 372 373 var revertRequest structs.JobRevertRequest 374 if err := decodeBody(req, &revertRequest); err != nil { 375 return nil, CodedError(400, err.Error()) 376 } 377 if revertRequest.JobID == "" { 378 return nil, CodedError(400, "JobID must be specified") 379 } 380 if revertRequest.JobID != jobName { 381 return nil, CodedError(400, "Job ID does not match") 382 } 383 384 s.parseRegion(req, &revertRequest.Region) 385 386 var out structs.JobRegisterResponse 387 if err := s.agent.RPC("Job.Revert", &revertRequest, &out); err != nil { 388 return nil, err 389 } 390 391 setMeta(resp, &out.QueryMeta) 392 return out, nil 393 } 394 395 func (s *HTTPServer) jobSummaryRequest(resp http.ResponseWriter, req *http.Request, name string) (interface{}, error) { 396 args := structs.JobSummaryRequest{ 397 JobID: name, 398 } 399 if s.parse(resp, req, &args.Region, &args.QueryOptions) { 400 return nil, nil 401 } 402 403 var out structs.JobSummaryResponse 404 if err := s.agent.RPC("Job.Summary", &args, &out); err != nil { 405 return nil, err 406 } 407 408 setMeta(resp, &out.QueryMeta) 409 if out.JobSummary == nil { 410 return nil, CodedError(404, "job not found") 411 } 412 setIndex(resp, out.Index) 413 return out.JobSummary, nil 414 } 415 416 func (s *HTTPServer) jobDispatchRequest(resp http.ResponseWriter, req *http.Request, name string) (interface{}, error) { 417 if req.Method != "PUT" && req.Method != "POST" { 418 return nil, CodedError(405, ErrInvalidMethod) 419 } 420 args := structs.JobDispatchRequest{} 421 if err := decodeBody(req, &args); err != nil { 422 return nil, CodedError(400, err.Error()) 423 } 424 if args.JobID != "" && args.JobID != name { 425 return nil, CodedError(400, "Job ID does not match") 426 } 427 if args.JobID == "" { 428 args.JobID = name 429 } 430 431 s.parseRegion(req, &args.Region) 432 433 var out structs.JobDispatchResponse 434 if err := s.agent.RPC("Job.Dispatch", &args, &out); err != nil { 435 return nil, err 436 } 437 setIndex(resp, out.Index) 438 return out, nil 439 } 440 441 func ApiJobToStructJob(job *api.Job) *structs.Job { 442 job.Canonicalize() 443 444 j := &structs.Job{ 445 Stop: *job.Stop, 446 Region: *job.Region, 447 ID: *job.ID, 448 ParentID: *job.ParentID, 449 Name: *job.Name, 450 Type: *job.Type, 451 Priority: *job.Priority, 452 AllAtOnce: *job.AllAtOnce, 453 Datacenters: job.Datacenters, 454 Payload: job.Payload, 455 Meta: job.Meta, 456 VaultToken: *job.VaultToken, 457 } 458 459 if l := len(job.Constraints); l != 0 { 460 j.Constraints = make([]*structs.Constraint, l) 461 for i, c := range job.Constraints { 462 con := &structs.Constraint{} 463 ApiConstraintToStructs(c, con) 464 j.Constraints[i] = con 465 } 466 } 467 468 if job.Update != nil { 469 j.Update = structs.UpdateStrategy{ 470 Stagger: job.Update.Stagger, 471 MaxParallel: job.Update.MaxParallel, 472 } 473 } 474 475 if job.Periodic != nil { 476 j.Periodic = &structs.PeriodicConfig{ 477 Enabled: *job.Periodic.Enabled, 478 SpecType: *job.Periodic.SpecType, 479 ProhibitOverlap: *job.Periodic.ProhibitOverlap, 480 TimeZone: *job.Periodic.TimeZone, 481 } 482 483 if job.Periodic.Spec != nil { 484 j.Periodic.Spec = *job.Periodic.Spec 485 } 486 } 487 488 if job.ParameterizedJob != nil { 489 j.ParameterizedJob = &structs.ParameterizedJobConfig{ 490 Payload: job.ParameterizedJob.Payload, 491 MetaRequired: job.ParameterizedJob.MetaRequired, 492 MetaOptional: job.ParameterizedJob.MetaOptional, 493 } 494 } 495 496 if l := len(job.TaskGroups); l != 0 { 497 j.TaskGroups = make([]*structs.TaskGroup, l) 498 for i, taskGroup := range job.TaskGroups { 499 tg := &structs.TaskGroup{} 500 ApiTgToStructsTG(taskGroup, tg) 501 j.TaskGroups[i] = tg 502 } 503 } 504 505 return j 506 } 507 508 func ApiTgToStructsTG(taskGroup *api.TaskGroup, tg *structs.TaskGroup) { 509 tg.Name = *taskGroup.Name 510 tg.Count = *taskGroup.Count 511 tg.Meta = taskGroup.Meta 512 513 if l := len(taskGroup.Constraints); l != 0 { 514 tg.Constraints = make([]*structs.Constraint, l) 515 for k, constraint := range taskGroup.Constraints { 516 c := &structs.Constraint{} 517 ApiConstraintToStructs(constraint, c) 518 tg.Constraints[k] = c 519 } 520 } 521 522 tg.RestartPolicy = &structs.RestartPolicy{ 523 Attempts: *taskGroup.RestartPolicy.Attempts, 524 Interval: *taskGroup.RestartPolicy.Interval, 525 Delay: *taskGroup.RestartPolicy.Delay, 526 Mode: *taskGroup.RestartPolicy.Mode, 527 } 528 529 tg.EphemeralDisk = &structs.EphemeralDisk{ 530 Sticky: *taskGroup.EphemeralDisk.Sticky, 531 SizeMB: *taskGroup.EphemeralDisk.SizeMB, 532 Migrate: *taskGroup.EphemeralDisk.Migrate, 533 } 534 535 if l := len(taskGroup.Tasks); l != 0 { 536 tg.Tasks = make([]*structs.Task, l) 537 for l, task := range taskGroup.Tasks { 538 t := &structs.Task{} 539 ApiTaskToStructsTask(task, t) 540 tg.Tasks[l] = t 541 } 542 } 543 } 544 545 func ApiTaskToStructsTask(apiTask *api.Task, structsTask *structs.Task) { 546 structsTask.Name = apiTask.Name 547 structsTask.Driver = apiTask.Driver 548 structsTask.User = apiTask.User 549 structsTask.Leader = apiTask.Leader 550 structsTask.Config = apiTask.Config 551 structsTask.Env = apiTask.Env 552 structsTask.Meta = apiTask.Meta 553 structsTask.KillTimeout = *apiTask.KillTimeout 554 555 if l := len(apiTask.Constraints); l != 0 { 556 structsTask.Constraints = make([]*structs.Constraint, l) 557 for i, constraint := range apiTask.Constraints { 558 c := &structs.Constraint{} 559 ApiConstraintToStructs(constraint, c) 560 structsTask.Constraints[i] = c 561 } 562 } 563 564 if l := len(apiTask.Services); l != 0 { 565 structsTask.Services = make([]*structs.Service, l) 566 for i, service := range apiTask.Services { 567 structsTask.Services[i] = &structs.Service{ 568 Name: service.Name, 569 PortLabel: service.PortLabel, 570 Tags: service.Tags, 571 } 572 573 if l := len(service.Checks); l != 0 { 574 structsTask.Services[i].Checks = make([]*structs.ServiceCheck, l) 575 for j, check := range service.Checks { 576 structsTask.Services[i].Checks[j] = &structs.ServiceCheck{ 577 Name: check.Name, 578 Type: check.Type, 579 Command: check.Command, 580 Args: check.Args, 581 Path: check.Path, 582 Protocol: check.Protocol, 583 PortLabel: check.PortLabel, 584 Interval: check.Interval, 585 Timeout: check.Timeout, 586 InitialStatus: check.InitialStatus, 587 TLSSkipVerify: check.TLSSkipVerify, 588 } 589 } 590 } 591 } 592 } 593 594 structsTask.Resources = &structs.Resources{ 595 CPU: *apiTask.Resources.CPU, 596 MemoryMB: *apiTask.Resources.MemoryMB, 597 IOPS: *apiTask.Resources.IOPS, 598 } 599 600 if l := len(apiTask.Resources.Networks); l != 0 { 601 structsTask.Resources.Networks = make([]*structs.NetworkResource, l) 602 for i, nw := range apiTask.Resources.Networks { 603 structsTask.Resources.Networks[i] = &structs.NetworkResource{ 604 CIDR: nw.CIDR, 605 IP: nw.IP, 606 MBits: *nw.MBits, 607 } 608 609 if l := len(nw.DynamicPorts); l != 0 { 610 structsTask.Resources.Networks[i].DynamicPorts = make([]structs.Port, l) 611 for j, dp := range nw.DynamicPorts { 612 structsTask.Resources.Networks[i].DynamicPorts[j] = structs.Port{ 613 Label: dp.Label, 614 Value: dp.Value, 615 } 616 } 617 } 618 619 if l := len(nw.ReservedPorts); l != 0 { 620 structsTask.Resources.Networks[i].ReservedPorts = make([]structs.Port, l) 621 for j, rp := range nw.ReservedPorts { 622 structsTask.Resources.Networks[i].ReservedPorts[j] = structs.Port{ 623 Label: rp.Label, 624 Value: rp.Value, 625 } 626 } 627 } 628 } 629 } 630 631 structsTask.LogConfig = &structs.LogConfig{ 632 MaxFiles: *apiTask.LogConfig.MaxFiles, 633 MaxFileSizeMB: *apiTask.LogConfig.MaxFileSizeMB, 634 } 635 636 if l := len(apiTask.Artifacts); l != 0 { 637 structsTask.Artifacts = make([]*structs.TaskArtifact, l) 638 for k, ta := range apiTask.Artifacts { 639 structsTask.Artifacts[k] = &structs.TaskArtifact{ 640 GetterSource: *ta.GetterSource, 641 GetterOptions: ta.GetterOptions, 642 RelativeDest: *ta.RelativeDest, 643 } 644 } 645 } 646 647 if apiTask.Vault != nil { 648 structsTask.Vault = &structs.Vault{ 649 Policies: apiTask.Vault.Policies, 650 Env: *apiTask.Vault.Env, 651 ChangeMode: *apiTask.Vault.ChangeMode, 652 ChangeSignal: *apiTask.Vault.ChangeSignal, 653 } 654 } 655 656 if l := len(apiTask.Templates); l != 0 { 657 structsTask.Templates = make([]*structs.Template, l) 658 for i, template := range apiTask.Templates { 659 structsTask.Templates[i] = &structs.Template{ 660 SourcePath: *template.SourcePath, 661 DestPath: *template.DestPath, 662 EmbeddedTmpl: *template.EmbeddedTmpl, 663 ChangeMode: *template.ChangeMode, 664 ChangeSignal: *template.ChangeSignal, 665 Splay: *template.Splay, 666 Perms: *template.Perms, 667 LeftDelim: *template.LeftDelim, 668 RightDelim: *template.RightDelim, 669 } 670 } 671 } 672 673 if apiTask.DispatchPayload != nil { 674 structsTask.DispatchPayload = &structs.DispatchPayloadConfig{ 675 File: apiTask.DispatchPayload.File, 676 } 677 } 678 } 679 680 func ApiConstraintToStructs(c1 *api.Constraint, c2 *structs.Constraint) { 681 c2.LTarget = c1.LTarget 682 c2.RTarget = c1.RTarget 683 c2.Operand = c1.Operand 684 }