From 0a88aa5cd46d642dccccccc47ceda597e7176668 Mon Sep 17 00:00:00 2001 From: Alex Collins Date: Thu, 29 Dec 2022 15:38:56 -0800 Subject: [PATCH] feat: auto-rebuild --- README.md | 45 ++++++++++++++++++++++------- demo/bar/bar | Bin 6837618 -> 6837618 bytes internal/proc/container.go | 2 -- internal/proc/host.go | 13 +++++++-- internal/proc/key_lock.go | 13 +++++++++ internal/types/types.go | 33 +++++++++++++++++++++ kit.yaml | 7 +++++ up.go | 57 +++++++++++++++++++------------------ 8 files changed, 128 insertions(+), 42 deletions(-) create mode 100644 internal/proc/key_lock.go diff --git a/README.md b/README.md index d407874..62a3859 100644 --- a/README.md +++ b/README.md @@ -2,18 +2,20 @@ This is a tool to enable local development of containerized applications. -It allows you to specify a set of process that run in **containers** or on the **host**. The process are run concurrently and their status is **muxed into a single terminal** window (so you're not overwhelmed by output). It **probe** your processes to see if they've gone wrong, automatically restarting them. When you're done, **ctrl+c** to and they're all cleanly stopped. **Logs are captured** so you can look at them anytime. +It allows you to specify a set of process that run in **containers** or on the **host**. The process are run +concurrently and their status is **muxed into a single terminal** window (so you're not overwhelmed by output). It * +*probes** your processes to see if they've gone wrong, automatically restarting them. When your source code changes, +container and host process **auto-rebuild** and restart. When you're done, **ctrl+c** to +and they're all cleanly stopped. **Logs are captured** so you can look at them anytime. It's arguably the mutant offspring of other tools: -| tool | container processes | host processes | ctrl+c to stop | terminal mux | log capture | probes | -|---------------------|---------------------|----------------|----------------|--------------|-------------|--------| -| `kit` | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | -| `docker compose up` | ✔ | ✖ | ✖? | ✔ | ✔ | ✖ | -| `podman play kube` | ✔ | ✖ | ✖ | ✖ | ✔ | ✔? | -| `foreman run` | ✖ | ✔ | ✔ | ✔ | ✖ | ✖ | - -Volumes are not yet supported. +| tool | container processes | host processes | auto re-build | ctrl+c to stop | terminal mux | log capture | probes | +|---------------------|---------------------|----------------|---------------|----------------|--------------|-------------|--------| +| `kit` | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | +| `docker compose up` | ✔ | ✖ | ✖ | ✖? | ✔ | ✔ | ✖ | +| `podman play kube` | ✔ | ✖ | ✖ | ✖ | ✖ | ✔ | ✔? | +| `foreman run` | ✖ | ✔ | ✖ | ✔ | ✔ | ✖ | ✖ | Tilt, Skaffold, and Garden are in the same problem space. @@ -74,7 +76,7 @@ If `image` field is omitted, the value of `command` is used to start the process command: [ go, run, ./demo/foo ] ``` -If `image` is path to a directory containing a `Hostfile`. That file is run run on the host as the build step; +If `image` is path to a directory containing a `Hostfile`. That file is run run on the host as the build step; ```bash #!/bin/sh @@ -91,6 +93,29 @@ go build . command: [ ./demo/bar/bar ] ``` +### Auto Rebuild and Restart + +If anything in the build directory changes, then the process auto-rebuilds and restarts. + +It's often not convenient to keep your source code in the same directory as the build, but you could use a "toucher" to +touch the build directory whenever the source changes: + +```yaml + - command: + - bash + - -c + - | + set -eux + + while true; do + if [[ src/main -nt src/test/app ]]; then + touch src/test/app + fi + sleep 2 + done + name: toucher +``` + ### Init Containers Init containers are started before the main containers. They are allowed to run to completion before the main containers diff --git a/demo/bar/bar b/demo/bar/bar index 5aacd73b79b7aa5b9e7671fac1ea5e1da1816eb8..4865cde1bd4b713ef67a286d8e68453f513f3605 100755 GIT binary patch delta 775 zcmbWxO-Pdg0LF33Rx4*N%hI-LX*DZr+q;imQ@7{g&v85)dNLd<5 z*CIx@3Ph+&hhSa$AUhNibg{Yw30^vMu?U37{@yzU&+oZC@GL%hP_T$6T5U&APKc4A zzQK&$)7cv89}Z^xeWw0F`I68fXL_XMa6pO-dNW;;$$RenMR6dL9cheq4YiJ>6M<;A zNs3$u4>bo;q42O^bI8f2k%Tm%XeSlzDy&8U)DU*i+y=TYs_Y=L|IboqTLyf$yS>~Q5+V@Y>zo5 zQBvYon{2U|1#8UVwC9$t8g*(xs9A}1Qa@%y753u*4x$=|a2O_-VS#{^jSY4;)d&x~Xu>fx!-p2Mq78loa2!E| z(2fptB8&*4=t4JoZ~`ZB3cWauGmsI3f;bXL=H054aNST^G*fx!MZy?dxD{Tmd)Dc< zewn)bF6sTuL-}d!{}wzkSoDDW3m6Q~hC~w>DQY|Lx;@b@t`i-{YgM=>bjJ_ZNvi0b&3E delta 775 zcmbWxO-zyj0LJmk&rA#Z)S#lMun*t9g_0!0W>1L?!$(t+Srw+g8^gP>Bd~mO%h{rNnS00oBG-*owc*WT?BZi=#;C|ikDC_yR8P>u>zVl}F;2Gyv+TCBtJz834T0UNOi zo3RCTsK-{wp};n5#||j56T6_oZtTI{yeLzrQVm**S*wc~jh3h=#LQ8XL5f-gy*OYs znFe&CRWBH{k|Xlg`j&)a4XeH5#xF`*8pV(TGFPK#S$B9tId;f*ArV zu%Zcv(TpQFiWb;l&nvR6%JKg^I?d6x>FagQXa9&q&Q|7t6D}Ns8y>Ww9bR<62S1J@ zfKCL_g>LlV1VRX-7ZIGqDV)X`^x-V}5rv2tBn;$jWpVutW&hLrk%^_qT}8{nJ9i>G zwU`cke=#@oT2*mH)n0FSlD~61KI2zjYSo>uwdQ`xzczgS^vd|-YKQ&TimI89bB2XS yFPl|Mv%&XqK{+7>3I(z7Wwf;XkMh>TLTYldIQ91Vn|W8wqUPI&`Pp=JqU|q+wE}4X diff --git a/internal/proc/container.go b/internal/proc/container.go index b3f1dae..3ed1834 100644 --- a/internal/proc/container.go +++ b/internal/proc/container.go @@ -68,8 +68,6 @@ func (h *container) Build(ctx context.Context, stdout, stderr io.Writer) error { } func (h *container) Run(ctx context.Context, stdout, stderr io.Writer) error { - ctx, cancel := context.WithCancel(ctx) - defer cancel() if err := h.remove(ctx); err != nil { return err } diff --git a/internal/proc/host.go b/internal/proc/host.go index 3123643..8843ff9 100644 --- a/internal/proc/host.go +++ b/internal/proc/host.go @@ -26,8 +26,17 @@ func (h *host) Build(ctx context.Context, stdout, stderr io.Writer) error { ctx, cancel := context.WithCancel(ctx) defer cancel() if f, ok := imageIsHostfile(h.Image); ok { + + _, err := stdout.Write([]byte(fmt.Sprintf("waiting for mutex on %s to unblock...", h.Image))) + if err != nil { + return err + } + mutex := KeyLock(h.Image) + mutex.Lock() + defer mutex.Unlock() + cmd := exec.CommandContext(ctx, f) - cmd.Dir = h.WorkingDir + cmd.Dir = h.Image cmd.Stdin = os.Stdin cmd.Stdout = stdout cmd.Stderr = stderr @@ -97,7 +106,7 @@ var _ Interface = &host{} const hostfile = "Hostfile" func imageIsHostfile(image string) (string, bool) { - f := filepath.Join(image, hostfile) + f, _ := filepath.Abs(filepath.Join(image, hostfile)) _, err := os.Stat(f) return f, err == nil } diff --git a/internal/proc/key_lock.go b/internal/proc/key_lock.go new file mode 100644 index 0000000..107c4cd --- /dev/null +++ b/internal/proc/key_lock.go @@ -0,0 +1,13 @@ +package proc + +import "sync" + +var locks = &sync.Map{} + +// KeyLock return a mutex for the key. +// This func never frees un-locked mutexes. It is only suitable for use-cases with a small number of keys. +func KeyLock(key string) *sync.Mutex { + actual, _ := locks.LoadOrStore(key, &sync.Mutex{}) + mutex := actual.(*sync.Mutex) + return mutex +} diff --git a/internal/types/types.go b/internal/types/types.go index 90ab361..4955f69 100644 --- a/internal/types/types.go +++ b/internal/types/types.go @@ -87,10 +87,31 @@ func (p Probe) GetSuccessThreshold() int { return int(p.SuccessThreshold) } +func (p *Probe) DeepCopy() *Probe { + if p == nil { + return nil + } + return &Probe{ + InitialDelaySeconds: p.InitialDelaySeconds, + PeriodSeconds: p.PeriodSeconds, + TCPSocket: p.TCPSocket.DeepCopy(), + HTTPGet: p.HTTPGet.DeepCopy(), + SuccessThreshold: p.SuccessThreshold, + FailureThreshold: p.FailureThreshold, + } +} + type TCPSocketAction struct { Port intstr.IntOrString `json:"port"` } +func (a *TCPSocketAction) DeepCopy() *TCPSocketAction { + if a == nil { + return nil + } + return &TCPSocketAction{Port: a.Port} +} + type HTTPGetAction struct { Scheme string `json:"scheme,omitempty"` Port *intstr.IntOrString `json:"port,omitempty"` @@ -115,6 +136,18 @@ func (a HTTPGetAction) GetPort() int32 { return a.Port.IntVal } +func (a *HTTPGetAction) DeepCopy() *HTTPGetAction { + if a == nil { + return nil + } + return &HTTPGetAction{ + Scheme: a.Scheme, + Port: a.Port, + Path: a.Path, + } + +} + type VolumeMount struct { Name string `json:"name"` MountPath string `json:"mountPath"` diff --git a/kit.yaml b/kit.yaml index e834bdf..f6ddb8b 100644 --- a/kit.yaml +++ b/kit.yaml @@ -41,6 +41,13 @@ spec: volumeMounts: - mountPath: /work name: work + - command: + - sh + - -c + - | + set -eux + sleep 10 + name: kef initContainers: - command: - sleep diff --git a/up.go b/up.go index 908e4df..0f53215 100644 --- a/up.go +++ b/up.go @@ -75,7 +75,7 @@ func up() *cobra.Command { icon = color.RedString("▓") } } - line := fmt.Sprintf("%s %-10s [%-7s] %s", icon, state.Name, reason, logEntries[c.Name].String()) + line := fmt.Sprintf("%s %-10s [%-7s] %s", icon, k8sstrings.ShortenString(state.Name, 10), reason, logEntries[c.Name].String()) log.Println(k8sstrings.ShortenString(line, width)) } time.Sleep(time.Second / 2) @@ -140,7 +140,7 @@ func up() *cobra.Command { processCtx, stopProcess := context.WithCancel(ctx) wg.Add(1) - go func(name, image string) { + go func(name, image string, livenessProbe, readinessProbe *types.Probe) { defer handleCrash(stopEverything) defer wg.Done() for { @@ -181,6 +181,31 @@ func up() *cobra.Command { } } }() + if probe := livenessProbe; probe != nil { + liveFunc := func(name string, live bool, err error) { + if !live { + stopRun() + } + } + go probeLoop(runCtx, stopEverything, name, *probe, liveFunc) + } + if probe := readinessProbe; probe != nil { + readyFunc := func(name string, ready bool, err error) { + state.Ready = ready + if err != nil { + logEntry.Level = "error" + logEntry.Msg = err.Error() + + } + } + go probeLoop(ctx, stopEverything, name, *probe, readyFunc) + } + + go func() { + defer handleCrash(stopEverything) + <-ctx.Done() + stopProcess() + }() if err := prc.Run(runCtx, stdout, stderr); err != nil { return fmt.Errorf("failed to run: %v", err) } @@ -205,33 +230,9 @@ func up() *cobra.Command { } } } - }(c.Name, c.Image) + }(c.Name, c.Image, c.LivenessProbe.DeepCopy(), c.ReadinessProbe) - go func() { - defer handleCrash(stopEverything) - <-ctx.Done() - stopProcess() - }() - - if probe := c.LivenessProbe; probe != nil { - liveFunc := func(name string, live bool, err error) { - if !live { - stopProcess() - } - } - go probeLoop(ctx, stopEverything, c.Name, *probe, liveFunc) - } - if probe := c.ReadinessProbe; probe != nil { - readyFunc := func(name string, ready bool, err error) { - state.Ready = ready - if err != nil { - logEntry.Level = "error" - logEntry.Msg = err.Error() - - } - } - go probeLoop(ctx, stopEverything, c.Name, *probe, readyFunc) - } + time.Sleep(time.Second / 4) } wg.Wait()