lib/chef-cli/command/generator_commands/cookbook.rb



#
# Copyright:: 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_relative "base"
require_relative "../../dist"

module ChefCLI
  module Command
    module GeneratorCommands

      # ## CookbookFile
      # chef generate cookbook path/to/basename --generator-cookbook=path/to/generator
      #
      # Generates a basic cookbook directory structure. Most file types are
      # omitted, the user is expected to add additional files as needed using
      # the relevant generators.
      class Cookbook < Base

        banner "Usage: #{ChefCLI::Dist::EXEC} generate cookbook NAME [options]"

        attr_reader :errors

        attr_reader :cookbook_name_or_path

        option :berks,
          short:       "-b",
          long:        "--berks",
          description: "Generate cookbooks using Berkshelf dependency resolution.",
          boolean:     true,
          default:     nil

        option :kitchen,
          long:        "--kitchen CONFIGURATION",
          description: "Generate cookbooks with a specific Test Kitchen configuration (dokken|vagrant) - defaults to vagrant",
          default:     "vagrant"

        option :policy,
          short:        "-P",
          long:         "--policy",
          description:  "Generate a cookbook using Policyfile dependency resolution (default).",
          boolean:      true,
          default:      nil

        option :specs,
          short:        "-s",
          long:         "--specs",
          description:  "Generate a cookbook with sample ChefSpec specs",
          boolean:      true,
          default:      nil

        option :workflow,
          short:        "-w",
          long:         "--workflow",
          description:  "DEPRECATED: Generate a cookbook with a full #{ChefCLI::Dist::WORKFLOW} build cookbook.",
          boolean:      true,
          default:      false

        option :verbose,
          short:        "-V",
          long:         "--verbose",
          description:  "Show detailed output from the generator",
          boolean:      true,
          default:      false

        option :yaml,
          short:        "-y",
          long:         "--yaml",
          description:  "Generate a cookbook with YAML Recipe configuration file as the default.",
          boolean:      true,
          default:      nil

        option :pipeline,
          long:         "--pipeline PIPELINE",
          description:  "Use PIPELINE to set target branch to something other than master for the #{ChefCLI::Dist::WORKFLOW} build_cookbook",
          default:      "master"

        options.merge!(SharedGeneratorOptions.options)

        def initialize(params)
          @params_valid = true
          @cookbook_name = nil
          @policy_mode = true
          @verbose = false
          @specs = false
          super
        end

        def run
          read_and_validate_params
          if params_valid?
            setup_context
            msg("Generating cookbook #{cookbook_name}")
            chef_runner.converge
            msg("")
            emit_post_create_message
            0
          else
            err(opt_parser)
            1
          end
        rescue ChefCLI::ChefRunnerError => e
          err("ERROR: #{e}")
          1
        end

        def emit_post_create_message
          default_recipe_file = yaml ? "default.yml" : "default.rb"
          if have_delivery_config?
            msg("Your cookbook is ready. To setup the pipeline, type `cd #{cookbook_name_or_path}`, then run `delivery init`")
          else
            msg("Your cookbook is ready. Type `cd #{cookbook_name_or_path}` to enter it.")
            msg("\nThere are several commands you can run to get started locally developing and testing your cookbook.")
            msg("Type `delivery local --help` to see a full list of local testing commands.")
            msg("\nWhy not start by writing an InSpec test? Tests for the default recipe are stored at:\n")
            msg("test/integration/default/default_test.rb")
            msg("\nIf you'd prefer to dive right in, the default recipe can be found at:")
            msg("\nrecipes/#{default_recipe_file}\n")
          end
        end

        def setup_context
          super
          Generator.add_attr_to_context(:skip_git_init, cookbook_path_in_git_repo?)
          Generator.add_attr_to_context(:cookbook_root, cookbook_root)
          Generator.add_attr_to_context(:cookbook_name, cookbook_name)
          Generator.add_attr_to_context(:recipe_name, recipe_name)
          Generator.add_attr_to_context(:include_chef_repo_source, false)
          Generator.add_attr_to_context(:policy_name, policy_name)
          Generator.add_attr_to_context(:policy_run_list, policy_run_list)
          Generator.add_attr_to_context(:policy_local_cookbook, ".")

          Generator.add_attr_to_context(:enable_workflow, enable_workflow?)
          Generator.add_attr_to_context(:workflow_project_dir, cookbook_full_path)
          Generator.add_attr_to_context(:build_cookbook_parent_is_cookbook, true)
          Generator.add_attr_to_context(:workflow_project_git_initialized, have_git? && !cookbook_path_in_git_repo?)

          Generator.add_attr_to_context(:verbose, verbose?)
          Generator.add_attr_to_context(:specs, specs?)

          Generator.add_attr_to_context(:use_policyfile, policy_mode?)
          Generator.add_attr_to_context(:pipeline, pipeline)
          Generator.add_attr_to_context(:kitchen, kitchen)
          Generator.add_attr_to_context(:vscode_dir, create_vscode_dir?)
          Generator.add_attr_to_context(:yaml, yaml)
        end

        def kitchen
          config[:kitchen]
        end

        def pipeline
          config[:pipeline]
        end

        def yaml
          config[:yaml]
        end

        def policy_name
          cookbook_name
        end

        def policy_run_list
          "#{cookbook_name}::#{recipe_name}"
        end

        def recipe
          "cookbook"
        end

        def recipe_name
          "default"
        end

        def cookbook_name
          File.basename(cookbook_full_path)
        end

        def cookbook_root
          File.dirname(cookbook_full_path)
        end

        def cookbook_full_path
          File.expand_path(cookbook_name_or_path, Dir.pwd)
        end

        def policy_mode?
          @policy_mode
        end

        def enable_workflow?
          config[:workflow]
        end

        def verbose?
          @verbose
        end

        def specs?
          @specs
        end

        #
        # Is there a .delivery/cli.toml in the current dir or any of the parent dirs
        #
        # @return [Boolean]
        #
        def have_delivery_config?
          # delivery-cli's logic is to look recursively upward for
          # .delivery/cli.toml starting from pwd:
          # https://github.com/chef/delivery-cli/blob/22cbef3987ebd0aee98405b7e161a100edc87e49/src/delivery/config/mod.rs#L225-L247

          Pathname.pwd.ascend.any? { |path| path.join(".delivery/cli.toml").exist? }
        end

        def read_and_validate_params
          arguments = parse_options(params)
          @cookbook_name_or_path = arguments[0]
          if !@cookbook_name_or_path
            @params_valid = false
          elsif File.basename(@cookbook_name_or_path).include?("-")
            msg("Hyphens are discouraged in cookbook names as they may cause problems with custom resources. See https://docs.chef.io/workstation/ctl_chef/#chef-generate-cookbook for more information.")
          end

          if config[:berks] && config[:policy]
            err("Berkshelf and Policyfiles are mutually exclusive. Please specify only one.")
            @params_valid = false
          end

          if config[:berks]
            @policy_mode = false
          end

          if config[:verbose]
            @verbose = true
          end

          if config[:specs]
            @specs = true
          end

          true
        end

        def params_valid?
          @params_valid
        end

        def cookbook_path_in_git_repo?
          Pathname.new(cookbook_full_path).ascend do |dir|
            return true if File.directory?(File.join(dir.to_s, ".git"))
          end
          false
        end

        def create_vscode_dir?
          ::File.exist?("/Applications/Visual Studio Code.app") || ::File.exist?("#{ENV["APPDATA"]}\\Code")
        end
      end
    end
  end
end