forked from elastic/beats
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathmagefile.go
424 lines (356 loc) · 11.9 KB
/
magefile.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
// or more contributor license agreements. Licensed under the Elastic License;
// you may not use this file except in compliance with the Elastic License.
//go:build mage
// +build mage
package main
import (
"bytes"
"context"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"runtime"
"strings"
"time"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/client"
"github.com/magefile/mage/mg"
"github.com/magefile/mage/sh"
"github.com/pkg/errors"
devtools "github.com/elastic/beats/v7/dev-tools/mage"
// mage:import
_ "github.com/elastic/beats/v7/dev-tools/mage/target/common"
// mage:import
_ "github.com/elastic/beats/v7/dev-tools/mage/target/unittest"
// mage:import
_ "github.com/elastic/beats/v7/dev-tools/mage/target/integtest/notests"
// mage:import
_ "github.com/elastic/beats/v7/dev-tools/mage/target/test"
)
const (
hubID = "elastic"
logDriverName = "elastic-logging-plugin"
dockerPluginName = hubID + "/" + logDriverName
packageStagingDir = "build/package/"
packageEndDir = "build/distributions/"
rootImageName = "rootfsimage"
dockerfileTmpl = "Dockerfile.tmpl"
)
var (
buildDir = filepath.Join(packageStagingDir, logDriverName)
dockerExportPath = filepath.Join(packageStagingDir, "temproot.tar")
platformMap = map[string]map[string]interface{}{
"amd64": map[string]interface{}{
"from": "alpine:3.10",
},
"arm64": map[string]interface{}{
"from": "arm64v8/alpine:3.10",
},
}
)
func init() {
devtools.BeatLicense = "Elastic License"
devtools.BeatDescription = "The Docker Logging Driver is a docker plugin for the Elastic Stack."
devtools.Platforms = devtools.Platforms.Filter("linux/amd64 linux/arm64")
}
// getPluginName returns the fully qualified name:version string.
func getPluginName() (string, error) {
version, err := devtools.BeatQualifiedVersion()
if err != nil {
return "", errors.Wrap(err, "error getting beats version")
}
return dockerPluginName + ":" + version, nil
}
// createContainer builds the plugin and creates the container that will later become the rootfs used by the plugin
func createContainer(ctx context.Context, cli *client.Client, arch string) error {
dockerLogBeatDir, err := os.Getwd()
if err != nil {
return errors.Wrap(err, "error getting work dir")
}
if !strings.Contains(dockerLogBeatDir, "dockerlogbeat") {
return errors.Errorf("not in dockerlogbeat directory: %s", dockerLogBeatDir)
}
dockerfile := filepath.Join(packageStagingDir, "Dockerfile")
err = devtools.ExpandFile(dockerfileTmpl, dockerfile, platformMap[arch])
if err != nil {
return errors.Wrap(err, "error while expanding Dockerfile template")
}
// start to build the root container that'll be used to build the plugin
tmpDir, err := ioutil.TempDir("", "dockerBuildTar")
if err != nil {
return errors.Wrap(err, "error locating temp dir")
}
defer sh.Rm(tmpDir)
tarPath := filepath.Join(tmpDir, "tarRoot.tar")
err = sh.RunV("tar", "cf", tarPath, "./")
if err != nil {
return errors.Wrap(err, "error creating tar")
}
buildContext, err := os.Open(tarPath)
if err != nil {
return errors.Wrap(err, "error opening temp dur")
}
defer buildContext.Close()
buildOpts := types.ImageBuildOptions{
Tags: []string{rootImageName},
Dockerfile: dockerfile,
}
// build, wait for output
buildResp, err := cli.ImageBuild(ctx, buildContext, buildOpts)
if err != nil {
return errors.Wrap(err, "error building final container image")
}
defer buildResp.Body.Close()
// This blocks until the build operation completes
buildStr, errBufRead := ioutil.ReadAll(buildResp.Body)
if errBufRead != nil {
return errors.Wrap(err, "error reading from docker output")
}
fmt.Printf("%s\n", string(buildStr))
return nil
}
// BuildContainer builds docker rootfs container root
// There's a somewhat complicated process for this:
// * Create a container to build the plugin itself
// * Copy that to a bare-bones container that will become the runc container used by docker
// * Export that container
// * Unpack the tar from the exported container
// * send this to the plugin create API endpoint
func BuildContainer(ctx context.Context) error {
// setup
cli, err := newDockerClient(ctx)
if err != nil {
return errors.Wrap(err, "error creating docker client")
}
devtools.CreateDir(packageStagingDir)
devtools.CreateDir(packageEndDir)
err = os.MkdirAll(filepath.Join(buildDir, "rootfs"), 0755)
if err != nil {
return errors.Wrap(err, "error creating build dir")
}
for _, plat := range devtools.Platforms {
arch := plat.GOARCH()
if runtime.GOARCH != arch {
fmt.Println("Skippping building for", arch, "as runtime is different")
continue
}
err = createContainer(ctx, cli, arch)
if err != nil {
return errors.Wrap(err, "error creating base container")
}
// create the container that will become our rootfs
CreatedContainerBody, err := cli.ContainerCreate(ctx, &container.Config{Image: rootImageName}, nil, nil, nil, "")
if err != nil {
return errors.Wrap(err, "error creating container")
}
defer func() {
// cleanup
if _, noClean := os.LookupEnv("DOCKERLOGBEAT_NO_CLEANUP"); !noClean {
err = cleanDockerArtifacts(ctx, CreatedContainerBody.ID, cli)
if err != nil {
fmt.Fprintf(os.Stderr, "Error cleaning up docker: %s", err)
}
}
}()
fmt.Printf("Got image: %#v\n", CreatedContainerBody.ID)
file, err := os.Create(dockerExportPath)
if err != nil {
return errors.Wrap(err, "error creating tar archive")
}
// export the container to a tar file
exportReader, err := cli.ContainerExport(ctx, CreatedContainerBody.ID)
if err != nil {
return errors.Wrap(err, "error exporting container")
}
_, err = io.Copy(file, exportReader)
if err != nil {
return errors.Wrap(err, "error writing exported container")
}
// misc prepare operations
err = devtools.Copy("config.json", filepath.Join(buildDir, "config.json"))
if err != nil {
return errors.Wrap(err, "error copying config.json")
}
// unpack the tar file into a root directory, which is the format needed for the docker plugin create tool
err = sh.RunV("tar", "-xf", dockerExportPath, "-C", filepath.Join(buildDir, "rootfs"))
if err != nil {
return errors.Wrap(err, "error unpacking exported container")
}
}
return nil
}
func cleanDockerArtifacts(ctx context.Context, containerID string, cli *client.Client) error {
fmt.Printf("Removing container %s\n", containerID)
err := cli.ContainerRemove(ctx, containerID, types.ContainerRemoveOptions{RemoveVolumes: true, Force: true})
if err != nil {
return errors.Wrap(err, "error removing container")
}
resp, err := cli.ImageRemove(ctx, rootImageName, types.ImageRemoveOptions{Force: true})
if err != nil {
return errors.Wrap(err, "error removing image")
}
fmt.Printf("Removed image: %#v\n", resp)
return nil
}
// Uninstall removes working objects and containers
func Uninstall(ctx context.Context) error {
cli, err := newDockerClient(ctx)
if err != nil {
return errors.Wrap(err, "error creating docker client")
}
// check to see if we have a plugin we need to remove
plugins, err := cli.PluginList(ctx, filters.Args{})
if err != nil {
return errors.Wrap(err, "error getting list of plugins")
}
toRemoveName := ""
for _, plugin := range plugins {
if strings.Contains(plugin.Name, logDriverName) {
toRemoveName = plugin.Name
break
}
}
if toRemoveName == "" {
return nil
}
err = cli.PluginDisable(ctx, toRemoveName, types.PluginDisableOptions{Force: true})
if err != nil {
return errors.Wrap(err, "error disabling plugin")
}
err = cli.PluginRemove(ctx, toRemoveName, types.PluginRemoveOptions{Force: true})
if err != nil {
return errors.Wrap(err, "error removing plugin")
}
return nil
}
// Install installs the plugin
func Install(ctx context.Context) error {
mg.Deps(Uninstall)
if _, err := os.Stat(filepath.Join(packageStagingDir, "rootfs")); os.IsNotExist(err) {
mg.Deps(Build)
}
name, err := getPluginName()
if err != nil {
return err
}
cli, err := newDockerClient(ctx)
if err != nil {
return errors.Wrap(err, "error creating docker client")
}
archive, err := tar(buildDir, "rootfs", "config.json")
if err != nil {
return errors.Wrap(err, "error creating archive of work dir")
}
err = cli.PluginCreate(ctx, archive, types.PluginCreateOptions{RepoName: name})
if err != nil {
return errors.Wrap(err, "error creating plugin")
}
err = cli.PluginEnable(ctx, name, types.PluginEnableOptions{})
if err != nil {
return errors.Wrap(err, "error enabling plugin")
}
return nil
}
func tar(dir string, files ...string) (io.Reader, error) {
var archive bytes.Buffer
var stdErr bytes.Buffer
args := append([]string{"-C", dir, "-cf", "-"}, files...)
_, err := sh.Exec(nil, &archive, &stdErr, "tar", args...)
if err != nil {
return nil, errors.Wrap(err, stdErr.String())
}
return &archive, nil
}
// Export exports a "ready" root filesystem and config.json into a tarball
func Export() error {
version, err := devtools.BeatQualifiedVersion()
if err != nil {
return errors.Wrap(err, "error getting beats version")
}
if devtools.Snapshot {
version = version + "-SNAPSHOT"
}
for _, plat := range devtools.Platforms {
arch := plat.GOARCH()
tarballName := fmt.Sprintf("%s-%s-%s-%s.tar.gz", logDriverName, version, "docker-plugin", arch)
outpath := filepath.Join("../..", packageEndDir, tarballName)
err = os.Chdir(packageStagingDir)
if err != nil {
return errors.Wrap(err, "error changing directory")
}
err = sh.RunV("tar", "zcf", outpath,
filepath.Join(logDriverName, "rootfs"),
filepath.Join(logDriverName, "config.json"))
if err != nil {
return errors.Wrap(err, "error creating release tarball")
}
return errors.Wrap(devtools.CreateSHA512File(outpath), "failed to create .sha512 file")
}
return nil
}
// CrossBuild cross-builds the beat for all target platforms.
func CrossBuild() error {
return devtools.CrossBuild()
}
// Build builds the base container used by the docker plugin
func Build() {
mg.SerialDeps(CrossBuild, BuildContainer)
}
// GolangCrossBuild build the Beat binary inside the golang-builder.
// Do not use directly, use crossBuild instead.
func GolangCrossBuild() error {
buildArgs := devtools.DefaultBuildArgs()
buildArgs.CGO = false
buildArgs.Static = true
buildArgs.OutputDir = "build/plugin"
return devtools.GolangCrossBuild(buildArgs)
}
// Package builds a "release" tarball that can be used later with `docker plugin create`
func Package() {
start := time.Now()
defer func() { fmt.Println("package ran for", time.Since(start)) }()
if !isSupportedPlatform() {
fmt.Println(">> package: skipping because no supported platform is enabled")
return
}
mg.SerialDeps(Build, Export)
}
// Package packages the Beat for IronBank distribution.
//
// Use SNAPSHOT=true to build snapshots.
func Ironbank() error {
fmt.Println(">> Ironbank: this module is not subscribed to the IronBank releases.")
return nil
}
func isSupportedPlatform() bool {
_, isAMD64Selected := devtools.Platforms.Get("linux/amd64")
_, isARM64Selected := devtools.Platforms.Get("linux/arm64")
arch := runtime.GOARCH
if arch == "amd64" && isARM64Selected {
devtools.Platforms = devtools.Platforms.Remove("linux/arm64")
} else if arch == "arm64" && isAMD64Selected {
devtools.Platforms = devtools.Platforms.Remove("linux/amd64")
}
return len(devtools.Platforms) > 0
}
// BuildAndInstall builds and installs the plugin
func BuildAndInstall() {
mg.SerialDeps(Build, Install)
}
// Update is currently a dummy test for the `testsuite` target
func Update() {
fmt.Println(">> update: There is no Update for The Elastic Log Plugin")
}
func newDockerClient(ctx context.Context) (*client.Client, error) {
cli, err := client.NewClientWithOpts(client.FromEnv)
if err != nil {
return nil, err
}
cli.NegotiateAPIVersion(ctx)
return cli, nil
}