dev-tools/mage/pkgtypes.go (833 lines of code) (raw):

// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one // or more contributor license agreements. Licensed under the Elastic License 2.0; // you may not use this file except in compliance with the Elastic License 2.0. package mage import ( "archive/tar" "archive/zip" "bytes" "compress/gzip" "fmt" "hash/fnv" "io" "io/fs" "log" "math" "os" "path/filepath" "reflect" "regexp" "runtime" "slices" "strconv" "strings" "github.com/magefile/mage/mg" "github.com/magefile/mage/sh" "gopkg.in/yaml.v3" "github.com/elastic/elastic-agent/dev-tools/mage/pkgcommon" "github.com/elastic/elastic-agent/dev-tools/packaging" ) 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}}{{if .Qualifier}}-{{.Qualifier}}{{end}}-{{.Version}}{{if .Snapshot}}-SNAPSHOT{{end}}{{if .OS}}-{{.OS}}{{end}}{{if .Arch}}-{{.Arch}}{{end}}" // defaultRootDir is the default name of the root directory contained inside of zip and // tar.gz packages. // NOTE: This uses .BeatName instead of .Name because we wanted the internal // directory to not include "-oss". defaultRootDir = "{{.BeatName}}{{if .Qualifier}}-{{.Qualifier}}{{end}}-{{.Version}}{{if .Snapshot}}-SNAPSHOT{{end}}{{if .OS}}-{{.OS}}{{end}}{{if .Arch}}-{{.Arch}}{{end}}" componentConfigMode os.FileMode = 0600 rpm = "rpm" deb = "deb" zipExt = "zip" targz = "tar.gz" docker = "docker" invalid = "invalid" ) var ( configFilePattern = regexp.MustCompile(`.*\.yml$|.*\.yml\.disabled$`) componentConfigFilePattern = regexp.MustCompile(`.*beat\.spec\.yml$|.*beat\.yml$|apm-server\.yml$|apm-server\.spec\.yml$|elastic-agent\.yml$`) ) // Alias for pkgcommon.PackageType. This type is moved to the pkgcommon to // resolve circular dependency problems type PackageType pkgcommon.PackageType // List of possible package types. var ( RPM PackageType = PackageType(pkgcommon.RPM) Deb = PackageType(pkgcommon.Deb) Zip = PackageType(pkgcommon.Zip) TarGz = PackageType(pkgcommon.TarGz) Docker = PackageType(pkgcommon.Docker) ) // OSPackageArgs define a set of package types to build for an operating // system using the contained PackageSpec. type OSPackageArgs struct { OS string `yaml:"os"` Arch string `yaml:"arch,omitempty"` 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"` FIPS bool `yaml:"fips"` Version string `yaml:"version,omitempty"` License string `yaml:"license,omitempty"` URL string `yaml:"url,omitempty"` Description string `yaml:"description,omitempty"` DockerVariant DockerVariant `yaml:"docker_variant,omitempty"` PreInstallScript string `yaml:"pre_install_script,omitempty"` PostInstallScript string `yaml:"post_install_script,omitempty"` PostRmScript string `yaml:"post_rm_script,omitempty"` Files map[string]PackageFile `yaml:"files"` Qualifier string `yaml:"qualifier,omitempty"` // Optional OutputFile string `yaml:"output_file,omitempty"` // Optional ExtraVars map[string]string `yaml:"extra_vars,omitempty"` // Optional ExtraTags []string `yaml:"extra_tags,omitempty"` // Optional Components []packaging.BinarySpec `yaml:"components"` // Optional: Components required for this package evalContext map[string]interface{} packageDir string localPreInstallScript string localPostInstallScript string localPostRmScript string } // add new prop into package file called expand spc // expand spec is checked during packaging and expands to multiple files // if expand is not present file is copied normally // 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. ConfigMode os.FileMode `yaml:"config_mode,omitempty"` Config bool `yaml:"config"` // Mark file as config in the package (deb and rpm only). Modules bool `yaml:"modules"` // Mark directory as directory with modules. Dep func(PackageSpec) error `yaml:"-" hash:"-" json:"-"` // Dependency to invoke during Evaluate. Owner string `yaml:"owner,omitempty"` // File Owner, for user and group name (rpm only). SkipOnMissing bool `yaml:"skip_on_missing,omitempty"` // Prevents build failure if the file is missing. Symlink bool `yaml:"symlink"` // Symlink marks file as a symlink pointing from target to source. ExpandSpec bool `yaml:"expand_spec,omitempty"` // Optional } // OSArchNames defines the names of architectures for use in packages. var OSArchNames = map[string]map[PackageType]map[string]string{ "windows": { Zip: { "386": "x86", "amd64": "x86_64", }, }, "darwin": { TarGz: { "386": "x86", "amd64": "x86_64", "arm64": "aarch64", // "universal": "universal", }, }, "linux": { RPM: { "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: { "386": "i386", "amd64": "amd64", "armv5": "armel", "armv6": "armel", "armv7": "armhf", "arm64": "arm64", "mips": "mips", "mipsle": "mipsel", "mips64le": "mips64el", "ppc64le": "ppc64el", "s390x": "s390x", }, TarGz: { "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", }, Docker: { "amd64": "amd64", "arm64": "arm64", }, }, "aix": { TarGz: { "ppc64": "ppc64", }, }, } // 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 "", fmt.Errorf("arch names for os=%v are not defined", platform.GOOS()) } archMap, found := names[t] if !found { return "", fmt.Errorf("arch names for %v on os=%v are not defined", t, platform.GOOS()) } arch, found := archMap[platform.Arch()] if !found { return "", fmt.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 zipExt case TarGz: return targz case Docker: return docker 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 targz, "tgz", "targz": *typ = TarGz case zipExt: *typ = Zip case docker: *typ = Docker default: return fmt.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 } // PackagingDir returns the path that should be used for building and packaging. // The path returned guarantees that packaging operations can run in isolation. func (typ PackageType) PackagingDir(home string, target BuildPlatform, spec PackageSpec) (string, error) { root := home if typ == Docker { root = filepath.Join(root, spec.ImageName()) } targetPath := typ.AddFileExtension(spec.Name + "-" + target.GOOS() + "-" + target.Arch()) return filepath.Join(root, targetPath), nil } // 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) case Docker: return PackageDocker(spec) default: return fmt.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 } clone.ExtraVars = make(map[string]string, len(s.ExtraVars)) for k, v := range s.ExtraVars { clone.ExtraVars[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(fmt.Errorf("failed to ReplaceFile because target=%v does not exist", target)) } s.Files[target] = file } // ExtraVar adds or replaces a variable to `extra_vars` in package specs. func (s *PackageSpec) ExtraVar(key, value string) { if s.ExtraVars == nil { s.ExtraVars = make(map[string]string) } s.ExtraVars[key] = value } // 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{}) { if err := s.ExpandFile(src, dst, args...); err != nil { panic(err) } } // 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...) } if s.evalContext == nil { s.evalContext = map[string]interface{}{} } for k, v := range s.ExtraVars { s.evalContext[k] = mustExpand(v) } if s.ExtraTags != nil { for i, tag := range s.ExtraTags { s.ExtraTags[i] = mustExpand(tag) } } 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.PreInstallScript = mustExpand(s.PreInstallScript) s.PostInstallScript = mustExpand(s.PostInstallScript) s.PostRmScript = mustExpand(s.PostRmScript) 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)) } s.evalContext["PackageDir"] = s.packageDir s.evalContext["fips"] = s.FIPS 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(fmt.Errorf("failed executing package file dependency for target=%v: %w", target, err)) } } 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(fmt.Errorf("failed to expand content template for target=%v: %w", target, err)) } f.Source = filepath.Join(s.packageDir, filepath.Base(f.Target)) if err = os.WriteFile(CreateDir(f.Source), []byte(content), 0644); err != nil { panic(fmt.Errorf("failed to write file containing content for target=%v: %w", target, err)) } case f.Template != "": f.Source = filepath.Join(s.packageDir, filepath.Base(f.Template)) if err := s.ExpandFile(f.Template, CreateDir(f.Source)); err != nil { panic(fmt.Errorf("failed to expand template file for target=%v: %w", target, err)) } default: panic(fmt.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 err := copyInstallScript(s, s.PreInstallScript, &s.localPreInstallScript); err != nil { panic(err) } if err := copyInstallScript(s, s.PostInstallScript, &s.localPostInstallScript); err != nil { panic(err) } if err := copyInstallScript(s, s.PostRmScript, &s.localPostRmScript); err != nil { panic(err) } return s } // ImageName computes the image name from the spec. func (s PackageSpec) ImageName() string { if s.DockerVariant == Basic { return s.Name } if s.DockerVariant == EdotCollector || s.DockerVariant == EdotCollectorWolfi { // no suffix for basic docker variant return s.Name } return fmt.Sprintf("%s-%s", s.Name, s.DockerVariant) } func copyInstallScript(spec PackageSpec, script string, local *string) error { if script == "" { return nil } *local = filepath.Join(spec.packageDir, "scripts", filepath.Base(script)) if filepath.Ext(*local) == ".tmpl" { *local = strings.TrimSuffix(*local, ".tmpl") } if strings.HasSuffix(*local, "."+spec.Name) { *local = strings.TrimSuffix(*local, "."+spec.Name) } if err := spec.ExpandFile(script, createDir(*local)); err != nil { return fmt.Errorf("failed to copy install script to package dir: %w", err) } if err := os.Chmod(*local, 0755); err != nil { return fmt.Errorf("failed to chmod install script: %w", err) } return nil } func (s PackageSpec) hash() string { out, err := yaml.Marshal(s) if err != nil { panic(fmt.Errorf("failed to marshal spec: %w", err)) } h := fnv.New64() h.Write(out) hash := strconv.FormatUint(h.Sum64(), 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) } return s.MustExpand(defaultRootDir) } // 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 pkgFile.Symlink { // not supported on zip archives continue } if err := addFileToZip(w, baseDir, pkgFile); err != nil { p, _ := filepath.Abs(pkgFile.Source) return fmt.Errorf("failed adding file=%+v to zip: %w", p, err) } } 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 := os.WriteFile(CreateDir(spec.OutputFile), buf.Bytes(), 0644); err != nil { return fmt.Errorf("failed to write zip file: %w", 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 } if err := CreateSHA512File(spec.OutputFile); err != nil { return fmt.Errorf("failed to create .sha512 file: %w", err) } return nil } // 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() // // Replace the darwin-universal by darwin-x86_64 and darwin-arm64. Also // // keep the other files. // if spec.Name == "elastic-agent" && spec.OS == "darwin" && spec.Arch == "universal" { // newFiles := map[string]PackageFile{} // for filename, pkgFile := range spec.Files { // if strings.Contains(pkgFile.Target, "darwin-universal") && // strings.Contains(pkgFile.Target, "downloads") { // // amdFilename, amdpkgFile := replaceFileArch(filename, pkgFile, "x86_64") // armFilename, armpkgFile := replaceFileArch(filename, pkgFile, "aarch64") // // newFiles[amdFilename] = amdpkgFile // newFiles[armFilename] = armpkgFile // } else { // newFiles[filename] = pkgFile // } // } // // spec.Files = newFiles // } // Add files to tar. for _, pkgFile := range spec.Files { if pkgFile.Symlink { continue } if err := addFileToTar(w, baseDir, pkgFile); err != nil { return fmt.Errorf("failed adding file=%+v to tar: %w", pkgFile, err) } } // same for symlinks so they can point to files in tar for _, pkgFile := range spec.Files { if !pkgFile.Symlink { continue } tmpdir, err := os.MkdirTemp("", "TmpSymlinkDropPath") if err != nil { return err } defer os.RemoveAll(tmpdir) if err := addSymlinkToTar(tmpdir, w, baseDir, pkgFile); err != nil { return fmt.Errorf("failed adding file=%+v to tar: %w", pkgFile, err) } } 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 } if err := CreateSHA512File(spec.OutputFile); err != nil { return fmt.Errorf("failed to create .sha512 file: %w", err) } return nil } // 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 fmt.Errorf("unsupported package type=%v for runFPM", fpmPackageType) } if err := HaveDocker(); err != nil { return fmt.Errorf("packaging %v files requires docker: %w", 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 packageType == RPM { args = append(args, "--rpm-rpmbuild-define", "_build_id_links none", "--rpm-digest", "sha256", ) } 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.localPreInstallScript != "" { args = append(args, "--before-install", spec.localPreInstallScript) } if spec.localPostInstallScript != "" { args = append(args, "--after-install", spec.localPostInstallScript) } if spec.localPostRmScript != "" { args = append(args, "--after-remove", spec.localPostRmScript) } for _, pf := range spec.Files { if pf.Config { args = append(args, "--config-files", pf.Target) } if pf.Owner != "" { args = append(args, "--rpm-attr", fmt.Sprintf("%04o,%s,%s:%s", pf.Mode, pf.Owner, pf.Owner, pf.Target)) } } args = append(args, "-p", spec.OutputFile, inputTar, ) if err = dockerRun(args...); err != nil { return fmt.Errorf("failed while running FPM in docker: %w", err) } if err := CreateSHA512File(spec.OutputFile); err != nil { return fmt.Errorf("failed to create .sha512 file: %w", err) } return nil } func addUIDGidEnvArgs(args []string) ([]string, error) { if runtime.GOOS == "windows" { return args, nil } info, err := GetDockerInfo() if err != nil { return args, fmt.Errorf("failed to get docker info: %w", err) } 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, "-e", "EXEC_UID="+strconv.Itoa(uid), "-e", "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 fs.FileInfo, err error) error { if err != nil { if pkgFile.SkipOnMissing && os.IsNotExist(err) { return nil } return err } header, err := zip.FileInfoHeader(info) if err != nil { return err } switch { case componentConfigFilePattern.MatchString(info.Name()): header.SetMode(componentConfigMode & os.ModePerm) case pkgFile.ConfigMode > 0 && configFilePattern.MatchString(info.Name()): header.SetMode(pkgFile.ConfigMode & os.ModePerm) case info.Mode().IsRegular() && pkgFile.Mode > 0: header.SetMode(pkgFile.Mode & os.ModePerm) case 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 { excludedFiles := []string{} return filepath.WalkDir(pkgFile.Source, func(path string, d fs.DirEntry, err error) error { if err != nil { if pkgFile.SkipOnMissing && os.IsNotExist(err) { return nil } return err } if slices.Contains(excludedFiles, d.Name()) { // it's a file we have to exclude if mg.Verbose() { log.Printf("Skipping file %q...", path) } return nil } info, err := d.Info() 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 switch { case componentConfigFilePattern.MatchString(info.Name()): header.Mode = int64(componentConfigMode & os.ModePerm) case pkgFile.ConfigMode > 0 && configFilePattern.MatchString(info.Name()): header.Mode = int64(pkgFile.ConfigMode & os.ModePerm) case info.Mode().IsRegular() && pkgFile.Mode > 0: header.Mode = int64(pkgFile.Mode & os.ModePerm) case 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(mustConvertToUnit32(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() }) } // addSymlinkToTar adds a symlink file to a tar archive. func addSymlinkToTar(tmpdir string, ar *tar.Writer, baseDir string, pkgFile PackageFile) error { // create symlink we can work with later, header will be updated later link := filepath.Join(tmpdir, "link") target := tmpdir if err := os.Symlink(target, link); err != nil { return err } return filepath.Walk(link, func(path string, info fs.FileInfo, err error) error { if err != nil { if pkgFile.SkipOnMissing && os.IsNotExist(err) { return 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 switch { case componentConfigFilePattern.MatchString(info.Name()): header.Mode = int64(componentConfigMode & os.ModePerm) case pkgFile.ConfigMode > 0 && configFilePattern.MatchString(info.Name()): header.Mode = int64(pkgFile.ConfigMode & os.ModePerm) case info.Mode().IsRegular() && pkgFile.Mode > 0: header.Mode = int64(pkgFile.Mode & os.ModePerm) case info.IsDir(): header.Mode = int64(0755) } header.Name = filepath.Join(baseDir, pkgFile.Target) if filepath.IsAbs(pkgFile.Target) { header.Name = pkgFile.Target } header.Linkname = pkgFile.Source header.Typeflag = tar.TypeSymlink if mg.Verbose() { log.Println("Adding", os.FileMode(mustConvertToUnit32(header.Mode)), header.Name) } if err := ar.WriteHeader(header); err != nil { return err } return nil }) } // PackageDocker packages the Beat into a docker image. func PackageDocker(spec PackageSpec) error { if err := HaveDocker(); err != nil { return fmt.Errorf("docker daemon required to build images: %w", err) } b, err := newDockerBuilder(spec) if err != nil { return err } return b.Build() } func mustConvertToUnit32(i int64) uint32 { if i > math.MaxUint32 { panic(fmt.Sprintf("%d is bigger than math.MaxUint32", i)) } return uint32(i) // #nosec }