terraform {
  required_providers {
    kubernetes = {
      source  = "hashicorp/kubernetes"
      version = "2.16.1"
    }
    helm = {
      source = "hashicorp/helm"
      version = "~> 3.0"
    }
  }
}

%{ for index, cluster in clusters ~}

provider "kubernetes" {
  alias = "cluster_${index}"
  config_path = "./kubeconfig.${index}"
}

provider "helm" {
  alias = "cluster_${index}"
  kubernetes = {
    config_path = "./kubeconfig.${index}"
  }
}

# Password used for SSH and code-server access
resource "random_string" "shpod_${index}" {
  length  = 6
  special = false
  upper   = false
}

resource "kubernetes_namespace" "shpod_${index}" {
  provider = kubernetes.cluster_${index}
  metadata {
    name = "shpod"
  }
}

data "kubernetes_service" "shpod_${index}" {
  depends_on = [ helm_release.shpod_${index} ]
  provider = kubernetes.cluster_${index}
  metadata {
    name = "shpod"
    namespace = "shpod"
  }
}

resource "helm_release" "shpod_${index}" {
  provider = helm.cluster_${index}
  repository = "https://shpod.in"
  chart = "shpod"
  name = "shpod"
  namespace = "shpod"
  create_namespace = false
  values = [
    yamlencode({
      service = {
        type = "NodePort"
      }
      resources = {
        requests = {
          cpu = "100m"
          memory = "500M"
        }
        limits = {
          cpu = "1"
          memory = "1000M"
        }
      }
      persistentVolume = {
        enabled = true
      }
      ssh = {
        password = random_string.shpod_${index}.result
      }
      rbac = {
        cluster = {
          clusterRoles = [ "cluster-admin" ]
        }
      }
      codeServer = {
        enabled = true
      }
    })
  ]
}

resource "helm_release" "metrics_server_${index}" {
  # Some providers pre-install metrics-server.
  # Some don't. Let's install metrics-server,
  # but only if it's not already installed.
  count = yamldecode(file("./flags.${index}"))["has_metrics_server"] ? 0 : 1
  provider = helm.cluster_${index}
  repository = "https://kubernetes-sigs.github.io/metrics-server/"
  chart = "metrics-server"
  version = "3.8.2"
  name = "metrics-server"
  namespace = "metrics-server"
  create_namespace = true
  values = [
    yamlencode({
      args = [ "--kubelet-insecure-tls" ]
    })
  ]
}

# This section here deserves a little explanation.
#
# When we access a cluster with shpod (either through SSH or code-server)
# there is no kubeconfig file - we simply use "in-cluster" authentication
# with a ServiceAccount token. This is a bit unusual, and ideally, I would
# prefer to have a "normal" kubeconfig file in the students' shell.
#
# So what we're doing here, is that we're populating a ConfigMap with
# a kubeconfig file; and in the initialization scripts (e.g. bashrc) we
# automatically download the kubeconfig file from the ConfigMap and place
# it in ~/.kube/kubeconfig.
#
# But, which kubeconfig file should we use? We could use the "normal"
# kubeconfig file that was generated by the provider; but in some cases,
# that kubeconfig file might use a token instead of a certificate for
# user authentication - and ideally, I would like to have a certificate
# so that in the section about auth and RBAC, we can dissect that TLS
# certificate and explain where our permissions come from.
#
# So we're creating a TLS key pair; using the CSR API to issue a user
# certificate belongong to a special group; and grant the cluster-admin
# role to that group; then we use the kubeconfig file generated by the
# provider but override the user with that TLS key pair.
#
# This is not strictly necessary but it streamlines the lesson on auth.
#
# Lastly - in the ConfigMap we actually put both the original kubeconfig,
# and the one where we injected our new user (just in case we want to
# use or look at the original for any reason).

resource "kubernetes_config_map" "kubeconfig_${index}" {
  provider = kubernetes.cluster_${index}
  metadata {
    name = "kubeconfig"
    namespace = kubernetes_namespace.shpod_${index}.metadata.0.name
  }
  data = {
    kubeconfig_from_provider = file("./kubeconfig.${index}")
    kubeconfig_cluster_admin = <<-EOT
      kind: Config
      apiVersion: v1
      current-context: cluster-admin@k8s-${index}
      clusters:
      - name: k8s-${index}
        cluster:
          certificate-authority-data: $${yamldecode(file("./kubeconfig.${index}")).clusters.0.cluster.certificate-authority-data}
          server: $${yamldecode(file("./kubeconfig.${index}")).clusters.0.cluster.server}
      contexts:
      - name: cluster-admin@k8s-${index}
        context:
          cluster: k8s-${index}
          user: cluster-admin
      users:
      - name: cluster-admin
        user:
          client-key-data: $${base64encode(tls_private_key.cluster_admin_${index}.private_key_pem)}
          client-certificate-data: $${base64encode(kubernetes_certificate_signing_request_v1.cluster_admin_${index}.certificate)}
    EOT
  }
}

resource "tls_private_key" "cluster_admin_${index}" {
  algorithm = "RSA"
}

resource "tls_cert_request" "cluster_admin_${index}" {
  private_key_pem = tls_private_key.cluster_admin_${index}.private_key_pem
  subject {
    common_name = "cluster-admin"
    # Note: CSR API v1 doesn't allow issuing certs with "system:masters" anymore.
    #organization = "system:masters"
    # We'll use this custom group name instead.cluster-admin user.
    organization = "shpod-cluster-admins"
  }
}

resource "kubernetes_cluster_role_binding" "shpod_cluster_admin_${index}" {
  provider = kubernetes.cluster_${index}
  metadata {
    name = "shpod-cluster-admin"
  }
  role_ref {
    api_group = "rbac.authorization.k8s.io"
    kind = "ClusterRole"
    name = "cluster-admin"
  }
  subject {
    api_group = "rbac.authorization.k8s.io"
    kind = "Group"
    name = "shpod-cluster-admins"
  }
}

resource "kubernetes_certificate_signing_request_v1" "cluster_admin_${index}" {
  provider = kubernetes.cluster_${index}
  metadata {
    name = "cluster-admin"
  }
  spec {
    usages = ["client auth"]
    request = tls_cert_request.cluster_admin_${index}.cert_request_pem
    signer_name = "kubernetes.io/kube-apiserver-client"
  }
  auto_approve = true
}

%{ endfor ~}

output "ips_txt" {
  value = join("\n", [
  %{ for index, cluster in clusters ~}
    join("\n", concat(
      split(" ", file("./externalips.${index}"))
    )),
  %{ endfor ~}
  ""
  ])
}

output "logins_jsonl" {
  value = join("\n", [
  %{ for index, cluster in clusters ~}
    jsonencode({
      login = "k8s",
      password = random_string.shpod_${index}.result,
      port = data.kubernetes_service.shpod_${index}.spec[0].port[0].node_port,
      codeServerPort = data.kubernetes_service.shpod_${index}.spec[0].port[1].node_port,
      ipaddrs = replace(file("./externalips.${index}"), " ", "\t"),
    }),
  %{ endfor ~}
  ""
  ])
}
