Skip to content
This repository has been archived by the owner on Jan 8, 2024. It is now read-only.

plugin/k8s: Add StatusFunc to Deploy and Release #1547

Merged
merged 9 commits into from
May 27, 2021
74 changes: 74 additions & 0 deletions builtin/k8s/platform.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"strings"
"time"

"github.com/golang/protobuf/ptypes"
"github.com/hashicorp/go-hclog"
"github.com/mitchellh/mapstructure"
corev1 "k8s.io/api/core/v1"
Expand All @@ -20,8 +21,10 @@ import (
"github.com/hashicorp/waypoint-plugin-sdk/component"
"github.com/hashicorp/waypoint-plugin-sdk/docs"
"github.com/hashicorp/waypoint-plugin-sdk/framework/resource"
sdk "github.com/hashicorp/waypoint-plugin-sdk/proto/gen"
"github.com/hashicorp/waypoint-plugin-sdk/terminal"
"github.com/hashicorp/waypoint/builtin/docker"
"github.com/hashicorp/waypoint/internal/clierrors"
)

const (
Expand Down Expand Up @@ -70,6 +73,10 @@ func (p *Platform) ValidateAuth() error {
return nil
}

func (p *Platform) StatusFunc() interface{} {
return p.Status
}

// DefaultReleaserFunc implements component.PlatformReleaser
func (p *Platform) DefaultReleaserFunc() interface{} {
var rc ReleaserConfig
Expand Down Expand Up @@ -641,6 +648,72 @@ func (p *Platform) Destroy(
return rm.DestroyAll(ctx, log, sg, ui)
}

func (p *Platform) Status(
ctx context.Context,
log hclog.Logger,
deployment *Deployment,
ui terminal.UI,
) (*sdk.StatusReport, error) {
sg := ui.StepGroup()
defer sg.Wait()

step := sg.Add("Gathering health report for Kubernetes platform...")
defer step.Abort()
briancain marked this conversation as resolved.
Show resolved Hide resolved

csInfo, err := p.getClientset()
if err != nil {
return nil, err
}
clientSet := csInfo.Clientset
namespace := csInfo.Namespace
if p.config.Namespace != "" {
namespace = p.config.Namespace
}

podClient := clientSet.CoreV1().Pods(namespace)
podLabelId := fmt.Sprintf("app=%s", deployment.Name)
podList, err := podClient.List(ctx, metav1.ListOptions{LabelSelector: podLabelId})
if err != nil {
ui.Output(
"Error listing pods to determine application health: %s", clierrors.Humanize(err),
terminal.WithErrorStyle(),
)
return nil, err
}

// Create our status report
step.Update("Building status report for running pods...")
result := buildStatusReport(podList)

result.TimeGenerated = ptypes.TimestampNow()
log.Debug("status report complete")

// update output based on main health state
step.Update("Finished building report for Kubernetes platform")
step.Done()

st := ui.Status()
defer st.Close()

st.Update("Determining overall container health...")
if result.Health == sdk.StatusReport_READY {
st.Step(terminal.StatusOK, fmt.Sprintf("Deployment %q is reporting ready!", deployment.Name))
} else if result.Health == sdk.StatusReport_PARTIAL {
st.Step(terminal.StatusWarn, fmt.Sprintf("Deployment %q is reporting partially available!", deployment.Name))
} else {
st.Step(terminal.StatusError, fmt.Sprintf("Deployment %q is reporting not ready!", deployment.Name))
}

// More UI detail for non-ready resources
for _, resource := range result.Resources {
if resource.Health != sdk.StatusReport_READY {
st.Step(terminal.StatusWarn, fmt.Sprintf("Resource %q is reporting %q", resource.Name, resource.Health.String()))
}
}

return &result, nil
}

// Config is the configuration structure for the Platform.
type Config struct {
// Annotations are added to the pod spec of the deployed application. This is
Expand Down Expand Up @@ -961,4 +1034,5 @@ var (
_ component.Configurable = (*Platform)(nil)
_ component.Documented = (*Platform)(nil)
_ component.Destroyer = (*Platform)(nil)
_ component.Status = (*Platform)(nil)
)
4 changes: 3 additions & 1 deletion builtin/k8s/release.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,6 @@ func (*Release) newService(name string) *corev1.Service {

func (r *Release) URL() string { return r.Url }

var _ component.Release = (*Release)(nil)
var (
_ component.Release = (*Release)(nil)
)
81 changes: 81 additions & 0 deletions builtin/k8s/releaser.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ import (
"strconv"
"time"

"github.com/golang/protobuf/ptypes"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/waypoint/internal/clierrors"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
Expand All @@ -15,6 +17,7 @@ import (

"github.com/hashicorp/waypoint-plugin-sdk/component"
"github.com/hashicorp/waypoint-plugin-sdk/docs"
sdk "github.com/hashicorp/waypoint-plugin-sdk/proto/gen"
"github.com/hashicorp/waypoint-plugin-sdk/terminal"
)

Expand All @@ -41,6 +44,11 @@ func (r *Releaser) DestroyFunc() interface{} {
return r.Destroy
}

// StatusFunc implements component.Status
func (r *Releaser) StatusFunc() interface{} {
return r.Status
}

// Release creates a Kubernetes service configured for the deployment
func (r *Releaser) Release(
ctx context.Context,
Expand Down Expand Up @@ -268,6 +276,78 @@ func (r *Releaser) Destroy(
return nil
}

func (r *Releaser) Status(
ctx context.Context,
log hclog.Logger,
release *Release,
ui terminal.UI,
) (*sdk.StatusReport, error) {
sg := ui.StepGroup()
defer sg.Wait()

step := sg.Add("Gathering health report for Kubernetes platform...")
defer step.Abort()
briancain marked this conversation as resolved.
Show resolved Hide resolved

// Get our client
clientset, namespace, _, err := clientset(r.config.KubeconfigPath, r.config.Context)
if err != nil {
return nil, err
}
if r.config.Namespace != "" {
namespace = r.config.Namespace
}

serviceclient := clientset.CoreV1().Services(namespace)
service, err := serviceclient.Get(ctx, release.ServiceName, metav1.GetOptions{})
if err != nil {
return nil, err
}

appName := service.Spec.Selector["name"]
podClient := clientset.CoreV1().Pods(namespace)
podLabelId := fmt.Sprintf("app=%s", appName)
podList, err := podClient.List(ctx, metav1.ListOptions{LabelSelector: podLabelId})
if err != nil {
ui.Output(
"Error listing pods to determine application health: %s", clierrors.Humanize(err),
terminal.WithErrorStyle(),
)
return nil, err
}

// Create our status report
step.Update("Building status report for running pods...")
result := buildStatusReport(podList)

result.TimeGenerated = ptypes.TimestampNow()
log.Debug("status report complete")

// update output based on main health state
step.Update("Finished building report for Kubernetes platform")
step.Done()

st := ui.Status()
defer st.Close()

st.Update("Determining overall container health...")
if result.Health == sdk.StatusReport_READY {
st.Step(terminal.StatusOK, fmt.Sprintf("Release %q is reporting ready!", appName))
} else if result.Health == sdk.StatusReport_PARTIAL {
st.Step(terminal.StatusWarn, fmt.Sprintf("Release %q is reporting partially available!", appName))
} else {
st.Step(terminal.StatusError, fmt.Sprintf("Release %q is reporting not ready!", appName))
}

// More UI detail for non-ready resources
for _, resource := range result.Resources {
if resource.Health != sdk.StatusReport_READY {
st.Step(terminal.StatusWarn, fmt.Sprintf("Resource %q is reporting %q", resource.Name, resource.Health.String()))
}
}

return &result, nil
}

// ReleaserConfig is the configuration structure for the Releaser.
type ReleaserConfig struct {
// Annotations to be applied to the kube service.
Expand Down Expand Up @@ -377,4 +457,5 @@ var (
_ component.Destroyer = (*Releaser)(nil)
_ component.Configurable = (*Releaser)(nil)
_ component.Documented = (*Releaser)(nil)
_ component.Status = (*Releaser)(nil)
)
81 changes: 81 additions & 0 deletions builtin/k8s/status_report_util.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package k8s

import (
corev1 "k8s.io/api/core/v1"

sdk "github.com/hashicorp/waypoint-plugin-sdk/proto/gen"
)

// Translate a K8S container status into a Waypoint Health Status
func containerStatusToHealth(
containerStatus corev1.ContainerStatus,
) *sdk.StatusReport_Resource {
var resourceHealth sdk.StatusReport_Resource
resourceHealth.Name = containerStatus.Name

// ContainerStatus.State is a struct of possible container states. If one
// is set, that is the current state of the container
if containerStatus.State.Running != nil || containerStatus.Ready == true {
resourceHealth.Health = sdk.StatusReport_READY
resourceHealth.HealthMessage = "container is reporting running" // no message defined by k8s api
} else if containerStatus.State.Waiting != nil {
resourceHealth.Health = sdk.StatusReport_PARTIAL
resourceHealth.HealthMessage = containerStatus.State.Waiting.Message
} else if containerStatus.State.Terminated != nil {
resourceHealth.Health = sdk.StatusReport_DOWN
resourceHealth.HealthMessage = containerStatus.State.Terminated.Message
} else {
resourceHealth.Health = sdk.StatusReport_UNKNOWN
resourceHealth.HealthMessage = "container health could not be determined"
}

return &resourceHealth
}

// Translate a Pod Phase into a Waypoint Health Status
func podPhaseToHealth(
phase corev1.PodPhase,
) sdk.StatusReport_Health {
var healthResult sdk.StatusReport_Health

switch phase {
briancain marked this conversation as resolved.
Show resolved Hide resolved
case corev1.PodPending:
healthResult = sdk.StatusReport_ALIVE
case corev1.PodRunning:
healthResult = sdk.StatusReport_READY
case corev1.PodSucceeded:
healthResult = sdk.StatusReport_PARTIAL
case corev1.PodFailed:
healthResult = sdk.StatusReport_DOWN
case corev1.PodUnknown:
healthResult = sdk.StatusReport_UNKNOWN
default:
healthResult = sdk.StatusReport_UNKNOWN
}

return healthResult
}

// Take a list of Pods and build a Waypoint Status Report based on their reported health
func buildStatusReport(
podList *corev1.PodList,
) sdk.StatusReport {
var result sdk.StatusReport
result.External = true
resources := make([]*sdk.StatusReport_Resource, len(podList.Items))

// Report on most recently observed status of a deployments pod
for _, pod := range podList.Items {
// Overall Pod Health
podStatus := pod.Status
result.HealthMessage = podStatus.Message
result.Health = podPhaseToHealth(podStatus.Phase)
briancain marked this conversation as resolved.
Show resolved Hide resolved

// Pod containers health
for i, containerStatus := range podStatus.ContainerStatuses {
resources[i] = containerStatusToHealth(containerStatus)
}
}

return result
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ require (
github.com/hashicorp/vault/api v1.0.5-0.20200519221902-385fac77e20f
github.com/hashicorp/vault/sdk v0.1.14-0.20201202172114-ee5ebeb30fef
github.com/hashicorp/waypoint-hzn v0.0.0-20201008221232-97cd4d9120b9
github.com/hashicorp/waypoint-plugin-sdk v0.0.0-20210518161041-53694fdd7d98
github.com/hashicorp/waypoint-plugin-sdk v0.0.0-20210526204726-417a273d6412
github.com/imdario/mergo v0.3.11
github.com/improbable-eng/grpc-web v0.13.0
github.com/kevinburke/go-bindata v3.22.0+incompatible
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -684,8 +684,8 @@ github.com/hashicorp/vault/sdk v0.1.14-0.20201202172114-ee5ebeb30fef h1:YKouRHFf
github.com/hashicorp/vault/sdk v0.1.14-0.20201202172114-ee5ebeb30fef/go.mod h1:cAGI4nVnEfAyMeqt9oB+Mase8DNn3qA/LDNHURiwssY=
github.com/hashicorp/waypoint-hzn v0.0.0-20201008221232-97cd4d9120b9 h1:i9hzlv2SpmaNcQ8ZLGn01fp2Gqyejj0juVs7rYDgecE=
github.com/hashicorp/waypoint-hzn v0.0.0-20201008221232-97cd4d9120b9/go.mod h1:ObgQSWSX9rsNofh16kctm6XxLW2QW1Ay6/9ris6T6DU=
github.com/hashicorp/waypoint-plugin-sdk v0.0.0-20210518161041-53694fdd7d98 h1:9xcGgccOCi9PL1yj2pnhs8VN2mQYuC+Ym2RQMmR2zWA=
github.com/hashicorp/waypoint-plugin-sdk v0.0.0-20210518161041-53694fdd7d98/go.mod h1:3EzMFm8svUyyR35eop57AZmFBqFbIJVx6/MkALuNEjo=
github.com/hashicorp/waypoint-plugin-sdk v0.0.0-20210526204726-417a273d6412 h1:qw6H1ef5BJsze++0GoPrFNXw0lV57mk2ADvdau0l2Jw=
github.com/hashicorp/waypoint-plugin-sdk v0.0.0-20210526204726-417a273d6412/go.mod h1:3EzMFm8svUyyR35eop57AZmFBqFbIJVx6/MkALuNEjo=
github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM=
github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM=
github.com/hashicorp/yamux v0.0.0-20190923154419-df201c70410d/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM=
Expand Down