/
checker.go
190 lines (164 loc) · 5.03 KB
/
checker.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
// Package checker provides a framework for running a set of checks against an OpenStack cloud.
package checker
import (
"bytes"
"context"
"os"
"time"
"github.com/gophercloud/gophercloud"
"github.com/gophercloud/gophercloud/openstack"
"github.com/gophercloud/utils/openstack/clientconfig"
"golang.org/x/exp/slog"
"golang.org/x/sync/errgroup"
)
// CheckerFactory creates a new `Checker` instance
type CheckerFactory func(authOpts *gophercloud.AuthOptions, opts CloudOptions) (Checker, error) //nolint:revive // checker.CheckerFactory is fine
// Checker is a single check that can be run against an OpenStack cloud.
// E.g. create a network, create a server, etc.
type Checker interface {
GetName() string
Check(ctx context.Context, providerClient *gophercloud.ProviderClient, region string, output *bytes.Buffer) error
}
// CheckResult stores the result from Checker.Check.
// It is used to produce metrics or display results.
type CheckResult struct {
Cloud string
Name string
Error error
Start time.Time
Duration time.Duration
Output string
}
// CheckResultCallback is a callback function that is called for each CheckResult.
// If true is returned, then additional checks should be stopped.
type CheckResultCallback func(r CheckResult) bool
// CheckManager runs a set of checks against an OpenStack cloud
type CheckManager struct {
authOpts *gophercloud.AuthOptions
opts CloudOptions
checks []Checker
cloud string
region string
}
// New creates a new CheckManager instance
func New(cloud string, opts CloudOptions, factories []CheckerFactory) (*CheckManager, error) {
clientopts := clientconfig.ClientOpts{Cloud: cloud}
authOpts, err := clientconfig.AuthOptions(&clientopts)
if err != nil {
return nil, err
}
// would be nice if clientconfig.AuthOptions() would somehow return the region to us
region := os.Getenv("OS_REGION_NAME")
if cloud != "" {
var clientConfigCloud *clientconfig.Cloud
clientConfigCloud, err = clientconfig.GetCloudFromYAML(&clientopts)
if err != nil {
return nil, err
}
region = clientConfigCloud.RegionName
}
cm := &CheckManager{
authOpts: authOpts,
opts: opts,
cloud: cloud,
region: region,
}
for i := range factories {
checkfactory := factories[i]
check, err := checkfactory(authOpts, opts)
if err != nil {
return nil, err
}
cm.checks = append(cm.checks, check)
}
return cm, nil
}
// Run runs all registered checks in parallel and calls the callback function for each result
func (cm *CheckManager) Run(ctx context.Context, callback CheckResultCallback, checks ...string) error {
g := errgroup.Group{}
for _, c := range cm.getChecksToRun(checks...) {
check := c // loop invariant
g.Go(func() error {
interval := 60
timeout := interval
if _, err := cm.opts.Int(check.GetName(), "interval", &interval); err != nil {
return err
}
if _, err := cm.opts.Int(check.GetName(), "timeout", &timeout); err != nil {
return err
}
ticker := time.NewTicker(time.Duration(interval) * time.Second)
for {
// Run the check immediately
slog.Debug("running check",
"check", check.GetName(),
"interval", interval,
"timeout", timeout,
)
var output bytes.Buffer
start := time.Now()
// We consciously create a new client from scratch on each run instead
// of re-authenticating a client across multiple runs. This allows us to
// verify the token workflow more like a real client would.
providerClient, err := cm.createAuthenticatedClient()
if err == nil {
checkCtx, cancel := context.WithTimeout(ctx, time.Duration(timeout)*time.Second)
err = check.Check(checkCtx, providerClient, cm.region, &output)
cancel()
}
// callback even if we failed to create the providerClient
done := callback(CheckResult{
Cloud: cm.cloud,
Name: check.GetName(),
Error: err,
Start: start,
Duration: time.Since(start),
Output: output.String(),
})
if done {
return nil
}
// Wait for the next interval, or until the context is done
select {
case <-ctx.Done():
return nil
case <-ticker.C:
}
}
})
}
return g.Wait()
}
// GetCloud returns the cloud that this manager has been configured for
func (cm *CheckManager) GetCloud() string {
return cm.cloud
}
func (cm *CheckManager) getChecksToRun(checks ...string) []Checker {
var checksToRun []Checker
if len(checks) > 0 {
checksToRun = make([]Checker, 0, len(checks))
for _, name := range checks {
for _, check := range cm.checks {
if check.GetName() == name {
checksToRun = append(checksToRun, check)
break
}
}
}
} else {
checksToRun = cm.checks
}
return checksToRun
}
func (cm *CheckManager) createAuthenticatedClient() (*gophercloud.ProviderClient, error) {
providerClient, err := openstack.NewClient(cm.authOpts.IdentityEndpoint)
if err != nil {
return nil, err
}
providerClient.HTTPClient = newHTTPClient()
err = openstack.Authenticate(providerClient, *cm.authOpts)
if err != nil {
return nil, err
}
return providerClient, nil
}