Skip to content
This repository was archived by the owner on Mar 27, 2026. It is now read-only.
Open
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
7 changes: 6 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@ Both client libraries are pre-1.0, and they have separate versioning.

## Unreleased

No unreleased changes.
- Added support for the `MODAL_FORCE_BUILD` environment variable and `force_build` config option for `modal.toml`. When set, this applies to all image builds.
- Added an optional `forceBuild` parameter to `images.fromRegistry`, `images.fromAwsEcr`, `images.fromGcpArtifactRegistry`, and `Image.build` to the JS SDK.
- Added `secret` field to `ImageFromRegistryParams` in the JS SDK to match the Go SDK pattern. The separate `secret` parameter in `images.fromRegistry()` is now deprecated.

**Breaking changes:**
- Added `...Params` struct arguments, and an optional `ForceBuild` parameter, to `Images.FromRegistry`, `Images.FromAwsEcr`, `Images.FromGcpArtifactRegistry`, and `Image.Build` to the Go SDK.

## modal-js/v0.6.0, modal-go/v0.6.0

Expand Down
10 changes: 10 additions & 0 deletions modal-go/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"fmt"
"os"
"path/filepath"
"strings"

"github.com/pelletier/go-toml/v2"
)
Expand All @@ -20,6 +21,7 @@ type Profile struct {
Environment string
ImageBuilderVersion string
LogLevel string
ForceBuild bool
}

// rawProfile mirrors the TOML structure on disk.
Expand All @@ -30,6 +32,7 @@ type rawProfile struct {
Environment string `toml:"environment"`
ImageBuilderVersion string `toml:"image_builder_version"`
LogLevel string `toml:"loglevel"`
ForceBuild bool `toml:"force_build"`
Active bool `toml:"active"`
}

Expand Down Expand Up @@ -95,6 +98,7 @@ func getProfile(name string, cfg config) Profile {
environment := firstNonEmpty(os.Getenv("MODAL_ENVIRONMENT"), raw.Environment)
imageBuilderVersion := firstNonEmpty(os.Getenv("MODAL_IMAGE_BUILDER_VERSION"), raw.ImageBuilderVersion)
logLevel := firstNonEmpty(os.Getenv("MODAL_LOGLEVEL"), raw.LogLevel)
forceBuild := envBool("MODAL_FORCE_BUILD") || raw.ForceBuild

return Profile{
ServerURL: serverURL,
Expand All @@ -103,6 +107,7 @@ func getProfile(name string, cfg config) Profile {
Environment: environment,
ImageBuilderVersion: imageBuilderVersion,
LogLevel: logLevel,
ForceBuild: forceBuild,
}
}

Expand All @@ -115,6 +120,11 @@ func firstNonEmpty(values ...string) string {
return ""
}

func envBool(key string) bool {
v := strings.ToLower(os.Getenv(key))
return v != "" && v != "0" && v != "false"
}

func environmentName(environment string, profile Profile) string {
return firstNonEmpty(environment, profile.Environment)
}
Expand Down
2 changes: 1 addition & 1 deletion modal-go/examples/sandbox-prewarm/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ func main() {

// With `.Build(app)`, we create an Image object on Modal that eagerly pulls
// from the registry.
image, err := mc.Images.FromRegistry("alpine:3.21", nil).Build(ctx, app)
image, err := mc.Images.FromRegistry("alpine:3.21", nil).Build(ctx, app, nil)
if err != nil {
log.Fatalf("Unable to build Image: %v", err)
}
Expand Down
2 changes: 1 addition & 1 deletion modal-go/examples/sandbox-private-image/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ func main() {
log.Fatalf("Failed to get Secret: %v", err)
}

image := mc.Images.FromAwsEcr("459781239556.dkr.ecr.us-east-1.amazonaws.com/ecr-private-registry-test-7522615:python", secret)
image := mc.Images.FromAwsEcr("459781239556.dkr.ecr.us-east-1.amazonaws.com/ecr-private-registry-test-7522615:python", secret, nil)

sb, err := mc.Sandboxes.Create(ctx, app, image, &modal.SandboxCreateParams{
Command: []string{"python", "-c", `import sys; sys.stdout.write(sys.stdin.read())`},
Expand Down
49 changes: 41 additions & 8 deletions modal-go/image.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ import (
// ImageService provides Image related operations.
type ImageService interface {
FromRegistry(tag string, params *ImageFromRegistryParams) *Image
FromAwsEcr(tag string, secret *Secret) *Image
FromGcpArtifactRegistry(tag string, secret *Secret) *Image
FromAwsEcr(tag string, secret *Secret, params *ImageFromAwsEcrParams) *Image
FromGcpArtifactRegistry(tag string, secret *Secret, params *ImageFromGcpArtifactRegistryParams) *Image
FromID(ctx context.Context, imageID string) (*Image, error)
Delete(ctx context.Context, imageID string, params *ImageDeleteParams) error
}
Expand Down Expand Up @@ -53,13 +53,30 @@ type Image struct {
imageRegistryConfig *pb.ImageRegistryConfig
tag string
layers []layer
forceBuild bool

client *Client
}

// ImageFromRegistryParams are options for creating an Image from a registry.
type ImageFromRegistryParams struct {
Secret *Secret // Secret for private registry authentication.
Secret *Secret // Secret containing credentials for private registry authentication.
ForceBuild bool // Ignore cached builds, similar to 'docker build --no-cache'.
}

// ImageFromAwsEcrParams are options for creating an Image from AWS ECR.
type ImageFromAwsEcrParams struct {
ForceBuild bool // Ignore cached builds, similar to 'docker build --no-cache'.
}

// ImageFromGcpArtifactRegistryParams are options for creating an Image from GCP Artifact Registry.
type ImageFromGcpArtifactRegistryParams struct {
ForceBuild bool // Ignore cached builds, similar to 'docker build --no-cache'.
}

// ImageBuildParams are options for Image.Build().
type ImageBuildParams struct {
ForceBuild bool // Ignore cached builds, similar to 'docker build --no-cache'.
}

// FromRegistry builds a Modal Image from a public or private image registry without any changes.
Expand All @@ -80,12 +97,16 @@ func (s *imageServiceImpl) FromRegistry(tag string, params *ImageFromRegistryPar
imageRegistryConfig: imageRegistryConfig,
tag: tag,
layers: []layer{{}},
forceBuild: params.ForceBuild,
client: s.client,
}
}

// FromAwsEcr creates an Image from an AWS ECR tag
func (s *imageServiceImpl) FromAwsEcr(tag string, secret *Secret) *Image {
func (s *imageServiceImpl) FromAwsEcr(tag string, secret *Secret, params *ImageFromAwsEcrParams) *Image {
if params == nil {
params = &ImageFromAwsEcrParams{}
}
imageRegistryConfig := pb.ImageRegistryConfig_builder{
RegistryAuthType: pb.RegistryAuthType_REGISTRY_AUTH_TYPE_AWS,
SecretId: secret.SecretID,
Expand All @@ -96,12 +117,16 @@ func (s *imageServiceImpl) FromAwsEcr(tag string, secret *Secret) *Image {
imageRegistryConfig: imageRegistryConfig,
tag: tag,
layers: []layer{{}},
forceBuild: params.ForceBuild,
client: s.client,
}
}

// FromGcpArtifactRegistry creates an Image from a GCP Artifact Registry tag.
func (s *imageServiceImpl) FromGcpArtifactRegistry(tag string, secret *Secret) *Image {
func (s *imageServiceImpl) FromGcpArtifactRegistry(tag string, secret *Secret, params *ImageFromGcpArtifactRegistryParams) *Image {
if params == nil {
params = &ImageFromGcpArtifactRegistryParams{}
}
imageRegistryConfig := pb.ImageRegistryConfig_builder{
RegistryAuthType: pb.RegistryAuthType_REGISTRY_AUTH_TYPE_GCP,
SecretId: secret.SecretID,
Expand All @@ -111,6 +136,7 @@ func (s *imageServiceImpl) FromGcpArtifactRegistry(tag string, secret *Secret) *
imageRegistryConfig: imageRegistryConfig,
tag: tag,
layers: []layer{{}},
forceBuild: params.ForceBuild,
client: s.client,
}
}
Expand Down Expand Up @@ -166,6 +192,7 @@ func (image *Image) DockerfileCommands(commands []string, params *ImageDockerfil
tag: image.tag,
imageRegistryConfig: image.imageRegistryConfig,
layers: newLayers,
forceBuild: image.forceBuild,
client: image.client,
}
}
Expand Down Expand Up @@ -219,7 +246,7 @@ func (image *Image) waitForBuildIteration(ctx context.Context, imageID string, l
}

// Build eagerly builds an Image on Modal.
func (image *Image) Build(ctx context.Context, app *App) (*Image, error) {
func (image *Image) Build(ctx context.Context, app *App, params *ImageBuildParams) (*Image, error) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Being able to force build everything feels like a DevX gotcha, that I'm not sure how to resolve.

If a developer force builds a registry image, then sandbox in production using that image with DockerfileCommands will run into higher latency because that image will be rebuilt.

// Image is already hyrdated
if image.ImageID != "" {
return image, nil
Expand All @@ -233,8 +260,12 @@ func (image *Image) Build(ctx context.Context, app *App) (*Image, error) {
}
}

var currentImageID string
forceBuild := image.client.profile.ForceBuild || image.forceBuild
if params != nil {
forceBuild = forceBuild || params.ForceBuild
}

var currentImageID string
for i, currentLayer := range image.layers {
if err := ctx.Err(); err != nil {
return nil, err
Expand Down Expand Up @@ -273,6 +304,8 @@ func (image *Image) Build(ctx context.Context, app *App) (*Image, error) {
}.Build()}
}

forceBuild = forceBuild || currentLayer.forceBuild

resp, err := image.client.cpClient.ImageGetOrCreate(
ctx,
pb.ImageGetOrCreateRequest_builder{
Expand All @@ -286,7 +319,7 @@ func (image *Image) Build(ctx context.Context, app *App) (*Image, error) {
BaseImages: baseImages,
}.Build(),
BuilderVersion: imageBuilderVersion("", image.client.profile),
ForceBuild: currentLayer.forceBuild,
ForceBuild: forceBuild,
}.Build(),
)
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion modal-go/sandbox.go
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,7 @@ func (s *sandboxServiceImpl) Create(ctx context.Context, app *App, image *Image,
params = &SandboxCreateParams{}
}

image, err := image.Build(ctx, app)
image, err := image.Build(ctx, app, nil)
if err != nil {
return nil, err
}
Expand Down
Loading