CI: move grafana-build into pkg/build (#105640)

* move grafana-build into pkg/build
This commit is contained in:
Kevin Minehart
2025-05-20 10:48:00 -05:00
committed by GitHub
parent 759933d3e2
commit 13f4cf162e
222 changed files with 17387 additions and 442 deletions
+273
View File
@@ -0,0 +1,273 @@
package artifacts
import (
"context"
"errors"
"fmt"
"log/slog"
"os"
"dagger.io/dagger"
"github.com/grafana/grafana/pkg/build/daggerbuild/pipeline"
"github.com/urfave/cli/v2"
"golang.org/x/sync/errgroup"
"golang.org/x/sync/semaphore"
)
func Action(r Registerer, c *cli.Context) error {
// ArtifactStrings represent an artifact with a list of boolean options, like
// targz:linux/amd64:enterprise
artifactStrings := c.StringSlice("artifacts")
logLevel := slog.LevelInfo
if c.Bool("verbose") {
logLevel = slog.LevelDebug
}
var (
ctx = c.Context
log = slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
Level: logLevel,
}))
parallel = c.Int64("parallel")
destination = c.String("destination")
platform = dagger.Platform(c.String("platform"))
verify = c.Bool("verify")
checksum = c.Bool("checksum")
)
if len(artifactStrings) == 0 {
return errors.New("no artifacts specified. At least 1 artifact is required using the '--artifact' or '-a' flag")
}
log.Debug("Connecting to dagger daemon...")
daggerOpts := []dagger.ClientOpt{}
if logLevel == slog.LevelDebug {
daggerOpts = append(daggerOpts, dagger.WithLogOutput(os.Stderr))
}
client, err := dagger.Connect(ctx, daggerOpts...)
if err != nil {
return err
}
log.Debug("Connected to dagger daemon")
var state pipeline.StateHandler = &pipeline.State{
Log: log,
Client: client,
CLIContext: c,
Platform: platform,
}
registered := r.Initializers()
log.Debug("Generating artifacts from artifact strings...")
// Initialize the artifacts that were specified by the artifacts commands.
// These are specified by using artifact strings, or comma-delimited lists of flags.
artifacts, err := ArtifactsFromStrings(ctx, log, artifactStrings, registered, state)
if err != nil {
return err
}
log.Debug("Done generating artifact metadata")
state = pipeline.StateWithLogger(
log.With("service", "state"),
state,
)
// The artifact store is responsible for storing built artifacts and issuing them to artifacts that use them as dependencies using the artifact's filename as the key.
store := pipeline.NewArtifactStore(log)
opts := &pipeline.ArtifactContainerOpts{
Client: client,
Log: log,
State: state,
Platform: platform,
Store: store,
}
// Build each artifact and their dependencies, essentially constructing a dag using Dagger.
for i, v := range artifacts {
filename, err := v.Handler.Filename(ctx)
if err != nil {
return fmt.Errorf("error processing artifact string '%s': %w", artifactStrings[i], err)
}
log := log.With("filename", filename, "artifact", v.ArtifactString)
log.Info("Adding artifact to dag...")
if err := BuildArtifact(ctx, log, v, opts); err != nil {
return err
}
log.Info("Done adding artifact")
}
wg := &errgroup.Group{}
sm := semaphore.NewWeighted(parallel)
log.Info("Exporting artifacts...")
// Export the files from the dag, causing the containers to trigger.
for _, v := range artifacts {
log := log.With("artifact", v.ArtifactString, "action", "export")
wg.Go(ExportArtifactFunc(ctx, client, sm, log, v, store, destination, checksum))
}
if verify {
// Export the files from the dag, causing the containers to trigger.
for _, v := range artifacts {
log := log.With("artifact", v.ArtifactString, "action", "validate")
wg.Go(VerifyArtifactFunc(ctx, client, sm, log, v, store, destination))
}
}
return wg.Wait()
}
func BuildArtifact(ctx context.Context, log *slog.Logger, a *pipeline.Artifact, opts *pipeline.ArtifactContainerOpts) error {
store := opts.Store
exists, err := store.Exists(ctx, a)
if err != nil {
return err
}
if exists {
return nil
}
// populate the dependency list
dependencies, err := a.Handler.Dependencies(ctx)
if err != nil {
return err
}
// Get the files / directories that the dependencies define,
// and store the result for re-use.
for _, v := range dependencies {
f, err := v.Handler.Filename(ctx)
if err != nil {
return err
}
log := log.With("artifact", v.ArtifactString, "filename", f)
if err := BuildArtifact(ctx, log, v, opts); err != nil {
return err
}
}
switch a.Type {
case pipeline.ArtifactTypeDirectory:
dir, err := BuildArtifactDirectory(ctx, a, opts)
if err != nil {
return err
}
return store.StoreDirectory(ctx, a, dir)
case pipeline.ArtifactTypeFile:
file, err := BuildArtifactFile(ctx, a, opts)
if err != nil {
return err
}
return store.StoreFile(ctx, a, file)
}
return nil
}
func Command(r Registerer) func(c *cli.Context) error {
return func(c *cli.Context) error {
if err := Action(r, c); err != nil {
return cli.Exit(err, 1)
}
return nil
}
}
func BuildArtifactFile(ctx context.Context, a *pipeline.Artifact, opts *pipeline.ArtifactContainerOpts) (*dagger.File, error) {
builder, err := a.Handler.Builder(ctx, opts)
if err != nil {
return nil, err
}
return a.Handler.BuildFile(ctx, builder, opts)
}
func BuildArtifactDirectory(ctx context.Context, a *pipeline.Artifact, opts *pipeline.ArtifactContainerOpts) (*dagger.Directory, error) {
builder, err := a.Handler.Builder(ctx, opts)
if err != nil {
return nil, err
}
return a.Handler.BuildDir(ctx, builder, opts)
}
func ExportArtifactFunc(ctx context.Context, d *dagger.Client, sm *semaphore.Weighted, log *slog.Logger, v *pipeline.Artifact, store pipeline.ArtifactStore, dst string, checksum bool) func() error {
return func() error {
log.Info("Started exporting artifact...")
log.Info("Acquiring semaphore")
if err := sm.Acquire(ctx, 1); err != nil {
log.Info("Error acquiring semaphore", "error", err)
return err
}
log.Info("Acquired semaphore")
defer sm.Release(1)
filename, err := v.Handler.Filename(ctx)
if err != nil {
return fmt.Errorf("error processing artifact string '%s': %w", v.ArtifactString, err)
}
log.Info("Exporting artifact")
paths, err := store.Export(ctx, d, v, dst, checksum)
if err != nil {
return fmt.Errorf("error exporting artifact '%s': %w", filename, err)
}
for _, v := range paths {
if _, err := fmt.Fprintf(Stdout, "%s\n", v); err != nil {
return fmt.Errorf("error writing to stdout: %w", err)
}
}
log.Info("Done exporting artifact")
return nil
}
}
func verifyArtifact(ctx context.Context, client *dagger.Client, v *pipeline.Artifact, store pipeline.ArtifactStore) error {
switch v.Type {
case pipeline.ArtifactTypeDirectory:
file, err := store.Directory(ctx, v)
if err != nil {
return err
}
if err := v.Handler.VerifyDirectory(ctx, client, file); err != nil {
return err
}
case pipeline.ArtifactTypeFile:
file, err := store.File(ctx, v)
if err != nil {
return err
}
if err := v.Handler.VerifyFile(ctx, client, file); err != nil {
return err
}
}
return nil
}
func VerifyArtifactFunc(ctx context.Context, d *dagger.Client, sm *semaphore.Weighted, log *slog.Logger, v *pipeline.Artifact, store pipeline.ArtifactStore, dst string) func() error {
return func() error {
log.Info("Started verifying artifact...")
log.Info("Acquiring semaphore")
if err := sm.Acquire(ctx, 1); err != nil {
log.Info("Error acquiring semaphore", "error", err)
return err
}
log.Info("Acquired semaphore")
defer sm.Release(1)
if err := verifyArtifact(ctx, d, v, store); err != nil {
return err
}
return nil
}
}
+246
View File
@@ -0,0 +1,246 @@
package artifacts
import (
"context"
"log/slog"
"path/filepath"
"dagger.io/dagger"
"github.com/grafana/grafana/pkg/build/daggerbuild/arguments"
"github.com/grafana/grafana/pkg/build/daggerbuild/backend"
"github.com/grafana/grafana/pkg/build/daggerbuild/flags"
"github.com/grafana/grafana/pkg/build/daggerbuild/packages"
"github.com/grafana/grafana/pkg/build/daggerbuild/pipeline"
)
var (
BackendArguments = []pipeline.Argument{
arguments.GrafanaDirectory,
arguments.EnterpriseDirectory,
arguments.GoVersion,
arguments.ViceroyVersion,
}
BackendFlags = flags.JoinFlags(
flags.PackageNameFlags,
flags.DistroFlags(),
)
)
var BackendInitializer = Initializer{
InitializerFunc: NewBackendFromString,
Arguments: BackendArguments,
}
type Backend struct {
// Name allows different backend compilations to be different even if all other factors are the same.
// For example, Grafana Enterprise, Grafana, and Grafana Pro may be built using the same options,
// but are fundamentally different because of the source code of the binary.
Name packages.Name
Src *dagger.Directory
Distribution backend.Distribution
BuildOpts *backend.BuildOpts
GoVersion string
ViceroyVersion string
GoBuildCache *dagger.CacheVolume
GoModCache *dagger.CacheVolume
// Version is embedded in the binary at build-time
Version string
}
func (b *Backend) Builder(ctx context.Context, opts *pipeline.ArtifactContainerOpts) (*dagger.Container, error) {
return backend.Builder(
opts.Client,
opts.Log,
b.Distribution,
b.BuildOpts,
opts.Platform,
b.Src,
b.GoVersion,
b.ViceroyVersion,
b.GoBuildCache,
b.GoModCache,
)
}
func (b *Backend) Dependencies(ctx context.Context) ([]*pipeline.Artifact, error) {
return nil, nil
}
func (b *Backend) BuildFile(ctx context.Context, builder *dagger.Container, opts *pipeline.ArtifactContainerOpts) (*dagger.File, error) {
panic("not implemented") // TODO: Implement
}
func (b *Backend) BuildDir(ctx context.Context, builder *dagger.Container, opts *pipeline.ArtifactContainerOpts) (*dagger.Directory, error) {
f, err := b.Filename(ctx)
if err != nil {
return nil, err
}
return backend.Build(
opts.Client,
builder,
b.Src,
b.Distribution,
f,
b.BuildOpts,
), nil
}
func (b *Backend) Publisher(ctx context.Context, opts *pipeline.ArtifactContainerOpts) (*dagger.Container, error) {
panic("not implemented") // TODO: Implement
}
func (b *Backend) PublishFile(ctx context.Context, opts *pipeline.ArtifactPublishFileOpts) error {
panic("not implemented") // TODO: Implement
}
func (b *Backend) PublishDir(ctx context.Context, opts *pipeline.ArtifactPublishDirOpts) error {
panic("not implemented") // TODO: Implement
}
// Filename should return a deterministic file or folder name that this build will produce.
// This filename is used as a map key for caching, so implementers need to ensure that arguments or flags that affect the output
// also affect the filename to ensure that there are no collisions.
// For example, the backend for `linux/amd64` and `linux/arm64` should not both produce a `bin` folder, they should produce a
// `bin/linux-amd64` folder and a `bin/linux-arm64` folder. Callers can mount this as `bin` or whatever if they want.
func (b *Backend) Filename(ctx context.Context) (string, error) {
return filepath.Join("bin", string(b.Name), string(b.Distribution)), nil
}
func (b *Backend) VerifyFile(ctx context.Context, client *dagger.Client, file *dagger.File) error {
// Not a file
return nil
}
func (b *Backend) VerifyDirectory(ctx context.Context, client *dagger.Client, dir *dagger.Directory) error {
// Nothing to do (yet)
return nil
}
type NewBackendOpts struct {
Name packages.Name
Enterprise bool
Src *dagger.Directory
Distribution backend.Distribution
GoVersion string
ViceroyVersion string
Version string
Experiments []string
Tags []string
Static bool
WireTag string
GoBuildCache *dagger.CacheVolume
GoModCache *dagger.CacheVolume
}
func NewBackendFromString(ctx context.Context, log *slog.Logger, artifact string, state pipeline.StateHandler) (*pipeline.Artifact, error) {
goVersion, err := state.String(ctx, arguments.GoVersion)
if err != nil {
return nil, err
}
viceroyVersion, err := state.String(ctx, arguments.ViceroyVersion)
if err != nil {
return nil, err
}
goModCache, err := state.CacheVolume(ctx, arguments.GoModCache)
if err != nil {
return nil, err
}
goBuildCache, err := state.CacheVolume(ctx, arguments.GoBuildCache)
if err != nil {
return nil, err
}
// 1. Figure out the options that were provided as part of the artifact string.
// For example, `linux/amd64:grafana`.
options, err := pipeline.ParseFlags(artifact, TargzFlags)
if err != nil {
return nil, err
}
static, err := options.Bool(flags.Static)
if err != nil {
return nil, err
}
wireTag, err := options.String(flags.WireTag)
if err != nil {
return nil, err
}
experiments, err := options.StringSlice(flags.GoExperiments)
if err != nil {
return nil, err
}
tags, err := options.StringSlice(flags.GoTags)
if err != nil {
return nil, err
}
p, err := GetPackageDetails(ctx, options, state)
if err != nil {
return nil, err
}
src, err := GrafanaDir(ctx, state, p.Enterprise)
if err != nil {
return nil, err
}
bopts := &backend.BuildOpts{
Version: p.Version,
Enterprise: p.Enterprise,
ExperimentalFlags: experiments,
Static: static,
WireTag: wireTag,
Tags: tags,
}
return pipeline.ArtifactWithLogging(ctx, log, &pipeline.Artifact{
ArtifactString: artifact,
Type: pipeline.ArtifactTypeDirectory,
Flags: BackendFlags,
Handler: &Backend{
Name: p.Name,
Distribution: p.Distribution,
BuildOpts: bopts,
GoVersion: goVersion,
ViceroyVersion: viceroyVersion,
Src: src,
GoModCache: goModCache,
GoBuildCache: goBuildCache,
},
})
}
func NewBackend(ctx context.Context, log *slog.Logger, artifact string, opts *NewBackendOpts) (*pipeline.Artifact, error) {
bopts := &backend.BuildOpts{
Version: opts.Version,
Enterprise: opts.Enterprise,
ExperimentalFlags: opts.Experiments,
Tags: opts.Tags,
Static: opts.Static,
WireTag: opts.WireTag,
}
log.Info("Initializing backend artifact with options", "static", opts.Static, "version", opts.Version, "name", opts.Name, "distro", opts.Distribution)
return pipeline.ArtifactWithLogging(ctx, log, &pipeline.Artifact{
ArtifactString: artifact,
Type: pipeline.ArtifactTypeDirectory,
Flags: BackendFlags,
Handler: &Backend{
Name: opts.Name,
Distribution: opts.Distribution,
BuildOpts: bopts,
GoVersion: opts.GoVersion,
ViceroyVersion: opts.ViceroyVersion,
Src: opts.Src,
GoModCache: opts.GoModCache,
GoBuildCache: opts.GoBuildCache,
},
})
}
+75
View File
@@ -0,0 +1,75 @@
package artifacts
import (
"sort"
"strings"
"log/slog"
"github.com/grafana/grafana/pkg/build/daggerbuild/cmd/flags"
"github.com/urfave/cli/v2"
)
func ArtifactFlags(r Registerer) []cli.Flag {
artifactsFlag := &cli.StringSliceFlag{
Name: "artifacts",
Aliases: []string{"a"},
}
buildFlag := &cli.BoolFlag{
Name: "build",
Value: true,
}
publishFlag := &cli.BoolFlag{
Name: "publish",
Usage: "If true, then the artifacts that are built will be published. If `--build=false` and the artifacts are found in the --destination, then those artifacts are not built and are published instead.",
Value: true,
}
verifyFlag := &cli.BoolFlag{
Name: "verify",
Usage: "If true, then the artifacts that are built will be verified with e2e tests or similar after being exported, depending on the artifact",
Value: false,
}
flags := flags.Join(
[]cli.Flag{
artifactsFlag,
buildFlag,
publishFlag,
verifyFlag,
flags.Platform,
},
flags.PublishFlags,
flags.ConcurrencyFlags,
[]cli.Flag{
flags.Verbose,
},
)
// All of these artifacts are the registered artifacts. These should mostly stay the same no matter what.
initializers := r.Initializers()
// Add all of the CLI flags that are defined by each artifact's arguments.
m := map[string]cli.Flag{}
// For artifact arguments that specify flags, we'll coalesce them here and add them to the list of flags.
for _, n := range initializers {
for _, arg := range n.Arguments {
for _, f := range arg.Flags {
fn := strings.Join(f.Names(), ",")
m[fn] = f
slog.Debug("global flag added by argument in artifact", "flag", fn, "arg", arg.Name)
}
}
}
for _, v := range m {
flags = append(flags, v)
}
sort.Slice(flags, func(i, j int) bool {
return strings.Compare(flags[i].Names()[0], flags[j].Names()[0]) <= 0
})
return flags
}
+147
View File
@@ -0,0 +1,147 @@
package artifacts
import (
"context"
"fmt"
"log/slog"
"path/filepath"
"dagger.io/dagger"
"github.com/grafana/grafana/pkg/build/daggerbuild/arguments"
"github.com/grafana/grafana/pkg/build/daggerbuild/flags"
"github.com/grafana/grafana/pkg/build/daggerbuild/frontend"
"github.com/grafana/grafana/pkg/build/daggerbuild/pipeline"
)
var (
FrontendFlags = flags.PackageNameFlags
FrontendArguments = []pipeline.Argument{
arguments.YarnCacheDirectory,
}
)
var FrontendInitializer = Initializer{
InitializerFunc: NewFrontendFromString,
Arguments: FrontendArguments,
}
type Frontend struct {
Enterprise bool
Version string
Src *dagger.Directory
YarnCache *dagger.CacheVolume
}
// The frontend does not have any artifact dependencies.
func (f *Frontend) Dependencies(ctx context.Context) ([]*pipeline.Artifact, error) {
return nil, nil
}
// Builder will return a node.js alpine container that matches the .nvmrc in the Grafana source repository
func (f *Frontend) Builder(ctx context.Context, opts *pipeline.ArtifactContainerOpts) (*dagger.Container, error) {
return FrontendBuilder(ctx, f.Src, f.YarnCache, opts)
}
func (f *Frontend) BuildFile(ctx context.Context, builder *dagger.Container, opts *pipeline.ArtifactContainerOpts) (*dagger.File, error) {
panic("not implemented") // Frontend doesn't return a file
}
func (f *Frontend) BuildDir(ctx context.Context, builder *dagger.Container, opts *pipeline.ArtifactContainerOpts) (*dagger.Directory, error) {
return frontend.Build(builder), nil
}
func (f *Frontend) Publisher(ctx context.Context, opts *pipeline.ArtifactContainerOpts) (*dagger.Container, error) {
panic("not implemented") // TODO: Implement
}
func (f *Frontend) PublishFile(ctx context.Context, opts *pipeline.ArtifactPublishFileOpts) error {
panic("not implemented") // TODO: Implement
}
func (f *Frontend) PublishDir(ctx context.Context, opts *pipeline.ArtifactPublishDirOpts) error {
panic("not implemented") // TODO: Implement
}
// Filename should return a deterministic file or folder name that this build will produce.
// This filename is used as a map key for caching, so implementers need to ensure that arguments or flags that affect the output
// also affect the filename to ensure that there are no collisions.
// For example, the backend for `linux/amd64` and `linux/arm64` should not both produce a `bin` folder, they should produce a
// `bin/linux-amd64` folder and a `bin/linux-arm64` folder. Callers can mount this as `bin` or whatever if they want.
func (f *Frontend) Filename(ctx context.Context) (string, error) {
n := "grafana"
if f.Enterprise {
n = "grafana-enterprise"
}
// Important note: this path is only used in two ways:
// 1. When requesting an artifact be built and exported, this is the path where it will be exported to
// 2. In a map to distinguish when the same artifact is being built more than once
return filepath.Join(f.Version, n, "public"), nil
}
func (f *Frontend) VerifyFile(ctx context.Context, client *dagger.Client, file *dagger.File) error {
// Should never be called since this isn't a File.
return nil
}
func (f *Frontend) VerifyDirectory(ctx context.Context, client *dagger.Client, dir *dagger.Directory) error {
// Nothing to do to verify these (for now?)
return nil
}
func NewFrontendFromString(ctx context.Context, log *slog.Logger, artifact string, state pipeline.StateHandler) (*pipeline.Artifact, error) {
options, err := pipeline.ParseFlags(artifact, FrontendFlags)
if err != nil {
return nil, err
}
enterprise, err := options.Bool(flags.Enterprise)
if err != nil {
return nil, err
}
src, err := GrafanaDir(ctx, state, enterprise)
if err != nil {
return nil, err
}
cache, err := state.CacheVolume(ctx, arguments.YarnCacheDirectory)
if err != nil {
return nil, err
}
version, err := state.String(ctx, arguments.Version)
if err != nil {
return nil, err
}
return NewFrontend(ctx, log, artifact, version, enterprise, src, cache)
}
func NewFrontend(ctx context.Context, log *slog.Logger, artifact, version string, enterprise bool, src *dagger.Directory, cache *dagger.CacheVolume) (*pipeline.Artifact, error) {
return pipeline.ArtifactWithLogging(ctx, log, &pipeline.Artifact{
ArtifactString: artifact,
Type: pipeline.ArtifactTypeDirectory,
Flags: FrontendFlags,
Handler: &Frontend{
Enterprise: enterprise,
Version: version,
Src: src,
YarnCache: cache,
},
})
}
func FrontendBuilder(
ctx context.Context,
src *dagger.Directory,
cache *dagger.CacheVolume,
opts *pipeline.ArtifactContainerOpts,
) (*dagger.Container, error) {
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)
}
return frontend.Builder(opts.Client, opts.Platform, src, nodeVersion, cache), nil
}
@@ -0,0 +1,16 @@
package artifacts
import (
"context"
"dagger.io/dagger"
"github.com/grafana/grafana/pkg/build/daggerbuild/arguments"
"github.com/grafana/grafana/pkg/build/daggerbuild/pipeline"
)
func GrafanaDir(ctx context.Context, state pipeline.StateHandler, enterprise bool) (*dagger.Directory, error) {
if enterprise {
return state.Directory(ctx, arguments.EnterpriseDirectory)
}
return state.Directory(ctx, arguments.GrafanaDirectory)
}
+114
View File
@@ -0,0 +1,114 @@
package artifacts
import (
"context"
"log/slog"
"path/filepath"
"strings"
"dagger.io/dagger"
"github.com/grafana/grafana/pkg/build/daggerbuild/arguments"
"github.com/grafana/grafana/pkg/build/daggerbuild/flags"
"github.com/grafana/grafana/pkg/build/daggerbuild/frontend"
"github.com/grafana/grafana/pkg/build/daggerbuild/pipeline"
)
var (
NPMPackagesFlags = flags.PackageNameFlags
NPMPackagesArguments = []pipeline.Argument{
arguments.YarnCacheDirectory,
}
)
var NPMPackagesInitializer = Initializer{
InitializerFunc: NewNPMPackagesFromString,
Arguments: NPMPackagesArguments,
}
type NPMPackages struct {
Src *dagger.Directory
YarnCache *dagger.CacheVolume
Version string
}
// The frontend does not have any artifact dependencies.
func (f *NPMPackages) Dependencies(ctx context.Context) ([]*pipeline.Artifact, error) {
return nil, nil
}
// Builder will return a node.js alpine container that matches the .nvmrc in the Grafana source repository
func (f *NPMPackages) Builder(ctx context.Context, opts *pipeline.ArtifactContainerOpts) (*dagger.Container, error) {
return FrontendBuilder(ctx, f.Src, f.YarnCache, opts)
}
func (f *NPMPackages) BuildFile(ctx context.Context, builder *dagger.Container, opts *pipeline.ArtifactContainerOpts) (*dagger.File, error) {
panic("not implemented") // NPMPackages doesn't return a file
}
func (f *NPMPackages) BuildDir(ctx context.Context, builder *dagger.Container, opts *pipeline.ArtifactContainerOpts) (*dagger.Directory, error) {
return frontend.NPMPackages(builder, opts.Client, opts.Log, f.Src, strings.TrimPrefix(f.Version, "v"))
}
func (f *NPMPackages) Publisher(ctx context.Context, opts *pipeline.ArtifactContainerOpts) (*dagger.Container, error) {
panic("not implemented") // TODO: Implement
}
func (f *NPMPackages) PublishFile(ctx context.Context, opts *pipeline.ArtifactPublishFileOpts) error {
panic("not implemented") // TODO: Implement
}
func (f *NPMPackages) PublishDir(ctx context.Context, opts *pipeline.ArtifactPublishDirOpts) error {
panic("not implemented") // TODO: Implement
}
func (f *NPMPackages) VerifyFile(ctx context.Context, client *dagger.Client, file *dagger.File) error {
// Not a file
return nil
}
func (f *NPMPackages) VerifyDirectory(ctx context.Context, client *dagger.Client, dir *dagger.Directory) error {
// Nothing to verify (yet?)
return nil
}
// Filename should return a deterministic file or folder name that this build will produce.
// This filename is used as a map key for caching, so implementers need to ensure that arguments or flags that affect the output
// also affect the filename to ensure that there are no collisions.
// For example, the backend for `linux/amd64` and `linux/arm64` should not both produce a `bin` folder, they should produce a
// `bin/linux-amd64` folder and a `bin/linux-arm64` folder. Callers can mount this as `bin` or whatever if they want.
func (f *NPMPackages) Filename(ctx context.Context) (string, error) {
// Important note: this path is only used in two ways:
// 1. When requesting an artifact be built and exported, this is the path where it will be exported to
// 2. In a map to distinguish when the same artifact is being built more than once
return filepath.Join(f.Version, "npm-packages"), nil
}
func NewNPMPackagesFromString(ctx context.Context, log *slog.Logger, artifact string, state pipeline.StateHandler) (*pipeline.Artifact, error) {
grafanaDir, err := GrafanaDir(ctx, state, false)
if err != nil {
return nil, err
}
cache, err := state.CacheVolume(ctx, arguments.YarnCacheDirectory)
if err != nil {
return nil, err
}
version, err := state.String(ctx, arguments.Version)
if err != nil {
return nil, err
}
return NewNPMPackages(ctx, log, artifact, grafanaDir, version, cache)
}
func NewNPMPackages(ctx context.Context, log *slog.Logger, artifact string, src *dagger.Directory, version string, cache *dagger.CacheVolume) (*pipeline.Artifact, error) {
return pipeline.ArtifactWithLogging(ctx, log, &pipeline.Artifact{
ArtifactString: artifact,
Type: pipeline.ArtifactTypeDirectory,
Flags: NPMPackagesFlags,
Handler: &NPMPackages{
Src: src,
YarnCache: cache,
Version: version,
},
})
}
@@ -0,0 +1,179 @@
package artifacts
import (
"context"
"log/slog"
"strings"
"dagger.io/dagger"
"github.com/grafana/grafana/pkg/build/daggerbuild/arguments"
"github.com/grafana/grafana/pkg/build/daggerbuild/backend"
"github.com/grafana/grafana/pkg/build/daggerbuild/flags"
"github.com/grafana/grafana/pkg/build/daggerbuild/fpm"
"github.com/grafana/grafana/pkg/build/daggerbuild/packages"
"github.com/grafana/grafana/pkg/build/daggerbuild/pipeline"
)
var (
DebArguments = TargzArguments
DebFlags = flags.JoinFlags(
TargzFlags,
[]pipeline.Flag{
flags.NightlyFlag,
},
)
)
var DebInitializer = Initializer{
InitializerFunc: NewDebFromString,
Arguments: TargzArguments,
}
// PacakgeDeb uses a built tar.gz package to create a .deb installer for debian based Linux distributions.
type Deb struct {
Name packages.Name
Version string
BuildID string
Distribution backend.Distribution
Enterprise bool
NameOverride string
Tarball *pipeline.Artifact
// Src is the source tree of Grafana. This should only be used in the verify function.
Src *dagger.Directory
YarnCache *dagger.CacheVolume
}
func (d *Deb) Dependencies(ctx context.Context) ([]*pipeline.Artifact, error) {
return []*pipeline.Artifact{
d.Tarball,
}, nil
}
func (d *Deb) Builder(ctx context.Context, opts *pipeline.ArtifactContainerOpts) (*dagger.Container, error) {
return fpm.Builder(opts.Client), nil
}
func debVersion(version string) string {
// If there is a `+security-` modifier to the version, simply use `-`
return strings.ReplaceAll(version, "+security-", "-")
}
func (d *Deb) BuildFile(ctx context.Context, builder *dagger.Container, opts *pipeline.ArtifactContainerOpts) (*dagger.File, error) {
targz, err := opts.Store.File(ctx, d.Tarball)
if err != nil {
return nil, err
}
return fpm.Build(builder, fpm.BuildOpts{
Name: d.Name,
Enterprise: d.Enterprise,
Version: debVersion(d.Version),
BuildID: d.BuildID,
Distribution: d.Distribution,
PackageType: fpm.PackageTypeDeb,
NameOverride: d.NameOverride,
ConfigFiles: [][]string{
{"/src/packaging/deb/default/grafana-server", "/pkg/etc/default/grafana-server"},
{"/src/packaging/deb/init.d/grafana-server", "/pkg/etc/init.d/grafana-server"},
{"/src/packaging/deb/systemd/grafana-server.service", "/pkg/usr/lib/systemd/system/grafana-server.service"},
},
AfterInstall: "/src/packaging/deb/control/postinst",
BeforeRemove: "/src/packaging/deb/control/prerm",
Depends: []string{
"adduser",
"musl",
},
EnvFolder: "/pkg/etc/default",
ExtraArgs: []string{
"--deb-no-default-config-files",
},
}, targz), nil
}
func (d *Deb) BuildDir(ctx context.Context, builder *dagger.Container, opts *pipeline.ArtifactContainerOpts) (*dagger.Directory, error) {
panic("not implemented") // TODO: Implement
}
func (d *Deb) Publisher(ctx context.Context, opts *pipeline.ArtifactContainerOpts) (*dagger.Container, error) {
panic("not implemented") // TODO: Implement
}
func (d *Deb) PublishFile(ctx context.Context, opts *pipeline.ArtifactPublishFileOpts) error {
panic("not implemented") // TODO: Implement
}
func (d *Deb) PublishDir(ctx context.Context, opts *pipeline.ArtifactPublishDirOpts) error {
panic("not implemented") // TODO: Implement
}
// Filename should return a deterministic file or folder name that this build will produce.
// This filename is used as a map key for caching, so implementers need to ensure that arguments or flags that affect the output
// also affect the filename to ensure that there are no collisions.
// For example, the backend for `linux/amd64` and `linux/arm64` should not both produce a `bin` folder, they should produce a
// `bin/linux-amd64` folder and a `bin/linux-arm64` folder. Callers can mount this as `bin` or whatever if they want.
func (d *Deb) Filename(ctx context.Context) (string, error) {
name := d.Name
if d.NameOverride != "" {
name = packages.Name(d.NameOverride)
}
return packages.FileName(name, d.Version, d.BuildID, d.Distribution, "deb")
}
func (d *Deb) VerifyFile(ctx context.Context, client *dagger.Client, file *dagger.File) error {
return fpm.VerifyDeb(ctx, client, file, d.Src, d.YarnCache, d.Distribution, d.Enterprise)
}
func (d *Deb) VerifyDirectory(ctx context.Context, client *dagger.Client, dir *dagger.Directory) error {
panic("not implemented") // TODO: Implement
}
func NewDebFromString(ctx context.Context, log *slog.Logger, artifact string, state pipeline.StateHandler) (*pipeline.Artifact, error) {
tarball, err := NewTarballFromString(ctx, log, artifact, state)
if err != nil {
return nil, err
}
options, err := pipeline.ParseFlags(artifact, DebFlags)
if err != nil {
return nil, err
}
p, err := GetPackageDetails(ctx, options, state)
if err != nil {
return nil, err
}
src, err := state.Directory(ctx, arguments.GrafanaDirectory)
if err != nil {
return nil, err
}
yarnCache, err := state.CacheVolume(ctx, arguments.YarnCacheDirectory)
if err != nil {
return nil, err
}
debname := string(p.Name)
if nightly, _ := options.Bool(flags.Nightly); nightly {
debname += "-nightly"
}
if rpi, _ := options.Bool(flags.RPI); rpi {
debname += "-rpi"
}
return pipeline.ArtifactWithLogging(ctx, log, &pipeline.Artifact{
ArtifactString: artifact,
Handler: &Deb{
Name: p.Name,
Version: p.Version,
BuildID: p.BuildID,
Distribution: p.Distribution,
Enterprise: p.Enterprise,
Tarball: tarball,
Src: src,
YarnCache: yarnCache,
NameOverride: debname,
},
Type: pipeline.ArtifactTypeFile,
Flags: TargzFlags,
})
}
@@ -0,0 +1,265 @@
package artifacts
import (
"context"
"fmt"
"log/slog"
"strings"
"dagger.io/dagger"
"github.com/grafana/grafana/pkg/build/daggerbuild/arguments"
"github.com/grafana/grafana/pkg/build/daggerbuild/backend"
"github.com/grafana/grafana/pkg/build/daggerbuild/docker"
"github.com/grafana/grafana/pkg/build/daggerbuild/flags"
"github.com/grafana/grafana/pkg/build/daggerbuild/packages"
"github.com/grafana/grafana/pkg/build/daggerbuild/pipeline"
)
var (
DockerArguments = arguments.Join(
TargzArguments,
[]pipeline.Argument{
arguments.DockerRegistry,
arguments.DockerOrg,
arguments.AlpineImage,
arguments.UbuntuImage,
arguments.TagFormat,
arguments.UbuntuTagFormat,
arguments.BoringTagFormat,
},
)
DockerFlags = flags.JoinFlags(
TargzFlags,
flags.DockerFlags,
)
)
var DockerInitializer = Initializer{
InitializerFunc: NewDockerFromString,
Arguments: DockerArguments,
}
// PacakgeDocker uses a built tar.gz package to create a docker image from the Dockerfile in the tar.gz
type Docker struct {
Name packages.Name
Version string
BuildID string
Distro backend.Distribution
Enterprise bool
Ubuntu bool
Registry string
Repositories []string
Org string
BaseImage string
TagFormat string
Tarball *pipeline.Artifact
// Src is the Grafana source code for running e2e tests when validating.
// The grafana source should not be used for anything else when building a docker image. All files in the Docker image, including the Dockerfile, should be
// from the tar.gz file.
Src *dagger.Directory
YarnCache *dagger.CacheVolume
}
func (d *Docker) Dependencies(ctx context.Context) ([]*pipeline.Artifact, error) {
return []*pipeline.Artifact{
d.Tarball,
}, nil
}
func (d *Docker) Builder(ctx context.Context, opts *pipeline.ArtifactContainerOpts) (*dagger.Container, error) {
targz, err := opts.Store.File(ctx, d.Tarball)
if err != nil {
return nil, err
}
return docker.Builder(opts.Client, opts.Client.Host().UnixSocket("/var/run/docker.sock"), targz), nil
}
func (d *Docker) BuildFile(ctx context.Context, builder *dagger.Container, opts *pipeline.ArtifactContainerOpts) (*dagger.File, error) {
// Unlike most other things we push to, docker image tags do not support all characters.
// Specifically, the `+` character used in the `buildmetadata` section of semver.
version := strings.ReplaceAll(d.Version, "+", "-")
tags, err := docker.Tags(d.Org, d.Registry, d.Repositories, d.TagFormat, packages.NameOpts{
Name: d.Name,
Version: version,
BuildID: d.BuildID,
Distro: d.Distro,
})
if err != nil {
return nil, err
}
buildOpts := &docker.BuildOpts{
// Tags are provided as the '-t' argument, and can include the registry domain as well as the repository.
// Docker build supports building the same image with multiple tags.
// You might want to also include a 'latest' version of the tag.
Tags: tags,
Platform: backend.Platform(d.Distro),
BuildArgs: []string{
"GRAFANA_TGZ=grafana.tar.gz",
"GO_SRC=tgz-builder",
"JS_SRC=tgz-builder",
fmt.Sprintf("BASE_IMAGE=%s", d.BaseImage),
},
}
b := docker.Build(opts.Client, builder, buildOpts)
return docker.Save(b, buildOpts), nil
}
func (d *Docker) BuildDir(ctx context.Context, builder *dagger.Container, opts *pipeline.ArtifactContainerOpts) (*dagger.Directory, error) {
panic("This artifact does not produce directories")
}
func (d *Docker) Publisher(ctx context.Context, opts *pipeline.ArtifactContainerOpts) (*dagger.Container, error) {
socket := opts.Client.Host().UnixSocket("/var/run/docker.sock")
return opts.Client.Container().From("docker").WithUnixSocket("/var/run/docker.sock", socket), nil
}
func (d *Docker) PublishFile(ctx context.Context, opts *pipeline.ArtifactPublishFileOpts) error {
panic("not implemented")
}
func (d *Docker) PublishDir(ctx context.Context, opts *pipeline.ArtifactPublishDirOpts) error {
panic("This artifact does not produce directories")
}
// Filename should return a deterministic file or folder name that this build will produce.
// This filename is used as a map key for caching, so implementers need to ensure that arguments or flags that affect the output
// also affect the filename to ensure that there are no collisions.
// For example, the backend for `linux/amd64` and `linux/arm64` should not both produce a `bin` folder, they should produce a
// `bin/linux-amd64` folder and a `bin/linux-arm64` folder. Callers can mount this as `bin` or whatever if they want.
func (d *Docker) Filename(ctx context.Context) (string, error) {
ext := "docker.tar.gz"
if d.Ubuntu {
ext = "ubuntu.docker.tar.gz"
}
return packages.FileName(d.Name, d.Version, d.BuildID, d.Distro, ext)
}
func (d *Docker) VerifyFile(ctx context.Context, client *dagger.Client, file *dagger.File) error {
// Currently verifying riscv64 is unsupported (because alpine and ubuntu don't have riscv64 images yet)
if _, arch := backend.OSAndArch(d.Distro); arch == "riscv64" {
return nil
}
return docker.Verify(ctx, client, file, d.Src, d.YarnCache, d.Distro)
}
func (d *Docker) VerifyDirectory(ctx context.Context, client *dagger.Client, dir *dagger.Directory) error {
panic("not implemented") // TODO: Implement
}
func NewDockerFromString(ctx context.Context, log *slog.Logger, artifact string, state pipeline.StateHandler) (*pipeline.Artifact, error) {
options, err := pipeline.ParseFlags(artifact, DockerFlags)
if err != nil {
return nil, err
}
p, err := GetPackageDetails(ctx, options, state)
if err != nil {
return nil, err
}
tarball, err := NewTarballFromString(ctx, log, artifact, state)
if err != nil {
return nil, err
}
ubuntu, err := options.Bool(flags.Ubuntu)
if err != nil {
return nil, err
}
// Ubuntu Version to use as the base for the Grafana docker image (if this is a ubuntu artifact)
// This shouldn't fail if it's not set by the user, instead it'll default to 22.04 or something.
ubuntuImage, err := state.String(ctx, arguments.UbuntuImage)
if err != nil {
return nil, err
}
// Same for Alpine
alpineImage, err := state.String(ctx, arguments.AlpineImage)
if err != nil {
return nil, err
}
registry, err := state.String(ctx, arguments.DockerRegistry)
if err != nil {
return nil, err
}
org, err := state.String(ctx, arguments.DockerOrg)
if err != nil {
return nil, err
}
repos, err := options.StringSlice(flags.DockerRepositories)
if err != nil {
return nil, err
}
format, err := state.String(ctx, arguments.TagFormat)
if err != nil {
return nil, err
}
ubuntuFormat, err := state.String(ctx, arguments.UbuntuTagFormat)
if err != nil {
return nil, err
}
boringFormat, err := state.String(ctx, arguments.BoringTagFormat)
if err != nil {
return nil, err
}
base := alpineImage
if ubuntu {
format = ubuntuFormat
base = ubuntuImage
}
if p.Name == packages.PackageEnterpriseBoring {
format = boringFormat
}
src, err := state.Directory(ctx, arguments.GrafanaDirectory)
if err != nil {
return nil, err
}
yarnCache, err := state.CacheVolume(ctx, arguments.YarnCacheDirectory)
if err != nil {
return nil, err
}
log.Info("initializing Docker artifact", "Org", org, "registry", registry, "repos", repos, "tag", format)
return pipeline.ArtifactWithLogging(ctx, log, &pipeline.Artifact{
ArtifactString: artifact,
Handler: &Docker{
Name: p.Name,
Version: p.Version,
BuildID: p.BuildID,
Distro: p.Distribution,
Enterprise: p.Enterprise,
Tarball: tarball,
Ubuntu: ubuntu,
BaseImage: base,
Registry: registry,
Org: org,
Repositories: repos,
TagFormat: format,
Src: src,
YarnCache: yarnCache,
},
Type: pipeline.ArtifactTypeFile,
Flags: DockerFlags,
})
}
@@ -0,0 +1,202 @@
package artifacts
import (
"context"
"fmt"
"log/slog"
"strings"
"dagger.io/dagger"
"github.com/grafana/grafana/pkg/build/daggerbuild/arguments"
"github.com/grafana/grafana/pkg/build/daggerbuild/backend"
"github.com/grafana/grafana/pkg/build/daggerbuild/docker"
"github.com/grafana/grafana/pkg/build/daggerbuild/flags"
"github.com/grafana/grafana/pkg/build/daggerbuild/packages"
"github.com/grafana/grafana/pkg/build/daggerbuild/pipeline"
)
var (
EntDockerArguments = arguments.Join(
DebArguments,
[]pipeline.Argument{
arguments.HGDirectory,
arguments.EntDockerRegistry,
arguments.EntDockerOrg,
arguments.EntDockerRepo,
arguments.HGTagFormat,
},
)
EntDockerFlags = flags.JoinFlags(
DebFlags,
flags.DockerFlags,
)
)
var EntDockerInitializer = Initializer{
InitializerFunc: NewEntDockerFromString,
Arguments: EntDockerArguments,
}
// EntDocker uses a built deb installer to create a docker image
type EntDocker struct {
Name packages.Name
Version string
BuildID string
Distro backend.Distribution
EntDir *dagger.Directory
// EntRegistry is the docker registry when using the `enterprise` name. (e.g. hub.docker.io)
EntRegistry string
// EntOrg is the docker org when using the `enterprise` name. (e.g. grafana)
EntOrg string
// EntOrg is the docker repo when using the `enterprise` name. (e.g. grafana-enterprise)
EntRepo string
// TagFormat is the docker tag format when using the `enterprise` name. (e.g. {{ .version }}-{{ .os }}-{{ .arch }})
TagFormat string
Deb *pipeline.Artifact
}
func (d *EntDocker) Dependencies(ctx context.Context) ([]*pipeline.Artifact, error) {
return []*pipeline.Artifact{
d.Deb,
}, nil
}
func (d *EntDocker) Builder(ctx context.Context, opts *pipeline.ArtifactContainerOpts) (*dagger.Container, error) {
deb, err := opts.Store.File(ctx, d.Deb)
if err != nil {
return nil, fmt.Errorf("error getting deb from state: %w", err)
}
socket := opts.Client.Host().UnixSocket("/var/run/docker.sock")
return opts.Client.Container().From("docker").
WithUnixSocket("/var/run/docker.sock", socket).
WithMountedDirectory("/src", d.EntDir).
WithMountedFile("/src/grafana.deb", deb).
WithWorkdir("/src"), nil
}
func (d *EntDocker) BuildFile(ctx context.Context, builder *dagger.Container, opts *pipeline.ArtifactContainerOpts) (*dagger.File, error) {
tags, err := docker.Tags(d.EntOrg, d.EntRegistry, []string{d.EntRepo}, d.TagFormat, packages.NameOpts{
Name: d.Name,
Version: d.Version,
BuildID: d.BuildID,
Distro: d.Distro,
})
if err != nil {
return nil, err
}
builder = docker.Build(opts.Client, builder, &docker.BuildOpts{
Dockerfile: "./docker/hosted-grafana-all/Dockerfile",
Tags: tags,
Target: "hosted-grafana-localenterprise",
Platform: dagger.Platform("linux/amd64"),
BuildArgs: []string{
"RELEASE_TYPE=main",
// I think because deb files use a ~ as a version delimiter of some kind, so the hg docker image uses that instead of a -
fmt.Sprintf("GRAFANA_VERSION=%s", strings.Replace(d.Version, "-", "~", 1)),
},
})
// Save the resulting docker image to the local filesystem
return builder.WithExec([]string{"docker", "save", tags[0], "-o", "enterprise.tar"}).File("enterprise.tar"), nil
}
func (d *EntDocker) BuildDir(ctx context.Context, builder *dagger.Container, opts *pipeline.ArtifactContainerOpts) (*dagger.Directory, error) {
panic("This artifact does not produce directories")
}
func (d *EntDocker) Publisher(ctx context.Context, opts *pipeline.ArtifactContainerOpts) (*dagger.Container, error) {
panic("not implemented")
}
func (d *EntDocker) PublishFile(ctx context.Context, opts *pipeline.ArtifactPublishFileOpts) error {
panic("not implemented")
}
func (d *EntDocker) PublishDir(ctx context.Context, opts *pipeline.ArtifactPublishDirOpts) error {
panic("This artifact does not produce directories")
}
// Filename should return a deterministic file or folder name that this build will produce.
// This filename is used as a map key for caching, so implementers need to ensure that arguments or flags that affect the output
// also affect the filename to ensure that there are no collisions.
// For example, the backend for `linux/amd64` and `linux/arm64` should not both produce a `bin` folder, they should produce a
// `bin/linux-amd64` folder and a `bin/linux-arm64` folder. Callers can mount this as `bin` or whatever if they want.
func (d *EntDocker) Filename(ctx context.Context) (string, error) {
ext := "docker-enterprise.tar.gz"
return packages.FileName(d.Name, d.Version, d.BuildID, d.Distro, ext)
}
func (d *EntDocker) VerifyFile(ctx context.Context, client *dagger.Client, file *dagger.File) error {
return nil
}
func (d *EntDocker) VerifyDirectory(ctx context.Context, client *dagger.Client, dir *dagger.Directory) error {
panic("not implemented") // TODO: Implement
}
func NewEntDockerFromString(ctx context.Context, log *slog.Logger, artifact string, state pipeline.StateHandler) (*pipeline.Artifact, error) {
options, err := pipeline.ParseFlags(artifact, DockerFlags)
if err != nil {
return nil, err
}
p, err := GetPackageDetails(ctx, options, state)
if err != nil {
return nil, err
}
deb, err := NewDebFromString(ctx, log, artifact, state)
if err != nil {
return nil, err
}
entRegistry, err := state.String(ctx, arguments.EntDockerRegistry)
if err != nil {
return nil, err
}
entOrg, err := state.String(ctx, arguments.EntDockerOrg)
if err != nil {
return nil, err
}
entRepo, err := state.String(ctx, arguments.EntDockerRepo)
if err != nil {
return nil, err
}
tagFormat, err := state.String(ctx, arguments.HGTagFormat)
if err != nil {
return nil, err
}
dir, err := state.Directory(ctx, arguments.HGDirectory)
if err != nil {
return nil, err
}
log.Info("initializing Enterprise Docker artifact", "Org", entOrg, "registry", entRegistry, "repo", entRepo, "tag", tagFormat)
return pipeline.ArtifactWithLogging(ctx, log, &pipeline.Artifact{
ArtifactString: artifact,
Handler: &EntDocker{
Name: p.Name,
Version: p.Version,
BuildID: p.BuildID,
Distro: p.Distribution,
EntDir: dir,
Deb: deb,
EntRegistry: entRegistry,
EntOrg: entOrg,
EntRepo: entRepo,
TagFormat: tagFormat,
},
Type: pipeline.ArtifactTypeFile,
Flags: DockerFlags,
})
}
@@ -0,0 +1,203 @@
package artifacts
import (
"context"
"fmt"
"log/slog"
"strings"
"dagger.io/dagger"
"github.com/grafana/grafana/pkg/build/daggerbuild/arguments"
"github.com/grafana/grafana/pkg/build/daggerbuild/backend"
"github.com/grafana/grafana/pkg/build/daggerbuild/docker"
"github.com/grafana/grafana/pkg/build/daggerbuild/flags"
"github.com/grafana/grafana/pkg/build/daggerbuild/packages"
"github.com/grafana/grafana/pkg/build/daggerbuild/pipeline"
)
var (
ProDockerArguments = arguments.Join(
DebArguments,
[]pipeline.Argument{
arguments.HGDirectory,
arguments.ProDockerRegistry,
arguments.ProDockerOrg,
arguments.ProDockerRepo,
arguments.HGTagFormat,
},
)
ProDockerFlags = flags.JoinFlags(
DebFlags,
flags.DockerFlags,
)
)
var ProDockerInitializer = Initializer{
InitializerFunc: NewProDockerFromString,
Arguments: ProDockerArguments,
}
// ProDocker uses a built deb installer to create a docker image
type ProDocker struct {
Name packages.Name
Version string
BuildID string
Distro backend.Distribution
ProDir *dagger.Directory
// ProRegistry is the docker registry when using the `pro` name. (e.g. hub.docker.io)
ProRegistry string
// ProOrg is the docker org when using the `pro` name. (e.g. grafana)
ProOrg string
// ProOrg is the docker repo when using the `pro` name. (e.g. grafana-pro)
ProRepo string
// TagFormat is the docker tag format when using the `pro` name. (e.g. {{ .version }}-{{ .os }}-{{ .arch }})
TagFormat string
// Building the Pro image requires a Debian package instead of a tar.gz
Deb *pipeline.Artifact
}
func (d *ProDocker) Dependencies(ctx context.Context) ([]*pipeline.Artifact, error) {
return []*pipeline.Artifact{
d.Deb,
}, nil
}
func (d *ProDocker) Builder(ctx context.Context, opts *pipeline.ArtifactContainerOpts) (*dagger.Container, error) {
deb, err := opts.Store.File(ctx, d.Deb)
if err != nil {
return nil, fmt.Errorf("error getting deb from state: %w", err)
}
socket := opts.Client.Host().UnixSocket("/var/run/docker.sock")
return opts.Client.Container().From("docker").
WithUnixSocket("/var/run/docker.sock", socket).
WithMountedDirectory("/src", d.ProDir).
WithMountedFile("/src/grafana.deb", deb).
WithWorkdir("/src"), nil
}
func (d *ProDocker) BuildFile(ctx context.Context, builder *dagger.Container, opts *pipeline.ArtifactContainerOpts) (*dagger.File, error) {
tags, err := docker.Tags(d.ProOrg, d.ProRegistry, []string{d.ProRepo}, d.TagFormat, packages.NameOpts{
Name: d.Name,
Version: d.Version,
BuildID: d.BuildID,
Distro: d.Distro,
})
if err != nil {
return nil, err
}
builder = docker.Build(opts.Client, builder, &docker.BuildOpts{
Dockerfile: "./docker/hosted-grafana-all/Dockerfile",
Tags: tags,
Target: "hosted-grafana-localpro",
Platform: dagger.Platform("linux/amd64"),
BuildArgs: []string{
"RELEASE_TYPE=main",
// I think because deb files use a ~ as a version delimiter of some kind, so the hg docker image uses that instead of a -
fmt.Sprintf("GRAFANA_VERSION=%s", strings.Replace(d.Version, "-", "~", 1)),
},
})
// Save the resulting docker image to the local filesystem
return builder.WithExec([]string{"docker", "save", tags[0], "-o", "pro.tar"}).File("pro.tar"), nil
}
func (d *ProDocker) BuildDir(ctx context.Context, builder *dagger.Container, opts *pipeline.ArtifactContainerOpts) (*dagger.Directory, error) {
panic("This artifact does not produce directories")
}
func (d *ProDocker) Publisher(ctx context.Context, opts *pipeline.ArtifactContainerOpts) (*dagger.Container, error) {
panic("not implemented")
}
func (d *ProDocker) PublishFile(ctx context.Context, opts *pipeline.ArtifactPublishFileOpts) error {
panic("not implemented")
}
func (d *ProDocker) PublishDir(ctx context.Context, opts *pipeline.ArtifactPublishDirOpts) error {
panic("This artifact does not produce directories")
}
// Filename should return a deterministic file or folder name that this build will produce.
// This filename is used as a map key for caching, so implementers need to ensure that arguments or flags that affect the output
// also affect the filename to ensure that there are no collisions.
// For example, the backend for `linux/amd64` and `linux/arm64` should not both produce a `bin` folder, they should produce a
// `bin/linux-amd64` folder and a `bin/linux-arm64` folder. Callers can mount this as `bin` or whatever if they want.
func (d *ProDocker) Filename(ctx context.Context) (string, error) {
ext := "docker-pro.tar.gz"
return packages.FileName(d.Name, d.Version, d.BuildID, d.Distro, ext)
}
func (d *ProDocker) VerifyFile(ctx context.Context, client *dagger.Client, file *dagger.File) error {
return nil
}
func (d *ProDocker) VerifyDirectory(ctx context.Context, client *dagger.Client, dir *dagger.Directory) error {
panic("not implemented") // TODO: Implement
}
func NewProDockerFromString(ctx context.Context, log *slog.Logger, artifact string, state pipeline.StateHandler) (*pipeline.Artifact, error) {
options, err := pipeline.ParseFlags(artifact, DockerFlags)
if err != nil {
return nil, err
}
p, err := GetPackageDetails(ctx, options, state)
if err != nil {
return nil, err
}
deb, err := NewDebFromString(ctx, log, artifact, state)
if err != nil {
return nil, err
}
proRegistry, err := state.String(ctx, arguments.ProDockerRegistry)
if err != nil {
return nil, err
}
proOrg, err := state.String(ctx, arguments.ProDockerOrg)
if err != nil {
return nil, err
}
proRepo, err := state.String(ctx, arguments.ProDockerRepo)
if err != nil {
return nil, err
}
tagFormat, err := state.String(ctx, arguments.HGTagFormat)
if err != nil {
return nil, err
}
dir, err := state.Directory(ctx, arguments.HGDirectory)
if err != nil {
return nil, err
}
log.Info("initializing Pro Docker artifact", "Org", proOrg, "registry", proRegistry, "repo", proRepo, "tag", tagFormat)
return pipeline.ArtifactWithLogging(ctx, log, &pipeline.Artifact{
ArtifactString: artifact,
Handler: &ProDocker{
Name: p.Name,
Version: p.Version,
BuildID: p.BuildID,
Distro: p.Distribution,
ProDir: dir,
Deb: deb,
ProRegistry: proRegistry,
ProOrg: proOrg,
ProRepo: proRepo,
TagFormat: tagFormat,
},
Type: pipeline.ArtifactTypeFile,
Flags: DockerFlags,
})
}
@@ -0,0 +1,121 @@
package artifacts
import (
"context"
"fmt"
"log/slog"
"dagger.io/dagger"
"github.com/grafana/grafana/pkg/build/daggerbuild/backend"
"github.com/grafana/grafana/pkg/build/daggerbuild/msi"
"github.com/grafana/grafana/pkg/build/daggerbuild/packages"
"github.com/grafana/grafana/pkg/build/daggerbuild/pipeline"
)
var (
MSIArguments = TargzArguments
MSIFlags = TargzFlags
)
var MSIInitializer = Initializer{
InitializerFunc: NewMSIFromString,
Arguments: TargzArguments,
}
// PacakgeMSI uses a built tar.gz package to create a .exe installer for exeian based Linux distributions.
type MSI struct {
Name packages.Name
Version string
BuildID string
Distribution backend.Distribution
Enterprise bool
Tarball *pipeline.Artifact
}
func (d *MSI) Dependencies(ctx context.Context) ([]*pipeline.Artifact, error) {
return []*pipeline.Artifact{
d.Tarball,
}, nil
}
func (d *MSI) Builder(ctx context.Context, opts *pipeline.ArtifactContainerOpts) (*dagger.Container, error) {
return msi.Builder(opts.Client)
}
func (d *MSI) BuildFile(ctx context.Context, builder *dagger.Container, opts *pipeline.ArtifactContainerOpts) (*dagger.File, error) {
targz, err := opts.Store.File(ctx, d.Tarball)
if err != nil {
return nil, err
}
return msi.Build(opts.Client, builder, targz, d.Version, d.Enterprise)
}
func (d *MSI) BuildDir(ctx context.Context, builder *dagger.Container, opts *pipeline.ArtifactContainerOpts) (*dagger.Directory, error) {
// Not a directory so this shouldn't be called
return nil, nil
}
func (d *MSI) Publisher(ctx context.Context, opts *pipeline.ArtifactContainerOpts) (*dagger.Container, error) {
return nil, nil
}
func (d *MSI) PublishFile(ctx context.Context, opts *pipeline.ArtifactPublishFileOpts) error {
panic("not implemented") // TODO: Implement
}
func (d *MSI) PublishDir(ctx context.Context, opts *pipeline.ArtifactPublishDirOpts) error {
// Not a directory so this shouldn't be called
return nil
}
// Filename should return a deterministic file or folder name that this build will produce.
// This filename is used as a map key for caching, so implementers need to ensure that arguments or flags that affect the output
// also affect the filename to ensure that there are no collisions.
// For example, the backend for `linux/amd64` and `linux/arm64` should not both produce a `bin` folder, they should produce a
// `bin/linux-amd64` folder and a `bin/linux-arm64` folder. Callers can mount this as `bin` or whatever if they want.
func (d *MSI) Filename(ctx context.Context) (string, error) {
return packages.FileName(d.Name, d.Version, d.BuildID, d.Distribution, "msi")
}
func (d *MSI) VerifyFile(ctx context.Context, client *dagger.Client, file *dagger.File) error {
return nil
}
func (d *MSI) VerifyDirectory(ctx context.Context, client *dagger.Client, dir *dagger.Directory) error {
panic("not implemented") // TODO: Implement
}
func NewMSIFromString(ctx context.Context, log *slog.Logger, artifact string, state pipeline.StateHandler) (*pipeline.Artifact, error) {
targz, err := NewTarballFromString(ctx, log, artifact, state)
if err != nil {
return nil, err
}
options, err := pipeline.ParseFlags(artifact, MSIFlags)
if err != nil {
return nil, err
}
p, err := GetPackageDetails(ctx, options, state)
if err != nil {
return nil, err
}
if !backend.IsWindows(p.Distribution) {
return nil, fmt.Errorf("distribution ('%s') for exe '%s' is not a Windows distribution", string(p.Distribution), artifact)
}
return pipeline.ArtifactWithLogging(ctx, log, &pipeline.Artifact{
ArtifactString: artifact,
Handler: &MSI{
Name: p.Name,
Version: p.Version,
BuildID: p.BuildID,
Distribution: p.Distribution,
Enterprise: p.Enterprise,
Tarball: targz,
},
Type: pipeline.ArtifactTypeFile,
Flags: ZipFlags,
})
}
@@ -0,0 +1,242 @@
package artifacts
import (
"context"
"encoding/base64"
"fmt"
"log/slog"
"strings"
"dagger.io/dagger"
"github.com/grafana/grafana/pkg/build/daggerbuild/arguments"
"github.com/grafana/grafana/pkg/build/daggerbuild/backend"
"github.com/grafana/grafana/pkg/build/daggerbuild/flags"
"github.com/grafana/grafana/pkg/build/daggerbuild/fpm"
"github.com/grafana/grafana/pkg/build/daggerbuild/gpg"
"github.com/grafana/grafana/pkg/build/daggerbuild/packages"
"github.com/grafana/grafana/pkg/build/daggerbuild/pipeline"
)
var (
RPMArguments = TargzArguments
RPMFlags = flags.JoinFlags(
TargzFlags,
[]pipeline.Flag{
flags.SignFlag,
flags.NightlyFlag,
},
)
)
var RPMInitializer = Initializer{
InitializerFunc: NewRPMFromString,
Arguments: arguments.Join(
TargzArguments,
[]pipeline.Argument{
arguments.GPGPublicKey,
arguments.GPGPrivateKey,
arguments.GPGPassphrase,
},
),
}
// PacakgeRPM uses a built tar.gz package to create a .rpm installer for RHEL-ish Linux distributions.
type RPM struct {
Name packages.Name
Version string
BuildID string
Distribution backend.Distribution
Enterprise bool
Sign bool
NameOverride string
GPGPublicKey string
GPGPrivateKey string
GPGPassphrase string
Src *dagger.Directory
YarnCache *dagger.CacheVolume
Tarball *pipeline.Artifact
}
func (d *RPM) Dependencies(ctx context.Context) ([]*pipeline.Artifact, error) {
return []*pipeline.Artifact{
d.Tarball,
}, nil
}
func (d *RPM) Builder(ctx context.Context, opts *pipeline.ArtifactContainerOpts) (*dagger.Container, error) {
return fpm.Builder(opts.Client), nil
}
func rpmVersion(version string) string {
// https://docs.fedoraproject.org/en-US/packaging-guidelines/Versioning/#_snapshots
// If there's a buildmeta revision, then use that as a snapshot version
return strings.ReplaceAll(version, "+", "^")
}
func (d *RPM) BuildFile(ctx context.Context, builder *dagger.Container, opts *pipeline.ArtifactContainerOpts) (*dagger.File, error) {
targz, err := opts.Store.File(ctx, d.Tarball)
if err != nil {
return nil, err
}
rpm := fpm.Build(builder, fpm.BuildOpts{
Name: d.Name,
Enterprise: d.Enterprise,
Version: rpmVersion(d.Version),
BuildID: d.BuildID,
Distribution: d.Distribution,
PackageType: fpm.PackageTypeRPM,
NameOverride: d.NameOverride,
ConfigFiles: [][]string{
{"/src/packaging/rpm/sysconfig/grafana-server", "/pkg/etc/sysconfig/grafana-server"},
{"/src/packaging/rpm/systemd/grafana-server.service", "/pkg/usr/lib/systemd/system/grafana-server.service"},
},
AfterInstall: "/src/packaging/rpm/control/postinst",
Depends: []string{
"/sbin/service",
},
ExtraArgs: []string{
"--rpm-posttrans=/src/packaging/rpm/control/posttrans",
"--rpm-digest=sha256",
},
EnvFolder: "/pkg/etc/sysconfig",
}, targz)
if !d.Sign {
return rpm, nil
}
return gpg.Sign(opts.Client, rpm, gpg.GPGOpts{
GPGPublicKey: d.GPGPublicKey,
GPGPrivateKey: d.GPGPrivateKey,
GPGPassphrase: d.GPGPassphrase,
}), nil
}
func (d *RPM) BuildDir(ctx context.Context, builder *dagger.Container, opts *pipeline.ArtifactContainerOpts) (*dagger.Directory, error) {
panic("not implemented") // TODO: Implement
}
func (d *RPM) Publisher(ctx context.Context, opts *pipeline.ArtifactContainerOpts) (*dagger.Container, error) {
panic("not implemented") // TODO: Implement
}
func (d *RPM) PublishFile(ctx context.Context, opts *pipeline.ArtifactPublishFileOpts) error {
panic("not implemented") // TODO: Implement
}
func (d *RPM) PublishDir(ctx context.Context, opts *pipeline.ArtifactPublishDirOpts) error {
panic("not implemented") // TODO: Implement
}
// Filename should return a deterministic file or folder name that this build will produce.
// This filename is used as a map key for caching, so implementers need to ensure that arguments or flags that affect the output
// also affect the filename to ensure that there are no collisions.
// For example, the backend for `linux/amd64` and `linux/arm64` should not both produce a `bin` folder, they should produce a
// `bin/linux-amd64` folder and a `bin/linux-arm64` folder. Callers can mount this as `bin` or whatever if they want.
func (d *RPM) Filename(ctx context.Context) (string, error) {
name := d.Name
if d.NameOverride != "" {
name = packages.Name(d.NameOverride)
}
return packages.FileName(name, d.Version, d.BuildID, d.Distribution, "rpm")
}
func (d *RPM) VerifyFile(ctx context.Context, client *dagger.Client, file *dagger.File) error {
return nil
// return fpm.VerifyRpm(ctx, client, file, d.Src, d.YarnCache, d.Distribution, d.Enterprise, d.Sign, d.GPGPublicKey, d.GPGPrivateKey, d.GPGPassphrase)
}
func (d *RPM) VerifyDirectory(ctx context.Context, client *dagger.Client, dir *dagger.Directory) error {
panic("not implemented") // TODO: Implement
}
func NewRPMFromString(ctx context.Context, log *slog.Logger, artifact string, state pipeline.StateHandler) (*pipeline.Artifact, error) {
tarball, err := NewTarballFromString(ctx, log, artifact, state)
if err != nil {
return nil, err
}
options, err := pipeline.ParseFlags(artifact, RPMFlags)
if err != nil {
return nil, err
}
p, err := GetPackageDetails(ctx, options, state)
if err != nil {
return nil, err
}
sign, err := options.Bool(flags.Sign)
if err != nil {
return nil, err
}
src, err := state.Directory(ctx, arguments.GrafanaDirectory)
if err != nil {
return nil, err
}
yarnCache, err := state.CacheVolume(ctx, arguments.YarnCacheDirectory)
if err != nil {
return nil, err
}
var gpgPublicKey, gpgPrivateKey, gpgPassphrase string
if sign {
pubb64, err := state.String(ctx, arguments.GPGPublicKey)
if err != nil {
return nil, err
}
pub, err := base64.StdEncoding.DecodeString(pubb64)
if err != nil {
return nil, fmt.Errorf("gpg-private-key-base64 cannot be decoded %w", err)
}
privb64, err := state.String(ctx, arguments.GPGPrivateKey)
if err != nil {
return nil, err
}
priv, err := base64.StdEncoding.DecodeString(privb64)
if err != nil {
return nil, fmt.Errorf("gpg-private-key-base64 cannot be decoded %w", err)
}
pass, err := state.String(ctx, arguments.GPGPassphrase)
if err != nil {
return nil, err
}
gpgPublicKey = string(pub)
gpgPrivateKey = string(priv)
gpgPassphrase = pass
}
rpmname := string(p.Name)
if nightly, _ := options.Bool(flags.Nightly); nightly {
rpmname += "-nightly"
}
if rpi, _ := options.Bool(flags.RPI); rpi {
rpmname += "-rpi"
}
return pipeline.ArtifactWithLogging(ctx, log, &pipeline.Artifact{
ArtifactString: artifact,
Handler: &RPM{
Name: p.Name,
Version: p.Version,
BuildID: p.BuildID,
Distribution: p.Distribution,
Enterprise: p.Enterprise,
Tarball: tarball,
Sign: sign,
Src: src,
YarnCache: yarnCache,
GPGPublicKey: gpgPublicKey,
GPGPrivateKey: gpgPrivateKey,
GPGPassphrase: gpgPassphrase,
NameOverride: rpmname,
},
Type: pipeline.ArtifactTypeFile,
Flags: TargzFlags,
})
}
@@ -0,0 +1,382 @@
package artifacts
import (
"context"
"fmt"
"log/slog"
"dagger.io/dagger"
"github.com/grafana/grafana/pkg/build/daggerbuild/arguments"
"github.com/grafana/grafana/pkg/build/daggerbuild/backend"
"github.com/grafana/grafana/pkg/build/daggerbuild/containers"
"github.com/grafana/grafana/pkg/build/daggerbuild/e2e"
"github.com/grafana/grafana/pkg/build/daggerbuild/flags"
"github.com/grafana/grafana/pkg/build/daggerbuild/frontend"
"github.com/grafana/grafana/pkg/build/daggerbuild/packages"
"github.com/grafana/grafana/pkg/build/daggerbuild/pipeline"
"github.com/grafana/grafana/pkg/build/daggerbuild/targz"
)
var (
TargzArguments = []pipeline.Argument{
// Tarballs need the Build ID and version for naming the package properly.
arguments.BuildID,
arguments.Version,
// The grafanadirectory has contents like the LICENSE.txt and such that need to be included in the package
arguments.GrafanaDirectory,
// The go version used to build the backend
arguments.GoVersion,
arguments.ViceroyVersion,
arguments.YarnCacheDirectory,
}
TargzFlags = flags.JoinFlags(
flags.StdPackageFlags(),
)
)
var TargzInitializer = Initializer{
InitializerFunc: NewTarballFromString,
Arguments: TargzArguments,
}
type Tarball struct {
Distribution backend.Distribution
Name packages.Name
BuildID string
Version string
GoVersion string
Enterprise bool
Grafana *dagger.Directory
YarnCache *dagger.CacheVolume
// Dependent artifacts
Backend *pipeline.Artifact
Frontend *pipeline.Artifact
NPMPackages *pipeline.Artifact
BundledPlugins *pipeline.Artifact
Storybook *pipeline.Artifact
}
func NewTarballFromString(ctx context.Context, log *slog.Logger, artifact string, state pipeline.StateHandler) (*pipeline.Artifact, error) {
goVersion, err := state.String(ctx, arguments.GoVersion)
if err != nil {
return nil, err
}
viceroyVersion, err := state.String(ctx, arguments.ViceroyVersion)
if err != nil {
return nil, err
}
// 1. Figure out the options that were provided as part of the artifact string.
// For example, `linux/amd64:grafana`.
options, err := pipeline.ParseFlags(artifact, TargzFlags)
if err != nil {
return nil, err
}
static, err := options.Bool(flags.Static)
if err != nil {
return nil, err
}
wireTag, err := options.String(flags.WireTag)
if err != nil {
return nil, err
}
tags, err := options.StringSlice(flags.GoTags)
if err != nil {
return nil, err
}
experiments, err := options.StringSlice(flags.GoExperiments)
if err != nil {
return nil, err
}
yarnCache, err := state.CacheVolume(ctx, arguments.YarnCacheDirectory)
if err != nil {
return nil, err
}
goModCache, err := state.CacheVolume(ctx, arguments.GoModCache)
if err != nil {
return nil, err
}
goBuildCache, err := state.CacheVolume(ctx, arguments.GoBuildCache)
if err != nil {
return nil, err
}
p, err := GetPackageDetails(ctx, options, state)
if err != nil {
return nil, err
}
log.Info("Initializing tar.gz artifact with options", "name", p.Name, "build ID", p.BuildID, "version", p.Version, "distro", p.Distribution, "static", static, "enterprise", p.Enterprise)
src, err := GrafanaDir(ctx, state, p.Enterprise)
if err != nil {
return nil, err
}
return NewTarball(ctx, log, artifact, p.Distribution, p.Enterprise, p.Name, p.Version, p.BuildID, src, yarnCache, goModCache, goBuildCache, static, wireTag, tags, goVersion, viceroyVersion, experiments)
}
// NewTarball returns a properly initialized Tarball artifact.
// There are a lot of options that can affect how a tarball is built; most of which define different ways for the backend to be built.
func NewTarball(
ctx context.Context,
log *slog.Logger,
artifact string,
distro backend.Distribution,
enterprise bool,
name packages.Name,
version string,
buildID string,
src *dagger.Directory,
cache *dagger.CacheVolume,
goModCache *dagger.CacheVolume,
goBuildCache *dagger.CacheVolume,
static bool,
wireTag string,
tags []string,
goVersion string,
viceroyVersion string,
experiments []string,
) (*pipeline.Artifact, error) {
backendArtifact, err := NewBackend(ctx, log, artifact, &NewBackendOpts{
Name: name,
Version: version,
Distribution: distro,
Src: src,
Static: static,
WireTag: wireTag,
Tags: tags,
GoVersion: goVersion,
ViceroyVersion: viceroyVersion,
Experiments: experiments,
Enterprise: enterprise,
GoBuildCache: goBuildCache,
GoModCache: goModCache,
})
if err != nil {
return nil, err
}
frontendArtifact, err := NewFrontend(ctx, log, version, artifact, enterprise, src, cache)
if err != nil {
return nil, err
}
bundledPluginsArtifact, err := NewBundledPlugins(ctx, log, artifact, src, version, cache)
if err != nil {
return nil, err
}
npmArtifact, err := NewNPMPackages(ctx, log, artifact, src, version, cache)
if err != nil {
return nil, err
}
storybookArtifact, err := NewStorybook(ctx, log, artifact, src, version, cache)
if err != nil {
return nil, err
}
tarball := &Tarball{
Name: name,
Distribution: distro,
Version: version,
GoVersion: goVersion,
BuildID: buildID,
Grafana: src,
Enterprise: enterprise,
YarnCache: cache,
Backend: backendArtifact,
Frontend: frontendArtifact,
NPMPackages: npmArtifact,
BundledPlugins: bundledPluginsArtifact,
Storybook: storybookArtifact,
}
return pipeline.ArtifactWithLogging(ctx, log, &pipeline.Artifact{
ArtifactString: artifact,
Handler: tarball,
Type: pipeline.ArtifactTypeFile,
Flags: TargzFlags,
})
}
func (t *Tarball) Builder(ctx context.Context, opts *pipeline.ArtifactContainerOpts) (*dagger.Container, error) {
version := t.Version
container := opts.Client.Container().
From("alpine:3.18.4").
WithExec([]string{"apk", "add", "--update", "tar"}).
WithExec([]string{"/bin/sh", "-c", fmt.Sprintf("echo %s > VERSION", version)})
return container, nil
}
func (t *Tarball) BuildFile(ctx context.Context, b *dagger.Container, opts *pipeline.ArtifactContainerOpts) (*dagger.File, error) {
var (
state = opts.State
log = opts.Log
)
log.Debug("Getting grafana dir from state...")
// The Grafana directory is used for other packaged data like Dockerfile, license.txt, etc.
grafanaDir := t.Grafana
backendDir, err := opts.Store.Directory(ctx, t.Backend)
if err != nil {
return nil, err
}
frontendDir, err := opts.Store.Directory(ctx, t.Frontend)
if err != nil {
return nil, err
}
npmDir, err := opts.Store.Directory(ctx, t.NPMPackages)
if err != nil {
return nil, err
}
storybookDir, err := opts.Store.Directory(ctx, t.Storybook)
if err != nil {
return nil, err
}
pluginsDir, err := opts.Store.Directory(ctx, t.BundledPlugins)
if err != nil {
return nil, err
}
version, err := state.String(ctx, arguments.Version)
if err != nil {
return nil, err
}
files := []targz.MappedFile{
targz.NewMappedFile("VERSION", b.File("VERSION")),
targz.NewMappedFile("LICENSE", grafanaDir.File("LICENSE")),
targz.NewMappedFile("NOTICE.md", grafanaDir.File("NOTICE.md")),
targz.NewMappedFile("README.md", grafanaDir.File("README.md")),
targz.NewMappedFile("Dockerfile", grafanaDir.File("Dockerfile")),
targz.NewMappedFile("tools/zoneinfo.zip", opts.Client.Container().From(fmt.Sprintf("golang:%s", t.GoVersion)).File("/usr/local/go/lib/time/zoneinfo.zip")),
}
directories := []targz.MappedDirectory{
targz.NewMappedDir("conf", grafanaDir.Directory("conf")),
targz.NewMappedDir("docs/sources", grafanaDir.Directory("docs/sources")),
targz.NewMappedDir("packaging/deb", grafanaDir.Directory("packaging/deb")),
targz.NewMappedDir("packaging/rpm", grafanaDir.Directory("packaging/rpm")),
targz.NewMappedDir("packaging/docker", grafanaDir.Directory("packaging/docker")),
targz.NewMappedDir("packaging/wrappers", grafanaDir.Directory("packaging/wrappers")),
targz.NewMappedDir("bin", backendDir),
targz.NewMappedDir("public", frontendDir),
targz.NewMappedDir("npm-artifacts", npmDir),
targz.NewMappedDir("storybook", storybookDir),
targz.NewMappedDir("plugins-bundled", pluginsDir),
}
root := fmt.Sprintf("grafana-%s", version)
return targz.Build(
b,
&targz.Opts{
Root: root,
Files: files,
Directories: directories,
},
), nil
}
func (t *Tarball) BuildDir(ctx context.Context, builder *dagger.Container, opts *pipeline.ArtifactContainerOpts) (*dagger.Directory, error) {
panic("not implemented") // TODO: Implement
}
func (t *Tarball) Publisher(ctx context.Context, opts *pipeline.ArtifactContainerOpts) (*dagger.Container, error) {
panic("not implemented") // TODO: Implement
}
func (t *Tarball) PublishFile(ctx context.Context, opts *pipeline.ArtifactPublishFileOpts) error {
return nil
}
func (t *Tarball) PublishDir(ctx context.Context, opts *pipeline.ArtifactPublishDirOpts) error {
panic("not implemented") // TODO: Implement
}
func (t *Tarball) VerifyFile(ctx context.Context, client *dagger.Client, file *dagger.File) error {
// Currently verifying riscv64 is unsupported (because alpine and ubuntu don't have riscv64 images yet)
// windows/darwin verification may never be supported.
os, arch := backend.OSAndArch(t.Distribution)
if os != "linux" || arch == "riscv64" {
return nil
}
return verifyTarball(ctx, client, file, t.Grafana, t.YarnCache, t.Distribution, t.Enterprise)
}
func (t *Tarball) VerifyDirectory(ctx context.Context, client *dagger.Client, dir *dagger.Directory) error {
panic("not implemented") // TODO: Implement
}
func (t *Tarball) Dependencies(ctx context.Context) ([]*pipeline.Artifact, error) {
return []*pipeline.Artifact{
t.Backend,
t.Frontend,
t.NPMPackages,
t.BundledPlugins,
t.Storybook,
}, nil
}
func (t *Tarball) Filename(ctx context.Context) (string, error) {
return packages.FileName(t.Name, t.Version, t.BuildID, t.Distribution, "tar.gz")
}
func verifyTarball(
ctx context.Context,
d *dagger.Client,
pkg *dagger.File,
src *dagger.Directory,
yarnCache *dagger.CacheVolume,
distro backend.Distribution,
enterprise bool,
) error {
nodeVersion, err := frontend.NodeVersion(d, src).Stdout(ctx)
if err != nil {
return fmt.Errorf("failed to get node version from source code: %w", err)
}
var (
platform = backend.Platform(distro)
archive = containers.ExtractedArchive(d, pkg)
)
// This grafana service runs in the background for the e2e tests
service := d.Container(dagger.ContainerOpts{
Platform: platform,
}).From("ubuntu:22.04").
WithExec([]string{"apt-get", "update", "-yq"}).
WithExec([]string{"apt-get", "install", "-yq", "ca-certificates", "libfontconfig1"}).
WithDirectory("/src", archive).
WithWorkdir("/src")
if err := e2e.ValidateLicense(ctx, service, "/src/LICENSE", enterprise); err != nil {
return err
}
service = service.
WithExec([]string{"./bin/grafana", "server"}).
WithExposedPort(3000)
if _, err := containers.ExitError(ctx, e2e.ValidatePackage(d, service.AsService(), src, yarnCache, nodeVersion)); err != nil {
return err
}
return nil
}
@@ -0,0 +1,113 @@
package artifacts
import (
"context"
"log/slog"
"dagger.io/dagger"
"github.com/grafana/grafana/pkg/build/daggerbuild/backend"
"github.com/grafana/grafana/pkg/build/daggerbuild/packages"
"github.com/grafana/grafana/pkg/build/daggerbuild/pipeline"
"github.com/grafana/grafana/pkg/build/daggerbuild/zip"
)
var (
ZipArguments = TargzArguments
ZipFlags = TargzFlags
)
var ZipInitializer = Initializer{
InitializerFunc: NewZipFromString,
Arguments: TargzArguments,
}
// PacakgeZip uses a built tar.gz package to create a .zip package for zipian based Linux distributions.
type Zip struct {
Name packages.Name
Version string
BuildID string
Distribution backend.Distribution
Enterprise bool
Tarball *pipeline.Artifact
}
func (d *Zip) Dependencies(ctx context.Context) ([]*pipeline.Artifact, error) {
return []*pipeline.Artifact{
d.Tarball,
}, nil
}
func (d *Zip) Builder(ctx context.Context, opts *pipeline.ArtifactContainerOpts) (*dagger.Container, error) {
return zip.Builder(opts.Client), nil
}
func (d *Zip) BuildFile(ctx context.Context, builder *dagger.Container, opts *pipeline.ArtifactContainerOpts) (*dagger.File, error) {
targz, err := opts.Store.File(ctx, d.Tarball)
if err != nil {
return nil, err
}
return zip.Build(builder, targz), nil
}
func (d *Zip) BuildDir(ctx context.Context, builder *dagger.Container, opts *pipeline.ArtifactContainerOpts) (*dagger.Directory, error) {
panic("not implemented") // TODO: Implement
}
func (d *Zip) Publisher(ctx context.Context, opts *pipeline.ArtifactContainerOpts) (*dagger.Container, error) {
return nil, nil
}
func (d *Zip) PublishFile(ctx context.Context, opts *pipeline.ArtifactPublishFileOpts) error {
return nil
}
func (d *Zip) PublishDir(ctx context.Context, opts *pipeline.ArtifactPublishDirOpts) error {
panic("not implemented") // TODO: Implement
}
func (d *Zip) VerifyFile(ctx context.Context, client *dagger.Client, file *dagger.File) error {
return nil
}
func (d *Zip) VerifyDirectory(ctx context.Context, client *dagger.Client, dir *dagger.Directory) error {
panic("not implemented") // TODO: Implement
}
// Filename should return a deterministic file or folder name that this build will produce.
// This filename is used as a map key for caching, so implementers need to ensure that arguments or flags that affect the output
// also affect the filename to ensure that there are no collisions.
// For example, the backend for `linux/amd64` and `linux/arm64` should not both produce a `bin` folder, they should produce a
// `bin/linux-amd64` folder and a `bin/linux-arm64` folder. Callers can mount this as `bin` or whatever if they want.
func (d *Zip) Filename(ctx context.Context) (string, error) {
return packages.FileName(d.Name, d.Version, d.BuildID, d.Distribution, "zip")
}
func NewZipFromString(ctx context.Context, log *slog.Logger, artifact string, state pipeline.StateHandler) (*pipeline.Artifact, error) {
tarball, err := NewTarballFromString(ctx, log, artifact, state)
if err != nil {
return nil, err
}
options, err := pipeline.ParseFlags(artifact, ZipFlags)
if err != nil {
return nil, err
}
p, err := GetPackageDetails(ctx, options, state)
if err != nil {
return nil, err
}
return pipeline.ArtifactWithLogging(ctx, log, &pipeline.Artifact{
ArtifactString: artifact,
Handler: &Zip{
Name: p.Name,
Version: p.Version,
BuildID: p.BuildID,
Distribution: p.Distribution,
Enterprise: p.Enterprise,
Tarball: tarball,
},
Type: pipeline.ArtifactTypeFile,
Flags: TargzFlags,
})
}
@@ -0,0 +1,52 @@
package artifacts
import (
"context"
"github.com/grafana/grafana/pkg/build/daggerbuild/arguments"
"github.com/grafana/grafana/pkg/build/daggerbuild/backend"
"github.com/grafana/grafana/pkg/build/daggerbuild/flags"
"github.com/grafana/grafana/pkg/build/daggerbuild/packages"
"github.com/grafana/grafana/pkg/build/daggerbuild/pipeline"
)
type PackageDetails struct {
Name packages.Name
Enterprise bool
Version string
BuildID string
Distribution backend.Distribution
}
func GetPackageDetails(ctx context.Context, options *pipeline.OptionsHandler, state pipeline.StateHandler) (PackageDetails, error) {
distro, err := options.String(flags.Distribution)
if err != nil {
return PackageDetails{}, err
}
version, err := state.String(ctx, arguments.Version)
if err != nil {
return PackageDetails{}, err
}
buildID, err := state.String(ctx, arguments.BuildID)
if err != nil {
return PackageDetails{}, err
}
name, err := options.String(flags.PackageName)
if err != nil {
return PackageDetails{}, err
}
enterprise, err := options.Bool(flags.Enterprise)
if err != nil {
return PackageDetails{}, err
}
return PackageDetails{
Name: packages.Name(name),
Version: version,
BuildID: buildID,
Distribution: backend.Distribution(distro),
Enterprise: enterprise,
}, nil
}
@@ -0,0 +1,76 @@
package artifacts
import (
"context"
"errors"
"fmt"
"log/slog"
"strings"
"github.com/grafana/grafana/pkg/build/daggerbuild/pipeline"
)
var (
ErrorArtifactCollision = errors.New("artifact argument specifies two different artifacts")
ErrorDuplicateArgument = errors.New("artifact argument specifies duplicate or incompatible arguments")
ErrorNoArtifact = errors.New("could not find compatible artifact for argument string")
ErrorFlagNotFound = errors.New("no option available for the given flag")
)
func findInitializer(val string, initializers map[string]Initializer) (Initializer, error) {
c := strings.Split(val, ":")
var initializer *Initializer
// Find the artifact that is requested by `val`.
// The artifact can be defined anywhere in the artifact string. Example: `linux/amd64:grafana:targz` or `linux/amd64:grafana:targz` are the same, where targz is the artifact.
for _, v := range c {
n, ok := initializers[v]
if !ok {
continue
}
if initializer != nil {
return Initializer{}, fmt.Errorf("%s: %w", val, ErrorArtifactCollision)
}
initializer = &n
}
if initializer == nil {
return Initializer{}, fmt.Errorf("%s: %w", val, ErrorNoArtifact)
}
return *initializer, nil
}
// The ArtifactsFromStrings function should provide all of the necessary arguments to produce each artifact
// dleimited by colons. It's a repeated flag, so all permutations are stored in 1 instance of the ArtifactsFlag struct.
// Examples:
// * targz:linux/amd64 -- Will produce a "Grafana" tar.gz for "linux/amd64".
// * targz:enterprise:linux/amd64 -- Will produce a "Grafana" tar.gz for "linux/amd64".
func ArtifactsFromStrings(ctx context.Context, log *slog.Logger, a []string, registered map[string]Initializer, state pipeline.StateHandler) ([]*pipeline.Artifact, error) {
artifacts := make([]*pipeline.Artifact, len(a))
for i, v := range a {
n, err := Parse(ctx, log, v, registered, state)
if err != nil {
return nil, err
}
artifacts[i] = n
}
return artifacts, nil
}
// Parse parses the artifact string `artifact` and finds the matching initializer.
func Parse(ctx context.Context, log *slog.Logger, artifact string, initializers map[string]Initializer, state pipeline.StateHandler) (*pipeline.Artifact, error) {
artifact = strings.TrimSpace(artifact)
initializer, err := findInitializer(artifact, initializers)
if err != nil {
return nil, err
}
initializerFunc := initializer.InitializerFunc
// TODO soon, the initializer might need more info about flags
return initializerFunc(ctx, log, artifact, state)
}
@@ -0,0 +1,38 @@
package artifacts_test
// var TestArtifact struct {
// }
//
// func TestParse(t *testing.T) {
// v := "artifact:flag1:flag2"
//
// exampleArtifact := &pipeline.Artifact{
// Name: "example",
// }
//
// argument1 := &pipeline.Argument{
// Name: "argument1",
// }
//
// argument2 := &pipeline.Argument{
// Name: "argument2",
// }
//
// res, err := artifacts.Parse(v, map[string]artifacts.ArgumentOption{
// "artifact": {Artifact: exampleArtifact},
// "argument1": {Arguments: []*pipeline.Argument{argument1}},
// "argument2": {Arguments: []*pipeline.Argument{argument2}},
// })
//
// if err != nil {
// t.Fatal(err)
// }
//
// if res.Artifact.Name != exampleArtifact.Name {
// t.Fatal("Parse should return the example artifact")
// }
//
// if len(res.Arguments) != 2 {
// t.Fatal("Parse should return 2 Arguments")
// }
// }
@@ -0,0 +1,91 @@
package artifacts
import (
"context"
"log/slog"
"path"
"dagger.io/dagger"
"github.com/grafana/grafana/pkg/build/daggerbuild/arguments"
"github.com/grafana/grafana/pkg/build/daggerbuild/flags"
"github.com/grafana/grafana/pkg/build/daggerbuild/frontend"
"github.com/grafana/grafana/pkg/build/daggerbuild/packages"
"github.com/grafana/grafana/pkg/build/daggerbuild/pipeline"
)
var (
BundledPluginsFlags = flags.PackageNameFlags
BundledPluginsArguments = []pipeline.Argument{
arguments.YarnCacheDirectory,
}
)
type BundledPlugins struct {
Name packages.Name
Src *dagger.Directory
YarnCache *dagger.CacheVolume
Version string
}
// The frontend does not have any artifact dependencies.
func (f *BundledPlugins) Dependencies(ctx context.Context) ([]*pipeline.Artifact, error) {
return nil, nil
}
// Builder will return a node.js alpine container that matches the .nvmrc in the Grafana source repository
func (f *BundledPlugins) Builder(ctx context.Context, opts *pipeline.ArtifactContainerOpts) (*dagger.Container, error) {
return FrontendBuilder(ctx, f.Src, f.YarnCache, opts)
}
func (f *BundledPlugins) BuildFile(ctx context.Context, builder *dagger.Container, opts *pipeline.ArtifactContainerOpts) (*dagger.File, error) {
panic("not implemented") // BundledPlugins doesn't return a file
}
func (f *BundledPlugins) BuildDir(ctx context.Context, builder *dagger.Container, opts *pipeline.ArtifactContainerOpts) (*dagger.Directory, error) {
return frontend.BuildPlugins(builder), nil
}
func (f *BundledPlugins) Publisher(ctx context.Context, opts *pipeline.ArtifactContainerOpts) (*dagger.Container, error) {
return nil, nil
}
func (f *BundledPlugins) PublishFile(ctx context.Context, opts *pipeline.ArtifactPublishFileOpts) error {
panic("not implemented") // TODO: Implement
}
func (f *BundledPlugins) PublishDir(ctx context.Context, opts *pipeline.ArtifactPublishDirOpts) error {
return nil
}
func (f *BundledPlugins) VerifyFile(ctx context.Context, client *dagger.Client, file *dagger.File) error {
return nil
}
func (f *BundledPlugins) VerifyDirectory(ctx context.Context, client *dagger.Client, dir *dagger.Directory) error {
panic("not implemented") // TODO: Implement
}
// Filename should return a deterministic file or folder name that this build will produce.
// This filename is used as a map key for caching, so implementers need to ensure that arguments or flags that affect the output
// also affect the filename to ensure that there are no collisions.
// For example, the backend for `linux/amd64` and `linux/arm64` should not both produce a `bin` folder, they should produce a
// `bin/linux-amd64` folder and a `bin/linux-arm64` folder. Callers can mount this as `bin` or whatever if they want.
func (f *BundledPlugins) Filename(ctx context.Context) (string, error) {
// Important note: this path is only used in two ways:
// 1. When requesting an artifact be built and exported, this is the path where it will be exported to
// 2. In a map to distinguish when the same artifact is being built more than once
return path.Join("bin", "bundled-plugins"), nil
}
func NewBundledPlugins(ctx context.Context, log *slog.Logger, artifact string, src *dagger.Directory, version string, cacheVolume *dagger.CacheVolume) (*pipeline.Artifact, error) {
return pipeline.ArtifactWithLogging(ctx, log, &pipeline.Artifact{
ArtifactString: artifact,
Type: pipeline.ArtifactTypeDirectory,
Flags: BundledPluginsFlags,
Handler: &BundledPlugins{
Src: src,
YarnCache: cacheVolume,
Version: version,
},
})
}
@@ -0,0 +1,13 @@
package artifacts
import "github.com/grafana/grafana/pkg/build/daggerbuild/pipeline"
type Initializer struct {
InitializerFunc pipeline.ArtifactInitializer
Arguments []pipeline.Argument
}
type Registerer interface {
Register(string, Initializer) error
Initializers() map[string]Initializer
}
@@ -0,0 +1 @@
package artifacts
@@ -0,0 +1,113 @@
package artifacts
import (
"context"
"log/slog"
"path/filepath"
"dagger.io/dagger"
"github.com/grafana/grafana/pkg/build/daggerbuild/arguments"
"github.com/grafana/grafana/pkg/build/daggerbuild/flags"
"github.com/grafana/grafana/pkg/build/daggerbuild/frontend"
"github.com/grafana/grafana/pkg/build/daggerbuild/pipeline"
)
var (
StorybookFlags = flags.PackageNameFlags
StorybookArguments = []pipeline.Argument{
arguments.YarnCacheDirectory,
}
)
var StorybookInitializer = Initializer{
InitializerFunc: NewStorybookFromString,
Arguments: StorybookArguments,
}
type Storybook struct {
Src *dagger.Directory
YarnCache *dagger.CacheVolume
Version string
}
// The frontend does not have any artifact dependencies.
func (f *Storybook) Dependencies(ctx context.Context) ([]*pipeline.Artifact, error) {
return nil, nil
}
// Builder will return a node.js alpine container that matches the .nvmrc in the Grafana source repository
func (f *Storybook) Builder(ctx context.Context, opts *pipeline.ArtifactContainerOpts) (*dagger.Container, error) {
return FrontendBuilder(ctx, f.Src, f.YarnCache, opts)
}
func (f *Storybook) BuildFile(ctx context.Context, builder *dagger.Container, opts *pipeline.ArtifactContainerOpts) (*dagger.File, error) {
// Not a file
return nil, nil
}
func (f *Storybook) BuildDir(ctx context.Context, builder *dagger.Container, opts *pipeline.ArtifactContainerOpts) (*dagger.Directory, error) {
return frontend.Storybook(builder, f.Src, f.Version), nil
}
func (f *Storybook) Publisher(ctx context.Context, opts *pipeline.ArtifactContainerOpts) (*dagger.Container, error) {
panic("not implemented") // TODO: Implement
}
func (f *Storybook) PublishFile(ctx context.Context, opts *pipeline.ArtifactPublishFileOpts) error {
panic("not implemented") // TODO: Implement
}
func (f *Storybook) PublishDir(ctx context.Context, opts *pipeline.ArtifactPublishDirOpts) error {
panic("not implemented") // TODO: Implement
}
func (f *Storybook) VerifyFile(ctx context.Context, client *dagger.Client, file *dagger.File) error {
// Not a file
return nil
}
func (f *Storybook) VerifyDirectory(ctx context.Context, client *dagger.Client, dir *dagger.Directory) error {
return nil
}
// Filename should return a deterministic file or folder name that this build will produce.
// This filename is used as a map key for caching, so implementers need to ensure that arguments or flags that affect the output
// also affect the filename to ensure that there are no collisions.
// For example, the backend for `linux/amd64` and `linux/arm64` should not both produce a `bin` folder, they should produce a
// `bin/linux-amd64` folder and a `bin/linux-arm64` folder. Callers can mount this as `bin` or whatever if they want.
func (f *Storybook) Filename(ctx context.Context) (string, error) {
// Important note: this path is only used in two ways:
// 1. When requesting an artifact be built and exported, this is the path where it will be exported to
// 2. In a map to distinguish when the same artifact is being built more than once
return filepath.Join(f.Version, "storybook"), nil
}
func NewStorybookFromString(ctx context.Context, log *slog.Logger, artifact string, state pipeline.StateHandler) (*pipeline.Artifact, error) {
grafanaDir, err := GrafanaDir(ctx, state, false)
if err != nil {
return nil, err
}
cacheDir, err := state.CacheVolume(ctx, arguments.YarnCacheDirectory)
if err != nil {
return nil, err
}
version, err := state.String(ctx, arguments.Version)
if err != nil {
return nil, err
}
return NewStorybook(ctx, log, artifact, grafanaDir, version, cacheDir)
}
func NewStorybook(ctx context.Context, log *slog.Logger, artifact string, src *dagger.Directory, version string, cache *dagger.CacheVolume) (*pipeline.Artifact, error) {
return pipeline.ArtifactWithLogging(ctx, log, &pipeline.Artifact{
ArtifactString: artifact,
Type: pipeline.ArtifactTypeDirectory,
Flags: StorybookFlags,
Handler: &Storybook{
Src: src,
YarnCache: cache,
Version: version,
},
})
}
@@ -0,0 +1,31 @@
package artifacts
import (
"io"
"os"
"sync"
)
// SyncWriter wraps a writer and makes its writes synchronous, preventing multiple threads writing to the same writer
// from creating wacky looking output.
type SyncWriter struct {
Writer io.Writer
mutex *sync.Mutex
}
func NewSyncWriter(w io.Writer) *SyncWriter {
return &SyncWriter{
Writer: w,
mutex: &sync.Mutex{},
}
}
func (w *SyncWriter) Write(b []byte) (int, error) {
w.mutex.Lock()
defer w.mutex.Unlock()
return w.Writer.Write(b)
}
var Stdout = NewSyncWriter(os.Stdout)
@@ -0,0 +1,94 @@
package artifacts
import (
"context"
"log/slog"
"dagger.io/dagger"
"github.com/grafana/grafana/pkg/build/daggerbuild/arguments"
"github.com/grafana/grafana/pkg/build/daggerbuild/pipeline"
)
var (
VersionArguments = []pipeline.Argument{
arguments.GrafanaDirectory,
arguments.Version,
}
VersionFlags = TargzFlags
)
var VersionInitializer = Initializer{
InitializerFunc: NewVersionFromString,
Arguments: VersionArguments,
}
type Version struct {
// Version is embedded in the binary at build-time
Version string
}
func (b *Version) Builder(ctx context.Context, opts *pipeline.ArtifactContainerOpts) (*dagger.Container, error) {
return opts.Client.Container().WithNewFile("/VERSION", b.Version), nil
}
func (b *Version) Dependencies(ctx context.Context) ([]*pipeline.Artifact, error) {
return []*pipeline.Artifact{}, nil
}
func (b *Version) BuildFile(ctx context.Context, builder *dagger.Container, opts *pipeline.ArtifactContainerOpts) (*dagger.File, error) {
return builder.File("/VERSION"), nil
}
func (b *Version) BuildDir(ctx context.Context, builder *dagger.Container, opts *pipeline.ArtifactContainerOpts) (*dagger.Directory, error) {
return nil, nil
}
func (b *Version) Publisher(ctx context.Context, opts *pipeline.ArtifactContainerOpts) (*dagger.Container, error) {
return nil, nil
}
func (b *Version) PublishFile(ctx context.Context, opts *pipeline.ArtifactPublishFileOpts) error {
return nil
}
func (b *Version) PublishDir(ctx context.Context, opts *pipeline.ArtifactPublishDirOpts) error {
panic("not implemented") // TODO: Implement
}
// Filename should return a deterministic file or folder name that this build will produce.
// This filename is used as a map key for caching, so implementers need to ensure that arguments or flags that affect the output
// also affect the filename to ensure that there are no collisions.
// For example, the backend for `linux/amd64` and `linux/arm64` should not both produce a `bin` folder, they should produce a
// `bin/linux-amd64` folder and a `bin/linux-arm64` folder. Callers can mount this as `bin` or whatever if they want.
func (b *Version) Filename(ctx context.Context) (string, error) {
return "VERSION", nil
}
func (b *Version) VerifyFile(ctx context.Context, client *dagger.Client, file *dagger.File) error {
return nil
}
func (b *Version) VerifyDirectory(ctx context.Context, client *dagger.Client, dir *dagger.Directory) error {
return nil
}
func NewVersionFromString(ctx context.Context, log *slog.Logger, artifact string, state pipeline.StateHandler) (*pipeline.Artifact, error) {
version, err := state.String(ctx, arguments.Version)
if err != nil {
return nil, err
}
return NewVersion(ctx, log, artifact, version)
}
func NewVersion(ctx context.Context, log *slog.Logger, artifact, version string) (*pipeline.Artifact, error) {
return pipeline.ArtifactWithLogging(ctx, log, &pipeline.Artifact{
ArtifactString: artifact,
Type: pipeline.ArtifactTypeFile,
Flags: VersionFlags,
Handler: &Version{
Version: version,
},
})
}