github.com/oam-dev/kubevela@v1.9.11/pkg/workflow/operation/operation.go (about) 1 /* 2 Copyright 2021 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 operation 18 19 import ( 20 "context" 21 "fmt" 22 "io" 23 24 "github.com/pkg/errors" 25 corev1 "k8s.io/api/core/v1" 26 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 27 "k8s.io/apimachinery/pkg/types" 28 "k8s.io/client-go/util/retry" 29 "sigs.k8s.io/controller-runtime/pkg/client" 30 31 workflowv1alpha1 "github.com/kubevela/workflow/api/v1alpha1" 32 wfTypes "github.com/kubevela/workflow/pkg/types" 33 wfUtils "github.com/kubevela/workflow/pkg/utils" 34 35 "github.com/oam-dev/kubevela/apis/core.oam.dev/common" 36 "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" 37 "github.com/oam-dev/kubevela/pkg/appfile" 38 "github.com/oam-dev/kubevela/pkg/controller/core.oam.dev/v1beta1/application" 39 "github.com/oam-dev/kubevela/pkg/controller/utils" 40 "github.com/oam-dev/kubevela/pkg/oam" 41 "github.com/oam-dev/kubevela/pkg/resourcetracker" 42 "github.com/oam-dev/kubevela/pkg/rollout" 43 kubevelaapp "github.com/oam-dev/kubevela/pkg/utils/app" 44 errors3 "github.com/oam-dev/kubevela/pkg/utils/errors" 45 ) 46 47 // NewApplicationWorkflowOperator get an workflow operator with k8sClient, ioWriter(optional, useful for cli) and application 48 func NewApplicationWorkflowOperator(cli client.Client, w io.Writer, app *v1beta1.Application) wfUtils.WorkflowOperator { 49 return appWorkflowOperator{ 50 cli: cli, 51 outputWriter: w, 52 application: app, 53 } 54 } 55 56 // NewApplicationWorkflowStepOperator get an workflow step operator with k8sClient, ioWriter(optional, useful for cli) and application 57 func NewApplicationWorkflowStepOperator(cli client.Client, w io.Writer, app *v1beta1.Application) wfUtils.WorkflowStepOperator { 58 return appWorkflowStepOperator{ 59 cli: cli, 60 outputWriter: w, 61 application: app, 62 } 63 } 64 65 type appWorkflowOperator struct { 66 cli client.Client 67 outputWriter io.Writer 68 application *v1beta1.Application 69 } 70 71 type appWorkflowStepOperator struct { 72 cli client.Client 73 outputWriter io.Writer 74 application *v1beta1.Application 75 } 76 77 // Suspend a running workflow 78 func (wo appWorkflowOperator) Suspend(ctx context.Context) error { 79 app := wo.application 80 if app.Status.Workflow == nil { 81 return fmt.Errorf("the workflow in application is not running") 82 } 83 var err error 84 if err = rollout.SuspendRollout(ctx, wo.cli, app, wo.outputWriter); err != nil { 85 return err 86 } 87 if err := SuspendWorkflow(ctx, wo.cli, app, ""); err != nil { 88 return err 89 } 90 return writeOutputF(wo.outputWriter, "Successfully suspend workflow: %s\n", app.Name) 91 } 92 93 // Suspend a suspending workflow 94 func (wo appWorkflowStepOperator) Suspend(ctx context.Context, step string) error { 95 if step == "" { 96 return fmt.Errorf("step can not be empty") 97 } 98 app := wo.application 99 if app.Status.Workflow == nil { 100 return fmt.Errorf("the workflow in application is not running") 101 } 102 if app.Status.Workflow.Terminated { 103 return fmt.Errorf("can not suspend a terminated workflow") 104 } 105 106 if err := SuspendWorkflow(ctx, wo.cli, app, step); err != nil { 107 return err 108 } 109 return writeOutputF(wo.outputWriter, "Successfully suspend workflow %s from step %s \n", app.Name, step) 110 } 111 112 // SuspendWorkflow suspend workflow 113 func SuspendWorkflow(ctx context.Context, kubecli client.Client, app *v1beta1.Application, stepName string) error { 114 app.Status.Workflow.Suspend = true 115 steps := app.Status.Workflow.Steps 116 found := stepName == "" 117 118 for i, step := range steps { 119 for j, sub := range step.SubStepsStatus { 120 if sub.Phase != workflowv1alpha1.WorkflowStepPhaseRunning { 121 continue 122 } 123 if stepName == "" { 124 wfUtils.OperateSteps(steps, i, j, workflowv1alpha1.WorkflowStepPhaseSuspending) 125 } else if stepName == sub.Name { 126 wfUtils.OperateSteps(steps, i, j, workflowv1alpha1.WorkflowStepPhaseSuspending) 127 found = true 128 break 129 } 130 } 131 if step.Phase != workflowv1alpha1.WorkflowStepPhaseRunning { 132 continue 133 } 134 if stepName == "" { 135 wfUtils.OperateSteps(steps, i, -1, workflowv1alpha1.WorkflowStepPhaseSuspending) 136 } else if stepName == step.Name { 137 wfUtils.OperateSteps(steps, i, -1, workflowv1alpha1.WorkflowStepPhaseSuspending) 138 found = true 139 break 140 } 141 } 142 if !found { 143 return fmt.Errorf("can not find step %s", stepName) 144 } 145 if err := retry.RetryOnConflict(retry.DefaultBackoff, func() error { 146 return kubecli.Status().Patch(ctx, app, client.Merge) 147 }); err != nil { 148 return err 149 } 150 return nil 151 } 152 153 // Resume a suspending workflow 154 func (wo appWorkflowOperator) Resume(ctx context.Context) error { 155 app := wo.application 156 if app.Status.Workflow == nil { 157 return fmt.Errorf("the workflow in application is not running") 158 } 159 if app.Status.Workflow.Terminated { 160 return fmt.Errorf("can not resume a terminated workflow") 161 } 162 163 var rolloutResumed bool 164 var err error 165 166 if rolloutResumed, err = rollout.ResumeRollout(ctx, wo.cli, app, wo.outputWriter); err != nil { 167 return err 168 } 169 if !rolloutResumed && !app.Status.Workflow.Suspend { 170 return writeOutputF(wo.outputWriter, "workflow %s is not suspended.\n", app.Name) 171 } 172 173 if app.Status.Workflow.Suspend { 174 if err = ResumeWorkflow(ctx, wo.cli, app, ""); err != nil { 175 return err 176 } 177 } 178 return writeOutputF(wo.outputWriter, "Successfully resume workflow: %s\n", app.Name) 179 } 180 181 // Resume a suspending workflow 182 func (wo appWorkflowStepOperator) Resume(ctx context.Context, step string) error { 183 if step == "" { 184 return fmt.Errorf("step can not be empty") 185 } 186 app := wo.application 187 if app.Status.Workflow == nil { 188 return fmt.Errorf("the workflow in application is not running") 189 } 190 if app.Status.Workflow.Terminated { 191 return fmt.Errorf("can not resume a terminated workflow") 192 } 193 194 if !app.Status.Workflow.Suspend { 195 return writeOutputF(wo.outputWriter, "workflow %s is not suspended.\n", app.Name) 196 } 197 198 if app.Status.Workflow.Suspend { 199 if err := ResumeWorkflow(ctx, wo.cli, app, step); err != nil { 200 return err 201 } 202 } 203 return writeOutputF(wo.outputWriter, "Successfully resume workflow %s from step %s \n", app.Name, step) 204 } 205 206 // ResumeWorkflow resume workflow 207 func ResumeWorkflow(ctx context.Context, kubecli client.Client, app *v1beta1.Application, stepName string) error { 208 app.Status.Workflow.Suspend = false 209 steps := app.Status.Workflow.Steps 210 found := stepName == "" 211 212 for i, step := range steps { 213 for j, sub := range step.SubStepsStatus { 214 if sub.Phase != workflowv1alpha1.WorkflowStepPhaseSuspending { 215 continue 216 } 217 if stepName == "" { 218 wfUtils.OperateSteps(steps, i, j, workflowv1alpha1.WorkflowStepPhaseRunning) 219 } else if stepName == sub.Name { 220 wfUtils.OperateSteps(steps, i, j, workflowv1alpha1.WorkflowStepPhaseRunning) 221 found = true 222 break 223 } 224 } 225 if step.Phase != workflowv1alpha1.WorkflowStepPhaseSuspending { 226 continue 227 } 228 if stepName == "" { 229 wfUtils.OperateSteps(steps, i, -1, workflowv1alpha1.WorkflowStepPhaseRunning) 230 } else if stepName == step.Name { 231 wfUtils.OperateSteps(steps, i, -1, workflowv1alpha1.WorkflowStepPhaseRunning) 232 found = true 233 break 234 } 235 } 236 237 if !found { 238 return fmt.Errorf("can not find step %s", stepName) 239 } 240 if err := kubecli.Status().Patch(ctx, app, client.Merge); err != nil { 241 return err 242 } 243 return nil 244 } 245 246 // Rollback a running in middle state workflow. 247 // nolint 248 func (wo appWorkflowOperator) Rollback(ctx context.Context) error { 249 app := wo.application 250 if app.Status.Workflow != nil && !app.Status.Workflow.Terminated && !app.Status.Workflow.Suspend && !app.Status.Workflow.Finished { 251 return fmt.Errorf("can not rollback a running workflow") 252 } 253 if oam.GetPublishVersion(app) == "" { 254 if app.Status.LatestRevision == nil || app.Status.LatestRevision.Name == "" { 255 return fmt.Errorf("the latest revision is not set: %s", app.Name) 256 } 257 // get the last revision 258 revision := &v1beta1.ApplicationRevision{} 259 if err := wo.cli.Get(ctx, types.NamespacedName{Name: app.Status.LatestRevision.Name, Namespace: app.Namespace}, revision); err != nil { 260 return fmt.Errorf("failed to get the latest revision: %w", err) 261 } 262 263 app.Spec = revision.Spec.Application.Spec 264 if err := wo.cli.Status().Update(ctx, app); err != nil { 265 return err 266 } 267 268 fmt.Printf("Successfully rollback workflow to the latest revision: %s\n", app.Name) 269 return nil 270 } 271 272 appRevs, err := application.GetSortedAppRevisions(ctx, wo.cli, app.Name, app.Namespace) 273 if err != nil { 274 return errors.Wrapf(err, "failed to list revisions for application %s/%s", app.Namespace, app.Name) 275 } 276 277 // find succeeded revision to rollback 278 var rev *v1beta1.ApplicationRevision 279 var outdatedRev []*v1beta1.ApplicationRevision 280 for i := range appRevs { 281 candidate := appRevs[len(appRevs)-i-1] 282 _rev := candidate.DeepCopy() 283 if !candidate.Status.Succeeded || oam.GetPublishVersion(_rev) == "" { 284 outdatedRev = append(outdatedRev, _rev) 285 continue 286 } 287 rev = _rev 288 break 289 } 290 if rev == nil { 291 return errors.Errorf("failed to find previous succeeded revision for application %s/%s", app.Namespace, app.Name) 292 } 293 publishVersion := oam.GetPublishVersion(rev) 294 revisionNumber, err := utils.ExtractRevision(rev.Name) 295 if err != nil { 296 return errors.Wrapf(err, "failed to extract revision number from revision %s", rev.Name) 297 } 298 _, currentRT, historyRTs, _, err := resourcetracker.ListApplicationResourceTrackers(ctx, wo.cli, app) 299 if err != nil { 300 return errors.Wrapf(err, "failed to list resource trackers for application %s/%s", app.Namespace, app.Name) 301 } 302 var matchRT *v1beta1.ResourceTracker 303 for _, rt := range append(historyRTs, currentRT) { 304 if rt == nil { 305 continue 306 } 307 labels := rt.GetLabels() 308 if labels != nil && labels[oam.LabelAppRevision] == rev.Name { 309 matchRT = rt.DeepCopy() 310 } 311 } 312 if matchRT == nil { 313 return errors.Errorf("cannot find resource tracker for previous revision %s, unable to rollback", rev.Name) 314 } 315 if matchRT.DeletionTimestamp != nil { 316 return errors.Errorf("previous revision %s is being recycled, unable to rollback", rev.Name) 317 } 318 err = wo.writeOutput("Find succeeded application revision %s (PublishVersion: %s) to rollback.\n") 319 if err != nil { 320 return err 321 } 322 appKey := client.ObjectKeyFromObject(app) 323 // rollback application spec and freeze 324 controllerRequirement, err := kubevelaapp.FreezeApplication(ctx, wo.cli, app, func() { 325 app.Spec = rev.Spec.Application.Spec 326 oam.SetPublishVersion(app, publishVersion) 327 }) 328 if err != nil { 329 return errors.Wrapf(err, "failed to rollback application spec to revision %s (PublishVersion: %s)", rev.Name, publishVersion) 330 } 331 err = writeOutputF(wo.outputWriter, "Application spec rollback successfully.\n") 332 if err != nil { 333 return err 334 } 335 // rollback application status 336 if err = retry.RetryOnConflict(retry.DefaultBackoff, func() error { 337 if err = wo.cli.Get(ctx, appKey, app); err != nil { 338 return err 339 } 340 app.Status.Workflow = rev.Status.Workflow 341 app.Status.Services = []common.ApplicationComponentStatus{} 342 app.Status.AppliedResources = []common.ClusterObjectReference{} 343 for _, rsc := range matchRT.Spec.ManagedResources { 344 app.Status.AppliedResources = append(app.Status.AppliedResources, rsc.ClusterObjectReference) 345 } 346 app.Status.LatestRevision = &common.Revision{ 347 Name: rev.Name, 348 Revision: int64(revisionNumber), 349 RevisionHash: rev.GetLabels()[oam.LabelAppRevisionHash], 350 } 351 return wo.cli.Status().Update(ctx, app) 352 }); err != nil { 353 return errors.Wrapf(err, "failed to rollback application status to revision %s (PublishVersion: %s)", rev.Name, publishVersion) 354 } 355 356 err = writeOutputF(wo.outputWriter, "Application status rollback successfully.\n") 357 if err != nil { 358 return err 359 } 360 // update resource tracker generation 361 matchRTKey := client.ObjectKeyFromObject(matchRT) 362 if err = retry.RetryOnConflict(retry.DefaultBackoff, func() error { 363 if err = wo.cli.Get(ctx, matchRTKey, matchRT); err != nil { 364 return err 365 } 366 matchRT.Spec.ApplicationGeneration = app.Generation 367 return wo.cli.Update(ctx, matchRT) 368 }); err != nil { 369 return errors.Wrapf(err, "failed to update application generation in resource tracker") 370 } 371 372 // unfreeze application 373 if err = kubevelaapp.UnfreezeApplication(ctx, wo.cli, app, nil, controllerRequirement); err != nil { 374 return errors.Wrapf(err, "failed to resume application to restart") 375 } 376 377 rollback, err := rollout.RollbackRollout(ctx, wo.cli, app, wo.outputWriter) 378 if err != nil { 379 return err 380 } 381 382 if rollback { 383 err = writeOutputF(wo.outputWriter, "Successfully rollback app.\n") 384 if err != nil { 385 return err 386 } 387 } 388 389 // clean up outdated revisions 390 var errs errors3.ErrorList 391 for _, _rev := range outdatedRev { 392 if err = wo.cli.Delete(ctx, _rev); err != nil { 393 errs = append(errs, err) 394 } 395 } 396 if errs.HasError() { 397 return errors.Wrapf(errs, "failed to clean up outdated revisions") 398 } 399 400 err = writeOutputF(wo.outputWriter, "Application outdated revision cleaned up.\n") 401 if err != nil { 402 return err 403 } 404 return nil 405 } 406 407 // Restart a terminated or finished workflow. 408 func (wo appWorkflowOperator) Restart(ctx context.Context) error { 409 app := wo.application 410 status := app.Status.Workflow 411 if status == nil { 412 return fmt.Errorf("the workflow in application is not running") 413 } 414 // reset the workflow status to restart the workflow 415 app.Status.Workflow = nil 416 417 if err := wo.cli.Status().Update(ctx, app); err != nil { 418 return err 419 } 420 421 return writeOutputF(wo.outputWriter, "Successfully restart workflow: %s\n", app.Name) 422 } 423 424 // Restart a terminated or finished workflow. 425 func (wo appWorkflowStepOperator) Restart(ctx context.Context, step string) error { 426 if step == "" { 427 return fmt.Errorf("step can not be empty") 428 } 429 app := wo.application 430 status := app.Status.Workflow 431 if status == nil { 432 return fmt.Errorf("the workflow in application is not running") 433 } 434 status.Terminated = false 435 status.Suspend = false 436 status.Finished = false 437 if !status.EndTime.IsZero() { 438 status.EndTime = metav1.Time{} 439 } 440 var cm *corev1.ConfigMap 441 if status.ContextBackend != nil { 442 if err := wo.cli.Get(ctx, client.ObjectKey{Namespace: app.Namespace, Name: status.ContextBackend.Name}, cm); err != nil { 443 return err 444 } 445 } 446 appParser := appfile.NewApplicationParser(wo.cli, nil) 447 appFile, err := appParser.GenerateAppFile(ctx, app) 448 if err != nil { 449 return fmt.Errorf("failed to parse appfile: %w", err) 450 } 451 stepStatus, cm, err := wfUtils.CleanStatusFromStep(appFile.WorkflowSteps, status.Steps, *appFile.WorkflowMode, cm, step) 452 if err != nil { 453 return err 454 } 455 status.Steps = stepStatus 456 if err := retry.RetryOnConflict(retry.DefaultBackoff, func() error { 457 return wo.cli.Status().Update(ctx, app) 458 }); err != nil { 459 return err 460 } 461 if cm != nil { 462 if err := retry.RetryOnConflict(retry.DefaultBackoff, func() error { 463 return wo.cli.Update(ctx, cm) 464 }); err != nil { 465 return err 466 } 467 } 468 return writeOutputF(wo.outputWriter, "Successfully restart workflow %s from step %s\n", app.Name, step) 469 } 470 471 func (wo appWorkflowOperator) Terminate(ctx context.Context) error { 472 app := wo.application 473 if err := TerminateWorkflow(ctx, wo.cli, app); err != nil { 474 return err 475 } 476 477 return writeOutputF(wo.outputWriter, "Successfully terminate workflow: %s\n", app.Name) 478 } 479 480 // TerminateWorkflow terminate workflow 481 func TerminateWorkflow(ctx context.Context, kubecli client.Client, app *v1beta1.Application) error { 482 // set the workflow terminated to true 483 app.Status.Workflow.Terminated = true 484 // set the workflow suspend to false 485 app.Status.Workflow.Suspend = false 486 steps := app.Status.Workflow.Steps 487 for i, step := range steps { 488 switch step.Phase { 489 case workflowv1alpha1.WorkflowStepPhaseFailed: 490 if step.Reason != wfTypes.StatusReasonFailedAfterRetries && step.Reason != wfTypes.StatusReasonTimeout { 491 steps[i].Reason = wfTypes.StatusReasonTerminate 492 } 493 case workflowv1alpha1.WorkflowStepPhaseRunning, workflowv1alpha1.WorkflowStepPhaseSuspending: 494 steps[i].Phase = workflowv1alpha1.WorkflowStepPhaseFailed 495 steps[i].Reason = wfTypes.StatusReasonTerminate 496 default: 497 } 498 for j, sub := range step.SubStepsStatus { 499 switch sub.Phase { 500 case workflowv1alpha1.WorkflowStepPhaseFailed: 501 if sub.Reason != wfTypes.StatusReasonFailedAfterRetries && sub.Reason != wfTypes.StatusReasonTimeout { 502 steps[i].SubStepsStatus[j].Reason = wfTypes.StatusReasonTerminate 503 } 504 case workflowv1alpha1.WorkflowStepPhaseRunning, workflowv1alpha1.WorkflowStepPhaseSuspending: 505 steps[i].SubStepsStatus[j].Phase = workflowv1alpha1.WorkflowStepPhaseFailed 506 steps[i].SubStepsStatus[j].Reason = wfTypes.StatusReasonTerminate 507 default: 508 } 509 } 510 } 511 512 if err := kubecli.Status().Patch(ctx, app, client.Merge); err != nil { 513 return err 514 } 515 return nil 516 } 517 518 func (wo appWorkflowOperator) writeOutput(str string) error { 519 if wo.outputWriter == nil { 520 return nil 521 } 522 _, err := wo.outputWriter.Write([]byte(str)) 523 return err 524 } 525 526 func writeOutputF(outputWriter io.Writer, format string, a ...interface{}) error { 527 if outputWriter == nil { 528 return nil 529 } 530 _, err := fmt.Fprintf(outputWriter, format, a...) 531 return err 532 }