This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

core / spindle / engine / engine.go
5.9 kB 201 lines
1package engine 2 3import ( 4 "context" 5 "errors" 6 "fmt" 7 "log/slog" 8 "path/filepath" 9 "sync" 10 11 "tangled.org/core/notifier" 12 "tangled.org/core/spindle/config" 13 "tangled.org/core/spindle/db" 14 "tangled.org/core/spindle/models" 15 "tangled.org/core/spindle/secrets" 16) 17 18var ( 19 ErrTimedOut = errors.New("timed out") 20 ErrWorkflowFailed = errors.New("workflow failed") 21) 22 23type workflowFinalizer interface { 24 FinalizeWorkflow(ctx context.Context, wid models.WorkflowId, wf *models.Workflow, wfLogger models.WorkflowLogger) error 25} 26 27func StartWorkflows(l *slog.Logger, vault secrets.Manager, cfg *config.Config, db *db.DB, n *notifier.Notifier, ctx context.Context, pipeline *models.Pipeline, pipelineId models.PipelineId) { 28 l.Info("starting all workflows in parallel", "pipeline", pipelineId) 29 30 var allSecrets []secrets.UnlockedSecret 31 // never pass secrets to pipelines that run untrusted (e.g. fork) code 32 if pipeline.TrustedSource && pipeline.RepoDid != "" { 33 if res, err := vault.GetSecretsUnlocked(ctx, secrets.RepoIdentifier(pipeline.RepoDid.String())); err == nil { 34 allSecrets = res 35 } 36 } else if !pipeline.TrustedSource { 37 l.Info("skipping secrets for untrusted pipeline source", "pipeline", pipelineId) 38 } 39 40 secretValues := make([]string, len(allSecrets)) 41 for i, s := range allSecrets { 42 secretValues[i] = s.Value 43 } 44 45 s3, err := NewS3(cfg.S3.LogBucket) 46 if err != nil { 47 l.Error("error creating s3 client", "err", err) 48 } 49 50 // wid.String() is lossy so two different names can map to the same key 51 // eg. "foo bar" and "foo-bar"... 52 wfCounts := make(map[string]int) 53 for _, wfs := range pipeline.Workflows { 54 for _, w := range wfs { 55 wid := models.WorkflowId{ 56 PipelineId: pipelineId, 57 Name: w.Name, 58 } 59 wfCounts[wid.String()]++ 60 } 61 } 62 63 var wg sync.WaitGroup 64 for eng, wfs := range pipeline.Workflows { 65 workflowTimeout := eng.WorkflowTimeout() 66 l.Info("using workflow timeout", "timeout", workflowTimeout) 67 68 for _, w := range wfs { 69 w := w 70 wid := models.WorkflowId{ 71 PipelineId: pipelineId, 72 Name: w.Name, 73 } 74 75 if wfCounts[wid.String()] > 1 { 76 l.Warn("skipping workflow due to name collision", "wid", wid, "key", wid.String()) 77 dbErr := db.StatusFailed(wid, fmt.Sprintf("colliding workflow name: %s; rename to something else", wid.String()), -1, n) 78 if dbErr != nil { 79 l.Error("failed to set workflow status to failed", "wid", wid, "err", dbErr) 80 } 81 continue 82 } 83 84 wg.Go(func() { 85 86 defer func() { 87 if s3 != nil { 88 logFile := filepath.Join(cfg.Server.LogDir, fmt.Sprintf("%s.log", wid.String())) 89 if err := s3.WriteFile(ctx, logFile); err != nil { 90 l.Error("error uploading logs", "err", err) 91 } 92 } 93 }() 94 95 wfLogger, err := models.NewFileWorkflowLogger(cfg.Server.LogDir, wid, secretValues) 96 if err != nil { 97 l.Warn("failed to setup step logger; logs will not be persisted", "error", err) 98 wfLogger = models.NullLogger{} 99 } else { 100 l.Info("setup step logger; logs will be persisted", "logDir", cfg.Server.LogDir, "wid", wid) 101 defer wfLogger.Close() 102 } 103 104 l.Info("waiting for slot", "wid", wid) 105 slot := WorkflowSlot(NoopSlot{}) 106 if s, ok := eng.(WorkflowSlotter); ok { 107 var err error 108 slot, err = s.AcquireWorkflowSlot(ctx, wid, &w) 109 if err != nil { 110 l.Error("failed to acquire slot", "wid", wid, "err", err) 111 dbErr := db.StatusFailed(wid, err.Error(), -1, n) 112 if dbErr != nil { 113 l.Error("failed to set workflow status to failed", "wid", wid, "err", dbErr) 114 } 115 return 116 } 117 } 118 defer slot.Release() 119 120 err = db.StatusRunning(wid, n) 121 if err != nil { 122 l.Error("failed to set workflow status to running", "wid", wid, "err", err) 123 return 124 } 125 126 err = eng.SetupWorkflow(ctx, wid, &w, wfLogger) 127 if err != nil { 128 // TODO(winter): Should this always set StatusFailed? 129 // In the original, we only do in a subset of cases. 130 l.Error("setting up workflow", "wid", wid, "err", err) 131 132 destroyErr := eng.DestroyWorkflow(ctx, wid) 133 if destroyErr != nil { 134 l.Error("failed to destroy workflow after setup failure", "error", destroyErr) 135 } 136 137 dbErr := db.StatusFailed(wid, err.Error(), -1, n) 138 if dbErr != nil { 139 l.Error("failed to set workflow status to failed", "wid", wid, "err", dbErr) 140 } 141 return 142 } 143 defer eng.DestroyWorkflow(ctx, wid) 144 145 ctx, cancel := context.WithTimeout(ctx, workflowTimeout) 146 defer cancel() 147 148 for stepIdx, step := range w.Steps { 149 // log start of step 150 if wfLogger != nil { 151 wfLogger. 152 ControlWriter(stepIdx, step, models.StepStatusStart). 153 Write([]byte{0}) 154 } 155 156 err = eng.RunStep(ctx, wid, &w, stepIdx, allSecrets, wfLogger) 157 158 // log end of step 159 if wfLogger != nil { 160 wfLogger. 161 ControlWriter(stepIdx, step, models.StepStatusEnd). 162 Write([]byte{0}) 163 } 164 165 if err != nil { 166 if errors.Is(err, ErrTimedOut) { 167 dbErr := db.StatusTimeout(wid, n) 168 if dbErr != nil { 169 l.Error("failed to set workflow status to timeout", "wid", wid, "err", dbErr) 170 } 171 } else { 172 dbErr := db.StatusFailed(wid, err.Error(), -1, n) 173 if dbErr != nil { 174 l.Error("failed to set workflow status to failed", "wid", wid, "err", dbErr) 175 } 176 } 177 return 178 } 179 } 180 181 if finalizer, ok := eng.(workflowFinalizer); ok { 182 if err := finalizer.FinalizeWorkflow(ctx, wid, &w, wfLogger); err != nil { 183 dbErr := db.StatusFailed(wid, err.Error(), -1, n) 184 if dbErr != nil { 185 l.Error("failed to set workflow status to failed", "wid", wid, "err", dbErr) 186 } 187 return 188 } 189 } 190 191 err = db.StatusSuccess(wid, n) 192 if err != nil { 193 l.Error("failed to set workflow status to success", "wid", wid, "err", err) 194 } 195 }) 196 } 197 } 198 199 wg.Wait() 200 l.Info("all workflows completed") 201}