[v8.5.x] CI: Backport grafana-com publishing (#65168)
[v8.5.x] Backport grafana-com publishing This should complete #56797.
This commit is contained in:
+5
-1
@@ -3233,6 +3233,8 @@ steps:
|
||||
- publish-linux-packages-deb
|
||||
- publish-linux-packages-rpm
|
||||
environment:
|
||||
GCP_KEY:
|
||||
from_secret: gcp_key
|
||||
GRAFANA_COM_API_KEY:
|
||||
from_secret: grafana_api_key
|
||||
image: grafana/grafana-ci-deploy:1.3.1
|
||||
@@ -3326,6 +3328,8 @@ steps:
|
||||
- publish-linux-packages-deb
|
||||
- publish-linux-packages-rpm
|
||||
environment:
|
||||
GCP_KEY:
|
||||
from_secret: gcp_key
|
||||
GRAFANA_COM_API_KEY:
|
||||
from_secret: grafana_api_key
|
||||
image: grafana/grafana-ci-deploy:1.3.1
|
||||
@@ -4792,6 +4796,6 @@ kind: secret
|
||||
name: aws_secret_access_key
|
||||
---
|
||||
kind: signature
|
||||
hmac: bf3d2885e476653b0f3bcecb4e0335b2d243285d90e3d74492bf794a90615e06
|
||||
hmac: 702107d1f512b722de5103aa8c80ff0f0048b3e42acbc3af17064301414def49
|
||||
|
||||
...
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package main
|
||||
|
||||
import "github.com/urfave/cli/v2"
|
||||
|
||||
// ArgCountWrapper will cause the action to fail if there were not exactly `num` args provided.
|
||||
func ArgCountWrapper(num int, action cli.ActionFunc) cli.ActionFunc {
|
||||
return func(ctx *cli.Context) error {
|
||||
if ctx.NArg() != num {
|
||||
if err := cli.ShowSubcommandHelp(ctx); err != nil {
|
||||
return cli.Exit(err.Error(), 1)
|
||||
}
|
||||
return cli.Exit("", 1)
|
||||
}
|
||||
|
||||
return action(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
// ArgCountWrapper will cause the action to fail if there were more than `num` args provided.
|
||||
func MaxArgCountWrapper(max int, action cli.ActionFunc) cli.ActionFunc {
|
||||
return func(ctx *cli.Context) error {
|
||||
if ctx.NArg() > max {
|
||||
if err := cli.ShowSubcommandHelp(ctx); err != nil {
|
||||
return cli.Exit(err.Error(), 1)
|
||||
}
|
||||
return cli.Exit("", 1)
|
||||
}
|
||||
|
||||
return action(ctx)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package main
|
||||
|
||||
import "github.com/urfave/cli/v2"
|
||||
|
||||
var (
|
||||
jobsFlag = cli.IntFlag{
|
||||
Name: "jobs",
|
||||
Usage: "Number of parallel jobs",
|
||||
}
|
||||
buildIDFlag = cli.StringFlag{
|
||||
Name: "build-id",
|
||||
Usage: "Optionally supply a build ID to be part of the version",
|
||||
}
|
||||
editionFlag = cli.StringFlag{
|
||||
Name: "edition",
|
||||
Usage: "The edition of Grafana to build (oss or enterprise)",
|
||||
Value: "oss",
|
||||
}
|
||||
variantsFlag = cli.StringFlag{
|
||||
Name: "variants",
|
||||
Usage: "Comma-separated list of variants to build",
|
||||
}
|
||||
triesFlag = cli.IntFlag{
|
||||
Name: "tries",
|
||||
Usage: "Specify number of tries before failing",
|
||||
Value: 1,
|
||||
}
|
||||
noInstallDepsFlag = cli.BoolFlag{
|
||||
Name: "no-install-deps",
|
||||
Usage: "Don't install dependencies",
|
||||
}
|
||||
signingAdminFlag = cli.BoolFlag{
|
||||
Name: "signing-admin",
|
||||
Usage: "Use manifest signing admin API endpoint?",
|
||||
}
|
||||
signFlag = cli.BoolFlag{
|
||||
Name: "sign",
|
||||
Usage: "Enable plug-in signing (you must set GRAFANA_API_KEY)",
|
||||
}
|
||||
dryRunFlag = cli.BoolFlag{
|
||||
Name: "dry-run",
|
||||
Usage: "Only simulate actions",
|
||||
}
|
||||
gcpKeyFlag = cli.StringFlag{
|
||||
Name: "gcp-key",
|
||||
Usage: "Google Cloud Platform key file",
|
||||
Required: true,
|
||||
}
|
||||
gitHubTokenFlag = cli.StringFlag{
|
||||
Name: "github-token",
|
||||
Value: "",
|
||||
EnvVars: []string{"GITHUB_TOKEN"},
|
||||
Usage: "GitHub token",
|
||||
}
|
||||
tagFlag = cli.StringFlag{
|
||||
Name: "tag",
|
||||
Usage: "Grafana version tag",
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,323 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/urfave/cli/v2"
|
||||
|
||||
"github.com/grafana/grafana/pkg/build/config"
|
||||
"github.com/grafana/grafana/pkg/build/gcloud"
|
||||
"github.com/grafana/grafana/pkg/build/gcloud/storage"
|
||||
"github.com/grafana/grafana/pkg/build/packaging"
|
||||
)
|
||||
|
||||
const grafanaAPI = "https://grafana.com/api"
|
||||
|
||||
// GrafanaCom implements the sub-command "grafana-com".
|
||||
func GrafanaCom(c *cli.Context) error {
|
||||
bucketStr := c.String("src-bucket")
|
||||
edition := config.Edition(c.String("edition"))
|
||||
|
||||
if err := gcloud.ActivateServiceAccount(); err != nil {
|
||||
return fmt.Errorf("couldn't activate service account, err: %w", err)
|
||||
}
|
||||
|
||||
metadata, err := config.GenerateMetadata(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
releaseMode, err := metadata.GetReleaseMode()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
version := metadata.GrafanaVersion
|
||||
if releaseMode.Mode == config.Cronjob {
|
||||
gcs, err := storage.New()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
bucket := gcs.Bucket(bucketStr)
|
||||
latestMainVersion, err := storage.GetLatestMainBuild(c.Context, bucket, filepath.Join(string(edition), "main"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
version = latestMainVersion
|
||||
}
|
||||
|
||||
dryRun := c.Bool("dry-run")
|
||||
simulateRelease := c.Bool("simulate-release")
|
||||
// Test release mode and dryRun imply simulateRelease
|
||||
if releaseMode.IsTest || dryRun {
|
||||
simulateRelease = true
|
||||
}
|
||||
|
||||
grafanaAPIKey := strings.TrimSpace(os.Getenv("GRAFANA_COM_API_KEY"))
|
||||
if grafanaAPIKey == "" {
|
||||
return cli.Exit("the environment variable GRAFANA_COM_API_KEY must be set", 1)
|
||||
}
|
||||
whatsNewURL, releaseNotesURL, err := getReleaseURLs()
|
||||
if err != nil {
|
||||
return cli.Exit(err.Error(), 1)
|
||||
}
|
||||
|
||||
// TODO: Verify config values
|
||||
cfg := packaging.PublishConfig{
|
||||
Config: config.Config{
|
||||
Version: version,
|
||||
},
|
||||
Edition: edition,
|
||||
ReleaseMode: releaseMode,
|
||||
GrafanaAPIKey: grafanaAPIKey,
|
||||
WhatsNewURL: whatsNewURL,
|
||||
ReleaseNotesURL: releaseNotesURL,
|
||||
DryRun: dryRun,
|
||||
TTL: c.String("ttl"),
|
||||
SimulateRelease: simulateRelease,
|
||||
}
|
||||
|
||||
if err := publishPackages(cfg); err != nil {
|
||||
return cli.Exit(err.Error(), 1)
|
||||
}
|
||||
|
||||
log.Println("Successfully published packages to grafana.com!")
|
||||
return nil
|
||||
}
|
||||
|
||||
func getReleaseURLs() (string, string, error) {
|
||||
type grafanaConf struct {
|
||||
WhatsNewURL string `json:"whatsNewUrl"`
|
||||
ReleaseNotesURL string `json:"releaseNotesUrl"`
|
||||
}
|
||||
type packageConf struct {
|
||||
Grafana grafanaConf `json:"grafana"`
|
||||
}
|
||||
|
||||
pkgB, err := os.ReadFile("package.json")
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed to read package.json: %w", err)
|
||||
}
|
||||
|
||||
var pconf packageConf
|
||||
if err := json.Unmarshal(pkgB, &pconf); err != nil {
|
||||
return "", "", fmt.Errorf("failed to decode package.json: %w", err)
|
||||
}
|
||||
if _, err := url.ParseRequestURI(pconf.Grafana.WhatsNewURL); err != nil {
|
||||
return "", "", fmt.Errorf("grafana.whatsNewUrl is invalid in package.json: %q", pconf.Grafana.WhatsNewURL)
|
||||
}
|
||||
if _, err := url.ParseRequestURI(pconf.Grafana.ReleaseNotesURL); err != nil {
|
||||
return "", "", fmt.Errorf("grafana.releaseNotesUrl is invalid in package.json: %q",
|
||||
pconf.Grafana.ReleaseNotesURL)
|
||||
}
|
||||
|
||||
return pconf.Grafana.WhatsNewURL, pconf.Grafana.ReleaseNotesURL, nil
|
||||
}
|
||||
|
||||
// publishPackages publishes packages to grafana.com.
|
||||
func publishPackages(cfg packaging.PublishConfig) error {
|
||||
log.Printf("Publishing Grafana packages, version %s, %s edition, %s mode, dryRun: %v, simulating: %v...\n",
|
||||
cfg.Version, cfg.Edition, cfg.ReleaseMode.Mode, cfg.DryRun, cfg.SimulateRelease)
|
||||
|
||||
versionStr := fmt.Sprintf("v%s", cfg.Version)
|
||||
log.Printf("Creating release %s at grafana.com...\n", versionStr)
|
||||
|
||||
var sfx string
|
||||
var pth string
|
||||
switch cfg.Edition {
|
||||
case config.EditionOSS:
|
||||
pth = "oss"
|
||||
case config.EditionEnterprise:
|
||||
pth = "enterprise"
|
||||
sfx = packaging.EnterpriseSfx
|
||||
default:
|
||||
return fmt.Errorf("unrecognized edition %q", cfg.Edition)
|
||||
}
|
||||
|
||||
switch cfg.ReleaseMode.Mode {
|
||||
case config.MainMode, config.DownstreamMode, config.CronjobMode:
|
||||
pth = path.Join(pth, packaging.MainFolder)
|
||||
default:
|
||||
pth = path.Join(pth, packaging.ReleaseFolder)
|
||||
}
|
||||
|
||||
product := fmt.Sprintf("grafana%s", sfx)
|
||||
pth = path.Join(pth, product)
|
||||
baseArchiveURL := fmt.Sprintf("https://dl.grafana.com/%s", pth)
|
||||
|
||||
var builds []buildRepr
|
||||
for _, ba := range packaging.ArtifactConfigs {
|
||||
u := ba.GetURL(baseArchiveURL, cfg)
|
||||
|
||||
sha256, err := getSHA256(u)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
builds = append(builds, buildRepr{
|
||||
OS: ba.Os,
|
||||
URL: u,
|
||||
SHA256: string(sha256),
|
||||
Arch: ba.Arch,
|
||||
})
|
||||
}
|
||||
|
||||
r := releaseRepr{
|
||||
Version: cfg.Version,
|
||||
ReleaseDate: time.Now().UTC(),
|
||||
Builds: builds,
|
||||
Stable: cfg.ReleaseMode.Mode == config.TagMode && !cfg.ReleaseMode.IsBeta && !cfg.ReleaseMode.IsTest,
|
||||
Beta: cfg.ReleaseMode.IsBeta,
|
||||
Nightly: cfg.ReleaseMode.Mode == config.CronjobMode,
|
||||
}
|
||||
if cfg.ReleaseMode.Mode == config.TagMode || r.Beta {
|
||||
r.WhatsNewURL = cfg.WhatsNewURL
|
||||
r.ReleaseNotesURL = cfg.ReleaseNotesURL
|
||||
}
|
||||
|
||||
if err := postRequest(cfg, "versions", r, fmt.Sprintf("create release %s", r.Version)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := postRequest(cfg, fmt.Sprintf("versions/%s", cfg.Version), r,
|
||||
fmt.Sprintf("update release %s", cfg.Version)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, b := range r.Builds {
|
||||
if err := postRequest(cfg, fmt.Sprintf("versions/%s/packages", cfg.Version), b,
|
||||
fmt.Sprintf("create build %s %s", b.OS, b.Arch)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := postRequest(cfg, fmt.Sprintf("versions/%s/packages/%s/%s", cfg.Version, b.Arch, b.OS), b,
|
||||
fmt.Sprintf("update build %s %s", b.OS, b.Arch)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getSHA256(u string) ([]byte, error) {
|
||||
shaURL := fmt.Sprintf("%s.sha256", u)
|
||||
// nolint:gosec
|
||||
resp, err := http.Get(shaURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
if err := resp.Body.Close(); err != nil {
|
||||
log.Println("failed to close response body, err: %w", err)
|
||||
}
|
||||
}()
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
return nil, fmt.Errorf("failed downloading %s: %s", u, resp.Status)
|
||||
}
|
||||
|
||||
sha256, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return sha256, nil
|
||||
}
|
||||
|
||||
func postRequest(cfg packaging.PublishConfig, pth string, obj interface{}, descr string) error {
|
||||
var sfx string
|
||||
switch cfg.Edition {
|
||||
case config.EditionOSS:
|
||||
case config.EditionEnterprise:
|
||||
sfx = packaging.EnterpriseSfx
|
||||
default:
|
||||
return fmt.Errorf("unrecognized edition %q", cfg.Edition)
|
||||
}
|
||||
product := fmt.Sprintf("grafana%s", sfx)
|
||||
|
||||
jsonB, err := json.Marshal(obj)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to JSON encode release: %w", err)
|
||||
}
|
||||
|
||||
u, err := constructURL(product, pth)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req, err := http.NewRequest(http.MethodPost, u, bytes.NewReader(jsonB))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", cfg.GrafanaAPIKey))
|
||||
req.Header.Add("Content-Type", "application/json")
|
||||
|
||||
log.Printf("Posting to grafana.com API, %s - JSON: %s\n", u, string(jsonB))
|
||||
if cfg.SimulateRelease {
|
||||
log.Println("Only simulating request")
|
||||
return nil
|
||||
}
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed posting to %s (%s): %s", u, descr, err)
|
||||
}
|
||||
defer func() {
|
||||
if err := resp.Body.Close(); err != nil {
|
||||
log.Println("failed to close response body, err: %w", err)
|
||||
}
|
||||
}()
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if strings.Contains(string(body), "already exists") || strings.Contains(string(body), "Nothing to update") {
|
||||
log.Printf("Already exists: %s\n", descr)
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("failed posting to %s (%s): %s", u, descr, resp.Status)
|
||||
}
|
||||
|
||||
log.Printf("Successfully posted to grafana.com API, %s\n", u)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func constructURL(product string, pth string) (string, error) {
|
||||
productPath := filepath.Clean(filepath.Join("/", product, pth))
|
||||
u, err := url.Parse(grafanaAPI)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
u.Path = path.Join(u.Path, productPath)
|
||||
return u.String(), err
|
||||
}
|
||||
|
||||
type buildRepr struct {
|
||||
OS string `json:"os"`
|
||||
URL string `json:"url"`
|
||||
SHA256 string `json:"sha256"`
|
||||
Arch string `json:"arch"`
|
||||
}
|
||||
|
||||
type releaseRepr struct {
|
||||
Version string `json:"version"`
|
||||
ReleaseDate time.Time `json:"releaseDate"`
|
||||
Stable bool `json:"stable"`
|
||||
Beta bool `json:"beta"`
|
||||
Nightly bool `json:"nightly"`
|
||||
WhatsNewURL string `json:"whatsNewUrl"`
|
||||
ReleaseNotesURL string `json:"releaseNotesUrl"`
|
||||
Builds []buildRepr `json:"-"`
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_constructURL(t *testing.T) {
|
||||
type args struct {
|
||||
product string
|
||||
pth string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
wantErr bool
|
||||
}{
|
||||
{name: "cleans .. sequence", args: args{"..", ".."}, want: "https://grafana.com/api", wantErr: false},
|
||||
{name: "doesn't clean anything - non malicious url", args: args{"foo", "bar"}, want: "https://grafana.com/api/foo/bar", wantErr: false},
|
||||
{name: "doesn't clean anything - three dots", args: args{"...", "..."}, want: "https://grafana.com/api/.../...", wantErr: false},
|
||||
{name: "cleans .", args: args{"..", ".."}, want: "https://grafana.com/api", wantErr: false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := constructURL(tt.args.product, tt.args.pth)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("constructURL() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("constructURL() got = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
var additionalCommands []*cli.Command = make([]*cli.Command, 0, 5)
|
||||
|
||||
//nolint:unused
|
||||
func registerAppCommand(c *cli.Command) {
|
||||
additionalCommands = append(additionalCommands, c)
|
||||
}
|
||||
|
||||
func main() {
|
||||
app := cli.NewApp()
|
||||
app.Commands = cli.Commands{
|
||||
{
|
||||
Name: "publish",
|
||||
Usage: "Publish packages to Grafana com and repositories",
|
||||
Subcommands: cli.Commands{
|
||||
{
|
||||
Name: "grafana-com",
|
||||
Usage: "Publish packages to grafana.com",
|
||||
Action: GrafanaCom,
|
||||
Flags: []cli.Flag{
|
||||
&editionFlag,
|
||||
&buildIDFlag,
|
||||
&dryRunFlag,
|
||||
&cli.StringFlag{
|
||||
Name: "src-bucket",
|
||||
Value: "grafana-downloads",
|
||||
Usage: "Google Cloud Storage bucket",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
app.Commands = append(app.Commands, additionalCommands...)
|
||||
|
||||
if err := app.Run(os.Args); err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package compilers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
const (
|
||||
ArmV6 = "/opt/rpi-tools/arm-bcm2708/arm-linux-gnueabihf/bin/arm-linux-gnueabihf-gcc"
|
||||
Armv7 = "arm-linux-gnueabihf-gcc"
|
||||
Armv7Musl = "/tmp/arm-linux-musleabihf-cross/bin/arm-linux-musleabihf-gcc"
|
||||
Arm64 = "aarch64-linux-gnu-gcc"
|
||||
Arm64Musl = "/tmp/aarch64-linux-musl-cross/bin/aarch64-linux-musl-gcc"
|
||||
Osx64 = "/tmp/osxcross/target/bin/o64-clang"
|
||||
Win64 = "x86_64-w64-mingw32-gcc"
|
||||
LinuxX64 = "/tmp/x86_64-centos6-linux-gnu/bin/x86_64-centos6-linux-gnu-gcc"
|
||||
LinuxX64Musl = "/tmp/x86_64-linux-musl-cross/bin/x86_64-linux-musl-gcc"
|
||||
)
|
||||
|
||||
func Install() error {
|
||||
// From the os.TempDir documentation:
|
||||
// On Unix systems, it returns $TMPDIR if non-empty,
|
||||
// else /tmp. On Windows, it uses GetTempPath,
|
||||
// returning the first non-empty value from %TMP%, %TEMP%, %USERPROFILE%,
|
||||
// or the Windows directory. On Plan 9, it returns /tmp.
|
||||
tmp := os.TempDir()
|
||||
|
||||
var (
|
||||
centosArchive = "x86_64-centos6-linux-gnu.tar.xz"
|
||||
osxArchive = "osxcross.tar.xz"
|
||||
)
|
||||
|
||||
for _, fname := range []string{centosArchive, osxArchive} {
|
||||
path := filepath.Join(tmp, fname)
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
return fmt.Errorf("stat error: %w", err)
|
||||
}
|
||||
// Ignore gosec G204 as this function is only used in the build process.
|
||||
//nolint:gosec
|
||||
cmd := exec.Command("tar", "xfJ", fname)
|
||||
cmd.Dir = tmp
|
||||
if output, err := cmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("failed to unpack %q: %q, %w", fname, output, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package config
|
||||
|
||||
type Config struct {
|
||||
Version string
|
||||
Bucket string
|
||||
DebRepoBucket string
|
||||
DebDBBucket string
|
||||
RPMRepoBucket string
|
||||
GPGPassPath string
|
||||
GPGPrivateKey string
|
||||
GPGPublicKey string
|
||||
NumWorkers int
|
||||
GitHubUser string
|
||||
GitHubToken string
|
||||
PullEnterprise bool
|
||||
PackageVersion string
|
||||
SignPackages bool
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package config
|
||||
|
||||
type Edition string
|
||||
|
||||
const (
|
||||
EditionOSS Edition = "oss"
|
||||
EditionEnterprise Edition = "enterprise"
|
||||
EditionEnterprise2 Edition = "enterprise2"
|
||||
)
|
||||
@@ -0,0 +1,102 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/grafana/pkg/build/droneutil"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
func GenerateMetadata(c *cli.Context) (Metadata, error) {
|
||||
var metadata Metadata
|
||||
version := ""
|
||||
|
||||
event, err := droneutil.GetDroneEventFromEnv()
|
||||
if err != nil {
|
||||
return Metadata{}, err
|
||||
}
|
||||
|
||||
tag, ok := os.LookupEnv("DRONE_TAG")
|
||||
if !ok {
|
||||
fmt.Println("DRONE_TAG envvar not present, %w", err)
|
||||
}
|
||||
|
||||
var releaseMode ReleaseMode
|
||||
switch event {
|
||||
case string(PullRequestMode):
|
||||
releaseMode = ReleaseMode{Mode: PullRequestMode}
|
||||
case Push:
|
||||
mode, err := CheckDroneTargetBranch()
|
||||
if err != nil {
|
||||
return Metadata{}, err
|
||||
}
|
||||
releaseMode = ReleaseMode{Mode: mode}
|
||||
case Custom:
|
||||
if edition, _ := os.LookupEnv("EDITION"); edition == string(EditionEnterprise2) {
|
||||
releaseMode = ReleaseMode{Mode: Enterprise2Mode}
|
||||
if tag != "" {
|
||||
version = strings.TrimPrefix(tag, "v")
|
||||
}
|
||||
break
|
||||
}
|
||||
mode, err := CheckDroneTargetBranch()
|
||||
if err != nil {
|
||||
return Metadata{}, err
|
||||
}
|
||||
// if there is a custom event targeting the main branch, that's an enterprise downstream build
|
||||
if mode == MainBranch {
|
||||
releaseMode = ReleaseMode{Mode: DownstreamMode}
|
||||
} else {
|
||||
releaseMode = ReleaseMode{Mode: mode}
|
||||
}
|
||||
case Tag, Promote:
|
||||
if tag == "" {
|
||||
return Metadata{}, fmt.Errorf("DRONE_TAG envvar not present for a tag/promotion event, %w", err)
|
||||
}
|
||||
version = strings.TrimPrefix(tag, "v")
|
||||
mode, err := CheckSemverSuffix()
|
||||
if err != nil {
|
||||
return Metadata{}, err
|
||||
}
|
||||
releaseMode = mode
|
||||
case Cronjob:
|
||||
releaseMode = ReleaseMode{Mode: CronjobMode}
|
||||
}
|
||||
|
||||
if version == "" {
|
||||
version, err = generateVersionFromBuildID()
|
||||
if err != nil {
|
||||
return Metadata{}, err
|
||||
}
|
||||
}
|
||||
|
||||
currentCommit, err := GetDroneCommit()
|
||||
if err != nil {
|
||||
return Metadata{}, err
|
||||
}
|
||||
metadata = Metadata{
|
||||
GrafanaVersion: version,
|
||||
ReleaseMode: releaseMode,
|
||||
GrabplVersion: c.App.Version,
|
||||
CurrentCommit: currentCommit,
|
||||
}
|
||||
|
||||
fmt.Printf("building Grafana version: %s, release mode: %+v", metadata.GrafanaVersion, metadata.ReleaseMode)
|
||||
|
||||
return metadata, nil
|
||||
}
|
||||
|
||||
func generateVersionFromBuildID() (string, error) {
|
||||
buildID, ok := os.LookupEnv("DRONE_BUILD_NUMBER")
|
||||
if !ok {
|
||||
return "", fmt.Errorf("unable to get DRONE_BUILD_NUMBER environmental variable")
|
||||
}
|
||||
var err error
|
||||
version, err := GetGrafanaVersion(buildID, ".")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return version, nil
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
const (
|
||||
DroneBuildEvent = "DRONE_BUILD_EVENT"
|
||||
DroneTargetBranch = "DRONE_TARGET_BRANCH"
|
||||
DroneTag = "DRONE_TAG"
|
||||
DroneSemverPrerelease = "DRONE_SEMVER_PRERELEASE"
|
||||
DroneBuildNumber = "DRONE_BUILD_NUMBER"
|
||||
)
|
||||
|
||||
const (
|
||||
hashedGrafanaVersion = "9.2.0-12345pre"
|
||||
versionedBranch = "v9.2.x"
|
||||
)
|
||||
|
||||
func TestGetMetadata(t *testing.T) {
|
||||
tcs := []struct {
|
||||
envMap map[string]string
|
||||
expVersion string
|
||||
mode ReleaseMode
|
||||
}{
|
||||
{map[string]string{DroneBuildEvent: PullRequest, DroneTargetBranch: "", DroneTag: "", DroneSemverPrerelease: "", DroneBuildNumber: "12345"}, hashedGrafanaVersion, ReleaseMode{Mode: PullRequestMode}},
|
||||
{map[string]string{DroneBuildEvent: Push, DroneTargetBranch: versionedBranch, DroneTag: "", DroneSemverPrerelease: "", DroneBuildNumber: "12345"}, hashedGrafanaVersion, ReleaseMode{Mode: ReleaseBranchMode}},
|
||||
{map[string]string{DroneBuildEvent: Push, DroneTargetBranch: MainBranch, DroneTag: "", DroneSemverPrerelease: "", DroneBuildNumber: "12345"}, hashedGrafanaVersion, ReleaseMode{Mode: MainMode}},
|
||||
{map[string]string{DroneBuildEvent: Custom, DroneTargetBranch: versionedBranch, DroneTag: "", DroneSemverPrerelease: "", DroneBuildNumber: "12345"}, hashedGrafanaVersion, ReleaseMode{Mode: ReleaseBranchMode}},
|
||||
{map[string]string{DroneBuildEvent: Custom, DroneTargetBranch: MainBranch, DroneTag: "", DroneSemverPrerelease: "", DroneBuildNumber: "12345"}, hashedGrafanaVersion, ReleaseMode{Mode: DownstreamMode}},
|
||||
{map[string]string{DroneBuildEvent: Custom, DroneTargetBranch: MainBranch, DroneTag: "", DroneSemverPrerelease: "", DroneBuildNumber: "12345", "EDITION": string(EditionEnterprise2)}, hashedGrafanaVersion, ReleaseMode{Mode: Enterprise2Mode}},
|
||||
{map[string]string{DroneBuildEvent: Tag, DroneTargetBranch: "", DroneTag: "v9.2.0", DroneSemverPrerelease: "", DroneBuildNumber: "12345"}, "9.2.0", ReleaseMode{Mode: TagMode, IsBeta: false, IsTest: false}},
|
||||
{map[string]string{DroneBuildEvent: Tag, DroneTargetBranch: "", DroneTag: "v9.2.0-beta", DroneSemverPrerelease: "beta", DroneBuildNumber: "12345"}, "9.2.0-beta", ReleaseMode{Mode: TagMode, IsBeta: true, IsTest: false}},
|
||||
{map[string]string{DroneBuildEvent: Tag, DroneTargetBranch: "", DroneTag: "v9.2.0-test", DroneSemverPrerelease: "test", DroneBuildNumber: "12345"}, "9.2.0-test", ReleaseMode{Mode: TagMode, IsBeta: false, IsTest: true}},
|
||||
{map[string]string{DroneBuildEvent: Promote, DroneTargetBranch: "", DroneTag: "v9.2.0", DroneSemverPrerelease: "", DroneBuildNumber: "12345"}, "9.2.0", ReleaseMode{Mode: TagMode, IsBeta: false, IsTest: false}},
|
||||
{map[string]string{DroneBuildEvent: Promote, DroneTargetBranch: "", DroneTag: "v9.2.0-beta", DroneSemverPrerelease: "beta", DroneBuildNumber: "12345"}, "9.2.0-beta", ReleaseMode{Mode: TagMode, IsBeta: true, IsTest: false}},
|
||||
{map[string]string{DroneBuildEvent: Promote, DroneTargetBranch: "", DroneTag: "v9.2.0-test", DroneSemverPrerelease: "test", DroneBuildNumber: "12345"}, "9.2.0-test", ReleaseMode{Mode: TagMode, IsBeta: false, IsTest: true}},
|
||||
}
|
||||
|
||||
ctx := cli.NewContext(cli.NewApp(), &flag.FlagSet{}, nil)
|
||||
for _, tc := range tcs {
|
||||
t.Run("Should return valid metadata, ", func(t *testing.T) {
|
||||
setUpEnv(t, tc.envMap)
|
||||
testMetadata(t, ctx, tc.expVersion, tc.mode)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func testMetadata(t *testing.T, ctx *cli.Context, version string, releaseMode ReleaseMode) {
|
||||
t.Helper()
|
||||
|
||||
metadata, err := GenerateMetadata(ctx)
|
||||
require.NoError(t, err)
|
||||
t.Run("with a valid version", func(t *testing.T) {
|
||||
expVersion := metadata.GrafanaVersion
|
||||
require.Equal(t, expVersion, version)
|
||||
})
|
||||
|
||||
t.Run("with a valid release mode from the built-in list", func(t *testing.T) {
|
||||
expMode := metadata.ReleaseMode
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expMode, releaseMode)
|
||||
})
|
||||
}
|
||||
|
||||
func setUpEnv(t *testing.T, envMap map[string]string) {
|
||||
t.Helper()
|
||||
|
||||
os.Clearenv()
|
||||
err := os.Setenv("DRONE_COMMIT", "abcd12345")
|
||||
require.NoError(t, err)
|
||||
for k, v := range envMap {
|
||||
err := os.Setenv(k, v)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"version": "9.2.0-pre"
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/build/executil"
|
||||
)
|
||||
|
||||
type Revision struct {
|
||||
Timestamp int64
|
||||
SHA256 string
|
||||
Branch string
|
||||
}
|
||||
|
||||
func GrafanaTimestamp(ctx context.Context, dir string) (int64, error) {
|
||||
out, err := executil.OutputAt(ctx, dir, "git", "show", "-s", "--format=%ct")
|
||||
if err != nil {
|
||||
return time.Now().Unix(), nil
|
||||
}
|
||||
|
||||
stamp, err := strconv.ParseInt(out, 10, 64)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to parse output from git show: %q", out)
|
||||
}
|
||||
|
||||
return stamp, nil
|
||||
}
|
||||
|
||||
// GrafanaRevision uses git commands to get information about the checked out Grafana code located at 'grafanaDir'.
|
||||
// This could maybe be a more generic "Describe" function in the "git" package.
|
||||
func GrafanaRevision(ctx context.Context, grafanaDir string) (Revision, error) {
|
||||
stamp, err := GrafanaTimestamp(ctx, grafanaDir)
|
||||
if err != nil {
|
||||
return Revision{}, err
|
||||
}
|
||||
|
||||
sha, err := executil.OutputAt(ctx, grafanaDir, "git", "rev-parse", "--short", "HEAD")
|
||||
if err != nil {
|
||||
return Revision{}, err
|
||||
}
|
||||
|
||||
branch, err := executil.OutputAt(ctx, grafanaDir, "git", "rev-parse", "--abbrev-ref", "HEAD")
|
||||
if err != nil {
|
||||
return Revision{}, err
|
||||
}
|
||||
|
||||
return Revision{
|
||||
SHA256: sha,
|
||||
Branch: branch,
|
||||
Timestamp: stamp,
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package config
|
||||
|
||||
// Variant is the OS / Architecture combination that Grafana can be compiled for.
|
||||
type Variant string
|
||||
|
||||
const (
|
||||
VariantLinuxAmd64 Variant = "linux-amd64"
|
||||
VariantLinuxAmd64Musl Variant = "linux-amd64-musl"
|
||||
VariantArmV6 Variant = "linux-armv6"
|
||||
VariantArmV7 Variant = "linux-armv7"
|
||||
VariantArmV7Musl Variant = "linux-armv7-musl"
|
||||
VariantArm64 Variant = "linux-arm64"
|
||||
VariantArm64Musl Variant = "linux-arm64-musl"
|
||||
VariantDarwinAmd64 Variant = "darwin-amd64"
|
||||
VariantWindowsAmd64 Variant = "windows-amd64"
|
||||
)
|
||||
|
||||
var AllVariants = []Variant{
|
||||
VariantArmV6,
|
||||
VariantArmV7,
|
||||
VariantArmV7Musl,
|
||||
VariantArm64,
|
||||
VariantArm64Musl,
|
||||
VariantDarwinAmd64,
|
||||
VariantWindowsAmd64,
|
||||
VariantLinuxAmd64,
|
||||
VariantLinuxAmd64Musl,
|
||||
}
|
||||
|
||||
// Architecture is an allowed value in the GOARCH environment variable.
|
||||
type Architecture string
|
||||
|
||||
const (
|
||||
ArchAMD64 Architecture = "amd64"
|
||||
ArchARMv6 Architecture = "armv6"
|
||||
ArchARMv7 Architecture = "armv7"
|
||||
ArchARM64 Architecture = "arm64"
|
||||
ArchARMHF Architecture = "armhf"
|
||||
ArchARMHFP Architecture = "armhfp"
|
||||
ArchARM Architecture = "arm"
|
||||
)
|
||||
|
||||
type OS string
|
||||
|
||||
const (
|
||||
OSWindows OS = "windows"
|
||||
OSDarwin OS = "darwin"
|
||||
OSLinux OS = "linux"
|
||||
)
|
||||
|
||||
type LibC string
|
||||
|
||||
const (
|
||||
LibCMusl = "musl"
|
||||
)
|
||||
|
||||
// Distribution is the base os image where the Grafana image is built on.
|
||||
type Distribution string
|
||||
|
||||
const (
|
||||
Ubuntu Distribution = "ubuntu"
|
||||
Alpine Distribution = "alpine"
|
||||
)
|
||||
@@ -0,0 +1,157 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/grafana/pkg/build/git"
|
||||
)
|
||||
|
||||
type Metadata struct {
|
||||
GrafanaVersion string `json:"version,omitempty"`
|
||||
ReleaseMode ReleaseMode `json:"releaseMode,omitempty"`
|
||||
GrabplVersion string `json:"grabplVersion,omitempty"`
|
||||
CurrentCommit string `json:"currentCommit,omitempty"`
|
||||
}
|
||||
|
||||
type ReleaseMode struct {
|
||||
Mode VersionMode `json:"mode,omitempty"`
|
||||
IsBeta bool `json:"isBeta,omitempty"`
|
||||
IsTest bool `json:"isTest,omitempty"`
|
||||
}
|
||||
|
||||
type PluginSignature struct {
|
||||
Sign bool `json:"sign,omitempty"`
|
||||
AdminSign bool `json:"adminSign,omitempty"`
|
||||
}
|
||||
|
||||
type Docker struct {
|
||||
ShouldSave bool `json:"shouldSave,omitempty"`
|
||||
Distribution []Distribution `json:"distribution,omitempty"`
|
||||
Architectures []Architecture `json:"archs,omitempty"`
|
||||
PrereleaseBucket string `json:"prereleaseBucket,omitempty"`
|
||||
}
|
||||
|
||||
type Buckets struct {
|
||||
Artifacts string `json:"artifacts,omitempty"`
|
||||
ArtifactsEnterprise2 string `json:"artifactsEnterprise2,omitempty"`
|
||||
CDNAssets string `json:"CDNAssets,omitempty"`
|
||||
CDNAssetsDir string `json:"CDNAssetsDir,omitempty"`
|
||||
Storybook string `json:"storybook,omitempty"`
|
||||
StorybookSrcDir string `json:"storybookSrcDir,omitempty"`
|
||||
}
|
||||
|
||||
// BuildConfig represents the struct that defines all of the different variables used to build Grafana
|
||||
type BuildConfig struct {
|
||||
Variants []Variant `json:"variants,omitempty"`
|
||||
PluginSignature PluginSignature `json:"pluginSignature,omitempty"`
|
||||
Docker Docker `json:"docker,omitempty"`
|
||||
Buckets Buckets `json:"buckets,omitempty"`
|
||||
}
|
||||
|
||||
func (md *Metadata) GetReleaseMode() (ReleaseMode, error) {
|
||||
return md.ReleaseMode, nil
|
||||
}
|
||||
|
||||
// VersionMap is a map of versions. Each key of the Versions map is an event that uses the the config as the value for that key.
|
||||
// For example, the 'pull_request' key will have data in it that might cause Grafana to be built differently in a pull request,
|
||||
// than the way it will be built in 'main'
|
||||
type VersionMap map[VersionMode]BuildConfig
|
||||
|
||||
// GetBuildConfig reads the embedded config.json and decodes it.
|
||||
func GetBuildConfig(mode VersionMode) (*BuildConfig, error) {
|
||||
if v, ok := Versions[mode]; ok {
|
||||
return &v, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("mode '%s' not found in version list", mode)
|
||||
}
|
||||
|
||||
// GetGrafanaVersion gets the Grafana version from the package.json
|
||||
func GetGrafanaVersion(buildID, grafanaDir string) (string, error) {
|
||||
pkgJSONPath := filepath.Join(grafanaDir, "package.json")
|
||||
//nolint:gosec
|
||||
pkgJSONB, err := os.ReadFile(pkgJSONPath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read %q: %w", pkgJSONPath, err)
|
||||
}
|
||||
pkgObj := map[string]interface{}{}
|
||||
if err := json.Unmarshal(pkgJSONB, &pkgObj); err != nil {
|
||||
return "", fmt.Errorf("failed decoding %q: %w", pkgJSONPath, err)
|
||||
}
|
||||
|
||||
version := pkgObj["version"].(string)
|
||||
if version == "" {
|
||||
return "", fmt.Errorf("failed to read version from %q", pkgJSONPath)
|
||||
}
|
||||
if buildID != "" {
|
||||
buildID = shortenBuildID(buildID)
|
||||
verComponents := strings.Split(version, "-")
|
||||
version = verComponents[0]
|
||||
if len(verComponents) > 1 {
|
||||
buildID = fmt.Sprintf("%s%s", buildID, verComponents[1])
|
||||
}
|
||||
version = fmt.Sprintf("%s-%s", version, buildID)
|
||||
}
|
||||
|
||||
return version, nil
|
||||
}
|
||||
|
||||
func CheckDroneTargetBranch() (VersionMode, error) {
|
||||
rePRCheckBranch := git.PRCheckRegexp()
|
||||
reRlsBranch := regexp.MustCompile(`^v\d+\.\d+\.x$`)
|
||||
target := os.Getenv("DRONE_TARGET_BRANCH")
|
||||
if target == "" {
|
||||
return "", fmt.Errorf("failed to get DRONE_TARGET_BRANCH environmental variable")
|
||||
} else if target == string(MainMode) {
|
||||
return MainMode, nil
|
||||
}
|
||||
if reRlsBranch.MatchString(target) {
|
||||
return ReleaseBranchMode, nil
|
||||
}
|
||||
if rePRCheckBranch.MatchString(target) {
|
||||
return PullRequestMode, nil
|
||||
}
|
||||
fmt.Printf("unrecognized target branch: %s, defaulting to %s", target, PullRequestMode)
|
||||
return PullRequestMode, nil
|
||||
}
|
||||
|
||||
func CheckSemverSuffix() (ReleaseMode, error) {
|
||||
reBetaRls := regexp.MustCompile(`beta.*`)
|
||||
reTestRls := regexp.MustCompile(`test.*`)
|
||||
tagSuffix, ok := os.LookupEnv("DRONE_SEMVER_PRERELEASE")
|
||||
if !ok || tagSuffix == "" {
|
||||
fmt.Println("DRONE_SEMVER_PRERELEASE doesn't exist for a tag, this is a release event...")
|
||||
return ReleaseMode{Mode: TagMode}, nil
|
||||
}
|
||||
switch {
|
||||
case reBetaRls.MatchString(tagSuffix):
|
||||
return ReleaseMode{Mode: TagMode, IsBeta: true}, nil
|
||||
case reTestRls.MatchString(tagSuffix):
|
||||
return ReleaseMode{Mode: TagMode, IsTest: true}, nil
|
||||
default:
|
||||
fmt.Printf("DRONE_SEMVER_PRERELEASE is custom string, release event with %s suffix\n", tagSuffix)
|
||||
return ReleaseMode{Mode: TagMode}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func GetDroneCommit() (string, error) {
|
||||
commit := strings.TrimSpace(os.Getenv("DRONE_COMMIT"))
|
||||
if commit == "" {
|
||||
return "", fmt.Errorf("the environment variable DRONE_COMMIT is missing")
|
||||
}
|
||||
return commit, nil
|
||||
}
|
||||
|
||||
func shortenBuildID(buildID string) string {
|
||||
buildID = strings.ReplaceAll(buildID, "-", "")
|
||||
if len(buildID) < 9 {
|
||||
return buildID
|
||||
}
|
||||
|
||||
return buildID[0:8]
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package config
|
||||
|
||||
// VersionMode defines the source event that created a release or published version
|
||||
type VersionMode string
|
||||
|
||||
const (
|
||||
MainMode VersionMode = "main"
|
||||
TagMode VersionMode = "release"
|
||||
ReleaseBranchMode VersionMode = "branch"
|
||||
PullRequestMode VersionMode = "pull_request"
|
||||
DownstreamMode VersionMode = "downstream"
|
||||
Enterprise2Mode VersionMode = "enterprise2"
|
||||
CronjobMode VersionMode = "cron"
|
||||
)
|
||||
|
||||
const (
|
||||
Tag = "tag"
|
||||
PullRequest = "pull_request"
|
||||
Push = "push"
|
||||
Custom = "custom"
|
||||
Promote = "promote"
|
||||
Cronjob = "cron"
|
||||
)
|
||||
|
||||
const (
|
||||
MainBranch = "main"
|
||||
)
|
||||
@@ -0,0 +1,206 @@
|
||||
package config
|
||||
|
||||
const PublicBucket = "grafana-downloads"
|
||||
|
||||
var Versions = VersionMap{
|
||||
PullRequestMode: {
|
||||
Variants: []Variant{
|
||||
VariantLinuxAmd64,
|
||||
VariantLinuxAmd64Musl,
|
||||
VariantDarwinAmd64,
|
||||
VariantWindowsAmd64,
|
||||
},
|
||||
PluginSignature: PluginSignature{
|
||||
Sign: false,
|
||||
AdminSign: false,
|
||||
},
|
||||
Docker: Docker{
|
||||
ShouldSave: false,
|
||||
Architectures: []Architecture{
|
||||
ArchAMD64,
|
||||
},
|
||||
Distribution: []Distribution{
|
||||
Alpine,
|
||||
},
|
||||
},
|
||||
},
|
||||
MainMode: {
|
||||
Variants: []Variant{
|
||||
VariantArmV6,
|
||||
VariantArmV7,
|
||||
VariantArmV7Musl,
|
||||
VariantArm64,
|
||||
VariantArm64Musl,
|
||||
VariantDarwinAmd64,
|
||||
VariantWindowsAmd64,
|
||||
VariantLinuxAmd64,
|
||||
VariantLinuxAmd64Musl,
|
||||
},
|
||||
PluginSignature: PluginSignature{
|
||||
Sign: true,
|
||||
AdminSign: true,
|
||||
},
|
||||
Docker: Docker{
|
||||
ShouldSave: false,
|
||||
Architectures: []Architecture{
|
||||
ArchAMD64,
|
||||
ArchARM64,
|
||||
ArchARMv7, // GOARCH=ARM is used for both armv6 and armv7. They are differentiated by the GOARM variable.
|
||||
},
|
||||
Distribution: []Distribution{
|
||||
Alpine,
|
||||
Ubuntu,
|
||||
},
|
||||
},
|
||||
Buckets: Buckets{
|
||||
Artifacts: "grafana-downloads",
|
||||
ArtifactsEnterprise2: "grafana-downloads-enterprise2",
|
||||
CDNAssets: "grafana-static-assets",
|
||||
Storybook: "grafana-storybook",
|
||||
},
|
||||
},
|
||||
DownstreamMode: {
|
||||
Variants: []Variant{
|
||||
VariantArmV6,
|
||||
VariantArmV7,
|
||||
VariantArmV7Musl,
|
||||
VariantArm64,
|
||||
VariantArm64Musl,
|
||||
VariantDarwinAmd64,
|
||||
VariantWindowsAmd64,
|
||||
VariantLinuxAmd64,
|
||||
VariantLinuxAmd64Musl,
|
||||
},
|
||||
PluginSignature: PluginSignature{
|
||||
Sign: true,
|
||||
AdminSign: true,
|
||||
},
|
||||
Docker: Docker{
|
||||
ShouldSave: true,
|
||||
Architectures: []Architecture{
|
||||
ArchAMD64,
|
||||
ArchARM64,
|
||||
ArchARMv7, // GOARCH=ARM is used for both armv6 and armv7. They are differentiated by the GOARM variable.
|
||||
},
|
||||
Distribution: []Distribution{
|
||||
Alpine,
|
||||
Ubuntu,
|
||||
},
|
||||
},
|
||||
Buckets: Buckets{
|
||||
Artifacts: "grafana-downloads",
|
||||
ArtifactsEnterprise2: "grafana-downloads-enterprise2",
|
||||
CDNAssets: "grafana-static-assets",
|
||||
},
|
||||
},
|
||||
ReleaseBranchMode: {
|
||||
Variants: []Variant{
|
||||
VariantArmV6,
|
||||
VariantArmV7,
|
||||
VariantArmV7Musl,
|
||||
VariantArm64,
|
||||
VariantArm64Musl,
|
||||
VariantDarwinAmd64,
|
||||
VariantWindowsAmd64,
|
||||
VariantLinuxAmd64,
|
||||
VariantLinuxAmd64Musl,
|
||||
},
|
||||
PluginSignature: PluginSignature{
|
||||
Sign: true,
|
||||
AdminSign: true,
|
||||
},
|
||||
Docker: Docker{
|
||||
ShouldSave: true,
|
||||
Architectures: []Architecture{
|
||||
ArchAMD64,
|
||||
ArchARM64,
|
||||
ArchARMv7,
|
||||
},
|
||||
Distribution: []Distribution{
|
||||
Alpine,
|
||||
Ubuntu,
|
||||
},
|
||||
},
|
||||
Buckets: Buckets{
|
||||
Artifacts: "grafana-downloads",
|
||||
ArtifactsEnterprise2: "grafana-downloads-enterprise2",
|
||||
CDNAssets: "grafana-static-assets",
|
||||
},
|
||||
},
|
||||
TagMode: {
|
||||
Variants: []Variant{
|
||||
VariantArmV6,
|
||||
VariantArmV7,
|
||||
VariantArmV7Musl,
|
||||
VariantArm64,
|
||||
VariantArm64Musl,
|
||||
VariantDarwinAmd64,
|
||||
VariantWindowsAmd64,
|
||||
VariantLinuxAmd64,
|
||||
VariantLinuxAmd64Musl,
|
||||
},
|
||||
PluginSignature: PluginSignature{
|
||||
Sign: true,
|
||||
AdminSign: true,
|
||||
},
|
||||
Docker: Docker{
|
||||
ShouldSave: true,
|
||||
Architectures: []Architecture{
|
||||
ArchAMD64,
|
||||
ArchARM64,
|
||||
ArchARMv7,
|
||||
},
|
||||
Distribution: []Distribution{
|
||||
Alpine,
|
||||
Ubuntu,
|
||||
},
|
||||
PrereleaseBucket: "grafana-prerelease/artifacts/docker",
|
||||
},
|
||||
Buckets: Buckets{
|
||||
Artifacts: "grafana-prerelease/artifacts/downloads",
|
||||
ArtifactsEnterprise2: "grafana-prerelease/artifacts/downloads-enterprise2",
|
||||
CDNAssets: "grafana-prerelease",
|
||||
CDNAssetsDir: "artifacts/static-assets",
|
||||
Storybook: "grafana-prerelease",
|
||||
StorybookSrcDir: "artifacts/storybook",
|
||||
},
|
||||
},
|
||||
Enterprise2Mode: {
|
||||
Variants: []Variant{
|
||||
VariantArmV6,
|
||||
VariantArmV7,
|
||||
VariantArmV7Musl,
|
||||
VariantArm64,
|
||||
VariantArm64Musl,
|
||||
VariantDarwinAmd64,
|
||||
VariantWindowsAmd64,
|
||||
VariantLinuxAmd64,
|
||||
VariantLinuxAmd64Musl,
|
||||
},
|
||||
PluginSignature: PluginSignature{
|
||||
Sign: true,
|
||||
AdminSign: true,
|
||||
},
|
||||
Docker: Docker{
|
||||
ShouldSave: true,
|
||||
Architectures: []Architecture{
|
||||
ArchAMD64,
|
||||
ArchARM64,
|
||||
ArchARMv7,
|
||||
},
|
||||
Distribution: []Distribution{
|
||||
Alpine,
|
||||
Ubuntu,
|
||||
},
|
||||
PrereleaseBucket: "grafana-prerelease/artifacts/docker",
|
||||
},
|
||||
Buckets: Buckets{
|
||||
Artifacts: "grafana-prerelease/artifacts/downloads",
|
||||
ArtifactsEnterprise2: "grafana-prerelease/artifacts/downloads-enterprise2",
|
||||
CDNAssets: "grafana-prerelease",
|
||||
CDNAssetsDir: "artifacts/static-assets",
|
||||
Storybook: "grafana-prerelease",
|
||||
StorybookSrcDir: "artifacts/storybook",
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package cryptoutil
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
)
|
||||
|
||||
func MD5File(fpath string) error {
|
||||
// Ignore gosec G304 as this function is only used in the build process.
|
||||
//nolint:gosec
|
||||
fd, err := os.Open(fpath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if err := fd.Close(); err != nil {
|
||||
log.Printf("error closing file at '%s': %s", fpath, err.Error())
|
||||
}
|
||||
}()
|
||||
|
||||
h := md5.New() // nolint:gosec
|
||||
if _, err = io.Copy(h, fd); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// nolint:gosec
|
||||
if err := os.WriteFile(fpath+".md5", []byte(fmt.Sprintf("%x\n", h.Sum(nil))), 0664); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
// Package droneutil provides utility functions for working with Drone.
|
||||
package droneutil
|
||||
@@ -0,0 +1,34 @@
|
||||
package droneutil
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Lookup is the equivalent of os.LookupEnv, but also accepts a list of strings rather than only checking os.Environ()
|
||||
func Lookup(values []string, val string) (string, bool) {
|
||||
for _, v := range values {
|
||||
prefix := val + "="
|
||||
if strings.HasPrefix(v, prefix) {
|
||||
return strings.TrimPrefix(v, prefix), true
|
||||
}
|
||||
}
|
||||
|
||||
return "", false
|
||||
}
|
||||
|
||||
// GetDroneEvent looks for the "DRONE_BUILD_EVENT" in the provided env list and returns the value.
|
||||
// if it was not found, then an error is returned.
|
||||
func GetDroneEvent(env []string) (string, error) {
|
||||
event, ok := Lookup(env, "DRONE_BUILD_EVENT")
|
||||
if !ok {
|
||||
return "", fmt.Errorf("failed to get DRONE_BUILD_EVENT environmental variable")
|
||||
}
|
||||
return event, nil
|
||||
}
|
||||
|
||||
// GetDroneEventFromEnv returns the value of DRONE_BUILD_EVENT from os.Environ()
|
||||
func GetDroneEventFromEnv() (string, error) {
|
||||
return GetDroneEvent(os.Environ())
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package droneutil_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/build/droneutil"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestGetDroneEvent(t *testing.T) {
|
||||
t.Run("Should return the Drone Event", func(t *testing.T) {
|
||||
env := []string{"DRONE_BUILD_EVENT=pull_request"}
|
||||
droneEvent, err := droneutil.GetDroneEvent(env)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, droneEvent, "pull_request")
|
||||
})
|
||||
t.Run("Should return error, Drone Event env var is missing", func(t *testing.T) {
|
||||
droneEvent, err := droneutil.GetDroneEvent([]string{})
|
||||
require.Error(t, err)
|
||||
require.Empty(t, droneEvent)
|
||||
})
|
||||
}
|
||||
|
||||
func TestLookup(t *testing.T) {
|
||||
env := []string{"", "EXAMPLE_KEY=value", "EXAMPLE_KEY"}
|
||||
t.Run("A valid lookup should return a string and no error", func(t *testing.T) {
|
||||
val, ok := droneutil.Lookup(env, "EXAMPLE_KEY")
|
||||
require.True(t, ok)
|
||||
require.Equal(t, val, "value")
|
||||
})
|
||||
|
||||
t.Run("An invalid lookup should return an error", func(t *testing.T) {
|
||||
_, ok := droneutil.Lookup(env, "EXAMPLE_KEY_DOES_NOT_EXIST")
|
||||
require.False(t, ok)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package errutil
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type Group struct {
|
||||
cancel func()
|
||||
wg sync.WaitGroup
|
||||
errOnce sync.Once
|
||||
err error
|
||||
}
|
||||
|
||||
func GroupWithContext(ctx context.Context) (*Group, context.Context) {
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
return &Group{cancel: cancel}, ctx
|
||||
}
|
||||
|
||||
// Wait waits for any wrapped goroutines to finish and returns any error having occurred in one of them.
|
||||
func (g *Group) Wait() error {
|
||||
log.Println("Waiting on Group")
|
||||
g.wg.Wait()
|
||||
if g.cancel != nil {
|
||||
log.Println("Group canceling its context after waiting")
|
||||
g.cancel()
|
||||
}
|
||||
return g.err
|
||||
}
|
||||
|
||||
// Cancel cancels the associated context.
|
||||
func (g *Group) Cancel() {
|
||||
log.Println("Group's Cancel method being called")
|
||||
g.cancel()
|
||||
}
|
||||
|
||||
// Wrap wraps a function to be executed in a goroutine.
|
||||
func (g *Group) Wrap(f func() error) func() {
|
||||
g.wg.Add(1)
|
||||
return func() {
|
||||
defer g.wg.Done()
|
||||
|
||||
if err := f(); err != nil {
|
||||
g.errOnce.Do(func() {
|
||||
log.Printf("An error occurred in Group: %s", err)
|
||||
g.err = err
|
||||
if g.cancel != nil {
|
||||
log.Println("Group canceling its context due to error")
|
||||
g.cancel()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Go wraps the provided function and executes it in a goroutine.
|
||||
func (g *Group) Go(f func() error) {
|
||||
wrapped := g.Wrap(f)
|
||||
go wrapped()
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package executil
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func RunAt(ctx context.Context, dir, cmd string, args ...string) error {
|
||||
// Ignore gosec G204 as this function is only used in the build process.
|
||||
//nolint:gosec
|
||||
c := exec.CommandContext(ctx, cmd, args...)
|
||||
c.Dir = dir
|
||||
|
||||
b, err := c.CombinedOutput()
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w. '%s %v': %s", err, cmd, args, string(b))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func Run(ctx context.Context, cmd string, args ...string) error {
|
||||
return RunAt(ctx, ".", cmd, args...)
|
||||
}
|
||||
|
||||
func OutputAt(ctx context.Context, dir, cmd string, args ...string) (string, error) {
|
||||
// Ignore gosec G204 as this function is only used in the build process.
|
||||
//nolint:gosec
|
||||
c := exec.CommandContext(ctx, cmd, args...)
|
||||
c.Dir = dir
|
||||
|
||||
b, err := c.CombinedOutput()
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return strings.TrimSpace(string(b)), nil
|
||||
}
|
||||
|
||||
func Output(ctx context.Context, cmd string, args ...string) (string, error) {
|
||||
return OutputAt(ctx, ".", cmd, args...)
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package fsutil_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/build/fsutil"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestCopyFile(t *testing.T) {
|
||||
src, err := os.CreateTemp("", "")
|
||||
require.NoError(t, err)
|
||||
defer func() {
|
||||
if err := os.RemoveAll(src.Name()); err != nil {
|
||||
t.Log(err)
|
||||
}
|
||||
}()
|
||||
|
||||
err = os.WriteFile(src.Name(), []byte("Contents"), 0600)
|
||||
require.NoError(t, err)
|
||||
|
||||
dst, err := os.CreateTemp("", "")
|
||||
require.NoError(t, err)
|
||||
defer func() {
|
||||
if err := os.RemoveAll(dst.Name()); err != nil {
|
||||
t.Log(err)
|
||||
}
|
||||
}()
|
||||
|
||||
err = fsutil.CopyFile(src.Name(), dst.Name())
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestCopyFile_Permissions(t *testing.T) {
|
||||
perms := os.FileMode(0700)
|
||||
if runtime.GOOS == "windows" {
|
||||
// Windows doesn't have file Unix style file permissions
|
||||
// It seems you have either 0444 for read-only or 0666 for read-write
|
||||
perms = os.FileMode(0666)
|
||||
}
|
||||
|
||||
src, err := os.CreateTemp("", "")
|
||||
require.NoError(t, err)
|
||||
|
||||
defer func() {
|
||||
if err := os.RemoveAll(src.Name()); err != nil {
|
||||
t.Log(err)
|
||||
}
|
||||
}()
|
||||
|
||||
err = os.WriteFile(src.Name(), []byte("Contents"), perms)
|
||||
require.NoError(t, err)
|
||||
err = os.Chmod(src.Name(), perms)
|
||||
require.NoError(t, err)
|
||||
|
||||
dst, err := os.CreateTemp("", "")
|
||||
require.NoError(t, err)
|
||||
defer func() {
|
||||
if err := os.RemoveAll(dst.Name()); err != nil {
|
||||
t.Log(err)
|
||||
}
|
||||
}()
|
||||
|
||||
err = fsutil.CopyFile(src.Name(), dst.Name())
|
||||
require.NoError(t, err)
|
||||
|
||||
fi, err := os.Stat(dst.Name())
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, perms, fi.Mode()&os.ModePerm)
|
||||
}
|
||||
|
||||
// Test case where destination directory doesn't exist.
|
||||
func TestCopyFile_NonExistentDestDir(t *testing.T) {
|
||||
src, err := os.CreateTemp("", "")
|
||||
require.NoError(t, err)
|
||||
defer func() {
|
||||
if err := os.RemoveAll(src.Name()); err != nil {
|
||||
t.Log(err)
|
||||
}
|
||||
}()
|
||||
|
||||
err = fsutil.CopyFile(src.Name(), "non-existent/dest")
|
||||
require.EqualError(t, err, "destination directory doesn't exist: \"non-existent\"")
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package fsutil
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// CopyFile copies a file from src to dst.
|
||||
//
|
||||
// If src and dst files exist, and are the same, then return success. Otherwise, attempt to create a hard link
|
||||
// between the two files. If that fails, copy the file contents from src to dst.
|
||||
func CopyFile(src, dst string) (err error) {
|
||||
absSrc, err := filepath.Abs(src)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get absolute path of source file %q: %w", src, err)
|
||||
}
|
||||
sfi, err := os.Stat(src)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("couldn't stat source file %q: %w", absSrc, err)
|
||||
return
|
||||
}
|
||||
if !sfi.Mode().IsRegular() {
|
||||
// Cannot copy non-regular files (e.g., directories, symlinks, devices, etc.)
|
||||
return fmt.Errorf("non-regular source file %s (%q)", absSrc, sfi.Mode().String())
|
||||
}
|
||||
dpath := filepath.Dir(dst)
|
||||
exists, err := Exists(dpath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !exists {
|
||||
err = fmt.Errorf("destination directory doesn't exist: %q", dpath)
|
||||
return
|
||||
}
|
||||
|
||||
var dfi os.FileInfo
|
||||
dfi, err = os.Stat(dst)
|
||||
if err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if !(dfi.Mode().IsRegular()) {
|
||||
return fmt.Errorf("non-regular destination file %s (%q)", dfi.Name(), dfi.Mode().String())
|
||||
}
|
||||
if os.SameFile(sfi, dfi) {
|
||||
return copyPermissions(sfi.Name(), dfi.Name())
|
||||
}
|
||||
}
|
||||
|
||||
err = copyFileContents(src, dst)
|
||||
return err
|
||||
}
|
||||
|
||||
// copyFileContents copies the contents of the file named src to the file named
|
||||
// by dst. The file will be created if it does not already exist. If the
|
||||
// destination file exists, all it's contents will be replaced by the contents
|
||||
// of the source file.
|
||||
func copyFileContents(src, dst string) (err error) {
|
||||
//nolint:gosec
|
||||
in, err := os.Open(src)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
if err := in.Close(); err != nil {
|
||||
log.Println("error closing file", err)
|
||||
}
|
||||
}()
|
||||
|
||||
//nolint:gosec
|
||||
out, err := os.Create(dst)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
if cerr := out.Close(); cerr != nil && err == nil {
|
||||
err = cerr
|
||||
}
|
||||
}()
|
||||
|
||||
if _, err = io.Copy(out, in); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err := out.Sync(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return copyPermissions(src, dst)
|
||||
}
|
||||
|
||||
func copyPermissions(src, dst string) error {
|
||||
sfi, err := os.Lstat(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := os.Chmod(dst, sfi.Mode()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package fsutil
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
// CreateTempFile generates a temp filepath, based on the provided suffix.
|
||||
// A typical generated path looks like /var/folders/abcd/abcdefg/A/1137975807.
|
||||
func CreateTempFile(sfx string) (string, error) {
|
||||
var suffix string
|
||||
if sfx != "" {
|
||||
suffix = fmt.Sprintf("*-%s", sfx)
|
||||
} else {
|
||||
suffix = sfx
|
||||
}
|
||||
f, err := os.CreateTemp("", suffix)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := f.Close(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return f.Name(), nil
|
||||
}
|
||||
|
||||
// CreateTempDir generates a temp directory, based on the provided suffix.
|
||||
// A typical generated path looks like /var/folders/abcd/abcdefg/A/1137975807/.
|
||||
func CreateTempDir(sfx string) (string, error) {
|
||||
var suffix string
|
||||
if sfx != "" {
|
||||
suffix = fmt.Sprintf("*-%s", sfx)
|
||||
} else {
|
||||
suffix = sfx
|
||||
}
|
||||
dir, err := os.MkdirTemp("", suffix)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return dir, nil
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package fsutil
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestCreateTempFile(t *testing.T) {
|
||||
t.Run("empty suffix, expects pattern like: /var/folders/abcd/abcdefg/A/1137975807", func(t *testing.T) {
|
||||
filePath, err := CreateTempFile("")
|
||||
require.NoError(t, err)
|
||||
|
||||
pathParts := strings.Split(filePath, "/")
|
||||
require.Greater(t, len(pathParts), 1)
|
||||
require.Len(t, strings.Split(pathParts[len(pathParts)-1], "-"), 1)
|
||||
})
|
||||
|
||||
t.Run("non-empty suffix, expects /var/folders/abcd/abcdefg/A/1137975807-foobar", func(t *testing.T) {
|
||||
filePath, err := CreateTempFile("foobar")
|
||||
require.NoError(t, err)
|
||||
|
||||
pathParts := strings.Split(filePath, "/")
|
||||
require.Greater(t, len(pathParts), 1)
|
||||
require.Len(t, strings.Split(pathParts[len(pathParts)-1], "-"), 2)
|
||||
})
|
||||
}
|
||||
|
||||
func TestCreateTempDir(t *testing.T) {
|
||||
t.Run("empty suffix, expects pattern like: /var/folders/abcd/abcdefg/A/1137975807/", func(t *testing.T) {
|
||||
filePath, err := CreateTempFile("")
|
||||
require.NoError(t, err)
|
||||
|
||||
pathParts := strings.Split(filePath, "/")
|
||||
require.Greater(t, len(pathParts), 1)
|
||||
require.Len(t, strings.Split(pathParts[len(pathParts)-1], "-"), 1)
|
||||
})
|
||||
|
||||
t.Run("non-empty suffix, expects /var/folders/abcd/abcdefg/A/1137975807-foobar/", func(t *testing.T) {
|
||||
filePath, err := CreateTempFile("foobar")
|
||||
require.NoError(t, err)
|
||||
|
||||
pathParts := strings.Split(filePath, "/")
|
||||
require.Greater(t, len(pathParts), 1)
|
||||
require.Len(t, strings.Split(pathParts[len(pathParts)-1], "-"), 2)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package fsutil_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/build/fsutil"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestExists_NonExistent(t *testing.T) {
|
||||
exists, err := fsutil.Exists("non-existent")
|
||||
require.NoError(t, err)
|
||||
|
||||
require.False(t, exists)
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package fsutil
|
||||
|
||||
import "os"
|
||||
|
||||
// Exists determines whether a file/directory exists or not.
|
||||
func Exists(fpath string) (bool, error) {
|
||||
_, err := os.Stat(fpath)
|
||||
if err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
return false, err
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package gcloud
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func GetDecodedKey() ([]byte, error) {
|
||||
gcpKey := strings.TrimSpace(os.Getenv("GCP_KEY"))
|
||||
if gcpKey == "" {
|
||||
return nil, fmt.Errorf("the environment variable GCP_KEY must be set")
|
||||
}
|
||||
|
||||
gcpKeyB, err := base64.StdEncoding.DecodeString(gcpKey)
|
||||
if err != nil {
|
||||
// key is not always base64 encoded
|
||||
validKey := []byte(gcpKey)
|
||||
if json.Valid(validKey) {
|
||||
return validKey, nil
|
||||
}
|
||||
return nil, fmt.Errorf("error decoding the gcp_key, err: %q", err)
|
||||
}
|
||||
|
||||
return gcpKeyB, nil
|
||||
}
|
||||
|
||||
func ActivateServiceAccount() error {
|
||||
byteKey, err := GetDecodedKey()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
f, err := os.CreateTemp("", "*.json")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if err := os.Remove(f.Name()); err != nil {
|
||||
log.Printf("error removing %s: %s", f.Name(), err)
|
||||
}
|
||||
}()
|
||||
|
||||
defer func() {
|
||||
if err := f.Close(); err != nil {
|
||||
log.Println("error closing file:", err)
|
||||
}
|
||||
}()
|
||||
|
||||
if _, err := f.Write(byteKey); err != nil {
|
||||
return fmt.Errorf("failed to write GCP key file: %w", err)
|
||||
}
|
||||
keyArg := fmt.Sprintf("--key-file=%s", f.Name())
|
||||
//nolint:gosec
|
||||
cmd := exec.Command("gcloud", "auth", "activate-service-account", keyArg)
|
||||
|
||||
if output, err := cmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("failed to sign into GCP: %w\n%s", err, output)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,457 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"mime"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"cloud.google.com/go/storage"
|
||||
"github.com/grafana/grafana/pkg/build/fsutil"
|
||||
"github.com/grafana/grafana/pkg/build/gcloud"
|
||||
"google.golang.org/api/iterator"
|
||||
"google.golang.org/api/option"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrorNilBucket is returned when a function is called where a bucket argument is expected and the bucket is nil.
|
||||
ErrorNilBucket = errors.New("a bucket must be provided")
|
||||
)
|
||||
|
||||
const (
|
||||
// maxThreads specify the number of max threads that can run at the same time.
|
||||
// Set to 1000, since the maximum number of simultaneous open files for the runners is 1024.
|
||||
maxThreads = 1000
|
||||
)
|
||||
|
||||
// Client wraps the gcloud storage Client with convenient helper functions.
|
||||
// By using an embedded type we can still use the functions provided by storage.Client if we need to.
|
||||
type Client struct {
|
||||
storage.Client
|
||||
}
|
||||
|
||||
// File represents a file in Google Cloud Storage.
|
||||
type File struct {
|
||||
FullPath string
|
||||
PathTrimmed string
|
||||
}
|
||||
|
||||
// New creates a new Client by checking for the Google Cloud SDK auth key and/or environment variable.
|
||||
func New() (*Client, error) {
|
||||
client, err := newClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Client{
|
||||
Client: *client,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// newClient initializes the google-cloud-storage (GCS) client.
|
||||
// It first checks for the application-default_credentials.json file then the GCP_KEY environment variable.
|
||||
func newClient() (*storage.Client, error) {
|
||||
ctx := context.Background()
|
||||
|
||||
byteKey, err := gcloud.GetDecodedKey()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get gcp key, err: %w", err)
|
||||
}
|
||||
client, err := storage.NewClient(ctx, option.WithCredentialsJSON(byteKey))
|
||||
if err != nil {
|
||||
log.Println("failed to login with GCP_KEY, trying with default application credentials...")
|
||||
client, err = storage.NewClient(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open Google Cloud Storage client: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return client, nil
|
||||
}
|
||||
|
||||
// CopyLocalDir copies a local directory 'dir' to the bucket 'bucket' at the path 'bucketPath'.
|
||||
func (client *Client) CopyLocalDir(ctx context.Context, dir string, bucket *storage.BucketHandle, bucketPath string, trim bool) error {
|
||||
if bucket == nil {
|
||||
return ErrorNilBucket
|
||||
}
|
||||
|
||||
files, err := ListLocalFiles(dir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Printf("Number or files to be copied over: %d\n", len(files))
|
||||
|
||||
for _, chunk := range asChunks(files, maxThreads) {
|
||||
var wg sync.WaitGroup
|
||||
for _, f := range chunk {
|
||||
wg.Add(1)
|
||||
go func(file File) {
|
||||
defer wg.Done()
|
||||
err = client.Copy(ctx, file, bucket, bucketPath, trim)
|
||||
if err != nil {
|
||||
log.Printf("failed to copy objects, err: %s\n", err.Error())
|
||||
}
|
||||
}(f)
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Copy copies a single local file into the bucket at the provided path.
|
||||
// trim variable should be set to true if the full object path is needed - false otherwise.
|
||||
func (client *Client) Copy(ctx context.Context, file File, bucket *storage.BucketHandle, remote string, trim bool) error {
|
||||
if bucket == nil {
|
||||
return ErrorNilBucket
|
||||
}
|
||||
|
||||
localFile, err := os.Open(file.FullPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open file %s, err: %q", file.FullPath, err)
|
||||
}
|
||||
defer func() {
|
||||
if err := localFile.Close(); err != nil {
|
||||
log.Println("failed to close localfile", "err", err)
|
||||
}
|
||||
}()
|
||||
|
||||
extension := strings.ToLower(path.Ext(file.FullPath))
|
||||
contentType := mime.TypeByExtension(extension)
|
||||
|
||||
filePath := file.FullPath
|
||||
if trim {
|
||||
filePath = file.PathTrimmed
|
||||
}
|
||||
|
||||
objectPath := path.Join(remote, filePath)
|
||||
|
||||
wc := bucket.Object(objectPath).NewWriter(ctx)
|
||||
wc.ContentType = contentType
|
||||
defer func() {
|
||||
if err := wc.Close(); err != nil {
|
||||
log.Println("failed to close writer", "err", err)
|
||||
}
|
||||
}()
|
||||
|
||||
if _, err = io.Copy(wc, localFile); err != nil {
|
||||
return fmt.Errorf("failed to copy to Cloud Storage: %w", err)
|
||||
}
|
||||
|
||||
log.Printf("Successfully uploaded tarball to Google Cloud Storage, path: %s/%s\n", remote, file.FullPath)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CopyRemoteDir copies an entire directory 'from' from the bucket 'fromBucket' into the 'toBucket' at the path 'to'.
|
||||
func (client *Client) CopyRemoteDir(ctx context.Context, fromBucket *storage.BucketHandle, from string, toBucket *storage.BucketHandle, to string) error {
|
||||
if toBucket == nil || fromBucket == nil {
|
||||
return ErrorNilBucket
|
||||
}
|
||||
|
||||
files, err := ListRemoteFiles(ctx, fromBucket, FilesFilter{Prefix: from})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var ch = make(chan File, len(files))
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(maxThreads)
|
||||
|
||||
for i := 0; i < maxThreads; i++ {
|
||||
go func() {
|
||||
for {
|
||||
file, ok := <-ch
|
||||
if !ok {
|
||||
wg.Done()
|
||||
return
|
||||
}
|
||||
if err := client.RemoteCopy(ctx, file, fromBucket, toBucket, to); err != nil {
|
||||
log.Printf("failed to copy files between buckets: err: %s\n", err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
ch <- file
|
||||
}
|
||||
|
||||
close(ch)
|
||||
wg.Wait()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoteCopy will copy the file 'file' from the 'fromBucket' to the 'toBucket' at the path 'path'.
|
||||
func (client *Client) RemoteCopy(ctx context.Context, file File, fromBucket, toBucket *storage.BucketHandle, path string) error {
|
||||
// Should this be path.Join instead of filepath.Join? filepath.Join on Windows will produce `\\` separators instead of `/`.
|
||||
var (
|
||||
src = fromBucket.Object(file.FullPath)
|
||||
dstObject = filepath.Join(path, file.PathTrimmed)
|
||||
dst = toBucket.Object(dstObject)
|
||||
)
|
||||
|
||||
if _, err := dst.CopierFrom(src).Run(ctx); err != nil {
|
||||
return fmt.Errorf("failed to copy object %s, to %s, err: %w", file.FullPath, dstObject, err)
|
||||
}
|
||||
|
||||
log.Printf("%s was successfully copied to %v bucket!.\n\n", file.FullPath, toBucket)
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteDir deletes a directory at 'path' from the bucket.
|
||||
func (client *Client) DeleteDir(ctx context.Context, bucket *storage.BucketHandle, path string) error {
|
||||
if bucket == nil {
|
||||
return ErrorNilBucket
|
||||
}
|
||||
|
||||
files, err := ListRemoteFiles(ctx, bucket, FilesFilter{Prefix: path})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var ch = make(chan string, len(files))
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(maxThreads)
|
||||
|
||||
for i := 0; i < maxThreads; i++ {
|
||||
go func() {
|
||||
for {
|
||||
fullPath, ok := <-ch
|
||||
if !ok {
|
||||
wg.Done()
|
||||
return
|
||||
}
|
||||
err := client.Delete(ctx, bucket, fullPath)
|
||||
if err != nil && !errors.Is(err, storage.ErrObjectNotExist) {
|
||||
log.Printf("failed to delete objects, err %s\n", err.Error())
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
ch <- file.FullPath
|
||||
}
|
||||
|
||||
close(ch)
|
||||
wg.Wait()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete deletes single item from the bucket at 'path'.
|
||||
func (client *Client) Delete(ctx context.Context, bucket *storage.BucketHandle, path string) error {
|
||||
object := bucket.Object(path)
|
||||
if err := object.Delete(ctx); err != nil {
|
||||
return fmt.Errorf("cannot delete %s, err: %w", path, err)
|
||||
}
|
||||
log.Printf("Successfully deleted tarball to Google Cloud Storage, path: %s", path)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListLocalFiles lists files in a local filesystem.
|
||||
func ListLocalFiles(dir string) ([]File, error) {
|
||||
var files []File
|
||||
err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
|
||||
if !info.IsDir() {
|
||||
files = append(files, File{
|
||||
FullPath: path,
|
||||
// Strip the dir name from the filepath
|
||||
PathTrimmed: strings.ReplaceAll(path, dir, ""),
|
||||
})
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error walking path: %v", err)
|
||||
}
|
||||
|
||||
return files, nil
|
||||
}
|
||||
|
||||
type FilesFilter struct {
|
||||
Prefix string
|
||||
FileExts []string
|
||||
}
|
||||
|
||||
// ListRemoteFiles lists all the files in the directory (filtering by FilesFilter) and returns a File struct for each one.
|
||||
func ListRemoteFiles(ctx context.Context, bucket *storage.BucketHandle, filter FilesFilter) ([]File, error) {
|
||||
if bucket == nil {
|
||||
return []File{}, ErrorNilBucket
|
||||
}
|
||||
|
||||
it := bucket.Objects(ctx, &storage.Query{
|
||||
Prefix: filter.Prefix,
|
||||
})
|
||||
|
||||
var files []File
|
||||
for {
|
||||
attrs, err := it.Next()
|
||||
if err != nil {
|
||||
if errors.Is(err, iterator.Done) {
|
||||
break
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to iterate through bucket, err: %w", err)
|
||||
}
|
||||
|
||||
extMatch := len(filter.FileExts) == 0
|
||||
for _, ext := range filter.FileExts {
|
||||
if ext == filepath.Ext(attrs.Name) {
|
||||
extMatch = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if extMatch {
|
||||
files = append(files, File{FullPath: attrs.Name, PathTrimmed: strings.TrimPrefix(attrs.Name, filter.Prefix)})
|
||||
}
|
||||
}
|
||||
|
||||
return files, nil
|
||||
}
|
||||
|
||||
// DownloadDirectory downloads files from bucket (filtering by FilesFilter) to destPath on disk.
|
||||
func (client *Client) DownloadDirectory(ctx context.Context, bucket *storage.BucketHandle, destPath string, filter FilesFilter) error {
|
||||
if bucket == nil {
|
||||
return ErrorNilBucket
|
||||
}
|
||||
|
||||
files, err := ListRemoteFiles(ctx, bucket, filter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// return err if dir already exists
|
||||
exists, err := fsutil.Exists(destPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if exists {
|
||||
return fmt.Errorf("destination path %q already exists", destPath)
|
||||
}
|
||||
|
||||
err = os.MkdirAll(destPath, 0750)
|
||||
if err != nil && !os.IsExist(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
err = client.downloadFile(ctx, bucket, file.FullPath, filepath.Join(destPath, file.PathTrimmed))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetLatestMainBuild gets the latest main build which is successfully uploaded to the gcs bucket.
|
||||
func GetLatestMainBuild(ctx context.Context, bucket *storage.BucketHandle, path string) (string, error) {
|
||||
if bucket == nil {
|
||||
return "", ErrorNilBucket
|
||||
}
|
||||
|
||||
it := bucket.Objects(ctx, &storage.Query{
|
||||
Prefix: path,
|
||||
})
|
||||
|
||||
var files []string
|
||||
for {
|
||||
attrs, err := it.Next()
|
||||
if errors.Is(err, iterator.Done) {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to iterate through bucket, err: %w", err)
|
||||
}
|
||||
|
||||
files = append(files, attrs.Name)
|
||||
}
|
||||
|
||||
var latestVersion string
|
||||
for i := len(files) - 1; i >= 0; i-- {
|
||||
captureVersion := regexp.MustCompile(`(\d+\.\d+\.\d+-\d+pre)`)
|
||||
if captureVersion.MatchString(files[i]) {
|
||||
latestVersion = captureVersion.FindString(files[i])
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return latestVersion, nil
|
||||
}
|
||||
|
||||
// downloadFile downloads an object to a file.
|
||||
func (client *Client) downloadFile(ctx context.Context, bucket *storage.BucketHandle, objectName, destFileName string) error {
|
||||
if bucket == nil {
|
||||
return ErrorNilBucket
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(ctx, time.Second*10)
|
||||
defer cancel()
|
||||
|
||||
// nolint:gosec
|
||||
f, err := os.Create(destFileName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("os.Create: %v", err)
|
||||
}
|
||||
|
||||
rc, err := bucket.Object(objectName).NewReader(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Object(%q).NewReader: %v", objectName, err)
|
||||
}
|
||||
defer func() {
|
||||
if err := rc.Close(); err != nil {
|
||||
log.Println("failed to close reader", "err", err)
|
||||
}
|
||||
}()
|
||||
|
||||
if _, err := io.Copy(f, rc); err != nil {
|
||||
return fmt.Errorf("io.Copy: %v", err)
|
||||
}
|
||||
|
||||
if err = f.Close(); err != nil {
|
||||
return fmt.Errorf("f.Close: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// asChunks will split the supplied []File into slices with a max size of `chunkSize`
|
||||
// []string{"a", "b", "c"}, 1 => [][]string{[]string{"a"}, []string{"b"}, []string{"c"}}
|
||||
// []string{"a", "b", "c"}, 2 => [][]string{[]string{"a", "b"}, []string{"c"}}.
|
||||
func asChunks(files []File, chunkSize int) [][]File {
|
||||
var fileChunks [][]File
|
||||
|
||||
if len(files) == 0 {
|
||||
return [][]File{}
|
||||
}
|
||||
|
||||
if len(files) > chunkSize && chunkSize > 0 {
|
||||
for i := 0; i < len(files); i += chunkSize {
|
||||
end := i + chunkSize
|
||||
|
||||
if end > len(files) {
|
||||
end = len(files)
|
||||
}
|
||||
fileChunks = append(fileChunks, files[i:end])
|
||||
}
|
||||
} else {
|
||||
fileChunks = [][]File{files}
|
||||
}
|
||||
return fileChunks
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_asChunks(t *testing.T) {
|
||||
type args struct {
|
||||
files []File
|
||||
chunkSize int
|
||||
}
|
||||
tcs := []struct {
|
||||
name string
|
||||
args args
|
||||
expected [][]File
|
||||
}{
|
||||
{
|
||||
name: "Happy path #1",
|
||||
args: args{
|
||||
files: []File{
|
||||
{FullPath: "/a"},
|
||||
{FullPath: "/b"},
|
||||
{FullPath: "/c"},
|
||||
{FullPath: "/1"},
|
||||
{FullPath: "/2"},
|
||||
{FullPath: "/3"},
|
||||
},
|
||||
chunkSize: 5,
|
||||
},
|
||||
expected: [][]File{
|
||||
{{FullPath: "/a"}, {FullPath: "/b"}, {FullPath: "/c"}, {FullPath: "/1"}, {FullPath: "/2"}},
|
||||
{{FullPath: "/3"}},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Happy path #2",
|
||||
args: args{
|
||||
files: []File{
|
||||
{FullPath: "/a"},
|
||||
{FullPath: "/b"},
|
||||
{FullPath: "/c"},
|
||||
{FullPath: "/1"},
|
||||
{FullPath: "/2"},
|
||||
{FullPath: "/3"},
|
||||
},
|
||||
chunkSize: 2,
|
||||
},
|
||||
expected: [][]File{
|
||||
{{FullPath: "/a"}, {FullPath: "/b"}},
|
||||
{{FullPath: "/c"}, {FullPath: "/1"}},
|
||||
{{FullPath: "/2"}, {FullPath: "/3"}},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Happy path #3",
|
||||
args: args{
|
||||
files: []File{
|
||||
{FullPath: "/a"},
|
||||
{FullPath: "/b"},
|
||||
{FullPath: "/c"},
|
||||
},
|
||||
chunkSize: 1,
|
||||
},
|
||||
expected: [][]File{
|
||||
{{FullPath: "/a"}},
|
||||
{{FullPath: "/b"}},
|
||||
{{FullPath: "/c"}},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "A chunkSize with 0 value returns the input as a single chunk",
|
||||
args: args{
|
||||
files: []File{
|
||||
{FullPath: "/a"},
|
||||
{FullPath: "/b"},
|
||||
{FullPath: "/c"},
|
||||
},
|
||||
chunkSize: 0,
|
||||
},
|
||||
expected: [][]File{
|
||||
{
|
||||
{FullPath: "/a"},
|
||||
{FullPath: "/b"},
|
||||
{FullPath: "/c"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "A chunkSize with negative value returns the input as a single chunk",
|
||||
args: args{
|
||||
files: []File{
|
||||
{FullPath: "/a"},
|
||||
{FullPath: "/b"},
|
||||
{FullPath: "/c"},
|
||||
},
|
||||
chunkSize: -1,
|
||||
},
|
||||
expected: [][]File{
|
||||
{
|
||||
{FullPath: "/a"},
|
||||
{FullPath: "/b"},
|
||||
{FullPath: "/c"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "A chunkSize greater than the size on input returns the input as a single chunk",
|
||||
args: args{
|
||||
files: []File{
|
||||
{FullPath: "/a"},
|
||||
{FullPath: "/b"},
|
||||
{FullPath: "/c"},
|
||||
},
|
||||
chunkSize: 5,
|
||||
},
|
||||
expected: [][]File{
|
||||
{
|
||||
{FullPath: "/a"},
|
||||
{FullPath: "/b"},
|
||||
{FullPath: "/c"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "A chunkSize equal the size on input returns the input as a single chunk",
|
||||
args: args{
|
||||
files: []File{
|
||||
{FullPath: "/a"},
|
||||
{FullPath: "/b"},
|
||||
{FullPath: "/c"},
|
||||
},
|
||||
chunkSize: 3,
|
||||
},
|
||||
expected: [][]File{
|
||||
{
|
||||
{FullPath: "/a"},
|
||||
{FullPath: "/b"},
|
||||
{FullPath: "/c"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "An empty input returns empty chunks",
|
||||
args: args{
|
||||
files: []File{},
|
||||
chunkSize: 3,
|
||||
},
|
||||
expected: [][]File{},
|
||||
},
|
||||
}
|
||||
for _, tc := range tcs {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result := asChunks(tc.args.files, tc.args.chunkSize)
|
||||
require.Equal(t, tc.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package git
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
const (
|
||||
MainBranch = "main"
|
||||
HomeDir = "."
|
||||
RepoOwner = "grafana"
|
||||
OSSRepo = "grafana"
|
||||
EnterpriseRepo = "grafana-enterprise"
|
||||
EnterpriseCheckName = "Grafana Enterprise"
|
||||
EnterpriseCheckDescription = "Downstream tests to ensure that your changes are compatible with Grafana Enterprise"
|
||||
)
|
||||
|
||||
func PRCheckRegexp() *regexp.Regexp {
|
||||
reBranch, err := regexp.Compile(`^prc-([0-9]+)-([A-Za-z0-9]+)\/(.+)$`)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("Failed to compile regexp: %s", err))
|
||||
}
|
||||
|
||||
return reBranch
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package git_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/build/git"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestPRCheckRegexp(t *testing.T) {
|
||||
type match struct {
|
||||
String string
|
||||
Commit string
|
||||
Branch string
|
||||
PR string
|
||||
}
|
||||
|
||||
var (
|
||||
shouldMatch = []match{
|
||||
{
|
||||
String: "prc-1-a1b2c3d4/branch-name",
|
||||
Branch: "branch-name",
|
||||
Commit: "a1b2c3d4",
|
||||
PR: "1",
|
||||
},
|
||||
{
|
||||
String: "prc-111-a1b2c3d4/branch/name",
|
||||
Branch: "branch/name",
|
||||
Commit: "a1b2c3d4",
|
||||
PR: "111",
|
||||
},
|
||||
{
|
||||
String: "prc-102930122-a1b2c3d4/branch-name",
|
||||
Branch: "branch-name",
|
||||
Commit: "a1b2c3d4",
|
||||
PR: "102930122",
|
||||
},
|
||||
}
|
||||
|
||||
shouldNotMatch = []string{"prc-a/branch", "km/test", "test", "prc", "prc/test", "price"}
|
||||
)
|
||||
|
||||
regex := git.PRCheckRegexp()
|
||||
|
||||
for _, v := range shouldMatch {
|
||||
assert.Truef(t, regex.MatchString(v.String), "regex '%s' should match %s", regex.String(), v)
|
||||
m := regex.FindStringSubmatch(v.String)
|
||||
assert.Equal(t, m[1], v.PR)
|
||||
assert.Equal(t, m[2], v.Commit)
|
||||
assert.Equal(t, m[3], v.Branch)
|
||||
}
|
||||
|
||||
for _, v := range shouldNotMatch {
|
||||
assert.False(t, regex.MatchString(v), "regex '%s' should not match %s", regex.String(), v)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
package golangutils
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/grafana/pkg/build/config"
|
||||
)
|
||||
|
||||
type BuildOpts struct {
|
||||
// Package refers to the path to the `main` package containing `func main`
|
||||
Package string
|
||||
|
||||
// Output is used as the -o argument in the go build command
|
||||
Output string
|
||||
|
||||
// Workdir should define some place in the module where the package path resolves.
|
||||
// Go commands need to be ran inside a the Go module directory.
|
||||
Workdir string
|
||||
|
||||
GoOS config.OS
|
||||
GoArch config.Architecture
|
||||
GoArm string
|
||||
Go386 string
|
||||
CC string
|
||||
LibC string
|
||||
|
||||
CGoEnabled bool
|
||||
CGoCFlags string
|
||||
|
||||
// LdFlags are joined by a space character and provided to the -ldflags argument.
|
||||
// A valid element here would be `-X 'main.version=1.0.0'`.
|
||||
LdFlags []string
|
||||
|
||||
Stdout io.ReadWriter
|
||||
Stderr io.ReadWriter
|
||||
Stdin io.ReadWriter
|
||||
|
||||
// ExtraEnv allows consumers to provide extra env args that are not defined above.
|
||||
// A single element should be formatted using like so: {NAME}={VALUE}. Example: GOOS=linux.
|
||||
ExtraEnv []string
|
||||
|
||||
// ExtraArgs allows consumers to provide extra arguments that are not defined above.
|
||||
// Flag names and values should be two separate elements.
|
||||
// These flags will be appended to the command arguments before the package path in "go build".
|
||||
ExtraArgs []string
|
||||
}
|
||||
|
||||
// Env constructs a list of key/value pairs for setting a build command's environment.
|
||||
// Should we consider using something to unmarshal the struct to env?
|
||||
func (opts BuildOpts) Env() []string {
|
||||
env := []string{}
|
||||
if opts.CGoEnabled {
|
||||
env = append(env, "CGO_ENABLED=1")
|
||||
}
|
||||
|
||||
if opts.GoOS != "" {
|
||||
env = append(env, fmt.Sprintf("GOOS=%s", opts.GoOS))
|
||||
}
|
||||
|
||||
if opts.GoArch != "" {
|
||||
env = append(env, fmt.Sprintf("GOARCH=%s", opts.GoArch))
|
||||
}
|
||||
|
||||
if opts.CC != "" {
|
||||
env = append(env, fmt.Sprintf("CC=%s", opts.CC))
|
||||
}
|
||||
|
||||
if opts.CGoCFlags != "" {
|
||||
env = append(env, fmt.Sprintf("CGO_CFLAGS=%s", opts.CGoCFlags))
|
||||
}
|
||||
|
||||
if opts.GoArm != "" {
|
||||
env = append(env, fmt.Sprintf("GOARM=%s", opts.GoArm))
|
||||
}
|
||||
|
||||
if opts.ExtraEnv != nil {
|
||||
return append(opts.ExtraEnv, env...)
|
||||
}
|
||||
|
||||
return env
|
||||
}
|
||||
|
||||
// Args constructs a list of flags and values for use with the exec.Command type when running "go build".
|
||||
func (opts BuildOpts) Args() []string {
|
||||
args := []string{}
|
||||
|
||||
if opts.LdFlags != nil {
|
||||
args = append(args, "-ldflags", strings.Join(opts.LdFlags, " "))
|
||||
}
|
||||
|
||||
if opts.Output != "" {
|
||||
args = append(args, "-o", opts.Output)
|
||||
}
|
||||
|
||||
if opts.ExtraArgs != nil {
|
||||
args = append(args, opts.ExtraArgs...)
|
||||
}
|
||||
|
||||
args = append(args, opts.Package)
|
||||
|
||||
return args
|
||||
}
|
||||
|
||||
// Build runs the go build process in the current shell given the opts.
|
||||
// This function will panic if no Stdout/Stderr/Stdin is provided in the opts.
|
||||
func RunBuild(ctx context.Context, opts BuildOpts) error {
|
||||
env := opts.Env()
|
||||
args := append([]string{"build"}, opts.Args()...)
|
||||
// Ignore gosec G304 as this function is only used in the build process.
|
||||
//nolint:gosec
|
||||
cmd := exec.CommandContext(ctx, "go", args...)
|
||||
cmd.Env = env
|
||||
|
||||
cmd.Stdout = opts.Stdout
|
||||
cmd.Stderr = opts.Stderr
|
||||
cmd.Stdin = opts.Stdin
|
||||
cmd.Dir = opts.Workdir
|
||||
|
||||
return cmd.Run()
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
// Package golangutils holds utility functions, wrappers, and types for building Go binaries for Grafana.
|
||||
package golangutils
|
||||
@@ -0,0 +1,123 @@
|
||||
package grafana
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/grafana/grafana/pkg/build/config"
|
||||
"github.com/grafana/grafana/pkg/build/cryptoutil"
|
||||
"github.com/grafana/grafana/pkg/build/golangutils"
|
||||
)
|
||||
|
||||
var binaries = []string{"grafana", "grafana-server", "grafana-cli"}
|
||||
|
||||
const (
|
||||
SuffixEnterprise2 = "-enterprise2"
|
||||
)
|
||||
|
||||
const (
|
||||
ExtensionExe = ".exe"
|
||||
)
|
||||
|
||||
func GrafanaLDFlags(version string, r config.Revision) []string {
|
||||
return []string{
|
||||
"-w",
|
||||
fmt.Sprintf("-X main.version=%s", version),
|
||||
fmt.Sprintf("-X main.commit=%s", r.SHA256),
|
||||
fmt.Sprintf("-X main.buildstamp=%d", r.Timestamp),
|
||||
fmt.Sprintf("-X main.buildBranch=%s", r.Branch),
|
||||
}
|
||||
}
|
||||
|
||||
// BinaryFolder returns the path to where the Grafana binary is build given the provided arguments.
|
||||
func BinaryFolder(edition config.Edition, args BuildArgs) string {
|
||||
sfx := ""
|
||||
if edition == config.EditionEnterprise2 {
|
||||
sfx = SuffixEnterprise2
|
||||
}
|
||||
|
||||
arch := string(args.GoArch)
|
||||
if args.GoArch == config.ArchARM {
|
||||
arch = string(args.GoArch) + "v" + args.GoArm
|
||||
}
|
||||
|
||||
format := fmt.Sprintf("%s-%s", args.GoOS, arch)
|
||||
if args.LibC != "" {
|
||||
format += fmt.Sprintf("-%s", args.LibC)
|
||||
}
|
||||
format += sfx
|
||||
|
||||
if args.GoOS == config.OSWindows {
|
||||
format += ExtensionExe
|
||||
}
|
||||
|
||||
return format
|
||||
}
|
||||
|
||||
func GrafanaDescriptor(opts golangutils.BuildOpts) string {
|
||||
libcPart := ""
|
||||
if opts.LibC != "" {
|
||||
libcPart = fmt.Sprintf("/%s", opts.LibC)
|
||||
}
|
||||
arch := string(opts.GoArch)
|
||||
if opts.GoArch == config.ArchARM {
|
||||
arch = string(opts.GoArch) + "v" + opts.GoArm
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s/%s%s", opts.GoOS, arch, libcPart)
|
||||
}
|
||||
|
||||
// BuildGrafanaBinary builds a certain binary according to certain parameters.
|
||||
func BuildGrafanaBinary(ctx context.Context, name, version string, args BuildArgs, edition config.Edition) error {
|
||||
opts := args.BuildOpts
|
||||
opts.ExtraEnv = os.Environ()
|
||||
|
||||
revision, err := config.GrafanaRevision(ctx, opts.Workdir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
folder := BinaryFolder(edition, args)
|
||||
|
||||
if opts.GoOS == config.OSWindows {
|
||||
name += ExtensionExe
|
||||
}
|
||||
|
||||
binary := filepath.Join(opts.Workdir, "bin", folder, name)
|
||||
opts.Output = binary
|
||||
|
||||
if err := os.RemoveAll(binary); err != nil {
|
||||
return fmt.Errorf("failed to remove %q: %w", binary, err)
|
||||
}
|
||||
|
||||
if err := os.RemoveAll(binary + ".md5"); err != nil {
|
||||
return fmt.Errorf("failed to remove %q: %w", binary+".md5", err)
|
||||
}
|
||||
|
||||
descriptor := GrafanaDescriptor(opts)
|
||||
|
||||
log.Printf("Building %q for %s\nwith env: %v", binary, descriptor, opts.Env())
|
||||
|
||||
opts.LdFlags = append(args.LdFlags, GrafanaLDFlags(version, revision)...)
|
||||
|
||||
if edition == config.EditionEnterprise2 {
|
||||
opts.ExtraArgs = []string{"-tags=pro"}
|
||||
}
|
||||
|
||||
log.Printf("Running command 'go %s'", opts.Args())
|
||||
|
||||
if err := golangutils.RunBuild(ctx, opts); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create an MD5 checksum of the binary, to be included in the archive for
|
||||
// automatic upgrades.
|
||||
if err := cryptoutil.MD5File(binary); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
package grafana
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/grafana/grafana/pkg/build/compilers"
|
||||
"github.com/grafana/grafana/pkg/build/config"
|
||||
"github.com/grafana/grafana/pkg/build/golangutils"
|
||||
)
|
||||
|
||||
// BuildArgs represent the build parameters that define the "go build" behavior of a single variant.
|
||||
// These arguments are applied as environment variables and arguments to the "go build" command.
|
||||
type BuildArgs struct {
|
||||
golangutils.BuildOpts
|
||||
DebArch config.Architecture
|
||||
RPMArch config.Architecture
|
||||
}
|
||||
|
||||
type BuildVariantOpts struct {
|
||||
Variant config.Variant
|
||||
Edition config.Edition
|
||||
|
||||
Version string
|
||||
GrafanaDir string
|
||||
}
|
||||
|
||||
// BuildVariant builds a certain variant of the grafana-server and grafana-cli binaries sequentially.
|
||||
func BuildVariant(ctx context.Context, opts BuildVariantOpts) error {
|
||||
grafanaDir, err := filepath.Abs(opts.GrafanaDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var (
|
||||
args = VariantBuildArgs(opts.Variant)
|
||||
)
|
||||
|
||||
for _, binary := range binaries {
|
||||
// Note that for Golang cmd paths we must use the relative path and the Linux file separators (/) even for Windows users.
|
||||
var (
|
||||
pkg = fmt.Sprintf("./pkg/cmd/%s", binary)
|
||||
stdout = bytes.NewBuffer(nil)
|
||||
stderr = bytes.NewBuffer(nil)
|
||||
)
|
||||
|
||||
args.Workdir = grafanaDir
|
||||
args.Stdout = stdout
|
||||
args.Stderr = stderr
|
||||
args.Package = pkg
|
||||
|
||||
if err := BuildGrafanaBinary(ctx, binary, opts.Version, args, opts.Edition); err != nil {
|
||||
return fmt.Errorf("failed to build %s for %s: %w\nstdout: %s\nstderr: %s", pkg, opts.Variant, err, stdout.String(), stderr.String())
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var ldFlagsStatic = []string{"-linkmode=external", "-extldflags=-static"}
|
||||
|
||||
var variantArgs = map[config.Variant]BuildArgs{
|
||||
config.VariantArmV6: {
|
||||
BuildOpts: golangutils.BuildOpts{
|
||||
GoOS: config.OSLinux,
|
||||
CGoEnabled: true,
|
||||
GoArch: config.ArchARM,
|
||||
GoArm: "6",
|
||||
CC: compilers.ArmV6,
|
||||
},
|
||||
DebArch: config.ArchARMHF,
|
||||
},
|
||||
config.VariantArmV7: {
|
||||
BuildOpts: golangutils.BuildOpts{
|
||||
GoOS: config.OSLinux,
|
||||
CGoEnabled: true,
|
||||
GoArch: config.ArchARM,
|
||||
GoArm: "7",
|
||||
CC: compilers.Armv7,
|
||||
},
|
||||
DebArch: config.ArchARMHF,
|
||||
RPMArch: config.ArchARMHFP,
|
||||
},
|
||||
config.VariantArmV7Musl: {
|
||||
BuildOpts: golangutils.BuildOpts{
|
||||
GoOS: config.OSLinux,
|
||||
CGoEnabled: true,
|
||||
GoArch: config.ArchARM,
|
||||
GoArm: "7",
|
||||
LibC: config.LibCMusl,
|
||||
CC: compilers.Armv7Musl,
|
||||
LdFlags: ldFlagsStatic,
|
||||
},
|
||||
},
|
||||
config.VariantArm64: {
|
||||
BuildOpts: golangutils.BuildOpts{
|
||||
GoOS: config.OSLinux,
|
||||
CGoEnabled: true,
|
||||
GoArch: config.ArchARM64,
|
||||
CC: compilers.Arm64,
|
||||
},
|
||||
DebArch: config.ArchARM64,
|
||||
RPMArch: "aarch64",
|
||||
},
|
||||
config.VariantArm64Musl: {
|
||||
BuildOpts: golangutils.BuildOpts{
|
||||
GoOS: config.OSLinux,
|
||||
GoArch: config.ArchARM64,
|
||||
CGoEnabled: true,
|
||||
CC: compilers.Arm64Musl,
|
||||
LibC: config.LibCMusl,
|
||||
LdFlags: ldFlagsStatic,
|
||||
},
|
||||
},
|
||||
config.VariantDarwinAmd64: {
|
||||
BuildOpts: golangutils.BuildOpts{
|
||||
GoOS: config.OSDarwin,
|
||||
CGoEnabled: true,
|
||||
GoArch: config.ArchAMD64,
|
||||
CC: compilers.Osx64,
|
||||
},
|
||||
},
|
||||
config.VariantWindowsAmd64: {
|
||||
BuildOpts: golangutils.BuildOpts{
|
||||
GoOS: config.OSWindows,
|
||||
GoArch: config.ArchAMD64,
|
||||
CC: compilers.Win64,
|
||||
CGoEnabled: true,
|
||||
CGoCFlags: "-D_WIN32_WINNT=0x0601",
|
||||
},
|
||||
},
|
||||
config.VariantLinuxAmd64: {
|
||||
BuildOpts: golangutils.BuildOpts{
|
||||
GoOS: config.OSLinux,
|
||||
GoArch: config.ArchAMD64,
|
||||
CC: compilers.LinuxX64,
|
||||
},
|
||||
DebArch: config.ArchAMD64,
|
||||
RPMArch: config.ArchAMD64,
|
||||
},
|
||||
config.VariantLinuxAmd64Musl: {
|
||||
BuildOpts: golangutils.BuildOpts{
|
||||
GoOS: config.OSLinux,
|
||||
GoArch: config.ArchAMD64,
|
||||
CC: compilers.LinuxX64Musl,
|
||||
LibC: config.LibCMusl,
|
||||
LdFlags: ldFlagsStatic,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func VariantBuildArgs(v config.Variant) BuildArgs {
|
||||
if val, ok := variantArgs[v]; ok {
|
||||
return val
|
||||
}
|
||||
|
||||
return BuildArgs{}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
package packaging
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/grafana/pkg/build/config"
|
||||
)
|
||||
|
||||
const ReleaseFolder = "release"
|
||||
const MainFolder = "main"
|
||||
const EnterpriseSfx = "-enterprise"
|
||||
const CacheSettings = "Cache-Control:public, max-age="
|
||||
|
||||
type buildArtifact struct {
|
||||
Os string
|
||||
Arch string
|
||||
urlPostfix string
|
||||
packagePostfix string
|
||||
}
|
||||
|
||||
type PublishConfig struct {
|
||||
config.Config
|
||||
|
||||
Edition config.Edition
|
||||
ReleaseMode config.ReleaseMode
|
||||
GrafanaAPIKey string
|
||||
WhatsNewURL string
|
||||
ReleaseNotesURL string
|
||||
DryRun bool
|
||||
TTL string
|
||||
SimulateRelease bool
|
||||
}
|
||||
|
||||
const rhelOS = "rhel"
|
||||
const debOS = "deb"
|
||||
|
||||
func (t buildArtifact) GetURL(baseArchiveURL string, cfg PublishConfig) string {
|
||||
rev := ""
|
||||
prefix := "-"
|
||||
if t.Os == debOS {
|
||||
prefix = "_"
|
||||
} else if t.Os == rhelOS {
|
||||
rev = "-1"
|
||||
}
|
||||
|
||||
version := cfg.Version
|
||||
verComponents := strings.Split(version, "-")
|
||||
if len(verComponents) > 2 {
|
||||
panic(fmt.Sprintf("Version string contains more than one hyphen: %q", version))
|
||||
}
|
||||
|
||||
switch t.Os {
|
||||
case debOS, rhelOS:
|
||||
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).
|
||||
version = fmt.Sprintf("%s~%s", verComponents[0], verComponents[1])
|
||||
}
|
||||
}
|
||||
|
||||
// https://dl.grafana.com/oss/main/grafana_8.5.0~54094pre_armhf.deb: 404 Not Found
|
||||
url := fmt.Sprintf("%s%s%s%s%s%s", baseArchiveURL, t.packagePostfix, prefix, version, rev, t.urlPostfix)
|
||||
return url
|
||||
}
|
||||
|
||||
var ArtifactConfigs = []buildArtifact{
|
||||
{
|
||||
Os: debOS,
|
||||
Arch: "arm64",
|
||||
urlPostfix: "_arm64.deb",
|
||||
},
|
||||
{
|
||||
Os: rhelOS,
|
||||
Arch: "arm64",
|
||||
urlPostfix: ".aarch64.rpm",
|
||||
},
|
||||
{
|
||||
Os: "linux",
|
||||
Arch: "arm64",
|
||||
urlPostfix: ".linux-arm64.tar.gz",
|
||||
},
|
||||
{
|
||||
Os: debOS,
|
||||
Arch: "armv7",
|
||||
urlPostfix: "_armhf.deb",
|
||||
},
|
||||
{
|
||||
Os: debOS,
|
||||
Arch: "armv6",
|
||||
packagePostfix: "-rpi",
|
||||
urlPostfix: "_armhf.deb",
|
||||
},
|
||||
{
|
||||
Os: rhelOS,
|
||||
Arch: "armv7",
|
||||
urlPostfix: ".armhfp.rpm",
|
||||
},
|
||||
{
|
||||
Os: "linux",
|
||||
Arch: "armv6",
|
||||
urlPostfix: ".linux-armv6.tar.gz",
|
||||
},
|
||||
{
|
||||
Os: "linux",
|
||||
Arch: "armv7",
|
||||
urlPostfix: ".linux-armv7.tar.gz",
|
||||
},
|
||||
{
|
||||
Os: "darwin",
|
||||
Arch: "amd64",
|
||||
urlPostfix: ".darwin-amd64.tar.gz",
|
||||
},
|
||||
{
|
||||
Os: "deb",
|
||||
Arch: "amd64",
|
||||
urlPostfix: "_amd64.deb",
|
||||
},
|
||||
{
|
||||
Os: rhelOS,
|
||||
Arch: "amd64",
|
||||
urlPostfix: ".x86_64.rpm",
|
||||
},
|
||||
{
|
||||
Os: "linux",
|
||||
Arch: "amd64",
|
||||
urlPostfix: ".linux-amd64.tar.gz",
|
||||
},
|
||||
{
|
||||
Os: "win",
|
||||
Arch: "amd64",
|
||||
urlPostfix: ".windows-amd64.zip",
|
||||
},
|
||||
{
|
||||
Os: "win-installer",
|
||||
Arch: "amd64",
|
||||
urlPostfix: ".windows-amd64.msi",
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
package packaging
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/urfave/cli/v2"
|
||||
|
||||
"github.com/grafana/grafana/pkg/build/config"
|
||||
"github.com/grafana/grafana/pkg/build/fsutil"
|
||||
"github.com/grafana/grafana/pkg/infra/fs"
|
||||
)
|
||||
|
||||
func writeAptlyConf(dbDir, repoDir string) error {
|
||||
aptlyConf := fmt.Sprintf(`{
|
||||
"rootDir": "%s",
|
||||
"downloadConcurrency": 4,
|
||||
"downloadSpeedLimit": 0,
|
||||
"architectures": [],
|
||||
"dependencyFollowSuggests": false,
|
||||
"dependencyFollowRecommends": false,
|
||||
"dependencyFollowAllVariants": false,
|
||||
"dependencyFollowSource": false,
|
||||
"dependencyVerboseResolve": false,
|
||||
"gpgDisableSign": false,
|
||||
"gpgDisableVerify": false,
|
||||
"gpgProvider": "gpg2",
|
||||
"downloadSourcePackages": false,
|
||||
"skipLegacyPool": true,
|
||||
"ppaDistributorID": "ubuntu",
|
||||
"ppaCodename": "",
|
||||
"skipContentsPublishing": false,
|
||||
"FileSystemPublishEndpoints": {
|
||||
"repo": {
|
||||
"rootDir": "%s",
|
||||
"linkMethod": "copy"
|
||||
}
|
||||
},
|
||||
"S3PublishEndpoints": {},
|
||||
"SwiftPublishEndpoints": {}
|
||||
}
|
||||
`, dbDir, repoDir)
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(filepath.Join(home, ".aptly.conf"), []byte(aptlyConf), 0600)
|
||||
}
|
||||
|
||||
// downloadDebs downloads Deb packages.
|
||||
func downloadDebs(cfg PublishConfig, workDir string) error {
|
||||
if cfg.Bucket == "" {
|
||||
panic("cfg.Bucket has to be set")
|
||||
}
|
||||
if !strings.HasSuffix(workDir, string(filepath.Separator)) {
|
||||
workDir += string(filepath.Separator)
|
||||
}
|
||||
|
||||
var version string
|
||||
if cfg.ReleaseMode.Mode == config.TagMode {
|
||||
if cfg.ReleaseMode.IsBeta {
|
||||
version = strings.ReplaceAll(cfg.Version, "-", "~")
|
||||
} else {
|
||||
version = cfg.Version
|
||||
}
|
||||
}
|
||||
if version == "" {
|
||||
panic(fmt.Sprintf("Unrecognized version mode %s", cfg.ReleaseMode.Mode))
|
||||
}
|
||||
|
||||
var sfx string
|
||||
switch cfg.Edition {
|
||||
case config.EditionOSS:
|
||||
case config.EditionEnterprise:
|
||||
sfx = EnterpriseSfx
|
||||
default:
|
||||
return fmt.Errorf("unrecognized edition %q", cfg.Edition)
|
||||
}
|
||||
|
||||
u := fmt.Sprintf("gs://%s/%s/%s/grafana%s_%s_*.deb*", cfg.Bucket,
|
||||
strings.ToLower(string(cfg.Edition)), ReleaseFolder, sfx, version)
|
||||
log.Printf("Downloading Deb packages %q...\n", u)
|
||||
args := []string{
|
||||
"-m",
|
||||
"cp",
|
||||
u,
|
||||
workDir,
|
||||
}
|
||||
//nolint:gosec
|
||||
cmd := exec.Command("gsutil", args...)
|
||||
if output, err := cmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("failed to download Deb packages %q: %w\n%s", u, err, output)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateDebRepo updates the Debian repository with the new release.
|
||||
func UpdateDebRepo(cfg PublishConfig, workDir string) error {
|
||||
if cfg.ReleaseMode.Mode != config.TagMode {
|
||||
panic(fmt.Sprintf("Unsupported version mode: %s", cfg.ReleaseMode.Mode))
|
||||
}
|
||||
|
||||
if cfg.ReleaseMode.IsTest {
|
||||
if cfg.Config.DebDBBucket == DefaultDebDBBucket {
|
||||
return fmt.Errorf("in test-release mode, the default Deb DB bucket shouldn't be used")
|
||||
}
|
||||
if cfg.Config.DebRepoBucket == DefaultDebRepoBucket {
|
||||
return fmt.Errorf("in test-release mode, the default Deb repo bucket shouldn't be used")
|
||||
}
|
||||
}
|
||||
|
||||
if err := downloadDebs(cfg, workDir); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
repoName := "grafana"
|
||||
if cfg.ReleaseMode.IsBeta {
|
||||
repoName = "beta"
|
||||
}
|
||||
|
||||
repoRoot, err := fsutil.CreateTempDir("deb-repo")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if err := os.RemoveAll(repoRoot); err != nil {
|
||||
log.Printf("Failed to remove temporary directory %q: %s\n", repoRoot, err.Error())
|
||||
}
|
||||
}()
|
||||
|
||||
dbDir := filepath.Join(repoRoot, "db")
|
||||
repoDir := filepath.Join(repoRoot, "repo")
|
||||
tmpDir := filepath.Join(repoRoot, "tmp")
|
||||
for _, dpath := range []string{dbDir, repoDir, tmpDir} {
|
||||
if err := os.MkdirAll(dpath, 0750); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := writeAptlyConf(dbDir, repoDir); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Download the Debian repo database
|
||||
u := fmt.Sprintf("gs://%s/%s", cfg.DebDBBucket, strings.ToLower(string(cfg.Edition)))
|
||||
log.Printf("Downloading Debian repo database from %s...\n", u)
|
||||
//nolint:gosec
|
||||
cmd := exec.Command("gsutil", "-m", "rsync", "-r", "-d", u, dbDir)
|
||||
if output, err := cmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("failed to download Debian repo database: %w\n%s", err, output)
|
||||
}
|
||||
|
||||
if err := addPkgsToRepo(cfg, workDir, tmpDir, repoName); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Println("Updating local Debian package repository...")
|
||||
// Update published local repository. This assumes that there exists already a local, published repo.
|
||||
for _, tp := range []string{"stable", "beta"} {
|
||||
passArg := fmt.Sprintf("-passphrase-file=%s", cfg.GPGPassPath)
|
||||
//nolint:gosec
|
||||
cmd := exec.Command("aptly", "publish", "update", "-batch", passArg, "-force-overwrite", tp,
|
||||
"filesystem:repo:grafana")
|
||||
if output, err := cmd.CombinedOutput(); err != nil {
|
||||
return cli.Exit(fmt.Sprintf("failed to update Debian %q repository: %s", tp, output), 1)
|
||||
}
|
||||
}
|
||||
|
||||
// Update database in GCS
|
||||
u = fmt.Sprintf("gs://%s/%s", cfg.DebDBBucket, strings.ToLower(string(cfg.Edition)))
|
||||
if cfg.DryRun {
|
||||
log.Printf("Simulating upload of Debian repo database to GCS (%s)\n", u)
|
||||
} else {
|
||||
log.Printf("Uploading Debian repo database to GCS (%s)...\n", u)
|
||||
//nolint:gosec
|
||||
cmd = exec.Command("gsutil", "-m", "rsync", "-r", "-d", dbDir, u)
|
||||
if output, err := cmd.CombinedOutput(); err != nil {
|
||||
return cli.Exit(fmt.Sprintf("failed to upload Debian repo database to GCS: %s", output), 1)
|
||||
}
|
||||
}
|
||||
|
||||
// Update metadata and binaries in repository bucket
|
||||
u = fmt.Sprintf("gs://%s/%s/deb", cfg.DebRepoBucket, strings.ToLower(string(cfg.Edition)))
|
||||
grafDir := filepath.Join(repoDir, "grafana")
|
||||
if cfg.DryRun {
|
||||
log.Printf("Simulating upload of Debian repo resources to GCS (%s)\n", u)
|
||||
} else {
|
||||
log.Printf("Uploading Debian repo resources to GCS (%s)...\n", u)
|
||||
//nolint:gosec
|
||||
cmd = exec.Command("gsutil", "-m", "rsync", "-r", "-d", grafDir, u)
|
||||
if output, err := cmd.CombinedOutput(); err != nil {
|
||||
return cli.Exit(fmt.Sprintf("failed to upload Debian repo resources to GCS: %s", output), 1)
|
||||
}
|
||||
allRepoResources := fmt.Sprintf("%s/**/*", u)
|
||||
log.Printf("Setting cache ttl for Debian repo resources on GCS (%s)...\n", allRepoResources)
|
||||
//nolint:gosec
|
||||
cmd = exec.Command("gsutil", "-m", "setmeta", "-h", CacheSettings+cfg.TTL, allRepoResources)
|
||||
if output, err := cmd.CombinedOutput(); err != nil {
|
||||
return cli.Exit(fmt.Sprintf("failed to set cache ttl for Debian repo resources on GCS: %s", output), 1)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func addPkgsToRepo(cfg PublishConfig, workDir, tmpDir, repoName string) error {
|
||||
var sfx string
|
||||
switch cfg.Edition {
|
||||
case config.EditionOSS:
|
||||
case config.EditionEnterprise:
|
||||
sfx = EnterpriseSfx
|
||||
default:
|
||||
return fmt.Errorf("unsupported edition %q", cfg.Edition)
|
||||
}
|
||||
|
||||
log.Printf("Adding packages to Debian %q repo...\n", repoName)
|
||||
// TODO: Be more specific about filename pattern
|
||||
debs, err := filepath.Glob(filepath.Join(workDir, fmt.Sprintf("grafana%s*.deb", sfx)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, deb := range debs {
|
||||
basename := filepath.Base(deb)
|
||||
if strings.Contains(basename, "latest") {
|
||||
continue
|
||||
}
|
||||
|
||||
tgt := filepath.Join(tmpDir, basename)
|
||||
if err := fs.CopyFile(deb, tgt); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// XXX: Adds too many packages in enterprise (Arve: What does this mean exactly?)
|
||||
//nolint:gosec
|
||||
cmd := exec.Command("aptly", "repo", "add", "-force-replace", repoName, tmpDir)
|
||||
if output, err := cmd.CombinedOutput(); err != nil {
|
||||
return cli.Exit(fmt.Sprintf("failed to add packages to local Debian repository: %s", output), 1)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
// Package packaging holds functions and types for creating the tar.gz, deb, and rpm packages of Grafana.
|
||||
package packaging
|
||||
@@ -0,0 +1 @@
|
||||
package packaging
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,22 @@
|
||||
package packaging_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/build/config"
|
||||
"github.com/grafana/grafana/pkg/build/packaging"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestPackageRegexp(t *testing.T) {
|
||||
t.Run("It should match enterprise2 packages", func(t *testing.T) {
|
||||
rgx := packaging.PackageRegexp(config.EditionEnterprise2)
|
||||
matches := []string{
|
||||
"grafana-enterprise2-1.2.3-4567pre.linux-amd64.tar.gz",
|
||||
"grafana-enterprise2-1.2.3-4567pre.linux-amd64.tar.gz.sha256",
|
||||
}
|
||||
for _, v := range matches {
|
||||
assert.Truef(t, rgx.MatchString(v), "'%s' should match regex '%s'", v, rgx.String())
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,370 @@
|
||||
package packaging
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
// Consider switching this over to a community fork unless there is
|
||||
// an option to move us away from OpenPGP.
|
||||
"golang.org/x/crypto/openpgp" //nolint:staticcheck
|
||||
"golang.org/x/crypto/openpgp/armor" //nolint:staticcheck
|
||||
"golang.org/x/crypto/openpgp/packet" //nolint:staticcheck
|
||||
|
||||
"github.com/grafana/grafana/pkg/build/config"
|
||||
"github.com/grafana/grafana/pkg/build/fsutil"
|
||||
"github.com/grafana/grafana/pkg/infra/fs"
|
||||
)
|
||||
|
||||
// UpdateRPMRepo updates the RPM repository with the new release.
|
||||
func UpdateRPMRepo(cfg PublishConfig, workDir string) error {
|
||||
if cfg.ReleaseMode.Mode != config.TagMode {
|
||||
panic(fmt.Sprintf("Unsupported version mode %s", cfg.ReleaseMode.Mode))
|
||||
}
|
||||
|
||||
if cfg.ReleaseMode.IsTest && cfg.Config.RPMRepoBucket == DefaultRPMRepoBucket {
|
||||
return fmt.Errorf("in test-release mode, the default RPM repo bucket shouldn't be used")
|
||||
}
|
||||
|
||||
if err := downloadRPMs(cfg, workDir); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
repoRoot, err := fsutil.CreateTempDir("rpm-repo")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if err := os.RemoveAll(repoRoot); err != nil {
|
||||
log.Printf("Failed to remove %q: %s\n", repoRoot, err.Error())
|
||||
}
|
||||
}()
|
||||
|
||||
repoName := "rpm"
|
||||
if cfg.ReleaseMode.IsBeta {
|
||||
repoName = "rpm-beta"
|
||||
}
|
||||
folderURI := fmt.Sprintf("gs://%s/%s/%s", cfg.RPMRepoBucket, strings.ToLower(string(cfg.Edition)), repoName)
|
||||
|
||||
// Download the RPM database
|
||||
log.Printf("Downloading RPM database from GCS (%s)...\n", folderURI)
|
||||
//nolint:gosec
|
||||
cmd := exec.Command("gsutil", "-m", "rsync", "-r", "-d", folderURI, repoRoot)
|
||||
if output, err := cmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("failed to download RPM database from GCS: %w\n%s", err, output)
|
||||
}
|
||||
|
||||
// Add the new release to the repo
|
||||
var sfx string
|
||||
switch cfg.Edition {
|
||||
case config.EditionOSS:
|
||||
case config.EditionEnterprise:
|
||||
sfx = EnterpriseSfx
|
||||
default:
|
||||
return fmt.Errorf("unsupported edition %q", cfg.Edition)
|
||||
}
|
||||
allRPMs, err := filepath.Glob(filepath.Join(workDir, fmt.Sprintf("grafana%s-*.rpm", sfx)))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to list RPMs in %q: %w", workDir, err)
|
||||
}
|
||||
rpms := []string{}
|
||||
for _, rpm := range allRPMs {
|
||||
if strings.Contains(rpm, "-latest") {
|
||||
continue
|
||||
}
|
||||
|
||||
rpms = append(rpms, rpm)
|
||||
}
|
||||
// XXX: What does the following comment mean?
|
||||
// adds to many files for enterprise
|
||||
for _, rpm := range rpms {
|
||||
if err := fs.CopyFile(rpm, filepath.Join(repoRoot, filepath.Base(rpm))); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
//nolint:gosec
|
||||
cmd = exec.Command("createrepo", repoRoot)
|
||||
if output, err := cmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("failed to create repo at %q: %w\n%s", repoRoot, err, output)
|
||||
}
|
||||
|
||||
if err := signRPMRepo(repoRoot, cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Update the repo in GCS
|
||||
|
||||
// Sync packages first to avoid cache misses
|
||||
if cfg.DryRun {
|
||||
log.Printf("Simulating upload of RPMs to GCS (%s)\n", folderURI)
|
||||
} else {
|
||||
log.Printf("Uploading RPMs to GCS (%s)...\n", folderURI)
|
||||
args := []string{"-m", "cp"}
|
||||
args = append(args, rpms...)
|
||||
args = append(args, folderURI)
|
||||
//nolint:gosec
|
||||
cmd = exec.Command("gsutil", args...)
|
||||
if output, err := cmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("failed to upload RPMs to GCS: %w\n%s", err, output)
|
||||
}
|
||||
}
|
||||
|
||||
if cfg.DryRun {
|
||||
log.Printf("Simulating upload of RPM repo metadata to GCS (%s)\n", folderURI)
|
||||
} else {
|
||||
log.Printf("Uploading RPM repo metadata to GCS (%s)...\n", folderURI)
|
||||
//nolint:gosec
|
||||
cmd = exec.Command("gsutil", "-m", "rsync", "-r", "-d", repoRoot, folderURI)
|
||||
if output, err := cmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("failed to upload RPM repo metadata to GCS: %w\n%s", err, output)
|
||||
}
|
||||
allRepoResources := fmt.Sprintf("%s/**/*", folderURI)
|
||||
log.Printf("Setting cache ttl for RPM repo resources on GCS (%s)...\n", allRepoResources)
|
||||
//nolint:gosec
|
||||
cmd = exec.Command("gsutil", "-m", "setmeta", "-h", CacheSettings+cfg.TTL, allRepoResources)
|
||||
if output, err := cmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("failed to set cache ttl for RPM repo resources on GCS: %w\n%s", err, output)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// downloadRPMs downloads RPM packages.
|
||||
func downloadRPMs(cfg PublishConfig, workDir string) error {
|
||||
if !strings.HasSuffix(workDir, string(filepath.Separator)) {
|
||||
workDir += string(filepath.Separator)
|
||||
}
|
||||
var version string
|
||||
if cfg.ReleaseMode.Mode == config.TagMode {
|
||||
if cfg.ReleaseMode.IsBeta {
|
||||
version = strings.ReplaceAll(cfg.Version, "-", "~")
|
||||
} else {
|
||||
version = cfg.Version
|
||||
}
|
||||
}
|
||||
if version == "" {
|
||||
panic(fmt.Sprintf("Unrecognized version mode %s", cfg.ReleaseMode.Mode))
|
||||
}
|
||||
|
||||
var sfx string
|
||||
switch cfg.Edition {
|
||||
case config.EditionOSS:
|
||||
case config.EditionEnterprise:
|
||||
sfx = EnterpriseSfx
|
||||
default:
|
||||
return fmt.Errorf("unrecognized edition %q", cfg.Edition)
|
||||
}
|
||||
|
||||
u := fmt.Sprintf("gs://%s/%s/%s/grafana%s-%s-*.*.rpm*", cfg.Bucket,
|
||||
strings.ToLower(string(cfg.Edition)), ReleaseFolder, sfx, version)
|
||||
log.Printf("Downloading RPM packages %q...\n", u)
|
||||
args := []string{
|
||||
"-m",
|
||||
"cp",
|
||||
u,
|
||||
workDir,
|
||||
}
|
||||
//nolint:gosec
|
||||
cmd := exec.Command("gsutil", args...)
|
||||
if output, err := cmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("failed to download RPM packages %q: %w\n%s", u, err, output)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getPublicKey(cfg PublishConfig) (*packet.PublicKey, error) {
|
||||
f, err := os.Open(cfg.GPGPublicKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open %q: %w", cfg.GPGPublicKey, err)
|
||||
}
|
||||
defer func(f *os.File) {
|
||||
err := f.Close()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}(f)
|
||||
|
||||
block, err := armor.Decode(f)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if block.Type != openpgp.PublicKeyType {
|
||||
return nil, fmt.Errorf("invalid public key block type: %q", block.Type)
|
||||
}
|
||||
|
||||
packetReader := packet.NewReader(block.Body)
|
||||
pkt, err := packetReader.Next()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
key, ok := pkt.(*packet.PublicKey)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("got non-public key from packet reader: %T", pkt)
|
||||
}
|
||||
|
||||
return key, nil
|
||||
}
|
||||
|
||||
func getPrivateKey(cfg PublishConfig) (*packet.PrivateKey, error) {
|
||||
f, err := os.Open(cfg.GPGPrivateKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open %q: %w", cfg.GPGPrivateKey, err)
|
||||
}
|
||||
defer func(f *os.File) {
|
||||
err := f.Close()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}(f)
|
||||
|
||||
passphraseB, err := os.ReadFile(cfg.GPGPassPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read %q: %w", cfg.GPGPrivateKey, err)
|
||||
}
|
||||
passphraseB = bytes.TrimSuffix(passphraseB, []byte("\n"))
|
||||
|
||||
block, err := armor.Decode(f)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if block.Type != openpgp.PrivateKeyType {
|
||||
return nil, fmt.Errorf("invalid private key block type: %q", block.Type)
|
||||
}
|
||||
|
||||
packetReader := packet.NewReader(block.Body)
|
||||
pkt, err := packetReader.Next()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
key, ok := pkt.(*packet.PrivateKey)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("got non-private key from packet reader: %T", pkt)
|
||||
}
|
||||
|
||||
if err := key.Decrypt(passphraseB); err != nil {
|
||||
return nil, fmt.Errorf("failed to decrypt private key: %w", err)
|
||||
}
|
||||
return key, nil
|
||||
}
|
||||
|
||||
// signRPMRepo signs an RPM repository using PGP.
|
||||
// The signature gets written to the file repodata/repomd.xml.asc.
|
||||
func signRPMRepo(repoRoot string, cfg PublishConfig) error {
|
||||
if cfg.GPGPublicKey == "" || cfg.GPGPrivateKey == "" {
|
||||
return fmt.Errorf("private or public key is empty")
|
||||
}
|
||||
|
||||
log.Printf("Signing RPM repo")
|
||||
|
||||
pubKey, err := getPublicKey(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
privKey, err := getPrivateKey(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pcfg := packet.Config{
|
||||
DefaultHash: crypto.SHA256,
|
||||
DefaultCipher: packet.CipherAES256,
|
||||
DefaultCompressionAlgo: packet.CompressionZLIB,
|
||||
CompressionConfig: &packet.CompressionConfig{
|
||||
Level: 9,
|
||||
},
|
||||
RSABits: 4096,
|
||||
}
|
||||
currentTime := pcfg.Now()
|
||||
uid := packet.NewUserId("", "", "")
|
||||
|
||||
isPrimaryID := false
|
||||
keyLifetimeSecs := uint32(86400 * 365)
|
||||
signer := openpgp.Entity{
|
||||
PrimaryKey: pubKey,
|
||||
PrivateKey: privKey,
|
||||
Identities: map[string]*openpgp.Identity{
|
||||
uid.Id: {
|
||||
Name: uid.Name,
|
||||
UserId: uid,
|
||||
SelfSignature: &packet.Signature{
|
||||
CreationTime: currentTime,
|
||||
SigType: packet.SigTypePositiveCert,
|
||||
PubKeyAlgo: packet.PubKeyAlgoRSA,
|
||||
Hash: pcfg.Hash(),
|
||||
IsPrimaryId: &isPrimaryID,
|
||||
FlagsValid: true,
|
||||
FlagSign: true,
|
||||
FlagCertify: true,
|
||||
IssuerKeyId: &pubKey.KeyId,
|
||||
},
|
||||
},
|
||||
},
|
||||
Subkeys: []openpgp.Subkey{
|
||||
{
|
||||
PublicKey: pubKey,
|
||||
PrivateKey: privKey,
|
||||
Sig: &packet.Signature{
|
||||
CreationTime: currentTime,
|
||||
SigType: packet.SigTypeSubkeyBinding,
|
||||
PubKeyAlgo: packet.PubKeyAlgoRSA,
|
||||
Hash: pcfg.Hash(),
|
||||
PreferredHash: []uint8{8}, // SHA-256
|
||||
FlagsValid: true,
|
||||
FlagEncryptStorage: true,
|
||||
FlagEncryptCommunications: true,
|
||||
IssuerKeyId: &pubKey.KeyId,
|
||||
KeyLifetimeSecs: &keyLifetimeSecs,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Ignore gosec G304 as this function is only used in the build process.
|
||||
//nolint:gosec
|
||||
freader, err := os.Open(filepath.Join(repoRoot, "repodata", "repomd.xml"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func(freader *os.File) {
|
||||
err := freader.Close()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}(freader)
|
||||
|
||||
// Ignore gosec G304 as this function is only used in the build process.
|
||||
//nolint:gosec
|
||||
sigwriter, err := os.Create(filepath.Join(repoRoot, "repodata", "repomd.xml.asc"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func(sigwriter *os.File) {
|
||||
err := sigwriter.Close()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}(sigwriter)
|
||||
|
||||
if err := openpgp.ArmoredDetachSignText(sigwriter, &signer, freader, nil); err != nil {
|
||||
return fmt.Errorf("failed to write PGP signature: %w", err)
|
||||
}
|
||||
|
||||
if err := sigwriter.Close(); err != nil {
|
||||
return fmt.Errorf("failed to write PGP signature: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
package packaging
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/build/config"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const pubKey = `-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
Version: OpenPGP.js v4.10.10
|
||||
Comment: https://openpgpjs.org
|
||||
|
||||
xsBNBGM1b9wBCADZM49X7vwOS93KbgA6yhpwrYf8ZlzksGcDaYgp1IzvqHbs
|
||||
xeU1mmBYVH/bSKRDG0tt3Qdky4Nvl4Oqd+g0e2ZGjmlEy9zUiPTTK/BtXT+5
|
||||
s8oqih2NIAkyF91BNZABAgvh/vJdYImhYeUQBDqMJgqZ/Y/Ha31N7rSW+jUt
|
||||
LHspbN0ztJYjuEd/bg2NKH7Gs/AyNvX9IQTC4k7iRRafx7q/PBCVtsk+NCwz
|
||||
BEkL93xpAdcdYiMNrRP2eIHQjBmNZ/oUCkcDsLCBvcSq6P2lGpNnpPzVoTJf
|
||||
v2qrWkVn5txJJsOkmBGpEDbECPunVilrWO6RPomP0yYkr6NE4XeCJ3QhABEB
|
||||
AAHNGWR1bW15IDxkdW1teUBob3RtYWlsLmNvbT7CwI0EEAEIACAFAmM1b9wG
|
||||
CwkHCAMCBBUICgIEFgIBAAIZAQIbAwIeAQAhCRAoJ1i5w6kkAxYhBCQv+iwt
|
||||
IFn7vj9PLygnWLnDqSQDPxkH/0Ju2Cah+bOxl09uv2Ft2BVlQh0u+wJyRVgs
|
||||
KxTxldAXFZwMrN4wK/GUoGWDiy2tzNtoVE6GpxWUj+LvSGFaVLNVjW+Le77I
|
||||
BP/sl1wKHJbQhseKc7Mz5Zj3i0F1FPM+rLik7tNk6kiEBqYVyyXahyT98Hu1
|
||||
1OKEV+8NiRG47iNgd/dpgEdVSS4DN/dL6m5q+CVy9YnlR+wXxF/2xcMmWBzR
|
||||
V2cPVw0JzunpUV8lDDQ/n1sPw61D3oL1aH0bkn8aA8pEceKOVIYOaja7LkLX
|
||||
uSlROlALA/M2fuubradW9I3FcrJNn+/xA52el2l/Hn/Syf9GQV/Ll/R+qKIo
|
||||
Z57xWd7OwE0EYzVv3AEIAJl/PNYOF2prNKY58BfZ74XurDb9mNlZ1wsIqrOu
|
||||
J/euzHEnzkCAjMUuXV7wcugjQlmpcZn6Y0QmQ2uX7SwPCMovDvngbXeAfbdd
|
||||
6FUKecQ0sG54Plm8HSMNdjetdUVl7ACxjJO8Rdc/Asx7ua7gMm42CVfqMj4L
|
||||
qN5foUBlaKJ1iGKUpQ+673UQWMYeOBuu9G8awbSzGaphN97CIX7xEMGzGeff
|
||||
yHLHK+MsfX935uDgDwJQzxJKEugIJDMKgWOLgVz1jRCsJKHlywHTWpVuMiKY
|
||||
Wnuq4tDNLBUQtaRL7uclG7Wejw/XNN0uD/zNHPgF5rmlYHVhrtDbBCP2XqTn
|
||||
WU8AEQEAAcLAdgQYAQgACQUCYzVv3AIbDAAhCRAoJ1i5w6kkAxYhBCQv+iwt
|
||||
IFn7vj9PLygnWLnDqSQDFqYH/AkdNaPUQlE7RQBigNRGOFBuqjhbLsV/rZf+
|
||||
/4K6wDHojM606lgLm57T4NUXnk53VIF3KO8+v8N11mCtPb+zBngfvVU14COC
|
||||
HEDNdOK19TlR+tH25cftfUiF+OJsgMQysErGuFEtwLE6TNzpQIcnw7SbjxMr
|
||||
EGacF9xCBKexB6MlR3GwJ2LBUJm3Lq/fvqImztoTlKDsrpk4JOH5FfYG+G2f
|
||||
1zU73fVsCCElX4qA/49rRQf0RNfhjRjmHULP8hSvCXUEhfiBggEgxof/vKlC
|
||||
qauHC55luuIeabju8HaXTjpz019cq+3IUgewX/ky0PhQXEW9SoODKabPY2yS
|
||||
yUbHFm4=
|
||||
=OCSx
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
`
|
||||
|
||||
const privKey = `-----BEGIN PGP PRIVATE KEY BLOCK-----
|
||||
Version: OpenPGP.js v4.10.10
|
||||
Comment: https://openpgpjs.org
|
||||
|
||||
xcMGBGM1b9wBCADZM49X7vwOS93KbgA6yhpwrYf8ZlzksGcDaYgp1IzvqHbs
|
||||
xeU1mmBYVH/bSKRDG0tt3Qdky4Nvl4Oqd+g0e2ZGjmlEy9zUiPTTK/BtXT+5
|
||||
s8oqih2NIAkyF91BNZABAgvh/vJdYImhYeUQBDqMJgqZ/Y/Ha31N7rSW+jUt
|
||||
LHspbN0ztJYjuEd/bg2NKH7Gs/AyNvX9IQTC4k7iRRafx7q/PBCVtsk+NCwz
|
||||
BEkL93xpAdcdYiMNrRP2eIHQjBmNZ/oUCkcDsLCBvcSq6P2lGpNnpPzVoTJf
|
||||
v2qrWkVn5txJJsOkmBGpEDbECPunVilrWO6RPomP0yYkr6NE4XeCJ3QhABEB
|
||||
AAH+CQMIuDEg1p2Y6zbg0EQ3JvsP7VQBGsuXg9khTjktoxhwici/d+rcIW7Q
|
||||
SuKWJGqs83LTeeGmS+9etNtf3LqRdPnI7f0qbT47mAqvp2gn7Rvbrabk+5Jj
|
||||
AQS/DDLlWNiWsPrMBMZ7TZpiQ+g7gnIZaV10taFupYJr69AjtED+NPu8LOvZ
|
||||
2ItK9xBqOwl5mkNe7ps/uTT6jwYSWxeObp4ymnLDLONY3eHuaYP9QB/NSlw0
|
||||
80Wo5qBPljlU8JdbEoLFU4gY6wkEbLa/DVbEVXSHfWVtr8jZbzHW39TBxpG2
|
||||
Dxk52EVyu8Gf9XIQN2ZjDP3CzBGmlxJjLxLUD4GmRSPaDGK7LCN9ZztaXy3Y
|
||||
WtF6RJfNzEoDdCaV0kkM3AskQDsQ+CWsDVsbbQyDtfncVG6cDzqmoDrBCSq1
|
||||
Bsoz07k2hj9VP0aP2xU78qcpJWO2rmhAHy9W2NqjXSBJriy1JXrK5o2/lUUr
|
||||
94R8NLvqeVbInUw/zovVctaujHIBhNKL9wn2T0LWrA2OEJUz0HWo6ZQSaNzl
|
||||
Obtz0M8gCj/4sDYjRAiDk50FzOcZp8ijYQFVypQTVzHki5T/JfvBnMpo+4Uc
|
||||
93QB1woyiZuJCIj7DpY3MkZ5fTDtgJPa+0k8r+lPnAmE6auGUaH7JRKhbBu0
|
||||
8faDwaiSv3kD3EEDffoWX/axLLYta9jTDnitTXbf1jY03pdJeiU/ZX0BQTZi
|
||||
pehZ/6yi/qXM/F8HDVEWriSLqVsMLrXXeFIojAc3fJ/QPpAZSx6E/Fe2xh8c
|
||||
yURov5krU1zNJDwqC3SjHsHQ/UlLtamDDmmuXX+xb1CwIDd6WksGsCbe/LoN
|
||||
TxViV4hOjIeh5TwRP5jQaqsVKCT8fzoDrRXy76taT+Zaaen+J6rC51HQwyEq
|
||||
Qgf1e7WodzN3r10UV6/L/wNkfdWJgf5MzRlkdW1teSA8ZHVtbXlAaG90bWFp
|
||||
bC5jb20+wsCNBBABCAAgBQJjNW/cBgsJBwgDAgQVCAoCBBYCAQACGQECGwMC
|
||||
HgEAIQkQKCdYucOpJAMWIQQkL/osLSBZ+74/Ty8oJ1i5w6kkAz8ZB/9Cbtgm
|
||||
ofmzsZdPbr9hbdgVZUIdLvsCckVYLCsU8ZXQFxWcDKzeMCvxlKBlg4strczb
|
||||
aFROhqcVlI/i70hhWlSzVY1vi3u+yAT/7JdcChyW0IbHinOzM+WY94tBdRTz
|
||||
Pqy4pO7TZOpIhAamFcsl2ock/fB7tdTihFfvDYkRuO4jYHf3aYBHVUkuAzf3
|
||||
S+puavglcvWJ5UfsF8Rf9sXDJlgc0VdnD1cNCc7p6VFfJQw0P59bD8OtQ96C
|
||||
9Wh9G5J/GgPKRHHijlSGDmo2uy5C17kpUTpQCwPzNn7rm62nVvSNxXKyTZ/v
|
||||
8QOdnpdpfx5/0sn/RkFfy5f0fqiiKGee8Vnex8MGBGM1b9wBCACZfzzWDhdq
|
||||
azSmOfAX2e+F7qw2/ZjZWdcLCKqzrif3rsxxJ85AgIzFLl1e8HLoI0JZqXGZ
|
||||
+mNEJkNrl+0sDwjKLw754G13gH23XehVCnnENLBueD5ZvB0jDXY3rXVFZewA
|
||||
sYyTvEXXPwLMe7mu4DJuNglX6jI+C6jeX6FAZWiidYhilKUPuu91EFjGHjgb
|
||||
rvRvGsG0sxmqYTfewiF+8RDBsxnn38hyxyvjLH1/d+bg4A8CUM8SShLoCCQz
|
||||
CoFji4Fc9Y0QrCSh5csB01qVbjIimFp7quLQzSwVELWkS+7nJRu1no8P1zTd
|
||||
Lg/8zRz4Bea5pWB1Ya7Q2wQj9l6k51lPABEBAAH+CQMIwr3YSD15lYrgItoy
|
||||
MDsrWqMMHJsSxusbQiK0KLgjFBuDuTolsu9zqQCHEm2dxChqT+yQ6AeeynRD
|
||||
pDMVkHEvhShvGUhB6Bu5wClHj8+xFpyprShE/KbEuppNdfIRgWVYc7UX+TYz
|
||||
6BymqhzKyIw2Q33ocrXgTRZ02HM7urKVvAhsJCEff0paByOzCspiv/TPRihi
|
||||
7GAZY0wFLDPe9qr+07ExT2ndMDX8Xb1mlg8IeaSWUaNilm7M8oW3xnUBnXeD
|
||||
XglTkObGeRVXAINim9uL4soT3lamN4QwgBus9WzFqOOCMk11fjatY8kY1zX9
|
||||
epO27igGtMwTFl11XcQLlFyvlgPBeWtFam7RiDPa3VF0XubmBYZBmqWpccWs
|
||||
xl0xHCtUK7Pd0O4kSqxsL9cB0MX9iR1yPkM8wA++Mp6pEfNcXUrGIdlie0H5
|
||||
aCq8rguYG5VuFosSUatdCbpRVGBxGnhxHes0mNTPgwAoAVNYBWXH5iq5HxKy
|
||||
i3Zy5V7ZKSyDrfg/0AajtDW5h3g+wglUI9UCdT4tNLFwYbhHqGH2xdBztYI0
|
||||
iSJ7COLmo26smkA8UXxsrlw8PWPzpbhQOG06EbMjncJimJDMI1YDC6ag7M5l
|
||||
OcG9uXZQ22ipAz5CSPtyL0/0WAp4yyn1VQRBK42n/y9ld+dMbuq6majazb15
|
||||
6sEgHUKERcwGs0Ftfj5Zamwhm7ZoIe26XEqvcshpQpv1Q9hktluVeSbiVaBe
|
||||
Nl8zUZHlo/0zUc5j7G5Up58t+ChSsyOFJGM7CGkKHHawBZYCs0EcpsdAPr3T
|
||||
1C8A0Wt9POTETYM4pZFOoLds6VTolZZcxeBN5YPoN2kbwFpOgPJN09Zz8z8S
|
||||
4psQRV4KQ92XDPZ/6q2BH5i2+F2ZwUsvCR4DwgzbVGZSRV6mM7lkjZSmnWfC
|
||||
AE7DUl7XwsB2BBgBCAAJBQJjNW/cAhsMACEJECgnWLnDqSQDFiEEJC/6LC0g
|
||||
Wfu+P08vKCdYucOpJAMWpgf8CR01o9RCUTtFAGKA1EY4UG6qOFsuxX+tl/7/
|
||||
grrAMeiMzrTqWAubntPg1ReeTndUgXco7z6/w3XWYK09v7MGeB+9VTXgI4Ic
|
||||
QM104rX1OVH60fblx+19SIX44myAxDKwSsa4US3AsTpM3OlAhyfDtJuPEysQ
|
||||
ZpwX3EIEp7EHoyVHcbAnYsFQmbcur9++oibO2hOUoOyumTgk4fkV9gb4bZ/X
|
||||
NTvd9WwIISVfioD/j2tFB/RE1+GNGOYdQs/yFK8JdQSF+IGCASDGh/+8qUKp
|
||||
q4cLnmW64h5puO7wdpdOOnPTX1yr7chSB7Bf+TLQ+FBcRb1Kg4Mpps9jbJLJ
|
||||
RscWbg==
|
||||
=KJNy
|
||||
-----END PGP PRIVATE KEY BLOCK-----
|
||||
`
|
||||
|
||||
// Dummy GPG keys, used only for testing
|
||||
// nolint:gosec
|
||||
const passPhrase = `MkDgjkrgdGxt`
|
||||
|
||||
func TestSignRPMRepo(t *testing.T) {
|
||||
repoDir := t.TempDir()
|
||||
workDir := t.TempDir()
|
||||
pubKeyPath := filepath.Join(workDir, "pub.key")
|
||||
err := os.WriteFile(pubKeyPath, []byte(pubKey), 0600)
|
||||
require.NoError(t, err)
|
||||
privKeyPath := filepath.Join(workDir, "priv.key")
|
||||
err = os.WriteFile(privKeyPath, []byte(privKey), 0600)
|
||||
require.NoError(t, err)
|
||||
passPhrasePath := filepath.Join(workDir, "passphrase")
|
||||
err = os.WriteFile(passPhrasePath, []byte(passPhrase), 0600)
|
||||
require.NoError(t, err)
|
||||
err = os.Mkdir(filepath.Join(repoDir, "repodata"), 0700)
|
||||
require.NoError(t, err)
|
||||
err = os.WriteFile(filepath.Join(repoDir, "repodata", "repomd.xml"), []byte("<xml></xml>"), 0600)
|
||||
require.NoError(t, err)
|
||||
|
||||
cfg := PublishConfig{
|
||||
Config: config.Config{
|
||||
GPGPrivateKey: privKeyPath,
|
||||
GPGPublicKey: pubKeyPath,
|
||||
GPGPassPath: passPhrasePath,
|
||||
},
|
||||
}
|
||||
|
||||
err = signRPMRepo(repoDir, cfg)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/grafana/grafana/pkg/build/config"
|
||||
"github.com/grafana/grafana/pkg/build/errutil"
|
||||
"github.com/grafana/grafana/pkg/build/syncutil"
|
||||
"github.com/grafana/grafana/pkg/infra/fs"
|
||||
)
|
||||
|
||||
type PluginSigningMode = int
|
||||
|
||||
// BuildPlugins builds internal plugins.
|
||||
// The built plugins are placed in plugins-bundled/dist/.
|
||||
func Build(ctx context.Context, grafanaDir string, p syncutil.WorkerPool, g *errutil.Group, verMode *config.BuildConfig) error {
|
||||
log.Printf("Building plugins in %q...", grafanaDir)
|
||||
|
||||
root := filepath.Join(grafanaDir, "plugins-bundled", "internal")
|
||||
fis, err := os.ReadDir(root)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for i := range fis {
|
||||
fi := fis[i]
|
||||
if !fi.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
dpath := filepath.Join(root, fi.Name())
|
||||
|
||||
p.Schedule(g.Wrap(func() error {
|
||||
log.Printf("Building plugin %q...", dpath)
|
||||
|
||||
cmd := exec.Command("yarn", "build")
|
||||
cmd.Dir = dpath
|
||||
if output, err := cmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("yarn build failed: %s", output)
|
||||
}
|
||||
|
||||
dstPath := filepath.Join("plugins-bundled", "dist", fi.Name())
|
||||
if err := fs.CopyRecursive(filepath.Join(dpath, "dist"), dstPath); err != nil {
|
||||
return err
|
||||
}
|
||||
if !verMode.PluginSignature.Sign {
|
||||
return nil
|
||||
}
|
||||
|
||||
return BuildManifest(ctx, dstPath, verMode.PluginSignature.AdminSign)
|
||||
}))
|
||||
}
|
||||
|
||||
if err := g.Wait(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Printf("Built all plug-ins successfully!")
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/grafana/grafana/pkg/build/errutil"
|
||||
"github.com/grafana/grafana/pkg/build/syncutil"
|
||||
)
|
||||
|
||||
// logCloseError executes the closeFunc; if it returns an error, it is logged by the log package.
|
||||
func logCloseError(closeFunc func() error) {
|
||||
if err := closeFunc(); err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
// logCloseError executes the closeFunc; if it returns an error, it is logged by the log package.
|
||||
func logError(err error) {
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
// 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"`
|
||||
}
|
||||
|
||||
// downloadPlugins downloads Grafana plugins that should be bundled into packages.
|
||||
//
|
||||
// The plugin archives are downloaded into <grafanaDir>/plugins-bundled.
|
||||
func Download(ctx context.Context, grafanaDir string, p syncutil.WorkerPool) error {
|
||||
g, _ := errutil.GroupWithContext(ctx)
|
||||
|
||||
log.Println("Downloading external plugins...")
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
for i := range m.Plugins {
|
||||
pm := m.Plugins[i]
|
||||
p.Schedule(g.Wrap(func() error {
|
||||
tgt := filepath.Join(grafanaDir, "plugins-bundled", fmt.Sprintf("%s-%s.zip", pm.Name, pm.Version))
|
||||
//nolint:gosec
|
||||
out, err := os.Create(tgt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer logCloseError(out.Close)
|
||||
|
||||
u := fmt.Sprintf("http://storage.googleapis.com/plugins-ci/plugins/%s/%s-%s.zip", pm.Name, pm.Name,
|
||||
pm.Version)
|
||||
log.Printf("Downloading plugin %q to %q...", u, tgt)
|
||||
// nolint:gosec
|
||||
resp, err := http.Get(u)
|
||||
if err != nil {
|
||||
return fmt.Errorf("downloading %q failed: %w", u, err)
|
||||
}
|
||||
defer logError(resp.Body.Close())
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("failed to download %q, status code %d", u, resp.StatusCode)
|
||||
}
|
||||
|
||||
if _, err := io.Copy(out, resp.Body); err != nil {
|
||||
return fmt.Errorf("downloading %q failed: %w", u, err)
|
||||
}
|
||||
if err := out.Close(); err != nil {
|
||||
return fmt.Errorf("downloading %q failed: %w", u, err)
|
||||
}
|
||||
|
||||
//nolint:gosec
|
||||
fd, err := os.Open(tgt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer logCloseError(fd.Close)
|
||||
|
||||
h := sha256.New()
|
||||
if _, err := io.Copy(h, fd); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
chksum := hex.EncodeToString(h.Sum(nil))
|
||||
if chksum != pm.Checksum {
|
||||
return fmt.Errorf("plugin %q has bad checksum: %s (expected %s)", u, chksum, pm.Checksum)
|
||||
}
|
||||
|
||||
return Unzip(tgt, filepath.Join(grafanaDir, "plugins-bundled"))
|
||||
}))
|
||||
}
|
||||
|
||||
return g.Wait()
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
type manifest struct {
|
||||
Plugin string `json:"plugin"`
|
||||
Version string `json:"version"`
|
||||
Files map[string]string `json:"files"`
|
||||
}
|
||||
|
||||
func getManifest(dpath string, chksums map[string]string) (manifest, error) {
|
||||
m := manifest{}
|
||||
|
||||
type pluginInfo struct {
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
type plugin struct {
|
||||
ID string `json:"id"`
|
||||
Info pluginInfo `json:"info"`
|
||||
}
|
||||
|
||||
//nolint:gosec
|
||||
f, err := os.Open(filepath.Join(dpath, "plugin.json"))
|
||||
if err != nil {
|
||||
return m, err
|
||||
}
|
||||
decoder := json.NewDecoder(f)
|
||||
var p plugin
|
||||
if err := decoder.Decode(&p); err != nil {
|
||||
return m, err
|
||||
}
|
||||
|
||||
if p.ID == "" {
|
||||
return m, fmt.Errorf("plugin.json doesn't define id")
|
||||
}
|
||||
if p.Info.Version == "" {
|
||||
return m, fmt.Errorf("plugin.json doesn't define info.version")
|
||||
}
|
||||
|
||||
return manifest{
|
||||
Plugin: p.ID,
|
||||
Version: p.Info.Version,
|
||||
Files: chksums,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// BuildManifest requests a plugin's signed manifest file fromt he Grafana API.
|
||||
// If signingAdmin is true, the manifest signing admin endpoint (without plugin ID) will be used, and requires
|
||||
// an admin API key.
|
||||
func BuildManifest(ctx context.Context, dpath string, signingAdmin bool) error {
|
||||
log.Printf("Building manifest for plug-in at %q", dpath)
|
||||
|
||||
apiKey := os.Getenv("GRAFANA_API_KEY")
|
||||
if apiKey == "" {
|
||||
return fmt.Errorf("GRAFANA_API_KEY must be set")
|
||||
}
|
||||
|
||||
manifestPath := filepath.Join(dpath, "MANIFEST.txt")
|
||||
chksums, err := getChksums(dpath, manifestPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m, err := getManifest(dpath, chksums)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b := bytes.NewBuffer(nil)
|
||||
encoder := json.NewEncoder(b)
|
||||
if err := encoder.Encode(&m); err != nil {
|
||||
return err
|
||||
}
|
||||
jsonB := b.Bytes()
|
||||
u := "https://grafana.com/api/plugins/ci/sign"
|
||||
if !signingAdmin {
|
||||
u = fmt.Sprintf("https://grafana.com/api/plugins/%s/ci/sign", m.Plugin)
|
||||
}
|
||||
log.Printf("Requesting signed manifest from Grafana API...")
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", u, bytes.NewReader(jsonB))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", apiKey))
|
||||
req.Header.Add("Content-Type", "application/json")
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get signed manifest from Grafana API: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
if err := resp.Body.Close(); err != nil {
|
||||
log.Println("failed to close response body, err: %w", err)
|
||||
}
|
||||
}()
|
||||
if resp.StatusCode != 200 {
|
||||
msg, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
log.Printf("Failed to read response body: %s", err)
|
||||
msg = []byte("")
|
||||
}
|
||||
return fmt.Errorf("request for signed manifest failed with status code %d: %s", resp.StatusCode, string(msg))
|
||||
}
|
||||
|
||||
log.Printf("Successfully signed manifest via Grafana API, writing to %q", manifestPath)
|
||||
//nolint:gosec
|
||||
f, err := os.Create(manifestPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create %s: %w", manifestPath, err)
|
||||
}
|
||||
defer func() {
|
||||
if err := f.Close(); err != nil {
|
||||
log.Println("failed to close file, err: %w", err)
|
||||
}
|
||||
}()
|
||||
if _, err := io.Copy(f, resp.Body); err != nil {
|
||||
return fmt.Errorf("failed to write %s: %w", manifestPath, err)
|
||||
}
|
||||
if err := f.Close(); err != nil {
|
||||
return fmt.Errorf("failed to write %s: %w", manifestPath, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getChksums(dpath, manifestPath string) (map[string]string, error) {
|
||||
manifestPath = filepath.Clean(manifestPath)
|
||||
|
||||
chksums := map[string]string{}
|
||||
if err := filepath.Walk(dpath, func(path string, fi os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if fi.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
path = filepath.Clean(path)
|
||||
|
||||
// Handle symbolic links
|
||||
if fi.Mode()&os.ModeSymlink == os.ModeSymlink {
|
||||
finalPath, err := filepath.EvalSymlinks(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Printf("Handling symlink %q, pointing to %q", path, finalPath)
|
||||
|
||||
info, err := os.Stat(finalPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
if _, err := filepath.Rel(dpath, finalPath); err != nil {
|
||||
return fmt.Errorf("symbolic link %q targets a file outside of the plugin directory: %q", path, finalPath)
|
||||
}
|
||||
|
||||
if finalPath == manifestPath {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if path == manifestPath {
|
||||
return nil
|
||||
}
|
||||
|
||||
h := sha256.New()
|
||||
//nolint:gosec
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer logCloseError(f.Close)
|
||||
if _, err := io.Copy(h, f); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
relPath, err := filepath.Rel(dpath, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
chksums[relPath] = fmt.Sprintf("%x", h.Sum(nil))
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return chksums, nil
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// Unzip unzips a plugin.
|
||||
func Unzip(fpath, tgtDir string) error {
|
||||
log.Printf("Unzipping plugin %q into %q...", fpath, tgtDir)
|
||||
|
||||
r, err := zip.OpenReader(fpath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer logCloseError(r.Close)
|
||||
|
||||
// Closure to address file descriptors issue with all the deferred .Close() methods
|
||||
extractAndWriteFile := func(f *zip.File) error {
|
||||
log.Printf("Extracting zip member %q...", f.Name)
|
||||
|
||||
rc, err := f.Open()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer logCloseError(rc.Close)
|
||||
|
||||
//nolint:gosec
|
||||
dstPath := filepath.Join(tgtDir, f.Name)
|
||||
|
||||
if f.FileInfo().IsDir() {
|
||||
return os.MkdirAll(dstPath, f.Mode())
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(dstPath), f.Mode()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
//nolint:gosec
|
||||
fd, err := os.OpenFile(dstPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer logCloseError(fd.Close)
|
||||
|
||||
// nolint:gosec
|
||||
if _, err := io.Copy(fd, rc); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return fd.Close()
|
||||
}
|
||||
|
||||
for _, f := range r.File {
|
||||
if err := extractAndWriteFile(f); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package stringutil
|
||||
|
||||
func Contains(arr []string, s string) bool {
|
||||
for _, e := range arr {
|
||||
if e == s {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package syncutil
|
||||
|
||||
import (
|
||||
"log"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
func worker(jobs chan func()) {
|
||||
for j := range jobs {
|
||||
j()
|
||||
}
|
||||
}
|
||||
|
||||
// WorkerPool represents a concurrent worker pool.
|
||||
type WorkerPool struct {
|
||||
NumWorkers int
|
||||
jobs chan func()
|
||||
}
|
||||
|
||||
// NewWorkerPool constructs a new WorkerPool.
|
||||
func NewWorkerPool(numWorkers int) WorkerPool {
|
||||
if numWorkers <= 0 {
|
||||
numWorkers = runtime.NumCPU()
|
||||
}
|
||||
log.Printf("Creating worker pool with %d workers", numWorkers)
|
||||
jobs := make(chan func(), 100)
|
||||
for i := 0; i < numWorkers; i++ {
|
||||
go worker(jobs)
|
||||
}
|
||||
return WorkerPool{
|
||||
NumWorkers: numWorkers,
|
||||
jobs: jobs,
|
||||
}
|
||||
}
|
||||
|
||||
// Schedule schedules a job to be executed by a worker in the pool.
|
||||
func (p WorkerPool) Schedule(job func()) {
|
||||
p.jobs <- job
|
||||
}
|
||||
|
||||
func (p WorkerPool) Close() {
|
||||
close(p.jobs)
|
||||
}
|
||||
@@ -1041,6 +1041,7 @@ def publish_grafanacom_step(edition, ver_mode):
|
||||
],
|
||||
'environment': {
|
||||
'GRAFANA_COM_API_KEY': from_secret('grafana_api_key'),
|
||||
'GCP_KEY': from_secret('gcp_key'),
|
||||
},
|
||||
'commands': [
|
||||
cmd,
|
||||
|
||||
Reference in New Issue
Block a user