mirror of
https://github.com/grafana/grafana.git
synced 2025-07-29 19:32:51 +08:00

Remove usage of http.DefaultClient and http.DefaultTransport Part of grafana/data-sources#484
407 lines
10 KiB
Go
407 lines
10 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"net"
|
|
"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/gcom"
|
|
"github.com/grafana/grafana/pkg/build/packaging"
|
|
"github.com/grafana/grafana/pkg/build/versions"
|
|
)
|
|
|
|
const grafanaAPI = "https://grafana.com/api"
|
|
|
|
var httpClient = http.Client{
|
|
Transport: &http.Transport{
|
|
Proxy: http.ProxyFromEnvironment,
|
|
DialContext: func(dialer *net.Dialer) func(context.Context, string, string) (net.Conn, error) {
|
|
return dialer.DialContext
|
|
}(&net.Dialer{
|
|
Timeout: 30 * time.Second,
|
|
KeepAlive: 30 * time.Second,
|
|
}),
|
|
ForceAttemptHTTP2: true,
|
|
MaxIdleConns: 100,
|
|
IdleConnTimeout: 90 * time.Second,
|
|
TLSHandshakeTimeout: 10 * time.Second,
|
|
ExpectContinueTimeout: 1 * time.Second,
|
|
},
|
|
}
|
|
|
|
// 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
|
|
semver := versions.ParseSemver(version)
|
|
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)
|
|
}
|
|
|
|
pkgjson, err := getPackageJSON()
|
|
if err != nil {
|
|
return cli.Exit(err.Error(), 1)
|
|
}
|
|
|
|
whatsNewURL, releaseNotesURL, err := getReleaseURLs(semver, pkgjson)
|
|
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
|
|
}
|
|
|
|
type grafanaConf struct {
|
|
WhatsNewURL string `json:"whatsNewUrl"`
|
|
ReleaseNotesURL string `json:"releaseNotesUrl"`
|
|
}
|
|
|
|
type packageConf struct {
|
|
Grafana grafanaConf `json:"grafana"`
|
|
}
|
|
|
|
func getPackageJSON() (*packageConf, error) {
|
|
pkgB, err := os.ReadFile("package.json")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read package.json: %w", err)
|
|
}
|
|
|
|
var pconf packageConf
|
|
if err := json.Unmarshal(pkgB, &pconf); err != nil {
|
|
return nil, fmt.Errorf("failed to decode package.json: %w", err)
|
|
}
|
|
|
|
return &pconf, nil
|
|
}
|
|
|
|
func getReleaseURLs(semver versions.Semver, pconf *packageConf) (string, string, error) {
|
|
u := fmt.Sprintf(pconf.Grafana.WhatsNewURL, semver.Major, semver.Minor, semver.Patch)
|
|
if _, err := url.ParseRequestURI(u); 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 u, pconf.Grafana.ReleaseNotesURL, nil
|
|
}
|
|
|
|
func Builds(baseURL *url.URL, grafana, version string, packages []packaging.BuildArtifact) ([]GCOMPackage, error) {
|
|
builds := make([]GCOMPackage, len(packages))
|
|
for i, v := range packages {
|
|
var (
|
|
os = v.Distro
|
|
arch = v.Arch
|
|
)
|
|
|
|
if v.Distro == "windows" {
|
|
os = "win"
|
|
if v.Ext == "msi" {
|
|
os = "win-installer"
|
|
}
|
|
}
|
|
|
|
if v.Distro == "rhel" {
|
|
if arch == "aarch64" {
|
|
arch = "arm64"
|
|
}
|
|
if arch == "x86_64" {
|
|
arch = "amd64"
|
|
}
|
|
}
|
|
|
|
if v.Distro == "deb" {
|
|
if arch == "armhf" {
|
|
arch = "armv7"
|
|
if v.RaspberryPi {
|
|
log.Println(v.Distro, arch, "raspberrypi == true")
|
|
arch = "armv6"
|
|
}
|
|
}
|
|
}
|
|
|
|
u := gcom.GetURL(baseURL, version, grafana, v.Distro, v.Arch, v.Ext, v.Musl, v.RaspberryPi)
|
|
builds[i] = GCOMPackage{
|
|
OS: os,
|
|
URL: u.String(),
|
|
Arch: arch,
|
|
}
|
|
}
|
|
|
|
return builds, 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 (
|
|
pth string
|
|
grafana = "grafana"
|
|
)
|
|
|
|
switch cfg.Edition {
|
|
case config.EditionOSS:
|
|
pth = "oss"
|
|
case config.EditionEnterprise:
|
|
grafana = "grafana-enterprise"
|
|
pth = "enterprise"
|
|
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)
|
|
}
|
|
|
|
pth = path.Join(pth)
|
|
baseArchiveURL := &url.URL{
|
|
Scheme: "https",
|
|
Host: "dl.grafana.com",
|
|
Path: pth,
|
|
}
|
|
|
|
builds, err := Builds(baseArchiveURL, grafana, cfg.Version, packaging.ArtifactConfigs)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
r := Release{
|
|
Version: cfg.Version,
|
|
ReleaseDate: time.Now().UTC(),
|
|
Builds: builds,
|
|
Stable: cfg.ReleaseMode.Mode == config.TagMode && !cfg.ReleaseMode.IsPreview && !cfg.ReleaseMode.IsTest,
|
|
Beta: cfg.ReleaseMode.IsPreview,
|
|
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 i, v := range r.Builds {
|
|
sha, err := getSHA256(v.URL)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
r.Builds[i].SHA256 = string(sha)
|
|
}
|
|
|
|
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, body any, 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(body)
|
|
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 := httpClient.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 GCOMPackage struct {
|
|
OS string `json:"os"`
|
|
URL string `json:"url"`
|
|
SHA256 string `json:"sha256"`
|
|
Arch string `json:"arch"`
|
|
}
|
|
|
|
type Release 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 []GCOMPackage `json:"-"`
|
|
}
|