From 09e9ad0f0ee1b5c5ecefc0f18895a5159ef9d75a Mon Sep 17 00:00:00 2001 From: Ludovico Magnocavallo Date: Fri, 21 Oct 2022 01:45:39 +0200 Subject: [PATCH 1/8] github extra stage --- fast/extras/00-cicd-github/cicd-versions.tf | 11 ++ fast/extras/00-cicd-github/main.tf | 112 ++++++++++++++++++ .../00-cicd-github/providers.tf} | 3 + fast/extras/00-cicd-github/variables.tf | 60 ++++++++++ fast/stages/{00-cicd => 00-cicd_}/README.md | 0 fast/stages/00-cicd_/cicd-providers.tf | 24 ++++ fast/stages/{00-cicd => 00-cicd_}/github.tf | 23 ++-- fast/stages/{00-cicd => 00-cicd_}/gitlab.tf | 4 - .../{00-cicd/cicd.tf => 00-cicd_/main.tf} | 14 ++- .../{00-cicd => 00-cicd_}/outputs-files.tf | 0 .../{00-cicd => 00-cicd_}/outputs-gcs.tf | 0 fast/stages/{00-cicd => 00-cicd_}/outputs.tf | 0 .../stages/{00-cicd => 00-cicd_}/variables.tf | 0 fast/stages/{00-cicd => 00-cicd_}/versions.tf | 0 14 files changed, 230 insertions(+), 21 deletions(-) create mode 100644 fast/extras/00-cicd-github/cicd-versions.tf create mode 100644 fast/extras/00-cicd-github/main.tf rename fast/{stages/00-cicd/main.tf => extras/00-cicd-github/providers.tf} (92%) create mode 100644 fast/extras/00-cicd-github/variables.tf rename fast/stages/{00-cicd => 00-cicd_}/README.md (100%) create mode 100644 fast/stages/00-cicd_/cicd-providers.tf rename fast/stages/{00-cicd => 00-cicd_}/github.tf (74%) rename fast/stages/{00-cicd => 00-cicd_}/gitlab.tf (97%) rename fast/stages/{00-cicd/cicd.tf => 00-cicd_/main.tf} (70%) rename fast/stages/{00-cicd => 00-cicd_}/outputs-files.tf (100%) rename fast/stages/{00-cicd => 00-cicd_}/outputs-gcs.tf (100%) rename fast/stages/{00-cicd => 00-cicd_}/outputs.tf (100%) rename fast/stages/{00-cicd => 00-cicd_}/variables.tf (100%) rename fast/stages/{00-cicd => 00-cicd_}/versions.tf (100%) diff --git a/fast/extras/00-cicd-github/cicd-versions.tf b/fast/extras/00-cicd-github/cicd-versions.tf new file mode 100644 index 0000000000..23651dced6 --- /dev/null +++ b/fast/extras/00-cicd-github/cicd-versions.tf @@ -0,0 +1,11 @@ +terraform { + required_version = ">= 1.3.1" + required_providers { + github = { + source = "integrations/github" + version = "~> 4.0" + } + } +} + + diff --git a/fast/extras/00-cicd-github/main.tf b/fast/extras/00-cicd-github/main.tf new file mode 100644 index 0000000000..afa8d13e29 --- /dev/null +++ b/fast/extras/00-cicd-github/main.tf @@ -0,0 +1,112 @@ +/** + * Copyright 2022 Google LLC + * + * 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. + */ + +locals { + _modules_repository = [ + for k, v in var.repositories : k if v.modules_source + ] + _populate_md = flatten([ + for k, v in var.repositories : [ + for f in fileset(path.module, "${v.populate_with}/*.md") : { + repository = k + file = f + name = replace(f, "${v.populate_with}/", "") + } + ] if v.populate_with != null + ]) + _populate_tf = flatten([ + for k, v in var.repositories : [ + for f in fileset(path.module, "${v.populate_with}/*.tf") : { + repository = k + file = f + name = replace(f, "${v.populate_with}/", "") + } + ] if v.populate_with != null + ]) + modules_repository = ( + length(local._modules_repository) > 0 ? local._modules_repository.0 : null + ) + repository_files = { + for k in concat( + local._populate_md, + [ + for f in local._populate_tf : + f if !startswith(f.name, "0") && f.name != "globals.tf" + ] + ) : "${k.repository}/${k.name}" => k + } +} + +resource "github_repository" "default" { + for_each = var.repositories + name = each.key + description = ( + each.value.description != null + ? each.value.description + : "FAST stage ${each.key}." + ) + visibility = each.value.visibility + auto_init = each.value.auto_init + allow_auto_merge = try(each.value.allow.auto_merge, null) + allow_merge_commit = try(each.value.allow.merge_commit, null) + allow_rebase_merge = try(each.value.allow.rebase_merge, null) + allow_squash_merge = try(each.value.allow.squash_merge, null) + has_issues = try(each.value.features.issues, null) + has_projects = try(each.value.features.projects, null) + has_wiki = try(each.value.features.wiki, null) + gitignore_template = try(each.value.templates.gitignore, null) + license_template = try(each.value.templates.license, null) + + dynamic "template" { + for_each = try(each.value.templates.repository, null) != null ? [""] : [] + content { + owner = each.value.templates.repository.owner + repository = each.value.templates.repository.name + } + } +} + +resource "tls_private_key" "default" { + count = local.modules_repository != null ? 1 : 0 + algorithm = "ED25519" +} + +resource "github_actions_secret" "default" { + count = local.modules_repository != null ? 1 : 0 + repository = github_repository.default[local.modules_repository].name + secret_name = "CICD_MODULES_KEY" + plaintext_value = tls_private_key.default.0.private_key_openssh +} + +resource "github_repository_file" "default" { + for_each = local.repository_files + repository = github_repository.default[each.value.repository].name + branch = "main" + file = each.value.name + content = ( + endswith(each.value.name, ".tf") && local.modules_repository != null + ? replace( + file(each.value.file), + "/source\\s*=\\s*\"../../../", + "source = \"git@github.com:${var.organization}/${local.modules_repository}.git/" + ) + : file(each.value.file) + ) + commit_message = "Managed by Terraform" + commit_author = "Terraform User" + commit_email = "terraform@example.com" + overwrite_on_create = true +} diff --git a/fast/stages/00-cicd/main.tf b/fast/extras/00-cicd-github/providers.tf similarity index 92% rename from fast/stages/00-cicd/main.tf rename to fast/extras/00-cicd-github/providers.tf index 3ea74b550c..63954de899 100644 --- a/fast/stages/00-cicd/main.tf +++ b/fast/extras/00-cicd-github/providers.tf @@ -14,3 +14,6 @@ * limitations under the License. */ +provider "github" { + owner = var.organization +} diff --git a/fast/extras/00-cicd-github/variables.tf b/fast/extras/00-cicd-github/variables.tf new file mode 100644 index 0000000000..85d8aa7394 --- /dev/null +++ b/fast/extras/00-cicd-github/variables.tf @@ -0,0 +1,60 @@ +/** + * Copyright 2022 Google LLC + * + * 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. + */ + + +variable "organization" { + description = "GitHub organization." + type = string +} + +variable "repositories" { + description = "Repositories to create." + type = map(object({ + allow = optional(object({ + auto_merge = optional(bool) + merge_commit = optional(bool) + rebase_merge = optional(bool) + squash_merge = optional(bool) + })) + auto_init = optional(bool) + description = optional(string) + features = optional(object({ + issues = optional(bool) + projects = optional(bool) + wiki = optional(bool) + })) + modules_source = optional(bool, false) + templates = optional(object({ + gitignore = optional(string, "Terraform") + license = optional(string) + repository = optional(object({ + name = string + owner = string + })) + }), {}) + populate_with = optional(string) + visibility = optional(string, "private") + })) + default = {} + nullable = true + validation { + condition = alltrue([ + for k, v in var.repositories : + try(regex("^[a-zA-Z0-9_.]+$", k), null) != null + ]) + error_message = "Repository names must match '^[a-zA-Z0-9_.]+$'." + } +} diff --git a/fast/stages/00-cicd/README.md b/fast/stages/00-cicd_/README.md similarity index 100% rename from fast/stages/00-cicd/README.md rename to fast/stages/00-cicd_/README.md diff --git a/fast/stages/00-cicd_/cicd-providers.tf b/fast/stages/00-cicd_/cicd-providers.tf new file mode 100644 index 0000000000..5ea51121b1 --- /dev/null +++ b/fast/stages/00-cicd_/cicd-providers.tf @@ -0,0 +1,24 @@ +/** + * Copyright 2022 Google LLC + * + * 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. + */ + +provider "github" { + base_url = var.github.url + owner = local.github_groups[0] +} + +provider "gitlab" { + base_url = var.gitlab.url +} diff --git a/fast/stages/00-cicd/github.tf b/fast/stages/00-cicd_/github.tf similarity index 74% rename from fast/stages/00-cicd/github.tf rename to fast/stages/00-cicd_/github.tf index 88e73618ef..b4c7a57095 100644 --- a/fast/stages/00-cicd/github.tf +++ b/fast/stages/00-cicd_/github.tf @@ -15,12 +15,9 @@ */ locals { - github_groups = distinct([for k, v in local.cicd_repositories_by_system["github"] : v.group]) -} - -provider "github" { - base_url = var.github.url - owner = local.github_groups[0] + github_groups = distinct([ + for k, v in local.cicd_repositories_by_system["github"] : v.group + ]) } data "github_organization" "organization" { @@ -29,17 +26,21 @@ data "github_organization" "organization" { } data "github_repository" "repositories" { - for_each = { for name, repo in local.cicd_repositories_by_system["github"] : name => repo if !try(repo.create, true) } + for_each = { + for name, repo in local.cicd_repositories_by_system["github"] : + name => repo if !try(repo.create, true) + } full_name = format("%s/%s", each.value.group, each.value.name) } resource "github_repository" "repositories" { - for_each = { for name, repo in local.cicd_repositories_by_system["github"] : name => repo if try(repo.create, true) } - + for_each = { + for name, repo in local.cicd_repositories_by_system["github"] : + name => repo if try(repo.create, true) + } name = each.value.name description = each.value.description - - visibility = var.github.visibility + visibility = var.github.visibility } resource "github_actions_secret" "actions-modules-key" { diff --git a/fast/stages/00-cicd/gitlab.tf b/fast/stages/00-cicd_/gitlab.tf similarity index 97% rename from fast/stages/00-cicd/gitlab.tf rename to fast/stages/00-cicd_/gitlab.tf index 47f1515e67..bb6e28b1a1 100644 --- a/fast/stages/00-cicd/gitlab.tf +++ b/fast/stages/00-cicd_/gitlab.tf @@ -19,10 +19,6 @@ locals { gitlab_existing_groups = distinct([for k, v in local.cicd_repositories_by_system["gitlab"] : v.group if !try(v.create_group, false)]) } -provider "gitlab" { - base_url = var.gitlab.url -} - data "gitlab_group" "group" { for_each = toset(local.gitlab_existing_groups) full_path = each.value diff --git a/fast/stages/00-cicd/cicd.tf b/fast/stages/00-cicd_/main.tf similarity index 70% rename from fast/stages/00-cicd/cicd.tf rename to fast/stages/00-cicd_/main.tf index 948ab60edc..a9627f0e53 100644 --- a/fast/stages/00-cicd/cicd.tf +++ b/fast/stages/00-cicd_/main.tf @@ -17,18 +17,20 @@ locals { supported_cicd_systems = ["gitlab", "github", "sourcerepo"] cicd_repositories = { - for k, v in coalesce(var.cicd_repositories, {}) : k => merge(v, - { group = join("/", slice(split("/", v.name), 0, length(split("/", v.name)) - 1)) }, - { name = element(split("/", v.name), length(split("/", v.name)) - 1) }, - { create_group = try(v.create_group, true) }) + for k, v in coalesce(var.cicd_repositories, {}) : k => merge(v, { + create_group = try(v.create_group, true) + group = join("/", slice(split("/", v.name), 0, length(split("/", v.name)) - 1)) + name = element(split("/", v.name), length(split("/", v.name)) - 1) + }) if( v != null && contains(local.supported_cicd_systems, try(v.type, "")) ) } - cicd_repositories_by_system = { for system in local.supported_cicd_systems : system => { - for k, v in local.cicd_repositories : k => v if v.type == system + cicd_repositories_by_system = { + for system in local.supported_cicd_systems : system => { + for k, v in local.cicd_repositories : k => v if v.type == system } } } diff --git a/fast/stages/00-cicd/outputs-files.tf b/fast/stages/00-cicd_/outputs-files.tf similarity index 100% rename from fast/stages/00-cicd/outputs-files.tf rename to fast/stages/00-cicd_/outputs-files.tf diff --git a/fast/stages/00-cicd/outputs-gcs.tf b/fast/stages/00-cicd_/outputs-gcs.tf similarity index 100% rename from fast/stages/00-cicd/outputs-gcs.tf rename to fast/stages/00-cicd_/outputs-gcs.tf diff --git a/fast/stages/00-cicd/outputs.tf b/fast/stages/00-cicd_/outputs.tf similarity index 100% rename from fast/stages/00-cicd/outputs.tf rename to fast/stages/00-cicd_/outputs.tf diff --git a/fast/stages/00-cicd/variables.tf b/fast/stages/00-cicd_/variables.tf similarity index 100% rename from fast/stages/00-cicd/variables.tf rename to fast/stages/00-cicd_/variables.tf diff --git a/fast/stages/00-cicd/versions.tf b/fast/stages/00-cicd_/versions.tf similarity index 100% rename from fast/stages/00-cicd/versions.tf rename to fast/stages/00-cicd_/versions.tf From d2d15e7409b74b0b26d089e14f1e971dd6432c8f Mon Sep 17 00:00:00 2001 From: Ludovico Magnocavallo Date: Fri, 21 Oct 2022 01:46:31 +0200 Subject: [PATCH 2/8] remove original cicd stage --- fast/stages/00-cicd_/README.md | 206 ------------------------- fast/stages/00-cicd_/cicd-providers.tf | 24 --- fast/stages/00-cicd_/github.tf | 52 ------- fast/stages/00-cicd_/gitlab.tf | 66 -------- fast/stages/00-cicd_/main.tf | 40 ----- fast/stages/00-cicd_/outputs-files.tf | 24 --- fast/stages/00-cicd_/outputs-gcs.tf | 23 --- fast/stages/00-cicd_/outputs.tf | 34 ---- fast/stages/00-cicd_/variables.tf | 145 ----------------- fast/stages/00-cicd_/versions.tf | 38 ----- 10 files changed, 652 deletions(-) delete mode 100644 fast/stages/00-cicd_/README.md delete mode 100644 fast/stages/00-cicd_/cicd-providers.tf delete mode 100644 fast/stages/00-cicd_/github.tf delete mode 100644 fast/stages/00-cicd_/gitlab.tf delete mode 100644 fast/stages/00-cicd_/main.tf delete mode 100644 fast/stages/00-cicd_/outputs-files.tf delete mode 100644 fast/stages/00-cicd_/outputs-gcs.tf delete mode 100644 fast/stages/00-cicd_/outputs.tf delete mode 100644 fast/stages/00-cicd_/variables.tf delete mode 100644 fast/stages/00-cicd_/versions.tf diff --git a/fast/stages/00-cicd_/README.md b/fast/stages/00-cicd_/README.md deleted file mode 100644 index a0b9273352..0000000000 --- a/fast/stages/00-cicd_/README.md +++ /dev/null @@ -1,206 +0,0 @@ -# CI/CD bootstrap - -The primary purpose of this stage is to set up your CI/CD project structure automatically, with most of the necessary configuration to run the pipelines out of the box. - -## How to run this stage - -This stage is meant to be executed after the [bootstrap](../00-bootstrap) stage has run, as it leverages the automation service account and bucket created there. -The entire stage is optional, you may also choose to create your repositories manually. - -### Providers configuration - -The default way of making sure you have the right permissions, is to use the identity of the service account pre-created for this stage during bootstrap, and that you are a member of the group that can impersonate it via provider-level configuration (`gcp-devops` or `organization-admins`). - -To simplify setup, the previous stage pre-configures a valid providers file in its output, and optionally writes it to a local file if the `outputs_location` variable is set to a valid path. - -If you have set a valid value for `outputs_location` in the bootstrap stage (see the [bootstrap stage README](../00-bootstrap/#output-files-and-cross-stage-variables) for more details), simply link the relevant `providers.tf` file from this stage's folder in the path you specified: - -```bash -# `outputs_location` is set to `~/fast-config` -ln -s ~/fast-config/providers/00-cicd-providers.tf . -``` - -If you have not configured `outputs_location` in bootstrap, you can derive the providers file from that stage's outputs: - -```bash -cd ../00-bootstrap -terraform output -json providers | jq -r '.["00-cicd"]' \ - > ../00-cicd/providers.tf -``` - -If you want to continue to rely on `outputs_location` logic, create a `terraform.tfvars` file and configure it as described [here](../00-bootstrap/#output-files-and-cross-stage-variables). - -### Variable configuration - -There are two broad sets of variables you will need to fill in: - -- variables shared by other stages (org id, billing account id, etc.), or derived from a resource managed by a different stage (folder id, automation project id, etc.) -- variables specific to resources managed by this stage - -To avoid the tedious job of filling in the first group of variable with values derived from other stages' outputs, the same mechanism used above for the provider configuration can be used to leverage pre-configured `.tfvars` files. - -If you configured a valid path for `outputs_location` in the bootstrap stage, simply link the relevant `terraform-*.auto.tfvars.json` files from the outputs folder. For this stage, you need the `.tfvars` file compiled manually for the bootstrap stage, and the one generated by it: - -```bash -# `outputs_location` is set to `~/fast-config` -ln -s ~/fast-config/tfvars/00-bootstrap.auto.tfvars.json . -# also copy the tfvars file used for the bootstrap stage -cp ../00-bootstrap/terraform.tfvars . -``` - -A second set of variables is specific to this stage, they are all optional so if you need to customize them, create an extra `terraform.tfvars` file or add them to the file copied from bootstrap. - -Refer to the [Variables](#variables) table at the bottom of this document, for a full list of variables, their origin (e.g. a stage or specific to this one), and descriptions explaining their meaning. The sections below also describe some of the possible customizations. - -### CI/CD systems - -#### Gitlab - -To configure Gitlab, add the following variable: - -```hcl -gitlab = { - url = "https://gitlab.com" # Or self-hosted URL - project_visibility = "private" - shared_runners_enabled = true -} -``` - -Also set `GITLAB_TOKEN` to a token that has appropriate permissions. - -#### GitHub - -To configure GitHub, add the following variable: - -```hcl -github = { - url = null # Or GitHub Enterprise base URL - visibility = "private" -} -``` - -Also set `GITHUB_TOKEN` to a token that has appropriate permissions. - -### CI/CD repositories - -While the other stages create the necessary supporting structure for their CI/CD pipelines, like service accounts -and such, the `00-cicd` stage creates all the repositories in your CI/CD system through automation. Its -configuration is essentially a combination of all the `cicd_repositories` variables of the other stages -plus additional CI/CD system specific configuration information. - -This is an example of configuring the repositories in this stage. - -```hcl -cicd_repositories = { - bootstrap = { - branch = null - identity_provider = "github-sample" - name = "my-gh-org/fast-bootstrap" - description = "Google Cloud bootstrapping" - type = "github" - create = true - create_group = true - } - cicd = { - branch = null - identity_provider = "github-sample" - name = "my-gh-org/fast-cicd" - description = "Fabric FAST CI/CD setup" - type = "github" - create = true - create_group = true - } - resman = { - branch = "main" - identity_provider = "github-sample" - name = "my-gh-org/fast-resman" - description = "Google Cloud resource management" - type = "github" - create = true - create_group = true - } - networking = { - branch = "main" - identity_provider = "github-sample" - name = "my-gh-org/fast-networking" - description = "Google Cloud networking setup" - type = "github" - create = true - create_group = true - } - security = { - branch = "main" - identity_provider = "github-sample" - description = "Google Cloud security settings" - name = "my-gh-org/fast-security" - type = "github" - create = true - create_group = true - } - data-platform = { - branch = "main" - identity_provider = "github-sample" - name = "my-gh-org/fast-data-platform" - description = "Google Cloud data platform" - type = "github" - create = true - create_group = true - } - project-factory = { - branch = "main" - identity_provider = "github-sample" - name = "my-gh-org/fast-project-factory" - description = "Google Cloud project factory" - type = "github" - create = true - create_group = true - } -} -``` - -The `type` attribute can be set to one of the supported repository types: `github` or `gitlab`. - -Once the stage is applied the generated output files will contain pre-configured workflow files for each repository, that will use Workload Identity Federation via a dedicated service account for each repository to impersonate the automation service account for the stage. - -Once done, you can run this stage: - -```bash -terraform init -terraform apply -``` - - - - -## Files - -| name | description | resources | -|---|---|---| -| [cicd.tf](./cicd.tf) | None | tls_private_key | -| [github.tf](./github.tf) | None | github_actions_secret · github_repository | -| [gitlab.tf](./gitlab.tf) | None | gitlab_group · gitlab_project · gitlab_project_variable | -| [main.tf](./main.tf) | Module-level locals and resources. | | -| [outputs-files.tf](./outputs-files.tf) | Output files persistence to local filesystem. | local_file | -| [outputs-gcs.tf](./outputs-gcs.tf) | Output files persistence to automation GCS bucket. | google_storage_bucket_object | -| [outputs.tf](./outputs.tf) | Module outputs. | | -| [variables.tf](./variables.tf) | Module variables. | | -| [versions.tf](./versions.tf) | Version pins. | | - -## Variables - -| name | description | type | required | default | producer | -|---|---|:---:|:---:|:---:|:---:| -| [automation](variables.tf#L17) | Automation resources created by the bootstrap stage. | object({…}) | ✓ | | 00-bootstrap | -| [cicd_repositories](variables.tf#L35) | CI/CD repository configuration. Identity providers reference keys in the `federated_identity_providers` variable. Set to null to disable, or set individual repositories to null if not needed. | object({…}) | | null | | -| [custom_roles](variables.tf#L132) | Custom roles defined at the org level, in key => id format. | object({…}) | | null | 00-bootstrap | -| [github](variables.tf#L120) | GitHub settings | object({…}) | | {…} | | -| [gitlab](variables.tf#L106) | Gitlab settings | object({…}) | | {…} | | -| [outputs_location](variables.tf#L141) | Enable writing provider, tfvars and CI/CD workflow files to local filesystem. Leave null to disable | string | | null | | - -## Outputs - -| name | description | sensitive | consumers | -|---|---|:---:|---| -| [tfvars](outputs.tf#L30) | Terraform variable files for the following stages. | ✓ | | - - diff --git a/fast/stages/00-cicd_/cicd-providers.tf b/fast/stages/00-cicd_/cicd-providers.tf deleted file mode 100644 index 5ea51121b1..0000000000 --- a/fast/stages/00-cicd_/cicd-providers.tf +++ /dev/null @@ -1,24 +0,0 @@ -/** - * Copyright 2022 Google LLC - * - * 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. - */ - -provider "github" { - base_url = var.github.url - owner = local.github_groups[0] -} - -provider "gitlab" { - base_url = var.gitlab.url -} diff --git a/fast/stages/00-cicd_/github.tf b/fast/stages/00-cicd_/github.tf deleted file mode 100644 index b4c7a57095..0000000000 --- a/fast/stages/00-cicd_/github.tf +++ /dev/null @@ -1,52 +0,0 @@ -/** - * Copyright 2022 Google LLC - * - * 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. - */ - -locals { - github_groups = distinct([ - for k, v in local.cicd_repositories_by_system["github"] : v.group - ]) -} - -data "github_organization" "organization" { - for_each = toset(local.github_groups) - name = each.value -} - -data "github_repository" "repositories" { - for_each = { - for name, repo in local.cicd_repositories_by_system["github"] : - name => repo if !try(repo.create, true) - } - full_name = format("%s/%s", each.value.group, each.value.name) -} - -resource "github_repository" "repositories" { - for_each = { - for name, repo in local.cicd_repositories_by_system["github"] : - name => repo if try(repo.create, true) - } - name = each.value.name - description = each.value.description - visibility = var.github.visibility -} - -resource "github_actions_secret" "actions-modules-key" { - for_each = { for name, repo in local.cicd_repositories_by_system["github"] : name => repo } - - repository = try(each.value.create, true) ? github_repository.repositories[each.key].name : data.github_repository.repositories[each.key].name - secret_name = "CICD_MODULES_KEY" - plaintext_value = tls_private_key.cicd-modules-key.private_key_openssh -} diff --git a/fast/stages/00-cicd_/gitlab.tf b/fast/stages/00-cicd_/gitlab.tf deleted file mode 100644 index bb6e28b1a1..0000000000 --- a/fast/stages/00-cicd_/gitlab.tf +++ /dev/null @@ -1,66 +0,0 @@ -/** - * Copyright 2022 Google LLC - * - * 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. - */ - -locals { - gitlab_create_groups = distinct([for k, v in local.cicd_repositories_by_system["gitlab"] : v.group if try(v.create_group, false)]) - gitlab_existing_groups = distinct([for k, v in local.cicd_repositories_by_system["gitlab"] : v.group if !try(v.create_group, false)]) -} - -data "gitlab_group" "group" { - for_each = toset(local.gitlab_existing_groups) - full_path = each.value -} - -data "gitlab_project" "projects" { - for_each = { for name, repo in local.cicd_repositories_by_system["gitlab"] : name => repo if !try(repo.create, true) } - id = format("%s/%s", each.value.group, each.value.name) -} - -resource "gitlab_group" "group" { - for_each = toset(local.gitlab_create_groups) - - name = each.value - path = each.value - description = "Cloud Foundation Fabric FAST: github.com/GoogleCloudPlatform/cloud-foundation-fabric/tree/master/fast/" -} - -resource "gitlab_project" "projects" { - for_each = { for name, repo in local.cicd_repositories_by_system["gitlab"] : name => repo if try(repo.create, true) } - - name = each.value.name - namespace_id = each.value.create_group ? gitlab_group.group[each.value.group].id : data.gitlab_group.group[each.value.group].id - description = each.value.description - - visibility_level = var.gitlab.project_visibility - shared_runners_enabled = var.gitlab.shared_runners_enabled - auto_devops_enabled = false -} - -resource "gitlab_project_variable" "project-modules-key" { - for_each = { for name, repo in local.cicd_repositories_by_system["gitlab"] : name => repo } - - project = try(each.value.create, true) ? gitlab_project.projects[each.key].id : data.gitlab_project.projects[each.key].id - - key = "CICD_MODULES_KEY" - value = base64encode(tls_private_key.cicd-modules-key.private_key_openssh) - - protected = false - masked = true - variable_type = "env_var" -} - - - diff --git a/fast/stages/00-cicd_/main.tf b/fast/stages/00-cicd_/main.tf deleted file mode 100644 index a9627f0e53..0000000000 --- a/fast/stages/00-cicd_/main.tf +++ /dev/null @@ -1,40 +0,0 @@ -/** - * Copyright 2022 Google LLC - * - * 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. - */ - -locals { - supported_cicd_systems = ["gitlab", "github", "sourcerepo"] - cicd_repositories = { - for k, v in coalesce(var.cicd_repositories, {}) : k => merge(v, { - create_group = try(v.create_group, true) - group = join("/", slice(split("/", v.name), 0, length(split("/", v.name)) - 1)) - name = element(split("/", v.name), length(split("/", v.name)) - 1) - }) - if( - v != null - && - contains(local.supported_cicd_systems, try(v.type, "")) - ) - } - cicd_repositories_by_system = { - for system in local.supported_cicd_systems : system => { - for k, v in local.cicd_repositories : k => v if v.type == system - } - } -} - -resource "tls_private_key" "cicd-modules-key" { - algorithm = "ED25519" -} diff --git a/fast/stages/00-cicd_/outputs-files.tf b/fast/stages/00-cicd_/outputs-files.tf deleted file mode 100644 index 6c201fc4c7..0000000000 --- a/fast/stages/00-cicd_/outputs-files.tf +++ /dev/null @@ -1,24 +0,0 @@ -/** - * Copyright 2022 Google LLC - * - * 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. - */ - -# tfdoc:file:description Output files persistence to local filesystem. - -resource "local_file" "tfvars" { - for_each = var.outputs_location == null ? {} : { 1 = 1 } - file_permission = "0644" - filename = "${pathexpand(var.outputs_location)}/tfvars/00-cicd.auto.tfvars.json" - content = jsonencode(local.tfvars) -} diff --git a/fast/stages/00-cicd_/outputs-gcs.tf b/fast/stages/00-cicd_/outputs-gcs.tf deleted file mode 100644 index 3a03ae4d71..0000000000 --- a/fast/stages/00-cicd_/outputs-gcs.tf +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Copyright 2022 Google LLC - * - * 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. - */ - -# tfdoc:file:description Output files persistence to automation GCS bucket. - -resource "google_storage_bucket_object" "tfvars" { - bucket = var.automation.outputs_bucket - name = "tfvars/00-bootstrap.auto.tfvars.json" - content = jsonencode(local.tfvars) -} diff --git a/fast/stages/00-cicd_/outputs.tf b/fast/stages/00-cicd_/outputs.tf deleted file mode 100644 index 6430e2b314..0000000000 --- a/fast/stages/00-cicd_/outputs.tf +++ /dev/null @@ -1,34 +0,0 @@ -/** - * Copyright 2022 Google LLC - * - * 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. - */ - -locals { - gitlab_cicd_https = { for k, v in local.cicd_repositories_by_system["gitlab"] : k => v.create ? gitlab_project.projects[k].http_url_to_repo : data.gitlab_project.projects[k].http_url_to_repo } - gitlab_cicd_ssh = { for k, v in local.cicd_repositories_by_system["gitlab"] : k => v.create ? gitlab_project.projects[k].ssh_url_to_repo : data.gitlab_project.projects[k].ssh_url_to_repo } - github_cicd_https = { for k, v in local.cicd_repositories_by_system["github"] : k => v.create ? github_repository.repositories[k].http_clone_url : data.github_repository.repositories[k].http_clone_url } - github_cicd_ssh = { for k, v in local.cicd_repositories_by_system["github"] : k => v.create ? github_repository.repositories[k].git_clone_url : data.github_repository.repositories[k].git_clone_url } - - tfvars = { - cicd_repositories = merge(local.cicd_repositories_by_system["gitlab"], local.cicd_repositories_by_system["github"]) - cicd_ssh_urls = merge(local.gitlab_cicd_ssh, local.github_cicd_ssh) - cicd_https_urls = merge(local.gitlab_cicd_https, local.github_cicd_https) - } -} - -output "tfvars" { - description = "Terraform variable files for the following stages." - sensitive = true - value = local.tfvars -} diff --git a/fast/stages/00-cicd_/variables.tf b/fast/stages/00-cicd_/variables.tf deleted file mode 100644 index c5cc5f695c..0000000000 --- a/fast/stages/00-cicd_/variables.tf +++ /dev/null @@ -1,145 +0,0 @@ -/** - * Copyright 2022 Google LLC - * - * 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. - */ - -variable "automation" { - # tfdoc:variable:source 00-bootstrap - description = "Automation resources created by the bootstrap stage." - type = object({ - outputs_bucket = string - project_id = string - project_number = string - federated_identity_pool = string - federated_identity_providers = map(object({ - issuer = string - issuer_uri = string - name = string - principal_tpl = string - principalset_tpl = string - })) - }) -} - -variable "cicd_repositories" { - description = "CI/CD repository configuration. Identity providers reference keys in the `federated_identity_providers` variable. Set to null to disable, or set individual repositories to null if not needed." - type = object({ - bootstrap = object({ - branch = string - name = string - description = string - type = string - create = bool - create_group = bool - }) - resman = object({ - branch = string - name = string - description = string - type = string - create = bool - create_group = bool - }) - networking = object({ - branch = string - name = string - description = string - type = string - create = bool - create_group = bool - }) - security = object({ - branch = string - name = string - description = string - type = string - create = bool - create_group = bool - }) - data-platform = object({ - branch = string - name = string - description = string - type = string - create = bool - create_group = bool - }) - project-factory = object({ - branch = string - name = string - description = string - type = string - create = bool - create_group = bool - }) - }) - default = null - validation { - condition = alltrue([ - for k, v in coalesce(var.cicd_repositories, {}) : - v == null || try(v.name, null) != null - ]) - error_message = "Non-null repositories need a non-null name." - } - validation { - condition = alltrue([ - for k, v in coalesce(var.cicd_repositories, {}) : - v == null || ( - contains(["github", "gitlab", "sourcerepo"], coalesce(try(v.type, null), "null")) - ) - ]) - error_message = "Invalid repository type, supported types: 'github' 'gitlab' or 'sourcerepo'." - } -} - -variable "gitlab" { - description = "Gitlab settings" - type = object({ - url = string - project_visibility = string - shared_runners_enabled = bool - }) - default = { - url = "https://gitlab.com" - project_visibility = "private" - shared_runners_enabled = true - } -} - -variable "github" { - description = "GitHub settings" - type = object({ - url = string - visibility = string - }) - default = { - url = null - visibility = "private" - } -} - -variable "custom_roles" { - # tfdoc:variable:source 00-bootstrap - description = "Custom roles defined at the org level, in key => id format." - type = object({ - service_project_network_admin = string - }) - default = null -} - -variable "outputs_location" { - description = "Enable writing provider, tfvars and CI/CD workflow files to local filesystem. Leave null to disable" - type = string - default = null -} diff --git a/fast/stages/00-cicd_/versions.tf b/fast/stages/00-cicd_/versions.tf deleted file mode 100644 index 1e30586f59..0000000000 --- a/fast/stages/00-cicd_/versions.tf +++ /dev/null @@ -1,38 +0,0 @@ -# Copyright 2022 Google LLC -# -# 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 -# -# https://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. - -terraform { - required_version = ">= 1.3.1" - required_providers { - google = { - source = "hashicorp/google" - version = ">= 4.36.0" # tftest - } - google-beta = { - source = "hashicorp/google-beta" - version = ">= 4.36.0" # tftest - } - github = { - source = "integrations/github" - version = "~> 4.0" - } - gitlab = { - source = "gitlabhq/gitlab" - version = ">= 3.16.1" - } - - } -} - - From b60bfdaed75b40ca1214f0aca578c1da24322006 Mon Sep 17 00:00:00 2001 From: Ludovico Magnocavallo Date: Fri, 21 Oct 2022 01:59:47 +0200 Subject: [PATCH 3/8] allow setting commit attributes via variabes --- fast/extras/00-cicd-github/cicd-versions.tf | 16 +++++++++ fast/extras/00-cicd-github/main.tf | 6 ++-- fast/extras/00-cicd-github/variables.tf | 10 ++++++ tests/fast/stages/s00_cicd/__init__.py | 13 -------- tests/fast/stages/s00_cicd/test_providers.py | 34 -------------------- 5 files changed, 29 insertions(+), 50 deletions(-) delete mode 100644 tests/fast/stages/s00_cicd/__init__.py delete mode 100644 tests/fast/stages/s00_cicd/test_providers.py diff --git a/fast/extras/00-cicd-github/cicd-versions.tf b/fast/extras/00-cicd-github/cicd-versions.tf index 23651dced6..3186511c6f 100644 --- a/fast/extras/00-cicd-github/cicd-versions.tf +++ b/fast/extras/00-cicd-github/cicd-versions.tf @@ -1,3 +1,19 @@ +/** + * Copyright 2022 Google LLC + * + * 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. + */ + terraform { required_version = ">= 1.3.1" required_providers { diff --git a/fast/extras/00-cicd-github/main.tf b/fast/extras/00-cicd-github/main.tf index afa8d13e29..338acc6965 100644 --- a/fast/extras/00-cicd-github/main.tf +++ b/fast/extras/00-cicd-github/main.tf @@ -105,8 +105,8 @@ resource "github_repository_file" "default" { ) : file(each.value.file) ) - commit_message = "Managed by Terraform" - commit_author = "Terraform User" - commit_email = "terraform@example.com" + commit_message = "${var.commmit_config.message} (${each.value.name})" + commit_author = var.commmit_config.author + commit_email = var.commmit_config.email overwrite_on_create = true } diff --git a/fast/extras/00-cicd-github/variables.tf b/fast/extras/00-cicd-github/variables.tf index 85d8aa7394..21f2833466 100644 --- a/fast/extras/00-cicd-github/variables.tf +++ b/fast/extras/00-cicd-github/variables.tf @@ -14,6 +14,16 @@ * limitations under the License. */ +variable "commmit_config" { + description = "Configure commit metadata." + type = object({ + author = optional(string, "FAST loader") + email = optional(string, "fast-loader@fast.gcp.tf") + message = optional(string, "FAST initial loading") + }) + default = {} + nullable = false +} variable "organization" { description = "GitHub organization." diff --git a/tests/fast/stages/s00_cicd/__init__.py b/tests/fast/stages/s00_cicd/__init__.py deleted file mode 100644 index 6d6d1266c3..0000000000 --- a/tests/fast/stages/s00_cicd/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -# Copyright 2022 Google LLC -# -# 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. diff --git a/tests/fast/stages/s00_cicd/test_providers.py b/tests/fast/stages/s00_cicd/test_providers.py deleted file mode 100644 index e45c869e3d..0000000000 --- a/tests/fast/stages/s00_cicd/test_providers.py +++ /dev/null @@ -1,34 +0,0 @@ -# Copyright 2022 Google LLC -# -# 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. - -import os -''' -github = { - source = "integrations/github" - version = "~> 4.0" -} -gitlab = { - source = "gitlabhq/gitlab" - version = ">= 3.16.1" -} -''' - - -def test_providers(basedir): - "Test providers file." - p = os.path.join(basedir, 'fast/stages/00-cicd/versions.tf') - with open(p) as f: - data = f.read() - assert 'integrations/github' in data - assert 'gitlabhq/gitlab' in data From 38d06f34bc2f4a5076186e2b5aece58c74229049 Mon Sep 17 00:00:00 2001 From: Ludovico Magnocavallo Date: Fri, 21 Oct 2022 02:03:47 +0200 Subject: [PATCH 4/8] remove reference to deleted stage --- fast/stages/README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/fast/stages/README.md b/fast/stages/README.md index 29fdc4c0f2..9b41bf1cae 100644 --- a/fast/stages/README.md +++ b/fast/stages/README.md @@ -24,8 +24,6 @@ To destroy a previous FAST deployment follow the instructions detailed in [clean - [Bootstrap](00-bootstrap/README.md) Enables critical organization-level functionality that depends on broad permissions. It has two primary purposes. The first is to bootstrap the resources needed for automation of this and the following stages (service accounts, GCS buckets). And secondly, it applies the minimum amount of configuration needed at the organization level, to avoid the need of broad permissions later on, and to implement a minimum of security features like sinks and exports from the start.\ Exports: automation variables, organization-level custom roles -- [CI/CD Bootstrap](00-cicd/README.md) - Optionally set up CI/CD repositories and project structure automatically for GitHub and Gitlab. This stage is not needed if repository are created manually. - [Resource Management](01-resman/README.md) Creates the base resource hierarchy (folders) and the automation resources required later to delegate deployment of each part of the hierarchy to separate stages. This stage also configures organization-level policies and any exceptions needed by different branches of the resource hierarchy.\ Exports: folder ids, automation service account emails From 896e5d2ec59ce7fa0abbc1591d7a1d7640a271cc Mon Sep 17 00:00:00 2001 From: Ludovico Magnocavallo Date: Sun, 23 Oct 2022 17:21:43 +0200 Subject: [PATCH 5/8] optional repo creation, documentation --- fast/extras/00-cicd-github/README.md | 70 ++++++++++++++ fast/extras/00-cicd-github/github_token.png | Bin 0 -> 56718 bytes fast/extras/00-cicd-github/main.tf | 100 +++++++++++--------- fast/extras/00-cicd-github/variables.tf | 48 +++++----- fast/extras/README.md | 5 + 5 files changed, 156 insertions(+), 67 deletions(-) create mode 100644 fast/extras/00-cicd-github/README.md create mode 100644 fast/extras/00-cicd-github/github_token.png create mode 100644 fast/extras/README.md diff --git a/fast/extras/00-cicd-github/README.md b/fast/extras/00-cicd-github/README.md new file mode 100644 index 0000000000..ee06d73b44 --- /dev/null +++ b/fast/extras/00-cicd-github/README.md @@ -0,0 +1,70 @@ +# FAST GitHub repository management + +This small extra stage allows creation and management of GitHub repositories used to host FAST stage code, including initial population of files and rewriting of module sources. + +This stage is designed for quick repository creation in a GitHub organization, and is not suited for medium or long-term repository management especially if you enable initial population of files. + +## Initial population caveats + +Initial file population of repositories is controlled via the `populate_from` attribute, and needs a bit of care: + +- never run this stage gain with the same variables used for population once the repository starts being used, as **Terraform will manage file state and revert any changes at each apply**, which is probably not what you want. +- be mindful when enabling initial population of the modules repository, as the number of resulting files to manage is very close to the GitHub hourly limit for their API + +The scenario for which this stage has been designed is one-shot creation and/or population of stage repositories, running it multiple times with different variables and Terraform states if incrmental creation is needed for subsequent FAST stages (e.g. GKE, data platform, etc.). + +## GitHub provider credentials + +A [GitHub token](https://github.com/settings/tokens) is needed to authenticate against their API. The token needs organization-level permissions, like shown in this screenshot: + +

+ GitHub token scopes. +

+ +## Variable configuration + +The `organization` required variable sets the GitHub organization where repositories will be created, and is used to configure the Terraform provider. + +The `repositories` variable is where you configure which repositories to create, whether initial population of files is desired, and which repository is used to host modules. + +This is an example that creates repositories for stages 00 and 01, defines an existing repositories as the source for modules, and populates initial files for stages 00, 01, and 02: + +```hcl +organization = "ludomagno" +repositories = { + fast_00_bootstrap = { + create_options = { + description = "FAST bootstrap." + features = { + issues = true + } + } + populate_from = "../../stages/00-bootstrap" + } + fast_01_resman = { + create_options = { + description = "FAST resource management." + features = { + issues = true + } + } + populate_from = "../../stages/01-resman" + } + fast_02_networking = { + populate_from = "../../stages/02-networking-peering" + } + fast_modules = { + has_modules = true + } +} +``` + +The `create_options` repository attribute controls creation: if the attribute is not present, the repository is assumed to be already existing. + +Initial population depends on a modules repository being configured, identified by the `has_modules` attribute, and on `populate_from` attributes in each repository where population is required, pointing to the folder holding the files to be committed. + +Finally, a `commit_config` variable is optional: it can be used to configure author, email and message used in commits for initial population of files, its defaults are probably fine for most use cases. + +## Modules secret + +When initial population is configured for a repository, this stage also adds a secret with the private key used to authenticate against the modules repository. This matches the configuration of the GitHub workflow files created for each FAST stage when CI/CD is enabled. diff --git a/fast/extras/00-cicd-github/github_token.png b/fast/extras/00-cicd-github/github_token.png new file mode 100644 index 0000000000000000000000000000000000000000..02426484cef4229b03a518703be6f4772627a2a5 GIT binary patch literal 56718 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 literal 0 HcmV?d00001 diff --git a/fast/extras/00-cicd-github/main.tf b/fast/extras/00-cicd-github/main.tf index 338acc6965..140230d06d 100644 --- a/fast/extras/00-cicd-github/main.tf +++ b/fast/extras/00-cicd-github/main.tf @@ -16,65 +16,69 @@ locals { _modules_repository = [ - for k, v in var.repositories : k if v.modules_source + for k, v in var.repositories : local.repositories[k] if v.has_modules ] - _populate_md = flatten([ + _repository_files = flatten([ for k, v in var.repositories : [ - for f in fileset(path.module, "${v.populate_with}/*.md") : { + for f in concat( + [for f in fileset(path.module, "${v.populate_from}/*.md") : f], + [for f in fileset(path.module, "${v.populate_from}/*.tf") : f] + ) : { repository = k file = f - name = replace(f, "${v.populate_with}/", "") + name = replace(f, "${v.populate_from}/", "") } - ] if v.populate_with != null - ]) - _populate_tf = flatten([ - for k, v in var.repositories : [ - for f in fileset(path.module, "${v.populate_with}/*.tf") : { - repository = k - file = f - name = replace(f, "${v.populate_with}/", "") - } - ] if v.populate_with != null + ] if v.populate_from != null ]) modules_repository = ( - length(local._modules_repository) > 0 ? local._modules_repository.0 : null + length(local._modules_repository) > 0 + ? local._modules_repository.0 + : null ) + repositories = { + for k, v in var.repositories : + k => v.create_options == null ? k : github_repository.default[k].name + } repository_files = { - for k in concat( - local._populate_md, - [ - for f in local._populate_tf : - f if !startswith(f.name, "0") && f.name != "globals.tf" - ] - ) : "${k.repository}/${k.name}" => k + for k in local._repository_files : + "${k.repository}/${k.name}" => k + if !endswith(k.name, ".tf") || ( + !startswith(k.name, "0") && k.name != "globals.tf" + ) } } resource "github_repository" "default" { - for_each = var.repositories - name = each.key + for_each = { + for k, v in var.repositories : k => v if v.create_options != null + } + name = each.key description = ( - each.value.description != null - ? each.value.description + each.value.create_options.description != null + ? each.value.create_options.description : "FAST stage ${each.key}." ) - visibility = each.value.visibility - auto_init = each.value.auto_init - allow_auto_merge = try(each.value.allow.auto_merge, null) - allow_merge_commit = try(each.value.allow.merge_commit, null) - allow_rebase_merge = try(each.value.allow.rebase_merge, null) - allow_squash_merge = try(each.value.allow.squash_merge, null) - has_issues = try(each.value.features.issues, null) - has_projects = try(each.value.features.projects, null) - has_wiki = try(each.value.features.wiki, null) - gitignore_template = try(each.value.templates.gitignore, null) - license_template = try(each.value.templates.license, null) + visibility = each.value.create_options.visibility + auto_init = each.value.create_options.auto_init + allow_auto_merge = try(each.value.create_options.allow.auto_merge, null) + allow_merge_commit = try(each.value.create_options.allow.merge_commit, null) + allow_rebase_merge = try(each.value.create_options.allow.rebase_merge, null) + allow_squash_merge = try(each.value.create_options.allow.squash_merge, null) + has_issues = try(each.value.create_options.features.issues, null) + has_projects = try(each.value.create_options.features.projects, null) + has_wiki = try(each.value.create_options.features.wiki, null) + gitignore_template = try(each.value.create_options.templates.gitignore, null) + license_template = try(each.value.create_options.templates.license, null) dynamic "template" { - for_each = try(each.value.templates.repository, null) != null ? [""] : [] + for_each = ( + try(each.value.create_options.templates.repository, null) != null + ? [""] + : [] + ) content { - owner = each.value.templates.repository.owner - repository = each.value.templates.repository.name + owner = each.value.create_options.templates.repository.owner + repository = each.value.create_options.templates.repository.name } } } @@ -85,15 +89,23 @@ resource "tls_private_key" "default" { } resource "github_actions_secret" "default" { - count = local.modules_repository != null ? 1 : 0 - repository = github_repository.default[local.modules_repository].name + for_each = local.modules_repository == null ? {} : { + for k, v in local.repositories : + k => v if( + k != local.modules_repository && + var.repositories[k].populate_from != null + ) + } + repository = local.repositories[local.modules_repository] secret_name = "CICD_MODULES_KEY" plaintext_value = tls_private_key.default.0.private_key_openssh } resource "github_repository_file" "default" { - for_each = local.repository_files - repository = github_repository.default[each.value.repository].name + for_each = ( + local.modules_repository == null ? {} : local.repository_files + ) + repository = local.repositories[each.value.repository] branch = "main" file = each.value.name content = ( diff --git a/fast/extras/00-cicd-github/variables.tf b/fast/extras/00-cicd-github/variables.tf index 21f2833466..29b79225e8 100644 --- a/fast/extras/00-cicd-github/variables.tf +++ b/fast/extras/00-cicd-github/variables.tf @@ -33,30 +33,32 @@ variable "organization" { variable "repositories" { description = "Repositories to create." type = map(object({ - allow = optional(object({ - auto_merge = optional(bool) - merge_commit = optional(bool) - rebase_merge = optional(bool) - squash_merge = optional(bool) - })) - auto_init = optional(bool) - description = optional(string) - features = optional(object({ - issues = optional(bool) - projects = optional(bool) - wiki = optional(bool) - })) - modules_source = optional(bool, false) - templates = optional(object({ - gitignore = optional(string, "Terraform") - license = optional(string) - repository = optional(object({ - name = string - owner = string + create_options = optional(object({ + allow = optional(object({ + auto_merge = optional(bool) + merge_commit = optional(bool) + rebase_merge = optional(bool) + squash_merge = optional(bool) })) - }), {}) - populate_with = optional(string) - visibility = optional(string, "private") + auto_init = optional(bool) + description = optional(string) + features = optional(object({ + issues = optional(bool) + projects = optional(bool) + wiki = optional(bool) + })) + templates = optional(object({ + gitignore = optional(string, "Terraform") + license = optional(string) + repository = optional(object({ + name = string + owner = string + })) + }), {}) + visibility = optional(string, "private") + })) + has_modules = optional(bool, false) + populate_from = optional(string) })) default = {} nullable = true diff --git a/fast/extras/README.md b/fast/extras/README.md new file mode 100644 index 0000000000..121fa4b049 --- /dev/null +++ b/fast/extras/README.md @@ -0,0 +1,5 @@ +# FAST extra stages + +This folder contains additional helper stages for FAST, which can be used to simplify specific operational tasks: + +- [GitHub repository management](./00-cicd-github/) From a7c47c0e369845d44e2253497a204bf005012705 Mon Sep 17 00:00:00 2001 From: Ludovico Magnocavallo Date: Sun, 23 Oct 2022 17:54:20 +0200 Subject: [PATCH 6/8] tfdoc --- fast/extras/00-cicd-github/README.md | 22 +++++++++++++++++++++ fast/extras/00-cicd-github/cicd-versions.tf | 2 ++ fast/extras/00-cicd-github/providers.tf | 2 ++ 3 files changed, 26 insertions(+) diff --git a/fast/extras/00-cicd-github/README.md b/fast/extras/00-cicd-github/README.md index ee06d73b44..ee8cf51fef 100644 --- a/fast/extras/00-cicd-github/README.md +++ b/fast/extras/00-cicd-github/README.md @@ -68,3 +68,25 @@ Finally, a `commit_config` variable is optional: it can be used to configure aut ## Modules secret When initial population is configured for a repository, this stage also adds a secret with the private key used to authenticate against the modules repository. This matches the configuration of the GitHub workflow files created for each FAST stage when CI/CD is enabled. + + + + +## Files + +| name | description | resources | +|---|---|---| +| [cicd-versions.tf](./cicd-versions.tf) | provider version | | +| [main.tf](./main.tf) | Module-level locals and resources. | github_actions_secret · github_repository · github_repository_file · tls_private_key | +| [providers.tf](./providers.tf) | provider configuration | | +| [variables.tf](./variables.tf) | Module variables. | | + +## Variables + +| name | description | type | required | default | +|---|---|:---:|:---:|:---:| +| [organization](variables.tf#L28) | GitHub organization. | string | ✓ | | +| [commmit_config](variables.tf#L17) | Configure commit metadata. | object({…}) | | {} | +| [repositories](variables.tf#L33) | Repositories to create. | map(object({…})) | | {} | + + diff --git a/fast/extras/00-cicd-github/cicd-versions.tf b/fast/extras/00-cicd-github/cicd-versions.tf index 3186511c6f..e6528a6de3 100644 --- a/fast/extras/00-cicd-github/cicd-versions.tf +++ b/fast/extras/00-cicd-github/cicd-versions.tf @@ -14,6 +14,8 @@ * limitations under the License. */ +# tfdoc:file:description provider version + terraform { required_version = ">= 1.3.1" required_providers { diff --git a/fast/extras/00-cicd-github/providers.tf b/fast/extras/00-cicd-github/providers.tf index 63954de899..979c642de0 100644 --- a/fast/extras/00-cicd-github/providers.tf +++ b/fast/extras/00-cicd-github/providers.tf @@ -14,6 +14,8 @@ * limitations under the License. */ +# tfdoc:file:description provider configuration + provider "github" { owner = var.organization } From 95ca32d2b2cbf3fae3a04cccb3586441c8bdec1b Mon Sep 17 00:00:00 2001 From: Ludovico Magnocavallo Date: Sun, 23 Oct 2022 17:55:13 +0200 Subject: [PATCH 7/8] tfdoc --- fast/extras/00-cicd-github/README.md | 4 ++-- fast/extras/00-cicd-github/cicd-versions.tf | 2 +- fast/extras/00-cicd-github/providers.tf | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/fast/extras/00-cicd-github/README.md b/fast/extras/00-cicd-github/README.md index ee8cf51fef..3094202ad2 100644 --- a/fast/extras/00-cicd-github/README.md +++ b/fast/extras/00-cicd-github/README.md @@ -76,9 +76,9 @@ When initial population is configured for a repository, this stage also adds a s | name | description | resources | |---|---|---| -| [cicd-versions.tf](./cicd-versions.tf) | provider version | | +| [cicd-versions.tf](./cicd-versions.tf) | Provider version. | | | [main.tf](./main.tf) | Module-level locals and resources. | github_actions_secret · github_repository · github_repository_file · tls_private_key | -| [providers.tf](./providers.tf) | provider configuration | | +| [providers.tf](./providers.tf) | Provider configuration. | | | [variables.tf](./variables.tf) | Module variables. | | ## Variables diff --git a/fast/extras/00-cicd-github/cicd-versions.tf b/fast/extras/00-cicd-github/cicd-versions.tf index e6528a6de3..09f544cba0 100644 --- a/fast/extras/00-cicd-github/cicd-versions.tf +++ b/fast/extras/00-cicd-github/cicd-versions.tf @@ -14,7 +14,7 @@ * limitations under the License. */ -# tfdoc:file:description provider version +# tfdoc:file:description Provider version. terraform { required_version = ">= 1.3.1" diff --git a/fast/extras/00-cicd-github/providers.tf b/fast/extras/00-cicd-github/providers.tf index 979c642de0..29be30ae98 100644 --- a/fast/extras/00-cicd-github/providers.tf +++ b/fast/extras/00-cicd-github/providers.tf @@ -14,7 +14,7 @@ * limitations under the License. */ -# tfdoc:file:description provider configuration +# tfdoc:file:description Provider configuration. provider "github" { owner = var.organization From 937ce139c6a94e0de7fdccc38a54a1cda8b9fdcb Mon Sep 17 00:00:00 2001 From: Simone Ruffilli Date: Sun, 23 Oct 2022 19:37:01 +0200 Subject: [PATCH 8/8] Update README.md --- fast/extras/00-cicd-github/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fast/extras/00-cicd-github/README.md b/fast/extras/00-cicd-github/README.md index 3094202ad2..2db9952c51 100644 --- a/fast/extras/00-cicd-github/README.md +++ b/fast/extras/00-cicd-github/README.md @@ -11,7 +11,7 @@ Initial file population of repositories is controlled via the `populate_from` at - never run this stage gain with the same variables used for population once the repository starts being used, as **Terraform will manage file state and revert any changes at each apply**, which is probably not what you want. - be mindful when enabling initial population of the modules repository, as the number of resulting files to manage is very close to the GitHub hourly limit for their API -The scenario for which this stage has been designed is one-shot creation and/or population of stage repositories, running it multiple times with different variables and Terraform states if incrmental creation is needed for subsequent FAST stages (e.g. GKE, data platform, etc.). +The scenario for which this stage has been designed is one-shot creation and/or population of stage repositories, running it multiple times with different variables and Terraform states if incremental creation is needed for subsequent FAST stages (e.g. GKE, data platform, etc.). ## GitHub provider credentials