3 Commits

Author SHA1 Message Date
Niklas Ye 3036abf70e Set the chart's placeholder version to 0.6.0
CI / chart (push) Successful in 1s
CI / security (push) Successful in 1m21s
CI / test (push) Successful in 7m8s
Release / test (push) Successful in 1m39s
Release / chart (push) Successful in 3s
Release / image (push) Successful in 7m37s
Release / scan-image (push) Successful in 3s
Cosmetic: make helm-package passes --version and --app-version from the tag,
so these fields decide nothing about what is published. They are kept in
step so the tree does not state an older number than the release it heads
for.

Claude-Session: https://claude.ai/code/session_016mBLURvJoMuUEr9cB2RpUN
2026-10-12 21:42:30 +02:00
Niklas Ye 2d8fe0f275 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
2026-10-12 21:42:30 +02:00
niklas 7fb8042265 Merge pull request 'Authenticate with a seeded operator key; fold escalation and switches into TerdutTeam' (#12) from operator-key-and-team-config into main
CI / chart (push) Successful in 1s
CI / security (push) Successful in 56s
CI / test (push) Successful in 2m31s
Reviewed-on: #12
2026-10-09 20:20:13 +00:00
12 changed files with 119 additions and 18 deletions
+1 -1
View File
@@ -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}]
+21 -1
View File
@@ -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
+20
View File
@@ -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
+2 -2
View File
@@ -6,8 +6,8 @@ type: application
# These fields decide nothing: `make helm-package` passes --version and
# --app-version from the release tag (same reasoning as terdut-server's own
# chart). They're for whoever reads the tree before a tag exists.
version: 0.5.0
appVersion: "v0.5.0"
version: 0.6.0
appVersion: "v0.6.0"
keywords:
- kubernetes
@@ -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: |-
+6
View File
@@ -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
+3 -1
View File
@@ -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:
+3 -1
View File
@@ -8,7 +8,9 @@ spec:
displayName: Payments
escalation:
repeatCount: 1
fallbackTopic: payments-fallback
fallback:
type: ntfy
address: payments-fallback
levels:
- timeout: 5m
targets:
+7 -3
View File
@@ -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"}))
+10 -3
View File
@@ -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