Skip to content

Commit

Permalink
feat: auto-rebuild
Browse files Browse the repository at this point in the history
  • Loading branch information
alexec committed Dec 29, 2022
1 parent 7a9c575 commit 0a88aa5
Show file tree
Hide file tree
Showing 8 changed files with 128 additions and 42 deletions.
45 changes: 35 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
Binary file modified demo/bar/bar
Binary file not shown.
2 changes: 0 additions & 2 deletions internal/proc/container.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
13 changes: 11 additions & 2 deletions internal/proc/host.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}
13 changes: 13 additions & 0 deletions internal/proc/key_lock.go
Original file line number Diff line number Diff line change
@@ -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
}
33 changes: 33 additions & 0 deletions internal/types/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand All @@ -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"`
Expand Down
7 changes: 7 additions & 0 deletions kit.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,13 @@ spec:
volumeMounts:
- mountPath: /work
name: work
- command:
- sh
- -c
- |
set -eux
sleep 10
name: kef
initContainers:
- command:
- sleep
Expand Down
57 changes: 29 additions & 28 deletions up.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
}
Expand All @@ -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()
Expand Down

0 comments on commit 0a88aa5

Please sign in to comment.