- Template: bump default memory from 128Mi to 256Mi (matches deploy-app skill guidance) - ResourceQuota: add requests.storage (20Gi) and persistentvolumeclaims (5) defaults - CONTRIBUTING.md: agent-friendly contributor guide for namespace-owners
267 lines
7.2 KiB
HCL
267 lines
7.2 KiB
HCL
variable "tls_secret_name" {}
|
|
variable "tier" { type = string }
|
|
|
|
variable "k8s_users" {
|
|
type = map(object({
|
|
role = string # "admin", "power-user", "namespace-owner"
|
|
email = string # OIDC email claim
|
|
namespaces = optional(list(string), []) # for namespace-owners
|
|
domains = optional(list(string), []) # subdomains for user apps
|
|
quota = optional(object({
|
|
cpu_requests = optional(string, "2")
|
|
memory_requests = optional(string, "4Gi")
|
|
memory_limits = optional(string, "8Gi")
|
|
pods = optional(string, "20")
|
|
storage_requests = optional(string, "20Gi")
|
|
persistentvolumeclaims = optional(string, "5")
|
|
}), {})
|
|
}))
|
|
default = {}
|
|
}
|
|
|
|
# --- Admin role ---
|
|
# Binds to built-in cluster-admin ClusterRole
|
|
|
|
resource "kubernetes_cluster_role_binding" "admin_users" {
|
|
for_each = nonsensitive({ for name, user in var.k8s_users : name => user if user.role == "admin" })
|
|
|
|
metadata {
|
|
name = "oidc-admin-${each.key}"
|
|
}
|
|
|
|
role_ref {
|
|
api_group = "rbac.authorization.k8s.io"
|
|
kind = "ClusterRole"
|
|
name = "cluster-admin"
|
|
}
|
|
|
|
subject {
|
|
kind = "User"
|
|
name = each.value.email
|
|
api_group = "rbac.authorization.k8s.io"
|
|
}
|
|
}
|
|
|
|
# --- Power-user role ---
|
|
# Can manage workloads cluster-wide but cannot modify RBAC, nodes, or persistent volumes
|
|
|
|
resource "kubernetes_cluster_role" "power_user" {
|
|
metadata {
|
|
name = "oidc-power-user"
|
|
}
|
|
|
|
# Core resources
|
|
rule {
|
|
api_groups = [""]
|
|
resources = ["pods", "pods/log", "pods/exec", "services", "endpoints", "configmaps", "secrets", "persistentvolumeclaims", "events", "namespaces"]
|
|
verbs = ["get", "list", "watch"]
|
|
}
|
|
|
|
rule {
|
|
api_groups = [""]
|
|
resources = ["pods", "services", "configmaps", "secrets", "persistentvolumeclaims"]
|
|
verbs = ["create", "update", "patch", "delete"]
|
|
}
|
|
|
|
# Apps
|
|
rule {
|
|
api_groups = ["apps"]
|
|
resources = ["deployments", "statefulsets", "daemonsets", "replicasets"]
|
|
verbs = ["get", "list", "watch", "create", "update", "patch", "delete"]
|
|
}
|
|
|
|
# Batch
|
|
rule {
|
|
api_groups = ["batch"]
|
|
resources = ["jobs", "cronjobs"]
|
|
verbs = ["get", "list", "watch", "create", "update", "patch", "delete"]
|
|
}
|
|
|
|
# Networking
|
|
rule {
|
|
api_groups = ["networking.k8s.io"]
|
|
resources = ["ingresses", "networkpolicies"]
|
|
verbs = ["get", "list", "watch", "create", "update", "patch", "delete"]
|
|
}
|
|
|
|
# Autoscaling
|
|
rule {
|
|
api_groups = ["autoscaling"]
|
|
resources = ["horizontalpodautoscalers"]
|
|
verbs = ["get", "list", "watch", "create", "update", "patch", "delete"]
|
|
}
|
|
|
|
# Read-only on cluster-level resources
|
|
rule {
|
|
api_groups = [""]
|
|
resources = ["nodes"]
|
|
verbs = ["get", "list", "watch"]
|
|
}
|
|
|
|
rule {
|
|
api_groups = ["storage.k8s.io"]
|
|
resources = ["storageclasses"]
|
|
verbs = ["get", "list", "watch"]
|
|
}
|
|
|
|
rule {
|
|
api_groups = ["rbac.authorization.k8s.io"]
|
|
resources = ["clusterroles", "clusterrolebindings", "roles", "rolebindings"]
|
|
verbs = ["get", "list", "watch"]
|
|
}
|
|
}
|
|
|
|
resource "kubernetes_cluster_role_binding" "power_users" {
|
|
for_each = nonsensitive({ for name, user in var.k8s_users : name => user if user.role == "power-user" })
|
|
|
|
metadata {
|
|
name = "oidc-power-user-${each.key}"
|
|
}
|
|
|
|
role_ref {
|
|
api_group = "rbac.authorization.k8s.io"
|
|
kind = "ClusterRole"
|
|
name = kubernetes_cluster_role.power_user.metadata[0].name
|
|
}
|
|
|
|
subject {
|
|
kind = "User"
|
|
name = each.value.email
|
|
api_group = "rbac.authorization.k8s.io"
|
|
}
|
|
}
|
|
|
|
# --- Namespace-owner role ---
|
|
# Full admin within assigned namespaces + read-only cluster-wide
|
|
|
|
locals {
|
|
# Flatten user->namespace pairs for iteration
|
|
namespace_owner_pairs = flatten([
|
|
for name, user in var.k8s_users : [
|
|
for ns in user.namespaces : {
|
|
user_key = name
|
|
namespace = ns
|
|
email = user.email
|
|
quota = user.quota
|
|
}
|
|
] if user.role == "namespace-owner"
|
|
])
|
|
}
|
|
|
|
resource "kubernetes_role_binding" "namespace_owner" {
|
|
for_each = nonsensitive({ for pair in local.namespace_owner_pairs : "${pair.user_key}-${pair.namespace}" => pair })
|
|
|
|
metadata {
|
|
name = "namespace-owner-${each.value.user_key}"
|
|
namespace = each.value.namespace
|
|
}
|
|
|
|
role_ref {
|
|
api_group = "rbac.authorization.k8s.io"
|
|
kind = "ClusterRole"
|
|
name = "admin" # Built-in ClusterRole with full namespace access
|
|
}
|
|
|
|
subject {
|
|
kind = "User"
|
|
name = each.value.email
|
|
api_group = "rbac.authorization.k8s.io"
|
|
}
|
|
}
|
|
|
|
# Read-only cluster-wide access for namespace owners
|
|
resource "kubernetes_cluster_role" "namespace_owner_readonly" {
|
|
metadata {
|
|
name = "oidc-namespace-owner-readonly"
|
|
}
|
|
|
|
rule {
|
|
api_groups = [""]
|
|
resources = ["namespaces", "nodes"]
|
|
verbs = ["get", "list", "watch"]
|
|
}
|
|
|
|
rule {
|
|
api_groups = [""]
|
|
resources = ["pods", "services", "configmaps", "events"]
|
|
verbs = ["get", "list", "watch"]
|
|
}
|
|
|
|
rule {
|
|
api_groups = ["apps"]
|
|
resources = ["deployments", "statefulsets", "daemonsets"]
|
|
verbs = ["get", "list", "watch"]
|
|
}
|
|
}
|
|
|
|
resource "kubernetes_cluster_role_binding" "namespace_owner_readonly" {
|
|
for_each = nonsensitive({ for name, user in var.k8s_users : name => user if user.role == "namespace-owner" })
|
|
|
|
metadata {
|
|
name = "oidc-ns-owner-readonly-${each.key}"
|
|
}
|
|
|
|
role_ref {
|
|
api_group = "rbac.authorization.k8s.io"
|
|
kind = "ClusterRole"
|
|
name = kubernetes_cluster_role.namespace_owner_readonly.metadata[0].name
|
|
}
|
|
|
|
subject {
|
|
kind = "User"
|
|
name = each.value.email
|
|
api_group = "rbac.authorization.k8s.io"
|
|
}
|
|
}
|
|
|
|
# Resource quotas per user namespace
|
|
resource "kubernetes_resource_quota" "user_namespace_quota" {
|
|
for_each = nonsensitive({ for pair in local.namespace_owner_pairs : "${pair.user_key}-${pair.namespace}" => pair })
|
|
|
|
metadata {
|
|
name = "user-quota"
|
|
namespace = each.value.namespace
|
|
}
|
|
|
|
spec {
|
|
hard = {
|
|
"requests.cpu" = each.value.quota.cpu_requests
|
|
"requests.memory" = each.value.quota.memory_requests
|
|
"limits.memory" = each.value.quota.memory_limits
|
|
"pods" = each.value.quota.pods
|
|
"requests.storage" = each.value.quota.storage_requests
|
|
"persistentvolumeclaims" = each.value.quota.persistentvolumeclaims
|
|
}
|
|
}
|
|
|
|
depends_on = [kubernetes_role_binding.namespace_owner]
|
|
}
|
|
|
|
# ConfigMap with user-role mapping for the self-service portal
|
|
resource "kubernetes_config_map" "user_roles" {
|
|
metadata {
|
|
name = "k8s-user-roles"
|
|
namespace = "k8s-portal"
|
|
}
|
|
|
|
data = {
|
|
"users.json" = jsonencode({
|
|
for name, user in var.k8s_users : user.email => {
|
|
role = user.role
|
|
namespaces = user.namespaces
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
# TLS secret in each user namespace (so they can create HTTPS ingresses)
|
|
module "user_namespace_tls" {
|
|
for_each = nonsensitive(toset(flatten([
|
|
for name, user in var.k8s_users : user.namespaces
|
|
if user.role == "namespace-owner"
|
|
])))
|
|
|
|
source = "../../../../modules/kubernetes/setup_tls_secret"
|
|
namespace = each.value
|
|
tls_secret_name = var.tls_secret_name
|
|
}
|