Skip to content

Commit e0613a8

Browse files
Merge pull request #90 from steadybit/status-check-mode
feat: provide service status check mode
2 parents ba7d7f2 + 0246263 commit e0613a8

File tree

7 files changed

+401
-116
lines changed

7 files changed

+401
-116
lines changed

CHANGELOG.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,18 @@
11
# Changelog
22

3+
## v1.0.14
4+
5+
- Provide service status check mode to verify if the given state was observed at least once or all the time.
6+
7+
## v1.0.13
8+
9+
- Fix missing property in StackState API request issue.
10+
- Update dependencies
11+
312
## v1.0.12
413

5-
- update dependencies
614
- Use uid instead of name for user statement in Dockerfile
15+
- Update dependencies
716

817
## v1.0.11
918

extservice/common.go

Lines changed: 53 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,60 @@
11
// SPDX-License-Identifier: MIT
2-
// SPDX-FileCopyrightText: 2022 Steadybit GmbH
2+
// SPDX-FileCopyrightText: 2024 Steadybit GmbH
33

44
package extservice
55

6-
import "github.com/go-resty/resty/v2"
7-
8-
var RestyClient *resty.Client
6+
import (
7+
"context"
8+
"fmt"
9+
"github.com/go-resty/resty/v2"
10+
)
911

1012
const (
11-
serviceTargetType = "com.steadybit.extension_stackstate.service"
12-
serviceIcon = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA5Ny4yNyA5Ni42MSI+PGcgZmlsbD0iY3VycmVudENvbG9yIj48cGF0aCBkPSJNMTcuOTUgMjkuN2wzMC43OS0xNy43Mkw3OS41MyAyOS43IDUwLjY0IDQ2LjI5Yy0xLjE3LjY5LTIuNjUuNjktMy44MSAwTDE3Ljk1IDI5Ljd6Ii8+PHBhdGggZD0iTTQ2Ljg0IDUzLjY0TDI2LjcxIDQyLjA2bC04Ljc2IDUuMDYgMjguODggMTYuNTljMS4xNy42OSAyLjY1LjY5IDMuODEgMGwyOC44OC0xNi41OS04Ljc2LTUuMDYtMjAuMTMgMTEuNThjLTEuMTcuNjktMi42NS42OS0zLjgxIDB6Ii8+PHBhdGggZD0iTTQ2Ljg0IDcxLjQ1TDI2LjcxIDU5Ljg3bC04Ljc2IDUuMDYgMjguODggMTYuNTljMS4xNy42OSAyLjY1LjY5IDMuODEgMGwyOC44OC0xNi41OS04Ljc2LTUuMDYtMjAuMDggMTEuNThjLTEuMjEuNjktMi42OS42OS0zLjg1IDB6Ii8+PGc+PHBhdGggZD0iTTAgNDguMzJjMCA4LjU2IDIuMjUgMTYuNTkgNi4xNiAyMy41Nmw1LjQ2LTMuMTZ2LTQuOTdjMC0xLjM0Ljc0LTIuNiAxLjkxLTMuMjhsNi45LTMuOTgtNi42OC0zLjg1Yy0xLjE3LS42OS0xLjkxLTEuOTUtMS45MS0zLjI4VjQ1LjljMC0xLjM0LjctMi42NCAxLjkxLTMuMjhsNi42NC0zLjg5LTYuNTktMy44Yy0xLjE3LS42OS0xLjkxLTEuOTUtMS45MS0zLjI4di0zLjAyYzAtMS4zOS43NC0yLjY0IDEuOTEtMy4zM0w0NS4yMyA3LjI3Vi4xM0MxOS45OSAxLjgxIDAgMjIuNzggMCA0OC4zMnpNOTcuMjcgNDguMjRjMCA4LjU2LTIuMjUgMTYuNTktNi4xNiAyMy41NmwtNS40Ni0zLjE2di00Ljk3YzAtMS4zNC0uNzQtMi42LTEuOTEtMy4yOGwtNi45LTMuOTggNi42OC0zLjg1YzEuMTctLjY5IDEuOTEtMS45NSAxLjkxLTMuMjh2LTMuNDZjMC0xLjM0LS43LTIuNjQtMS45MS0zLjI4bC02LjY0LTMuODkgNi41OS0zLjhjMS4xNy0uNjkgMS45MS0xLjk1IDEuOTEtMy4yOHYtMy4wMmMwLTEuMzktLjc0LTIuNjQtMS45MS0zLjMzTDUyLjA0IDcuMTdWMGMyNS4yNCAxLjY5IDQ1LjIzIDIyLjY5IDQ1LjIzIDQ4LjI0ek00OC42MSA5Ni42MWMxNS45NiAwIDMwLjE0LTcuNjkgMzguOTktMTkuNTNsLTguMzMtNC43NUw1MC42OSA4OC43Yy0xLjE3LjY5LTIuNjUuNjktMy44MSAwTDE4IDcyLjE1bC01LjMgMy4wMi0uNi4zNC0yLjU2IDEuNDdjOC44OSAxMS44OSAyMy4wNyAxOS42MyAzOS4wNyAxOS42M3oiLz48L2c+PC9nPjwvc3ZnPg=="
13+
serviceTargetType = "com.steadybit.extension_stackstate.service"
14+
serviceIcon = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA5Ny4yNyA5Ni42MSI+PGcgZmlsbD0iY3VycmVudENvbG9yIj48cGF0aCBkPSJNMTcuOTUgMjkuN2wzMC43OS0xNy43Mkw3OS41MyAyOS43IDUwLjY0IDQ2LjI5Yy0xLjE3LjY5LTIuNjUuNjktMy44MSAwTDE3Ljk1IDI5Ljd6Ii8+PHBhdGggZD0iTTQ2Ljg0IDUzLjY0TDI2LjcxIDQyLjA2bC04Ljc2IDUuMDYgMjguODggMTYuNTljMS4xNy42OSAyLjY1LjY5IDMuODEgMGwyOC44OC0xNi41OS04Ljc2LTUuMDYtMjAuMTMgMTEuNThjLTEuMTcuNjktMi42NS42OS0zLjgxIDB6Ii8+PHBhdGggZD0iTTQ2Ljg0IDcxLjQ1TDI2LjcxIDU5Ljg3bC04Ljc2IDUuMDYgMjguODggMTYuNTljMS4xNy42OSAyLjY1LjY5IDMuODEgMGwyOC44OC0xNi41OS04Ljc2LTUuMDYtMjAuMDggMTEuNThjLTEuMjEuNjktMi42OS42OS0zLjg1IDB6Ii8+PGc+PHBhdGggZD0iTTAgNDguMzJjMCA4LjU2IDIuMjUgMTYuNTkgNi4xNiAyMy41Nmw1LjQ2LTMuMTZ2LTQuOTdjMC0xLjM0Ljc0LTIuNiAxLjkxLTMuMjhsNi45LTMuOTgtNi42OC0zLjg1Yy0xLjE3LS42OS0xLjkxLTEuOTUtMS45MS0zLjI4VjQ1LjljMC0xLjM0LjctMi42NCAxLjkxLTMuMjhsNi42NC0zLjg5LTYuNTktMy44Yy0xLjE3LS42OS0xLjkxLTEuOTUtMS45MS0zLjI4di0zLjAyYzAtMS4zOS43NC0yLjY0IDEuOTEtMy4zM0w0NS4yMyA3LjI3Vi4xM0MxOS45OSAxLjgxIDAgMjIuNzggMCA0OC4zMnpNOTcuMjcgNDguMjRjMCA4LjU2LTIuMjUgMTYuNTktNi4xNiAyMy41NmwtNS40Ni0zLjE2di00Ljk3YzAtMS4zNC0uNzQtMi42LTEuOTEtMy4yOGwtNi45LTMuOTggNi42OC0zLjg1YzEuMTctLjY5IDEuOTEtMS45NSAxLjkxLTMuMjh2LTMuNDZjMC0xLjM0LS43LTIuNjQtMS45MS0zLjI4bC02LjY0LTMuODkgNi41OS0zLjhjMS4xNy0uNjkgMS45MS0xLjk1IDEuOTEtMy4yOHYtMy4wMmMwLTEuMzktLjc0LTIuNjQtMS45MS0zLjMzTDUyLjA0IDcuMTdWMGMyNS4yNCAxLjY5IDQ1LjIzIDIyLjY5IDQ1LjIzIDQ4LjI0ek00OC42MSA5Ni42MWMxNS45NiAwIDMwLjE0LTcuNjkgMzguOTktMTkuNTNsLTguMzMtNC43NUw1MC42OSA4OC43Yy0xLjE3LjY5LTIuNjUuNjktMy44MSAwTDE4IDcyLjE1bC01LjMgMy4wMi0uNi4zNC0yLjU2IDEuNDdjOC44OSAxMS44OSAyMy4wNyAxOS42MyAzOS4wNyAxOS42M3oiLz48L2c+PC9nPjwvc3ZnPg=="
15+
statusCheckModeAtLeastOnce = "atLeastOnce"
16+
statusCheckModeAllTheTime = "allTheTime"
1317
)
18+
19+
var Client *StackStateHttpClient
20+
21+
type StackStateHttpClient struct {
22+
Client *resty.Client
23+
}
24+
25+
func (s *StackStateHttpClient) GetServiceSnapshot(ctx context.Context, serviceId string) (*resty.Response, ViewSnapshotResponseWrapper, error) {
26+
return s.executeSnapshotQuery(ctx, fmt.Sprintf("(id = \\\"%s\\\")", serviceId))
27+
}
28+
29+
func (s *StackStateHttpClient) GetServiceSnapshots(ctx context.Context) (*resty.Response, ViewSnapshotResponseWrapper, error) {
30+
return s.executeSnapshotQuery(ctx, "(type = \\\"service\\\")")
31+
}
32+
33+
func (s *StackStateHttpClient) executeSnapshotQuery(ctx context.Context, query string) (*resty.Response, ViewSnapshotResponseWrapper, error) {
34+
requestBody := fmt.Sprintf(`{
35+
"_type": "ViewSnapshotRequest",
36+
"query": "%v",
37+
"queryVersion": "0.0.1",
38+
"metadata": {
39+
"_type": "QueryMetadata",
40+
"groupingEnabled": false,
41+
"showIndirectRelations": false,
42+
"minGroupSize": 0,
43+
"groupedByLayer": false,
44+
"groupedByDomain": false,
45+
"groupedByRelation": false,
46+
"showCause": "NONE",
47+
"autoGrouping": false,
48+
"connectedComponents": false,
49+
"neighboringComponents": false,
50+
"showFullComponent": false
51+
}
52+
}`, query)
53+
var stackStateResponse ViewSnapshotResponseWrapper
54+
response, err := s.Client.R().
55+
SetContext(ctx).
56+
SetBody([]byte(requestBody)).
57+
SetResult(&stackStateResponse).
58+
Post("/snapshot")
59+
return response, stackStateResponse, err
60+
}

extservice/service_check.go

Lines changed: 71 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
*/
44

55
// SPDX-License-Identifier: MIT
6-
// SPDX-FileCopyrightText: 2022 Steadybit GmbH
6+
// SPDX-FileCopyrightText: 2024 Steadybit GmbH
77

88
package extservice
99

@@ -32,11 +32,17 @@ var (
3232
)
3333

3434
type ServiceStatusCheckState struct {
35-
ServiceId string
36-
ServiceName string
37-
ClusterName string
38-
End time.Time
39-
ExpectedStatus string
35+
ServiceId string
36+
ServiceName string
37+
ClusterName string
38+
End time.Time
39+
ExpectedStatus string
40+
StatusCheckMode string
41+
StatusCheckSuccess bool
42+
}
43+
44+
type GetSnapshotApi interface {
45+
GetServiceSnapshot(ctx context.Context, serviceId string) (*resty.Response, ViewSnapshotResponseWrapper, error)
4046
}
4147

4248
func NewServiceStatusCheckAction() action_kit_sdk.Action[ServiceStatusCheckState] {
@@ -105,6 +111,25 @@ func (m *ServiceStatusCheckAction) Describe() action_kit_api.ActionDescription {
105111
Required: extutil.Ptr(false),
106112
Order: extutil.Ptr(2),
107113
},
114+
{
115+
Name: "statusCheckMode",
116+
Label: "Status Check Mode",
117+
Description: extutil.Ptr("How often should the status be expected?"),
118+
Type: action_kit_api.String,
119+
DefaultValue: extutil.Ptr(statusCheckModeAllTheTime),
120+
Options: extutil.Ptr([]action_kit_api.ParameterOption{
121+
action_kit_api.ExplicitParameterOption{
122+
Label: "All the time",
123+
Value: statusCheckModeAllTheTime,
124+
},
125+
action_kit_api.ExplicitParameterOption{
126+
Label: "At least once",
127+
Value: statusCheckModeAtLeastOnce,
128+
},
129+
}),
130+
Required: extutil.Ptr(true),
131+
Order: extutil.Ptr(4),
132+
},
108133
},
109134
Widgets: extutil.Ptr([]action_kit_api.Widget{
110135
action_kit_api.StateOverTimeWidget{
@@ -149,12 +174,18 @@ func (m *ServiceStatusCheckAction) Prepare(_ context.Context, state *ServiceStat
149174
if request.Config["expectedStatus"] != nil {
150175
expectedStatus = fmt.Sprintf("%v", request.Config["expectedStatus"])
151176
}
177+
var statusCheckMode = statusCheckModeAllTheTime
178+
if request.Config["statusCheckMode"] != nil {
179+
statusCheckMode = fmt.Sprintf("%v", request.Config["statusCheckMode"])
180+
}
152181

153182
state.ServiceId = serviceId[0]
154183
state.ServiceName = request.Target.Attributes["k8s.service.name"][0]
155184
state.ClusterName = request.Target.Attributes["k8s.cluster-name"][0]
156185
state.End = end
157186
state.ExpectedStatus = expectedStatus
187+
state.StatusCheckMode = statusCheckMode
188+
state.StatusCheckSuccess = state.StatusCheckMode == statusCheckModeAllTheTime
158189

159190
return nil, nil
160191
}
@@ -164,41 +195,15 @@ func (m *ServiceStatusCheckAction) Start(_ context.Context, _ *ServiceStatusChec
164195
}
165196

166197
func (m *ServiceStatusCheckAction) Status(ctx context.Context, state *ServiceStatusCheckState) (*action_kit_api.StatusResult, error) {
167-
return MonitorStatusCheckStatus(ctx, state, RestyClient)
198+
return MonitorStatusCheckStatus(ctx, state, Client)
168199
}
169200

170-
func MonitorStatusCheckStatus(ctx context.Context, state *ServiceStatusCheckState, client *resty.Client) (*action_kit_api.StatusResult, error) {
201+
func MonitorStatusCheckStatus(ctx context.Context, state *ServiceStatusCheckState, api GetSnapshotApi) (*action_kit_api.StatusResult, error) {
171202
now := time.Now()
172-
173-
requestBody := fmt.Sprintf(`{
174-
"_type": "ViewSnapshotRequest",
175-
"query": "(id = \"%s\")",
176-
"queryVersion": "0.0.1",
177-
"metadata": {
178-
"_type": "QueryMetadata",
179-
"groupingEnabled": false,
180-
"showIndirectRelations": false,
181-
"minGroupSize": 0,
182-
"groupedByLayer": false,
183-
"groupedByDomain": false,
184-
"groupedByRelation": false,
185-
"showCause": "NONE",
186-
"autoGrouping": false,
187-
"connectedComponents": false,
188-
"neighboringComponents": false,
189-
"showFullComponent": false
190-
}
191-
}`, state.ServiceId)
192-
193-
var stackStateResponse ViewSnapshotResponseWrapper
194-
res, err := client.R().
195-
SetContext(ctx).
196-
SetBody([]byte(requestBody)).
197-
SetResult(&stackStateResponse).
198-
Post("/snapshot")
203+
res, stackStateResponse, err := api.GetServiceSnapshot(ctx, state.ServiceId)
199204

200205
if err != nil {
201-
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))
206+
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))
202207
}
203208

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

223228
completed := now.After(state.End)
224229
var checkError *action_kit_api.ActionKitError
225-
if len(state.ExpectedStatus) > 0 && component.State.HealthState != state.ExpectedStatus {
226-
checkError = extutil.Ptr(action_kit_api.ActionKitError{
227-
Title: fmt.Sprintf("Service '%s' (id %s) has status '%s' whereas '%s' is expected.",
228-
component.Name,
229-
state.ServiceId,
230-
component.State.HealthState,
231-
state.ExpectedStatus),
232-
Status: extutil.Ptr(action_kit_api.Failed),
233-
})
230+
if len(state.ExpectedStatus) > 0 {
231+
componentHealthState := component.State.HealthState
232+
if state.StatusCheckMode == statusCheckModeAllTheTime {
233+
if componentHealthState != state.ExpectedStatus || !state.StatusCheckSuccess {
234+
state.StatusCheckSuccess = false
235+
completed = true
236+
checkError = extutil.Ptr(action_kit_api.ActionKitError{
237+
Title: fmt.Sprintf("Service '%s' (id %s) has status '%s' whereas '%s' is expected.",
238+
component.Name,
239+
state.ServiceId,
240+
componentHealthState,
241+
state.ExpectedStatus),
242+
Status: extutil.Ptr(action_kit_api.Failed),
243+
})
244+
}
245+
} else if state.StatusCheckMode == statusCheckModeAtLeastOnce {
246+
if componentHealthState == state.ExpectedStatus || state.StatusCheckSuccess {
247+
state.StatusCheckSuccess = true
248+
completed = true
249+
}
250+
if completed && !state.StatusCheckSuccess {
251+
checkError = extutil.Ptr(action_kit_api.ActionKitError{
252+
Title: fmt.Sprintf("Service '%s' (id %s) didn't have status '%s' at least once.",
253+
component.Name,
254+
state.ServiceId,
255+
state.ExpectedStatus),
256+
Status: extutil.Ptr(action_kit_api.Failed),
257+
})
258+
}
259+
}
234260
}
235261

236262
metrics := []action_kit_api.Metric{

0 commit comments

Comments
 (0)