diff --git a/examples/create_rosa_cluster/create_rosa_sts_cluster/main.tf b/examples/create_rosa_cluster/create_rosa_sts_cluster/main.tf index 6021545..7586678 100644 --- a/examples/create_rosa_cluster/create_rosa_sts_cluster/main.tf +++ b/examples/create_rosa_cluster/create_rosa_sts_cluster/main.tf @@ -37,42 +37,11 @@ locals { sts_roles = { role_arn = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:role/ManagedOpenShift-Installer-Role", support_role_arn = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:role/ManagedOpenShift-Support-Role", - operator_iam_roles = [ - { - name = "cloud-credential-operator-iam-ro-creds", - namespace = "openshift-cloud-credential-operator", - role_arn = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:role/${var.operator_role_prefix}-openshift-cloud-credential-operator-cloud-c", - }, - { - name = "installer-cloud-credentials", - namespace = "openshift-image-registry", - role_arn = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:role/${var.operator_role_prefix}-openshift-image-registry-installer-cloud-cr", - }, - { - name = "cloud-credentials", - namespace = "openshift-ingress-operator", - role_arn = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:role/${var.operator_role_prefix}-openshift-ingress-operator-cloud-credential", - }, - { - name = "ebs-cloud-credentials", - namespace = "openshift-cluster-csi-drivers", - role_arn = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:role/${var.operator_role_prefix}-openshift-cluster-csi-drivers-ebs-cloud-cre", - }, - { - name = "cloud-credentials", - namespace = "openshift-cloud-network-config-controller", - role_arn = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:role/${var.operator_role_prefix}-openshift-cloud-network-config-controller-c", - }, - { - name = "aws-cloud-credentials", - namespace = "openshift-machine-api", - role_arn = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:role/${var.operator_role_prefix}-openshift-machine-api-aws-cloud-credentials", - }, - ] instance_iam_roles = { master_role_arn = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:role/ManagedOpenShift-ControlPlane-Role", worker_role_arn = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:role/ManagedOpenShift-Worker-Role" - }, + }, + operator_role_prefix = var.operator_role_prefix, } } @@ -90,13 +59,17 @@ resource "ocm_cluster_rosa_classic" "rosa_sts_cluster" { sts = local.sts_roles } +data "ocm_rosa_operator_roles" "operator_roles" { + cluster_id = ocm_cluster_rosa_classic.rosa_sts_cluster.id + operator_role_prefix = var.operator_role_prefix + account_role_prefix = var.account_role_prefix +} + module operator_roles { - source = "git::https://github.com/openshift-online/terraform-provider-ocm.git//modules/operator_roles" + source = "git::https://github.com/openshift-online/terraform-provider-ocm.git//modules/aws_roles" cluster_id = ocm_cluster_rosa_classic.rosa_sts_cluster.id - operator_role_prefix = var.operator_role_prefix - account_role_prefix = var.account_role_prefix rh_oidc_provider_thumbprint = ocm_cluster_rosa_classic.rosa_sts_cluster.sts.thumbprint rh_oidc_provider_url = ocm_cluster_rosa_classic.rosa_sts_cluster.sts.oidc_endpoint_url - + operator_roles_properties = data.ocm_rosa_operator_roles.operator_roles.operator_iam_roles } diff --git a/examples/create_rosa_cluster/create_rosa_sts_cluster/output.tf b/examples/create_rosa_cluster/create_rosa_sts_cluster/output.tf new file mode 100644 index 0000000..a84947a --- /dev/null +++ b/examples/create_rosa_cluster/create_rosa_sts_cluster/output.tf @@ -0,0 +1,15 @@ +output operator_iam_roles { + value = data.ocm_rosa_operator_roles.operator_roles.operator_iam_roles +} + +output cluster_id { + value = ocm_cluster_rosa_classic.rosa_sts_cluster.id +} + +output rh_oidc_provider_thumbprint { + value = ocm_cluster_rosa_classic.rosa_sts_cluster.sts.thumbprint +} + +output rh_oidc_provider_url { + value = ocm_cluster_rosa_classic.rosa_sts_cluster.sts.oidc_endpoint_url +} \ No newline at end of file diff --git a/examples/create_rosa_cluster/create_rosa_sts_cluster/variables.tf b/examples/create_rosa_cluster/create_rosa_sts_cluster/variables.tf index 6011bf3..f22f81b 100644 --- a/examples/create_rosa_cluster/create_rosa_sts_cluster/variables.tf +++ b/examples/create_rosa_cluster/create_rosa_sts_cluster/variables.tf @@ -9,6 +9,7 @@ variable operator_role_prefix { variable account_role_prefix { type = string + default = "" } variable url { diff --git a/go.mod b/go.mod index 3403c32..3d6d22a 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/openshift-online/terraform-provider-ocm go 1.17 require ( + github.com/hashicorp/go-version v1.3.0 github.com/hashicorp/terraform-plugin-framework v0.5.0 github.com/hashicorp/terraform-plugin-go v0.5.0 github.com/onsi/ginkgo/v2 v2.1.4 @@ -69,4 +70,4 @@ require ( gopkg.in/yaml.v3 v3.0.1 // indirect ) -replace github.com/openshift-online/ocm-sdk-go v0.1.240 => github.com/openshift-online/ocm-sdk-go v0.1.275 +replace github.com/openshift-online/ocm-sdk-go => github.com/openshift-online/ocm-sdk-go v0.1.275 diff --git a/go.sum b/go.sum index 8672ff0..b8e0996 100644 --- a/go.sum +++ b/go.sum @@ -168,6 +168,8 @@ github.com/hashicorp/go-plugin v1.4.1/go.mod h1:5fGEH17QVwTTcR0zV7yhDPLLmFX9YSZ3 github.com/hashicorp/go-uuid v1.0.2 h1:cfejS+Tpcp13yd5nYHWDI6qVCny6wyX2Mt5SGur2IGE= github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/go-version v1.3.0 h1:McDWVJIU/y+u1BRV06dPaLfLCaT7fUTJLp5r04x7iNw= +github.com/hashicorp/go-version v1.3.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/terraform-plugin-framework v0.5.0 h1:QUBNSZHiRJrQpbjqCdPcw5MRLU1TyzpQCrA4eRId364= diff --git a/modules/operator_roles/LICENSE b/modules/aws_roles/LICENSE similarity index 100% rename from modules/operator_roles/LICENSE rename to modules/aws_roles/LICENSE diff --git a/modules/aws_roles/README.md b/modules/aws_roles/README.md new file mode 100644 index 0000000..31c8f96 --- /dev/null +++ b/modules/aws_roles/README.md @@ -0,0 +1,177 @@ +# operator role module + +Create rosa operator roles and identity provider in an declarative way +Terraform AWS ROSA STS Roles + +In order to deploy [ROSA](https://docs.openshift.com/rosa/welcome/index.html) with [STS](https://docs.openshift.com/rosa/rosa_planning/rosa-sts-aws-prereqs.html), AWS Account needs to have the following roles placed: + +* Account Roles (One per AWS account) +* OCM Roles (For OCM UI, One per OCM Org) +* User Role (For OCM UI, One per OCM user account) +* Operator Roles (One Per Cluster) +* OIDC Identity Provider (One Per Cluster) + +This terraform module tries to replicate rosa CLI roles creation so that: + +* Users have a declartive way to create AWS roles. +* Users can implement security/infrastructure as code practices. +* Batch creation of operator roles. + +## Prerequisites + +* AWS Admin Account configured by using AWS CLI in AWS configuration file +* OCM Account and OCM CLI +* ROSA CLI + +## Get OCM Information + +When creating operator IAM roles, the roles require cluster id, operator role prefix, OIDC endpoint url and thumbprint + + +The information can be retrieved from ocm cli. +``` +ocm whoami +{ + "kind": "Account", + "id": "26kcPSEHi0Y6MkTS7OowxfFYmZo", + "href": "/api/accounts_mgmt/v1/accounts/26kcPSEHi0Y6MkTS7OowxfFYmZo", + "created_at": "2022-03-22T17:43:19Z", + "email": "shading+mobb@redhat.com", + "first_name": "Shaozhen", + "last_name": "Ding", + "organization": { + "kind": "Organization", + "id": "1rkxPO7W12geIcRWITwI0I8VIQV", + "href": "/api/accounts_mgmt/v1/organizations/1rkxPO7W12geIcRWITwI0I8VIQV", + "created_at": "2021-04-27T14:31:03Z", + "ebs_account_id": "7113273", + "external_id": "14540493", + "name": "Red Hat, Inc.", + "updated_at": "2022-06-16T14:17:02Z" + }, + "updated_at": "2022-05-25T02:02:02Z", + "username": "shading_mobb" +} +``` + +## Get Clusters Information. + +In order to create operator roles for clusters. +Users need to provide cluster id, OIDC Endpoint URL and thumbprint and operator roles properties list. + +``` + rosa describe cluster -c shaozhenprivate -o json +{ + "kind": "Cluster", + "id": "1srtno3qggal8ujsegvtb2njvbmhdu8c", + "href": "/api/clusters_mgmt/v1/clusters/1srtno3qggal8ujsegvtb2njvbmhdu8c", + "aws": { + "sts": { + "oidc_endpoint_url": "https://rh-oidc.s3.us-east-1.amazonaws.com/1srtno3qggal8ujsegvtb2njvbmhdu8c", + "operator_iam_roles": [ + { + "id": "", + "name": "ebs-cloud-credentials", + "namespace": "openshift-cluster-csi-drivers", + "role_arn": "arn:aws:iam::${AWS_ACCOUNT_ID}:role/shaozhenprivate-w4e1-openshift-cluster-csi-drivers-ebs-cloud-cre", + "service_account": "" + }, +``` + +In the above example: + +* cluster_id = 1srtno3qggal8ujsegvtb2njvbmhdu8c +* operator_role_prefix = shaozhenprivate-w4e1 +* account_role_prefix = ManagedOpenShift +* rh_oidc_endpoint_url = rh-oidc.s3.us-east-1.amazonaws.com +* thumberprint - calculated + + +The operator roles properties variable is the output of the data source `ocm_rosa_operator_roles` and it's a list of 6 maps which looks like: +``` +operator_iam_roles = [ + { + "operator_name" = "cloud-credentials" + "operator_namespace" = "openshift-ingress-operator" + "policy_name" = "ManagedOpenShift-openshift-ingress-operator-cloud-credentials" + "role_arn" = "arn:aws:iam::765374464689:role/terrafom-operator-openshift-ingress-operator-cloud-credentials" + "role_name" = "terrafom-operator-openshift-ingress-operator-cloud-credentials" + "service_accounts" = [ + "system:serviceaccount:openshift-ingress-operator:ingress-operator", + ] + }, + { + "operator_name" = "ebs-cloud-credentials" + "operator_namespace" = "openshift-cluster-csi-drivers" + "policy_name" = "ManagedOpenShift-openshift-cluster-csi-drivers-ebs-cloud-credent" + "role_arn" = "arn:aws:iam::765374464689:role/terrafom-operator-openshift-cluster-csi-drivers-ebs-cloud-creden" + "role_name" = "terrafom-operator-openshift-cluster-csi-drivers-ebs-cloud-creden" + "service_accounts" = [ + "system:serviceaccount:openshift-cluster-csi-drivers:aws-ebs-csi-driver-operator", + "system:serviceaccount:openshift-cluster-csi-drivers:aws-ebs-csi-driver-controller-sa", + ] + }, + { + "operator_name" = "cloud-credentials" + "operator_namespace" = "openshift-cloud-network-config-controller" + "policy_name" = "ManagedOpenShift-openshift-cloud-network-config-controller-cloud" + "role_arn" = "arn:aws:iam::765374464689:role/terrafom-operator-openshift-cloud-network-config-controller-clou" + "role_name" = "terrafom-operator-openshift-cloud-network-config-controller-clou" + "service_accounts" = [ + "system:serviceaccount:openshift-cloud-network-config-controller:cloud-network-config-controller", + ] + }, + { + "operator_name" = "aws-cloud-credentials" + "operator_namespace" = "openshift-machine-api" + "policy_name" = "ManagedOpenShift-openshift-machine-api-aws-cloud-credentials" + "role_arn" = "arn:aws:iam::765374464689:role/terrafom-operator-openshift-machine-api-aws-cloud-credentials" + "role_name" = "terrafom-operator-openshift-machine-api-aws-cloud-credentials" + "service_accounts" = [ + "system:serviceaccount:openshift-machine-api:machine-api-controllers", + ] + }, + { + "operator_name" = "cloud-credential-operator-iam-ro-creds" + "operator_namespace" = "openshift-cloud-credential-operator" + "policy_name" = "ManagedOpenShift-openshift-cloud-credential-operator-cloud-crede" + "role_arn" = "arn:aws:iam::765374464689:role/terrafom-operator-openshift-cloud-credential-operator-cloud-cred" + "role_name" = "terrafom-operator-openshift-cloud-credential-operator-cloud-cred" + "service_accounts" = [ + "system:serviceaccount:openshift-cloud-credential-operator:cloud-credential-operator", + ] + }, + { + "operator_name" = "installer-cloud-credentials" + "operator_namespace" = "openshift-image-registry" + "policy_name" = "ManagedOpenShift-openshift-image-registry-installer-cloud-creden" + "role_arn" = "arn:aws:iam::765374464689:role/terrafom-operator-openshift-image-registry-installer-cloud-crede" + "role_name" = "terrafom-operator-openshift-image-registry-installer-cloud-crede" + "service_accounts" = [ + "system:serviceaccount:openshift-image-registry:cluster-image-registry-operator", + "system:serviceaccount:openshift-image-registry:registry", + ] + }, +] + +``` +## Usage + +### Sample Usage + +``` +data "ocm_rosa_operator_roles" "operator_roles" { + cluster_id = ocm_cluster_rosa_classic.rosa_sts_cluster.id + operator_role_prefix = var.operator_role_prefix + account_role_prefix = var.account_role_prefix +} + +module operator_roles { + source = "git::https://github.com/openshift-online/terraform-provider-ocm.git//modules/operator_roles" + + cluster_id = ocm_cluster_rosa_classic.rosa_sts_cluster.id + rh_oidc_provider_thumbprint = ocm_cluster_rosa_classic.rosa_sts_cluster.sts.thumbprint + rh_oidc_provider_url = ocm_cluster_rosa_classic.rosa_sts_cluster.sts.oidc_endpoint_url + operator_roles_properties = data.ocm_rosa_operator_roles.operator_roles.operator_iam_roles +} +``` diff --git a/modules/operator_roles/cloud_identity_provider.tf b/modules/aws_roles/cloud_identity_provider.tf similarity index 100% rename from modules/operator_roles/cloud_identity_provider.tf rename to modules/aws_roles/cloud_identity_provider.tf diff --git a/modules/aws_roles/main.tf b/modules/aws_roles/main.tf new file mode 100644 index 0000000..ebb6cb6 --- /dev/null +++ b/modules/aws_roles/main.tf @@ -0,0 +1,19 @@ +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 4.0" + } + } +} + +module rosa_operator_roles { + source = "./operator_roles" + count = 6 + + cluster_id = var.cluster_id + rh_oidc_provider_url = var.rh_oidc_provider_url + rh_oidc_provider_thumbprint = var.rh_oidc_provider_thumbprint + operator_role_properties = var.operator_roles_properties[count.index] +} + diff --git a/modules/operator_roles/machine_api_role.tf b/modules/aws_roles/operator_roles/operator_role_resource.tf similarity index 50% rename from modules/operator_roles/machine_api_role.tf rename to modules/aws_roles/operator_roles/operator_role_resource.tf index 1bc9369..66ee58d 100644 --- a/modules/operator_roles/machine_api_role.tf +++ b/modules/aws_roles/operator_roles/operator_role_resource.tf @@ -1,5 +1,7 @@ -resource "aws_iam_role" "machine_api_role" { - name = "${var.operator_role_prefix}-openshift-machine-api-aws-cloud-credentials" +data "aws_caller_identity" "current" {} + +resource "aws_iam_role" "operator_role" { + name = var.operator_role_properties.role_name assume_role_policy = jsonencode({ Version = "2012-10-17" Statement = [ @@ -8,7 +10,7 @@ resource "aws_iam_role" "machine_api_role" { Effect = "Allow" Condition = { StringEquals = { - "${var.rh_oidc_provider_url}:sub" = ["system:serviceaccount:openshift-machine-api:machine-api-controllers"] + "${var.rh_oidc_provider_url}:sub" = var.operator_role_properties.service_accounts } } Principal = { @@ -21,12 +23,13 @@ resource "aws_iam_role" "machine_api_role" { tags = { red-hat-managed = true rosa_cluster_id = var.cluster_id - operator_namespace = "openshift-machine-api" - operator_name = "aws-cloud-credentials" + operator_namespace = var.operator_role_properties.operator_namespace + operator_name = var.operator_role_properties.operator_name } } -resource "aws_iam_role_policy_attachment" "machine_api_role_policy_attachment" { - role = aws_iam_role.machine_api_role.name - policy_arn = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:policy/${var.account_role_prefix}-openshift-machine-api-aws-cloud-credentials" -} \ No newline at end of file +resource "aws_iam_role_policy_attachment" "operator_role_policy_attachment" { + role = aws_iam_role.operator_role.name + policy_arn = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:policy/${var.operator_role_properties.policy_name}" +} + diff --git a/modules/aws_roles/operator_roles/variables.tf b/modules/aws_roles/operator_roles/variables.tf new file mode 100644 index 0000000..a051ef2 --- /dev/null +++ b/modules/aws_roles/operator_roles/variables.tf @@ -0,0 +1,29 @@ +variable cluster_id { + description = "cluster ID" + type = string +} + +variable rh_oidc_provider_url { + description = "oidc provider url" + type = string + default = "rh-oidc.s3.us-east-1.amazonaws.com" +} + +variable rh_oidc_provider_thumbprint { + description = "Thumbprint for the variable `rh_oidc_provider_url`" + type = string + default = "917e732d330f9a12404f73d8bea36948b929dffc" +} + +variable operator_role_properties { + description = "" + type = object({ + role_name = string + policy_name = string + service_accounts = list(string) + operator_name = string + operator_namespace = string + role_arn = string + }) + +} diff --git a/modules/aws_roles/variables.tf b/modules/aws_roles/variables.tf new file mode 100644 index 0000000..8820745 --- /dev/null +++ b/modules/aws_roles/variables.tf @@ -0,0 +1,33 @@ +variable cluster_id { + description = "cluster ID" + type = string +} + +variable rh_oidc_provider_url { + description = "oidc provider url" + type = string + default = "rh-oidc.s3.us-east-1.amazonaws.com" +} + +variable rh_oidc_provider_thumbprint { + description = "Thumbprint for https://rh-oidc.s3.us-east-1.amazonaws.com" + type = string + default = "917e732d330f9a12404f73d8bea36948b929dffc" +} + +variable operator_roles_properties { + description = "List of ROSA Operator IAM Roles" + type = list(object({ + role_name = string + policy_name = string + service_accounts = list(string) + operator_name = string + operator_namespace = string + role_arn = string + })) + validation { + condition = length(var.operator_roles_properties) == 6 + error_message = "The list of operator roles should contains 6 elements" + } + +} diff --git a/modules/operator_roles/README.md b/modules/operator_roles/README.md deleted file mode 100644 index 1e9c0ea..0000000 --- a/modules/operator_roles/README.md +++ /dev/null @@ -1,102 +0,0 @@ -# operator role module - -Create rosa operator roles and identity provider in an declarative way -Terraform AWS ROSA STS Roles - -In order to deploy [ROSA](https://docs.openshift.com/rosa/welcome/index.html) with [STS](https://docs.openshift.com/rosa/rosa_planning/rosa-sts-aws-prereqs.html), AWS Account needs to have the following roles placed: - -* Account Roles (One per AWS account) -* OCM Roles (For OCM UI, One per OCM Org) -* User Role (For OCM UI, One per OCM user account) -* Operator Roles (One Per Cluster) -* OIDC Identity Provider (One Per Cluster) - -This terraform module tries to replicate rosa CLI roles creation so that: - -* Users have a declartive way to create AWS roles. -* Users can implement security/infrastructure as code practices. -* Batch creation of operator roles. - -## Prerequisites - -* AWS Admin Account -* OCM Account and OCM CLI -* ROSA CLI - -## Get OCM Information - -When creating operator IAM roles, the roles require cluster id, operator role prefix, OIDC endpoint url and thumbprint - - -The information can be retrieved from ocm cli. -``` -ocm whoami -{ - "kind": "Account", - "id": "26kcPSEHi0Y6MkTS7OowxfFYmZo", - "href": "/api/accounts_mgmt/v1/accounts/26kcPSEHi0Y6MkTS7OowxfFYmZo", - "created_at": "2022-03-22T17:43:19Z", - "email": "shading+mobb@redhat.com", - "first_name": "Shaozhen", - "last_name": "Ding", - "organization": { - "kind": "Organization", - "id": "1rkxPO7W12geIcRWITwI0I8VIQV", - "href": "/api/accounts_mgmt/v1/organizations/1rkxPO7W12geIcRWITwI0I8VIQV", - "created_at": "2021-04-27T14:31:03Z", - "ebs_account_id": "7113273", - "external_id": "14540493", - "name": "Red Hat, Inc.", - "updated_at": "2022-06-16T14:17:02Z" - }, - "updated_at": "2022-05-25T02:02:02Z", - "username": "shading_mobb" -} -``` - -## Get Clusters Information. - -In order to create operator roles for clusters. Users need to provide cluster id, operator role prefix, OIDC Endpoint URL and thumbprint - -``` - rosa describe cluster -c shaozhenprivate -o json -{ - "kind": "Cluster", - "id": "1srtno3qggal8ujsegvtb2njvbmhdu8c", - "href": "/api/clusters_mgmt/v1/clusters/1srtno3qggal8ujsegvtb2njvbmhdu8c", - "aws": { - "sts": { - "oidc_endpoint_url": "https://rh-oidc.s3.us-east-1.amazonaws.com/1srtno3qggal8ujsegvtb2njvbmhdu8c", - "operator_iam_roles": [ - { - "id": "", - "name": "ebs-cloud-credentials", - "namespace": "openshift-cluster-csi-drivers", - "role_arn": "arn:aws:iam::${AWS_ACCOUNT_ID}:role/shaozhenprivate-w4e1-openshift-cluster-csi-drivers-ebs-cloud-cre", - "service_account": "" - }, -``` - -In the above example: - -* cluster_id = 1srtno3qggal8ujsegvtb2njvbmhdu8c -* operator_role_prefix = shaozhenprivate-w4e1 -* account_role_prefix = ManagedOpenShift -* rh_oidc_endpoint_url = rh-oidc.s3.us-east-1.amazonaws.com -* thumberprint - calculated - -## Usage - -### Sample Usage - -``` -module operator_role { - source = "https://github.com/openshift-online/terraform-provider-ocm/modules/ocm_cluster_rosa_classic/operator_roles" - ## Create a list of operator roles for clusters. The cluster id and operator_role_prefix can retrieve from OCM. - cluster_id = "1ssjjr1b2npkg9c70e8kqehfeqmscqeu" - operator_role_prefix = "terraform-prefix" - account_role_prefix = "ManagedOpenShift" - rh_oidc_provider_url = "rh-oidc.s3.us-east-1.amazonaws.com/1srtno3qggal8ujsegvtb2njvbmhdu8c" - rh_oidc_provider_thumbprint = "917e732d330f9a12404f73d8bea36948b929dffc" -} -``` diff --git a/modules/operator_roles/cloud_credential_role.tf b/modules/operator_roles/cloud_credential_role.tf deleted file mode 100644 index 2468e2a..0000000 --- a/modules/operator_roles/cloud_credential_role.tf +++ /dev/null @@ -1,32 +0,0 @@ -resource "aws_iam_role" "cloud-credential_role" { - name = "${var.operator_role_prefix}-openshift-cloud-credential-operator-cloud-c" - assume_role_policy = jsonencode({ - Version = "2012-10-17" - Statement = [ - { - Action = "sts:AssumeRoleWithWebIdentity" - Effect = "Allow" - Condition = { - StringEquals = { - "${var.rh_oidc_provider_url}:sub" = ["system:serviceaccount:openshift-cloud-credential-operator:cloud-credential-operator"] - } - } - Principal = { - Federated = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:oidc-provider/${var.rh_oidc_provider_url}" - } - }, - ] - }) - - tags = { - red-hat-managed = true - rosa_cluster_id = var.cluster_id - operator_namespace = "openshift-cloud-credential-operator" - operator_name = "cloud-credential-operator-iam-ro-creds" - } -} - -resource "aws_iam_role_policy_attachment" "cloud-credential_role_policy_attachment" { - role = aws_iam_role.cloud-credential_role.name - policy_arn = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:policy/${var.account_role_prefix}-openshift-cloud-credential-operator-cloud-crede" -} \ No newline at end of file diff --git a/modules/operator_roles/cloud_network_config_role.tf b/modules/operator_roles/cloud_network_config_role.tf deleted file mode 100644 index 1268738..0000000 --- a/modules/operator_roles/cloud_network_config_role.tf +++ /dev/null @@ -1,32 +0,0 @@ -resource "aws_iam_role" "cloud_network_config_role" { - name = "${var.operator_role_prefix}-openshift-cloud-network-config-controller-c" - assume_role_policy = jsonencode({ - Version = "2012-10-17" - Statement = [ - { - Action = "sts:AssumeRoleWithWebIdentity" - Effect = "Allow" - Condition = { - StringEquals = { - "${var.rh_oidc_provider_url}:sub" = ["system:serviceaccount:openshift-cloud-network-config-controller:cloud-network-config-controller"] - } - } - Principal = { - Federated = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:oidc-provider/${var.rh_oidc_provider_url}" - } - }, - ] - }) - - tags = { - red-hat-managed = true - rosa_cluster_id = var.cluster_id - operator_namespace = "openshift-cloud-network-config-controller" - operator_name = "cloud-credentials" - } -} - -resource "aws_iam_role_policy_attachment" "cloud_network_config_role_policy_attachment" { - role = aws_iam_role.cloud_network_config_role.name - policy_arn = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:policy/${var.account_role_prefix}-openshift-cloud-network-config-controller-cloud" -} \ No newline at end of file diff --git a/modules/operator_roles/csi_driver_role.tf b/modules/operator_roles/csi_driver_role.tf deleted file mode 100644 index 8a673ff..0000000 --- a/modules/operator_roles/csi_driver_role.tf +++ /dev/null @@ -1,35 +0,0 @@ -resource "aws_iam_role" "csi_drivers_role" { - name = "${var.operator_role_prefix}-openshift-cluster-csi-drivers-ebs-cloud-cre" - assume_role_policy = jsonencode({ - Version = "2012-10-17" - Statement = [ - { - Action = "sts:AssumeRoleWithWebIdentity" - Effect = "Allow" - Condition = { - StringEquals = { - "${var.rh_oidc_provider_url}:sub" = [ - "system:serviceaccount:openshift-cluster-csi-drivers:aws-ebs-csi-driver-operator", - "system:serviceaccount:openshift-cluster-csi-drivers:aws-ebs-csi-driver-controller-sa" - ] - } - } - Principal = { - Federated = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:oidc-provider/${var.rh_oidc_provider_url}" - } - }, - ] - }) - - tags = { - red-hat-managed = true - rosa_cluster_id = var.cluster_id - operator_namespace = "openshift-cluster-csi-drivers" - operator_name = "ebs-cloud-credentials" - } -} - -resource "aws_iam_role_policy_attachment" "csi_drivers_role_policy_attachment" { - role = aws_iam_role.csi_drivers_role.name - policy_arn = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:policy/${var.account_role_prefix}-openshift-cluster-csi-drivers-ebs-cloud-credent" -} \ No newline at end of file diff --git a/modules/operator_roles/current_account.tf b/modules/operator_roles/current_account.tf deleted file mode 100644 index d78fce4..0000000 --- a/modules/operator_roles/current_account.tf +++ /dev/null @@ -1 +0,0 @@ -data "aws_caller_identity" "current" {} \ No newline at end of file diff --git a/modules/operator_roles/image_registry_role.tf b/modules/operator_roles/image_registry_role.tf deleted file mode 100644 index d51381b..0000000 --- a/modules/operator_roles/image_registry_role.tf +++ /dev/null @@ -1,35 +0,0 @@ -resource "aws_iam_role" "image_registry_role" { - name = "${var.operator_role_prefix}-openshift-image-registry-installer-cloud-cr" - assume_role_policy = jsonencode({ - Version = "2012-10-17" - Statement = [ - { - Action = "sts:AssumeRoleWithWebIdentity" - Effect = "Allow" - Condition = { - StringEquals = { - "${var.rh_oidc_provider_url}:sub" = [ - "system:serviceaccount:openshift-image-registry:cluster-image-registry-operator", - "system:serviceaccount:openshift-image-registry:registry" - ] - } - } - Principal = { - Federated = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:oidc-provider/${var.rh_oidc_provider_url}" - } - }, - ] - }) - - tags = { - red-hat-managed = true - rosa_cluster_id = var.cluster_id - operator_namespace = "openshift-image-registry" - operator_name = "installer-cloud-credentials" - } -} - -resource "aws_iam_role_policy_attachment" "image_registry_role_policy_attachment" { - role = aws_iam_role.image_registry_role.name - policy_arn = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:policy/${var.account_role_prefix}-openshift-image-registry-installer-cloud-creden" -} \ No newline at end of file diff --git a/modules/operator_roles/ingress_role.tf b/modules/operator_roles/ingress_role.tf deleted file mode 100644 index d64686b..0000000 --- a/modules/operator_roles/ingress_role.tf +++ /dev/null @@ -1,32 +0,0 @@ -resource "aws_iam_role" "ingress_operator_role" { - name = "${var.operator_role_prefix}-openshift-ingress-operator-cloud-credential" - assume_role_policy = jsonencode({ - Version = "2012-10-17" - Statement = [ - { - Action = "sts:AssumeRoleWithWebIdentity" - Effect = "Allow" - Condition = { - StringEquals = { - "${var.rh_oidc_provider_url}:sub" = ["system:serviceaccount:openshift-ingress-operator:ingress-operator"] - } - } - Principal = { - Federated = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:oidc-provider/${var.rh_oidc_provider_url}" - } - }, - ] - }) - - tags = { - red-hat-managed = true - rosa_cluster_id = var.cluster_id - operator_namespace = "openshift-ingress-operator" - operator_name = "cloud-credentials" - } -} - -resource "aws_iam_role_policy_attachment" "ingress_operator_role_policy_attachment" { - role = aws_iam_role.ingress_operator_role.name - policy_arn = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:policy/${var.account_role_prefix}-openshift-ingress-operator-cloud-credentials" -} \ No newline at end of file diff --git a/modules/operator_roles/main.tf b/modules/operator_roles/main.tf deleted file mode 100644 index a89221e..0000000 --- a/modules/operator_roles/main.tf +++ /dev/null @@ -1,9 +0,0 @@ -terraform { - required_providers { - aws = { - source = "hashicorp/aws" - version = "~> 4.0" - } - } -} - diff --git a/modules/operator_roles/variables.tf b/modules/operator_roles/variables.tf deleted file mode 100644 index 7318191..0000000 --- a/modules/operator_roles/variables.tf +++ /dev/null @@ -1,30 +0,0 @@ -variable cluster_id { - description = "cluster ID" - type = string -} - -variable operator_role_prefix { - description = "operator role prefix" - type = string - default = "terraform-rosa-classic" -} - -variable rh_oidc_provider_url { - description = "oidc provider url" - type = string - default = "rh-oidc.s3.us-east-1.amazonaws.com" -} - -variable rh_oidc_provider_thumbprint { - description = "Thumbprint for https://rh-oidc.s3.us-east-1.amazonaws.com" - type = string - default = "917e732d330f9a12404f73d8bea36948b929dffc" -} - - -variable account_role_prefix { - description = "account role prefix" - type = string - default = "ManagedOpenShift" -} - diff --git a/provider/cluster_rosa_classic_resource.go b/provider/cluster_rosa_classic_resource.go index 1f26917..91009ce 100644 --- a/provider/cluster_rosa_classic_resource.go +++ b/provider/cluster_rosa_classic_resource.go @@ -22,19 +22,22 @@ import ( "encoding/hex" "fmt" "github.com/hashicorp/terraform-plugin-framework/attr" + "net/http" + "net/url" + "strings" + + semver "github.com/hashicorp/go-version" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" cmv1 "github.com/openshift-online/ocm-sdk-go/clustersmgmt/v1" "github.com/openshift-online/ocm-sdk-go/logging" - "net/http" - "net/url" - "strings" ) const ( awsCloudProvider = "aws" rosaProduct = "rosa" + MinVersion = "4.10" ) type ClusterRosaClassicResourceType struct { @@ -240,7 +243,7 @@ func (t *ClusterRosaClassicResourceType) NewResource(ctx context.Context, // Cast the provider interface to the specific implementation: parent := p.(*Provider) - // Get the collection of clusters: + // Get the collection: collection := parent.connection.ClustersMgmt().V1().Clusters() // Create the resource: @@ -345,15 +348,7 @@ func (r *ClusterRosaClassicResource) Create(ctx context.Context, instanceIamRoles.WorkerRoleARN(state.Sts.InstanceIAMRoles.WorkerRoleARN.Value) sts.InstanceIAMRoles(instanceIamRoles) - operatorRoles := make([]*cmv1.OperatorIAMRoleBuilder, 0) - for _, operatorRole := range state.Sts.OperatorIAMRoles { - r := cmv1.NewOperatorIAMRole() - r.Name(operatorRole.Name.Value) - r.Namespace(operatorRole.Namespace.Value) - r.RoleARN(operatorRole.RoleARN.Value) - operatorRoles = append(operatorRoles, r) - } - sts.OperatorIAMRoles(operatorRoles...) + sts.OperatorRolePrefix(state.Sts.OperatorRolePrefix.Value) aws.STS(sts) } @@ -385,7 +380,32 @@ func (r *ClusterRosaClassicResource) Create(ctx context.Context, builder.Network(network) } if !state.Version.Unknown && !state.Version.Null { - builder.Version(cmv1.NewVersion().ID(state.Version.Value)) + // TODO: update it to support all cluster versions + isSupported, err := checkSupportedVersion(state.Version.Value) + if err != nil { + r.logger.Error(ctx, "Error validating required cluster version %s\", err)") + response.Diagnostics.AddError( + "Can't build cluster", + fmt.Sprintf( + "Can't check if cluster version is supported '%s': %v", + state.Version.Value, err, + ), + ) + return + } + if isSupported { + builder.Version(cmv1.NewVersion().ID(state.Version.Value)) + } else { + r.logger.Error(ctx, "Cluster version %s is not supported", state.Version.Value) + response.Diagnostics.AddError( + "Can't build cluster", + fmt.Sprintf( + "Cluster version '%s' is not supported, the minimun supported version is %s", + state.Version.Value, MinVersion, + ), + ) + return + } } proxy := cmv1.NewProxy() @@ -394,6 +414,7 @@ func (r *ClusterRosaClassicResource) Create(ctx context.Context, proxy.HTTPSProxy(state.Proxy.HttpsProxy.Value) builder.Proxy(proxy) } + object, err := builder.Build() if err != nil { response.Diagnostics.AddError( @@ -661,7 +682,6 @@ func (r *ClusterRosaClassicResource) populateState(ctx context.Context, object * sts, ok := object.AWS().GetSTS() if ok { - state.Sts = &Sts{} oidc_endpoint_url := sts.OIDCEndpointURL() if strings.HasPrefix(oidc_endpoint_url, "https://") { oidc_endpoint_url = strings.TrimPrefix(oidc_endpoint_url, "https://") @@ -686,7 +706,15 @@ func (r *ClusterRosaClassicResource) populateState(ctx context.Context, object * } } - + // TODO: fix a bug in uhc-cluster-services + if state.Sts.OperatorRolePrefix.Unknown || state.Sts.OperatorRolePrefix.Null { + operatorRolePrefix, ok := sts.GetOperatorRolePrefix() + if ok { + state.Sts.OperatorRolePrefix = types.String{ + Value: operatorRolePrefix, + } + } + } thumbprint, err := getThumbprint(sts.OIDCEndpointURL()) if err != nil { r.logger.Error(ctx, "cannot get thumbprint", err) @@ -698,21 +726,6 @@ func (r *ClusterRosaClassicResource) populateState(ctx context.Context, object * Value: thumbprint, } } - - for _, operatorRole := range sts.OperatorIAMRoles() { - r := OperatorIAMRole{ - Name: types.String{ - Value: operatorRole.Name(), - }, - Namespace: types.String{ - Value: operatorRole.Namespace(), - }, - RoleARN: types.String{ - Value: operatorRole.RoleARN(), - }, - } - state.Sts.OperatorIAMRoles = append(state.Sts.OperatorIAMRoles, r) - } } subnetIds, ok := object.AWS().GetSubnetIDs() @@ -825,3 +838,16 @@ func sha1Hash(data []byte) string { hashed := hasher.Sum(nil) return hex.EncodeToString(hashed) } + +func checkSupportedVersion(clusterVersion string) (bool, error) { + v1, err := semver.NewVersion(clusterVersion) + if err != nil { + return false, err + } + v2, err := semver.NewVersion(MinVersion) + if err != nil { + return false, err + } + //Cluster version is greater than or equal to MinVersion + return v1.GreaterThanOrEqual(v2), nil +} diff --git a/provider/cluster_rosa_classic_state.go b/provider/cluster_rosa_classic_state.go index e252ce4..67ec448 100644 --- a/provider/cluster_rosa_classic_state.go +++ b/provider/cluster_rosa_classic_state.go @@ -51,21 +51,15 @@ type ClusterRosaClassicState struct { } type Sts struct { - OIDCEndpointURL types.String `tfsdk:"oidc_endpoint_url"` - Thumbprint types.String `tfsdk:"thumbprint"` - RoleARN types.String `tfsdk:"role_arn"` - SupportRoleArn types.String `tfsdk:"support_role_arn"` - InstanceIAMRoles InstanceIAMRole `tfsdk:"instance_iam_roles"` - OperatorIAMRoles []OperatorIAMRole `tfsdk:"operator_iam_roles"` + OIDCEndpointURL types.String `tfsdk:"oidc_endpoint_url"` + Thumbprint types.String `tfsdk:"thumbprint"` + RoleARN types.String `tfsdk:"role_arn"` + SupportRoleArn types.String `tfsdk:"support_role_arn"` + InstanceIAMRoles InstanceIAMRole `tfsdk:"instance_iam_roles"` + OperatorRolePrefix types.String `tfsdk:"operator_role_prefix"` } type InstanceIAMRole struct { MasterRoleARN types.String `tfsdk:"master_role_arn"` WorkerRoleARN types.String `tfsdk:"worker_role_arn"` } - -type OperatorIAMRole struct { - Name types.String `tfsdk:"name"` - Namespace types.String `tfsdk:"namespace"` - RoleARN types.String `tfsdk:"role_arn"` -} diff --git a/provider/provider.go b/provider/provider.go index 40d3a2c..cd7e7ff 100644 --- a/provider/provider.go +++ b/provider/provider.go @@ -222,10 +222,11 @@ func (p *Provider) GetResources(ctx context.Context) (result map[string]tfsdk.Re func (p *Provider) GetDataSources(ctx context.Context) (result map[string]tfsdk.DataSourceType, diags diag.Diagnostics) { result = map[string]tfsdk.DataSourceType{ - "ocm_cloud_providers": &CloudProvidersDataSourceType{}, - "ocm_groups": &GroupsDataSourceType{}, - "ocm_machine_types": &MachineTypesDataSourceType{}, - "ocm_versions": &VersionsDataSourceType{}, + "ocm_cloud_providers": &CloudProvidersDataSourceType{}, + "ocm_rosa_operator_roles": &RosaOperatorRolesDataSourceType{}, + "ocm_groups": &GroupsDataSourceType{}, + "ocm_machine_types": &MachineTypesDataSourceType{}, + "ocm_versions": &VersionsDataSourceType{}, } return } diff --git a/provider/rosa_operator_roles_data_source.go b/provider/rosa_operator_roles_data_source.go new file mode 100644 index 0000000..e601bce --- /dev/null +++ b/provider/rosa_operator_roles_data_source.go @@ -0,0 +1,242 @@ +/* +Copyright (c) 2021 Red Hat, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package provider + +import ( + "context" + "fmt" + "github.com/hashicorp/terraform-plugin-framework/attr" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" + cmv1 "github.com/openshift-online/ocm-sdk-go/clustersmgmt/v1" + "github.com/openshift-online/ocm-sdk-go/logging" +) + +type RosaOperatorRolesDataSourceType struct { +} + +type RosaOperatorRolesDataSource struct { + logger logging.Logger + clustersClient *cmv1.ClustersClient + awsInquiries *cmv1.AWSInquiriesClient +} + +const ( + DefaultAccountRolePrefix = "ManagedOpenShift" + serviceAccountFmt = "system:serviceaccount:%s:%s" +) + +func (t *RosaOperatorRolesDataSourceType) GetSchema(ctx context.Context) (result tfsdk.Schema, + diags diag.Diagnostics) { + result = tfsdk.Schema{ + Description: "List of rosa operator role for a specific cluster.", + Attributes: map[string]tfsdk.Attribute{ + "cluster_id": { + Description: "Cluster id.", + Type: types.StringType, + Required: true, + }, + "operator_role_prefix": { + Description: "Operator role prefix.", + Type: types.StringType, + Required: true, + }, + "account_role_prefix": { + Description: "Account role prefix.", + Type: types.StringType, + Optional: true, + }, + "operator_iam_roles": { + Description: "Operator IAM Roles.", + Attributes: tfsdk.ListNestedAttributes( + t.itemAttributes(), + tfsdk.ListNestedAttributesOptions{}, + ), + Computed: true, + }, + }, + } + return +} + +func (t *RosaOperatorRolesDataSourceType) itemAttributes() map[string]tfsdk.Attribute { + return map[string]tfsdk.Attribute{ + "operator_name": { + Description: "Operator Name", + Type: types.StringType, + Computed: true, + }, + "operator_namespace": { + Description: "Kubernetes Namespace", + Type: types.StringType, + Computed: true, + }, + "role_arn": { + Description: "AWS Role ARN", + Type: types.StringType, + Computed: true, + }, + "role_name": { + Description: "policy name", + Type: types.StringType, + Computed: true, + }, + "policy_name": { + Description: "policy name", + Type: types.StringType, + Computed: true, + }, + "service_accounts": { + Description: "service accounts", + Type: types.ListType{ + ElemType: types.StringType, + }, + Computed: true, + }, + } +} + +func (t *RosaOperatorRolesDataSourceType) NewDataSource(ctx context.Context, + p tfsdk.Provider) (result tfsdk.DataSource, diags diag.Diagnostics) { + // Cast the provider interface to the specific implementation: + parent := p.(*Provider) + + // Get the collection of clusters: + clustersClient := parent.connection.ClustersMgmt().V1().Clusters() + awsInquiries := parent.connection.ClustersMgmt().V1().AWSInquiries() + + // Create the resource: + result = &RosaOperatorRolesDataSource{ + logger: parent.logger, + clustersClient: clustersClient, + awsInquiries: awsInquiries, + } + return +} + +func (t *RosaOperatorRolesDataSource) Read(ctx context.Context, request tfsdk.ReadDataSourceRequest, + response *tfsdk.ReadDataSourceResponse) { + // Get the state: + state := &RosaOperatorRolesState{} + diags := request.Config.Get(ctx, state) + response.Diagnostics.Append(diags...) + if response.Diagnostics.HasError() { + return + } + + stsOperatorRolesList, err := t.awsInquiries.STSCredentialRequests().List().Send() + if err != nil { + t.logger.Error(ctx, "Failed to get operator list") + return + } + + stsOperatorMap := make(map[string]*cmv1.STSOperator) + stsOperatorRolesList.Items().Each(func(stsCredentialRequest *cmv1.STSCredentialRequest) bool { + // TODO: check the MinVersion of the operator role + t.logger.Debug(ctx, "Operator name: %s, namespace %s, service account %s", + stsCredentialRequest.Operator().Name(), + stsCredentialRequest.Operator().Namespace(), + stsCredentialRequest.Operator().ServiceAccounts(), + ) + // The key can't be stsCredentialRequest.Operator().Name() because of constants between + // the name of `ingress_operator_cloud_credentials` and `cloud_network_config_controller_cloud_credentials` + // both of them includes the same Name `cloud-credentials` and it cannot be fixed + stsOperatorMap[stsCredentialRequest.Operator().Namespace()] = stsCredentialRequest.Operator() + return true + }) + + get, err := t.clustersClient.Cluster(state.ClusterID.Value).Get().SendContext(ctx) + if err != nil { + response.Diagnostics.AddError( + "Can't find cluster", + fmt.Sprintf( + "Can't find cluster with identifier '%s': %v", + state.ClusterID.Value, err, + ), + ) + return + } + object := get.Body() + sts, ok := object.AWS().GetSTS() + if ok { + accountRolePrefix := DefaultAccountRolePrefix + if !state.AccountRolePrefix.Unknown && !state.AccountRolePrefix.Null && state.AccountRolePrefix.Value != "" { + accountRolePrefix = state.AccountRolePrefix.Value + } + + // TODO: use the sts.OperatorRolePrefix() if not empty + // There is a bug in the return value of sts.OperatorRolePrefix() - it's always empty string + for _, operatorRole := range sts.OperatorIAMRoles() { + r := OperatorIAMRole{ + Name: types.String{ + Value: operatorRole.Name(), + }, + Namespace: types.String{ + Value: operatorRole.Namespace(), + }, + RoleARN: types.String{ + Value: operatorRole.RoleARN(), + }, + RoleName: types.String{ + Value: getRoleName(state.OperatorRolePrefix.Value, operatorRole), + }, + PolicyName: types.String{ + Value: getPolicyName(accountRolePrefix, operatorRole.Namespace(), operatorRole.Name()), + }, + ServiceAccounts: buildServiceAccountsArray(stsOperatorMap[operatorRole.Namespace()].ServiceAccounts(), operatorRole.Namespace()), + } + state.OperatorIAMRoles = append(state.OperatorIAMRoles, &r) + } + } + // Save the state: + diags = response.State.Set(ctx, state) + response.Diagnostics.Append(diags...) +} + +// TODO: should be in a separate repo +func getRoleName(rolePrefix string, operatorRole *cmv1.OperatorIAMRole) string { + role := fmt.Sprintf("%s-%s-%s", rolePrefix, operatorRole.Namespace(), operatorRole.Name()) + if len(role) > 64 { + role = role[0:64] + } + return role +} + +// TODO: should be in a separate repo +func getPolicyName(prefix string, namespace string, name string) string { + policy := fmt.Sprintf("%s-%s-%s", prefix, namespace, name) + if len(policy) > 64 { + policy = policy[0:64] + } + return policy +} + +func buildServiceAccountsArray(serviceAccountArr []string, operatorNamespace string) types.List { + serviceAccounts := types.List{ + ElemType: types.StringType, + Elems: []attr.Value{}, + } + + for _, v := range serviceAccountArr { + serviceAccount := fmt.Sprintf(serviceAccountFmt, operatorNamespace, v) + serviceAccounts.Elems = append(serviceAccounts.Elems, types.String{Value: serviceAccount}) + } + + return serviceAccounts +} diff --git a/provider/rosa_operator_roles_state.go b/provider/rosa_operator_roles_state.go new file mode 100644 index 0000000..b77e09c --- /dev/null +++ b/provider/rosa_operator_roles_state.go @@ -0,0 +1,35 @@ +/* +Copyright (c) 2021 Red Hat, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package provider + +import "github.com/hashicorp/terraform-plugin-framework/types" + +type RosaOperatorRolesState struct { + ClusterID types.String `tfsdk:"cluster_id"` + OperatorRolePrefix types.String `tfsdk:"operator_role_prefix"` + AccountRolePrefix types.String `tfsdk:"account_role_prefix"` + OperatorIAMRoles []*OperatorIAMRole `tfsdk:"operator_iam_roles"` +} + +type OperatorIAMRole struct { + Name types.String `tfsdk:"operator_name"` + Namespace types.String `tfsdk:"operator_namespace"` + RoleARN types.String `tfsdk:"role_arn"` + RoleName types.String `tfsdk:"role_name"` + PolicyName types.String `tfsdk:"policy_name"` + ServiceAccounts types.List `tfsdk:"service_accounts"` +} diff --git a/provider/sts.go b/provider/sts.go index c3b71b6..64f0be2 100644 --- a/provider/sts.go +++ b/provider/sts.go @@ -43,28 +43,11 @@ func stsResource() tfsdk.NestedAttributes { }), Required: true, }, - "operator_iam_roles": { - Description: "Operator IAM Roles", - Attributes: tfsdk.ListNestedAttributes(map[string]tfsdk.Attribute{ - "name": { - Description: "Operator Name", - Type: types.StringType, - Required: true, - }, - "namespace": { - Description: "Kubernetes Namespace", - Type: types.StringType, - Required: true, - }, - "role_arn": { - Description: "AWS Role ARN", - Type: types.StringType, - Required: true, - }, - }, tfsdk.ListNestedAttributesOptions{ - MinItems: 6, - MaxItems: 6}), - Required: true, + "operator_role_prefix": { + Description: "Operator IAM Role prefix", + Type: types.StringType, + Required: true, }, }) + } diff --git a/subsystem/cluster_resource_rosa_test.go b/subsystem/cluster_resource_rosa_test.go index e930e3a..982b90b 100644 --- a/subsystem/cluster_resource_rosa_test.go +++ b/subsystem/cluster_resource_rosa_test.go @@ -30,13 +30,7 @@ var _ = Describe("Cluster creation", func() { // a cluster. const template = `{ "id": "123", - "product": { - "id": "rosa" - }, "name": "my-cluster", - "cloud_provider": { - "id": "aws" - }, "region": { "id": "us-west-1" }, @@ -53,9 +47,6 @@ var _ = Describe("Cluster creation", func() { "id": "r5.xlarge" } }, - "ccs": { - "enabled": false - }, "network": { "machine_cidr": "10.0.0.0/16", "service_cidr": "172.30.0.0/16", @@ -223,12 +214,7 @@ var _ = Describe("Cluster creation", func() { VerifyJQ(`.aws.sts.support_role_arn`, "arn:aws:iam::account-id:role/ManagedOpenShift-Support-Role"), VerifyJQ(`.aws.sts.instance_iam_roles.master_role_arn`, "arn:aws:iam::account-id:role/ManagedOpenShift-ControlPlane-Role"), VerifyJQ(`.aws.sts.instance_iam_roles.worker_role_arn`, "arn:aws:iam::account-id:role/ManagedOpenShift-Worker-Role"), - VerifyJQ(`.aws.sts.operator_iam_roles.[0].role_arn`, "arn:aws:iam::account-id:role/cloud-credential"), - VerifyJQ(`.aws.sts.operator_iam_roles.[1].role_arn`, "arn:aws:iam::account-id:role/image-registry"), - VerifyJQ(`.aws.sts.operator_iam_roles.[2].role_arn`, "arn:aws:iam::account-id:role/ingress"), - VerifyJQ(`.aws.sts.operator_iam_roles.[3].role_arn`, "arn:aws:iam::account-id:role/ebs"), - VerifyJQ(`.aws.sts.operator_iam_roles.[4].role_arn`, "arn:aws:iam::account-id:role/cloud-network-config"), - VerifyJQ(`.aws.sts.operator_iam_roles.[5].role_arn`, "arn:aws:iam::account-id:role/machine-api"), + VerifyJQ(`.aws.sts.operator_role_prefix`, "terraform-operator"), RespondWithPatchedJSON(http.StatusOK, template, `[ { "op": "add", @@ -243,38 +229,7 @@ var _ = Describe("Cluster creation", func() { "master_role_arn" : "arn:aws:iam::account-id:role/ManagedOpenShift-ControlPlane-Role", "worker_role_arn" : "arn:aws:iam::account-id:role/ManagedOpenShift-Worker-Role" }, - "operator_iam_roles" : [ - { - "name": "cloud-credential-operator-iam-ro-creds", - "namespace": "openshift-cloud-credential-operator", - "role_arn": "arn:aws:iam::account-id:role/cloud-credential" - }, - { - "name": "installer-cloud-credentials", - "namespace": "openshift-image-registry", - "role_arn": "arn:aws:iam::account-id:role/image-registry" - }, - { - "name": "cloud-credentials", - "namespace": "openshift-ingress-operator", - "role_arn": "arn:aws:iam::account-id:role/ingress" - }, - { - "name": "ebs-cloud-credentials", - "namespace": "openshift-cluster-csi-drivers", - "role_arn": "arn:aws:iam::account-id:role/ebs" - }, - { - "name": "cloud-credentials", - "namespace": "openshift-cloud-network-config-controller", - "role_arn": "arn:aws:iam::account-id:role/cloud-network-config" - }, - { - "name": "aws-cloud-credentials", - "namespace": "openshift-machine-api", - "role_arn": "arn:aws:iam::account-id:role/machine-api" - } - ] + "operator_role_prefix" : "terraform-operator" } } } @@ -291,42 +246,11 @@ var _ = Describe("Cluster creation", func() { sts = { role_arn = "arn:aws:iam::account-id:role/ManagedOpenShift-Installer-Role", support_role_arn = "arn:aws:iam::account-id:role/ManagedOpenShift-Support-Role", - operator_iam_roles = [ - { - name = "cloud-credential-operator-iam-ro-creds", - namespace = "openshift-cloud-credential-operator", - role_arn = "arn:aws:iam::account-id:role/cloud-credential", - }, - { - name = "installer-cloud-credentials", - namespace = "openshift-image-registry", - role_arn = "arn:aws:iam::account-id:role/image-registry", - }, - { - name = "cloud-credentials", - namespace = "openshift-ingress-operator", - role_arn = "arn:aws:iam::account-id:role/ingress", - }, - { - name = "ebs-cloud-credentials", - namespace = "openshift-cluster-csi-drivers", - role_arn = "arn:aws:iam::account-id:role/ebs", - }, - { - name = "cloud-credentials", - namespace = "openshift-cloud-network-config-controller", - role_arn = "arn:aws:iam::account-id:role/cloud-network-config", - }, - { - name = "aws-cloud-credentials", - namespace = "openshift-machine-api", - role_arn = "arn:aws:iam::account-id:role/machine-api", - }, - ] instance_iam_roles = { master_role_arn = "arn:aws:iam::account-id:role/ManagedOpenShift-ControlPlane-Role", worker_role_arn = "arn:aws:iam::account-id:role/ManagedOpenShift-Worker-Role" }, + "operator_role_prefix" : "terraform-operator" } } `) diff --git a/subsystem/rosa_operator_roles_data_source_test.go b/subsystem/rosa_operator_roles_data_source_test.go new file mode 100644 index 0000000..38900db --- /dev/null +++ b/subsystem/rosa_operator_roles_data_source_test.go @@ -0,0 +1,302 @@ +package provider + +import ( + "fmt" + "net/http" + + . "github.com/onsi/ginkgo/v2/dsl/core" // nolint + . "github.com/onsi/gomega" // nolint + . "github.com/onsi/gomega/ghttp" // nolint + . "github.com/openshift-online/ocm-sdk-go/testing" // nolint +) + +const ( + // This is the cluster that will be returned by the server when asked to retrieve a cluster with ID 123 + getClusterJson = `{ + "kind": "", + "id": "123", + "name": "my-cluster", + "region": { + "id": "us-west-1" + }, + "multi_az": true, + "api": { + "url": "https://my-api.example.com" + }, + "console": { + "url": "https://my-console.example.com" + }, + "nodes": { + "compute": 3, + "compute_machine_type": { + "id": "r5.xlarge" + } + }, + "network": { + "machine_cidr": "10.0.0.0/16", + "service_cidr": "172.30.0.0/16", + "pod_cidr": "10.128.0.0/14", + "host_prefix": 23 + }, + "version": { + "id": "openshift-4.8.0" + }, + "state": "ready", + "aws": { + "private_link": false, + "sts": { + "enabled": true, + "role_arn": "arn:aws:iam::account-id:role/TerraformAccount-Installer-Role", + "support_role_arn": "arn:aws:iam::account-id:role/TerraformAccount-Support-Role", + "oidc_endpoint_url": "https://oidc_endpoint_url", + "thumbprint": "111111", + "instance_iam_roles": { + "master_role_arn": "arn:aws:iam::account-id:role/TerraformAccount-ControlPlane-Role", + "worker_role_arn": "arn:aws:iam::account-id:role/TerraformAccount-Worker-Role" + }, + "operator_role_prefix": "terraform-operator", + "operator_iam_roles": [ + { + "id": "", + "name": "ebs-cloud-credentials", + "namespace": "openshift-cluster-csi-drivers", + "role_arn": "arn:aws:iam::765374464689:role/terrafom-operator-openshift-cluster-csi-drivers-ebs-cloud-creden", + "service_account": "" + }, + { + "id": "", + "name": "cloud-credentials", + "namespace": "openshift-cloud-network-config-controller", + "role_arn": "arn:aws:iam::765374464689:role/terrafom-operator-openshift-cloud-network-config-controller-clou", + "service_account": "" + } + ] + } + } +}` + getClusterWithDefaultAccountPrefixJson = `{ + "kind": "", + "id": "123", + "name": "my-cluster", + "region": { + "id": "us-west-1" + }, + "multi_az": true, + "api": { + "url": "https://my-api.example.com" + }, + "console": { + "url": "https://my-console.example.com" + }, + "nodes": { + "compute": 3, + "compute_machine_type": { + "id": "r5.xlarge" + } + }, + "network": { + "machine_cidr": "10.0.0.0/16", + "service_cidr": "172.30.0.0/16", + "pod_cidr": "10.128.0.0/14", + "host_prefix": 23 + }, + "version": { + "id": "openshift-4.8.0" + }, + "state": "ready", + "aws": { + "private_link": false, + "sts": { + "enabled": true, + "role_arn": "arn:aws:iam::account-id:role/ManagedOpenShift-Installer-Role", + "support_role_arn": "arn:aws:iam::account-id:role/ManagedOpenShift-Support-Role", + "oidc_endpoint_url": "https://oidc_endpoint_url", + "thumbprint": "111111", + "instance_iam_roles": { + "master_role_arn": "arn:aws:iam::account-id:role/ManagedOpenShift-ControlPlane-Role", + "worker_role_arn": "arn:aws:iam::account-id:role/ManagedOpenShift-Worker-Role" + }, + "operator_role_prefix": "terraform-operator", + "operator_iam_roles": [ + { + "id": "", + "name": "ebs-cloud-credentials", + "namespace": "openshift-cluster-csi-drivers", + "role_arn": "arn:aws:iam::765374464689:role/terrafom-operator-openshift-cluster-csi-drivers-ebs-cloud-creden", + "service_account": "" + }, + { + "id": "", + "name": "cloud-credentials", + "namespace": "openshift-cloud-network-config-controller", + "role_arn": "arn:aws:iam::765374464689:role/terrafom-operator-openshift-cloud-network-config-controller-clou", + "service_account": "" + } + ] + } + } +}` + getStsCredentialRequests = `{ + "page": 1, + "size": 6, + "total": 6, + "items": [ + { + "kind": "STSOperator", + "name": "cluster_csi_drivers_ebs_cloud_credentials", + "operator": { + "name": "ebs-cloud-credentials", + "namespace": "openshift-cluster-csi-drivers", + "service_accounts": [ + "aws-ebs-csi-driver-operator", + "aws-ebs-csi-driver-controller-sa" + ], + "min_version": "", + "max_version": "" + } + }, + { + "kind": "STSOperator", + "name": "cloud_network_config_controller_cloud_credentials", + "operator": { + "name": "cloud-credentials", + "namespace": "openshift-cloud-network-config-controller", + "service_accounts": [ + "cloud-network-config-controller" + ], + "min_version": "4.10", + "max_version": "" + } + } + ] +}` +) + +var _ = Describe("ROSA Operator IAM roles data source", func() { + + It("Can list Operator IAM roles", func() { + // Prepare the server: + server.AppendHandlers( + CombineHandlers( + VerifyRequest(http.MethodGet, "/api/clusters_mgmt/v1/aws_inquiries/sts_credential_requests"), + RespondWithJSON(http.StatusOK, getStsCredentialRequests), + ), + CombineHandlers( + VerifyRequest(http.MethodGet, "/api/clusters_mgmt/v1/clusters/123"), + RespondWithJSON(http.StatusOK, getClusterJson), + ), + ) + + // Run the apply command: + terraform.Source(` + data "ocm_rosa_operator_roles" "operator_roles" { + cluster_id = "123" + operator_role_prefix = "terraform-operator" + account_role_prefix = "TerraformAccountPrefix" + } + `) + Expect(terraform.Apply()).To(BeZero()) + + // Check the state: + resource := terraform.Resource("ocm_rosa_operator_roles", "operator_roles") + //Expect(resource).To(MatchJQ(`.attributes.items | length`, 1)) + Expect(resource).To(MatchJQ(`.attributes.operator_role_prefix`, "terraform-operator")) + Expect(resource).To(MatchJQ(`.attributes.account_role_prefix`, "TerraformAccountPrefix")) + Expect(resource).To(MatchJQ(`.attributes.cluster_id`, "123")) + Expect(resource).To(MatchJQ(`.attributes.operator_iam_roles | length`, 2)) + compareResultOfRoles(resource, 0, + "ebs-cloud-credentials", + "openshift-cluster-csi-drivers", + "TerraformAccountPrefix-openshift-cluster-csi-drivers-ebs-cloud-c", + "arn:aws:iam::765374464689:role/terrafom-operator-openshift-cluster-csi-drivers-ebs-cloud-creden", + "terraform-operator-openshift-cluster-csi-drivers-ebs-cloud-crede", + 2, + []string{ + "system:serviceaccount:openshift-cluster-csi-drivers:aws-ebs-csi-driver-operator", + "system:serviceaccount:openshift-cluster-csi-drivers:aws-ebs-csi-driver-controller-sa", + }, + ) + + compareResultOfRoles(resource, 1, + "cloud-credentials", + "openshift-cloud-network-config-controller", + "TerraformAccountPrefix-openshift-cloud-network-config-controller", + "arn:aws:iam::765374464689:role/terrafom-operator-openshift-cloud-network-config-controller-clou", + "terraform-operator-openshift-cloud-network-config-controller-clo", + 1, + []string{"system:serviceaccount:openshift-cloud-network-config-controller:cloud-network-config-controller"}, + ) + }) + + It("Can list Operator IAM roles without account role prefix", func() { + // Prepare the server: + server.AppendHandlers( + CombineHandlers( + VerifyRequest(http.MethodGet, "/api/clusters_mgmt/v1/aws_inquiries/sts_credential_requests"), + RespondWithJSON(http.StatusOK, getStsCredentialRequests), + ), + CombineHandlers( + VerifyRequest(http.MethodGet, "/api/clusters_mgmt/v1/clusters/123"), + RespondWithJSON(http.StatusOK, getClusterWithDefaultAccountPrefixJson), + ), + ) + + // Run the apply command: + terraform.Source(` + data "ocm_rosa_operator_roles" "operator_roles" { + cluster_id = "123" + operator_role_prefix = "terraform-operator" + } + `) + Expect(terraform.Apply()).To(BeZero()) + + // Check the state: + resource := terraform.Resource("ocm_rosa_operator_roles", "operator_roles") + //Expect(resource).To(MatchJQ(`.attributes.items | length`, 1)) + Expect(resource).To(MatchJQ(`.attributes.operator_role_prefix`, "terraform-operator")) + Expect(resource).To(MatchJQ(`.attributes.cluster_id`, "123")) + Expect(resource).To(MatchJQ(`.attributes.operator_iam_roles | length`, 2)) + compareResultOfRoles(resource, 0, + "ebs-cloud-credentials", + "openshift-cluster-csi-drivers", + "ManagedOpenShift-openshift-cluster-csi-drivers-ebs-cloud-credent", + "arn:aws:iam::765374464689:role/terrafom-operator-openshift-cluster-csi-drivers-ebs-cloud-creden", + "terraform-operator-openshift-cluster-csi-drivers-ebs-cloud-crede", + 2, + []string{ + "system:serviceaccount:openshift-cluster-csi-drivers:aws-ebs-csi-driver-operator", + "system:serviceaccount:openshift-cluster-csi-drivers:aws-ebs-csi-driver-controller-sa", + }, + ) + + compareResultOfRoles(resource, 1, + "cloud-credentials", + "openshift-cloud-network-config-controller", + "ManagedOpenShift-openshift-cloud-network-config-controller-cloud", + "arn:aws:iam::765374464689:role/terrafom-operator-openshift-cloud-network-config-controller-clou", + "terraform-operator-openshift-cloud-network-config-controller-clo", + 1, + []string{"system:serviceaccount:openshift-cloud-network-config-controller:cloud-network-config-controller"}, + ) + }) +}) + +func compareResultOfRoles(resource interface{}, index int, name, namespace, policyName, roleArn, roleName string, serviceAccountLen int, serviceAccounts []string) { + operatorNameFmt := ".attributes.operator_iam_roles[%v].operator_name" + operatorNamespaceFmt := ".attributes.operator_iam_roles[%v].operator_namespace" + policyNameFmt := ".attributes.operator_iam_roles[%v].policy_name" + roleArnFmt := ".attributes.operator_iam_roles[%v].role_arn" + roleNameFmt := ".attributes.operator_iam_roles[%v].role_name" + serviceAccountLenFmt := ".attributes.operator_iam_roles[%v].service_accounts\t|\tlength " + serviceAccountFmt := ".attributes.operator_iam_roles[%v].service_accounts[%v]" + + Expect(resource).To(MatchJQ(fmt.Sprintf(operatorNameFmt, index), name)) + Expect(resource).To(MatchJQ(fmt.Sprintf(operatorNamespaceFmt, index), namespace)) + Expect(resource).To(MatchJQ(fmt.Sprintf(policyNameFmt, index), policyName)) + Expect(resource).To(MatchJQ(fmt.Sprintf(roleArnFmt, index), roleArn)) + Expect(resource).To(MatchJQ(fmt.Sprintf(roleNameFmt, index), roleName)) + Expect(resource).To(MatchJQ(fmt.Sprintf(serviceAccountLenFmt, index), serviceAccountLen)) + for k, v := range serviceAccounts { + Expect(resource).To(MatchJQ(fmt.Sprintf(serviceAccountFmt, index, k), v)) + } +}