From a0d79eeb8cf8327336b46953092afb815be2ad41 Mon Sep 17 00:00:00 2001 From: Andrew Stoltz <1578013+astoltz@users.noreply.github.com> Date: Wed, 17 Jun 2026 13:47:40 -0500 Subject: [PATCH] hm4: own hosting operator CRDs and RBAC --- .../fc-system/clusterrole-mysql-operator.json | 209 ++++++++++++++++++ .../fc-system/clusterrole-mysql-web.json | 154 +++++++++++++ .../fc-system/clusterrole-php-operator.json | 133 +++++++++++ apps-gx10/fc-system/clusterrole-php-web.json | 113 ++++++++++ .../clusterrolebinding-mysql-operator.json | 26 +++ .../clusterrolebinding-mysql-web.json | 19 ++ .../clusterrolebinding-php-operator.json | 26 +++ .../fc-system/clusterrolebinding-php-web.json | 19 ++ .../fc-system/crd-mysqlinstancecrds.json | 151 +++++++++++++ apps-gx10/fc-system/crd-mysqlreplicacrds.json | 167 ++++++++++++++ .../fc-system/crd-phpapplicationcrds.json | 85 +++++++ apps-gx10/fc-system/crd-phpinstancecrds.json | 78 +++++++ .../FleetManifestLintTests.cs | 37 ++++ 13 files changed, 1217 insertions(+) create mode 100644 apps-gx10/fc-system/clusterrole-mysql-operator.json create mode 100644 apps-gx10/fc-system/clusterrole-mysql-web.json create mode 100644 apps-gx10/fc-system/clusterrole-php-operator.json create mode 100644 apps-gx10/fc-system/clusterrole-php-web.json create mode 100644 apps-gx10/fc-system/clusterrolebinding-mysql-operator.json create mode 100644 apps-gx10/fc-system/clusterrolebinding-mysql-web.json create mode 100644 apps-gx10/fc-system/clusterrolebinding-php-operator.json create mode 100644 apps-gx10/fc-system/clusterrolebinding-php-web.json create mode 100644 apps-gx10/fc-system/crd-mysqlinstancecrds.json create mode 100644 apps-gx10/fc-system/crd-mysqlreplicacrds.json create mode 100644 apps-gx10/fc-system/crd-phpapplicationcrds.json create mode 100644 apps-gx10/fc-system/crd-phpinstancecrds.json diff --git a/apps-gx10/fc-system/clusterrole-mysql-operator.json b/apps-gx10/fc-system/clusterrole-mysql-operator.json new file mode 100644 index 0000000..3207a88 --- /dev/null +++ b/apps-gx10/fc-system/clusterrole-mysql-operator.json @@ -0,0 +1,209 @@ +{ + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "ClusterRole", + "metadata": { + "name": "mysql-operator", + "labels": { + "app.kubernetes.io/name": "mysql-operator", + "app.kubernetes.io/instance": "mysql-operator", + "app.kubernetes.io/managed-by": "flowercore", + "app.kubernetes.io/component": "operator", + "app.kubernetes.io/part-of": "flowercore-mysql" + } + }, + "rules": [ + { + "apiGroups": [ + "flowercore.io" + ], + "resources": [ + "mysqlinstancecrds" + ], + "verbs": [ + "get", + "list", + "watch", + "patch", + "update" + ] + }, + { + "apiGroups": [ + "flowercore.io" + ], + "resources": [ + "mysqlinstancecrds/status" + ], + "verbs": [ + "get", + "patch", + "update" + ] + }, + { + "apiGroups": [ + "flowercore.io" + ], + "resources": [ + "mysqlinstancecrds/finalizers" + ], + "verbs": [ + "get", + "patch", + "update" + ] + }, + { + "apiGroups": [ + "flowercore.io" + ], + "resources": [ + "mysqlreplicacrds" + ], + "verbs": [ + "get", + "list", + "watch", + "patch", + "update", + "create", + "delete" + ] + }, + { + "apiGroups": [ + "flowercore.io" + ], + "resources": [ + "mysqlreplicacrds/status" + ], + "verbs": [ + "get", + "patch", + "update" + ] + }, + { + "apiGroups": [ + "flowercore.io" + ], + "resources": [ + "mysqlreplicacrds/finalizers" + ], + "verbs": [ + "get", + "patch", + "update" + ] + }, + { + "apiGroups": [ + "apps" + ], + "resources": [ + "deployments" + ], + "verbs": [ + "get", + "list", + "create", + "update", + "delete" + ] + }, + { + "apiGroups": [ + "" + ], + "resources": [ + "services" + ], + "verbs": [ + "get", + "list", + "create", + "delete" + ] + }, + { + "apiGroups": [ + "" + ], + "resources": [ + "secrets" + ], + "verbs": [ + "get", + "list", + "create", + "delete" + ] + }, + { + "apiGroups": [ + "" + ], + "resources": [ + "persistentvolumeclaims" + ], + "verbs": [ + "get", + "list", + "create", + "delete" + ] + }, + { + "apiGroups": [ + "" + ], + "resources": [ + "namespaces" + ], + "verbs": [ + "get", + "list", + "create" + ] + }, + { + "apiGroups": [ + "batch" + ], + "resources": [ + "jobs" + ], + "verbs": [ + "get", + "list", + "create" + ] + }, + { + "apiGroups": [ + "" + ], + "resources": [ + "events" + ], + "verbs": [ + "create", + "patch" + ] + }, + { + "apiGroups": [ + "coordination.k8s.io" + ], + "resources": [ + "leases" + ], + "verbs": [ + "get", + "list", + "create", + "update" + ] + } + ] +} diff --git a/apps-gx10/fc-system/clusterrole-mysql-web.json b/apps-gx10/fc-system/clusterrole-mysql-web.json new file mode 100644 index 0000000..f48ff92 --- /dev/null +++ b/apps-gx10/fc-system/clusterrole-mysql-web.json @@ -0,0 +1,154 @@ +{ + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "ClusterRole", + "metadata": { + "name": "mysql-web" + }, + "rules": [ + { + "apiGroups": [ + "" + ], + "resources": [ + "namespaces", + "pods", + "services", + "secrets", + "configmaps", + "persistentvolumeclaims" + ], + "verbs": [ + "get", + "list", + "watch", + "create", + "update", + "patch", + "delete" + ] + }, + { + "apiGroups": [ + "apps" + ], + "resources": [ + "deployments", + "statefulsets" + ], + "verbs": [ + "get", + "list", + "watch", + "create", + "update", + "patch", + "delete" + ] + }, + { + "apiGroups": [ + "" + ], + "resources": [ + "pods/log", + "pods/exec" + ], + "verbs": [ + "get", + "create" + ] + }, + { + "apiGroups": [ + "" + ], + "resources": [ + "nodes" + ], + "verbs": [ + "list" + ] + }, + { + "apiGroups": [ + "flowercore.io" + ], + "resources": [ + "mysqlinstancecrds" + ], + "verbs": [ + "get", + "list", + "watch", + "create", + "update", + "patch", + "delete" + ] + }, + { + "apiGroups": [ + "flowercore.io" + ], + "resources": [ + "mysqlreplicacrds" + ], + "verbs": [ + "get", + "list", + "watch", + "create", + "update", + "patch", + "delete" + ] + }, + { + "apiGroups": [ + "flowercore.io" + ], + "resources": [ + "mysqlreplicacrds/status" + ], + "verbs": [ + "get", + "patch", + "update" + ] + }, + { + "apiGroups": [ + "cert-manager.io" + ], + "resources": [ + "certificates" + ], + "verbs": [ + "get", + "list", + "watch", + "create", + "update", + "patch", + "delete" + ] + }, + { + "apiGroups": [ + "traefik.io" + ], + "resources": [ + "ingressroutes" + ], + "verbs": [ + "get", + "list", + "watch", + "create", + "update", + "patch", + "delete" + ] + } + ] +} diff --git a/apps-gx10/fc-system/clusterrole-php-operator.json b/apps-gx10/fc-system/clusterrole-php-operator.json new file mode 100644 index 0000000..549e35b --- /dev/null +++ b/apps-gx10/fc-system/clusterrole-php-operator.json @@ -0,0 +1,133 @@ +{ + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "ClusterRole", + "metadata": { + "name": "php-operator", + "labels": { + "app.kubernetes.io/name": "php-operator", + "app.kubernetes.io/instance": "php-operator", + "app.kubernetes.io/managed-by": "flowercore", + "app.kubernetes.io/component": "operator", + "app.kubernetes.io/part-of": "flowercore-php" + } + }, + "rules": [ + { + "apiGroups": [ + "flowercore.io" + ], + "resources": [ + "phpinstancecrds", + "phpapplicationcrds" + ], + "verbs": [ + "get", + "list", + "watch", + "patch", + "update" + ] + }, + { + "apiGroups": [ + "flowercore.io" + ], + "resources": [ + "phpinstancecrds/status", + "phpapplicationcrds/status" + ], + "verbs": [ + "get", + "patch", + "update" + ] + }, + { + "apiGroups": [ + "flowercore.io" + ], + "resources": [ + "phpinstancecrds/finalizers", + "phpapplicationcrds/finalizers" + ], + "verbs": [ + "get", + "patch", + "update" + ] + }, + { + "apiGroups": [ + "apps" + ], + "resources": [ + "deployments" + ], + "verbs": [ + "get", + "list", + "create", + "update", + "delete" + ] + }, + { + "apiGroups": [ + "" + ], + "resources": [ + "services", + "secrets", + "persistentvolumeclaims", + "configmaps" + ], + "verbs": [ + "get", + "list", + "create", + "update", + "patch", + "delete" + ] + }, + { + "apiGroups": [ + "" + ], + "resources": [ + "namespaces" + ], + "verbs": [ + "get", + "list", + "create" + ] + }, + { + "apiGroups": [ + "" + ], + "resources": [ + "events" + ], + "verbs": [ + "create", + "patch" + ] + }, + { + "apiGroups": [ + "coordination.k8s.io" + ], + "resources": [ + "leases" + ], + "verbs": [ + "get", + "list", + "create", + "update" + ] + } + ] +} diff --git a/apps-gx10/fc-system/clusterrole-php-web.json b/apps-gx10/fc-system/clusterrole-php-web.json new file mode 100644 index 0000000..2b501de --- /dev/null +++ b/apps-gx10/fc-system/clusterrole-php-web.json @@ -0,0 +1,113 @@ +{ + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "ClusterRole", + "metadata": { + "name": "php-web" + }, + "rules": [ + { + "apiGroups": [ + "" + ], + "resources": [ + "namespaces", + "pods", + "services", + "secrets", + "configmaps", + "persistentvolumeclaims" + ], + "verbs": [ + "get", + "list", + "watch", + "create", + "update", + "patch", + "delete" + ] + }, + { + "apiGroups": [ + "apps" + ], + "resources": [ + "deployments" + ], + "verbs": [ + "get", + "list", + "watch", + "create", + "update", + "patch", + "delete" + ] + }, + { + "apiGroups": [ + "" + ], + "resources": [ + "pods/log", + "pods/exec" + ], + "verbs": [ + "get", + "create" + ] + }, + { + "apiGroups": [ + "traefik.io" + ], + "resources": [ + "ingressroutes", + "middlewares" + ], + "verbs": [ + "get", + "list", + "watch", + "create", + "update", + "patch", + "delete" + ] + }, + { + "apiGroups": [ + "cert-manager.io" + ], + "resources": [ + "certificates" + ], + "verbs": [ + "get", + "list", + "watch", + "create", + "update", + "patch", + "delete" + ] + }, + { + "apiGroups": [ + "flowercore.io" + ], + "resources": [ + "phpapplicationcrds" + ], + "verbs": [ + "get", + "list", + "watch", + "create", + "update", + "patch", + "delete" + ] + } + ] +} diff --git a/apps-gx10/fc-system/clusterrolebinding-mysql-operator.json b/apps-gx10/fc-system/clusterrolebinding-mysql-operator.json new file mode 100644 index 0000000..657b273 --- /dev/null +++ b/apps-gx10/fc-system/clusterrolebinding-mysql-operator.json @@ -0,0 +1,26 @@ +{ + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "ClusterRoleBinding", + "metadata": { + "name": "mysql-operator", + "labels": { + "app.kubernetes.io/name": "mysql-operator", + "app.kubernetes.io/instance": "mysql-operator", + "app.kubernetes.io/managed-by": "flowercore", + "app.kubernetes.io/component": "operator", + "app.kubernetes.io/part-of": "flowercore-mysql" + } + }, + "roleRef": { + "apiGroup": "rbac.authorization.k8s.io", + "kind": "ClusterRole", + "name": "mysql-operator" + }, + "subjects": [ + { + "kind": "ServiceAccount", + "name": "mysql-operator", + "namespace": "fc-system" + } + ] +} diff --git a/apps-gx10/fc-system/clusterrolebinding-mysql-web.json b/apps-gx10/fc-system/clusterrolebinding-mysql-web.json new file mode 100644 index 0000000..db67863 --- /dev/null +++ b/apps-gx10/fc-system/clusterrolebinding-mysql-web.json @@ -0,0 +1,19 @@ +{ + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "ClusterRoleBinding", + "metadata": { + "name": "mysql-web" + }, + "subjects": [ + { + "kind": "ServiceAccount", + "name": "mysql-web", + "namespace": "fc-mysql" + } + ], + "roleRef": { + "kind": "ClusterRole", + "name": "mysql-web", + "apiGroup": "rbac.authorization.k8s.io" + } +} diff --git a/apps-gx10/fc-system/clusterrolebinding-php-operator.json b/apps-gx10/fc-system/clusterrolebinding-php-operator.json new file mode 100644 index 0000000..78212e6 --- /dev/null +++ b/apps-gx10/fc-system/clusterrolebinding-php-operator.json @@ -0,0 +1,26 @@ +{ + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "ClusterRoleBinding", + "metadata": { + "name": "php-operator", + "labels": { + "app.kubernetes.io/name": "php-operator", + "app.kubernetes.io/instance": "php-operator", + "app.kubernetes.io/managed-by": "flowercore", + "app.kubernetes.io/component": "operator", + "app.kubernetes.io/part-of": "flowercore-php" + } + }, + "roleRef": { + "apiGroup": "rbac.authorization.k8s.io", + "kind": "ClusterRole", + "name": "php-operator" + }, + "subjects": [ + { + "kind": "ServiceAccount", + "name": "php-operator", + "namespace": "fc-system" + } + ] +} diff --git a/apps-gx10/fc-system/clusterrolebinding-php-web.json b/apps-gx10/fc-system/clusterrolebinding-php-web.json new file mode 100644 index 0000000..47aa02a --- /dev/null +++ b/apps-gx10/fc-system/clusterrolebinding-php-web.json @@ -0,0 +1,19 @@ +{ + "apiVersion": "rbac.authorization.k8s.io/v1", + "kind": "ClusterRoleBinding", + "metadata": { + "name": "php-web" + }, + "subjects": [ + { + "kind": "ServiceAccount", + "name": "php-web", + "namespace": "fc-php" + } + ], + "roleRef": { + "kind": "ClusterRole", + "name": "php-web", + "apiGroup": "rbac.authorization.k8s.io" + } +} diff --git a/apps-gx10/fc-system/crd-mysqlinstancecrds.json b/apps-gx10/fc-system/crd-mysqlinstancecrds.json new file mode 100644 index 0000000..35a59f9 --- /dev/null +++ b/apps-gx10/fc-system/crd-mysqlinstancecrds.json @@ -0,0 +1,151 @@ +{ + "apiVersion": "apiextensions.k8s.io/v1", + "kind": "CustomResourceDefinition", + "metadata": { + "name": "mysqlinstancecrds.flowercore.io" + }, + "spec": { + "group": "flowercore.io", + "names": { + "kind": "MySqlInstanceCrd", + "listKind": "MySqlInstanceCrdList", + "plural": "mysqlinstancecrds", + "singular": "mysqlinstancecrd", + "shortNames": [ + "mysql", + "mysqlinst" + ] + }, + "scope": "Namespaced", + "versions": [ + { + "name": "v1", + "served": true, + "storage": true, + "schema": { + "openAPIV3Schema": { + "type": "object", + "properties": { + "spec": { + "type": "object", + "properties": { + "action": { + "type": "string", + "description": "Action to execute (e.g., provision, backup, restore, scale, decommission, healthcheck). Dispatched by the reconciler when this value changes." + }, + "labels": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "Key-value labels for provisioning config (image-tag, mysql-version [deprecated], storage-size, replicas, port, max-connections) and billing/filtering." + } + } + }, + "status": { + "type": "object", + "properties": { + "lastAction": { + "type": "string", + "description": "Name of the last action dispatched for this resource." + }, + "lastActionStatus": { + "type": "string", + "description": "Outcome of the last action (Completed, Failed)." + }, + "lastActionTimestamp": { + "type": "string", + "format": "date-time", + "description": "UTC timestamp when the last action was dispatched." + }, + "message": { + "type": "string", + "description": "Human-readable message, typically populated on failure." + }, + "ready": { + "type": "string", + "description": "Convenience field mirroring the Ready condition status (True, False, Unknown). Used by printer columns since JSONPath cannot filter condition arrays." + }, + "retryCount": { + "type": "integer", + "description": "Number of consecutive transient failure retries for the current action. Reset to zero on success or when a new action is dispatched." + }, + "nextRetryAt": { + "type": "string", + "format": "date-time", + "description": "UTC timestamp for the next retry attempt. Set on transient failure using exponential backoff (2^retryCount seconds, max 5 minutes)." + }, + "conditions": { + "type": "array", + "description": "Kubernetes-style status conditions.", + "items": { + "type": "object", + "properties": { + "type": { + "type": "string", + "description": "Condition type (e.g., Ready, Degraded)." + }, + "status": { + "type": "string", + "description": "Condition status (True, False, Unknown)." + }, + "lastTransitionTime": { + "type": "string", + "format": "date-time", + "description": "UTC timestamp of last status transition." + }, + "reason": { + "type": "string", + "description": "Machine-readable reason code." + }, + "message": { + "type": "string", + "description": "Human-readable details about the condition." + } + } + } + } + } + } + } + } + }, + "subresources": { + "status": {} + }, + "additionalPrinterColumns": [ + { + "name": "Ready", + "type": "string", + "jsonPath": ".status.ready", + "description": "Whether the instance is ready" + }, + { + "name": "Action", + "type": "string", + "jsonPath": ".spec.action", + "description": "Current action" + }, + { + "name": "Status", + "type": "string", + "jsonPath": ".status.lastActionStatus", + "description": "Last action status" + }, + { + "name": "Message", + "type": "string", + "jsonPath": ".status.message", + "description": "Status message", + "priority": 1 + }, + { + "name": "Age", + "type": "date", + "jsonPath": ".metadata.creationTimestamp" + } + ] + } + ] + } +} diff --git a/apps-gx10/fc-system/crd-mysqlreplicacrds.json b/apps-gx10/fc-system/crd-mysqlreplicacrds.json new file mode 100644 index 0000000..6542209 --- /dev/null +++ b/apps-gx10/fc-system/crd-mysqlreplicacrds.json @@ -0,0 +1,167 @@ +{ + "apiVersion": "apiextensions.k8s.io/v1", + "kind": "CustomResourceDefinition", + "metadata": { + "name": "mysqlreplicacrds.flowercore.io" + }, + "spec": { + "group": "flowercore.io", + "names": { + "kind": "MySqlReplicaCrd", + "listKind": "MySqlReplicaCrdList", + "plural": "mysqlreplicacrds", + "singular": "mysqlreplicacrd", + "shortNames": [ + "mysqlreplica", + "mysqlrepl" + ] + }, + "scope": "Namespaced", + "versions": [ + { + "name": "v1", + "served": true, + "storage": true, + "schema": { + "openAPIV3Schema": { + "type": "object", + "properties": { + "spec": { + "type": "object", + "properties": { + "action": { + "type": "string", + "description": "Reserved for ADR-039 action dispatch. The replica reconciler is declarative \u2014 promote / failover go through the MySQL Manager Web API instead." + }, + "labels": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "Replica topology fields carried as labels (primary-instance, replica-instance, replication-user, replication-user-secret, auto-failover)." + } + } + }, + "status": { + "type": "object", + "properties": { + "phase": { + "type": "string", + "description": "Replication phase (Pending, Configuring, Running, Degraded, Failed, Promoted)." + }, + "lastApplied": { + "type": "string", + "format": "date-time", + "description": "UTC timestamp of the last successful reconcile." + }, + "currentSourceHost": { + "type": "string", + "description": "SOURCE_HOST currently configured on the replica." + }, + "currentSourcePort": { + "type": "integer", + "description": "SOURCE_PORT currently configured on the replica." + }, + "secondsBehindSource": { + "type": "integer", + "format": "int64", + "description": "Seconds_Behind_Source from SHOW REPLICA STATUS." + }, + "replicaIoRunning": { + "type": "string", + "description": "Replica_IO_Running (Yes/No/Connecting)." + }, + "replicaSqlRunning": { + "type": "string", + "description": "Replica_SQL_Running." + }, + "lastError": { + "type": "string", + "description": "Most recent IO / SQL error text." + }, + "lastAction": { + "type": "string" + }, + "lastActionStatus": { + "type": "string" + }, + "lastActionTimestamp": { + "type": "string", + "format": "date-time" + }, + "message": { + "type": "string" + }, + "ready": { + "type": "string" + }, + "retryCount": { + "type": "integer" + }, + "nextRetryAt": { + "type": "string", + "format": "date-time" + }, + "conditions": { + "type": "array", + "items": { + "type": "object", + "properties": { + "type": { + "type": "string" + }, + "status": { + "type": "string" + }, + "lastTransitionTime": { + "type": "string", + "format": "date-time" + }, + "reason": { + "type": "string" + }, + "message": { + "type": "string" + } + } + } + } + } + } + } + } + }, + "subresources": { + "status": {} + }, + "additionalPrinterColumns": [ + { + "name": "Phase", + "type": "string", + "jsonPath": ".status.phase" + }, + { + "name": "Source", + "type": "string", + "jsonPath": ".status.currentSourceHost" + }, + { + "name": "Lag", + "type": "integer", + "jsonPath": ".status.secondsBehindSource" + }, + { + "name": "Ready", + "type": "string", + "jsonPath": ".status.ready" + }, + { + "name": "Age", + "type": "date", + "jsonPath": ".metadata.creationTimestamp" + } + ] + } + ] + } +} diff --git a/apps-gx10/fc-system/crd-phpapplicationcrds.json b/apps-gx10/fc-system/crd-phpapplicationcrds.json new file mode 100644 index 0000000..926e669 --- /dev/null +++ b/apps-gx10/fc-system/crd-phpapplicationcrds.json @@ -0,0 +1,85 @@ +{ + "apiVersion": "apiextensions.k8s.io/v1", + "kind": "CustomResourceDefinition", + "metadata": { + "name": "phpapplicationcrds.flowercore.io" + }, + "spec": { + "group": "flowercore.io", + "names": { + "kind": "PhpApplicationCrd", + "listKind": "PhpApplicationCrdList", + "plural": "phpapplicationcrds", + "singular": "phpapplicationcrd", + "shortNames": [ + "phpapp", + "phpapps" + ] + }, + "scope": "Namespaced", + "versions": [ + { + "name": "v1", + "served": true, + "storage": true, + "schema": { + "openAPIV3Schema": { + "type": "object", + "properties": { + "spec": { + "type": "object", + "x-kubernetes-preserve-unknown-fields": true + }, + "status": { + "type": "object", + "x-kubernetes-preserve-unknown-fields": true + } + } + } + }, + "subresources": { + "status": {} + }, + "additionalPrinterColumns": [ + { + "name": "Ready", + "type": "string", + "jsonPath": ".status.ready", + "description": "Whether the application is ready" + }, + { + "name": "Phase", + "type": "string", + "jsonPath": ".status.phase", + "description": "Current lifecycle phase" + }, + { + "name": "Last", + "type": "string", + "jsonPath": ".status.lastCompletedPhase", + "description": "Last completed subphase" + }, + { + "name": "Version", + "type": "string", + "jsonPath": ".status.version", + "description": "Installed application version", + "priority": 1 + }, + { + "name": "Error", + "type": "string", + "jsonPath": ".status.lastError", + "description": "Last reconcile error", + "priority": 1 + }, + { + "name": "Age", + "type": "date", + "jsonPath": ".metadata.creationTimestamp" + } + ] + } + ] + } +} diff --git a/apps-gx10/fc-system/crd-phpinstancecrds.json b/apps-gx10/fc-system/crd-phpinstancecrds.json new file mode 100644 index 0000000..f34019b --- /dev/null +++ b/apps-gx10/fc-system/crd-phpinstancecrds.json @@ -0,0 +1,78 @@ +{ + "apiVersion": "apiextensions.k8s.io/v1", + "kind": "CustomResourceDefinition", + "metadata": { + "name": "phpinstancecrds.flowercore.io" + }, + "spec": { + "group": "flowercore.io", + "names": { + "kind": "PhpInstanceCrd", + "listKind": "PhpInstanceCrdList", + "plural": "phpinstancecrds", + "singular": "phpinstancecrd", + "shortNames": [ + "php", + "phpinst" + ] + }, + "scope": "Namespaced", + "versions": [ + { + "name": "v1", + "served": true, + "storage": true, + "schema": { + "openAPIV3Schema": { + "type": "object", + "properties": { + "spec": { + "type": "object", + "x-kubernetes-preserve-unknown-fields": true + }, + "status": { + "type": "object", + "x-kubernetes-preserve-unknown-fields": true + } + } + } + }, + "subresources": { + "status": {} + }, + "additionalPrinterColumns": [ + { + "name": "Ready", + "type": "string", + "jsonPath": ".status.ready", + "description": "Whether the instance is ready" + }, + { + "name": "Action", + "type": "string", + "jsonPath": ".spec.action", + "description": "Current action" + }, + { + "name": "Status", + "type": "string", + "jsonPath": ".status.lastActionStatus", + "description": "Last action status" + }, + { + "name": "Message", + "type": "string", + "jsonPath": ".status.message", + "description": "Status message", + "priority": 1 + }, + { + "name": "Age", + "type": "date", + "jsonPath": ".metadata.creationTimestamp" + } + ] + } + ] + } +} diff --git a/tests/bluejay-infra-lint/FleetManifestLintTests.cs b/tests/bluejay-infra-lint/FleetManifestLintTests.cs index 77a2659..4e62284 100644 --- a/tests/bluejay-infra-lint/FleetManifestLintTests.cs +++ b/tests/bluejay-infra-lint/FleetManifestLintTests.cs @@ -981,6 +981,43 @@ public sealed class FleetManifestLintTests gatewayManifest.Should().Contain("port: 5400"); } + [Fact] + public void Gx10HostingManagers_ProvisioningCrdsAndRbacMustBeGitOpsOwned() + { + var requiredDocuments = new Dictionary( + StringComparer.Ordinal) + { + ["crd-mysqlinstancecrds.json"] = ("CustomResourceDefinition", "mysqlinstancecrds.flowercore.io", ["mysqlinstancecrds", "status"]), + ["crd-mysqlreplicacrds.json"] = ("CustomResourceDefinition", "mysqlreplicacrds.flowercore.io", ["mysqlreplicacrds", "status"]), + ["crd-phpinstancecrds.json"] = ("CustomResourceDefinition", "phpinstancecrds.flowercore.io", ["phpinstancecrds", "status"]), + ["crd-phpapplicationcrds.json"] = ("CustomResourceDefinition", "phpapplicationcrds.flowercore.io", ["phpapplicationcrds", "status"]), + ["clusterrole-mysql-operator.json"] = ("ClusterRole", "mysql-operator", ["mysqlinstancecrds", "mysqlreplicacrds", "deployments", "persistentvolumeclaims", "leases"]), + ["clusterrolebinding-mysql-operator.json"] = ("ClusterRoleBinding", "mysql-operator", ["ServiceAccount", "mysql-operator", "fc-system"]), + ["clusterrole-php-operator.json"] = ("ClusterRole", "php-operator", ["phpinstancecrds", "phpapplicationcrds", "deployments", "persistentvolumeclaims", "leases"]), + ["clusterrolebinding-php-operator.json"] = ("ClusterRoleBinding", "php-operator", ["ServiceAccount", "php-operator", "fc-system"]), + ["clusterrole-mysql-web.json"] = ("ClusterRole", "mysql-web", ["mysqlinstancecrds", "mysqlreplicacrds", "certificates", "ingressroutes", "pods/exec"]), + ["clusterrolebinding-mysql-web.json"] = ("ClusterRoleBinding", "mysql-web", ["ServiceAccount", "mysql-web", "fc-mysql"]), + ["clusterrole-php-web.json"] = ("ClusterRole", "php-web", ["phpapplicationcrds", "certificates", "ingressroutes", "pods/exec"]), + ["clusterrolebinding-php-web.json"] = ("ClusterRoleBinding", "php-web", ["ServiceAccount", "php-web", "fc-php"]), + }; + + foreach (var (fileName, expected) in requiredDocuments) + { + var path = Path.Combine(Inventory.BluejayRoot, "apps-gx10", "fc-system", fileName); + File.Exists(path).Should().BeTrue($"{fileName} must be durable in GX10 GitOps"); + + var raw = File.ReadAllText(path); + using var document = JsonDocument.Parse(raw); + document.RootElement.GetProperty("kind").GetString().Should().Be(expected.Kind); + document.RootElement.GetProperty("metadata").GetProperty("name").GetString().Should().Be(expected.Name); + + foreach (var requiredText in expected.RequiredText) + { + raw.Should().Contain(requiredText, $"{fileName} should preserve the live provisioning contract"); + } + } + } + [Fact] public void DnsAndMediaGitOpsAdoption_PreservesLiveStorageAndImageShape() {