hybrid_platforms_conductor 32.13.4 → 32.14.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +12 -0
  3. data/bin/get_impacted_nodes +1 -1
  4. data/bin/setup +6 -1
  5. data/docs/plugins.md +1 -0
  6. data/docs/plugins/platform_handler/serverless_chef.md +105 -0
  7. data/lib/hybrid_platforms_conductor/deployer.rb +2 -1
  8. data/lib/hybrid_platforms_conductor/hpc_plugins/platform_handler/serverless_chef.rb +440 -0
  9. data/lib/hybrid_platforms_conductor/hpc_plugins/platform_handler/serverless_chef/dsl_parser.rb +51 -0
  10. data/lib/hybrid_platforms_conductor/hpc_plugins/platform_handler/serverless_chef/recipes_tree_builder.rb +271 -0
  11. data/lib/hybrid_platforms_conductor/nodes_handler.rb +9 -5
  12. data/lib/hybrid_platforms_conductor/version.rb +1 -1
  13. data/spec/hybrid_platforms_conductor_test.rb +3 -0
  14. data/spec/hybrid_platforms_conductor_test/api/deployer/provisioner_spec.rb +23 -0
  15. data/spec/hybrid_platforms_conductor_test/api/nodes_handler/cmdbs_plugins_api_spec.rb +11 -0
  16. data/spec/hybrid_platforms_conductor_test/api/platform_handlers/serverless_chef/config_dsl_spec.rb +17 -0
  17. data/spec/hybrid_platforms_conductor_test/api/platform_handlers/serverless_chef/deploy_output_parsing_spec.rb +94 -0
  18. data/spec/hybrid_platforms_conductor_test/api/platform_handlers/serverless_chef/diff_impacts_spec.rb +317 -0
  19. data/spec/hybrid_platforms_conductor_test/api/platform_handlers/serverless_chef/inventory_spec.rb +65 -0
  20. data/spec/hybrid_platforms_conductor_test/api/platform_handlers/serverless_chef/packaging_spec.rb +213 -0
  21. data/spec/hybrid_platforms_conductor_test/api/platform_handlers/serverless_chef/services_deployment_spec.rb +268 -0
  22. data/spec/hybrid_platforms_conductor_test/helpers/serverless_chef_helpers.rb +53 -0
  23. data/spec/hybrid_platforms_conductor_test/serverless_chef_repositories/1_node/chef_versions.yml +3 -0
  24. data/spec/hybrid_platforms_conductor_test/serverless_chef_repositories/1_node/nodes/node.json +14 -0
  25. data/spec/hybrid_platforms_conductor_test/serverless_chef_repositories/1_node/policyfiles/test_policy.rb +3 -0
  26. data/spec/hybrid_platforms_conductor_test/serverless_chef_repositories/data_bags/chef_versions.yml +3 -0
  27. data/spec/hybrid_platforms_conductor_test/serverless_chef_repositories/data_bags/data_bags/my_bag/my_item.json +4 -0
  28. data/spec/hybrid_platforms_conductor_test/serverless_chef_repositories/data_bags/nodes/node.json +14 -0
  29. data/spec/hybrid_platforms_conductor_test/serverless_chef_repositories/data_bags/policyfiles/test_policy.rb +3 -0
  30. data/spec/hybrid_platforms_conductor_test/serverless_chef_repositories/recipes/cookbooks/test_cookbook_1/recipes/default.rb +1 -0
  31. data/spec/hybrid_platforms_conductor_test/serverless_chef_repositories/recipes/cookbooks/test_cookbook_2/libraries/default.rb +4 -0
  32. data/spec/hybrid_platforms_conductor_test/serverless_chef_repositories/recipes/cookbooks/test_cookbook_2/recipes/default.rb +1 -0
  33. data/spec/hybrid_platforms_conductor_test/serverless_chef_repositories/recipes/cookbooks/test_cookbook_2/recipes/other_recipe.rb +1 -0
  34. data/spec/hybrid_platforms_conductor_test/serverless_chef_repositories/recipes/cookbooks/test_cookbook_2/resources/my_resource.rb +1 -0
  35. data/spec/hybrid_platforms_conductor_test/serverless_chef_repositories/recipes/nodes/node1.json +10 -0
  36. data/spec/hybrid_platforms_conductor_test/serverless_chef_repositories/recipes/nodes/node2.json +10 -0
  37. data/spec/hybrid_platforms_conductor_test/serverless_chef_repositories/recipes/policyfiles/test_policy_1.rb +4 -0
  38. data/spec/hybrid_platforms_conductor_test/serverless_chef_repositories/recipes/policyfiles/test_policy_2.rb +4 -0
  39. data/spec/hybrid_platforms_conductor_test/serverless_chef_repositories/several_cookbooks/config.rb +1 -0
  40. data/spec/hybrid_platforms_conductor_test/serverless_chef_repositories/several_cookbooks/cookbooks/test_cookbook_1/recipes/default.rb +1 -0
  41. data/spec/hybrid_platforms_conductor_test/serverless_chef_repositories/several_cookbooks/nodes/node1.json +10 -0
  42. data/spec/hybrid_platforms_conductor_test/serverless_chef_repositories/several_cookbooks/nodes/node2.json +10 -0
  43. data/spec/hybrid_platforms_conductor_test/serverless_chef_repositories/several_cookbooks/other_cookbooks/test_cookbook_2/libraries/default.rb +4 -0
  44. data/spec/hybrid_platforms_conductor_test/serverless_chef_repositories/several_cookbooks/other_cookbooks/test_cookbook_2/recipes/default.rb +1 -0
  45. data/spec/hybrid_platforms_conductor_test/serverless_chef_repositories/several_cookbooks/other_cookbooks/test_cookbook_2/recipes/other_recipe.rb +1 -0
  46. data/spec/hybrid_platforms_conductor_test/serverless_chef_repositories/several_cookbooks/other_cookbooks/test_cookbook_2/resources/my_resource.rb +1 -0
  47. data/spec/hybrid_platforms_conductor_test/serverless_chef_repositories/several_cookbooks/policyfiles/test_policy_1.rb +4 -0
  48. data/spec/hybrid_platforms_conductor_test/serverless_chef_repositories/several_cookbooks/policyfiles/test_policy_2.rb +4 -0
  49. data/spec/hybrid_platforms_conductor_test/serverless_chef_repositories/several_nodes/chef_versions.yml +3 -0
  50. data/spec/hybrid_platforms_conductor_test/serverless_chef_repositories/several_nodes/nodes/local.json +10 -0
  51. data/spec/hybrid_platforms_conductor_test/serverless_chef_repositories/several_nodes/nodes/node1.json +10 -0
  52. data/spec/hybrid_platforms_conductor_test/serverless_chef_repositories/several_nodes/nodes/node2.json +10 -0
  53. data/spec/hybrid_platforms_conductor_test/serverless_chef_repositories/several_nodes/policyfiles/test_policy_1.rb +3 -0
  54. data/spec/hybrid_platforms_conductor_test/serverless_chef_repositories/several_nodes/policyfiles/test_policy_2.rb +3 -0
  55. metadata +187 -143
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2e55b2875d12648408fdd837b525b0f96728211c114062b6de22aefc59baec16
4
- data.tar.gz: cbe6360d9b3944604c4505ea90de60ee2dfe4bd1f6d51d7a287b43ad5e3f0ee4
3
+ metadata.gz: 99c918a62cd924ceaca95073e944df866b9b1ecce771dcb710122ec501cadf95
4
+ data.tar.gz: 959326a99c32ac8eb120af28c8b3c34304e2efa61df64f43a99545d0d33f88e3
5
5
  SHA512:
6
- metadata.gz: c62d9ea4f8b68df98961b0cbf4e48f1ecfe3ab2b48bc30474f972c93d8affc40022f71a1226d804e3045acb19ebac4b46d2eb07ced1c87ba0513e1085a8d0ed9
7
- data.tar.gz: 7b62f76dcdd766d4232c1e40ef6f1209d337b75378912f563d835c805c20b595063aaae9126c672e2674fca692927c262ada8157318d846ae397c799b977508a
6
+ metadata.gz: 8e6c3a54a5c04f21aaeeba1a047366a7f427680077712742c0b2a7977cecfe14c4058fdd1e1c6c66842b603a7f7590bfea6eae0d975240e19a4ffa8214043c17
7
+ data.tar.gz: e8dd3ef50cec1a0dd7e94161159a30f359fca736bd6e7ab14ab916e2d743d48889bf891c3ca2bf626d3f087e1472ff43eb1abc0370e3fe63226cb0a8595950ad
data/CHANGELOG.md CHANGED
@@ -1,3 +1,15 @@
1
+ # [v32.14.0](https://github.com/sweet-delights/hybrid-platforms-conductor/compare/v32.13.4...v32.14.0) (2021-05-31 09:05:45)
2
+
3
+ ## Global changes
4
+ ### Patches
5
+
6
+ * [[Feature(platform_handler_serverless_chef)] [#58] Add the serverless_chef platform handler](https://github.com/sweet-delights/hybrid-platforms-conductor/commit/6f84757096a802c79a18a6c9c1440b27e73decd1)
7
+
8
+ ## Changes for platform_handler_serverless_chef
9
+ ### Features
10
+
11
+ * [[Feature(platform_handler_serverless_chef)] [#58] Add the serverless_chef platform handler](https://github.com/sweet-delights/hybrid-platforms-conductor/commit/6f84757096a802c79a18a6c9c1440b27e73decd1)
12
+
1
13
  # [v32.13.4](https://github.com/sweet-delights/hybrid-platforms-conductor/compare/v32.13.3...v32.13.4) (2021-05-11 14:00:47)
2
14
 
3
15
  ## Global changes
@@ -10,7 +10,7 @@ executable = HybridPlatformsConductor::Executable.new(nodes_selection_options: f
10
10
  opts.on('-f', '--from-commit COMMIT_ID', "Specify the GIT commit from which we look for diffs. Defaults to #{git_from}.") do |commit_id|
11
11
  git_from = commit_id
12
12
  end
13
- opts.on('-p', '--platform PLATFORM_NAME', "Specify the repository on which to perform the diff. Possible values are #{platforms_handler.known_platforms.join(', ')}") do |platform_name|
13
+ opts.on('-p', '--platform PLATFORM_NAME', "Specify the repository on which to perform the diff. Possible values are #{platforms_handler.known_platforms.map(&:name).join(', ')}") do |platform_name|
14
14
  platform = platform_name
15
15
  end
16
16
  opts.on('-s', '--smallest-test-sample', 'Display the minimal set of nodes to check that would validate all modifications.') do
data/bin/setup CHANGED
@@ -7,5 +7,10 @@ platforms_handler = executable.platforms_handler
7
7
  executable.parse_options!
8
8
 
9
9
  platforms_handler.known_platforms.each do |platform|
10
- platform.setup if platform.respond_to?(:setup)
10
+ if platform.respond_to?(:setup)
11
+ executable.out "===== Setup platform #{platform.name}..."
12
+ platform.setup
13
+ executable.out "===== Platform #{platform.name} setup successfully."
14
+ executable.out ''
15
+ end
11
16
  end
data/docs/plugins.md CHANGED
@@ -107,6 +107,7 @@ Examples of platform handlers are:
107
107
  Check the [sample plugin file](../lib/hybrid_platforms_conductor/hpc_plugins/platform_handler/platform_handler_plugin.rb.sample) to know more about the API that needs to be implemented by such plugins.
108
108
 
109
109
  Plugins shipped by default:
110
+ * [`serverless_chef`](plugins/platform_handler/serverless_chef.md)
110
111
  * [`yaml_inventory`](plugins/platform_handler/yaml_inventory.md)
111
112
 
112
113
  <a name="provisioner"></a>
@@ -0,0 +1,105 @@
1
+ # PlatformHandler plugin: `serverless_chef`
2
+
3
+ The `serverless_chef` platform handler is supporting a [Chef repository](https://docs.chef.io/chef_repo/), and deploying services from this repository with using a Chef Infra Server. It uses a client-only deployment process.
4
+
5
+ The Chef repository concepts supported by this plugin are:
6
+ * nodes,
7
+ * policies,
8
+ * data bags,
9
+ * cookbooks,
10
+ * knife configuration,
11
+ * node attributes,
12
+ * policy attributes.
13
+
14
+ The Chef repository concepts not supported by this plugin are:
15
+ * roles,
16
+ * environments.
17
+
18
+ ## Requirements
19
+
20
+ The platform repository has to contain a file named `chef_versions.yml` that will define the required Chef components' versions fro this Chef repository.
21
+
22
+ Here is the structure of this Yaml file:
23
+ * **`workstation`** (*String*): Version of the [Chef Workstation](https://downloads.chef.io/tools/workstation) to be installed locally during the [setup](/docs/executables/setup.md) phase.
24
+ * **`client`** (*String*): Version of the [Chef Infra Client](https://docs.chef.io/chef_client_overview/) to be installed on nodes that will be deployed.
25
+
26
+ Versions can be of the form `major.minor.patch` or only `major.minor` to benefit automatically from latest patch versions.
27
+
28
+ Example of `chef_versions.yml`:
29
+ ```yaml
30
+ ---
31
+ workstation: '21.5'
32
+ client: '17.0'
33
+ ```
34
+
35
+ ## Inventory
36
+
37
+ Inventory is read directly from the `nodes/*.json` files that are present in a Chef repository.
38
+
39
+ Nodes are expected to use [policies](https://docs.chef.io/policy/) to know which service is to be deployed on a node.
40
+
41
+ Metadata is taken from the normal attributes defined in the node's json file.
42
+
43
+ Example of node json:
44
+ ```json
45
+ {
46
+ "name": "test-node",
47
+ "normal": {
48
+ "description": "Single test node",
49
+ "image": "debian_9",
50
+ "private_ips": ["172.16.0.1"],
51
+ "metadata_property": "metadata_value"
52
+ },
53
+ "policy_name": "service_name",
54
+ "policy_group": "test_group"
55
+ }
56
+ ```
57
+
58
+ ## Services
59
+
60
+ Services available from a Chef repository are parsed from the [policy files](https://docs.chef.io/policyfile/) stored in `policyfiles/*.rb`.
61
+ 1 policy file is 1 service.
62
+
63
+ Services are being deployed by packaging the policy using [`chef install`](https://docs.chef.io/workstation/ctl_chef/#chef-install), [`chef export`](https://docs.chef.io/workstation/ctl_chef/#chef-export), which ensures packaging processes optimal and independent from 1 policy to another.
64
+
65
+ Then packaged services are uploaded on the node to configured and deployment is done remotely using [Chef Infra Client in local mode](https://docs.chef.io/ctl_chef_client/#run-in-local-mode) on the remote node.
66
+
67
+ ## Config DSL extension
68
+
69
+ ### `helpers_including_recipes`
70
+
71
+ The `helpers_including_recipes` DSL helps understanding dependencies between cookbooks in your Chef repository.
72
+ This is used only by the Hybrid Platform Conductor processes that compute which services are being impacted by some git diffs. For example the [`get_impacted_nodes` executable](/docs/executables/get_impacted_nodes.md), and is completely optional.
73
+
74
+ This helper defines which recipes are being used by a given library helper.
75
+ For example if you define a helper named `my_configs` in a library that will use internally a recipe named `my_cookbook::configs` you will need to declare this dependency using the `helpers_including_recipes` for Hybrid Platforms Conductor to know about this dependency.
76
+ This way, if another recipe (let's say `my_cookbook::production`) uses this helper and a git diff reports a modification in the `my_cookbook::configs` recipe, every node using `my_cookbook::production` will be marked as impacted by such a git diff.
77
+
78
+ The helper takes a Hash as parameters: for each helper name, it gives a list of used recipes.
79
+
80
+ For example:
81
+ ```ruby
82
+ helpers_including_recipes(
83
+ my_configs: %w[my_cookbook::configs]
84
+ )
85
+ ```
86
+
87
+ ## Used credentials
88
+
89
+ | Credential | Usage
90
+ | --- | --- |
91
+
92
+ ## Used Metadata
93
+
94
+ | Metadata | Type | Usage
95
+ | --- | --- | --- |
96
+ | `use_local_chef` | `Boolean` | If set to true, then run chef-client locally instead of deploying on a remote node |
97
+
98
+ ## Used environment variables
99
+
100
+ | Variable | Usage
101
+ | --- | --- |
102
+
103
+ ## External tools dependencies
104
+
105
+ * `curl`: Used to install Chef Workstation.
@@ -318,8 +318,9 @@ module HybridPlatformsConductor
318
318
  actions_executor: @actions_executor
319
319
  )
320
320
  instance.with_running_instance(stop_on_exit: true, destroy_on_exit: !reuse_instance, port: 22) do
321
- # Test-provisioned nodes have SSH Session Exec capabilities
321
+ # Test-provisioned nodes have SSH Session Exec capabilities and are not local
322
322
  sub_executable.nodes_handler.override_metadata_of node, :ssh_session_exec, 'true'
323
+ sub_executable.nodes_handler.override_metadata_of node, :local_node, false
323
324
  # Test-provisioned nodes use default sudo
324
325
  sub_executable.config.sudo_procs.replace(sub_executable.config.sudo_procs.map do |sudo_proc_info|
325
326
  {
@@ -0,0 +1,440 @@
1
+ require 'fileutils'
2
+ require 'json'
3
+ require 'yaml'
4
+ require 'hybrid_platforms_conductor/platform_handler'
5
+ require 'hybrid_platforms_conductor/hpc_plugins/platform_handler/serverless_chef/dsl_parser'
6
+ require 'hybrid_platforms_conductor/hpc_plugins/platform_handler/serverless_chef/recipes_tree_builder'
7
+
8
+ module HybridPlatformsConductor
9
+
10
+ module HpcPlugins
11
+
12
+ module PlatformHandler
13
+
14
+ # Handle a Chef repository without using a Chef Infra Server.
15
+ # Inventory is read from nodes/*.json.
16
+ # Services are defined from policy files in policyfiles/*.rb.
17
+ # Roles are not supported as they are considered made obsolete with the usage of policies by the Chef community.
18
+ # Required Chef versions are taken from a chef_versions.yml file containing the following keys:
19
+ # * *workstation* (String): The Chef Workstation version to be installed during setup (can be specified as major.minor only)
20
+ # * *client* (String): The Chef Infra Client version to be installed during nodes deployment (can be specified as major.minor only)
21
+ class ServerlessChef < HybridPlatformsConductor::PlatformHandler
22
+
23
+ # Add a Mixin to the DSL parsing the platforms configuration file.
24
+ # This can be used by any plugin to add plugin-specific configuration getters and setters, accessible later from NodesHandler instances.
25
+ # An optional initializer can also be given.
26
+ # [API] - Those calls are optional
27
+ module MyDSLExtension
28
+
29
+ # The list of library helpers we know include some recipes.
30
+ # This is used when parsing some recipe code: if such a helper is encountered then we assume a dependency on a given recipe.
31
+ # Hash< Symbol, Array<String> >: List of recipes definitions per helper name.
32
+ attr_reader :known_helpers_including_recipes
33
+
34
+ # Initialize the DSL
35
+ def init_serverless_chef
36
+ @known_helpers_including_recipes = {}
37
+ end
38
+
39
+ # Define helpers including recipes
40
+ #
41
+ # Parameters::
42
+ # * *included_recipes* (Hash< Symbol, Array<String> >): List of recipes definitions per helper name.
43
+ def helpers_including_recipes(included_recipes)
44
+ @known_helpers_including_recipes.merge!(included_recipes)
45
+ end
46
+
47
+ end
48
+ self.extend_config_dsl_with MyDSLExtension, :init_serverless_chef
49
+
50
+ # Setup the platform, install dependencies...
51
+ # [API] - This method is optional.
52
+ # [API] - @cmd_runner is accessible.
53
+ def setup
54
+ required_version = YAML.load_file("#{@repository_path}/chef_versions.yml")['workstation']
55
+ Bundler.with_unbundled_env do
56
+ exit_status, stdout, _stderr = @cmd_runner.run_cmd '/opt/chef-workstation/bin/chef --version', expected_code: [0, 127]
57
+ existing_version =
58
+ if exit_status == 127
59
+ 'not installed'
60
+ else
61
+ expected_match = stdout.match(/^Chef Workstation version: (.+)\.\d+$/)
62
+ expected_match.nil? ? 'unreadable' : expected_match[1]
63
+ end
64
+ log_debug "Current Chef version: #{existing_version}. Required version: #{required_version}"
65
+ @cmd_runner.run_cmd "curl -L https://omnitruck.chef.io/install.sh | sudo bash -s -- -P chef-workstation -v #{required_version}" unless existing_version == required_version
66
+ end
67
+ end
68
+
69
+ # Get the list of known nodes.
70
+ # [API] - This method is mandatory.
71
+ #
72
+ # Result::
73
+ # * Array<String>: List of node names
74
+ def known_nodes
75
+ Dir.glob("#{@repository_path}/nodes/*.json").map { |file| File.basename(file, '.json') }
76
+ end
77
+
78
+ # Get the metadata of a given node.
79
+ # [API] - This method is mandatory.
80
+ #
81
+ # Parameters::
82
+ # * *node* (String): Node to read metadata from
83
+ # Result::
84
+ # * Hash<Symbol,Object>: The corresponding metadata
85
+ def metadata_for(node)
86
+ (json_for(node)['normal'] || {}).transform_keys(&:to_sym)
87
+ end
88
+
89
+ # Return the services for a given node
90
+ # [API] - This method is mandatory.
91
+ #
92
+ # Parameters::
93
+ # * *node* (String): node to read configuration from
94
+ # Result::
95
+ # * Array<String>: The corresponding services
96
+ def services_for(node)
97
+ [json_for(node)['policy_name']]
98
+ end
99
+
100
+ # Get the list of services we can deploy
101
+ # [API] - This method is mandatory.
102
+ #
103
+ # Result::
104
+ # * Array<String>: The corresponding services
105
+ def deployable_services
106
+ Dir.glob("#{@repository_path}/policyfiles/*.rb").map { |file| File.basename(file, '.rb') }
107
+ end
108
+
109
+ # Package the repository, ready to be deployed on artefacts or directly to a node.
110
+ # [API] - This method is optional.
111
+ # [API] - @cmd_runner is accessible.
112
+ # [API] - @actions_executor is accessible.
113
+ #
114
+ # Parameters::
115
+ # * *services* (Hash< String, Array<String> >): Services to be deployed, per node
116
+ # * *secrets* (Hash): Secrets to be used for deployment
117
+ # * *local_environment* (Boolean): Are we deploying to a local environment?
118
+ def package(services:, secrets:, local_environment:)
119
+ # Make a stamp of the info that has been packaged, so that we don't package it again if useless
120
+ package_info = {
121
+ secrets: secrets,
122
+ commit: info[:commit].nil? ? Time.now.utc.strftime('%F %T') : info[:commit][:id],
123
+ other_files:
124
+ if info[:status].nil?
125
+ {}
126
+ else
127
+ Hash[
128
+ (info[:status][:added_files] + info[:status][:changed_files] + info[:status][:untracked_files]).
129
+ sort.
130
+ map { |f| [f, File.mtime("#{@repository_path}/#{f}").strftime('%F %T')] }
131
+ ]
132
+ end,
133
+ deleted_files: info[:status].nil? ? [] : info[:status][:deleted_files].sort
134
+ }
135
+ # Each service is packaged individually.
136
+ services.values.flatten.sort.uniq.each do |service|
137
+ package_dir = "dist/#{local_environment ? 'local' : 'prod'}/#{service}"
138
+ package_info_file = "#{@repository_path}/#{package_dir}/hpc_package.info"
139
+ current_package_info = File.exist?(package_info_file) ? JSON.parse(File.read(package_info_file)).transform_keys(&:to_sym) : {}
140
+ unless current_package_info == package_info
141
+ Bundler.with_unbundled_env do
142
+ # If the policy lock file does not exist, generate it
143
+ @cmd_runner.run_cmd "cd #{@repository_path} && /opt/chef-workstation/bin/chef install policyfiles/#{service}.rb" unless File.exist?("#{@repository_path}/policyfiles/#{service}.lock.json")
144
+ extra_cp_data_bags = File.exist?("#{@repository_path}/data_bags") ? " && cp -ar data_bags/ #{package_dir}/" : ''
145
+ @cmd_runner.run_cmd "cd #{@repository_path} && \
146
+ sudo rm -rf #{package_dir} && \
147
+ /opt/chef-workstation/bin/chef export policyfiles/#{service}.rb #{package_dir}#{extra_cp_data_bags}"
148
+ end
149
+ unless @cmd_runner.dry_run
150
+ # Create secrets file
151
+ secrets_file = "#{@repository_path}/#{package_dir}/data_bags/hpc_secrets/hpc_secrets.json"
152
+ FileUtils.mkdir_p(File.dirname(secrets_file))
153
+ File.write(secrets_file, secrets.merge(id: 'hpc_secrets').to_json)
154
+ # Remember the package info
155
+ File.write(package_info_file, package_info.to_json)
156
+ end
157
+ end
158
+ end
159
+ end
160
+
161
+ # Prepare deployments.
162
+ # This method is called just before getting and executing the actions to be deployed.
163
+ # It is called once per platform.
164
+ # [API] - This method is optional.
165
+ # [API] - @cmd_runner is accessible.
166
+ # [API] - @actions_executor is accessible.
167
+ #
168
+ # Parameters::
169
+ # * *services* (Hash< String, Array<String> >): Services to be deployed, per node
170
+ # * *secrets* (Hash): Secrets to be used for deployment
171
+ # * *local_environment* (Boolean): Are we deploying to a local environment?
172
+ # * *why_run* (Boolean): Are we deploying in why-run mode?
173
+ def prepare_for_deploy(services:, secrets:, local_environment:, why_run:)
174
+ @local_env = local_environment
175
+ end
176
+
177
+ # Get the list of actions to perform to deploy on a given node.
178
+ # Those actions can be executed in parallel with other deployments on other nodes. They must be thread safe.
179
+ # [API] - This method is mandatory.
180
+ # [API] - @cmd_runner is accessible.
181
+ # [API] - @actions_executor is accessible.
182
+ #
183
+ # Parameters::
184
+ # * *node* (String): Node to deploy on
185
+ # * *service* (String): Service to be deployed
186
+ # * *use_why_run* (Boolean): Do we use a why-run mode? [default = true]
187
+ # Result::
188
+ # * Array< Hash<Symbol,Object> >: List of actions to be done
189
+ def actions_to_deploy_on(node, service, use_why_run: true)
190
+ package_dir = "#{@repository_path}/dist/#{@local_env ? 'local' : 'prod'}/#{service}"
191
+ # Generate the nodes attributes file
192
+ unless @cmd_runner.dry_run
193
+ FileUtils.mkdir_p "#{package_dir}/nodes"
194
+ File.write("#{package_dir}/nodes/#{node}.json", (known_nodes.include?(node) ? metadata_for(node) : {}).merge(@nodes_handler.metadata_of(node)).to_json)
195
+ end
196
+ if @nodes_handler.get_use_local_chef_of(node)
197
+ # Just run the chef-client directly from the packaged repository
198
+ [{ bash: "cd #{package_dir} && sudo SSL_CERT_DIR=/etc/ssl/certs /opt/chef-workstation/bin/chef-client --local-mode --json-attributes nodes/#{node}.json#{use_why_run ? ' --why-run' : ''}" }]
199
+ else
200
+ # Upload the package and run it from the node
201
+ package_name = File.basename(package_dir)
202
+ chef_versions_file = "#{@repository_path}/chef_versions.yml"
203
+ raise "Missing file #{chef_versions_file} specifying the Chef Infra Client version to be deployed" unless File.exist?(chef_versions_file)
204
+ required_chef_client_version = YAML.load_file(chef_versions_file)['client']
205
+ sudo = (@actions_executor.connector(:ssh).ssh_user == 'root' ? '' : "#{@nodes_handler.sudo_on(node)} ")
206
+ [
207
+ {
208
+ # Install dependencies
209
+ remote_bash: [
210
+ 'set -e',
211
+ 'set -o pipefail',
212
+ "if [ -n \"$(command -v apt)\" ]; then #{sudo}apt update && #{sudo}apt install -y curl build-essential ; else #{sudo}yum groupinstall 'Development Tools' && #{sudo}yum install -y curl ; fi",
213
+ 'mkdir -p ./hpc_deploy',
214
+ "curl --location https://omnitruck.chef.io/install.sh | tac | tac | #{sudo}bash -s -- -d /opt/artefacts -v #{required_chef_client_version} -s once"
215
+ ]
216
+ },
217
+ {
218
+ scp: { package_dir => './hpc_deploy' },
219
+ remote_bash: [
220
+ 'set -e',
221
+ "cd ./hpc_deploy/#{package_name}",
222
+ "#{sudo}SSL_CERT_DIR=/etc/ssl/certs /opt/chef/bin/chef-client --local-mode --chef-license=accept --json-attributes nodes/#{node}.json#{use_why_run ? ' --why-run' : ''}",
223
+ 'cd ..',
224
+ "#{sudo}rm -rf #{package_name}"
225
+ ]
226
+ }
227
+ ]
228
+ end
229
+ end
230
+
231
+ # Parse stdout and stderr of a given deploy run and get the list of tasks with their status
232
+ # [API] - This method is mandatory.
233
+ #
234
+ # Parameters::
235
+ # * *stdout* (String): stdout to be parsed
236
+ # * *stderr* (String): stderr to be parsed
237
+ # Result::
238
+ # * Array< Hash<Symbol,Object> >: List of task properties. The following properties should be returned, among free ones:
239
+ # * *name* (String): Task name
240
+ # * *status* (Symbol): Task status. Should be one of:
241
+ # * *:changed*: The task has been changed
242
+ # * *:identical*: The task has not been changed
243
+ # * *diffs* (String): Differences, if any
244
+ def parse_deploy_output(stdout, stderr)
245
+ tasks = []
246
+ current_task = nil
247
+ stdout.split("\n").each do |line|
248
+ # Remove control chars and spaces around
249
+ case line.gsub(/\e\[[^\x40-\x7E]*[\x40-\x7E]/, '').strip
250
+ when /^\* (\w+\[[^\]]+\]) action (.+)$/
251
+ # New task
252
+ task_name = $1
253
+ task_action = $2
254
+ current_task = {
255
+ name: task_name,
256
+ action: task_action,
257
+ status: :identical
258
+ }
259
+ tasks << current_task
260
+ when /^- (.+)$/
261
+ # Diff on the current task
262
+ diff_description = $1
263
+ unless current_task.nil?
264
+ current_task[:diffs] = '' unless current_task.key?(:diffs)
265
+ current_task[:diffs] << "#{diff_description}\n"
266
+ current_task[:status] = :changed
267
+ end
268
+ end
269
+ end
270
+ tasks
271
+ end
272
+
273
+ # Get the list of impacted nodes and services from a files diff.
274
+ # [API] - This method is optional
275
+ #
276
+ # Parameters::
277
+ # * *files_diffs* (Hash< String, Hash< Symbol, Object > >): List of diffs info, per file name having a diff. Diffs info have the following properties:
278
+ # * *moved_to* (String): The new file path, in case it has been moved [optional]
279
+ # * *diff* (String): The diff content
280
+ # Result::
281
+ # * Array<String>: The list of nodes impacted by this diff
282
+ # * Array<String>: The list of services impacted by this diff
283
+ # * Boolean: Are there some files that have a global impact (meaning all nodes are potentially impacted by this diff)?
284
+ def impacts_from(files_diffs)
285
+ impacted_nodes = []
286
+ impacted_services = []
287
+ # List of impacted [cookbook, recipe]
288
+ # Array< [Symbol, Symbol] >
289
+ impacted_recipes = []
290
+ impacted_global = false
291
+ files_diffs.keys.sort.each do |impacted_file|
292
+ if impacted_file =~ /^policyfiles\/([^\/]+)\.rb$/
293
+ log_debug "[#{impacted_file}] - Impacted service: #{$1}"
294
+ impacted_services << $1
295
+ elsif impacted_file =~ /^policyfiles\/([^\/]+)\.lock.json$/
296
+ log_debug "[#{impacted_file}] - Impacted service: #{$1}"
297
+ impacted_services << $1
298
+ elsif impacted_file =~ /^nodes\/([^\/]+)\.json/
299
+ log_debug "[#{impacted_file}] - Impacted node: #{$1}"
300
+ impacted_nodes << $1
301
+ else
302
+ cookbook_path = known_cookbook_paths.find { |cookbooks_path| impacted_file =~ /^#{Regexp.escape(cookbooks_path)}\/.+$/ }
303
+ if cookbook_path.nil?
304
+ # Global file
305
+ log_debug "[#{impacted_file}] - Global file impacted"
306
+ impacted_global = true
307
+ else
308
+ # File belonging to a cookbook
309
+ cookbook_name, file_path = impacted_file.match(/^#{cookbook_path}\/(\w+)\/(.+)$/)[1..2]
310
+ cookbook = cookbook_name.to_sym
311
+ # Small helper to register a recipe
312
+ register = proc do |source, recipe_name, cookbook_name: cookbook|
313
+ cookbook_name = cookbook_name.to_sym if cookbook_name.is_a?(String)
314
+ log_debug "[#{impacted_file}] - Impacted recipe from #{source}: #{cookbook_name}::#{recipe_name}"
315
+ impacted_recipes << [cookbook_name, recipe_name.to_sym]
316
+ end
317
+ case file_path
318
+ when /recipes\/(.+)\.rb/
319
+ register.call('direct', $1)
320
+ when /attributes\/.+\.rb/, 'metadata.rb'
321
+ # Consider all recipes are impacted
322
+ Dir.glob("#{@repository_path}/#{cookbook_path}/#{cookbook}/recipes/*.rb") do |recipe_path|
323
+ register.call('attributes', File.basename(recipe_path, '.rb'))
324
+ end
325
+ when /(templates|files)\/(.+)/
326
+ # Find recipes using this file name
327
+ included_file = File.basename($2)
328
+ template_regexp = /["']#{Regexp.escape(included_file)}["']/
329
+ Dir.glob("#{@repository_path}/#{cookbook_path}/#{cookbook}/recipes/*.rb") do |recipe_path|
330
+ register.call("included file #{included_file}", File.basename(recipe_path, '.rb')) if File.read(recipe_path) =~ template_regexp
331
+ end
332
+ when /resources\/(.+)/
333
+ # Find any recipe using this resource
334
+ included_resource = "#{cookbook}_#{File.basename($1, '.rb')}"
335
+ resource_regexp = /(\W|^)#{Regexp.escape(included_resource)}(\W|$)/
336
+ known_cookbook_paths.each do |cookbooks_path|
337
+ Dir.glob("#{@repository_path}/#{cookbooks_path}/**/recipes/*.rb") do |recipe_path|
338
+ if File.read(recipe_path) =~ resource_regexp
339
+ cookbook_name, recipe_name = recipe_path.match(/#{cookbooks_path}\/(\w+)\/recipes\/(\w+)\.rb/)[1..2]
340
+ register.call("included resource #{included_resource}", recipe_name, cookbook_name: cookbook_name)
341
+ end
342
+ end
343
+ end
344
+ when /libraries\/(.+)/
345
+ # Find any recipe using methods from this library
346
+ lib_methods_regexps = File.read("#{@repository_path}/#{impacted_file}").scan(/(\W|^)def\s+(\w+)(\W|$)/).map { |_grp1, method_name, _grp2| /(\W|^)#{Regexp.escape(method_name)}(\W|$)/ }
347
+ known_cookbook_paths.each do |cookbooks_path|
348
+ Dir.glob("#{@repository_path}/#{cookbooks_path}/**/recipes/*.rb") do |recipe_path|
349
+ file_content = File.read(recipe_path)
350
+ found_lib_regexp = lib_methods_regexps.find { |regexp| file_content =~ regexp }
351
+ unless found_lib_regexp.nil?
352
+ cookbook_name, recipe_name = recipe_path.match(/#{cookbooks_path}\/(\w+)\/recipes\/(\w+)\.rb/)[1..2]
353
+ register.call("included library helper #{found_lib_regexp.source[6..-7]}", recipe_name, cookbook_name: cookbook_name)
354
+ end
355
+ end
356
+ end
357
+ when 'README.md', 'README.rdoc', 'CHANGELOG.md', '.rubocop.yml'
358
+ # Ignore them
359
+ else
360
+ log_warn "[#{impacted_file}] - Unknown impact for cookbook file belonging to #{cookbook}"
361
+ # Consider all recipes are impacted by default
362
+ Dir.glob("#{@repository_path}/#{cookbook_path}/#{cookbook}/recipes/*.rb") do |recipe_path|
363
+ register.call('attributes', File.basename(recipe_path, '.rb'))
364
+ end
365
+ end
366
+ end
367
+ end
368
+ end
369
+
370
+ # Devise the impacted services from the impacted recipes we just found.
371
+ impacted_recipes.uniq!
372
+ log_debug "* #{impacted_recipes.size} impacted recipes:\n#{impacted_recipes.map { |(cookbook, recipe)| "#{cookbook}::#{recipe}" }.sort.join("\n")}"
373
+
374
+ recipes_tree = RecipesTreeBuilder.new(@config, self).full_recipes_tree
375
+ [
376
+ impacted_nodes,
377
+ (
378
+ impacted_services +
379
+ # Gather the list of services using the impacted recipes
380
+ impacted_recipes.map do |(cookbook, recipe)|
381
+ recipe_info = recipes_tree.dig cookbook, recipe
382
+ recipe_info.nil? ? [] : recipe_info[:used_by_policies]
383
+ end.flatten
384
+ ).sort.uniq,
385
+ impacted_global
386
+ ]
387
+ end
388
+
389
+ # Return the list of possible cookbook paths from this repository only.
390
+ # Returned paths are relative to the repository path.
391
+ #
392
+ # Result::
393
+ # * Array<String>: Known cookbook paths
394
+ def known_cookbook_paths
395
+ # Keep a cache of it for performance.
396
+ unless defined?(@cookbook_paths)
397
+ config_file = "#{@repository_path}/config.rb"
398
+ @cookbook_paths = (
399
+ ['cookbooks'] +
400
+ if File.exist?(config_file)
401
+ # Read the knife configuration to get cookbook paths
402
+ dsl_parser = DslParser.new
403
+ dsl_parser.parse(config_file)
404
+ cookbook_path_call = dsl_parser.calls.find { |call_info| call_info[:method] == :cookbook_path }
405
+ cookbook_path_call.nil? ? [] : cookbook_path_call[:args].first
406
+ else
407
+ []
408
+ end
409
+ ).
410
+ map do |dir|
411
+ # Only keep dirs that actually exist and are part of our repository
412
+ full_path = dir.start_with?('/') ? dir : File.expand_path("#{@repository_path}/#{dir}")
413
+ full_path.start_with?(@repository_path) && File.exist?(full_path) ? full_path.gsub("#{@repository_path}/", '') : nil
414
+ end.
415
+ compact.
416
+ sort.
417
+ uniq
418
+ end
419
+ @cookbook_paths
420
+ end
421
+
422
+ private
423
+
424
+ # Return the JSON associated to a node
425
+ #
426
+ # Parameters::
427
+ # * *node* (String): The node to search for
428
+ # Result::
429
+ # * Hash: JSON object of this node
430
+ def json_for(node)
431
+ JSON.parse(File.read("#{@repository_path}/nodes/#{node}.json"))
432
+ end
433
+
434
+ end
435
+
436
+ end
437
+
438
+ end
439
+
440
+ end