grafana/pkg/build/packaging/grafana.go
2023-08-30 18:46:47 +03:00

1134 lines
32 KiB
Go

package packaging
import (
"archive/tar"
"archive/zip"
"compress/gzip"
"context"
"crypto/sha256"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"github.com/grafana/grafana/pkg/build/config"
"github.com/grafana/grafana/pkg/build/errutil"
"github.com/grafana/grafana/pkg/build/grafana"
"github.com/grafana/grafana/pkg/build/plugins"
"github.com/grafana/grafana/pkg/build/syncutil"
"github.com/grafana/grafana/pkg/infra/fs"
)
var (
ErrorNoBinaries = errors.New("no binaries found")
ErrorNoDebArch = errors.New("deb architecture not defined")
ErrorNoRPMArch = errors.New("rpm architecture not defined")
)
const (
maxAttempts = 3
enterpriseSfx = "-enterprise"
enterprise2Sfx = "-enterprise2"
DefaultDebDBBucket = "grafana-aptly-db"
DefaultDebRepoBucket = "grafana-repo"
DefaultRPMRepoBucket = "grafana-repo"
DefaultTTLSeconds = "300"
)
// PackageRegexp returns a regexp for matching packages corresponding to a certain Grafana edition.
func PackageRegexp(edition config.Edition) *regexp.Regexp {
var sfx string
switch edition {
case config.EditionOSS:
case config.EditionEnterprise:
sfx = "-enterprise"
case config.EditionEnterprise2:
sfx = "-enterprise2"
default:
panic(fmt.Sprintf("unrecognized edition %q", edition))
}
rePkg, err := regexp.Compile(fmt.Sprintf(`^grafana%s(?:-rpi)?[-_][^-_]+.*$`, sfx))
if err != nil {
panic(fmt.Sprintf("Failed to compile regexp: %s", err))
}
return rePkg
}
// PackageGrafana packages Grafana for various variants.
func PackageGrafana(
ctx context.Context,
version string,
grafanaDir string,
cfg config.Config,
edition config.Edition,
variants []config.Variant,
shouldSign bool,
p syncutil.WorkerPool,
) error {
if err := packageGrafana(ctx, edition, version, grafanaDir, variants, shouldSign, p); err != nil {
return err
}
if cfg.SignPackages {
if err := signRPMPackages(edition, cfg, grafanaDir); err != nil {
return err
}
}
if err := checksumPackages(grafanaDir, edition); err != nil {
return err
}
return nil
}
func packageGrafana(
ctx context.Context,
edition config.Edition,
version string,
grafanaDir string,
variants []config.Variant,
shouldSign bool,
p syncutil.WorkerPool,
) error {
distDir := filepath.Join(grafanaDir, "dist")
exists, err := fs.Exists(distDir)
if err != nil {
return err
}
if !exists {
log.Printf("directory %s doesn't exist - creating...", distDir)
//nolint
if err := os.MkdirAll(distDir, 0o755); err != nil {
return fmt.Errorf("couldn't create dist: %w", err)
}
}
var m pluginsManifest
manifestPath := filepath.Join(grafanaDir, "plugins-bundled", "external.json")
//nolint:gosec
manifestB, err := os.ReadFile(manifestPath)
if err != nil {
return fmt.Errorf("failed to open plugins manifest %q: %w", manifestPath, err)
}
if err := json.Unmarshal(manifestB, &m); err != nil {
return err
}
g, ctx := errutil.GroupWithContext(ctx)
for _, v := range variants {
packageVariant(ctx, v, edition, version, grafanaDir, shouldSign, g, p, m)
}
if err := g.Wait(); err != nil {
return err
}
return nil
}
// packageVariant packages Grafana for a certain variant.
func packageVariant(
ctx context.Context,
v config.Variant,
edition config.Edition,
version string,
grafanaDir string,
shouldSign bool,
g *errutil.Group,
p syncutil.WorkerPool,
m pluginsManifest,
) {
p.Schedule(g.Wrap(func() error {
// We've experienced spurious packaging failures, so retry on failure.
i := 0
for {
if err := realPackageVariant(ctx, v, edition, version, grafanaDir, m, shouldSign); err != nil {
i++
if i < maxAttempts {
log.Printf("Packaging for variant %s, %s edition failed: %s, trying again", v, edition, err)
continue
}
return err
}
break
}
return nil
}))
}
// signRPMPackages signs the RPM packages.
func signRPMPackages(edition config.Edition, cfg config.Config, grafanaDir string) error {
log.Printf("Signing %s RPM packages...", edition)
var sfx string
switch edition {
case config.EditionOSS:
case config.EditionEnterprise:
sfx = enterpriseSfx
case config.EditionEnterprise2:
sfx = enterprise2Sfx
default:
panic(fmt.Sprintf("Unrecognized edition %s", edition))
}
rpms, err := filepath.Glob(filepath.Join(grafanaDir, "dist", fmt.Sprintf("grafana%s-*.rpm", sfx)))
if err != nil {
return err
}
if len(rpms) > 0 {
rpmArgs := append([]string{"--addsign"}, rpms...)
log.Printf("Invoking rpm with args: %+v", rpmArgs)
//nolint:gosec
cmd := exec.Command("rpm", rpmArgs...)
if output, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("failed to sign RPM packages: %s", output)
}
if err := os.Remove(cfg.GPGPassPath); err != nil {
return fmt.Errorf("failed to remove %q: %w", cfg.GPGPassPath, err)
}
log.Printf("Verifying %s RPM packages...", edition)
// The output changed between rpm versions
reOutput := regexp.MustCompile("(?:digests signatures OK)|(?:pgp.+OK)")
for _, p := range rpms {
//nolint:gosec
cmd := exec.Command("rpm", "-K", p)
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("failed to verify RPM signature: %w", err)
}
if !reOutput.Match(output) {
return fmt.Errorf("RPM package %q not verified: %s", p, output)
}
}
}
return nil
}
// checksumPackages generates package checksums with SHA-256.
func checksumPackages(grafanaDir string, edition config.Edition) error {
log.Printf("Checksumming %s packages...", edition)
distDir := filepath.Join(grafanaDir, "dist")
rePkg := PackageRegexp(edition)
if err := filepath.Walk(distDir, func(fpath string, info os.FileInfo, err error) error {
if err != nil {
var pathErr *os.PathError
if errors.As(err, &pathErr) {
log.Printf("path error in walk function for file %q: %s", pathErr.Path, pathErr.Err.Error())
return nil
}
return fmt.Errorf("walking through dist folder failed: %w", err)
}
if info.IsDir() {
return nil
}
fname := filepath.Base(fpath)
if strings.HasSuffix(fname, ".sha256") || strings.HasSuffix(fname, ".version") || !rePkg.MatchString(fname) {
log.Printf("Ignoring non-package %q", fpath)
return nil
}
return shaFile(fpath)
}); err != nil {
return fmt.Errorf("checksumming packages in %q failed: %w", distDir, err)
}
log.Printf("Successfully checksummed %s packages", edition)
return nil
}
func shaFile(fpath string) error {
//nolint:gosec
fd, err := os.Open(fpath)
if err != nil {
return fmt.Errorf("failed to open %q: %w", fpath, err)
}
defer func() {
if err := fd.Close(); err != nil {
log.Println(err)
}
}()
h := sha256.New()
_, err = io.Copy(h, fd)
if err != nil {
return fmt.Errorf("failed to read %q: %w", fpath, err)
}
//nolint:gosec
out, err := os.Create(fpath + ".sha256")
if err != nil {
return fmt.Errorf("failed to create %q: %w", fpath+".sha256", err)
}
defer func() {
if err := out.Close(); err != nil {
log.Println("failed to close file", out.Name())
}
}()
if _, err = fmt.Fprintf(out, "%x\n", h.Sum(nil)); err != nil {
return fmt.Errorf("failed to write %q: %w", out.Name(), err)
}
return nil
}
// createPackage creates a Linux package.
func createPackage(srcDir string, options linuxPackageOptions) error {
binary := "grafana"
cliBinary := "grafana-cli"
serverBinary := "grafana-server"
packageRoot, err := os.MkdirTemp("", "grafana-linux-pack")
if err != nil {
return fmt.Errorf("failed to create temporary directory: %w", err)
}
defer func() {
if err := os.RemoveAll(packageRoot); err != nil {
log.Println(err)
}
}()
for _, dname := range []string{
options.homeDir,
options.configDir,
"etc/init.d",
options.etcDefaultPath,
"usr/lib/systemd/system",
"usr/sbin",
} {
dpath := filepath.Join(packageRoot, dname)
//nolint
if err := os.MkdirAll(dpath, 0o755); err != nil {
return fmt.Errorf("failed to make directory %q: %w", dpath, err)
}
}
if err := fs.CopyFile(filepath.Join(options.wrapperFilePath, binary),
filepath.Join(packageRoot, "usr", "sbin", binary)); err != nil {
return err
}
if err := fs.CopyFile(filepath.Join(options.wrapperFilePath, cliBinary),
filepath.Join(packageRoot, "usr", "sbin", cliBinary)); err != nil {
return err
}
if err := fs.CopyFile(filepath.Join(options.wrapperFilePath, serverBinary),
filepath.Join(packageRoot, "usr", "sbin", serverBinary)); err != nil {
return err
}
if err := fs.CopyFile(options.initdScriptSrc, filepath.Join(packageRoot, options.initdScriptFilePath)); err != nil {
return err
}
if err := fs.CopyFile(options.defaultFileSrc, filepath.Join(packageRoot, options.etcDefaultFilePath)); err != nil {
return err
}
if err := fs.CopyFile(options.systemdFileSrc, filepath.Join(packageRoot, options.systemdServiceFilePath)); err != nil {
return err
}
if err := fs.CopyRecursive(srcDir, filepath.Join(packageRoot, options.homeDir)); err != nil {
return err
}
if err := executeFPM(options, packageRoot, srcDir); err != nil {
return err
}
return nil
}
func executeFPM(options linuxPackageOptions, packageRoot, srcDir string) error {
name := "grafana"
vendor := "Grafana"
if options.edition == config.EditionEnterprise || options.edition == config.EditionEnterprise2 {
vendor += " Enterprise"
if options.edition == config.EditionEnterprise2 {
name += enterprise2Sfx
} else if options.edition == config.EditionEnterprise {
name += enterpriseSfx
}
}
if options.goArch == config.ArchARM && options.goArm == "6" {
name += "-rpi"
}
pkgVersion := packageVersion(options)
args := []string{
"-s", "dir",
"--description", "Grafana",
"-C", packageRoot,
"--url", "https://grafana.com",
"--maintainer", "contact@grafana.com",
"--config-files", options.initdScriptFilePath,
"--config-files", options.etcDefaultFilePath,
"--config-files", options.systemdServiceFilePath,
"--after-install", options.postinstSrc,
"--version", pkgVersion,
"-p", "dist/",
"--name", name,
"--vendor", vendor,
"-a", string(options.packageArch),
}
if options.prermSrc != "" {
args = append(args, "--before-remove", options.prermSrc)
}
if options.edition == config.EditionEnterprise || options.edition == config.EditionEnterprise2 || options.goArch == config.ArchARMv6 {
args = append(args, "--conflicts", "grafana")
}
if options.edition == config.EditionOSS {
args = append(args, "--license", "\"AGPLv3\"")
}
switch options.packageType {
case packageTypeRpm:
args = append(args, "-t", "rpm", "--rpm-posttrans", "packaging/rpm/control/posttrans")
args = append(args, "--rpm-digest", "sha256")
case packageTypeDeb:
args = append(args, "-t", "deb", "--deb-no-default-config-files")
default:
panic(fmt.Sprintf("Unrecognized package type %d", options.packageType))
}
for _, dep := range options.depends {
args = append(args, "--depends", dep)
}
args = append(args, ".")
distDir := filepath.Join(options.grafanaDir, "dist")
log.Printf("Generating package in %q (source directory %q)", distDir, srcDir)
cmdStr := "fpm"
for _, arg := range args {
if strings.Contains(arg, " ") {
arg = fmt.Sprintf("'%s'", arg)
}
cmdStr += fmt.Sprintf(" %s", arg)
}
log.Printf("Creating %s package: %s...", options.packageType, cmdStr)
const rvmPath = "/etc/profile.d/rvm.sh"
exists, err := fs.Exists(rvmPath)
if err != nil {
return err
}
if exists {
cmdStr = fmt.Sprintf("source %q && %s", rvmPath, cmdStr)
log.Printf("Sourcing %q before running fpm", rvmPath)
}
//nolint:gosec
cmd := exec.Command("/bin/bash", "-c", cmdStr)
cmd.Dir = options.grafanaDir
if output, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("failed to run fpm: %s", output)
}
return nil
}
// copyPubDir copies public/ from grafanaDir to tmpDir.
func copyPubDir(grafanaDir, tmpDir string) error {
srcPubDir := filepath.Join(grafanaDir, "public")
tgtPubDir := filepath.Join(tmpDir, "public")
log.Printf("Copying %q to %q...", srcPubDir, tgtPubDir)
if err := fs.CopyRecursive(srcPubDir, tgtPubDir); err != nil {
return fmt.Errorf("failed to copy %q to %q: %w", srcPubDir, tgtPubDir, err)
}
return nil
}
// copyBinaries copies binaries from grafanaDir into tmpDir.
func copyBinaries(grafanaDir, tmpDir string, args grafana.BuildArgs, edition config.Edition) error {
tgtDir := filepath.Join(tmpDir, "bin")
//nolint
if err := os.MkdirAll(tgtDir, 0o755); err != nil {
return fmt.Errorf("failed to make directory %q: %w", tgtDir, err)
}
binDir := filepath.Join(grafanaDir, "bin", grafana.BinaryFolder(edition, args))
files, err := os.ReadDir(binDir)
if err != nil {
return fmt.Errorf("failed to list files in %q: %w", binDir, err)
}
if len(files) == 0 {
return fmt.Errorf("%w in %s", ErrorNoBinaries, binDir)
}
for _, file := range files {
srcPath := filepath.Join(binDir, file.Name())
tgtPath := filepath.Join(tgtDir, file.Name())
if err := fs.CopyFile(srcPath, tgtPath); err != nil {
return err
}
}
return nil
}
// copyConfFiles copies configuration files from grafanaDir into tmpDir.
func copyConfFiles(grafanaDir, tmpDir string) error {
//nolint:gosec
if err := os.MkdirAll(filepath.Join(tmpDir, "conf"), 0o755); err != nil {
return fmt.Errorf("failed to create dir %q: %w", filepath.Join(tmpDir, "conf"), err)
}
confDir := filepath.Join(grafanaDir, "conf")
infos, err := os.ReadDir(confDir)
if err != nil {
return fmt.Errorf("failed to list files in %q: %w", confDir, err)
}
for _, info := range infos {
fpath := filepath.Join(confDir, info.Name())
if err := fs.CopyRecursive(fpath, filepath.Join(tmpDir, "conf", info.Name())); err != nil {
return err
}
}
return nil
}
// copyPlugins copies plugins from grafanaDir into tmpDir.
func copyPlugins(ctx context.Context, v config.Variant, grafanaDir, tmpDir string, m pluginsManifest, shouldSign bool) error {
log.Printf("Copying plugins for package variant %s...", v)
variant2Sfx := map[config.Variant]string{
config.VariantLinuxAmd64: "linux_amd64",
config.VariantDarwinAmd64: "darwin_amd64",
config.VariantWindowsAmd64: "windows_amd64.exe",
}
tgtDir := filepath.Join(tmpDir, "plugins-bundled")
exists, err := fs.Exists(tgtDir)
if err != nil {
return err
}
if !exists {
//nolint:gosec
if err := os.MkdirAll(tgtDir, 0o755); err != nil {
return err
}
}
pluginsDir := filepath.Join(grafanaDir, "plugins-bundled")
// External plugins.
for _, pm := range m.Plugins {
srcDir := filepath.Join(pluginsDir, fmt.Sprintf("%s-%s", pm.Name, pm.Version))
dstDir := filepath.Join(tgtDir, fmt.Sprintf("%s-%s", pm.Name, pm.Version))
log.Printf("Copying external plugin %q to %q...", srcDir, dstDir)
//nolint:gosec
jsonB, err := os.ReadFile(filepath.Join(srcDir, "plugin.json"))
if err != nil {
return fmt.Errorf("failed to read %q: %w", filepath.Join(srcDir, "plugin.json"), err)
}
var plugJSON map[string]any
if err := json.Unmarshal(jsonB, &plugJSON); err != nil {
return err
}
plugExe, ok := plugJSON["executable"].(string)
var wantExe string
if ok && strings.TrimSpace(plugExe) != "" {
sfx := variant2Sfx[v]
if sfx == "" {
log.Printf("External plugin %s-%s doesn't have an executable for variant %s - ignoring",
pm.Name, pm.Version, v)
continue
}
wantExe = fmt.Sprintf("%s_%s", plugExe, sfx)
log.Printf("The external plugin should contain an executable %q", wantExe)
exists, err := fs.Exists(filepath.Join(srcDir, wantExe))
if err != nil {
return err
}
if !exists {
log.Printf("External plugin %s-%s doesn't have an executable of the right format: %q - ignoring",
pm.Name, pm.Version, wantExe)
continue
}
}
if err := filepath.Walk(srcDir, func(pth string, info os.FileInfo, err error) error {
if err != nil {
return err
}
log.Printf("Handling %q", pth)
relPath := strings.TrimPrefix(pth, srcDir)
relPath = strings.TrimPrefix(relPath, "/")
dstPath := filepath.Join(dstDir, relPath)
if info.IsDir() {
log.Printf("Making directory %q", dstPath)
//nolint:gosec
return os.MkdirAll(dstPath, info.Mode())
}
if wantExe != "" {
m, err := regexp.MatchString(fmt.Sprintf(`^%s_[^/]+$`, plugExe), relPath)
if err != nil {
return err
}
if m && relPath != wantExe {
// Ignore other executable variants
log.Printf("Ignoring executable variant %q", pth)
return nil
}
}
log.Printf("Copying %q to %q", pth, dstPath)
return fs.CopyFile(pth, dstPath)
}); err != nil {
return fmt.Errorf("failed to copy external plugin %q to %q: %w", srcDir, dstDir, err)
}
if shouldSign {
if err := plugins.BuildManifest(ctx, dstDir, true); err != nil {
return fmt.Errorf("failed to generate signed manifest for external plugin %q: %w", dstDir, err)
}
}
}
return copyInternalPlugins(pluginsDir, tmpDir)
}
func copyInternalPlugins(pluginsDir, tmpDir string) error {
tgtDir := filepath.Join(tmpDir, "plugins-bundled", "internal")
srcDir := filepath.Join(pluginsDir, "dist")
exists, err := fs.Exists(tgtDir)
if err != nil {
return err
}
if !exists {
//nolint:gosec
if err := os.MkdirAll(tgtDir, 0o755); err != nil {
return err
}
}
// Copy over internal plugins.
fis, err := os.ReadDir(srcDir)
if err != nil {
return fmt.Errorf("failed to list internal plugins in %q: %w", srcDir, err)
}
for _, fi := range fis {
srcPath := filepath.Join(srcDir, fi.Name())
if !fi.IsDir() {
log.Printf("Ignoring non-directory %q", srcPath)
continue
}
dstPath := filepath.Join(tgtDir, fi.Name())
log.Printf("Copying internal plugin %q to %q...", srcPath, dstPath)
if err := fs.CopyRecursive(srcPath, dstPath); err != nil {
return fmt.Errorf("failed to copy %q to %q: %w", srcPath, dstPath, err)
}
}
return nil
}
func realPackageVariant(ctx context.Context, v config.Variant, edition config.Edition, version, grafanaDir string,
m pluginsManifest, shouldSign bool) error {
log.Printf("Packaging Grafana %s for %s...", edition, v)
enableDeb := false
enableRpm := false
switch v {
case config.VariantLinuxAmd64:
enableDeb = true
enableRpm = true
case config.VariantArmV6:
enableDeb = true
case config.VariantArmV7:
enableDeb = true
enableRpm = true
case config.VariantArm64:
enableDeb = true
enableRpm = true
default:
}
tmpDir, err := os.MkdirTemp("", "")
if err != nil {
return fmt.Errorf("failed to create temporary directory: %w", err)
}
defer func() {
if err := os.RemoveAll(tmpDir); err != nil {
log.Println(err)
}
}()
args := grafana.VariantBuildArgs(v)
if err := copyPubDir(grafanaDir, tmpDir); err != nil {
return err
}
if err := copyBinaries(grafanaDir, tmpDir, args, edition); err != nil {
return err
}
if err := copyConfFiles(grafanaDir, tmpDir); err != nil {
return err
}
if err := copyPlugins(ctx, v, grafanaDir, tmpDir, m, shouldSign); err != nil {
return err
}
if v == config.VariantWindowsAmd64 {
toolsDir := filepath.Join(tmpDir, "tools")
//nolint:gosec
if err := os.MkdirAll(toolsDir, 0o755); err != nil {
return fmt.Errorf("failed to create tools dir %q: %w", toolsDir, err)
}
if err := fs.CopyFile("/usr/local/go/lib/time/zoneinfo.zip",
filepath.Join(tmpDir, "tools", "zoneinfo.zip")); err != nil {
return err
}
}
if err := os.WriteFile(filepath.Join(tmpDir, "VERSION"), []byte(version), 0664); err != nil {
return fmt.Errorf("failed to write %s/VERSION: %w", tmpDir, err)
}
if err := createArchive(tmpDir, edition, v, version, grafanaDir); err != nil {
return err
}
if enableDeb {
if args.DebArch == "" {
return fmt.Errorf("%w for %s", ErrorNoDebArch, v)
}
if err := createPackage(tmpDir, linuxPackageOptions{
edition: edition,
version: version,
grafanaDir: grafanaDir,
goArch: args.GoArch,
goArm: args.GoArm,
packageType: packageTypeDeb,
packageArch: args.DebArch,
homeDir: "/usr/share/grafana",
homeBinDir: "/usr/share/grafana/bin",
binPath: "/usr/sbin",
configDir: "/etc/grafana",
etcDefaultPath: "/etc/default",
etcDefaultFilePath: "/etc/default/grafana-server",
initdScriptFilePath: "/etc/init.d/grafana-server",
systemdServiceFilePath: "/usr/lib/systemd/system/grafana-server.service",
postinstSrc: filepath.Join(grafanaDir, "packaging", "deb", "control", "postinst"),
prermSrc: filepath.Join(grafanaDir, "packaging", "deb", "control", "prerm"),
initdScriptSrc: filepath.Join(grafanaDir, "packaging", "deb", "init.d", "grafana-server"),
defaultFileSrc: filepath.Join(grafanaDir, "packaging", "deb", "default", "grafana-server"),
systemdFileSrc: filepath.Join(grafanaDir, "packaging", "deb", "systemd", "grafana-server.service"),
wrapperFilePath: filepath.Join(grafanaDir, "packaging", "wrappers"),
depends: []string{"adduser", "libfontconfig1"},
}); err != nil {
return err
}
}
if !enableRpm {
return nil
}
if args.RPMArch == "" {
return fmt.Errorf("%w for %s", ErrorNoRPMArch, v)
}
if err := createPackage(tmpDir, linuxPackageOptions{
edition: edition,
version: version,
grafanaDir: grafanaDir,
goArch: args.GoArch,
packageType: packageTypeRpm,
packageArch: args.RPMArch,
homeDir: "/usr/share/grafana",
homeBinDir: "/usr/share/grafana/bin",
binPath: "/usr/sbin",
configDir: "/etc/grafana",
etcDefaultPath: "/etc/sysconfig",
etcDefaultFilePath: "/etc/sysconfig/grafana-server",
initdScriptFilePath: "/etc/init.d/grafana-server",
systemdServiceFilePath: "/usr/lib/systemd/system/grafana-server.service",
postinstSrc: filepath.Join(grafanaDir, "packaging", "rpm", "control", "postinst"),
initdScriptSrc: filepath.Join(grafanaDir, "packaging", "rpm", "init.d", "grafana-server"),
defaultFileSrc: filepath.Join(grafanaDir, "packaging", "rpm", "sysconfig", "grafana-server"),
systemdFileSrc: filepath.Join(grafanaDir, "packaging", "rpm", "systemd", "grafana-server.service"),
wrapperFilePath: filepath.Join(grafanaDir, "packaging", "wrappers"),
depends: []string{"/sbin/service", "fontconfig", "freetype", "urw-fonts"},
}); err != nil {
return err
}
return nil
}
// pluginManifest has details of an external plugin package.
type pluginManifest struct {
Name string `json:"name"`
Version string `json:"version"`
Checksum string `json:"checksum"`
}
// pluginsManifest represents a manifest of Grafana's external plugins.
type pluginsManifest struct {
Plugins []pluginManifest `json:"plugins"`
}
// packageVersion converts a Grafana version into the corresponding package version.
func packageVersion(options linuxPackageOptions) string {
verComponents := strings.Split(options.version, "-")
if len(verComponents) > 2 {
panic(fmt.Sprintf("Version string contains more than one hyphen: %q", options.version))
}
switch options.packageType {
case packageTypeDeb, packageTypeRpm:
if len(verComponents) > 1 {
// With Debian and RPM packages, it's customary to prefix any pre-release component with a ~, since this
// is considered of lower lexical value than the empty character, and this way pre-release versions are
// considered to be of a lower version than the final version (which lacks this suffix).
return fmt.Sprintf("%s~%s", verComponents[0], verComponents[1])
}
return options.version
default:
panic(fmt.Sprintf("Unrecognized package type %s", options.packageType))
}
}
type packageType int
func (pt packageType) String() string {
switch pt {
case packageTypeDeb:
return "Debian"
case packageTypeRpm:
return "RPM"
default:
panic(fmt.Sprintf("Unrecognized package type %d", pt))
}
}
const (
packageTypeDeb packageType = iota
packageTypeRpm
)
type linuxPackageOptions struct {
edition config.Edition
packageType packageType
version string
grafanaDir string
goArch config.Architecture
goArm string
packageArch config.Architecture
homeDir string
homeBinDir string
binPath string
configDir string
etcDefaultPath string
etcDefaultFilePath string
initdScriptFilePath string
systemdServiceFilePath string
postinstSrc string
prermSrc string
initdScriptSrc string
defaultFileSrc string
systemdFileSrc string
wrapperFilePath string
depends []string
}
// createArchive makes a distribution archive.
func createArchive(srcDir string, edition config.Edition, v config.Variant, version, grafanaDir string) error {
distDir := filepath.Join(grafanaDir, "dist")
exists, err := fs.Exists(distDir)
if err != nil {
return err
}
if !exists {
log.Printf("directory %s doesn't exist - creating...", distDir)
//nolint:gosec
if err := os.MkdirAll(distDir, 0o755); err != nil {
return fmt.Errorf("couldn't create dist: %w", err)
}
}
sfx := ""
if edition == config.EditionEnterprise2 {
sfx = enterprise2Sfx
} else if edition == config.EditionEnterprise {
sfx = enterpriseSfx
}
if v != config.VariantWindowsAmd64 {
return createTarball(srcDir, version, string(v), sfx, grafanaDir)
}
return createZip(srcDir, version, string(v), sfx, grafanaDir)
}
func createZip(srcDir, version, variantStr, sfx, grafanaDir string) error {
fpath := filepath.Join(grafanaDir, "dist", fmt.Sprintf("grafana%s-%s.%s.zip", sfx, version, variantStr))
//nolint:gosec
tgt, err := os.Create(fpath)
if err != nil {
return fmt.Errorf("failed to create %q: %w", fpath, err)
}
defer func() {
if err := tgt.Close(); err != nil && !errors.Is(err, os.ErrClosed) {
log.Println(err)
}
}()
//nolint:gosec
if err := os.Chmod(fpath, 0664); err != nil {
return fmt.Errorf("failed to set permissions on %q: %w", fpath, err)
}
zipWriter := zip.NewWriter(tgt)
defer func() {
if err := zipWriter.Close(); err != nil {
log.Println(err)
}
}()
for _, fname := range []string{"LICENSE", "README.md", "NOTICE.md"} {
fpath := filepath.Join(grafanaDir, fname)
fi, err := os.Lstat(fpath)
if err != nil {
return fmt.Errorf("couldn't stat %q: %w", fpath, err)
}
hdr, err := zip.FileInfoHeader(fi)
if err != nil {
return fmt.Errorf("failed to open zip header: %w", err)
}
// Enable compression, as it's disabled by default
hdr.Method = zip.Deflate
hdr.Name = fmt.Sprintf("grafana-%s/%s", version, fname)
w, err := zipWriter.CreateHeader(hdr)
if err != nil {
return fmt.Errorf("failed writing zip header: %w", err)
}
//nolint:gosec
src, err := os.Open(fpath)
if err != nil {
return fmt.Errorf("failed to open %q: %w", fname, err)
}
if _, err := io.Copy(w, src); err != nil {
if err := src.Close(); err != nil {
log.Println(err)
}
return fmt.Errorf("failed writing zip entry: %w", err)
}
if err := src.Close(); err != nil {
log.Println(err)
}
}
if err := filepath.Walk(srcDir, func(fpath string, fi os.FileInfo, err error) error {
if err != nil {
return err
}
if fpath == srcDir {
return nil
}
hdr, err := zip.FileInfoHeader(fi)
if err != nil {
return fmt.Errorf("failed to open zip header: %s", err)
}
// Enable compression, as it's disabled by default
hdr.Method = zip.Deflate
hdr.Name = fmt.Sprintf("grafana-%s/%s", version, strings.TrimPrefix(fpath, fmt.Sprintf("%s/", srcDir)))
if fi.IsDir() {
// A trailing slash means it's a directory
if hdr.Name[len(hdr.Name)-1] != '/' {
hdr.Name += "/"
}
}
w, err := zipWriter.CreateHeader(hdr)
if err != nil {
return fmt.Errorf("failed writing zip header: %s", err)
}
if fi.IsDir() {
return nil
}
//nolint:gosec
src, err := os.Open(fpath)
if err != nil {
return fmt.Errorf("failed to open %q: %w", fpath, err)
}
if _, err := io.Copy(w, src); err != nil {
if err := src.Close(); err != nil {
log.Println(err)
}
return fmt.Errorf("failed writing zip entry: %w", err)
}
if err := src.Close(); err != nil {
log.Println(err)
}
return nil
}); err != nil {
return fmt.Errorf("failed to traverse directory %q: %w", srcDir, err)
}
if err := zipWriter.Close(); err != nil {
return fmt.Errorf("failed writing %q: %w", fpath, err)
}
if err := tgt.Close(); err != nil {
return fmt.Errorf("failed writing %q: %w", fpath, err)
}
log.Printf("Successfully created %q", fpath)
return nil
}
// nolint
func createTarball(srcDir, version, variantStr, sfx, grafanaDir string) error {
fpath := filepath.Join(grafanaDir, "dist", fmt.Sprintf("grafana%s-%s.%s.tar.gz", sfx, version, variantStr))
//nolint:gosec
tgt, err := os.Create(fpath)
if err != nil {
return fmt.Errorf("failed to create %q: %w", fpath, err)
}
defer func() {
if err := tgt.Close(); err != nil && !errors.Is(err, os.ErrClosed) {
log.Println(err)
}
}()
//nolint:gosec
if err := os.Chmod(fpath, 0664); err != nil {
return fmt.Errorf("failed to set permissions on %q: %w", fpath, err)
}
gzWriter, err := gzip.NewWriterLevel(tgt, gzip.BestCompression)
if err != nil {
return fmt.Errorf("failed to create gzip writer: %w", err)
}
defer func() {
if err := gzWriter.Close(); err != nil {
log.Println(err)
}
}()
tarWriter := tar.NewWriter(gzWriter)
defer func() {
if err := tarWriter.Close(); err != nil {
log.Println(err)
}
}()
for _, fname := range []string{"LICENSE", "README.md", "NOTICE.md"} {
fpath := filepath.Join(grafanaDir, fname)
fi, err := os.Lstat(fpath)
if err != nil {
return fmt.Errorf("couldn't stat %q: %w", fpath, err)
}
hdr, err := tar.FileInfoHeader(fi, "")
if err != nil {
return fmt.Errorf("failed getting tar header: %w", err)
}
hdr.Name = fmt.Sprintf("grafana-%s/%s", version, fname)
if err := tarWriter.WriteHeader(hdr); err != nil {
return fmt.Errorf("failed writing tar header: %w", err)
}
//nolint:gosec
src, err := os.Open(fpath)
if err != nil {
return fmt.Errorf("failed to open %q: %w", fname, err)
}
if _, err := io.Copy(tarWriter, src); err != nil {
if err := src.Close(); err != nil {
log.Println(err)
}
return fmt.Errorf("failed writing tar entry: %w", err)
}
if err := src.Close(); err != nil {
log.Println(err)
}
}
if err := filepath.Walk(srcDir, func(fpath string, fi os.FileInfo, err error) error {
if err != nil {
return err
}
if fpath == srcDir {
return nil
}
linkTgt := ""
if fi.Mode()&os.ModeSymlink != 0 {
log.Printf("reading link '%s'", fpath)
linkTgt, err = os.Readlink(fpath)
if err != nil {
return err
}
linkTgt = fmt.Sprintf("grafana-%s/%s", version, linkTgt)
}
hdr, err := tar.FileInfoHeader(fi, linkTgt)
if err != nil {
return fmt.Errorf("failed getting tar header: %w", err)
}
hdr.Name = fmt.Sprintf("grafana-%s/%s", version, strings.TrimPrefix(fpath, fmt.Sprintf("%s/", srcDir)))
if err := tarWriter.WriteHeader(hdr); err != nil {
return fmt.Errorf("failed writing tar header: %w", err)
}
if fi.IsDir() {
return nil
}
//nolint:gosec
src, err := os.Open(fpath)
if err != nil {
return fmt.Errorf("failed to open %q: %w", fpath, err)
}
if _, err := io.Copy(tarWriter, src); err != nil {
if err := src.Close(); err != nil {
log.Println(err)
}
return fmt.Errorf("failed writing tar entry: %w", err)
}
if err := src.Close(); err != nil {
log.Println(err)
}
return nil
}); err != nil {
return fmt.Errorf("failed to traverse directory %q: %w", srcDir, err)
}
if err := tarWriter.Close(); err != nil {
return fmt.Errorf("failed writing %q: %w", fpath, err)
}
if err := gzWriter.Close(); err != nil {
return fmt.Errorf("failed writing %q: %w", fpath, err)
}
if err := tgt.Close(); err != nil {
return fmt.Errorf("failed writing %q: %w", fpath, err)
}
st, err := os.Stat(fpath)
if err != nil {
return err
}
perms := st.Mode() & os.ModePerm
log.Printf("Successfully created package %q (permissions: %o)", fpath, perms)
return nil
}