From 72cad8f3be60b3d3dcc7296c62416b334dc72b93 Mon Sep 17 00:00:00 2001 From: Corey Hemminger Date: Mon, 4 Mar 2024 17:25:18 -0600 Subject: [PATCH] fix idempotency of policy and user creation (#9) * fix idempotency of policy and user creation --- .github/workflows/ci.yml | 28 +++++-- CHANGELOG.md | 4 + README.md | 18 ---- attributes/default.rb | 34 ++++---- kitchen.yml | 8 +- libraries/helpers.rb | 33 ++------ metadata.rb | 2 +- recipes/chef_automatev2.rb | 24 ++++-- resources/iam_policy.rb | 100 +++++++++++++++++++++++ resources/iam_user.rb | 83 +++++++++++++++++++ test/integration/default/default_test.rb | 12 ++- 11 files changed, 261 insertions(+), 85 deletions(-) create mode 100644 resources/iam_policy.rb create mode 100644 resources/iam_user.rb diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8bb6f62..d58ccf4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,15 +15,12 @@ jobs: cookstylelint: uses: Stromweld/github-workflows/.github/workflows/cookstyle-lint.yml@main - integration-dokken: + integration: runs-on: ubuntu-latest strategy: matrix: os: - - amazonlinux-2023 - centos-7 - - centos-stream-8 - - centos-stream-9 - almalinux-8 - almalinux-9 - rockylinux-8 @@ -31,13 +28,30 @@ jobs: - ubuntu-2004 - ubuntu-2204 suite: - - default + - server - automate - supermarket + exclude: + - os: almalinux-9 + suite: supermarket + - os: rockylinux-8 + suite: server + - os: rockylinux-8 + suite: supermarket + - os: rockylinux-9 + suite: server + - os: rockylinux-9 + suite: supermarket + - os: ubuntu-2204 + suite: supermarket fail-fast: false steps: - name: Check out code uses: actions/checkout@main + - name: Install Vagrant + run: | + sudo apt-get update + sudo apt-get install -y vagrant virtualbox - name: Install Chef uses: actionshub/chef-install@main - name: Test-Kitchen Converge @@ -48,7 +62,6 @@ jobs: action: converge env: CHEF_LICENSE: accept-no-persist - KITCHEN_LOCAL_YAML: kitchen.dokken.yml - name: Test-Kitchen Verify uses: actionshub/test-kitchen@main with: @@ -57,7 +70,6 @@ jobs: action: verify env: CHEF_LICENSE: accept-no-persist - KITCHEN_LOCAL_YAML: kitchen.dokken.yml check: if: always() @@ -66,7 +78,7 @@ jobs: - yamllint - jsonlint - cookstylelint - - integration-dokken + - integration runs-on: Ubuntu-latest steps: - name: Decide whether the needed jobs succeeded or failed diff --git a/CHANGELOG.md b/CHANGELOG.md index 03ba449..4dc97b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ This file is used to list changes made in each version of the chef_software cookbook. +## 2.2.0 (2024-03-04) + +- [Corey Hemminger] - Moved iam_policy and iam_user creation to resources, fixed idempotency in resources + ## 2.1.2 (2023-03-31) - [Corey Hemminger] - Hacky way of adding idempotency to chef-server org admin association diff --git a/README.md b/README.md index 95c8620..69df256 100644 --- a/README.md +++ b/README.md @@ -10,24 +10,6 @@ Please refer to the chef-ingredient cookbook {accept_license: true,},}, config: <<~EOC api_fqdn "#{node['chef_software']['chef_server_api_fqdn']}" topology "standalone" #{"data_collector['root_url'] = 'https://#{node['chef_software']['chef_automate_api_fqdn']}/data-collector/v0/' data_collector['proxy'] = true profiles['root_url'] = 'https://#{node['chef_software']['chef_supermarket_api_fqdn']}'" if node['chef_software']['chef_automate_api_fqdn']} #{"oc_id['applications'] ||= {} oc_id['applications']['supermarket'] = {'redirect_uri' => 'https://#{node['chef_software']['chef_supermarket_api_fqdn']}/auth/chef_oauth2/callback',}" if node['chef_software']['chef_supermarket_api_fqdn']} EOC} | (Hash) Used to add configuration options to chef-server | -| ['chef_software']['chef_user'] | {test1: {first_name: 'Test',last_name: '1',email: 'test1@example.com',password: 'Test1234!',},} | (Hash) Hash of hashes used to manage chef-server users | -| ['chef_software']['chef_org'] | {testing: {org_full_name: 'Testing Chef Server', admins: %w(test1), users: %w(),},} | (Hash) Hash of hashes used to manage chef-server organizations | -| ['chef_software']['chef_supermarket'] | {chef_server_url: "https://#{node['chef_software']['chef_server_api_fqdn']}", chef_oauth2_app_id: 'testGUID', chef_oauth2_secret: 'testGUID', chef_oauth2_verify_ssl: false, accept_license: true, config: {fqdn: node['chef_software']['chef_supermarket_api_fqdn'], smtp_address: 'localhost', smtp_port: 25, from_email: 'chef-supermarket.example.com', features: 'tools,gravatar,github,announcement,fieri', fieri_key: 'randomstuff', fieri_supermarket_endpoint: node['chef_software']['chef_supermarket_api_fqdn'],},} | (Hash) Used to add configuration options to chef-supermarket | - ## Recipes ### default recipe diff --git a/attributes/default.rb b/attributes/default.rb index d9be5d7..30e336a 100644 --- a/attributes/default.rb +++ b/attributes/default.rb @@ -22,13 +22,13 @@ default['chef_software']['automate_admin_token'] = nil default['chef_software']['chef_automatev2'] = { - products: %w(automate infra-server builder desktop), + products: %w(automate infra-server builder), accept_license: true, - config: (<<~EOC + config: <<~EOC, [global.v1] fqdn = "#{node['chef_software']['chef_automate_api_fqdn']}" - EOC - ), + EOC + } default['chef_software']['automatev2_local_users'] = { @@ -53,24 +53,26 @@ effect: 'ALLOW', actions: ['*'], projects: ['*'], - role: 'owner' - } - ] + role: 'owner', + }, + ], }, }, } default['chef_software']['chef_server'] = { accept_license: true, - config: (<<~EOC - api_fqdn "#{node['chef_software']['chef_server_api_fqdn']}" - topology "standalone" - #{"oc_id['applications'] ||= {} -oc_id['applications']['supermarket'] = { - 'redirect_uri' => 'https://#{node['chef_software']['chef_supermarket_api_fqdn']}/auth/chef_oauth2/callback' -}" if node['chef_software']['chef_supermarket_api_fqdn']} - EOC - ), + config: <<~EOC, + api_fqdn "#{node['chef_software']['chef_server_api_fqdn']}" + topology "standalone" + #{if node['chef_software']['chef_supermarket_api_fqdn'] + "oc_id['applications'] ||= {} + oc_id['applications']['supermarket'] = { + 'redirect_uri' => 'https://#{node['chef_software']['chef_supermarket_api_fqdn']}/auth/chef_oauth2/callback' + }" + end} + EOC + } default['chef_software']['chef_user'] = { diff --git a/kitchen.yml b/kitchen.yml index d3a005c..f444d63 100755 --- a/kitchen.yml +++ b/kitchen.yml @@ -32,21 +32,23 @@ platforms: - name: ubuntu-22.04 suites: - - name: default + - name: server named_run_list: 'chef_server' driver: customize: - memory: 3328 + memory: 4096 verifier: inspec_tests: - test/integration/default - test/integration/chef_server attributes: + chef_software: + automate_admin_token: mIUYdbBD6U8a9wg3IAbXScEjiXs= - name: automate named_run_list: 'chef_automatev2' driver: customize: - memory: 3072 + memory: 4096 verifier: inspec_tests: - test/integration/default diff --git a/libraries/helpers.rb b/libraries/helpers.rb index 7580036..768f3d1 100644 --- a/libraries/helpers.rb +++ b/libraries/helpers.rb @@ -1,36 +1,15 @@ module ChefSoftware module Helpers - def get_iam_user(user) - Mash.new(JSON.parse(shell_out("curl --insecure -s -H \"api-token: #{node['chef_software']['automate_admin_token']}\" https://localhost/apis/iam/v2/users/#{user}").stdout)) + def get_iam_user(user, token) + Mash.new(JSON.parse(shell_out("curl --insecure -s -H \"api-token: #{token}\" https://localhost/apis/iam/v2/users/#{user}").stdout)) end - def get_iam_policy(policy_name) - Mash.new(JSON.parse(shell_out("curl --insecure -s -H \"api-token: #{node['chef_software']['automate_admin_token']}\" https://localhost/apis/iam/v2/policies/#{policy_name}").stdout)) + def get_iam_policy(policy_name, token) + Mash.new(JSON.parse(shell_out("curl --insecure -s -H \"api-token: #{token}\" https://localhost/apis/iam/v2/policies/#{policy_name}").stdout)) end - def create_iam_user(user_json) - json = user_json.to_json - execute "create local user #{user_json['id']}" do - command "curl --insecure -s -H \"api-token: #{node['chef_software']['automate_admin_token']}\" -H \"Content-Type: application/json\" -d '#{json}' https://localhost/apis/iam/v2/users" - not_if { user_json['id'].eql?(get_iam_user(user_json['id'])['user']['id']) } - #sensitive true - end - end - - def create_iam_policy(policy_json) - json = policy_json.to_json - test_policy_statements = get_iam_policy(policy_json['id'])['policy']['statements']&.each do |state| - state.delete('resources') - end - execute "generate iam policy #{policy_json['id']}" do - command "curl --insecure -s -H \"api-token: #{node['chef_software']['automate_admin_token']}\" -H \"Content-Type: application/json\" -d '#{json}' https://localhost/apis/iam/v2/policies" - not_if { - policy_json['id'].eql?(get_iam_policy(policy_json['id'])['policy']['id']) && - policy_json['members'].eql?(get_iam_policy(policy_json['id'])['policy']['members']) && - policy_json['statements'].eql?(test_policy_statements) - } - #sensitive true - end + def kitchen_create_api_token(name) + shell_out("chef-automate iam token create #{name} --admin").stdout.strip end end end diff --git a/metadata.rb b/metadata.rb index 15180b7..ef270d1 100644 --- a/metadata.rb +++ b/metadata.rb @@ -3,7 +3,7 @@ maintainer_email 'hemminger@hotmail.com' license 'Apache-2.0' description 'Installs/Configures chef server, chef automate2, chef supermarket' -version '2.1.2' +version '2.2.0' chef_version '>= 16.4' issues_url 'https://github.com/Stromweld/chef_software/issues' diff --git a/recipes/chef_automatev2.rb b/recipes/chef_automatev2.rb index fb84214..398b97c 100644 --- a/recipes/chef_automatev2.rb +++ b/recipes/chef_automatev2.rb @@ -22,13 +22,27 @@ end end -if node['chef_software']['automate_admin_token'] - node['chef_software']['automatev2_local_users']&.each do |name, hash| - create_iam_user(hash['user_json']) +if kitchen? + ruby_block 'create_automate_admin_token' do + block do + node.run_state['automate_admin_token'] = kitchen_create_api_token('admin') + end end +end + +node['chef_software']['automatev2_local_users']&.each do |name, hash| + iam_user name do + user_hash hash['user_json'] + api_token lazy { kitchen? ? node.run_state['automate_admin_token'] : node['chef_software']['automate_admin_token'] } + action :create + end +end - node['chef_software']['automatev2_iam_policies']&.each do |name, hash| - create_iam_policy(hash['policy_json']) +node['chef_software']['automatev2_iam_policies']&.each do |name, hash| + iam_policy name do + policy_hash hash['policy_json'] + api_token lazy { kitchen? ? node.run_state['automate_admin_token'] : node['chef_software']['automate_admin_token'] } + action :create end end diff --git a/resources/iam_policy.rb b/resources/iam_policy.rb new file mode 100644 index 0000000..309ebbf --- /dev/null +++ b/resources/iam_policy.rb @@ -0,0 +1,100 @@ +# To learn more about Custom Resources, see https://docs.chef.io/custom_resources.html +# +# Author:: Corey Hemminger +# Cookbook:: chef_software +# Resource:: iam_policy +# +# Copyright:: 2024, Corey Hemminger +# +# 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. +unified_mode true +provides :iam_policy + +description 'manage IAM policies' + +property :name, String, + name_property: true, + description: 'Name of the IAM policy' + +property :policy_hash, Hash, + required: true, + description: 'Policy json in ruby hash format' + +property :api_token, String, + required: true, + sensitive: true, + description: 'Automate API token' + +action :create do + description 'Create a new IAM user' + + name = new_resource.name + policy_hash = new_resource.policy_hash + policy_json = policy_hash.to_json + api_token = new_resource.api_token + # Try to fetch policy from server + srv_policy = get_iam_policy(policy_hash['id'], api_token) + # Test if policy on server exists and any errors contacting server + test_result = if srv_policy['error'].eql?("no policy with ID \"#{policy_hash['id']}\" found") + true + elsif srv_policy['error'] + raise srv_policy['error'].inspect + elsif srv_policy['policy']['id'].eql?(policy_hash['id']) + false + else + raise "Unable to determine status of policy ensure this policy_hash id doesn't match an existing srv_policy\npolicy_hash: #{policy_hash['id'].inspect}\nsrv_policy: #{srv_policy['id'].inspect}\nor the error message from server says \"no policy with ID \"#{policy_hash['id']}\" found\"\nError_msg: #{srv_policy['error'].inspect}\n" + end + execute "create iam policy #{name}" do + command "curl --insecure -s -H \"api-token: #{api_token}\" -H \"Content-Type: application/json\" -d '#{policy_json}' https://localhost/apis/iam/v2/policies" + only_if { test_result } + sensitive true + end +end + +action :update do + name = new_resource.name + policy_hash = new_resource.policy_hash + policy_json = policy_hash.to_json + api_token = new_resource.api_token + # Try to fetch policy from server + srv_policy = get_iam_policy(policy_json['id'], api_token) + Chef::Log.info("\nuserpolicy: #{policy_json.inspect}\nsrv_policy: #{srv_policy.inspect}\n") + # Test policy from server and desired policy match key by key from desired policy + test_result = if srv_policy['error'] + raise srv_policy['error'].inspect + else + test = true + policy_hash.each_key do |key| + if key.eql?('statements') + policy_hash['statements'].each_index do |i| + policy_hash['statements'][i].each_key do |statement_key| + test = policy_hash['statements'][i][statement_key].eql?(srv_policy['policy']['statements'][i][statement_key]) + break if test.eql?(false) + end + break if test.eql?(false) + end + break if test.eql?(false) + next + end + break if test.eql?(false) + test = policy_hash[key].eql?(srv_policy['policy'][key]) + break if test.eql?(false) + end + test + end + execute "update iam policy #{name}" do + command "curl -X PUT --insecure -s -H \"api-token: #{api_token}\" -H \"Content-Type: application/json\" -d '#{policy_json}' https://localhost/apis/iam/v2/policies/#{policy_hash['id']}" + not_if { test_result } + sensitive true + end +end diff --git a/resources/iam_user.rb b/resources/iam_user.rb new file mode 100644 index 0000000..9e47559 --- /dev/null +++ b/resources/iam_user.rb @@ -0,0 +1,83 @@ +# To learn more about Custom Resources, see https://docs.chef.io/custom_resources.html +# +# Author:: Corey Hemminger +# Cookbook:: chef_software +# Resource:: iam_user +# +# Copyright:: 2024, Corey Hemminger +# +# 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. +unified_mode true +provides :iam_user + +description 'manage IAM users' + +property :name, String, + name_property: true, + description: 'Name of the IAM user' + +property :user_hash, Hash, + required: true, + sensitive: true, + description: 'User json in ruby hash format' + +property :api_token, String, + required: true, + # sensitive: true, + description: 'Automate API token' + +action :create do + description 'Create a new IAM user' + + name = new_resource.name + user_hash = new_resource.user_hash + user_json = user_hash.to_json + api_token = new_resource.api_token + # Try to fetch user from server + srv_user = get_iam_user(user_hash['id'], api_token) + # Test if user on server exists and any errors contacting server + test_result = if srv_user['error'].eql?('No user record found') + true + elsif srv_user['error'] + raise srv_user['error'].inspect + elsif srv_user['user']['id'].eql?(user_hash['id']) + false + else + raise "Unable to determine status of user, ensure this user_hash id doesn't match an existing srv_user\nuser_hash: #{user_hash['id'].inspect}\nsrv_user: #{srv_user['id'].inspect}\nor the error message from server says 'No user record found'\nError_msg: #{srv_user['error'].inspect}\n" + end + execute "create local user #{name}" do + command "curl --insecure -s -H \"api-token: #{api_token}\" -H \"Content-Type: application/json\" -d '#{user_json}' https://localhost/apis/iam/v2/users" + only_if { test_result } + sensitive true + end +end + +action :update do + name = new_resource.name + user_hash = new_resource.user_hash + user_json = user_hash.to_json + api_token = new_resource.api_token + # Try to fetch user from server + srv_user = get_iam_user(user_hash['id'], api_token) + # Test user from server and desired user match key by key from desired policy + test_result = if srv_user['error'] + raise srv_user['error'].inspect + else + user_hash['id'].eql?(srv_user['user']['id']) && user_hash['name'].eql?(srv_user['user']['name']) + end + execute "update local user #{name}" do + command "curl -X PUT --insecure -s -H \"api-token: #{api_token}\" -H \"Content-Type: application/json\" -d '#{user_json}' https://localhost/apis/iam/v2/users/#{user_hash['id']}" + not_if { test_result } + sensitive true + end +end diff --git a/test/integration/default/default_test.rb b/test/integration/default/default_test.rb index b954290..d739df4 100644 --- a/test/integration/default/default_test.rb +++ b/test/integration/default/default_test.rb @@ -1,13 +1,11 @@ -# # encoding: utf-8 +# InSpec test for recipe chef_automate -# Inspec test for recipe chef_software::default - -# The Inspec reference, with examples and extensive documentation, can be -# found at http://inspec.io/docs/reference/resources/ +# The InSpec reference, with examples and extensive documentation, can be +# found at https://www.inspec.io/docs/reference/resources/ # Listening port -%w(80 443).each do |port| - describe port(port) do +%w(80 443).each do |port_num| + describe port(port_num) do it { should be_listening } end end