Skip to content

Commit d4a464a

Browse files
scotwellsclaude
andcommitted
feat: add ActivityPolicy configurations for activity timelines
Add ActivityPolicy resources for DNSZone and DNSRecordSet that define how API operations and controller events appear in activity timelines: DNSZone policies: - Audit rules for create/update/delete from API server audit logs - Event rules for ZoneProgrammed and ZoneProgrammingFailed events - Templates use domain-name and nameservers annotations for rich summaries DNSRecordSet policies: - Audit rules for create/update/delete from API server audit logs - Event rules for RecordSetProgrammed and RecordSetProgrammingFailed events - Separate templates for single vs multi-record sets - Templates use record-type, record-names, domain-name annotations Also updates event annotations to properly handle multi-record sets: - Rename record-name to record-names (plural) - Collect all unique owner names, not just the first The policies leverage the structured annotations emitted by the DNS controllers (dns.networking.miloapis.com/* prefix) to build human-readable activity summaries without hardcoding display text in the controller code. Co-Authored-By: Claude Opus 4.5 <[email protected]>
1 parent a7cd813 commit d4a464a

File tree

6 files changed

+174
-5
lines changed

6 files changed

+174
-5
lines changed

config/activity/kustomization.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# SPDX-License-Identifier: AGPL-3.0-only
2+
3+
apiVersion: kustomize.config.k8s.io/v1beta1
4+
kind: Kustomization
5+
6+
resources:
7+
- policies
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
# SPDX-License-Identifier: AGPL-3.0-only
2+
3+
# ActivityPolicy for DNSRecordSet resources.
4+
# Defines how DNSRecordSet API operations and controller events appear in activity timelines.
5+
#
6+
# Audit rules handle CRUD operations captured by the Kubernetes API server audit log.
7+
# Event rules handle async controller events for programming outcomes.
8+
apiVersion: activity.miloapis.com/v1alpha1
9+
kind: ActivityPolicy
10+
metadata:
11+
name: dns.networking.miloapis.com-dnsrecordset
12+
spec:
13+
resource:
14+
apiGroup: dns.networking.miloapis.com
15+
kind: DNSRecordSet
16+
17+
# Audit log rules for CRUD operations.
18+
# These are automatically captured by the API server and don't require controller code.
19+
auditRules:
20+
- match: "audit.verb == 'create'"
21+
summary: "{{ actor }} created {{ audit.responseObject.spec.recordType }} record {{ link(audit.objectRef.name, audit.responseObject) }} in zone {{ audit.responseObject.spec.dnsZoneRef.name }}"
22+
23+
- match: "audit.verb == 'delete'"
24+
summary: "{{ actor }} deleted DNS record {{ audit.objectRef.name }}"
25+
26+
- match: "audit.verb in ['update', 'patch'] && !audit.objectRef.subresource"
27+
summary: "{{ actor }} updated {{ audit.requestObject.spec.recordType }} record {{ link(audit.objectRef.name, audit.objectRef) }}"
28+
29+
# Status subresource updates are system-initiated and typically not shown to users,
30+
# but can be enabled if needed for debugging.
31+
# - match: "audit.verb in ['update', 'patch'] && audit.objectRef.subresource == 'status'"
32+
# summary: "System updated status of DNS record {{ link(audit.objectRef.name, audit.objectRef) }}"
33+
34+
# Event rules for controller-emitted Kubernetes events.
35+
# These capture async outcomes that audit logs cannot represent.
36+
#
37+
# A DNSRecordSet can contain multiple records with potentially different names
38+
# (e.g., "www" and "api" A records in one set). The annotations reflect this:
39+
# - record-count: total number of record entries
40+
# - record-names: comma-separated unique names (e.g., "www" or "www,api")
41+
# - ip-addresses: all IPs for A/AAAA types (may span multiple names)
42+
eventRules:
43+
# RecordSetProgrammed: DNS records successfully programmed to the DNS provider.
44+
# Annotations available:
45+
# dns.networking.miloapis.com/event-type: dns.recordset.programmed
46+
# dns.networking.miloapis.com/domain-name: example.com
47+
# dns.networking.miloapis.com/record-type: A
48+
# dns.networking.miloapis.com/record-names: www (or "www,api" for multi-name)
49+
# dns.networking.miloapis.com/record-count: 2
50+
# dns.networking.miloapis.com/ip-addresses: 192.168.1.1,192.168.1.2 (A/AAAA only)
51+
# dns.networking.miloapis.com/zone-ref: my-zone
52+
# dns.networking.miloapis.com/resource-name: my-recordset
53+
# dns.networking.miloapis.com/resource-namespace: my-project
54+
55+
# Single A/AAAA record (most common case): show name and IPs
56+
- match: "event.reason == 'RecordSetProgrammed' && event.annotations['dns.networking.miloapis.com/record-type'] in ['A', 'AAAA'] && event.annotations['dns.networking.miloapis.com/record-count'] == '1'"
57+
summary: "{{ event.annotations['dns.networking.miloapis.com/record-type'] }} record {{ link(event.regarding.name, event.regarding) }} ({{ event.annotations['dns.networking.miloapis.com/record-names'] }}.{{ event.annotations['dns.networking.miloapis.com/domain-name'] }} -> {{ event.annotations['dns.networking.miloapis.com/ip-addresses'] }}) is now active"
58+
59+
# Multiple A/AAAA records: show count and names
60+
- match: "event.reason == 'RecordSetProgrammed' && event.annotations['dns.networking.miloapis.com/record-type'] in ['A', 'AAAA'] && event.annotations['dns.networking.miloapis.com/record-count'] != '1'"
61+
summary: "{{ event.annotations['dns.networking.miloapis.com/record-count'] }} {{ event.annotations['dns.networking.miloapis.com/record-type'] }} records {{ link(event.regarding.name, event.regarding) }} ({{ event.annotations['dns.networking.miloapis.com/record-names'] }}.{{ event.annotations['dns.networking.miloapis.com/domain-name'] }}) are now active"
62+
63+
# Single non-IP record: show name
64+
- match: "event.reason == 'RecordSetProgrammed' && !(event.annotations['dns.networking.miloapis.com/record-type'] in ['A', 'AAAA']) && event.annotations['dns.networking.miloapis.com/record-count'] == '1'"
65+
summary: "{{ event.annotations['dns.networking.miloapis.com/record-type'] }} record {{ link(event.regarding.name, event.regarding) }} ({{ event.annotations['dns.networking.miloapis.com/record-names'] }}.{{ event.annotations['dns.networking.miloapis.com/domain-name'] }}) is now active"
66+
67+
# Multiple non-IP records: show count and names
68+
- match: "event.reason == 'RecordSetProgrammed' && !(event.annotations['dns.networking.miloapis.com/record-type'] in ['A', 'AAAA']) && event.annotations['dns.networking.miloapis.com/record-count'] != '1'"
69+
summary: "{{ event.annotations['dns.networking.miloapis.com/record-count'] }} {{ event.annotations['dns.networking.miloapis.com/record-type'] }} records {{ link(event.regarding.name, event.regarding) }} ({{ event.annotations['dns.networking.miloapis.com/record-names'] }}.{{ event.annotations['dns.networking.miloapis.com/domain-name'] }}) are now active"
70+
71+
# RecordSetProgrammingFailed: DNS record programming failed after previous success.
72+
# Annotations available:
73+
# dns.networking.miloapis.com/event-type: dns.recordset.programming_failed
74+
# dns.networking.miloapis.com/domain-name: example.com
75+
# dns.networking.miloapis.com/record-type: A
76+
# dns.networking.miloapis.com/record-names: www
77+
# dns.networking.miloapis.com/failure-reason: Provider API error
78+
# dns.networking.miloapis.com/zone-ref: my-zone
79+
# dns.networking.miloapis.com/resource-name: my-recordset
80+
# dns.networking.miloapis.com/resource-namespace: my-project
81+
- match: "event.reason == 'RecordSetProgrammingFailed'"
82+
summary: "{{ event.annotations['dns.networking.miloapis.com/record-type'] }} record {{ link(event.regarding.name, event.regarding) }} programming failed: {{ event.annotations['dns.networking.miloapis.com/failure-reason'] }}"
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# SPDX-License-Identifier: AGPL-3.0-only
2+
3+
# ActivityPolicy for DNSZone resources.
4+
# Defines how DNSZone API operations and controller events appear in activity timelines.
5+
#
6+
# Audit rules handle CRUD operations captured by the Kubernetes API server audit log.
7+
# Event rules handle async controller events for programming outcomes.
8+
apiVersion: activity.miloapis.com/v1alpha1
9+
kind: ActivityPolicy
10+
metadata:
11+
name: dns.networking.miloapis.com-dnszone
12+
spec:
13+
resource:
14+
apiGroup: dns.networking.miloapis.com
15+
kind: DNSZone
16+
17+
# Audit log rules for CRUD operations.
18+
# These are automatically captured by the API server and don't require controller code.
19+
auditRules:
20+
- match: "audit.verb == 'create'"
21+
summary: "{{ actor }} created DNS zone {{ link(audit.objectRef.name, audit.responseObject) }} for domain {{ audit.responseObject.spec.domainName }}"
22+
23+
- match: "audit.verb == 'delete'"
24+
summary: "{{ actor }} deleted DNS zone {{ audit.objectRef.name }}"
25+
26+
- match: "audit.verb in ['update', 'patch'] && !audit.objectRef.subresource"
27+
summary: "{{ actor }} updated DNS zone {{ link(audit.objectRef.name, audit.objectRef) }}"
28+
29+
# Status subresource updates are system-initiated and typically not shown to users,
30+
# but can be enabled if needed for debugging.
31+
# - match: "audit.verb in ['update', 'patch'] && audit.objectRef.subresource == 'status'"
32+
# summary: "System updated status of DNS zone {{ link(audit.objectRef.name, audit.objectRef) }}"
33+
34+
# Event rules for controller-emitted Kubernetes events.
35+
# These capture async outcomes that audit logs cannot represent.
36+
eventRules:
37+
# ZoneProgrammed: DNS zone successfully programmed to the DNS provider.
38+
# Annotations available:
39+
# dns.networking.miloapis.com/event-type: dns.zone.programmed
40+
# dns.networking.miloapis.com/domain-name: example.com
41+
# dns.networking.miloapis.com/zone-class: production
42+
# dns.networking.miloapis.com/nameservers: ns1.example.com,ns2.example.com
43+
# dns.networking.miloapis.com/resource-name: my-zone
44+
# dns.networking.miloapis.com/resource-namespace: my-project
45+
- match: "event.reason == 'ZoneProgrammed'"
46+
summary: "DNS zone {{ link(event.regarding.name, event.regarding) }} for {{ event.annotations['dns.networking.miloapis.com/domain-name'] }} is now active with nameservers {{ event.annotations['dns.networking.miloapis.com/nameservers'] }}"
47+
48+
# ZoneProgrammingFailed: DNS zone programming failed after previous success.
49+
# Annotations available:
50+
# dns.networking.miloapis.com/event-type: dns.zone.programming_failed
51+
# dns.networking.miloapis.com/domain-name: example.com
52+
# dns.networking.miloapis.com/failure-reason: Provider API error
53+
# dns.networking.miloapis.com/resource-name: my-zone
54+
# dns.networking.miloapis.com/resource-namespace: my-project
55+
- match: "event.reason == 'ZoneProgrammingFailed'"
56+
summary: "DNS zone {{ link(event.regarding.name, event.regarding) }} programming failed: {{ event.annotations['dns.networking.miloapis.com/failure-reason'] }}"
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# SPDX-License-Identifier: AGPL-3.0-only
2+
3+
apiVersion: kustomize.config.k8s.io/v1beta1
4+
kind: Kustomization
5+
6+
resources:
7+
- dnszone-policy.yaml
8+
- dnsrecordset-policy.yaml

internal/controller/events.go

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ const (
3232
// DNSRecordSet-specific annotations.
3333
AnnotationZoneRef = "dns.networking.miloapis.com/zone-ref"
3434
AnnotationRecordCount = "dns.networking.miloapis.com/record-count"
35-
AnnotationRecordName = "dns.networking.miloapis.com/record-name"
35+
AnnotationRecordNames = "dns.networking.miloapis.com/record-names"
3636
AnnotationIPAddresses = "dns.networking.miloapis.com/ip-addresses"
3737

3838
// Shared failure annotation.
@@ -169,8 +169,8 @@ func recordRecordSetActivityEventWithData(
169169
}
170170
if len(rs.Spec.Records) > 0 {
171171
annotations[AnnotationRecordCount] = strconv.Itoa(len(rs.Spec.Records))
172-
// Use the first record entry's name as the record name annotation.
173-
annotations[AnnotationRecordName] = rs.Spec.Records[0].Name
172+
// Collect unique record names preserving order of first occurrence.
173+
annotations[AnnotationRecordNames] = strings.Join(uniqueRecordNames(rs), ",")
174174
}
175175
// Extract IP addresses for A/AAAA record types.
176176
if ips := extractIPAddresses(rs); len(ips) > 0 {
@@ -182,6 +182,22 @@ func recordRecordSetActivityEventWithData(
182182
recorder.AnnotatedEventf(rs, annotations, eventType, reason, messageFmt, args...)
183183
}
184184

185+
// uniqueRecordNames returns a deduplicated list of record names from the
186+
// DNSRecordSet, preserving the order of first occurrence. This handles the
187+
// common case where multiple records share the same name (e.g., round-robin A
188+
// records) as well as the less common case of different names in one set.
189+
func uniqueRecordNames(rs *dnsv1alpha1.DNSRecordSet) []string {
190+
seen := make(map[string]struct{})
191+
var names []string
192+
for _, r := range rs.Spec.Records {
193+
if _, ok := seen[r.Name]; !ok {
194+
seen[r.Name] = struct{}{}
195+
names = append(names, r.Name)
196+
}
197+
}
198+
return names
199+
}
200+
185201
// extractIPAddresses collects IP address content values from A and AAAA record
186202
// entries in the given DNSRecordSet. Returns nil for all other record types.
187203
func extractIPAddresses(rs *dnsv1alpha1.DNSRecordSet) []string {

internal/controller/events_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -689,8 +689,8 @@ func TestRecordRecordSetActivityEventWithData_ResourceIdentityAnnotations(t *tes
689689
if got := ann[AnnotationRecordCount]; got != "2" {
690690
t.Errorf("%s = %q, want %q", AnnotationRecordCount, got, "2")
691691
}
692-
if got := ann[AnnotationRecordName]; got != "www" {
693-
t.Errorf("%s = %q, want %q", AnnotationRecordName, got, "www")
692+
if got := ann[AnnotationRecordNames]; got != "www,api" {
693+
t.Errorf("%s = %q, want %q", AnnotationRecordNames, got, "www,api")
694694
}
695695
}
696696

0 commit comments

Comments
 (0)