github.com/janma/nomad@v0.11.3/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 11 "github.com/hashicorp/nomad/api" 12 "github.com/hashicorp/nomad/helper" 13 "github.com/hashicorp/nomad/jobspec" 14 "github.com/hashicorp/nomad/nomad/structs" 15 ) 16 17 func (s *HTTPServer) JobsRequest(resp http.ResponseWriter, req *http.Request) (interface{}, error) { 18 switch req.Method { 19 case "GET": 20 return s.jobListRequest(resp, req) 21 case "PUT", "POST": 22 return s.jobUpdate(resp, req, "") 23 default: 24 return nil, CodedError(405, ErrInvalidMethod) 25 } 26 } 27 28 func (s *HTTPServer) jobListRequest(resp http.ResponseWriter, req *http.Request) (interface{}, error) { 29 args := structs.JobListRequest{} 30 if s.parse(resp, req, &args.Region, &args.QueryOptions) { 31 return nil, nil 32 } 33 34 var out structs.JobListResponse 35 if err := s.agent.RPC("Job.List", &args, &out); err != nil { 36 return nil, err 37 } 38 39 setMeta(resp, &out.QueryMeta) 40 if out.Jobs == nil { 41 out.Jobs = make([]*structs.JobListStub, 0) 42 } 43 return out.Jobs, nil 44 } 45 46 func (s *HTTPServer) JobSpecificRequest(resp http.ResponseWriter, req *http.Request) (interface{}, error) { 47 path := strings.TrimPrefix(req.URL.Path, "/v1/job/") 48 switch { 49 case strings.HasSuffix(path, "/evaluate"): 50 jobName := strings.TrimSuffix(path, "/evaluate") 51 return s.jobForceEvaluate(resp, req, jobName) 52 case strings.HasSuffix(path, "/allocations"): 53 jobName := strings.TrimSuffix(path, "/allocations") 54 return s.jobAllocations(resp, req, jobName) 55 case strings.HasSuffix(path, "/evaluations"): 56 jobName := strings.TrimSuffix(path, "/evaluations") 57 return s.jobEvaluations(resp, req, jobName) 58 case strings.HasSuffix(path, "/periodic/force"): 59 jobName := strings.TrimSuffix(path, "/periodic/force") 60 return s.periodicForceRequest(resp, req, jobName) 61 case strings.HasSuffix(path, "/plan"): 62 jobName := strings.TrimSuffix(path, "/plan") 63 return s.jobPlan(resp, req, jobName) 64 case strings.HasSuffix(path, "/summary"): 65 jobName := strings.TrimSuffix(path, "/summary") 66 return s.jobSummaryRequest(resp, req, jobName) 67 case strings.HasSuffix(path, "/dispatch"): 68 jobName := strings.TrimSuffix(path, "/dispatch") 69 return s.jobDispatchRequest(resp, req, jobName) 70 case strings.HasSuffix(path, "/versions"): 71 jobName := strings.TrimSuffix(path, "/versions") 72 return s.jobVersions(resp, req, jobName) 73 case strings.HasSuffix(path, "/revert"): 74 jobName := strings.TrimSuffix(path, "/revert") 75 return s.jobRevert(resp, req, jobName) 76 case strings.HasSuffix(path, "/deployments"): 77 jobName := strings.TrimSuffix(path, "/deployments") 78 return s.jobDeployments(resp, req, jobName) 79 case strings.HasSuffix(path, "/deployment"): 80 jobName := strings.TrimSuffix(path, "/deployment") 81 return s.jobLatestDeployment(resp, req, jobName) 82 case strings.HasSuffix(path, "/stable"): 83 jobName := strings.TrimSuffix(path, "/stable") 84 return s.jobStable(resp, req, jobName) 85 case strings.HasSuffix(path, "/scale"): 86 jobName := strings.TrimSuffix(path, "/scale") 87 return s.jobScale(resp, req, jobName) 88 default: 89 return s.jobCRUD(resp, req, path) 90 } 91 } 92 93 func (s *HTTPServer) jobForceEvaluate(resp http.ResponseWriter, req *http.Request, 94 jobName string) (interface{}, error) { 95 if req.Method != "PUT" && req.Method != "POST" { 96 return nil, CodedError(405, ErrInvalidMethod) 97 } 98 var args structs.JobEvaluateRequest 99 100 // TODO(preetha): remove in 0.9 101 // COMPAT: For backwards compatibility allow using this endpoint without a payload 102 if req.ContentLength == 0 { 103 args = structs.JobEvaluateRequest{ 104 JobID: jobName, 105 } 106 } else { 107 if err := decodeBody(req, &args); err != nil { 108 return nil, CodedError(400, err.Error()) 109 } 110 if args.JobID == "" { 111 return nil, CodedError(400, "Job ID must be specified") 112 } 113 114 if jobName != "" && args.JobID != jobName { 115 return nil, CodedError(400, "JobID not same as job name") 116 } 117 } 118 s.parseWriteRequest(req, &args.WriteRequest) 119 120 var out structs.JobRegisterResponse 121 if err := s.agent.RPC("Job.Evaluate", &args, &out); err != nil { 122 return nil, err 123 } 124 setIndex(resp, out.Index) 125 return out, nil 126 } 127 128 func (s *HTTPServer) jobPlan(resp http.ResponseWriter, req *http.Request, 129 jobName string) (interface{}, error) { 130 if req.Method != "PUT" && req.Method != "POST" { 131 return nil, CodedError(405, ErrInvalidMethod) 132 } 133 134 var args api.JobPlanRequest 135 if err := decodeBody(req, &args); err != nil { 136 return nil, CodedError(400, err.Error()) 137 } 138 if args.Job == nil { 139 return nil, CodedError(400, "Job must be specified") 140 } 141 if args.Job.ID == nil { 142 return nil, CodedError(400, "Job must have a valid ID") 143 } 144 if jobName != "" && *args.Job.ID != jobName { 145 return nil, CodedError(400, "Job ID does not match") 146 } 147 148 // Region in http request query param takes precedence over region in job hcl config 149 if args.WriteRequest.Region != "" { 150 args.Job.Region = helper.StringToPtr(args.WriteRequest.Region) 151 } 152 // If 'global' region is specified or if no region is given, 153 // default to region of the node you're submitting to 154 if args.Job.Region == nil || *args.Job.Region == "" || *args.Job.Region == api.GlobalRegion { 155 args.Job.Region = &s.agent.config.Region 156 } 157 158 sJob := ApiJobToStructJob(args.Job) 159 160 planReq := structs.JobPlanRequest{ 161 Job: sJob, 162 Diff: args.Diff, 163 PolicyOverride: args.PolicyOverride, 164 WriteRequest: structs.WriteRequest{ 165 Region: sJob.Region, 166 }, 167 } 168 // parseWriteRequest overrides Namespace, Region and AuthToken 169 // based on values from the original http request 170 s.parseWriteRequest(req, &planReq.WriteRequest) 171 planReq.Namespace = sJob.Namespace 172 173 var out structs.JobPlanResponse 174 if err := s.agent.RPC("Job.Plan", &planReq, &out); err != nil { 175 return nil, err 176 } 177 setIndex(resp, out.Index) 178 return out, nil 179 } 180 181 func (s *HTTPServer) ValidateJobRequest(resp http.ResponseWriter, req *http.Request) (interface{}, error) { 182 // Ensure request method is POST or PUT 183 if !(req.Method == "POST" || req.Method == "PUT") { 184 return nil, CodedError(405, ErrInvalidMethod) 185 } 186 187 var validateRequest api.JobValidateRequest 188 if err := decodeBody(req, &validateRequest); err != nil { 189 return nil, CodedError(400, err.Error()) 190 } 191 if validateRequest.Job == nil { 192 return nil, CodedError(400, "Job must be specified") 193 } 194 195 job := ApiJobToStructJob(validateRequest.Job) 196 197 args := structs.JobValidateRequest{ 198 Job: job, 199 WriteRequest: structs.WriteRequest{ 200 Region: validateRequest.Region, 201 }, 202 } 203 s.parseWriteRequest(req, &args.WriteRequest) 204 args.Namespace = job.Namespace 205 206 var out structs.JobValidateResponse 207 if err := s.agent.RPC("Job.Validate", &args, &out); err != nil { 208 return nil, err 209 } 210 211 return out, nil 212 } 213 214 func (s *HTTPServer) periodicForceRequest(resp http.ResponseWriter, req *http.Request, 215 jobName string) (interface{}, error) { 216 if req.Method != "PUT" && req.Method != "POST" { 217 return nil, CodedError(405, ErrInvalidMethod) 218 } 219 220 args := structs.PeriodicForceRequest{ 221 JobID: jobName, 222 } 223 s.parseWriteRequest(req, &args.WriteRequest) 224 225 var out structs.PeriodicForceResponse 226 if err := s.agent.RPC("Periodic.Force", &args, &out); err != nil { 227 return nil, err 228 } 229 setIndex(resp, out.Index) 230 return out, nil 231 } 232 233 func (s *HTTPServer) jobAllocations(resp http.ResponseWriter, req *http.Request, 234 jobName string) (interface{}, error) { 235 if req.Method != "GET" { 236 return nil, CodedError(405, ErrInvalidMethod) 237 } 238 allAllocs, _ := strconv.ParseBool(req.URL.Query().Get("all")) 239 240 args := structs.JobSpecificRequest{ 241 JobID: jobName, 242 All: allAllocs, 243 } 244 if s.parse(resp, req, &args.Region, &args.QueryOptions) { 245 return nil, nil 246 } 247 248 var out structs.JobAllocationsResponse 249 if err := s.agent.RPC("Job.Allocations", &args, &out); err != nil { 250 return nil, err 251 } 252 253 setMeta(resp, &out.QueryMeta) 254 if out.Allocations == nil { 255 out.Allocations = make([]*structs.AllocListStub, 0) 256 } 257 for _, alloc := range out.Allocations { 258 alloc.SetEventDisplayMessages() 259 } 260 return out.Allocations, nil 261 } 262 263 func (s *HTTPServer) jobEvaluations(resp http.ResponseWriter, req *http.Request, 264 jobName string) (interface{}, error) { 265 if req.Method != "GET" { 266 return nil, CodedError(405, ErrInvalidMethod) 267 } 268 args := structs.JobSpecificRequest{ 269 JobID: jobName, 270 } 271 if s.parse(resp, req, &args.Region, &args.QueryOptions) { 272 return nil, nil 273 } 274 275 var out structs.JobEvaluationsResponse 276 if err := s.agent.RPC("Job.Evaluations", &args, &out); err != nil { 277 return nil, err 278 } 279 280 setMeta(resp, &out.QueryMeta) 281 if out.Evaluations == nil { 282 out.Evaluations = make([]*structs.Evaluation, 0) 283 } 284 return out.Evaluations, nil 285 } 286 287 func (s *HTTPServer) jobDeployments(resp http.ResponseWriter, req *http.Request, 288 jobName string) (interface{}, error) { 289 if req.Method != "GET" { 290 return nil, CodedError(405, ErrInvalidMethod) 291 } 292 all, _ := strconv.ParseBool(req.URL.Query().Get("all")) 293 args := structs.JobSpecificRequest{ 294 JobID: jobName, 295 All: all, 296 } 297 if s.parse(resp, req, &args.Region, &args.QueryOptions) { 298 return nil, nil 299 } 300 301 var out structs.DeploymentListResponse 302 if err := s.agent.RPC("Job.Deployments", &args, &out); err != nil { 303 return nil, err 304 } 305 306 setMeta(resp, &out.QueryMeta) 307 if out.Deployments == nil { 308 out.Deployments = make([]*structs.Deployment, 0) 309 } 310 return out.Deployments, nil 311 } 312 313 func (s *HTTPServer) jobLatestDeployment(resp http.ResponseWriter, req *http.Request, 314 jobName string) (interface{}, error) { 315 if req.Method != "GET" { 316 return nil, CodedError(405, ErrInvalidMethod) 317 } 318 args := structs.JobSpecificRequest{ 319 JobID: jobName, 320 } 321 if s.parse(resp, req, &args.Region, &args.QueryOptions) { 322 return nil, nil 323 } 324 325 var out structs.SingleDeploymentResponse 326 if err := s.agent.RPC("Job.LatestDeployment", &args, &out); err != nil { 327 return nil, err 328 } 329 330 setMeta(resp, &out.QueryMeta) 331 return out.Deployment, nil 332 } 333 334 func (s *HTTPServer) jobCRUD(resp http.ResponseWriter, req *http.Request, 335 jobName string) (interface{}, error) { 336 switch req.Method { 337 case "GET": 338 return s.jobQuery(resp, req, jobName) 339 case "PUT", "POST": 340 return s.jobUpdate(resp, req, jobName) 341 case "DELETE": 342 return s.jobDelete(resp, req, jobName) 343 default: 344 return nil, CodedError(405, ErrInvalidMethod) 345 } 346 } 347 348 func (s *HTTPServer) jobQuery(resp http.ResponseWriter, req *http.Request, 349 jobName string) (interface{}, error) { 350 args := structs.JobSpecificRequest{ 351 JobID: jobName, 352 } 353 if s.parse(resp, req, &args.Region, &args.QueryOptions) { 354 return nil, nil 355 } 356 357 var out structs.SingleJobResponse 358 if err := s.agent.RPC("Job.GetJob", &args, &out); err != nil { 359 return nil, err 360 } 361 362 setMeta(resp, &out.QueryMeta) 363 if out.Job == nil { 364 return nil, CodedError(404, "job not found") 365 } 366 367 // Decode the payload if there is any 368 job := out.Job 369 if len(job.Payload) != 0 { 370 decoded, err := snappy.Decode(nil, out.Job.Payload) 371 if err != nil { 372 return nil, err 373 } 374 job = job.Copy() 375 job.Payload = decoded 376 } 377 378 return job, nil 379 } 380 381 func (s *HTTPServer) jobUpdate(resp http.ResponseWriter, req *http.Request, 382 jobName string) (interface{}, error) { 383 var args api.JobRegisterRequest 384 if err := decodeBody(req, &args); err != nil { 385 return nil, CodedError(400, err.Error()) 386 } 387 if args.Job == nil { 388 return nil, CodedError(400, "Job must be specified") 389 } 390 391 if args.Job.ID == nil { 392 return nil, CodedError(400, "Job ID hasn't been provided") 393 } 394 if jobName != "" && *args.Job.ID != jobName { 395 return nil, CodedError(400, "Job ID does not match name") 396 } 397 398 // Region in http request query param takes precedence over region in job hcl config 399 if args.WriteRequest.Region != "" { 400 args.Job.Region = helper.StringToPtr(args.WriteRequest.Region) 401 } 402 // If 'global' region is specified or if no region is given, 403 // default to region of the node you're submitting to 404 if args.Job.Region == nil || *args.Job.Region == "" || *args.Job.Region == api.GlobalRegion { 405 args.Job.Region = &s.agent.config.Region 406 } 407 408 sJob := ApiJobToStructJob(args.Job) 409 410 regReq := structs.JobRegisterRequest{ 411 Job: sJob, 412 EnforceIndex: args.EnforceIndex, 413 JobModifyIndex: args.JobModifyIndex, 414 PolicyOverride: args.PolicyOverride, 415 WriteRequest: structs.WriteRequest{ 416 Region: sJob.Region, 417 AuthToken: args.WriteRequest.SecretID, 418 }, 419 } 420 // parseWriteRequest overrides Namespace, Region and AuthToken 421 // based on values from the original http request 422 s.parseWriteRequest(req, ®Req.WriteRequest) 423 regReq.Namespace = sJob.Namespace 424 425 var out structs.JobRegisterResponse 426 if err := s.agent.RPC("Job.Register", ®Req, &out); err != nil { 427 return nil, err 428 } 429 setIndex(resp, out.Index) 430 return out, nil 431 } 432 433 func (s *HTTPServer) jobDelete(resp http.ResponseWriter, req *http.Request, 434 jobName string) (interface{}, error) { 435 436 purgeStr := req.URL.Query().Get("purge") 437 var purgeBool bool 438 if purgeStr != "" { 439 var err error 440 purgeBool, err = strconv.ParseBool(purgeStr) 441 if err != nil { 442 return nil, fmt.Errorf("Failed to parse value of %q (%v) as a bool: %v", "purge", purgeStr, err) 443 } 444 } 445 446 args := structs.JobDeregisterRequest{ 447 JobID: jobName, 448 Purge: purgeBool, 449 } 450 s.parseWriteRequest(req, &args.WriteRequest) 451 452 var out structs.JobDeregisterResponse 453 if err := s.agent.RPC("Job.Deregister", &args, &out); err != nil { 454 return nil, err 455 } 456 setIndex(resp, out.Index) 457 return out, nil 458 } 459 460 func (s *HTTPServer) jobScale(resp http.ResponseWriter, req *http.Request, 461 jobName string) (interface{}, error) { 462 463 switch req.Method { 464 case "GET": 465 return s.jobScaleStatus(resp, req, jobName) 466 case "PUT", "POST": 467 return s.jobScaleAction(resp, req, jobName) 468 default: 469 return nil, CodedError(405, ErrInvalidMethod) 470 } 471 } 472 473 func (s *HTTPServer) jobScaleStatus(resp http.ResponseWriter, req *http.Request, 474 jobName string) (interface{}, error) { 475 476 args := structs.JobScaleStatusRequest{ 477 JobID: jobName, 478 } 479 if s.parse(resp, req, &args.Region, &args.QueryOptions) { 480 return nil, nil 481 } 482 483 var out structs.JobScaleStatusResponse 484 if err := s.agent.RPC("Job.ScaleStatus", &args, &out); err != nil { 485 return nil, err 486 } 487 488 setMeta(resp, &out.QueryMeta) 489 if out.JobScaleStatus == nil { 490 return nil, CodedError(404, "job not found") 491 } 492 493 return out.JobScaleStatus, nil 494 } 495 496 func (s *HTTPServer) jobScaleAction(resp http.ResponseWriter, req *http.Request, 497 jobName string) (interface{}, error) { 498 499 if req.Method != "PUT" && req.Method != "POST" { 500 return nil, CodedError(405, ErrInvalidMethod) 501 } 502 503 var args api.ScalingRequest 504 if err := decodeBody(req, &args); err != nil { 505 return nil, CodedError(400, err.Error()) 506 } 507 508 namespace := args.Target[structs.ScalingTargetNamespace] 509 targetJob := args.Target[structs.ScalingTargetJob] 510 if targetJob != "" && targetJob != jobName { 511 return nil, CodedError(400, "job ID in payload did not match URL") 512 } 513 514 scaleReq := structs.JobScaleRequest{ 515 JobID: jobName, 516 Namespace: namespace, 517 Target: args.Target, 518 Count: args.Count, 519 PolicyOverride: args.PolicyOverride, 520 Message: args.Message, 521 Error: args.Error, 522 Meta: args.Meta, 523 } 524 // parseWriteRequest overrides Namespace, Region and AuthToken 525 // based on values from the original http request 526 s.parseWriteRequest(req, &scaleReq.WriteRequest) 527 528 var out structs.JobRegisterResponse 529 if err := s.agent.RPC("Job.Scale", &scaleReq, &out); err != nil { 530 return nil, err 531 } 532 setIndex(resp, out.Index) 533 return out, nil 534 } 535 536 func (s *HTTPServer) jobVersions(resp http.ResponseWriter, req *http.Request, 537 jobName string) (interface{}, error) { 538 539 diffsStr := req.URL.Query().Get("diffs") 540 var diffsBool bool 541 if diffsStr != "" { 542 var err error 543 diffsBool, err = strconv.ParseBool(diffsStr) 544 if err != nil { 545 return nil, fmt.Errorf("Failed to parse value of %q (%v) as a bool: %v", "diffs", diffsStr, err) 546 } 547 } 548 549 args := structs.JobVersionsRequest{ 550 JobID: jobName, 551 Diffs: diffsBool, 552 } 553 if s.parse(resp, req, &args.Region, &args.QueryOptions) { 554 return nil, nil 555 } 556 557 var out structs.JobVersionsResponse 558 if err := s.agent.RPC("Job.GetJobVersions", &args, &out); err != nil { 559 return nil, err 560 } 561 562 setMeta(resp, &out.QueryMeta) 563 if len(out.Versions) == 0 { 564 return nil, CodedError(404, "job versions not found") 565 } 566 567 return out, nil 568 } 569 570 func (s *HTTPServer) jobRevert(resp http.ResponseWriter, req *http.Request, 571 jobName string) (interface{}, error) { 572 573 if req.Method != "PUT" && req.Method != "POST" { 574 return nil, CodedError(405, ErrInvalidMethod) 575 } 576 577 var revertRequest structs.JobRevertRequest 578 if err := decodeBody(req, &revertRequest); err != nil { 579 return nil, CodedError(400, err.Error()) 580 } 581 if revertRequest.JobID == "" { 582 return nil, CodedError(400, "JobID must be specified") 583 } 584 if revertRequest.JobID != jobName { 585 return nil, CodedError(400, "Job ID does not match") 586 } 587 588 s.parseWriteRequest(req, &revertRequest.WriteRequest) 589 590 var out structs.JobRegisterResponse 591 if err := s.agent.RPC("Job.Revert", &revertRequest, &out); err != nil { 592 return nil, err 593 } 594 595 setMeta(resp, &out.QueryMeta) 596 return out, nil 597 } 598 599 func (s *HTTPServer) jobStable(resp http.ResponseWriter, req *http.Request, 600 jobName string) (interface{}, error) { 601 602 if req.Method != "PUT" && req.Method != "POST" { 603 return nil, CodedError(405, ErrInvalidMethod) 604 } 605 606 var stableRequest structs.JobStabilityRequest 607 if err := decodeBody(req, &stableRequest); err != nil { 608 return nil, CodedError(400, err.Error()) 609 } 610 if stableRequest.JobID == "" { 611 return nil, CodedError(400, "JobID must be specified") 612 } 613 if stableRequest.JobID != jobName { 614 return nil, CodedError(400, "Job ID does not match") 615 } 616 617 s.parseWriteRequest(req, &stableRequest.WriteRequest) 618 619 var out structs.JobStabilityResponse 620 if err := s.agent.RPC("Job.Stable", &stableRequest, &out); err != nil { 621 return nil, err 622 } 623 624 setIndex(resp, out.Index) 625 return out, nil 626 } 627 628 func (s *HTTPServer) jobSummaryRequest(resp http.ResponseWriter, req *http.Request, name string) (interface{}, error) { 629 args := structs.JobSummaryRequest{ 630 JobID: name, 631 } 632 if s.parse(resp, req, &args.Region, &args.QueryOptions) { 633 return nil, nil 634 } 635 636 var out structs.JobSummaryResponse 637 if err := s.agent.RPC("Job.Summary", &args, &out); err != nil { 638 return nil, err 639 } 640 641 setMeta(resp, &out.QueryMeta) 642 if out.JobSummary == nil { 643 return nil, CodedError(404, "job not found") 644 } 645 setIndex(resp, out.Index) 646 return out.JobSummary, nil 647 } 648 649 func (s *HTTPServer) jobDispatchRequest(resp http.ResponseWriter, req *http.Request, name string) (interface{}, error) { 650 if req.Method != "PUT" && req.Method != "POST" { 651 return nil, CodedError(405, ErrInvalidMethod) 652 } 653 args := structs.JobDispatchRequest{} 654 if err := decodeBody(req, &args); err != nil { 655 return nil, CodedError(400, err.Error()) 656 } 657 if args.JobID != "" && args.JobID != name { 658 return nil, CodedError(400, "Job ID does not match") 659 } 660 if args.JobID == "" { 661 args.JobID = name 662 } 663 664 s.parseWriteRequest(req, &args.WriteRequest) 665 666 var out structs.JobDispatchResponse 667 if err := s.agent.RPC("Job.Dispatch", &args, &out); err != nil { 668 return nil, err 669 } 670 setIndex(resp, out.Index) 671 return out, nil 672 } 673 674 // JobsParseRequest parses a hcl jobspec and returns a api.Job 675 func (s *HTTPServer) JobsParseRequest(resp http.ResponseWriter, req *http.Request) (interface{}, error) { 676 if req.Method != http.MethodPut && req.Method != http.MethodPost { 677 return nil, CodedError(405, ErrInvalidMethod) 678 } 679 680 args := &api.JobsParseRequest{} 681 if err := decodeBody(req, &args); err != nil { 682 return nil, CodedError(400, err.Error()) 683 } 684 if args.JobHCL == "" { 685 return nil, CodedError(400, "Job spec is empty") 686 } 687 688 jobfile := strings.NewReader(args.JobHCL) 689 jobStruct, err := jobspec.Parse(jobfile) 690 if err != nil { 691 return nil, CodedError(400, err.Error()) 692 } 693 694 if args.Canonicalize { 695 jobStruct.Canonicalize() 696 } 697 return jobStruct, nil 698 } 699 700 func ApiJobToStructJob(job *api.Job) *structs.Job { 701 job.Canonicalize() 702 703 j := &structs.Job{ 704 Stop: *job.Stop, 705 Region: *job.Region, 706 Namespace: *job.Namespace, 707 ID: *job.ID, 708 ParentID: *job.ParentID, 709 Name: *job.Name, 710 Type: *job.Type, 711 Priority: *job.Priority, 712 AllAtOnce: *job.AllAtOnce, 713 Datacenters: job.Datacenters, 714 Payload: job.Payload, 715 Meta: job.Meta, 716 ConsulToken: *job.ConsulToken, 717 VaultToken: *job.VaultToken, 718 Constraints: ApiConstraintsToStructs(job.Constraints), 719 Affinities: ApiAffinitiesToStructs(job.Affinities), 720 } 721 722 // Update has been pushed into the task groups. stagger and max_parallel are 723 // preserved at the job level, but all other values are discarded. The job.Update 724 // api value is merged into TaskGroups already in api.Canonicalize 725 if job.Update != nil && job.Update.MaxParallel != nil && *job.Update.MaxParallel > 0 { 726 j.Update = structs.UpdateStrategy{} 727 728 if job.Update.Stagger != nil { 729 j.Update.Stagger = *job.Update.Stagger 730 } 731 if job.Update.MaxParallel != nil { 732 j.Update.MaxParallel = *job.Update.MaxParallel 733 } 734 } 735 736 if l := len(job.Spreads); l != 0 { 737 j.Spreads = make([]*structs.Spread, l) 738 for i, apiSpread := range job.Spreads { 739 j.Spreads[i] = ApiSpreadToStructs(apiSpread) 740 } 741 } 742 743 if job.Periodic != nil { 744 j.Periodic = &structs.PeriodicConfig{ 745 Enabled: *job.Periodic.Enabled, 746 SpecType: *job.Periodic.SpecType, 747 ProhibitOverlap: *job.Periodic.ProhibitOverlap, 748 TimeZone: *job.Periodic.TimeZone, 749 } 750 751 if job.Periodic.Spec != nil { 752 j.Periodic.Spec = *job.Periodic.Spec 753 } 754 } 755 756 if job.ParameterizedJob != nil { 757 j.ParameterizedJob = &structs.ParameterizedJobConfig{ 758 Payload: job.ParameterizedJob.Payload, 759 MetaRequired: job.ParameterizedJob.MetaRequired, 760 MetaOptional: job.ParameterizedJob.MetaOptional, 761 } 762 } 763 764 if l := len(job.TaskGroups); l != 0 { 765 j.TaskGroups = make([]*structs.TaskGroup, l) 766 for i, taskGroup := range job.TaskGroups { 767 tg := &structs.TaskGroup{} 768 ApiTgToStructsTG(j, taskGroup, tg) 769 j.TaskGroups[i] = tg 770 } 771 } 772 773 return j 774 } 775 776 func ApiTgToStructsTG(job *structs.Job, taskGroup *api.TaskGroup, tg *structs.TaskGroup) { 777 tg.Name = *taskGroup.Name 778 tg.Count = *taskGroup.Count 779 tg.Meta = taskGroup.Meta 780 tg.Constraints = ApiConstraintsToStructs(taskGroup.Constraints) 781 tg.Affinities = ApiAffinitiesToStructs(taskGroup.Affinities) 782 tg.Networks = ApiNetworkResourceToStructs(taskGroup.Networks) 783 tg.Services = ApiServicesToStructs(taskGroup.Services) 784 785 tg.RestartPolicy = &structs.RestartPolicy{ 786 Attempts: *taskGroup.RestartPolicy.Attempts, 787 Interval: *taskGroup.RestartPolicy.Interval, 788 Delay: *taskGroup.RestartPolicy.Delay, 789 Mode: *taskGroup.RestartPolicy.Mode, 790 } 791 792 if taskGroup.ShutdownDelay != nil { 793 tg.ShutdownDelay = taskGroup.ShutdownDelay 794 } 795 796 if taskGroup.StopAfterClientDisconnect != nil { 797 tg.StopAfterClientDisconnect = taskGroup.StopAfterClientDisconnect 798 } 799 800 if taskGroup.ReschedulePolicy != nil { 801 tg.ReschedulePolicy = &structs.ReschedulePolicy{ 802 Attempts: *taskGroup.ReschedulePolicy.Attempts, 803 Interval: *taskGroup.ReschedulePolicy.Interval, 804 Delay: *taskGroup.ReschedulePolicy.Delay, 805 DelayFunction: *taskGroup.ReschedulePolicy.DelayFunction, 806 MaxDelay: *taskGroup.ReschedulePolicy.MaxDelay, 807 Unlimited: *taskGroup.ReschedulePolicy.Unlimited, 808 } 809 } 810 811 if taskGroup.Migrate != nil { 812 tg.Migrate = &structs.MigrateStrategy{ 813 MaxParallel: *taskGroup.Migrate.MaxParallel, 814 HealthCheck: *taskGroup.Migrate.HealthCheck, 815 MinHealthyTime: *taskGroup.Migrate.MinHealthyTime, 816 HealthyDeadline: *taskGroup.Migrate.HealthyDeadline, 817 } 818 } 819 820 if taskGroup.Scaling != nil { 821 tg.Scaling = ApiScalingPolicyToStructs(tg.Count, taskGroup.Scaling).TargetTaskGroup(job, tg) 822 } 823 824 tg.EphemeralDisk = &structs.EphemeralDisk{ 825 Sticky: *taskGroup.EphemeralDisk.Sticky, 826 SizeMB: *taskGroup.EphemeralDisk.SizeMB, 827 Migrate: *taskGroup.EphemeralDisk.Migrate, 828 } 829 830 if l := len(taskGroup.Spreads); l != 0 { 831 tg.Spreads = make([]*structs.Spread, l) 832 for k, spread := range taskGroup.Spreads { 833 tg.Spreads[k] = ApiSpreadToStructs(spread) 834 } 835 } 836 837 if l := len(taskGroup.Volumes); l != 0 { 838 tg.Volumes = make(map[string]*structs.VolumeRequest, l) 839 for k, v := range taskGroup.Volumes { 840 if v.Type != structs.VolumeTypeHost && v.Type != structs.VolumeTypeCSI { 841 // Ignore volumes we don't understand in this iteration currently. 842 // - This is because we don't currently have a way to return errors here. 843 continue 844 } 845 846 vol := &structs.VolumeRequest{ 847 Name: v.Name, 848 Type: v.Type, 849 ReadOnly: v.ReadOnly, 850 Source: v.Source, 851 } 852 853 if v.MountOptions != nil { 854 vol.MountOptions = &structs.CSIMountOptions{ 855 FSType: v.MountOptions.FSType, 856 MountFlags: v.MountOptions.MountFlags, 857 } 858 } 859 860 tg.Volumes[k] = vol 861 } 862 } 863 864 if taskGroup.Update != nil { 865 tg.Update = &structs.UpdateStrategy{ 866 Stagger: *taskGroup.Update.Stagger, 867 MaxParallel: *taskGroup.Update.MaxParallel, 868 HealthCheck: *taskGroup.Update.HealthCheck, 869 MinHealthyTime: *taskGroup.Update.MinHealthyTime, 870 HealthyDeadline: *taskGroup.Update.HealthyDeadline, 871 ProgressDeadline: *taskGroup.Update.ProgressDeadline, 872 Canary: *taskGroup.Update.Canary, 873 } 874 875 // boolPtr fields may be nil, others will have pointers to default values via Canonicalize 876 if taskGroup.Update.AutoRevert != nil { 877 tg.Update.AutoRevert = *taskGroup.Update.AutoRevert 878 } 879 880 if taskGroup.Update.AutoPromote != nil { 881 tg.Update.AutoPromote = *taskGroup.Update.AutoPromote 882 } 883 } 884 885 if l := len(taskGroup.Tasks); l != 0 { 886 tg.Tasks = make([]*structs.Task, l) 887 for l, task := range taskGroup.Tasks { 888 t := &structs.Task{} 889 ApiTaskToStructsTask(task, t) 890 tg.Tasks[l] = t 891 } 892 } 893 } 894 895 // ApiTaskToStructsTask is a copy and type conversion between the API 896 // representation of a task from a struct representation of a task. 897 func ApiTaskToStructsTask(apiTask *api.Task, structsTask *structs.Task) { 898 structsTask.Name = apiTask.Name 899 structsTask.Driver = apiTask.Driver 900 structsTask.User = apiTask.User 901 structsTask.Leader = apiTask.Leader 902 structsTask.Config = apiTask.Config 903 structsTask.Env = apiTask.Env 904 structsTask.Meta = apiTask.Meta 905 structsTask.KillTimeout = *apiTask.KillTimeout 906 structsTask.ShutdownDelay = apiTask.ShutdownDelay 907 structsTask.KillSignal = apiTask.KillSignal 908 structsTask.Kind = structs.TaskKind(apiTask.Kind) 909 structsTask.Constraints = ApiConstraintsToStructs(apiTask.Constraints) 910 structsTask.Affinities = ApiAffinitiesToStructs(apiTask.Affinities) 911 structsTask.CSIPluginConfig = ApiCSIPluginConfigToStructsCSIPluginConfig(apiTask.CSIPluginConfig) 912 913 if apiTask.RestartPolicy != nil { 914 structsTask.RestartPolicy = &structs.RestartPolicy{ 915 Attempts: *apiTask.RestartPolicy.Attempts, 916 Interval: *apiTask.RestartPolicy.Interval, 917 Delay: *apiTask.RestartPolicy.Delay, 918 Mode: *apiTask.RestartPolicy.Mode, 919 } 920 } 921 922 if l := len(apiTask.VolumeMounts); l != 0 { 923 structsTask.VolumeMounts = make([]*structs.VolumeMount, l) 924 for i, mount := range apiTask.VolumeMounts { 925 structsTask.VolumeMounts[i] = &structs.VolumeMount{ 926 Volume: *mount.Volume, 927 Destination: *mount.Destination, 928 ReadOnly: *mount.ReadOnly, 929 PropagationMode: *mount.PropagationMode, 930 } 931 } 932 } 933 934 if l := len(apiTask.Services); l != 0 { 935 structsTask.Services = make([]*structs.Service, l) 936 for i, service := range apiTask.Services { 937 structsTask.Services[i] = &structs.Service{ 938 Name: service.Name, 939 PortLabel: service.PortLabel, 940 Tags: service.Tags, 941 CanaryTags: service.CanaryTags, 942 EnableTagOverride: service.EnableTagOverride, 943 AddressMode: service.AddressMode, 944 Meta: helper.CopyMapStringString(service.Meta), 945 CanaryMeta: helper.CopyMapStringString(service.CanaryMeta), 946 } 947 948 if l := len(service.Checks); l != 0 { 949 structsTask.Services[i].Checks = make([]*structs.ServiceCheck, l) 950 for j, check := range service.Checks { 951 structsTask.Services[i].Checks[j] = &structs.ServiceCheck{ 952 Name: check.Name, 953 Type: check.Type, 954 Command: check.Command, 955 Args: check.Args, 956 Path: check.Path, 957 Protocol: check.Protocol, 958 PortLabel: check.PortLabel, 959 AddressMode: check.AddressMode, 960 Interval: check.Interval, 961 Timeout: check.Timeout, 962 InitialStatus: check.InitialStatus, 963 TLSSkipVerify: check.TLSSkipVerify, 964 Header: check.Header, 965 Method: check.Method, 966 GRPCService: check.GRPCService, 967 GRPCUseTLS: check.GRPCUseTLS, 968 } 969 if check.CheckRestart != nil { 970 structsTask.Services[i].Checks[j].CheckRestart = &structs.CheckRestart{ 971 Limit: check.CheckRestart.Limit, 972 Grace: *check.CheckRestart.Grace, 973 IgnoreWarnings: check.CheckRestart.IgnoreWarnings, 974 } 975 } 976 } 977 } 978 } 979 } 980 981 structsTask.Resources = ApiResourcesToStructs(apiTask.Resources) 982 983 structsTask.LogConfig = &structs.LogConfig{ 984 MaxFiles: *apiTask.LogConfig.MaxFiles, 985 MaxFileSizeMB: *apiTask.LogConfig.MaxFileSizeMB, 986 } 987 988 if l := len(apiTask.Artifacts); l != 0 { 989 structsTask.Artifacts = make([]*structs.TaskArtifact, l) 990 for k, ta := range apiTask.Artifacts { 991 structsTask.Artifacts[k] = &structs.TaskArtifact{ 992 GetterSource: *ta.GetterSource, 993 GetterOptions: ta.GetterOptions, 994 GetterMode: *ta.GetterMode, 995 RelativeDest: *ta.RelativeDest, 996 } 997 } 998 } 999 1000 if apiTask.Vault != nil { 1001 structsTask.Vault = &structs.Vault{ 1002 Policies: apiTask.Vault.Policies, 1003 Env: *apiTask.Vault.Env, 1004 ChangeMode: *apiTask.Vault.ChangeMode, 1005 ChangeSignal: *apiTask.Vault.ChangeSignal, 1006 } 1007 } 1008 1009 if l := len(apiTask.Templates); l != 0 { 1010 structsTask.Templates = make([]*structs.Template, l) 1011 for i, template := range apiTask.Templates { 1012 structsTask.Templates[i] = &structs.Template{ 1013 SourcePath: *template.SourcePath, 1014 DestPath: *template.DestPath, 1015 EmbeddedTmpl: *template.EmbeddedTmpl, 1016 ChangeMode: *template.ChangeMode, 1017 ChangeSignal: *template.ChangeSignal, 1018 Splay: *template.Splay, 1019 Perms: *template.Perms, 1020 LeftDelim: *template.LeftDelim, 1021 RightDelim: *template.RightDelim, 1022 Envvars: *template.Envvars, 1023 VaultGrace: *template.VaultGrace, 1024 } 1025 } 1026 } 1027 1028 if apiTask.DispatchPayload != nil { 1029 structsTask.DispatchPayload = &structs.DispatchPayloadConfig{ 1030 File: apiTask.DispatchPayload.File, 1031 } 1032 } 1033 1034 if apiTask.Lifecycle != nil { 1035 structsTask.Lifecycle = &structs.TaskLifecycleConfig{ 1036 Hook: apiTask.Lifecycle.Hook, 1037 Sidecar: apiTask.Lifecycle.Sidecar, 1038 } 1039 } 1040 } 1041 1042 func ApiCSIPluginConfigToStructsCSIPluginConfig(apiConfig *api.TaskCSIPluginConfig) *structs.TaskCSIPluginConfig { 1043 if apiConfig == nil { 1044 return nil 1045 } 1046 1047 sc := &structs.TaskCSIPluginConfig{} 1048 sc.ID = apiConfig.ID 1049 sc.Type = structs.CSIPluginType(apiConfig.Type) 1050 sc.MountDir = apiConfig.MountDir 1051 return sc 1052 } 1053 1054 func ApiResourcesToStructs(in *api.Resources) *structs.Resources { 1055 if in == nil { 1056 return nil 1057 } 1058 1059 out := &structs.Resources{ 1060 CPU: *in.CPU, 1061 MemoryMB: *in.MemoryMB, 1062 } 1063 1064 // COMPAT(0.10): Only being used to issue warnings 1065 if in.IOPS != nil { 1066 out.IOPS = *in.IOPS 1067 } 1068 1069 if len(in.Networks) != 0 { 1070 out.Networks = ApiNetworkResourceToStructs(in.Networks) 1071 } 1072 1073 if l := len(in.Devices); l != 0 { 1074 out.Devices = make([]*structs.RequestedDevice, l) 1075 for i, d := range in.Devices { 1076 out.Devices[i] = &structs.RequestedDevice{ 1077 Name: d.Name, 1078 Count: *d.Count, 1079 Constraints: ApiConstraintsToStructs(d.Constraints), 1080 Affinities: ApiAffinitiesToStructs(d.Affinities), 1081 } 1082 } 1083 } 1084 1085 return out 1086 } 1087 1088 func ApiNetworkResourceToStructs(in []*api.NetworkResource) []*structs.NetworkResource { 1089 var out []*structs.NetworkResource 1090 if len(in) == 0 { 1091 return out 1092 } 1093 out = make([]*structs.NetworkResource, len(in)) 1094 for i, nw := range in { 1095 out[i] = &structs.NetworkResource{ 1096 Mode: nw.Mode, 1097 CIDR: nw.CIDR, 1098 IP: nw.IP, 1099 MBits: *nw.MBits, 1100 } 1101 1102 if l := len(nw.DynamicPorts); l != 0 { 1103 out[i].DynamicPorts = make([]structs.Port, l) 1104 for j, dp := range nw.DynamicPorts { 1105 out[i].DynamicPorts[j] = structs.Port{ 1106 Label: dp.Label, 1107 Value: dp.Value, 1108 To: dp.To, 1109 } 1110 } 1111 } 1112 1113 if l := len(nw.ReservedPorts); l != 0 { 1114 out[i].ReservedPorts = make([]structs.Port, l) 1115 for j, rp := range nw.ReservedPorts { 1116 out[i].ReservedPorts[j] = structs.Port{ 1117 Label: rp.Label, 1118 Value: rp.Value, 1119 To: rp.To, 1120 } 1121 } 1122 } 1123 } 1124 1125 return out 1126 } 1127 1128 //TODO(schmichael) refactor and reuse in service parsing above 1129 func ApiServicesToStructs(in []*api.Service) []*structs.Service { 1130 if len(in) == 0 { 1131 return nil 1132 } 1133 1134 out := make([]*structs.Service, len(in)) 1135 for i, s := range in { 1136 out[i] = &structs.Service{ 1137 Name: s.Name, 1138 PortLabel: s.PortLabel, 1139 Tags: s.Tags, 1140 CanaryTags: s.CanaryTags, 1141 EnableTagOverride: s.EnableTagOverride, 1142 AddressMode: s.AddressMode, 1143 Meta: helper.CopyMapStringString(s.Meta), 1144 CanaryMeta: helper.CopyMapStringString(s.CanaryMeta), 1145 } 1146 1147 if l := len(s.Checks); l != 0 { 1148 out[i].Checks = make([]*structs.ServiceCheck, l) 1149 for j, check := range s.Checks { 1150 out[i].Checks[j] = &structs.ServiceCheck{ 1151 Name: check.Name, 1152 Type: check.Type, 1153 Command: check.Command, 1154 Args: check.Args, 1155 Path: check.Path, 1156 Protocol: check.Protocol, 1157 PortLabel: check.PortLabel, 1158 Expose: check.Expose, 1159 AddressMode: check.AddressMode, 1160 Interval: check.Interval, 1161 Timeout: check.Timeout, 1162 InitialStatus: check.InitialStatus, 1163 TLSSkipVerify: check.TLSSkipVerify, 1164 Header: check.Header, 1165 Method: check.Method, 1166 GRPCService: check.GRPCService, 1167 GRPCUseTLS: check.GRPCUseTLS, 1168 TaskName: check.TaskName, 1169 } 1170 if check.CheckRestart != nil { 1171 out[i].Checks[j].CheckRestart = &structs.CheckRestart{ 1172 Limit: check.CheckRestart.Limit, 1173 Grace: *check.CheckRestart.Grace, 1174 IgnoreWarnings: check.CheckRestart.IgnoreWarnings, 1175 } 1176 } 1177 } 1178 } 1179 1180 if s.Connect != nil { 1181 out[i].Connect = ApiConsulConnectToStructs(s.Connect) 1182 } 1183 1184 } 1185 1186 return out 1187 } 1188 1189 func ApiConsulConnectToStructs(in *api.ConsulConnect) *structs.ConsulConnect { 1190 if in == nil { 1191 return nil 1192 } 1193 return &structs.ConsulConnect{ 1194 Native: in.Native, 1195 SidecarService: apiConnectSidecarServiceToStructs(in.SidecarService), 1196 SidecarTask: apiConnectSidecarTaskToStructs(in.SidecarTask), 1197 } 1198 } 1199 1200 func apiConnectSidecarServiceToStructs(in *api.ConsulSidecarService) *structs.ConsulSidecarService { 1201 if in == nil { 1202 return nil 1203 } 1204 return &structs.ConsulSidecarService{ 1205 Port: in.Port, 1206 Tags: helper.CopySliceString(in.Tags), 1207 Proxy: apiConnectSidecarServiceProxyToStructs(in.Proxy), 1208 } 1209 } 1210 1211 func apiConnectSidecarServiceProxyToStructs(in *api.ConsulProxy) *structs.ConsulProxy { 1212 if in == nil { 1213 return nil 1214 } 1215 return &structs.ConsulProxy{ 1216 LocalServiceAddress: in.LocalServiceAddress, 1217 LocalServicePort: in.LocalServicePort, 1218 Upstreams: apiUpstreamsToStructs(in.Upstreams), 1219 Expose: apiConsulExposeConfigToStructs(in.ExposeConfig), 1220 Config: in.Config, 1221 } 1222 } 1223 1224 func apiUpstreamsToStructs(in []*api.ConsulUpstream) []structs.ConsulUpstream { 1225 if len(in) == 0 { 1226 return nil 1227 } 1228 upstreams := make([]structs.ConsulUpstream, len(in)) 1229 for i, upstream := range in { 1230 upstreams[i] = structs.ConsulUpstream{ 1231 DestinationName: upstream.DestinationName, 1232 LocalBindPort: upstream.LocalBindPort, 1233 } 1234 } 1235 return upstreams 1236 } 1237 1238 func apiConsulExposeConfigToStructs(in *api.ConsulExposeConfig) *structs.ConsulExposeConfig { 1239 if in == nil { 1240 return nil 1241 } 1242 return &structs.ConsulExposeConfig{ 1243 Paths: apiConsulExposePathsToStructs(in.Path), 1244 } 1245 } 1246 1247 func apiConsulExposePathsToStructs(in []*api.ConsulExposePath) []structs.ConsulExposePath { 1248 if len(in) == 0 { 1249 return nil 1250 } 1251 paths := make([]structs.ConsulExposePath, len(in)) 1252 for i, path := range in { 1253 paths[i] = structs.ConsulExposePath{ 1254 Path: path.Path, 1255 Protocol: path.Protocol, 1256 LocalPathPort: path.LocalPathPort, 1257 ListenerPort: path.ListenerPort, 1258 } 1259 } 1260 return paths 1261 } 1262 1263 func apiConnectSidecarTaskToStructs(in *api.SidecarTask) *structs.SidecarTask { 1264 if in == nil { 1265 return nil 1266 } 1267 return &structs.SidecarTask{ 1268 Name: in.Name, 1269 Driver: in.Driver, 1270 User: in.User, 1271 Config: in.Config, 1272 Env: in.Env, 1273 Resources: ApiResourcesToStructs(in.Resources), 1274 Meta: in.Meta, 1275 ShutdownDelay: in.ShutdownDelay, 1276 KillSignal: in.KillSignal, 1277 KillTimeout: in.KillTimeout, 1278 LogConfig: apiLogConfigToStructs(in.LogConfig), 1279 } 1280 } 1281 1282 func apiLogConfigToStructs(in *api.LogConfig) *structs.LogConfig { 1283 if in == nil { 1284 return nil 1285 } 1286 return &structs.LogConfig{ 1287 MaxFiles: dereferenceInt(in.MaxFiles), 1288 MaxFileSizeMB: dereferenceInt(in.MaxFileSizeMB), 1289 } 1290 } 1291 1292 func dereferenceInt(in *int) int { 1293 if in == nil { 1294 return 0 1295 } 1296 return *in 1297 } 1298 1299 func ApiConstraintsToStructs(in []*api.Constraint) []*structs.Constraint { 1300 if in == nil { 1301 return nil 1302 } 1303 1304 out := make([]*structs.Constraint, len(in)) 1305 for i, ac := range in { 1306 out[i] = ApiConstraintToStructs(ac) 1307 } 1308 1309 return out 1310 } 1311 1312 func ApiConstraintToStructs(in *api.Constraint) *structs.Constraint { 1313 if in == nil { 1314 return nil 1315 } 1316 1317 return &structs.Constraint{ 1318 LTarget: in.LTarget, 1319 RTarget: in.RTarget, 1320 Operand: in.Operand, 1321 } 1322 } 1323 1324 func ApiAffinitiesToStructs(in []*api.Affinity) []*structs.Affinity { 1325 if in == nil { 1326 return nil 1327 } 1328 1329 out := make([]*structs.Affinity, len(in)) 1330 for i, ac := range in { 1331 out[i] = ApiAffinityToStructs(ac) 1332 } 1333 1334 return out 1335 } 1336 1337 func ApiAffinityToStructs(a1 *api.Affinity) *structs.Affinity { 1338 return &structs.Affinity{ 1339 LTarget: a1.LTarget, 1340 Operand: a1.Operand, 1341 RTarget: a1.RTarget, 1342 Weight: *a1.Weight, 1343 } 1344 } 1345 1346 func ApiSpreadToStructs(a1 *api.Spread) *structs.Spread { 1347 ret := &structs.Spread{} 1348 ret.Attribute = a1.Attribute 1349 ret.Weight = *a1.Weight 1350 if a1.SpreadTarget != nil { 1351 ret.SpreadTarget = make([]*structs.SpreadTarget, len(a1.SpreadTarget)) 1352 for i, st := range a1.SpreadTarget { 1353 ret.SpreadTarget[i] = &structs.SpreadTarget{ 1354 Value: st.Value, 1355 Percent: st.Percent, 1356 } 1357 } 1358 } 1359 return ret 1360 }