6 Commits

Author SHA1 Message Date
Niklas Ye 295dc3a9ec Set the chart's placeholder version to 0.6.1
Release / image (push) Successful in 6m45s
CI / chart (push) Successful in 1s
CI / security (push) Successful in 1m32s
Release / chart (push) Successful in 1s
Release / scan-image (push) Successful in 36s
CI / test (push) Successful in 7m24s
Release / test (push) Successful in 1m36s
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, as in 3036abf.

Claude-Session: https://claude.ai/code/session_016mBLURvJoMuUEr9cB2RpUN
2026-10-22 09:35:04 +02:00
Niklas Ye 69e610a5c2 Scan and build with Go 1.26.10
govulncheck fails on GO-2026-6809, a crypto/x509 flaw in the standard
library that is fixed in 1.26.10. Move the workflows' golang image off 1.26.9
the way terdut-server did, so the scan and the shipped binary agree.

Claude-Session: https://claude.ai/code/session_016mBLURvJoMuUEr9cB2RpUN
2026-10-21 22:07:54 +02:00
Niklas Ye 93d2a00a00 Pin the demo to terdut-server v0.48.0
v0.43.0 predates v0.47.2, which lets the instance service account list a
team's dead man's switches. Without it GET /api/teams/{id}/deadman/switches
404s for the operator's key, so a TerdutTeam never becomes ready against the
demo's server. The demo is meant to track the current release.

Claude-Session: https://claude.ai/code/session_016mBLURvJoMuUEr9cB2RpUN
2026-10-21 22:07:50 +02:00
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
14 changed files with 124 additions and 23 deletions
+2 -2
View File
@@ -55,7 +55,7 @@ jobs:
test:
runs-on: ubuntu-latest
container:
image: golang:1.26.9-bookworm
image: golang:1.26.10-bookworm
volumes:
- go-mod-cache:/go/pkg/mod
- go-build-cache:/root/.cache/go-build
@@ -85,7 +85,7 @@ jobs:
security:
runs-on: ubuntu-latest
container:
image: golang:1.26.9-bookworm
image: golang:1.26.10-bookworm
volumes:
- go-mod-cache:/go/pkg/mod
- go-build-cache:/root/.cache/go-build
+1 -1
View File
@@ -26,7 +26,7 @@ jobs:
test:
runs-on: ubuntu-latest
container:
image: golang:1.26.9-bookworm
image: golang:1.26.10-bookworm
volumes:
- go-mod-cache:/go/pkg/mod
- go-build-cache:/root/.cache/go-build
+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.1
appVersion: "v0.6.1"
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: |-
+8 -2
View File
@@ -15,7 +15,7 @@ spec:
image:
repository: git.ryuvia.com/niklas/terdut-server
# v0.36.0 is the floor now that replicas below is 2 (this demo pins
# the current release, v0.43.0, so it shows the current web UI too): that
# the current release, v0.48.0, so it shows the current web UI too): that
# release put the sweeper, the notifier and the migration runner each
# behind a Postgres advisory lock, and gave incident creation its own
# conflict resolution, which is what makes a second replica safe
@@ -24,7 +24,7 @@ spec:
# can adopt/rotate a key on a team-scoped account it didn't just create
# in the same call -- without which terdutteam-* can wedge permanently
# on the crash-window race this demo hit live, niklas/terdut-operator#3.)
tag: v0.43.0
tag: v0.48.0
# Matches this CRD's own spec.replicas default (v0.4.0) -- stated
# explicitly, like every other field in this file, rather than left to
# the default. RollingUpdate follows automatically; this operator does
@@ -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