Skip to content

Commit 62a6d5a

Browse files
authored
Support new Docker models (#1308)
* Support new Docker models * Update cli.nix vendorHash
1 parent 00dbc0d commit 62a6d5a

File tree

11 files changed

+212
-37
lines changed

11 files changed

+212
-37
lines changed

pkgs/defang/cli.nix

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ buildGoModule {
77
pname = "defang-cli";
88
version = "git";
99
src = ../../src;
10-
vendorHash = "sha256-VkI1L/yuZKRU8e4TeNZKXmB0hCxI+RvY8iqDlzWrY2s="; # TODO: use fetchFromGitHub
10+
vendorHash = "sha256-4QMrneh4I2gTFf7erVnakqPDZakFzceGaN+ieAMNPX0="; # TODO: use fetchFromGitHub
1111

1212
subPackages = [ "cmd/cli" ];
1313

src/go.mod

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ require (
3232
github.com/aws/smithy-go v1.22.1
3333
github.com/awslabs/goformation/v7 v7.13.1
3434
github.com/bufbuild/connect-go v1.10.0
35-
github.com/compose-spec/compose-go/v2 v2.7.2-0.20250703132301-891fce532a51
35+
github.com/compose-spec/compose-go/v2 v2.7.2-0.20250715094302-8da9902241f9
3636
github.com/digitalocean/godo v1.131.1
3737
github.com/docker/docker v25.0.6+incompatible
3838
github.com/golang-jwt/jwt/v5 v5.2.2
@@ -108,6 +108,7 @@ require (
108108
go.opentelemetry.io/contrib/detectors/gcp v1.34.0 // indirect
109109
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect
110110
go.opentelemetry.io/otel/sdk/metric v1.35.0 // indirect
111+
go.yaml.in/yaml/v3 v3.0.4 // indirect
111112
golang.org/x/crypto v0.37.0 // indirect
112113
golang.org/x/net v0.39.0 // indirect
113114
google.golang.org/genproto/googleapis/api v0.0.0-20250422160041-2d3770c4ea7f // indirect

src/go.sum

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,8 +114,8 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF
114114
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
115115
github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42 h1:Om6kYQYDUk5wWbT0t0q6pvyM49i9XZAv9dDrkDA7gjk=
116116
github.com/cncf/xds/go v0.0.0-20250121191232-2f005788dc42/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
117-
github.com/compose-spec/compose-go/v2 v2.7.2-0.20250703132301-891fce532a51 h1:AjI75N9METifYMZK7eNt8XIgY9Sryv+1w3XDA7X2vZQ=
118-
github.com/compose-spec/compose-go/v2 v2.7.2-0.20250703132301-891fce532a51/go.mod h1:Zow/3eYNOnl2T4qLGZEizf8d/ht1qfy09G7WGOSzGOY=
117+
github.com/compose-spec/compose-go/v2 v2.7.2-0.20250715094302-8da9902241f9 h1:kqvhWCmg3fVAPbfE8aJdV+qX1VqK4oK/DRI5yxeVd4E=
118+
github.com/compose-spec/compose-go/v2 v2.7.2-0.20250715094302-8da9902241f9/go.mod h1:veko/VB7URrg/tKz3vmIAQDaz+CGiXH8vZsW79NmAww=
119119
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
120120
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
121121
github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0=
@@ -329,6 +329,8 @@ go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt
329329
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
330330
go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I=
331331
go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM=
332+
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
333+
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
332334
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
333335
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
334336
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=

src/pkg/cli/compose/context.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -180,11 +180,11 @@ func getRemoteBuildContext(ctx context.Context, provider client.Provider, projec
180180
}
181181

182182
var archiveType ArchiveType
183-
// If we have a Railpack build, we use a zip archive
184183
if build.Dockerfile == RAILPACK {
184+
// If we have a Railpack build, we use a zip archive
185185
archiveType = ArchiveTypeZip
186-
// We use tar for all other builds
187186
} else {
187+
// We use gzip tar for all other builds
188188
archiveType = ArchiveTypeGzip
189189
}
190190

@@ -363,8 +363,8 @@ func WalkContextFolder(root, dockerfile string, fn func(path string, de os.DirEn
363363

364364
slashPath := filepath.ToSlash(relPath)
365365

366-
// we need the Dockerfile, even if it's in the .dockerignore file
367366
if relPath == dockerfile {
367+
// we need the Dockerfile, even if it's in the .dockerignore file
368368
} else if relPath == dockerignore {
369369
// we need the .dockerignore file too: it might ignore itself and/or the Dockerfile, but is needed by the builder
370370
} else {

src/pkg/cli/compose/context_test.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,6 @@ func TestUploadArchive(t *testing.T) {
7575
t.Errorf("Expected %v, got %v", server.URL+path, url)
7676
}
7777
})
78-
7978
})
8079

8180
t.Run("force upload without digest", func(t *testing.T) {

src/pkg/cli/compose/fixup.go

Lines changed: 80 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,12 @@ import (
1212
"github.com/DefangLabs/defang/src/pkg/cli/client"
1313
"github.com/DefangLabs/defang/src/pkg/term"
1414
defangv1 "github.com/DefangLabs/defang/src/protos/io/defang/v1"
15-
"github.com/compose-spec/compose-go/v2/types"
1615
composeTypes "github.com/compose-spec/compose-go/v2/types"
1716
)
1817

1918
const RAILPACK = "*Railpack"
2019

21-
func FixupServices(ctx context.Context, provider client.Provider, project *types.Project, upload UploadMode) error {
20+
func FixupServices(ctx context.Context, provider client.Provider, project *composeTypes.Project, upload UploadMode) error {
2221
// Preload the current config so we can detect which environment variables should be passed as "secrets"
2322
config, err := provider.ListConfig(ctx, &defangv1.ListConfigsRequest{Project: project.Name})
2423
if err != nil {
@@ -69,6 +68,12 @@ func FixupServices(ctx context.Context, provider client.Provider, project *types
6968
project.Services[svccfg.Name] = svccfg
7069
}
7170

71+
for name, model := range project.Models {
72+
model.Name = name // ensure the model has a name
73+
svccfg := fixupModel(model, project)
74+
project.Services[svccfg.Name] = *svccfg
75+
}
76+
7277
svcNameReplacer := NewServiceNameReplacer(provider, project)
7378

7479
for _, svccfg := range project.Services {
@@ -189,17 +194,17 @@ func parsePortString(port string) (uint32, error) {
189194
}
190195
}
191196

192-
func fixupLLM(svccfg *types.ServiceConfig) {
197+
func fixupLLM(svccfg *composeTypes.ServiceConfig) {
193198
image := getImageRepo(svccfg.Image)
194199
if strings.HasSuffix(image, "/openai-access-gateway") && len(svccfg.Ports) == 0 {
195200
// HACK: we must have at least one host port to get a CNAME for the service
196201
var port uint32 = 80
197202
term.Debugf("service %q: adding LLM host port %d", svccfg.Name, port)
198-
svccfg.Ports = []types.ServicePortConfig{{Target: port, Mode: Mode_HOST, Protocol: Protocol_TCP}}
203+
svccfg.Ports = []composeTypes.ServicePortConfig{{Target: port, Mode: Mode_HOST, Protocol: Protocol_TCP}}
199204
}
200205
}
201206

202-
func fixupPostgresService(svccfg *types.ServiceConfig, provider client.Provider, upload UploadMode) error {
207+
func fixupPostgresService(svccfg *composeTypes.ServiceConfig, provider client.Provider, upload UploadMode) error {
203208
if _, ok := provider.(*client.PlaygroundProvider); ok && upload != UploadModeEstimate {
204209
term.Warnf("service %q: managed postgres is not supported in the Playground; consider using BYOC (https://s.defang.io/byoc)", svccfg.Name)
205210
}
@@ -215,12 +220,12 @@ func fixupPostgresService(svccfg *types.ServiceConfig, provider client.Provider,
215220
}
216221
}
217222
term.Debugf("service %q: adding postgres host port %d", svccfg.Name, port)
218-
svccfg.Ports = []types.ServicePortConfig{{Target: port, Mode: Mode_HOST, Protocol: Protocol_TCP}}
223+
svccfg.Ports = []composeTypes.ServicePortConfig{{Target: port, Mode: Mode_HOST, Protocol: Protocol_TCP}}
219224
}
220225
return nil
221226
}
222227

223-
func fixupMongoService(svccfg *types.ServiceConfig, provider client.Provider, upload UploadMode) error {
228+
func fixupMongoService(svccfg *composeTypes.ServiceConfig, provider client.Provider, upload UploadMode) error {
224229
if _, ok := provider.(*client.PlaygroundProvider); ok && upload != UploadModeEstimate {
225230
term.Warnf("service %q: managed mongodb is not supported in the Playground; consider using BYOC (https://s.defang.io/byoc)", svccfg.Name)
226231
}
@@ -250,12 +255,12 @@ func fixupMongoService(svccfg *types.ServiceConfig, provider client.Provider, up
250255
break // done
251256
}
252257
term.Debugf("service %q: adding mongodb host port %d", svccfg.Name, port)
253-
svccfg.Ports = []types.ServicePortConfig{{Target: port, Mode: Mode_HOST, Protocol: Protocol_TCP}}
258+
svccfg.Ports = []composeTypes.ServicePortConfig{{Target: port, Mode: Mode_HOST, Protocol: Protocol_TCP}}
254259
}
255260
return nil
256261
}
257262

258-
func fixupRedisService(svccfg *types.ServiceConfig, provider client.Provider, upload UploadMode) error {
263+
func fixupRedisService(svccfg *composeTypes.ServiceConfig, provider client.Provider, upload UploadMode) error {
259264
if _, ok := provider.(*client.PlaygroundProvider); ok && upload != UploadModeEstimate {
260265
term.Warnf("service %q: Managed redis is not supported in the Playground; consider using BYOC (https://s.defang.io/byoc)", svccfg.Name)
261266
}
@@ -275,55 +280,102 @@ func fixupRedisService(svccfg *types.ServiceConfig, provider client.Provider, up
275280
}
276281
}
277282
term.Debugf("service %q: adding redis host port %d", svccfg.Name, port)
278-
svccfg.Ports = []types.ServicePortConfig{{Target: port, Mode: Mode_HOST, Protocol: Protocol_TCP}}
283+
svccfg.Ports = []composeTypes.ServicePortConfig{{Target: port, Mode: Mode_HOST, Protocol: Protocol_TCP}}
279284
}
280285
return nil
281286
}
282287

283-
func fixupModelProvider(svccfg *types.ServiceConfig, project *types.Project) {
284-
// Declare a private network for the model provider
285-
const modelProviderNetwork = "model_provider_private"
288+
// Declare a private network for the model provider
289+
const modelProviderNetwork = "model_provider_private"
290+
291+
func fixupModel(model composeTypes.ModelConfig, project *composeTypes.Project) *composeTypes.ServiceConfig {
292+
svccfg := &composeTypes.ServiceConfig{
293+
Name: model.Name,
294+
Extensions: model.Extensions,
295+
}
296+
makeAccessGatewayService(svccfg, project, model.Model) // TODO: pass other model options too
297+
return svccfg
298+
}
299+
300+
func fixupModelProvider(svccfg *composeTypes.ServiceConfig, project *composeTypes.Project) {
301+
var model string
302+
if modelVals := svccfg.Provider.Options["model"]; len(modelVals) == 1 {
303+
model = modelVals[0]
304+
}
305+
makeAccessGatewayService(svccfg, project, model)
306+
}
286307

308+
func makeAccessGatewayService(svccfg *composeTypes.ServiceConfig, project *composeTypes.Project, model string) {
287309
// Local Docker sets [SERVICE]_URL and [SERVICE]_MODEL environment variables on the dependent services
288310
envName := strings.ToUpper(svccfg.Name) // TODO: handle characters that are not allowed in env vars, like '-'
289-
urlEnv := envName + "_URL"
311+
endpointEnvVar := envName + "_URL"
290312
urlVal := "http://" + svccfg.Name + "/api/v1/"
291-
modelEnvKey := envName + "_MODEL"
292-
modelVals := svccfg.Provider.Options["model"]
313+
modelEnvVar := envName + "_MODEL"
293314

294315
empty := ""
295-
// svccfg.Deploy.Resources.Reservations.Limits = &types.Resources{} TODO: avoid memory limits warning
316+
// svccfg.Deploy.Resources.Reservations.Limits = &composeTypes.Resources{} TODO: avoid memory limits warning
296317
if svccfg.Environment == nil {
297-
svccfg.Environment = types.MappingWithEquals{}
318+
svccfg.Environment = composeTypes.MappingWithEquals{}
298319
}
299320
if _, exists := svccfg.Environment["OPENAI_API_KEY"]; !exists {
300321
svccfg.Environment["OPENAI_API_KEY"] = &empty // disable auth; see https://github.com/DefangLabs/openai-access-gateway/pull/5
301322
}
302-
// svccfg.HealthCheck = &types.ServiceHealthCheckConfig{} TODO: add healthcheck
323+
// svccfg.HealthCheck = &composeTypes.ServiceHealthCheckConfig{} TODO: add healthcheck
303324
svccfg.Image = "defangio/openai-access-gateway"
304325
if svccfg.Networks == nil {
305326
// New compose-go versions do not create networks for "provider:" services, so we need to create it here
306-
svccfg.Networks = make(map[string]*types.ServiceNetworkConfig)
327+
svccfg.Networks = make(map[string]*composeTypes.ServiceNetworkConfig)
307328
} else {
308329
delete(svccfg.Networks, "default") // remove the default network
309330
}
310331
svccfg.Networks[modelProviderNetwork] = nil
311-
svccfg.Ports = []types.ServicePortConfig{{Target: 80, Mode: Mode_HOST, Protocol: Protocol_TCP}}
332+
svccfg.Ports = []composeTypes.ServicePortConfig{{Target: 80, Mode: Mode_HOST, Protocol: Protocol_TCP}}
312333
svccfg.Provider = nil // remove "provider:" because current backend will not accept it
313-
project.Networks[modelProviderNetwork] = types.NetworkConfig{Name: modelProviderNetwork}
334+
project.Networks[modelProviderNetwork] = composeTypes.NetworkConfig{Name: modelProviderNetwork}
314335

315-
// Set environment variables (url and model) for any service that depends on the provider pseudo service
336+
// Set environment variables (url and model) for any service that depends on the model
316337
for _, dependency := range project.Services {
317338
if _, ok := dependency.DependsOn[svccfg.Name]; ok {
318339
if dependency.Environment == nil {
319-
dependency.Environment = make(types.MappingWithEquals)
340+
dependency.Environment = make(composeTypes.MappingWithEquals)
341+
}
342+
dependency.Networks[modelProviderNetwork] = nil
343+
if _, ok := dependency.Environment[endpointEnvVar]; !ok {
344+
dependency.Environment[endpointEnvVar] = &urlVal
345+
}
346+
if _, ok := dependency.Environment[modelEnvVar]; !ok && model != "" {
347+
dependency.Environment[modelEnvVar] = &model
348+
}
349+
}
350+
351+
if modelDep, ok := dependency.Models[svccfg.Name]; ok {
352+
endpointVar := endpointEnvVar
353+
if modelDep != nil && modelDep.EndpointVariable != "" {
354+
endpointVar = modelDep.EndpointVariable
355+
}
356+
modelVar := modelEnvVar
357+
if modelDep != nil && modelDep.ModelVariable != "" {
358+
modelVar = modelDep.ModelVariable
359+
}
360+
if dependency.Environment == nil {
361+
dependency.Environment = make(composeTypes.MappingWithEquals)
320362
}
321363
dependency.Networks[modelProviderNetwork] = nil
322-
if _, ok := dependency.Environment[urlEnv]; !ok {
323-
dependency.Environment[urlEnv] = &urlVal
364+
if _, ok := dependency.Environment[endpointVar]; !ok {
365+
dependency.Environment[endpointVar] = &urlVal
366+
}
367+
if _, ok := dependency.Environment[modelVar]; !ok && model != "" {
368+
dependency.Environment[modelVar] = &model
324369
}
325-
if _, ok := dependency.Environment[modelEnvKey]; !ok && len(modelVals) == 1 {
326-
dependency.Environment[modelEnvKey] = &modelVals[0]
370+
// If the model is not already declared as a dependency, add it
371+
if _, ok := dependency.DependsOn[svccfg.Name]; !ok {
372+
if dependency.DependsOn == nil {
373+
dependency.DependsOn = make(map[string]composeTypes.ServiceDependency)
374+
}
375+
dependency.DependsOn[svccfg.Name] = composeTypes.ServiceDependency{
376+
Condition: composeTypes.ServiceConditionStarted,
377+
Required: true,
378+
}
327379
}
328380
}
329381
}

src/pkg/cli/compose/validation.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ func ValidateProject(project *composeTypes.Project) error {
5353
continue
5454
}
5555
if gcp.SafeLabelValue(svccfg.Name) == gcp.SafeLabelValue(services[j].Name) { // TODO: Shouldn't be just gcp specific
56-
errs = append(errs, fmt.Errorf("The service names %q and %q normalize to the same value, which causes a conflict. Please use distinct names that differ after normalization", svccfg.Name, services[j].Name))
56+
errs = append(errs, fmt.Errorf("the service names %q and %q normalize to the same value, which causes a conflict. Please use distinct names that differ after normalization", svccfg.Name, services[j].Name))
5757
}
5858
}
5959
}

src/testdata/models/compose.yaml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
services:
2+
app:
3+
image: app
4+
models: # per docs, should support list as well
5+
ai_model: {}
6+
withendpoint:
7+
image: app
8+
models:
9+
my_model:
10+
endpoint_var: MODEL_URL
11+
12+
models:
13+
ai_model:
14+
model: ai/model
15+
my_model:
16+
model: ai/model
17+
context_size: 1024
18+
runtime_flags:
19+
- "--a-flag"
20+
- "--another-flag=42"
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
{
2+
"ai_model": {
3+
"command": null,
4+
"entrypoint": null,
5+
"environment": {
6+
"OPENAI_API_KEY": ""
7+
},
8+
"image": "defangio/openai-access-gateway",
9+
"networks": {
10+
"model_provider_private": null
11+
},
12+
"ports": [
13+
{
14+
"mode": "host",
15+
"target": 80,
16+
"protocol": "tcp"
17+
}
18+
]
19+
},
20+
"app": {
21+
"command": null,
22+
"entrypoint": null,
23+
"environment": {
24+
"AI_MODEL_MODEL": "ai/model",
25+
"AI_MODEL_URL": "http://ai-model/api/v1/"
26+
},
27+
"image": "app",
28+
"models": {
29+
"ai_model": {}
30+
},
31+
"networks": {
32+
"default": null,
33+
"model_provider_private": null
34+
}
35+
},
36+
"my_model": {
37+
"command": null,
38+
"entrypoint": null,
39+
"environment": {
40+
"OPENAI_API_KEY": ""
41+
},
42+
"image": "defangio/openai-access-gateway",
43+
"networks": {
44+
"model_provider_private": null
45+
},
46+
"ports": [
47+
{
48+
"mode": "host",
49+
"target": 80,
50+
"protocol": "tcp"
51+
}
52+
]
53+
},
54+
"withendpoint": {
55+
"command": null,
56+
"entrypoint": null,
57+
"environment": {
58+
"MODEL_URL": "http://my-model/api/v1/",
59+
"MY_MODEL_MODEL": "ai/model"
60+
},
61+
"image": "app",
62+
"models": {
63+
"my_model": {
64+
"endpoint_var": "MODEL_URL"
65+
}
66+
},
67+
"networks": {
68+
"default": null,
69+
"model_provider_private": null
70+
}
71+
}
72+
}

0 commit comments

Comments
 (0)