Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
97 changes: 84 additions & 13 deletions cmd/hatchet-cli/cli/internal/drivers/docker/hatchet_lite.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,18 @@ func printInfo(message string) {
fmt.Println(infoStyle.Render(fmt.Sprintf(" %s", message)))
}

// PullPolicy controls when Docker images are pulled, mirroring Docker Compose's pull_policy.
type PullPolicy string
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Should PullPolicy be public?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Sorry for overlooking that, it should not be public, since its only used internally within the docker package


const (
// PullPolicyAlways always pulls the image from the registry (default, existing behavior).
PullPolicyAlways PullPolicy = "always"
// PullPolicyMissing only pulls if the image is not already available locally.
PullPolicyMissing PullPolicy = "missing"
// PullPolicyNever never pulls; the image must already exist locally.
PullPolicyNever PullPolicy = "never"
)

Comment on lines +39 to +44
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

IMO we can remove these comments. Perhaps just leave the original on PullPolicy

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

ok

// hatchet lite opts
const (
defaultpostgresName = "postgres"
Expand All @@ -54,6 +66,8 @@ type HatchetLiteOpts struct {
serviceName string
overrideDashboardPort int
overrideGrpcPort int
imageTag string
pullPolicy PullPolicy
}

func initDefaultHatchetLiteOpts() *HatchetLiteOpts {
Expand All @@ -62,6 +76,8 @@ func initDefaultHatchetLiteOpts() *HatchetLiteOpts {
hatchetName: defaulthatchetName,
projectName: defaultprojectName,
serviceName: defaultserviceName,
imageTag: "latest",
pullPolicy: PullPolicyAlways,
}
}

Expand Down Expand Up @@ -133,6 +149,31 @@ func WithOverrideGrpcPort(port int) HatchetLiteOpt {
}
}

// WithImageTag sets the image tag for the hatchet-lite container (e.g. "v0.83.1").
func WithImageTag(tag string) HatchetLiteOpt {
return func(o *HatchetLiteOpts) error {
if tag == "" {
return fmt.Errorf("image tag must not be empty")
}

o.imageTag = tag
return nil
}
}

// WithPullPolicy sets the image pull policy. Valid values are "always", "missing", and "never".
func WithPullPolicy(policy string) HatchetLiteOpt {
return func(o *HatchetLiteOpts) error {
switch PullPolicy(policy) {
case PullPolicyAlways, PullPolicyMissing, PullPolicyNever:
o.pullPolicy = PullPolicy(policy)
return nil
default:
return fmt.Errorf("invalid pull policy %q: must be \"always\", \"missing\", or \"never\"", policy)
}
}
}

func (d *DockerDriver) RunHatchetLite(ctx context.Context, opts ...HatchetLiteOpt) error {
hatchetLiteOpts := initDefaultHatchetLiteOpts()

Expand Down Expand Up @@ -249,14 +290,9 @@ func (d *DockerDriver) startPostgresContainer(ctx context.Context, opts *Hatchet
imageName := "postgres:17"
containerName := canonicalContainerName(opts.projectName, opts.postgresName)

out, err := d.apiClient.ImagePull(ctx, imageName, image.PullOptions{})
if err != nil {
if err := d.pullImageWithPolicy(ctx, imageName, opts.pullPolicy); err != nil {
return fmt.Errorf("could not pull image %s: %w", imageName, err)
}
defer out.Close()

// Display progress while pulling the image
displayImagePullProgress(out, imageName)

// Get image details for proper labeling
imageInspect, err := d.apiClient.ImageInspect(ctx, imageName)
Expand Down Expand Up @@ -356,17 +392,12 @@ func (d *DockerDriver) stopPostgresContainer(ctx context.Context, opts *HatchetL
}

func (d *DockerDriver) startHatchetLiteContainer(ctx context.Context, opts *HatchetLiteOpts, networkId string, dashboardPort, grpcPort int, sharedLabels map[string]string) error {
imageName := "ghcr.io/hatchet-dev/hatchet/hatchet-lite:latest"
imageName := "ghcr.io/hatchet-dev/hatchet/hatchet-lite:" + opts.imageTag
containerName := canonicalContainerName(opts.projectName, opts.hatchetName)

out, err := d.apiClient.ImagePull(ctx, imageName, image.PullOptions{})
if err != nil {
if err := d.pullImageWithPolicy(ctx, imageName, opts.pullPolicy); err != nil {
return fmt.Errorf("could not pull image %s: %w", imageName, err)
}
defer out.Close()

// Display progress while pulling the image
displayImagePullProgress(out, imageName)

// Get image details for proper labeling
imageInspect, err := d.apiClient.ImageInspect(ctx, imageName)
Expand Down Expand Up @@ -552,3 +583,43 @@ func getSharedLabels(opts *HatchetLiteOpts) map[string]string {
"com.docker.compose.project": opts.projectName,
}
}

// pullImageWithPolicy pulls an image according to the specified pull policy.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

super-nit: don't think we need docs IMO -- this is a private + internal function unlikely to be used anywhere else in the codebase.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Apologies for the over documentation, I was using some AI assistance to ensure the logic was clear but definitely went a bit overboard

func (d *DockerDriver) pullImageWithPolicy(ctx context.Context, imageName string, policy PullPolicy) error {
switch policy {
case PullPolicyNever:
// Verify the image exists locally.
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Can we remove these comments? Would help tighten up the diff!

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

sure

_, err := d.apiClient.ImageInspect(ctx, imageName)
if err != nil {
return fmt.Errorf("image %s not found locally and pull policy is \"never\": %w", imageName, err)
}

printInfo(fmt.Sprintf("Using local image %s (pull policy: never)", imageName))

return nil
case PullPolicyMissing:
// Only pull if the image is not present locally.
if _, err := d.apiClient.ImageInspect(ctx, imageName); err == nil {
printInfo(fmt.Sprintf("Image %s already exists locally, skipping pull (pull policy: missing)", imageName))

return nil
}

// Image not found locally, fall through to pull.
printInfo(fmt.Sprintf("Image %s not found locally, pulling... (pull policy: missing)", imageName))
case PullPolicyAlways:
// Always pull, this is the default behavior.
default:
return fmt.Errorf("unknown pull policy: %q", policy)
}

out, err := d.apiClient.ImagePull(ctx, imageName, image.PullOptions{})
if err != nil {
return fmt.Errorf("could not pull image %s: %w", imageName, err)
}
defer out.Close()

displayImagePullProgress(out, imageName)

return nil
}
27 changes: 24 additions & 3 deletions cmd/hatchet-cli/cli/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,26 @@ var startCmd = &cobra.Command{
hatchet server start --dashboard-port 9000 --grpc-port 8077 --project-name my-hatchet

# Start server with custom profile name
hatchet server start --profile my-local`,
hatchet server start --profile my-local

# Start server with a specific hatchet-lite version
hatchet server start --tag v0.83.1

# Start server without pulling images (use local images only)
hatchet server start --pull-policy never

# Only pull images if they are not already available locally
hatchet server start --pull-policy missing`,
Run: func(cmd *cobra.Command, args []string) {
// Get flag values
dashboardPort, _ := cmd.Flags().GetInt("dashboard-port")
grpcPort, _ := cmd.Flags().GetInt("grpc-port")
projectName, _ := cmd.Flags().GetString("project-name")
profileName, _ := cmd.Flags().GetString("profile")
tag, _ := cmd.Flags().GetString("tag")
pullPolicy, _ := cmd.Flags().GetString("pull-policy")

result, err := startLocalServer(cmd, profileName, dashboardPort, grpcPort, projectName)
result, err := startLocalServer(cmd, profileName, dashboardPort, grpcPort, projectName, tag, pullPolicy)
if err != nil {
cli.Logger.Fatalf("%v", err)
}
Expand Down Expand Up @@ -94,7 +105,7 @@ type ServerStartResult struct {
}

// startLocalServer starts a local Hatchet server and returns connection details
func startLocalServer(cmd *cobra.Command, profileName string, dashboardPort, grpcPort int, projectName string) (*ServerStartResult, error) {
func startLocalServer(cmd *cobra.Command, profileName string, dashboardPort, grpcPort int, projectName, tag, pullPolicy string) (*ServerStartResult, error) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

nit: function signature is getting quite long. Wdyt of passing in a struct here instead? Or perhaps instead replacing all of these with varadic HatchetLiteOpt fns?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

well second option will be better i think, refactored startLocalServer to use the variadic HatchetLiteOpt pattern. Its much cleaner now. Thanks for the tip

dockerDriver, err := docker.NewDockerDriver(cmd.Context())
if err != nil {
return nil, fmt.Errorf("Docker is required to run a local server. Please ensure Docker is installed and running: %w", err)
Expand Down Expand Up @@ -126,6 +137,14 @@ func startLocalServer(cmd *cobra.Command, profileName string, dashboardPort, grp
opts = append(opts, docker.WithProjectName(projectName))
}

if tag != "" {
opts = append(opts, docker.WithImageTag(tag))
}

if pullPolicy != "" {
opts = append(opts, docker.WithPullPolicy(pullPolicy))
}

err = dockerDriver.RunHatchetLite(cmd.Context(), opts...)
if err != nil {
return nil, fmt.Errorf("could not start hatchet-lite container: %w", err)
Expand Down Expand Up @@ -183,6 +202,8 @@ func init() {
startCmd.Flags().IntP("grpc-port", "g", 0, "Port for the Hatchet gRPC server (default: auto-detect starting at 7077)")
startCmd.Flags().StringP("project-name", "p", "", "Docker project name for containers (default: hatchet-cli)")
startCmd.Flags().StringP("profile", "n", "local", "Name for the local profile (default: local)")
startCmd.Flags().StringP("tag", "t", "latest", `Image tag for the hatchet-lite container (e.g. "v0.83.1")`)
startCmd.Flags().String("pull-policy", "always", `Image pull policy: "always", "missing", or "never"`)

stopCmd.Flags().StringP("project-name", "p", "", "Docker project name for containers (default: hatchet-cli)")
}
2 changes: 1 addition & 1 deletion cmd/hatchet-cli/cli/worker.go
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,7 @@ func handleNoProfiles(cmd *cobra.Command) string {
func startLocalServerAndCreateProfile(cmd *cobra.Command) string {
fmt.Println(styles.InfoMessage("Starting local Hatchet server..."))

result, err := startLocalServer(cmd, "local", 0, 0, "")
result, err := startLocalServer(cmd, "local", 0, 0, "", "latest", "always")
if err != nil {
cli.Logger.Errorf("%v", err)
fmt.Println()
Expand Down
Loading