Skip to content

Commit b39b9af

Browse files
authored
Add manifest to package (#3910)
* Add manifest to package * Add tests for manifest in zip packages * Add manifest to rpm and deb packages
1 parent 5f578c5 commit b39b9af

File tree

7 files changed

+248
-21
lines changed

7 files changed

+248
-21
lines changed

dev-tools/mage/common.go

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -480,6 +480,14 @@ func untar(sourceFile, destinationDir string) error {
480480
if err = writer.Close(); err != nil {
481481
return err
482482
}
483+
case tar.TypeSymlink:
484+
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
485+
return err
486+
}
487+
if err := os.Symlink(header.Linkname, path); err != nil {
488+
return fmt.Errorf("error creating symlink %s pointing to %s: %w", path, header.Linkname, err)
489+
}
490+
483491
default:
484492
return fmt.Errorf("unable to untar type=%c in file=%s", header.Typeflag, path)
485493
}
@@ -861,21 +869,21 @@ var parseVersionRegex = regexp.MustCompile(`(?m)^[^\d]*(?P<major>\d+)\.(?P<minor
861869

862870
// ParseVersion extracts the major, minor, and optional patch number from a
863871
// version string.
864-
func ParseVersion(version string) (major, minor, patch int, err error) {
872+
func ParseVersion(version string) (int, int, int, error) {
865873
names := parseVersionRegex.SubexpNames()
866874
matches := parseVersionRegex.FindStringSubmatch(version)
867875
if len(matches) == 0 {
868-
err = fmt.Errorf("failed to parse version '%v'", version)
869-
return
876+
err := fmt.Errorf("failed to parse version '%v'", version)
877+
return 0, 0, 0, err
870878
}
871879

872880
data := map[string]string{}
873881
for i, match := range matches {
874882
data[names[i]] = match
875883
}
876-
major, _ = strconv.Atoi(data["major"])
877-
minor, _ = strconv.Atoi(data["minor"])
878-
patch, _ = strconv.Atoi(data["patch"])
884+
major, _ := strconv.Atoi(data["major"])
885+
minor, _ := strconv.Atoi(data["minor"])
886+
patch, _ := strconv.Atoi(data["patch"])
879887
return major, minor, patch, nil
880888
}
881889

dev-tools/mage/settings.go

Lines changed: 52 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"go/build"
1111
"log"
1212
"os"
13+
"path"
1314
"path/filepath"
1415
"regexp"
1516
"strconv"
@@ -19,11 +20,13 @@ import (
1920

2021
"golang.org/x/text/cases"
2122
"golang.org/x/text/language"
23+
"gopkg.in/yaml.v3"
2224

2325
"github.com/magefile/mage/sh"
2426
"golang.org/x/tools/go/vcs"
2527

2628
"github.com/elastic/elastic-agent/dev-tools/mage/gotool"
29+
v1 "github.com/elastic/elastic-agent/pkg/api/v1"
2730
)
2831

2932
const (
@@ -43,7 +46,8 @@ const (
4346
agentPackageVersionEnvVar = "AGENT_PACKAGE_VERSION"
4447

4548
// Mapped functions
46-
agentPackageVersionMappedFunc = "agent_package_version"
49+
agentPackageVersionMappedFunc = "agent_package_version"
50+
agentManifestGeneratorMappedFunc = "manifest"
4751
)
4852

4953
// Common settings with defaults derived from files, CWD, and environment.
@@ -91,18 +95,19 @@ var (
9195
ManifestURL string
9296

9397
FuncMap = map[string]interface{}{
94-
"beat_doc_branch": BeatDocBranch,
95-
"beat_version": BeatQualifiedVersion,
96-
"commit": CommitHash,
97-
"commit_short": CommitHashShort,
98-
"date": BuildDate,
99-
"elastic_beats_dir": ElasticBeatsDir,
100-
"go_version": GoVersion,
101-
"repo": GetProjectRepoInfo,
102-
"title": func(s string) string { return cases.Title(language.English, cases.NoLower).String(s) },
103-
"tolower": strings.ToLower,
104-
"contains": strings.Contains,
105-
agentPackageVersionMappedFunc: AgentPackageVersion,
98+
"beat_doc_branch": BeatDocBranch,
99+
"beat_version": BeatQualifiedVersion,
100+
"commit": CommitHash,
101+
"commit_short": CommitHashShort,
102+
"date": BuildDate,
103+
"elastic_beats_dir": ElasticBeatsDir,
104+
"go_version": GoVersion,
105+
"repo": GetProjectRepoInfo,
106+
"title": func(s string) string { return cases.Title(language.English, cases.NoLower).String(s) },
107+
"tolower": strings.ToLower,
108+
"contains": strings.Contains,
109+
agentPackageVersionMappedFunc: AgentPackageVersion,
110+
agentManifestGeneratorMappedFunc: PackageManifest,
106111
}
107112
)
108113

@@ -296,6 +301,40 @@ func AgentPackageVersion() (string, error) {
296301
return BeatQualifiedVersion()
297302
}
298303

304+
func PackageManifest() (string, error) {
305+
m := v1.NewManifest()
306+
m.Package.Snapshot = Snapshot
307+
packageVersion, err := AgentPackageVersion()
308+
if err != nil {
309+
return "", fmt.Errorf("retrieving agent package version: %w", err)
310+
}
311+
m.Package.Version = packageVersion
312+
commitHashShort, err := CommitHashShort()
313+
if err != nil {
314+
return "", fmt.Errorf("retrieving agent commit hash: %w", err)
315+
}
316+
317+
versionedHomePath := path.Join("data", fmt.Sprintf("%s-%s", BeatName, commitHashShort))
318+
m.Package.VersionedHome = versionedHomePath
319+
m.Package.PathMappings = []map[string]string{{}}
320+
m.Package.PathMappings[0][versionedHomePath] = fmt.Sprintf("data/elastic-agent-%s%s-%s", m.Package.Version, SnapshotSuffix(), commitHashShort)
321+
m.Package.PathMappings[0]["manifest.yaml"] = fmt.Sprintf("data/elastic-agent-%s%s-%s/manifest.yaml", m.Package.Version, SnapshotSuffix(), commitHashShort)
322+
yamlBytes, err := yaml.Marshal(m)
323+
if err != nil {
324+
return "", fmt.Errorf("marshaling manifest: %w", err)
325+
326+
}
327+
return string(yamlBytes), nil
328+
}
329+
330+
func SnapshotSuffix() string {
331+
if !Snapshot {
332+
return ""
333+
}
334+
335+
return "-SNAPSHOT"
336+
}
337+
299338
var (
300339
elasticBeatsDirValue string
301340
elasticBeatsDirErr error

dev-tools/packaging/package_test.go

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"fmt"
2020
"io"
2121
"os"
22+
"path"
2223
"path/filepath"
2324
"regexp"
2425
"slices"
@@ -27,6 +28,12 @@ import (
2728

2829
"github.com/blakesmith/ar"
2930
"github.com/cavaliercoder/go-rpm"
31+
"github.com/stretchr/testify/assert"
32+
"github.com/stretchr/testify/require"
33+
"gopkg.in/yaml.v3"
34+
35+
"github.com/elastic/elastic-agent/dev-tools/mage"
36+
v1 "github.com/elastic/elastic-agent/pkg/api/v1"
3037
)
3138

3239
const (
@@ -159,6 +166,14 @@ func checkTar(t *testing.T, file string) {
159166
checkModulesPermissions(t, p)
160167
checkModulesOwner(t, p, true)
161168
checkLicensesPresent(t, "", p)
169+
170+
t.Run(p.Name+"_check_manifest_file", func(t *testing.T) {
171+
tempExtractionPath := t.TempDir()
172+
err = mage.Extract(file, tempExtractionPath)
173+
require.NoError(t, err, "error extracting tar archive")
174+
containingDir := strings.TrimSuffix(path.Base(file), ".tar.gz")
175+
checkManifestFileContents(t, filepath.Join(tempExtractionPath, containingDir))
176+
})
162177
}
163178

164179
func checkZip(t *testing.T, file string) {
@@ -174,6 +189,48 @@ func checkZip(t *testing.T, file string) {
174189
checkModulesDPresent(t, "", p)
175190
checkModulesPermissions(t, p)
176191
checkLicensesPresent(t, "", p)
192+
193+
t.Run(p.Name+"_check_manifest_file", func(t *testing.T) {
194+
tempExtractionPath := t.TempDir()
195+
err = mage.Extract(file, tempExtractionPath)
196+
require.NoError(t, err, "error extracting zip archive")
197+
containingDir := strings.TrimSuffix(path.Base(file), ".zip")
198+
checkManifestFileContents(t, filepath.Join(tempExtractionPath, containingDir))
199+
})
200+
}
201+
202+
func checkManifestFileContents(t *testing.T, extractedPackageDir string) {
203+
t.Log("Checking file manifest.yaml")
204+
manifestReadCloser, err := os.Open(filepath.Join(extractedPackageDir, "manifest.yaml"))
205+
if err != nil {
206+
t.Errorf("opening manifest %s : %v", "manifest.yaml", err)
207+
}
208+
defer func(closer io.ReadCloser) {
209+
err := closer.Close()
210+
assert.NoError(t, err, "error closing manifest file")
211+
}(manifestReadCloser)
212+
213+
var m v1.PackageManifest
214+
err = yaml.NewDecoder(manifestReadCloser).Decode(&m)
215+
if err != nil {
216+
t.Errorf("unmarshaling package manifest: %v", err)
217+
}
218+
219+
assert.Equal(t, v1.ManifestKind, m.Kind, "manifest specifies wrong kind")
220+
assert.Equal(t, v1.VERSION, m.Version, "manifest specifies wrong api version")
221+
222+
if assert.NotEmpty(t, m.Package.PathMappings, "path mappings in manifest are empty") {
223+
versionedHome := m.Package.VersionedHome
224+
assert.DirExistsf(t, filepath.Join(extractedPackageDir, versionedHome), "versionedHome directory %q not found in %q", versionedHome, extractedPackageDir)
225+
if assert.Contains(t, m.Package.PathMappings[0], versionedHome, "path mappings in manifest do not contain the extraction path for versionedHome") {
226+
// the first map should have the mapping for the data/elastic-agent-****** path)
227+
mappedPath := m.Package.PathMappings[0][versionedHome]
228+
assert.Contains(t, mappedPath, m.Package.Version, "mapped path for versionedHome does not contain the package version")
229+
if m.Package.Snapshot {
230+
assert.Contains(t, mappedPath, "SNAPSHOT", "mapped path for versionedHome does not contain the snapshot qualifier")
231+
}
232+
}
233+
}
177234
}
178235

179236
const (
@@ -667,9 +724,9 @@ func readTarContents(tarName string, data io.Reader) (*packageFile, error) {
667724
type inspector func(pkg, file string, contents io.Reader) error
668725

669726
func readZip(t *testing.T, zipFile string, inspectors ...inspector) (*packageFile, error) {
670-
r, err := zip.OpenReader(zipFile)
727+
r, err := openZip(zipFile)
671728
if err != nil {
672-
return nil, err
729+
return nil, fmt.Errorf("opening zip: %w", err)
673730
}
674731
defer r.Close()
675732

@@ -699,6 +756,14 @@ func readZip(t *testing.T, zipFile string, inspectors ...inspector) (*packageFil
699756
return p, nil
700757
}
701758

759+
func openZip(zipFile string) (*zip.ReadCloser, error) {
760+
r, err := zip.OpenReader(zipFile)
761+
if err != nil {
762+
return nil, err
763+
}
764+
return r, nil
765+
}
766+
702767
func readDocker(dockerFile string) (*packageFile, *dockerInfo, error) {
703768
// Read the manifest file first so that the config file and layer
704769
// names are known in advance.

dev-tools/packaging/packages.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,10 @@ shared:
7474
mode: 0755
7575
config_mode: 0644
7676
skip_on_missing: true
77+
/var/lib/{{.BeatName}}/data/{{.BeatName}}-{{ commit_short }}/manifest.yaml:
78+
mode: 0644
79+
content: >
80+
{{ manifest }}
7781
7882
# MacOS pkg spec for community beats.
7983
- &macos_agent_pkg_spec
@@ -159,6 +163,10 @@ shared:
159163
content: >
160164
{{ commit }}
161165
mode: 0644
166+
'manifest.yaml':
167+
mode: 0644
168+
content: >
169+
{{ manifest }}
162170
163171
- &agent_binary_files
164172
'{{.BeatName}}{{.BinaryExt}}':

pkg/api/v1/api.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
2+
// or more contributor license agreements. Licensed under the Elastic License;
3+
// you may not use this file except in compliance with the Elastic License.
4+
5+
// Package v1 contains definitions for elastic-agent/v1 objects
6+
package v1
7+
8+
const VERSION = "co.elastic.agent/v1"
9+
10+
type apiObject struct {
11+
Version string `yaml:"version" json:"version"`
12+
Kind string `yaml:"kind" json:"kind"`
13+
}

pkg/api/v1/manifest.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
2+
// or more contributor license agreements. Licensed under the Elastic License;
3+
// you may not use this file except in compliance with the Elastic License.
4+
5+
package v1
6+
7+
import (
8+
"fmt"
9+
"io"
10+
11+
"gopkg.in/yaml.v2"
12+
)
13+
14+
const ManifestKind = "PackageManifest"
15+
16+
type PackageDesc struct {
17+
Version string `yaml:"version,omitempty" json:"version,omitempty"`
18+
Snapshot bool `yaml:"snapshot,omitempty" json:"snapshot,omitempty"`
19+
VersionedHome string `yaml:"versioned-home,omitempty" json:"versionedHome,omitempty"`
20+
PathMappings []map[string]string `yaml:"path-mappings,omitempty" json:"pathMappings,omitempty"`
21+
}
22+
23+
type PackageManifest struct {
24+
apiObject `yaml:",inline"`
25+
Package PackageDesc `yaml:"package" json:"package"`
26+
}
27+
28+
func NewManifest() *PackageManifest {
29+
return &PackageManifest{
30+
apiObject: apiObject{
31+
Version: VERSION,
32+
Kind: ManifestKind,
33+
},
34+
}
35+
}
36+
37+
func ParseManifest(r io.Reader) (*PackageManifest, error) {
38+
m := new(PackageManifest)
39+
err := yaml.NewDecoder(r).Decode(m)
40+
if err != nil {
41+
return nil, fmt.Errorf("decoding package manifest: %w", err)
42+
}
43+
44+
return m, nil
45+
}

pkg/api/v1/manifest_test.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
2+
// or more contributor license agreements. Licensed under the Elastic License;
3+
// you may not use this file except in compliance with the Elastic License.
4+
5+
package v1
6+
7+
import (
8+
"strings"
9+
"testing"
10+
11+
"github.com/stretchr/testify/assert"
12+
)
13+
14+
func TestEmptyManifest(t *testing.T) {
15+
m := NewManifest()
16+
assert.Equal(t, VERSION, m.Version)
17+
assert.Equal(t, ManifestKind, m.Kind)
18+
}
19+
20+
func TestParseManifest(t *testing.T) {
21+
22+
manifest := `
23+
# version and kind to uniquely identify the schema
24+
version: co.elastic.agent/v1
25+
kind: PackageManifest
26+
27+
# description of the package itself
28+
package:
29+
version: 8.12.0
30+
snapshot: false
31+
versioned-home: data/elastic-agent-4f2d39/
32+
# generic path mapping:
33+
# - key is a prefix representing a path relative to the top of the archive
34+
# - value is the substitution to be applied when extracting the files
35+
path-mappings:
36+
- data/elastic-agent-4f2d39/ : data/elastic-agent-8.12.0/
37+
foo: bar
38+
- manifest.yaml : data/elastic-agent-8.12.0/manifest.yaml
39+
`
40+
m, err := ParseManifest(strings.NewReader(manifest))
41+
assert.NoError(t, err)
42+
assert.Equal(t, VERSION, m.Version)
43+
assert.Equal(t, ManifestKind, m.Kind)
44+
45+
assert.Equal(t, m.Package.Version, "8.12.0")
46+
assert.Equal(t, m.Package.Snapshot, false)
47+
assert.Equal(t, m.Package.VersionedHome, "data/elastic-agent-4f2d39/")
48+
assert.Equal(t, m.Package.PathMappings, []map[string]string{{"data/elastic-agent-4f2d39/": "data/elastic-agent-8.12.0/", "foo": "bar"}, {"manifest.yaml": "data/elastic-agent-8.12.0/manifest.yaml"}})
49+
}

0 commit comments

Comments
 (0)