Files
pack/buildkit/frontend.go
T
2025-03-03 16:33:29 -05:00

199 lines
4.8 KiB
Go

package buildkit
import (
"context"
"encoding/json"
"fmt"
"os"
"strings"
"github.com/charmbracelet/log"
"github.com/moby/buildkit/client/llb"
"github.com/moby/buildkit/exporter/containerimage/exptypes"
"github.com/moby/buildkit/frontend/gateway/client"
gw "github.com/moby/buildkit/frontend/gateway/grpcclient"
"github.com/moby/buildkit/util/appcontext"
"github.com/pkg/errors"
"github.com/railwayapp/railpack/core/plan"
)
const (
// The default local mount of where to look for the config file
// This is "dockerfile" because that is commonly used for the config file mount
configMountName = "dockerfile"
// The default filename for the serialized Railpack plan
defaultRailpackPlan = "railpack-plan.json"
secretsHash = "secrets-hash"
cacheKey = "cache-key"
)
func StartFrontend() {
log.Info("Starting frontend")
ctx := appcontext.Context()
if err := gw.RunFromEnvironment(ctx, Build); err != nil {
log.Error("error: %+v\n", err)
os.Exit(1)
}
}
func Build(ctx context.Context, c client.Client) (*client.Result, error) {
opts := c.BuildOpts().Opts
buildArgs := parseBuildArgs(opts)
cacheKey := buildArgs[cacheKey]
secretsHash := buildArgs[secretsHash]
// TODO: Support building for multiple platforms
buildPlatform, err := validatePlatform(opts)
if err != nil {
return nil, err
}
plan, err := readRailpackPlan(ctx, c)
if err != nil {
return nil, err
}
_, err = json.MarshalIndent(plan, "", " ")
if err != nil {
return nil, fmt.Errorf("error marshalling plan: %w", err)
}
llbState, image, err := ConvertPlanToLLB(plan, ConvertPlanOptions{
BuildPlatform: buildPlatform,
SecretsHash: secretsHash,
CacheKey: cacheKey,
SessionID: c.BuildOpts().SessionID,
})
if err != nil {
return nil, fmt.Errorf("error converting plan to LLB: %w", err)
}
def, err := llbState.Marshal(ctx)
if err != nil {
return nil, fmt.Errorf("error marshalling LLB state: %w", err)
}
imageBytes, err := json.Marshal(image)
if err != nil {
return nil, fmt.Errorf("error marshalling image: %w", err)
}
res, err := c.Solve(ctx, client.SolveRequest{
Definition: def.ToPB(),
})
if err != nil {
return nil, err
}
res.AddMeta(exptypes.ExporterImageConfigKey, imageBytes)
return res, nil
}
func readRailpackPlan(ctx context.Context, c client.Client) (*plan.BuildPlan, error) {
opts := c.BuildOpts().Opts
filename := opts["filename"]
if filename == "" {
filename = defaultRailpackPlan
}
fileContents, err := readFile(ctx, c, filename)
if err != nil {
return nil, errors.Wrap(err, "failed to read railpack plan")
}
plan := plan.NewBuildPlan()
err = json.Unmarshal([]byte(fileContents), plan)
if err != nil {
return nil, errors.Wrap(err, "failed to parse railpack plan")
}
return plan, nil
}
// validatePlatform checks if the platform is supported and returns the corresponding BuildPlatform
func validatePlatform(opts map[string]string) (BuildPlatform, error) {
platformStr := opts["platform"]
if platformStr == "" {
// Default to host platform if none specified
return DetermineBuildPlatformFromHost(), nil
}
// Error if multiple platforms are specified
if strings.Contains(platformStr, ",") {
return BuildPlatform{}, fmt.Errorf("multiple platforms are not supported, got: %s", platformStr)
}
// Match against supported platforms
switch platformStr {
case PlatformLinuxAMD64.String():
return PlatformLinuxAMD64, nil
case PlatformLinuxARM64.String():
return PlatformLinuxARM64, nil
default:
return BuildPlatform{}, fmt.Errorf("unsupported platform: %s. Must be one of: %s, %s",
platformStr,
PlatformLinuxAMD64.String(),
PlatformLinuxARM64.String())
}
}
// Read a file from the build context
func readFile(ctx context.Context, c client.Client, filename string) (string, error) {
// Create a Local source for the dockerfile
src := llb.Local(configMountName,
llb.FollowPaths([]string{filename}),
llb.SessionID(c.BuildOpts().SessionID),
llb.WithCustomName("load build definition from "+filename),
)
srcDef, err := src.Marshal(ctx)
if err != nil {
return "", errors.Wrap(err, "failed to marshal local source")
}
res, err := c.Solve(ctx, client.SolveRequest{
Definition: srcDef.ToPB(),
})
if err != nil {
return "", errors.Wrap(err, "failed to resolve dockerfile")
}
ref, err := res.SingleRef()
if err != nil {
return "", err
}
content, err := ref.ReadFile(ctx, client.ReadRequest{
Filename: filename,
})
if err != nil {
return "", errors.Wrap(err, "failed to read file")
}
fileContents := string(content)
return fileContents, nil
}
func parseBuildArgs(opts map[string]string) map[string]string {
buildArgs := make(map[string]string)
for key, arg := range opts {
if !strings.HasPrefix(key, "build-arg:") {
continue
}
// Remove the "build-arg:" prefix
name := strings.TrimPrefix(key, "build-arg:")
buildArgs[name] = arg
}
return buildArgs
}