github.com/kubevela/workflow@v0.6.0/pkg/utils/operation.go (about) 1 /* 2 Copyright 2022 The KubeVela Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package utils 18 19 import ( 20 "context" 21 "fmt" 22 "io" 23 "strings" 24 25 "cuelang.org/go/cue" 26 "cuelang.org/go/cue/ast" 27 "cuelang.org/go/cue/format" 28 corev1 "k8s.io/api/core/v1" 29 kerrors "k8s.io/apimachinery/pkg/api/errors" 30 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 31 "k8s.io/client-go/util/retry" 32 "sigs.k8s.io/controller-runtime/pkg/client" 33 34 "github.com/kubevela/workflow/api/v1alpha1" 35 wfContext "github.com/kubevela/workflow/pkg/context" 36 "github.com/kubevela/workflow/pkg/cue/model/sets" 37 "github.com/kubevela/workflow/pkg/cue/model/value" 38 wfTypes "github.com/kubevela/workflow/pkg/types" 39 ) 40 41 // WorkflowOperator is operation handler for workflow's suspend/resume/rollback/restart/terminate 42 type WorkflowOperator interface { 43 Suspend(ctx context.Context) error 44 Resume(ctx context.Context) error 45 Rollback(ctx context.Context) error 46 Restart(ctx context.Context) error 47 Terminate(ctx context.Context) error 48 } 49 50 // WorkflowStepOperator is operation handler for workflow steps' operations 51 type WorkflowStepOperator interface { 52 Suspend(ctx context.Context, step string) error 53 Resume(ctx context.Context, step string) error 54 Restart(ctx context.Context, step string) error 55 } 56 57 type workflowRunOperator struct { 58 cli client.Client 59 outputWriter io.Writer 60 run *v1alpha1.WorkflowRun 61 } 62 63 type workflowRunStepOperator struct { 64 cli client.Client 65 outputWriter io.Writer 66 run *v1alpha1.WorkflowRun 67 } 68 69 // NewWorkflowRunOperator get an workflow operator with k8sClient, ioWriter(optional, useful for cli) and workflow run 70 func NewWorkflowRunOperator(cli client.Client, w io.Writer, run *v1alpha1.WorkflowRun) WorkflowOperator { 71 return workflowRunOperator{ 72 cli: cli, 73 outputWriter: w, 74 run: run, 75 } 76 } 77 78 // NewWorkflowRunStepOperator get an workflow step operator with k8sClient, ioWriter(optional, useful for cli) and workflow run 79 func NewWorkflowRunStepOperator(cli client.Client, w io.Writer, run *v1alpha1.WorkflowRun) WorkflowStepOperator { 80 return workflowRunStepOperator{ 81 cli: cli, 82 outputWriter: w, 83 run: run, 84 } 85 } 86 87 // Suspend suspend workflow 88 func (wo workflowRunOperator) Suspend(ctx context.Context) error { 89 run := wo.run 90 if run.Status.Terminated { 91 return fmt.Errorf("can not suspend a terminated workflow") 92 } 93 94 if err := SuspendWorkflow(ctx, wo.cli, run, ""); err != nil { 95 return err 96 } 97 98 return writeOutputF(wo.outputWriter, "Successfully suspend workflow: %s\n", run.Name) 99 } 100 101 // Suspend suspend the workflow from a specific step 102 func (wo workflowRunStepOperator) Suspend(ctx context.Context, step string) error { 103 if step == "" { 104 return fmt.Errorf("step can not be empty") 105 } 106 run := wo.run 107 if run.Status.Terminated { 108 return fmt.Errorf("can not suspend a terminated workflow") 109 } 110 111 if err := SuspendWorkflow(ctx, wo.cli, run, step); err != nil { 112 return err 113 } 114 115 return writeOutputF(wo.outputWriter, "Successfully suspend workflow %s from step %s\n", run.Name, step) 116 } 117 118 // Resume resume a suspended workflow 119 func (wo workflowRunOperator) Resume(ctx context.Context) error { 120 run := wo.run 121 if run.Status.Terminated { 122 return fmt.Errorf("can not resume a terminated workflow") 123 } 124 125 if run.Status.Suspend { 126 if err := ResumeWorkflow(ctx, wo.cli, run, ""); err != nil { 127 return err 128 } 129 } 130 return writeOutputF(wo.outputWriter, "Successfully resume workflow: %s\n", run.Name) 131 } 132 133 // Resume resume a suspended workflow from a specific step 134 func (wo workflowRunStepOperator) Resume(ctx context.Context, step string) error { 135 if step == "" { 136 return fmt.Errorf("step can not be empty") 137 } 138 run := wo.run 139 if run.Status.Terminated { 140 return fmt.Errorf("can not resume a terminated workflow") 141 } 142 143 if run.Status.Suspend { 144 if err := ResumeWorkflow(ctx, wo.cli, run, step); err != nil { 145 return err 146 } 147 } 148 return writeOutputF(wo.outputWriter, "Successfully resume workflow %s from step %s\n", run.Name, step) 149 } 150 151 // SuspendWorkflow suspend workflow 152 func SuspendWorkflow(ctx context.Context, cli client.Client, run *v1alpha1.WorkflowRun, stepName string) error { 153 run.Status.Suspend = true 154 steps := run.Status.Steps 155 found := stepName == "" 156 157 for i, step := range steps { 158 if step.Phase == v1alpha1.WorkflowStepPhaseRunning { 159 if stepName == "" { 160 OperateSteps(steps, i, -1, v1alpha1.WorkflowStepPhaseSuspending) 161 } else if stepName == step.Name { 162 OperateSteps(steps, i, -1, v1alpha1.WorkflowStepPhaseSuspending) 163 found = true 164 break 165 } 166 } 167 for j, sub := range step.SubStepsStatus { 168 if sub.Phase == v1alpha1.WorkflowStepPhaseRunning { 169 if stepName == "" { 170 OperateSteps(steps, i, j, v1alpha1.WorkflowStepPhaseSuspending) 171 } else if stepName == sub.Name { 172 OperateSteps(steps, i, j, v1alpha1.WorkflowStepPhaseSuspending) 173 found = true 174 break 175 } 176 } 177 } 178 } 179 if !found { 180 return fmt.Errorf("can not find step %s", stepName) 181 } 182 if err := retry.RetryOnConflict(retry.DefaultBackoff, func() error { 183 return cli.Status().Patch(ctx, run, client.Merge) 184 }); err != nil { 185 return err 186 } 187 return nil 188 } 189 190 // OperateSteps handles the operations to the steps 191 func OperateSteps(status []v1alpha1.WorkflowStepStatus, i, j int, phase v1alpha1.WorkflowStepPhase) { 192 if j == -1 { 193 status[i].Phase = phase 194 for k, v := range status[i].SubStepsStatus { 195 if !wfTypes.IsStepFinish(v.Phase, v.Reason) { 196 status[i].SubStepsStatus[k].Phase = phase 197 } 198 } 199 } else { 200 status[i].SubStepsStatus[j].Phase = phase 201 } 202 } 203 204 // ResumeWorkflow resume workflow 205 func ResumeWorkflow(ctx context.Context, cli client.Client, run *v1alpha1.WorkflowRun, stepName string) error { 206 run.Status.Suspend = false 207 steps := run.Status.Steps 208 found := stepName == "" 209 210 for i, step := range steps { 211 if step.Phase == v1alpha1.WorkflowStepPhaseSuspending { 212 if stepName == "" { 213 OperateSteps(steps, i, -1, v1alpha1.WorkflowStepPhaseRunning) 214 } else if stepName == step.Name { 215 OperateSteps(steps, i, -1, v1alpha1.WorkflowStepPhaseRunning) 216 found = true 217 break 218 } 219 } 220 for j, sub := range step.SubStepsStatus { 221 if sub.Phase == v1alpha1.WorkflowStepPhaseSuspending { 222 if stepName == "" { 223 OperateSteps(steps, i, j, v1alpha1.WorkflowStepPhaseRunning) 224 } else if stepName == sub.Name { 225 OperateSteps(steps, i, j, v1alpha1.WorkflowStepPhaseRunning) 226 found = true 227 break 228 } 229 } 230 } 231 } 232 if !found { 233 return fmt.Errorf("can not find step %s", stepName) 234 } 235 if err := retry.RetryOnConflict(retry.DefaultBackoff, func() error { 236 return cli.Status().Patch(ctx, run, client.Merge) 237 }); err != nil { 238 return err 239 } 240 return nil 241 } 242 243 // Rollback is not supported for WorkflowRun 244 func (wo workflowRunOperator) Rollback(ctx context.Context) error { 245 return fmt.Errorf("can not rollback a WorkflowRun") 246 } 247 248 // Restart restart workflow 249 func (wo workflowRunOperator) Restart(ctx context.Context) error { 250 run := wo.run 251 if err := RestartWorkflow(ctx, wo.cli, run, ""); err != nil { 252 return err 253 } 254 return writeOutputF(wo.outputWriter, "Successfully restart workflow: %s\n", run.Name) 255 } 256 257 // Restart restart workflow from a specific step 258 func (wo workflowRunStepOperator) Restart(ctx context.Context, step string) error { 259 if step == "" { 260 return fmt.Errorf("step can not be empty") 261 } 262 run := wo.run 263 if err := RestartWorkflow(ctx, wo.cli, run, step); err != nil { 264 return err 265 } 266 return writeOutputF(wo.outputWriter, "Successfully restart workflow %s from step %s\n", run.Name, step) 267 } 268 269 // RestartWorkflow restart workflow 270 func RestartWorkflow(ctx context.Context, cli client.Client, run *v1alpha1.WorkflowRun, step string) error { 271 if step != "" { 272 return RestartFromStep(ctx, cli, run, step) 273 } 274 if run.Status.ContextBackend != nil { 275 cm := &corev1.ConfigMap{} 276 if err := cli.Get(ctx, client.ObjectKey{Namespace: run.Namespace, Name: run.Status.ContextBackend.Name}, cm); err == nil { 277 if err := cli.Delete(ctx, cm); err != nil { 278 return err 279 } 280 } else if !kerrors.IsNotFound(err) { 281 return err 282 } 283 } 284 // reset the workflow status to restart the workflow 285 run.Status = v1alpha1.WorkflowRunStatus{} 286 287 if err := cli.Status().Update(ctx, run); err != nil { 288 return err 289 } 290 291 return nil 292 } 293 294 // Terminate terminate workflow 295 func (wo workflowRunOperator) Terminate(ctx context.Context) error { 296 run := wo.run 297 if err := TerminateWorkflow(ctx, wo.cli, run); err != nil { 298 return err 299 } 300 return writeOutputF(wo.outputWriter, "Successfully terminate workflow: %s\n", run.Name) 301 } 302 303 // TerminateWorkflow terminate workflow 304 func TerminateWorkflow(ctx context.Context, cli client.Client, run *v1alpha1.WorkflowRun) error { 305 // set the workflow terminated to true 306 run.Status.Terminated = true 307 // set the workflow suspend to false 308 run.Status.Suspend = false 309 steps := run.Status.Steps 310 for i, step := range steps { 311 switch step.Phase { 312 case v1alpha1.WorkflowStepPhaseFailed: 313 if step.Reason != wfTypes.StatusReasonFailedAfterRetries && step.Reason != wfTypes.StatusReasonTimeout { 314 steps[i].Reason = wfTypes.StatusReasonTerminate 315 } 316 case v1alpha1.WorkflowStepPhaseRunning, v1alpha1.WorkflowStepPhaseSuspending: 317 steps[i].Phase = v1alpha1.WorkflowStepPhaseFailed 318 steps[i].Reason = wfTypes.StatusReasonTerminate 319 default: 320 } 321 for j, sub := range step.SubStepsStatus { 322 switch sub.Phase { 323 case v1alpha1.WorkflowStepPhaseFailed: 324 if sub.Reason != wfTypes.StatusReasonFailedAfterRetries && sub.Reason != wfTypes.StatusReasonTimeout { 325 steps[i].SubStepsStatus[j].Reason = wfTypes.StatusReasonTerminate 326 } 327 case v1alpha1.WorkflowStepPhaseRunning, v1alpha1.WorkflowStepPhaseSuspending: 328 steps[i].SubStepsStatus[j].Phase = v1alpha1.WorkflowStepPhaseFailed 329 steps[i].SubStepsStatus[j].Reason = wfTypes.StatusReasonTerminate 330 default: 331 } 332 } 333 } 334 335 if err := retry.RetryOnConflict(retry.DefaultBackoff, func() error { 336 return cli.Status().Patch(ctx, run, client.Merge) 337 }); err != nil { 338 return err 339 } 340 return nil 341 } 342 343 // RestartFromStep restart workflow from a failed step 344 func RestartFromStep(ctx context.Context, cli client.Client, run *v1alpha1.WorkflowRun, stepName string) error { 345 if stepName == "" { 346 return fmt.Errorf("step name can not be empty") 347 } 348 run.Status.Terminated = false 349 run.Status.Suspend = false 350 run.Status.Finished = false 351 if !run.Status.EndTime.IsZero() { 352 run.Status.EndTime = metav1.Time{} 353 } 354 mode := run.Status.Mode 355 356 var steps []v1alpha1.WorkflowStep 357 if run.Spec.WorkflowSpec != nil { 358 steps = run.Spec.WorkflowSpec.Steps 359 } else { 360 workflow := &v1alpha1.Workflow{} 361 if err := cli.Get(ctx, client.ObjectKey{Namespace: run.Namespace, Name: run.Spec.WorkflowRef}, workflow); err != nil { 362 return err 363 } 364 steps = workflow.Steps 365 } 366 367 cm := &corev1.ConfigMap{} 368 if run.Status.ContextBackend != nil { 369 if err := cli.Get(ctx, client.ObjectKey{Namespace: run.Namespace, Name: run.Status.ContextBackend.Name}, cm); err != nil { 370 return err 371 } 372 } 373 stepStatus, cm, err := CleanStatusFromStep(steps, run.Status.Steps, mode, cm, stepName) 374 if err != nil { 375 return err 376 } 377 run.Status.Steps = stepStatus 378 if err := retry.RetryOnConflict(retry.DefaultBackoff, func() error { 379 return cli.Status().Update(ctx, run) 380 }); err != nil { 381 return err 382 } 383 if err := retry.RetryOnConflict(retry.DefaultBackoff, func() error { 384 return cli.Update(ctx, cm) 385 }); err != nil { 386 return err 387 } 388 389 return nil 390 } 391 392 // CleanStatusFromStep cleans status and context data from a specified step 393 func CleanStatusFromStep(steps []v1alpha1.WorkflowStep, stepStatus []v1alpha1.WorkflowStepStatus, mode v1alpha1.WorkflowExecuteMode, contextCM *corev1.ConfigMap, stepName string) ([]v1alpha1.WorkflowStepStatus, *corev1.ConfigMap, error) { 394 found := false 395 dependency := make([]string, 0) 396 for i, step := range stepStatus { 397 if step.Name == stepName { 398 if step.Phase != v1alpha1.WorkflowStepPhaseFailed { 399 return nil, nil, fmt.Errorf("can not restart from a non-failed step") 400 } 401 dependency = getStepDependency(steps, stepName, mode.Steps == v1alpha1.WorkflowModeDAG) 402 stepStatus = deleteStepStatus(dependency, stepStatus, stepName, false) 403 found = true 404 break 405 } 406 for _, sub := range step.SubStepsStatus { 407 if sub.Name == stepName { 408 if sub.Phase != v1alpha1.WorkflowStepPhaseFailed { 409 return nil, nil, fmt.Errorf("can not restart from a non-failed step") 410 } 411 subDependency := getStepDependency(steps, stepName, mode.SubSteps == v1alpha1.WorkflowModeDAG) 412 stepStatus[i].SubStepsStatus = deleteSubStepStatus(subDependency, step.SubStepsStatus, stepName) 413 stepStatus[i].Phase = v1alpha1.WorkflowStepPhaseRunning 414 stepStatus[i].Reason = "" 415 stepDependency := getStepDependency(steps, step.Name, mode.Steps == v1alpha1.WorkflowModeDAG) 416 stepStatus = deleteStepStatus(stepDependency, stepStatus, stepName, true) 417 dependency = mergeUniqueStringSlice(subDependency, stepDependency) 418 found = true 419 break 420 } 421 } 422 } 423 if !found { 424 return nil, nil, fmt.Errorf("failed step %s not found", stepName) 425 } 426 if contextCM != nil && contextCM.Data != nil { 427 v, err := value.NewValue(contextCM.Data[wfContext.ConfigMapKeyVars], nil, "") 428 if err != nil { 429 return nil, nil, err 430 } 431 s, err := clearContextVars(steps, v, stepName, dependency) 432 if err != nil { 433 return nil, nil, err 434 } 435 contextCM.Data[wfContext.ConfigMapKeyVars] = s 436 } 437 return stepStatus, contextCM, nil 438 } 439 440 // nolint:staticcheck 441 func clearContextVars(steps []v1alpha1.WorkflowStep, v *value.Value, stepName string, dependency []string) (string, error) { 442 outputs := make([]string, 0) 443 for _, step := range steps { 444 if step.Name == stepName || stringsContain(dependency, step.Name) { 445 for _, output := range step.Outputs { 446 outputs = append(outputs, output.Name) 447 } 448 } 449 for _, sub := range step.SubSteps { 450 if sub.Name == stepName || stringsContain(dependency, sub.Name) { 451 for _, output := range sub.Outputs { 452 outputs = append(outputs, output.Name) 453 } 454 } 455 } 456 } 457 node := v.CueValue().Syntax(cue.ResolveReferences(true)) 458 x, ok := node.(*ast.StructLit) 459 if !ok { 460 return "", fmt.Errorf("value is not a struct lit") 461 } 462 element := make([]ast.Decl, 0) 463 for i := range x.Elts { 464 if field, ok := x.Elts[i].(*ast.Field); ok { 465 label := strings.Trim(sets.LabelStr(field.Label), `"`) 466 if !stringsContain(outputs, label) { 467 element = append(element, field) 468 } 469 } 470 } 471 x.Elts = element 472 b, err := format.Node(x) 473 if err != nil { 474 return "", err 475 } 476 return string(b), nil 477 } 478 479 func deleteStepStatus(dependency []string, steps []v1alpha1.WorkflowStepStatus, stepName string, group bool) []v1alpha1.WorkflowStepStatus { 480 status := make([]v1alpha1.WorkflowStepStatus, 0) 481 for _, step := range steps { 482 if group && !stringsContain(dependency, step.Name) { 483 status = append(status, step) 484 continue 485 } 486 if !group && !stringsContain(dependency, step.Name) && step.Name != stepName { 487 status = append(status, step) 488 } 489 } 490 return status 491 } 492 493 func deleteSubStepStatus(dependency []string, subSteps []v1alpha1.StepStatus, stepName string) []v1alpha1.StepStatus { 494 status := make([]v1alpha1.StepStatus, 0) 495 for _, step := range subSteps { 496 if !stringsContain(dependency, step.Name) && step.Name != stepName { 497 status = append(status, step) 498 } 499 } 500 return status 501 } 502 503 func stringsContain(items []string, source string) bool { 504 for _, item := range items { 505 if item == source { 506 return true 507 } 508 } 509 return false 510 } 511 512 func getStepDependency(steps []v1alpha1.WorkflowStep, stepName string, dag bool) []string { 513 if !dag { 514 dependency := make([]string, 0) 515 for i, step := range steps { 516 if step.Name == stepName { 517 for index := i + 1; index < len(steps); index++ { 518 dependency = append(dependency, steps[index].Name) 519 } 520 return dependency 521 } 522 for j, sub := range step.SubSteps { 523 if sub.Name == stepName { 524 for index := j + 1; index < len(step.SubSteps); index++ { 525 dependency = append(dependency, step.SubSteps[index].Name) 526 } 527 return dependency 528 } 529 } 530 } 531 return dependency 532 } 533 dependsOn := make(map[string][]string) 534 stepOutputs := make(map[string]string) 535 for _, step := range steps { 536 for _, output := range step.Outputs { 537 stepOutputs[output.Name] = step.Name 538 } 539 dependsOn[step.Name] = step.DependsOn 540 for _, sub := range step.SubSteps { 541 for _, output := range sub.Outputs { 542 stepOutputs[output.Name] = sub.Name 543 } 544 dependsOn[sub.Name] = sub.DependsOn 545 } 546 } 547 for _, step := range steps { 548 for _, input := range step.Inputs { 549 if name, ok := stepOutputs[input.From]; ok && !stringsContain(dependsOn[step.Name], name) { 550 dependsOn[step.Name] = append(dependsOn[step.Name], name) 551 } 552 } 553 for _, sub := range step.SubSteps { 554 for _, input := range sub.Inputs { 555 if name, ok := stepOutputs[input.From]; ok && !stringsContain(dependsOn[sub.Name], name) { 556 dependsOn[sub.Name] = append(dependsOn[sub.Name], name) 557 } 558 } 559 } 560 } 561 return findDependency(stepName, dependsOn) 562 } 563 564 func mergeUniqueStringSlice(a, b []string) []string { 565 for _, item := range b { 566 if !stringsContain(a, item) { 567 a = append(a, item) 568 } 569 } 570 return a 571 } 572 573 func findDependency(stepName string, dependsOn map[string][]string) []string { 574 dependency := make([]string, 0) 575 for step, deps := range dependsOn { 576 for _, dep := range deps { 577 if dep == stepName { 578 dependency = append(dependency, step) 579 dependency = append(dependency, findDependency(step, dependsOn)...) 580 } 581 } 582 } 583 return dependency 584 } 585 586 func writeOutputF(outputWriter io.Writer, format string, a ...interface{}) error { 587 if outputWriter == nil { 588 return nil 589 } 590 _, err := fmt.Fprintf(outputWriter, format, a...) 591 return err 592 }