Skip to content

Commit c1576d0

Browse files
authored
Merge pull request #555 from abiosoft/subscribe-lifecycle-events
Subscribe to particular lifecycle events
2 parents 1010dae + c1611f2 commit c1576d0

11 files changed

Lines changed: 182 additions & 7 deletions

File tree

doc/.wordlist.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ JSON
2626
keypair
2727
lang
2828
LLMs
29+
Lifecycle
2930
MacOS
3031
NIC
3132
NICs

doc/reference/events.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,10 @@ Records of particular API or migration actions, along with associated metadata.
6767
"scopes": [
6868
"lifecycle",
6969
"logging"
70+
],
71+
"lifecycle_actions": [
72+
"instance-imported",
73+
"migration-created"
7074
]
7175
}
7276
]

doc/reference/settings.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ See [Events](events) for more information about logging events.
2727
| `retry_count` | Number of attempts to make against the logging target. | number | 3 |
2828
| `retry_timeout` | How long to wait between retrying a log. | number(h/m/s) | 10s |
2929
| `scopes` | Logging scopes to send to the logging target. | list of strings | `logging`,`lifecycle` |
30+
| `lifecycle_actions` | Lifecycle event actions to send. If empty, all are sent. | list of strings | |
3031

3132
## Network settings
3233

doc/rest-api.yaml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -726,6 +726,9 @@ definitions:
726726
x-go-name: AllowUnknownOS
727727
type: object
728728
x-go-package: github.com/FuturFusion/migration-manager/shared/api
729+
LifecycleAction:
730+
type: string
731+
x-go-package: github.com/FuturFusion/migration-manager/shared/api
729732
LogScope:
730733
title: LogScope is a type of log that a logging target will receive.
731734
type: string
@@ -1214,6 +1217,13 @@ definitions:
12141217
example: WARN
12151218
type: string
12161219
x-go-name: Level
1220+
lifecycle_actions:
1221+
description: Lifecycle event actions to receive when the "lifecycle" scope is enabled. If empty, all lifecycle events are sent.
1222+
example: '[instance-imported, migration-created]'
1223+
items:
1224+
$ref: '#/definitions/LifecycleAction'
1225+
type: array
1226+
x-go-name: LifecycleActions
12171227
name:
12181228
description: Name identifying the logging target.
12191229
example: foo

internal/logger/webhook.go

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,10 @@ import (
2424
)
2525

2626
type webhookLog struct {
27-
name string
28-
level slog.Level
29-
scopes []api.LogScope
27+
name string
28+
level slog.Level
29+
scopes []api.LogScope
30+
actions []api.LifecycleAction
3031

3132
client *http.Client
3233
address string
@@ -139,7 +140,8 @@ func WebhookConfigChanged(oldCfgs, newCfgs []api.SystemSettingsLog) bool {
139140
oldCfgs[i].CACert != newCfgs[i].CACert ||
140141
oldCfgs[i].RetryCount != newCfgs[i].RetryCount ||
141142
oldCfgs[i].RetryTimeout != newCfgs[i].RetryTimeout ||
142-
!slices.Equal(oldCfgs[i].Scopes, newCfgs[i].Scopes) {
143+
!slices.Equal(oldCfgs[i].Scopes, newCfgs[i].Scopes) ||
144+
!slices.Equal(oldCfgs[i].LifecycleActions, newCfgs[i].LifecycleActions) {
143145
return true
144146
}
145147
}
@@ -157,6 +159,7 @@ func NewWebhookLogger(cfg api.SystemSettingsLog) (slog.Handler, error) {
157159
password: cfg.Password,
158160
retry: cfg.RetryCount,
159161
scopes: cfg.Scopes,
162+
actions: cfg.LifecycleActions,
160163

161164
client: &http.Client{},
162165
}
@@ -220,8 +223,14 @@ func (w *webhookLog) Handle(ctx context.Context, r slog.Record) error {
220223
} else {
221224
var b []byte
222225
var err error
226+
var action api.LifecycleAction
223227
r.Attrs(func(a slog.Attr) bool {
224228
if a.Key == "event" {
229+
lifecycle, ok := a.Value.Any().(api.EventLifecycle)
230+
if ok {
231+
action = api.LifecycleAction(lifecycle.Action)
232+
}
233+
225234
b, err = json.Marshal(a.Value.Any())
226235
if err != nil {
227236
return false
@@ -234,6 +243,13 @@ func (w *webhookLog) Handle(ctx context.Context, r slog.Record) error {
234243
return err
235244
}
236245

246+
if len(w.actions) > 0 {
247+
// verify action in the list of filtered lifecycle actions
248+
if !slices.Contains(w.actions, action) {
249+
return nil
250+
}
251+
}
252+
237253
event.Type = api.LogScopeLifecycle
238254
event.Metadata = b
239255
}

internal/logger/webhook_test.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,75 @@ func TestLogWebhook(t *testing.T) {
171171
return log.Error
172172
},
173173
},
174+
{
175+
name: "success - lifecycle event type matches",
176+
numReqs: 2,
177+
cfg: api.SystemSettingsLog{
178+
Name: "webhook",
179+
Type: api.LogTypeWebhook,
180+
Level: "warn",
181+
Address: "*",
182+
RetryCount: 3,
183+
RetryTimeout: api.AsDuration(10 * time.Second),
184+
Scopes: []api.LogScope{api.LogScopeLifecycle, api.LogScopeLogging},
185+
LifecycleActions: []api.LifecycleAction{event.MigrationCreated},
186+
},
187+
instanceData: api.Instance{
188+
Source: "src1",
189+
InstanceProperties: api.InstanceProperties{
190+
UUID: uuidA,
191+
Location: "/path/to/instance1",
192+
InstancePropertiesConfigurable: api.InstancePropertiesConfigurable{Name: "instance1"},
193+
NICs: []api.InstancePropertiesNIC{{UUID: uuidB}, {UUID: uuidC}},
194+
},
195+
},
196+
queueData: api.QueueEntry{
197+
InstanceUUID: uuidA,
198+
InstanceName: "instance1",
199+
BatchName: "batch1",
200+
MigrationWindow: api.MigrationWindow{Name: "window1", Config: api.MigrationWindowConfig{Capacity: 10}},
201+
Placement: api.Placement{TargetName: "tgt1"},
202+
},
203+
204+
wantResps: []api.Event{
205+
{Type: api.LogScopeLifecycle, Metadata: []byte("lifecycle")}, // apply wantLifecycle
206+
defaultLog,
207+
},
208+
wantLifecycle: api.EventLifecycle{
209+
Action: string(event.MigrationCreated),
210+
Entities: []string{
211+
"/1.0/queue/" + uuidA.String(),
212+
"/1.0/instances/" + uuidA.String(),
213+
"/1.0/batches/batch1",
214+
"/1.0/sources/src1",
215+
"/1.0/targets/tgt1",
216+
"/1.0/networks/" + uuidB.String(),
217+
"/1.0/networks/" + uuidC.String(),
218+
},
219+
Metadata: []byte("*"), // apply objects
220+
},
221+
sendLog: func(log *slog.Logger) func(msg string, args ...any) {
222+
return log.Error
223+
},
224+
},
225+
{
226+
name: "success - lifecycle event type does not match",
227+
numReqs: 1,
228+
cfg: api.SystemSettingsLog{
229+
Name: "webhook",
230+
Type: api.LogTypeWebhook,
231+
Level: "warn",
232+
Address: "*",
233+
RetryCount: 3,
234+
RetryTimeout: api.AsDuration(10 * time.Second),
235+
Scopes: []api.LogScope{api.LogScopeLifecycle, api.LogScopeLogging},
236+
LifecycleActions: []api.LifecycleAction{event.InstanceImported},
237+
},
238+
wantResps: []api.Event{defaultLog},
239+
sendLog: func(log *slog.Logger) func(msg string, args ...any) {
240+
return log.Error
241+
},
242+
},
174243
{
175244
name: "success - discard log level",
176245
numReqs: 0,

shared/api/system.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,10 @@ type SystemSettingsLog struct {
8585
// Logging scopes to send to the logging target.
8686
// Example: [logging, lifecycle]
8787
Scopes []LogScope `json:"scopes" yaml:"scopes"`
88+
89+
// Lifecycle event actions to receive when the "lifecycle" scope is enabled. If empty, all lifecycle events are sent.
90+
// Example: [instance-imported, migration-created]
91+
LifecycleActions []LifecycleAction `json:"lifecycle_actions" yaml:"lifecycle_actions"`
8892
}
8993

9094
// SystemNetwork represents the system's network configuration.

ui/src/App.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,11 @@ function App() {
5757
<Route path="/ui" element={<Home />} />
5858
<Route path="/ui/settings" element={<Settings />} />
5959
<Route path="/ui/settings/:activeTab" element={<Settings />} />
60-
<Route path="/ui/settings/logging/add" element={<Settings />} />
61-
<Route path="/ui/settings/logging/:itemId" element={<Settings />} />
60+
<Route path="/ui/settings/:activeTab/add" element={<Settings />} />
61+
<Route
62+
path="/ui/settings/:activeTab/:itemId"
63+
element={<Settings />}
64+
/>
6265
<Route path="/ui/sources" element={<Source />} />
6366
<Route path="/ui/sources/create" element={<SourceCreate />} />
6467
<Route path="/ui/sources/:name" element={<SourceDetail />} />

ui/src/components/SystemLoggingForm.tsx

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,12 @@ import { FC } from "react";
22
import { Button, Form } from "react-bootstrap";
33
import { useFormik } from "formik";
44
import { SystemSettingsLog } from "types/settings";
5-
import { LogLevel, LogScopeValues, LogTypeValues } from "util/settings";
5+
import {
6+
LogLevel,
7+
LogScopeValues,
8+
LogTypeValues,
9+
LifecycleActionValues,
10+
} from "util/settings";
611

712
interface Props {
813
logTarget?: SystemSettingsLog;
@@ -21,6 +26,7 @@ const SystemLoggingForm: FC<Props> = ({ logTarget, index, onSubmit }) => {
2126
ca_cert: logTarget?.ca_cert ?? "",
2227
retry_count: logTarget?.retry_count ?? 0,
2328
scopes: logTarget?.scopes ?? [],
29+
event_types: logTarget?.event_types ?? [],
2430
};
2531

2632
const formik = useFormik({
@@ -142,6 +148,24 @@ const SystemLoggingForm: FC<Props> = ({ logTarget, index, onSubmit }) => {
142148
))}
143149
</Form.Select>
144150
</Form.Group>
151+
<Form.Group className="mb-3" controlId="event_types">
152+
<Form.Label>
153+
Lifecycle event types (requires lifecycle scope)
154+
</Form.Label>
155+
<Form.Select
156+
name="event_types"
157+
multiple
158+
value={formik.values.event_types}
159+
onChange={formik.handleChange}
160+
onBlur={formik.handleBlur}
161+
>
162+
{LifecycleActionValues.map((option) => (
163+
<option key={option} value={option}>
164+
{option}
165+
</option>
166+
))}
167+
</Form.Select>
168+
</Form.Group>
145169
</Form>
146170
</div>
147171
<div className="fixed-footer p-3">

ui/src/types/settings.d.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {
22
ACMEChallengeValues,
33
LogTypeValues,
44
LogScopeValues,
5+
LifecycleActionValues,
56
} from "util/settings";
67

78
export interface SystemNetwork {
@@ -11,6 +12,7 @@ export interface SystemNetwork {
1112

1213
export type LogType = (typeof LogTypeValues)[number];
1314
export type LogScope = (typeof LogScopeValues)[number];
15+
export type LifecycleAction = (typeof LifecycleActionValues)[number];
1416

1517
export interface SystemSettingsLog {
1618
name: string;
@@ -22,6 +24,7 @@ export interface SystemSettingsLog {
2224
ca_cert: string;
2325
retry_count: number;
2426
scopes: LogScope[];
27+
event_types: LifecycleAction[];
2528
}
2629

2730
export interface SystemSettings {

0 commit comments

Comments
 (0)