Skip to content

Commit

Permalink
feat: own types
Browse files Browse the repository at this point in the history
  • Loading branch information
alexec committed Dec 22, 2022
1 parent 3355463 commit 9691a23
Show file tree
Hide file tree
Showing 6 changed files with 165 additions and 53 deletions.
16 changes: 12 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
# Joy

This is a tool to enable local development of containerized applications. It uses conventional Kubernetes Pod YAML, but
allows
you to run the process on your host (like `Procfile`), in Docker using just-in-time build (like Docker Compose).
This is a tool to enable local development of containerized applications.

Only high-level status is shown to your terminal:
It allows you to specify a set of process (in containers or on the host) that are run concurrently in a single terminal
window.
Those processes are managed together as a group. They are all stopped when you're done.

It automatically build containers when needed. It allows you to run init processes before your main processes. It uses
familiar pod syntax to do this.

Rather than printing logs to the terminal (so you're overwhelmed), logs are saved in files so you can open them in
your IDE.

You can define probes that test each container and host process is up and running.

```
▓ foo [dead ] 2022/12/21 17:48:19 listening on 8080
Expand Down
25 changes: 12 additions & 13 deletions internal/proc/containerProc.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@ package proc
import (
"context"
"fmt"
"github.com/alexec/joy/internal/types"
"io"
"os"
"path/filepath"
"time"

"github.com/docker/docker/api/types"
dockertypes "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/client"
Expand All @@ -19,9 +20,10 @@ import (
)

type ContainerProc struct {
corev1.Container
types.Container
cli *client.Client
image string
TTY bool
}

func (h *ContainerProc) Init(ctx context.Context) error {
Expand All @@ -42,7 +44,7 @@ func (h *ContainerProc) Build(ctx context.Context, stdout, stderr io.Writer) err
return err
}
defer r.Close()
resp, err := cli.ImageBuild(ctx, r, types.ImageBuildOptions{Dockerfile: filepath.Base(image), Tags: []string{h.Name}})
resp, err := cli.ImageBuild(ctx, r, dockertypes.ImageBuildOptions{Dockerfile: filepath.Base(image), Tags: []string{h.Name}})
if err != nil {
return err
}
Expand All @@ -52,8 +54,8 @@ func (h *ContainerProc) Build(ctx context.Context, stdout, stderr io.Writer) err
return err
}
h.image = h.Name
} else if h.ImagePullPolicy != corev1.PullNever {
r, err := cli.ImagePull(ctx, h.Image, types.ImagePullOptions{})
} else if h.ImagePullPolicy != string(corev1.PullNever) {
r, err := cli.ImagePull(ctx, h.Image, dockertypes.ImagePullOptions{})
if err != nil {
return err
}
Expand All @@ -80,10 +82,7 @@ func (h *ContainerProc) Run(ctx context.Context, stdout, stderr io.Writer) error
return err
}
portSet[port] = struct{}{}
hostPort := p.HostPort
if hostPort == 0 {
hostPort = p.ContainerPort
}
hostPort := p.GetHostPort()
portBindings[port] = []nat.PortBinding{{
HostPort: fmt.Sprint(hostPort),
}}
Expand Down Expand Up @@ -112,12 +111,12 @@ func (h *ContainerProc) Run(ctx context.Context, stdout, stderr io.Writer) error
return err
}

err = cli.ContainerStart(ctx, created.ID, types.ContainerStartOptions{})
err = cli.ContainerStart(ctx, created.ID, dockertypes.ContainerStartOptions{})
if err != nil {
return err
}

logs, err := cli.ContainerLogs(ctx, h.Name, types.ContainerLogsOptions{
logs, err := cli.ContainerLogs(ctx, h.Name, dockertypes.ContainerLogsOptions{
ShowStdout: true,
ShowStderr: true,
Follow: true,
Expand All @@ -132,13 +131,13 @@ func (h *ContainerProc) Run(ctx context.Context, stdout, stderr io.Writer) error

func (h *ContainerProc) Stop(ctx context.Context, grace time.Duration) error {
cli := h.cli
list, err := cli.ContainerList(ctx, types.ContainerListOptions{All: true})
list, err := cli.ContainerList(ctx, dockertypes.ContainerListOptions{All: true})
if err != nil {
return err
}
for _, existing := range list {
if existing.Labels["name"] == h.Name {
err = cli.ContainerRemove(ctx, existing.ID, types.ContainerRemoveOptions{Force: true})
err = cli.ContainerRemove(ctx, existing.ID, dockertypes.ContainerRemoveOptions{Force: true})
if err != nil {
return err
}
Expand Down
5 changes: 2 additions & 3 deletions internal/proc/hostProc.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,16 @@ package proc
import (
"context"
"fmt"
"github.com/alexec/joy/internal/types"
"io"
"os"
"os/exec"
"syscall"
"time"

corev1 "k8s.io/api/core/v1"
)

type HostProc struct {
corev1.Container
types.Container
process *os.Process
}

Expand Down
124 changes: 124 additions & 0 deletions internal/types/joy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package types

import (
"fmt"
"k8s.io/apimachinery/pkg/util/intstr"
"strings"
"time"
)

type Metadata struct {
Name string `json:"name"`
}

type Joy struct {
Spec Spec `json:"spec"`
ApiVersion string `json:"apiVersion,omitempty"`
Kind string `json:"kind,omitempty"`
Metadata *Metadata `json:"metadata,omitempty"`
}

type Spec struct {
TerminationGracePeriodSeconds *int32 `json:"terminationGracePeriodSeconds,omitempty"`
InitContainers []Container `json:"initContainers,omitempty"`
Containers []Container `json:"containers,omitempty"`
}

func (s Spec) GetTerminationGracePeriod() time.Duration {
if s.TerminationGracePeriodSeconds != nil {
return time.Duration(*s.TerminationGracePeriodSeconds) * time.Second
}
return 30 * time.Second
}

type TCPSocketAction struct {
Port intstr.IntOrString `json:"port"`
}

type HTTPGetAction struct {
Scheme string `json:"scheme,omitempty"`
Port *intstr.IntOrString `json:"port,omitempty"`
Path string `json:"path,omitempty"`
}

func (a HTTPGetAction) GetProto() string {
if a.Scheme == "" {
return "http"
}
return strings.ToLower(a.Scheme)
}

func (a HTTPGetAction) GetURL() string {
return fmt.Sprintf("%s://localhost:%v%s", a.GetProto(), a.GetPort(), a.Path)
}

func (a HTTPGetAction) GetPort() int32 {
if a.Port == nil {
return 80
}
return a.Port.IntVal
}

type Probe struct {
InitialDelaySeconds int32 `json:"initialDelaySeconds,omitempty"`
PeriodSeconds int32 `json:"periodSeconds,omitempty"`
TCPSocket *TCPSocketAction `json:"tcpSocket,omitempty"`
HTTPGet *HTTPGetAction `json:"httpGet,omitempty"`
SuccessThreshold int32 `json:"successThreshold,omitempty"`
FailureThreshold int32 `json:"failureThreshold,omitempty"`
}

func (p Probe) GetInitialDelay() time.Duration {
return time.Duration(p.InitialDelaySeconds) * time.Second
}

func (p Probe) GetPeriod() time.Duration {
if p.PeriodSeconds == 0 {
return 10 * time.Second
}
return time.Duration(p.PeriodSeconds) * time.Second
}

func (p Probe) GetFailureThreshold() int {
if p.FailureThreshold == 0 {
return 1
}
return int(p.FailureThreshold)
}

func (p Probe) GetSuccessThreshold() int {
if p.SuccessThreshold == 0 {
return 1
}
return int(p.SuccessThreshold)
}

type EnvVar struct {
Name string `json:"name"`
Value string `json:"value"`
}

type ContainerPort struct {
ContainerPort int `json:"containerPort,omitempty"`
HostPort int `json:"hostPort"`
}

func (p ContainerPort) GetHostPort() int {
if p.HostPort == 0 {
return p.ContainerPort
}
return p.HostPort
}

type Container struct {
Name string `json:"name"`
Image string `json:"image,omitempty"`
ImagePullPolicy string `json:"imagePullPolicy,omitempty"`
LivenessProbe *Probe `json:"livenessProbe,omitempty"`
ReadinessProbe *Probe `json:"readinessProbe,omitempty"`
Command []string `json:"command,omitempty"`
Args []string `json:"args,omitempty"`
WorkingDir string `json:"workingDir,omitempty"`
Env []EnvVar `json:"env,omitempty"`
Ports []ContainerPort `json:"ports,omitempty"`
}
20 changes: 8 additions & 12 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import (

"github.com/fatih/color"
"golang.org/x/crypto/ssh/terminal"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/util/runtime"
"sigs.k8s.io/yaml"
)
Expand All @@ -46,7 +45,7 @@ func main() {

in, err := os.ReadFile("joy.yaml")
must(err)
pod := &corev1.Pod{}
pod := &types.Joy{}
must(yaml.UnmarshalStrict(in, pod))

var names []string
Expand Down Expand Up @@ -80,12 +79,9 @@ func main() {

_ = os.Mkdir("logs", 0777)

terminationGracePeriod := 30 * time.Second
if pod.Spec.TerminationGracePeriodSeconds != nil {
terminationGracePeriod = time.Duration(*pod.Spec.TerminationGracePeriodSeconds) * time.Second
}
terminationGracePeriod := pod.Spec.GetTerminationGracePeriod()

for _, containers := range [][]corev1.Container{pod.Spec.InitContainers, pod.Spec.Containers} {
for _, containers := range [][]types.Container{pod.Spec.InitContainers, pod.Spec.Containers} {
wg := &sync.WaitGroup{}

states = map[string]*types.State{}
Expand Down Expand Up @@ -124,9 +120,9 @@ func main() {
stdout := io.MultiWriter(logFile, states[c.Name].Stdout())
stderr := io.MultiWriter(logFile, states[c.Name].Stderr())
if c.Image == "" {
pd = &proc.HostProc{Container: *c.DeepCopy()}
pd = &proc.HostProc{Container: c}
} else {
pd = &proc.ContainerProc{Container: *c.DeepCopy()}
pd = &proc.ContainerProc{Container: c}
}

err = pd.Init(ctx)
Expand Down Expand Up @@ -178,7 +174,7 @@ func main() {
}
}()

if p := c.LivenessProbe; p != nil {
if probe := c.LivenessProbe; probe != nil {
liveFunc := func(name string, live bool, err error) {
if live {
states[name].Phase = types.LivePhase
Expand All @@ -194,7 +190,7 @@ func main() {
}
}
}
go probeLoop(ctx, name, *p.DeepCopy(), liveFunc)
go probeLoop(ctx, name, *probe, liveFunc)
}
if probe := c.ReadinessProbe; probe != nil {
readyFunc := func(name string, ready bool, err error) {
Expand All @@ -207,7 +203,7 @@ func main() {
state.Log = types.LogEntry{Level: "error", Msg: err.Error()}
}
}
go probeLoop(ctx, name, *probe.DeepCopy(), readyFunc)
go probeLoop(ctx, name, *probe, readyFunc)
}
time.Sleep(time.Second)
}
Expand Down
28 changes: 7 additions & 21 deletions probe.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,18 @@ package main
import (
"context"
"fmt"
"github.com/alexec/joy/internal/types"
"net"
"net/http"
"strings"
"time"

corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/util/runtime"
)

func probeLoop(ctx context.Context, name string, probe corev1.Probe, callback func(name string, ok bool, err error)) {
func probeLoop(ctx context.Context, name string, probe types.Probe, callback func(name string, ok bool, err error)) {
defer runtime.HandleCrash()
initialDelay := time.Duration(probe.InitialDelaySeconds) * time.Second
period := time.Duration(probe.PeriodSeconds) * time.Second
if period == 0 {
period = 10 * time.Second
}
initialDelay := probe.GetInitialDelay()
period := probe.GetPeriod()
time.Sleep(initialDelay)
successes, failures := 0, 0
for {
Expand All @@ -30,11 +26,7 @@ func probeLoop(ctx context.Context, name string, probe corev1.Probe, callback fu
_, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", tcp.Port.IntVal))
callback(name, err == nil, err)
} else if httpGet := probe.HTTPGet; httpGet != nil {
proto := strings.ToLower(string(httpGet.Scheme))
if proto == "" {
proto = "http"
}
resp, err := http.Get(fmt.Sprintf("%s://localhost:%v%s", proto, httpGet.Port.IntValue(), httpGet.Path))
resp, err := http.Get(httpGet.GetURL())
ok := err == nil && resp.StatusCode < 300
if ok {
successes++
Expand All @@ -43,14 +35,8 @@ func probeLoop(ctx context.Context, name string, probe corev1.Probe, callback fu
successes = 0
failures++
}
successThreshold := int(probe.SuccessThreshold)
if successThreshold == 0 {
successThreshold = 1
}
failureThreshold := int(probe.FailureThreshold)
if failureThreshold == 0 {
failureThreshold = 1
}
successThreshold := probe.GetSuccessThreshold()
failureThreshold := probe.GetFailureThreshold()
if successes == successThreshold {
callback(name, ok, nil)
successes = 0
Expand Down

0 comments on commit 9691a23

Please sign in to comment.