Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"enabledPlugins": {
"datum-platform@datum-claude-code-plugins": true,
"datum-gtm@datum-claude-code-plugins": true
},
"extraKnownMarketplaces": {
"datum-claude-code-plugins": {
"source": {
"source": "github",
"repo": "datum-cloud/claude-code-plugins"
}
}
}
}
59 changes: 59 additions & 0 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
name: E2E Tests

on:
push:
branches:
- main
pull_request:
branches:
- main

jobs:
e2e:
name: Run Chainsaw E2E Tests
runs-on: ubuntu-latest
steps:
- name: Clone the code
uses: actions/checkout@v4

- name: Setup Go
uses: actions/setup-go@v5
with:
go-version-file: go.mod

- name: Setup Kind
uses: helm/kind-action@v1
with:
install_only: true

- name: Bootstrap e2e environment
run: make bootstrap-e2e

- name: Wait for controllers to be ready
run: |
kubectl --context kind-dns-upstream -n dns-replicator-system rollout status deployment/dns-operator-controller-manager --timeout=120s
kubectl --context kind-dns-downstream -n dns-agent-system rollout status statefulset/pdns-auth --timeout=120s

- name: Prepare kubeconfigs for Chainsaw
run: make chainsaw-prepare-kubeconfigs

- name: Run Chainsaw tests
run: make chainsaw-test

- name: Collect diagnostic logs
if: failure()
run: |
echo "=== Upstream controller logs ==="
kubectl --context kind-dns-upstream -n dns-replicator-system logs deployment/dns-operator-controller-manager --tail=200 || true
echo ""
echo "=== Upstream controller describe ==="
kubectl --context kind-dns-upstream -n dns-replicator-system describe deployment/dns-operator-controller-manager || true
echo ""
echo "=== Downstream agent logs ==="
kubectl --context kind-dns-downstream -n dns-agent-system logs statefulset/pdns-auth --tail=100 || true
echo ""
echo "=== DNSZones in upstream ==="
kubectl --context kind-dns-upstream get dnszones -A || true
echo ""
echo "=== DNSZones in downstream ==="
kubectl --context kind-dns-downstream get dnszones -A || true
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,6 @@ go.work.sum
# Editor/IDE
# .idea/
# .vscode/

# Local claude settings
.claude/settings.local.json
6 changes: 6 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,7 @@ secret-from-file: ## Create or update a secret from a file in NAMESPACE on CONTE
bootstrap-downstream: ## Create kind downstream and deploy agent with embedded PowerDNS
CLUSTER=$(DOWNSTREAM_CLUSTER_NAME) $(MAKE) kind-create
CLUSTER=$(DOWNSTREAM_CLUSTER_NAME) $(MAKE) kind-load-image
CONTEXT=kind-$(DOWNSTREAM_CLUSTER_NAME) $(MAKE) install-dns-operator-crds
CONTEXT=kind-$(DOWNSTREAM_CLUSTER_NAME) KUSTOMIZE_DIR=config/overlays/agent-powerdns $(MAKE) kustomize-apply
# Export external kubeconfig for downstream cluster (reachable from host/other containers)
CLUSTER=$(DOWNSTREAM_CLUSTER_NAME) OUT=dev/kind.downstream.kubeconfig $(MAKE) export-kind-kubeconfig-raw
Expand Down Expand Up @@ -459,6 +460,11 @@ install-networking-crds: controller-gen ## Generate and install networking servi
output:crd:dir=dev/crds/network-services
$(KUBECTL) --context $(CONTEXT) apply -f dev/crds/network-services

.PHONY: install-dns-operator-crds
install-dns-operator-crds: kustomize ## Install DNS operator CRDs into CONTEXT
@test -n "$(CONTEXT)" || { echo "CONTEXT is required (e.g., kind-$(DOWNSTREAM_CLUSTER_NAME))"; exit 1; }
$(KUSTOMIZE) build config/crd | $(KUBECTL) --context $(CONTEXT) apply -f -

.PHONY: install-gateway-api-crds
install-gateway-api-crds: ## Install Kubernetes Gateway API CRDs into CONTEXT
@test -n "$(CONTEXT)" || { echo "CONTEXT is required (e.g., kind-$(UPSTREAM_CLUSTER_NAME))"; exit 1; }
Expand Down
7 changes: 7 additions & 0 deletions config/milo/activity/kustomization.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# SPDX-License-Identifier: AGPL-3.0-only

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

resources:
- policies
142 changes: 142 additions & 0 deletions config/milo/activity/policies/dnsrecordset-policy.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
# SPDX-License-Identifier: AGPL-3.0-only

# ActivityPolicy for DNSRecordSet resources.
# Defines how DNSRecordSet API operations and controller events appear in activity timelines.
#
# Audit rules handle CRUD operations captured by the Kubernetes API server audit log.
# Event rules handle async controller events for programming outcomes.
#
# Design principles:
# - Use FQDNs (www.example.com) as display text, not resource names
# - Show record values (IPs, CNAME targets, MX hosts) where available
# - Action-oriented language ("added", "removed", "is now resolving to")
# - Type-specific phrasing for common record types
# - Graceful fallback to resource name when display annotations are missing
#
# Controller-provided annotations on resource (for audit rules):
# dns.networking.miloapis.com/display-name: www.example.com
# dns.networking.miloapis.com/display-value: 192.0.2.10
#
# Controller-provided annotations on events:
# dns.networking.miloapis.com/display-name: www.example.com
# dns.networking.miloapis.com/cname-target: api.internal.example.com
# dns.networking.miloapis.com/mx-hosts: 10 mail.example.com, 20 mail2.example.com
apiVersion: activity.miloapis.com/v1alpha1
kind: ActivityPolicy
metadata:
name: dns.networking.miloapis.com-dnsrecordset
spec:
resource:
apiGroup: dns.networking.miloapis.com
kind: DNSRecordSet

# Audit log rules for CRUD operations.
# These use display-name and display-value annotations set by the controller.
# Rules are ordered from most specific to fallback.
# All rules include fallbacks for resources without display annotations.
auditRules:
# ----- CREATE RULES (type-specific with display annotations) -----
# A/AAAA records with display annotations: "added www.example.com pointing to 192.0.2.10"
- match: "audit.verb == 'create' && audit.responseObject.spec.recordType in ['A', 'AAAA'] && has(audit.responseObject.metadata.annotations) && 'dns.networking.miloapis.com/display-name' in audit.responseObject.metadata.annotations"
summary: "{{ actor }} added {{ link(audit.responseObject.metadata.annotations['dns.networking.miloapis.com/display-name'], audit.responseObject) }} pointing to {{ audit.responseObject.metadata.annotations['dns.networking.miloapis.com/display-value'] }}"

# CNAME records with display annotations: "added api.example.com as an alias for api.internal.example.com"
- match: "audit.verb == 'create' && audit.responseObject.spec.recordType == 'CNAME' && has(audit.responseObject.metadata.annotations) && 'dns.networking.miloapis.com/display-name' in audit.responseObject.metadata.annotations"
summary: "{{ actor }} added {{ link(audit.responseObject.metadata.annotations['dns.networking.miloapis.com/display-name'], audit.responseObject) }} as an alias for {{ audit.responseObject.metadata.annotations['dns.networking.miloapis.com/display-value'] }}"

# ALIAS records with display annotations: "added example.com as an alias for lb.example.com"
- match: "audit.verb == 'create' && audit.responseObject.spec.recordType == 'ALIAS' && has(audit.responseObject.metadata.annotations) && 'dns.networking.miloapis.com/display-name' in audit.responseObject.metadata.annotations"
summary: "{{ actor }} added {{ link(audit.responseObject.metadata.annotations['dns.networking.miloapis.com/display-name'], audit.responseObject) }} as an alias for {{ audit.responseObject.metadata.annotations['dns.networking.miloapis.com/display-value'] }}"

# MX records with display annotations: "configured mail for example.com using 10 mail.example.com"
- match: "audit.verb == 'create' && audit.responseObject.spec.recordType == 'MX' && has(audit.responseObject.metadata.annotations) && 'dns.networking.miloapis.com/display-name' in audit.responseObject.metadata.annotations"
summary: "{{ actor }} configured mail for {{ link(audit.responseObject.metadata.annotations['dns.networking.miloapis.com/display-name'], audit.responseObject) }} using {{ audit.responseObject.metadata.annotations['dns.networking.miloapis.com/display-value'] }}"

# TXT records with display annotations: "added TXT record for example.com"
- match: "audit.verb == 'create' && audit.responseObject.spec.recordType == 'TXT' && has(audit.responseObject.metadata.annotations) && 'dns.networking.miloapis.com/display-name' in audit.responseObject.metadata.annotations"
summary: "{{ actor }} added TXT record for {{ link(audit.responseObject.metadata.annotations['dns.networking.miloapis.com/display-name'], audit.responseObject) }}"

# NS records with display annotations: "added nameservers for sub.example.com"
- match: "audit.verb == 'create' && audit.responseObject.spec.recordType == 'NS' && has(audit.responseObject.metadata.annotations) && 'dns.networking.miloapis.com/display-name' in audit.responseObject.metadata.annotations"
summary: "{{ actor }} added nameservers for {{ link(audit.responseObject.metadata.annotations['dns.networking.miloapis.com/display-name'], audit.responseObject) }}"

# Generic create with display annotations: "added SRV record for _sip._tcp.example.com"
- match: "audit.verb == 'create' && has(audit.responseObject.metadata.annotations) && 'dns.networking.miloapis.com/display-name' in audit.responseObject.metadata.annotations"
summary: "{{ actor }} added {{ audit.responseObject.spec.recordType }} record for {{ link(audit.responseObject.metadata.annotations['dns.networking.miloapis.com/display-name'], audit.responseObject) }}"

# Create fallback (no display annotations): "added A record my-recordset"
- match: "audit.verb == 'create'"
summary: "{{ actor }} added {{ audit.responseObject.spec.recordType }} record {{ link(audit.objectRef.name, audit.responseObject) }}"

# ----- DELETE RULES -----
# Delete with display annotations available
- match: "audit.verb == 'delete' && has(audit.responseObject.metadata.annotations) && 'dns.networking.miloapis.com/display-name' in audit.responseObject.metadata.annotations"
summary: "{{ actor }} removed {{ link(audit.responseObject.metadata.annotations['dns.networking.miloapis.com/display-name'], audit.responseObject) }} ({{ audit.responseObject.spec.recordType }})"

# Delete fallback (no display annotations): "removed A record my-recordset"
- match: "audit.verb == 'delete'"
summary: "{{ actor }} removed {{ audit.responseObject.spec.recordType }} record {{ link(audit.objectRef.name, audit.objectRef) }}"

# ----- UPDATE RULES (type-specific with display annotations) -----
# A/AAAA records with display annotations: "updated www.example.com to point to 192.0.2.20"
- match: "audit.verb in ['update', 'patch'] && !has(audit.objectRef.subresource) && audit.responseObject.spec.recordType in ['A', 'AAAA'] && has(audit.responseObject.metadata.annotations) && 'dns.networking.miloapis.com/display-name' in audit.responseObject.metadata.annotations"
summary: "{{ actor }} updated {{ link(audit.responseObject.metadata.annotations['dns.networking.miloapis.com/display-name'], audit.responseObject) }} to point to {{ audit.responseObject.metadata.annotations['dns.networking.miloapis.com/display-value'] }}"

# CNAME records with display annotations: "updated api.example.com to alias api.v2.example.com"
- match: "audit.verb in ['update', 'patch'] && !has(audit.objectRef.subresource) && audit.responseObject.spec.recordType == 'CNAME' && has(audit.responseObject.metadata.annotations) && 'dns.networking.miloapis.com/display-name' in audit.responseObject.metadata.annotations"
summary: "{{ actor }} updated {{ link(audit.responseObject.metadata.annotations['dns.networking.miloapis.com/display-name'], audit.responseObject) }} to alias {{ audit.responseObject.metadata.annotations['dns.networking.miloapis.com/display-value'] }}"

# Generic update with display annotations
- match: "audit.verb in ['update', 'patch'] && !has(audit.objectRef.subresource) && has(audit.responseObject.metadata.annotations) && 'dns.networking.miloapis.com/display-name' in audit.responseObject.metadata.annotations"
summary: "{{ actor }} updated {{ audit.responseObject.spec.recordType }} record {{ link(audit.responseObject.metadata.annotations['dns.networking.miloapis.com/display-name'], audit.responseObject) }}"

# Update fallback (no display annotations): "updated A record my-recordset"
- match: "audit.verb in ['update', 'patch'] && !has(audit.objectRef.subresource)"
summary: "{{ actor }} updated {{ audit.responseObject.spec.recordType }} record {{ link(audit.objectRef.name, audit.responseObject) }}"

# Status subresource updates are system-initiated and typically not shown to users
# - match: "audit.verb in ['update', 'patch'] && audit.objectRef.subresource == 'status'"
# summary: "System updated status of {{ link(audit.objectRef.name, audit.responseObject) }}"

# Event rules for controller-emitted Kubernetes events.
# These capture async outcomes that audit logs cannot represent.
# Events always have annotations set by the controller, so no fallback needed.
#
# Available annotations on all events:
# dns.networking.miloapis.com/event-type: dns.recordset.programmed
# dns.networking.miloapis.com/domain-name: example.com
# dns.networking.miloapis.com/record-type: A
# dns.networking.miloapis.com/record-names: www
# dns.networking.miloapis.com/record-count: 1
# dns.networking.miloapis.com/display-name: www.example.com
#
# Type-specific annotations:
# dns.networking.miloapis.com/ip-addresses: 192.0.2.10 (A/AAAA only)
# dns.networking.miloapis.com/cname-target: target.example.com (CNAME only)
# dns.networking.miloapis.com/mx-hosts: 10 mail.example.com (MX only)
eventRules:
# ----- PROGRAMMED EVENTS (type-specific, conversational tone) -----
# A/AAAA records: "Your record is live! www.example.com resolves to 192.0.2.10"
- match: "event.reason == 'RecordSetProgrammed' && event.annotations['dns.networking.miloapis.com/record-type'] in ['A', 'AAAA']"
summary: "Your record is live! {{ link(event.annotations['dns.networking.miloapis.com/display-name'], event.regarding) }} resolves to {{ event.annotations['dns.networking.miloapis.com/ip-addresses'] }}"

# CNAME records: "Your record is live! api.example.com resolves to api.internal.example.com"
- match: "event.reason == 'RecordSetProgrammed' && event.annotations['dns.networking.miloapis.com/record-type'] == 'CNAME'"
summary: "Your record is live! {{ link(event.annotations['dns.networking.miloapis.com/display-name'], event.regarding) }} resolves to {{ event.annotations['dns.networking.miloapis.com/cname-target'] }}"

# MX records: "Mail is ready! example.com routes to 10 mail.example.com"
- match: "event.reason == 'RecordSetProgrammed' && event.annotations['dns.networking.miloapis.com/record-type'] == 'MX'"
summary: "Mail is ready! {{ link(event.annotations['dns.networking.miloapis.com/display-name'], event.regarding) }} routes to {{ event.annotations['dns.networking.miloapis.com/mx-hosts'] }}"

# TXT records: "Your TXT record is live! example.com"
- match: "event.reason == 'RecordSetProgrammed' && event.annotations['dns.networking.miloapis.com/record-type'] == 'TXT'"
summary: "Your TXT record is live! {{ link(event.annotations['dns.networking.miloapis.com/display-name'], event.regarding) }}"

# Generic programmed fallback: "Your NS record is live! sub.example.com"
- match: "event.reason == 'RecordSetProgrammed'"
summary: "Your {{ event.annotations['dns.networking.miloapis.com/record-type'] }} record is live! {{ link(event.annotations['dns.networking.miloapis.com/display-name'], event.regarding) }}"

# ----- PROGRAMMING FAILED EVENTS -----
# "We hit a snag configuring www.example.com: Provider API error"
- match: "event.reason == 'RecordSetProgrammingFailed'"
summary: "We hit a snag configuring {{ link(event.annotations['dns.networking.miloapis.com/display-name'], event.regarding) }}: {{ event.annotations['dns.networking.miloapis.com/failure-reason'] }}"
65 changes: 65 additions & 0 deletions config/milo/activity/policies/dnszone-policy.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# SPDX-License-Identifier: AGPL-3.0-only

# ActivityPolicy for DNSZone resources.
# Defines how DNSZone API operations and controller events appear in activity timelines.
#
# Audit rules handle CRUD operations captured by the Kubernetes API server audit log.
# Event rules handle async controller events for programming outcomes.
#
# Design principles:
# - Use domain names (example.com) as display text, not resource names (example-com)
# - Action-oriented language ("created zone", not "created DNS zone")
# - Show relevant DNS data (nameservers) where available
apiVersion: activity.miloapis.com/v1alpha1
kind: ActivityPolicy
metadata:
name: dns.networking.miloapis.com-dnszone
spec:
resource:
apiGroup: dns.networking.miloapis.com
kind: DNSZone

# Audit log rules for CRUD operations.
# These are automatically captured by the API server and don't require controller code.
auditRules:
# Zone creation - use domain name as display text in link
- match: "audit.verb == 'create'"
summary: "{{ actor }} created zone {{ link(audit.responseObject.spec.domainName, audit.responseObject) }}"

# Zone deletion - use domain name from request object (response may be empty)
- match: "audit.verb == 'delete'"
summary: "{{ actor }} deleted zone {{ audit.requestObject.spec.domainName }}"

# Zone update - use domain name as display text
- match: "audit.verb in ['update', 'patch'] && !has(audit.objectRef.subresource)"
summary: "{{ actor }} updated zone {{ link(audit.responseObject.spec.domainName, audit.responseObject) }}"

# Status subresource updates are system-initiated and typically not shown to users,
# but can be enabled if needed for debugging.
# - match: "audit.verb in ['update', 'patch'] && audit.objectRef.subresource == 'status'"
# summary: "System updated status of zone {{ link(audit.responseObject.spec.domainName, audit.responseObject) }}"

# Event rules for controller-emitted Kubernetes events.
# These capture async outcomes that audit logs cannot represent.
# Uses conversational tone for system-initiated events.
eventRules:
# ZoneProgrammed: DNS zone successfully programmed to the DNS provider.
# Annotations available:
# dns.networking.miloapis.com/event-type: dns.zone.programmed
# dns.networking.miloapis.com/domain-name: example.com
# dns.networking.miloapis.com/zone-class: production
# dns.networking.miloapis.com/nameservers: ns1.example.com,ns2.example.com
# dns.networking.miloapis.com/resource-name: my-zone
# dns.networking.miloapis.com/resource-namespace: my-project
- match: "event.reason == 'ZoneProgrammed'"
summary: "Your zone is ready! {{ link(event.annotations['dns.networking.miloapis.com/domain-name'], event.regarding) }} is live with nameservers {{ event.annotations['dns.networking.miloapis.com/nameservers'] }}"

# ZoneProgrammingFailed: DNS zone programming failed after previous success.
# Annotations available:
# dns.networking.miloapis.com/event-type: dns.zone.programming_failed
# dns.networking.miloapis.com/domain-name: example.com
# dns.networking.miloapis.com/failure-reason: Provider API error
# dns.networking.miloapis.com/resource-name: my-zone
# dns.networking.miloapis.com/resource-namespace: my-project
- match: "event.reason == 'ZoneProgrammingFailed'"
summary: "We hit a snag configuring {{ link(event.annotations['dns.networking.miloapis.com/domain-name'], event.regarding) }}: {{ event.annotations['dns.networking.miloapis.com/failure-reason'] }}"
8 changes: 8 additions & 0 deletions config/milo/activity/policies/kustomization.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# SPDX-License-Identifier: AGPL-3.0-only

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

resources:
- dnszone-policy.yaml
- dnsrecordset-policy.yaml
Loading
Loading