Skip to content

Commit

Permalink
Merge pull request #90 from steadybit/status-check-mode
Browse files Browse the repository at this point in the history
feat: provide service status check mode
  • Loading branch information
bertschneider authored Feb 11, 2025
2 parents ba7d7f2 + 0246263 commit e0613a8
Show file tree
Hide file tree
Showing 7 changed files with 401 additions and 116 deletions.
11 changes: 10 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
# Changelog

## v1.0.14

- Provide service status check mode to verify if the given state was observed at least once or all the time.

## v1.0.13

- Fix missing property in StackState API request issue.
- Update dependencies

## v1.0.12

- update dependencies
- Use uid instead of name for user statement in Dockerfile
- Update dependencies

## v1.0.11

Expand Down
59 changes: 53 additions & 6 deletions extservice/common.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,60 @@
// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: 2022 Steadybit GmbH
// SPDX-FileCopyrightText: 2024 Steadybit GmbH

package extservice

import "github.com/go-resty/resty/v2"

var RestyClient *resty.Client
import (
"context"
"fmt"
"github.com/go-resty/resty/v2"
)

const (
serviceTargetType = "com.steadybit.extension_stackstate.service"
serviceIcon = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA5Ny4yNyA5Ni42MSI+PGcgZmlsbD0iY3VycmVudENvbG9yIj48cGF0aCBkPSJNMTcuOTUgMjkuN2wzMC43OS0xNy43Mkw3OS41MyAyOS43IDUwLjY0IDQ2LjI5Yy0xLjE3LjY5LTIuNjUuNjktMy44MSAwTDE3Ljk1IDI5Ljd6Ii8+PHBhdGggZD0iTTQ2Ljg0IDUzLjY0TDI2LjcxIDQyLjA2bC04Ljc2IDUuMDYgMjguODggMTYuNTljMS4xNy42OSAyLjY1LjY5IDMuODEgMGwyOC44OC0xNi41OS04Ljc2LTUuMDYtMjAuMTMgMTEuNThjLTEuMTcuNjktMi42NS42OS0zLjgxIDB6Ii8+PHBhdGggZD0iTTQ2Ljg0IDcxLjQ1TDI2LjcxIDU5Ljg3bC04Ljc2IDUuMDYgMjguODggMTYuNTljMS4xNy42OSAyLjY1LjY5IDMuODEgMGwyOC44OC0xNi41OS04Ljc2LTUuMDYtMjAuMDggMTEuNThjLTEuMjEuNjktMi42OS42OS0zLjg1IDB6Ii8+PGc+PHBhdGggZD0iTTAgNDguMzJjMCA4LjU2IDIuMjUgMTYuNTkgNi4xNiAyMy41Nmw1LjQ2LTMuMTZ2LTQuOTdjMC0xLjM0Ljc0LTIuNiAxLjkxLTMuMjhsNi45LTMuOTgtNi42OC0zLjg1Yy0xLjE3LS42OS0xLjkxLTEuOTUtMS45MS0zLjI4VjQ1LjljMC0xLjM0LjctMi42NCAxLjkxLTMuMjhsNi42NC0zLjg5LTYuNTktMy44Yy0xLjE3LS42OS0xLjkxLTEuOTUtMS45MS0zLjI4di0zLjAyYzAtMS4zOS43NC0yLjY0IDEuOTEtMy4zM0w0NS4yMyA3LjI3Vi4xM0MxOS45OSAxLjgxIDAgMjIuNzggMCA0OC4zMnpNOTcuMjcgNDguMjRjMCA4LjU2LTIuMjUgMTYuNTktNi4xNiAyMy41NmwtNS40Ni0zLjE2di00Ljk3YzAtMS4zNC0uNzQtMi42LTEuOTEtMy4yOGwtNi45LTMuOTggNi42OC0zLjg1YzEuMTctLjY5IDEuOTEtMS45NSAxLjkxLTMuMjh2LTMuNDZjMC0xLjM0LS43LTIuNjQtMS45MS0zLjI4bC02LjY0LTMuODkgNi41OS0zLjhjMS4xNy0uNjkgMS45MS0xLjk1IDEuOTEtMy4yOHYtMy4wMmMwLTEuMzktLjc0LTIuNjQtMS45MS0zLjMzTDUyLjA0IDcuMTdWMGMyNS4yNCAxLjY5IDQ1LjIzIDIyLjY5IDQ1LjIzIDQ4LjI0ek00OC42MSA5Ni42MWMxNS45NiAwIDMwLjE0LTcuNjkgMzguOTktMTkuNTNsLTguMzMtNC43NUw1MC42OSA4OC43Yy0xLjE3LjY5LTIuNjUuNjktMy44MSAwTDE4IDcyLjE1bC01LjMgMy4wMi0uNi4zNC0yLjU2IDEuNDdjOC44OSAxMS44OSAyMy4wNyAxOS42MyAzOS4wNyAxOS42M3oiLz48L2c+PC9nPjwvc3ZnPg=="
serviceTargetType = "com.steadybit.extension_stackstate.service"
serviceIcon = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA5Ny4yNyA5Ni42MSI+PGcgZmlsbD0iY3VycmVudENvbG9yIj48cGF0aCBkPSJNMTcuOTUgMjkuN2wzMC43OS0xNy43Mkw3OS41MyAyOS43IDUwLjY0IDQ2LjI5Yy0xLjE3LjY5LTIuNjUuNjktMy44MSAwTDE3Ljk1IDI5Ljd6Ii8+PHBhdGggZD0iTTQ2Ljg0IDUzLjY0TDI2LjcxIDQyLjA2bC04Ljc2IDUuMDYgMjguODggMTYuNTljMS4xNy42OSAyLjY1LjY5IDMuODEgMGwyOC44OC0xNi41OS04Ljc2LTUuMDYtMjAuMTMgMTEuNThjLTEuMTcuNjktMi42NS42OS0zLjgxIDB6Ii8+PHBhdGggZD0iTTQ2Ljg0IDcxLjQ1TDI2LjcxIDU5Ljg3bC04Ljc2IDUuMDYgMjguODggMTYuNTljMS4xNy42OSAyLjY1LjY5IDMuODEgMGwyOC44OC0xNi41OS04Ljc2LTUuMDYtMjAuMDggMTEuNThjLTEuMjEuNjktMi42OS42OS0zLjg1IDB6Ii8+PGc+PHBhdGggZD0iTTAgNDguMzJjMCA4LjU2IDIuMjUgMTYuNTkgNi4xNiAyMy41Nmw1LjQ2LTMuMTZ2LTQuOTdjMC0xLjM0Ljc0LTIuNiAxLjkxLTMuMjhsNi45LTMuOTgtNi42OC0zLjg1Yy0xLjE3LS42OS0xLjkxLTEuOTUtMS45MS0zLjI4VjQ1LjljMC0xLjM0LjctMi42NCAxLjkxLTMuMjhsNi42NC0zLjg5LTYuNTktMy44Yy0xLjE3LS42OS0xLjkxLTEuOTUtMS45MS0zLjI4di0zLjAyYzAtMS4zOS43NC0yLjY0IDEuOTEtMy4zM0w0NS4yMyA3LjI3Vi4xM0MxOS45OSAxLjgxIDAgMjIuNzggMCA0OC4zMnpNOTcuMjcgNDguMjRjMCA4LjU2LTIuMjUgMTYuNTktNi4xNiAyMy41NmwtNS40Ni0zLjE2di00Ljk3YzAtMS4zNC0uNzQtMi42LTEuOTEtMy4yOGwtNi45LTMuOTggNi42OC0zLjg1YzEuMTctLjY5IDEuOTEtMS45NSAxLjkxLTMuMjh2LTMuNDZjMC0xLjM0LS43LTIuNjQtMS45MS0zLjI4bC02LjY0LTMuODkgNi41OS0zLjhjMS4xNy0uNjkgMS45MS0xLjk1IDEuOTEtMy4yOHYtMy4wMmMwLTEuMzktLjc0LTIuNjQtMS45MS0zLjMzTDUyLjA0IDcuMTdWMGMyNS4yNCAxLjY5IDQ1LjIzIDIyLjY5IDQ1LjIzIDQ4LjI0ek00OC42MSA5Ni42MWMxNS45NiAwIDMwLjE0LTcuNjkgMzguOTktMTkuNTNsLTguMzMtNC43NUw1MC42OSA4OC43Yy0xLjE3LjY5LTIuNjUuNjktMy44MSAwTDE4IDcyLjE1bC01LjMgMy4wMi0uNi4zNC0yLjU2IDEuNDdjOC44OSAxMS44OSAyMy4wNyAxOS42MyAzOS4wNyAxOS42M3oiLz48L2c+PC9nPjwvc3ZnPg=="
statusCheckModeAtLeastOnce = "atLeastOnce"
statusCheckModeAllTheTime = "allTheTime"
)

var Client *StackStateHttpClient

type StackStateHttpClient struct {
Client *resty.Client
}

func (s *StackStateHttpClient) GetServiceSnapshot(ctx context.Context, serviceId string) (*resty.Response, ViewSnapshotResponseWrapper, error) {
return s.executeSnapshotQuery(ctx, fmt.Sprintf("(id = \\\"%s\\\")", serviceId))
}

func (s *StackStateHttpClient) GetServiceSnapshots(ctx context.Context) (*resty.Response, ViewSnapshotResponseWrapper, error) {
return s.executeSnapshotQuery(ctx, "(type = \\\"service\\\")")
}

func (s *StackStateHttpClient) executeSnapshotQuery(ctx context.Context, query string) (*resty.Response, ViewSnapshotResponseWrapper, error) {
requestBody := fmt.Sprintf(`{
"_type": "ViewSnapshotRequest",
"query": "%v",
"queryVersion": "0.0.1",
"metadata": {
"_type": "QueryMetadata",
"groupingEnabled": false,
"showIndirectRelations": false,
"minGroupSize": 0,
"groupedByLayer": false,
"groupedByDomain": false,
"groupedByRelation": false,
"showCause": "NONE",
"autoGrouping": false,
"connectedComponents": false,
"neighboringComponents": false,
"showFullComponent": false
}
}`, query)
var stackStateResponse ViewSnapshotResponseWrapper
response, err := s.Client.R().
SetContext(ctx).
SetBody([]byte(requestBody)).
SetResult(&stackStateResponse).
Post("/snapshot")
return response, stackStateResponse, err
}
116 changes: 71 additions & 45 deletions extservice/service_check.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
*/

// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: 2022 Steadybit GmbH
// SPDX-FileCopyrightText: 2024 Steadybit GmbH

package extservice

Expand Down Expand Up @@ -32,11 +32,17 @@ var (
)

type ServiceStatusCheckState struct {
ServiceId string
ServiceName string
ClusterName string
End time.Time
ExpectedStatus string
ServiceId string
ServiceName string
ClusterName string
End time.Time
ExpectedStatus string
StatusCheckMode string
StatusCheckSuccess bool
}

type GetSnapshotApi interface {
GetServiceSnapshot(ctx context.Context, serviceId string) (*resty.Response, ViewSnapshotResponseWrapper, error)
}

func NewServiceStatusCheckAction() action_kit_sdk.Action[ServiceStatusCheckState] {
Expand Down Expand Up @@ -105,6 +111,25 @@ func (m *ServiceStatusCheckAction) Describe() action_kit_api.ActionDescription {
Required: extutil.Ptr(false),
Order: extutil.Ptr(2),
},
{
Name: "statusCheckMode",
Label: "Status Check Mode",
Description: extutil.Ptr("How often should the status be expected?"),
Type: action_kit_api.String,
DefaultValue: extutil.Ptr(statusCheckModeAllTheTime),
Options: extutil.Ptr([]action_kit_api.ParameterOption{
action_kit_api.ExplicitParameterOption{
Label: "All the time",
Value: statusCheckModeAllTheTime,
},
action_kit_api.ExplicitParameterOption{
Label: "At least once",
Value: statusCheckModeAtLeastOnce,
},
}),
Required: extutil.Ptr(true),
Order: extutil.Ptr(4),
},
},
Widgets: extutil.Ptr([]action_kit_api.Widget{
action_kit_api.StateOverTimeWidget{
Expand Down Expand Up @@ -149,12 +174,18 @@ func (m *ServiceStatusCheckAction) Prepare(_ context.Context, state *ServiceStat
if request.Config["expectedStatus"] != nil {
expectedStatus = fmt.Sprintf("%v", request.Config["expectedStatus"])
}
var statusCheckMode = statusCheckModeAllTheTime
if request.Config["statusCheckMode"] != nil {
statusCheckMode = fmt.Sprintf("%v", request.Config["statusCheckMode"])
}

state.ServiceId = serviceId[0]
state.ServiceName = request.Target.Attributes["k8s.service.name"][0]
state.ClusterName = request.Target.Attributes["k8s.cluster-name"][0]
state.End = end
state.ExpectedStatus = expectedStatus
state.StatusCheckMode = statusCheckMode
state.StatusCheckSuccess = state.StatusCheckMode == statusCheckModeAllTheTime

return nil, nil
}
Expand All @@ -164,41 +195,15 @@ func (m *ServiceStatusCheckAction) Start(_ context.Context, _ *ServiceStatusChec
}

func (m *ServiceStatusCheckAction) Status(ctx context.Context, state *ServiceStatusCheckState) (*action_kit_api.StatusResult, error) {
return MonitorStatusCheckStatus(ctx, state, RestyClient)
return MonitorStatusCheckStatus(ctx, state, Client)
}

func MonitorStatusCheckStatus(ctx context.Context, state *ServiceStatusCheckState, client *resty.Client) (*action_kit_api.StatusResult, error) {
func MonitorStatusCheckStatus(ctx context.Context, state *ServiceStatusCheckState, api GetSnapshotApi) (*action_kit_api.StatusResult, error) {
now := time.Now()

requestBody := fmt.Sprintf(`{
"_type": "ViewSnapshotRequest",
"query": "(id = \"%s\")",
"queryVersion": "0.0.1",
"metadata": {
"_type": "QueryMetadata",
"groupingEnabled": false,
"showIndirectRelations": false,
"minGroupSize": 0,
"groupedByLayer": false,
"groupedByDomain": false,
"groupedByRelation": false,
"showCause": "NONE",
"autoGrouping": false,
"connectedComponents": false,
"neighboringComponents": false,
"showFullComponent": false
}
}`, state.ServiceId)

var stackStateResponse ViewSnapshotResponseWrapper
res, err := client.R().
SetContext(ctx).
SetBody([]byte(requestBody)).
SetResult(&stackStateResponse).
Post("/snapshot")
res, stackStateResponse, err := api.GetServiceSnapshot(ctx, state.ServiceId)

if err != nil {
return nil, extutil.Ptr(extension_kit.ToError(fmt.Sprintf("Failed to retrieve service states from Stack State for Service ID %s. Full response: %v", state.ServiceId, res.String()), err))
return nil, extutil.Ptr(extension_kit.ToError(fmt.Sprintf("Failed to retrieve service states from StackState for Service ID %s. Full response: %v", state.ServiceId, res.String()), err))
}

var component Component
Expand All @@ -222,15 +227,36 @@ func MonitorStatusCheckStatus(ctx context.Context, state *ServiceStatusCheckStat

completed := now.After(state.End)
var checkError *action_kit_api.ActionKitError
if len(state.ExpectedStatus) > 0 && component.State.HealthState != state.ExpectedStatus {
checkError = extutil.Ptr(action_kit_api.ActionKitError{
Title: fmt.Sprintf("Service '%s' (id %s) has status '%s' whereas '%s' is expected.",
component.Name,
state.ServiceId,
component.State.HealthState,
state.ExpectedStatus),
Status: extutil.Ptr(action_kit_api.Failed),
})
if len(state.ExpectedStatus) > 0 {
componentHealthState := component.State.HealthState
if state.StatusCheckMode == statusCheckModeAllTheTime {
if componentHealthState != state.ExpectedStatus || !state.StatusCheckSuccess {
state.StatusCheckSuccess = false
completed = true
checkError = extutil.Ptr(action_kit_api.ActionKitError{
Title: fmt.Sprintf("Service '%s' (id %s) has status '%s' whereas '%s' is expected.",
component.Name,
state.ServiceId,
componentHealthState,
state.ExpectedStatus),
Status: extutil.Ptr(action_kit_api.Failed),
})
}
} else if state.StatusCheckMode == statusCheckModeAtLeastOnce {
if componentHealthState == state.ExpectedStatus || state.StatusCheckSuccess {
state.StatusCheckSuccess = true
completed = true
}
if completed && !state.StatusCheckSuccess {
checkError = extutil.Ptr(action_kit_api.ActionKitError{
Title: fmt.Sprintf("Service '%s' (id %s) didn't have status '%s' at least once.",
component.Name,
state.ServiceId,
state.ExpectedStatus),
Status: extutil.Ptr(action_kit_api.Failed),
})
}
}
}

metrics := []action_kit_api.Metric{
Expand Down
Loading

0 comments on commit e0613a8

Please sign in to comment.