def generate_metadata_json
      @logger.operation(:metadata_generation) do
        # note we will use the same tmpdir path inside and outside the
        # container only because it is non-trivial to invoke mktmpdir inside
        # the container.
        tmpdir, created = create_tmpdir

        # path constants
        src_knife_script_path = ::File.expand_path(
            ::File.join(__FILE__, '../../../../scripts', KNIFE_METADATA_SCRIPT_NAME))
        dst_knife_script_dir = tmpdir
        dst_knife_script_path = ::File.join(dst_knife_script_dir, KNIFE_METADATA_SCRIPT_NAME)
        jailed_repo_dir = ::File.join(tmpdir, UNDEFINED_COOKBOOK_NAME)
        jailed_cookbook_dir = (@cookbook.pos == '.' && jailed_repo_dir) || ::File.join(jailed_repo_dir, @cookbook.pos)
        jailed_metadata_json_path = ::File.join(jailed_cookbook_dir, JSON_METADATA)
        freed_metadata_dir = (@cookbook.pos == '.' && freed_dir) || ::File.join(freed_dir, @cookbook.pos)
        freed_metadata_json_path = ::File.join(freed_metadata_dir, JSON_METADATA)

        # in the multi-pass case we will run this scanner only on the first pass
        # so the 'metadata.json' file should not exist. the read-only scanner,
        # which is safe outside of containment, should be used subsequently.
        # the entire 'freed' directory should have been removed upon the next
        # successful retrieval so that this scanner will succeed.
        if ::File.file?(freed_metadata_json_path)
          raise MetadataError, "Refused to overwrite already-generated metadata file: #{freed_metadata_json_path}"
        end

        # jail the repo using the legacy semantics for copying files in and out
        # of jail.
        copy_out = { jailed_metadata_json_path => freed_metadata_json_path }

        # copy files into the jail once per repository (i.e. not once per
        # cookbook within the repository).
        if created
          copy_in = generate_copy_in(@cookbook.repo_dir, jailed_repo_dir)
          copy_in[src_knife_script_path] = dst_knife_script_path

          # note that at this point we previously used Warden as a container
          # for the copied-in files but now we assume that the current process
          # is already in a container (i.e. Docker) and so this copying is
          # more about creating a writable directory for knife than about
          # containment. the checked-out repo should be read-only to this
          # contained process due to running with limited privileges.
          do_copy_in(copy_in)
        end

        # HACK: support ad-hoc testing in dev-mode by using the current version
        # for rbenv shell.
        if ::ENV['RBENV_VERSION'].to_s.empty?
          ruby = 'ruby'
        else
          ruby = `which ruby`.chomp
        end

        # execute knife as a child process. any constraints are assumed to be
        # imposed on the current process by a container (timeout, memory, etc.)
        shell = ::RightGit::Shell::Default
        output = StringIO.new
        begin
          shell.execute(
            "#{ruby} #{dst_knife_script_path.inspect} #{jailed_cookbook_dir.inspect} 2>&1",
            directory: dst_knife_script_dir,
            outstream: output,
            raise_on_failure: true,
            set_env_vars: { LC_ALL: 'en_US.UTF-8' },  # character encoding for emitted JSON
            clear_env_vars: %w{BUNDLE_BIN_PATH BUNDLE_GEMFILE},
            timeout: KNIFE_METADATA_TIMEOUT)
          output = output.string
        rescue ::RightGit::Shell::ShellError => e
          output = output.string
          raise MetadataError, "Failed to run chef knife: #{e.message}\n#{output[0, 1024]}"
        end

        # free files from jail.
        do_copy_out(copy_out)

        # load and return freed metadata.
        return ::File.read(freed_metadata_json_path)
      end
    end
def generate_metadata_json
      @logger.operation(:metadata_generation) do
        # note we will use the same tmpdir path inside and outside the
        # container only because it is non-trivial to invoke mktmpdir inside
        # the container.
        tmpdir, created = create_tmpdir

        # path constants
        src_knife_script_path = ::File.expand_path(
            ::File.join(__FILE__, '../../../../scripts', KNIFE_METADATA_SCRIPT_NAME))
        dst_knife_script_dir = tmpdir
        dst_knife_script_path = ::File.join(dst_knife_script_dir, KNIFE_METADATA_SCRIPT_NAME)
        jailed_repo_dir = ::File.join(tmpdir, UNDEFINED_COOKBOOK_NAME)
        jailed_cookbook_dir = (@cookbook.pos == '.' && jailed_repo_dir) || ::File.join(jailed_repo_dir, @cookbook.pos)
        jailed_metadata_json_path = ::File.join(jailed_cookbook_dir, JSON_METADATA)
        freed_metadata_dir = (@cookbook.pos == '.' && freed_dir) || ::File.join(freed_dir, @cookbook.pos)
        freed_metadata_json_path = ::File.join(freed_metadata_dir, JSON_METADATA)

        # in the multi-pass case we will run this scanner only on the first pass
        # so the 'metadata.json' file should not exist. the read-only scanner,
        # which is safe outside of containment, should be used subsequently.
        # the entire 'freed' directory should have been removed upon the next
        # successful retrieval so that this scanner will succeed.
        if ::File.file?(freed_metadata_json_path)
          raise MetadataError, "Refused to overwrite already-generated metadata file: #{freed_metadata_json_path}"
        end

        # jail the repo using the legacy semantics for copying files in and out
        # of jail.
        copy_out = { jailed_metadata_json_path => freed_metadata_json_path }

        # copy files into the jail once per repository (i.e. not once per
        # cookbook within the repository).
        if created
          copy_in = generate_copy_in(@cookbook.repo_dir, jailed_repo_dir)
          copy_in[src_knife_script_path] = dst_knife_script_path

          # note that at this point we previously used Warden as a container
          # for the copied-in files but now we assume that the current process
          # is already in a container (i.e. Docker) and so this copying is
          # more about creating a writable directory for knife than about
          # containment. the checked-out repo should be read-only to this
          # contained process due to running with limited privileges.
          do_copy_in(copy_in)
        end

        # HACK: support ad-hoc testing in dev-mode by using the current version
        # for rbenv shell.
        if ::ENV['RBENV_VERSION'].to_s.empty?
          ruby = 'ruby'
        else
          ruby = `which ruby`.chomp
        end

        # execute knife as a child process. any constraints are assumed to be
        # imposed on the current process by a container (timeout, memory, etc.)
        shell = ::RightGit::Shell::Default
        output = StringIO.new
        begin
          shell.execute(
            "#{ruby} #{dst_knife_script_path.inspect} #{jailed_cookbook_dir.inspect} 2>&1",
            directory: dst_knife_script_dir,
            outstream: output,
            raise_on_failure: true,
            set_env_vars: { LC_ALL: 'en_US.UTF-8' },  # character encoding for emitted JSON
            clear_env_vars: %w{BUNDLE_BIN_PATH BUNDLE_GEMFILE},
            timeout: KNIFE_METADATA_TIMEOUT)
          output = output.string
        rescue ::RightGit::Shell::ShellError => e
          output = output.string
          raise MetadataError, "Failed to run chef knife: #{e.message}\n#{output[0, 1024]}"
        end

        # free files from jail.
        do_copy_out(copy_out)

        # load and return freed metadata.
        return ::File.read(freed_metadata_json_path)
      end
    end
Executes the 'metadata.rb' file from a cookbook. Because we don't want
 to evaluate arbitrary Ruby code, we need to sandbox it first.

 in order for knife metadata to succeed in the general case we need to
 copy some (but not all) of the cookbook directory AND its ancestors (if
 any) into the container. we will try and restrict copying to what might
 plausibly be referenced by 'metadata.rb' but this could be anything like
 a LICENSE, README, etc. the best heuristic seems to be to copy any file
 whose size is small (less than 128K) because 'metadata.rb' should not be
 executing binaries and should only consume text files of a reasonable
 size. if these restrictions cause a problem then the user is free to
 pre-knife his own 'metadata.json' file and check it into the repo.

 note the selection of the jailed cookbook dir is specific to the
 behavior of knife metadata. the cookbook name is defined when the
 'metadata.rb' declares the name attribute, but the name attribute is
 optional. when no name attribute is declared, the metadata automagically
 uses the parent directory name. this works okay so long as the parent
 directory name is actually the cookbook name. in the case of a repo with
 'metadata.rb' at the root (i.e. no checked-in parent directory) then the
 cookbook name is undefined. in this case, we want the cookbook name to
 be 'undefined' to remind the user to declare the name explicitly.

 === Returns
 @return [String] metadata JSON text