1package commands
2
3import (
4	"fmt"
5	"net/url"
6	"os"
7	"os/signal"
8	"strconv"
9	"syscall"
10	"time"
11
12	"github.com/concourse/concourse/atc"
13	"github.com/concourse/concourse/fly/commands/internal/displayhelpers"
14	"github.com/concourse/concourse/fly/commands/internal/executehelpers"
15	"github.com/concourse/concourse/fly/commands/internal/flaghelpers"
16	"github.com/concourse/concourse/fly/commands/internal/templatehelpers"
17	"github.com/concourse/concourse/fly/config"
18	"github.com/concourse/concourse/fly/eventstream"
19	"github.com/concourse/concourse/fly/rc"
20	"github.com/concourse/concourse/fly/ui"
21	"github.com/concourse/concourse/fly/ui/progress"
22	"github.com/concourse/concourse/go-concourse/concourse"
23	"github.com/vbauerster/mpb/v4"
24)
25
26type ExecuteCommand struct {
27	TaskConfig     atc.PathFlag                       `short:"c" long:"config" required:"true"                description:"The task config to execute"`
28	Privileged     bool                               `short:"p" long:"privileged"                            description:"Run the task with full privileges"`
29	IncludeIgnored bool                               `          long:"include-ignored"                       description:"Including .gitignored paths. Disregards .gitignore entries and uploads everything"`
30	Inputs         []flaghelpers.InputPairFlag        `short:"i" long:"input"       value-name:"NAME=PATH"    description:"An input to provide to the task (can be specified multiple times)"`
31	InputMappings  []flaghelpers.InputMappingPairFlag `short:"m" long:"input-mapping"       value-name:"[NAME=STRING]"    description:"Map a resource to a different name as task input"`
32	InputsFrom     flaghelpers.JobFlag                `short:"j" long:"inputs-from" value-name:"PIPELINE/JOB" description:"A job to base the inputs on"`
33	Outputs        []flaghelpers.OutputPairFlag       `short:"o" long:"output"      value-name:"NAME=PATH"    description:"An output to fetch from the task (can be specified multiple times)"`
34	Image          string                             `long:"image" description:"Image resource for the one-off build"`
35	Tags           []string                           `          long:"tag"         value-name:"TAG"          description:"A tag for a specific environment (can be specified multiple times)"`
36	Var            []flaghelpers.VariablePairFlag     `short:"v"  long:"var"       value-name:"[NAME=STRING]"  unquote:"false"  description:"Specify a string value to set for a variable in the pipeline"`
37	YAMLVar        []flaghelpers.YAMLVariablePairFlag `short:"y"  long:"yaml-var"  value-name:"[NAME=YAML]"    unquote:"false"  description:"Specify a YAML value to set for a variable in the pipeline"`
38	VarsFrom       []atc.PathFlag                     `short:"l"  long:"load-vars-from"  description:"Variable flag that can be used for filling in template values in configuration from a YAML file"`
39}
40
41func (command *ExecuteCommand) Execute(args []string) error {
42	target, err := rc.LoadTarget(Fly.Target, Fly.Verbose)
43	if err != nil {
44		return err
45	}
46
47	err = target.Validate()
48	if err != nil {
49		return err
50	}
51
52	taskConfig, err := command.CreateTaskConfig(args)
53	if err != nil {
54		return err
55	}
56
57	planFactory := atc.NewPlanFactory(time.Now().Unix())
58
59	inputs, inputMappings, imageResource, resourceTypes, err := executehelpers.DetermineInputs(
60		planFactory,
61		target.Team(),
62		taskConfig.Inputs,
63		command.Inputs,
64		command.InputMappings,
65		command.Image,
66		command.InputsFrom,
67		command.IncludeIgnored,
68		taskConfig.Platform,
69		command.Tags,
70	)
71	if err != nil {
72		return err
73	}
74
75	if imageResource != nil {
76		taskConfig.ImageResource = imageResource
77	}
78
79	outputs, err := executehelpers.DetermineOutputs(
80		planFactory,
81		taskConfig.Outputs,
82		command.Outputs,
83	)
84	if err != nil {
85		return err
86	}
87
88	plan, err := executehelpers.CreateBuildPlan(
89		planFactory,
90		target,
91		command.Privileged,
92		inputs,
93		inputMappings,
94		resourceTypes,
95		outputs,
96		taskConfig,
97		command.Tags,
98	)
99
100	if err != nil {
101		return err
102	}
103
104	client := target.Client()
105	clientURL, err := url.Parse(client.URL())
106	if err != nil {
107		return err
108	}
109
110	var build atc.Build
111	var buildURL *url.URL
112
113	if command.InputsFrom.PipelineName != "" {
114		build, err = target.Team().CreatePipelineBuild(command.InputsFrom.PipelineName, plan)
115		if err != nil {
116			return err
117		}
118	} else {
119		build, err = target.Team().CreateBuild(plan)
120		if err != nil {
121			return err
122		}
123	}
124
125	buildURL, err = url.Parse(fmt.Sprintf("/builds/%d", build.ID))
126	if err != nil {
127		return err
128	}
129
130	fmt.Printf("executing build %d at %s \n", build.ID, clientURL.ResolveReference(buildURL))
131
132	terminate := make(chan os.Signal, 1)
133
134	go abortOnSignal(client, terminate, build)
135
136	signal.Notify(terminate, syscall.SIGINT, syscall.SIGTERM)
137
138	eventSource, err := client.BuildEvents(strconv.Itoa(build.ID))
139	if err != nil {
140		return err
141	}
142
143	renderOptions := eventstream.RenderOptions{}
144
145	exitCode := eventstream.Render(os.Stdout, eventSource, renderOptions)
146	eventSource.Close()
147
148	artifactList, err := client.ListBuildArtifacts(strconv.Itoa(build.ID))
149	if err != nil {
150		return err
151	}
152
153	artifacts := map[string]atc.WorkerArtifact{}
154
155	for _, artifact := range artifactList {
156		artifacts[artifact.Name] = artifact
157	}
158
159	prog := progress.New()
160
161	for _, output := range outputs {
162		name := output.Name
163		path := output.Path
164
165		artifact, ok := artifacts[name]
166		if !ok {
167			continue
168		}
169
170		prog.Go("downloading "+output.Name, func(bar *mpb.Bar) error {
171			return executehelpers.Download(bar, target.Team(), artifact.ID, path)
172		})
173	}
174
175	err = prog.Wait()
176	if err != nil {
177		displayhelpers.FailWithErrorf("downloading failed: %s", err)
178		return err
179	}
180
181	os.Exit(exitCode)
182
183	return nil
184}
185
186func (command *ExecuteCommand) CreateTaskConfig(args []string) (atc.TaskConfig, error) {
187
188	taskTemplate := templatehelpers.NewYamlTemplateWithParams(
189		command.TaskConfig,
190		command.VarsFrom,
191		command.Var,
192		command.YAMLVar,
193	)
194
195	taskTemplateEvaluated, err := taskTemplate.Evaluate(false, false)
196	if err != nil {
197		return atc.TaskConfig{}, err
198	}
199
200	return config.OverrideTaskParams(taskTemplateEvaluated, args)
201}
202
203func abortOnSignal(
204	client concourse.Client,
205	terminate <-chan os.Signal,
206	build atc.Build,
207) {
208	<-terminate
209
210	fmt.Fprintf(ui.Stderr, "\naborting...\n")
211
212	err := client.AbortBuild(strconv.Itoa(build.ID))
213	if err != nil {
214		fmt.Fprintln(ui.Stderr, "failed to abort:", err)
215		os.Exit(2)
216	}
217
218	// if told to terminate again, exit immediately
219	<-terminate
220	fmt.Fprintln(ui.Stderr, "exiting immediately")
221	os.Exit(2)
222}
223