Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 32 additions & 13 deletions FLIGHTCONTROL.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
# Flightcontrol fork notes

This is a fork of [railwayapp/railpack](https://github.com/railwayapp/railpack),
maintained solely to carry one patch: `buildkit/frontend.go` never populated
`CacheImports` on its own internal `client.SolveRequest` calls, so
maintained to carry the Flightcontrol-specific frontend behavior described
below.

## Registry cache imports

`buildkit/frontend.go` never populated `CacheImports` on its own internal
`client.SolveRequest` calls, so
`docker buildx build --cache-from` / `--cache-to` never reached the vertices
`railpack-frontend` defines. Export worked (that's driven by the outer solve,
not the frontend), but import never produced a single cache hit, even across
Expand All @@ -21,26 +26,40 @@ fix. It merges two sources, in order of reliability:
`gateway.v0` frontends in some invocation paths - kept for correctness with
upstream behavior, not required for our own use.

## Artifact-only exports

The frontend accepts an `export-path` build argument containing an absolute
path in the final deploy filesystem. When set, the frontend returns a scratch
state containing only that directory. Flightcontrol static builds combine this
with BuildKit's local exporter to avoid serializing and loading the full
runtime image solely to copy static assets back out.

```bash
docker buildx build \
--build-arg export-path=/app/dist \
--output type=local,dest=extracted-static \
-f railpack-plan.json .
```

## Structure

- `main` mirrors `railwayapp/railpack:main` untouched, for easy diffing/merging
against upstream.
- `flightcontrol/v<version>-cache-imports` branches carry the patch rebased
onto each upstream release tag we actually run in production. The image we
- `flightcontrol/v<version>` branches carry Flightcontrol's patches rebased
onto each upstream release tag we actually run in production (older branches
may retain a patch-specific suffix). The image we
publish is always built from one of these branches, never from `main`.

## Rebuilding after an upstream version bump

```bash
git fetch origin
git checkout -b flightcontrol/v<new-version>-cache-imports v<new-version>
git cherry-pick flightcontrol/v0.29.0-cache-imports # or re-apply buildkit/frontend.go's patch by hand if it's drifted
docker build --platform linux/amd64 -f images/alpine/frontend/Dockerfile -t <registry>/railpack-frontend:v<new-version>-fc1 .
docker push <registry>/railpack-frontend:v<new-version>-fc1
git checkout -b flightcontrol/v<new-version> v<new-version>
git cherry-pick <flightcontrol-patch-commits>
docker build --platform linux/amd64 -f images/alpine/frontend/Dockerfile -t <registry>/railpack-frontend:v<new-version>-fc<N> .
docker push <registry>/railpack-frontend:v<new-version>-fc<N>
```

Then update `railpackFrontendImage` in
`packages/tower-go/pkg/steps/ci/build_helpers/railpack_instructions.go` (in
the `flightcontrolhq/add-railpacks` monorepo) to point at the new tag, and add
the corresponding `--build-arg cache-from=...` wiring if it isn't already
there.
Then update the immutable frontend tag and its matching feature gate in
`packages/tower-go/pkg/steps/ci/build_helpers/railpack_instructions.go` in the
Flightcontrol monorepo.
31 changes: 31 additions & 0 deletions buildkit/frontend.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"encoding/json"
"fmt"
"os"
"path"
"strings"

"github.com/charmbracelet/log"
Expand All @@ -32,6 +33,7 @@ const (
secretsHash = "secrets-hash"
cacheKey = "cache-key"
githubToken = "github-token"
exportPath = "export-path"

// Cache import opt keys, matching github.com/moby/buildkit/frontend/dockerui's
// keyCacheFrom/keyCacheImports so `docker buildx build --cache-from` reaches
Expand All @@ -58,6 +60,7 @@ func Build(ctx context.Context, c client.Client) (*client.Result, error) {
cacheKey := buildArgs[cacheKey]
secretsHash := buildArgs[secretsHash]
githubToken := buildArgs[githubToken]
requestedExportPath := buildArgs[exportPath]
cacheImports := parseCacheImports(opts, buildArgs)

// TODO: Support building for multiple platforms
Expand Down Expand Up @@ -86,6 +89,12 @@ func Build(ctx context.Context, c client.Client) (*client.Result, error) {
if err != nil {
return nil, fmt.Errorf("error converting plan to LLB: %w", err)
}
if requestedExportPath != "" {
llbState, err = makeExportState(llbState, requestedExportPath)
if err != nil {
return nil, err
}
}

def, err := llbState.Marshal(ctx)
if err != nil {
Expand All @@ -110,6 +119,28 @@ func Build(ctx context.Context, c client.Client) (*client.Result, error) {
return res, nil
}

// makeExportState replaces the full deploy filesystem with a scratch state
// containing only exportPath. This lets callers use BuildKit's local exporter
// without materializing the runtime image and all of its parent layers.
func makeExportState(source *llb.State, exportPath string) (*llb.State, error) {
cleanExportPath := path.Clean(exportPath)
if !path.IsAbs(cleanExportPath) || cleanExportPath == "/" {
return nil, fmt.Errorf("export-path must be an absolute path below root, got %q", exportPath)
}

destination := "/" + path.Base(cleanExportPath)
state := llb.Scratch().File(
llb.Copy(*source, cleanExportPath, destination, &llb.CopyInfo{
CopyDirContentsOnly: true,
CreateDestPath: true,
FollowSymlinks: true,
}),
llb.WithCustomNamef("[railpack] export %s", cleanExportPath),
)

return &state, nil
}

// parseCacheImports builds the CacheImports list for this frontend's own
// internal Solve call. Upstream railpack never set this field at all, so
// `docker buildx build --cache-from=...` never reached the vertices this
Expand Down
38 changes: 38 additions & 0 deletions buildkit/frontend_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package buildkit

import (
"context"
"testing"

"github.com/moby/buildkit/client/llb"
)

func TestParseBuildArgs(t *testing.T) {
Expand Down Expand Up @@ -29,3 +32,38 @@ func TestParseBuildArgs(t *testing.T) {
}
}
}

func TestMakeExportState(t *testing.T) {
source := llb.Scratch().File(llb.Mkdir("/app/dist", 0755, llb.WithParents(true)))

exportState, err := makeExportState(&source, "/app/dist")
if err != nil {
t.Fatalf("makeExportState returned an error: %v", err)
}

definition, err := exportState.Marshal(context.Background())
if err != nil {
t.Fatalf("marshal export state: %v", err)
}

foundExportOperation := false
for _, metadata := range definition.ToPB().Metadata {
if metadata.Description["llb.customname"] == "[railpack] export /app/dist" {
foundExportOperation = true
break
}
}
if !foundExportOperation {
t.Fatal("export state does not contain the expected copy operation")
}
}

func TestMakeExportStateRejectsInvalidPath(t *testing.T) {
source := llb.Scratch()

for _, exportPath := range []string{"dist", "/"} {
if _, err := makeExportState(&source, exportPath); err == nil {
t.Errorf("makeExportState(%q) should return an error", exportPath)
}
}
}
Loading