# # Copyright:: Copyright (c) 2014 Chef Software Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # require "spec_helper" require "shared/custom_generator_cookbook" require "shared/setup_git_committer_config" require "chef-dk/command/generator_commands/cookbook" describe ChefDK::Command::GeneratorCommands::Cookbook do include_context("setup_git_committer_config") let(:argv) { %w{new_cookbook} } let(:stdout_io) { StringIO.new } let(:stderr_io) { StringIO.new } let(:expected_cookbook_file_relpaths) do %w{ .gitignore .kitchen.yml test test/smoke test/smoke/default/default_test.rb Berksfile chefignore LICENSE metadata.rb README.md recipes recipes/default.rb spec spec/spec_helper.rb spec/unit spec/unit/recipes spec/unit/recipes/default_spec.rb } end let(:expected_cookbook_files) do expected_cookbook_file_relpaths.map do |relpath| File.join(tempdir, "new_cookbook", relpath) end end let(:non_delivery_breadcrumb) do <<-EOF Your cookbook is ready. Type `cd new_cookbook` to enter it. There are several commands you can run to get started locally developing and testing your cookbook. Type `delivery local --help` to see a full list. Why not start by writing a test? Tests for the default recipe are stored at: test/smoke/default/default_test.rb If you'd prefer to dive right in, the default recipe can be found at: recipes/default.rb EOF end subject(:cookbook_generator) do g = described_class.new(argv) allow(g).to receive(:cookbook_path_in_git_repo?).and_return(false) allow(g).to receive(:stdout).and_return(stdout_io) g end def generator_context ChefDK::Generator.context end before do ChefDK::Generator.reset end include_examples "custom generator cookbook" do let(:generator_arg) { "new_cookbook" } let(:generator_name) { "cookbook" } end it "configures the chef runner" do expect(cookbook_generator.chef_runner).to be_a(ChefDK::ChefRunner) expect(cookbook_generator.chef_runner.cookbook_path).to eq(File.expand_path("lib/chef-dk/skeletons", project_root)) end context "when given invalid/incomplete arguments" do let(:expected_help_message) do "Usage: chef generate cookbook NAME [options]\n" end def with_argv(argv) generator = described_class.new(argv) allow(generator).to receive(:stdout).and_return(stdout_io) allow(generator).to receive(:stderr).and_return(stderr_io) generator end it "prints usage when args are empty" do with_argv([]).run expect(stderr_io.string).to include(expected_help_message) end it "errors if both berks and policyfiles are requested" do expect(with_argv(%w{my_cookbook --berks --policy}).run).to eq(1) message = "Berkshelf and Policyfiles are mutually exclusive. Please specify only one." expect(stderr_io.string).to include(message) end it "warns if a hyphenated cookbook name is passed" do expect(with_argv(%w{my-cookbook}).run).to eq(0) message = "Hyphens are discouraged in cookbook names as they may cause problems with custom resources. See https://docs.chef.io/ctl_chef.html#chef-generate-cookbook for more information." expect(stdout_io.string).to include(message) end end context "when given the name of the cookbook to generate" do let(:argv) { %w{new_cookbook} } before do reset_tempdir end it "configures the generator context" do cookbook_generator.read_and_validate_params cookbook_generator.setup_context expect(generator_context.cookbook_root).to eq(Dir.pwd) expect(generator_context.cookbook_name).to eq("new_cookbook") expect(generator_context.recipe_name).to eq("default") expect(generator_context.verbose).to be(false) end it "creates a new cookbook" do Dir.chdir(tempdir) do allow(cookbook_generator.chef_runner).to receive(:stdout).and_return(stdout_io) expect(cookbook_generator.run).to eq(0) end generated_files = Dir.glob("#{tempdir}/new_cookbook/**/*", File::FNM_DOTMATCH) expected_cookbook_files.each do |expected_file| expect(generated_files).to include(expected_file) end end context "by default configure for delivery" do let(:dot_delivery) { File.join(tempdir, "new_cookbook", ".delivery") } before do Dir.chdir(tempdir) do allow(cookbook_generator.chef_runner).to receive(:stdout).and_return(stdout_io) expect(cookbook_generator.run).to eq(0) end end describe ".delivery/project.toml" do let(:file) { File.join(tempdir, "new_cookbook", ".delivery", "project.toml") } let(:expected_content) do <<-PROJECT_DOT_TOML # Delivery Prototype for Local Phases Execution # # The purpose of this file is to prototype a new way to execute # phases locally on your workstation. The delivery-cli will read # this file and execute the command(s) that are configured for # each phase. You can customize them by just modifying the phase # key on this file. # # By default these phases are configured for Cookbook Workflow only # # As this is still a prototype we are not modifying the current # config.json file and it will continue working as usual. [local_phases] unit = "chef exec rspec spec/" lint = "chef exec cookstyle" # Foodcritic includes rules only appropriate for community cookbooks # uploaded to Supermarket. We turn off any rules tagged "supermarket" # by default. If you plan to share this cookbook you should remove # '-t ~supermarket' below to enable supermarket rules. syntax = "chef exec foodcritic . -t ~supermarket" provision = "chef exec kitchen create" deploy = "chef exec kitchen converge" smoke = "chef exec kitchen verify" # The functional phase is optional, you can define it by uncommenting # the line below and running the command: `delivery local functional` # functional = "" cleanup = "chef exec kitchen destroy" # Remote project.toml file # # Specify a remote URI location for the `project.toml` file. # This is useful for teams that wish to centrally manage the behavior # of the `delivery local` command across many different projects. # # remote_file = "https://url/project.toml" PROJECT_DOT_TOML end it "exists with default config for Cookbook Workflow" do expect(IO.read(file)).to eq(expected_content) end end describe ".delivery/config.json" do let(:file) { File.join(tempdir, "new_cookbook", ".delivery", "config.json") } let(:expected_content) do <<-CONFIG_DOT_JSON { "version": "2", "build_cookbook": { "name": "build_cookbook", "path": ".delivery/build_cookbook" }, "delivery-truck": { "lint": { "enable_cookstyle": true } }, "skip_phases": [], "job_dispatch": { "version": "v2" }, "dependencies": [] } CONFIG_DOT_JSON end it "configures delivery to use a local build cookbook" do expect(IO.read(file)).to eq(expected_content) end end describe "build cookbook recipes" do let(:file) do File.join(dot_delivery, "build_cookbook", "recipes", "publish.rb") end let(:expected_content) do <<-CONFIG_DOT_JSON # # Cookbook:: build_cookbook # Recipe:: publish # # Copyright:: 2017, The Authors, All Rights Reserved. include_recipe 'delivery-truck::publish' CONFIG_DOT_JSON end it "delegates functionality to delivery-truck" do expect(IO.read(file)).to include(expected_content) end end describe "build cookbook Berksfile" do let(:file) do File.join(dot_delivery, "build_cookbook", "Berksfile") end let(:expected_content) do <<-CONFIG_DOT_JSON source 'https://supermarket.chef.io' metadata group :delivery do cookbook 'test', path: './test/fixtures/cookbooks/test' end CONFIG_DOT_JSON end it "sets the sources for delivery library cookbooks to github" do expect(IO.read(file)).to include(expected_content) end end end context "when passed delivery option" do let(:argv) { %w{new_cookbook --delivery} } it "still works with no action" do Dir.chdir(tempdir) do allow(cookbook_generator.chef_runner).to receive(:stdout).and_return(stdout_io) expect(cookbook_generator.run).to eq(0) end end end context "when given the verbose flag" do let(:argv) { %w{ new_cookbook --verbose } } it "configures the generator context with verbose mode enabled" do cookbook_generator.read_and_validate_params cookbook_generator.setup_context expect(generator_context.verbose).to be(true) end it "emits verbose output" do Dir.chdir(tempdir) do allow(cookbook_generator.chef_runner).to receive(:stdout).and_return(stdout_io) expect(cookbook_generator.run).to eq(0) end # The normal chef formatter puts a heading for each recipe like this. # Full output is large and subject to change with minor changes in the # generator cookbook, so we just look for this line expected_line = "Recipe: code_generator::cookbook" actual = stdout_io.string expect(actual).to include(expected_line) end end context "when no delivery CLI configuration is present" do it "detects no delivery config" do Dir.chdir(tempdir) do expect(cookbook_generator.have_delivery_config?).to be(false) end end it "emits concise output" do Dir.chdir(tempdir) do allow(cookbook_generator.chef_runner).to receive(:stdout).and_return(stdout_io) expect(cookbook_generator.run).to eq(0) end expected = <<-OUTPUT Generating cookbook new_cookbook - Ensuring correct cookbook file content - Committing cookbook files to git - Ensuring delivery configuration - Ensuring correct delivery build cookbook content - Adding delivery configuration to feature branch - Adding build cookbook to feature branch - Merging delivery content feature branch to master #{non_delivery_breadcrumb} OUTPUT actual = stdout_io.string # the formatter will add escape sequences to turn off any colors actual.gsub!("\e[0m", "") expect(actual).to eq(expected) end end context "when a delivery CLI config is present" do # Setup a situation like this: # there is a dir for the delivery organization with the # `.delivery/cli.toml` in it. Inside that is another dir (maybe IRL this # would be "cookbooks"), then we create the cookbook inside that. let(:tempdir_subdir) { File.join(tempdir, "subdirectory") } let(:dot_delivery_dir) { File.join(tempdir, ".delivery") } let(:dot_delivery_cli_toml) { File.join(dot_delivery_dir, "cli.toml") } before do Dir.mkdir(tempdir_subdir) Dir.mkdir(dot_delivery_dir) FileUtils.touch(dot_delivery_cli_toml) end it "detects the delivery config" do Dir.chdir(tempdir_subdir) do expect(cookbook_generator.have_delivery_config?).to be(true) end end it "emits concise output" do Dir.chdir(tempdir) do allow(cookbook_generator.chef_runner).to receive(:stdout).and_return(stdout_io) expect(cookbook_generator.run).to eq(0) end expected = <<-OUTPUT Generating cookbook new_cookbook - Ensuring correct cookbook file content - Committing cookbook files to git - Ensuring delivery configuration - Ensuring correct delivery build cookbook content - Adding delivery configuration to feature branch - Adding build cookbook to feature branch - Merging delivery content feature branch to master Your cookbook is ready. To setup the pipeline, type `cd new_cookbook`, then run `delivery init` OUTPUT actual = stdout_io.string # the formatter will add escape sequences to turn off any colors actual.gsub!("\e[0m", "") expect(actual).to eq(expected) end end shared_examples_for "a generated file" do |context_var| before do Dir.chdir(tempdir) do allow(cookbook_generator.chef_runner).to receive(:stdout).and_return(stdout_io) expect(cookbook_generator.run).to eq(0) end end it "should contain #{context_var} from the generator context" do expect(File.read(file)).to match line end end describe "README.md" do let(:file) { File.join(tempdir, "new_cookbook", "README.md") } include_examples "a generated file", :cookbook_name do let(:line) { "# new_cookbook" } end end # This shared example group requires a let binding for # `expected_kitchen_yml_content` shared_examples_for "kitchen_yml_and_integration_tests" do describe "Generating Test Kitchen and integration testing files" do describe "generating kitchen config" do before do Dir.chdir(tempdir) do allow(cookbook_generator.chef_runner).to receive(:stdout).and_return(stdout_io) expect(cookbook_generator.run).to eq(0) end end let(:file) { File.join(tempdir, "new_cookbook", ".kitchen.yml") } it "creates a .kitchen.yml with the expected content" do expect(IO.read(file)).to eq(expected_kitchen_yml_content) end end describe "test/smoke/default/default_test.rb" do let(:file) { File.join(tempdir, "new_cookbook", "test", "smoke", "default", "default_test.rb") } include_examples "a generated file", :cookbook_name do let(:line) { "describe port" } end end end end # This shared example group requires you to define a let binding for # `expected_chefspec_spec_helper_content` shared_examples_for "chefspec_spec_helper_file" do describe "Generating ChefSpec files" do before do Dir.chdir(tempdir) do allow(cookbook_generator.chef_runner).to receive(:stdout).and_return(stdout_io) expect(cookbook_generator.run).to eq(0) end end let(:file) { File.join(tempdir, "new_cookbook", "spec", "spec_helper.rb") } it "creates a spec/spec_helper.rb for ChefSpec with the expected content" do expect(IO.read(file)).to eq(expected_chefspec_spec_helper_content) end end end context "when configured for Policyfiles" do let(:argv) { %w{new_cookbook --policy} } describe "Policyfile.rb" do let(:file) { File.join(tempdir, "new_cookbook", "Policyfile.rb") } let(:expected_content) do <<-POLICYFILE_RB # Policyfile.rb - Describe how you want Chef to build your system. # # For more information on the Policyfile feature, visit # https://docs.chef.io/policyfile.html # A name that describes what the system you're building with Chef does. name "new_cookbook" # Where to find external cookbooks: default_source :supermarket # run_list: chef-client will run these recipes in the order specified. run_list "new_cookbook::default" # Specify a custom source for a single cookbook: cookbook "new_cookbook", path: "." POLICYFILE_RB end before do Dir.chdir(tempdir) do allow(cookbook_generator.chef_runner).to receive(:stdout).and_return(stdout_io) expect(cookbook_generator.run).to eq(0) end end it "has a run_list and cookbook path that will work out of the box" do expect(IO.read(file)).to eq(expected_content) end end include_examples "kitchen_yml_and_integration_tests" do let(:expected_kitchen_yml_content) do <<-KITCHEN_YML --- driver: name: vagrant ## The forwarded_port port feature lets you connect to ports on the VM guest via ## localhost on the host. ## see also: https://docs.vagrantup.com/v2/networking/forwarded_ports.html # network: # - ["forwarded_port", {guest: 80, host: 8080}] provisioner: name: policyfile_zero ## require_chef_omnibus specifies a specific chef version to install. You can ## also set this to `true` to always use the latest version. ## see also: https://docs.chef.io/config_yml_kitchen.html # require_chef_omnibus: 12.8.1 verifier: name: inspec platforms: - name: ubuntu-16.04 - name: centos-7 suites: - name: default verifier: inspec_tests: - test/smoke/default attributes: KITCHEN_YML end end include_examples "chefspec_spec_helper_file" do let(:expected_chefspec_spec_helper_content) do <<-SPEC_HELPER # frozen_string_literal: true require 'chefspec' require 'chefspec/policyfile' SPEC_HELPER end end end context "when configured for Berkshelf" do let(:argv) { %w{new_cookbook --berks} } describe "Berksfile" do let(:file) { File.join(tempdir, "new_cookbook", "Berksfile") } let(:expected_content) do <<-POLICYFILE_RB # frozen_string_literal: true source 'https://supermarket.chef.io' metadata POLICYFILE_RB end before do Dir.chdir(tempdir) do allow(cookbook_generator.chef_runner).to receive(:stdout).and_return(stdout_io) expect(cookbook_generator.run).to eq(0) end end it "pulls deps from metadata" do expect(IO.read(file)).to eq(expected_content) end end include_examples "kitchen_yml_and_integration_tests" do let(:expected_kitchen_yml_content) do <<-KITCHEN_YML --- driver: name: vagrant provisioner: name: chef_zero # You may wish to disable always updating cookbooks in CI or other testing environments. # For example: # always_update_cookbooks: <%= !ENV['CI'] %> always_update_cookbooks: true verifier: name: inspec platforms: - name: ubuntu-16.04 - name: centos-7 suites: - name: default run_list: - recipe[new_cookbook::default] verifier: inspec_tests: - test/smoke/default attributes: KITCHEN_YML end end include_examples "chefspec_spec_helper_file" do let(:expected_chefspec_spec_helper_content) do <<-SPEC_HELPER # frozen_string_literal: true require 'chefspec' require 'chefspec/berkshelf' SPEC_HELPER end end end describe "metadata.rb" do let(:file) { File.join(tempdir, "new_cookbook", "metadata.rb") } include_examples "a generated file", :cookbook_name do let(:line) { /name\s+'new_cookbook'.+# issues_url.+# source_url/m } end end describe "recipes/default.rb" do let(:file) { File.join(tempdir, "new_cookbook", "recipes", "default.rb") } include_examples "a generated file", :cookbook_name do let(:line) { "# Cookbook:: new_cookbook" } end end describe "spec/unit/recipes/default_spec.rb" do let(:file) { File.join(tempdir, "new_cookbook", "spec", "unit", "recipes", "default_spec.rb") } include_examples "a generated file", :cookbook_name do let(:line) { "describe 'new_cookbook::default' do" } end end end context "when given the path to the cookbook to generate" do let(:argv) { [ File.join(tempdir, "a_new_cookbook") ] } before do reset_tempdir end it "configures the generator context" do cookbook_generator.read_and_validate_params cookbook_generator.setup_context expect(generator_context.cookbook_root).to eq(tempdir) expect(generator_context.cookbook_name).to eq("a_new_cookbook") end end context "when given generic arguments to populate the generator context" do let(:argv) { [ "new_cookbook", "--generator-arg", "key1=value1", "-a", "key2=value2", "-a", " key3 = value3 " ] } before do reset_tempdir end it "configures the generator context for long form option key1" do cookbook_generator.read_and_validate_params cookbook_generator.setup_context expect(generator_context.key1).to eq("value1") end it "configures the generator context for short form option key2" do cookbook_generator.read_and_validate_params cookbook_generator.setup_context expect(generator_context.key2).to eq("value2") end it "configures the generator context for key3 containing additional spaces" do cookbook_generator.read_and_validate_params cookbook_generator.setup_context expect(generator_context.key3).to eq("value3") end end end