1// Copyright 2021 Google Inc.
2//
3// Use of this source code is governed by a BSD-style license that can be
4// found in the LICENSE file.
5//
6// This executable builds the Docker images based off the Skia executables in the
7// gcr.io/skia-public/skia-release image. It then issues a PubSub notification to have those apps
8// tagged and deployed by docker_pushes_watcher.
9// See //docker_pushes_watcher/README.md in the infra repo for more.
10package main
11
12import (
13	"context"
14	"flag"
15	"fmt"
16	"os"
17	"path"
18	"path/filepath"
19
20	"cloud.google.com/go/pubsub"
21	"google.golang.org/api/option"
22
23	"go.skia.org/infra/go/auth"
24	infra_common "go.skia.org/infra/go/common"
25	docker_pubsub "go.skia.org/infra/go/docker/build/pubsub"
26	sk_exec "go.skia.org/infra/go/exec"
27	"go.skia.org/infra/task_driver/go/lib/auth_steps"
28	"go.skia.org/infra/task_driver/go/lib/bazel"
29	"go.skia.org/infra/task_driver/go/lib/checkout"
30	"go.skia.org/infra/task_driver/go/lib/docker"
31	"go.skia.org/infra/task_driver/go/lib/golang"
32	"go.skia.org/infra/task_driver/go/lib/os_steps"
33	"go.skia.org/infra/task_driver/go/td"
34	"go.skia.org/infra/task_scheduler/go/types"
35	"go.skia.org/skia/infra/bots/task_drivers/common"
36)
37
38var (
39	// Required properties for this task.
40	projectId     = flag.String("project_id", "", "ID of the Google Cloud project.")
41	taskId        = flag.String("task_id", "", "ID of this task.")
42	taskName      = flag.String("task_name", "", "Name of the task.")
43	workdir       = flag.String("workdir", ".", "Working directory")
44	infraRevision = flag.String("infra_revision", "origin/main", "Specifies which revision of the infra repo the images should be built off")
45
46	checkoutFlags = checkout.SetupFlags(nil)
47
48	// Optional flags.
49	local  = flag.Bool("local", false, "True if running locally (as opposed to on the bots)")
50	output = flag.String("o", "", "If provided, dump a JSON blob of step data to the given file. Prints to stdout if '-' is given.")
51)
52
53const (
54	fiddlerImageName = "fiddler"
55	apiImageName     = "api"
56)
57
58func buildPushFiddlerImage(ctx context.Context, dkr *docker.Docker, tag, infraCheckoutDir string, topic *pubsub.Topic) error {
59	// Run skia-release image and extract products out of /tmp/skia/skia. See
60	// https://skia.googlesource.com/skia/+/0e845dc8b05cb2d40d1c880184e33dd76081283a/docker/skia-release/Dockerfile#33
61	productsDir, err := os_steps.TempDir(ctx, "", "")
62	if err != nil {
63		return err
64	}
65	volumes := []string{
66		fmt.Sprintf("%s:/OUT", productsDir),
67	}
68	skiaCopyCmd := []string{"/bin/sh", "-c", "cd /tmp; tar cvzf skia.tar.gz --directory=/tmp/skia skia; cp /tmp/skia.tar.gz /OUT/"}
69	releaseImg := fmt.Sprintf("gcr.io/skia-public/skia-release:%s", tag)
70	if err := dkr.Run(ctx, releaseImg, skiaCopyCmd, volumes, nil); err != nil {
71		return err
72	}
73
74	err = td.Do(ctx, td.Props("Build "+fiddlerImageName+" image").Infra(), func(ctx context.Context) error {
75		runCmd := &sk_exec.Command{
76			Name:       "make",
77			Args:       []string{"release-fiddler-ci"},
78			InheritEnv: true,
79			Env: []string{
80				"COPY_FROM_DIR=" + productsDir,
81				"STABLE_DOCKER_TAG=" + tag,
82			},
83			Dir:       filepath.Join(infraCheckoutDir, "fiddlek"),
84			LogStdout: true,
85			LogStderr: true,
86		}
87		_, err := sk_exec.RunCommand(ctx, runCmd)
88		if err != nil {
89			return err
90		}
91		return nil
92	})
93	if err != nil {
94		return err
95	}
96	if err := docker.PublishToTopic(ctx, "gcr.io/skia-public/"+fiddlerImageName, tag, infra_common.REPO_SKIA, topic); err != nil {
97		return err
98	}
99
100	return cleanupTempFiles(ctx, dkr, releaseImg, volumes)
101}
102
103func cleanupTempFiles(ctx context.Context, dkr *docker.Docker, image string, volumes []string) error {
104	// Remove all temporary files from the host machine. Swarming gets upset if there are root-owned
105	// files it cannot clean up.
106	cleanupCmd := []string{"/bin/sh", "-c", "rm -rf /OUT/*"}
107	return dkr.Run(ctx, image, cleanupCmd, volumes, nil)
108}
109
110func buildPushApiImage(ctx context.Context, dkr *docker.Docker, tag, checkoutDir, infraCheckoutDir string, topic *pubsub.Topic) error {
111	tempDir, err := os_steps.TempDir(ctx, "", "")
112	if err != nil {
113		return err
114	}
115	// Change perms of the directory for doxygen to be able to write to it.
116	if err := os.Chmod(tempDir, 0777); err != nil {
117		return err
118	}
119	// Run Doxygen pointing to the location of the checkout and the out dir.
120	volumes := []string{
121		fmt.Sprintf("%s:/OUT", tempDir),
122		fmt.Sprintf("%s:/CHECKOUT", checkoutDir),
123	}
124	env := []string{
125		"OUTPUT_DIRECTORY=/OUT",
126	}
127	doxygenCmd := []string{"/bin/sh", "-c", "cd /CHECKOUT/tools/doxygen && doxygen ProdDoxyfile"}
128	doxygenImg := "gcr.io/skia-public/doxygen:testing-slim"
129	// Make sure we have the latest doxygen image.
130	if err := dkr.Pull(ctx, doxygenImg); err != nil {
131		return err
132	}
133	if err := dkr.Run(ctx, doxygenImg, doxygenCmd, volumes, env); err != nil {
134		return err
135	}
136
137	err = td.Do(ctx, td.Props("Build "+apiImageName+" image").Infra(), func(ctx context.Context) error {
138		runCmd := &sk_exec.Command{
139			Name:       "make",
140			Args:       []string{"release-api-ci"},
141			InheritEnv: true,
142			Env: []string{
143				"COPY_FROM_DIR=" + filepath.Join(tempDir, "html"),
144				"STABLE_DOCKER_TAG=" + tag,
145			},
146			Dir:       filepath.Join(infraCheckoutDir, "api"),
147			LogStdout: true,
148			LogStderr: true,
149		}
150		_, err := sk_exec.RunCommand(ctx, runCmd)
151		if err != nil {
152			return err
153		}
154		return nil
155	})
156	if err != nil {
157		return err
158	}
159	if err := docker.PublishToTopic(ctx, "gcr.io/skia-public/"+apiImageName, tag, infra_common.REPO_SKIA, topic); err != nil {
160		return err
161	}
162
163	return cleanupTempFiles(ctx, dkr, doxygenImg, volumes)
164}
165
166func main() {
167	bazelFlags := common.MakeBazelFlags(common.MakeBazelFlagsOpts{})
168
169	// Setup.
170	ctx := td.StartRun(projectId, taskId, taskName, output, local)
171	defer td.EndRun(ctx)
172
173	bazelFlags.Validate(ctx)
174
175	if *infraRevision == "" {
176		td.Fatalf(ctx, "Must specify --infra_revision")
177	}
178
179	rs, err := checkout.GetRepoState(checkoutFlags)
180	if err != nil {
181		td.Fatal(ctx, err)
182	}
183	wd, err := os_steps.Abs(ctx, *workdir)
184	if err != nil {
185		td.Fatal(ctx, err)
186	}
187	// Check out the Skia repo code.
188	co, err := checkout.EnsureGitCheckout(ctx, path.Join(wd, "repo"), rs)
189	if err != nil {
190		td.Fatal(ctx, err)
191	}
192	skiaCheckoutDir := co.Dir()
193
194	// Checkout out the Skia infra repo at the specified commit.
195	infraRS := types.RepoState{
196		Repo:     infra_common.REPO_SKIA_INFRA,
197		Revision: *infraRevision,
198	}
199	infraCheckoutDir := filepath.Join("infra_repo")
200	if _, err := checkout.EnsureGitCheckout(ctx, infraCheckoutDir, infraRS); err != nil {
201		td.Fatal(ctx, err)
202	}
203
204	// Setup go.
205	ctx = golang.WithEnv(ctx, wd)
206
207	// Ensure that the bazel cache is setup.
208	opts := bazel.BazelOptions{
209		CachePath: *bazelFlags.CacheDir,
210	}
211	if err := bazel.EnsureBazelRCFile(ctx, opts); err != nil {
212		td.Fatal(ctx, err)
213	}
214
215	// Create token source with scope for cloud registry (storage) and pubsub.
216	ts, err := auth_steps.Init(ctx, *local, auth.ScopeUserinfoEmail, auth.ScopeFullControl, pubsub.ScopePubSub)
217	if err != nil {
218		td.Fatal(ctx, err)
219	}
220
221	// Create pubsub client.
222	client, err := pubsub.NewClient(ctx, docker_pubsub.TOPIC_PROJECT_ID, option.WithTokenSource(ts))
223	if err != nil {
224		td.Fatal(ctx, err)
225	}
226	topic := client.Topic(docker_pubsub.TOPIC)
227
228	// Figure out which tag to use for docker build and push.
229	tag := rs.Revision
230	if rs.Issue != "" && rs.Patchset != "" {
231		tag = fmt.Sprintf("%s_%s", rs.Issue, rs.Patchset)
232	}
233
234	// Instantiate docker.
235	dkr, err := docker.New(ctx, ts)
236	if err != nil {
237		td.Fatal(ctx, err)
238	}
239
240	// Build and push all apps of interest below.
241	if err := buildPushApiImage(ctx, dkr, tag, skiaCheckoutDir, infraCheckoutDir, topic); err != nil {
242		td.Fatal(ctx, err)
243	}
244	if err := buildPushFiddlerImage(ctx, dkr, tag, infraCheckoutDir, topic); err != nil {
245		td.Fatal(ctx, err)
246	}
247
248	if !*local {
249		if err := common.BazelCleanIfLowDiskSpace(ctx, *bazelFlags.CacheDir, skiaCheckoutDir, "bazelisk"); err != nil {
250			td.Fatal(ctx, err)
251		}
252	}
253}
254