feat(uptime-monitor): deploy IngressMonitorController with zero monitors (REN-6)
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful

Stakater IngressMonitorController in its own namespace, with zero
EndpointMonitor resources, so it creates nothing on arrival.

- Vendored EndpointMonitor CRD from chart 2.2.13, image pinned to v2.2.13
  and deliberately kept out of Flux image automation.
- Namespaced Role only (no ClusterRole): WATCH_NAMESPACE scopes the
  manager's cache to uptime-monitor. Verbs derived from what v2.2.13
  actually calls; every omission is justified inline in rbac.yaml.
- The ExternalSecret now renders IMC's config.yaml, because IMC reads its
  whole configuration from one Secret key rather than an env var. Config
  stays in git; only the key is injected.
- Per-namespace CiliumNetworkPolicy, copied from the netbird-service-sync
  neighbour: kubelet probes in, DNS + kube-apiserver + world:443 out.
- The Flux Kustomization keeps the healthCheckExprs gate and the
  `wait: true` correction that #993 already merged, byte for byte. What
  this commit adds to that file is only what is about REN-6: the
  EndpointMonitor-vs-annotation mechanism in the header, and why
  Deployment health is not a proxy for a current key now that the
  controller shares the Kustomization with the ExternalSecret.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
atlas-bot
2026-09-26 00:29:56 +00:00
committed by atlas-bot
co-authored by Paperclip Claude Opus 5
parent 3d6c9940d6
commit c40c337e36
9 changed files with 807 additions and 18 deletions
@@ -0,0 +1,464 @@
# VENDORED, DO NOT EDIT BY HAND.
#
# Source: stakater/IngressMonitorController chart 2.2.13,
# charts/ingressmonitorcontroller/crds/endpointmonitor.stakater.com_endpointmonitors.yaml
# Copied verbatim at commit aaffd8a866aa09a77030979d938e766895bde5d4.
#
# Vendored rather than installed via HelmRelease because this Kustomization
# ships hand-written RBAC (see rbac.yaml) — the chart's own Role grants pods,
# services and `secrets: list`, none of which this controller uses. Pulling the
# chart for the CRD alone would drag those back in.
#
# To update: bump the image tag in deployment.yaml and re-copy this file from
# the matching chart tag. Nothing here is Renovate-managed, so the tag and this
# file move together, by hand, in one reviewable commit.
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.16.1
name: endpointmonitors.endpointmonitor.stakater.com
spec:
group: endpointmonitor.stakater.com
names:
kind: EndpointMonitor
listKind: EndpointMonitorList
plural: endpointmonitors
singular: endpointmonitor
scope: Namespaced
versions:
- name: v1alpha1
schema:
openAPIV3Schema:
description: EndpointMonitor is the Schema for the endpointmonitors API
properties:
apiVersion:
description: |-
APIVersion defines the versioned schema of this representation of an object.
Servers should convert recognized schemas to the latest internal value, and
may reject unrecognized values.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources
type: string
kind:
description: |-
Kind is a string value representing the REST resource this object represents.
Servers may infer this from the endpoint the client submits requests to.
Cannot be updated.
In CamelCase.
More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
type: string
metadata:
type: object
spec:
description: EndpointMonitorSpec defines the desired state of EndpointMonitor
properties:
appInsightsConfig:
description: Configuration for AppInsights Monitor Provider
properties:
frequency:
description: 'Sets how often the test should run from each test
location. Possible values: `300,600,900` seconds'
type: integer
retryEnable:
description: 'If its `true`, falied test will be retry after a
short interval. Possible values: `true, false`'
type: boolean
statusCode:
description: Returned status code that is counted as a success
type: integer
type: object
forceHttps:
description: Force monitor endpoint to use HTTPS
type: boolean
gcloudConfig:
description: Configuration for Google Cloud Monitor Provider
properties:
projectId:
description: Google Cloud Project ID
type: string
type: object
grafanaConfig:
description: Configuration for Grafana Cloud Monitor Provider
properties:
alertSensitivity:
default: none
description: |-
The alertSensitivity value defaults to none if there are no alerts or can be set to low, medium,
or high to correspond to the check alert levels.
enum:
- none
- low
- medium
- high
type: string
frequency:
description: The frequency value specifies how often the check
runs in milliseconds
format: int64
type: integer
probes:
description: |-
Probes are the monitoring agents responsible for simulating user interactions with your web applications
or services. These agents periodically send requests to predefined URLs and record the responses,
checking for expected outcomes and measuring performance.
items:
type: string
type: array
tenantId:
format: int64
type: integer
type: object
healthEndpoint:
type: string
pingdomConfig:
description: Configuration for Pingdom Monitor Provider
properties:
alertContacts:
description: '`-` separated contact id''s (e.g. "1234567_8_9-9876543_2_1")'
type: string
alertIntegrations:
description: '`-` separated set list of integrations ids (e.g.
"91166-12168")'
type: string
basicAuthUser:
description: Required for basic-authentication
type: string
notifyWhenBackUp:
description: Set to "false" to disable recovery notifications
type: boolean
paused:
description: Set to "true" to pause checks
type: boolean
postDataEnvVar:
description: |-
Data that should be posted to the web page, for example submission data for a sign-up or login form.
The data needs to be formatted in the same way as a web browser would send it to the web server.
Because post data contains sensitive secret this field is only a reference to an environment variable.
type: string
requestHeaders:
description: Custom request headers
type: string
requestHeadersEnvVar:
description: |-
Custom request headers that should be read from an environment variable as it possibly contains sensitive data.
An example would be an API token.
type: string
resolution:
description: The pingdom check interval in minutes
type: integer
sendNotificationWhenDown:
description: How many failed check attempts before notifying
type: integer
shouldContain:
description: Set to text string that has to be present in the
HTML code of the page
type: string
sslDownDaysBefore:
description: |-
Consider down prior to certificate expiring
Select the number of days prior to your certificate expiry date that you want to consider the check down.
At this day your check will be considered down and if applicable a down alert will be sent.
type: integer
tags:
description: Comma separated set of tags to apply to check (e.g.
"testing,aws")
type: string
teamAlertContacts:
description: '`-` separated team id''s (e.g. "1234567_8_9-9876543_2_1")'
type: string
verifyCertificate:
description: |-
Monitor SSL/TLS certificate
Monitor the validity of your SSL/TLS certificate. With this enabled Uptime checks will be considered DOWN when
the certificate becomes invalid or expires.
SSL/TLS certificate monitoring is available for HTTP checks.
type: boolean
type: object
pingdomTransactionConfig:
description: Configuration for Pingdom Transaction Monitor Provider
properties:
alertContacts:
description: '`-` separated contact id''s (e.g. "1234567_8_9-9876543_2_1")'
type: string
alertIntegrations:
description: '`-` separated set list of integrations ids (e.g.
"91166-12168")'
type: string
custom_message:
description: Custom message that is part of the email and webhook
alerts
type: string
interval:
description: 'TMS test intervals in minutes. Allowed intervals:
5,10,20,60,720,1440. The interval you''re allowed to set may
vary depending on your current plan.'
enum:
- 5
- 10
- 20
- 60
- 720
- 1440
type: integer
paused:
description: 'Check status: active or inactive'
type: boolean
region:
description: 'Name of the region where the check is executed.
Supported regions: us-east, us-west, eu, au'
enum:
- us-east
- us-west
- eu
- au
type: string
send_notification_when_down:
description: Send notification when down X times
format: int64
type: integer
severity_level:
description: 'Check importance- how important are the alerts when
the check fails. Allowed values: low, high'
enum:
- low
- high
type: string
steps:
description: steps to be executed as part of the check
items:
description: PingdomStep respresents a step of the script to
run a transcaction check
properties:
args:
additionalProperties:
type: string
description: |-
contains the html element with assigned value
the key element is always lowercase for example {"url": "https://www.pingdom.com"}
see available values at https://pkg.go.dev/github.com/karlderkaefer/pingdom-golang-client@latest/pkg/pingdom/client/tmschecks#StepArg
type: object
function:
description: |-
contains the function that is executed as part of the step
commands: go_to, click, fill, check, uncheck, sleep, select_radio, basic_auth, submit, wait_for_element, wait_for_contains
validations: url, exists, not_exists, contains, not_contains, field_contains, field_not_contains, is_checked, is_not_checked, radio_selected, dropdown_selected, dropdown_not_selected
see updated list https://docs.pingdom.com/api/#section/TMS-Steps-Vocabulary/Script-transaction-checks
type: string
required:
- args
- function
type: object
type: array
tags:
description: List of tags for a check. The tag name may contain
the characters 'A-Z', 'a-z', '0-9', '_' and '-'. The maximum
length of a tag is 64 characters.
items:
type: string
type: array
teamAlertContacts:
description: '`-` separated team id''s (e.g. "1234567_8_9-9876543_2_1")'
type: string
required:
- steps
type: object
providers:
description: Comma separated list of providers
type: string
statusCakeConfig:
description: Configuration for StatusCake Monitor Provider
properties:
basicAuthSecret:
description: Basic Auth Secret Name
type: string
basicAuthUser:
description: Basic Auth User
type: string
checkRate:
default: 300
description: Set Check Rate for the monitor.
type: integer
confirmation:
description: Confirmation value ranges from (0,10)
maximum: 10
minimum: 0
type: integer
contactGroup:
description: Contact Group to be alerted.
type: string
enableSslAlert:
description: Enable SSL Alert
type: boolean
findString:
description: String to look for within the response. Considered
down if not found
type: string
followRedirect:
description: Enable ingress redirects
type: boolean
paused:
description: Pause the service
type: boolean
pingUrl:
description: Webhook for alerts
type: string
port:
description: TCP Port
type: integer
rawPostData:
description: RawPostData can be used to send parameters within
the URL. Changes the request from a GET to a POST
type: string
realBrowser:
description: Enable Real Browser
type: boolean
regions:
description: Comma separated list of Node Location IDs
type: string
statusCodes:
description: Comma separated list of HTTP codes to trigger error
on
type: string
testTags:
description: Comma separated list of tags
type: string
testType:
description: Set Test type - HTTP, TCP, PING, or Heartbeat
enum:
- HTTP
- TCP
- PING
- Heartbeat
type: string
timeout:
description: Timeout is used to set a user agent string.
maximum: 75
minimum: 5
type: integer
triggerRate:
description: Minutes to wait before sending an alert
type: integer
userAgent:
description: UserAgent is used to set a user agent string.
type: string
type: object
x-kubernetes-validations:
- message: checkRate must be between 30 and 172800 seconds for Heartbeat
monitors
rule: self.testType != 'Heartbeat' || (self.checkRate >= 30 && self.checkRate
<= 172800)
- message: 'checkRate for uptime monitors must be one of: 0, 30, 60,
300, 900, 1800, 3600, 86400'
rule: self.testType == 'Heartbeat' || self.checkRate in [0, 30,
60, 300, 900, 1800, 3600, 86400]
updownConfig:
description: Configuration for Updown Monitor Provider
properties:
enable:
description: Enable or disable checks
type: boolean
period:
description: The pingdom check interval in seconds
type: integer
publishPage:
description: Make status page public or not
type: boolean
requestHeaders:
description: Additional request headers for API calls
type: string
type: object
uptimeConfig:
description: Configuration for Uptime Monitor Provider
properties:
checkType:
description: The uptime check type that can be HTTP/DNS/ICMP etc.
type: string
contacts:
description: Add one or more contact groups separated by `,`
type: string
interval:
description: The uptime check interval in seconds
type: integer
locations:
description: Add different locations for the check
type: string
tags:
description: Add one or more tags for the check separated by `,`
type: string
type: object
uptimeRobotConfig:
description: Configuration for UptimeRobot Monitor Provider
properties:
alertContacts:
description: The uptimerobot alertContacts to be associated with
this monitor
type: string
customHTTPStatuses:
description: |-
Defines which http status codes are treated as up or down
For ex: 200:0_401:1_503:1 (to accept 200 as down and 401 and 503 as up)
type: string
interval:
description: The uptimerobot check interval in seconds
minimum: 60
type: integer
keywordExists:
description: Alert if value exist (yes) or doesn't exist (no)
(Only if monitor-type is keyword)
enum:
- "yes"
- "no"
type: string
keywordValue:
description: keyword to check on URL (e.g.'search' or '404') (Only
if monitor-type is keyword)
type: string
maintenanceWindows:
description: Specify maintenanceWindows i.e. once or recurring
“do-not-monitor periods”
type: string
monitorType:
description: The uptimerobot monitor type (http or keyword)
enum:
- http
- keyword
type: string
statusPages:
description: The uptimerobot public status page ID to add this
monitor to
type: string
type: object
url:
description: URL to monitor
type: string
urlFrom:
description: URL to monitor from either an ingress or route reference
properties:
ingressRef:
description: IngressURLSource selects an Ingress to populate the
URL with
properties:
name:
type: string
required:
- name
type: object
routeRef:
description: RouteURLSource selects a Route to populate the URL
with
properties:
name:
type: string
required:
- name
type: object
type: object
type: object
status:
description: EndpointMonitorStatus defines the observed state of EndpointMonitor
type: object
type: object
served: true
storage: true
subresources:
status: {}
@@ -0,0 +1,118 @@
---
# Stakater IngressMonitorController (REN-6).
#
# It ships with zero EndpointMonitor resources, which is the whole point of this
# first deployment: the reconciler is keyed on one custom resource per event
# (endpointmonitor_controller.go:62-107) and never enumerates the UptimeRobot
# account, so with no EndpointMonitors it reconciles nothing and makes no API
# call to UptimeRobot at all. The 9 monitors already in that account are not
# read, not updated and not deleted.
#
# Plain manifests, not the upstream HelmRelease: the chart's RBAC grants pods,
# services and `secrets: list`, and it also creates a Service and optional
# cert-manager objects for a metrics endpoint that is disabled below. The
# maintained thing we depend on is the upstream image, which is pinned here.
apiVersion: apps/v1
kind: Deployment
metadata:
name: ingressmonitorcontroller
namespace: uptime-monitor
labels:
app.kubernetes.io/name: ingressmonitorcontroller
annotations:
# The API key is read ONCE, at startup (pkg/config/config.go
# `LoadControllerConfig` runs before the manager starts), so a rotated
# Secret is not picked up until the pod restarts. Reloader — which this
# cluster already runs — does that restart. Same idiom as
# apps/external-dns/helmrelease.yaml.
secret.reloader.stakater.com/reload: uptime-robot-api-key
spec:
replicas: 1
strategy:
# Recreate, not RollingUpdate, and no --leader-elect. A rolling update would
# briefly run two reconcilers against the same UptimeRobot account; Recreate
# makes "at most one controller pod, ever" a property of the Deployment
# instead of something a Lease has to arbitrate. It costs a few seconds of
# controller downtime per upgrade, which for a monitor-syncing controller is
# nothing, and it removes the leases/events RBAC entirely.
type: Recreate
selector:
matchLabels:
app.kubernetes.io/name: ingressmonitorcontroller
template:
metadata:
labels:
app.kubernetes.io/name: ingressmonitorcontroller
spec:
serviceAccountName: ingressmonitorcontroller
automountServiceAccountToken: true
securityContext:
runAsNonRoot: true
runAsUser: 65532
runAsGroup: 65532
seccompProfile:
type: RuntimeDefault
containers:
- name: manager
# Pinned, and deliberately NOT under Flux image automation. rbac.yaml
# is derived from what this exact version calls; an automatic bump
# could add a call the Role does not cover, silently.
image: ghcr.io/stakater/ingressmonitorcontroller:v2.2.13
command: ["/manager"]
args:
- --health-probe-bind-address=:8081
# Metrics off. Enabling them means either TLS certs or
# `--metrics-secure=false`, and the authn/authz filter
# (cmd/main.go, `filters.WithAuthenticationAndAuthorization`) wants a
# ClusterRole for TokenReview/SubjectAccessReview — a cluster-scoped
# grant this namespace otherwise does not need. A ServiceMonitor for
# this controller is worth having, but it is its own change.
- --metrics-bind-address=0
env:
# Scopes the manager's cache to this namespace
# (cmd/main.go `buildDefaultNamespaces`), which is what makes the
# namespaced Role in rbac.yaml sufficient — there is no ClusterRole.
- name: WATCH_NAMESPACE
value: uptime-monitor
# The namespace the config Secret is looked up in. Read from the
# ServiceAccount token file when unset; set explicitly so the lookup
# is visible in the manifest.
- name: OPERATOR_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
# IMC reads its whole configuration from key `config.yaml` of this
# Secret — providers, API key and enableMonitorDeletion together.
# ESO renders that key; see externalsecret.yaml for the contents and
# for why the config lives in git while the key does not.
- name: CONFIG_SECRET_NAME
value: uptime-robot-api-key
# Both probes are served by the manager on :8081 and are dialled by
# the kubelet, which Cilium identifies as the `host` entity — the only
# ingress the namespace policy admits
# (infrastructure/network-policies/uptime-monitor-ns.yaml). The port is
# not exposed as a containerPort because nothing else may reach it.
livenessProbe:
httpGet:
path: /healthz
port: 8081
initialDelaySeconds: 15
periodSeconds: 20
readinessProbe:
httpGet:
path: /readyz
port: 8081
initialDelaySeconds: 5
periodSeconds: 10
resources:
requests:
cpu: 10m
memory: 64Mi
limits:
memory: 128Mi
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
terminationGracePeriodSeconds: 10
@@ -12,12 +12,45 @@
# refreshInterval 1h matches the un-rotated ExternalSecrets here (velero,
# wiki-*). The rotated ones use 10m so an automated roll propagates fast — if
# kubernetes/infrastructure/openbao/rotation/ ever gains an uptime-robot
# rotator, drop this to 10m and annotate the controller Deployment for
# stakater/reloader, which the cluster already runs.
# rotator, drop this to 10m. The controller Deployment already carries the
# stakater/reloader annotation, so a new value restarts the pod that has the
# old one cached.
#
# The `eso` role already reads this path: eso-read grants `homelab/data/*`
# (kubernetes/infrastructure/openbao/policies/eso-read.hcl:31). No OpenBao
# policy change is needed for this key.
#
# ---
# Why this Secret holds a rendered `config.yaml` rather than a bare key (REN-6):
# IngressMonitorController does not read an API key from an env var. It reads
# its ENTIRE configuration — provider list, API key, enableMonitorDeletion —
# from one Secret key named `config.yaml` (pkg/config/config.go,
# `IngressMonitorControllerSecretConfigKey`), at startup, through the manager's
# APIReader. So the choice is not "key or config"; it is which Secret the config
# lives in. Templating it here keeps every non-secret decision in git and in
# review, with only the key itself injected at sync time.
#
# The template replaces the Secret's data rather than merging, so `apiKey` is a
# template input and the rendered `config.yaml` is the single output key.
#
# Two values are load-bearing:
#
# apiURL — REQUIRED. `Setup()` assigns `monitor.url = p.ApiURL` with no
# default (pkg/monitors/uptimerobot/uptime-monitor.go:39-44); omit it and
# every request goes to a relative URL.
#
# enableMonitorDeletion — true, so that removing an EndpointMonitor removes
# its monitor, which is what the mission asks for. It is bounded: deletion is
# reachable only via `GetByName`, which filters `m.FriendlyName == name` —
# exact equality — against the name the controller itself computed from the
# default template `{{.Name}}-{{.Namespace}}`. Every name it can compute ends
# in `-uptime-monitor`; none of the 9 monitors already in the account does.
# Set it to false for a create/update-only kill switch.
#
# monitorNameTemplate is deliberately absent: an empty value makes
# GetNameTemplateFormat fall back to that same default (pkg/util/template-utils.go:9),
# and writing it out here would mean escaping Go template syntax inside a Go
# template for no gain.
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
@@ -30,8 +63,17 @@ spec:
name: openbao
target:
name: uptime-robot-api-key
template:
engineVersion: v2
data:
config.yaml: |
providers:
- name: UptimeRobot
apiKey: "{{ .apiKey }}"
apiURL: https://api.uptimerobot.com/v2/
enableMonitorDeletion: true
data:
- secretKey: api-key
- secretKey: apiKey
remoteRef:
key: infrastructure/uptime-robot
property: api_key
@@ -1,4 +1,7 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- crd-endpointmonitors.yaml
- externalsecret.yaml
- rbac.yaml
- deployment.yaml
+86
View File
@@ -0,0 +1,86 @@
---
# ServiceAccount + namespaced RBAC for IngressMonitorController (REN-6).
#
# There is NO ClusterRole here, on purpose. The Deployment sets
# WATCH_NAMESPACE=uptime-monitor, which makes the manager build a
# namespace-scoped cache (cmd/main.go `buildDefaultNamespaces`), so every read
# the controller performs is confined to this namespace. A ClusterRole would
# grant reach the process cannot use.
#
# The rules below were derived from what the pinned image actually calls, not
# from the chart's rules or the upstream kubebuilder markers. Deliberately NOT
# granted, and why:
#
# - `""/pods`, `""/services` — read from exactly one place,
# `tryGetHealthEndpointFromIngress` (pkg/kube/wrappers/ingress-wrapper.go:126),
# which is reached only from the `urlFrom.ingressRef` path and only when the
# EndpointMonitor sets no explicit `healthEndpoint`. It is a best-effort
# guess at a readiness-probe path: both the Get and the List log the error
# and return `"", false` on failure, so a denial costs a log line, not a
# reconcile. Since the ingressRef path can only reach this namespace anyway
# (see the ingresses rule below) and nothing here serves HTTP, the grant
# would be inert. Add it if an Ingress ever lands in `uptime-monitor`.
# - `route.openshift.io/routes` — OpenShift only; `kube.IsOpenshift` is false
# here, so the Route scheme is never even registered (cmd/main.go init()).
# - `endpointmonitors/finalizers` — IMC uses no finalizers. Deletion is driven
# by the reconciler's IsNotFound branch, not by a finalizer
# (endpointmonitor_controller.go:74-81); `grep -rn Finalizer internal/controller/`
# has no hits.
# - leases / events — no leader election (see the Recreate strategy comment in
# deployment.yaml) and the reconciler holds no EventRecorder.
#
# Re-check this list whenever the image tag in deployment.yaml moves: it is
# pinned and not Renovate-managed precisely so that never happens silently.
apiVersion: v1
kind: ServiceAccount
metadata:
name: ingressmonitorcontroller
namespace: uptime-monitor
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: ingressmonitorcontroller
namespace: uptime-monitor
rules:
# Its own custom resource — the only thing the reconciler is keyed on.
- apiGroups: ["endpointmonitor.stakater.com"]
resources: ["endpointmonitors"]
verbs: ["get", "list", "watch"]
# Not exercised by v2.2.13 (`grep -rn "Status()" internal/controller/` is
# empty), but granted: it is this CRD's own subresource, so it confers no
# reach outside the resource already listed above, and it is the one write an
# operator of this shape is expected to gain.
- apiGroups: ["endpointmonitor.stakater.com"]
resources: ["endpointmonitors/status"]
verbs: ["get", "update", "patch"]
# Read-only. Used to resolve an EndpointMonitor's URL from an Ingress via
# `spec.urlFrom.ingressRef`. Note that pkg/kube/util/url.go:71 resolves that
# ref against the EndpointMonitor's OWN namespace, so this grant can only ever
# reach Ingresses in `uptime-monitor` — it cannot read an app's Ingress.
- apiGroups: ["networking.k8s.io"]
resources: ["ingresses"]
verbs: ["get", "list", "watch"]
# The controller loads its entire configuration — including the UptimeRobot
# API key — from one Secret key at startup (pkg/config/config.go
# `LoadControllerConfig`). It reads it through the manager's APIReader, i.e. a
# direct GET by name and not a cached list, which is why `get` alone is enough
# and why resourceNames can pin it to the single Secret ESO renders.
- apiGroups: [""]
resources: ["secrets"]
resourceNames: ["uptime-robot-api-key"]
verbs: ["get"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: ingressmonitorcontroller
namespace: uptime-monitor
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: ingressmonitorcontroller
subjects:
- kind: ServiceAccount
name: ingressmonitorcontroller
namespace: uptime-monitor
+31 -10
View File
@@ -1,7 +1,11 @@
# Automated uptime monitoring (REN-2): Stakater IngressMonitorController keeps
# UptimeRobot monitors in sync with `monitor.stakater.com/enabled` annotations on
# Ingress objects. This Kustomization starts with the API key only; the
# controller, its ServiceAccount and its RBAC land here in REN-6.
# UptimeRobot monitors in sync with `EndpointMonitor` custom resources. (Not
# Ingress annotations — `monitor.stakater.com/enabled` does nothing in IMC v2;
# there is no Ingress reconciler in the source, and docs/migration-guide.md
# calls the annotation mechanism obsolete. The opt-in property survives intact:
# creating an EndpointMonitor per host is more explicit than an annotation, and
# the default state is still nothing-monitored.) This Kustomization holds the
# API key, the controller Deployment, its ServiceAccount and its RBAC (REN-6).
#
# A DEDICATED Kustomization rather than adding the ExternalSecret to
# ./kubernetes/infrastructure/post-crds, which is where the other
@@ -36,6 +40,22 @@
# holds this Kustomization NotReady, and it is safe to fail loudly here
# precisely because the blast radius is this Kustomization alone: nothing
# `dependsOn` it.
#
# That gate earns its keep now that the controller is here (REN-6) rather than
# only the key. IMC GETs its whole configuration from the Secret once, at
# startup, through the manager's APIReader, and never revalidates. Which failure
# it papers over depends on which one happens:
#
# * Secret absent (first sync never succeeded — `creationPolicy` is the default
# Owner, so no successful sync means no Secret at all): IMC panics in
# `LoadControllerConfig` and the pod CrashLoopBackOffs. That one is loud.
# * Secret present but stale, because a LATER refresh failed: the pod starts
# against the old value, reports Available, and the first sign of trouble is
# a rejected UptimeRobot call on some future reconcile. Nothing about
# Deployment health says the key is current.
#
# The second case is the live one — the Secret exists today — and the CEL rule
# below is what turns it into a NotReady Kustomization instead of a green one.
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
@@ -43,9 +63,10 @@ metadata:
namespace: flux-system
spec:
interval: 10m
# 2m, not the 1m most app Kustomizations use: the one object here is an
# ExternalSecret against a key a human may still be placing in OpenBao, and a
# retry every minute against a 404 is just noise in the ESO and Flux logs.
# 2m, not the 1m most app Kustomizations use: what stalls this Kustomization
# is an ExternalSecret against a key a human may still be placing in OpenBao,
# and a retry every minute against a 404 is just noise in the ESO and Flux
# logs.
retryInterval: 2m
timeout: 5m
sourceRef:
@@ -55,10 +76,10 @@ spec:
prune: true
wait: true
# No name/namespace qualifier, so this is evaluated against every
# ExternalSecret in this Kustomization's inventory. Today that is exactly one
# (apps/uptime-monitor/externalsecret.yaml); when the IMC Deployment joins in
# REN-6 it is still one, and any ExternalSecret added here later should be held
# to the same bar anyway.
# ExternalSecret in this Kustomization's inventory. That is exactly one
# (apps/uptime-monitor/externalsecret.yaml) — the IMC Deployment added in
# REN-6 brought no second one — and any ExternalSecret added here later should
# be held to the same bar anyway.
#
# `external-secrets.io/v1` is the version every one of the 54 ExternalSecret
# manifests in this repo is written against, so it is the GVK Flux records in
@@ -245,14 +245,14 @@ kind: Namespace
metadata:
name: paperclip
---
# IngressMonitorController, which syncs UptimeRobot monitors from Ingress
# annotations (REN-2). Declared here rather than in the workload Kustomization
# IngressMonitorController, which syncs UptimeRobot monitors from
# `EndpointMonitor` custom resources (REN-2; not Ingress annotations — those are
# obsolete in IMC v2). Declared here rather than in the workload Kustomization
# for the reason spelled out above `woodpecker`: the `infrastructure` KS creates
# namespaces, and `infrastructure-crds` (post-crds, which includes
# ../network-policies/) runs after it — so declaring it in the workload KS would
# invert that order the moment this namespace gains a CiliumNetworkPolicy, which
# it will when the controller arrives and needs scoped egress to the UptimeRobot
# API. It holds nothing but the API key Secret until then.
# invert that order the moment this namespace gains a CiliumNetworkPolicy. It
# has one now: ../network-policies/uptime-monitor-ns.yaml.
#
# No pod-security labels: the controller needs no hostPath, hostNetwork or
# privilege, so the cluster's default `baseline` enforcement is enough.
@@ -29,4 +29,5 @@ resources:
- wordpress-ns.yaml
- ai-ns.yaml
- navidrome-ns.yaml
- uptime-monitor-ns.yaml
- allow-cilium-health.yaml
@@ -0,0 +1,54 @@
---
# Baseline policy for the `uptime-monitor` namespace (REN-6).
#
# Copied from netbird-service-sync-ns.yaml, which is the closest existing
# neighbour in this directory: a controller that serves nothing and only calls
# out — kube-apiserver plus one external HTTPS API. Same shape here.
#
# No Traefik ingress rule, because nothing in this namespace is behind an
# Ingress. (For anyone reaching for a wiki note: the selector to admit Traefik
# lives in `ingress` with label `app.kubernetes.io/name: traefik`, as in
# paperclip-ns.yaml. An `ingress-nginx` selector matches nothing in this cluster
# and would admit nothing, silently.)
#
# Deliberately no OpenBao egress: the controller never talks to OpenBao. ESO, in
# the external-secrets namespace, resolves the key and writes the Secret; the
# controller only reads that Secret through the API server.
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: default-deny
namespace: uptime-monitor
spec:
endpointSelector: {}
ingress:
# Kubelet liveness/readiness probes on :8081 arrive with the `host`
# identity. This is the only ingress the namespace admits — without it the
# controller pod fails its probes and CrashLoopBackOffs, with nothing in its
# own logs to say why.
- fromEntities:
- host
- remote-node
egress:
- toEndpoints:
- matchLabels:
"k8s:io.kubernetes.pod.namespace": kube-system
k8s-app: kube-dns
toPorts:
- ports:
- port: "53"
protocol: UDP
- port: "53"
protocol: TCP
# EndpointMonitor watch, Ingress reads, and the startup GET of the config
# Secret.
- toEntities:
- kube-apiserver
# api.uptimerobot.com. Not reached at all until an EndpointMonitor exists:
# the controller makes no API call at startup.
- toEntities:
- world
toPorts:
- ports:
- port: "443"
protocol: TCP