Use a typed escalation fallback instead of fallbackTopic
TerdutTeam spec.escalation.fallback is now {type, address}, mirroring the
server's notification channel model. CRDs regenerated.
Claude-Session: https://claude.ai/code/session_016mBLURvJoMuUEr9cB2RpUN
This commit is contained in:
@@ -197,7 +197,7 @@ spec:
|
||||
oidc: {memberGroup: terdut-platform-members, ownerGroup: terdut-platform-owners}
|
||||
escalation:
|
||||
repeatCount: 2
|
||||
fallbackTopic: platform-oncall
|
||||
fallback: {type: ntfy, address: platform-oncall}
|
||||
levels:
|
||||
- timeout: 5m
|
||||
targets: [{kind: oncall}, {kind: user, username: alice}]
|
||||
|
||||
@@ -94,6 +94,24 @@ type EscalationLevel struct {
|
||||
Targets []EscalationTarget `json:"targets"`
|
||||
}
|
||||
|
||||
// NotificationTarget is somewhere a page can go without belonging to a person:
|
||||
// a notification provider the server has configured, and an address that
|
||||
// provider understands.
|
||||
type NotificationTarget struct {
|
||||
// type is the notification provider, e.g. "ntfy". The server refuses a
|
||||
// provider it has not been configured with.
|
||||
// +required
|
||||
// +kubebuilder:validation:MinLength=1
|
||||
// +kubebuilder:validation:MaxLength=32
|
||||
Type string `json:"type"`
|
||||
|
||||
// address is where that provider delivers it: an ntfy topic, say.
|
||||
// +required
|
||||
// +kubebuilder:validation:MinLength=1
|
||||
// +kubebuilder:validation:MaxLength=256
|
||||
Address string `json:"address"`
|
||||
}
|
||||
|
||||
// EscalationSpec is a team's escalation ladder.
|
||||
type EscalationSpec struct {
|
||||
// +kubebuilder:validation:Minimum=0
|
||||
@@ -101,8 +119,10 @@ type EscalationSpec struct {
|
||||
// +optional
|
||||
RepeatCount int64 `json:"repeatCount,omitempty"`
|
||||
|
||||
// fallback is paged once when the ladder has run out of levels and repeats.
|
||||
// Omitted, the chain just ends there.
|
||||
// +optional
|
||||
FallbackTopic string `json:"fallbackTopic,omitempty"`
|
||||
Fallback *NotificationTarget `json:"fallback,omitempty"`
|
||||
|
||||
// +required
|
||||
// +kubebuilder:validation:MinItems=1
|
||||
|
||||
@@ -110,6 +110,11 @@ func (in *EscalationLevel) DeepCopy() *EscalationLevel {
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *EscalationSpec) DeepCopyInto(out *EscalationSpec) {
|
||||
*out = *in
|
||||
if in.Fallback != nil {
|
||||
in, out := &in.Fallback, &out.Fallback
|
||||
*out = new(NotificationTarget)
|
||||
**out = **in
|
||||
}
|
||||
if in.Levels != nil {
|
||||
in, out := &in.Levels, &out.Levels
|
||||
*out = make([]EscalationLevel, len(*in))
|
||||
@@ -189,6 +194,21 @@ func (in *NetworkingSpec) DeepCopy() *NetworkingSpec {
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *NotificationTarget) DeepCopyInto(out *NotificationTarget) {
|
||||
*out = *in
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NotificationTarget.
|
||||
func (in *NotificationTarget) DeepCopy() *NotificationTarget {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(NotificationTarget)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *NotifySpec) DeepCopyInto(out *NotifySpec) {
|
||||
*out = *in
|
||||
|
||||
@@ -116,8 +116,28 @@ spec:
|
||||
escalation is this team's escalation ladder. Omitted, the team has none
|
||||
(the server's plain reminder behaviour applies).
|
||||
properties:
|
||||
fallbackTopic:
|
||||
type: string
|
||||
fallback:
|
||||
description: |-
|
||||
fallback is paged once when the ladder has run out of levels and repeats.
|
||||
Omitted, the chain just ends there.
|
||||
properties:
|
||||
address:
|
||||
description: 'address is where that provider delivers it:
|
||||
an ntfy topic, say.'
|
||||
maxLength: 256
|
||||
minLength: 1
|
||||
type: string
|
||||
type:
|
||||
description: |-
|
||||
type is the notification provider, e.g. "ntfy". The server refuses a
|
||||
provider it has not been configured with.
|
||||
maxLength: 32
|
||||
minLength: 1
|
||||
type: string
|
||||
required:
|
||||
- address
|
||||
- type
|
||||
type: object
|
||||
levels:
|
||||
items:
|
||||
description: |-
|
||||
|
||||
@@ -113,8 +113,28 @@ spec:
|
||||
escalation is this team's escalation ladder. Omitted, the team has none
|
||||
(the server's plain reminder behaviour applies).
|
||||
properties:
|
||||
fallbackTopic:
|
||||
type: string
|
||||
fallback:
|
||||
description: |-
|
||||
fallback is paged once when the ladder has run out of levels and repeats.
|
||||
Omitted, the chain just ends there.
|
||||
properties:
|
||||
address:
|
||||
description: 'address is where that provider delivers it:
|
||||
an ntfy topic, say.'
|
||||
maxLength: 256
|
||||
minLength: 1
|
||||
type: string
|
||||
type:
|
||||
description: |-
|
||||
type is the notification provider, e.g. "ntfy". The server refuses a
|
||||
provider it has not been configured with.
|
||||
maxLength: 32
|
||||
minLength: 1
|
||||
type: string
|
||||
required:
|
||||
- address
|
||||
- type
|
||||
type: object
|
||||
levels:
|
||||
items:
|
||||
description: |-
|
||||
|
||||
@@ -41,6 +41,12 @@ spec:
|
||||
sweeper:
|
||||
staleAfter: 6h
|
||||
archiveAfter: 168h
|
||||
# No ntfy anywhere in this demo can actually be reached; the URL only has to
|
||||
# exist so the server has an "ntfy" notification provider and accepts the
|
||||
# teams' escalation fallback (02, 03), which names one. A server with no
|
||||
# provider refuses a fallback it could never deliver.
|
||||
notify:
|
||||
ntfyURL: "http://ntfy.invalid"
|
||||
# No oidc block: password login only, so there's nothing external to
|
||||
# register a redirect URI with before this demo can sign in.
|
||||
passwordLogin: true
|
||||
|
||||
@@ -24,7 +24,9 @@ spec:
|
||||
# until that person exists -- run-demo.sh creates alice for exactly that.
|
||||
escalation:
|
||||
repeatCount: 2
|
||||
fallbackTopic: platform-fallback
|
||||
fallback:
|
||||
type: ntfy
|
||||
address: platform-fallback
|
||||
levels:
|
||||
- timeout: 5m
|
||||
targets:
|
||||
|
||||
@@ -8,7 +8,9 @@ spec:
|
||||
displayName: Payments
|
||||
escalation:
|
||||
repeatCount: 1
|
||||
fallbackTopic: payments-fallback
|
||||
fallback:
|
||||
type: ntfy
|
||||
address: payments-fallback
|
||||
levels:
|
||||
- timeout: 5m
|
||||
targets:
|
||||
|
||||
@@ -55,10 +55,14 @@ func buildEscalationRequest(spec *terdutv1alpha1.EscalationSpec) (tdclient.SetEs
|
||||
Targets: targets,
|
||||
}
|
||||
}
|
||||
var fallback tdclient.NotificationTarget
|
||||
if spec.Fallback != nil {
|
||||
fallback = tdclient.NotificationTarget{Type: spec.Fallback.Type, Address: spec.Fallback.Address}
|
||||
}
|
||||
return tdclient.SetEscalationRequest{
|
||||
RepeatCount: spec.RepeatCount,
|
||||
FallbackTopic: spec.FallbackTopic,
|
||||
Levels: levels,
|
||||
RepeatCount: spec.RepeatCount,
|
||||
Fallback: fallback,
|
||||
Levels: levels,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -300,7 +300,7 @@ var _ = Describe("TerdutTeam Controller", func() {
|
||||
Describe("spec.escalation", func() {
|
||||
ladder := func() *terdutv1alpha1.EscalationSpec {
|
||||
return &terdutv1alpha1.EscalationSpec{
|
||||
RepeatCount: 2, FallbackTopic: "oncall",
|
||||
RepeatCount: 2, Fallback: &terdutv1alpha1.NotificationTarget{Type: "ntfy", Address: "oncall"},
|
||||
Levels: []terdutv1alpha1.EscalationLevel{
|
||||
{Timeout: "5m", Targets: []terdutv1alpha1.EscalationTarget{{Kind: terdutv1alpha1.EscalationTargetOncall}}},
|
||||
{Timeout: "15m", Targets: []terdutv1alpha1.EscalationTarget{{Kind: terdutv1alpha1.EscalationTargetUser, Username: "alice"}}},
|
||||
@@ -320,7 +320,7 @@ var _ = Describe("TerdutTeam Controller", func() {
|
||||
|
||||
got := fake.escalation[getTeam(ctx).Status.TeamID]
|
||||
Expect(got.RepeatCount).To(Equal(int64(2)))
|
||||
Expect(got.FallbackTopic).To(Equal("oncall"))
|
||||
Expect(got.Fallback).To(Equal(tdclient.NotificationTarget{Type: "ntfy", Address: "oncall"}))
|
||||
Expect(got.Levels).To(HaveLen(2))
|
||||
Expect(got.Levels[0].TimeoutSeconds).To(Equal(int64(300)))
|
||||
Expect(got.Levels[1].Targets[0]).To(Equal(tdclient.EscalationTargetRequest{Kind: "user", Username: "alice"}))
|
||||
|
||||
@@ -208,10 +208,17 @@ type EscalationLevelRequest struct {
|
||||
Targets []EscalationTargetRequest `json:"targets"`
|
||||
}
|
||||
|
||||
// NotificationTarget is a notification provider and an address it understands.
|
||||
// Both empty means none.
|
||||
type NotificationTarget struct {
|
||||
Type string `json:"type"`
|
||||
Address string `json:"address"`
|
||||
}
|
||||
|
||||
type SetEscalationRequest struct {
|
||||
RepeatCount int64 `json:"repeat_count"`
|
||||
FallbackTopic string `json:"fallback_topic"`
|
||||
Levels []EscalationLevelRequest `json:"levels"`
|
||||
RepeatCount int64 `json:"repeat_count"`
|
||||
Fallback NotificationTarget `json:"fallback"`
|
||||
Levels []EscalationLevelRequest `json:"levels"`
|
||||
}
|
||||
|
||||
// SetEscalation calls PUT /api/teams/{teamID}/escalation -- an upsert
|
||||
|
||||
Reference in New Issue
Block a user