diff --git a/api/api.go b/api/api.go index 6784414..47597fc 100644 --- a/api/api.go +++ b/api/api.go @@ -162,13 +162,14 @@ type ( } PollStepResponse struct { - Exited bool `json:"exited,omitempty"` - ExitCode int `json:"exit_code,omitempty"` - Error string `json:"error,omitempty"` - OOMKilled bool `json:"oom_killed,omitempty"` - Outputs map[string]string `json:"outputs,omitempty"` - Artifact []byte `json:"artifact,omitempty"` - OutputV2 []*OutputV2 `json:"outputV2,omitempty"` + Exited bool `json:"exited,omitempty"` + ExitCode int `json:"exit_code,omitempty"` + Error string `json:"error,omitempty"` + OOMKilled bool `json:"oom_killed,omitempty"` + Outputs map[string]string `json:"outputs,omitempty"` + Artifact []byte `json:"artifact,omitempty"` + OutputV2 []*OutputV2 `json:"outputV2,omitempty"` + OptimizationState string `json:"optimization_state,omitempty"` } StreamOutputRequest struct { diff --git a/pipeline/runtime/common.go b/pipeline/runtime/common.go index 3ca7069..903df53 100644 --- a/pipeline/runtime/common.go +++ b/pipeline/runtime/common.go @@ -12,6 +12,7 @@ import ( "os" "strings" + "github.com/drone/runner-go/pipeline/runtime" "github.com/harness/godotenv/v3" "github.com/harness/harness-docker-runner/api" "github.com/harness/harness-docker-runner/engine" @@ -209,3 +210,11 @@ func IsFeatureFlagEnabled(featureFlagName string, engine *engine.Engine, step *s val, ok := step.Envs[featureFlagName] return ok && val == trueValue } + +// checkStepSuccess checks if the step was successful based on the return values +func checkStepSuccess(state *runtime.State, err error) bool { + if err == nil && state != nil && state.ExitCode == 0 && state.Exited { + return true + } + return false +} diff --git a/pipeline/runtime/run.go b/pipeline/runtime/run.go index d0be428..3143d2a 100644 --- a/pipeline/runtime/run.go +++ b/pipeline/runtime/run.go @@ -19,16 +19,20 @@ import ( "github.com/harness/harness-docker-runner/pipeline" tiCfg "github.com/harness/lite-engine/ti/config" "github.com/harness/lite-engine/ti/report" + "github.com/harness/lite-engine/ti/savings" + "github.com/harness/ti-client/types" ) func executeRunStep(ctx context.Context, engine *engine.Engine, r *api.StartStepRequest, out io.Writer, tiConfig *tiCfg.Cfg) ( - *runtime.State, map[string]string, []byte, []*api.OutputV2, error) { + *runtime.State, map[string]string, []byte, []*api.OutputV2, string, error) { + start := time.Now() step := toStep(r) step.Command = r.Run.Command step.Entrypoint = r.Run.Entrypoint + optimizationState := types.DISABLED if (len(r.OutputVars) > 0 || len(r.Outputs) > 0) && (len(step.Entrypoint) == 0 || len(step.Command) == 0) { - return nil, nil, nil, nil, fmt.Errorf("output variable should not be set for unset entrypoint or command") + return nil, nil, nil, nil, string(optimizationState), fmt.Errorf("output variable should not be set for unset entrypoint or command") } enablePluginOutputSecrets := IsFeatureFlagEnabled(ciEnablePluginOutputSecrets, engine, step) @@ -63,11 +67,17 @@ func executeRunStep(ctx context.Context, engine *engine.Engine, r *api.StartStep step.Envs["PLUGIN_ARTIFACT_FILE"] = artifactFile exited, err := engine.Run(ctx, step, out) + timeTakenMs := time.Since(start).Milliseconds() logrus.WithField("step_id", r.ID).WithField("stage_id", r.StageRuntimeID).Traceln("completed step run") if rerr := report.ParseAndUploadTests(ctx, r.TestReport, r.WorkingDir, step.Name, log, time.Now(), tiConfig, r.Envs); rerr != nil { logrus.WithError(rerr).WithField("step", step.Name).Errorln("failed to upload report") } + // Parse and upload savings to TI + if tiConfig.GetParseSavings() { + optimizationState = savings.ParseAndUploadSavings(ctx, r.WorkingDir, log, step.Name, checkStepSuccess(exited, err), timeTakenMs, tiConfig, r.Envs) + } + artifact, _ := fetchArtifactDataFromArtifactFile(artifactFile, out) if exited != nil && exited.Exited && exited.ExitCode == 0 { if enablePluginOutputSecrets { @@ -127,12 +137,12 @@ func executeRunStep(ctx context.Context, engine *engine.Engine, r *api.StartStep } } - return exited, outputs, artifact, outputsV2, finalErr + return exited, outputs, artifact, outputsV2, string(optimizationState), finalErr } else { outputs, err := fetchOutputVariables(outputFile, out, false) // nolint:govet if err != nil { - return exited, nil, nil, nil, err + return exited, nil, nil, nil, string(optimizationState), err } // Delete output variable file if ferr := os.Remove(outputFile); ferr != nil { @@ -149,11 +159,11 @@ func executeRunStep(ctx context.Context, engine *engine.Engine, r *api.StartStep }) } } - return exited, outputs, artifact, outputsV2, err + return exited, outputs, artifact, outputsV2, string(optimizationState), err } - return exited, outputs, artifact, nil, err + return exited, outputs, artifact, nil, string(optimizationState), err } } - return exited, nil, artifact, nil, err + return exited, nil, artifact, nil, string(optimizationState), err } diff --git a/pipeline/runtime/runtest.go b/pipeline/runtime/runtest.go index c8d2fa2..ebd217b 100644 --- a/pipeline/runtime/runtest.go +++ b/pipeline/runtime/runtest.go @@ -18,6 +18,8 @@ import ( tiCfg "github.com/harness/lite-engine/ti/config" "github.com/harness/lite-engine/ti/instrumentation" "github.com/harness/lite-engine/ti/report" + "github.com/harness/lite-engine/ti/savings" + "github.com/harness/ti-client/types" "github.com/sirupsen/logrus" ) @@ -26,14 +28,14 @@ const ( ) func executeRunTestStep(ctx context.Context, engine *engine.Engine, r *api.StartStepRequest, out io.Writer, tiConfig *tiCfg.Cfg) ( - *runtime.State, map[string]string, []byte, []*api.OutputV2, error) { + *runtime.State, map[string]string, []byte, []*api.OutputV2, string, error) { + start := time.Now() log := logrus.New() log.Out = out - - start := time.Now() + optimizationState := types.DISABLED cmd, err := instrumentation.GetCmd(ctx, &r.RunTest, r.Name, r.WorkingDir, log, r.Envs, tiConfig) if err != nil { - return nil, nil, nil, nil, err + return nil, nil, nil, nil, string(optimizationState), err } step := toStep(r) @@ -41,7 +43,7 @@ func executeRunTestStep(ctx context.Context, engine *engine.Engine, r *api.Start step.Entrypoint = r.RunTest.Entrypoint if (len(r.OutputVars) > 0 || len(r.Outputs) > 0) && len(step.Entrypoint) == 0 || len(step.Command) == 0 { - return nil, nil, nil, nil, fmt.Errorf("output variable should not be set for unset entrypoint or command") + return nil, nil, nil, nil, string(optimizationState), fmt.Errorf("output variable should not be set for unset entrypoint or command") } enablePluginOutputSecrets := IsFeatureFlagEnabled(ciEnablePluginOutputSecrets, engine, step) @@ -63,6 +65,7 @@ func executeRunTestStep(ctx context.Context, engine *engine.Engine, r *api.Start step.Envs["PLUGIN_ARTIFACT_FILE"] = artifactFile exited, err := engine.Run(ctx, step, out) + timeTakenMs := time.Since(start).Milliseconds() if rerr := report.ParseAndUploadTests(ctx, r.TestReport, r.WorkingDir, step.Name, log, time.Now(), tiConfig, r.Envs); rerr != nil { log.WithError(rerr).Errorln("failed to upload report") } @@ -71,6 +74,11 @@ func executeRunTestStep(ctx context.Context, engine *engine.Engine, r *api.Start log.WithError(uerr).Errorln("unable to collect callgraph") } + // Parse and upload savings to TI + if tiConfig.GetParseSavings() { + optimizationState = savings.ParseAndUploadSavings(ctx, r.WorkingDir, log, step.Name, checkStepSuccess(exited, err), timeTakenMs, tiConfig, r.Envs) + } + artifact, _ := fetchArtifactDataFromArtifactFile(artifactFile, out) var outputs map[string]string var outputErr error @@ -92,7 +100,7 @@ func executeRunTestStep(ctx context.Context, engine *engine.Engine, r *api.Start }) } } - return exited, outputs, artifact, outputsV2, outputErr + return exited, outputs, artifact, outputsV2, string(optimizationState), outputErr } } else if len(r.OutputVars) > 0 { if exited != nil && exited.Exited && exited.ExitCode == 0 { @@ -101,8 +109,8 @@ func executeRunTestStep(ctx context.Context, engine *engine.Engine, r *api.Start } else { outputs, outputErr = fetchOutputVariables(outputFile, out, false) // nolint:govet } - return exited, outputs, artifact, nil, outputErr + return exited, outputs, artifact, nil, string(optimizationState), outputErr } } - return exited, nil, artifact, nil, err + return exited, nil, artifact, nil, string(optimizationState), err } diff --git a/pipeline/runtime/runtestsV2.go b/pipeline/runtime/runtestsV2.go index 5dcb1b7..bb43afd 100644 --- a/pipeline/runtime/runtestsV2.go +++ b/pipeline/runtime/runtestsV2.go @@ -21,6 +21,8 @@ import ( "github.com/harness/lite-engine/ti/callgraph" tiCfg "github.com/harness/lite-engine/ti/config" "github.com/harness/lite-engine/ti/report" + "github.com/harness/lite-engine/ti/savings" + "github.com/harness/ti-client/types" ) const ( @@ -28,15 +30,16 @@ const ( ) func executeRunTestsV2Step(ctx context.Context, engine *engine.Engine, r *api.StartStepRequest, out io.Writer, tiConfig *tiCfg.Cfg) ( - *runtime.State, map[string]string, []byte, []*api.OutputV2, error) { + *runtime.State, map[string]string, []byte, []*api.OutputV2, string, error) { + start := time.Now() log := logrus.New() log.Out = out step := toStep(r) - start := time.Now() + optimizationState := types.DISABLED step.Entrypoint = r.RunTestsV2.Entrypoint preCmd, err := leRuntime.SetupRunTestV2(ctx, &r.RunTestsV2, step.Name, r.WorkingDir, log, r.Envs, tiConfig) if err != nil { - return nil, nil, nil, nil, err + return nil, nil, nil, nil, string(optimizationState), err } command := r.RunTestsV2.Command[0] if preCmd != "" { @@ -45,7 +48,7 @@ func executeRunTestsV2Step(ctx context.Context, engine *engine.Engine, r *api.St step.Command = []string{command} if (len(r.OutputVars) > 0 || len(r.Outputs) > 0) && (len(step.Entrypoint) == 0 || len(step.Command) == 0) { - return nil, nil, nil, nil, fmt.Errorf("output variable should not be set for unset entrypoint or command") + return nil, nil, nil, nil, string(optimizationState), fmt.Errorf("output variable should not be set for unset entrypoint or command") } enablePluginOutputSecrets := IsFeatureFlagEnabled(ciEnablePluginOutputSecrets, engine, step) @@ -77,6 +80,7 @@ func executeRunTestsV2Step(ctx context.Context, engine *engine.Engine, r *api.St step.Envs["PLUGIN_ARTIFACT_FILE"] = artifactFile exited, err := engine.Run(ctx, step, out) + timeTakenMs := time.Since(start).Milliseconds() logrus.WithField("step_id", r.ID).WithField("stage_id", r.StageRuntimeID).Traceln("completed step runtestv2") if len(r.TestReport.Junit.Paths) == 0 { @@ -91,6 +95,11 @@ func executeRunTestsV2Step(ctx context.Context, engine *engine.Engine, r *api.St log.WithError(uerr).Errorln("unable to collect callgraph") } + // Parse and upload savings to TI + if tiConfig.GetParseSavings() { + optimizationState = savings.ParseAndUploadSavings(ctx, r.WorkingDir, log, step.Name, checkStepSuccess(exited, err), timeTakenMs, tiConfig, r.Envs) + } + artifact, _ := fetchArtifactDataFromArtifactFile(artifactFile, out) if exited != nil && exited.Exited && exited.ExitCode == 0 { if enablePluginOutputSecrets { @@ -150,12 +159,12 @@ func executeRunTestsV2Step(ctx context.Context, engine *engine.Engine, r *api.St } } - return exited, outputs, artifact, outputsV2, finalErr + return exited, outputs, artifact, outputsV2, string(optimizationState), finalErr } else { outputs, err := fetchOutputVariables(outputFile, out, false) // nolint:govet if err != nil { - return exited, nil, nil, nil, err + return exited, nil, nil, nil, string(optimizationState), err } // Delete output variable file if ferr := os.Remove(outputFile); ferr != nil { @@ -172,11 +181,11 @@ func executeRunTestsV2Step(ctx context.Context, engine *engine.Engine, r *api.St }) } } - return exited, outputs, artifact, outputsV2, err + return exited, outputs, artifact, outputsV2, string(optimizationState), err } - return exited, outputs, artifact, nil, err + return exited, outputs, artifact, nil, string(optimizationState), err } } - return exited, nil, artifact, nil, err + return exited, nil, artifact, nil, string(optimizationState), err } diff --git a/pipeline/runtime/step_executor.go b/pipeline/runtime/step_executor.go index 0f03688..9815cbf 100644 --- a/pipeline/runtime/step_executor.go +++ b/pipeline/runtime/step_executor.go @@ -26,12 +26,13 @@ import ( type ExecutionStatus int type StepStatus struct { - Status ExecutionStatus - State *runtime.State - StepErr error - Outputs map[string]string - Artifact []byte - OutputV2 []*api.OutputV2 + Status ExecutionStatus + State *runtime.State + StepErr error + Outputs map[string]string + Artifact []byte + OutputV2 []*api.OutputV2 + OptimizationState string } const ( @@ -74,8 +75,8 @@ func (e *StepExecutor) StartStep(ctx context.Context, r *api.StartStepRequest, s e.mu.Unlock() go func() { - state, outputs, artifact, outputV2, stepErr := e.executeStep(r, secrets, client, tiConfig) - status := StepStatus{Status: Complete, State: state, StepErr: stepErr, Outputs: outputs, Artifact: artifact, OutputV2: outputV2} + state, outputs, artifact, outputV2, optimizationState, stepErr := e.executeStep(r, secrets, client, tiConfig) + status := StepStatus{Status: Complete, State: state, StepErr: stepErr, Outputs: outputs, Artifact: artifact, OutputV2: outputV2, OptimizationState: optimizationState} e.mu.Lock() e.stepStatus[r.ID] = status channels := e.stepWaitCh[r.ID] @@ -199,7 +200,7 @@ func (e *StepExecutor) executeStepDrone(r *api.StartStepRequest, tiConfig *tiCfg r.Kind = api.Run // only this kind is supported - exited, _, _, _, err := e.run(ctx, e.engine, r, stepLog, tiConfig) + exited, _, _, _, _, err := e.run(ctx, e.engine, r, stepLog, tiConfig) if ctx.Err() == context.Canceled || ctx.Err() == context.DeadlineExceeded { logr.WithError(err).Warnln("step execution canceled") return nil, ctx.Err() @@ -230,10 +231,10 @@ func (e *StepExecutor) executeStepDrone(r *api.StartStepRequest, tiConfig *tiCfg return runStep() } -func (e *StepExecutor) executeStep(r *api.StartStepRequest, secrets []string, client logstream.Client, tiConfig *tiCfg.Cfg) (*runtime.State, map[string]string, []byte, []*api.OutputV2, error) { +func (e *StepExecutor) executeStep(r *api.StartStepRequest, secrets []string, client logstream.Client, tiConfig *tiCfg.Cfg) (*runtime.State, map[string]string, []byte, []*api.OutputV2, string, error) { if r.LogDrone { state, err := e.executeStepDrone(r, tiConfig) - return state, nil, nil, nil, err + return state, nil, nil, nil, "", err } wc := livelog.New(client, r.LogKey, r.Name, getNudges()) @@ -256,7 +257,7 @@ func (e *StepExecutor) executeStep(r *api.StartStepRequest, secrets []string, cl e.run(ctx, e.engine, r, wr, tiConfig) // nolint:errcheck wc.Close() }() - return &runtime.State{Exited: false}, nil, nil, nil, nil + return &runtime.State{Exited: false}, nil, nil, nil, "", nil } var result error @@ -268,7 +269,7 @@ func (e *StepExecutor) executeStep(r *api.StartStepRequest, secrets []string, cl defer cancel() } - exited, outputs, artifact, outputV2, err := e.run(ctx, e.engine, r, wr, tiConfig) + exited, outputs, artifact, outputV2, optimizationState, err := e.run(ctx, e.engine, r, wr, tiConfig) if err != nil { result = multierror.Append(result, err) } @@ -283,7 +284,7 @@ func (e *StepExecutor) executeStep(r *api.StartStepRequest, secrets []string, cl // DeadlineExceeded error this indicates the step was timed out. switch ctx.Err() { case context.Canceled, context.DeadlineExceeded: - return nil, nil, nil, nil, ctx.Err() + return nil, nil, nil, nil, "", ctx.Err() } if exited != nil { @@ -299,11 +300,11 @@ func (e *StepExecutor) executeStep(r *api.StartStepRequest, secrets []string, cl logrus.WithField("id", r.ID).Infof("received exit code %d\n", exited.ExitCode) } } - return exited, outputs, artifact, outputV2, result + return exited, outputs, artifact, outputV2, optimizationState, result } func (e *StepExecutor) run(ctx context.Context, engine *engine.Engine, r *api.StartStepRequest, out io.Writer, tiConfig *tiCfg.Cfg) ( - *runtime.State, map[string]string, []byte, []*api.OutputV2, error) { + *runtime.State, map[string]string, []byte, []*api.OutputV2, string, error) { if r.Kind == api.Run { return executeRunStep(ctx, engine, r, out, tiConfig) } @@ -315,10 +316,11 @@ func (e *StepExecutor) run(ctx context.Context, engine *engine.Engine, r *api.St func convertStatus(status StepStatus) *api.PollStepResponse { r := &api.PollStepResponse{ - Exited: true, - Outputs: status.Outputs, - Artifact: status.Artifact, - OutputV2: status.OutputV2, + Exited: true, + Outputs: status.Outputs, + Artifact: status.Artifact, + OutputV2: status.OutputV2, + OptimizationState: status.OptimizationState, } stepErr := status.StepErr