diff --git a/Gemfile b/Gemfile index 1ece7c3..31521a8 100644 --- a/Gemfile +++ b/Gemfile @@ -16,4 +16,5 @@ ruby '2.5.3' source 'https://rubygems.org/' do gem 'kitchen-terraform', '~> 4.1.0' + gem 'retriable', '~> 3.1.2' end diff --git a/examples/simple_example/README.md b/examples/simple_example/README.md index 6dfcb60..c53dc07 100644 --- a/examples/simple_example/README.md +++ b/examples/simple_example/README.md @@ -149,8 +149,8 @@ $ curl -H Metadata-Flavor:Google http://metadata.google.internal/computeMetadata | Name | Description | |------|-------------| | nat_ip | Public IP address of the example compute instance. | -| project_id | | -| region | | +| project_id | The project id used when managing resources. | +| region | The region used when managing resources. | [^]: (autogen_docs_end) diff --git a/examples/simple_example/files/startup-script-custom b/examples/simple_example/files/startup-script-custom index bf06a39..0802cb9 100644 --- a/examples/simple_example/files/startup-script-custom +++ b/examples/simple_example/files/startup-script-custom @@ -1,5 +1,16 @@ #! /bin/bash + +# add example_user for test +sudo useradd -m example_user1 + +# add another user for inverse test - \ +# sudo should not work for this user +sudo useradd -m example_user2 +sudo sh -c 'echo testpassword2019 | passwd --stdin example_user2' + +stdlib::info "Checking whether to add any users to sudoers ..." +stdlib::setup_sudoers + URL=${URL:-"http://ifconfig.co/json"} stdlib::info "Fetching ${URL}" stdlib::cmd curl --silent "${URL}" -echo diff --git a/examples/simple_example/main.tf b/examples/simple_example/main.tf index 10cd920..769285e 100644 --- a/examples/simple_example/main.tf +++ b/examples/simple_example/main.tf @@ -23,6 +23,7 @@ provider "google" { module "startup-scripts" { source = "../../" + enable_setup_sudoers = true } data "google_compute_image" "os" { @@ -30,6 +31,12 @@ data "google_compute_image" "os" { family = "centos-7" } +resource "google_compute_project_metadata" "example" { + metadata = { + sudoers = "example_user1,example_user2" + } +} + resource "google_compute_instance" "example" { name = "startup-scripts-example1" description = "Startup Scripts Example" @@ -49,6 +56,7 @@ resource "google_compute_instance" "example" { boot_disk { auto_delete = true + initialize_params { image = "${data.google_compute_image.os.self_link}" type = "pd-standard" @@ -57,6 +65,7 @@ resource "google_compute_instance" "example" { network_interface { network = "default" + access_config { // Ephemeral IP } diff --git a/files/setup_sudoers.sh b/files/setup_sudoers.sh new file mode 100644 index 0000000..518e84b --- /dev/null +++ b/files/setup_sudoers.sh @@ -0,0 +1,53 @@ +#! /bin/bash +# Copyright 2018 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. + +# Read the project metadata key named "sudoers" and add each comma separated value to +# the sudoers file. +stdlib::setup_sudoers() { + local user user_list sudoers_file + user_list="$(stdlib::metadata_get -k 'project/attributes/sudoers')" + + if [[ -z "${user_list}" ]]; then + stdlib::debug "Skipping sudoers setup. \ + The value of the project metadata key named sudoers is empty. \ + Set sudoers to a comma separated list to enable sudo \ + support, e.g. sudoers=jmccune,pames" + return 0 + fi + + sudoers_file="/etc/sudoers" + sudoers_d="/etc/sudoers.d" + + for user in ${user_list//,/ }; do + if [[ -f "${sudoers_d}/${user}" ]] \ + && ( grep -q "^${user}"'\b' "${sudoers_d}/${user}" ) \ + || (grep -q "^${user}"'\b' "${sudoers_file}") ; then + stdlib::debug "User ${user} is already in /etc/sudoers or \ + /etc/sudoers.d/${user}, taking no action" + else + stdlib::info "Adding ${user} to /etc/sudoers.d/${user}" + echo -e "${user}\tALL= (ALL)\tNOPASSWD: ALL" \ + > "${sudoers_d}/${user}" \ + && chmod 0440 "${sudoers_d}/${user}" + fi + done + + if visudo -c ; then + stdlib::info "sudoers config valid!" + else + stdlib::error "sudoers config invalid!" + return 1 + fi +} diff --git a/main.tf b/main.tf index 478e067..b36edb7 100644 --- a/main.tf +++ b/main.tf @@ -17,17 +17,21 @@ locals { stdlib_head = "${file("${path.module}/files/startup-script-stdlib-head.sh")}" gsutil_el = "${var.enable_init_gsutil_crcmod_el ? file("${path.module}/files/init_gsutil_crcmod_el.sh") : ""}" + sudoers = "${var.enable_setup_sudoers ? file("${path.module}/files/setup_sudoers.sh") : ""}" get_from_bucket = "${var.enable_get_from_bucket ? file("${path.module}/files/get_from_bucket.sh") : ""}" setup_init_script = "${(var.enable_setup_init_script && var.enable_get_from_bucket) ? file("${path.module}/files/setup_init_script.sh") : ""}" stdlib_body = "${file("${path.module}/files/startup-script-stdlib-body.sh")}" + # List representing complete content, to be concatenated together. stdlib_list = [ "${local.stdlib_head}", "${local.gsutil_el}", "${local.get_from_bucket}", "${local.setup_init_script}", + "${local.sudoers}", "${local.stdlib_body}", ] + # Final content output to the user stdlib = "${join("", local.stdlib_list)}" } diff --git a/test/fixtures/simulated_ci_environment/README.md b/test/fixtures/simulated_ci_environment/README.md index 7fec0a1..978bcaf 100644 --- a/test/fixtures/simulated_ci_environment/README.md +++ b/test/fixtures/simulated_ci_environment/README.md @@ -48,6 +48,6 @@ For example: | Name | Description | |------|-------------| -| phoogle_sa | The SA KEY JSON content. Store in GOOGLE_CREDENTIALS. | +| service_account_private_key | The SA KEY JSON content. Store in GOOGLE_CREDENTIALS. This is equivalent to the `phoogle_sa` output in the infra repository | [^]: (autogen_docs_end) diff --git a/test/integration/simple_example/controls/sudoers.rb b/test/integration/simple_example/controls/sudoers.rb new file mode 100644 index 0000000..ba9fc4b --- /dev/null +++ b/test/integration/simple_example/controls/sudoers.rb @@ -0,0 +1,41 @@ +# Copyright 2018 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. + +require 'retriable' + +control 'simple startup-script-custom' do + title "With the simple example of startup-script-custom calling stdlib::info and stdlib::cmd" + + describe "gcloud ... get-serial-port-output startup-scripts-example1" do + # Avoid racing against the instance boot sequence + before :all do + Retriable.retriable(tries: 20) do + get_serial_port_output = "gcloud compute instances get-serial-port-output startup-scripts-example1" + @cmd = command("#{get_serial_port_output} --project #{attribute('project_id')} --zone #{attribute('region')}-a") + if not %r{systemd: Startup finished}.match(@cmd.stdout) + raise StandardError, "Not found: 'systemd: Startup finished' in console output, cannot proceed" + end + end + end + + subject do + @cmd + end + + its('exit_status') { should be 0 } + its('stdout') { should match(%r{Info \[\d+\]: Adding example_user1 to /etc/sudoers}) } + its('stdout') { should match(%r{Info \[\d+\]: sudoers config valid!}) } + its('stdout') { should match('INFO startup-script: Return code 0.') } + end +end diff --git a/variables.tf b/variables.tf index 146e9bb..599b84f 100644 --- a/variables.tf +++ b/variables.tf @@ -27,3 +27,8 @@ variable "enable_setup_init_script" { description = "If not false, include stdlib::setup_init_script() prior to executing startup-script-custom. Call this function to load an init script from GCS into /etc/init.d and initialize it with chkconfig. This function depends on stdlib::get_from_bucket, so this function won't be enabled if enable_get_from_bucket is false." default = "false" } + +variable "enable_setup_sudoers" { + description = "If true, include stdlib::setup_sudoers() prior to executing startup-script-custom. Call this function from startup-script-custom to setup unix usernames in sudoers Comma separated values must be posted to the project metadata key project/attributes/sudoers" + default = "false" +}