github.com/Benchkram/bob@v0.0.0-20220321080157-7c8f3876e225/bob/playbook/playbook.go (about) 1 package playbook 2 3 import ( 4 "bytes" 5 "errors" 6 "fmt" 7 "sort" 8 "time" 9 10 "github.com/Benchkram/bob/bobtask" 11 "github.com/Benchkram/bob/bobtask/buildinfo" 12 "github.com/Benchkram/bob/bobtask/hash" 13 "github.com/Benchkram/bob/pkg/boblog" 14 "github.com/Benchkram/bob/pkg/buildinfostore" 15 "github.com/Benchkram/errz" 16 "github.com/logrusorgru/aurora" 17 ) 18 19 // The playbook defines the order in which tasks are allowed to run. 20 // Also determines the possibility to run tasks in parallel. 21 22 var ErrTaskDoesNotExist = fmt.Errorf("task does not exist") 23 var ErrDone = fmt.Errorf("playbook is done") 24 var ErrFailed = fmt.Errorf("playbook failed") 25 var ErrUnexpectedTaskState = fmt.Errorf("task state is unsexpected") 26 27 type Playbook struct { 28 // taskChannel is closed when the root 29 // task completes. 30 taskChannel chan bobtask.Task 31 32 // errorChannel to transport errors to the caller 33 errorChannel chan error 34 35 // root task 36 root string 37 38 Tasks StatusMap 39 40 namePad int 41 42 done bool 43 44 // start is the point in time the playbook started 45 start time.Time 46 // end is the point in time the playbook ended 47 end time.Time 48 49 // enableCaching allows artifacts to be read & written to a store. 50 // Default: true. 51 enableCaching bool 52 } 53 54 func New(root string, opts ...Option) *Playbook { 55 p := &Playbook{ 56 taskChannel: make(chan bobtask.Task, 10), 57 errorChannel: make(chan error), 58 Tasks: make(StatusMap), 59 enableCaching: true, 60 root: root, 61 } 62 63 for _, opt := range opts { 64 if opt == nil { 65 continue 66 } 67 opt(p) 68 } 69 70 return p 71 } 72 73 type RebuildCause string 74 75 func (rc *RebuildCause) String() string { 76 return string(*rc) 77 } 78 79 const ( 80 TaskInputChanged RebuildCause = "input-changed" 81 TaskForcedRebuild RebuildCause = "forced" 82 DependencyChanged RebuildCause = "dependency-changed" 83 TargetInvalid RebuildCause = "target-invalid" 84 ) 85 86 // TaskNeedsRebuild check if a tasks need a rebuild by looking at it's hash value 87 // and it's child tasks. 88 func (p *Playbook) TaskNeedsRebuild(taskname string, hashIn hash.In) (rebuildRequired bool, cause RebuildCause, err error) { 89 ts, ok := p.Tasks[taskname] 90 if !ok { 91 return false, "", ErrTaskDoesNotExist 92 } 93 task := ts.Task 94 coloredName := task.ColoredName() 95 96 // returns true if rebuild strategy set to `always` 97 if task.Rebuild() == bobtask.RebuildAlways { 98 boblog.Log.V(3).Info(fmt.Sprintf("%-*s\tREBUILDING\t(rebuild set to always)", p.namePad, coloredName)) 99 return true, TaskForcedRebuild, nil 100 } 101 102 rebuildRequired, err = task.NeedsRebuild(&bobtask.RebuildOptions{HashIn: &hashIn}) 103 errz.Fatal(err) 104 if rebuildRequired { 105 boblog.Log.V(3).Info(fmt.Sprintf("%-*s\tNEEDS REBUILD\t(input changed)", p.namePad, coloredName)) 106 return true, TaskInputChanged, nil 107 } 108 109 var Done = fmt.Errorf("done") 110 // Check if task needs a rebuild due to its dependencies changing 111 err = p.Tasks.walk(task.Name(), func(tn string, t *Status, err error) error { 112 if err != nil { 113 return err 114 } 115 116 // TODO: In case the task does not exist check if a artifact can be used? 117 // Part of no-permission-workflow. 118 119 // Ignore the task itself 120 if task.Name() == tn { 121 return nil 122 } 123 124 // Require a rebuild if the dependend task did require a rebuild 125 if t.State() != StateNoRebuildRequired { 126 boblog.Log.V(3).Info(fmt.Sprintf("%-*s\tNEEDS REBUILD\t(dependecy changed)", p.namePad, coloredName)) 127 rebuildRequired = true 128 // Bail out early 129 return Done 130 } 131 132 return nil 133 }) 134 135 if errors.Is(err, Done) { 136 return true, DependencyChanged, nil 137 } 138 139 if !rebuildRequired { 140 // check rebuild due to invalidated targets 141 target, err := task.Target() 142 if err != nil { 143 return true, "", err 144 } 145 if target != nil { 146 // On a invalid traget a rebuild is required 147 rebuildRequired = !target.Verify() 148 149 // Try to load a target from the store when a rebuild is required. 150 // If not assure the artifact exists in the store. 151 if rebuildRequired { 152 boblog.Log.V(2).Info(fmt.Sprintf("[task:%s] trying to get target from store", taskname)) 153 ok, err := task.ArtifactUnpack(hashIn) 154 boblog.Log.Error(err, "Unable to get target from store") 155 156 if ok { 157 rebuildRequired = false 158 } else { 159 boblog.Log.V(3).Info(fmt.Sprintf("[task:%s] failed to get target from store", taskname)) 160 } 161 } else { 162 if !task.ArtifactExists(hashIn) && p.enableCaching { 163 err = task.ArtifactPack(hashIn) 164 boblog.Log.Error(err, "Unable to send target to store") 165 } 166 } 167 168 if rebuildRequired { 169 boblog.Log.V(3).Info(fmt.Sprintf("%-*s\tNEEDS REBUILD\t(invalid targets)", p.namePad, coloredName)) 170 } 171 } 172 } 173 174 return rebuildRequired, TargetInvalid, err 175 } 176 177 func (p *Playbook) Play() (err error) { 178 return p.play() 179 } 180 181 func (p *Playbook) play() error { 182 183 if p.done { 184 return ErrDone 185 } 186 187 if p.start.IsZero() { 188 p.start = time.Now() 189 } 190 191 // Walk the task chain and determine the next build task. Send it to the task channel. 192 // Returns `taskQueued` when a task has been send to the taskChannel. 193 // Returns `taskFailed` when a task has failed. 194 // Once it returns `nil` the playbook is done with it's work. 195 var taskQueued = fmt.Errorf("task queued") 196 var taskFailed = fmt.Errorf("task failed") 197 err := p.Tasks.walk(p.root, func(taskname string, task *Status, err error) error { 198 if err != nil { 199 return err 200 } 201 202 // fmt.Printf("walking task %s which is in state %s\n", taskname, task.State()) 203 204 switch task.State() { 205 case StatePending: 206 // Check if all dependent tasks are completed 207 for _, dependentTaskName := range task.Task.DependsOn { 208 t, ok := p.Tasks[dependentTaskName] 209 if !ok { 210 //fmt.Printf("Task %s does not exist", dependentTaskName) 211 return ErrTaskDoesNotExist 212 } 213 // fmt.Printf("dependentTask %s which is in state %s\n", t.Task.Name(), t.State()) 214 215 state := t.State() 216 if state != StateCompleted && state != StateNoRebuildRequired { 217 // A dependent task is not completed. 218 // So this task is not yet ready to run. 219 return nil 220 } 221 } 222 case StateFailed: 223 return taskFailed 224 case StateCanceled: 225 return nil 226 case StateNoRebuildRequired: 227 return nil 228 case StateCompleted: 229 return nil 230 default: 231 } 232 233 // fmt.Printf("sending task %s to channel\n", task.Task.Name()) 234 // setting the task start time before passing it to channel 235 task.Start = time.Now() 236 p.taskChannel <- task.Task 237 return taskQueued 238 }) 239 240 // taskQueued => return nil (happy path) 241 // taskFailed => return PlaybookFailed 242 // default => return err 243 if err != nil { 244 if errors.Is(err, taskQueued) { 245 return nil 246 } 247 if errors.Is(err, taskFailed) { 248 return ErrFailed 249 } 250 return err 251 } 252 253 // no work done, usually happens when 254 // no task needs a rebuild. 255 p.Done() 256 257 return nil 258 } 259 260 func (p *Playbook) Done() { 261 if !p.done { 262 p.done = true 263 p.end = time.Now() 264 close(p.taskChannel) 265 } 266 } 267 268 // TaskChannel returns the next task 269 func (p *Playbook) TaskChannel() <-chan bobtask.Task { 270 return p.taskChannel 271 } 272 273 func (p *Playbook) ErrorChannel() <-chan error { 274 return p.errorChannel 275 } 276 277 func (p *Playbook) setTaskState(taskname string, state State, taskError error) error { 278 task, ok := p.Tasks[taskname] 279 if !ok { 280 return ErrTaskDoesNotExist 281 } 282 283 task.SetState(state, taskError) 284 switch state { 285 case StateCompleted, StateCanceled, StateNoRebuildRequired: 286 task.End = time.Now() 287 } 288 289 p.Tasks[taskname] = task 290 return nil 291 } 292 293 func (p *Playbook) pack(taskname string, hash hash.In) error { 294 task, ok := p.Tasks[taskname] 295 if !ok { 296 return ErrTaskDoesNotExist 297 } 298 return task.Task.ArtifactPack(hash) 299 } 300 301 func (p *Playbook) storeHash(taskname string, buildinfo *buildinfo.I) error { 302 task, ok := p.Tasks[taskname] 303 if !ok { 304 return ErrTaskDoesNotExist 305 } 306 307 return task.Task.WriteBuildinfo(buildinfo) 308 } 309 310 func (p *Playbook) ExecutionTime() time.Duration { 311 return p.end.Sub(p.start) 312 } 313 314 // TaskStatus returns the current state of a task 315 func (p *Playbook) TaskStatus(taskname string) (ts *Status, _ error) { 316 status, ok := p.Tasks[taskname] 317 if !ok { 318 return ts, ErrTaskDoesNotExist 319 } 320 return status, nil 321 } 322 323 // TaskCompleted sets a task to completed 324 func (p *Playbook) TaskCompleted(taskname string) (err error) { 325 defer errz.Recover(&err) 326 327 task, ok := p.Tasks[taskname] 328 if !ok { 329 return ErrTaskDoesNotExist 330 } 331 332 // compute input hash 333 hashIn, err := task.Task.HashIn() 334 errz.Fatal(err) 335 336 buildInfo, err := task.ReadBuildinfo() 337 if err != nil { 338 if errors.Is(err, buildinfostore.ErrBuildInfoDoesNotExist) { 339 // assure buildinfo is initialized correctly 340 buildInfo = buildinfo.New() 341 } else { 342 errz.Fatal(err) 343 } 344 } 345 buildInfo.Info.Taskname = task.Name() 346 347 target, err := task.Task.Target() 348 errz.Fatal(err) 349 350 if target != nil { 351 targetHash, err := target.Hash() 352 if err != nil { 353 return err 354 } 355 356 buildInfo.Targets[hashIn] = targetHash 357 358 // gather target hashes of dependent tasks 359 err = p.Tasks.walk(taskname, func(tn string, task *Status, err error) error { 360 if err != nil { 361 return err 362 } 363 if taskname == tn { 364 return nil 365 } 366 367 target, err := task.Target() 368 if err != nil { 369 return err 370 } 371 if target == nil { 372 return nil 373 } 374 375 switch task.State() { 376 case StateCompleted: 377 fallthrough 378 case StateNoRebuildRequired: 379 h, err := target.Hash() 380 if err != nil { 381 return err 382 } 383 hashIn, err := task.HashIn() 384 if err != nil { 385 return err 386 } 387 buildInfo.Targets[hashIn] = h 388 default: 389 boblog.Log.V(1).Info(string(task.state)) 390 return ErrUnexpectedTaskState 391 } 392 393 return nil 394 }) 395 errz.Fatal(err) 396 } 397 398 err = p.storeHash(taskname, buildInfo) 399 errz.Fatal(err) 400 401 // TODO: use target hash? 402 if p.enableCaching { 403 err = p.pack(taskname, hashIn) 404 errz.Fatal(err) 405 } 406 407 err = p.setTaskState(taskname, StateCompleted, nil) 408 errz.Fatal(err) 409 410 err = p.play() 411 if err != nil { 412 if !errors.Is(err, ErrDone) { 413 errz.Fatal(err) 414 } 415 } 416 417 return nil 418 } 419 420 // TaskNoRebuildRequired sets a task's state to indicate that no rebuild is required 421 func (p *Playbook) TaskNoRebuildRequired(taskname string) (err error) { 422 defer errz.Recover(&err) 423 424 err = p.setTaskState(taskname, StateNoRebuildRequired, nil) 425 errz.Fatal(err) 426 427 err = p.play() 428 if err != nil { 429 if !errors.Is(err, ErrDone) { 430 errz.Fatal(err) 431 } 432 } 433 434 return nil 435 } 436 437 // TaskFailed sets a task to failed 438 func (p *Playbook) TaskFailed(taskname string, taskErr error) (err error) { 439 defer errz.Recover(&err) 440 441 err = p.setTaskState(taskname, StateFailed, taskErr) 442 errz.Fatal(err) 443 444 // p.errorChannel <- fmt.Errorf("Task %s failed", taskname) 445 446 // give the playbook the chance to set 447 // the state to done. 448 _ = p.play() 449 450 return nil 451 } 452 453 // TaskCanceled sets a task to canceled 454 func (p *Playbook) TaskCanceled(taskname string) (err error) { 455 defer errz.Recover(&err) 456 457 err = p.setTaskState(taskname, StateCanceled, nil) 458 errz.Fatal(err) 459 460 // p.errorChannel <- fmt.Errorf("Task %s cancelled", taskname) 461 462 return nil 463 } 464 465 func (p *Playbook) List() (err error) { 466 defer errz.Recover(&err) 467 468 keys := make([]string, 0, len(p.Tasks)) 469 for k := range p.Tasks { 470 keys = append(keys, k) 471 } 472 sort.Strings(keys) 473 474 for _, k := range keys { 475 fmt.Println(k) 476 } 477 478 return nil 479 } 480 481 func (p *Playbook) String() string { 482 description := bytes.NewBufferString("") 483 484 fmt.Fprint(description, "Playbook:\n") 485 486 keys := make([]string, 0, len(p.Tasks)) 487 for k := range p.Tasks { 488 keys = append(keys, k) 489 } 490 sort.Strings(keys) 491 492 for _, k := range keys { 493 task := p.Tasks[k] 494 fmt.Fprintf(description, " %s(%s): %s\n", k, task.Task.Name(), task.State()) 495 } 496 497 return description.String() 498 } 499 500 type State string 501 502 // Summary state indicators. 503 // The nbsp are intended to align on the cli. 504 func (s *State) Summary() string { 505 switch *s { 506 case StatePending: 507 return "⌛ " 508 case StateCompleted: 509 return aurora.Green("✔").Bold().String() + " " 510 case StateNoRebuildRequired: 511 return aurora.Green("cached").String() + " " 512 case StateFailed: 513 return aurora.Red("failed").String() + " " 514 case StateCanceled: 515 return aurora.Faint("canceled").String() 516 default: 517 return "" 518 } 519 } 520 521 func (s *State) Short() string { 522 switch *s { 523 case StatePending: 524 return "pending" 525 case StateCompleted: 526 return "done" 527 case StateNoRebuildRequired: 528 return "cached" 529 case StateFailed: 530 return "failed" 531 case StateCanceled: 532 return "canceled" 533 default: 534 return "" 535 } 536 } 537 538 const ( 539 StatePending State = "PENDING" 540 StateCompleted State = "COMPLETED" 541 StateNoRebuildRequired State = "CACHED" 542 StateFailed State = "FAILED" 543 StateCanceled State = "CANCELED" 544 )