diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml new file mode 100644 index 0000000..773e61d --- /dev/null +++ b/.github/workflows/lint.yaml @@ -0,0 +1,38 @@ +name: Lint + +on: + pull_request: + branches: + - main + +jobs: + tflint: + runs-on: ${{ matrix.os }} + + strategy: + matrix: + os: [ubuntu-latest] + + steps: + - uses: actions/checkout@v2 + name: Checkout source code + + - uses: actions/cache@v2 + name: Cache plugin dir + with: + path: ~/.tflint.d/plugins + key: ${{ matrix.os }}-tflint-${{ hashFiles('.tflint.hcl') }} + + - uses: terraform-linters/setup-tflint@v1 + name: Setup TFLint + with: + tflint_version: v0.53.0 + + - name: Show version + run: tflint --version + + - name: Init TFLint + run: tflint --init + + - name: Run TFLint + run: tflint -f compact diff --git a/README.md b/README.md index 838b954..2679566 100644 --- a/README.md +++ b/README.md @@ -61,19 +61,26 @@ No modules. | [google_project_iam_member.project](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/project_iam_member) | resource | | [google_project_iam_member.scoped_project](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/project_iam_member) | resource | | [google_project_iam_member.scoped_service_account_user](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/project_iam_member) | resource | +| [google_project_iam_member.workload_identity_project](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/project_iam_member) | resource | +| [google_project_iam_member.workload_identity_scoped_project](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/project_iam_member) | resource | +| [google_project_iam_member.workload_identity_scoped_service_account_user](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/project_iam_member) | resource | | [google_service_account.castai_service_account](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/service_account) | resource | | [google_service_account_key.castai_key](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/service_account_key) | resource | | [castai_gke_user_policies.gke](https://registry.terraform.io/providers/castai/castai/latest/docs/data-sources/gke_user_policies) | data source | -| [google_project.project](https://registry.terraform.io/providers/hashicorp/google/latest/docs/data-sources/project) | data source | ## Inputs | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| +| [cloud\_proxy\_service\_account\_name](#input\_cloud\_proxy\_service\_account\_name) | Name of the cloud-proxy Kubernetes Service Account | `string` | `"castai-cloud-proxy"` | no | +| [cloud\_proxy\_service\_account\_namespace](#input\_cloud\_proxy\_service\_account\_namespace) | Namespace of the cloud-proxy Kubernetes Service Account | `string` | `"castai-agent"` | no | | [compute\_manager\_project\_ids](#input\_compute\_manager\_project\_ids) | Projects list for shared sole tenancy nodes | `list(string)` | `[]` | no | +| [create\_service\_account](#input\_create\_service\_account) | Whether an Service Account with private key should be created | `bool` | `true` | no | | [gke\_cluster\_name](#input\_gke\_cluster\_name) | GKE cluster name for which to create IAM roles | `string` | n/a | yes | | [project\_id](#input\_project\_id) | The project id from GCP | `string` | n/a | yes | | [service\_accounts\_unique\_ids](#input\_service\_accounts\_unique\_ids) | Service Accounts' unique IDs used by node pools in the cluster | `list(string)` | `[]` | no | +| [setup\_cloud\_proxy\_workload\_identity](#input\_setup\_cloud\_proxy\_workload\_identity) | Whether the workload identity for castai-cloud-proxy should be setup | `bool` | `false` | no | +| [workload\_identity\_namespace](#input\_workload\_identity\_namespace) | Override workload identity namespace, default is .svc.id.goog | `string` | `""` | no | ## Outputs diff --git a/main.tf b/main.tf index cb45bd1..f542e1c 100644 --- a/main.tf +++ b/main.tf @@ -1,28 +1,16 @@ ## IAM user required for CAST.AI locals { - service_account_id = "castai-gke-tf-${substr(sha1(var.gke_cluster_name), 0, 8)}" - service_account_email = "${local.service_account_id}@${var.project_id}.iam.gserviceaccount.com" - custom_role_id = "castai.gkeAccess.${substr(sha1(var.gke_cluster_name), 0, 8)}.tf" - condition_expression = join("||", formatlist("resource.name.startsWith(\"projects/-/serviceAccounts/%s\")", var.service_accounts_unique_ids)) - default_permissions = ["roles/iam.serviceAccountUser", "projects/${var.project_id}/roles/${local.custom_role_id}"] - scoped_permissions = ["projects/${var.project_id}/roles/${local.custom_role_id}"] + custom_role_id = "castai.gkeAccess.${substr(sha1(var.gke_cluster_name), 0, 8)}.tf" + condition_expression = join("||", formatlist("resource.name.startsWith(\"projects/-/serviceAccounts/%s\")", var.service_accounts_unique_ids)) + default_permissions = ["roles/iam.serviceAccountUser", "projects/${var.project_id}/roles/${local.custom_role_id}"] + scoped_permissions = ["projects/${var.project_id}/roles/${local.custom_role_id}"] compute_manager_project_ids = var.compute_manager_project_ids } -resource "google_service_account" "castai_service_account" { - account_id = local.service_account_id - display_name = "Service account to manage ${var.gke_cluster_name} cluster via CAST" - project = var.project_id -} - data "castai_gke_user_policies" "gke" {} -data "google_project" "project" { - project_id = var.project_id -} - resource "google_project_iam_custom_role" "castai_role" { role_id = local.custom_role_id title = "Role to manage GKE cluster via CAST AI" @@ -32,43 +20,6 @@ resource "google_project_iam_custom_role" "castai_role" { stage = "GA" } -resource "google_project_iam_member" "project" { - for_each = ( - length(var.service_accounts_unique_ids) == 0 ? - { for permission in local.default_permissions : permission => permission } : - {} - ) - - project = var.project_id - role = each.key - member = "serviceAccount:${local.service_account_email}" -} - -resource "google_project_iam_member" "scoped_project" { - for_each = ( - length(var.service_accounts_unique_ids) > 0 ? - { for permission in local.scoped_permissions : permission => permission } : - {} - ) - project = var.project_id - role = each.key - member = "serviceAccount:${local.service_account_email}" -} - -resource "google_project_iam_member" "scoped_service_account_user" { - count = length(var.service_accounts_unique_ids) > 0 ? 1 : 0 - project = var.project_id - - role = "roles/iam.serviceAccountUser" - member = "serviceAccount:${local.service_account_email}" - - condition { - title = "iam_condition" - description = "IAM condition with limited scope" - expression = local.condition_expression - } -} - resource "google_project_iam_custom_role" "compute_manager_role" { for_each = toset(local.compute_manager_project_ids) @@ -86,10 +37,6 @@ resource "google_project_iam_binding" "compute_manager_binding" { project = each.key role = "projects/${each.key}/roles/castai.gkeAccess.${substr(sha1(each.key), 0, 8)}.tf" - members = ["serviceAccount:${local.service_account_email}"] + members = compact(["serviceAccount:${local.service_account_email}", var.setup_cloud_proxy_workload_identity ? local.workload_identity_sa : null]) } -resource "google_service_account_key" "castai_key" { - service_account_id = google_service_account.castai_service_account.name - public_key_type = "TYPE_X509_PEM_FILE" -} diff --git a/output.tf b/output.tf index 015a69a..c10c948 100644 --- a/output.tf +++ b/output.tf @@ -1,5 +1,5 @@ output "private_key" { - value = base64decode(google_service_account_key.castai_key.private_key) + value = var.create_service_account ? base64decode(google_service_account_key.castai_key[0].private_key) : "" sensitive = true depends_on = [ @@ -12,9 +12,10 @@ output "private_key" { } output "service_account_id" { - value = google_service_account.castai_service_account.account_id + value = var.create_service_account ? google_service_account.castai_service_account[0].account_id : "" } output "service_account_email" { - value = google_service_account.castai_service_account.email + value = var.create_service_account ? google_service_account.castai_service_account[0].email : "" } + diff --git a/service_account.tf b/service_account.tf new file mode 100644 index 0000000..2352c5c --- /dev/null +++ b/service_account.tf @@ -0,0 +1,64 @@ +moved { + from = google_service_account.castai_service_account + to = google_service_account.castai_service_account[0] +} + +moved { + from = google_service_account_key.castai_key + to = google_service_account_key.castai_key[0] +} + +locals { + service_account_id = "castai-gke-tf-${substr(sha1(var.gke_cluster_name), 0, 8)}" + service_account_email = "${local.service_account_id}@${var.project_id}.iam.gserviceaccount.com" +} + +resource "google_service_account" "castai_service_account" { + count = var.create_service_account ? 1 : 0 + account_id = local.service_account_id + display_name = "Service account to manage ${var.gke_cluster_name} cluster via CAST" + project = var.project_id +} + +resource "google_service_account_key" "castai_key" { + count = var.create_service_account ? 1 : 0 + service_account_id = google_service_account.castai_service_account[0].name + public_key_type = "TYPE_X509_PEM_FILE" +} + +resource "google_project_iam_member" "project" { + for_each = ( + var.create_service_account && length(var.service_accounts_unique_ids) == 0 ? + { for permission in local.default_permissions : permission => permission } : + {} + ) + + project = var.project_id + role = each.key + member = "serviceAccount:${local.service_account_email}" +} + +resource "google_project_iam_member" "scoped_project" { + for_each = ( + var.create_service_account && length(var.service_accounts_unique_ids) > 0 ? + { for permission in local.scoped_permissions : permission => permission } : + {} + ) + project = var.project_id + role = each.key + member = "serviceAccount:${local.service_account_email}" +} + +resource "google_project_iam_member" "scoped_service_account_user" { + count = var.create_service_account && length(var.service_accounts_unique_ids) > 0 ? 1 : 0 + project = var.project_id + + role = "roles/iam.serviceAccountUser" + member = "serviceAccount:${local.service_account_email}" + + condition { + title = "iam_condition" + description = "IAM condition with limited scope" + expression = local.condition_expression + } +} diff --git a/variables.tf b/variables.tf index 4c8502d..34aba7b 100644 --- a/variables.tf +++ b/variables.tf @@ -19,3 +19,34 @@ variable "compute_manager_project_ids" { description = "Projects list for shared sole tenancy nodes" default = [] } + +variable "create_service_account" { + type = bool + description = "Whether an Service Account with private key should be created" + default = true +} + +variable "setup_cloud_proxy_workload_identity" { + type = bool + description = "Whether the workload identity for castai-cloud-proxy should be setup" + default = false +} + +variable "workload_identity_namespace" { + type = string + description = "Override workload identity namespace, default is .svc.id.goog" + default = "" +} + +variable "cloud_proxy_service_account_namespace" { + type = string + description = "Namespace of the cloud-proxy Kubernetes Service Account" + default = "castai-agent" +} + + +variable "cloud_proxy_service_account_name" { + type = string + description = "Name of the cloud-proxy Kubernetes Service Account" + default = "castai-cloud-proxy" +} diff --git a/workload_identity.tf b/workload_identity.tf new file mode 100644 index 0000000..bce5e05 --- /dev/null +++ b/workload_identity.tf @@ -0,0 +1,42 @@ +locals { + workload_identity_namespace = var.workload_identity_namespace != "" ? var.workload_identity_namespace : "${var.project_id}.svc.id.goog" + workload_identity_sa = "serviceAccount:${local.workload_identity_namespace}[${var.cloud_proxy_service_account_namespace}/${var.cloud_proxy_service_account_name}]" +} + +resource "google_project_iam_member" "workload_identity_project" { + for_each = ( + var.setup_cloud_proxy_workload_identity && length(var.service_accounts_unique_ids) == 0 ? + { for permission in local.default_permissions : permission => permission } : + {} + ) + + project = var.project_id + role = each.key + member = local.workload_identity_sa +} + +resource "google_project_iam_member" "workload_identity_scoped_project" { + for_each = ( + var.setup_cloud_proxy_workload_identity && length(var.service_accounts_unique_ids) > 0 ? + { for permission in local.scoped_permissions : permission => permission } : + {} + ) + project = var.project_id + role = each.key + member = local.workload_identity_sa +} + +resource "google_project_iam_member" "workload_identity_scoped_service_account_user" { + count = var.setup_cloud_proxy_workload_identity && length(var.service_accounts_unique_ids) > 0 ? 1 : 0 + project = var.project_id + + role = "roles/iam.serviceAccountUser" + member = local.workload_identity_sa + + condition { + title = "iam_condition" + description = "IAM condition with limited scope" + expression = local.condition_expression + } +} +