From 74f548b5c34fd0764a38d8b35c71ef1ce2c700e9 Mon Sep 17 00:00:00 2001 From: Andrew Kroh Date: Thu, 7 Jun 2018 19:29:59 -0400 Subject: [PATCH] Refactor Beat packaging and cross-building MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This refactors Beat packaging to use a declarative YAML based packaging specification. This makes it easier to customize the contents of packages (e.g. adding additional X-Pack content or simply tailoring what's included for single Beat). The specification itself is pretty simple. It consists of package metadata and a list of files to include. The values can be templated which allows for a single package spec to be reused across Beats. Here's an example spec for an OSS Windows zip package. ``` spec: name: '{{.BeatName}}-oss' service_name: '{{.BeatServiceName}}' os: '{{.GOOS}}' arch: '{{.PackageArch}}' vendor: '{{.BeatVendor}}' version: '{{ beat_version }}' license: ASL 2.0 url: '{{.BeatURL}}' description: '{{.BeatDescription}}' files: '{{.BeatName}}{{.BinaryExt}}': source: build/golang-crossbuild/{{.BeatName}}-{{.GOOS}}-{{.Platform.Arch}}{{.BinaryExt}} mode: 0755 fields.yml: source: fields.yml mode: 0644 LICENSE.txt: source: '{{ repo.RootDir }}/licenses/APACHE-LICENSE-2.0.txt' mode: 0644 NOTICE.txt: source: '{{ repo.RootDir }}/NOTICE.txt' mode: 0644 README.md: template: '{{ elastic_beats_dir }}/dev-tools/mage/templates/common/README.md.tmpl' mode: 0644 .build_hash.txt: content: > {{ commit }} mode: 0644 '{{.BeatName}}.reference.yml': source: '{{.BeatName}}.reference.yml' mode: 0644 '{{.BeatName}}.yml': source: '{{.BeatName}}.yml' mode: 0600 config: true kibana: source: _meta/kibana.generated mode: 0644 install-service-{{.BeatName}}.ps1: template: '{{ elastic_beats_dir }}/dev-tools/mage/templates/windows/install-service.ps1.tmpl' mode: 0755 uninstall-service-{{.BeatName}}.ps1: template: '{{ elastic_beats_dir }}/dev-tools/mage/templates/windows/uninstall-service.ps1.tmpl' mode: 0755 ``` Each Beat has two build targets for packaging. - `make snapshot` - `make release` The set of target platforms can be influenced by the `PLATFORMS` environment variable which accepts an platform selection expression. For example to add ARMv7 (for your Raspberry Pi) to the default set of platforms you would use `PLATFORMS='+linux/armv7' make snapshot`. Or to only build for Windows set `PLATFORMS='windows'`. Full details can be found in the godocs for `NewPlatformList`. For the release manager there are two new top-level targets that take care of ensuring that the proper Go version is used. The naming here aligns with what several of the other projects are using for their release manager targets. - `make release-manager-snapshot` - `make release-manager-release` Build Process Details ---- Below is the command line output of building and packaging Packetbeat for linux/armv7 only. I'll describe each step in the build process. ``` $ PLATFORMS='+all linux/armv7' make snapshot Installing mage from vendor >> golangCrossBuild: Building for linux/armv7 >> buildGoDaemon: Building for linux/armv7 >> Building using: cmd='build/mage-linux-amd64 golangCrossBuild', env=[CC=arm-linux-gnueabihf-gcc, CXX=arm-linux-gnueabihf-g++, GOARCH=arm, GOARM=7, GOOS=linux, PLATFORM_ID=linux-armv7] >> Building using: cmd='build/mage-linux-amd64 buildGoDaemon', env=[CC=arm-linux-gnueabihf-gcc, CXX=arm-linux-gnueabihf-g++, GOARCH=arm, GOARM=7, GOOS=linux, PLATFORM_ID=linux-armv7] grammar.y: warning: 38 shift/reduce conflicts [-Wconflicts-sr] >> package: Building packetbeat type=tar.gz for platform=linux/armv7 >> package: Building packetbeat-oss type=deb for platform=linux/armv7 >> package: Building packetbeat type=rpm for platform=linux/armv7 >> package: Building packetbeat type=deb for platform=linux/armv7 >> package: Building packetbeat-oss type=tar.gz for platform=linux/armv7 >> package: Building packetbeat-oss type=rpm for platform=linux/armv7 >> Testing package contents package ran for 1m6.597416206s $ tree build/distributions/ build/distributions/ ├── packetbeat-oss-7.0.0-alpha1-SNAPSHOT-armhf.deb ├── packetbeat-oss-7.0.0-alpha1-SNAPSHOT-armhf.deb.sha512 ├── packetbeat-oss-7.0.0-alpha1-SNAPSHOT-armhfp.rpm ├── packetbeat-oss-7.0.0-alpha1-SNAPSHOT-armhfp.rpm.sha512 ├── packetbeat-oss-7.0.0-alpha1-SNAPSHOT-linux-armv7.tar.gz └── packetbeat-oss-7.0.0-alpha1-SNAPSHOT-linux-armv7.tar.gz.sha512 ``` 1. Install [mage](https://magefile.org/) from the vendor directory to `$GOPATH/bin`. Mage is used to provide a simple gmake like wrapper around build logic that's written in Go. Additionally it makes is possible to build and package without gmake by invoking mage directly [think Windows users]. For example: ``` $ mage -l Targets: build builds the Beat binary. buildGoDaemon builds the go-daemon binary (use crossBuildGoDaemon). clean cleans all generated files and build artifacts. crossBuild cross-builds the beat for all target platforms. crossBuildGoDaemon cross-builds the go-daemon binary using Docker. golangCrossBuild build the Beat binary inside of the golang-builder. package packages the Beat for distribution. testPackages tests the generated packages (i.e. update updates the generated files (aka make update). ``` 2. Cross-build Packetbeat for linux/armv7. Cross-building requires Docker and uses images hosted at `docker.elastic.co/beats-dev/golang-crossbuild`. The repo for these images is https://github.com/elastic/golang-crossbuild. These images give us wider platform support for cross-building than we had. `mage golangCrossBuild` is invoked inside of the container. This handles cross-compiling libpcap and then invoking `go build` with the proper args. 3. Cross-build go-daemon for linux/armv7. This is actually done in parallel with the other cross-builds. GOMAXPROCS determines the number of concurrent jobs. `mage buildGoDaemon` is invoked inside of the container. 4. After all cross-builds complete, packaging begins. The package types are decided based on the package specs that are registered for each OS. Zip and tar.gz files are built natively with Go. RPM and deb packages are first generated as tar.gz where we have full control over the target file names, ownership, and modes regardless of the underlying filesystem [think Windows]. Then FPM is invoked inside of Docker to translate the tar.gz file into a proper RPM or deb. 5. SHA512 side-car files are generated for each package. Go is used for this so no special command line tools are needed. 6. The generated packages are inspected with `dev-tools/package_test.go`. This looks at the contents of the packages to ensure the files have the expected ownership and modes (e.g. the config file should have 0600). Changes - Add Boot2Docker workaround for shared volume permissions - Mirror the libpcap source to S3 - Use MAX_PARALLEL to control the number of parallel jobs. The default value is the lesser of the NumCPU and NCPU from the Docker daemon. - Add jenkins_package.sh for Jenkins. --- CHANGELOG-developer.asciidoc | 1 + Makefile | 78 +- auditbeat/Makefile | 50 +- auditbeat/magefile.go | 212 +++++ .../_meta/{config.yml.tpl => config.yml.tmpl} | 16 +- .../_meta/{config.yml.tpl => config.yml.tmpl} | 12 +- auditbeat/scripts/generate_config.go | 33 +- dev-tools/common.bash | 4 + dev-tools/deploy | 4 +- dev-tools/jenkins_ci.ps1 | 4 + dev-tools/jenkins_release.sh | 18 + dev-tools/mage/.gitignore | 1 + dev-tools/mage/build.go | 144 ++++ dev-tools/mage/clean.go | 54 ++ dev-tools/mage/common.go | 690 ++++++++++++++++ dev-tools/mage/crossbuild.go | 239 ++++++ .../mage/files/linux/systemd-daemon-reload.sh | 4 + dev-tools/mage/files/packages.yml | 222 +++++ dev-tools/mage/godaemon.go | 72 ++ dev-tools/mage/pkg.go | 108 +++ dev-tools/mage/pkg_test.go | 120 +++ dev-tools/mage/pkgspecs.go | 100 +++ dev-tools/mage/pkgtypes.go | 764 ++++++++++++++++++ dev-tools/mage/platforms.go | 464 +++++++++++ dev-tools/mage/platforms_test.go | 158 ++++ dev-tools/mage/settings.go | 573 +++++++++++++ .../mage/templates/common/README.md.tmpl | 27 + .../mage/templates/common/magefile.go.tmpl | 72 ++ dev-tools/mage/templates/deb/init.sh.tmpl | 188 +++++ .../mage/templates/linux/beatname.sh.tmpl | 11 + .../mage/templates/linux/systemd.unit.tmpl | 12 + dev-tools/mage/templates/rpm/init.sh.tmpl | 119 +++ .../windows/install-service.ps1.tmpl | 14 + .../windows/uninstall-service.ps1.tmpl | 7 + dev-tools/mage/testdata/config.yml | 7 + dev-tools/package_test.go | 2 +- filebeat/Makefile | 6 - filebeat/magefile.go | 88 ++ .../beat/{beat}/{LICENSE => LICENSE.txt} | 0 generator/beat/{beat}/Makefile | 15 +- generator/beat/{beat}/{NOTICE => NOTICE.txt} | 0 generator/beat/{beat}/magefile.go | 91 +++ .../{beat}/{LICENSE => LICENSE.txt} | 0 generator/metricbeat/{beat}/Makefile | 22 +- .../metricbeat/{beat}/{NOTICE => NOTICE.txt} | 0 generator/metricbeat/{beat}/magefile.go | 91 +++ heartbeat/Makefile | 6 - heartbeat/magefile.go | 90 +++ libbeat/scripts/Makefile | 181 +---- magefile.go | 72 ++ metricbeat/Makefile | 12 - metricbeat/magefile.go | 155 ++++ packetbeat/Makefile | 19 - packetbeat/lib/windows-64/.gitignore | 1 + packetbeat/lib/windows-64/sha256 | 1 + packetbeat/lib/windows-64/wpcap.dll | Bin 0 -> 281104 bytes packetbeat/magefile.go | 405 ++++++++++ winlogbeat/Makefile | 11 - winlogbeat/magefile.go | 90 +++ 59 files changed, 5602 insertions(+), 358 deletions(-) create mode 100644 auditbeat/magefile.go rename auditbeat/module/auditd/_meta/{config.yml.tpl => config.yml.tmpl} (65%) rename auditbeat/module/file_integrity/_meta/{config.yml.tpl => config.yml.tmpl} (90%) create mode 100755 dev-tools/jenkins_release.sh create mode 100644 dev-tools/mage/.gitignore create mode 100644 dev-tools/mage/build.go create mode 100644 dev-tools/mage/clean.go create mode 100644 dev-tools/mage/common.go create mode 100644 dev-tools/mage/crossbuild.go create mode 100644 dev-tools/mage/files/linux/systemd-daemon-reload.sh create mode 100644 dev-tools/mage/files/packages.yml create mode 100644 dev-tools/mage/godaemon.go create mode 100644 dev-tools/mage/pkg.go create mode 100644 dev-tools/mage/pkg_test.go create mode 100644 dev-tools/mage/pkgspecs.go create mode 100644 dev-tools/mage/pkgtypes.go create mode 100644 dev-tools/mage/platforms.go create mode 100644 dev-tools/mage/platforms_test.go create mode 100644 dev-tools/mage/settings.go create mode 100644 dev-tools/mage/templates/common/README.md.tmpl create mode 100644 dev-tools/mage/templates/common/magefile.go.tmpl create mode 100644 dev-tools/mage/templates/deb/init.sh.tmpl create mode 100644 dev-tools/mage/templates/linux/beatname.sh.tmpl create mode 100644 dev-tools/mage/templates/linux/systemd.unit.tmpl create mode 100644 dev-tools/mage/templates/rpm/init.sh.tmpl create mode 100644 dev-tools/mage/templates/windows/install-service.ps1.tmpl create mode 100644 dev-tools/mage/templates/windows/uninstall-service.ps1.tmpl create mode 100644 dev-tools/mage/testdata/config.yml create mode 100644 filebeat/magefile.go rename generator/beat/{beat}/{LICENSE => LICENSE.txt} (100%) rename generator/beat/{beat}/{NOTICE => NOTICE.txt} (100%) create mode 100644 generator/beat/{beat}/magefile.go rename generator/metricbeat/{beat}/{LICENSE => LICENSE.txt} (100%) rename generator/metricbeat/{beat}/{NOTICE => NOTICE.txt} (100%) create mode 100644 generator/metricbeat/{beat}/magefile.go create mode 100644 heartbeat/magefile.go create mode 100644 magefile.go create mode 100644 metricbeat/magefile.go create mode 100644 packetbeat/lib/windows-64/.gitignore create mode 100644 packetbeat/lib/windows-64/sha256 create mode 100644 packetbeat/lib/windows-64/wpcap.dll create mode 100644 packetbeat/magefile.go create mode 100644 winlogbeat/magefile.go diff --git a/CHANGELOG-developer.asciidoc b/CHANGELOG-developer.asciidoc index 1f0e0f02e28c..2858f6674057 100644 --- a/CHANGELOG-developer.asciidoc +++ b/CHANGELOG-developer.asciidoc @@ -24,6 +24,7 @@ The list below covers the major changes between 6.3.0 and master only. - Port fields.yml collector to Golang {pull}6911[6911] - Dashboards under _meta/kibana are expected to be decoded. See https://github.com/elastic/beats/pull/7224 for a conversion script. {pull}7265[7265] - Constructor `(github.com/elastic/beats/libbeat/output/codec/json).New` expects a new `escapeHTML` parameter. {pull}7445[7445] +- Packaging has been refactored and updates are required. See the PR for migration details. {pull}7388[7388] ==== Bugfixes diff --git a/Makefile b/Makefile index 1fc93c8947ac..b339d3c819af 100644 --- a/Makefile +++ b/Makefile @@ -1,9 +1,8 @@ BUILD_DIR=$(CURDIR)/build COVERAGE_DIR=$(BUILD_DIR)/coverage -BEATS=packetbeat filebeat winlogbeat metricbeat heartbeat auditbeat +BEATS?=auditbeat filebeat heartbeat metricbeat packetbeat winlogbeat PROJECTS=libbeat $(BEATS) PROJECTS_ENV=libbeat filebeat metricbeat -SNAPSHOT?=yes PYTHON_ENV?=$(BUILD_DIR)/python-env VIRTUALENV_PARAMS?= FIND=find . -type f -not -path "*/vendor/*" -not -path "*/build/*" -not -path "*/.git/*" @@ -62,6 +61,7 @@ clean: @rm -rf build @$(foreach var,$(PROJECTS),$(MAKE) -C $(var) clean || exit 1;) @$(MAKE) -C generator clean + @-mage -clean 2> /dev/null # Cleans up the vendor directory from unnecessary files # This should always be run after updating the dependencies @@ -109,51 +109,12 @@ lint: @go get $(GOLINT_REPO) $(REVIEWDOG_REPO) $(REVIEWDOG) $(REVIEWDOG_OPTIONS) -# Collects all dashboards and generates dashboard folder for https://github.com/elastic/beats-dashboards/tree/master/dashboards -.PHONY: beats-dashboards -beats-dashboards: - @mkdir -p build/dashboards - @$(foreach var,$(BEATS),cp -r $(var)/_meta/kibana.generated/ build/dashboards/$(var) || exit 1;) - # Builds the documents for each beat .PHONY: docs docs: @$(foreach var,$(PROJECTS),BUILD_DIR=${BUILD_DIR} $(MAKE) -C $(var) docs || exit 1;) sh ./script/build_docs.sh dev-guide github.com/elastic/beats/docs/devguide ${BUILD_DIR} -.PHONY: package-all -package-all: update beats-dashboards - @$(foreach var,$(BEATS),SNAPSHOT=$(SNAPSHOT) $(MAKE) -C $(var) package-all || exit 1;) - - @echo "Start building the dashboards package" - @mkdir -p build/upload/ - @BUILD_DIR=${BUILD_DIR} UPLOAD_DIR=${BUILD_DIR}/upload SNAPSHOT=$(SNAPSHOT) $(MAKE) -C dev-tools/packer package-dashboards ${BUILD_DIR}/upload/build_id.txt - @mv build/upload build/dashboards-upload - - @# Copy build files over to top build directory - @mkdir -p build/upload/ - @$(foreach var,$(BEATS),cp -r $(var)/build/upload/ build/upload/$(var) || exit 1;) - @cp -r build/dashboards-upload build/upload/dashboards - @# Run tests on the generated packages. - @go test ./dev-tools/package_test.go -files "${BUILD_DIR}/upload/*/*" - -# Upload nightly builds to S3 -.PHONY: upload-nightlies-s3 -upload-nightlies-s3: all - aws s3 cp --recursive --acl public-read build/upload s3://beats-nightlies - -# Run after building to sign packages and publish to APT and YUM repos. -.PHONY: package-upload -upload-package: - $(MAKE) -C dev-tools/packer deb-rpm-s3 - # You must export AWS_ACCESS_KEY= and export AWS_SECRET_KEY= - # before running this make target. - dev-tools/packer/docker/deb-rpm-s3/deb-rpm-s3.sh - -.PHONY: release-upload -upload-release: - aws s3 cp --recursive --acl public-read build/upload s3://download.elasticsearch.org/beats/ - .PHONY: notice notice: python-env @echo "Generating NOTICE" @@ -171,3 +132,38 @@ python-env: .PHONY: test-apm test-apm: sh ./script/test_apm.sh + +### Packaging targets #### + +# Builds a snapshot release. +.PHONY: snapshot +snapshot: + @$(MAKE) SNAPSHOT=true release + +# Builds a release. +.PHONY: release +release: beats-dashboards + @$(foreach var,$(BEATS),$(MAKE) -C $(var) release || exit 1;) + @$(foreach var,$(BEATS),mkdir -p build/distributions/$(var) && mv -f $(var)/build/distributions/* build/distributions/$(var)/ || exit 1;) + +# Builds a snapshot release. The Go version defined in .go-version will be +# installed and used for the build. +.PHONY: release-manager-snapshot +release-manager-snapshot: + ./dev-tools/run_with_go_ver $(MAKE) snapshot + +# Builds a snapshot release. The Go version defined in .go-version will be +# installed and used for the build. +.PHONY: release-manager-release +release-manager-release: + ./dev-tools/run_with_go_ver $(MAKE) release + +# Installs the mage build tool from the vendor directory. +.PHONY: mage +mage: + @go install github.com/elastic/beats/vendor/github.com/magefile/mage + +# Collects dashboards from all Beats and generates a zip file distribution. +.PHONY: beats-dashboards +beats-dashboards: mage update + @mage packageBeatDashboards diff --git a/auditbeat/Makefile b/auditbeat/Makefile index e453ca16cb0d..36c51fbf0e62 100644 --- a/auditbeat/Makefile +++ b/auditbeat/Makefile @@ -1,6 +1,5 @@ BEAT_NAME=auditbeat BEAT_TITLE=Auditbeat -BEAT_DESCRIPTION=Audit the activities of users and processes on your system. SYSTEM_TESTS=true TEST_ENVIRONMENT?=true GOX_OS?=linux windows ## @Building List of all OS to be supported by "make crosscompile". @@ -12,49 +11,6 @@ FIELDS_FILE_PATH=module # Path to the libbeat Makefile include ${ES_BEATS}/libbeat/scripts/Makefile -# This is called by the beats packer before building starts -.PHONY: before-build -before-build: - @cat ${ES_BEATS}/auditbeat/_meta/common.p1.yml \ - <(go run scripts/generate_config.go -os windows -concat) \ - ${ES_BEATS}/auditbeat/_meta/common.p2.yml \ - ${ES_BEATS}/libbeat/_meta/config.yml > \ - ${PREFIX}/${BEAT_NAME}-win.yml - @cat ${ES_BEATS}/auditbeat/_meta/common.reference.yml \ - <(go run scripts/generate_config.go -os windows -concat -ref) \ - ${ES_BEATS}/libbeat/_meta/config.reference.yml > \ - ${PREFIX}/${BEAT_NAME}-win.reference.yml - - @cat ${ES_BEATS}/auditbeat/_meta/common.p1.yml \ - <(go run scripts/generate_config.go -os darwin -concat) \ - ${ES_BEATS}/auditbeat/_meta/common.p2.yml \ - ${ES_BEATS}/libbeat/_meta/config.yml > \ - ${PREFIX}/${BEAT_NAME}-darwin.yml - @cat ${ES_BEATS}/auditbeat/_meta/common.reference.yml \ - <(go run scripts/generate_config.go -os darwin -concat -ref) \ - ${ES_BEATS}/libbeat/_meta/config.reference.yml > \ - ${PREFIX}/${BEAT_NAME}-darwin.reference.yml - - @cat ${ES_BEATS}/auditbeat/_meta/common.p1.yml \ - <(go run scripts/generate_config.go -os linux -arch amd64 -concat) \ - ${ES_BEATS}/auditbeat/_meta/common.p2.yml \ - ${ES_BEATS}/libbeat/_meta/config.yml > \ - ${PREFIX}/${BEAT_NAME}-linux.yml - @cat ${ES_BEATS}/auditbeat/_meta/common.reference.yml \ - <(go run scripts/generate_config.go -os linux -concat -ref) \ - ${ES_BEATS}/libbeat/_meta/config.reference.yml > \ - ${PREFIX}/${BEAT_NAME}-linux.reference.yml - - @cat ${ES_BEATS}/auditbeat/_meta/common.p1.yml \ - <(go run scripts/generate_config.go -os linux -arch i386 -concat) \ - ${ES_BEATS}/auditbeat/_meta/common.p2.yml \ - ${ES_BEATS}/libbeat/_meta/config.yml > \ - ${PREFIX}/${BEAT_NAME}-linux-386.yml - @cat ${ES_BEATS}/auditbeat/_meta/common.reference.yml \ - <(go run scripts/generate_config.go -os linux -concat -ref) \ - ${ES_BEATS}/libbeat/_meta/config.reference.yml > \ - ${PREFIX}/${BEAT_NAME}-linux-386.reference.yml - # Collects all dependencies and then calls update .PHONY: collect collect: fields collect-docs configs kibana @@ -63,10 +19,10 @@ collect: fields collect-docs configs kibana .PHONY: configs configs: python-env @cat ${ES_BEATS}/auditbeat/_meta/common.p1.yml \ - <(go run scripts/generate_config.go -os ${DEV_OS} -concat) \ - ${ES_BEATS}/auditbeat/_meta/common.p2.yml > _meta/beat.yml + <(go run scripts/generate_config.go -os ${DEV_OS} -concat) \ + ${ES_BEATS}/auditbeat/_meta/common.p2.yml > _meta/beat.yml @cat ${ES_BEATS}/auditbeat/_meta/common.reference.yml \ - <(go run scripts/generate_config.go -os ${DEV_OS} -ref -concat) > _meta/beat.reference.yml + <(go run scripts/generate_config.go -os ${DEV_OS} -ref -concat) > _meta/beat.reference.yml # Collects all module docs .PHONY: collect-docs diff --git a/auditbeat/magefile.go b/auditbeat/magefile.go new file mode 100644 index 000000000000..9f1723917d40 --- /dev/null +++ b/auditbeat/magefile.go @@ -0,0 +1,212 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +// +build mage + +package main + +import ( + "fmt" + "regexp" + "time" + + "github.com/magefile/mage/mg" + "github.com/magefile/mage/sh" + "github.com/pkg/errors" + + "github.com/elastic/beats/dev-tools/mage" +) + +func init() { + mage.BeatDescription = "Audit the activities of users and processes on your system." +} + +// Build builds the Beat binary. +func Build() error { + return mage.Build(mage.DefaultBuildArgs()) +} + +// GolangCrossBuild build the Beat binary inside of the golang-builder. +// Do not use directly, use crossBuild instead. +func GolangCrossBuild() error { + return mage.GolangCrossBuild(mage.DefaultGolangCrossBuildArgs()) +} + +// BuildGoDaemon builds the go-daemon binary (use crossBuildGoDaemon). +func BuildGoDaemon() error { + return mage.BuildGoDaemon() +} + +// CrossBuild cross-builds the beat for all target platforms. +func CrossBuild() error { + return mage.CrossBuild() +} + +// CrossBuildGoDaemon cross-builds the go-daemon binary using Docker. +func CrossBuildGoDaemon() error { + return mage.CrossBuildGoDaemon() +} + +// Clean cleans all generated files and build artifacts. +func Clean() error { + return mage.Clean() +} + +// Package packages the Beat for distribution. +// Use SNAPSHOT=true to build snapshots. +// Use PLATFORMS to control the target platforms. +func Package() { + start := time.Now() + defer func() { fmt.Println("package ran for", time.Since(start)) }() + + mage.UseElasticBeatPackaging() + customizePackaging() + + mg.Deps(Update) + mg.Deps(makeConfigTemplates, CrossBuild, CrossBuildGoDaemon) + mg.SerialDeps(mage.Package, TestPackages) +} + +// TestPackages tests the generated packages (i.e. file modes, owners, groups). +func TestPackages() error { + return mage.TestPackages() +} + +// Update updates the generated files (aka make update). +func Update() error { + return sh.Run("make", "update") +} + +// ----------------------------------------------------------------------------- +// Customizations specific to Auditbeat. +// - Config files are Go templates. + +const ( + configTemplateGlob = "module/*/_meta/config*.yml.tmpl" + shortConfigTemplate = "build/auditbeat.yml.tmpl" + referenceConfigTemplate = "build/auditbeat.reference.yml.tmpl" +) + +func makeConfigTemplates() error { + configFiles, err := mage.FindFiles(configTemplateGlob) + if err != nil { + return errors.Wrap(err, "failed to find config templates") + } + + var shortIn []string + shortIn = append(shortIn, "_meta/common.p1.yml") + shortIn = append(shortIn, configFiles...) + shortIn = append(shortIn, "_meta/common.p2.yml") + shortIn = append(shortIn, "../libbeat/_meta/config.yml") + if !mage.IsUpToDate(shortConfigTemplate, shortIn...) { + fmt.Println(">> Building", shortConfigTemplate) + mage.MustFileConcat(shortConfigTemplate, 0600, shortIn...) + mage.MustFindReplace(shortConfigTemplate, regexp.MustCompile("beatname"), "{{.BeatName}}") + mage.MustFindReplace(shortConfigTemplate, regexp.MustCompile("beat-index-prefix"), "{{.BeatIndexPrefix}}") + } + + var referenceIn []string + referenceIn = append(referenceIn, "_meta/common.reference.yml") + referenceIn = append(referenceIn, configFiles...) + referenceIn = append(referenceIn, "../libbeat/_meta/config.reference.yml") + if !mage.IsUpToDate(referenceConfigTemplate, referenceIn...) { + fmt.Println(">> Building", referenceConfigTemplate) + mage.MustFileConcat(referenceConfigTemplate, 0644, referenceIn...) + mage.MustFindReplace(referenceConfigTemplate, regexp.MustCompile("beatname"), "{{.BeatName}}") + mage.MustFindReplace(referenceConfigTemplate, regexp.MustCompile("beat-index-prefix"), "{{.BeatIndexPrefix}}") + } + + return nil +} + +// customizePackaging modifies the package specs to use templated config files +// instead of the defaults. +func customizePackaging() { + var ( + shortConfig = mage.PackageFile{ + Mode: 0600, + Source: "{{.PackageDir}}/auditbeat.yml", + Dep: generateShortConfig, + } + referenceConfig = mage.PackageFile{ + Mode: 0644, + Source: "{{.PackageDir}}/auditbeat.reference.yml", + Dep: generateReferenceConfig, + } + ) + + for _, args := range mage.Packages { + switch args.Types[0] { + case mage.TarGz, mage.Zip: + args.Spec.ReplaceFile("{{.BeatName}}.yml", shortConfig) + args.Spec.ReplaceFile("{{.BeatName}}.reference.yml", referenceConfig) + default: + args.Spec.ReplaceFile("/etc/{{.BeatName}}/{{.BeatName}}.yml", shortConfig) + args.Spec.ReplaceFile("/etc/{{.BeatName}}/{{.BeatName}}.reference.yml", referenceConfig) + } + } +} + +func generateReferenceConfig(spec mage.PackageSpec) error { + params := map[string]interface{}{ + "Reference": true, + "ArchBits": archBits, + } + return spec.ExpandFile(referenceConfigTemplate, + "{{.PackageDir}}/auditbeat.reference.yml", params) +} + +func generateShortConfig(spec mage.PackageSpec) error { + params := map[string]interface{}{ + "Reference": false, + "ArchBits": archBits, + } + return spec.ExpandFile(shortConfigTemplate, + "{{.PackageDir}}/auditbeat.yml", params) +} + +// archBits returns the number of bit width of the GOARCH architecture value. +// This function is used by the auditd module configuration templates to +// generate architecture specific audit rules. +func archBits(goarch string) int { + switch goarch { + case "386", "arm": + return 32 + default: + return 64 + } +} + +// Configs generates the auditbeat.yml and auditbeat.reference.yml config files. +// Set DEV_OS and DEV_ARCH to change the target host for the generated configs. +// Defaults to linux/amd64. +func Configs() { + mg.Deps(makeConfigTemplates) + + params := map[string]interface{}{ + "GOOS": mage.EnvOr("DEV_OS", "linux"), + "GOARCH": mage.EnvOr("DEV_ARCH", "amd64"), + "ArchBits": archBits, + "Reference": false, + } + fmt.Printf(">> Building auditbeat.yml for %v/%v\n", params["GOOS"], params["GOARCH"]) + mage.MustExpandFile(shortConfigTemplate, "auditbeat.yml", params) + + params["Reference"] = true + fmt.Printf(">> Building auditbeat.reference.yml for %v/%v\n", params["GOOS"], params["GOARCH"]) + mage.MustExpandFile(referenceConfigTemplate, "auditbeat.reference.yml", params) +} diff --git a/auditbeat/module/auditd/_meta/config.yml.tpl b/auditbeat/module/auditd/_meta/config.yml.tmpl similarity index 65% rename from auditbeat/module/auditd/_meta/config.yml.tpl rename to auditbeat/module/auditd/_meta/config.yml.tmpl index 30cb58df8423..ac1d21fa6cd0 100644 --- a/auditbeat/module/auditd/_meta/config.yml.tpl +++ b/auditbeat/module/auditd/_meta/config.yml.tmpl @@ -1,10 +1,10 @@ -{{ if eq .goos "linux" -}} -{{ if .reference -}} +{{ if eq .GOOS "linux" -}} +{{ if .Reference -}} # The auditd module collects events from the audit framework in the Linux # kernel. You need to specify audit rules for the events that you want to audit. {{ end -}} - module: auditd - {{ if .reference -}} + {{ if .Reference -}} resolve_ids: true failure_mode: silent backlog_limit: 8196 @@ -17,7 +17,7 @@ ## Create file watches (-w) or syscall audits (-a or -A). Uncomment these ## examples or add your own rules. - {{ if eq .goarch "amd64" -}} + {{ if eq .GOARCH "amd64" -}} ## If you are on a 64 bit platform, everything should be running ## in 64 bit mode. This rule will detect any use of the 32 bit syscalls ## because this might be a sign of someone exploiting a hole in the 32 @@ -26,10 +26,10 @@ {{ end -}} ## Executions. - #-a always,exit -F arch=b{{.arch_bits}} -S execve,execveat -k exec + #-a always,exit -F arch=b{{call .ArchBits .GOARCH}} -S execve,execveat -k exec ## External access (warning: these can be expensive to audit). - #-a always,exit -F arch=b{{.arch_bits}} -S accept,bind,connect -F key=external-access + #-a always,exit -F arch=b{{call .ArchBits .GOARCH}} -S accept,bind,connect -F key=external-access ## Identity changes. #-w /etc/group -p wa -k identity @@ -37,6 +37,6 @@ #-w /etc/gshadow -p wa -k identity ## Unauthorized access attempts. - #-a always,exit -F arch=b{{.arch_bits}} -S open,creat,truncate,ftruncate,openat,open_by_handle_at -F exit=-EACCES -k access - #-a always,exit -F arch=b{{.arch_bits}} -S open,creat,truncate,ftruncate,openat,open_by_handle_at -F exit=-EPERM -k access + #-a always,exit -F arch=b{{call .ArchBits .GOARCH}} -S open,creat,truncate,ftruncate,openat,open_by_handle_at -F exit=-EACCES -k access + #-a always,exit -F arch=b{{call .ArchBits .GOARCH}} -S open,creat,truncate,ftruncate,openat,open_by_handle_at -F exit=-EPERM -k access {{ end -}} diff --git a/auditbeat/module/file_integrity/_meta/config.yml.tpl b/auditbeat/module/file_integrity/_meta/config.yml.tmpl similarity index 90% rename from auditbeat/module/file_integrity/_meta/config.yml.tpl rename to auditbeat/module/file_integrity/_meta/config.yml.tmpl index 18f84a6f8a41..774a430eba32 100644 --- a/auditbeat/module/file_integrity/_meta/config.yml.tpl +++ b/auditbeat/module/file_integrity/_meta/config.yml.tmpl @@ -1,9 +1,9 @@ -{{ if .reference -}} +{{ if .Reference -}} # The file integrity module sends events when files are changed (created, # updated, deleted). The events contain file metadata and hashes. {{ end -}} - module: file_integrity - {{ if eq .goos "darwin" -}} + {{ if eq .GOOS "darwin" -}} paths: - /bin - /usr/bin @@ -11,7 +11,7 @@ - /sbin - /usr/sbin - /usr/local/sbin - {{ else if eq .goos "windows" -}} + {{ else if eq .GOOS "windows" -}} paths: - C:/windows - C:/windows/system32 @@ -25,15 +25,15 @@ - /usr/sbin - /etc {{- end }} -{{ if .reference }} +{{ if .Reference }} # List of regular expressions to filter out notifications for unwanted files. # Wrap in single quotes to workaround YAML escaping rules. By default no files # are ignored. - {{ if eq .goos "darwin" -}} + {{ if eq .GOOS "darwin" -}} exclude_files: - '\.DS_Store$' - '\.swp$' - {{ else if eq .goos "windows" -}} + {{ else if eq .GOOS "windows" -}} exclude_files: - '(?i)\.lnk$' - '(?i)\.swp$' diff --git a/auditbeat/scripts/generate_config.go b/auditbeat/scripts/generate_config.go index 60dc006eaa14..ef8f82928ff2 100644 --- a/auditbeat/scripts/generate_config.go +++ b/auditbeat/scripts/generate_config.go @@ -31,7 +31,7 @@ import ( "github.com/pkg/errors" ) -const defaultGlob = "module/*/_meta/config*.yml.tpl" +const defaultGlob = "module/*/_meta/config*.yml.tmpl" var ( goos = flag.String("os", runtime.GOOS, "generate config specific to the specified operating system") @@ -52,26 +52,29 @@ func findConfigFiles(globs []string) ([]string, error) { return configFiles, nil } +// archBits returns the number of bit width of the GOARCH architecture value. +// This function is used by the auditd module configuration templates to +// generate architecture specific audit rules. +func archBits(goarch string) int { + switch goarch { + case "386", "arm": + return 32 + default: + return 64 + } +} + func getConfig(file string) ([]byte, error) { tpl, err := template.ParseFiles(file) if err != nil { return nil, errors.Wrapf(err, "failed reading %v", file) } - var archBits string - switch *goarch { - case "i386": - archBits = "32" - case "amd64": - archBits = "64" - default: - return nil, fmt.Errorf("supporting only i386 and amd64 architecture") - } data := map[string]interface{}{ - "goarch": *goarch, - "goos": *goos, - "reference": *reference, - "arch_bits": archBits, + "GOARCH": *goarch, + "GOOS": *goos, + "Reference": *reference, + "ArchBits": archBits, } buf := new(bytes.Buffer) if err = tpl.Execute(buf, data); err != nil { @@ -126,7 +129,7 @@ func main() { if *concat { segments = append(segments, segment) } else { - output(segment, strings.TrimSuffix(file, ".tpl")) + output(segment, strings.TrimSuffix(file, ".tmpl")) } } diff --git a/dev-tools/common.bash b/dev-tools/common.bash index 048c835f2c00..cb8a8d5c0863 100644 --- a/dev-tools/common.bash +++ b/dev-tools/common.bash @@ -91,4 +91,8 @@ jenkins_setup() { # Workaround for Python virtualenv path being too long. export TEMP_PYTHON_ENV=$(mktemp -d) export PYTHON_ENV="${TEMP_PYTHON_ENV}/python-env" + + # Write cached magefile binaries to workspace to ensure + # each run starts from a clean slate. + export MAGEFILE_CACHE="${WORKSPACE}/.magefile" } diff --git a/dev-tools/deploy b/dev-tools/deploy index 812a003439d2..2b0de52e4c08 100755 --- a/dev-tools/deploy +++ b/dev-tools/deploy @@ -16,9 +16,9 @@ def main(): check_call("make clean", shell=True) print("Done building Docker images.") if args.no_snapshot: - check_call("make SNAPSHOT=no package-all", shell=True) + check_call("make release", shell=True) else: - check_call("make SNAPSHOT=yes package-all", shell=True) + check_call("make snapshot", shell=True) print("All done") if __name__ == "__main__": diff --git a/dev-tools/jenkins_ci.ps1 b/dev-tools/jenkins_ci.ps1 index 4db34c03bc8c..a27381a36e99 100755 --- a/dev-tools/jenkins_ci.ps1 +++ b/dev-tools/jenkins_ci.ps1 @@ -17,6 +17,10 @@ $env:GOPATH = $env:WORKSPACE $env:PATH = "$env:GOPATH\bin;C:\tools\mingw64\bin;$env:PATH" & gvm --format=powershell $(Get-Content .go-version) | Invoke-Expression +# Write cached magefile binaries to workspace to ensure +# each run starts from a clean slate. +$env:MAGEFILE_CACHE = "$env:WORKSPACE\.magefile" + if (Test-Path "$env:beat") { cd "$env:beat" } else { diff --git a/dev-tools/jenkins_release.sh b/dev-tools/jenkins_release.sh new file mode 100755 index 000000000000..a0915a7fe739 --- /dev/null +++ b/dev-tools/jenkins_release.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +set -euox pipefail + +: "${HOME:?Need to set HOME to a non-empty value.}" +: "${WORKSPACE:?Need to set WORKSPACE to a non-empty value.}" + +source ./dev-tools/common.bash + +jenkins_setup + +cleanup() { + echo "Running cleanup..." + rm -rf $TEMP_PYTHON_ENV + echo "Cleanup complete." +} +trap cleanup EXIT + +make release diff --git a/dev-tools/mage/.gitignore b/dev-tools/mage/.gitignore new file mode 100644 index 000000000000..378eac25d311 --- /dev/null +++ b/dev-tools/mage/.gitignore @@ -0,0 +1 @@ +build diff --git a/dev-tools/mage/build.go b/dev-tools/mage/build.go new file mode 100644 index 000000000000..86bd420930c4 --- /dev/null +++ b/dev-tools/mage/build.go @@ -0,0 +1,144 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package mage + +import ( + "fmt" + "go/build" + "log" + "os" + "path/filepath" + "strings" + + "github.com/magefile/mage/sh" + "github.com/pkg/errors" +) + +// BuildArgs are the arguments used for the "build" target and they define how +// "go build" is invoked. +type BuildArgs struct { + Name string // Name of binary. (On Windows '.exe' is appended.) + OutputDir string + CGO bool + Static bool + Env map[string]string + LDFlags []string + Vars map[string]string // Vars that are passed as -X key=value with the ldflags. + ExtraFlags []string +} + +// DefaultBuildArgs returns the default BuildArgs for use in builds. +func DefaultBuildArgs() BuildArgs { + args := BuildArgs{ + Name: BeatName, + CGO: build.Default.CgoEnabled, + Vars: map[string]string{ + "github.com/elastic/beats/libbeat/version.buildTime": "{{ date }}", + "github.com/elastic/beats/libbeat/version.commit": "{{ commit }}", + }, + } + + repo, err := GetProjectRepoInfo() + if err != nil { + panic(errors.Wrap(err, "failed to determine project repo info")) + } + + if !repo.IsElasticBeats() { + // Assume libbeat is vendored and prefix the variables. + prefix := repo.RootImportPath + "/vendor/" + prefixedVars := map[string]string{} + for k, v := range args.Vars { + prefixedVars[prefix+k] = v + } + args.Vars = prefixedVars + } + + return args +} + +// DefaultGolangCrossBuildArgs returns the default BuildArgs for use in +// cross-builds. +func DefaultGolangCrossBuildArgs() BuildArgs { + args := DefaultBuildArgs() + args.Name += "-" + Platform.GOOS + "-" + Platform.Arch + args.OutputDir = filepath.Join("build", "golang-crossbuild") + if bp, found := BuildPlatforms.Get(Platform.Name); found { + args.CGO = bp.Flags.SupportsCGO() + } + return args +} + +// GolangCrossBuild invokes "go build" inside of the golang-crossbuild Docker +// environment. +func GolangCrossBuild(params BuildArgs) error { + if os.Getenv("GOLANG_CROSSBUILD") != "1" { + return errors.New("Use the crossBuild target. golangCrossBuild can " + + "only be executed within the golang-crossbuild docker environment.") + } + + defer DockerChown(filepath.Join(params.OutputDir, params.Name+binaryExtension(GOOS))) + return Build(params) +} + +// Build invokes "go build" to produce a binary. +func Build(params BuildArgs) error { + fmt.Println(">> build: Building", params.Name) + + binaryName := params.Name + binaryExtension(GOOS) + + if params.OutputDir != "" { + if err := os.MkdirAll(params.OutputDir, 0755); err != nil { + return err + } + } + + // Environment + env := params.Env + if env == nil { + env = map[string]string{} + } + cgoEnabled := "0" + if params.CGO { + cgoEnabled = "1" + } + env["CGO_ENABLED"] = cgoEnabled + + // Spec + args := []string{ + "build", + "-o", + filepath.Join(params.OutputDir, binaryName), + } + args = append(args, params.ExtraFlags...) + + // ldflags + ldflags := params.LDFlags + if params.Static { + ldflags = append(ldflags, `-extldflags '-static'`) + } + for k, v := range params.Vars { + ldflags = append(ldflags, fmt.Sprintf("-X %v=%v", k, v)) + } + if len(ldflags) > 0 { + args = append(args, "-ldflags") + args = append(args, MustExpand(strings.Join(ldflags, " "))) + } + + log.Println("Adding build environment vars:", env) + return sh.RunWith(env, "go", args...) +} diff --git a/dev-tools/mage/clean.go b/dev-tools/mage/clean.go new file mode 100644 index 000000000000..912d372e1dff --- /dev/null +++ b/dev-tools/mage/clean.go @@ -0,0 +1,54 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package mage + +import ( + "github.com/magefile/mage/sh" +) + +// DefaultCleanPaths specifies a list of files or paths to recursively delete. +// The values may contain variables and will be expanded at the time of use. +var DefaultCleanPaths = []string{ + "build", + "docker-compose.yml.lock", + "{{.BeatName}}", + "{{.BeatName}}.exe", + "{{.BeatName}}.test", + "{{.BeatName}}.test.exe", + "fields.yml", + "_meta/fields.generated.yml", + "_meta/kibana.generated", + "_meta/kibana/5/index-pattern/{{.BeatName}}.json", + "_meta/kibana/6/index-pattern/{{.BeatName}}.json", +} + +// Clean clean generated build artifacts. +func Clean(pathLists ...[]string) error { + if len(pathLists) == 0 { + pathLists = [][]string{DefaultCleanPaths} + } + for _, paths := range pathLists { + for _, f := range paths { + f = MustExpand(f) + if err := sh.Rm(f); err != nil { + return err + } + } + } + return nil +} diff --git a/dev-tools/mage/common.go b/dev-tools/mage/common.go new file mode 100644 index 000000000000..1ab47d6b5ded --- /dev/null +++ b/dev-tools/mage/common.go @@ -0,0 +1,690 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package mage + +import ( + "archive/tar" + "archive/zip" + "bufio" + "bytes" + "compress/gzip" + "context" + "crypto/sha256" + "crypto/sha512" + "encoding/hex" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "log" + "net/http" + "os" + "path/filepath" + "regexp" + "runtime" + "strconv" + "strings" + "sync" + "text/template" + "time" + "unicode" + + "github.com/magefile/mage/sh" + "github.com/magefile/mage/target" + "github.com/magefile/mage/types" + "github.com/pkg/errors" +) + +// Expand expands the given Go text/template string. +func Expand(in string, args ...map[string]interface{}) (string, error) { + return expandTemplate("inline", in, FuncMap, EnvMap(args...)) +} + +// MustExpand expands the given Go text/template string. It panics if there is +// an error. +func MustExpand(in string, args ...map[string]interface{}) string { + out, err := Expand(in, args...) + if err != nil { + panic(err) + } + return out +} + +// ExpandFile expands the Go text/template read from src and writes the output +// to dst. +func ExpandFile(src, dst string, args ...map[string]interface{}) error { + return expandFile(src, dst, EnvMap(args...)) +} + +// MustExpandFile expands the Go text/template read from src and writes the +// output to dst. It panics if there is an error. +func MustExpandFile(src, dst string, args ...map[string]interface{}) { + if err := ExpandFile(src, dst, args...); err != nil { + panic(err) + } +} + +func expandTemplate(name, tmpl string, funcs template.FuncMap, args ...map[string]interface{}) (string, error) { + t := template.New(name).Option("missingkey=error") + if len(funcs) > 0 { + t = t.Funcs(funcs) + } + + t, err := t.Parse(tmpl) + if err != nil { + if name == "inline" { + return "", errors.Wrapf(err, "failed to parse template '%v'", tmpl) + } + return "", errors.Wrap(err, "failed to parse template") + } + + buf := new(bytes.Buffer) + if err := t.Execute(buf, joinMaps(args...)); err != nil { + if name == "inline" { + return "", errors.Wrapf(err, "failed to expand template '%v'", tmpl) + } + return "", errors.Wrap(err, "failed to expand template") + } + + return buf.String(), nil +} + +func joinMaps(args ...map[string]interface{}) map[string]interface{} { + switch len(args) { + case 0: + return nil + case 1: + return args[0] + } + + var out map[string]interface{} + for _, m := range args { + for k, v := range m { + out[k] = v + } + } + return out +} + +func expandFile(src, dst string, args ...map[string]interface{}) error { + tmplData, err := ioutil.ReadFile(src) + if err != nil { + return errors.Wrapf(err, "failed reading from template %v", src) + } + + output, err := expandTemplate(src, string(tmplData), FuncMap, args...) + if err != nil { + return err + } + + dst, err = expandTemplate("inline", dst, FuncMap, args...) + if err != nil { + return err + } + + if err = ioutil.WriteFile(createDir(dst), []byte(output), 0644); err != nil { + return errors.Wrap(err, "failed to write rendered template") + } + + return nil +} + +// CWD return the current working directory. +func CWD() string { + wd, err := os.Getwd() + if err != nil { + panic(errors.Wrap(err, "failed to get the CWD")) + } + return wd +} + +// EnvOr returns the value of the specified environment variable if it is +// non-empty. Otherwise it return def. +func EnvOr(name, def string) string { + s := os.Getenv(name) + if s == "" { + return def + } + return s +} + +var ( + dockerInfoValue *DockerInfo + dockerInfoErr error + dockerInfoOnce sync.Once +) + +// DockerInfo contains information about the docker daemon. +type DockerInfo struct { + OperatingSystem string `json:"OperatingSystem"` + Labels []string `json:"Labels"` + NCPU int `json:"NCPU"` + MemTotal int `json:"MemTotal"` +} + +// IsBoot2Docker returns true if the Docker OS is boot2docker. +func (info *DockerInfo) IsBoot2Docker() bool { + return strings.Contains(strings.ToLower(info.OperatingSystem), "boot2docker") +} + +// HaveDocker returns an error if docker is unavailable. +func HaveDocker() error { + if _, err := GetDockerInfo(); err != nil { + return errors.Wrap(err, "docker is not available") + } + return nil +} + +// GetDockerInfo returns data from the docker info command. +func GetDockerInfo() (*DockerInfo, error) { + dockerInfoOnce.Do(func() { + dockerInfoValue, dockerInfoErr = dockerInfo() + }) + + return dockerInfoValue, dockerInfoErr +} + +func dockerInfo() (*DockerInfo, error) { + data, err := sh.Output("docker", "info", "-f", "{{ json .}}") + if err != nil { + return nil, err + } + + var info DockerInfo + if err = json.Unmarshal([]byte(data), &info); err != nil { + return nil, err + } + + return &info, nil +} + +// FindReplace reads a file, performs a find/replace operation, then writes the +// output to the same file path. +func FindReplace(file string, re *regexp.Regexp, repl string) error { + info, err := os.Stat(file) + if err != nil { + return err + } + + contents, err := ioutil.ReadFile(file) + if err != nil { + return err + } + + out := re.ReplaceAllString(string(contents), repl) + return ioutil.WriteFile(file, []byte(out), info.Mode().Perm()) +} + +// MustFindReplace invokes FindReplace and panics if an error occurs. +func MustFindReplace(file string, re *regexp.Regexp, repl string) { + if err := FindReplace(file, re, repl); err != nil { + panic(errors.Wrap(err, "failed to find and replace")) + } +} + +// Copy copies a file or a directory (recursively) and preserves the permissions. +func Copy(src, dest string) error { + info, err := os.Stat(src) + if err != nil { + return errors.Wrapf(err, "failed to stat source file %v", src) + } + return recursiveCopy(src, dest, info) +} + +func fileCopy(src, dest string, info os.FileInfo) error { + srcFile, err := os.Open(src) + if err != nil { + return err + } + defer srcFile.Close() + + if !info.Mode().IsRegular() { + return errors.Errorf("failed to copy source file because it is not a regular file") + } + + destFile, err := os.OpenFile(createDir(dest), os.O_CREATE|os.O_TRUNC|os.O_WRONLY, info.Mode()&os.ModePerm) + if err != nil { + return err + } + defer destFile.Close() + + if _, err = io.Copy(destFile, srcFile); err != nil { + return err + } + return destFile.Close() +} + +func dirCopy(src, dest string, info os.FileInfo) error { + if err := os.MkdirAll(dest, info.Mode()); err != nil { + return errors.Wrap(err, "failed creating dirs") + } + + contents, err := ioutil.ReadDir(src) + if err != nil { + return errors.Wrapf(err, "failed to read dir %v", src) + } + + for _, info := range contents { + srcFile := filepath.Join(src, info.Name()) + destFile := filepath.Join(dest, info.Name()) + if err = recursiveCopy(srcFile, destFile, info); err != nil { + return errors.Wrapf(err, "failed to copy %v to %v", srcFile, destFile) + } + } + + return nil +} + +func recursiveCopy(src, dest string, info os.FileInfo) error { + if info.IsDir() { + return dirCopy(src, dest, info) + } + return fileCopy(src, dest, info) +} + +// DownloadFile downloads the given URL and writes the file to destinationDir. +// The path to the file is returned. +func DownloadFile(url, destinationDir string) (string, error) { + log.Println("Downloading", url) + + resp, err := http.Get(url) + if err != nil { + return "", errors.Wrap(err, "http get failed") + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return "", errors.Errorf("download failed with http status: %v", resp.StatusCode) + } + + name := filepath.Join(destinationDir, filepath.Base(url)) + f, err := os.Create(createDir(name)) + if err != nil { + return "", errors.Wrap(err, "failed to create output file") + } + defer f.Close() + + if _, err = io.Copy(f, resp.Body); err != nil { + return "", errors.Wrap(err, "failed to write file") + } + + return name, f.Close() +} + +// Extract extracts .zip, .tar.gz, or .tgz files to destinationDir. +func Extract(sourceFile, destinationDir string) error { + ext := filepath.Ext(sourceFile) + switch { + case strings.HasSuffix(sourceFile, ".tar.gz"), ext == ".tgz": + return untar(sourceFile, destinationDir) + case ext == ".zip": + return unzip(sourceFile, destinationDir) + default: + return errors.Errorf("failed to extract %v, unhandled file extension", sourceFile) + } +} + +func unzip(sourceFile, destinationDir string) error { + r, err := zip.OpenReader(sourceFile) + if err != nil { + return err + } + defer r.Close() + + if err = os.MkdirAll(destinationDir, 0755); err != nil { + return err + } + + extractAndWriteFile := func(f *zip.File) error { + innerFile, err := f.Open() + if err != nil { + return err + } + defer innerFile.Close() + + path := filepath.Join(destinationDir, f.Name) + if !strings.HasPrefix(path, destinationDir) { + return errors.Errorf("illegal file path in zip: %v", f.Name) + } + + if f.FileInfo().IsDir() { + return os.MkdirAll(path, f.Mode()) + } + + if err = os.MkdirAll(filepath.Dir(path), 0755); err != nil { + return err + } + + out, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode()) + if err != nil { + return err + } + defer out.Close() + + if _, err = io.Copy(out, innerFile); err != nil { + return err + } + + return out.Close() + } + + for _, f := range r.File { + err := extractAndWriteFile(f) + if err != nil { + return err + } + } + + return nil +} + +func untar(sourceFile, destinationDir string) error { + file, err := os.Open(sourceFile) + if err != nil { + return err + } + defer file.Close() + + var fileReader io.ReadCloser = file + + if strings.HasSuffix(sourceFile, ".gz") { + if fileReader, err = gzip.NewReader(file); err != nil { + return err + } + defer fileReader.Close() + } + + tarReader := tar.NewReader(fileReader) + + for { + header, err := tarReader.Next() + if err != nil { + if err == io.EOF { + break + } + return err + } + + path := filepath.Join(destinationDir, header.Name) + if !strings.HasPrefix(path, destinationDir) { + return errors.Errorf("illegal file path in tar: %v", header.Name) + } + + switch header.Typeflag { + case tar.TypeDir: + if err = os.MkdirAll(path, os.FileMode(header.Mode)); err != nil { + return err + } + case tar.TypeReg: + writer, err := os.Create(path) + if err != nil { + return err + } + + if _, err = io.Copy(writer, tarReader); err != nil { + return err + } + + if err = os.Chmod(path, os.FileMode(header.Mode)); err != nil { + return err + } + + if err = writer.Close(); err != nil { + return err + } + default: + return errors.Errorf("unable to untar type=%c in file=%s", header.Typeflag, path) + } + } + + return nil +} + +func isSeparator(r rune) bool { + return unicode.IsSpace(r) || r == ',' || r == ';' +} + +// RunCmds runs the given commands and stops upon the first error. +func RunCmds(cmds ...[]string) error { + for _, cmd := range cmds { + if err := sh.Run(cmd[0], cmd[1:]...); err != nil { + return err + } + } + return nil +} + +var ( + parallelJobsLock sync.Mutex + parallelJobsSemaphore chan int +) + +func parallelJobs() chan int { + parallelJobsLock.Lock() + defer parallelJobsLock.Unlock() + + if parallelJobsSemaphore == nil { + max := numParallel() + parallelJobsSemaphore = make(chan int, max) + log.Println("Max parallel jobs =", max) + } + + return parallelJobsSemaphore +} + +func numParallel() int { + if maxParallel := os.Getenv("MAX_PARALLEL"); maxParallel != "" { + if num, err := strconv.Atoi(maxParallel); err == nil && num > 0 { + return num + } + } + + // To be conservative use the minimum of the number of CPUs between the host + // and the Docker host. + maxParallel := runtime.NumCPU() + + info, err := GetDockerInfo() + if err == nil && info.NCPU < maxParallel { + maxParallel = info.NCPU + } + + return maxParallel +} + +// ParallelCtx runs the given functions in parallel with an upper limit set +// based on GOMAXPROCS. The provided ctx is passed to the functions (if they +// accept it as a param). +func ParallelCtx(ctx context.Context, fns ...interface{}) { + var fnWrappers []func(context.Context) error + for _, f := range fns { + fnWrapper := types.FuncTypeWrap(f) + if fnWrapper == nil { + panic("attempted to add a dep that did not match required function type") + } + fnWrappers = append(fnWrappers, fnWrapper) + } + + var mu sync.Mutex + var errs []string + var wg sync.WaitGroup + + for _, fw := range fnWrappers { + wg.Add(1) + go func(fw func(context.Context) error) { + defer func() { + if v := recover(); v != nil { + mu.Lock() + errs = append(errs, fmt.Sprint(v)) + mu.Unlock() + } + wg.Done() + <-parallelJobs() + }() + waitStart := time.Now() + parallelJobs() <- 1 + log.Println("Parallel job waited", time.Since(waitStart), "before starting.") + if err := fw(ctx); err != nil { + mu.Lock() + errs = append(errs, fmt.Sprint(err)) + mu.Unlock() + } + }(fw) + } + + wg.Wait() + if len(errs) > 0 { + panic(errors.Errorf(strings.Join(errs, "\n"))) + } +} + +// Parallel runs the given functions in parallel with an upper limit set based +// on GOMAXPROCS. +func Parallel(fns ...interface{}) { + ParallelCtx(context.Background(), fns...) +} + +// FindFiles return a list of file matching the given glob patterns. +func FindFiles(globs ...string) ([]string, error) { + var configFiles []string + for _, glob := range globs { + files, err := filepath.Glob(glob) + if err != nil { + return nil, errors.Wrapf(err, "failed on glob %v", glob) + } + configFiles = append(configFiles, files...) + } + return configFiles, nil +} + +// FileConcat concatenates files and writes the output to out. +func FileConcat(out string, perm os.FileMode, files ...string) error { + f, err := os.OpenFile(createDir(out), os.O_CREATE|os.O_TRUNC|os.O_WRONLY, perm) + if err != nil { + return errors.Wrap(err, "failed to create file") + } + defer f.Close() + + w := bufio.NewWriter(f) + + append := func(file string) error { + in, err := os.Open(file) + if err != nil { + return err + } + defer in.Close() + + if _, err := io.Copy(w, in); err != nil { + return err + } + + return nil + } + + for _, in := range files { + if err := append(in); err != nil { + return err + } + } + + if err = w.Flush(); err != nil { + return err + } + return f.Close() +} + +// MustFileConcat invokes FileConcat and panics if an error occurs. +func MustFileConcat(out string, perm os.FileMode, files ...string) { + if err := FileConcat(out, perm, files...); err != nil { + panic(err) + } +} + +// VerifySHA256 reads a file and verifies that its SHA256 sum matches the +// specified hash. +func VerifySHA256(file string, hash string) error { + f, err := os.Open(file) + if err != nil { + return errors.Wrap(err, "failed to open file for sha256 verification") + } + defer f.Close() + + sum := sha256.New() + if _, err := io.Copy(sum, f); err != nil { + return errors.Wrap(err, "failed reading from input file") + } + + computedHash := hex.EncodeToString(sum.Sum(nil)) + expectedHash := strings.TrimSpace(hash) + + if computedHash != expectedHash { + return errors.Errorf("SHA256 verification of %v failed. Expected=%v, "+ + "but computed=%v", f.Name(), expectedHash, computedHash) + } + log.Println("SHA256 OK:", f.Name()) + + return nil +} + +// CreateSHA512File computes the sha512 sum of the specified file the writes +// a sidecar file containing the hash and filename. +func CreateSHA512File(file string) error { + f, err := os.Open(file) + if err != nil { + return errors.Wrap(err, "failed to open file for sha512 summing") + } + defer f.Close() + + sum := sha512.New() + if _, err := io.Copy(sum, f); err != nil { + return errors.Wrap(err, "failed reading from input file") + } + + computedHash := hex.EncodeToString(sum.Sum(nil)) + out := fmt.Sprintf("%v %v", computedHash, filepath.Base(file)) + + return ioutil.WriteFile(file+".sha512", []byte(out), 0644) +} + +// IsUpToDate returns true iff dst exists and is older based on modtime than all +// of the sources. +func IsUpToDate(dst string, sources ...string) bool { + if len(sources) == 0 { + panic("No sources passed to IsUpToDate") + } + execute, err := target.Path(dst, sources...) + return err == nil && !execute +} + +// createDir creates the parent directory for the given file. +func createDir(file string) string { + // Create the output directory. + if dir := filepath.Dir(file); dir != "." { + if err := os.MkdirAll(dir, 0755); err != nil { + panic(errors.Wrapf(err, "failed to create parent dir for %v", file)) + } + } + return file +} + +// binaryExtension returns the appropriate file extension based on GOOS. +func binaryExtension(goos string) string { + if goos == "windows" { + return ".exe" + } + return "" +} diff --git a/dev-tools/mage/crossbuild.go b/dev-tools/mage/crossbuild.go new file mode 100644 index 000000000000..4a41cde08017 --- /dev/null +++ b/dev-tools/mage/crossbuild.go @@ -0,0 +1,239 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package mage + +import ( + "fmt" + "log" + "os" + "path/filepath" + "runtime" + "strconv" + "strings" + + "github.com/magefile/mage/mg" + "github.com/magefile/mage/sh" + "github.com/pkg/errors" +) + +const defaultCrossBuildTarget = "golangCrossBuild" + +// Platforms contains the set of target platforms for cross-builds. It can be +// modified at runtime by setting the PLATFORMS environment variable. +// See NewPlatformList for details about platform filtering expressions. +var Platforms = BuildPlatforms.Defaults() + +func init() { + // Allow overriding via PLATFORMS. + if expression := os.Getenv("PLATFORMS"); len(expression) > 0 { + Platforms = NewPlatformList(expression) + } +} + +// CrossBuildOption defines a option to the CrossBuild target. +type CrossBuildOption func(params *crossBuildParams) + +// ForPlatforms filters the platforms based on the given expression. +func ForPlatforms(expr string) func(params *crossBuildParams) { + return func(params *crossBuildParams) { + params.Platforms = params.Platforms.Filter(expr) + } +} + +// WithTarget specifies the mage target to execute inside the golang-crossbuild +// container. +func WithTarget(target string) func(params *crossBuildParams) { + return func(params *crossBuildParams) { + params.Target = target + } +} + +// Serially causes each cross-build target to be executed serially instead of +// in parallel. +func Serially() func(params *crossBuildParams) { + return func(params *crossBuildParams) { + params.Serial = true + } +} + +type crossBuildParams struct { + Platforms BuildPlatformList + Target string + Serial bool +} + +// CrossBuild executes a given build target once for each target platform. +func CrossBuild(options ...CrossBuildOption) error { + params := crossBuildParams{Platforms: Platforms, Target: defaultCrossBuildTarget} + for _, opt := range options { + opt(¶ms) + } + + // Docker is required for this target. + if err := HaveDocker(); err != nil { + return err + } + + if len(params.Platforms) == 0 { + log.Printf("Skipping cross-build of target=%v because platforms list is empty.", params.Target) + return nil + } + + // Build the magefile for Linux so we can run it inside the container. + mg.Deps(buildMage) + + log.Println("crossBuild: Platform list =", params.Platforms) + var deps []interface{} + for _, buildPlatform := range params.Platforms { + if !buildPlatform.Flags.CanCrossBuild() { + return fmt.Errorf("unsupported cross build platform %v", buildPlatform.Name) + } + + builder := GolangCrossBuilder{buildPlatform.Name, params.Target} + if params.Serial { + if err := builder.Build(); err != nil { + return errors.Wrapf(err, "failed cross-building target=%v for platform=%v", + params.Target, buildPlatform.Name) + } + } else { + deps = append(deps, builder.Build) + } + } + + // Each build runs in parallel. + Parallel(deps...) + return nil +} + +// buildMage pre-compiles the magefile to a binary using the native GOOS/GOARCH +// values for Docker. This is required to so that we can later pass GOOS and +// GOARCH to mage for the cross-build. It has the benefit of speeding up the +// build because the mage -compile is done only once rather than in each Docker +// container. +func buildMage() error { + env := map[string]string{ + "GOOS": "linux", + "GOARCH": "amd64", + } + return sh.RunWith(env, "mage", "-f", "-compile", filepath.Join("build", "mage-linux-amd64")) +} + +func crossBuildImage(platform string) (string, error) { + tagSuffix := "main" + + switch { + case strings.HasPrefix(platform, "darwin"): + tagSuffix = "darwin" + case strings.HasPrefix(platform, "linux/arm"): + tagSuffix = "arm" + case strings.HasPrefix(platform, "linux/mips"): + tagSuffix = "mips" + case strings.HasPrefix(platform, "linux/ppc"): + tagSuffix = "ppc" + case platform == "linux/s390x": + tagSuffix = "s390x" + case strings.HasPrefix(platform, "linux"): + // Use an older version of libc to gain greater OS compatibility. + // Debian 7 uses glibc 2.13. + tagSuffix = "main-debian7" + } + + goVersion, err := GoVersion() + if err != nil { + return "", err + } + + return beatsCrossBuildImage + ":" + goVersion + "-" + tagSuffix, nil +} + +// GolangCrossBuilder executes the specified mage target inside of the +// associated golang-crossbuild container image for the platform. +type GolangCrossBuilder struct { + Platform string + Target string +} + +// Build executes the build inside of Docker. +func (b GolangCrossBuilder) Build() error { + fmt.Printf(">> %v: Building for %v\n", b.Target, b.Platform) + + repoInfo, err := GetProjectRepoInfo() + if err != nil { + return errors.Wrap(err, "failed to determine repo root and package sub dir") + } + + mountPoint := filepath.ToSlash(filepath.Join("/go", "src", repoInfo.RootImportPath)) + workDir := mountPoint + if repoInfo.SubDir != "" { + workDir = filepath.ToSlash(filepath.Join(workDir, repoInfo.SubDir)) + } + + dockerRun := sh.RunCmd("docker", "run") + image, err := crossBuildImage(b.Platform) + if err != nil { + return errors.Wrap(err, "failed to determine golang-crossbuild image tag") + } + verbose := "" + if mg.Verbose() { + verbose = "true" + } + var args []string + if runtime.GOOS != "windows" { + args = append(args, + "--env", "EXEC_UID="+strconv.Itoa(os.Getuid()), + "--env", "EXEC_GID="+strconv.Itoa(os.Getgid()), + ) + } + args = append(args, + "--rm", + "--env", "MAGEFILE_VERBOSE="+verbose, + "--env", "MAGEFILE_TIMEOUT="+EnvOr("MAGEFILE_TIMEOUT", ""), + "-v", repoInfo.RootDir+":"+mountPoint, + "-w", workDir, + image, + "--build-cmd", "build/mage-linux-amd64 "+b.Target, + "-p", b.Platform, + ) + + return dockerRun(args...) +} + +// DockerChown chowns files generated during build. EXEC_UID and EXEC_GID must +// be set in the containers environment otherwise this is a noop. +func DockerChown(file string) { + // Chown files generated during build that are root owned. + uid, _ := strconv.Atoi(EnvOr("EXEC_UID", "-1")) + gid, _ := strconv.Atoi(EnvOr("EXEC_GID", "-1")) + if uid > 0 && gid > 0 { + if err := chownPaths(uid, gid, file); err != nil { + log.Println(err) + } + } +} + +// chownPaths will chown the file and all of the dirs specified in the path. +func chownPaths(uid, gid int, file string) error { + pathParts := strings.Split(file, string(filepath.Separator)) + for i := range pathParts { + chownDir := filepath.Join(pathParts[:i+1]...) + if err := os.Chown(chownDir, uid, gid); err != nil { + return errors.Wrapf(err, "failed to chown path=%v", chownDir) + } + } + return nil +} diff --git a/dev-tools/mage/files/linux/systemd-daemon-reload.sh b/dev-tools/mage/files/linux/systemd-daemon-reload.sh new file mode 100644 index 000000000000..65589fb789c2 --- /dev/null +++ b/dev-tools/mage/files/linux/systemd-daemon-reload.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash + +systemctl daemon-reload 2> /dev/null +exit 0 diff --git a/dev-tools/mage/files/packages.yml b/dev-tools/mage/files/packages.yml new file mode 100644 index 000000000000..d57c3c982708 --- /dev/null +++ b/dev-tools/mage/files/packages.yml @@ -0,0 +1,222 @@ +--- + +# This file contains the package specifications for both Community Beats and +# Official Beats. The shared section contains YAML anchors that are used to +# define common parts of the package in order to not repeat ourselves. + +shared: +- &common + name: '{{.BeatName}}' + service_name: '{{.BeatServiceName}}' + os: '{{.GOOS}}' + arch: '{{.PackageArch}}' + vendor: '{{.BeatVendor}}' + version: '{{ beat_version }}' + license: '{{.BeatLicense}}' + url: '{{.BeatURL}}' + description: '{{.BeatDescription}}' + +- &deb_rpm_spec + <<: *common + post_install_script: '{{ elastic_beats_dir }}/dev-tools/mage/files/linux/systemd-daemon-reload.sh' + files: + /usr/share/{{.BeatName}}/bin/{{.BeatName}}{{.BinaryExt}}: + source: build/golang-crossbuild/{{.BeatName}}-{{.GOOS}}-{{.Platform.Arch}}{{.BinaryExt}} + mode: 0755 + /etc/{{.BeatName}}/fields.yml: + source: fields.yml + mode: 0644 + /usr/share/{{.BeatName}}/LICENSE.txt: + source: '{{ repo.RootDir }}/LICENSE.txt' + mode: 0644 + /usr/share/{{.BeatName}}/NOTICE.txt: + source: '{{ repo.RootDir }}/NOTICE.txt' + mode: 0644 + /usr/share/{{.BeatName}}/README.md: + template: '{{ elastic_beats_dir }}/dev-tools/mage/templates/common/README.md.tmpl' + mode: 0644 + /usr/share/{{.BeatName}}/.build_hash.txt: + content: > + {{ commit }} + mode: 0644 + /etc/{{.BeatName}}/{{.BeatName}}.reference.yml: + source: '{{.BeatName}}.reference.yml' + mode: 0644 + /etc/{{.BeatName}}/{{.BeatName}}.yml: + source: '{{.BeatName}}.yml' + mode: 0600 + config: true + /usr/share/{{.BeatName}}/kibana: + source: _meta/kibana.generated + mode: 0644 + /usr/share/{{.BeatName}}/bin/{{.BeatName}}-god: + source: build/golang-crossbuild/god-{{.GOOS}}-{{.Platform.Arch}} + mode: 0755 + /usr/bin/{{.BeatName}}: + template: '{{ elastic_beats_dir }}/dev-tools/mage/templates/linux/beatname.sh.tmpl' + mode: 0755 + /lib/systemd/system/{{.BeatServiceName}}.service: + template: '{{ elastic_beats_dir }}/dev-tools/mage/templates/linux/systemd.unit.tmpl' + mode: 0755 + /etc/init.d/{{.BeatServiceName}}: + template: '{{ elastic_beats_dir }}/dev-tools/mage/templates/{{.PackageType}}/init.sh.tmpl' + mode: 0755 + +- &binary_files + '{{.BeatName}}{{.BinaryExt}}': + source: build/golang-crossbuild/{{.BeatName}}-{{.GOOS}}-{{.Platform.Arch}}{{.BinaryExt}} + mode: 0755 + fields.yml: + source: fields.yml + mode: 0644 + LICENSE.txt: + source: '{{ repo.RootDir }}/LICENSE.txt' + mode: 0644 + NOTICE.txt: + source: '{{ repo.RootDir }}/NOTICE.txt' + mode: 0644 + README.md: + template: '{{ elastic_beats_dir }}/dev-tools/mage/templates/common/README.md.tmpl' + mode: 0644 + .build_hash.txt: + content: > + {{ commit }} + mode: 0644 + '{{.BeatName}}.reference.yml': + source: '{{.BeatName}}.reference.yml' + mode: 0644 + '{{.BeatName}}.yml': + source: '{{.BeatName}}.yml' + mode: 0600 + config: true + kibana: + source: _meta/kibana.generated + mode: 0644 + +- &binary_spec + <<: *common + files: + <<: *binary_files + +- &windows_binary_spec + <<: *common + files: + <<: *binary_files + install-service-{{.BeatName}}.ps1: + template: '{{ elastic_beats_dir }}/dev-tools/mage/templates/windows/install-service.ps1.tmpl' + mode: 0755 + uninstall-service-{{.BeatName}}.ps1: + template: '{{ elastic_beats_dir }}/dev-tools/mage/templates/windows/uninstall-service.ps1.tmpl' + mode: 0755 + +- &elastic_license_for_binaries + license: "Elastic License" + files: + LICENSE.txt: + source: '{{ repo.RootDir }}/licenses/ELASTIC-LICENSE.txt' + mode: 0644 + +- &elastic_license_for_deb_rpm + license: "Elastic License" + files: + /usr/share/{{.BeatName}}/LICENSE.txt: + source: '{{ repo.RootDir }}/licenses/ELASTIC-LICENSE.txt' + mode: 0644 + +- &apache_license_for_binaries + license: "ASL 2.0" + files: + LICENSE.txt: + source: '{{ repo.RootDir }}/licenses/APACHE-LICENSE-2.0.txt' + mode: 0644 + +- &apache_license_for_deb_rpm + license: "Elastic License" + files: + /usr/share/{{.BeatName}}/LICENSE.txt: + source: '{{ repo.RootDir }}/licenses/APACHE-LICENSE-2.0.txt' + mode: 0644 + +# specs is a list of named packaging "flavors". +specs: + # Community Beats + community_beat: + - os: windows + types: [zip] + spec: + <<: *windows_binary_spec + + - os: darwin + types: [tgz] + spec: + <<: *binary_spec + + - os: linux + types: [tgz] + spec: + <<: *binary_spec + + - os: linux + types: [deb, rpm] + spec: + <<: *deb_rpm_spec + + # Official Beats + elastic_beat: + ### + # OSS Packages + ### + - os: windows + types: [zip] + spec: + <<: *windows_binary_spec + <<: *apache_license_for_binaries + name: '{{.BeatName}}-oss' + + - os: darwin + types: [tgz] + spec: + <<: *binary_spec + <<: *apache_license_for_binaries + name: '{{.BeatName}}-oss' + + - os: linux + types: [tgz] + spec: + <<: *binary_spec + <<: *apache_license_for_binaries + name: '{{.BeatName}}-oss' + + - os: linux + types: [deb, rpm] + spec: + <<: *deb_rpm_spec + <<: *apache_license_for_deb_rpm + name: '{{.BeatName}}-oss' + + ### + # Elastic Licensed Packages + ### + - os: windows + types: [zip] + spec: + <<: *windows_binary_spec + <<: *elastic_license_for_binaries + + - os: darwin + types: [tgz] + spec: + <<: *binary_spec + <<: *elastic_license_for_binaries + + - os: linux + types: [tgz] + spec: + <<: *binary_spec + <<: *elastic_license_for_binaries + + - os: linux + types: [deb, rpm] + spec: + <<: *deb_rpm_spec + <<: *elastic_license_for_deb_rpm diff --git a/dev-tools/mage/godaemon.go b/dev-tools/mage/godaemon.go new file mode 100644 index 000000000000..0f4cb2c0ec1d --- /dev/null +++ b/dev-tools/mage/godaemon.go @@ -0,0 +1,72 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package mage + +import ( + "errors" + "log" + "os" +) + +// BuildGoDaemon builds the go-deamon binary. +func BuildGoDaemon() error { + if GOOS != "linux" { + return errors.New("go-daemon only builds for linux") + } + + if os.Getenv("GOLANG_CROSSBUILD") != "1" { + return errors.New("Use the crossBuildGoDaemon target. buildGoDaemon can " + + "only be executed within the golang-crossbuild docker environment.") + } + + // Test if binaries are up-to-date. + output := MustExpand("build/golang-crossbuild/god-{{.Platform.GOOS}}-{{.Platform.Arch}}") + input := MustExpand("{{ elastic_beats_dir }}/dev-tools/vendor/github.com/tsg/go-daemon/god.c") + if IsUpToDate(output, input) { + log.Println(">>> buildGoDaemon is up-to-date for", Platform.Name) + return nil + } + + // Determine what compiler to use based on CC that is set by golang-crossbuild. + cc := os.Getenv("CC") + if cc == "" { + cc = "cc" + } + + compileCmd := []string{ + cc, + input, + "-o", createDir(output), + "-lpthread", "-static", + } + switch Platform.Name { + case "linux/amd64": + compileCmd = append(compileCmd, "-m64") + case "linux/386": + compileCmd = append(compileCmd, "-m32") + } + + defer DockerChown(output) + return RunCmds(compileCmd) +} + +// CrossBuildGoDaemon cross-build the go-daemon binary using the +// golang-crossbuild environment. +func CrossBuildGoDaemon() error { + return CrossBuild(ForPlatforms("linux"), WithTarget("buildGoDaemon")) +} diff --git a/dev-tools/mage/pkg.go b/dev-tools/mage/pkg.go new file mode 100644 index 000000000000..688e5c22118b --- /dev/null +++ b/dev-tools/mage/pkg.go @@ -0,0 +1,108 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package mage + +import ( + "fmt" + "log" + + "github.com/magefile/mage/mg" + "github.com/magefile/mage/sh" + "github.com/pkg/errors" +) + +// Package packages the Beat for distribution. It generates packages based on +// the set of target plaforms and registered packaging specifications. +func Package() error { + if len(Platforms) == 0 { + return errors.New("PLATFORMS environment must be set to a list of " + + "GOOS/Arch values, but Platforms is empty") + } + + if len(Packages) == 0 { + return errors.New("no package specs are registered. Call " + + "UseCommunityBeatPackaging or UseElasticBeatPackaging first.") + } + + var tasks []interface{} + for _, target := range Platforms { + for _, pkg := range Packages { + if pkg.OS != target.GOOS() { + continue + } + + for _, pkgType := range pkg.Types { + packageArch, err := getOSArchName(target, pkgType) + if err != nil { + log.Printf("Skipping arch %v for package type %v: %v", target.Arch(), pkgType, err) + continue + } + + spec := pkg.Spec.Clone() + spec.OS = target.GOOS() + spec.Arch = packageArch + spec.Snapshot = Snapshot + spec.evalContext = map[string]interface{}{ + "GOOS": target.GOOS(), + "GOARCH": target.GOARCH(), + "GOARM": target.GOARM(), + "Platform": target, + "PackageType": pkgType.String(), + "BinaryExt": binaryExtension(target.GOOS()), + } + spec.packageDir = packageStagingDir + "/" + pkgType.AddFileExtension(spec.Name+"-"+target.GOOS()+"-"+target.Arch()) + spec = spec.Evaluate() + + tasks = append(tasks, packageBuilder{target, spec, pkgType}.Build) + } + } + } + + Parallel(tasks...) + return nil +} + +type packageBuilder struct { + Platform BuildPlatform + Spec PackageSpec + Type PackageType +} + +func (b packageBuilder) Build() error { + fmt.Printf(">> package: Building %v type=%v for platform=%v\n", b.Spec.Name, b.Type, b.Platform.Name) + log.Printf("Package spec: %+v", b.Spec) + return errors.Wrapf(b.Type.Build(b.Spec), "failed building %v type=%v for platform=%v", + b.Spec.Name, b.Type, b.Platform.Name) +} + +// TestPackages executes the package tests on the produced binaries. These tests +// inspect things like file ownership and mode. +func TestPackages() error { + fmt.Println(">> Testing package contents") + var args []string + if mg.Verbose() { + args = append(args, "-v") + } + args = append(args, + MustExpand("{{ elastic_beats_dir }}/dev-tools/package_test.go"), + "-files", + MustExpand("{{.PWD}}/build/distributions/*"), + ) + goTest := sh.RunCmd("go", "test") + return goTest(args...) +} diff --git a/dev-tools/mage/pkg_test.go b/dev-tools/mage/pkg_test.go new file mode 100644 index 000000000000..f9bd3a708eda --- /dev/null +++ b/dev-tools/mage/pkg_test.go @@ -0,0 +1,120 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package mage + +import ( + "path/filepath" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "gopkg.in/yaml.v2" +) + +func testPackageSpec() PackageSpec { + return PackageSpec{ + Name: "brewbeat", + Version: "7.0.0", + Snapshot: true, + OS: "windows", + Arch: "x86_64", + Files: map[string]PackageFile{ + "brewbeat.yml": PackageFile{ + Source: "./testdata/config.yml", + Mode: 0644, + }, + "README.txt": PackageFile{ + Content: "Hello! {{.Version}}\n", + Mode: 0644, + }, + }, + } +} + +func TestPackageZip(t *testing.T) { + testPackage(t, PackageZip) +} + +func TestPackageTarGz(t *testing.T) { + testPackage(t, PackageTarGz) +} + +func TestPackageRPM(t *testing.T) { + if err := HaveDocker(); err != nil { + t.Skip("docker is required") + } + + testPackage(t, PackageRPM) +} + +func TestPackageDeb(t *testing.T) { + if err := HaveDocker(); err != nil { + t.Skip("docker is required") + } + + testPackage(t, PackageDeb) +} + +func testPackage(t testing.TB, pack func(PackageSpec) error) { + spec := testPackageSpec().Evaluate() + + readme := spec.Files["README.txt"] + readmePath := filepath.ToSlash(filepath.Clean(readme.Source)) + assert.True(t, strings.HasPrefix(readmePath, packageStagingDir)) + + if err := pack(spec); err != nil { + t.Fatal(err) + } +} + +func TestRepoRoot(t *testing.T) { + repo, err := GetProjectRepoInfo() + if err != nil { + t.Error(err) + } + + assert.Equal(t, "github.com/elastic/beats", repo.RootImportPath) + assert.True(t, filepath.IsAbs(repo.RootDir)) + cwd := filepath.Join(repo.RootDir, repo.SubDir) + assert.Equal(t, CWD(), cwd) +} + +func TestDumpVariables(t *testing.T) { + out, err := dumpVariables() + if err != nil { + t.Fatal(err) + } + t.Log(out) +} + +func TestLoadSpecs(t *testing.T) { + pkgs, err := LoadSpecs("files/packages.yml") + if err != nil { + t.Fatal(err) + } + + for flavor, s := range pkgs { + out, err := yaml.Marshal(s) + if err != nil { + t.Fatal(err) + } + if testing.Verbose() { + t.Log("Packaging flavor:", flavor, "\n", string(out)) + } + } +} diff --git a/dev-tools/mage/pkgspecs.go b/dev-tools/mage/pkgspecs.go new file mode 100644 index 000000000000..ee6c38a025a9 --- /dev/null +++ b/dev-tools/mage/pkgspecs.go @@ -0,0 +1,100 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package mage + +import ( + "io/ioutil" + "log" + "path/filepath" + + "github.com/pkg/errors" + "gopkg.in/yaml.v2" +) + +const packageSpecFile = "dev-tools/mage/files/packages.yml" + +// Packages defines the set of packages to be built when the package target is +// executed. +var Packages []OSPackageArgs + +// UseCommunityBeatPackaging configures the package target to build packages for +// a community Beat. +func UseCommunityBeatPackaging() { + beatsDir, err := ElasticBeatsDir() + if err != nil { + panic(err) + } + + err = LoadNamedSpec("community_beat", filepath.Join(beatsDir, packageSpecFile)) + if err != nil { + panic(err) + } +} + +// UseElasticBeatPackaging configures the package target to build packages for +// an Elastic Beat. This means it will generate two sets of packages -- one +// that is purely OSS under Apache 2.0 and one that is licensed under the +// Elastic License and may contain additional X-Pack features. +func UseElasticBeatPackaging() { + beatsDir, err := ElasticBeatsDir() + if err != nil { + panic(err) + } + + err = LoadNamedSpec("elastic_beat", filepath.Join(beatsDir, packageSpecFile)) + if err != nil { + panic(err) + } +} + +// LoadNamedSpec loads a packaging specification with the given name from the +// specified YAML file. name should be a sub-key of 'specs'. +func LoadNamedSpec(name, file string) error { + specs, err := LoadSpecs(file) + if err != nil { + return errors.Wrap(err, "failed to load spec file") + } + + packages, found := specs[name] + if !found { + return errors.Errorf("%v not found in package specs", name) + } + + log.Printf("%v package spec loaded from %v", name, file) + Packages = packages + return nil +} + +// LoadSpecs loads the packaging specifications from the specified YAML file. +func LoadSpecs(file string) (map[string][]OSPackageArgs, error) { + data, err := ioutil.ReadFile(file) + if err != nil { + return nil, errors.Wrap(err, "failed to read from spec file") + } + + type PackageYAML struct { + Specs map[string][]OSPackageArgs `yaml:"specs"` + } + + var packages PackageYAML + if err = yaml.Unmarshal(data, &packages); err != nil { + return nil, errors.Wrap(err, "failed to unmarshal spec data") + } + + return packages.Specs, nil +} diff --git a/dev-tools/mage/pkgtypes.go b/dev-tools/mage/pkgtypes.go new file mode 100644 index 000000000000..4cf153315348 --- /dev/null +++ b/dev-tools/mage/pkgtypes.go @@ -0,0 +1,764 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package mage + +import ( + "archive/tar" + "archive/zip" + "bytes" + "compress/gzip" + "fmt" + "io" + "io/ioutil" + "log" + "os" + "path/filepath" + "reflect" + "runtime" + "strconv" + "strings" + + "github.com/magefile/mage/mg" + "github.com/magefile/mage/sh" + "github.com/mitchellh/hashstructure" + "github.com/pkg/errors" +) + +const ( + // distributionsDir is the dir where packages are written. + distributionsDir = "build/distributions" + + // packageStagingDir is the staging directory for any temporary files that + // need to be written to disk for inclusion in a package. + packageStagingDir = "build/package" + + // defaultBinaryName specifies the output file for zip and tar.gz. + defaultBinaryName = "{{.Name}}-{{.Version}}{{if .Snapshot}}-SNAPSHOT{{end}}{{if .OS}}-{{.OS}}{{end}}{{if .Arch}}-{{.Arch}}{{end}}" +) + +// PackageType defines the file format of the package (e.g. zip, rpm, etc). +type PackageType int + +// List of possible package types. +const ( + RPM PackageType = iota + 1 + Deb + Zip + TarGz +) + +// OSPackageArgs define a set of package types to build for an operating +// system using the contained PackageSpec. +type OSPackageArgs struct { + OS string `yaml:"os"` + Types []PackageType `yaml:"types"` + Spec PackageSpec `yaml:"spec"` +} + +// PackageSpec specifies package metadata and the contents of the package. +type PackageSpec struct { + Name string `yaml:"name,omitempty"` + ServiceName string `yaml:"service_name,omitempty"` + OS string `yaml:"os,omitempty"` + Arch string `yaml:"arch,omitempty"` + Vendor string `yaml:"vendor,omitempty"` + Snapshot bool `yaml:"snapshot"` + Version string `yaml:"version,omitempty"` + License string `yaml:"license,omitempty"` + URL string `yaml:"url,omitempty"` + Description string `yaml:"description,omitempty"` + PostInstallScript string `yaml:"post_install_script,omitempty"` + Files map[string]PackageFile `yaml:"files"` + OutputFile string `yaml:"output_file,omitempty"` // Optional + + evalContext map[string]interface{} + packageDir string + localPostInstallScript string +} + +// PackageFile represents a file or directory within a package. +type PackageFile struct { + Source string `yaml:"source,omitempty"` // Regular source file or directory. + Content string `yaml:"content,omitempty"` // Inline template string. + Template string `yaml:"template,omitempty"` // Input template file. + Target string `yaml:"target,omitempty"` // Target location in package. Relative paths are added to a package specific directory (e.g. metricbeat-7.0.0-linux-x86_64). + Mode os.FileMode `yaml:"mode,omitempty"` // Target mode for file. Does not apply when source is a directory. + Config bool `yaml:"config"` // Mark file as config in the package (deb and rpm only). + Dep func(PackageSpec) error `yaml:"-" hash:"-" json:"-"` // Dependency to invoke during Evaluate. +} + +// OSArchNames defines the names of architectures for use in packages. +var OSArchNames = map[string]map[PackageType]map[string]string{ + "windows": map[PackageType]map[string]string{ + Zip: map[string]string{ + "386": "x86", + "amd64": "x86_64", + }, + }, + "darwin": map[PackageType]map[string]string{ + TarGz: map[string]string{ + "386": "x86", + "amd64": "x86_64", + }, + }, + "linux": map[PackageType]map[string]string{ + RPM: map[string]string{ + "386": "i686", + "amd64": "x86_64", + "armv7": "armhfp", + "arm64": "aarch64", + "mipsle": "mipsel", + "mips64le": "mips64el", + "ppc64": "ppc64", + "ppc64le": "ppc64le", + "s390x": "s390x", + }, + // https://www.debian.org/ports/ + Deb: map[string]string{ + "386": "i386", + "amd64": "amd64", + "armv5": "armel", + "armv6": "armel", + "armv7": "armhf", + "arm64": "arm64", + "mips": "mips", + "mipsle": "mipsel", + "mips64le": "mips64el", + "ppc64le": "ppc64el", + "s390x": "s390x", + }, + TarGz: map[string]string{ + "386": "x86", + "amd64": "x86_64", + "armv5": "armv5", + "armv6": "armv6", + "armv7": "armv7", + "arm64": "arm64", + "mips": "mips", + "mipsle": "mipsel", + "mips64": "mips64", + "mips64le": "mips64el", + "ppc64": "ppc64", + "ppc64le": "ppc64le", + "s390x": "s390x", + }, + }, +} + +// getOSArchName returns the architecture name to use in a package. +func getOSArchName(platform BuildPlatform, t PackageType) (string, error) { + names, found := OSArchNames[platform.GOOS()] + if !found { + return "", errors.Errorf("arch names for os=%v are not defined", + platform.GOOS()) + } + + archMap, found := names[t] + if !found { + return "", errors.Errorf("arch names for %v on os=%v are not defined", + t, platform.GOOS()) + } + + arch, found := archMap[platform.Arch()] + if !found { + return "", errors.Errorf("arch name associated with %v for %v on "+ + "os=%v is not defined", platform.Arch(), t, platform.GOOS()) + } + + return arch, nil +} + +// String returns the name of the package type. +func (typ PackageType) String() string { + switch typ { + case RPM: + return "rpm" + case Deb: + return "deb" + case Zip: + return "zip" + case TarGz: + return "tar.gz" + default: + return "invalid" + } +} + +// MarshalText returns the text representation of PackageType. +func (typ PackageType) MarshalText() ([]byte, error) { + return []byte(typ.String()), nil +} + +// UnmarshalText returns a PackageType based on the given text. +func (typ *PackageType) UnmarshalText(text []byte) error { + switch strings.ToLower(string(text)) { + case "rpm": + *typ = RPM + case "deb": + *typ = Deb + case "tar.gz", "tgz", "targz": + *typ = TarGz + case "zip": + *typ = Zip + default: + return errors.Errorf("unknown package type: %v", string(text)) + } + return nil +} + +// AddFileExtension returns a filename with the file extension added. If the +// filename already has the extension then it becomes a pass-through. +func (typ PackageType) AddFileExtension(file string) string { + ext := "." + strings.ToLower(typ.String()) + if !strings.HasSuffix(file, ext) { + return file + ext + } + return file +} + +// Build builds a package based on the provided spec. +func (typ PackageType) Build(spec PackageSpec) error { + switch typ { + case RPM: + return PackageRPM(spec) + case Deb: + return PackageDeb(spec) + case Zip: + return PackageZip(spec) + case TarGz: + return PackageTarGz(spec) + default: + return errors.Errorf("unknown package type: %v", typ) + } +} + +// Clone returns a deep clone of the spec. +func (s PackageSpec) Clone() PackageSpec { + clone := s + clone.Files = make(map[string]PackageFile, len(s.Files)) + for k, v := range s.Files { + clone.Files[k] = v + } + return clone +} + +// ReplaceFile replaces an existing file defined in the spec. The target must +// exist other it will panic. +func (s PackageSpec) ReplaceFile(target string, file PackageFile) { + _, found := s.Files[target] + if !found { + panic(errors.Errorf("failed to ReplaceFile because target=%v does not exist", target)) + } + + s.Files[target] = file +} + +// Expand expands a templated string using data from the spec. +func (s PackageSpec) Expand(in string, args ...map[string]interface{}) (string, error) { + return expandTemplate("inline", in, FuncMap, + EnvMap(append([]map[string]interface{}{s.evalContext, s.toMap()}, args...)...)) +} + +// MustExpand expands a templated string using data from the spec. It panics if +// an error occurs. +func (s PackageSpec) MustExpand(in string, args ...map[string]interface{}) string { + v, err := s.Expand(in, args...) + if err != nil { + panic(err) + } + return v +} + +// ExpandFile expands a template file using data from the spec. +func (s PackageSpec) ExpandFile(src, dst string, args ...map[string]interface{}) error { + return expandFile(src, dst, + EnvMap(append([]map[string]interface{}{s.evalContext, s.toMap()}, args...)...)) +} + +// MustExpandFile expands a template file using data from the spec. It panics if +// an error occurs. +func (s PackageSpec) MustExpandFile(src, dst string, args ...map[string]interface{}) error { + return s.ExpandFile(src, dst, args...) +} + +// Evaluate expands all variables used in the spec definition and writes any +// templated files used in the spec to disk. It panics if there is an error. +func (s PackageSpec) Evaluate(args ...map[string]interface{}) PackageSpec { + args = append([]map[string]interface{}{s.toMap(), s.evalContext}, args...) + mustExpand := func(in string) string { + if in == "" { + return "" + } + return MustExpand(in, args...) + } + + s.Name = mustExpand(s.Name) + s.ServiceName = mustExpand(s.ServiceName) + s.OS = mustExpand(s.OS) + s.Arch = mustExpand(s.Arch) + s.Vendor = mustExpand(s.Vendor) + s.Version = mustExpand(s.Version) + s.License = mustExpand(s.License) + s.URL = mustExpand(s.URL) + s.Description = mustExpand(s.Description) + s.PostInstallScript = mustExpand(s.PostInstallScript) + s.OutputFile = mustExpand(s.OutputFile) + + if s.ServiceName == "" { + s.ServiceName = s.Name + } + + if s.packageDir == "" { + outputFileName := filepath.Base(s.OutputFile) + + if outputFileName != "." { + s.packageDir = filepath.Join(packageStagingDir, outputFileName) + } else { + s.packageDir = filepath.Join(packageStagingDir, strings.Join([]string{s.Name, s.OS, s.Arch, s.hash()}, "-")) + } + } else { + s.packageDir = filepath.Clean(mustExpand(s.packageDir)) + } + if s.evalContext == nil { + s.evalContext = map[string]interface{}{} + } + s.evalContext["PackageDir"] = s.packageDir + + evaluatedFiles := make(map[string]PackageFile, len(s.Files)) + for target, f := range s.Files { + // Execute the dependency if it exists. + if f.Dep != nil { + if err := f.Dep(s); err != nil { + panic(errors.Wrapf(err, "failed executing package file dependency for target=%v", target)) + } + } + + f.Source = s.MustExpand(f.Source) + f.Template = s.MustExpand(f.Template) + f.Target = s.MustExpand(target) + target = f.Target + + // Expand templates. + switch { + case f.Source != "": + case f.Content != "": + content, err := s.Expand(f.Content) + if err != nil { + panic(errors.Wrapf(err, "failed to expand content template for target=%v", target)) + } + + f.Source = filepath.Join(s.packageDir, filepath.Base(f.Target)) + if err = ioutil.WriteFile(createDir(f.Source), []byte(content), 0644); err != nil { + panic(errors.Wrapf(err, "failed to write file containing content for target=%v", target)) + } + case f.Template != "": + f.Source = filepath.Join(s.packageDir, filepath.Base(f.Template)) + if err := s.ExpandFile(createDir(f.Template), f.Source); err != nil { + panic(errors.Wrapf(err, "failed to expand template file for target=%v", target)) + } + default: + panic(errors.Errorf("package file with target=%v must have either source, content, or template", target)) + } + + evaluatedFiles[f.Target] = f + } + // Replace the map instead of modifying the source. + s.Files = evaluatedFiles + + if s.PostInstallScript != "" { + // Copy the inside the build dir so that it's available inside of Docker for FPM. + s.localPostInstallScript = filepath.Join(s.packageDir, filepath.Base(s.PostInstallScript)) + if err := Copy(s.PostInstallScript, s.localPostInstallScript); err != nil { + panic(errors.Wrap(err, "failed to copy post install script to build dir")) + } + } + + return s +} + +func (s PackageSpec) hash() string { + h, err := hashstructure.Hash(s, nil) + if err != nil { + panic(errors.Wrap(err, "failed to compute hash of spec")) + } + + hash := strconv.FormatUint(h, 10) + if len(hash) > 10 { + hash = hash[0:10] + } + return hash +} + +// toMap returns a map containing the exported field names and their values. +func (s PackageSpec) toMap() map[string]interface{} { + out := make(map[string]interface{}) + v := reflect.ValueOf(s) + typ := v.Type() + + for i := 0; i < v.NumField(); i++ { + structField := typ.Field(i) + if !structField.Anonymous && structField.PkgPath == "" { + out[structField.Name] = v.Field(i).Interface() + } + } + + return out +} + +// rootDir returns the name of the root directory contained inside of zip and +// tar.gz packages. +func (s PackageSpec) rootDir() string { + if s.OutputFile != "" { + return filepath.Base(s.OutputFile) + } + + // NOTE: This uses .BeatName instead of .Name because we wanted the internal + // directory to not include "-oss". + return s.MustExpand("{{.BeatName}}-{{.Version}}{{if .Snapshot}}-SNAPSHOT{{end}}{{if .OS}}-{{.OS}}{{end}}{{if .Arch}}-{{.Arch}}{{end}}") +} + +// PackageZip packages a zip file. +func PackageZip(spec PackageSpec) error { + // Create a buffer to write our archive to. + buf := new(bytes.Buffer) + + // Create a new zip archive. + w := zip.NewWriter(buf) + baseDir := spec.rootDir() + + // Add files to zip. + for _, pkgFile := range spec.Files { + if err := addFileToZip(w, baseDir, pkgFile); err != nil { + return errors.Wrapf(err, "failed adding file=%+v to zip", pkgFile) + } + } + + if err := w.Close(); err != nil { + return err + } + + // Output the zip file. + if spec.OutputFile == "" { + outputZip, err := spec.Expand(defaultBinaryName + ".zip") + if err != nil { + return err + } + spec.OutputFile = filepath.Join(distributionsDir, outputZip) + } + spec.OutputFile = Zip.AddFileExtension(spec.OutputFile) + + // Write the zip file. + if err := ioutil.WriteFile(createDir(spec.OutputFile), buf.Bytes(), 0644); err != nil { + return errors.Wrap(err, "failed to write zip file") + } + + // Any packages beginning with "tmp-" are temporary by nature so don't have + // them a .sha512 file. + if strings.HasPrefix(filepath.Base(spec.OutputFile), "tmp-") { + return nil + } + + return errors.Wrap(CreateSHA512File(spec.OutputFile), "failed to create .sha512 file") +} + +// PackageTarGz packages a gzipped tar file. +func PackageTarGz(spec PackageSpec) error { + // Create a buffer to write our archive to. + buf := new(bytes.Buffer) + + // Create a new tar archive. + w := tar.NewWriter(buf) + baseDir := spec.rootDir() + + // Add files to tar. + for _, pkgFile := range spec.Files { + if err := addFileToTar(w, baseDir, pkgFile); err != nil { + return errors.Wrapf(err, "failed adding file=%+v to tar", pkgFile) + } + } + + if err := w.Close(); err != nil { + return err + } + + // Output tar.gz to disk. + if spec.OutputFile == "" { + outputTarGz, err := spec.Expand(defaultBinaryName + ".tar.gz") + if err != nil { + return err + } + spec.OutputFile = filepath.Join(distributionsDir, outputTarGz) + } + spec.OutputFile = TarGz.AddFileExtension(spec.OutputFile) + + // Open the output file. + log.Println("Creating output file at", spec.OutputFile) + outFile, err := os.Create(createDir(spec.OutputFile)) + if err != nil { + return err + } + defer outFile.Close() + + // Gzip compress the data. + gzWriter := gzip.NewWriter(outFile) + if _, err = gzWriter.Write(buf.Bytes()); err != nil { + return err + } + + // Close and flush. + if err = gzWriter.Close(); err != nil { + return err + } + + // Any packages beginning with "tmp-" are temporary by nature so don't have + // them a .sha512 file. + if strings.HasPrefix(filepath.Base(spec.OutputFile), "tmp-") { + return nil + } + + return errors.Wrap(CreateSHA512File(spec.OutputFile), "failed to create .sha512 file") +} + +// PackageDeb packages a deb file. This requires Docker to execute FPM. +func PackageDeb(spec PackageSpec) error { + return runFPM(spec, Deb) +} + +// PackageRPM packages a RPM file. This requires Docker to execute FPM. +func PackageRPM(spec PackageSpec) error { + return runFPM(spec, RPM) +} + +func runFPM(spec PackageSpec, packageType PackageType) error { + var fpmPackageType string + switch packageType { + case RPM, Deb: + fpmPackageType = packageType.String() + default: + return errors.Errorf("unsupported package type=%v for runFPM", fpmPackageType) + } + + if err := HaveDocker(); err != nil { + return fmt.Errorf("packaging %v files requires docker: %v", fpmPackageType, err) + } + + // Build a tar file as the input to FPM. + inputTar := filepath.Join(distributionsDir, "tmp-"+fpmPackageType+"-"+spec.rootDir()+"-"+spec.hash()+".tar.gz") + spec.OutputFile = inputTar + if err := PackageTarGz(spec); err != nil { + return err + } + defer os.Remove(inputTar) + + outputFile, err := spec.Expand("{{.Name}}-{{.Version}}{{if .Snapshot}}-SNAPSHOT{{end}}-{{.Arch}}") + if err != nil { + return err + } + spec.OutputFile = packageType.AddFileExtension(filepath.Join(distributionsDir, outputFile)) + + dockerRun := sh.RunCmd("docker", "run") + var args []string + + args, err = addUidGidEnvArgs(args) + if err != nil { + return err + } + + args = append(args, + "--rm", + "-w", "/app", + "-v", CWD()+":/app", + beatsFPMImage+":"+fpmVersion, + "fpm", "--force", + "--input-type", "tar", + "--output-type", fpmPackageType, + "--name", spec.ServiceName, + "--architecture", spec.Arch, + ) + if spec.Version != "" { + args = append(args, "--version", spec.Version) + } + if spec.Vendor != "" { + args = append(args, "--vendor", spec.Vendor) + } + if spec.License != "" { + args = append(args, "--license", strings.Replace(spec.License, " ", "-", -1)) + } + if spec.Description != "" { + args = append(args, "--description", spec.Description) + } + if spec.URL != "" { + args = append(args, "--url", spec.URL) + } + if spec.localPostInstallScript != "" { + args = append(args, "--after-install", spec.localPostInstallScript) + } + args = append(args, + "-p", spec.OutputFile, + inputTar, + ) + + if err = dockerRun(args...); err != nil { + return errors.Wrap(err, "failed while running FPM in docker") + } + + return errors.Wrap(CreateSHA512File(spec.OutputFile), "failed to create .sha512 file") +} + +func addUidGidEnvArgs(args []string) ([]string, error) { + if runtime.GOOS == "windows" { + return args, nil + } + + info, err := GetDockerInfo() + if err != nil { + return args, errors.Wrap(err, "failed to get docker info") + } + + uid, gid := os.Getuid(), os.Getgid() + if info.IsBoot2Docker() { + // Boot2Docker mounts vboxfs using 1000:50. + uid, gid = 1000, 50 + log.Printf("Boot2Docker is in use. Deploying workaround. "+ + "Using UID=%d GID=%d", uid, gid) + } + + return append(args, + "--env", "EXEC_UID="+strconv.Itoa(uid), + "--env", "EXEC_GID="+strconv.Itoa(gid), + ), nil +} + +// addFileToZip adds a file (or directory) to a zip archive. +func addFileToZip(ar *zip.Writer, baseDir string, pkgFile PackageFile) error { + return filepath.Walk(pkgFile.Source, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + header, err := zip.FileInfoHeader(info) + if err != nil { + return err + } + + if info.Mode().IsRegular() && pkgFile.Mode > 0 { + header.SetMode(pkgFile.Mode & os.ModePerm) + } else if info.IsDir() { + header.SetMode(0755) + } + + if filepath.IsAbs(pkgFile.Target) { + baseDir = "" + } + + relPath, err := filepath.Rel(pkgFile.Source, path) + if err != nil { + return err + } + + header.Name = filepath.Join(baseDir, pkgFile.Target, relPath) + + if info.IsDir() { + header.Name += string(filepath.Separator) + } else { + header.Method = zip.Deflate + } + + if mg.Verbose() { + log.Println("Adding", header.Mode(), header.Name) + } + + w, err := ar.CreateHeader(header) + if err != nil { + return err + } + + if info.IsDir() { + return nil + } + + file, err := os.Open(path) + if err != nil { + return err + } + defer file.Close() + + if _, err = io.Copy(w, file); err != nil { + return err + } + return file.Close() + }) +} + +// addFileToTar adds a file (or directory) to a tar archive. +func addFileToTar(ar *tar.Writer, baseDir string, pkgFile PackageFile) error { + return filepath.Walk(pkgFile.Source, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + header, err := tar.FileInfoHeader(info, info.Name()) + if err != nil { + return err + } + header.Uname, header.Gname = "root", "root" + header.Uid, header.Gid = 0, 0 + + if info.Mode().IsRegular() && pkgFile.Mode > 0 { + header.Mode = int64(pkgFile.Mode & os.ModePerm) + } else if info.IsDir() { + header.Mode = int64(0755) + } + + if filepath.IsAbs(pkgFile.Target) { + baseDir = "" + } + + relPath, err := filepath.Rel(pkgFile.Source, path) + if err != nil { + return err + } + + header.Name = filepath.Join(baseDir, pkgFile.Target, relPath) + if info.IsDir() { + header.Name += string(filepath.Separator) + } + + if mg.Verbose() { + log.Println("Adding", os.FileMode(header.Mode), header.Name) + } + if err := ar.WriteHeader(header); err != nil { + return err + } + + if info.IsDir() { + return nil + } + + file, err := os.Open(path) + if err != nil { + return err + } + defer file.Close() + + if _, err = io.Copy(ar, file); err != nil { + return err + } + return file.Close() + }) +} diff --git a/dev-tools/mage/platforms.go b/dev-tools/mage/platforms.go new file mode 100644 index 000000000000..c6039c55abac --- /dev/null +++ b/dev-tools/mage/platforms.go @@ -0,0 +1,464 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package mage + +import ( + "sort" + "strings" + + "github.com/pkg/errors" +) + +// BuildPlatforms is a list of GOOS/GOARCH pairs supported by Go. +// The list originated from 'go tool dist list -json'. +var BuildPlatforms = BuildPlatformList{ + {"android/386", CGOSupported}, + {"android/amd64", CGOSupported}, + {"android/arm", CGOSupported}, + {"android/arm64", CGOSupported}, + {"darwin/386", CGOSupported | CrossBuildSupported}, + {"darwin/amd64", CGOSupported | CrossBuildSupported | Default}, + {"darwin/arm", CGOSupported}, + {"darwin/arm64", CGOSupported}, + {"dragonfly/amd64", CGOSupported}, + {"freebsd/386", CGOSupported}, + {"freebsd/amd64", CGOSupported}, + {"freebsd/arm", 0}, + {"linux/386", CGOSupported | CrossBuildSupported | Default}, + {"linux/amd64", CGOSupported | CrossBuildSupported | Default}, + {"linux/armv5", CGOSupported | CrossBuildSupported}, + {"linux/armv6", CGOSupported | CrossBuildSupported}, + {"linux/armv7", CGOSupported | CrossBuildSupported}, + {"linux/arm64", CGOSupported | CrossBuildSupported}, + {"linux/mips", CGOSupported | CrossBuildSupported}, + {"linux/mips64", CGOSupported | CrossBuildSupported}, + {"linux/mips64le", CGOSupported | CrossBuildSupported}, + {"linux/mipsle", CGOSupported | CrossBuildSupported}, + {"linux/ppc64", CrossBuildSupported}, + {"linux/ppc64le", CGOSupported | CrossBuildSupported}, + {"linux/s390x", CGOSupported | CrossBuildSupported}, + {"nacl/386", 0}, + {"nacl/amd64p32", 0}, + {"nacl/arm", 0}, + {"netbsd/386", CGOSupported}, + {"netbsd/amd64", CGOSupported}, + {"netbsd/arm", CGOSupported}, + {"openbsd/386", CGOSupported}, + {"openbsd/amd64", CGOSupported}, + {"openbsd/arm", 0}, + {"plan9/386", 0}, + {"plan9/amd64", 0}, + {"plan9/arm", 0}, + {"solaris/amd64", CGOSupported}, + {"windows/386", CGOSupported | CrossBuildSupported | Default}, + {"windows/amd64", CGOSupported | CrossBuildSupported | Default}, +} + +// PlatformFeature specifies features that are supported for a platform. +type PlatformFeature uint8 + +// List of PlatformFeature types. +const ( + CGOSupported PlatformFeature = 1 << iota // CGO is supported. + CrossBuildSupported // Cross-build supported by golang-crossbuild. + Default // Built by default on crossBuild and package. +) + +var platformFlagNames = map[PlatformFeature]string{ + CGOSupported: "cgo", + CrossBuildSupported: "xbuild", + Default: "default", +} + +// String returns a string representation of the platform features. +func (f PlatformFeature) String() string { + if f == 0 { + return "none" + } + + var names []string + for value, name := range platformFlagNames { + if f&value > 0 { + names = append(names, name) + } + } + + return strings.Join(names, "|") +} + +// CanCrossBuild returns true if cross-building is supported by +// golang-crossbuild. +func (f PlatformFeature) CanCrossBuild() bool { + return f&CrossBuildSupported > 0 +} + +// SupportsCGO returns true if CGO is supported. +func (f PlatformFeature) SupportsCGO() bool { + return f&CGOSupported > 0 +} + +// BuildPlatform represents a target platform for builds. +type BuildPlatform struct { + Name string + Flags PlatformFeature +} + +// GOOS returns the GOOS value contained in the name. +func (p BuildPlatform) GOOS() string { + idx := strings.IndexByte(p.Name, '/') + if idx == -1 { + return p.Name + } + return p.Name[:idx] +} + +// Arch returns the architecture value contained in the name. +func (p BuildPlatform) Arch() string { + idx := strings.IndexByte(p.Name, '/') + if idx == -1 { + return "" + } + return p.Name[strings.IndexByte(p.Name, '/')+1:] +} + +// GOARCH returns the GOARCH value associated with the architecture contained +// in the name. For ARM the Arch and GOARCH can differ because the GOARM value +// is encoded in the Arch value. +func (p BuildPlatform) GOARCH() string { + // Allow armv7 to be interpreted as GOARCH=arm GOARM=7. + arch := p.Arch() + if strings.HasPrefix(arch, "armv") { + return "arm" + } + return arch +} + +// GOARM returns the ARM version. +func (p BuildPlatform) GOARM() string { + arch := p.Arch() + if strings.HasPrefix(arch, "armv") { + return strings.TrimPrefix(arch, "armv") + } + return "" +} + +// Attributes returns a new PlatformAttributes. +func (p BuildPlatform) Attributes() PlatformAttributes { + return MakePlatformAttributes(p.GOOS(), p.GOARCH(), p.GOARM()) +} + +// PlatformAttributes contains all of the data that can be extracted from a +// BuildPlatform name. +type PlatformAttributes struct { + Name string + GOOS string + GOARCH string + GOARM string + Arch string +} + +// MakePlatformAttributes returns a new PlatformAttributes. +func MakePlatformAttributes(goos, goarch, goarm string) PlatformAttributes { + arch := goarch + if goarch == "arm" && goarm != "" { + arch += "v" + goarm + } + + name := goos + if arch != "" { + name += "/" + arch + } + + return PlatformAttributes{ + Name: name, + GOOS: goos, + GOARCH: goarch, + GOARM: goarm, + Arch: arch, + } +} + +// String returns the string representation of the platform which has the format +// of "GOOS/Arch". +func (p PlatformAttributes) String() string { + return p.Name +} + +// BuildPlatformList is a list of BuildPlatforms that supports filtering. +type BuildPlatformList []BuildPlatform + +// Get returns the BuildPlatform matching the given name. +func (list BuildPlatformList) Get(name string) (BuildPlatform, bool) { + for _, bp := range list { + if bp.Name == name { + return bp, true + } + } + return BuildPlatform{}, false +} + +// Defaults returns the default platforms contained in the list. +func (list BuildPlatformList) Defaults() BuildPlatformList { + return list.filter(func(p BuildPlatform) bool { + return p.Flags&Default > 0 + }) +} + +// CrossBuild returns the platforms that support cross-building. +func (list BuildPlatformList) CrossBuild() BuildPlatformList { + return list.filter(func(p BuildPlatform) bool { + return p.Flags&CrossBuildSupported > 0 + }) +} + +// filter returns the platforms that match the given predicate. +func (list BuildPlatformList) filter(pred func(p BuildPlatform) bool) BuildPlatformList { + var out BuildPlatformList + for _, item := range list { + if pred(item) { + out = append(out, item) + } + } + return out +} + +// Remove returns a copy of list without platforms matching name. +func (list BuildPlatformList) Remove(name string) BuildPlatformList { + attrs := BuildPlatform{Name: name}.Attributes() + + if attrs.Arch == "" { + // Filter by GOOS only. + return list.filter(func(bp BuildPlatform) bool { + return bp.GOOS() != attrs.GOOS + }) + } + + return list.filter(func(bp BuildPlatform) bool { + return !(bp.GOOS() == attrs.GOOS && bp.Arch() == attrs.Arch) + }) +} + +// Select returns a new list containing the platforms that match name. +func (list BuildPlatformList) Select(name string) BuildPlatformList { + attrs := BuildPlatform{Name: name}.Attributes() + + if attrs.Arch == "" { + // Filter by GOOS only. + return list.filter(func(bp BuildPlatform) bool { + return bp.GOOS() == attrs.GOOS + }) + } + + return list.filter(func(bp BuildPlatform) bool { + return bp.GOOS() == attrs.GOOS && bp.Arch() == attrs.Arch + }) +} + +type platformExpression struct { + Add []string + Select []string + SelectCrossBuild bool + Remove []string +} + +func newPlatformExpression(expr string) (*platformExpression, error) { + if strings.TrimSpace(expr) == "" { + return nil, nil + } + + pe := &platformExpression{} + + // Parse the expression. + words := strings.FieldsFunc(expr, isSeparator) + for _, w := range words { + if strings.HasPrefix(w, "+") { + pe.Add = append(pe.Add, strings.TrimPrefix(w, "+")) + } else if strings.HasPrefix(w, "!") { + pe.Remove = append(pe.Remove, strings.TrimPrefix(w, "!")) + } else if w == "xbuild" { + pe.SelectCrossBuild = true + } else { + pe.Select = append(pe.Select, w) + } + } + + // Validate the names used. + checks := make([]string, 0, len(pe.Add)+len(pe.Select)+len(pe.Remove)) + checks = append(checks, pe.Add...) + checks = append(checks, pe.Select...) + checks = append(checks, pe.Remove...) + + for _, name := range checks { + if name == "all" || name == "defaults" { + continue + } + + var valid bool + for _, bp := range BuildPlatforms { + if bp.Name == name || bp.GOOS() == name { + valid = true + break + } + } + + if !valid { + return nil, errors.Errorf("invalid platform in expression: %v", name) + } + } + + return pe, nil +} + +// NewPlatformList returns a new BuildPlatformList based on given expression. +// +// By default the initial set include only the platforms designated as defaults. +// To add additional platforms to list use an addition term that is designated +// with a plug sign (e.g. "+netbsd" or "+linux/armv7"). Or you may use "+all" +// to change the initial set to include all possible platforms then filter +// from there (e.g. "+all linux windows"). +// +// The expression can consists of selections (e.g. "linux") and/or +// removals (e.g."!windows"). Each term can be valid GOOS or a valid GOOS/Arch +// pair. +// +// "xbuild" is a special selection term used to select all platforms that are +// cross-build eligible. +// "defaults" is a special selection or removal term that contains all platforms +// designated as a default. +// "all" is a special addition term for adding all valid GOOS/Arch pairs to the +// set. +func NewPlatformList(expr string) BuildPlatformList { + pe, err := newPlatformExpression(expr) + if err != nil { + panic(err) + } + if pe == nil { + return BuildPlatforms.Defaults() + } + + var out BuildPlatformList + if len(pe.Add) == 0 || (len(pe.Select) == 0 && len(pe.Remove) == 0) { + // Bootstrap list with default platforms when the expression is + // exclusively adds OR exclusively selects and removes. + out = BuildPlatforms.Defaults() + } + + all := BuildPlatforms + for _, name := range pe.Add { + if name == "all" { + out = make(BuildPlatformList, len(all)) + copy(out, all) + break + } + out = append(out, all.Select(name)...) + } + + if len(pe.Select) > 0 { + var selected BuildPlatformList + for _, name := range pe.Select { + selected = append(selected, out.Select(name)...) + } + out = selected + } + + for _, name := range pe.Remove { + if name == "defaults" { + for _, defaultBP := range all.Defaults() { + out = out.Remove(defaultBP.Name) + } + continue + } + out = out.Remove(name) + } + + if pe.SelectCrossBuild { + out = out.CrossBuild() + } + return out.deduplicate() +} + +// Filter creates a new list based on the provided expression. +// +// The expression can consists of selections (e.g. "linux") and/or +// removals (e.g."!windows"). Each term can be valid GOOS or a valid GOOS/Arch +// pair. +// +// "xbuild" is a special selection term used to select all platforms that are +// cross-build eligible. +// "defaults" is a special selection or removal term that contains all platforms +// designated as a default. +func (list BuildPlatformList) Filter(expr string) BuildPlatformList { + pe, err := newPlatformExpression(expr) + if err != nil { + panic(err) + } + if pe == nil { + return list + } + if len(pe.Add) > 0 { + panic(errors.Errorf("adds (%v) cannot be used in filter expressions", + strings.Join(pe.Add, ", "))) + } + + var out BuildPlatformList + if len(pe.Select) == 0 && !pe.SelectCrossBuild { + // Filter is only removals so clone the original list. + out = append(out, list...) + } + + if pe.SelectCrossBuild { + out = append(out, list.CrossBuild()...) + } + for _, name := range pe.Select { + if name == "defaults" { + out = append(out, list.Defaults()...) + continue + } + out = append(out, list.Select(name)...) + } + + for _, name := range pe.Remove { + if name == "defaults" { + for _, defaultBP := range BuildPlatforms.Defaults() { + out = out.Remove(defaultBP.Name) + } + continue + } + out = out.Remove(name) + } + + return out.deduplicate() +} + +// deduplicate removes duplicate platforms and sorts the list. +func (list BuildPlatformList) deduplicate() BuildPlatformList { + set := map[string]BuildPlatform{} + for _, item := range list { + set[item.Name] = item + } + + var out BuildPlatformList + for _, v := range set { + out = append(out, v) + } + + sort.Slice(out, func(i, j int) bool { + return out[i].Name < out[j].Name + }) + return out +} diff --git a/dev-tools/mage/platforms_test.go b/dev-tools/mage/platforms_test.go new file mode 100644 index 000000000000..76b568a48cff --- /dev/null +++ b/dev-tools/mage/platforms_test.go @@ -0,0 +1,158 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package mage + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestBuildPlatform(t *testing.T) { + bp := BuildPlatform{"windows/amd64", 0} + assert.Equal(t, "windows", bp.GOOS()) + assert.Equal(t, "amd64", bp.GOARCH()) + assert.Equal(t, "", bp.GOARM()) + assert.Equal(t, "amd64", bp.Arch()) + + bp = BuildPlatform{"linux/armv7", 0} + assert.Equal(t, "linux", bp.GOOS()) + assert.Equal(t, "arm", bp.GOARCH()) + assert.Equal(t, "7", bp.GOARM()) + assert.Equal(t, "armv7", bp.Arch()) + attrs := bp.Attributes() + assert.Equal(t, bp.Name, attrs.Name) + assert.Equal(t, "linux", attrs.GOOS) + assert.Equal(t, "arm", attrs.GOARCH) + assert.Equal(t, "7", attrs.GOARM) + assert.Equal(t, "armv7", attrs.Arch) + + bp = BuildPlatform{"linux", 0} + assert.Equal(t, "linux", bp.GOOS()) + assert.Equal(t, "", bp.GOARCH()) + assert.Equal(t, "", bp.GOARM()) + assert.Equal(t, "", bp.Arch()) + attrs = bp.Attributes() + assert.Equal(t, bp.Name, attrs.Name) + assert.Equal(t, "linux", attrs.GOOS) + assert.Equal(t, "", attrs.GOARCH) + assert.Equal(t, "", attrs.GOARM) + assert.Equal(t, "", attrs.Arch) +} + +func TestBuildPlatformsListRemove(t *testing.T) { + list := BuildPlatformList{ + {"linux/amd64", 0}, + {"linux/386", 0}, + } + + assert.ElementsMatch(t, + list.Remove("linux/386"), + BuildPlatformList{{"linux/amd64", 0}}, + ) +} + +func TestBuildPlatformsListRemoveOS(t *testing.T) { + list := BuildPlatformList{ + {"linux/amd64", 0}, + {"linux/386", 0}, + {"windows/amd64", 0}, + } + + assert.ElementsMatch(t, + list.Remove("linux"), + BuildPlatformList{{"windows/amd64", 0}}, + ) +} + +func TestBuildPlatformsListSelect(t *testing.T) { + list := BuildPlatformList{ + {"linux/amd64", 0}, + {"linux/386", 0}, + } + + assert.ElementsMatch(t, + list.Select("linux/386"), + BuildPlatformList{{"linux/386", 0}}, + ) +} + +func TestBuildPlatformsListDefaults(t *testing.T) { + list := BuildPlatformList{ + {"linux/amd64", Default}, + {"linux/386", 0}, + } + + assert.ElementsMatch(t, + list.Defaults(), + BuildPlatformList{{"linux/amd64", Default}}, + ) +} + +func TestBuildPlatformsListFilter(t *testing.T) { + assert.Len(t, BuildPlatforms.Filter("!linux/armv7"), len(BuildPlatforms)-1) + + assert.Len(t, BuildPlatforms.Filter("solaris"), 1) + assert.Len(t, BuildPlatforms.Defaults().Filter("solaris"), 0) + + assert.Len(t, BuildPlatforms.Filter("windows"), 2) + assert.Len(t, BuildPlatforms.Filter("windows/386"), 1) + assert.Len(t, BuildPlatforms.Filter("!defaults"), len(BuildPlatforms)-len(BuildPlatforms.Defaults())) + + defaults := BuildPlatforms.Defaults() + assert.ElementsMatch(t, + defaults.Filter("darwin"), + defaults.Filter("!windows !linux")) + assert.ElementsMatch(t, + defaults, + defaults.Filter("windows linux darwin")) + assert.ElementsMatch(t, + defaults, + append(defaults.Filter("darwin"), defaults.Filter("!darwin")...)) + assert.ElementsMatch(t, + BuildPlatforms, + BuildPlatforms.Filter("")) + assert.ElementsMatch(t, + BuildPlatforms.Filter("defaults"), + BuildPlatforms.Defaults()) +} + +func TestNewPlatformList(t *testing.T) { + assert.Len(t, NewPlatformList("+all !linux/armv7"), len(BuildPlatforms)-1) + assert.Len(t, NewPlatformList("+solaris"), len(BuildPlatforms.Defaults())+1) + assert.Len(t, NewPlatformList("solaris"), 0) + assert.Len(t, NewPlatformList("+all solaris"), 1) + assert.Len(t, NewPlatformList("+windows"), len(BuildPlatforms.Defaults())) + assert.Len(t, NewPlatformList("+linux/ppc64 !defaults"), 1) + + assert.ElementsMatch(t, + NewPlatformList("darwin"), + NewPlatformList("!windows !linux")) + assert.ElementsMatch(t, + BuildPlatforms.Defaults(), + NewPlatformList("windows linux darwin")) + assert.ElementsMatch(t, + BuildPlatforms.Defaults(), + append(NewPlatformList("darwin"), NewPlatformList("!darwin")...)) + assert.ElementsMatch(t, + BuildPlatforms.Defaults(), + NewPlatformList("")) + assert.ElementsMatch(t, + BuildPlatforms, + NewPlatformList("+all")) +} diff --git a/dev-tools/mage/settings.go b/dev-tools/mage/settings.go new file mode 100644 index 000000000000..dc66bdd33ea6 --- /dev/null +++ b/dev-tools/mage/settings.go @@ -0,0 +1,573 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package mage + +import ( + "fmt" + "go/build" + "io/ioutil" + "log" + "os" + "path/filepath" + "regexp" + "strconv" + "strings" + "sync" + "time" + + "github.com/magefile/mage/sh" + "github.com/pkg/errors" + "golang.org/x/tools/go/vcs" +) + +const ( + fpmVersion = "1.10.0" + + // Docker images. See https://github.com/elastic/golang-crossbuild. + beatsFPMImage = "docker.elastic.co/beats-dev/fpm" + beatsCrossBuildImage = "docker.elastic.co/beats-dev/golang-crossbuild" + + elasticBeatsImportPath = "github.com/elastic/beats" +) + +// Common settings with defaults derived from files, CWD, and environment. +var ( + GOOS = build.Default.GOOS + GOARCH = build.Default.GOARCH + GOARM = EnvOr("GOARM", "") + Platform = MakePlatformAttributes(GOOS, GOARCH, GOARM) + BinaryExt = "" + + BeatName = EnvOr("BEAT_NAME", filepath.Base(CWD())) + BeatServiceName = EnvOr("BEAT_SERVICE_NAME", BeatName) + BeatIndexPrefix = EnvOr("BEAT_INDEX_PREFIX", BeatName) + BeatDescription = EnvOr("BEAT_DESCRIPTION", "") + BeatVendor = EnvOr("BEAT_VENDOR", "Elastic") + BeatLicense = EnvOr("BEAT_LICENSE", "ASL 2.0") + BeatURL = EnvOr("BEAT_URL", "https://www.elastic.co/products/beats/"+BeatName) + + Snapshot bool + + FuncMap = map[string]interface{}{ + "beat_doc_branch": BeatDocBranch, + "beat_version": BeatVersion, + "commit": CommitHash, + "date": BuildDate, + "elastic_beats_dir": ElasticBeatsDir, + "go_version": GoVersion, + "repo": GetProjectRepoInfo, + "title": strings.Title, + } +) + +func init() { + if GOOS == "windows" { + BinaryExt = ".exe" + } + + var err error + Snapshot, err = strconv.ParseBool(EnvOr("SNAPSHOT", "false")) + if err != nil { + panic(errors.Errorf("failed to parse SNAPSHOT value", err)) + } +} + +// EnvMap returns map containing the common settings variables and all variables +// from the environment. args are appended to the output prior to adding the +// environment variables (so env vars have the highest precedence). +func EnvMap(args ...map[string]interface{}) map[string]interface{} { + envMap := varMap(args...) + + // Add the environment (highest precedence). + for _, e := range os.Environ() { + env := strings.SplitN(e, "=", 2) + envMap[env[0]] = env[1] + } + + return envMap +} + +func varMap(args ...map[string]interface{}) map[string]interface{} { + data := map[string]interface{}{ + "GOOS": GOOS, + "GOARCH": GOARCH, + "GOARM": GOARM, + "Platform": Platform, + "BinaryExt": BinaryExt, + "BeatName": BeatName, + "BeatServiceName": BeatServiceName, + "BeatIndexPrefix": BeatIndexPrefix, + "BeatDescription": BeatDescription, + "BeatVendor": BeatVendor, + "BeatLicense": BeatLicense, + "BeatURL": BeatURL, + "Snapshot": Snapshot, + } + + // Add the extra args to the map. + for _, m := range args { + for k, v := range m { + data[k] = v + } + } + + return data +} + +func dumpVariables() (string, error) { + var dumpTemplate = `## Variables + +GOOS = {{.GOOS}} +GOARCH = {{.GOARCH}} +GOARM = {{.GOARM}} +Platform = {{.Platform}} +BinaryExt = {{.BinaryExt}} +BeatName = {{.BeatName}} +BeatServiceName = {{.BeatServiceName}} +BeatIndexPrefix = {{.BeatIndexPrefix}} +BeatDescription = {{.BeatDescription}} +BeatVendor = {{.BeatVendor}} +BeatLicense = {{.BeatLicense}} +BeatURL = {{.BeatURL}} + +## Functions + +beat_doc_branch = {{ beat_doc_branch }} +beat_version = {{ beat_version }} +commit = {{ commit }} +date = {{ date }} +elastic_beats_dir = {{ elastic_beats_dir }} +go_version = {{ go_version }} +repo.RootImportPath = {{ repo.RootImportPath }} +repo.RootDir = {{ repo.RootDir }} +repo.ImportPath = {{ repo.ImportPath }} +repo.SubDir = {{ repo.SubDir }} +` + + return Expand(dumpTemplate) +} + +// DumpVariables writes the template variables and values to stdout. +func DumpVariables() error { + out, err := dumpVariables() + if err != nil { + return err + } + + fmt.Println(out) + return nil +} + +var ( + commitHash string + commitHashOnce sync.Once +) + +// CommitHash returns the full length git commit hash. +func CommitHash() (string, error) { + var err error + commitHashOnce.Do(func() { + commitHash, err = sh.Output("git", "rev-parse", "HEAD") + }) + return commitHash, err +} + +var ( + elasticBeatsDirValue string + elasticBeatsDirErr error + elasticBeatsDirLock sync.Mutex +) + +// ElasticBeatsDir returns the path to Elastic beats dir. +func ElasticBeatsDir() (string, error) { + elasticBeatsDirLock.Lock() + defer elasticBeatsDirLock.Unlock() + + if elasticBeatsDirValue != "" || elasticBeatsDirErr != nil { + return elasticBeatsDirValue, elasticBeatsDirErr + } + + elasticBeatsDirValue, elasticBeatsDirErr = findElasticBeatsDir() + if elasticBeatsDirErr == nil { + log.Println("Found Elastic Beats dir at", elasticBeatsDirValue) + } + return elasticBeatsDirValue, elasticBeatsDirErr +} + +// findElasticBeatsDir attempts to find the root of the Elastic Beats directory. +// It checks to see if the current project is elastic/beats, and then if not +// checks the vendor directory. +// +// If your project places the Beats files in a different location (specifically +// the dev-tools/ contents) then you can use SetElasticBeatsDir(). +func findElasticBeatsDir() (string, error) { + repo, err := GetProjectRepoInfo() + if err != nil { + return "", err + } + + if repo.IsElasticBeats() { + return repo.RootDir, nil + } + + const devToolsImportPath = elasticBeatsImportPath + "/dev-tools/mage" + + // Search in project vendor directories. + searchPaths := []string{ + filepath.Join(repo.RootDir, repo.SubDir, "vendor", devToolsImportPath), + filepath.Join(repo.RootDir, "vendor", devToolsImportPath), + } + + for _, path := range searchPaths { + if _, err := os.Stat(path); err == nil { + return filepath.Join(path, "../.."), nil + } + } + + return "", errors.Errorf("failed to find %v in the project's vendor", devToolsImportPath) +} + +// SetElasticBeatsDir explicilty sets the location of the Elastic Beats +// directory. If not set then it will attempt to locate it. +func SetElasticBeatsDir(dir string) { + elasticBeatsDirLock.Lock() + defer elasticBeatsDirLock.Unlock() + + info, err := os.Stat(dir) + if err != nil { + panic(errors.Wrapf(err, "failed to read elastic beats dir at %v", dir)) + } + + if !info.IsDir() { + panic(errors.Errorf("elastic beats dir=%v is not a directory", dir)) + } + + elasticBeatsDirValue = filepath.Clean(dir) +} + +var ( + buildDate = time.Now().UTC().Format(time.RFC3339) +) + +// BuildDate returns the time that the build started. +func BuildDate() string { + return buildDate +} + +var ( + goVersionValue string + goVersionErr error + goVersionOnce sync.Once +) + +// GoVersion returns the version of Go defined in the project's .go-version +// file. +func GoVersion() (string, error) { + goVersionOnce.Do(func() { + goVersionValue = os.Getenv("BEAT_GO_VERSION") + if goVersionValue != "" { + return + } + + goVersionValue, goVersionErr = getBuildVariableSources().GetGoVersion() + }) + + return goVersionValue, goVersionErr +} + +var ( + beatVersionRegex = regexp.MustCompile(`(?m)^const defaultBeatVersion = "(.+)"\r?$`) + beatVersionValue string + beatVersionErr error + beatVersionOnce sync.Once +) + +// BeatVersion returns the Beat's version. The value can be overridden by +// setting BEAT_VERSION in the environment. +func BeatVersion() (string, error) { + beatVersionOnce.Do(func() { + beatVersionValue = os.Getenv("BEAT_VERSION") + if beatVersionValue != "" { + return + } + + beatVersionValue, beatVersionErr = getBuildVariableSources().GetBeatVersion() + }) + + return beatVersionValue, beatVersionErr +} + +var ( + beatDocBranchRegex = regexp.MustCompile(`(?m)doc-branch:\s*([^\s]+)\r?$`) + beatDocBranchValue string + beatDocBranchErr error + beatDocBranchOnce sync.Once +) + +// BeatDocBranch returns the documentation branch name associated with the +// Beat branch. +func BeatDocBranch() (string, error) { + beatDocBranchOnce.Do(func() { + beatDocBranchValue = os.Getenv("BEAT_DOC_BRANCH") + if beatDocBranchValue != "" { + return + } + + beatDocBranchValue, beatDocBranchErr = getBuildVariableSources().GetDocBranch() + }) + + return beatDocBranchValue, beatDocBranchErr +} + +// --- BuildVariableSources + +var ( + // DefaultBeatBuildVariableSources contains the default locations build + // variables are read from by Elastic Beats. + DefaultBeatBuildVariableSources = &BuildVariableSources{ + BeatVersion: "{{ elastic_beats_dir }}/libbeat/version/version.go", + GoVersion: "{{ elastic_beats_dir }}/.go-version", + DocBranch: "{{ elastic_beats_dir }}/libbeat/docs/version.asciidoc", + } + + buildVariableSources *BuildVariableSources + buildVariableSourcesLock sync.Mutex +) + +// SetBuildVariableSources sets the BuildVariableSources that defines where +// certain build data should be sourced from. Community Beats must call this. +func SetBuildVariableSources(s *BuildVariableSources) { + buildVariableSourcesLock.Lock() + defer buildVariableSourcesLock.Unlock() + + buildVariableSources = s +} + +func getBuildVariableSources() *BuildVariableSources { + buildVariableSourcesLock.Lock() + defer buildVariableSourcesLock.Unlock() + + if buildVariableSources != nil { + return buildVariableSources + } + + repo, err := GetProjectRepoInfo() + if err != nil { + panic(err) + } + if repo.IsElasticBeats() { + buildVariableSources = DefaultBeatBuildVariableSources + return buildVariableSources + } + + panic(errors.Errorf("magefile must call mage.SetBuildVariableSources() "+ + "because it is not an elastic beat (repo=%+v)", repo.RootImportPath)) +} + +// BuildVariableSources is used to explicitly define what files contain build +// variables and how to parse the values from that file. This removes ambiguity +// about where the data is sources and allows a degree of customization for +// community Beats. +// +// Default parsers are used if one is not defined. +type BuildVariableSources struct { + // File containing the Beat version. + BeatVersion string + + // Parses the Beat version from the BeatVersion file. + BeatVersionParser func(data []byte) (string, error) + + // File containing the Go version to be used in cross-builds. + GoVersion string + + // Parses the Go version from the GoVersion file. + GoVersionParser func(data []byte) (string, error) + + // File containing the documentation branch. + DocBranch string + + // Parses the documentation branch from the DocBranch file. + DocBranchParser func(data []byte) (string, error) +} + +func (s *BuildVariableSources) expandVar(in string) (string, error) { + return expandTemplate("inline", in, map[string]interface{}{ + "elastic_beats_dir": ElasticBeatsDir, + }) +} + +// GetBeatVersion reads the BeatVersion file and parses the version from it. +func (s *BuildVariableSources) GetBeatVersion() (string, error) { + file, err := s.expandVar(s.BeatVersion) + if err != nil { + return "", err + } + + data, err := ioutil.ReadFile(file) + if err != nil { + return "", errors.Wrapf(err, "failed to read beat version file=%v", file) + } + + if s.BeatVersionParser == nil { + s.BeatVersionParser = parseBeatVersion + } + return s.BeatVersionParser(data) +} + +// GetGoVersion reads the GoVersion file and parses the version from it. +func (s *BuildVariableSources) GetGoVersion() (string, error) { + file, err := s.expandVar(s.GoVersion) + if err != nil { + return "", err + } + + data, err := ioutil.ReadFile(file) + if err != nil { + return "", errors.Wrapf(err, "failed to read go version file=%v", file) + } + + if s.GoVersionParser == nil { + s.GoVersionParser = parseGoVersion + } + return s.GoVersionParser(data) +} + +// GetDocBranch reads the DocBranch file and parses the branch from it. +func (s *BuildVariableSources) GetDocBranch() (string, error) { + file, err := s.expandVar(s.DocBranch) + if err != nil { + return "", err + } + + data, err := ioutil.ReadFile(file) + if err != nil { + return "", errors.Wrapf(err, "failed to read doc branch file=%v", file) + } + + if s.DocBranchParser == nil { + s.DocBranchParser = parseDocBranch + } + return s.DocBranchParser(data) +} + +func parseBeatVersion(data []byte) (string, error) { + matches := beatVersionRegex.FindSubmatch(data) + if len(matches) == 2 { + return string(matches[1]), nil + } + + return "", errors.New("failed to parse beat version file") +} + +func parseGoVersion(data []byte) (string, error) { + return strings.TrimSpace(string(data)), nil +} + +func parseDocBranch(data []byte) (string, error) { + matches := beatDocBranchRegex.FindSubmatch(data) + if len(matches) == 2 { + return string(matches[1]), nil + } + + return "", errors.New("failed to parse beat doc branch") +} + +// --- ProjectRepoInfo + +// ProjectRepoInfo contains information about the project's repo. +type ProjectRepoInfo struct { + RootImportPath string // Import path at the project root. + RootDir string // Root directory of the project. + ImportPath string // Import path of the current directory. + SubDir string // Relative path from the root dir to the current dir. +} + +// IsElasticBeats returns true if the current project is +// github.com/elastic/beats. +func (r *ProjectRepoInfo) IsElasticBeats() bool { + return r.RootImportPath == elasticBeatsImportPath +} + +var ( + repoInfoValue *ProjectRepoInfo + repoInfoErr error + repoInfoOnce sync.Once +) + +// GetProjectRepoInfo returns information about the repo including the root +// import path and the current directory's import path. +func GetProjectRepoInfo() (*ProjectRepoInfo, error) { + repoInfoOnce.Do(func() { + repoInfoValue, repoInfoErr = getProjectRepoInfo() + }) + + return repoInfoValue, repoInfoErr +} + +func getProjectRepoInfo() (*ProjectRepoInfo, error) { + var ( + cwd = CWD() + rootImportPath string + srcDir string + ) + + // Search upward from the CWD to determine the project root based on VCS. + var errs []string + for _, gopath := range filepath.SplitList(build.Default.GOPATH) { + gopath = filepath.Clean(gopath) + + if !strings.HasPrefix(cwd, gopath) { + // Fixes an issue on macOS when /var is actually /private/var. + var err error + gopath, err = filepath.EvalSymlinks(gopath) + if err != nil { + errs = append(errs, err.Error()) + continue + } + } + + srcDir = filepath.Join(gopath, "src") + _, root, err := vcs.FromDir(cwd, srcDir) + if err != nil { + // Try the next gopath. + errs = append(errs, err.Error()) + continue + } + rootImportPath = root + break + } + if rootImportPath == "" { + return nil, errors.Errorf("failed to determine root import path (Did "+ + "you git init?, Is the project in the GOPATH? GOPATH=%v, CWD=%v?): %v", + build.Default.GOPATH, cwd, errs) + } + + rootDir := filepath.Join(srcDir, rootImportPath) + subDir, err := filepath.Rel(rootDir, cwd) + if err != nil { + return nil, errors.Wrap(err, "failed to get relative path to repo root") + } + importPath := filepath.ToSlash(filepath.Join(rootImportPath, subDir)) + + return &ProjectRepoInfo{ + RootImportPath: rootImportPath, + RootDir: rootDir, + SubDir: subDir, + ImportPath: importPath, + }, nil +} diff --git a/dev-tools/mage/templates/common/README.md.tmpl b/dev-tools/mage/templates/common/README.md.tmpl new file mode 100644 index 000000000000..5754ce7f87f3 --- /dev/null +++ b/dev-tools/mage/templates/common/README.md.tmpl @@ -0,0 +1,27 @@ +# Welcome to {{.BeatName | title}} {{.Version}}{{if .Snapshot}}-SNAPSHOT{{end}} + +{{.Description}} + +## Getting Started + +To get started with {{.BeatName | title}}, you need to set up Elasticsearch on +your localhost first. After that, start {{.BeatName | title}} with: + + ./{{.BeatName}} -c {{.BeatName}}.yml -e + +This will start {{.BeatName | title }} and send the data to your Elasticsearch +instance. To load the dashboards for {{.BeatName | title}} into Kibana, run: + + ./{{.BeatName}} setup -e + +For further steps visit the +[Getting started](https://www.elastic.co/guide/en/beats/{{.BeatName}}/{{ beat_doc_branch }}/{{.BeatName}}-getting-started.html) guide. + +## Documentation + +Visit [Elastic.co Docs](https://www.elastic.co/guide/en/beats/{{.BeatName}}/{{ beat_doc_branch }}/index.html) +for the full {{.BeatName | title}} documentation. + +## Release notes + +https://www.elastic.co/guide/en/beats/libbeat/{{ beat_doc_branch }}/release-notes-{{.Version}}.html diff --git a/dev-tools/mage/templates/common/magefile.go.tmpl b/dev-tools/mage/templates/common/magefile.go.tmpl new file mode 100644 index 000000000000..1a9822d2491c --- /dev/null +++ b/dev-tools/mage/templates/common/magefile.go.tmpl @@ -0,0 +1,72 @@ +// +build mage + +package main + +import ( + "fmt" + "time" + + "github.com/magefile/mage/mg" + "github.com/magefile/mage/sh" + + "github.com/elastic/beats/dev-tools/mage" +) + +func init() { + mage.BeatDescription = "One sentence description of the Beat." +} + +// Build builds the Beat binary. +func Build() error { + return mage.Build(mage.DefaultBuildArgs()) +} + +// GolangCrossBuild build the Beat binary inside of the golang-builder. +// Do not use directly, use crossBuild instead. +func GolangCrossBuild() error { + return mage.GolangCrossBuild(mage.DefaultGolangCrossBuildArgs()) +} + +// BuildGoDaemon builds the go-daemon binary (use crossBuildGoDaemon). +func BuildGoDaemon() error { + return mage.BuildGoDaemon() +} + +// CrossBuild cross-builds the beat for all target platforms. +func CrossBuild() error { + return mage.CrossBuild() +} + +// CrossBuildGoDaemon cross-builds the go-daemon binary using Docker. +func CrossBuildGoDaemon() error { + return mage.CrossBuildGoDaemon() +} + +// Clean cleans all generated files and build artifacts. +func Clean() error { + return mage.Clean() +} + +// Package packages the Beat for distribution. +// Use SNAPSHOT=true to build snapshots. +// Use PLATFORMS to control the target platforms. +func Package() { + start := time.Now() + defer func() { fmt.Println("package ran for", time.Since(start)) }() + + mage.UseCommunityBeatPackaging() + + mg.Deps(Update) + mg.Deps(CrossBuild, CrossBuildGoDaemon) + mg.SerialDeps(mage.Package, TestPackages) +} + +// TestPackages tests the generated packages (i.e. file modes, owners, groups). +func TestPackages() error { + return mage.TestPackages() +} + +// Update updates the generated files (aka make update). +func Update() error { + return sh.Run("make", "update") +} diff --git a/dev-tools/mage/templates/deb/init.sh.tmpl b/dev-tools/mage/templates/deb/init.sh.tmpl new file mode 100644 index 000000000000..1e793c9184da --- /dev/null +++ b/dev-tools/mage/templates/deb/init.sh.tmpl @@ -0,0 +1,188 @@ +#!/bin/sh +### BEGIN INIT INFO +# Provides: {{.ServiceName}} +# Required-Start: $local_fs $network $syslog +# Required-Stop: $local_fs $network $syslog +# Default-Start: 2 3 4 5 +# Default-Stop: 0 1 6 +# Short-Description: {{.Description}} +# Description: {{.BeatName | title}} is a shipper part of the Elastic Beats +# family. Please see: https://www.elastic.co/products/beats +### END INIT INFO + +# Do NOT "set -e" + +# PATH should only include /usr/* if it runs after the mountnfs.sh script +PATH=/sbin:/usr/sbin:/bin:/usr/bin +DESC="{{.Description}}" +NAME="{{.BeatName}}" +DAEMON=/usr/share/${NAME}/bin/${NAME} +DAEMON_ARGS="-c /etc/${NAME}/${NAME}.yml -path.home /usr/share/${NAME} -path.config /etc/${NAME} -path.data /var/lib/${NAME} -path.logs /var/log/${NAME}" +TEST_ARGS="-e test config" +PIDFILE=/var/run/{{.ServiceName}}.pid +WRAPPER="/usr/share/${NAME}/bin/${NAME}-god" +BEAT_USER="root" +WRAPPER_ARGS="-r / -n -p $PIDFILE" +SCRIPTNAME=/etc/init.d/{{.ServiceName}} + +# Exit if the package is not installed +[ -x "$DAEMON" ] || exit 0 + +# Read configuration variable file if it is present +[ -r /etc/default/{{.ServiceName}} ] && . /etc/default/{{.ServiceName}} + +[ "$BEAT_USER" != "root" ] && WRAPPER_ARGS="$WRAPPER_ARGS -u $BEAT_USER" +USER_WRAPPER="su" +USER_WRAPPER_ARGS="$BEAT_USER -c" + +if command -v runuser >/dev/null 2>&1; then + USER_WRAPPER="runuser" +fi + +# Load the VERBOSE setting and other rcS variables +. /lib/init/vars.sh + +# Define LSB log_* functions. +# Depend on lsb-base (>= 3.2-14) to ensure that this file is present +# and status_of_proc is working. +. /lib/lsb/init-functions + +# +# Function that calls runs the service in foreground +# to test its configuration. +# +do_test() +{ + $USER_WRAPPER $USER_WRAPPER_ARGS "$DAEMON $DAEMON_ARGS $TEST_ARGS" +} + +# +# Function that starts the daemon/service +# +do_start() +{ + # Return + # 0 if daemon has been started + # 1 if daemon was already running + # 2 if daemon could not be started + start-stop-daemon --start \ + --pidfile $PIDFILE \ + --exec $WRAPPER -- $WRAPPER_ARGS -- $DAEMON $DAEMON_ARGS \ + || return 2 +} + +# +# Function that stops the daemon/service +# +do_stop() +{ + # Return + # 0 if daemon has been stopped + # 1 if daemon was already stopped + # 2 if daemon could not be stopped + # other if a failure occurred + start-stop-daemon --stop --quiet --retry=TERM/5/KILL/5 --pidfile $PIDFILE --exec $WRAPPER + RETVAL="$?" + [ "$RETVAL" = 2 ] && return 2 + # Wait for children to finish too if this is a daemon that forks + # and if the daemon is only ever run from this initscript. + # If the above conditions are not satisfied then add some other code + # that waits for the process to drop all resources that could be + # needed by services started subsequently. A last resort is to + # sleep for some time. + start-stop-daemon --stop --quiet --oknodo --retry=0/30/KILL/5 --exec $DAEMON + [ "$?" = 2 ] && return 2 + # Many daemons don't delete their pidfiles when they exit. + rm -f $PIDFILE + return "$RETVAL" +} + +# +# Function that sends a SIGHUP to the daemon/service +# +do_reload() { + # + # If the daemon can reload its configuration without + # restarting (for example, when it is sent a SIGHUP), + # then implement that here. + # + start-stop-daemon --stop --signal 1 --quiet --pidfile $PIDFILE --exec $DAEMON + return 0 +} + +case "$1" in + start) + [ "$VERBOSE" != no ] && log_daemon_msg "Starting $DESC" "$NAME" + do_test + case "$?" in + 0) ;; + *) + log_end_msg 1 + exit 1 + ;; + esac + do_start + case "$?" in + 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; + 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; + esac + ;; + stop) + [ "$VERBOSE" != no ] && log_daemon_msg "Stopping $DESC" "$NAME" + do_stop + case "$?" in + 0|1) [ "$VERBOSE" != no ] && log_end_msg 0 ;; + 2) [ "$VERBOSE" != no ] && log_end_msg 1 ;; + esac + ;; + status) + status_of_proc "$WRAPPER" "$NAME" && exit 0 || exit $? + ;; + #reload|force-reload) + # + # If do_reload() is not implemented then leave this commented out + # and leave 'force-reload' as an alias for 'restart'. + # + #log_daemon_msg "Reloading $DESC" "$NAME" + #do_reload + #log_end_msg $? + #;; + restart|force-reload) + # + # If the "reload" option is implemented then remove the + # 'force-reload' alias + # + log_daemon_msg "Restarting $DESC" "$NAME" + do_test + case "$?" in + 0) ;; + *) + log_end_msg 1 # Old process is still running + exit 1 + ;; + esac + + do_stop + case "$?" in + 0|1) + do_start + case "$?" in + 0) log_end_msg 0 ;; + 1) log_end_msg 1 ;; # Old process is still running + *) log_end_msg 1 ;; # Failed to start + esac + ;; + *) + # Failed to stop + log_end_msg 1 + ;; + esac + ;; + *) + #echo "Usage: $SCRIPTNAME {start|stop|restart|reload|force-reload}" >&2 + echo "Usage: $SCRIPTNAME {start|stop|status|restart|force-reload}" >&2 + exit 3 + ;; +esac + +: diff --git a/dev-tools/mage/templates/linux/beatname.sh.tmpl b/dev-tools/mage/templates/linux/beatname.sh.tmpl new file mode 100644 index 000000000000..fce8cfb62591 --- /dev/null +++ b/dev-tools/mage/templates/linux/beatname.sh.tmpl @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +# Script to run {{.BeatName | title}} in foreground with the same path settings that +# the init script / systemd unit file would do. + +exec /usr/share/{{.BeatName}}/bin/{{.BeatName}} \ + -path.home /usr/share/{{.BeatName}} \ + -path.config /etc/{{.BeatName}} \ + -path.data /var/lib/{{.BeatName}} \ + -path.logs /var/log/{{.BeatName}} \ + "$@" diff --git a/dev-tools/mage/templates/linux/systemd.unit.tmpl b/dev-tools/mage/templates/linux/systemd.unit.tmpl new file mode 100644 index 000000000000..5725ba3e2a37 --- /dev/null +++ b/dev-tools/mage/templates/linux/systemd.unit.tmpl @@ -0,0 +1,12 @@ +[Unit] +Description={{.Description}} +Documentation={{.URL}} +Wants=network-online.target +After=network-online.target + +[Service] +ExecStart=/usr/share/{{.BeatName}}/bin/{{.BeatName}} -c /etc/{{.BeatName}}/{{.BeatName}}.yml -path.home /usr/share/{{.BeatName}} -path.config /etc/{{.BeatName}} -path.data /var/lib/{{.BeatName}} -path.logs /var/log/{{.BeatName}} +Restart=always + +[Install] +WantedBy=multi-user.target diff --git a/dev-tools/mage/templates/rpm/init.sh.tmpl b/dev-tools/mage/templates/rpm/init.sh.tmpl new file mode 100644 index 000000000000..e9a32ae74a5f --- /dev/null +++ b/dev-tools/mage/templates/rpm/init.sh.tmpl @@ -0,0 +1,119 @@ +#!/bin/bash +# +# {{.ServiceName}} {{.BeatName}} shipper +# +# chkconfig: 2345 98 02 +# description: Starts and stops a single {{.BeatName}} instance on this system +# + +### BEGIN INIT INFO +# Provides: {{.ServiceName}} +# Required-Start: $local_fs $network $syslog +# Required-Stop: $local_fs $network $syslog +# Default-Start: 2 3 4 5 +# Default-Stop: 0 1 6 +# Short-Description: {{.Description}} +# Description: {{.BeatName | title}} is a shipper part of the Elastic Beats +# family. Please see: https://www.elastic.co/products/beats +### END INIT INFO + + + +PATH=/usr/bin:/sbin:/bin:/usr/sbin +export PATH + +[ -f /etc/sysconfig/{{.ServiceName}} ] && . /etc/sysconfig/{{.ServiceName}} +pidfile=${PIDFILE-/var/run/{{.ServiceName}}.pid} +agent=${BEATS_AGENT-/usr/share/{{.BeatName}}/bin/{{.BeatName}}} +args="-c /etc/{{.BeatName}}/{{.BeatName}}.yml -path.home /usr/share/{{.BeatName}} -path.config /etc/{{.BeatName}} -path.data /var/lib/{{.BeatName}} -path.logs /var/log/{{.BeatName}}" +test_args="-e test config" +beat_user="${BEAT_USER:-root}" +wrapper="/usr/share/{{.BeatName}}/bin/{{.BeatName}}-god" +wrapperopts="-r / -n -p $pidfile" +user_wrapper="su" +user_wrapperopts="$beat_user -c" +RETVAL=0 + +# Source function library. +. /etc/rc.d/init.d/functions + +# Determine if we can use the -p option to daemon, killproc, and status. +# RHEL < 5 can't. +if status | grep -q -- '-p' 2>/dev/null; then + daemonopts="--pidfile $pidfile" + pidopts="-p $pidfile" +fi + +if command -v runuser >/dev/null 2>&1; then + user_wrapper="runuser" +fi + +[ "$beat_user" != "root" ] && wrapperopts="$wrapperopts -u $beat_user" + +test() { + $user_wrapper $user_wrapperopts "$agent $args $test_args" +} + +start() { + echo -n $"Starting {{.BeatName}}: " + test + if [ $? -ne 0 ]; then + echo + exit 1 + fi + daemon $daemonopts $wrapper $wrapperopts -- $agent $args + RETVAL=$? + echo + return $RETVAL +} + +stop() { + echo -n $"Stopping {{.BeatName}}: " + killproc $pidopts $wrapper + RETVAL=$? + echo + [ $RETVAL = 0 ] && rm -f ${pidfile} +} + +restart() { + test + if [ $? -ne 0 ]; then + return 1 + fi + stop + start +} + +rh_status() { + status $pidopts $wrapper + RETVAL=$? + return $RETVAL +} + +rh_status_q() { + rh_status >/dev/null 2>&1 +} + +case "$1" in + start) + start + ;; + stop) + stop + ;; + restart) + restart + ;; + condrestart|try-restart) + rh_status_q || exit 0 + restart + ;; + status) + rh_status + ;; + *) + echo $"Usage: $0 {start|stop|status|restart|condrestart}" + exit 1 +esac + +exit $RETVAL diff --git a/dev-tools/mage/templates/windows/install-service.ps1.tmpl b/dev-tools/mage/templates/windows/install-service.ps1.tmpl new file mode 100644 index 000000000000..c4b4f682f3c7 --- /dev/null +++ b/dev-tools/mage/templates/windows/install-service.ps1.tmpl @@ -0,0 +1,14 @@ +# Delete and stop the service if it already exists. +if (Get-Service {{.BeatName}} -ErrorAction SilentlyContinue) { + $service = Get-WmiObject -Class Win32_Service -Filter "name='{{.BeatName}}'" + $service.StopService() + Start-Sleep -s 1 + $service.delete() +} + +$workdir = Split-Path $MyInvocation.MyCommand.Path + +# Create the new service. +New-Service -name {{.BeatName}} ` + -displayName {{.BeatName | title}} ` + -binaryPathName "`"$workdir\{{.BeatName}}.exe`" -c `"$workdir\{{.BeatName}}.yml`" -path.home `"$workdir`" -path.data `"C:\ProgramData\{{.BeatName}}`" -path.logs `"C:\ProgramData\{{.BeatName}}\logs`"" diff --git a/dev-tools/mage/templates/windows/uninstall-service.ps1.tmpl b/dev-tools/mage/templates/windows/uninstall-service.ps1.tmpl new file mode 100644 index 000000000000..902a13a885c6 --- /dev/null +++ b/dev-tools/mage/templates/windows/uninstall-service.ps1.tmpl @@ -0,0 +1,7 @@ +# Delete and stop the service if it already exists. +if (Get-Service {{.BeatName}} -ErrorAction SilentlyContinue) { + $service = Get-WmiObject -Class Win32_Service -Filter "name='{{.BeatName}}'" + $service.StopService() + Start-Sleep -s 1 + $service.delete() +} diff --git a/dev-tools/mage/testdata/config.yml b/dev-tools/mage/testdata/config.yml new file mode 100644 index 000000000000..3b5bf4a1dabd --- /dev/null +++ b/dev-tools/mage/testdata/config.yml @@ -0,0 +1,7 @@ +brewbeat.modules: +- module: milling +- module: mashing +- module: lautering +- module: boil +- module: fermenting +- module: bottle diff --git a/dev-tools/package_test.go b/dev-tools/package_test.go index 4b4dd7e75367..2021a42a4a29 100644 --- a/dev-tools/package_test.go +++ b/dev-tools/package_test.go @@ -47,7 +47,7 @@ const ( ) var ( - configFilePattern = regexp.MustCompile(`.*beat\.yml`) + configFilePattern = regexp.MustCompile(`.*beat\.yml|apm-server\.yml`) manifestFilePattern = regexp.MustCompile(`manifest.yml`) modulesDirPattern = regexp.MustCompile(`modules.d/$`) modulesFilePattern = regexp.MustCompile(`modules.d/.+`) diff --git a/filebeat/Makefile b/filebeat/Makefile index 78b897e54fba..1c2c6f8d47b9 100644 --- a/filebeat/Makefile +++ b/filebeat/Makefile @@ -1,19 +1,13 @@ BEAT_NAME?=filebeat BEAT_TITLE?=Filebeat -BEAT_DESCRIPTION?=Filebeat sends log files to Logstash or directly to Elasticsearch. SYSTEM_TESTS?=true TEST_ENVIRONMENT?=true GOX_FLAGS=-arch="amd64 386 arm ppc64 ppc64le" ES_BEATS?=.. FIELDS_FILE_PATH=module - include ${ES_BEATS}/libbeat/scripts/Makefile -# This is called by the beats packer before building starts -.PHONY: before-build -before-build: - # Collects all module dashboards .PHONY: kibana kibana: diff --git a/filebeat/magefile.go b/filebeat/magefile.go new file mode 100644 index 000000000000..a5c378e3d993 --- /dev/null +++ b/filebeat/magefile.go @@ -0,0 +1,88 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +// +build mage + +package main + +import ( + "fmt" + "time" + + "github.com/magefile/mage/mg" + "github.com/magefile/mage/sh" + + "github.com/elastic/beats/dev-tools/mage" +) + +func init() { + mage.BeatDescription = "Filebeat sends log files to Logstash or directly to Elasticsearch." +} + +// Build builds the Beat binary. +func Build() error { + return mage.Build(mage.DefaultBuildArgs()) +} + +// GolangCrossBuild build the Beat binary inside of the golang-builder. +// Do not use directly, use crossBuild instead. +func GolangCrossBuild() error { + return mage.GolangCrossBuild(mage.DefaultGolangCrossBuildArgs()) +} + +// BuildGoDaemon builds the go-daemon binary (use crossBuildGoDaemon). +func BuildGoDaemon() error { + return mage.BuildGoDaemon() +} + +// CrossBuild cross-builds the beat for all target platforms. +func CrossBuild() error { + return mage.CrossBuild() +} + +// CrossBuildGoDaemon cross-builds the go-daemon binary using Docker. +func CrossBuildGoDaemon() error { + return mage.CrossBuildGoDaemon() +} + +// Clean cleans all generated files and build artifacts. +func Clean() error { + return mage.Clean() +} + +// Package packages the Beat for distribution. +// Use SNAPSHOT=true to build snapshots. +// Use PLATFORMS to control the target platforms. +func Package() { + start := time.Now() + defer func() { fmt.Println("package ran for", time.Since(start)) }() + + mage.UseElasticBeatPackaging() + mg.Deps(Update) + mg.Deps(CrossBuild, CrossBuildGoDaemon) + mg.SerialDeps(mage.Package, TestPackages) +} + +// TestPackages tests the generated packages (i.e. file modes, owners, groups). +func TestPackages() error { + return mage.TestPackages() +} + +// Update updates the generated files (aka make update). +func Update() error { + return sh.Run("make", "update") +} diff --git a/generator/beat/{beat}/LICENSE b/generator/beat/{beat}/LICENSE.txt similarity index 100% rename from generator/beat/{beat}/LICENSE rename to generator/beat/{beat}/LICENSE.txt diff --git a/generator/beat/{beat}/Makefile b/generator/beat/{beat}/Makefile index 4d5248b74d82..911884af2844 100644 --- a/generator/beat/{beat}/Makefile +++ b/generator/beat/{beat}/Makefile @@ -1,29 +1,28 @@ BEAT_NAME={beat} BEAT_PATH={beat_path} BEAT_GOPATH=$(firstword $(subst :, ,${GOPATH})) -BEAT_URL=https://${BEAT_PATH} SYSTEM_TESTS=false TEST_ENVIRONMENT=false ES_BEATS?=./vendor/github.com/elastic/beats GOPACKAGES=$(shell govendor list -no-status +local) -PREFIX?=. -NOTICE_FILE=NOTICE GOBUILD_FLAGS=-i -ldflags "-X $(BEAT_PATH)/vendor/github.com/elastic/beats/libbeat/version.buildTime=$(NOW) -X $(BEAT_PATH)/vendor/github.com/elastic/beats/libbeat/version.commit=$(COMMIT_ID)" +MAGE_IMPORT_PATH=${BEAT_PATH}/vendor/github.com/magefile/mage # Path to the libbeat Makefile -include $(ES_BEATS)/libbeat/scripts/Makefile # Initial beat setup .PHONY: setup -setup: copy-vendor - $(MAKE) update +setup: copy-vendor update git-init # Copy beats into vendor directory .PHONY: copy-vendor copy-vendor: - mkdir -p vendor/github.com/elastic/ + mkdir -p vendor/github.com/elastic cp -R ${BEAT_GOPATH}/src/github.com/elastic/beats vendor/github.com/elastic/ rm -rf vendor/github.com/elastic/beats/.git vendor/github.com/elastic/beats/x-pack + mkdir -p vendor/github.com/magefile + cp -R ${BEAT_GOPATH}/src/github.com/elastic/beats/vendor/github.com/magefile/mage vendor/github.com/magefile .PHONY: git-init git-init: @@ -40,10 +39,6 @@ git-init: git add .travis.yml git commit -m "Add Travis CI" -# This is called by the beats packer before building starts -.PHONY: before-build -before-build: - # Collects all dependencies and then calls update .PHONY: collect collect: fields diff --git a/generator/beat/{beat}/NOTICE b/generator/beat/{beat}/NOTICE.txt similarity index 100% rename from generator/beat/{beat}/NOTICE rename to generator/beat/{beat}/NOTICE.txt diff --git a/generator/beat/{beat}/magefile.go b/generator/beat/{beat}/magefile.go new file mode 100644 index 000000000000..545af4c3d106 --- /dev/null +++ b/generator/beat/{beat}/magefile.go @@ -0,0 +1,91 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +// +build mage + +package main + +import ( + "fmt" + "time" + + "github.com/magefile/mage/mg" + "github.com/magefile/mage/sh" + + "github.com/elastic/beats/dev-tools/mage" +) + +func init() { + mage.SetBuildVariableSources(mage.DefaultBeatBuildVariableSources) + + mage.BeatDescription = "One sentence description of the Beat." +} + +// Build builds the Beat binary. +func Build() error { + return mage.Build(mage.DefaultBuildArgs()) +} + +// GolangCrossBuild build the Beat binary inside of the golang-builder. +// Do not use directly, use crossBuild instead. +func GolangCrossBuild() error { + return mage.GolangCrossBuild(mage.DefaultGolangCrossBuildArgs()) +} + +// BuildGoDaemon builds the go-daemon binary (use crossBuildGoDaemon). +func BuildGoDaemon() error { + return mage.BuildGoDaemon() +} + +// CrossBuild cross-builds the beat for all target platforms. +func CrossBuild() error { + return mage.CrossBuild() +} + +// CrossBuildGoDaemon cross-builds the go-daemon binary using Docker. +func CrossBuildGoDaemon() error { + return mage.CrossBuildGoDaemon() +} + +// Clean cleans all generated files and build artifacts. +func Clean() error { + return mage.Clean() +} + +// Package packages the Beat for distribution. +// Use SNAPSHOT=true to build snapshots. +// Use PLATFORMS to control the target platforms. +func Package() { + start := time.Now() + defer func() { fmt.Println("package ran for", time.Since(start)) }() + + mage.UseCommunityBeatPackaging() + + mg.Deps(Update) + mg.Deps(CrossBuild, CrossBuildGoDaemon) + mg.SerialDeps(mage.Package, TestPackages) +} + +// TestPackages tests the generated packages (i.e. file modes, owners, groups). +func TestPackages() error { + return mage.TestPackages() +} + +// Update updates the generated files (aka make update). +func Update() error { + return sh.Run("make", "update") +} diff --git a/generator/metricbeat/{beat}/LICENSE b/generator/metricbeat/{beat}/LICENSE.txt similarity index 100% rename from generator/metricbeat/{beat}/LICENSE rename to generator/metricbeat/{beat}/LICENSE.txt diff --git a/generator/metricbeat/{beat}/Makefile b/generator/metricbeat/{beat}/Makefile index c50dc3c3b4d3..29e060caecfc 100644 --- a/generator/metricbeat/{beat}/Makefile +++ b/generator/metricbeat/{beat}/Makefile @@ -1,30 +1,32 @@ BEAT_NAME={beat} BEAT_PATH={beat_path} -BEAT_URL=https://${BEAT_PATH} +BEAT_GOPATH=$(firstword $(subst :, ,${GOPATH})) SYSTEM_TESTS=false TEST_ENVIRONMENT=false ES_BEATS?=./vendor/github.com/elastic/beats GOPACKAGES=$(shell govendor list -no-status +local) -PREFIX?=. -NOTICE_FILE=NOTICE +GOBUILD_FLAGS=-i -ldflags "-X $(BEAT_PATH)/vendor/github.com/elastic/beats/libbeat/version.buildTime=$(NOW) -X $(BEAT_PATH)/vendor/github.com/elastic/beats/libbeat/version.commit=$(COMMIT_ID)" +MAGE_IMPORT_PATH=${BEAT_PATH}/vendor/github.com/magefile/mage # Path to the libbeat Makefile -include $(ES_BEATS)/metricbeat/Makefile # Initial beat setup .PHONY: setup -setup: copy-vendor - $(MAKE) create-metricset - $(MAKE) collect +setup: copy-vendor create-metricset collect git-init # Copy beats into vendor directory .PHONY: copy-vendor copy-vendor: - mkdir -p vendor/github.com/elastic/ + mkdir -p vendor/github.com/elastic cp -R ${GOPATH}/src/github.com/elastic/beats vendor/github.com/elastic/ ln -s ${PWD}/vendor/github.com/elastic/beats/metricbeat/scripts/generate_imports_helper.py ${PWD}/vendor/github.com/elastic/beats/script/generate_imports_helper.py rm -rf vendor/github.com/elastic/beats/.git vendor/github.com/elastic/beats/x-pack + mkdir -p vendor/github.com/magefile + cp -R ${BEAT_GOPATH}/src/github.com/elastic/beats/vendor/github.com/magefile/mage vendor/github.com/magefile -# This is called by the beats packer before building starts -.PHONY: before-build -before-build: +.PHONY: git-init +git-init: + git init + git add --all + git commit -m 'Initial add by Beat generator' diff --git a/generator/metricbeat/{beat}/NOTICE b/generator/metricbeat/{beat}/NOTICE.txt similarity index 100% rename from generator/metricbeat/{beat}/NOTICE rename to generator/metricbeat/{beat}/NOTICE.txt diff --git a/generator/metricbeat/{beat}/magefile.go b/generator/metricbeat/{beat}/magefile.go new file mode 100644 index 000000000000..545af4c3d106 --- /dev/null +++ b/generator/metricbeat/{beat}/magefile.go @@ -0,0 +1,91 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +// +build mage + +package main + +import ( + "fmt" + "time" + + "github.com/magefile/mage/mg" + "github.com/magefile/mage/sh" + + "github.com/elastic/beats/dev-tools/mage" +) + +func init() { + mage.SetBuildVariableSources(mage.DefaultBeatBuildVariableSources) + + mage.BeatDescription = "One sentence description of the Beat." +} + +// Build builds the Beat binary. +func Build() error { + return mage.Build(mage.DefaultBuildArgs()) +} + +// GolangCrossBuild build the Beat binary inside of the golang-builder. +// Do not use directly, use crossBuild instead. +func GolangCrossBuild() error { + return mage.GolangCrossBuild(mage.DefaultGolangCrossBuildArgs()) +} + +// BuildGoDaemon builds the go-daemon binary (use crossBuildGoDaemon). +func BuildGoDaemon() error { + return mage.BuildGoDaemon() +} + +// CrossBuild cross-builds the beat for all target platforms. +func CrossBuild() error { + return mage.CrossBuild() +} + +// CrossBuildGoDaemon cross-builds the go-daemon binary using Docker. +func CrossBuildGoDaemon() error { + return mage.CrossBuildGoDaemon() +} + +// Clean cleans all generated files and build artifacts. +func Clean() error { + return mage.Clean() +} + +// Package packages the Beat for distribution. +// Use SNAPSHOT=true to build snapshots. +// Use PLATFORMS to control the target platforms. +func Package() { + start := time.Now() + defer func() { fmt.Println("package ran for", time.Since(start)) }() + + mage.UseCommunityBeatPackaging() + + mg.Deps(Update) + mg.Deps(CrossBuild, CrossBuildGoDaemon) + mg.SerialDeps(mage.Package, TestPackages) +} + +// TestPackages tests the generated packages (i.e. file modes, owners, groups). +func TestPackages() error { + return mage.TestPackages() +} + +// Update updates the generated files (aka make update). +func Update() error { + return sh.Run("make", "update") +} diff --git a/heartbeat/Makefile b/heartbeat/Makefile index bc151e77cc12..16053181ce5f 100644 --- a/heartbeat/Makefile +++ b/heartbeat/Makefile @@ -1,7 +1,5 @@ BEAT_NAME=heartbeat BEAT_TITLE=Heartbeat -BEAT_PACKAGE_NAME=heartbeat-elastic -BEAT_DESCRIPTION?=Ping remote services for availability and log results to Elasticsearch or send to Logstash. SYSTEM_TESTS=true TEST_ENVIRONMENT=false FIELDS_FILE_PATH=monitors/active @@ -9,10 +7,6 @@ FIELDS_FILE_PATH=monitors/active # Path to the libbeat Makefile -include ../libbeat/scripts/Makefile -# This is called by the beats packer before building starts -.PHONY: before-build -before-build: - # Collects all dependencies and then calls update .PHONY: collect collect: fields imports kibana diff --git a/heartbeat/magefile.go b/heartbeat/magefile.go new file mode 100644 index 000000000000..f52d0935da9d --- /dev/null +++ b/heartbeat/magefile.go @@ -0,0 +1,90 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +// +build mage + +package main + +import ( + "fmt" + "time" + + "github.com/magefile/mage/mg" + "github.com/magefile/mage/sh" + + "github.com/elastic/beats/dev-tools/mage" +) + +func init() { + mage.BeatDescription = "Ping remote services for availability and log " + + "results to Elasticsearch or send to Logstash." + mage.BeatServiceName = "heartbeat-elastic" +} + +// Build builds the Beat binary. +func Build() error { + return mage.Build(mage.DefaultBuildArgs()) +} + +// GolangCrossBuild build the Beat binary inside of the golang-builder. +// Do not use directly, use crossBuild instead. +func GolangCrossBuild() error { + return mage.GolangCrossBuild(mage.DefaultGolangCrossBuildArgs()) +} + +// BuildGoDaemon builds the go-daemon binary (use crossBuildGoDaemon). +func BuildGoDaemon() error { + return mage.BuildGoDaemon() +} + +// CrossBuild cross-builds the beat for all target platforms. +func CrossBuild() error { + return mage.CrossBuild() +} + +// CrossBuildGoDaemon cross-builds the go-daemon binary using Docker. +func CrossBuildGoDaemon() error { + return mage.CrossBuildGoDaemon() +} + +// Clean cleans all generated files and build artifacts. +func Clean() error { + return mage.Clean() +} + +// Package packages the Beat for distribution. +// Use SNAPSHOT=true to build snapshots. +// Use PLATFORMS to control the target platforms. +func Package() { + start := time.Now() + defer func() { fmt.Println("package ran for", time.Since(start)) }() + + mage.UseElasticBeatPackaging() + mg.Deps(Update) + mg.Deps(CrossBuild, CrossBuildGoDaemon) + mg.SerialDeps(mage.Package, TestPackages) +} + +// TestPackages tests the generated packages (i.e. file modes, owners, groups). +func TestPackages() error { + return mage.TestPackages() +} + +// Update updates the generated files (aka make update). +func Update() error { + return sh.Run("make", "update") +} diff --git a/libbeat/scripts/Makefile b/libbeat/scripts/Makefile index 0ba105e9b604..73e839179660 100755 --- a/libbeat/scripts/Makefile +++ b/libbeat/scripts/Makefile @@ -2,7 +2,6 @@ ### Application using libbeat may override the following variables in their Makefile BEAT_NAME?=libbeat## @packaging Name of the binary BEAT_TITLE?=${BEAT_NAME}## @packaging Title of the application -BEAT_DESCRIPTION?=Sends events to Elasticsearch or Logstash ## @packaging Description of the application BEAT_PATH?=github.com/elastic/beats/${BEAT_NAME} BEAT_PACKAGE_NAME?=${BEAT_NAME} BEAT_INDEX_PREFIX?=${BEAT_NAME} @@ -21,6 +20,7 @@ ELASTIC_LICENSE_FILE?=../licenses/ELASTIC-LICENSE.txt SECCOMP_BINARY?=${BEAT_NAME} SECCOMP_BLACKLIST?=${ES_BEATS}/libbeat/common/seccomp/seccomp-profiler-blacklist.txt SECCOMP_ALLOWLIST?=${ES_BEATS}/libbeat/common/seccomp/seccomp-profiler-allow.txt +MAGE_IMPORT_PATH?=github.com/elastic/beats/vendor/github.com/magefile/mage space:=$() # comma:=, @@ -81,16 +81,6 @@ INTEGRATION_TESTS?= FIND=. ${PYTHON_ENV}/bin/activate; find . -type f -not -path "*/vendor/*" -not -path "*/build/*" -not -path "*/.git/*" PERM_EXEC?=$(shell [ `uname -s` = "Darwin" ] && echo "+111" || echo "/a+x") -# Cross compiling targets -CGO?=true ## @building if true, Build with C Go support -TARGETS?="windows/amd64 windows/386 darwin/amd64 linux/arm" ## @building list of platforms/architecture to be built by "make package" -TARGETS_OLD?="linux/amd64 linux/386" ## @building list of Debian6 architecture to be built by "make package" when CGO is true -PACKAGES?=${BEAT_NAME}/deb ${BEAT_NAME}/rpm ${BEAT_NAME}/darwin ${BEAT_NAME}/win ${BEAT_NAME}/bin ## @Building List of OS to be supported by "make package" -PACKAGES_EXPERIMENTAL?=${BEAT_NAME}/arm ## @Building List of experimental OS by "make package". Only build when SNAPSHOT=yes -SNAPSHOT?=yes ## @Building If yes, builds a snapshot version -BEATS_BUILDER_IMAGE?=tudorg/beats-builder ## @Building Name of the docker image to use when packaging the application -BEATS_BUILDER_DEB_IMAGE?=tudorg/beats-builder-deb7 ## @Building Name of the docker image to use when packaging the application for Debian 7 - ifeq ($(DOCKER_CACHE),0) DOCKER_NOCACHE=--no-cache endif @@ -100,11 +90,6 @@ ifeq ($(RACE_DETECTOR),1) RACE=-race endif -# Only build experimental targets for snapshots -ifneq ($(SNAPSHOT),yes) - PACKAGES_EXPERIMENTAL= -endif - ### BUILDING ### @@ -163,8 +148,9 @@ clean:: ## @build Cleans up all files generated by the build steps @rm -f docker-compose.yml.lock @rm -f ${BEAT_NAME} ${BEAT_NAME}.test ${BEAT_NAME}.exe ${BEAT_NAME}.test.exe @rm -f _meta/fields.generated.yml fields.yml - @rm -fr $(PWD)/_meta/kibana.generated + @rm -rf $(PWD)/_meta/kibana.generated @rm -f ${BEAT_NAME}.template*.json + @-mage -clean 2> /dev/null .PHONY: ci ci: ## @build Shortcut for continuous integration. This should always run before merging. @@ -370,7 +356,6 @@ endif docs: ## @build Builds the documents for the beat sh ${ES_BEATS}/script/build_docs.sh ${BEAT_NAME} ${BEAT_PATH}/docs ${BUILD_DIR} - .PHONY: docs-preview docs-preview: ## @build Preview the documents for the beat in the browser PREVIEW=1 $(MAKE) docs @@ -417,152 +402,6 @@ write-environment: env-logs: ${DOCKER_COMPOSE} logs -f - -### Packaging targets #### - -# Installs the files that need to get to the home path on installations -HOME_PREFIX?=/tmp/${BEAT_NAME} -.PHONY: install-home -install-home: - if [ -a ${NOTICE_FILE} ]; then \ - install -m 644 ${NOTICE_FILE} ${HOME_PREFIX}/; \ - fi - if [ -a ${LICENSE_FILE} ]; then \ - install -m 644 ${LICENSE_FILE} ${HOME_PREFIX}/LICENSE.txt; \ - fi - if [ -d _meta/module.generated ]; then \ - install -d -m 755 ${HOME_PREFIX}/module; \ - rsync -av _meta/module.generated/ ${HOME_PREFIX}/module/; \ - chmod -R go-w ${HOME_PREFIX}/module/; \ - fi - if [ -d _meta/kibana.generated ]; then \ - install -d -m 755 ${HOME_PREFIX}/kibana; \ - rsync -av _meta/kibana.generated/ ${HOME_PREFIX}/kibana/; \ - fi - -# Prepares for packaging. Builds binaries and creates homedir data -.PHONY: prepare-package -prepare-package: - # cross compile on ubuntu - docker run --rm \ - -v $(abspath ${ES_BEATS}/dev-tools/packer/xgo-scripts):/scripts \ - -v $(abspath ${PACKER_TEMPLATES_DIR}):/templates \ - -v $(abspath ../):/source \ - -v $(PKG_BUILD_DIR):/build \ - -e PUREGO="yes" \ - -e PACK=${BEAT_NAME} \ - -e BEFORE_BUILD=before_build.sh \ - -e SOURCE=/source \ - -e TARGETS=${TARGETS} \ - -e BUILDID=${BUILDID} \ - -e ES_BEATS=${ES_BEATS} \ - -e BEAT_PATH=${BEAT_PATH} \ - -e BEAT_NAME=${BEAT_NAME} \ - -e LICENSE_FILE=${LICENSE_FILE} \ - -e BEAT_REF_YAML=${BEAT_REF_YAML} \ - ${BEATS_BUILDER_IMAGE} - -# Prepares for packaging. Builds binaries with cgo -.PHONY: prepare-package-cgo -prepare-package-cgo: - - # cross compile on ubuntu - docker run --rm \ - -v $(abspath ${ES_BEATS}/dev-tools/packer/xgo-scripts):/scripts \ - -v $(abspath ${PACKER_TEMPLATES_DIR}):/templates \ - -v $(abspath ../):/source \ - -v $(PKG_BUILD_DIR):/build \ - -e PACK=${BEAT_NAME} \ - -e BEFORE_BUILD=before_build.sh \ - -e SOURCE=/source \ - -e TARGETS=${TARGETS} \ - -e BUILDID=${BUILDID} \ - -e ES_BEATS=${ES_BEATS} \ - -e BEAT_PATH=${BEAT_PATH} \ - -e BEAT_NAME=${BEAT_NAME} \ - -e LICENSE_FILE=${LICENSE_FILE} \ - -e BEAT_REF_YAML=${BEAT_REF_YAML} \ - ${BEATS_BUILDER_IMAGE} - - # linux builds on older debian for compatibility - docker run --rm \ - -v $(abspath ${ES_BEATS}/dev-tools/packer/xgo-scripts):/scripts \ - -v $(abspath ${PACKER_TEMPLATES_DIR}):/templates \ - -v $(abspath ..):/source \ - -v ${PKG_BUILD_DIR}:/build \ - -e PACK=${BEAT_NAME} \ - -e BEFORE_BUILD=before_build.sh \ - -e SOURCE=/source \ - -e TARGETS=${TARGETS_OLD} \ - -e BUILDID=${BUILDID} \ - -e ES_BEATS=${ES_BEATS} \ - -e BEAT_PATH=${BEAT_PATH} \ - -e BEAT_NAME=${BEAT_NAME} \ - -e LICENSE_FILE=${LICENSE_FILE} \ - -e BEAT_REF_YAML=${BEAT_REF_YAML} \ - ${BEATS_BUILDER_DEB_IMAGE} - -# Prepares images for packaging -.PHONY: package-setup -package-setup: - $(MAKE) -C ${ES_BEATS}/dev-tools/packer deps images - -.PHONY: package -package: ## @packaging Create binary packages for the beat. -package: update package-setup - echo "Start building packages for ${BEAT_NAME}" - - rm -rf ${PKG_BUILD_DIR} - mkdir -p ${PKG_BUILD_DIR} - mkdir -p ${PKG_UPLOAD_DIR} - - # Generates the package.yml file with all information needed to create packages - echo "beat_name: ${BEAT_NAME}" > ${PKG_BUILD_DIR}/package.yml - echo "beat_url: ${BEAT_URL}" >> ${PKG_BUILD_DIR}/package.yml - echo "beat_repo: ${BEAT_PATH}" >> ${PKG_BUILD_DIR}/package.yml - echo "beat_pkg_name: ${BEAT_PACKAGE_NAME}" >> ${PKG_BUILD_DIR}/package.yml - echo "beat_pkg_suffix: '${PKG_SUFFIX}'" >> ${PKG_BUILD_DIR}/package.yml - echo "beat_description: ${BEAT_DESCRIPTION}" >> ${PKG_BUILD_DIR}/package.yml - echo "beat_vendor: ${BEAT_VENDOR}" >> ${PKG_BUILD_DIR}/package.yml - echo "beat_license: ${BEAT_LICENSE}" >> ${PKG_BUILD_DIR}/package.yml - echo "beat_doc_url: ${BEAT_DOC_URL}" >> ${PKG_BUILD_DIR}/package.yml - - if [ -a version.yml ]; then \ - cat version.yml >> ${PKG_BUILD_DIR}/package.yml; \ - else \ - cat ${ES_BEATS}/dev-tools/packer/version.yml >> ${PKG_BUILD_DIR}/package.yml; \ - fi - - if [ $(CGO) = true ]; then \ - $(MAKE) prepare-package-cgo; \ - else \ - $(MAKE) prepare-package; \ - fi - - SNAPSHOT=${SNAPSHOT} BUILDID=${BUILDID} BEAT_PATH=${BEAT_PATH} BUILD_DIR=${PKG_BUILD_DIR} UPLOAD_DIR=${PKG_UPLOAD_DIR} $(MAKE) BEAT_REF_YAML=${BEAT_REF_YAML} -C ${ES_BEATS}/dev-tools/packer ${PACKAGES} ${PACKAGES_EXPERIMENTAL} ${BUILD_DIR}/upload/build_id.txt - - $(MAKE) fix-permissions - echo "Finished packages for ${BEAT_NAME}" - -# Packages the Beat without Elastic X-Pack content (OSS only). -.PHONY: package-oss -package-oss: - @$(MAKE) PKG_SUFFIX=-oss package - -# Packages the Beat with Elastic X-Pack content. -.PHONY: package-elastic -package-elastic: - @$(MAKE) BEAT_LICENSE="Elastic License" LICENSE_FILE=$(ELASTIC_LICENSE_FILE) package - -.PHONY: package-all -package-all: package-elastic package-oss - -package-dashboards: package-setup - mkdir -p ${BUILD_DIR} - cp -r _meta/kibana.generated ${BUILD_DIR}/dashboards - # build the dashboards package - BEAT_NAME=${BEAT_NAME} BUILD_DIR=${BUILD_DIR} SNAPSHOT=$(SNAPSHOT) $(MAKE) -C ${ES_BEATS}/dev-tools/packer package-dashboards ${shell pwd}/build/upload/build_id.txt - fix-permissions: # Change ownership of all files inside /build folder from root/root to current user/group docker run -v ${PWD}:/beat alpine:3.4 sh -c "find /beat -user 0 -exec chown -h $(shell id -u):$(shell id -g) {} \;" @@ -598,3 +437,17 @@ seccomp: seccomp-package: SECCOMP_BINARY=build/package/${BEAT_NAME}-linux-386 $(MAKE) seccomp SECCOMP_BINARY=build/package/${BEAT_NAME}-linux-amd64 $(MAKE) seccomp + +### Packaging targets #### + +.PHONY: mage +mage: + @go install ${MAGE_IMPORT_PATH} + +.PHONY: release +release: mage + @mage package + +.PHONY: package +snapshot: mage + @SNAPSHOT=true mage package diff --git a/magefile.go b/magefile.go new file mode 100644 index 000000000000..c4f9825d8630 --- /dev/null +++ b/magefile.go @@ -0,0 +1,72 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +// +build mage + +package main + +import ( + "path/filepath" + + "github.com/elastic/beats/dev-tools/mage" +) + +var ( + // Beats is a list of Beats to collect dashboards from. + Beats = []string{ + "auditbeat", + "filebeat", + "heartbeat", + "metricbeat", + "packetbeat", + "winlogbeat", + } +) + +// PackageBeatDashboards packages the dashboards from all Beats into a zip +// file. The dashboards must be generated first. +func PackageBeatDashboards() error { + version, err := mage.BeatVersion() + if err != nil { + return err + } + + spec := mage.PackageSpec{ + Name: "beat-dashboards", + Version: version, + Snapshot: mage.Snapshot, + Files: map[string]mage.PackageFile{ + ".build_hash.txt": mage.PackageFile{ + Content: "{{ commit }}\n", + }, + }, + OutputFile: "build/distributions/dashboards/{{.Name}}-{{.Version}}{{if .Snapshot}}-SNAPSHOT{{end}}", + } + + for _, beat := range Beats { + spec.Files[beat] = mage.PackageFile{ + Source: filepath.Join(beat, "_meta/kibana.generated"), + } + } + + return mage.PackageZip(spec.Evaluate()) +} + +// DumpVariables writes the template variables and values to stdout. +func DumpVariables() error { + return mage.DumpVariables() +} diff --git a/metricbeat/Makefile b/metricbeat/Makefile index 066869afedc5..9bc8621c46f6 100644 --- a/metricbeat/Makefile +++ b/metricbeat/Makefile @@ -1,7 +1,6 @@ # Name can be overwritten, as Metricbeat is also a library BEAT_NAME?=metricbeat BEAT_TITLE?=Metricbeat -BEAT_DESCRIPTION?=Metricbeat is a lightweight shipper for metrics. SYSTEM_TESTS?=true TEST_ENVIRONMENT?=true ES_BEATS?=.. @@ -48,17 +47,6 @@ imports: python-env @mkdir -p include @${PYTHON_ENV}/bin/python ${ES_BEATS}/script/generate_imports.py ${BEAT_PATH} -# This is called by the beats packer before building starts -.PHONY: before-build -before-build: -ifeq ($(BEAT_NAME), metricbeat) - # disable the system/load metricset on windows - sed -i.bk 's/- load/#- load/' $(PREFIX)/modules.d-win/system.yml - rm $(PREFIX)/modules.d-win/system.yml.bk - sed -i.bk 's/- load/#- load/' $(PREFIX)/metricbeat-win.reference.yml - rm $(PREFIX)/metricbeat-win.reference.yml.bk -endif - # Runs all collection steps and updates afterwards .PHONY: collect collect: fields collect-docs configs kibana imports diff --git a/metricbeat/magefile.go b/metricbeat/magefile.go new file mode 100644 index 000000000000..39f3245d2375 --- /dev/null +++ b/metricbeat/magefile.go @@ -0,0 +1,155 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +// +build mage + +package main + +import ( + "fmt" + "regexp" + "time" + + "github.com/magefile/mage/mg" + "github.com/magefile/mage/sh" + "github.com/pkg/errors" + + "github.com/elastic/beats/dev-tools/mage" +) + +func init() { + mage.BeatDescription = "Metricbeat is a lightweight shipper for metrics." +} + +// Build builds the Beat binary. +func Build() error { + return mage.Build(mage.DefaultBuildArgs()) +} + +// GolangCrossBuild build the Beat binary inside of the golang-builder. +// Do not use directly, use crossBuild instead. +func GolangCrossBuild() error { + return mage.GolangCrossBuild(mage.DefaultGolangCrossBuildArgs()) +} + +// BuildGoDaemon builds the go-daemon binary (use crossBuildGoDaemon). +func BuildGoDaemon() error { + return mage.BuildGoDaemon() +} + +// CrossBuild cross-builds the beat for all target platforms. +func CrossBuild() error { + return mage.CrossBuild() +} + +// CrossBuildGoDaemon cross-builds the go-daemon binary using Docker. +func CrossBuildGoDaemon() error { + return mage.CrossBuildGoDaemon() +} + +// Clean cleans all generated files and build artifacts. +func Clean() error { + return mage.Clean() +} + +// Package packages the Beat for distribution. +// Use SNAPSHOT=true to build snapshots. +// Use PLATFORMS to control the target platforms. +func Package() { + start := time.Now() + defer func() { fmt.Println("package ran for", time.Since(start)) }() + + mage.UseElasticBeatPackaging() + customizePackaging() + + mg.Deps(Update) + mg.Deps(CrossBuild, CrossBuildGoDaemon) + mg.SerialDeps(mage.Package, TestPackages) +} + +// TestPackages tests the generated packages (i.e. file modes, owners, groups). +func TestPackages() error { + return mage.TestPackages() +} + +// Update updates the generated files (aka make update). +func Update() error { + return sh.Run("make", "update") +} + +// ----------------------------------------------------------------------------- +// Customizations specific to Metricbeat. +// - Include modules.d directory in packages. +// - Disable system/load metricset for Windows. + +// customizePackaging modifies the package specs to add the modules.d directory. +// And for Windows it comments out the system/load metricset because it's +// not supported. +func customizePackaging() { + var ( + archiveModulesDir = "modules.d" + linuxPkgModulesDir = "/usr/share/{{.BeatName}}/modules.d" + + modulesDir = mage.PackageFile{ + Mode: 0644, + Source: "modules.d", + } + windowsModulesDir = mage.PackageFile{ + Mode: 0644, + Source: "{{.PackageDir}}/modules.d", + Dep: func(spec mage.PackageSpec) error { + if err := mage.Copy("modules.d", spec.MustExpand("{{.PackageDir}}/modules.d")); err != nil { + return errors.Wrap(err, "failed to copy modules.d dir") + } + + return mage.FindReplace( + spec.MustExpand("{{.PackageDir}}/modules.d/system.yml"), + regexp.MustCompile(`- load`), `#- load`) + }, + } + windowsReferenceConfig = mage.PackageFile{ + Mode: 0644, + Source: "{{.PackageDir}}/metricbeat.reference.yml", + Dep: func(spec mage.PackageSpec) error { + err := mage.Copy("metricbeat.reference.yml", + spec.MustExpand("{{.PackageDir}}/metricbeat.reference.yml")) + if err != nil { + return errors.Wrap(err, "failed to copy reference config") + } + + return mage.FindReplace( + spec.MustExpand("{{.PackageDir}}/metricbeat.reference.yml"), + regexp.MustCompile(`- load`), `#- load`) + }, + } + ) + + for _, args := range mage.Packages { + switch args.OS { + case "windows": + args.Spec.Files[archiveModulesDir] = windowsModulesDir + args.Spec.ReplaceFile("{{.BeatName}}.reference.yml", windowsReferenceConfig) + default: + switch args.Types[0] { + case mage.TarGz, mage.Zip: + args.Spec.Files[archiveModulesDir] = modulesDir + case mage.Deb, mage.RPM: + args.Spec.Files[linuxPkgModulesDir] = modulesDir + } + } + } +} diff --git a/packetbeat/Makefile b/packetbeat/Makefile index 1e0922f64264..7c0b53258343 100644 --- a/packetbeat/Makefile +++ b/packetbeat/Makefile @@ -1,31 +1,12 @@ BEAT_NAME?=packetbeat BEAT_TITLE?=Packetbeat -BEAT_DESCRIPTION?=Packetbeat analyzes network traffic and sends the data to Elasticsearch. SYSTEM_TESTS?=true TEST_ENVIRONMENT=false ES_BEATS?=.. FIELDS_FILE_PATH=protos -TARGETS?="windows/amd64 windows/386 darwin/amd64"## @building list of platforms/architecture to be built by "make package" -PACKAGES?=${BEAT_NAME}/deb ${BEAT_NAME}/rpm ${BEAT_NAME}/darwin ${BEAT_NAME}/win ${BEAT_NAME}/bin ## @Building List of OS to be supported by "make package" -PACKAGES_EXPERIMENTAL= - include ${ES_BEATS}/libbeat/scripts/Makefile - -# This is called by the beats packer before building starts -.PHONY: before-build -before-build: - sed -i.bk 's/device: any/device: en0/' $(PREFIX)/packetbeat-darwin.yml - rm $(PREFIX)/packetbeat-darwin.yml.bk - sed -i.bk 's/device: any/device: en0/' $(PREFIX)/packetbeat-darwin.reference.yml - rm $(PREFIX)/packetbeat-darwin.reference.yml.bk - # win - sed -i.bk 's/device: any/device: 0/' $(PREFIX)/packetbeat-win.yml - rm $(PREFIX)/packetbeat-win.yml.bk - sed -i.bk 's/device: any/device: 0/' $(PREFIX)/packetbeat-win.reference.yml - rm $(PREFIX)/packetbeat-win.reference.yml.bk - # Collects all dependencies and then calls update .PHONY: collect collect: fields imports diff --git a/packetbeat/lib/windows-64/.gitignore b/packetbeat/lib/windows-64/.gitignore new file mode 100644 index 000000000000..ad7fad347cd3 --- /dev/null +++ b/packetbeat/lib/windows-64/.gitignore @@ -0,0 +1 @@ +wpcap.def diff --git a/packetbeat/lib/windows-64/sha256 b/packetbeat/lib/windows-64/sha256 new file mode 100644 index 000000000000..7e8c15523dd1 --- /dev/null +++ b/packetbeat/lib/windows-64/sha256 @@ -0,0 +1 @@ +0948518b229fb502b9c063966fc3afafbb749241a1c184f6eb7d532e00bce1d8 wpcap.dll diff --git a/packetbeat/lib/windows-64/wpcap.dll b/packetbeat/lib/windows-64/wpcap.dll new file mode 100644 index 0000000000000000000000000000000000000000..ebb17ad55c754a57be4cf1916d729d1f804ed930 GIT binary patch literal 281104 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�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 literal 0 HcmV?d00001 diff --git a/packetbeat/magefile.go b/packetbeat/magefile.go new file mode 100644 index 000000000000..f1ff4723117e --- /dev/null +++ b/packetbeat/magefile.go @@ -0,0 +1,405 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +// +build mage + +package main + +import ( + "fmt" + "log" + "regexp" + "strings" + "time" + + "github.com/magefile/mage/mg" + "github.com/magefile/mage/sh" + "github.com/pkg/errors" + + "github.com/elastic/beats/dev-tools/mage" +) + +func init() { + mage.BeatDescription = "Packetbeat analyzes network traffic and sends the data to Elasticsearch." +} + +// Build builds the Beat binary. +func Build() error { + return mage.Build(mage.DefaultBuildArgs()) +} + +// GolangCrossBuild build the Beat binary inside of the golang-builder. +// Do not use directly, use crossBuild instead. +func GolangCrossBuild() error { + if dep, found := crossBuildDeps[mage.Platform.Name]; found { + mg.Deps(dep) + } + + params := mage.DefaultGolangCrossBuildArgs() + if flags, found := libpcapLDFLAGS[mage.Platform.Name]; found { + params.Env = map[string]string{ + "CGO_LDFLAGS": flags, + } + } + if flags, found := libpcapCFLAGS[mage.Platform.Name]; found { + params.Env["CGO_CFLAGS"] = flags + } + + return mage.GolangCrossBuild(params) +} + +// BuildGoDaemon builds the go-daemon binary (use crossBuildGoDaemon). +func BuildGoDaemon() error { + return mage.BuildGoDaemon() +} + +// CrossBuild cross-builds the beat for all target platforms. +func CrossBuild() error { + mg.Deps(patchCGODirectives) + defer undoPatchCGODirectives() + + // These Windows builds write temporary .s and .o files into the packetbeat + // dir so they cannot be run in parallel. Changing to a different CWD does + // not change where the temp files get written so that cannot be used as a + // fix. + if err := mage.CrossBuild(mage.ForPlatforms("windows"), mage.Serially()); err != nil { + return err + } + + return mage.CrossBuild(mage.ForPlatforms("!windows")) +} + +// CrossBuildGoDaemon cross-builds the go-daemon binary using Docker. +func CrossBuildGoDaemon() error { + return mage.CrossBuildGoDaemon() +} + +// Clean cleans all generated files and build artifacts. +func Clean() error { + return mage.Clean() +} + +// Package packages the Beat for distribution. +// Use SNAPSHOT=true to build snapshots. +// Use PLATFORMS to control the target platforms. +func Package() { + start := time.Now() + defer func() { fmt.Println("package ran for", time.Since(start)) }() + + mage.UseElasticBeatPackaging() + customizePackaging() + + mg.Deps(Update) + mg.Deps(CrossBuild, CrossBuildGoDaemon) + mg.SerialDeps(mage.Package, TestPackages) +} + +// TestPackages tests the generated packages (i.e. file modes, owners, groups). +func TestPackages() error { + return mage.TestPackages() +} + +// Update updates the generated files (aka make update). +func Update() error { + return sh.Run("make", "update") +} + +// ----------------------------------------------------------------------------- +// Customizations specific to Packetbeat. +// - Config file contains an OS specific device name (affects darwin, windows). +// - Must compile libpcap or winpcap during cross-compilation. +// - On Linux libpcap is statically linked. Darwin and Windows are dynamic. + +const ( + libpcapURL = "https://s3.amazonaws.com/beats-files/deps/libpcap-1.8.1.tar.gz" + libpcapSHA256 = "673dbc69fdc3f5a86fb5759ab19899039a8e5e6c631749e48dcd9c6f0c83541e" +) + +const ( + linuxPcapLDFLAGS = "-L/libpcap/libpcap-1.8.1 -lpcap" + linuxPcapCFLAGS = "-I /libpcap/libpcap-1.8.1" +) + +var libpcapLDFLAGS = map[string]string{ + "linux/386": linuxPcapLDFLAGS, + "linux/amd64": linuxPcapLDFLAGS, + "linux/arm64": linuxPcapLDFLAGS, + "linux/armv5": linuxPcapLDFLAGS, + "linux/armv6": linuxPcapLDFLAGS, + "linux/armv7": linuxPcapLDFLAGS, + "linux/mips": linuxPcapLDFLAGS, + "linux/mipsle": linuxPcapLDFLAGS, + "linux/mips64": linuxPcapLDFLAGS, + "linux/mips64le": linuxPcapLDFLAGS, + "linux/ppc64le": linuxPcapLDFLAGS, + "linux/s390x": linuxPcapLDFLAGS, + "darwin/amd64": "-lpcap", + "windows/amd64": "-L /libpcap/win/WpdPack/Lib/x64 -lwpcap", + "windows/386": "-L /libpcap/win/WpdPack/Lib -lwpcap", +} + +var libpcapCFLAGS = map[string]string{ + "linux/386": linuxPcapCFLAGS, + "linux/amd64": linuxPcapCFLAGS, + "linux/armv5": linuxPcapCFLAGS, + "linux/armv6": linuxPcapCFLAGS, + "linux/armv7": linuxPcapCFLAGS, + "linux/mips": linuxPcapCFLAGS, + "linux/mipsle": linuxPcapCFLAGS, + "linux/mips64": linuxPcapCFLAGS, + "linux/mips64le": linuxPcapCFLAGS, + "linux/ppc64le": linuxPcapCFLAGS, + "linux/s390x": linuxPcapCFLAGS, + "windows/amd64": "-I /libpcap/win/WpdPack/Include", + "windows/386": "-I /libpcap/win/WpdPack/Include", +} + +var crossBuildDeps = map[string]func() error{ + "linux/amd64": buildLibpcapLinuxAMD64, + "linux/386": buildLibpcapLinux386, + "linux/arm64": buildLibpcapLinuxARM64, + "linux/armv5": buildLibpcapLinuxARMv5, + "linux/armv6": buildLibpcapLinuxARMv6, + "linux/armv7": buildLibpcapLinuxARMv7, + "linux/mips": buildLibpcapLinuxMIPS, + "linux/mipsle": buildLibpcapLinuxMIPSLE, + "linux/mips64": buildLibpcapLinuxMIPS64, + "linux/mips64le": buildLibpcapLinuxMIPS64LE, + "linux/ppc64le": buildLibpcapLinuxPPC64LE, + "linux/s390x": buildLibpcapLinuxS390x, + "windows/amd64": installLibpcapWindowsAMD64, + "windows/386": installLibpcapWindows386, +} + +// buildLibpcapFromSource builds libpcap from source because the library needs +// to be compiled with -fPIC. +// See https://github.com/elastic/beats/pull/4217. +func buildLibpcapFromSource(params map[string]string) error { + tarFile, err := mage.DownloadFile(libpcapURL, "/libpcap") + if err != nil { + return errors.Wrap(err, "failed to download libpcap source") + } + + if err = mage.VerifySHA256(tarFile, libpcapSHA256); err != nil { + return err + } + + if err = mage.Extract(tarFile, "/libpcap"); err != nil { + return errors.Wrap(err, "failed to extract libpcap") + } + + var configureArgs []string + for k, v := range params { + if strings.HasPrefix(k, "-") { + delete(params, k) + configureArgs = append(configureArgs, k+"="+v) + } + } + + // Use sh -c here because sh.Run does not expose a way to change the CWD. + // This command only runs in Linux so this is fine. + return sh.RunWith(params, "sh", "-c", + "cd /libpcap/libpcap-1.8.1 && "+ + "./configure --enable-usb=no --enable-bluetooth=no --enable-dbus=no "+strings.Join(configureArgs, " ")+"&& "+ + "make") +} + +func buildLibpcapLinux386() error { + return buildLibpcapFromSource(map[string]string{ + "CFLAGS": "-m32", + "LDFLAGS": "-m32", + }) +} + +func buildLibpcapLinuxAMD64() error { + return buildLibpcapFromSource(map[string]string{}) +} + +func buildLibpcapLinuxARM64() error { + return buildLibpcapFromSource(map[string]string{ + "--host": "aarch64-unknown-linux-gnu", + "--with-pcap": "linux", + }) +} + +func buildLibpcapLinuxARMv5() error { + return buildLibpcapFromSource(map[string]string{ + "--host": "arm-linux-gnueabi", + "--with-pcap": "linux", + }) +} + +func buildLibpcapLinuxARMv6() error { + return buildLibpcapFromSource(map[string]string{ + "--host": "arm-linux-gnueabi", + "--with-pcap": "linux", + }) +} + +func buildLibpcapLinuxARMv7() error { + return buildLibpcapFromSource(map[string]string{ + "--host": "arm-linux-gnueabihf", + "--with-pcap": "linux", + }) +} + +func buildLibpcapLinuxMIPS() error { + return buildLibpcapFromSource(map[string]string{ + "--host": "mips-unknown-linux-gnu", + "--with-pcap": "linux", + }) +} + +func buildLibpcapLinuxMIPSLE() error { + return buildLibpcapFromSource(map[string]string{ + "--host": "mipsle-unknown-linux-gnu", + "--with-pcap": "linux", + }) +} + +func buildLibpcapLinuxMIPS64() error { + return buildLibpcapFromSource(map[string]string{ + "--host": "mips64-unknown-linux-gnu", + "--with-pcap": "linux", + }) +} + +func buildLibpcapLinuxMIPS64LE() error { + return buildLibpcapFromSource(map[string]string{ + "--host": "mips64le-unknown-linux-gnu", + "--with-pcap": "linux", + }) +} + +func buildLibpcapLinuxPPC64LE() error { + return buildLibpcapFromSource(map[string]string{ + "--host": "powerpc64le-linux-gnu", + "--with-pcap": "linux", + }) +} + +func buildLibpcapLinuxS390x() error { + return buildLibpcapFromSource(map[string]string{ + "--host": "s390x-ibm-linux-gnu", + "--with-pcap": "linux", + }) +} + +func installLibpcapWindowsAMD64() error { + mg.SerialDeps(installWinpcap, generateWin64StaticWinpcap) + return nil +} + +func installLibpcapWindows386() error { + return installWinpcap() +} + +func installWinpcap() error { + log.Println("Install Winpcap") + const wpdpackURL = "https://www.winpcap.org/install/bin/WpdPack_4_1_2.zip" + + winpcapZip, err := mage.DownloadFile(wpdpackURL, "/") + if err != nil { + return err + } + + if err = mage.Extract(winpcapZip, "/libpcap/win"); err != nil { + return err + } + + return nil +} + +func generateWin64StaticWinpcap() error { + log.Println(">> Generating 64-bit winpcap static lib") + + // Ref: https://github.com/elastic/beats/issues/1259 + defer mage.DockerChown("lib/windows-64/wpcap.def") + return mage.RunCmds( + // Requires mingw-w64-tools. + []string{"gendef", "lib/windows-64/wpcap.dll"}, + []string{"mv", "wpcap.def", "lib/windows-64/wpcap.def"}, + []string{"x86_64-w64-mingw32-dlltool", "--as-flags=--64", + "-m", "i386:x86-64", "-k", + "--output-lib", "/libpcap/win/WpdPack/Lib/x64/libwpcap.a", + "--input-def", "lib/windows-64/wpcap.def"}, + ) +} + +const pcapGoFile = "../vendor/github.com/tsg/gopacket/pcap/pcap.go" + +var cgoDirectiveRegex = regexp.MustCompile(`(?m)#cgo .*(?:LDFLAGS|CFLAGS).*$`) + +func patchCGODirectives() error { + // cgo directives do not support GOARM tags so we will clear the tags + // and set them via CGO_LDFLAGS and CGO_CFLAGS. + // Ref: https://github.com/golang/go/issues/7211 + log.Println("Patching", pcapGoFile, cgoDirectiveRegex.String()) + return mage.FindReplace(pcapGoFile, cgoDirectiveRegex, "") +} + +func undoPatchCGODirectives() error { + return sh.Run("git", "checkout", pcapGoFile) +} + +// customizePackaging modifies the device in the configuration files based on +// the target OS. +func customizePackaging() { + var ( + defaultDevice = map[string]string{ + "darwin": "en0", + "windows": "0", + } + + configYml = mage.PackageFile{ + Mode: 0600, + Source: "{{.PackageDir}}/{{.BeatName}}.yml", + Dep: func(spec mage.PackageSpec) error { + if err := mage.Copy("packetbeat.yml", + spec.MustExpand("{{.PackageDir}}/packetbeat.yml")); err != nil { + return errors.Wrap(err, "failed to copy config") + } + + return mage.FindReplace( + spec.MustExpand("{{.PackageDir}}/packetbeat.yml"), + regexp.MustCompile(`device: any`), "device: "+defaultDevice[spec.OS]) + }, + } + referenceConfigYml = mage.PackageFile{ + Mode: 0644, + Source: "{{.PackageDir}}/{{.BeatName}}.reference.yml", + Dep: func(spec mage.PackageSpec) error { + if err := mage.Copy("packetbeat.yml", + spec.MustExpand("{{.PackageDir}}/packetbeat.reference.yml")); err != nil { + return errors.Wrap(err, "failed to copy config") + } + + return mage.FindReplace( + spec.MustExpand("{{.PackageDir}}/packetbeat.reference.yml"), + regexp.MustCompile(`device: any`), "device: "+defaultDevice[spec.OS]) + }, + } + ) + + for _, args := range mage.Packages { + switch args.OS { + case "windows", "darwin": + args.Spec.ReplaceFile("{{.BeatName}}.yml", configYml) + args.Spec.ReplaceFile("{{.BeatName}}.reference.yml", referenceConfigYml) + } + } +} diff --git a/winlogbeat/Makefile b/winlogbeat/Makefile index 3b533836cd74..4d53434f531e 100644 --- a/winlogbeat/Makefile +++ b/winlogbeat/Makefile @@ -1,15 +1,8 @@ BEAT_NAME=winlogbeat BEAT_TITLE=Winlogbeat -BEAT_DESCRIPTION=Winlogbeat ships Windows event logs to Elasticsearch or Logstash. SYSTEM_TESTS=true TEST_ENVIRONMENT=false - GOX_OS=windows -TARGETS?="windows/amd64 windows/386" -PACKAGES=winlogbeat/win -PACKAGES_EXPERIMENTAL= - -CGO=false # don't need Cgo in Winlogbeat include ../libbeat/scripts/Makefile @@ -17,10 +10,6 @@ include ../libbeat/scripts/Makefile gen: GOOS=windows GOARCH=386 go generate -v -x ./... -# This is called by the beats packer before building starts -.PHONY: before-build -before-build: - # Collects all dependencies and then calls update .PHONY: collect collect: fields diff --git a/winlogbeat/magefile.go b/winlogbeat/magefile.go new file mode 100644 index 000000000000..5500f9fdca8e --- /dev/null +++ b/winlogbeat/magefile.go @@ -0,0 +1,90 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +// +build mage + +package main + +import ( + "fmt" + "time" + + "github.com/magefile/mage/mg" + "github.com/magefile/mage/sh" + + "github.com/elastic/beats/dev-tools/mage" +) + +func init() { + mage.BeatDescription = "Winlogbeat ships Windows event logs to Elasticsearch or Logstash." + + mage.Platforms = mage.Platforms.Filter("windows") +} + +// Build builds the Beat binary. +func Build() error { + return mage.Build(mage.DefaultBuildArgs()) +} + +// GolangCrossBuild build the Beat binary inside of the golang-builder. +// Do not use directly, use crossBuild instead. +func GolangCrossBuild() error { + return mage.GolangCrossBuild(mage.DefaultGolangCrossBuildArgs()) +} + +// BuildGoDaemon builds the go-daemon binary (use crossBuildGoDaemon). +func BuildGoDaemon() error { + return mage.BuildGoDaemon() +} + +// CrossBuild cross-builds the beat for all target platforms. +func CrossBuild() error { + return mage.CrossBuild() +} + +// CrossBuildGoDaemon cross-builds the go-daemon binary using Docker. +func CrossBuildGoDaemon() error { + return mage.CrossBuildGoDaemon() +} + +// Clean cleans all generated files and build artifacts. +func Clean() error { + return mage.Clean() +} + +// Package packages the Beat for distribution. +// Use SNAPSHOT=true to build snapshots. +// Use PLATFORMS to control the target platforms. +func Package() { + start := time.Now() + defer func() { fmt.Println("package ran for", time.Since(start)) }() + + mage.UseElasticBeatPackaging() + mg.Deps(Update) + mg.Deps(CrossBuild, CrossBuildGoDaemon) + mg.SerialDeps(mage.Package, TestPackages) +} + +// TestPackages tests the generated packages (i.e. file modes, owners, groups). +func TestPackages() error { + return mage.TestPackages() +} + +// Update updates the generated files (aka make update). +func Update() error { + return sh.Run("make", "update") +}