Skip to content

Commit dc32052

Browse files
authored
Merge pull request #520 from newrelic/chore/report-status
chore(install): add nerdstorageExecutionStatusReporter
2 parents 7102c75 + 72f1fef commit dc32052

20 files changed

+825
-81
lines changed

internal/install/command.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ var Command = &cobra.Command{
4747
rf := newServiceRecipeFetcher(&nrClient.NerdGraph)
4848
pf := newRegexProcessFilterer(rf)
4949
ff := newRecipeFileFetcher()
50+
er := newNerdStorageExecutionStatusReporter(&nrClient.NerdStorage)
5051

5152
i := newRecipeInstaller(ic,
5253
newPSUtilDiscoverer(pf),
@@ -55,6 +56,7 @@ var Command = &cobra.Command{
5556
newGoTaskRecipeExecutor(),
5657
newPollingRecipeValidator(&nrClient.Nrdb),
5758
ff,
59+
er,
5860
)
5961

6062
// Run the install.
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package install
2+
3+
import (
4+
"time"
5+
6+
"github.com/google/uuid"
7+
)
8+
9+
type executionStatusRollup struct {
10+
Complete bool `json:"complete"`
11+
DocumentID string
12+
EntityGuids []string `json:"entityGuids"`
13+
Statuses []executionStatus `json:"recipes"`
14+
Timestamp int64 `json:"timestamp"`
15+
}
16+
17+
type executionStatus struct {
18+
Name string `json:"name"`
19+
DisplayName string `json:"displayName"`
20+
Status executionStatusType `json:"status"`
21+
Errors []executionStatusRecipeError `json:"errors"`
22+
}
23+
24+
type executionStatusType string
25+
26+
var executionStatusTypes = struct {
27+
AVAILABLE executionStatusType
28+
FAILED executionStatusType
29+
INSTALLED executionStatusType
30+
SKIPPED executionStatusType
31+
}{
32+
AVAILABLE: "AVAILABLE",
33+
FAILED: "FAILED",
34+
INSTALLED: "INSTALLED",
35+
SKIPPED: "SKIPPED",
36+
}
37+
38+
type executionStatusRecipeError struct {
39+
Message string `json:"message"`
40+
Details string `json:"details"`
41+
}
42+
43+
func newExecutionStatusRollup() executionStatusRollup {
44+
s := executionStatusRollup{
45+
DocumentID: uuid.New().String(),
46+
Timestamp: getTimestamp(),
47+
}
48+
49+
return s
50+
}
51+
52+
func getTimestamp() int64 {
53+
return time.Now().Unix()
54+
}
55+
56+
func (s *executionStatusRollup) withAvailableRecipes(recipes []recipe) {
57+
for _, r := range recipes {
58+
e := recipeStatusEvent{recipe: r}
59+
s.withRecipeEvent(e, executionStatusTypes.AVAILABLE)
60+
}
61+
}
62+
63+
func (s *executionStatusRollup) withRecipeEvent(e recipeStatusEvent, rs executionStatusType) {
64+
found := s.getExecutionStatusRecipe(e.recipe)
65+
66+
if found != nil {
67+
found.Status = rs
68+
} else {
69+
e := &executionStatus{
70+
Name: e.recipe.Name,
71+
DisplayName: e.recipe.DisplayName,
72+
Status: rs,
73+
}
74+
s.Statuses = append(s.Statuses, *e)
75+
}
76+
77+
s.Timestamp = getTimestamp()
78+
}
79+
80+
func (s *executionStatusRollup) getExecutionStatusRecipe(r recipe) *executionStatus {
81+
var found *executionStatus
82+
for i, recipe := range s.Statuses {
83+
if recipe.Name == r.Name {
84+
found = &s.Statuses[i]
85+
}
86+
}
87+
88+
return found
89+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package install
2+
3+
type executionStatusReporter interface {
4+
reportRecipeFailed(event recipeStatusEvent) error
5+
reportRecipeInstalled(event recipeStatusEvent) error
6+
reportRecipesAvailable(recipes []recipe) error
7+
}
8+
9+
type recipeStatusEvent struct {
10+
recipe recipe
11+
msg string
12+
entityGUID string
13+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
//+ build unit
2+
package install
3+
4+
import (
5+
"testing"
6+
7+
"github.com/stretchr/testify/require"
8+
)
9+
10+
func TestNewExecutionStatus(t *testing.T) {
11+
s := newExecutionStatusRollup()
12+
require.NotEmpty(t, s.Timestamp)
13+
require.NotEmpty(t, s.DocumentID)
14+
}
15+
16+
func TestExecutionStatusWithAvailableRecipes_Basic(t *testing.T) {
17+
s := newExecutionStatusRollup()
18+
r := []recipe{{
19+
Name: "testRecipe1",
20+
}, {
21+
Name: "testRecipe2",
22+
}}
23+
24+
s.withAvailableRecipes(r)
25+
26+
require.NotEmpty(t, s.Statuses)
27+
require.Equal(t, len(r), len(s.Statuses))
28+
for _, recipeStatus := range s.Statuses {
29+
require.Equal(t, executionStatusTypes.AVAILABLE, recipeStatus.Status)
30+
}
31+
}
32+
33+
func TestExecutionStatusWithRecipeEvent_Basic(t *testing.T) {
34+
s := newExecutionStatusRollup()
35+
r := recipe{Name: "testRecipe"}
36+
e := recipeStatusEvent{recipe: r}
37+
38+
s.Timestamp = 0
39+
s.withRecipeEvent(e, executionStatusTypes.INSTALLED)
40+
41+
require.NotEmpty(t, s.Statuses)
42+
require.Equal(t, 1, len(s.Statuses))
43+
require.Equal(t, executionStatusTypes.INSTALLED, s.Statuses[0].Status)
44+
require.NotEmpty(t, s.Timestamp)
45+
}
46+
47+
func TestExecutionStatusWithRecipeEvent_RecipeExists(t *testing.T) {
48+
s := newExecutionStatusRollup()
49+
r := recipe{Name: "testRecipe"}
50+
e := recipeStatusEvent{recipe: r}
51+
52+
s.Timestamp = 0
53+
s.withRecipeEvent(e, executionStatusTypes.AVAILABLE)
54+
55+
require.NotEmpty(t, s.Statuses)
56+
require.Equal(t, 1, len(s.Statuses))
57+
require.Equal(t, executionStatusTypes.AVAILABLE, s.Statuses[0].Status)
58+
require.NotEmpty(t, s.Timestamp)
59+
60+
s.Timestamp = 0
61+
s.withRecipeEvent(e, executionStatusTypes.INSTALLED)
62+
63+
require.NotEmpty(t, s.Statuses)
64+
require.Equal(t, 1, len(s.Statuses))
65+
require.Equal(t, executionStatusTypes.INSTALLED, s.Statuses[0].Status)
66+
require.NotEmpty(t, s.Timestamp)
67+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package install
2+
3+
type mockExecutionStatusReporter struct {
4+
reportRecipesAvailableErr error
5+
reportRecipeFailedErr error
6+
reportRecipeInstalledErr error
7+
reportRecipesAvailableCallCount int
8+
reportRecipeFailedCallCount int
9+
reportRecipeInstalledCallCount int
10+
}
11+
12+
func newMockExecutionStatusReporter() *mockExecutionStatusReporter {
13+
return &mockExecutionStatusReporter{}
14+
}
15+
16+
func (r *mockExecutionStatusReporter) reportRecipeFailed(event recipeStatusEvent) error {
17+
r.reportRecipeFailedCallCount++
18+
return r.reportRecipeFailedErr
19+
}
20+
21+
func (r *mockExecutionStatusReporter) reportRecipeInstalled(event recipeStatusEvent) error {
22+
r.reportRecipeInstalledCallCount++
23+
return r.reportRecipeInstalledErr
24+
}
25+
26+
func (r *mockExecutionStatusReporter) reportRecipesAvailable(recipes []recipe) error {
27+
r.reportRecipesAvailableCallCount++
28+
return r.reportRecipesAvailableErr
29+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package install
2+
3+
import "github.com/newrelic/newrelic-client-go/pkg/nerdstorage"
4+
5+
// nolint:unused,deadcode
6+
type mockNerdstorageClient struct {
7+
respBody interface{}
8+
userScopeError error
9+
entityScopeError error
10+
writeDocumentWithUserScopeCallCount int
11+
writeDocumentWithEntityScopeCallCount int
12+
}
13+
14+
// nolint:unused,deadcode
15+
func newMockNerdstorageClient() *mockNerdstorageClient {
16+
return &mockNerdstorageClient{
17+
respBody: struct{}{},
18+
userScopeError: nil,
19+
entityScopeError: nil,
20+
}
21+
}
22+
23+
func (c *mockNerdstorageClient) WriteDocumentWithUserScope(nerdstorage.WriteDocumentInput) (interface{}, error) {
24+
c.writeDocumentWithUserScopeCallCount++
25+
return c.respBody, c.userScopeError
26+
}
27+
28+
func (c *mockNerdstorageClient) WriteDocumentWithEntityScope(string, nerdstorage.WriteDocumentInput) (interface{}, error) {
29+
c.writeDocumentWithEntityScopeCallCount++
30+
return c.respBody, c.entityScopeError
31+
}
Lines changed: 35 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,58 @@
11
package install
22

3-
import "context"
3+
import (
4+
"context"
5+
)
46

57
type mockRecipeFetcher struct {
6-
fetchRecipeFunc func(*discoveryManifest, string) (*recipe, error)
7-
fetchRecipesFunc func() ([]recipe, error)
8-
fetchRecommendationsFunc func(*discoveryManifest) ([]recipe, error)
8+
fetchRecipeErr error
9+
fetchRecipesErr error
10+
fetchRecommendationsErr error
11+
fetchRecipeCallCount int
12+
fetchRecipesCallCount int
13+
fetchRecommendationsCallCount int
14+
fetchRecipeVals []recipe
15+
fetchRecipeVal *recipe
16+
fetchRecipesVal []recipe
17+
fetchRecommendationsVal []recipe
918
}
1019

1120
func newMockRecipeFetcher() *mockRecipeFetcher {
1221
f := mockRecipeFetcher{}
13-
f.fetchRecipeFunc = defaultFetchRecipeFunc
14-
f.fetchRecipesFunc = defaultFetchRecipesFunc
15-
f.fetchRecommendationsFunc = defaultFetchRecommendationsFunc
16-
22+
f.fetchRecipesVal = []recipe{}
23+
f.fetchRecommendationsVal = []recipe{}
1724
return &f
1825
}
1926

2027
func (f *mockRecipeFetcher) fetchRecipe(ctx context.Context, manifest *discoveryManifest, friendlyName string) (*recipe, error) {
21-
return f.fetchRecipeFunc(manifest, friendlyName)
28+
f.fetchRecipeCallCount++
29+
30+
if len(f.fetchRecipeVals) > 0 {
31+
i := minOf(f.fetchRecipeCallCount, len(f.fetchRecipeVals)) - 1
32+
return &f.fetchRecipeVals[i], f.fetchRecipesErr
33+
}
34+
35+
return f.fetchRecipeVal, f.fetchRecipeErr
2236
}
2337

2438
func (f *mockRecipeFetcher) fetchRecipes(ctx context.Context) ([]recipe, error) {
25-
return f.fetchRecipesFunc()
39+
f.fetchRecipesCallCount++
40+
return f.fetchRecipesVal, f.fetchRecipesErr
2641
}
2742

2843
func (f *mockRecipeFetcher) fetchRecommendations(ctx context.Context, manifest *discoveryManifest) ([]recipe, error) {
29-
return f.fetchRecommendationsFunc(manifest)
44+
f.fetchRecommendationsCallCount++
45+
return f.fetchRecommendationsVal, f.fetchRecommendationsErr
3046
}
3147

32-
func defaultFetchRecipeFunc(manifest *discoveryManifest, friendlyName string) (*recipe, error) {
33-
return &recipe{}, nil
34-
}
48+
func minOf(vars ...int) int {
49+
min := vars[0]
3550

36-
func defaultFetchRecommendationsFunc(manifest *discoveryManifest) ([]recipe, error) {
37-
return []recipe{}, nil
38-
}
51+
for _, i := range vars {
52+
if min > i {
53+
min = i
54+
}
55+
}
3956

40-
func defaultFetchRecipesFunc() ([]recipe, error) {
41-
return []recipe{}, nil
57+
return min
4258
}

internal/install/mock_recipe_validator.go

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,17 @@ package install
33
import "context"
44

55
type mockRecipeValidator struct {
6-
result func(discoveryManifest, recipe) (bool, error)
6+
validateErr error
7+
validateCallCount int
8+
validateVal bool
9+
validateEntityGUIDVal string
710
}
811

912
func newMockRecipeValidator() *mockRecipeValidator {
10-
return &mockRecipeValidator{
11-
result: func(discoveryManifest, recipe) (bool, error) { return false, nil },
12-
}
13+
return &mockRecipeValidator{}
1314
}
1415

15-
func (m *mockRecipeValidator) validate(ctx context.Context, dm discoveryManifest, r recipe) (bool, error) {
16-
return m.result(dm, r)
16+
func (m *mockRecipeValidator) validate(ctx context.Context, dm discoveryManifest, r recipe) (bool, string, error) {
17+
m.validateCallCount++
18+
return m.validateVal, m.validateEntityGUIDVal, m.validateErr
1719
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package install
2+
3+
import (
4+
"github.com/newrelic/newrelic-client-go/pkg/nerdstorage"
5+
)
6+
7+
type nerdstorageClient interface {
8+
WriteDocumentWithUserScope(nerdstorage.WriteDocumentInput) (interface{}, error)
9+
WriteDocumentWithEntityScope(string, nerdstorage.WriteDocumentInput) (interface{}, error)
10+
}

0 commit comments

Comments
 (0)