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  }