Files
2025-06-11 08:43:06 +02:00

300 lines
9.9 KiB
Go

package arguments
import (
"context"
"fmt"
"log/slog"
"path"
"path/filepath"
"dagger.io/dagger"
"github.com/grafana/grafana/pkg/build/daggerbuild/cliutil"
"github.com/grafana/grafana/pkg/build/daggerbuild/containers"
"github.com/grafana/grafana/pkg/build/daggerbuild/daggerutil"
"github.com/grafana/grafana/pkg/build/daggerbuild/frontend"
"github.com/grafana/grafana/pkg/build/daggerbuild/git"
"github.com/grafana/grafana/pkg/build/daggerbuild/pipeline"
"github.com/urfave/cli/v2"
)
const BusyboxImage = "busybox:1.36"
func InitializeEnterprise(d *dagger.Client, grafana *dagger.Directory, enterprise *dagger.Directory) *dagger.Directory {
hash := d.Container().From("alpine/git").
WithDirectory("/src/grafana-enterprise", enterprise).
WithWorkdir("/src/grafana-enterprise").
WithEntrypoint([]string{}).
WithExec([]string{"/bin/sh", "-c", "git rev-parse HEAD > .buildinfo.enterprise-commit"}).
File("/src/grafana-enterprise/.buildinfo.enterprise-commit")
return d.Container().From(BusyboxImage).
WithDirectory("/src/grafana", grafana).
WithDirectory("/src/grafana-enterprise", enterprise).
WithWorkdir("/src/grafana-enterprise").
WithFile("/src/grafana/.buildinfo.enterprise-commit", hash).
WithExec([]string{"/bin/sh", "build.sh"}).
WithExec([]string{"cp", "LICENSE", "../grafana"}).
Directory("/src/grafana")
}
// GrafnaaOpts are populated by the 'GrafanaFlags' flags.
// These options define how to mount or clone the grafana/enterprise source code.
type GrafanaDirectoryOpts struct {
// GrafanaDir is the path to the Grafana source tree.
// If GrafanaDir is empty, then we're most likely cloning Grafana and using that as a directory.
GrafanaDir string
EnterpriseDir string
// GrafanaRepo will clone Grafana from a different repository when cloning Grafana.
GrafanaRepo string
EnterpriseRepo string
// GrafanaRef will checkout a specific tag, branch, or commit when cloning Grafana.
GrafanaRef string
EnterpriseRef string
// GitHubToken is used when cloning Grafana/Grafana Enterprise.
GitHubToken string
PatchesRepo string
PatchesPath string
PatchesRef string
}
func githubToken(ctx context.Context, token string) (string, error) {
// Since GrafanaDir was not provided, we must clone it.
ght := token
// If GitHubToken was not set from flag
if ght != "" {
return ght, nil
}
token, err := git.LookupGitHubToken(ctx)
if err != nil {
return "", err
}
if token == "" {
return "", fmt.Errorf("unable to acquire github token")
}
return token, nil
}
func GrafanaDirectoryOptsFromFlags(c cliutil.CLIContext) *GrafanaDirectoryOpts {
return &GrafanaDirectoryOpts{
GrafanaRepo: c.String("grafana-repo"),
EnterpriseRepo: c.String("enterprise-repo"),
GrafanaDir: c.String("grafana-dir"),
EnterpriseDir: c.String("enterprise-dir"),
GrafanaRef: c.String("grafana-ref"),
EnterpriseRef: c.String("enterprise-ref"),
GitHubToken: c.String("github-token"),
PatchesRepo: c.String("patches-repo"),
PatchesPath: c.String("patches-path"),
PatchesRef: c.String("patches-ref"),
}
}
func cloneOrMount(ctx context.Context, client *dagger.Client, localPath, repo, ref string, ght string) (*dagger.Directory, error) {
if localPath != "" {
absolute, err := filepath.Abs(localPath)
if err != nil {
return nil, fmt.Errorf("error getting absolute path for local dir: %w", err)
}
localPath = absolute
slog.Info("Using local directory for repository", "path", localPath, "repo", repo)
return daggerutil.HostDir(client, localPath)
}
ght, err := githubToken(ctx, ght)
if err != nil {
return nil, fmt.Errorf("error acquiring GitHub token: %w", err)
}
return git.CloneWithGitHubToken(client, ght, repo, ref)
}
func applyPatches(ctx context.Context, client *dagger.Client, src *dagger.Directory, repo, patchesPath, ref, ght string) (*dagger.Directory, error) {
ght, err := githubToken(ctx, ght)
if err != nil {
return nil, fmt.Errorf("error acquiring GitHub token: %w", err)
}
// Clone the patches repository on 'main'
dir, err := git.CloneWithGitHubToken(client, ght, repo, ref)
if err != nil {
return nil, fmt.Errorf("error cloning patches repository: %w", err)
}
entries, err := dir.Entries(ctx, dagger.DirectoryEntriesOpts{
Path: patchesPath,
})
if err != nil {
return nil, fmt.Errorf("error listing entries in repository: %w", err)
}
if len(entries) == 0 {
return nil, fmt.Errorf("no patches in the given path")
}
container := client.Container().From(git.GitImage).
WithEntrypoint([]string{}).
WithMountedDirectory("/src", src).
WithMountedDirectory("/patches", dir).
WithWorkdir("/src").
WithExec([]string{"git", "config", "--local", "user.name", "grafana"}).
WithExec([]string{"git", "config", "--local", "user.email", "engineering@grafana.com"})
for _, v := range entries {
if filepath.Ext(v) != ".patch" {
continue
}
container = container.WithExec([]string{"/bin/sh", "-c", fmt.Sprintf(`git am --3way --ignore-whitespace --ignore-space-change --committer-date-is-author-date %s > /dev/null 2>&1`, path.Join("/patches", patchesPath, v))})
}
return container.Directory("/src"), nil
}
func grafanaDirectory(ctx context.Context, opts *pipeline.ArgumentOpts) (any, error) {
o := GrafanaDirectoryOptsFromFlags(opts.CLIContext)
src, err := cloneOrMount(ctx, opts.Client, o.GrafanaDir, o.GrafanaRepo, o.GrafanaRef, o.GitHubToken)
if err != nil {
return nil, err
}
gitContainer := opts.Client.Container().From("alpine/git").
WithWorkdir("/src").
WithMountedDirectory("/src/.git", src.Directory(".git")).
WithEntrypoint([]string{})
commitFile := gitContainer.
WithExec([]string{"/bin/sh", "-c", "git rev-parse HEAD > .buildinfo.grafana-commit"}).
File("/src/.buildinfo.grafana-commit")
branchFile := gitContainer.
WithExec([]string{"/bin/sh", "-c", "git rev-parse --abbrev-ref HEAD > .buildinfo.grafana-branch"}).
File("/src/.buildinfo.grafana-branch")
src = src.
WithFile(".buildinfo.commit", commitFile).
WithFile(".buildinfo.branch", branchFile)
if o.PatchesRepo != "" {
withPatches, err := applyPatches(ctx, opts.Client, src, o.PatchesRepo, o.PatchesPath, o.PatchesRef, o.GitHubToken)
if err != nil {
opts.Log.Debug("patch application skipped", "error", err)
} else {
// Only replace src when there was no error.
src = withPatches
}
}
nodeVersion, err := frontend.NodeVersion(opts.Client, src).Stdout(ctx)
if err != nil {
return nil, fmt.Errorf("failed to get node version from source code: %w", err)
}
yarnCache, err := opts.State.CacheVolume(ctx, YarnCacheDirectory)
if err != nil {
return nil, err
}
container := frontend.YarnInstall(opts.Client, src, nodeVersion, yarnCache, opts.Platform)
if _, err := containers.ExitError(ctx, container); err != nil {
return nil, err
}
return container.Directory("/src"), nil
}
func enterpriseDirectory(ctx context.Context, opts *pipeline.ArgumentOpts) (any, error) {
// Get the Grafana directory...
o := GrafanaDirectoryOptsFromFlags(opts.CLIContext)
grafanaDir, err := grafanaDirectory(ctx, opts)
if err != nil {
return nil, fmt.Errorf("error initializing grafana directory: %w", err)
}
clone, err := cloneOrMount(ctx, opts.Client, o.EnterpriseDir, o.EnterpriseRepo, o.EnterpriseRef, o.GitHubToken)
if err != nil {
return nil, fmt.Errorf("error cloning or mounting Grafana Enterprise directory: %w", err)
}
return InitializeEnterprise(opts.Client, grafanaDir.(*dagger.Directory), clone), nil
}
var GrafanaDirectoryFlags = []cli.Flag{
&cli.StringFlag{
Name: "grafana-dir",
Usage: "Local Grafana dir to use, instead of git clone",
Required: false,
},
&cli.StringFlag{
Name: "enterprise-dir",
Usage: "Local Grafana Enterprise dir to use, instead of git clone",
Required: false,
},
&cli.StringFlag{
Name: "grafana-repo",
Usage: "Grafana repo to clone, not valid if --grafana-dir is set",
Required: false,
Value: "https://github.com/grafana/grafana.git",
},
&cli.StringFlag{
Name: "enterprise-repo",
Usage: "Grafana Enterprise repo to clone, not valid if --grafana-dir is set",
Required: false,
Value: "https://github.com/grafana/grafana-enterprise.git",
},
&cli.StringFlag{
Name: "grafana-ref",
Usage: "Grafana ref to clone, not valid if --grafana-dir is set",
Required: false,
Value: "main",
},
&cli.StringFlag{
Name: "enterprise-ref",
Usage: "Grafana Enterprise ref to clone, not valid if --grafana-dir is set",
Required: false,
Value: "main",
},
&cli.StringFlag{
Name: "github-token",
Usage: "GitHub token to use for git cloning, by default will be pulled from GitHub",
Required: false,
},
&cli.StringFlag{
Name: "patches-repo",
Usage: "GitHub repository that contains git patches to apply to the Grafana source code. Must be an https git URL",
},
&cli.StringFlag{
Name: "patches-path",
Usage: "Path to folder containing '.patch' files to apply",
},
&cli.StringFlag{
Name: "patches-ref",
Usage: "Ref to checkout in the patches repository",
Value: "main",
},
}
// GrafanaDirectory will provide the valueFunc that initializes and returns a *dagger.Directory that has Grafana in it.
// Where possible, when cloning and no authentication options are provided, the valuefunc will try to use the configured github CLI for cloning.
var GrafanaDirectory = pipeline.Argument{
Name: "grafana-dir",
Description: "The source tree of the Grafana repository",
Flags: GrafanaDirectoryFlags,
ValueFunc: grafanaDirectory,
}
// EnterpriseDirectory will provide the valueFunc that initializes and returns a *dagger.Directory that has Grafana Enterprise initialized it.
// Where possible, when cloning and no authentication options are provided, the valuefunc will try to use the configured github CLI for cloning.
var EnterpriseDirectory = pipeline.Argument{
Name: "enterprise-dir",
Description: "The source tree of Grafana Enterprise",
Flags: GrafanaDirectoryFlags,
ValueFunc: enterpriseDirectory,
}