hybrid_platforms_conductor 32.13.4 → 32.16.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +60 -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 +111 -0
  7. data/lib/hybrid_platforms_conductor/cmd_runner.rb +13 -1
  8. data/lib/hybrid_platforms_conductor/connector.rb +4 -2
  9. data/lib/hybrid_platforms_conductor/deployer.rb +2 -1
  10. data/lib/hybrid_platforms_conductor/hpc_plugins/connector/local.rb +1 -1
  11. data/lib/hybrid_platforms_conductor/hpc_plugins/platform_handler/serverless_chef.rb +535 -0
  12. data/lib/hybrid_platforms_conductor/hpc_plugins/platform_handler/serverless_chef/dsl_parser.rb +51 -0
  13. data/lib/hybrid_platforms_conductor/hpc_plugins/platform_handler/serverless_chef/recipes_tree_builder.rb +232 -0
  14. data/lib/hybrid_platforms_conductor/hpc_plugins/provisioner/podman.rb +1 -1
  15. data/lib/hybrid_platforms_conductor/nodes_handler.rb +9 -5
  16. data/lib/hybrid_platforms_conductor/version.rb +1 -1
  17. data/spec/hybrid_platforms_conductor_test.rb +3 -0
  18. data/spec/hybrid_platforms_conductor_test/api/cmd_runner_spec.rb +7 -0
  19. data/spec/hybrid_platforms_conductor_test/api/deployer/provisioner_spec.rb +23 -0
  20. data/spec/hybrid_platforms_conductor_test/api/nodes_handler/cmdbs_plugins_api_spec.rb +11 -0
  21. data/spec/hybrid_platforms_conductor_test/api/platform_handlers/serverless_chef/config_dsl_spec.rb +17 -0
  22. data/spec/hybrid_platforms_conductor_test/api/platform_handlers/serverless_chef/deploy_output_parsing_spec.rb +94 -0
  23. data/spec/hybrid_platforms_conductor_test/api/platform_handlers/serverless_chef/diff_impacts_spec.rb +317 -0
  24. data/spec/hybrid_platforms_conductor_test/api/platform_handlers/serverless_chef/inventory_spec.rb +65 -0
  25. data/spec/hybrid_platforms_conductor_test/api/platform_handlers/serverless_chef/packaging_spec.rb +293 -0
  26. data/spec/hybrid_platforms_conductor_test/api/platform_handlers/serverless_chef/services_deployment_spec.rb +272 -0
  27. data/spec/hybrid_platforms_conductor_test/helpers/cmd_runner_helpers.rb +1 -1
  28. data/spec/hybrid_platforms_conductor_test/helpers/serverless_chef_helpers.rb +53 -0
  29. data/spec/hybrid_platforms_conductor_test/serverless_chef_repositories/1_node/chef_versions.yml +3 -0
  30. data/spec/hybrid_platforms_conductor_test/serverless_chef_repositories/1_node/nodes/node.json +14 -0
  31. data/spec/hybrid_platforms_conductor_test/serverless_chef_repositories/1_node/policyfiles/test_policy.rb +3 -0
  32. data/spec/hybrid_platforms_conductor_test/serverless_chef_repositories/data_bags/chef_versions.yml +3 -0
  33. data/spec/hybrid_platforms_conductor_test/serverless_chef_repositories/data_bags/data_bags/my_bag/my_item.json +4 -0
  34. data/spec/hybrid_platforms_conductor_test/serverless_chef_repositories/data_bags/nodes/node.json +14 -0
  35. data/spec/hybrid_platforms_conductor_test/serverless_chef_repositories/data_bags/policyfiles/test_policy.rb +3 -0
  36. data/spec/hybrid_platforms_conductor_test/serverless_chef_repositories/hpc_test/chef_versions.yml +3 -0
  37. data/spec/hybrid_platforms_conductor_test/serverless_chef_repositories/hpc_test/cookbooks/hpc_test/recipes/after_run.rb +1 -0
  38. data/spec/hybrid_platforms_conductor_test/serverless_chef_repositories/hpc_test/cookbooks/hpc_test/recipes/before_run.rb +1 -0
  39. data/spec/hybrid_platforms_conductor_test/serverless_chef_repositories/hpc_test/nodes/node.json +10 -0
  40. data/spec/hybrid_platforms_conductor_test/serverless_chef_repositories/hpc_test/policyfiles/test_policy.rb +3 -0
  41. data/spec/hybrid_platforms_conductor_test/serverless_chef_repositories/recipes/cookbooks/test_cookbook_1/recipes/default.rb +1 -0
  42. data/spec/hybrid_platforms_conductor_test/serverless_chef_repositories/recipes/cookbooks/test_cookbook_2/libraries/default.rb +4 -0
  43. data/spec/hybrid_platforms_conductor_test/serverless_chef_repositories/recipes/cookbooks/test_cookbook_2/recipes/default.rb +1 -0
  44. data/spec/hybrid_platforms_conductor_test/serverless_chef_repositories/recipes/cookbooks/test_cookbook_2/recipes/other_recipe.rb +1 -0
  45. data/spec/hybrid_platforms_conductor_test/serverless_chef_repositories/recipes/cookbooks/test_cookbook_2/resources/my_resource.rb +1 -0
  46. data/spec/hybrid_platforms_conductor_test/serverless_chef_repositories/recipes/nodes/node1.json +10 -0
  47. data/spec/hybrid_platforms_conductor_test/serverless_chef_repositories/recipes/nodes/node2.json +10 -0
  48. data/spec/hybrid_platforms_conductor_test/serverless_chef_repositories/recipes/policyfiles/test_policy_1.rb +4 -0
  49. data/spec/hybrid_platforms_conductor_test/serverless_chef_repositories/recipes/policyfiles/test_policy_2.rb +4 -0
  50. data/spec/hybrid_platforms_conductor_test/serverless_chef_repositories/several_cookbooks/config.rb +1 -0
  51. data/spec/hybrid_platforms_conductor_test/serverless_chef_repositories/several_cookbooks/cookbooks/test_cookbook_1/recipes/default.rb +1 -0
  52. data/spec/hybrid_platforms_conductor_test/serverless_chef_repositories/several_cookbooks/nodes/node1.json +10 -0
  53. data/spec/hybrid_platforms_conductor_test/serverless_chef_repositories/several_cookbooks/nodes/node2.json +10 -0
  54. data/spec/hybrid_platforms_conductor_test/serverless_chef_repositories/several_cookbooks/other_cookbooks/test_cookbook_2/libraries/default.rb +4 -0
  55. data/spec/hybrid_platforms_conductor_test/serverless_chef_repositories/several_cookbooks/other_cookbooks/test_cookbook_2/recipes/default.rb +1 -0
  56. data/spec/hybrid_platforms_conductor_test/serverless_chef_repositories/several_cookbooks/other_cookbooks/test_cookbook_2/recipes/other_recipe.rb +1 -0
  57. data/spec/hybrid_platforms_conductor_test/serverless_chef_repositories/several_cookbooks/other_cookbooks/test_cookbook_2/resources/my_resource.rb +1 -0
  58. data/spec/hybrid_platforms_conductor_test/serverless_chef_repositories/several_cookbooks/policyfiles/test_policy_1.rb +4 -0
  59. data/spec/hybrid_platforms_conductor_test/serverless_chef_repositories/several_cookbooks/policyfiles/test_policy_2.rb +4 -0
  60. data/spec/hybrid_platforms_conductor_test/serverless_chef_repositories/several_nodes/chef_versions.yml +3 -0
  61. data/spec/hybrid_platforms_conductor_test/serverless_chef_repositories/several_nodes/nodes/local.json +10 -0
  62. data/spec/hybrid_platforms_conductor_test/serverless_chef_repositories/several_nodes/nodes/node1.json +10 -0
  63. data/spec/hybrid_platforms_conductor_test/serverless_chef_repositories/several_nodes/nodes/node2.json +10 -0
  64. data/spec/hybrid_platforms_conductor_test/serverless_chef_repositories/several_nodes/policyfiles/test_policy_1.rb +3 -0
  65. data/spec/hybrid_platforms_conductor_test/serverless_chef_repositories/several_nodes/policyfiles/test_policy_2.rb +3 -0
  66. metadata +192 -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: f78c78a80235a940f793af89344e6a972609f92d79ddce8acc6735b0e6b54ceb
4
+ data.tar.gz: 9c0bfef24e47646c61dd79899af40f33f063f90e73a626ed9c2617c7f7933593
5
5
  SHA512:
6
- metadata.gz: c62d9ea4f8b68df98961b0cbf4e48f1ecfe3ab2b48bc30474f972c93d8affc40022f71a1226d804e3045acb19ebac4b46d2eb07ced1c87ba0513e1085a8d0ed9
7
- data.tar.gz: 7b62f76dcdd766d4232c1e40ef6f1209d337b75378912f563d835c805c20b595063aaae9126c672e2674fca692927c262ada8157318d846ae397c799b977508a
6
+ metadata.gz: 60a24969416d8d674cad078642e575c1b22e3b4457dcabb5a436bf62a597d13388bf042851008d5a3b01a7de82ee6003c6856b90821adddc1436bf2a0ecbfdd1
7
+ data.tar.gz: 83cdf6e30fefad4d1d97cb1e1af84278e71aff6eadac4fe5d27232f70d6537a35a48d0e30494213a616796177fd1e6aa36f26bc6ac86d313b651ca2023ade1a0
data/CHANGELOG.md CHANGED
@@ -1,3 +1,63 @@
1
+ # [v32.16.2](https://github.com/sweet-delights/hybrid-platforms-conductor/compare/v32.16.1...v32.16.2) (2021-06-01 09:52:22)
2
+
3
+ ## Global changes
4
+ ### Patches
5
+
6
+ * [[Hotfix(platform_handler_serverless_chef)] Fix Chef Workstation bash steps when run by root](https://github.com/sweet-delights/hybrid-platforms-conductor/commit/1d99d1faa9cf66cd607eacc00205e312fd589715)
7
+
8
+ ## Changes for platform_handler_serverless_chef
9
+ ### Patches
10
+
11
+ * [[Hotfix(platform_handler_serverless_chef)] Fix Chef Workstation bash steps when run by root](https://github.com/sweet-delights/hybrid-platforms-conductor/commit/1d99d1faa9cf66cd607eacc00205e312fd589715)
12
+
13
+ # [v32.16.1](https://github.com/sweet-delights/hybrid-platforms-conductor/compare/v32.16.0...v32.16.1) (2021-06-01 09:21:47)
14
+
15
+ ## Global changes
16
+ ### Patches
17
+
18
+ * [[Hotfix(platform_handler_serverless_chef)] Fix Chef Workstation installation step](https://github.com/sweet-delights/hybrid-platforms-conductor/commit/58320d5394fec3bca9e1d0ffaf06ade42f1fd160)
19
+
20
+ ## Changes for platform_handler_serverless_chef
21
+ ### Patches
22
+
23
+ * [[Hotfix(platform_handler_serverless_chef)] Fix Chef Workstation installation step](https://github.com/sweet-delights/hybrid-platforms-conductor/commit/58320d5394fec3bca9e1d0ffaf06ade42f1fd160)
24
+
25
+ # [v32.16.0](https://github.com/sweet-delights/hybrid-platforms-conductor/compare/v32.15.0...v32.16.0) (2021-05-31 17:55:49)
26
+
27
+ ## Global changes
28
+ ### Patches
29
+
30
+ * [[Feature(cmd_runner)] Add option to force bash usage in run_cmd](https://github.com/sweet-delights/hybrid-platforms-conductor/commit/0711f1f33fc8f282b925726e0631ae78bf1337b5)
31
+
32
+ ## Changes for cmd_runner
33
+ ### Features
34
+
35
+ * [[Feature(cmd_runner)] Add option to force bash usage in run_cmd](https://github.com/sweet-delights/hybrid-platforms-conductor/commit/0711f1f33fc8f282b925726e0631ae78bf1337b5)
36
+
37
+ # [v32.15.0](https://github.com/sweet-delights/hybrid-platforms-conductor/compare/v32.14.0...v32.15.0) (2021-05-31 14:43:32)
38
+
39
+ ## Global changes
40
+ ### Patches
41
+
42
+ * [[Feature(platform_handler_serverless_chef)] Use user-defined cookbook hpc_test to tune chef-client runs in test environments](https://github.com/sweet-delights/hybrid-platforms-conductor/commit/2cfa010997b562a599308ceaaf62a8740ff51468)
43
+
44
+ ## Changes for platform_handler_serverless_chef
45
+ ### Features
46
+
47
+ * [[Feature(platform_handler_serverless_chef)] Use user-defined cookbook hpc_test to tune chef-client runs in test environments](https://github.com/sweet-delights/hybrid-platforms-conductor/commit/2cfa010997b562a599308ceaaf62a8740ff51468)
48
+
49
+ # [v32.14.0](https://github.com/sweet-delights/hybrid-platforms-conductor/compare/v32.13.4...v32.14.0) (2021-05-31 09:05:45)
50
+
51
+ ## Global changes
52
+ ### Patches
53
+
54
+ * [[Feature(platform_handler_serverless_chef)] [#58] Add the serverless_chef platform handler](https://github.com/sweet-delights/hybrid-platforms-conductor/commit/6f84757096a802c79a18a6c9c1440b27e73decd1)
55
+
56
+ ## Changes for platform_handler_serverless_chef
57
+ ### Features
58
+
59
+ * [[Feature(platform_handler_serverless_chef)] [#58] Add the serverless_chef platform handler](https://github.com/sweet-delights/hybrid-platforms-conductor/commit/6f84757096a802c79a18a6c9c1440b27e73decd1)
60
+
1
61
  # [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
62
 
3
63
  ## 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,111 @@
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
+ ## Test-wrapping cookbook `hpc_test`
68
+
69
+ If you have a cookbook named `hpc_test` in your Chef repository, then this plugin will wrap any run-list run in a local environment mode (used in tests) with the recipes `hpc_test::before_run` and `hpc_test::after_run`.
70
+
71
+ This way you can implement such recipes to adapt your Chef client runs to your test environment (like lack of connectivity, failing resources to be ignored...).
72
+
73
+ ## Config DSL extension
74
+
75
+ ### `helpers_including_recipes`
76
+
77
+ The `helpers_including_recipes` DSL helps understanding dependencies between cookbooks in your Chef repository.
78
+ 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.
79
+
80
+ This helper defines which recipes are being used by a given library helper.
81
+ 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.
82
+ 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.
83
+
84
+ The helper takes a Hash as parameters: for each helper name, it gives a list of used recipes.
85
+
86
+ For example:
87
+ ```ruby
88
+ helpers_including_recipes(
89
+ my_configs: %w[my_cookbook::configs]
90
+ )
91
+ ```
92
+
93
+ ## Used credentials
94
+
95
+ | Credential | Usage
96
+ | --- | --- |
97
+
98
+ ## Used Metadata
99
+
100
+ | Metadata | Type | Usage
101
+ | --- | --- | --- |
102
+ | `use_local_chef` | `Boolean` | If set to true, then run chef-client locally instead of deploying on a remote node |
103
+
104
+ ## Used environment variables
105
+
106
+ | Variable | Usage
107
+ | --- | --- |
108
+
109
+ ## External tools dependencies
110
+
111
+ * `curl`: Used to install Chef Workstation.
@@ -1,4 +1,5 @@
1
1
  require 'logger'
2
+ require 'tempfile'
2
3
  require 'tty-command'
3
4
  require 'hybrid_platforms_conductor/logger_helpers'
4
5
  require 'hybrid_platforms_conductor/io_router'
@@ -66,6 +67,7 @@ module HybridPlatformsConductor
66
67
  # * *timeout*: The command ended in timeout
67
68
  # * *timeout* (Integer or nil): Timeout to apply for the command to be run, or nil for no timeout [default: nil]
68
69
  # * *no_exception* (Boolean): If true, don't throw exception in case of error [default: false]
70
+ # * *force_bash* (Boolean): If true, then make sure command is invoked with bash instead of sh [default: false]
69
71
  # Result::
70
72
  # * Integer or Symbol: Exit status of the command, or Symbol in case of error. In case of dry-run mode the expected code is returned without executing anything.
71
73
  # * String: Standard output of the command
@@ -78,7 +80,8 @@ module HybridPlatformsConductor
78
80
  log_stderr_to_io: nil,
79
81
  expected_code: 0,
80
82
  timeout: nil,
81
- no_exception: false
83
+ no_exception: false,
84
+ force_bash: false
82
85
  )
83
86
  expected_code = [expected_code] unless expected_code.is_a?(Array)
84
87
  if @dry_run
@@ -101,6 +104,14 @@ module HybridPlatformsConductor
101
104
  nil
102
105
  end
103
106
  start_time = Time.now if log_debug?
107
+ bash_file = nil
108
+ if force_bash
109
+ bash_file = Tempfile.new('hpc_bash')
110
+ bash_file.write(cmd)
111
+ bash_file.chmod 0700
112
+ bash_file.close
113
+ cmd = "/bin/bash -c #{bash_file.path}"
114
+ end
104
115
  begin
105
116
  # Make sure we keep a trace of stdout and stderr, even if it was not asked, just to use it in case of exceptions raised
106
117
  cmd_result_stdout = ''
@@ -141,6 +152,7 @@ module HybridPlatformsConductor
141
152
  cmd_stderr = "#{cmd_result_stderr.empty? ? '' : "#{cmd_result_stderr}\n"}#{$!}\n#{$!.backtrace.join("\n")}"
142
153
  ensure
143
154
  file_output.close unless file_output.nil?
155
+ bash_file.unlink unless bash_file.nil?
144
156
  end
145
157
  if log_debug?
146
158
  elapsed = Time.now - start_time
@@ -65,17 +65,19 @@ module HybridPlatformsConductor
65
65
  #
66
66
  # Parameters::
67
67
  # * *cmd* (String): The command to be run
68
+ # * *force_bash* (Boolean): If true, then make sure command is invoked with bash instead of sh [default: false]
68
69
  # Result::
69
70
  # * Integer: Exit code
70
71
  # * String: Standard output
71
72
  # * String: Error output
72
- def run_cmd(cmd)
73
+ def run_cmd(cmd, force_bash: false)
73
74
  @cmd_runner.run_cmd(
74
75
  cmd,
75
76
  timeout: @timeout,
76
77
  log_to_stdout: false,
77
78
  log_stdout_to_io: @stdout_io,
78
- log_stderr_to_io: @stderr_io
79
+ log_stderr_to_io: @stderr_io,
80
+ force_bash: force_bash
79
81
  )
80
82
  end
81
83
 
@@ -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
  {
@@ -37,7 +37,7 @@ module HybridPlatformsConductor
37
37
  # Parameters::
38
38
  # * *bash_cmds* (String): Bash commands to execute
39
39
  def remote_bash(bash_cmds)
40
- run_cmd "cd #{workspace_for(@node)} ; #{bash_cmds}"
40
+ run_cmd "cd #{workspace_for(@node)} ; #{bash_cmds}", force_bash: true
41
41
  end
42
42
 
43
43
  # Execute an interactive shell on the remote node
@@ -0,0 +1,535 @@
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
+ # Constructor
51
+ #
52
+ # Parameters::
53
+ # * *platform_type* (Symbol): Platform type
54
+ # * *repository_path* (String): Repository path
55
+ # * *logger* (Logger): Logger to be used [default: Logger.new(STDOUT)]
56
+ # * *logger_stderr* (Logger): Logger to be used for stderr [default: Logger.new(STDERR)]
57
+ # * *config* (Config): Config to be used. [default: Config.new]
58
+ # * *cmd_runner* (CmdRunner): Command executor to be used. [default: CmdRunner.new]
59
+ def initialize(
60
+ platform_type,
61
+ repository_path,
62
+ logger: Logger.new(STDOUT),
63
+ logger_stderr: Logger.new(STDERR),
64
+ config: Config.new,
65
+ cmd_runner: CmdRunner.new
66
+ )
67
+ super
68
+ # Mutex for getting the full recipes tree
69
+ @recipes_tree_mutex = Mutex.new
70
+ end
71
+
72
+ # Setup the platform, install dependencies...
73
+ # [API] - This method is optional.
74
+ # [API] - @cmd_runner is accessible.
75
+ def setup
76
+ required_version = YAML.load_file("#{@repository_path}/chef_versions.yml")['workstation']
77
+ Bundler.with_unbundled_env do
78
+ exit_status, stdout, _stderr = @cmd_runner.run_cmd '/opt/chef-workstation/bin/chef --version', expected_code: [0, :command_error]
79
+ existing_version =
80
+ if exit_status == :command_error
81
+ 'not installed'
82
+ else
83
+ expected_match = stdout.match(/^Chef Workstation version: (.+)\.\d+$/)
84
+ expected_match.nil? ? 'unreadable' : expected_match[1]
85
+ end
86
+ log_debug "Current Chef version: #{existing_version}. Required version: #{required_version}"
87
+ @cmd_runner.run_cmd "curl -L https://omnitruck.chef.io/install.sh | #{@cmd_runner.root? ? '' : 'sudo '}bash -s -- -P chef-workstation -v #{required_version}" unless existing_version == required_version
88
+ end
89
+ end
90
+
91
+ # Get the list of known nodes.
92
+ # [API] - This method is mandatory.
93
+ #
94
+ # Result::
95
+ # * Array<String>: List of node names
96
+ def known_nodes
97
+ Dir.glob("#{@repository_path}/nodes/*.json").map { |file| File.basename(file, '.json') }
98
+ end
99
+
100
+ # Get the metadata of a given node.
101
+ # [API] - This method is mandatory.
102
+ #
103
+ # Parameters::
104
+ # * *node* (String): Node to read metadata from
105
+ # Result::
106
+ # * Hash<Symbol,Object>: The corresponding metadata
107
+ def metadata_for(node)
108
+ (json_for(node)['normal'] || {}).transform_keys(&:to_sym)
109
+ end
110
+
111
+ # Return the services for a given node
112
+ # [API] - This method is mandatory.
113
+ #
114
+ # Parameters::
115
+ # * *node* (String): node to read configuration from
116
+ # Result::
117
+ # * Array<String>: The corresponding services
118
+ def services_for(node)
119
+ [json_for(node)['policy_name']]
120
+ end
121
+
122
+ # Get the list of services we can deploy
123
+ # [API] - This method is mandatory.
124
+ #
125
+ # Result::
126
+ # * Array<String>: The corresponding services
127
+ def deployable_services
128
+ Dir.glob("#{@repository_path}/policyfiles/*.rb").map { |file| File.basename(file, '.rb') }
129
+ end
130
+
131
+ # Package the repository, ready to be deployed on artefacts or directly to a node.
132
+ # [API] - This method is optional.
133
+ # [API] - @cmd_runner is accessible.
134
+ # [API] - @actions_executor is accessible.
135
+ #
136
+ # Parameters::
137
+ # * *services* (Hash< String, Array<String> >): Services to be deployed, per node
138
+ # * *secrets* (Hash): Secrets to be used for deployment
139
+ # * *local_environment* (Boolean): Are we deploying to a local environment?
140
+ def package(services:, secrets:, local_environment:)
141
+ # Make a stamp of the info that has been packaged, so that we don't package it again if useless
142
+ package_info = {
143
+ secrets: secrets,
144
+ commit: info[:commit].nil? ? Time.now.utc.strftime('%F %T') : info[:commit][:id],
145
+ other_files:
146
+ if info[:status].nil?
147
+ {}
148
+ else
149
+ Hash[
150
+ (info[:status][:added_files] + info[:status][:changed_files] + info[:status][:untracked_files]).
151
+ sort.
152
+ map { |f| [f, File.mtime("#{@repository_path}/#{f}").strftime('%F %T')] }
153
+ ]
154
+ end,
155
+ deleted_files: info[:status].nil? ? [] : info[:status][:deleted_files].sort
156
+ }
157
+ # Each service is packaged individually.
158
+ services.values.flatten.sort.uniq.each do |service|
159
+ package_dir = "dist/#{local_environment ? 'local' : 'prod'}/#{service}"
160
+ package_info_file = "#{@repository_path}/#{package_dir}/hpc_package.info"
161
+ current_package_info = File.exist?(package_info_file) ? JSON.parse(File.read(package_info_file)).transform_keys(&:to_sym) : {}
162
+ unless current_package_info == package_info
163
+ Bundler.with_unbundled_env do
164
+ policy_file = "policyfiles/#{service}.rb"
165
+ if local_environment
166
+ local_policy_file = "policyfiles/#{service}.local.rb"
167
+ # In local mode, we always regenerate the lock file as we may modify the run list
168
+ run_list = known_cookbook_paths.any? { |cookbook_path| File.exist?("#{@repository_path}/#{cookbook_path}/hpc_test/recipes/before_run.rb") } ? ['hpc_test::before_run'] : []
169
+ dsl_parser = DslParser.new
170
+ dsl_parser.parse("#{@repository_path}/#{policy_file}")
171
+ run_list.concat dsl_parser.calls.find { |call_info| call_info[:method] == :run_list }[:args].flatten
172
+ run_list << 'hpc_test::after_run' if known_cookbook_paths.any? { |cookbook_path| File.exist?("#{@repository_path}/#{cookbook_path}/hpc_test/recipes/after_run.rb") }
173
+ File.write("#{@repository_path}/#{local_policy_file}", File.read("#{@repository_path}/#{policy_file}") + "\nrun_list #{run_list.map { |recipe| "'#{recipe}'" }.join(', ')}\n")
174
+ policy_file = local_policy_file
175
+ end
176
+ lock_file = "#{File.dirname(policy_file)}/#{File.basename(policy_file, '.rb')}.lock.json"
177
+ # If the policy lock file does not exist, generate it
178
+ @cmd_runner.run_cmd "cd #{@repository_path} && /opt/chef-workstation/bin/chef install #{policy_file}" unless File.exist?("#{@repository_path}/#{lock_file}")
179
+ extra_cp_data_bags = File.exist?("#{@repository_path}/data_bags") ? " && cp -ar data_bags/ #{package_dir}/" : ''
180
+ @cmd_runner.run_cmd "cd #{@repository_path} && \
181
+ #{@cmd_runner.root? ? '' : 'sudo '}rm -rf #{package_dir} && \
182
+ /opt/chef-workstation/bin/chef export #{policy_file} #{package_dir}#{extra_cp_data_bags}"
183
+ end
184
+ unless @cmd_runner.dry_run
185
+ # Create secrets file
186
+ secrets_file = "#{@repository_path}/#{package_dir}/data_bags/hpc_secrets/hpc_secrets.json"
187
+ FileUtils.mkdir_p(File.dirname(secrets_file))
188
+ File.write(secrets_file, secrets.merge(id: 'hpc_secrets').to_json)
189
+ # Remember the package info
190
+ File.write(package_info_file, package_info.to_json)
191
+ end
192
+ end
193
+ end
194
+ end
195
+
196
+ # Prepare deployments.
197
+ # This method is called just before getting and executing the actions to be deployed.
198
+ # It is called once per platform.
199
+ # [API] - This method is optional.
200
+ # [API] - @cmd_runner is accessible.
201
+ # [API] - @actions_executor is accessible.
202
+ #
203
+ # Parameters::
204
+ # * *services* (Hash< String, Array<String> >): Services to be deployed, per node
205
+ # * *secrets* (Hash): Secrets to be used for deployment
206
+ # * *local_environment* (Boolean): Are we deploying to a local environment?
207
+ # * *why_run* (Boolean): Are we deploying in why-run mode?
208
+ def prepare_for_deploy(services:, secrets:, local_environment:, why_run:)
209
+ @local_env = local_environment
210
+ end
211
+
212
+ # Get the list of actions to perform to deploy on a given node.
213
+ # Those actions can be executed in parallel with other deployments on other nodes. They must be thread safe.
214
+ # [API] - This method is mandatory.
215
+ # [API] - @cmd_runner is accessible.
216
+ # [API] - @actions_executor is accessible.
217
+ #
218
+ # Parameters::
219
+ # * *node* (String): Node to deploy on
220
+ # * *service* (String): Service to be deployed
221
+ # * *use_why_run* (Boolean): Do we use a why-run mode? [default = true]
222
+ # Result::
223
+ # * Array< Hash<Symbol,Object> >: List of actions to be done
224
+ def actions_to_deploy_on(node, service, use_why_run: true)
225
+ package_dir = "#{@repository_path}/dist/#{@local_env ? 'local' : 'prod'}/#{service}"
226
+ # Generate the nodes attributes file
227
+ unless @cmd_runner.dry_run
228
+ FileUtils.mkdir_p "#{package_dir}/nodes"
229
+ File.write("#{package_dir}/nodes/#{node}.json", (known_nodes.include?(node) ? metadata_for(node) : {}).merge(@nodes_handler.metadata_of(node)).to_json)
230
+ end
231
+ client_options = [
232
+ '--local-mode',
233
+ '--chef-license', 'accept',
234
+ '--json-attributes', "nodes/#{node}.json"
235
+ ]
236
+ client_options << '--why-run' if use_why_run
237
+ if @nodes_handler.get_use_local_chef_of(node)
238
+ # Just run the chef-client directly from the packaged repository
239
+ [{ bash: "cd #{package_dir} && #{@cmd_runner.root? ? '' : 'sudo '}SSL_CERT_DIR=/etc/ssl/certs /opt/chef-workstation/bin/chef-client #{client_options.join(' ')}" }]
240
+ else
241
+ # Upload the package and run it from the node
242
+ package_name = File.basename(package_dir)
243
+ chef_versions_file = "#{@repository_path}/chef_versions.yml"
244
+ raise "Missing file #{chef_versions_file} specifying the Chef Infra Client version to be deployed" unless File.exist?(chef_versions_file)
245
+ required_chef_client_version = YAML.load_file(chef_versions_file)['client']
246
+ sudo = (@actions_executor.connector(:ssh).ssh_user == 'root' ? '' : "#{@nodes_handler.sudo_on(node)} ")
247
+ [
248
+ {
249
+ # Install dependencies
250
+ remote_bash: [
251
+ 'set -e',
252
+ 'set -o pipefail',
253
+ "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",
254
+ 'mkdir -p ./hpc_deploy',
255
+ 'rm -rf ./hpc_deploy/tmp',
256
+ 'mkdir -p ./hpc_deploy/tmp',
257
+ 'curl --location https://omnitruck.chef.io/install.sh --output ./hpc_deploy/install.sh',
258
+ 'chmod a+x ./hpc_deploy/install.sh',
259
+ "#{sudo}TMPDIR=./hpc_deploy/tmp ./hpc_deploy/install.sh -d /opt/artefacts -v #{required_chef_client_version} -s once"
260
+ ]
261
+ },
262
+ {
263
+ scp: { package_dir => './hpc_deploy' },
264
+ remote_bash: [
265
+ 'set -e',
266
+ "cd ./hpc_deploy/#{package_name}",
267
+ "#{sudo}SSL_CERT_DIR=/etc/ssl/certs /opt/chef/bin/chef-client #{client_options.join(' ')}",
268
+ 'cd ..'
269
+ ] + (log_debug? ? [] : ["#{sudo}rm -rf ./hpc_deploy/#{package_name}"])
270
+ }
271
+ ]
272
+ end
273
+ end
274
+
275
+ # Parse stdout and stderr of a given deploy run and get the list of tasks with their status
276
+ # [API] - This method is mandatory.
277
+ #
278
+ # Parameters::
279
+ # * *stdout* (String): stdout to be parsed
280
+ # * *stderr* (String): stderr to be parsed
281
+ # Result::
282
+ # * Array< Hash<Symbol,Object> >: List of task properties. The following properties should be returned, among free ones:
283
+ # * *name* (String): Task name
284
+ # * *status* (Symbol): Task status. Should be one of:
285
+ # * *:changed*: The task has been changed
286
+ # * *:identical*: The task has not been changed
287
+ # * *diffs* (String): Differences, if any
288
+ def parse_deploy_output(stdout, stderr)
289
+ tasks = []
290
+ current_task = nil
291
+ stdout.split("\n").each do |line|
292
+ # Remove control chars and spaces around
293
+ case line.gsub(/\e\[[^\x40-\x7E]*[\x40-\x7E]/, '').strip
294
+ when /^\* (\w+\[[^\]]+\]) action (.+)$/
295
+ # New task
296
+ task_name = $1
297
+ task_action = $2
298
+ current_task = {
299
+ name: task_name,
300
+ action: task_action,
301
+ status: :identical
302
+ }
303
+ tasks << current_task
304
+ when /^- (.+)$/
305
+ # Diff on the current task
306
+ diff_description = $1
307
+ unless current_task.nil?
308
+ current_task[:diffs] = '' unless current_task.key?(:diffs)
309
+ current_task[:diffs] << "#{diff_description}\n"
310
+ current_task[:status] = :changed
311
+ end
312
+ end
313
+ end
314
+ tasks
315
+ end
316
+
317
+ # Get the list of impacted nodes and services from a files diff.
318
+ # [API] - This method is optional
319
+ #
320
+ # Parameters::
321
+ # * *files_diffs* (Hash< String, Hash< Symbol, Object > >): List of diffs info, per file name having a diff. Diffs info have the following properties:
322
+ # * *moved_to* (String): The new file path, in case it has been moved [optional]
323
+ # * *diff* (String): The diff content
324
+ # Result::
325
+ # * Array<String>: The list of nodes impacted by this diff
326
+ # * Array<String>: The list of services impacted by this diff
327
+ # * Boolean: Are there some files that have a global impact (meaning all nodes are potentially impacted by this diff)?
328
+ def impacts_from(files_diffs)
329
+ impacted_nodes = []
330
+ impacted_services = []
331
+ # List of impacted [cookbook, recipe]
332
+ # Array< [Symbol, Symbol] >
333
+ impacted_recipes = []
334
+ impacted_global = false
335
+ files_diffs.keys.sort.each do |impacted_file|
336
+ if impacted_file =~ /^policyfiles\/([^\/]+)\.rb$/
337
+ log_debug "[#{impacted_file}] - Impacted service: #{$1}"
338
+ impacted_services << $1
339
+ elsif impacted_file =~ /^policyfiles\/([^\/]+)\.lock.json$/
340
+ log_debug "[#{impacted_file}] - Impacted service: #{$1}"
341
+ impacted_services << $1
342
+ elsif impacted_file =~ /^nodes\/([^\/]+)\.json/
343
+ log_debug "[#{impacted_file}] - Impacted node: #{$1}"
344
+ impacted_nodes << $1
345
+ else
346
+ cookbook_path = known_cookbook_paths.find { |cookbooks_path| impacted_file =~ /^#{Regexp.escape(cookbooks_path)}\/.+$/ }
347
+ if cookbook_path.nil?
348
+ # Global file
349
+ log_debug "[#{impacted_file}] - Global file impacted"
350
+ impacted_global = true
351
+ else
352
+ # File belonging to a cookbook
353
+ cookbook_name, file_path = impacted_file.match(/^#{cookbook_path}\/(\w+)\/(.+)$/)[1..2]
354
+ cookbook = cookbook_name.to_sym
355
+ # Small helper to register a recipe
356
+ register = proc do |source, recipe_name, cookbook_name: cookbook|
357
+ cookbook_name = cookbook_name.to_sym if cookbook_name.is_a?(String)
358
+ log_debug "[#{impacted_file}] - Impacted recipe from #{source}: #{cookbook_name}::#{recipe_name}"
359
+ impacted_recipes << [cookbook_name, recipe_name.to_sym]
360
+ end
361
+ case file_path
362
+ when /recipes\/(.+)\.rb/
363
+ register.call('direct', $1)
364
+ when /attributes\/.+\.rb/, 'metadata.rb'
365
+ # Consider all recipes are impacted
366
+ Dir.glob("#{@repository_path}/#{cookbook_path}/#{cookbook}/recipes/*.rb") do |recipe_path|
367
+ register.call('attributes', File.basename(recipe_path, '.rb'))
368
+ end
369
+ when /(templates|files)\/(.+)/
370
+ # Find recipes using this file name
371
+ included_file = File.basename($2)
372
+ template_regexp = /["']#{Regexp.escape(included_file)}["']/
373
+ Dir.glob("#{@repository_path}/#{cookbook_path}/#{cookbook}/recipes/*.rb") do |recipe_path|
374
+ register.call("included file #{included_file}", File.basename(recipe_path, '.rb')) if File.read(recipe_path) =~ template_regexp
375
+ end
376
+ when /resources\/(.+)/
377
+ # Find any recipe using this resource
378
+ included_resource = "#{cookbook}_#{File.basename($1, '.rb')}"
379
+ resource_regexp = /(\W|^)#{Regexp.escape(included_resource)}(\W|$)/
380
+ known_cookbook_paths.each do |cookbooks_path|
381
+ Dir.glob("#{@repository_path}/#{cookbooks_path}/**/recipes/*.rb") do |recipe_path|
382
+ if File.read(recipe_path) =~ resource_regexp
383
+ cookbook_name, recipe_name = recipe_path.match(/#{cookbooks_path}\/(\w+)\/recipes\/(\w+)\.rb/)[1..2]
384
+ register.call("included resource #{included_resource}", recipe_name, cookbook_name: cookbook_name)
385
+ end
386
+ end
387
+ end
388
+ when /libraries\/(.+)/
389
+ # Find any recipe using methods from this library
390
+ 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|$)/ }
391
+ known_cookbook_paths.each do |cookbooks_path|
392
+ Dir.glob("#{@repository_path}/#{cookbooks_path}/**/recipes/*.rb") do |recipe_path|
393
+ file_content = File.read(recipe_path)
394
+ found_lib_regexp = lib_methods_regexps.find { |regexp| file_content =~ regexp }
395
+ unless found_lib_regexp.nil?
396
+ cookbook_name, recipe_name = recipe_path.match(/#{cookbooks_path}\/(\w+)\/recipes\/(\w+)\.rb/)[1..2]
397
+ register.call("included library helper #{found_lib_regexp.source[6..-7]}", recipe_name, cookbook_name: cookbook_name)
398
+ end
399
+ end
400
+ end
401
+ when 'README.md', 'README.rdoc', 'CHANGELOG.md', '.rubocop.yml'
402
+ # Ignore them
403
+ else
404
+ log_warn "[#{impacted_file}] - Unknown impact for cookbook file belonging to #{cookbook}"
405
+ # Consider all recipes are impacted by default
406
+ Dir.glob("#{@repository_path}/#{cookbook_path}/#{cookbook}/recipes/*.rb") do |recipe_path|
407
+ register.call('attributes', File.basename(recipe_path, '.rb'))
408
+ end
409
+ end
410
+ end
411
+ end
412
+ end
413
+
414
+ # Devise the impacted services from the impacted recipes we just found.
415
+ impacted_recipes.uniq!
416
+ log_debug "* #{impacted_recipes.size} impacted recipes:\n#{impacted_recipes.map { |(cookbook, recipe)| "#{cookbook}::#{recipe}" }.sort.join("\n")}"
417
+
418
+ recipes_tree = full_recipes_tree
419
+ [
420
+ impacted_nodes,
421
+ (
422
+ impacted_services +
423
+ # Gather the list of services using the impacted recipes
424
+ impacted_recipes.map do |(cookbook, recipe)|
425
+ recipe_info = recipes_tree.dig cookbook, recipe
426
+ recipe_info.nil? ? [] : recipe_info[:used_by_policies]
427
+ end.flatten
428
+ ).sort.uniq,
429
+ impacted_global
430
+ ]
431
+ end
432
+
433
+ # Return the list of possible cookbook paths from this repository only.
434
+ # Returned paths are relative to the repository path.
435
+ #
436
+ # Result::
437
+ # * Array<String>: Known cookbook paths
438
+ def known_cookbook_paths
439
+ # Keep a cache of it for performance.
440
+ unless defined?(@cookbook_paths)
441
+ config_file = "#{@repository_path}/config.rb"
442
+ @cookbook_paths = (
443
+ ['cookbooks'] +
444
+ if File.exist?(config_file)
445
+ # Read the knife configuration to get cookbook paths
446
+ dsl_parser = DslParser.new
447
+ dsl_parser.parse(config_file)
448
+ cookbook_path_call = dsl_parser.calls.find { |call_info| call_info[:method] == :cookbook_path }
449
+ cookbook_path_call.nil? ? [] : cookbook_path_call[:args].first
450
+ else
451
+ []
452
+ end
453
+ ).
454
+ map do |dir|
455
+ # Only keep dirs that actually exist and are part of our repository
456
+ full_path = dir.start_with?('/') ? dir : File.expand_path("#{@repository_path}/#{dir}")
457
+ full_path.start_with?(@repository_path) && File.exist?(full_path) ? full_path.gsub("#{@repository_path}/", '') : nil
458
+ end.
459
+ compact.
460
+ sort.
461
+ uniq
462
+ end
463
+ @cookbook_paths
464
+ end
465
+
466
+ # Get the run list of a given policy
467
+ #
468
+ # Parameters::
469
+ # * *policy* (String): Policy to get the run list from
470
+ # Result::
471
+ # * Array<[String or nil, Symbol, Symbol]>: Run list of the given policy, as [cookbook_dir, cookbook, recipe]
472
+ def policy_run_list(policy)
473
+ # Read the policy file
474
+ dsl_parser = DslParser.new
475
+ policy_file = "#{@repository_path}/policyfiles/#{policy}.rb"
476
+ dsl_parser.parse(policy_file)
477
+ run_list_call = dsl_parser.calls.find { |call_info| call_info[:method] == :run_list }
478
+ raise "Policy #{policy} has no run list defined in #{policy_file}" if run_list_call.nil?
479
+ run_list_call[:args].map { |recipe_def| decode_recipe(recipe_def) }
480
+ end
481
+
482
+ # Return the cookbook directory, cookbook name and recipe name from which a recipe definition is found.
483
+ # The following forms are handled:
484
+ # * cookbook
485
+ # * cookbook::recipe
486
+ # * recipe[cookbook]
487
+ # * recipe[cookbook::recipe]
488
+ #
489
+ # Parameters::
490
+ # * *recipe_def* (String): Recipe definition (cookbook or cookbook::recipe).
491
+ # Result::
492
+ # * String: The cookbook directory, or nil if unknown
493
+ # * Symbol: The cookbook name
494
+ # * Symbol: The recipe name
495
+ def decode_recipe(recipe_def)
496
+ recipe_def = $1 if recipe_def =~ /^recipe\[(.+)\]$/
497
+ cookbook, recipe = recipe_def.split('::').map(&:to_sym)
498
+ recipe = :default if recipe.nil?
499
+ # Find the cookbook it belongs to
500
+ cookbook_dir = known_cookbook_paths.find { |cookbook_path| File.exist?("#{@repository_path}/#{cookbook_path}/#{cookbook}") }
501
+ raise "Unknown recipe #{cookbook}::#{recipe} from cookbook #{@repository_path}/#{cookbook_dir}/#{cookbook}." if !cookbook_dir.nil? && !File.exist?("#{@repository_path}/#{cookbook_dir}/#{cookbook}/recipes/#{recipe}.rb")
502
+ return cookbook_dir, cookbook, recipe
503
+ end
504
+
505
+ private
506
+
507
+ # Return the JSON associated to a node
508
+ #
509
+ # Parameters::
510
+ # * *node* (String): The node to search for
511
+ # Result::
512
+ # * Hash: JSON object of this node
513
+ def json_for(node)
514
+ JSON.parse(File.read("#{@repository_path}/nodes/#{node}.json"))
515
+ end
516
+
517
+ # Get the full recipes tree.
518
+ # Keep it in a cache for performance.
519
+ #
520
+ # Result::
521
+ # * Hash: The recipes tree. See RecipesTreeBuilder#full_recipes_tree for the detailed signature
522
+ def full_recipes_tree
523
+ @recipes_tree_mutex.synchronize do
524
+ @recipes_tree = RecipesTreeBuilder.new(@config, self).full_recipes_tree unless defined?(@recipes_tree)
525
+ end
526
+ @recipes_tree
527
+ end
528
+
529
+ end
530
+
531
+ end
532
+
533
+ end
534
+
535
+ end