Skip to content

Commit d56406a

Browse files
committed
Move action execution related code to campaigns package
1 parent 57d1460 commit d56406a

File tree

10 files changed

+470
-435
lines changed

10 files changed

+470
-435
lines changed

cmd/src/actions_exec.go

Lines changed: 24 additions & 161 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package main
22

33
import (
44
"bufio"
5-
"bytes"
65
"context"
76
"encoding/json"
87
"flag"
@@ -19,43 +18,11 @@ import (
1918
"time"
2019

2120
"github.com/fatih/color"
22-
"github.com/hashicorp/go-multierror"
2321
"github.com/mattn/go-isatty"
2422
"github.com/pkg/errors"
25-
"github.com/sourcegraph/src-cli/schema"
26-
"github.com/xeipuuv/gojsonschema"
23+
"github.com/sourcegraph/src-cli/internal/campaigns"
2724
)
2825

29-
type Action struct {
30-
ScopeQuery string `json:"scopeQuery,omitempty"`
31-
Steps []*ActionStep `json:"steps"`
32-
}
33-
34-
type ActionStep struct {
35-
Type string `json:"type"` // "command"
36-
Image string `json:"image,omitempty"` // Docker image
37-
CacheDirs []string `json:"cacheDirs,omitempty"`
38-
Args []string `json:"args,omitempty"`
39-
40-
// ImageContentDigest is an internal field that should not be set by users.
41-
ImageContentDigest string
42-
}
43-
44-
type PatchInput struct {
45-
Repository string `json:"repository"`
46-
BaseRevision string `json:"baseRevision"`
47-
BaseRef string `json:"baseRef"`
48-
Patch string `json:"patch"`
49-
}
50-
51-
func userCacheDir() (string, error) {
52-
userCacheDir, err := os.UserCacheDir()
53-
if err != nil {
54-
return "", err
55-
}
56-
return filepath.Join(userCacheDir, "sourcegraph-src"), nil
57-
}
58-
5926
const defaultTimeout = 60 * time.Minute
6027

6128
func init() {
@@ -132,7 +99,7 @@ Format of the action JSON files:
13299
fmt.Println(usage)
133100
}
134101

135-
cacheDir, _ := userCacheDir()
102+
cacheDir, _ := campaigns.UserCacheDir()
136103
if cacheDir != "" {
137104
cacheDir = filepath.Join(cacheDir, "action-exec")
138105
}
@@ -211,12 +178,12 @@ Format of the action JSON files:
211178
}
212179
}
213180

214-
err = validateActionDefinition(actionFile)
181+
err = campaigns.ValidateActionDefinition(actionFile)
215182
if err != nil {
216183
return err
217184
}
218185

219-
var action Action
186+
var action campaigns.Action
220187
if err := jsonxUnmarshal(string(actionFile), &action); err != nil {
221188
return errors.Wrap(err, "invalid JSON action file")
222189
}
@@ -238,19 +205,21 @@ Format of the action JSON files:
238205
os.Exit(2)
239206
}()
240207

241-
logger := newActionLogger(*verbose, *keepLogsFlag)
208+
logger := campaigns.NewActionLogger(*verbose, *keepLogsFlag)
242209

243210
// Fetch Docker images etc.
244-
err = prepareAction(ctx, action, logger)
211+
err = campaigns.PrepareAction(ctx, action, logger)
245212
if err != nil {
246213
return errors.Wrap(err, "Failed to prepare action")
247214
}
248215

249-
opts := actionExecutorOptions{
250-
timeout: *timeoutFlag,
251-
keepLogs: *keepLogsFlag,
252-
clearCache: *clearCacheFlag,
253-
cache: actionExecutionDiskCache{dir: *cacheDirFlag},
216+
opts := campaigns.ExecutorOpts{
217+
Endpoint: cfg.Endpoint,
218+
AccessToken: cfg.AccessToken,
219+
Timeout: *timeoutFlag,
220+
KeepLogs: *keepLogsFlag,
221+
ClearCache: *clearCacheFlag,
222+
Cache: campaigns.ExecutionDiskCache{Dir: *cacheDirFlag},
254223
}
255224

256225
// Query repos over which to run action
@@ -264,15 +233,15 @@ Format of the action JSON files:
264233
totalSteps := len(repos) * len(action.Steps)
265234
logger.Start(totalSteps)
266235

267-
executor := newActionExecutor(action, *parallelismFlag, logger, opts)
236+
executor := campaigns.NewExecutor(action, *parallelismFlag, logger, opts)
268237
for _, repo := range repos {
269-
executor.enqueueRepo(repo)
238+
executor.EnqueueRepo(repo)
270239
}
271240

272-
go executor.start(ctx)
273-
err = executor.wait()
241+
go executor.Start(ctx)
242+
err = executor.Wait()
274243

275-
patches := executor.allPatches()
244+
patches := executor.AllPatches()
276245
if len(patches) == 0 {
277246
// We call os.Exit because we don't want to return the error
278247
// and have it printed.
@@ -344,115 +313,7 @@ Format of the action JSON files:
344313
})
345314
}
346315

347-
func formatValidationErrs(es []error) string {
348-
points := make([]string, len(es))
349-
for i, err := range es {
350-
points[i] = fmt.Sprintf("- %s", err)
351-
}
352-
353-
return fmt.Sprintf(
354-
"Validating action definition failed:\n%s\n",
355-
strings.Join(points, "\n"))
356-
}
357-
358-
func validateActionDefinition(def []byte) error {
359-
sl := gojsonschema.NewSchemaLoader()
360-
sc, err := sl.Compile(gojsonschema.NewStringLoader(schema.ActionSchemaJSON))
361-
if err != nil {
362-
return errors.Wrapf(err, "failed to compile actions schema")
363-
}
364-
365-
normalized, err := jsonxToJSON(string(def))
366-
if err != nil {
367-
return err
368-
}
369-
370-
res, err := sc.Validate(gojsonschema.NewBytesLoader(normalized))
371-
if err != nil {
372-
return errors.Wrap(err, "failed to validate config against schema")
373-
}
374-
375-
errs := &multierror.Error{ErrorFormat: formatValidationErrs}
376-
for _, err := range res.Errors() {
377-
e := err.String()
378-
// Remove `(root): ` from error formatting since these errors are
379-
// presented to users.
380-
e = strings.TrimPrefix(e, "(root): ")
381-
errs = multierror.Append(errs, errors.New(e))
382-
}
383-
384-
return errs.ErrorOrNil()
385-
}
386-
387-
func prepareAction(ctx context.Context, action Action, logger *actionLogger) error {
388-
// Build any Docker images.
389-
for _, step := range action.Steps {
390-
if step.Type == "docker" {
391-
// Set digests for Docker images so we don't cache action runs in 2 different images with
392-
// the same tag.
393-
var err error
394-
step.ImageContentDigest, err = getDockerImageContentDigest(ctx, step.Image, logger)
395-
if err != nil {
396-
return errors.Wrap(err, "Failed to get Docker image content digest")
397-
}
398-
}
399-
}
400-
401-
return nil
402-
}
403-
404-
// getDockerImageContentDigest gets the content digest for the image. Note that this
405-
// is different from the "distribution digest" (which is what you can use to specify
406-
// an image to `docker run`, as in `my/image@sha256:xxx`). We need to use the
407-
// content digest because the distribution digest is only computed for images that
408-
// have been pulled from or pushed to a registry. See
409-
// https://windsock.io/explaining-docker-image-ids/ under "A Final Twist" for a good
410-
// explanation.
411-
func getDockerImageContentDigest(ctx context.Context, image string, logger *actionLogger) (string, error) {
412-
// TODO!(sqs): is image id the right thing to use here? it is NOT the
413-
// digest. but the digest is not calculated for all images (unless they are
414-
// pulled/pushed from/to a registry), see
415-
// https://github.com/moby/moby/issues/32016.
416-
out, err := exec.CommandContext(ctx, "docker", "image", "inspect", "--format", "{{.Id}}", "--", image).CombinedOutput()
417-
if err != nil {
418-
if !strings.Contains(string(out), "No such image") {
419-
return "", fmt.Errorf("error inspecting docker image %q: %s", image, bytes.TrimSpace(out))
420-
}
421-
logger.Infof("Pulling Docker image %q...\n", image)
422-
pullCmd := exec.CommandContext(ctx, "docker", "image", "pull", image)
423-
prefix := fmt.Sprintf("docker image pull %s", image)
424-
pullCmd.Stdout = logger.InfoPipe(prefix)
425-
pullCmd.Stderr = logger.ErrorPipe(prefix)
426-
427-
err = pullCmd.Start()
428-
if err != nil {
429-
return "", fmt.Errorf("error pulling docker image %q: %s", image, err)
430-
}
431-
err = pullCmd.Wait()
432-
if err != nil {
433-
return "", fmt.Errorf("error pulling docker image %q: %s", image, err)
434-
}
435-
}
436-
out, err = exec.CommandContext(ctx, "docker", "image", "inspect", "--format", "{{.Id}}", "--", image).CombinedOutput()
437-
// This time, the image MUST be present, so the issue must be something else.
438-
if err != nil {
439-
return "", fmt.Errorf("error inspecting docker image %q: %s", image, bytes.TrimSpace(out))
440-
}
441-
id := string(bytes.TrimSpace(out))
442-
if id == "" {
443-
return "", fmt.Errorf("unexpected empty docker image content ID for %q", image)
444-
}
445-
return id, nil
446-
}
447-
448-
type ActionRepo struct {
449-
ID string
450-
Name string
451-
Rev string
452-
BaseRef string
453-
}
454-
455-
func actionRepos(ctx context.Context, scopeQuery string, includeUnsupported bool, logger *actionLogger) ([]ActionRepo, error) {
316+
func actionRepos(ctx context.Context, scopeQuery string, includeUnsupported bool, logger *campaigns.ActionLogger) ([]campaigns.ActionRepo, error) {
456317
hasCount, err := regexp.MatchString(`count:\d+`, scopeQuery)
457318
if err != nil {
458319
return nil, err
@@ -563,7 +424,7 @@ fragment repositoryFields on Repository {
563424

564425
skipped := []string{}
565426
unsupported := []string{}
566-
reposByID := map[string]ActionRepo{}
427+
reposByID := map[string]campaigns.ActionRepo{}
567428
for _, searchResult := range result.Data.Search.Results.Results {
568429

569430
var repo Repository
@@ -595,7 +456,7 @@ fragment repositoryFields on Repository {
595456
}
596457

597458
if _, ok := reposByID[repo.ID]; !ok {
598-
reposByID[repo.ID] = ActionRepo{
459+
reposByID[repo.ID] = campaigns.ActionRepo{
599460
ID: repo.ID,
600461
Name: repo.Name,
601462
Rev: repo.DefaultBranch.Target.OID,
@@ -604,7 +465,7 @@ fragment repositoryFields on Repository {
604465
}
605466
}
606467

607-
repos := make([]ActionRepo, 0, len(reposByID))
468+
repos := make([]campaigns.ActionRepo, 0, len(reposByID))
608469
for _, repo := range reposByID {
609470
repos = append(repos, repo)
610471
}
@@ -640,6 +501,8 @@ fragment repositoryFields on Repository {
640501
return repos, nil
641502
}
642503

504+
var yellow = color.New(color.FgYellow)
505+
643506
func isGitAvailable() bool {
644507
cmd := exec.Command("git", "version")
645508
if err := cmd.Run(); err != nil {

cmd/src/actions_exec_backend.go

Lines changed: 0 additions & 117 deletions
This file was deleted.

0 commit comments

Comments
 (0)