Skip to content

Commit

Permalink
dockercompose: add healthcheck to tilt ui (#6419)
Browse files Browse the repository at this point in the history
fixes #6413
fixes #6037

Signed-off-by: Nick Santos <nick.santos@docker.com>
  • Loading branch information
nicks authored Aug 1, 2024
1 parent c0e12e3 commit 80d4134
Show file tree
Hide file tree
Showing 15 changed files with 1,123 additions and 650 deletions.
19 changes: 14 additions & 5 deletions internal/controllers/core/dockercomposeservice/project.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ package dockercomposeservice
import (
"context"

"github.com/docker/docker/api/types"

"github.com/tilt-dev/tilt/internal/controllers/apicmp"
"github.com/tilt-dev/tilt/internal/dockercompose"
"github.com/tilt-dev/tilt/pkg/apis/core/v1alpha1"
"github.com/tilt-dev/tilt/pkg/logger"
)

Expand Down Expand Up @@ -92,9 +93,7 @@ func (r *Reconciler) runProjectWatch(pw *ProjectWatch) {
continue
}

cState := containerJSON.ContainerJSONBase.State
dcState := dockercompose.ToContainerState(cState)
r.recordContainerEvent(evt, dcState)
r.recordContainerEvent(ctx, evt, containerJSON)

case <-ctx.Done():
return
Expand All @@ -103,10 +102,20 @@ func (r *Reconciler) runProjectWatch(pw *ProjectWatch) {
}

// Record the container event and re-reconcile the dockercompose service.
func (r *Reconciler) recordContainerEvent(evt dockercompose.Event, state *v1alpha1.DockerContainerState) {
func (r *Reconciler) recordContainerEvent(ctx context.Context, evt dockercompose.Event, containerJSON types.ContainerJSON) {
cState := containerJSON.ContainerJSONBase.State
state := dockercompose.ToContainerState(cState)
healthcheckOutput := dockercompose.ToHealthcheckOutput(cState)

r.mu.Lock()
defer r.mu.Unlock()

oldOutput := r.healthcheckOutputByServiceName[evt.Service]
r.healthcheckOutputByServiceName[evt.Service] = healthcheckOutput
if healthcheckOutput != "" && oldOutput != healthcheckOutput {
logger.Get(ctx).Warnf("healthcheck: %s", healthcheckOutput)
}

result, ok := r.resultsByServiceName[evt.Service]
if !ok {
return
Expand Down
28 changes: 15 additions & 13 deletions internal/controllers/core/dockercomposeservice/reconciler.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,10 @@ type Reconciler struct {
mu sync.Mutex

// Protected by the mutex.
results map[types.NamespacedName]*Result
resultsByServiceName map[string]*Result
projectWatches map[string]*ProjectWatch
results map[types.NamespacedName]*Result
resultsByServiceName map[string]*Result
healthcheckOutputByServiceName map[string]string
projectWatches map[string]*ProjectWatch
}

func (r *Reconciler) CreateBuilder(mgr ctrl.Manager) (*builder.Builder, error) {
Expand All @@ -71,16 +72,17 @@ func NewReconciler(
disableQueue *DisableSubscriber,
) *Reconciler {
return &Reconciler{
ctrlClient: ctrlClient,
dcc: dcc,
dc: dc.ForOrchestrator(model.OrchestratorDC),
indexer: indexer.NewIndexer(scheme, indexDockerComposeService),
st: st,
requeuer: indexer.NewRequeuer(),
disableQueue: disableQueue,
results: make(map[types.NamespacedName]*Result),
resultsByServiceName: make(map[string]*Result),
projectWatches: make(map[string]*ProjectWatch),
ctrlClient: ctrlClient,
dcc: dcc,
dc: dc.ForOrchestrator(model.OrchestratorDC),
indexer: indexer.NewIndexer(scheme, indexDockerComposeService),
st: st,
requeuer: indexer.NewRequeuer(),
disableQueue: disableQueue,
results: make(map[types.NamespacedName]*Result),
resultsByServiceName: make(map[string]*Result),
healthcheckOutputByServiceName: make(map[string]string),
projectWatches: make(map[string]*ProjectWatch),
}
}

Expand Down
70 changes: 70 additions & 0 deletions internal/controllers/core/dockercomposeservice/reconciler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,76 @@ func TestContainerEvent(t *testing.T) {
s.ManifestTargets["fe"].State.DCRuntimeState().ContainerState.Status)
}

func TestContainerUnhealthy(t *testing.T) {
f := newFixture(t)
nn := types.NamespacedName{Name: "fe"}
obj := v1alpha1.DockerComposeService{
ObjectMeta: metav1.ObjectMeta{
Name: "fe",
Annotations: map[string]string{
v1alpha1.AnnotationManifest: "fe",
},
},
Spec: v1alpha1.DockerComposeServiceSpec{
Service: "fe",
Project: v1alpha1.DockerComposeProject{
YAML: "fake-yaml",
},
},
}
f.Create(&obj)

status := f.r.ForceApply(f.Context(), nn, obj.Spec, nil, false)
assert.Equal(t, "", status.ApplyError)
assert.Equal(t, true, status.ContainerState.Running)

container := dtypes.ContainerState{
Status: "running",
Running: true,
ExitCode: 0,
StartedAt: "2021-09-08T19:58:01.483005100Z",
Health: &dtypes.Health{
Status: dtypes.Unhealthy,
Log: []*dtypes.HealthcheckResult{
{
Output: "healthcheck failed",
},
},
},
}
containerID := "my-container-id"
f.dc.Containers[containerID] = container

event := dockercompose.Event{Type: dockercompose.TypeContainer, ID: containerID, Service: "fe"}
f.dcc.SendEvent(event)

require.Eventually(t, func() bool {
f.MustReconcile(nn)
f.MustGet(nn, &obj)
return obj.Status.ContainerState.HealthStatus == dtypes.Unhealthy
}, time.Second, 10*time.Millisecond, "container unhealthy")

assert.Equal(t, containerID, obj.Status.ContainerID)

f.MustReconcile(nn)
tmpf := tempdir.NewTempDirFixture(t)
s := store.NewState()
m := manifestbuilder.New(tmpf, "fe").WithDockerCompose().Build()
s.UpsertManifestTarget(store.NewManifestTarget(m))

for _, action := range f.Store.Actions() {
switch action := action.(type) {
case dockercomposeservices.DockerComposeServiceUpsertAction:
dockercomposeservices.HandleDockerComposeServiceUpsertAction(s, action)
}
}

assert.Equal(t, dtypes.Unhealthy,
s.ManifestTargets["fe"].State.DCRuntimeState().ContainerState.HealthStatus)

assert.Contains(t, f.Stdout(), "healthcheck failed")
}

func TestForceDelete(t *testing.T) {
f := newFixture(t)
nn := types.NamespacedName{Name: "fe"}
Expand Down
35 changes: 29 additions & 6 deletions internal/dockercompose/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,14 +122,37 @@ func ToContainerState(state *types.ContainerState) *v1alpha1.DockerContainerStat
}
}

health := state.Health
healthStatus := ""
if health != nil {
healthStatus = health.Status
}

return &v1alpha1.DockerContainerState{
Status: state.Status,
Running: state.Running,
Error: state.Error,
ExitCode: int32(state.ExitCode),
StartedAt: metav1.NewMicroTime(startedAt),
FinishedAt: metav1.NewMicroTime(finishedAt),
Status: state.Status,
Running: state.Running,
Error: state.Error,
ExitCode: int32(state.ExitCode),
StartedAt: metav1.NewMicroTime(startedAt),
FinishedAt: metav1.NewMicroTime(finishedAt),
HealthStatus: healthStatus,
}
}

// Returns the output of a healthcheck if the container is unhealthy.
func ToHealthcheckOutput(state *types.ContainerState) string {
health := state.Health
healthStatus := ""
if health == nil {
return ""
}

healthStatus = health.Status
if healthStatus != types.Unhealthy || len(health.Log) == 0 {
return ""
}

return health.Log[len(health.Log)-1].Output
}

// Convert a full into an apiserver-compatible status model.
Expand Down
5 changes: 5 additions & 0 deletions internal/hud/webview/convert.go
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,11 @@ func populateResourceInfoView(mt *store.ManifestTarget, r *v1alpha1.UIResource)
lState := mt.State.LocalRuntimeState()
r.Status.LocalResourceInfo = &v1alpha1.UIResourceLocal{PID: int64(lState.PID)}
}
if mt.Manifest.IsDC() {
r.Status.ComposeResourceInfo = &v1alpha1.UIResourceCompose{
HealthStatus: mt.State.DCRuntimeState().ContainerState.HealthStatus,
}
}
if mt.Manifest.IsK8s() {
kState := mt.State.K8sRuntimeState()
pod := kState.MostRecentPod()
Expand Down
3 changes: 3 additions & 0 deletions pkg/apis/core/v1alpha1/dockercomposeservice_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,9 @@ type DockerContainerState struct {
// When the container process finished.
// +optional
FinishedAt metav1.MicroTime `json:"finishedAt,omitempty" protobuf:"bytes,6,opt,name=finishedAt"`

// Status is one of Starting, Healthy or Unhealthy
HealthStatus string `json:"healthStatus,omitempty" protobuf:"bytes,7,opt,name=healthStatus"`
}

// How docker binds container ports to the host network
Expand Down
Loading

0 comments on commit 80d4134

Please sign in to comment.