kitchen-cinc 1.0.0

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.
@@ -0,0 +1,348 @@
1
+ #
2
+ # Copyright (C) 2015, Fletcher Nichol
3
+ # Copyright (C) 2026, Oregon State University
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # https://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ require "json" unless defined?(JSON)
18
+
19
+ module Kitchen
20
+ module Provisioner
21
+ module Cinc
22
+ # Internal object to manage common sandbox preparation for
23
+ # Cinc-related provisioners.
24
+ #
25
+ # @author Cinc Project
26
+ # @api private
27
+ class CommonSandbox
28
+ include Logging
29
+
30
+ # Constructs a new object, taking config, a sandbox path, and an
31
+ # instance.
32
+ #
33
+ # @param config [Hash] configuration hash
34
+ # @param sandbox_path [String] path to local sandbox directory
35
+ # @param instance [Instance] an instance
36
+ def initialize(config, sandbox_path, instance)
37
+ @config = config
38
+ @sandbox_path = sandbox_path
39
+ @instance = instance
40
+ end
41
+
42
+ # Populate the sandbox.
43
+ def populate
44
+ prepare_json
45
+ prepare_cache
46
+ prepare_cookbooks
47
+ prepare(:data)
48
+ prepare(:data_bags)
49
+ prepare(:environments)
50
+ prepare(:nodes)
51
+ prepare(:roles)
52
+ prepare(:clients)
53
+ prepare(
54
+ :secret,
55
+ type: :file,
56
+ dest_name: "encrypted_data_bag_secret",
57
+ key_name: :encrypted_data_bag_secret_key_path
58
+ )
59
+ end
60
+
61
+ private
62
+
63
+ # @return [Hash] configuration hash
64
+ # @api private
65
+ attr_reader :config
66
+
67
+ # @return [Instance] an instance
68
+ # @api private
69
+ attr_reader :instance
70
+
71
+ # @return [String] path to local sandbox directory
72
+ # @api private
73
+ attr_reader :sandbox_path
74
+
75
+ # @return [String] name of the policy_group, nil results in "local"
76
+ # @api private
77
+ attr_reader :policy_group
78
+
79
+ # Generates a list of all files in the cookbooks directory in the
80
+ # sandbox path.
81
+ #
82
+ # @return [Array<String>] an array of absolute paths to files
83
+ # @api private
84
+ def all_files_in_cookbooks
85
+ Util.list_directory(tmpbooks_dir, include_dot: true, recurse: true)
86
+ .select { |fn| File.file?(fn) }
87
+ end
88
+
89
+ # @return [String] an absolute path to a Policyfile, relative to the
90
+ # kitchen root
91
+ # @api private
92
+ def policyfile
93
+ basename = config[:policyfile_path] || config[:policyfile] || "Policyfile.rb"
94
+ File.expand_path(basename, config[:kitchen_root])
95
+ end
96
+
97
+ # @return [String] an absolute path to a Berksfile, relative to the
98
+ # kitchen root
99
+ # @api private
100
+ def berksfile
101
+ basename = config[:berksfile_path] || "Berksfile"
102
+ File.expand_path(basename, config[:kitchen_root])
103
+ end
104
+
105
+ # @return [String] an absolute path to a cookbooks/ directory, relative
106
+ # to the kitchen root
107
+ # @api private
108
+ def cookbooks_dir
109
+ File.join(config[:kitchen_root], "cookbooks")
110
+ end
111
+
112
+ # Copies a cookbooks/ directory into the sandbox path.
113
+ #
114
+ # @api private
115
+ def cp_cookbooks
116
+ info("Preparing cookbooks from project directory")
117
+ debug("Using cookbooks from #{cookbooks_dir}")
118
+
119
+ FileUtils.mkdir_p(tmpbooks_dir)
120
+ FileUtils.cp_r(File.join(cookbooks_dir, "."), tmpbooks_dir)
121
+
122
+ cp_site_cookbooks if File.directory?(site_cookbooks_dir)
123
+ cp_this_cookbook if File.exist?(metadata_rb)
124
+ end
125
+
126
+ # Copies a site-cookbooks/ directory into the sandbox path.
127
+ #
128
+ # @api private
129
+ def cp_site_cookbooks
130
+ info("Preparing site-cookbooks from project directory")
131
+ debug("Using cookbooks from #{site_cookbooks_dir}")
132
+
133
+ FileUtils.mkdir_p(tmpsitebooks_dir)
134
+ FileUtils.cp_r(File.join(site_cookbooks_dir, "."), tmpsitebooks_dir)
135
+ end
136
+
137
+ # Copies the current project, assumed to be a cookbook into the
138
+ # sandbox path.
139
+ #
140
+ # @api private
141
+ def cp_this_cookbook
142
+ info("Preparing current project directory as a cookbook")
143
+ debug("Using metadata.rb from #{metadata_rb}")
144
+
145
+ cb_name = MetadataChopper.extract(metadata_rb).first || raise(UserError,
146
+ "The metadata.rb does not define the 'name' key." \
147
+ " Please add: `name '<cookbook_name>'` to metadata.rb and retry")
148
+
149
+ cb_path = File.join(tmpbooks_dir, cb_name)
150
+
151
+ glob = Util.list_directory(config[:kitchen_root])
152
+
153
+ FileUtils.mkdir_p(cb_path)
154
+ FileUtils.cp_r(glob, cb_path)
155
+ end
156
+
157
+ # Removes all non-cookbook files in the sandbox path.
158
+ #
159
+ # @api private
160
+ def filter_only_cookbook_files
161
+ info("Removing non-cookbook files before transfer")
162
+ FileUtils.rm(all_files_in_cookbooks - only_cookbook_files)
163
+ Util.list_directory(tmpbooks_dir, recurse: true)
164
+ .reverse_each { |fn| FileUtils.rmdir(fn) if File.directory?(fn) && Dir.empty?(fn) }
165
+ end
166
+
167
+ # @return [Logger] the instance's logger or Test Kitchen's common
168
+ # logger otherwise
169
+ # @api private
170
+ def logger
171
+ instance ? instance.logger : Kitchen.logger
172
+ end
173
+
174
+ # Creates a minimal, no-op cookbook in the sandbox path.
175
+ #
176
+ # @api private
177
+ def make_fake_cookbook
178
+ info("Policyfile, Berksfile, cookbooks/, or metadata.rb not found " \
179
+ "so Cinc Client will run, but do nothing. Is this intended?")
180
+ name = File.basename(config[:kitchen_root])
181
+ fake_cb = File.join(tmpbooks_dir, name)
182
+ FileUtils.mkdir_p(fake_cb)
183
+ File.open(File.join(fake_cb, "metadata.rb"), "wb") do |file|
184
+ file.write(%{name "#{name}"\n})
185
+ end
186
+ end
187
+
188
+ # @return [String] an absolute path to a metadata.rb, relative to the
189
+ # kitchen root
190
+ # @api private
191
+ def metadata_rb
192
+ File.join(config[:kitchen_root], "metadata.rb")
193
+ end
194
+
195
+ # Generates a list of all typical cookbook files needed in a run,
196
+ # located in the cookbooks directory in the sandbox path.
197
+ #
198
+ # @return [Array<String>] an array of absolute paths to files
199
+ # @api private
200
+ def only_cookbook_files
201
+ glob = File.join("*", "{#{config[:cookbook_files_glob]}}")
202
+ Util.safe_glob(tmpbooks_dir, glob, File::FNM_DOTMATCH)
203
+ .select { |fn| File.file?(fn) && ! %w{. ..}.include?(fn) }
204
+ end
205
+
206
+ # Prepares a generic component source directory or file for
207
+ # inclusion in the sandbox path. These components might includes nodes,
208
+ # roles, etc.
209
+ #
210
+ # @param component [Symbol,String] a component name such as `:node`
211
+ # @param opts [Hash] optional configuration
212
+ # @option opts [Symbol] :type whether the component is a directory or
213
+ # file (default: `:directory`)
214
+ # @option opts [Symbol] :key_name the key name in the config hash from
215
+ # which to pull the source path (default: `"#{component}_path"`)
216
+ # @option opts [String] :dest_name the destination file or directory
217
+ # basename in the sandbox path (default: `component.to_s`)
218
+ # @api private
219
+ def prepare(component, opts = {})
220
+ opts = { type: :directory }.merge(opts)
221
+ key_name = opts.fetch(:key_name, "#{component}_path")
222
+ src = config[key_name.to_sym]
223
+ return if src.nil?
224
+
225
+ info("Preparing #{component}")
226
+ debug("Using #{component} from #{src}")
227
+
228
+ dest = File.join(sandbox_path, opts.fetch(:dest_name, component.to_s))
229
+
230
+ case opts[:type]
231
+ when :directory
232
+ FileUtils.mkdir_p(dest)
233
+ Array(src).each { |dir| FileUtils.cp_r(Util.list_directory(dir), dest) }
234
+ when :file
235
+ FileUtils.mkdir_p(File.dirname(dest))
236
+ Array(src).each { |file| FileUtils.cp_r(file, dest) }
237
+ end
238
+ end
239
+
240
+ # Prepares a cache directory for inclusion in the sandbox path.
241
+ #
242
+ # @api private
243
+ def prepare_cache
244
+ FileUtils.mkdir_p(File.join(sandbox_path, "cache"))
245
+ end
246
+
247
+ # Prepares cookbooks for inclusion in the sandbox path.
248
+ #
249
+ # @api private
250
+ def prepare_cookbooks
251
+ if File.exist?(policyfile)
252
+ resolve_with_policyfile
253
+ elsif File.exist?(berksfile)
254
+ resolve_with_berkshelf
255
+ elsif File.directory?(cookbooks_dir)
256
+ cp_cookbooks
257
+ elsif File.exist?(metadata_rb)
258
+ cp_this_cookbook
259
+ else
260
+ make_fake_cookbook
261
+ end
262
+
263
+ filter_only_cookbook_files
264
+ end
265
+
266
+ # Prepares a JSON file, sometimes called a dna.json or
267
+ # first-boot.json, for inclusion in the sandbox path.
268
+ #
269
+ # @api private
270
+ def prepare_json
271
+ dna = if File.exist?(policyfile)
272
+ update_dna_for_policyfile
273
+ else
274
+ config[:attributes].merge(run_list: config[:run_list])
275
+ end
276
+
277
+ info("Preparing dna.json")
278
+ debug("Creating dna.json from #{dna.inspect}")
279
+
280
+ File.open(File.join(sandbox_path, "dna.json"), "wb") do |file|
281
+ file.write(dna.to_json)
282
+ end
283
+ end
284
+
285
+ def update_dna_for_policyfile
286
+ policy = Cinc::Policyfile.new(
287
+ policyfile, sandbox_path,
288
+ logger:,
289
+ always_update: config[:always_update_cookbooks],
290
+ policy_group:
291
+ )
292
+ Kitchen.mutex.synchronize do
293
+ policy.compile
294
+ end
295
+ policy_name = JSON.parse(File.read(policy.lockfile))["name"]
296
+ policy_group = config[:policy_group] || "local"
297
+ config[:attributes].merge(policy_name:, policy_group:)
298
+ end
299
+
300
+ # Performs a Policyfile cookbook resolution inside a common mutex.
301
+ #
302
+ # @api private
303
+ def resolve_with_policyfile
304
+ Kitchen.mutex.synchronize do
305
+ Cinc::Policyfile.new(
306
+ policyfile, sandbox_path,
307
+ logger:,
308
+ always_update: config[:always_update_cookbooks],
309
+ policy_group: config[:policy_group]
310
+ ).resolve
311
+ end
312
+ end
313
+
314
+ # Performs a Berkshelf cookbook resolution inside a common mutex.
315
+ #
316
+ # @api private
317
+ def resolve_with_berkshelf
318
+ Kitchen.mutex.synchronize do
319
+ Cinc::Berkshelf.new(berksfile, tmpbooks_dir,
320
+ logger:,
321
+ always_update: config[:always_update_cookbooks]).resolve
322
+ end
323
+ end
324
+
325
+ # @return [String] an absolute path to a site-cookbooks/ directory,
326
+ # relative to the kitchen root
327
+ # @api private
328
+ def site_cookbooks_dir
329
+ File.join(config[:kitchen_root], "site-cookbooks")
330
+ end
331
+
332
+ # @return [String] an absolute path to a cookbooks/ directory in the
333
+ # sandbox path
334
+ # @api private
335
+ def tmpbooks_dir
336
+ File.join(sandbox_path, "cookbooks")
337
+ end
338
+
339
+ # @return [String] an absolute path to a site cookbooks directory in the
340
+ # sandbox path
341
+ # @api private
342
+ def tmpsitebooks_dir
343
+ File.join(sandbox_path, "cookbooks")
344
+ end
345
+ end
346
+ end
347
+ end
348
+ end
@@ -0,0 +1,154 @@
1
+ #
2
+ # Copyright (C) 2013, Fletcher Nichol
3
+ # Copyright (C) 2026, Oregon State University
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # https://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ require "shellwords" unless defined?(Shellwords)
18
+ require "rbconfig" unless defined?(RbConfig)
19
+
20
+ require "kitchen/errors"
21
+ require "kitchen/logging"
22
+ require "kitchen/shell_out"
23
+ require "kitchen/which"
24
+
25
+ module Kitchen
26
+ module Provisioner
27
+ module Cinc
28
+ # Cinc cookbook resolver that uses Policyfiles to calculate dependencies.
29
+ #
30
+ # @author Cinc Project
31
+ class Policyfile
32
+ include Logging
33
+ include ShellOut
34
+ include Which
35
+
36
+ # Creates a new cookbook resolver.
37
+ #
38
+ # @param policyfile [String] path to a Policyfile
39
+ # @param path [String] path in which to vendor the resulting
40
+ # cookbooks
41
+ # @param logger [Kitchen::Logger] a logger to use for output, defaults
42
+ # to `Kitchen.logger`
43
+ def initialize(policyfile, path, logger: Kitchen.logger, always_update: false, policy_group: nil)
44
+ @policyfile = policyfile
45
+ @path = path
46
+ @logger = logger
47
+ @always_update = always_update
48
+ @policy_group = policy_group
49
+ end
50
+
51
+ # Loads the library code required to use the resolver.
52
+ #
53
+ # @param logger [Kitchen::Logger] a logger to use for output, defaults
54
+ # to `Kitchen.logger`
55
+ def self.load!(logger: Kitchen.logger)
56
+ # intentionally left blank
57
+ end
58
+
59
+ # Performs the cookbook resolution and vendors the resulting cookbooks
60
+ # in the desired path.
61
+ def resolve
62
+ if policy_group
63
+ info("Exporting cookbook dependencies from Policyfile #{path} with policy_group #{policy_group} using `#{cli_path} export`...")
64
+ run_command("#{cli_path} export #{escape_path(policyfile)} #{escape_path(path)} --policy_group #{policy_group} --force")
65
+ else
66
+ info("Exporting cookbook dependencies from Policyfile #{path} using `#{cli_path} export`...")
67
+ run_command("#{cli_path} export #{escape_path(policyfile)} #{escape_path(path)} --force")
68
+ end
69
+ end
70
+
71
+ # Runs `cinc install` to determine the correct cookbook set and
72
+ # generate the policyfile lock.
73
+ def compile
74
+ if File.exist?(lockfile)
75
+ info("Installing cookbooks for Policyfile #{policyfile} using `#{cli_path} install`")
76
+ else
77
+ info("Policy lock file doesn't exist, running `#{cli_path} install` for Policyfile #{policyfile}...")
78
+ end
79
+ run_command("#{cli_path} install #{escape_path(policyfile)}")
80
+
81
+ if always_update
82
+ info("Updating policy lock using `#{cli_path} update`")
83
+ run_command("#{cli_path} update #{escape_path(policyfile)}")
84
+ end
85
+ end
86
+
87
+ # Return the path to the lockfile corresponding to this policyfile.
88
+ #
89
+ # @return [String]
90
+ def lockfile
91
+ policyfile.gsub(/\.rb\Z/, ".lock.json")
92
+ end
93
+
94
+ private
95
+
96
+ # @return [String] path to a Policyfile
97
+ # @api private
98
+ attr_reader :policyfile
99
+
100
+ # @return [String] path in which to vendor the resulting cookbooks
101
+ # @api private
102
+ attr_reader :path
103
+
104
+ # @return [Kitchen::Logger] a logger to use for output
105
+ # @api private
106
+ attr_reader :logger
107
+
108
+ # @return [Boolean] If true, always update cookbooks in the policy.
109
+ # @api private
110
+ attr_reader :always_update
111
+
112
+ # @return [String] name of the policy_group, nil results in "local"
113
+ # @api private
114
+ attr_reader :policy_group
115
+
116
+ # Escape spaces in a path in way that works with both Sh (Unix) and
117
+ # Windows.
118
+ #
119
+ # @param path [String] Path to escape
120
+ # @return [String]
121
+ # @api private
122
+ def escape_path(path)
123
+ if /mswin|mingw/.match?(RbConfig::CONFIG["host_os"])
124
+ if /[ \t\n\v"]/.match?(path)
125
+ "\"#{path.gsub(/[ \t\n\v\"\\]/) { |m| "\\" + m[0] }}\""
126
+ else
127
+ path
128
+ end
129
+ else
130
+ Shellwords.escape(path)
131
+ end
132
+ end
133
+
134
+ # Find the `cinc` or `cinc-cli` commands in the path. `cinc-cli` is the
135
+ # Ruby CLI shipped in the `cinc-cli` gem.
136
+ #
137
+ # @api private
138
+ # @returns [String]
139
+ def cli_path
140
+ @cli_path ||= which("cinc-cli") || which("cinc") || which("chef-cli") || which("chef") || no_cli_found_error
141
+ end
142
+
143
+ # @api private
144
+ def no_cli_found_error
145
+ @logger.fatal("The `cinc`, `cinc-cli`, `chef`, or `chef-cli` executables cannot be found in your " \
146
+ "PATH. Ensure you have installed Cinc Workstation " \
147
+ "from https://cinc.sh/download/ and that your PATH " \
148
+ "setting includes the path to the `cinc` or `cinc-cli` commands.")
149
+ raise UserError, "Could not find the cinc or cinc-cli executables in your PATH."
150
+ end
151
+ end
152
+ end
153
+ end
154
+ end
@@ -0,0 +1,124 @@
1
+ #
2
+ # Copyright (C) 2015, HiganWorks LLC
3
+ # Copyright (C) 2026, Oregon State University
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # https://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ # Usage:
18
+ #
19
+ # puts your recipes to `apply/` directory.
20
+ #
21
+ # An example of .kitchen.yml.
22
+ #
23
+ # ---
24
+ # driver:
25
+ # name: vagrant
26
+ #
27
+ # provisioner:
28
+ # name: cinc_apply
29
+ #
30
+ # platforms:
31
+ # - name: ubuntu-24.04
32
+ # - name: almalinux-10
33
+ #
34
+ # suites:
35
+ # - name: default
36
+ # run_list:
37
+ # - recipe1
38
+ # - recipe2
39
+ #
40
+ #
41
+ # The cinc-apply runs twice below.
42
+ #
43
+ # cinc-apply apply/recipe1.rb
44
+ # cinc-apply apply/recipe2.rb
45
+
46
+ require_relative "cinc_base"
47
+
48
+ module Kitchen
49
+ module Provisioner
50
+ # Cinc Apply provisioner.
51
+ #
52
+ # @author Cinc Project
53
+ class CincApply < CincBase
54
+ kitchen_provisioner_api_version 2
55
+
56
+ plugin_version Kitchen::VERSION
57
+
58
+ default_config :cinc_apply_path do |provisioner|
59
+ provisioner
60
+ .remote_path_join(%W{#{provisioner[:cinc_omnibus_root]} bin cinc-apply})
61
+ .tap { |path| path.concat(".bat") if provisioner.windows_os? }
62
+ end
63
+
64
+ default_config :ruby_bindir do |provisioner|
65
+ provisioner
66
+ .remote_path_join(%W{#{provisioner[:cinc_omnibus_root]} embedded bin})
67
+ end
68
+
69
+ default_config :apply_path do |provisioner|
70
+ provisioner.calculate_path("apply")
71
+ end
72
+ expand_path_for :apply_path
73
+
74
+ # (see CincBase#create_sandbox)
75
+ def create_sandbox
76
+ @sandbox_path = Dir.mktmpdir("#{instance.name}-sandbox-")
77
+ File.chmod(0755, sandbox_path)
78
+ info("Preparing files for transfer")
79
+ debug("Creating local sandbox in #{sandbox_path}")
80
+
81
+ prepare_json
82
+ prepare(:apply)
83
+ end
84
+
85
+ # (see CincBase#init_command)
86
+ def init_command
87
+ dirs = %w{
88
+ apply
89
+ }.sort.map { |dir| remote_path_join(config[:root_path], dir) }
90
+
91
+ vars = if powershell_shell?
92
+ init_command_vars_for_powershell(dirs)
93
+ else
94
+ init_command_vars_for_bourne(dirs)
95
+ end
96
+
97
+ prefix_command(shell_code_from_file(vars, "cinc_base_init_command"))
98
+ end
99
+
100
+ # (see CincSolo#run_command)
101
+ def run_command
102
+ level = config[:log_level]
103
+ lines = []
104
+ config[:run_list].map do |recipe|
105
+ cmd = sudo(config[:cinc_apply_path]).dup
106
+ .tap { |str| str.insert(0, "& ") if powershell_shell? }
107
+ args = [
108
+ "apply/#{recipe}.rb",
109
+ "--log_level #{level}",
110
+ "--no-color",
111
+ ]
112
+ args << "--logfile #{config[:log_file]}" if config[:log_file]
113
+
114
+ lines << wrap_shell_code(
115
+ [cmd, *args].join(" ")
116
+ .tap { |str| str.insert(0, reload_ps1_path) if windows_os? }
117
+ )
118
+ end
119
+
120
+ prefix_command(lines.join("\n"))
121
+ end
122
+ end
123
+ end
124
+ end