Skip to content
Merged
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
89 changes: 76 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,15 @@ 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

const (
pullPolicyAlways pullPolicy = "always"
pullPolicyMissing pullPolicy = "missing"
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
Contributor 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 +63,8 @@ type HatchetLiteOpts struct {
serviceName string
overrideDashboardPort int
overrideGrpcPort int
imageTag string
pullPolicy pullPolicy
}

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

Expand Down Expand Up @@ -133,6 +146,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 +287,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 +389,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 +580,38 @@ func getSharedLabels(opts *HatchetLiteOpts) map[string]string {
"com.docker.compose.project": opts.projectName,
}
}

func (d *DockerDriver) pullImageWithPolicy(ctx context.Context, imageName string, policy pullPolicy) error {
switch policy {
case pullPolicyNever:
_, 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:
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
}

printInfo(fmt.Sprintf("Image %s not found locally, pulling... (pull policy: missing)", imageName))
case pullPolicyAlways:
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
}
59 changes: 41 additions & 18 deletions cmd/hatchet-cli/cli/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,48 @@ 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")

opts := []docker.HatchetLiteOpt{}

if dashboardPort != 0 {
opts = append(opts, docker.WithOverrideDashboardPort(dashboardPort))
}

if grpcPort != 0 {
opts = append(opts, docker.WithOverrideGrpcPort(grpcPort))
}

if projectName != "" {
opts = append(opts, docker.WithProjectName(projectName))
}

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

result, err := startLocalServer(cmd, profileName, dashboardPort, grpcPort, projectName)
if pullPolicy != "" {
opts = append(opts, docker.WithPullPolicy(pullPolicy))
}

result, err := startLocalServer(cmd, profileName, opts...)
if err != nil {
cli.Logger.Fatalf("%v", err)
}
Expand Down Expand Up @@ -94,7 +127,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, opts ...docker.HatchetLiteOpt) (*ServerStartResult, error) {
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 All @@ -104,29 +137,17 @@ func startLocalServer(cmd *cobra.Command, profileName string, dashboardPort, grp
var actualDashboardPort, actualGrpcPort int

// Build options for RunHatchetLite
opts := []docker.HatchetLiteOpt{
allOpts := append(opts,
docker.WithCreateTokenCallback(func(tok string) {
token = tok
}),
docker.WithPortsCallback(func(dashboard, grpc int) {
actualDashboardPort = dashboard
actualGrpcPort = grpc
}),
}

if dashboardPort != 0 {
opts = append(opts, docker.WithOverrideDashboardPort(dashboardPort))
}

if grpcPort != 0 {
opts = append(opts, docker.WithOverrideGrpcPort(grpcPort))
}

if projectName != "" {
opts = append(opts, docker.WithProjectName(projectName))
}
)

err = dockerDriver.RunHatchetLite(cmd.Context(), opts...)
err = dockerDriver.RunHatchetLite(cmd.Context(), allOpts...)
if err != nil {
return nil, fmt.Errorf("could not start hatchet-lite container: %w", err)
}
Expand Down Expand Up @@ -183,6 +204,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")
if err != nil {
cli.Logger.Errorf("%v", err)
fmt.Println()
Expand Down
Loading