120 lines
2.7 KiB
Go
120 lines
2.7 KiB
Go
package run
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
|
|
"github.com/homarr-labs/homarr/tools/homarr-dev/internal/docker"
|
|
"github.com/homarr-labs/homarr/tools/homarr-dev/internal/gh"
|
|
)
|
|
|
|
type Options struct {
|
|
Context context.Context
|
|
PR int
|
|
Tag string
|
|
Demo bool
|
|
Env []string
|
|
FetchTitle bool
|
|
FindPort func(int) int
|
|
}
|
|
|
|
type Plan struct {
|
|
Image string
|
|
Name string
|
|
Volume string
|
|
Label string
|
|
HostPort int
|
|
Env []string
|
|
Platform string
|
|
Pull bool
|
|
PRNumber int
|
|
}
|
|
|
|
func BuildPlan(opts Options) (*Plan, error) {
|
|
p := &Plan{}
|
|
ctx := opts.Context
|
|
if ctx == nil {
|
|
ctx = context.Background()
|
|
}
|
|
findPort := opts.FindPort
|
|
if findPort == nil {
|
|
findPort = docker.FindFreePort
|
|
}
|
|
|
|
if opts.PR > 0 {
|
|
p.PRNumber = opts.PR
|
|
p.Image = fmt.Sprintf("ghcr.io/homarr-labs/homarr-test:pr-%d", opts.PR)
|
|
p.Name = fmt.Sprintf("homarr_pr_%d", opts.PR)
|
|
p.Volume = fmt.Sprintf("homarr_pr_%d_data", opts.PR)
|
|
p.Label = fmt.Sprintf("PR #%d", opts.PR)
|
|
p.Pull = true
|
|
p.Platform = "linux/amd64"
|
|
if opts.FetchTitle {
|
|
if pr, err := gh.GetPR(ctx, opts.PR); err == nil && pr.Title != "" {
|
|
p.Label = fmt.Sprintf("PR #%d — %s", opts.PR, pr.Title)
|
|
}
|
|
}
|
|
} else if opts.Tag != "" {
|
|
p.Image = "homarr:" + opts.Tag
|
|
p.Name = "homarr_" + opts.Tag
|
|
p.Volume = "homarr_" + opts.Tag + "_data"
|
|
p.Label = "tag: " + opts.Tag
|
|
} else {
|
|
return nil, fmt.Errorf("need a tag or --pr")
|
|
}
|
|
|
|
p.HostPort = findPort(7575)
|
|
if p.HostPort == 0 {
|
|
return nil, fmt.Errorf("no free host port is available")
|
|
}
|
|
p.Env = append(p.Env, opts.Env...)
|
|
if opts.Demo {
|
|
p.Env = append(p.Env, "DEMO_MODE=true", "UNSAFE_ENABLE_MOCK_INTEGRATION=true")
|
|
p.Label += " (demo)"
|
|
}
|
|
return p, nil
|
|
}
|
|
|
|
func StartDetached(p *Plan) error {
|
|
return start(p, true)
|
|
}
|
|
|
|
func StartForeground(p *Plan) error {
|
|
return start(p, false)
|
|
}
|
|
|
|
func start(p *Plan, daemon bool) error {
|
|
const attempts = 5
|
|
for attempt := 0; attempt < attempts; attempt++ {
|
|
err := docker.Start(docker.StartOptions{
|
|
Name: p.Name,
|
|
Image: p.Image,
|
|
Volume: p.Volume,
|
|
HostPort: p.HostPort,
|
|
Env: p.Env,
|
|
Platform: p.Platform,
|
|
PullAlways: p.Pull && attempt == 0,
|
|
Daemon: daemon,
|
|
})
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
if !docker.IsPortConflict(err) || attempt == attempts-1 {
|
|
if docker.IsPortConflict(err) {
|
|
return fmt.Errorf("unable to bind a host port after %d attempts; restart Docker or OrbStack: %w", attempts, err)
|
|
}
|
|
return err
|
|
}
|
|
previousPort := p.HostPort
|
|
p.HostPort = docker.FindFreePort(p.HostPort + 1)
|
|
if p.HostPort == 0 {
|
|
return fmt.Errorf("no free host port is available: %w", err)
|
|
}
|
|
if !daemon {
|
|
fmt.Fprintf(os.Stderr, "Port %d unavailable; retrying on %d.\n", previousPort, p.HostPort)
|
|
}
|
|
}
|
|
return nil
|
|
}
|