Skip to content

Commit 2c142c1

Browse files
authored
start with pooled builders (#4459)
* start with managed-builders * Add DisableManagedBuilders to config.yml * fix and add tests * --pooled-builder flag * rename to --builder-pool
1 parent c163990 commit 2c142c1

File tree

12 files changed

+257
-38
lines changed

12 files changed

+257
-38
lines changed

internal/build/imgsrc/docker.go

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -55,13 +55,18 @@ type dockerClientFactory struct {
5555
}
5656

5757
func newDockerClientFactory(daemonType DockerDaemonType, apiClient flyutil.Client, appName string, streams *iostreams.IOStreams, connectOverWireguard, recreateBuilder bool) *dockerClientFactory {
58+
useManagedBuilder := daemonType.UseManagedBuilder()
5859
remoteFactory := func() *dockerClientFactory {
5960
terminal.Debug("trying remote docker daemon")
6061
return &dockerClientFactory{
6162
mode: daemonType,
6263
remote: true,
6364
buildFn: func(ctx context.Context, build *build) (*dockerclient.Client, error) {
64-
return newRemoteDockerClient(ctx, apiClient, appName, streams, build, cachedDocker, connectOverWireguard, recreateBuilder)
65+
cfg := config.FromContext(ctx)
66+
if cfg.DisableManagedBuilders {
67+
useManagedBuilder = false
68+
}
69+
return newRemoteDockerClient(ctx, apiClient, appName, streams, build, cachedDocker, connectOverWireguard, useManagedBuilder, recreateBuilder)
6570
},
6671
apiClient: apiClient,
6772
appName: appName,
@@ -108,15 +113,15 @@ func newDockerClientFactory(daemonType DockerDaemonType, apiClient flyutil.Clien
108113
}
109114
}
110115

111-
func NewDockerDaemonType(allowLocal, allowRemote, prefersLocal, useDepot, useNixpacks bool) DockerDaemonType {
116+
func NewDockerDaemonType(allowLocal, allowRemote, prefersLocal, useDepot, useNixpacks bool, useManagedBuilder bool) DockerDaemonType {
112117
daemonType := DockerDaemonTypeNone
113118
if allowLocal {
114119
daemonType = daemonType | DockerDaemonTypeLocal
115120
}
116-
if allowRemote {
121+
if allowRemote || useManagedBuilder {
117122
daemonType = daemonType | DockerDaemonTypeRemote
118123
}
119-
if useDepot {
124+
if useDepot && !useManagedBuilder {
120125
daemonType = daemonType | DockerDaemonTypeDepot
121126
}
122127
if useNixpacks {
@@ -125,6 +130,9 @@ func NewDockerDaemonType(allowLocal, allowRemote, prefersLocal, useDepot, useNix
125130
if prefersLocal && !useDepot {
126131
daemonType = daemonType | DockerDaemonTypePrefersLocal
127132
}
133+
if useManagedBuilder {
134+
daemonType = daemonType | DockerDaemonTypeManaged
135+
}
128136
return daemonType
129137
}
130138

@@ -137,6 +145,7 @@ const (
137145
DockerDaemonTypePrefersLocal
138146
DockerDaemonTypeNixpacks
139147
DockerDaemonTypeDepot
148+
DockerDaemonTypeManaged
140149
)
141150

142151
func (t DockerDaemonType) String() string {
@@ -157,6 +166,9 @@ func (t DockerDaemonType) String() string {
157166
if t&DockerDaemonTypeDepot != 0 {
158167
strs = append(strs, "depot")
159168
}
169+
if t&DockerDaemonTypeManaged != 0 {
170+
strs = append(strs, "managed")
171+
}
160172
if len(strs) == 0 {
161173
return "none"
162174
}
@@ -192,6 +204,10 @@ func (t DockerDaemonType) UseDepot() bool {
192204
return (t & DockerDaemonTypeDepot) != 0
193205
}
194206

207+
func (t DockerDaemonType) UseManagedBuilder() bool {
208+
return (t & DockerDaemonTypeManaged) != 0
209+
}
210+
195211
func (t DockerDaemonType) PrefersLocal() bool {
196212
return (t & DockerDaemonTypePrefersLocal) != 0
197213
}
@@ -220,7 +236,7 @@ func logClearLinesAbove(streams *iostreams.IOStreams, count int) {
220236
}
221237
}
222238

223-
func newRemoteDockerClient(ctx context.Context, apiClient flyutil.Client, appName string, streams *iostreams.IOStreams, build *build, cachedClient *dockerclient.Client, connectOverWireguard, recreateBuilder bool) (c *dockerclient.Client, err error) {
239+
func newRemoteDockerClient(ctx context.Context, apiClient flyutil.Client, appName string, streams *iostreams.IOStreams, build *build, cachedClient *dockerclient.Client, connectOverWireguard, useManagedBuilder bool, recreateBuilder bool) (c *dockerclient.Client, err error) {
224240
ctx, span := tracing.GetTracer().Start(ctx, "build_remote_docker_client", trace.WithAttributes(
225241
attribute.Bool("connect_over_wireguard", connectOverWireguard),
226242
))
@@ -241,12 +257,20 @@ func newRemoteDockerClient(ctx context.Context, apiClient flyutil.Client, appNam
241257
var host string
242258
var app *fly.App
243259
var machine *fly.Machine
244-
machine, app, err = remoteBuilderMachine(ctx, apiClient, appName, recreateBuilder)
260+
if useManagedBuilder {
261+
machine, app, err = remoteManagedBuilderMachine(ctx, apiClient, appName)
262+
} else {
263+
machine, app, err = remoteBuilderMachine(ctx, apiClient, appName, recreateBuilder)
264+
}
245265
if err != nil {
246266
tracing.RecordError(span, err, "failed to init remote builder machine")
247267
return nil, err
248268
}
249269

270+
if useManagedBuilder {
271+
connectOverWireguard = false
272+
}
273+
250274
if !connectOverWireguard && !wglessCompatible {
251275
client := &http.Client{
252276
Transport: &http.Transport{
@@ -744,6 +768,20 @@ func remoteBuilderMachine(ctx context.Context, apiClient flyutil.Client, appName
744768
return builderMachine, builderApp, err
745769
}
746770

771+
func remoteManagedBuilderMachine(ctx context.Context, apiClient flyutil.Client, appName string) (*fly.Machine, *fly.App, error) {
772+
if v := os.Getenv("FLY_REMOTE_BUILDER_HOST"); v != "" {
773+
return nil, nil, nil
774+
}
775+
776+
region := os.Getenv("FLY_REMOTE_BUILDER_REGION")
777+
org, err := apiClient.GetOrganizationByApp(ctx, appName)
778+
if err != nil {
779+
return nil, nil, err
780+
}
781+
builderMachine, builderApp, err := EnsureFlyManagedBuilder(ctx, org, region)
782+
return builderMachine, builderApp, err
783+
}
784+
747785
func (d *dockerClientFactory) IsRemote() bool {
748786
return d.remote
749787
}

internal/build/imgsrc/docker_test.go

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,26 +8,30 @@ import (
88

99
func TestAllowedDockerDaemonMode(t *testing.T) {
1010
tests := []struct {
11-
allowLocal bool
12-
allowRemote bool
13-
preferslocal bool
14-
useDepot bool
15-
useNixpacks bool
16-
expected DockerDaemonType
11+
allowLocal bool
12+
allowRemote bool
13+
preferslocal bool
14+
useDepot bool
15+
useNixpacks bool
16+
useManagedBuilder bool
17+
expected DockerDaemonType
1718
}{
18-
{false, false, false, false, false, DockerDaemonTypeNone},
19-
{false, false, true, false, false, DockerDaemonTypeNone | DockerDaemonTypePrefersLocal},
20-
{false, true, false, false, false, DockerDaemonTypeNone | DockerDaemonTypeRemote},
21-
{false, true, true, false, false, DockerDaemonTypeNone | DockerDaemonTypeRemote | DockerDaemonTypePrefersLocal},
22-
{true, false, false, false, false, DockerDaemonTypeNone | DockerDaemonTypeLocal},
23-
{true, false, true, false, false, DockerDaemonTypeNone | DockerDaemonTypeLocal | DockerDaemonTypePrefersLocal},
24-
{true, true, false, false, false, DockerDaemonTypeNone | DockerDaemonTypeLocal | DockerDaemonTypeRemote},
25-
{true, true, true, false, false, DockerDaemonTypeNone | DockerDaemonTypeLocal | DockerDaemonTypeRemote | DockerDaemonTypePrefersLocal},
26-
{true, true, false, true, false, DockerDaemonTypeNone | DockerDaemonTypeDepot | DockerDaemonTypeRemote | DockerDaemonTypeLocal},
19+
{false, false, false, false, false, false, DockerDaemonTypeNone},
20+
{false, false, true, false, false, false, DockerDaemonTypeNone | DockerDaemonTypePrefersLocal},
21+
{false, true, false, false, false, false, DockerDaemonTypeNone | DockerDaemonTypeRemote},
22+
{false, true, true, false, false, false, DockerDaemonTypeNone | DockerDaemonTypeRemote | DockerDaemonTypePrefersLocal},
23+
{true, false, false, false, false, false, DockerDaemonTypeNone | DockerDaemonTypeLocal},
24+
{true, false, true, false, false, false, DockerDaemonTypeNone | DockerDaemonTypeLocal | DockerDaemonTypePrefersLocal},
25+
{true, true, false, false, false, false, DockerDaemonTypeNone | DockerDaemonTypeLocal | DockerDaemonTypeRemote},
26+
{true, true, true, false, false, false, DockerDaemonTypeNone | DockerDaemonTypeLocal | DockerDaemonTypeRemote | DockerDaemonTypePrefersLocal},
27+
{true, true, false, true, false, false, DockerDaemonTypeNone | DockerDaemonTypeDepot | DockerDaemonTypeRemote | DockerDaemonTypeLocal},
28+
{true, true, false, false, false, true, DockerDaemonTypeNone | DockerDaemonTypeRemote | DockerDaemonTypeLocal | DockerDaemonTypeManaged},
29+
{true, true, false, true, false, true, DockerDaemonTypeNone | DockerDaemonTypeRemote | DockerDaemonTypeLocal | DockerDaemonTypeManaged},
30+
{false, false, false, false, false, true, DockerDaemonTypeNone | DockerDaemonTypeRemote | DockerDaemonTypeManaged},
2731
}
2832

2933
for _, test := range tests {
30-
m := NewDockerDaemonType(test.allowLocal, test.allowRemote, test.preferslocal, test.useDepot, test.useNixpacks)
34+
m := NewDockerDaemonType(test.allowLocal, test.allowRemote, test.preferslocal, test.useDepot, test.useNixpacks, test.useManagedBuilder)
3135
assert.Equal(t, test.expected, m)
3236
}
3337
}

internal/build/imgsrc/ensure_builder.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"github.com/superfly/flyctl/internal/flyutil"
1515
"github.com/superfly/flyctl/internal/haikunator"
1616
"github.com/superfly/flyctl/internal/tracing"
17+
"github.com/superfly/flyctl/internal/uiexutil"
1718
"go.opentelemetry.io/otel/attribute"
1819
"go.opentelemetry.io/otel/trace"
1920
)
@@ -121,6 +122,18 @@ func EnsureBuilder(ctx context.Context, org *fly.Organization, region string, re
121122
return machine, app, nil
122123
}
123124

125+
func EnsureFlyManagedBuilder(ctx context.Context, org *fly.Organization, region string) (*fly.Machine, *fly.App, error) {
126+
ctx, span := tracing.GetTracer().Start(ctx, "ensure_fly_managed_builder")
127+
defer span.End()
128+
129+
app, machine, err := createFlyManagedBuilder(ctx, org, region)
130+
if err != nil {
131+
tracing.RecordError(span, err, "error creating fly managed builder")
132+
return nil, nil, err
133+
}
134+
return machine, app, nil
135+
}
136+
124137
type ValidateBuilderError int
125138

126139
func (e ValidateBuilderError) Error() string {
@@ -413,6 +426,29 @@ func createBuilder(ctx context.Context, org *fly.Organization, region, builderNa
413426
return
414427
}
415428

429+
func createFlyManagedBuilder(ctx context.Context, org *fly.Organization, region string) (app *fly.App, mach *fly.Machine, retErr error) {
430+
ctx, span := tracing.GetTracer().Start(ctx, "create_builder")
431+
defer span.End()
432+
433+
uiexClient := uiexutil.ClientFromContext(ctx)
434+
435+
response, error := uiexClient.CreateFlyManagedBuilder(ctx, org.Slug, region)
436+
if error != nil {
437+
tracing.RecordError(span, retErr, "error creating managed builder")
438+
return nil, nil, retErr
439+
}
440+
441+
builderApp := &fly.App{
442+
Name: response.Data.AppName,
443+
}
444+
445+
machine := &fly.Machine{
446+
ID: response.Data.MachineID,
447+
}
448+
449+
return builderApp, machine, nil
450+
}
451+
416452
func restartBuilderMachine(ctx context.Context, builderMachine *fly.Machine) error {
417453
ctx, span := tracing.GetTracer().Start(ctx, "restart_builder_machine")
418454
defer span.End()

internal/build/imgsrc/resolver.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,7 @@ func (r *Resolver) BuildImage(ctx context.Context, streams *iostreams.IOStreams,
248248

249249
if r.dockerFactory.mode.UseNixpacks() {
250250
strategies = append(strategies, &nixpacksBuilder{})
251-
} else if r.dockerFactory.mode.UseDepot() && len(opts.Buildpacks) == 0 && opts.Builder == "" && opts.BuiltIn == "" {
251+
} else if (r.dockerFactory.mode.UseDepot() && !r.dockerFactory.mode.UseManagedBuilder()) && len(opts.Buildpacks) == 0 && opts.Builder == "" && opts.BuiltIn == "" {
252252
strategies = append(strategies, &DepotBuilder{Scope: builderScope})
253253
} else {
254254
strategies = []imageBuilder{

internal/command/command_run.go

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626
"github.com/superfly/flyctl/internal/flag"
2727
"github.com/superfly/flyctl/internal/flapsutil"
2828
"github.com/superfly/flyctl/internal/flyutil"
29+
"github.com/superfly/flyctl/internal/launchdarkly"
2930
"github.com/superfly/flyctl/internal/state"
3031
)
3132

@@ -36,7 +37,26 @@ func DetermineImage(ctx context.Context, appName string, imageOrPath string) (im
3637
cfg = appconfig.ConfigFromContext(ctx)
3738
)
3839

39-
daemonType := imgsrc.NewDockerDaemonType(!flag.GetBool(ctx, "build-remote-only"), !flag.GetBool(ctx, "build-local-only"), env.IsCI(), flag.GetBool(ctx, "build-depot"), flag.GetBool(ctx, "build-nixpacks"))
40+
appCompact, err := client.GetAppCompact(ctx, appName)
41+
if err != nil {
42+
return nil, err
43+
}
44+
45+
// Start the feature flag client, if we haven't already
46+
if launchdarkly.ClientFromContext(ctx) == nil {
47+
ffClient, err := launchdarkly.NewClient(ctx, launchdarkly.UserInfo{
48+
OrganizationID: appCompact.Organization.InternalNumericID,
49+
UserID: 0,
50+
})
51+
if err != nil {
52+
return nil, fmt.Errorf("could not create feature flag client: %w", err)
53+
}
54+
ctx = launchdarkly.NewContextWithClient(ctx, ffClient)
55+
}
56+
57+
ldClient := launchdarkly.ClientFromContext(ctx)
58+
useManagedBuilder := ldClient.ManagedBuilderEnabled()
59+
daemonType := imgsrc.NewDockerDaemonType(!flag.GetBool(ctx, "build-remote-only"), !flag.GetBool(ctx, "build-local-only"), env.IsCI(), flag.GetBool(ctx, "build-depot"), flag.GetBool(ctx, "build-nixpacks"), useManagedBuilder)
4060
resolver := imgsrc.NewResolver(daemonType, client, appName, io, flag.GetWireguard(ctx), false)
4161

4262
// build if relative or absolute path

internal/command/deploy/deploy.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,12 @@ var CommonFlags = flag.Set{
178178
Description: "Number of times to retry a deployment if it fails",
179179
Default: "auto",
180180
},
181+
flag.String{
182+
Name: "builder-pool",
183+
Default: "auto",
184+
NoOptDefVal: "true",
185+
Description: "Experimental: Use pooled builder from Fly.io",
186+
},
181187
}
182188

183189
type Command struct {
@@ -198,6 +204,7 @@ func New() *Command {
198204
command.RequireSession,
199205
command.ChangeWorkingDirectoryToFirstArgIfPresent,
200206
command.RequireAppName,
207+
command.RequireUiex,
201208
)
202209
cmd.Args = cobra.MaximumNArgs(1)
203210

internal/command/deploy/deploy_build.go

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ func determineImage(ctx context.Context, appConfig *appconfig.Config, useWG, rec
5757

5858
ldClient := launchdarkly.ClientFromContext(ctx)
5959
depotBool := ldClient.GetFeatureFlagValue("use-depot-for-builds", true).(bool)
60+
useManagedBuilder := ldClient.ManagedBuilderEnabled()
6061

6162
switch flag.GetString(ctx, "depot") {
6263
case "", "true":
@@ -65,11 +66,23 @@ func determineImage(ctx context.Context, appConfig *appconfig.Config, useWG, rec
6566
depotBool = false
6667
case "auto":
6768
default:
68-
return nil, fmt.Errorf("invalid falue for the 'depot' flag. must be 'true', 'false', or ''")
69+
return nil, fmt.Errorf("invalid value for the 'depot' flag. must be 'true', 'false', or ''")
70+
}
71+
72+
switch flag.GetString(ctx, "builder-pool") {
73+
case "", "true":
74+
span.AddEvent("opt-in builder-pool")
75+
useManagedBuilder = true
76+
case "false":
77+
useManagedBuilder = false
78+
case "auto":
79+
// nothing
80+
default:
81+
return nil, fmt.Errorf("invalid value for the 'builder-pool' flag. must be 'true', 'false', or ''")
6982
}
7083

7184
tb := render.NewTextBlock(ctx, "Building image")
72-
daemonType := imgsrc.NewDockerDaemonType(!flag.GetRemoteOnly(ctx), !flag.GetLocalOnly(ctx), env.IsCI(), depotBool, flag.GetBool(ctx, "nixpacks"))
85+
daemonType := imgsrc.NewDockerDaemonType(!flag.GetRemoteOnly(ctx), !flag.GetLocalOnly(ctx), env.IsCI(), depotBool, flag.GetBool(ctx, "nixpacks"), useManagedBuilder)
7386

7487
client := flyutil.ClientFromContext(ctx)
7588
io := iostreams.FromContext(ctx)

internal/command/mpg/mpg_test.go

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,14 @@ import (
1919

2020
// MockUiexClient implements the uiexutil.Client interface for testing
2121
type MockUiexClient struct {
22-
ListMPGRegionsFunc func(ctx context.Context, orgSlug string) (uiex.ListMPGRegionsResponse, error)
23-
ListManagedClustersFunc func(ctx context.Context, orgSlug string) (uiex.ListManagedClustersResponse, error)
24-
GetManagedClusterFunc func(ctx context.Context, orgSlug string, id string) (uiex.GetManagedClusterResponse, error)
25-
GetManagedClusterByIdFunc func(ctx context.Context, id string) (uiex.GetManagedClusterResponse, error)
26-
CreateUserFunc func(ctx context.Context, id string, input uiex.CreateUserInput) (uiex.CreateUserResponse, error)
27-
CreateClusterFunc func(ctx context.Context, input uiex.CreateClusterInput) (uiex.CreateClusterResponse, error)
28-
DestroyClusterFunc func(ctx context.Context, orgSlug string, id string) error
22+
ListMPGRegionsFunc func(ctx context.Context, orgSlug string) (uiex.ListMPGRegionsResponse, error)
23+
ListManagedClustersFunc func(ctx context.Context, orgSlug string) (uiex.ListManagedClustersResponse, error)
24+
GetManagedClusterFunc func(ctx context.Context, orgSlug string, id string) (uiex.GetManagedClusterResponse, error)
25+
GetManagedClusterByIdFunc func(ctx context.Context, id string) (uiex.GetManagedClusterResponse, error)
26+
CreateUserFunc func(ctx context.Context, id string, input uiex.CreateUserInput) (uiex.CreateUserResponse, error)
27+
CreateClusterFunc func(ctx context.Context, input uiex.CreateClusterInput) (uiex.CreateClusterResponse, error)
28+
DestroyClusterFunc func(ctx context.Context, orgSlug string, id string) error
29+
CreateFlyManagedBuilderFunc func(ctx context.Context, orgSlug string, region string) (uiex.CreateFlyManagedBuilderResponse, error)
2930
}
3031

3132
func (m *MockUiexClient) ListMPGRegions(ctx context.Context, orgSlug string) (uiex.ListMPGRegionsResponse, error) {
@@ -63,6 +64,13 @@ func (m *MockUiexClient) CreateUser(ctx context.Context, id string, input uiex.C
6364
return uiex.CreateUserResponse{}, nil
6465
}
6566

67+
func (m *MockUiexClient) CreateFlyManagedBuilder(ctx context.Context, orgSlug string, region string) (uiex.CreateFlyManagedBuilderResponse, error) {
68+
if m.CreateUserFunc != nil {
69+
return m.CreateFlyManagedBuilderFunc(ctx, orgSlug, region)
70+
}
71+
return uiex.CreateFlyManagedBuilderResponse{}, nil
72+
}
73+
6674
func (m *MockUiexClient) CreateCluster(ctx context.Context, input uiex.CreateClusterInput) (uiex.CreateClusterResponse, error) {
6775
if m.CreateClusterFunc != nil {
6876
return m.CreateClusterFunc(ctx, input)

0 commit comments

Comments
 (0)