├── .expeditor
├── config.yml
├── run_linux_tests.sh
├── run_windows_tests.ps1
├── update_version.sh
└── verify.pipeline.yml
├── .github
├── CODEOWNERS
├── ISSUE_TEMPLATE
│ ├── BUG_TEMPLATE.md
│ ├── DESIGN_PROPOSAL.md
│ ├── ENHANCEMENT_REQUEST_TEMPLATE.md
│ └── SUPPORT_QUESTION.md
├── dependabot.yml
└── workflows
│ ├── lint.yaml
│ └── sonarqube.yml
├── .gitignore
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── Gemfile
├── LICENSE
├── README.md
├── Rakefile
├── VERSION
├── Vagrantfile
├── bin
└── chef-run
├── chef-apply.gemspec
├── coverage
└── .last_run.json
├── cspell.json
├── dev-doc
└── README.md
├── examples
└── chefconf_mainstage_demo
│ ├── README.md
│ └── deploy_website
│ ├── .gitignore
│ ├── LICENSE
│ ├── README.md
│ ├── chefignore
│ ├── files
│ └── default
│ │ └── demosite
│ │ ├── index.html
│ │ └── unikitten-plain.jpg
│ ├── metadata.rb
│ ├── recipes
│ └── default.rb
│ └── templates
│ └── default
│ ├── demosite.erb
│ └── yum_repo.erb
├── i18n
├── en.yml
└── errors
│ └── en.yml
├── lib
├── chef_apply.rb
└── chef_apply
│ ├── action
│ ├── base.rb
│ ├── converge_target.rb
│ ├── converge_target
│ │ └── ccr_failure_mapper.rb
│ ├── generate_local_policy.rb
│ ├── generate_temp_cookbook.rb
│ ├── generate_temp_cookbook
│ │ ├── recipe_lookup.rb
│ │ └── temp_cookbook.rb
│ ├── install_chef.rb
│ ├── install_chef
│ │ └── minimum_chef_version.rb
│ └── reporter.rb
│ ├── cli.rb
│ ├── cli
│ ├── help.rb
│ ├── options.rb
│ └── validation.rb
│ ├── config.rb
│ ├── error.rb
│ ├── errors
│ └── standard_error_resolver.rb
│ ├── file_fetcher.rb
│ ├── log.rb
│ ├── startup.rb
│ ├── status_reporter.rb
│ ├── target_host.rb
│ ├── target_host
│ ├── aix.rb
│ ├── linux.rb
│ ├── macos.rb
│ ├── solaris.rb
│ └── windows.rb
│ ├── target_resolver.rb
│ ├── telemeter.rb
│ ├── telemeter
│ └── patch.rb
│ ├── text.rb
│ ├── text
│ ├── error_translation.rb
│ └── text_wrapper.rb
│ ├── ui
│ ├── error_printer.rb
│ ├── plain_text_element.rb
│ ├── plain_text_header.rb
│ ├── terminal.rb
│ └── terminal
│ │ └── job.rb
│ └── version.rb
├── sonar-project.properties
├── spec
├── fixtures
│ └── custom_config.toml
├── integration
│ ├── chef-run_spec.rb
│ ├── fixtures
│ │ ├── chef_help.out
│ │ └── chef_version.out
│ └── spec_helper.rb
├── spec_helper.rb
├── support
│ └── matchers
│ │ └── output_to_terminal.rb
└── unit
│ ├── action
│ ├── base_spec.rb
│ ├── converge_target
│ │ └── ccr_failure_mapper_spec.rb
│ ├── converge_target_spec.rb
│ ├── generate_local_policy_spec.rb
│ ├── generate_temp_cookbook
│ │ ├── recipe_lookup_spec.rb
│ │ └── temp_cookbook_spec.rb
│ ├── generate_temp_cookbook_spec.rb
│ ├── install_chef
│ │ └── minimum_chef_version_spec.rb
│ └── install_chef_spec.rb
│ ├── cli
│ ├── options_spec.rb
│ └── validation_spec.rb
│ ├── cli_spec.rb
│ ├── config_spec.rb
│ ├── file_fetcher_spec.rb
│ ├── fixtures
│ └── multi-error.out
│ ├── log_spec.rb
│ ├── startup_spec.rb
│ ├── target_host
│ ├── aix_spec.rb
│ ├── linux_spec.rb
│ ├── macos_spec.rb
│ ├── solaris_spec.rb
│ └── windows_spec.rb
│ ├── target_host_spec.rb
│ ├── target_resolver_spec.rb
│ ├── telemeter_spec.rb
│ ├── text
│ └── error_translation_spec.rb
│ ├── ui
│ ├── error_printer_spec.rb
│ └── terminal_spec.rb
│ └── version_spec.rb
└── warning.txt
/.expeditor/config.yml:
--------------------------------------------------------------------------------
1 | # Documentation available at https://expeditor.chef.io/docs/getting-started/
2 | ---
3 |
4 | # Slack channel in Chef Software slack to send notifications about build failures, etc
5 | slack:
6 | notify_channel: chef-ws-notify
7 |
8 | # This publish is triggered by the `built_in:publish_rubygems` artifact_action.
9 | rubygems:
10 | - chef-apply
11 |
12 | github:
13 | # This deletes the GitHub PR branch after successfully merged into the release branch
14 | delete_branch_on_merge: true
15 | # The tag format to use (e.g. v1.0.0)
16 | version_tag_format: "v{{version}}"
17 | # allow bumping the minor release via label
18 | minor_bump_labels:
19 | - "Expeditor: Bump Version Minor"
20 | # allow bumping the major release via label
21 | major_bump_labels:
22 | - "Expeditor: Bump Version Major"
23 |
24 | changelog:
25 | rollup_header: Changes not yet released to rubygems.org
26 |
27 | subscriptions:
28 | # These actions are taken, in order they are specified, anytime a Pull Request is merged.
29 | - workload: pull_request_merged:{{github_repo}}:{{release_branch}}:*
30 | actions:
31 | - built_in:bump_version:
32 | ignore_labels:
33 | - "Expeditor: Skip Version Bump"
34 | - "Expeditor: Skip All"
35 | - bash:.expeditor/update_version.sh:
36 | only_if: built_in:bump_version
37 | - built_in:update_changelog:
38 | ignore_labels:
39 | - "Expeditor: Skip Changelog"
40 | - "Expeditor: Skip All"
41 | - built_in:build_gem:
42 | only_if: built_in:bump_version
43 |
44 | - workload: project_promoted:{{agent_id}}:*
45 | actions:
46 | - built_in:rollover_changelog
47 | - built_in:publish_rubygems
48 |
49 | pipelines:
50 | - verify:
51 | description: Pull Request validation tests
52 | public: true
53 |
--------------------------------------------------------------------------------
/.expeditor/run_linux_tests.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | #
3 | # This script runs a passed in command, but first setups up the bundler caching on the repo
4 |
5 | set -ue
6 |
7 | export USER="root"
8 | export LANG=C.UTF-8 LANGUAGE=C.UTF-8
9 |
10 | echo "--- bundle install"
11 |
12 | bundle config --local path vendor/bundle
13 | bundle install --jobs=7 --retry=3
14 |
15 | echo "+++ bundle exec task"
16 | bundle exec $@
17 |
--------------------------------------------------------------------------------
/.expeditor/run_windows_tests.ps1:
--------------------------------------------------------------------------------
1 | # Stop script execution when a non-terminating error occurs
2 | $ErrorActionPreference = "Stop"
3 | # This will run ruby test on windows platform
4 |
5 | Write-Output "--- Bundle install"
6 | bundle config set --local without docs debug
7 | bundle config set --local path vendor/bundle
8 | If ($lastexitcode -ne 0) { Exit $lastexitcode }
9 |
10 | bundle install --jobs=7 --retry=3
11 | If ($lastexitcode -ne 0) { Exit $lastexitcode }
12 |
13 | Write-Output "--- Bundle Execute"
14 |
15 | bundle exec rake spec
16 | If ($lastexitcode -ne 0) { Exit $lastexitcode }
--------------------------------------------------------------------------------
/.expeditor/update_version.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | #
3 | # After a PR merge, Chef Expeditor will bump the PATCH version in the VERSION file.
4 | # It then executes this file to update any other files/components with that new version.
5 | #
6 |
7 | set -evx
8 |
9 | sed -i -r "s/VERSION = \".*\"/VERSION = \"$(cat VERSION)\"/" lib/chef_apply/version.rb
10 |
11 | # Once Expeditor finshes executing this script, it will commit the changes and push
12 | # the commit as a new tag corresponding to the value in the VERSION file.
13 |
--------------------------------------------------------------------------------
/.expeditor/verify.pipeline.yml:
--------------------------------------------------------------------------------
1 | ---
2 | expeditor:
3 | cached_folders:
4 | - vendor
5 | defaults:
6 | buildkite:
7 | retry:
8 | automatic:
9 | limit: 1
10 | timeout_in_minutes: 30
11 |
12 | steps:
13 |
14 | - label: run-specs-ruby-2.7
15 | command:
16 | - .expeditor/run_linux_tests.sh "rake spec"
17 | expeditor:
18 | executor:
19 | docker:
20 | image: ruby:2.7
21 |
22 | - label: run-specs-ruby-3.0
23 | command:
24 | - .expeditor/run_linux_tests.sh "rake spec"
25 | expeditor:
26 | executor:
27 | docker:
28 | image: ruby:3.0
29 |
30 | - label: run-specs-ruby-3.1
31 | command:
32 | - .expeditor/run_linux_tests.sh "rake spec"
33 | expeditor:
34 | executor:
35 | docker:
36 | image: ruby:3.1
37 |
38 |
39 | - label: run-specs-ruby-2.7-windows
40 | command:
41 | - .expeditor/run_windows_tests.ps1
42 | expeditor:
43 | executor:
44 | docker:
45 | host_os: windows
46 | shell: ["powershell", "-Command"]
47 | image: rubydistros/windows-2019:2.7
48 | user: 'NT AUTHORITY\SYSTEM'
49 |
50 | - label: run-specs-ruby-3.0-windows
51 | command:
52 | - .expeditor/run_windows_tests.ps1
53 | expeditor:
54 | executor:
55 | docker:
56 | host_os: windows
57 | shell: ["powershell", "-Command"]
58 | image: rubydistros/windows-2019:3.0
59 | user: 'NT AUTHORITY\SYSTEM'
60 |
61 | - label: run-specs-ruby-3.1-windows
62 | command:
63 | - .expeditor/run_windows_tests.ps1
64 | expeditor:
65 | executor:
66 | docker:
67 | host_os: windows
68 | shell: ["powershell", "-Command"]
69 | image: rubydistros/windows-2019:3.1
70 | user: 'NT AUTHORITY\SYSTEM'
--------------------------------------------------------------------------------
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | # Order is important. The last matching pattern has the most precedence.
2 |
3 | * @chef/chef-workstation-owners @chef/chef-workstation-approvers @chef/chef-workstation-reviewers
4 | .expeditor/ @chef/jex-team
5 | *.md @chef/docs-team
6 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/BUG_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: � Bug Report
3 | about: If something isn't working as expected �.
4 | labels: "Status: Untriaged"
5 | ---
6 |
7 | ## Description
8 |
9 |
10 | ## Chef Apply Version
11 |
12 |
13 | ## Platform Version
14 |
15 |
16 | ## Replication Case
17 |
19 |
20 | ## Client Output
21 |
22 |
23 | ```
24 |
25 | ```
26 |
27 | ## Stacktrace
28 |
29 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/DESIGN_PROPOSAL.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Design Proposal
3 | about: I have a significant change I would like to propose and discuss before starting
4 | labels: "Status: Untriaged"
5 | ---
6 |
7 | ### When a Change Needs a Design Proposal
8 |
9 | A design proposal should be opened any time a change meets one of the following qualifications:
10 |
11 | - Significantly changes the user experience of a project in a way that impacts users.
12 | - Significantly changes the underlying architecture of the project in a way that impacts other developers.
13 | - Changes the development or testing process of the project such as a change of CI systems or test frameworks.
14 |
15 | ### Why We Use This Process
16 |
17 | - Allows all interested parties (including any community member) to discuss large impact changes to a project.
18 | - Serves as a durable paper trail for discussions regarding project architecture.
19 | - Forces design discussions to occur before PRs are created.
20 | - Reduces PR refactoring and rejected PRs.
21 |
22 | ---
23 |
24 |
25 |
26 | ## Motivation
27 |
28 |
33 |
34 | ## Specification
35 |
36 |
37 |
38 | ## Downstream Impact
39 |
40 |
41 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/ENHANCEMENT_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: 🚀 Enhancement Request
3 | about: I have a suggestion (and may want to implement it 🙂)!
4 | labels: "Status: Untriaged"
5 | ---
6 |
7 | ### Describe the Enhancement:
8 |
9 |
10 | ### Describe the Need:
11 |
12 |
13 | ### Current Alternative
14 |
15 |
16 | ### Can We Help You Implement This?:
17 |
18 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/SUPPORT_QUESTION.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: 🤗 Support Question
3 | about: If you have a question 💬, please check out our Slack
4 | ---
5 |
6 | We use GitHub issues to track bugs and feature requests. If you need help please post to our Mailing List or join the Chef Community Slack.
7 |
8 | * Chef Community Slack at https://community-slack.chef.io/.
9 | * Chef Mailing List https://discourse.chef.io/
10 |
11 | Support issues opened here will be closed and redirected to Slack or Discourse.
12 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: bundler
4 | directory: "/"
5 | schedule:
6 | interval: daily
7 | time: "06:00"
8 | timezone: America/Los_Angeles
9 | open-pull-requests-limit: 10
10 |
--------------------------------------------------------------------------------
/.github/workflows/lint.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | name: lint
3 |
4 | "on":
5 | pull_request:
6 | push:
7 | branches:
8 | - main
9 |
10 | jobs:
11 | chefstyle:
12 | runs-on: ubuntu-latest
13 | env:
14 | BUNDLE_WITHOUT: ruby_shadow:omnibus_package
15 | steps:
16 | - uses: actions/checkout@v3
17 | - uses: ruby/setup-ruby@v1
18 | with:
19 | ruby-version: 3.1
20 | bundler-cache: true
21 | - uses: r7kamura/rubocop-problem-matchers-action@v1 # this shows the failures in the PR
22 | - run: bundle exec chefstyle
23 |
24 | spellcheck:
25 | runs-on: ubuntu-latest
26 | steps:
27 | - uses: actions/checkout@v3
28 | - uses: carlosperate/download-file-action@v2.0.0
29 | id: download-custom-dictionary
30 | with:
31 | file-url: 'https://raw.githubusercontent.com/chef/chef_dictionary/main/chef.txt'
32 | file-name: 'chef_dictionary.txt'
33 | - uses: streetsidesoftware/cspell-action@v2.12.0
34 |
35 | coverage-test:
36 | name: Coverage
37 | runs-on: ubuntu-latest
38 | steps:
39 | - uses: actions/checkout@v3
40 | - name: Set up ruby 3.1
41 | uses: ruby/setup-ruby@v1
42 | with:
43 | ruby-version: 3.1
44 | bundler-cache: true
45 | - name: run specs
46 | run: bundle exec rake spec --trace
47 | - name: Simplecov Report
48 | uses: aki77/simplecov-report-action@v1
49 | with:
50 | token: ${{ secrets.GITHUB_TOKEN }}
51 | failedThreshold: 90
52 | resultPath: coverage/.last_run.json
53 |
--------------------------------------------------------------------------------
/.github/workflows/sonarqube.yml:
--------------------------------------------------------------------------------
1 | name: SonarQube scan
2 | on:
3 | # Trigger analysis when pushing to your main branches, and when creating a pull request.
4 | push:
5 | branches:
6 | - main # or the name of your main branch
7 | - develop
8 | - 'release/**'
9 | pull_request:
10 | types: [opened, synchronize, reopened]
11 |
12 | jobs:
13 | sonarqube:
14 | runs-on: ip-range-controlled
15 | # needs: [build]
16 | steps:
17 | - uses: actions/checkout@v3
18 | with:
19 | # Disabling shallow clone is recommended for improving relevancy of reporting
20 | fetch-depth: 0
21 | - name: SonarQube Scan
22 | uses: sonarsource/sonarqube-scan-action@master
23 | env:
24 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
25 | SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
26 | # If you wish to fail your job when the Quality Gate is red, uncomment the
27 | # following lines. This would typically be used to fail a deployment.
28 | # We do not recommend to use this in a pull request. Prefer using pull request
29 | # decoration instead.
30 | # - uses: sonarsource/sonarqube-quality-gate-action@master
31 | # timeout-minutes: 5
32 | # env:
33 | # SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .rspec_status
2 | .vagrant
3 | *.log
4 | coverage/
5 | .bundle
6 |
7 | # Chef Workstation is the source of truth for all locked gems
8 | Gemfile.lock
9 | .idea/
10 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | Please refer to the Chef Community Code of Conduct at https://www.chef.io/code-of-conduct/
2 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | Please refer to https://github.com/chef/chef/blob/main/CONTRIBUTING.md
2 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright:: Copyright (c) Chef Software Inc.
3 | # License:: Apache License, Version 2.0
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 | # http://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 |
18 | source "https://rubygems.org"
19 | gemspec
20 |
21 | group :development do
22 | gem "chefstyle", "2.2.2"
23 | gem "rake", ">= 10.1.0"
24 | gem "rspec", "~> 3.0"
25 | gem "simplecov"
26 | end
27 |
28 | group :debug do
29 | gem "pry"
30 | gem "pry-byebug"
31 | gem "pry-stack_explorer"
32 | gem "rb-readline"
33 | end
34 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Chef Apply
2 |
3 | [](https://badge.fury.io/rb/chef-apply)
4 |
5 | The ad-hoc execution tool for the Chef Infra ecosystem.
6 |
7 | **Umbrella Project**: [Workstation](https://github.com/chef/chef-oss-practices/blob/main/projects/chef-workstation.md)
8 |
9 | * **[Project State](https://github.com/chef/chef-oss-practices/blob/main/repo-management/repo-states.md):** Active
10 | * **Issues [Response Time Maximum](https://github.com/chef/chef-oss-practices/blob/main/repo-management/repo-states.md):** 14 days
11 | * **Pull Request [Response Time Maximum](https://github.com/chef/chef-oss-practices/blob/main/repo-management/repo-states.md):** 14 days
12 |
13 | ## Installation
14 |
15 | Add this line to your application's Gemfile:
16 |
17 | ```ruby
18 | gem 'chef-apply'
19 | ```
20 |
21 | And then execute:
22 |
23 | ```shell
24 | bundle
25 | ```
26 |
27 | Or install it yourself as:
28 |
29 | ```shell
30 | gem install chef-apply
31 | ```
32 |
33 | ## Contributing/Development
34 |
35 | Please read our [Community Contributions Guidelines](https://docs.chef.io/community_contributions.html), and ensure you are signing all your commits with DCO sign-off.
36 |
37 | The general development process is:
38 |
39 | 1. Fork this repo and clone it to your workstation.
40 | 2. Create a feature branch for your change.
41 | 3. Write code and tests.
42 | 4. Push your feature branch to GitHub and open a pull request against main.
43 |
44 | Once your repository is set up, you can start working on the code. We do utilize RSpec for test driven development, so you'll need to get a development environment running. Follow the above procedure ("Installing from Git") to get your local copy of the source running.
45 |
46 | ## License and Copyright
47 |
48 | Copyright 2008-2021, Chef Software, Inc.
49 |
50 | ```text
51 | Licensed under the Apache License, Version 2.0 (the "License");
52 | you may not use this file except in compliance with the License.
53 | You may obtain a copy of the License at
54 |
55 | http://www.apache.org/licenses/LICENSE-2.0
56 |
57 | Unless required by applicable law or agreed to in writing, software
58 | distributed under the License is distributed on an "AS IS" BASIS,
59 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
60 | See the License for the specific language governing permissions and
61 | limitations under the License.
62 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require "bundler/gem_tasks"
2 |
3 | begin
4 | require "rspec/core/rake_task"
5 |
6 | RSpec::Core::RakeTask.new(:spec) do |t|
7 | t.pattern = FileList["spec/**/*_spec.rb"]
8 | t.verbose = false
9 | t.rspec_opts = ["--color", "--format", "documentation"]
10 | end
11 | rescue LoadError
12 | desc "rspec is not installed, this task is disabled"
13 | task :spec do
14 | abort "rspec is not installed. bundle install first to make sure all dependencies are installed."
15 | end
16 | end
17 |
18 | begin
19 | require "chefstyle"
20 | require "rubocop/rake_task"
21 | desc "Run Chefstyle tests"
22 | RuboCop::RakeTask.new(:style) do |task|
23 | task.options += ["--display-cop-names", "--no-color"]
24 | end
25 | rescue LoadError
26 | puts "chefstyle gem is not installed. bundle install first to make sure all dependencies are installed."
27 | end
28 |
29 | task :console do
30 | require "irb"
31 | require "irb/completion"
32 | require "chef_apply"
33 | ARGV.clear
34 | IRB.start
35 | end
36 |
37 | task default: %i{style spec}
38 |
--------------------------------------------------------------------------------
/VERSION:
--------------------------------------------------------------------------------
1 | 0.9.6
--------------------------------------------------------------------------------
/Vagrantfile:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright:: Copyright (c) 2018 Chef Software Inc.
3 | # License:: Apache License, Version 2.0
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 | # http://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 |
18 | Vagrant.configure("2") do |config|
19 | config.ssh.forward_agent = true
20 |
21 | 1.upto(5).each do |num|
22 | name = "ubuntu#{num}"
23 | config.vm.define name do |node|
24 | node.vm.box = "bento/ubuntu-16.04"
25 | node.vm.hostname = "#{name}"
26 | node.vm.network "private_network", ip: "192.168.33.5#{num}"
27 | node.vm.network :forwarded_port, guest: 22, host: "222#{num}", id: "ssh", auto_correct: true
28 | # for convenience, use a common key so chef-apply can be run across multiple VMs
29 | node.ssh.private_key_path = ["~/.vagrant.d/insecure_private_key"]
30 | node.ssh.insert_key = false
31 | node.vm.provider "virtualbox" do |v|
32 | # Keep these light, we're not really using them except to
33 | # run chef client
34 | v.memory = 512
35 | v.cpus = 1
36 | # Allow host caching - many images don't have it by default but it significantly speeds up
37 | # disk IO (such as installing chef via dpkg)
38 | v.customize ["storagectl", :id, "--name", "SATA Controller", "--hostiocache", "on"]
39 | # disable logging client console on host
40 | v.customize ["modifyvm", :id, "--uartmode1", "disconnected"]
41 | end
42 | node.vm.provision "shell", inline: "echo 'MaxAuthTries 25' >> /etc/ssh/sshd_config"
43 | node.vm.provision "shell", inline: "service sshd restart"
44 | end
45 | end
46 |
47 | config.vm.define "windows1" do |node|
48 | node.vm.box = "tas50/windows_2016"
49 | node.vm.communicator = "winrm"
50 |
51 | # Admin user name and password
52 | node.winrm.username = "vagrant"
53 | node.winrm.password = "vagrant"
54 |
55 | node.vm.guest = :windows
56 | node.windows.halt_timeout = 15
57 |
58 | node.vm.network "private_network", ip: "192.168.33.61"
59 | node.vm.network :forwarded_port, guest: 3389, host: 3389, id: "rdp", auto_correct: true
60 | node.vm.network :forwarded_port, guest: 22, host: 2231, id: "ssh", auto_correct: true
61 |
62 | node.vm.provider :virtualbox do |v, override|
63 | # v.gui = true
64 | v.customize ["modifyvm", :id, "--memory", 2048]
65 | v.customize ["modifyvm", :id, "--cpus", 2]
66 | v.customize ["setextradata", "global", "GUI/SuppressMessages", "all" ]
67 | end
68 | end
69 |
70 | config.vm.define "macosx-test" do |node|
71 |
72 | # config.vm.define "macosx-test"
73 | node.vm.box = "yzgyyang/macOS-10.14"
74 | # config.ssh.private_key_path = ["~/.vagrant.d/insecure_private_key"]
75 | # Use NFS for the shared folder
76 | node.vm.synced_folder ".", "/vagrant",
77 | id: "core",
78 | nfs: true,
79 | mount_options: ["nolock,vers=3,udp,noatime,actimeo=1,resvport"],
80 | export_options: ["async,insecure,no_subtree_check,no_acl,no_root_squash"]
81 |
82 | # NFS needs host-only network
83 | node.vm.network :private_network, ip: "172.16.2.42"
84 | # config.vm.network :forwarded_port, guest: 22, host: "2232", id: "ssh", auto_correct: true
85 |
86 | node.vm.provider :virtualbox do |virtualbox|
87 | virtualbox.name = "macosx-test"
88 | virtualbox.memory = 4096
89 | virtualbox.cpus = 2
90 |
91 | # Show gui, incl. some power
92 | virtualbox.gui = true
93 |
94 | # Some needed OSX configs
95 | virtualbox.customize ["modifyvm", :id, "--cpuid-set", "00000001", "000106e5", "00100800", "0098e3fd", "bfebfbff"]
96 | virtualbox.customize ["setextradata", :id, "VBoxInternal/Devices/efi/0/Config/DmiSystemProduct", "MacBookPro11,3"]
97 | virtualbox.customize ["setextradata", :id, "VBoxInternal/Devices/efi/0/Config/DmiSystemVersion", "1.0"]
98 | virtualbox.customize ["setextradata", :id, "VBoxInternal/Devices/efi/0/Config/DmiBoardProduct", "Iloveapple"]
99 | virtualbox.customize ["setextradata", :id, "VBoxInternal/Devices/smc/0/Config/DeviceKey", "ourhardworkbythesewordsguardedpleasedontsteal(c)AppleComputerInc"]
100 | virtualbox.customize ["setextradata", :id, "VBoxInternal/Devices/smc/0/Config/GetKeyFromRealSMC", "1"]
101 |
102 | # set resolution on OSX:
103 | # 0,1,2,3,4,5 :: 640x480, 800x600, 1024x768, 1280x1024, 1440x900, 1920x1200
104 | virtualbox.customize ["setextradata", :id, "VBoxInternal2/EfiGopMode", "4"]
105 | end
106 | end
107 |
108 | config.vm.define "solaris" do |node|
109 | node.vm.box = "jonatasbaldin/solaris11"
110 | node.vm.hostname = "solaris"
111 | node.vm.network "private_network", ip: "192.168.33.62"
112 | node.vm.network :forwarded_port, guest: 22, host: "2232", id: "ssh", auto_correct: true
113 | node.ssh.private_key_path = ["~/.vagrant.d/insecure_private_key"]
114 | node.ssh.insert_key = false
115 | end
116 |
117 | config.vm.define "solaris4" do |node|
118 | config.vm.box = "MartijnDwars/solaris11_4"
119 | config.vm.box_version = "1.0.0"
120 | node.vm.hostname = "solaris4"
121 | node.vm.network "private_network", ip: "192.168.33.65"
122 | node.vm.network :forwarded_port, guest: 22, host: "2235", id: "ssh", auto_correct: true
123 | node.ssh.private_key_path = ["~/.vagrant.d/insecure_private_key"]
124 | node.ssh.insert_key = false
125 | end
126 | end
127 |
--------------------------------------------------------------------------------
/bin/chef-run:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | #
4 | # Copyright:: Copyright (c) 2018 Chef Software Inc.
5 | # License:: Apache License, Version 2.0
6 | #
7 | # Licensed under the Apache License, Version 2.0 (the "License");
8 | # you may not use this file except in compliance with the License.
9 | # You may obtain a copy of the License at
10 | #
11 | # http://www.apache.org/licenses/LICENSE-2.0
12 |
13 | # Unless required by applicable law or agreed to in writing, software
14 | # distributed under the License is distributed on an "AS IS" BASIS,
15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 | # See the License for the specific language governing permissions and
17 | # limitations under the License.
18 | #
19 | require "chef_apply/startup"
20 |
21 | # Perform initialization tasks then hand off control to
22 | # CLI to run the command.
23 | ChefApply::Startup.new(ARGV).run(enforce_license: true)
24 |
--------------------------------------------------------------------------------
/chef-apply.gemspec:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright:: Copyright (c) 2018 Chef Software Inc.
3 | # License:: Apache License, Version 2.0
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 | # http://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 |
18 | lib = File.expand_path("lib", __dir__)
19 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
20 | require "chef_apply/version"
21 |
22 | Gem::Specification.new do |spec|
23 | spec.name = "chef-apply"
24 | spec.version = ChefApply::VERSION
25 | spec.authors = ["Chef Software, Inc"]
26 | spec.email = ["workstation@chef.io"]
27 |
28 | spec.summary = "The ad-hoc execution tool for the Chef ecosystem."
29 | spec.description = "Ad-hoc management of individual nodes and devices."
30 | spec.homepage = "https://github.com/chef/chef-apply"
31 | spec.license = "Apache-2.0"
32 | spec.required_ruby_version = ">= 2.7"
33 |
34 | spec.files = %w{Rakefile LICENSE warning.txt} +
35 | Dir.glob("Gemfile*") + # Includes Gemfile and locks
36 | Dir.glob("*.gemspec") +
37 | Dir.glob("{bin,i18n,lib}/**/*", File::FNM_DOTMATCH).reject { |f| File.directory?(f) }
38 | spec.bindir = "bin"
39 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
40 | spec.require_paths = ["lib"]
41 |
42 | spec.add_dependency "mixlib-cli" # Provides argument handling DSL for CLI applications
43 | spec.add_dependency "mixlib-config", ">= 3.0.5" # shared chef configuration library that simplifies managing a configuration file
44 | spec.add_dependency "mixlib-log" # Basis for our traditional logger
45 | spec.add_dependency "mixlib-install" # URL resolver + install tool for chef products
46 | spec.add_dependency "r18n-desktop" # easy path to message text management via localization gem...
47 | spec.add_dependency "toml-rb" # This isn't ideal because mixlib-config uses 'tomlrb' but that library does not support a dumper
48 | spec.add_dependency "train-core", "~> 3.0" # remote connection management over ssh, winrm
49 | spec.add_dependency "train-winrm" # winrm transports were pulled out into this plugin
50 | spec.add_dependency "pastel" # A color library
51 | spec.add_dependency "tty-spinner" # Pretty output for status updates in the CLI
52 | spec.add_dependency "chef", ">= 16.0" # Needed to load cookbooks
53 | spec.add_dependency "chef-cli", ">= 2.0.10 " # Policyfile
54 | spec.add_dependency "chef-telemetry", ">= 1.0.2"
55 | spec.add_dependency "license-acceptance", ">= 1.0.11", "< 3"
56 |
57 | spec.post_install_message = File.read(File.expand_path("warning.txt", __dir__))
58 | end
59 |
--------------------------------------------------------------------------------
/coverage/.last_run.json:
--------------------------------------------------------------------------------
1 | {
2 | "result": {
3 | "line": 94.29
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/cspell.json:
--------------------------------------------------------------------------------
1 | // cSpell Settings
2 | {
3 | // Version of the setting file. Always 0.1
4 | "version": "0.2",
5 | // language - current active spelling language
6 | "language": "en",
7 | "dictionaryDefinitions": [
8 | {
9 | "name": "chef",
10 | "path": "https://raw.githubusercontent.com/chef/chef_dictionary/main/chef.txt",
11 | "description": "Custom Chef Dictionary"
12 | }
13 | ],
14 | "dictionaries": ["chef"],
15 | // words - list of words to be always considered correct
16 | "words": [
17 | "MACHINENAME",
18 | "ipaddress",
19 | "mypassword",
20 | "unikitten",
21 | "repos",
22 | "demosite",
23 | "tempfile",
24 | "CHEFTRN",
25 | "CHEFUPL",
26 | "Compat",
27 | "LASTEXITCODE",
28 | "tempdir",
29 | "errid",
30 | "extname",
31 | "ljust",
32 | "converger",
33 | "mktmpdir",
34 | "levenshteinian",
35 | "APPDATA",
36 | "multispinner",
37 | "MKTEMP",
38 | "CHEFLIC",
39 | "CHEFINS",
40 | "CHEFVAL",
41 | "mountpoint",
42 | "uploader",
43 | "trainerrormapper",
44 | "CHEFNET",
45 | "tmpdir",
46 | "CHEFTARG",
47 | "CHEFPOLICY",
48 | "CHEFCCR",
49 | "CHEFRANGE",
50 | "CHEFAPI",
51 | "CHEFMULTI",
52 | "CHEFRMT",
53 | "CHEFINT",
54 | "Alnum",
55 | "succ",
56 | "proto",
57 | "installp",
58 | "Qube",
59 | "objc",
60 | "kamura",
61 | "carlosperate",
62 | "Simplecov",
63 | "simplecov"
64 | ],
65 | // flagWords - list of words to be always considered incorrect
66 | // This is useful for offensive words and common spelling errors.
67 | // For example "hte" should be "the"
68 | "flagWords": [
69 | "hte"
70 | ],
71 | "ignorePaths": [
72 | "CHANGELOG.md",
73 | "**/*.gemspec",
74 | "**/Gemfile.lock",
75 | "**/Gemfile",
76 | ".expeditor/**/*",
77 | "**/*.yml",
78 | "**/*.toml",
79 | "**/Berksfile",
80 | "spec/**",
81 | "cspell.json",
82 | "Vagrantfile",
83 | "**/chefignore"
84 | ],
85 | "ignoreRegExpList": [
86 | // Ignore "'s" at the end of a word. If "Chef" is an accepted word, so is "Chef's".
87 | "/'s\\b/",
88 | // Ignore "'d" at the end of a word. If "dup" is an accepted word, so is "dup'd".
89 | "/'d\\b/"
90 | ]
91 | }
92 |
--------------------------------------------------------------------------------
/dev-doc/README.md:
--------------------------------------------------------------------------------
1 | # Development Docs
2 |
3 | chef-run/chef-apply is a tool to execute ad-hoc tasks on one or more target nodes using Chef Infra Client. To start with, familiarize yourself with chef-run’s arguments and flags by running chef-run -h
4 | link
5 |
6 | ## Development process
7 |
8 | 1. Fork this repo and clone it to your development system.
9 | 1. Create a feature branch for your change.
10 | 1. Write code and tests.
11 | 1. Push your feature branch to GitHub and open a pull request.
12 |
13 | ## Development setup
14 |
15 | ### With Vagrant box
16 |
17 | 1. This repository contains a Vagrantfile with machines; Ubuntu, Windows, MacOS. You need to have Vagrant and VirtualBox preinstalled
18 | 1. Make sure to add machine in host file e.g (in /etc/hosts add - 127.0.0.1 ubuntu1)
19 | 1. `vagrant status` to check status of VirtualBox created
20 | 1. `vagrant up MACHINENAME`
21 | 1. Once the machine is up, run this command format( based on user,port, and machine name)
22 |
23 | ```shell
24 | bundle exec chef-run ssh://vagrant@ubuntu1:2235 directory /tmp/foo --identity-file ~/.vagrant.d/insecure_private_key
25 | ```
26 |
27 | This will install chef client on desired platform using chef-apply
28 | To suspend a Vagrant machine use
29 |
30 | ```shell
31 | vagrant suspend MACHINENAME
32 | ```
33 |
34 | ### With instance
35 |
36 | ```shell
37 | bundle exec chef-run ssh://test@ipaddress directory /tmp/foo --password mypassword
38 | ```
39 |
40 | **Here is some pre-run use cases, and interim statuses that chef-run displays.**
41 |
42 | ```shell
43 | bundle exec chef-run ssh://my_user@host1:2222 directory /tmp/foo --identity-file ~/.ssh/id_rsa user test1 action=create
44 | ```
45 |
46 | ```shell
47 | [✔] Packaging cookbook... done!
48 | [✔] Generating local policyfile... exporting... done!
49 | [✔] Applying user[test1] from resource to target.
50 | └── [✔] [my_user] Successfully converged user[test1].
51 | ```
52 |
53 | Valid actions are:
54 |
55 | :nothing, :create, :remove, :modify, :manage, :lock, :unlock
56 |
57 | For more information, please consult the documentation for this resource:
58 |
59 |
60 | ```shell
61 | bundle exec chef-run ssh://my_user@host1:2222 directory /tmp/foo --identity-file ~/.ssh/id_rsa user test1 action=remove
62 | ```
63 |
64 | ```shell
65 | [✔] Packaging cookbook... done!
66 | [✔] Generating local policyfile... exporting... done!
67 | [✔] Applying user[test1] from resource to target.
68 | └── [✔] [my_user] Successfully converged user[test1].
69 | ```
70 |
71 | * To run test use rspec e.g. ```bundle exec rspec spec/unit/target_host_spec.rb```
72 |
73 | * To debug, use byebug
74 |
--------------------------------------------------------------------------------
/examples/chefconf_mainstage_demo/README.md:
--------------------------------------------------------------------------------
1 | h2. Deploy a simple website in two easy commands from this directory.
2 |
3 | 1. chef-run rhel-01 package ntp action=install
4 | 1. chef-run rhel-01 deploy_website
5 | 1. chef-run rhel-0[1:3] deploy_website
6 |
--------------------------------------------------------------------------------
/examples/chefconf_mainstage_demo/deploy_website/.gitignore:
--------------------------------------------------------------------------------
1 | .vagrant
2 | *~
3 | *#
4 | .#*
5 | \#*#
6 | .*.sw[a-z]
7 | *.un~
8 |
9 | # Bundler
10 | Gemfile.lock
11 | gems.locked
12 | bin/*
13 | .bundle/*
14 |
15 | # test kitchen
16 | .kitchen/
17 | .kitchen.local.yml
18 |
19 | # Chef
20 | Berksfile.lock
21 | .zero-knife.rb
22 | Policyfile.lock.json
23 |
--------------------------------------------------------------------------------
/examples/chefconf_mainstage_demo/deploy_website/README.md:
--------------------------------------------------------------------------------
1 | # deploy_website
2 |
3 | Example cookbook for deploying an nginx site to RedHat 7 hosts.
4 |
--------------------------------------------------------------------------------
/examples/chefconf_mainstage_demo/deploy_website/chefignore:
--------------------------------------------------------------------------------
1 | # Put files/directories that should be ignored in this file when uploading
2 | # to a chef-server or supermarket.
3 | # Lines that start with '# ' are comments.
4 |
5 | # OS generated files #
6 | ######################
7 | .DS_Store
8 | Icon?
9 | nohup.out
10 | ehthumbs.db
11 | Thumbs.db
12 |
13 | # SASS #
14 | ########
15 | .sass-cache
16 |
17 | # EDITORS #
18 | ###########
19 | \#*
20 | .#*
21 | *~
22 | *.sw[a-z]
23 | *.bak
24 | REVISION
25 | TAGS*
26 | tmtags
27 | *_flymake.*
28 | *_flymake
29 | *.tmproj
30 | .project
31 | .settings
32 | mkmf.log
33 |
34 | ## COMPILED ##
35 | ##############
36 | a.out
37 | *.o
38 | *.pyc
39 | *.so
40 | *.com
41 | *.class
42 | *.dll
43 | *.exe
44 | */rdoc/
45 |
46 | # Testing #
47 | ###########
48 | .watchr
49 | .rspec
50 | spec/*
51 | spec/fixtures/*
52 | test/*
53 | features/*
54 | examples/*
55 | Guardfile
56 | Procfile
57 | .kitchen*
58 | kitchen.yml*
59 | .rubocop.yml
60 | spec/*
61 | Rakefile
62 | .travis.yml
63 | .foodcritic
64 | .codeclimate.yml
65 |
66 | # SCM #
67 | #######
68 | .git
69 | */.git
70 | .gitignore
71 | .gitmodules
72 | .gitconfig
73 | .gitattributes
74 | .svn
75 | */.bzr/*
76 | */.hg/*
77 | */.svn/*
78 |
79 | # Berkshelf #
80 | #############
81 | Berksfile
82 | Berksfile.lock
83 | cookbooks/*
84 | tmp
85 |
86 | # Bundler #
87 | ###########
88 | vendor/*
89 |
90 | # Policyfile #
91 | ##############
92 | Policyfile.rb
93 | Policyfile.lock.json
94 |
95 | # Cookbooks #
96 | #############
97 | CONTRIBUTING*
98 | CHANGELOG*
99 | TESTING*
100 |
101 | # Vagrant #
102 | ###########
103 | .vagrant
104 | Vagrantfile
105 |
--------------------------------------------------------------------------------
/examples/chefconf_mainstage_demo/deploy_website/files/default/demosite/index.html:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/examples/chefconf_mainstage_demo/deploy_website/files/default/demosite/unikitten-plain.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chef/chef-apply/ca49954e9525724e98f2ca2b9de53b646d08e3e9/examples/chefconf_mainstage_demo/deploy_website/files/default/demosite/unikitten-plain.jpg
--------------------------------------------------------------------------------
/examples/chefconf_mainstage_demo/deploy_website/metadata.rb:
--------------------------------------------------------------------------------
1 | name "deploy_website"
2 | maintainer "Chef Workstation Team"
3 | maintainer_email "workstation@chef.io"
4 | license "Apache-2.0"
5 | description "Installs/Configures deploy_website"
6 | long_description "Installs/Configures deploy_website"
7 | version "0.1.0"
8 | chef_version ">= 14.1.1" if respond_to?(:chef_version)
9 |
--------------------------------------------------------------------------------
/examples/chefconf_mainstage_demo/deploy_website/recipes/default.rb:
--------------------------------------------------------------------------------
1 | #
2 | # Cookbook:: deploy_website
3 | # Recipe:: default
4 | #
5 |
6 | # yum_repository 'zenoss' do
7 | # description "Zenoss Stable repo"
8 | # baseurl "http://dev.zenoss.com/yum/stable/"
9 | # gpgkey 'http://dev.zenoss.com/yum/RPM-GPG-KEY-zenoss'
10 | # action :create
11 | # end
12 |
13 | template "/etc/yum.repos.d/nginx.repo" do
14 | source "yum_repo.erb"
15 | end
16 |
17 | package "nginx"
18 |
19 | file "/etc/nginx/conf.d/default.conf" do
20 | action :delete
21 | end
22 |
23 | remote_directory "/var/www/demosite" do
24 | source "demosite"
25 | mode "0755"
26 | recursive true
27 | action :create
28 | end
29 |
30 | # %w(sites-available sites-enabled).each do |dir|
31 | # directory "/etc/nginx/#{dir}" do
32 | # action :create
33 | # end
34 | # end
35 |
36 | template "/etc/nginx/conf.d/demosite.conf" do
37 | source "demosite.erb"
38 | notifies :restart, "service[nginx]"
39 | end
40 |
41 | # link "/etc/nginx/sites-enabled/demosite" do
42 | # to "/etc/nginx/sites-available/demosite"
43 | # notifies :restart, "service[nginx]"
44 | # end
45 |
46 | service "nginx" do
47 | action %i{start enable}
48 | end
49 |
--------------------------------------------------------------------------------
/examples/chefconf_mainstage_demo/deploy_website/templates/default/demosite.erb:
--------------------------------------------------------------------------------
1 | server {
2 | listen 80 default_server;
3 | listen [::]:80 default_server;
4 | root /var/www/demosite;
5 | index index.html;
6 | server_name '<%= node['cloud']['public_ipv4'] %>';
7 | location / {
8 | try_files $uri $uri/ =404;
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/examples/chefconf_mainstage_demo/deploy_website/templates/default/yum_repo.erb:
--------------------------------------------------------------------------------
1 | [nginx]
2 | name=nginx repo
3 | baseurl=http://nginx.org/packages/rhel/7/$basearch/
4 | gpgcheck=0
5 | enabled=1
6 |
--------------------------------------------------------------------------------
/lib/chef_apply.rb:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright:: Copyright (c) 2018 Chef Software Inc.
3 | # License:: Apache License, Version 2.0
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 | # http://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 |
18 | module ChefApply
19 |
20 | end
21 |
--------------------------------------------------------------------------------
/lib/chef_apply/action/base.rb:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright:: Copyright (c) 2017 Chef Software Inc.
3 | # License:: Apache License, Version 2.0
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 | # http://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 |
18 | require "chef/telemeter"
19 | require_relative "../error"
20 |
21 | module ChefApply
22 | module Action
23 | # Derive new Actions from Action::Base
24 | # "target_host" is a TargetHost that the action is being applied to. May be nil
25 | # if the action does not require a target.
26 | # "config" is hash containing any options that your command may need
27 | #
28 | # Implement perform_action to perform whatever action your class is intended to do.
29 | # Run time will be captured via telemetry and categorized under ":action" with the
30 | # unqualified class name of your Action.
31 | class Base
32 | attr_reader :target_host, :config
33 |
34 | def initialize(config = {})
35 | c = config.dup
36 | @target_host = c.delete :target_host
37 | # Remaining options are for child classes to make use of.
38 | @config = c
39 | end
40 |
41 | def run(&block)
42 | @notification_handler = block
43 | Telemeter.timed_action_capture(self) do
44 |
45 | perform_action
46 | rescue StandardError => e
47 | # Give the caller a chance to clean up - if an exception is
48 | # raised it'll otherwise get routed through the executing thread,
49 | # providing no means of feedback for the caller's current task.
50 | notify(:error, e)
51 | @error = e
52 |
53 | end
54 | # Raise outside the block to ensure that the telemetry capture completes
55 | raise @error unless @error.nil?
56 | end
57 |
58 | def name
59 | self.class.name.split("::").last
60 | end
61 |
62 | def perform_action
63 | raise NotImplemented
64 | end
65 |
66 | def notify(action, *args)
67 | return if @notification_handler.nil?
68 |
69 | ChefApply::Log.debug("[#{self.class.name}] Action: #{action}, Action Data: #{args}")
70 | @notification_handler.call(action, args) if @notification_handler
71 | end
72 | end
73 | end
74 | end
75 |
--------------------------------------------------------------------------------
/lib/chef_apply/action/converge_target/ccr_failure_mapper.rb:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright:: Copyright (c) 2017 Chef Software Inc.
3 | # License:: Apache License, Version 2.0
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 | # http://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 |
18 | require_relative "../../error"
19 |
20 | module ChefApply
21 | module Action
22 | class ConvergeTarget
23 | # This converts chef client run failures
24 | # to human-friendly exceptions with detail
25 | # and remediation steps based on the failure type.
26 | class CCRFailureMapper
27 | attr_reader :params
28 |
29 | def initialize(exception, params)
30 | @params = params
31 | @cause_line = exception
32 | end
33 |
34 | def raise_mapped_exception!
35 | if @cause_line.nil?
36 | raise RemoteChefRunFailedToResolveError.new(params[:failed_report_path])
37 | else
38 | errid, *args = exception_args_from_cause
39 | if errid.nil?
40 | raise RemoteChefClientRunFailedUnknownReason.new
41 | else
42 | raise RemoteChefClientRunFailed.new(errid, *args)
43 | end
44 |
45 | end
46 | end
47 |
48 | # Ideally we will write a custom handler to package up data we care
49 | # about and present it more directly https://docs.chef.io/handlers.html
50 | # For now, we'll just match the most common failures based on their
51 | # messages.
52 | def exception_args_from_cause
53 | # Ordering is important below. Some earlier tests are more detailed
54 | # cases of things that will match more general tests further down.
55 | case @cause_line
56 | when /.*had an error:(.*:)\s+(.*$)/
57 | # Some invalid property value cases, among others.
58 | ["CHEFCCR002", $2]
59 | when /.*Chef::Exceptions::ValidationFailed:\s+Option action must be equal to one of:\s+(.*)!\s+You passed :(.*)\./
60 | # Invalid action - specialization of invalid property value, below
61 | ["CHEFCCR003", $2, $1]
62 | when /.*Chef::Exceptions::ValidationFailed:\s+(.*)/
63 | # Invalid resource property value
64 | ["CHEFCCR004", $1]
65 | when /.*NameError: undefined local variable or method `(.+)' for cookbook.+/
66 | # Invalid resource type in most cases
67 | ["CHEFCCR005", $1]
68 | when /.*NoMethodError: undefined method `(.+)' for cookbook.+/
69 | # Invalid resource type in most cases
70 | ["CHEFCCR005", $1]
71 | when /.*undefined method `(.*)' for (.+)/
72 | # Unknown resource property
73 | ["CHEFCCR006", $1, $2]
74 |
75 | # Below would catch the general form of most errors, but the
76 | # message itself in those lines is not generally aligned
77 | # with the UX we want to provide.
78 | # when /.*Exception|Error.*:\s+(.*)/
79 | else
80 | nil
81 | end
82 | end
83 |
84 | class RemoteChefClientRunFailed < ChefApply::ErrorNoLogs
85 | def initialize(id, *args); super(id, *args); end
86 | end
87 |
88 | class RemoteChefClientRunFailedUnknownReason < ChefApply::ErrorNoStack
89 | def initialize(); super("CHEFCCR099"); end
90 | end
91 |
92 | class RemoteChefRunFailedToResolveError < ChefApply::ErrorNoStack
93 | def initialize(path); super("CHEFCCR001", path); end
94 | end
95 |
96 | end
97 |
98 | end
99 | end
100 | end
101 |
--------------------------------------------------------------------------------
/lib/chef_apply/action/generate_local_policy.rb:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright:: Copyright (c) 2017 Chef Software Inc.
3 | # License:: Apache License, Version 2.0
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 | # http://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 |
18 | require_relative "base"
19 | require_relative "../error"
20 | module ChefApply::Action
21 | class GenerateLocalPolicy < Base
22 | attr_reader :archive_file_location
23 | def initialize(config)
24 | super(config)
25 | @cookbook = config.delete :cookbook
26 | end
27 |
28 | def perform_action
29 | notify(:generating)
30 | installer.run
31 | notify(:exporting)
32 | exporter.run
33 | @archive_file_location = exporter.archive_file_location
34 | notify(:success)
35 | rescue ChefCLI::PolicyfileInstallError => e
36 | raise PolicyfileInstallError.new(e)
37 | end
38 |
39 | def exporter
40 | require "chef-cli/policyfile_services/export_repo"
41 | @exporter ||=
42 | ChefCLI::PolicyfileServices::ExportRepo.new(policyfile: @cookbook.policyfile_lock_path,
43 | root_dir: @cookbook.path,
44 | export_dir: @cookbook.export_path,
45 | archive: true, force: true)
46 | end
47 |
48 | def installer
49 | require "chef-cli/policyfile_services/install"
50 | require "chef-cli/ui"
51 | @installer ||=
52 | ChefCLI::PolicyfileServices::Install.new(ui: ChefCLI::UI.null, root_dir: @cookbook.path)
53 | end
54 |
55 | end
56 |
57 | class PolicyfileInstallError < ChefApply::Error
58 | def initialize(cause_err); super("CHEFPOLICY001", cause_err.message); end
59 | end
60 | end
61 |
--------------------------------------------------------------------------------
/lib/chef_apply/action/generate_temp_cookbook.rb:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright:: Copyright (c) 2018 Chef Software Inc.
3 | # License:: Apache License, Version 2.0
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 | # http://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_relative "base"
18 | require_relative "../error"
19 | module ChefApply
20 | module Action
21 | class GenerateTempCookbook < Base
22 | attr_reader :generated_cookbook
23 |
24 | def self.from_options(opts)
25 | if opts.key?(:recipe_spec)
26 | GenerateCookbookFromRecipe.new(opts)
27 | elsif opts.key?(:resource_name) &&
28 | opts.key?(:resource_type) &&
29 | opts.key?(:resource_properties)
30 | GenerateCookbookFromResource.new(opts)
31 | else
32 | raise MissingOptions.new(opts)
33 | end
34 | end
35 |
36 | def initialize(options)
37 | super(options)
38 | require_relative "generate_temp_cookbook/temp_cookbook"
39 | @generated_cookbook ||= TempCookbook.new
40 | end
41 |
42 | def perform_action
43 | notify(:generating)
44 | generate
45 | notify(:success)
46 | end
47 |
48 | def generate
49 | raise NotImplemented
50 | end
51 | end
52 |
53 | class GenerateCookbookFromRecipe < GenerateTempCookbook
54 | def generate
55 | recipe_specifier = config.delete :recipe_spec
56 | repo_paths = config.delete :cookbook_repo_paths
57 | ChefApply::Log.debug("Beginning to look for recipe specified as #{recipe_specifier}")
58 | if File.file?(recipe_specifier)
59 | ChefApply::Log.debug("#{recipe_specifier} is a valid path to a recipe")
60 | recipe_path = recipe_specifier
61 | else
62 | require_relative "generate_temp_cookbook/recipe_lookup"
63 | rl = RecipeLookup.new(repo_paths)
64 | cookbook_path_or_name, optional_recipe_name = rl.split(recipe_specifier)
65 | cookbook = rl.load_cookbook(cookbook_path_or_name)
66 | recipe_path = rl.find_recipe(cookbook, optional_recipe_name)
67 | end
68 | generated_cookbook.from_existing_recipe(recipe_path)
69 | end
70 | end
71 |
72 | class GenerateCookbookFromResource < GenerateTempCookbook
73 | def generate
74 | type = config.delete :resource_type
75 | name = config.delete :resource_name
76 | props = config.delete :resource_properties
77 | ChefApply::Log.debug("Generating cookbook for ad-hoc resource #{type}[#{name}]")
78 | generated_cookbook.from_resource(type, name, props)
79 | end
80 | end
81 |
82 | class MissingOptions < ChefApply::APIError
83 | def initialize(*args); super("CHEFAPI001", *args); end
84 | end
85 | end
86 | end
87 |
--------------------------------------------------------------------------------
/lib/chef_apply/action/generate_temp_cookbook/recipe_lookup.rb:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright:: Copyright (c) 2017 Chef Software Inc.
3 | # License:: Apache License, Version 2.0
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 | # http://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 |
18 | require "chef-config/config"
19 | require_relative "../../config"
20 | require_relative "../../error"
21 | require_relative "../../log"
22 | require_relative "../base"
23 |
24 | module ChefApply
25 | module Action
26 | class GenerateTempCookbook
27 | # When users are trying to converge a local recipe on a remote target, there
28 | # is a very specific (but expansive) set of things they can specify. This
29 | # class encapsulates that logic for testing purposes. We either return
30 | # a path to a recipe or we raise an error.
31 | class RecipeLookup
32 |
33 | attr_reader :cookbook_repo_paths
34 | def initialize(cookbook_repo_paths)
35 | @cookbook_repo_paths = cookbook_repo_paths
36 | end
37 |
38 | # The recipe specifier is provided by the customer as either a path OR
39 | # a cookbook and optional recipe name.
40 | def split(recipe_specifier)
41 | recipe_specifier.split("::")
42 | end
43 |
44 | # Given a cookbook path or name, try to load that cookbook. Either return
45 | # a cookbook object or raise an error.
46 | def load_cookbook(path_or_name)
47 | require "chef/exceptions"
48 | if File.directory?(path_or_name)
49 | cookbook_path = path_or_name
50 | # First, is there a cookbook in the specified dir that matches?
51 | require "chef/cookbook/cookbook_version_loader"
52 | begin
53 | v = Chef::Cookbook::CookbookVersionLoader.new(cookbook_path)
54 | v.load!
55 | cookbook = v.cookbook_version
56 | rescue Chef::Exceptions::CookbookNotFoundInRepo
57 | raise InvalidCookbook.new(cookbook_path)
58 | end
59 | else
60 | cookbook_name = path_or_name
61 | # Second, is there a cookbook in their local repository that matches?
62 | require "chef/cookbook_loader"
63 | cb_loader = Chef::CookbookLoader.new(cookbook_repo_paths)
64 | cb_loader.load_cookbooks
65 |
66 | begin
67 | cookbook = cb_loader[cookbook_name]
68 | rescue Chef::Exceptions::CookbookNotFoundInRepo
69 | cookbook_repo_paths.each do |repo_path|
70 | cookbook_path = File.join(repo_path, cookbook_name)
71 | if File.directory?(cookbook_path)
72 | raise InvalidCookbook.new(cookbook_path)
73 | end
74 | end
75 | raise CookbookNotFound.new(cookbook_name, cookbook_repo_paths)
76 | end
77 | end
78 | cookbook
79 | end
80 |
81 | # Find the specified recipe or default recipe if none is specified.
82 | # Raise an error if recipe cannot be found.
83 | def find_recipe(cookbook, recipe_name = nil)
84 | recipes = cookbook.recipe_filenames_by_name.merge(cookbook.recipe_yml_filenames_by_name)
85 | if recipe_name.nil?
86 | default_recipe = recipes["default"]
87 | raise NoDefaultRecipe.new(cookbook.root_dir, cookbook.name) if default_recipe.nil?
88 |
89 | default_recipe
90 | else
91 | recipe = recipes[recipe_name]
92 | raise RecipeNotFound.new(cookbook.root_dir, recipe_name, recipes.keys, cookbook.name) if recipe.nil?
93 |
94 | recipe
95 | end
96 | end
97 |
98 | class InvalidCookbook < ChefApply::Error
99 | def initialize(cookbook_path); super("CHEFVAL005", cookbook_path); end
100 | end
101 |
102 | class CookbookNotFound < ChefApply::Error
103 | def initialize(cookbook_name, repo_paths)
104 | repo_paths = repo_paths.join("\n")
105 | super("CHEFVAL006", cookbook_name, repo_paths)
106 | end
107 | end
108 |
109 | class NoDefaultRecipe < ChefApply::Error
110 | def initialize(cookbook_path, cookbook_name); super("CHEFVAL007", cookbook_path, cookbook_name); end
111 | end
112 |
113 | class RecipeNotFound < ChefApply::Error
114 | def initialize(cookbook_path, recipe_name, available_recipes, cookbook_name)
115 | available_recipes.map! { |r| "'#{r}'" }
116 | available_recipes = available_recipes.join(", ")
117 | super("CHEFVAL008", cookbook_path, recipe_name, available_recipes, cookbook_name)
118 | end
119 | end
120 |
121 | end
122 | end
123 | end
124 | end
125 |
--------------------------------------------------------------------------------
/lib/chef_apply/action/install_chef.rb:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright:: Copyright (c) 2017 Chef Software Inc.
3 | # License:: Apache License, Version 2.0
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 | # http://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_relative "base"
18 | require_relative "install_chef/minimum_chef_version"
19 | require "fileutils" unless defined?(FileUtils)
20 |
21 | module ChefApply
22 | module Action
23 | class InstallChef < Base
24 | def initialize(opts = { check_only: false })
25 | super
26 | end
27 |
28 | def perform_action
29 | if InstallChef::MinimumChefVersion.check!(target_host, config[:check_only]) == :minimum_version_met
30 | notify(:already_installed)
31 | else
32 | perform_local_install
33 | end
34 | end
35 |
36 | def upgrading?
37 | @upgrading
38 | end
39 |
40 | def perform_local_install
41 | package = lookup_artifact
42 | notify(:downloading)
43 | local_path = download_to_workstation(package.url)
44 | notify(:uploading)
45 | remote_path = upload_to_target(local_path)
46 | notify(:installing)
47 | target_host.install_package(remote_path)
48 | notify(:install_complete)
49 | end
50 |
51 | def perform_remote_install
52 | # TODO BOOTSTRAP - we'll need to implement this for both platforms
53 | # require "mixlib/install"
54 | # installer = Mixlib::Install.new({
55 | # platform: "windows",/etc -
56 | # product_name: "chef",
57 | # channel: :stable,
58 | # shell_type: :ps1,
59 | # version: "13",
60 | # })
61 | # target_host.run_command! installer.install_command
62 | raise NotImplementedError
63 | end
64 |
65 | def lookup_artifact
66 | return @artifact_info if @artifact_info
67 |
68 | require "mixlib/install"
69 | c = train_to_mixlib(target_host.platform)
70 | Mixlib::Install.new(c).artifact_info
71 | end
72 |
73 | def version_to_install
74 | lookup_artifact.version
75 | end
76 |
77 | def train_to_mixlib(platform)
78 | opts = {
79 | platform_version: platform.release,
80 | platform: platform.name,
81 | architecture: platform.arch,
82 | product_name: "chef",
83 | product_version: :latest,
84 | channel: :stable,
85 | platform_version_compatibility_mode: true,
86 | }
87 | case platform.name
88 | when /mac_os_x/
89 | if platform.release.to_i >= 17
90 | opts[:platform_version] = "10.13"
91 | else
92 | raise NotImplementedError
93 | end
94 | when /windows/
95 | opts[:platform] = "windows"
96 | when "redhat", "centos"
97 | opts[:platform] = "el"
98 | when "suse"
99 | opts[:platform] = "sles"
100 | when "solaris"
101 | opts[:platform] = "solaris2"
102 | when "aix"
103 | opts[:platform] = "aix"
104 | when "amazon"
105 | opts[:platform] = "el"
106 | if platform.release.to_i > 2010 # legacy Amazon version 1
107 | opts[:platform_version] = "6"
108 | else
109 | opts[:platform_version] = "7"
110 | end
111 | end
112 | opts
113 | end
114 |
115 | def download_to_workstation(url_path)
116 | require_relative "../file_fetcher"
117 | ChefApply::FileFetcher.fetch(url_path)
118 | end
119 |
120 | def upload_to_target(local_path)
121 | installer_dir = target_host.temp_dir
122 | remote_path = File.join(installer_dir, File.basename(local_path))
123 | target_host.upload_file(local_path, remote_path)
124 | remote_path
125 | end
126 | end
127 | end
128 | end
129 |
--------------------------------------------------------------------------------
/lib/chef_apply/action/install_chef/minimum_chef_version.rb:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright:: Copyright (c) 2017 Chef Software Inc.
3 | # License:: Apache License, Version 2.0
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 | # http://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 |
18 | require_relative "../../error"
19 | require_relative "minimum_chef_version"
20 |
21 | module ChefApply
22 | module Action
23 | class InstallChef < Base
24 | class MinimumChefVersion
25 |
26 | CONSTRAINTS = {
27 | windows: {
28 | 13 => Gem::Version.new("13.10.4"),
29 | 14 => Gem::Version.new("14.4.22"),
30 | },
31 | linux: {
32 | 13 => Gem::Version.new("13.10.4"),
33 | 14 => Gem::Version.new("14.1.1"),
34 | },
35 | macos: {
36 | 13 => Gem::Version.new("13.10.4"),
37 | 14 => Gem::Version.new("14.1.1"),
38 | },
39 | solaris: {
40 | 13 => Gem::Version.new("13.10.4"),
41 | 14 => Gem::Version.new("14.1.1"),
42 | },
43 | aix: {
44 | 13 => Gem::Version.new("13.10.4"),
45 | 14 => Gem::Version.new("14.1.1"),
46 | },
47 |
48 | }.freeze
49 |
50 | def self.check!(target, check_only)
51 | begin
52 | installed_version = target.installed_chef_version
53 | rescue ChefApply::TargetHost::ChefNotInstalled
54 | if check_only
55 | raise ClientNotInstalled.new
56 | end
57 |
58 | return :client_not_installed
59 | end
60 |
61 | os_constraints = CONSTRAINTS[target.base_os]
62 | min_14_version = os_constraints[14]
63 | min_13_version = os_constraints[13]
64 |
65 | case
66 | when installed_version >= Gem::Version.new("14.0.0") && installed_version < min_14_version
67 | raise Client14Outdated.new(installed_version, min_14_version)
68 | when installed_version >= Gem::Version.new("13.0.0") && installed_version < min_13_version
69 | raise Client13Outdated.new(installed_version, min_13_version, min_14_version)
70 | when installed_version < Gem::Version.new("13.0.0")
71 | # If they have Chef < 13.0.0 installed we want to show them the easiest upgrade path -
72 | # Chef 13 first and then Chef 14 since most customers cannot make the leap directly
73 | # to 14.
74 | raise Client13Outdated.new(installed_version, min_13_version, min_14_version)
75 | end
76 |
77 | :minimum_version_met
78 | end
79 |
80 | class ClientNotInstalled < ChefApply::ErrorNoLogs
81 | def initialize(); super("CHEFINS002"); end
82 | end
83 |
84 | class Client13Outdated < ChefApply::ErrorNoLogs
85 | def initialize(current_version, min_13_version, min_14_version)
86 | super("CHEFINS003", current_version, min_13_version, min_14_version)
87 | end
88 | end
89 |
90 | class Client14Outdated < ChefApply::ErrorNoLogs
91 | def initialize(current_version, target_version)
92 | super("CHEFINS004", current_version, target_version)
93 | end
94 | end
95 | end
96 | end
97 | end
98 | end
99 |
--------------------------------------------------------------------------------
/lib/chef_apply/action/reporter.rb:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright:: Copyright (c) 2017 Chef Software Inc.
3 | # License:: Apache License, Version 2.0
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 | # http://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 |
18 | require "chef/handler"
19 | require "chef/resource/directory"
20 |
21 | module ChefApply
22 | class Reporter < ::Chef::Handler
23 |
24 | def report
25 | if exception
26 | Chef::Log.error("Creating exception report")
27 | else
28 | Chef::Log.info("Creating run report")
29 | end
30 |
31 | # ensure start time and end time are output in the json properly in the event activesupport happens to be on the system
32 | run_data = data
33 | run_data[:start_time] = run_data[:start_time].to_s
34 | run_data[:end_time] = run_data[:end_time].to_s
35 |
36 | Chef::FileCache.store("run-report.json", Chef::JSONCompat.to_json_pretty(run_data), 0640)
37 | end
38 | end
39 | end
40 |
--------------------------------------------------------------------------------
/lib/chef_apply/cli/help.rb:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright:: Copyright (c) 2018 Chef Software Inc.
3 | # License:: Apache License, Version 2.0
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 | # http://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 |
18 | module ChefApply
19 | class CLI
20 | module Help
21 | T = ChefApply::Text.cli
22 | def show_help
23 | UI::Terminal.output format_help
24 | end
25 |
26 | def format_help
27 | help_text = banner.clone # This prevents us appending to the banner text
28 | help_text << "\n"
29 | help_text << format_flags
30 | end
31 |
32 | def format_flags
33 | flag_text = "FLAGS:\n"
34 | justify_length = 0
35 | options.each_value do |spec|
36 | justify_length = [justify_length, spec[:long].length + 4].max
37 | end
38 | options.sort.to_h.each_value do |flag_spec|
39 | short = flag_spec[:short] || " "
40 | short = short[0, 2] # We only want the flag portion, not the capture portion (if present)
41 | if short == " "
42 | short = " "
43 | else
44 | short = "#{short}, "
45 | end
46 | flags = "#{short}#{flag_spec[:long]}"
47 | flag_text << " #{flags.ljust(justify_length)} "
48 | ml_padding = " " * (justify_length + 8)
49 | first = true
50 | flag_spec[:description].split("\n").each do |d|
51 | flag_text << ml_padding unless first
52 | first = false
53 | flag_text << "#{d}\n"
54 | end
55 | end
56 | flag_text
57 | end
58 |
59 | def usage
60 | T.usage
61 | end
62 |
63 | def show_version
64 | require_relative "../version"
65 | UI::Terminal.output T.version.show(ChefApply::VERSION)
66 | end
67 | end
68 | end
69 | end
70 |
--------------------------------------------------------------------------------
/lib/chef_apply/cli/options.rb:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright:: Copyright (c) 2018 Chef Software Inc.
3 | # License:: Apache License, Version 2.0
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 | # http://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 |
18 | require_relative "../text"
19 |
20 | # Moving the options into here so the cli.rb file is smaller and easier to read
21 | # For options that need to be merged back into the global ChefApply::Config object
22 | # we do that with a proc in the option itself. We decided to do that because it is
23 | # an easy, straight forward way to merge those options when they do not directly
24 | # map back to keys in the Config global. IE, we cannot just do
25 | # `ChefApply::Config.merge!(options)` because the keys do not line up, and we do
26 | # not want all CLI params merged back into the global config object.
27 | # We know that the config is already loaded from the file (or program defaults)
28 | # because the `Startup` class was invoked to start the program.
29 | module ChefApply
30 | class CLI
31 | module Options
32 |
33 | T = ChefApply::Text.cli
34 | TS = ChefApply::Text.status
35 |
36 | def self.included(klass)
37 | klass.banner T.description + "\n" + T.usage_full
38 |
39 | klass.option :version,
40 | short: "-v",
41 | long: "--version",
42 | description: T.version.description,
43 | boolean: true
44 |
45 | klass.option :help,
46 | short: "-h",
47 | long: "--help",
48 | description: T.help.description,
49 | boolean: true
50 |
51 | # Special note:
52 | # config_path is pre-processed in startup.rb, and is shown here only
53 | # for the purpose of rendering help text.
54 | klass.option :config_path,
55 | short: "-c PATH",
56 | long: "--config PATH",
57 | description: T.default_config_location(ChefApply::Config.default_location),
58 | default: ChefApply::Config.default_location,
59 | proc: Proc.new { |path| ChefApply::Config.custom_location(path) }
60 |
61 | klass.option :identity_file,
62 | long: "--identity-file PATH",
63 | short: "-i PATH",
64 | description: T.identity_file,
65 | proc: (Proc.new do |paths|
66 | path = paths
67 | unless File.readable?(path)
68 | raise OptionValidationError.new("CHEFVAL001", nil, path)
69 | end
70 |
71 | path
72 | end)
73 |
74 | klass.option :ssl,
75 | long: "--[no-]ssl",
76 | description: T.ssl.desc(ChefApply::Config.connection.winrm.ssl),
77 | boolean: true,
78 | default: ChefApply::Config.connection.winrm.ssl,
79 | proc: Proc.new { |val| ChefApply::Config.connection.winrm.ssl(val) }
80 |
81 | klass.option :ssl_verify,
82 | long: "--[no-]ssl-verify",
83 | description: T.ssl.verify_desc(ChefApply::Config.connection.winrm.ssl_verify),
84 | boolean: true,
85 | default: ChefApply::Config.connection.winrm.ssl_verify,
86 | proc: Proc.new { |val| ChefApply::Config.connection.winrm.ssl_verify(val) }
87 |
88 | klass.option :protocol,
89 | long: "--protocol ",
90 | short: "-p",
91 | description: T.protocol_description(ChefApply::Config::SUPPORTED_PROTOCOLS.join(" "),
92 | ChefApply::Config.connection.default_protocol),
93 | default: ChefApply::Config.connection.default_protocol,
94 | proc: Proc.new { |val| ChefApply::Config.connection.default_protocol(val) }
95 |
96 | klass.option :user,
97 | long: "--user ",
98 | description: T.user_description
99 |
100 | klass.option :password,
101 | long: "--password ",
102 | description: T.password_description
103 |
104 | klass.option :cookbook_repo_paths,
105 | long: "--cookbook-repo-paths PATH",
106 | description: T.cookbook_repo_paths,
107 | default: ChefApply::Config.chef.cookbook_repo_paths,
108 | proc: (Proc.new do |paths|
109 | paths = paths.split(",")
110 | ChefApply::Config.chef.cookbook_repo_paths(paths)
111 | paths
112 | end)
113 |
114 | klass.option :install,
115 | long: "--[no-]install",
116 | default: true,
117 | boolean: true,
118 | description: T.install_description
119 |
120 | klass.option :sudo,
121 | long: "--[no-]sudo",
122 | description: T.sudo.flag_description.sudo,
123 | boolean: true,
124 | default: true
125 |
126 | klass.option :sudo_command,
127 | long: "--sudo-command ",
128 | default: "sudo",
129 | description: T.sudo.flag_description.command
130 |
131 | klass.option :sudo_password,
132 | long: "--sudo-password ",
133 | description: T.sudo.flag_description.password
134 |
135 | klass.option :sudo_options,
136 | long: "--sudo-options 'OPTIONS...'",
137 | description: T.sudo.flag_description.options
138 | end
139 |
140 | # I really don't like that mixlib-cli refers to the parsed command line flags in
141 | # a hash accessed via the `config` method. Thats just such an overloaded word.
142 | def parsed_options
143 | config
144 | end
145 | end
146 | end
147 | end
148 |
--------------------------------------------------------------------------------
/lib/chef_apply/cli/validation.rb:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright:: Copyright (c) 2018 Chef Software Inc.
3 | # License:: Apache License, Version 2.0
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 | # http://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 |
18 | require_relative "../error"
19 |
20 | module ChefApply
21 | class CLI
22 | module Validation
23 | PROPERTY_MATCHER = /^([a-zA-Z0-9_]+)=(.+)$/.freeze
24 | CB_MATCHER = '[\w\-]+'.freeze
25 |
26 | # The first param is always hostname. Then we either have
27 | # 1. A recipe designation
28 | # 2. A resource type and resource name followed by any properties
29 | def validate_params(params)
30 | if params.size < 2
31 | raise OptionValidationError.new("CHEFVAL002", self)
32 | end
33 |
34 | if params.size == 2
35 | # Trying to specify a recipe to run remotely, no properties
36 | cb = params[1]
37 | if File.exist?(cb)
38 | # This is a path specification, and we know it is valid
39 | elsif cb =~ /^#{CB_MATCHER}$/ || cb =~ /^#{CB_MATCHER}::#{CB_MATCHER}$/
40 | # They are specifying a cookbook as 'cb_name' or 'cb_name::recipe'
41 | else
42 | raise OptionValidationError.new("CHEFVAL004", self, cb)
43 | end
44 | elsif params.size >= 3
45 | properties = params[3..-1]
46 | properties.each do |property|
47 | unless property =~ PROPERTY_MATCHER
48 | raise OptionValidationError.new("CHEFVAL003", self, property)
49 | end
50 | end
51 | end
52 | end
53 |
54 | # Convert properties in the form k1=v1,k2=v2,kn=vn
55 | # into a hash, while validating correct form and format
56 | def properties_from_string(string_props)
57 | properties = {}
58 | string_props.each do |a|
59 | key, value = PROPERTY_MATCHER.match(a)[1..-1]
60 | value = transform_property_value(value)
61 | properties[key] = value
62 | end
63 | properties
64 | end
65 |
66 | # Incoming properties are always read as a string from the command line.
67 | # Depending on their type we should transform them so we do not try and pass
68 | # a string to a resource property that expects an integer or boolean.
69 | def transform_property_value(value)
70 | case value
71 | when /^0/
72 | # when it is a zero leading value like "0777" don't turn
73 | # it into a number (this is a mode flag)
74 | value
75 | when /^\d+$/
76 | value.to_i
77 | when /^\d+\.\d*$/, /^\d*\.\d+$/
78 | value.to_f
79 | when /^[:].+$/
80 | value.split(":").last.to_sym
81 | when /true/i
82 | true
83 | when /false/i
84 | false
85 | else
86 | value
87 | end
88 | end
89 | end
90 |
91 | class OptionValidationError < ChefApply::ErrorNoLogs
92 | attr_reader :command
93 | def initialize(id, calling_command, *args)
94 | super(id, *args)
95 | # TODO - this is getting cumbersome - move them to constructor options hash in base
96 | @decorate = false
97 | @command = calling_command
98 | end
99 | end
100 | end
101 |
102 | end
103 |
--------------------------------------------------------------------------------
/lib/chef_apply/config.rb:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright:: Copyright (c) 2018 Chef Software Inc.
3 | # License:: Apache License, Version 2.0
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 | # http://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 |
18 | require_relative "log"
19 | require "mixlib/config" unless defined?(Mixlib::Config)
20 | require "fileutils" unless defined?(FileUtils)
21 | require "pathname" unless defined?(Pathname)
22 | require "chef-config/config"
23 | require "chef-config/workstation_config_loader"
24 |
25 | module ChefApply
26 | class Config
27 | WS_BASE_PATH = File.join(Dir.home, ".chef-workstation/")
28 | SUPPORTED_PROTOCOLS = %w{ssh winrm}.freeze
29 |
30 | class << self
31 | @custom_location = nil
32 |
33 | # Ensure when we extend Mixlib::Config that we load
34 | # up the workstation config since we will need that
35 | # to converge later
36 | def initialize_mixlib_config
37 | super
38 | end
39 |
40 | def custom_location(path)
41 | @custom_location = path
42 | raise "No config file located at #{path}" unless exist?
43 | end
44 |
45 | def default_location
46 | File.join(WS_BASE_PATH, "config.toml")
47 | end
48 |
49 | def telemetry_path
50 | File.join(WS_BASE_PATH, "telemetry")
51 | end
52 |
53 | def telemetry_session_file
54 | File.join(telemetry_path, "TELEMETRY_SESSION_ID")
55 | end
56 |
57 | def telemetry_installation_identifier_file
58 | File.join(WS_BASE_PATH, "installation_id")
59 | end
60 |
61 | def base_log_directory
62 | File.dirname(log.location)
63 | end
64 |
65 | # These paths are relative to the log output path, which is user-configurable.
66 | def error_output_path
67 | File.join(base_log_directory, "errors.txt")
68 | end
69 |
70 | def stack_trace_path
71 | File.join(base_log_directory, "stack-trace.log")
72 | end
73 |
74 | def using_default_location?
75 | @custom_location.nil?
76 | end
77 |
78 | def location
79 | using_default_location? ? default_location : @custom_location
80 | end
81 |
82 | def load
83 | if exist?
84 | from_file(location)
85 | end
86 | end
87 |
88 | def exist?
89 | File.exist? location
90 | end
91 |
92 | def reset
93 | @custom_location = nil
94 | super
95 | end
96 | end
97 |
98 | extend Mixlib::Config
99 |
100 | # This configuration is shared among many components.
101 | # While enabling strict mode can provide a better experience
102 | # around validated config entries, chef-apply won't know about
103 | # config items that it doesn't own, and we don't want it to
104 | # fail to start when that happens.
105 | config_strict_mode false
106 |
107 | # When working on Chef Apply itself,
108 | # developers should set telemetry.dev to true
109 | # in their local configuration to ensure that dev usage
110 | # doesn't skew customer telemetry.
111 | config_context :telemetry do
112 | default(:dev_mode, false)
113 | default(:enabled, true)
114 | end
115 |
116 | config_context :log do
117 | default(:level, "warn")
118 | configurable(:location)
119 | .defaults_to(File.join(WS_BASE_PATH, "logs/default.log"))
120 | .writes_value { |p| File.expand_path(p) }
121 | # set the log level for the target host's chef-client run
122 | default(:target_level, nil)
123 | end
124 |
125 | config_context :cache do
126 | configurable(:path)
127 | .defaults_to(File.join(WS_BASE_PATH, "cache"))
128 | .writes_value { |p| File.expand_path(p) }
129 | end
130 |
131 | config_context :connection do
132 | default(:default_protocol, "ssh")
133 | default(:default_user, nil)
134 |
135 | config_context :winrm do
136 | default(:ssl, false)
137 | default(:ssl_verify, true)
138 | end
139 | end
140 |
141 | config_context :dev do
142 | default(:spinner, true)
143 | end
144 |
145 | config_context :chef do
146 | # We want to use any configured chef repo paths or trusted certs in
147 | # ~/.chef/knife.rb on the user's workstation. But because they could have
148 | # config that could mess up our Policyfile creation later we reset the
149 | # ChefConfig back to default after loading that.
150 | ChefConfig::WorkstationConfigLoader.new(nil, ChefApply::Log).load
151 | default(:cookbook_repo_paths, [ChefConfig::Config[:cookbook_path]].flatten)
152 | default(:trusted_certs_dir, ChefConfig::Config[:trusted_certs_dir])
153 | default(:chef_license, ChefConfig::Config[:chef_license])
154 | ChefConfig::Config.reset
155 | end
156 |
157 | config_context :data_collector do
158 | default :url, nil
159 | default :token, nil
160 | end
161 |
162 | config_context :updates do
163 | default :channel, nil
164 | default :interval_minutes, nil
165 | default :enable, nil
166 | end
167 | end
168 | end
169 |
--------------------------------------------------------------------------------
/lib/chef_apply/error.rb:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright:: Copyright (c) 2017 Chef Software Inc.
3 | # License:: Apache License, Version 2.0
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 | # http://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 |
18 | module ChefApply
19 | class Error < StandardError
20 | attr_reader :id, :params
21 | attr_accessor :show_stack, :show_log, :decorate
22 | def initialize(id, *params)
23 | @id = id
24 | @params = params || []
25 | @show_log = true
26 | @show_stack = true
27 | @decorate = true
28 | end
29 | end
30 |
31 | # These helpers are obsolete
32 | class ErrorNoLogs < Error
33 | def initialize(id, *params)
34 | super
35 | @show_log = false
36 | @show_stack = false
37 | end
38 | end
39 |
40 | class ErrorNoStack < Error
41 | def initialize(id, *params)
42 | super
43 | @show_log = true
44 | @show_stack = false
45 | end
46 | end
47 |
48 | class WrappedError < StandardError
49 | attr_accessor :target_host, :contained_exception
50 | def initialize(e, target_host)
51 | super(e.message)
52 | @contained_exception = e
53 | @target_host = target_host
54 | end
55 | end
56 |
57 | class MultiJobFailure < ErrorNoLogs
58 | attr_reader :jobs
59 | def initialize(jobs)
60 | super("CHEFMULTI001")
61 | @jobs = jobs
62 | @decorate = false
63 | end
64 | end
65 |
66 | # Provide a base type for internal usage errors that should not leak out
67 | # but may anyway.
68 | class APIError < Error
69 | end
70 | end
71 |
--------------------------------------------------------------------------------
/lib/chef_apply/errors/standard_error_resolver.rb:
--------------------------------------------------------------------------------
1 | module ChefApply
2 | module Errors
3 | # Provides mappings of common errors that we don't explicitly
4 | # handle, but can offer expanded help text around.
5 | class StandardErrorResolver
6 | def self.resolve_exception(exception)
7 | deps
8 | show_log = true
9 | show_stack = true
10 | case exception
11 | when OpenSSL::SSL::SSLError
12 | if exception.message =~ /SSL.*verify failed.*/
13 | id = "CHEFNET002"
14 | show_log = false
15 | show_stack = false
16 | end
17 | when SocketError then id = "CHEFNET001"; show_log = false; show_stack = false
18 | end
19 | if id.nil?
20 | exception
21 | else
22 | e = ChefApply::Error.new(id, exception.message)
23 | e.show_log = show_log
24 | e.show_stack = show_stack
25 | e
26 | end
27 | end
28 |
29 | def self.wrap_exception(original, target_host = nil)
30 | resolved_exception = resolve_exception(original)
31 | WrappedError.new(resolved_exception, target_host)
32 | end
33 |
34 | def self.unwrap_exception(wrapper)
35 | resolve_exception(wrapper.contained_exception)
36 | end
37 |
38 | def self.deps
39 | # Avoid loading additional includes until they're needed
40 | require "socket" unless defined?(Socket)
41 | require "openssl" unless defined?(OpenSSL)
42 | end
43 | end
44 | end
45 | end
46 |
--------------------------------------------------------------------------------
/lib/chef_apply/file_fetcher.rb:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright:: Copyright (c) 2017 Chef Software Inc.
3 | # License:: Apache License, Version 2.0
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 | # http://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 |
18 | require "net/http" unless defined?(Net::HTTP)
19 | require "uri" unless defined?(URI)
20 | require_relative "config"
21 | require_relative "log"
22 |
23 | module ChefApply
24 | class FileFetcher
25 | class << self
26 | # Simple fetcher of an http(s) url. Returns the local path
27 | # of the downloaded file.
28 | def fetch(path)
29 | cache_path = ChefApply::Config.cache.path
30 | FileUtils.mkdir_p(cache_path)
31 | url = URI.parse(path)
32 | name = File.basename(url.path)
33 | local_path = File.join(cache_path, name)
34 |
35 | # TODO header check for size or checksum?
36 | return local_path if File.exist?(local_path)
37 |
38 | download_file(url, local_path)
39 | local_path
40 | end
41 |
42 | def download_file(url, local_path)
43 | temp_path = "#{local_path}.downloading"
44 | file = open(temp_path, "wb")
45 | ChefApply::Log.debug "Downloading: #{temp_path}"
46 | Net::HTTP.start(url.host) do |http|
47 |
48 | http.request_get(url.path) do |resp|
49 | resp.read_body do |segment|
50 | file.write(segment)
51 | end
52 | end
53 | rescue e
54 | @error = true
55 | raise
56 | ensure
57 | file.close
58 | # If any failures occurred, don't risk keeping
59 | # an incomplete download that we'll see as 'cached'
60 | if @error
61 | FileUtils.rm_f(temp_path)
62 | else
63 | FileUtils.mv(temp_path, local_path)
64 | end
65 |
66 | end
67 | end
68 | end
69 | end
70 | end
71 |
--------------------------------------------------------------------------------
/lib/chef_apply/log.rb:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright:: Copyright (c) 2017 Chef Software Inc.
3 | # License:: Apache License, Version 2.0
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 | # http://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 |
18 | require "mixlib/log"
19 |
20 | module ChefApply
21 | class Log
22 | extend Mixlib::Log
23 |
24 | def self.setup(location, log_level)
25 | if location.is_a?(String)
26 | if location.casecmp("stdout") == 0
27 | @stream = $stdout
28 | else
29 | @stream = File.open(location, "w+")
30 | end
31 | end
32 | init(location)
33 | Log.level = log_level
34 | end
35 |
36 | def self.stream
37 | @stream
38 | end
39 | end
40 | end
41 |
--------------------------------------------------------------------------------
/lib/chef_apply/status_reporter.rb:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright:: Copyright (c) 2017 Chef Software Inc.
3 | # License:: Apache License, Version 2.0
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 | # http://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 |
18 | module ChefApply
19 | class StatusReporter
20 |
21 | def initialize(ui_element, prefix: nil, key: nil)
22 | @ui_element = ui_element
23 | @key = key
24 | @ui_element.update(prefix: prefix)
25 | end
26 |
27 | def update(msg)
28 | @ui_element.update({ @key => msg })
29 | end
30 |
31 | def success(msg)
32 | update(msg)
33 | @ui_element.success
34 | end
35 |
36 | def error(msg)
37 | update(msg)
38 | @ui_element.error
39 | end
40 |
41 | end
42 | end
43 |
--------------------------------------------------------------------------------
/lib/chef_apply/target_host/aix.rb:
--------------------------------------------------------------------------------
1 | module ChefApply
2 | class TargetHost
3 | module Aix
4 |
5 | def omnibus_manifest_path
6 | # Note that we can't use File::Join, because that will render for the
7 | # CURRENT platform - not the platform of the target.
8 | "/opt/chef/version-manifest.json"
9 | end
10 |
11 | def mkdir(path)
12 | run_command!("mkdir -p #{path}")
13 | end
14 |
15 | def chown(path, owner)
16 | # owner ||= user
17 | # run_command!("chown #{owner} '#{path}'")
18 | nil
19 | end
20 |
21 | def make_temp_dir
22 | # We will cache this so that we only run this once
23 | @tempdir ||= begin
24 | res = run_command!("bash -c '#{MKTEMP_COMMAND}'")
25 | res.stdout.chomp.strip
26 | end
27 | end
28 |
29 | def install_package(target_package_path)
30 | command = "installp -aXYgd #{target_package_path} all"
31 | run_command!(command)
32 | end
33 |
34 | def del_file(path)
35 | run_command!("rm -rf #{path}")
36 | end
37 |
38 | def del_dir(path)
39 | del_file(path)
40 | end
41 |
42 | def ws_cache_path
43 | "/var/chef-workstation"
44 | end
45 |
46 | # Nothing to escape in a unix-based path
47 | def normalize_path(path)
48 | path
49 | end
50 |
51 | MKTEMP_COMMAND = "d=$(mktemp -d -p${TMPDIR:-/tmp} chef_XXXXXX); echo $d".freeze
52 |
53 | end
54 | end
55 | end
56 |
--------------------------------------------------------------------------------
/lib/chef_apply/target_host/linux.rb:
--------------------------------------------------------------------------------
1 |
2 |
3 | module ChefApply
4 | class TargetHost
5 | module Linux
6 | def omnibus_manifest_path
7 | # TODO - if habitat install on target, this won't work
8 | # Note that we can't use File::Join, because that will render for the
9 | # CURRENT platform - not the platform of the target.
10 | "/opt/chef/version-manifest.json"
11 | end
12 |
13 | def mkdir(path)
14 | run_command!("mkdir -p #{path}")
15 | end
16 |
17 | def chown(path, owner)
18 | owner ||= user
19 | run_command!("chown #{owner} '#{path}'")
20 | nil
21 | end
22 |
23 | def make_temp_dir
24 | # We will cache this so that we only
25 | @tempdir ||= begin
26 | res = run_command!("bash -c '#{MKTEMP_COMMAND}'")
27 | res.stdout.chomp.strip
28 | end
29 | end
30 |
31 | def install_package(target_package_path)
32 | install_cmd = case File.extname(target_package_path)
33 | when ".rpm"
34 | "rpm -Uvh #{target_package_path}"
35 | when ".deb"
36 | "dpkg -i #{target_package_path}"
37 | end
38 | run_command!(install_cmd)
39 | nil
40 | end
41 |
42 | def del_file(path)
43 | run_command!("rm -rf #{path}")
44 | end
45 |
46 | def del_dir(path)
47 | del_file(path)
48 | end
49 |
50 | def ws_cache_path
51 | "/var/chef-workstation"
52 | end
53 |
54 | # Nothing to escape in a linux-based path
55 | def normalize_path(path)
56 | path
57 | end
58 |
59 | MKTEMP_COMMAND = "d=$(mktemp -d -p${TMPDIR:-/tmp} chef_XXXXXX); echo $d".freeze
60 |
61 | end
62 | end
63 | end
64 |
--------------------------------------------------------------------------------
/lib/chef_apply/target_host/macos.rb:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright:: Copyright (c) 2017 Chef Software Inc.
3 | # License:: Apache License, Version 2.0
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 | # http://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 |
18 | module ChefApply
19 | class TargetHost
20 | module MacOS
21 | def omnibus_manifest_path
22 | # TODO - if habitat install on target, this won't work
23 | # Note that we can't use File::Join, because that will render for the
24 | # CURRENT platform - not the platform of the target.
25 | "/opt/chef/version-manifest.json"
26 | end
27 |
28 | def mkdir(path)
29 | run_command!("mkdir -p #{path}")
30 | end
31 |
32 | def chown(path, owner)
33 | owner ||= user
34 | run_command!("chown #{owner} '#{path}'")
35 | nil
36 | end
37 |
38 | def install_package(remote_path)
39 | install_cmd = <<-EOS
40 | hdiutil detach "/Volumes/chef_software" >/dev/null 2>&1 || true
41 | hdiutil attach #{remote_path} -mountpoint "/Volumes/chef_software"
42 | cd / && sudo /usr/sbin/installer -pkg `sudo find "/Volumes/chef_software" -name \\*.pkg` -target /
43 | EOS
44 | run_command!(install_cmd)
45 | nil
46 | end
47 |
48 | def del_file(path)
49 | run_command!("rm -rf #{path}")
50 | end
51 |
52 | def del_dir(path)
53 | del_file(path)
54 | end
55 |
56 | def make_temp_dir
57 | installer_dir = "/tmp/chef-installer"
58 | run_command!("mkdir -p #{installer_dir}")
59 | run_command!("chmod 777 #{installer_dir}")
60 | installer_dir
61 | end
62 |
63 | def ws_cache_path
64 | "/var/chef-workstation"
65 | end
66 |
67 | end
68 | end
69 | end
--------------------------------------------------------------------------------
/lib/chef_apply/target_host/solaris.rb:
--------------------------------------------------------------------------------
1 | module ChefApply
2 | class TargetHost
3 | module Solaris
4 |
5 | def omnibus_manifest_path
6 | # Note that we can't use File::Join, because that will render for the
7 | # CURRENT platform - not the platform of the target.
8 | "/opt/chef/version-manifest.json"
9 | end
10 |
11 | def mkdir(path)
12 | run_command!("mkdir -p #{path}")
13 | end
14 |
15 | def chown(path, owner)
16 | owner ||= user
17 | run_command!("chown #{owner} '#{path}'")
18 | nil
19 | end
20 |
21 | def make_temp_dir
22 | # We will cache this so that we only run this once
23 | @tempdir ||= begin
24 | res = run_command!("bash -c '#{MKTEMP_COMMAND}'")
25 | res.stdout.chomp.strip
26 | end
27 | end
28 |
29 | def install_package(target_package_path)
30 | command = "pkg install -g #{target_package_path} chef"
31 | run_command!(command)
32 | end
33 |
34 | def del_file(path)
35 | run_command!("rm -rf #{path}")
36 | end
37 |
38 | def del_dir(path)
39 | del_file(path)
40 | end
41 |
42 | def ws_cache_path
43 | "/var/chef-workstation"
44 | end
45 |
46 | # Nothing to escape in a unix-based path
47 | def normalize_path(path)
48 | path
49 | end
50 |
51 | MKTEMP_COMMAND = "d=$(mktemp -d -p${TMPDIR:-/tmp} chef_XXXXXX); echo $d".freeze
52 |
53 | end
54 | end
55 | end
56 |
--------------------------------------------------------------------------------
/lib/chef_apply/target_host/windows.rb:
--------------------------------------------------------------------------------
1 |
2 | module ChefApply
3 | class TargetHost
4 | module Windows
5 | def omnibus_manifest_path
6 | # TODO - use a proper method to query the win installation path -
7 | # currently we're assuming the default, but this can be customized
8 | # at install time.
9 | # A working approach is below - but it runs very slowly (~10s) in testing
10 | # on a virtualbox windows vm:
11 | # (over winrm) Get-WmiObject Win32_Product | Where {$_.Name -match 'Chef Client'}
12 | # TODO - if habitat install on target, this won't work
13 | "c:\\opscode\\chef\\version-manifest.json"
14 | end
15 |
16 | def mkdir(path)
17 | run_command!("New-Item -ItemType Directory -Force -Path #{path}")
18 | end
19 |
20 | def chown(path, owner)
21 | # This implementation left intentionally blank.
22 | # To date, we have not needed chown functionality on windows;
23 | # when/if that changes we'll need to implement it here.
24 | nil
25 | end
26 |
27 | def make_temp_dir
28 | @tmpdir ||= begin
29 | res = run_command!(MKTEMP_COMMAND)
30 | res.stdout.chomp.strip
31 | end
32 | end
33 |
34 | def install_package(target_package_path)
35 | # While powershell does not mind the mixed path separators \ and /,
36 | # 'cmd.exe' definitely does - so we'll make the path cmd-friendly
37 | # before running the command
38 | cmd = "cmd /c msiexec /package #{target_package_path.tr("/", "\\")} /quiet"
39 | run_command!(cmd)
40 | nil
41 | end
42 |
43 | def del_file(path)
44 | run_command!("If (Test-Path #{path}) { Remove-Item -Force -Path #{path} }")
45 | end
46 |
47 | def del_dir(path)
48 | run_command!("Remove-Item -Recurse -Force –Path #{path}")
49 | end
50 |
51 | def ws_cache_path
52 | '#{ENV[\'APPDATA\']}/chef-workstation'
53 | end
54 |
55 | MKTEMP_COMMAND = "$parent = [System.IO.Path]::GetTempPath();" +
56 | "[string] $name = [System.Guid]::NewGuid();" +
57 | "$tmp = New-Item -ItemType Directory -Path " +
58 | "(Join-Path $parent $name);" +
59 | "$tmp.FullName".freeze
60 | end
61 | end
62 | end
63 |
--------------------------------------------------------------------------------
/lib/chef_apply/target_resolver.rb:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright:: Copyright (c) 2018 Chef Software Inc.
3 | # License:: Apache License, Version 2.0
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 | # http://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 |
18 | require_relative "target_host"
19 | require_relative "error"
20 |
21 | module ChefApply
22 | class TargetResolver
23 | MAX_EXPANDED_TARGETS = 24
24 |
25 | def initialize(target, default_protocol, conn_options)
26 | @default_proto = default_protocol
27 | @unparsed_target = target
28 | @split_targets = @unparsed_target.split(",")
29 | @conn_options = conn_options.dup
30 | @default_password = @conn_options.delete(:password)
31 | @default_user = @conn_options.delete(:user)
32 | end
33 |
34 | # Returns the list of targets as an array of TargetHost instances,
35 | # them to account for ranges embedded in the target name.
36 | def targets
37 | return @targets unless @targets.nil?
38 |
39 | expanded_urls = []
40 | @split_targets.each do |target|
41 | expanded_urls = (expanded_urls | expand_targets(target))
42 | end
43 | @targets = expanded_urls.map do |url|
44 | config = @conn_options.merge(config_for_target(url))
45 | TargetHost.new(config.delete(:url), config)
46 | end
47 | end
48 |
49 | def config_for_target(url)
50 | prefix, target = prefix_from_target(url)
51 |
52 | inline_password = nil
53 | inline_user = nil
54 | host = target
55 | # Default greedy-scan of the regex means that
56 | # $2 will resolve to content after the final "@"
57 | # URL credentials will take precedence over the default :user
58 | # in @conn_opts
59 | if target =~ /(.*)@(.*)/
60 | inline_credentials = $1
61 | host = $2
62 | # We'll use a non-greedy match to grab everything up to the first ':'
63 | # as username if there is no :, credentials is just the username
64 | if inline_credentials =~ /(.+?):(.*)/
65 | inline_user = $1
66 | inline_password = $2
67 | else
68 | inline_user = inline_credentials
69 | end
70 | end
71 | user, password = make_credentials(inline_user, inline_password)
72 | { url: "#{prefix}#{host}",
73 | user: user,
74 | password: password }
75 | end
76 |
77 | # Merge the inline user/pass with the default user/pass, giving
78 | # precedence to inline.
79 | def make_credentials(inline_user, inline_password)
80 | user = inline_user || @default_user
81 | user = nil if user && user.empty?
82 | password = (inline_password || @default_password)
83 | password = nil if password && password.empty?
84 | [user, password]
85 | end
86 |
87 | def prefix_from_target(target)
88 | if target =~ %r{^(.+?)://(.*)}
89 | # We'll store the existing prefix to avoid it interfering
90 | # with the check further below.
91 | if ChefApply::Config::SUPPORTED_PROTOCOLS.include? $1.downcase
92 | prefix = "#{$1}://"
93 | target = $2
94 | else
95 | raise UnsupportedProtocol.new($1)
96 | end
97 | else
98 | prefix = "#{@default_proto}://"
99 | end
100 | [prefix, target]
101 | end
102 |
103 | def expand_targets(target)
104 | @current_target = target # Hold onto this for error reporting
105 | do_parse([target.downcase])
106 | end
107 |
108 | private
109 |
110 | # A string matching PREFIX[x:y]POSTFIX:
111 | # POSTFIX can contain further ranges itself
112 | # This uses a greedy match (.*) to get include every character
113 | # up to the last "[" in PREFIX
114 | # $1 - prefix; $2 - x, $3 - y, $4 unprocessed/remaining text
115 | TARGET_WITH_RANGE = /^(.*)\[([\p{Alnum}]+):([\p{Alnum}]+)\](.*)/.freeze
116 |
117 | def do_parse(targets, depth = 0)
118 | raise TooManyRanges.new(@current_target) if depth > 2
119 |
120 | new_targets = []
121 | done = false
122 | targets.each do |target|
123 | if TARGET_WITH_RANGE =~ target
124 | # $1 - prefix; $2 - x, $3 - y, $4 unprocessed/remaining text
125 | expand_range(new_targets, $1, $2, $3, $4)
126 | else
127 | # Nothing more to expand
128 | done = true
129 | new_targets << target
130 | end
131 | end
132 | if done
133 | new_targets
134 | else
135 | do_parse(new_targets, depth + 1)
136 | end
137 | end
138 |
139 | def expand_range(dest, prefix, start, stop, suffix)
140 | prefix ||= ""
141 | suffix ||= ""
142 | start_is_int = Integer(start) >= 0 rescue false
143 | stop_is_int = Integer(stop) >= 0 rescue false
144 |
145 | if (start_is_int && !stop_is_int) || (stop_is_int && !start_is_int)
146 | raise InvalidRange.new(@current_target, "[#{start}:#{stop}]")
147 | end
148 |
149 | # Ensure that a numeric range doesn't get created as a string, which
150 | # would make the created Range further below fail to iterate for some values
151 | # because of ASCII sorting.
152 | if start_is_int
153 | start = Integer(start)
154 | end
155 |
156 | if stop_is_int
157 | stop = Integer(stop)
158 | end
159 |
160 | # For range to iterate correctly, the values must
161 | # be low,high
162 | if start > stop
163 | temp = stop; stop = start; start = temp
164 | end
165 | Range.new(start, stop).each do |value|
166 | # Ranges will resolve only numbers and letters,
167 | # not other ascii characters that happen to fall between.
168 | if start_is_int || /^[a-z0-9]/ =~ value
169 | dest << "#{prefix}#{value}#{suffix}"
170 | end
171 | # Stop expanding as soon as we go over limit to prevent
172 | # making the user wait for a massive accidental expansion
173 | if dest.length > MAX_EXPANDED_TARGETS
174 | raise TooManyTargets.new(@split_targets.length, MAX_EXPANDED_TARGETS)
175 | end
176 | end
177 | end
178 |
179 | class InvalidRange < ErrorNoLogs
180 | def initialize(unresolved_target, given_range)
181 | super("CHEFRANGE001", unresolved_target, given_range)
182 | end
183 | end
184 |
185 | class TooManyRanges < ErrorNoLogs
186 | def initialize(unresolved_target)
187 | super("CHEFRANGE002", unresolved_target)
188 | end
189 | end
190 |
191 | class TooManyTargets < ErrorNoLogs
192 | def initialize(num_top_level_targets, max_targets)
193 | super("CHEFRANGE003", num_top_level_targets, max_targets)
194 | end
195 | end
196 |
197 | class UnsupportedProtocol < ErrorNoLogs
198 | def initialize(attempted_protocol)
199 | super("CHEFVAL011", attempted_protocol,
200 | ChefApply::Config::SUPPORTED_PROTOCOLS.join(" "))
201 | end
202 | end
203 | end
204 | end
205 |
--------------------------------------------------------------------------------
/lib/chef_apply/telemeter.rb:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright:: Copyright (c) 2018-2019 Chef Software Inc.
3 | # Author:: Marc A. Paradise
4 | # License:: Apache License, Version 2.0
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # http://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 |
18 | require "chef/telemeter"
19 | # Monkey patch the telemetry lib to respect our config.toml
20 | # entry for telemetry.
21 | require_relative "telemeter/patch"
22 | module ChefApply
23 | class Telemeter
24 | def self.timed_action_capture(action, &block)
25 | # Note: we do not directly capture hostname for privacy concerns, but
26 | # using a sha1 digest will allow us to anonymously see
27 | # unique hosts to derive number of hosts affected by a command
28 | target = action.target_host
29 | target_data = { platform: {}, hostname_sha1: nil, transport_type: nil }
30 | if target
31 | target_data[:platform][:name] = target.base_os # :windows, :linux, eventually :macos
32 | target_data[:platform][:version] = target.version
33 | target_data[:platform][:architecture] = target.architecture
34 | target_data[:hostname_sha1] = Digest::SHA1.hexdigest(target.hostname.downcase)
35 | target_data[:transport_type] = target.transport_type
36 | end
37 | Chef::Telemeter.timed_capture(:action, { action: action.name, target: target_data }, &block)
38 | end
39 |
40 | def self.capture(name, data = {}, options = {})
41 | Chef::Telemeter.capture(name, data, options)
42 | end
43 | end
44 | end
45 |
--------------------------------------------------------------------------------
/lib/chef_apply/telemeter/patch.rb:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright:: Copyright (c) 2018 Chef Software Inc.
3 | # License:: Apache License, Version 2.0
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 | # http://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 |
18 | class Telemetry
19 | class Session
20 | # The telemetry session data is normally kept in .chef, which we don't have.
21 | def session_file
22 | ChefApply::Config.telemetry_session_file.freeze
23 | end
24 | end
25 |
26 | def deliver(data = {})
27 | if Chef::Telemeter.instance.enabled?
28 | payload = event.prepare(data)
29 | client.await.fire(payload)
30 | end
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/lib/chef_apply/text.rb:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright:: Copyright (c) 2017 Chef Software Inc.
3 | # License:: Apache License, Version 2.0
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 | # http://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 |
18 | require "r18n-desktop"
19 | require_relative "text/text_wrapper"
20 | require_relative "text/error_translation"
21 |
22 | # A very thin wrapper around R18n, the idea being that we're likely to replace r18n
23 | # down the road and don't want to have to change all of our commands.
24 | module ChefApply
25 | module Text
26 | def self._error_table
27 | # Though there may be several translations, en.yml will be the only one with
28 | # error metadata.
29 | path = File.join(_translation_path, "errors", "en.yml")
30 | raw_yaml = File.read(path)
31 | @error_table ||= YAML.load(raw_yaml, filename: _translation_path, symbolize_names: true)[:errors]
32 | end
33 |
34 | def self._translation_path
35 | @translation_path ||= File.join(File.dirname(__FILE__), "..", "..", "i18n")
36 | end
37 |
38 | def self.load
39 | R18n.from_env(Text._translation_path)
40 | R18n.extension_places << File.join(Text._translation_path, "errors")
41 | t = R18n.get.t
42 | t.translation_keys.each do |k|
43 | k = k.to_sym
44 | define_singleton_method k do |*args|
45 | TextWrapper.new(t.send(k, *args))
46 | end
47 | end
48 | end
49 |
50 | # Load on class load to ensure our text accessor methods are available from the start.
51 | load
52 | end
53 | end
54 |
--------------------------------------------------------------------------------
/lib/chef_apply/text/error_translation.rb:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright:: Copyright (c) 2017 Chef Software Inc.
3 | # License:: Apache License, Version 2.0
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 | # http://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 |
18 | module ChefApply
19 | module Text
20 | class ErrorTranslation
21 | ATTRIBUTES = %i{decorations header footer stack log}.freeze
22 | attr_reader :message, *ATTRIBUTES
23 |
24 | def initialize(id, params: [])
25 | # To get access to the metadata we'll go directly through the parsed yaml.
26 | # Accessing via R18n is unnecessarily complicated
27 | yml = Text._error_table
28 |
29 | # We'll still use our Text mechanism for the text itself so that
30 | # parameters, pluralization, etc will still work.
31 | # This will raise if the key doesn't exist.
32 | @message = Text.errors.send(id).text(*params)
33 | options = yml[:display_defaults]
34 |
35 | # Override any defaults if display metadata is given
36 | display_opts = yml[id.to_sym][:display]
37 | options = options.merge(display_opts) unless display_opts.nil?
38 |
39 | ATTRIBUTES.each do |attribute|
40 | instance_variable_set("@#{attribute}", options.delete(attribute))
41 | end
42 |
43 | if options.length > 0
44 | # Anything not in ATTRIBUTES is not supported. This will also catch
45 | # typos in attr names
46 | raise InvalidDisplayAttributes.new(id, options)
47 | end
48 | end
49 |
50 | def inspect
51 | inspection = "#{self}: "
52 | ATTRIBUTES.each do |attribute|
53 | inspection << "#{attribute}: #{send(attribute.to_s)}; "
54 | end
55 | inspection << "message: #{message.gsub("\n", "\\n")}"
56 | inspection
57 | end
58 |
59 | class InvalidDisplayAttributes < RuntimeError
60 | attr_reader :invalid_attrs
61 | def initialize(id, attrs)
62 | @invalid_attrs = attrs
63 | super("Invalid display attributes found for #{id}: #{attrs}")
64 | end
65 | end
66 |
67 | end
68 | end
69 | end
70 |
--------------------------------------------------------------------------------
/lib/chef_apply/text/text_wrapper.rb:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright:: Copyright (c) 2017 Chef Software Inc.
3 | # License:: Apache License, Version 2.0
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 | # http://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 |
18 | module ChefApply
19 | module Text
20 | # Our text spinner class really doesn't like handling the TranslatedString or Untranslated classes returned
21 | # by the R18n library. So instead we return these TextWrapper instances which have dynamically defined methods
22 | # corresponding to the known structure of the R18n text file. Most importantly, if a user has accessed
23 | # a leaf node in the code we return a regular String instead of the R18n classes.
24 | class TextWrapper
25 | def initialize(translation_tree)
26 | @tree = translation_tree
27 | @tree.translation_keys.each do |k|
28 | # Integer keys are not translatable - they're quantity indicators in the key that
29 | # are instead sent as arguments. If we see one here, it means it was not correctly
30 | # labeled as plural with !!pl in the parent key
31 | if k.class == Integer
32 | raise MissingPlural.new(@tree.instance_variable_get(:@path), k)
33 | end
34 |
35 | k = k.to_sym
36 | define_singleton_method k do |*args|
37 | subtree = @tree.send(k, *args)
38 | if subtree.translation_keys.empty?
39 | # If there are no more possible children, just return the translated value
40 | subtree.to_s
41 | else
42 | TextWrapper.new(subtree)
43 | end
44 | end
45 | end
46 | end
47 |
48 | def method_missing(name, *args)
49 | raise InvalidKey.new(@tree.instance_variable_get(:@path), name)
50 | end
51 |
52 | # TODO - make the checks for these conditions lint steps that run during build
53 | # instead of part of the shipped product.
54 | class TextError < RuntimeError
55 | attr_accessor :line
56 | def set_call_context
57 | # TODO - this can vary (8 isn't always right) - inspect
58 | @line = caller(8, 1).first
59 | if @line =~ %r{.*/lib/(.*\.rb):(\d+)}
60 | @line = "File: #{$1} Line: #{$2}"
61 | end
62 | end
63 | end
64 |
65 | class InvalidKey < TextError
66 | def initialize(path, terminus)
67 | set_call_context
68 | # Calling back into Text here seems icky, this is an error
69 | # that only engineering should see.
70 | message = "i18n key #{path}.#{terminus} does not exist.\n"
71 | message << "Referenced from #{line}"
72 | super(message)
73 | end
74 | end
75 |
76 | class MissingPlural < TextError
77 | def initialize(path, terminus)
78 | set_call_context
79 | message = "i18n key #{path}.#{terminus} appears to reference a pluralization.\n"
80 | message << "Please append the plural indicator '!!pl' to the end of #{path}.\n"
81 | message << "Referenced from #{line}"
82 | super(message)
83 | end
84 | end
85 | end
86 | end
87 | end
88 |
--------------------------------------------------------------------------------
/lib/chef_apply/ui/plain_text_element.rb:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright:: Copyright (c) 2017 Chef Software Inc.
3 | # License:: Apache License, Version 2.0
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 | # http://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 |
18 | module ChefApply
19 | module UI
20 | class PlainTextElement
21 | def initialize(format, opts)
22 | @orig_format = format
23 | @format = format
24 | @output = opts[:output]
25 | end
26 |
27 | def run(&block)
28 | yield
29 | end
30 |
31 | def update(params)
32 | # Some of this is particular to our usage -
33 | # prefix does not cause a text update, but does
34 | # change the prefix for future messages.
35 | if params.key?(:prefix)
36 | @format = @orig_format.gsub(":prefix", params[:prefix])
37 | return
38 | end
39 |
40 | if @succ
41 | ind = "OK"
42 | @succ = false
43 | log_method = :info
44 | elsif @err
45 | ind = "ERR"
46 | @err = false
47 | log_method = :error
48 | else
49 | log_method = :debug
50 | ind = " - "
51 | end
52 |
53 | # Since this is a generic type, we can replace any component
54 | # name in this regex - but for now :spinner is the only component
55 | # we're standing in for.
56 | msg = @format.gsub(/:spinner/, ind)
57 | params.each_pair do |k, v|
58 | msg.gsub!(/:#{k}/, v)
59 | end
60 | ChefApply::Log.send(log_method, msg)
61 | @output.puts(msg)
62 | end
63 |
64 | def error
65 | @err = true
66 | @succ = false
67 | end
68 |
69 | def success
70 | @succ = true
71 | @err = false
72 | end
73 |
74 | def auto_spin; end
75 | end
76 | end
77 | end
78 |
--------------------------------------------------------------------------------
/lib/chef_apply/ui/plain_text_header.rb:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright:: Copyright (c) 2017 Chef Software Inc.
3 | # License:: Apache License, Version 2.0
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 | # http://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_relative "plain_text_element"
18 |
19 | module ChefApply
20 | module UI
21 | class PlainTextHeader
22 | def initialize(format, opts)
23 | @format = format
24 | @output = opts[:output]
25 | @children = {}
26 | @threads = []
27 | end
28 |
29 | def register(child_format, child_opts, &block)
30 | child_opts[:output] = @output
31 | child = PlainTextElement.new(child_format, child_opts)
32 | @children[child] = block
33 | end
34 |
35 | def auto_spin
36 | msg = @format.gsub(/:spinner/, " HEADER ")
37 | @output.puts(msg)
38 | @children.each do |child, block|
39 | @threads << Thread.new { block.call(child) }
40 | end
41 | @threads.each(&:join)
42 | end
43 | end
44 | end
45 | end
46 |
--------------------------------------------------------------------------------
/lib/chef_apply/ui/terminal.rb:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright:: Copyright (c) 2018 Chef Software Inc.
3 | # License:: Apache License, Version 2.0
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 | # http://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 |
18 | require "tty-spinner"
19 | require "tty-cursor"
20 | require_relative "../status_reporter"
21 | require_relative "../config"
22 | require_relative "../log"
23 | require_relative "plain_text_element"
24 | require_relative "plain_text_header"
25 | module ChefApply
26 | module UI
27 | class Terminal
28 | class << self
29 | # To support matching in test
30 | attr_accessor :location
31 |
32 | def init(location)
33 | @location = location
34 | end
35 |
36 | def write(msg)
37 | @location.write(msg)
38 | end
39 |
40 | def output(msg)
41 | @location.puts msg
42 | end
43 |
44 | def render_parallel_jobs(header, jobs)
45 | # Do not indent the topmost 'parent' spinner, but do indent child spinners
46 | indent_style = { top: "",
47 | middle: TTY::Spinner::Multi::DEFAULT_INSET[:middle],
48 | bottom: TTY::Spinner::Multi::DEFAULT_INSET[:bottom] }
49 | # @option options [Hash] :style
50 | # keys :top :middle and :bottom can contain Strings that are used to
51 | # indent the spinners. Ignored if message is blank
52 | multispinner = get_multispinner.new("[:spinner] #{header}",
53 | output: @location,
54 | hide_cursor: true,
55 | style: indent_style)
56 | jobs.each do |job|
57 | multispinner.register(spinner_prefix(job.prefix), hide_cursor: true) do |spinner|
58 | reporter = StatusReporter.new(spinner, prefix: job.prefix, key: :status)
59 | job.run(reporter)
60 | end
61 | end
62 | multispinner.auto_spin
63 | ensure
64 | # Spinners hide the cursor for better appearance, so we need to make sure
65 | # we always bring it back
66 | show_cursor
67 | end
68 |
69 | def render_job(initial_msg, job)
70 | # TODO why do we have to pass prefix to both the spinner and the reporter?
71 | spinner = get_spinner.new(spinner_prefix(job.prefix), output: @location, hide_cursor: true)
72 | reporter = StatusReporter.new(spinner, prefix: job.prefix, key: :status)
73 | reporter.update(initial_msg)
74 | spinner.auto_spin
75 | job.run(reporter)
76 | end
77 |
78 | def spinner_prefix(prefix)
79 | spinner_msg = "[:spinner] "
80 | spinner_msg += ":prefix " unless prefix.empty?
81 | spinner_msg + ":status"
82 | end
83 |
84 | def get_multispinner
85 | ChefApply::Config.dev.spinner ? TTY::Spinner::Multi : PlainTextHeader
86 | end
87 |
88 | def get_spinner
89 | ChefApply::Config.dev.spinner ? TTY::Spinner : PlainTextElement
90 | end
91 |
92 | def show_cursor
93 | TTY::Cursor.show
94 | end
95 | end
96 | end
97 | end
98 | end
99 |
--------------------------------------------------------------------------------
/lib/chef_apply/ui/terminal/job.rb:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright:: Copyright (c) 2018 Chef Software Inc.
3 | # License:: Apache License, Version 2.0
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 | # http://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 |
18 | module ChefApply
19 | module UI
20 | class Terminal
21 | class Job
22 | attr_reader :proc, :prefix, :target_host, :exception
23 | def initialize(prefix, target_host, &block)
24 | @proc = block
25 | @prefix = prefix
26 | @target_host = target_host
27 | @error = nil
28 | end
29 |
30 | def run(reporter)
31 | @proc.call(reporter)
32 | rescue => e
33 | reporter.error(e.to_s)
34 | @exception = e
35 | end
36 | end
37 | end
38 | end
39 | end
40 |
--------------------------------------------------------------------------------
/lib/chef_apply/version.rb:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright:: Copyright (c) 2018 Chef Software Inc.
3 | # License:: Apache License, Version 2.0
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 | # http://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 |
18 | module ChefApply
19 | VERSION = "0.9.6".freeze
20 | end
21 |
--------------------------------------------------------------------------------
/sonar-project.properties:
--------------------------------------------------------------------------------
1 | # must be unique in a given SonarQube instance
2 | sonar.projectKey=chef_chef-apply_AYcN_IrYJ4YHsO5MtJIP
3 |
4 | sonar.projectName=chef-apply
5 |
6 | # defaults to 'not provided'
7 | #sonar.projectVersion=1.0
8 |
9 | sonar.sources=.
10 | sonar.exclusions=**/*_test.go
11 |
12 | sonar.tests=.
13 | sonar.test.inclusions=**/*_test.go
14 |
15 | # Encoding of the source code. Default is default system encoding
16 | #sonar.sourceEncoding=UTF-8
17 |
18 | # skip C-language processor
19 | sonar.c.file.suffixes=-
20 | sonar.cpp.file.suffixes=-
21 | sonar.objc.file.suffixes=-
--------------------------------------------------------------------------------
/spec/fixtures/custom_config.toml:
--------------------------------------------------------------------------------
1 | [telemetry]
2 | dev = true
3 |
--------------------------------------------------------------------------------
/spec/integration/chef-run_spec.rb:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright:: Copyright (c) 2018 Chef Software Inc.
3 | # License:: Apache License, Version 2.0
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 | # http://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 |
18 | require "spec_helper"
19 | require "integration/spec_helper"
20 | require "chef_apply/cli"
21 | require "chef_apply/version"
22 |
23 | RSpec.describe "chef_appl" do
24 | context "help output" do
25 | context "at the top level" do
26 | ["-h", "--help", ""].each do |arg|
27 | it "#{arg} displays correct help" do
28 | expect { run_cli_with(arg) }.to output(fixture_content("chef_help")).to_stdout
29 | end
30 | end
31 | end
32 | end
33 |
34 | context "version output" do
35 | ["-v", "--version"].each do |arg|
36 | it "#{arg} displays correct version" do
37 | expect { run_cli_with(arg) }.to output(fixture_content("chef_version")).to_stdout
38 | end
39 | end
40 | end
41 | end
42 |
--------------------------------------------------------------------------------
/spec/integration/fixtures/chef_help.out:
--------------------------------------------------------------------------------
1 | Chef Run is a tool to execute ad-hoc tasks using Chef Infra.
2 |
3 | chef-run [PROPERTIES] [FLAGS]
4 |
5 | Runs a single on the specified .
6 | [PROPERTIES] should be specified as key=value.
7 |
8 | For example:
9 |
10 | chef-run web01 service nginx action=restart
11 | chef-run web01,web02 service nginx action=restart
12 | chef-run web0[1:2] service nginx action=restart
13 |
14 | chef-run [FLAGS]
15 |
16 | Runs a single recipe located at on the specified .
17 |
18 | For example:
19 |
20 | chef-run web01 path/to/cookbook/recipe.rb
21 | chef-run web01,web02 path/to/cookbook
22 | chef-run web0[1:2] cookbook_name
23 | chef-run web01 cookbook_name::recipe_name
24 |
25 | ARGUMENTS:
26 | The hosts or IPs to target. Can also be an SSH or WinRM URLs
27 | in the form:
28 |
29 | ssh://[USERNAME]@example.com[:PORT]
30 | A Chef Infra resource, such as 'user' or 'package'
31 | The name, usually used to specify what 'thing' to set up with
32 | the resource. For example, given resource 'user', 'name' would be
33 | the name of the user you wanted to create.
34 | The recipe to converge. This can be provided as one of:
35 | 1. Full path to a recipe file
36 | 2. Cookbook name. First we check the working directory for this
37 | cookbook, then we check in the chef repository path. If a
38 | cookbook is found we run the default recipe.
39 | 3. This behaves similarly to 'cookbook name' above, but it also allows
40 | you to specify which recipe to use from the cookbook.
41 | Usage: cookbookname::recipename
42 |
43 | FLAGS:
44 | --chef-license ACCEPTANCE Accept the license for this product and any contained products ('accept', 'accept-no-persist', or 'accept-silent')
45 | -c, --config PATH Location of config file. Default: $HOME/.chef-workstation/config.toml
46 | --cookbook-repo-paths PATH Comma separated list of cookbook repository paths.
47 | -h, --help Show help and usage for `chef-run`
48 | -i, --identity-file PATH SSH identity file to use when connecting. Keys loaded into ssh-agent will also be used.
49 | --[no-]install Install Chef Infra Client on the target host(s) if it is not installed.
50 | This defaults to enabled - the installation will be performed
51 | if there is no Chef Infra Client on the target(s).
52 | --password Password to use for authentication to the target(s). The same
53 | password will be used for all targets.
54 | -p, --protocol The protocol to use for connecting to targets.
55 | The default is 'ssh', and it can be changed in config.toml by
56 | setting 'connection.default_protocol' to a supported option.
57 | --[no-]ssl Use SSL for WinRM. Current default: false
58 | --[no-]ssl-verify Verify peer certificate when using SSL for WinRM
59 | Use --ssl-no-verify when using SSL for WinRM and
60 | the remote host is using a self-signed certificate.
61 | Current default: true
62 | --[no-]sudo Whether to use root permissions on the target. Default: true
63 | --sudo-command Command to use for administrative/root access. Defaults to 'sudo'.
64 | --sudo-options 'OPTIONS...' Options to use with the sudo command. If there are multiple flags,
65 | quote them. For example: --sudo-options '-H -P -s'
66 | --sudo-password Password to use with the sudo command. This must be provided if
67 | password is required for sudo on the target(s). The same sudo password
68 | will be used for all targets.
69 | --user Username to use for authentication to the target(s). The same
70 | username will be used for all targets.
71 | -v, --version Show the current version of Chef Run.
72 |
--------------------------------------------------------------------------------
/spec/integration/fixtures/chef_version.out:
--------------------------------------------------------------------------------
1 | chef-run: $VERSION
2 |
--------------------------------------------------------------------------------
/spec/integration/spec_helper.rb:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright:: Copyright (c) 2018 Chef Software Inc.
3 | # License:: Apache License, Version 2.0
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 | # http://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 |
18 | require "chef_apply/startup"
19 | require "chef_apply/version"
20 |
21 | # Create the chef configuration directory and touch the config
22 | # file.
23 | # this makes sure our output doesn't include
24 | # an extra line telling us that it's created,
25 | # causing the first integration test to execute to fail on
26 | # CI.
27 | # TODO this is not ideal... let's look at
28 | # testing the output correctly in both cases,
29 | # possible forcing a specific test that will also create
30 | # the directory to run first.
31 | dir = File.join(Dir.home, ".chef-workstation")
32 | conf = File.join(dir, "config.toml")
33 | FileUtils.mkdir_p(dir) unless Dir.exist?(dir)
34 | FileUtils.touch(conf) unless File.exist?(conf)
35 |
36 | # Simple wrapper that runs the CLI and prevents it
37 | # from aborting all tests with a SystemExit.
38 | # We could shell out, but this will run a little faster as we
39 | # accumulate more things to test - and will work better to get
40 | # accurate simplecov coverage reporting.
41 | # usage:
42 | # expect {run_with_cli("blah")}.to output("blah").to_stdout
43 | def run_cli_with(args)
44 | ChefApply::Startup.new(args.split(" ")).run(enforce_license: true)
45 | rescue SystemExit
46 | end
47 |
48 | def fixture_content(name)
49 | content = File.read(File.join("spec/integration/fixtures", "#{name}.out"))
50 | # Replace $VERSION if present - this is updated automatically, so we can't include
51 | # the literal version value in the fixture without
52 | # having expeditor update it there too...
53 | content.gsub("$VERSION", ChefApply::VERSION)
54 | .gsub("$HOME", Dir.home)
55 | end
56 |
--------------------------------------------------------------------------------
/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright:: Copyright (c) 2018 Chef Software Inc.
3 | # License:: Apache License, Version 2.0
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 | # http://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 |
18 | require "bundler/setup"
19 | require "simplecov"
20 | require "rspec/expectations"
21 | require "support/matchers/output_to_terminal"
22 |
23 | # class << Kernel
24 | # alias :_require :require
25 | # def require(*args)
26 | #
27 | # show = false
28 | # args.each do |a|
29 | # if a =~ /chef_apply.*/
30 | # show = true
31 | # break
32 | # end
33 | # end
34 | #
35 | # $stderr.puts "from #{File.basename(caller[1])}: require: %s" % [args.inspect] if show
36 | # _require(*args)
37 | # end
38 | #
39 | # alias :_load :load
40 | # def load(*args)
41 | # show = false
42 | # args.each do |a|
43 | # if a =~ /chef_apply.*/
44 | # show = true
45 | # break
46 | # end
47 | # end
48 | # $stderr.puts "from #{File.basename(caller[1])}: load: %s" % [args.inspect] if show
49 | # _load(*args)
50 | # end
51 | #
52 | # end
53 | #
54 | # module Kernel
55 | # def require(*args)
56 | # Kernel.require(*args)
57 | # end
58 | # def load(*args)
59 | # Kernel.load(*args)
60 | # end
61 | # end
62 |
63 | RemoteExecResult = Struct.new(:exit_status, :stdout, :stderr)
64 |
65 | class ChefApply::MockReporter
66 | def update(msg); ChefApply::UI::Terminal.output msg; end
67 |
68 | def success(msg); ChefApply::UI::Terminal.output "SUCCESS: #{msg}"; end
69 |
70 | def error(msg); ChefApply::UI::Terminal.output "FAILURE: #{msg}"; end
71 | end
72 |
73 | RSpec::Matchers.define :exit_with_code do |expected_code|
74 | actual_code = nil
75 | match do |block|
76 | begin
77 | block.call
78 | rescue SystemExit => e
79 | actual_code = e.status
80 | end
81 | actual_code && actual_code == expected_code
82 | end
83 |
84 | failure_message do |block|
85 | result = actual.nil? ? " did not call exit" : " called exit(#{actual_code})"
86 | "expected exit(#{expected_code}) but it #{result}."
87 | end
88 |
89 | failure_message_when_negated do |block|
90 | "expected exit(#{expected_code}) but it did."
91 | end
92 |
93 | description do
94 | "expect exit(#{expected_code})"
95 | end
96 |
97 | supports_block_expectations do
98 | true
99 | end
100 | end
101 | # TODO would read better to make this a custom matcher.
102 | # Simulates a recursive string lookup on the Text object
103 | #
104 | # assert_string_lookup("tree.tree.tree.leaf", "a returned string")
105 | # TODO this can be more cleanly expressed as a custom matcher...
106 | def assert_string_lookup(key, retval = "testvalue")
107 | it "should look up string #{key}" do
108 | top_level_method, *call_seq = key.split(".")
109 | terminal_method = call_seq.pop
110 | tmock = double
111 | # Because ordering is important
112 | # (eg calling errors.hello is different from hello.errors),
113 | # we need to add this individually instead of using
114 | # `receive_messages`, which doesn't appear to give a way to
115 | # guarantee ordering
116 | expect(ChefApply::Text).to receive(top_level_method)
117 | .and_return(tmock)
118 | call_seq.each do |m|
119 | expect(tmock).to receive(m).ordered.and_return(tmock)
120 | end
121 | expect(tmock).to receive(terminal_method)
122 | .ordered.and_return(retval)
123 | subject.call
124 | end
125 | end
126 |
127 | RSpec.configure do |config|
128 | # Enable flags like --only-failures and --next-failure
129 | config.example_status_persistence_file_path = ".rspec_status"
130 | config.run_all_when_everything_filtered = true
131 | config.filter_run :focus
132 |
133 | # Disable RSpec exposing methods globally on `Module` and `main`
134 | config.disable_monkey_patching!
135 |
136 | config.expect_with :rspec do |c|
137 | c.syntax = :expect
138 | end
139 |
140 | config.mock_with :rspec do |mocks|
141 | mocks.verify_partial_doubles = true
142 | end
143 |
144 | config.before(:all) do
145 | ChefApply::Log.setup File::NULL, :error
146 | ChefApply::UI::Terminal.init(File.open(File::NULL, "w"))
147 | end
148 | end
149 |
150 | SimpleCov.start
151 |
--------------------------------------------------------------------------------
/spec/support/matchers/output_to_terminal.rb:
--------------------------------------------------------------------------------
1 | require "rspec/matchers/built_in/output"
2 | require "chef_apply/ui/terminal"
3 |
4 | # Custom behavior for the builtin output matcher
5 | # to allow it to handle to_terminal, which integrates
6 | # with our UI::Terminal interface.
7 | module RSpec
8 | module Matchers
9 | module BuiltIn
10 | class Output < BaseMatcher
11 | # @api private
12 | # Provides the implementation for `output`.
13 | # Not intended to be instantiated directly.
14 | def to_terminal
15 | @stream_capturer = CaptureTerminal
16 | self
17 | end
18 |
19 | module CaptureTerminal
20 | def self.name
21 | "terminal"
22 | end
23 |
24 | def self.capture(block)
25 | captured_stream = StringIO.new
26 | original_stream = ::ChefApply::UI::Terminal.location
27 | ::ChefApply::UI::Terminal.location = captured_stream
28 | block.call
29 | captured_stream.string
30 | ensure
31 | ::ChefApply::UI::Terminal.location = original_stream
32 | end
33 | end
34 | end
35 | end
36 | end
37 | end
38 |
--------------------------------------------------------------------------------
/spec/unit/action/base_spec.rb:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright:: Copyright (c) 2018 Chef Software Inc.
3 | # License:: Apache License, Version 2.0
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 | # http://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 |
18 | require "spec_helper"
19 | require "chef_apply/action/base"
20 | require "chef_apply/telemeter"
21 | require "chef_apply/target_host"
22 |
23 | RSpec.describe ChefApply::Action::Base do
24 | let(:family) { "windows" }
25 | let(:target_host) do
26 | p = double("platform", family: family)
27 | instance_double(ChefApply::TargetHost, platform: p)
28 | end
29 | let(:opts) do
30 | { target_host: target_host,
31 | other: "something-else" }
32 | end
33 | subject(:action) { ChefApply::Action::Base.new(opts) }
34 |
35 | context "#initialize" do
36 | it "properly initializes exposed attr readers" do
37 | expect(action.target_host).to eq target_host
38 | expect(action.config).to eq({ other: "something-else" })
39 | end
40 | end
41 |
42 | context "#run" do
43 | it "runs the underlying action, capturing timing via telemetry" do
44 | expect(ChefApply::Telemeter).to receive(:timed_action_capture).with(subject).and_yield
45 | expect(action).to receive(:perform_action)
46 | action.run
47 | end
48 |
49 | it "invokes an action handler when actions occur and a handler is provided" do
50 | @run_action = nil
51 | @args = nil
52 | expect(ChefApply::Telemeter).to receive(:timed_action_capture).with(subject).and_yield
53 | expect(action).to receive(:perform_action) { action.notify(:test_success, "some arg", "some other arg") }
54 | action.run { |action, args| @run_action = action; @args = args }
55 | expect(@run_action).to eq :test_success
56 | expect(@args).to eq ["some arg", "some other arg"]
57 | end
58 | end
59 |
60 | end
61 |
--------------------------------------------------------------------------------
/spec/unit/action/converge_target/ccr_failure_mapper_spec.rb:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright:: Copyright (c) 2018 Chef Software Inc.
3 | # License:: Apache License, Version 2.0
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 | # http://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 |
18 | require "spec_helper"
19 | require "chef_apply/action/converge_target/ccr_failure_mapper"
20 |
21 | RSpec.describe ChefApply::Action::ConvergeTarget::CCRFailureMapper do
22 | let(:cause_line) { nil }
23 | let(:resource) { "apt_package" }
24 | let(:params) do
25 | { resource: resource, resource_name: "a-test-thing",
26 | stderr: "an error", stdout: "other output" }
27 | end
28 | subject { ChefApply::Action::ConvergeTarget::CCRFailureMapper.new(cause_line, params) }
29 |
30 | describe "#exception_args_from_cause" do
31 | context "when resource properties have valid names but invalid values" do
32 | context "and the property is 'action'" do
33 | let(:cause_line) { "Chef::Exceptions::ValidationFailed: Option action must be equal to one of: nothing, install, upgrade, remove, purge, reconfig, lock, unlock! You passed :marve." }
34 | it "returns a correct CHEFCCR003" do
35 | expect(subject.exception_args_from_cause).to eq(
36 | ["CHEFCCR003", "marve",
37 | "nothing, install, upgrade, remove, purge, reconfig, lock, unlock"]
38 | )
39 | end
40 | end
41 |
42 | context "and the property is something else" do
43 | context "and details are available" do
44 | let(:cause_line) { "Chef::Exceptions::ValidationFailed: Option force must be a kind of [TrueClass, FalseClass]! You passed \"purle\"." }
45 | it "returns a correct CHEFCCR004 when details are available" do
46 | expect(subject.exception_args_from_cause).to eq(
47 | ["CHEFCCR004",
48 | "Option force must be a kind of [TrueClass, FalseClass]! You passed \"purle\"."]
49 | )
50 | end
51 | end
52 | context "And less detail is available" do
53 | let(:cause_line) { "Chef::Exceptions::User: linux_user[marc] ((chef-client cookbook)::(chef-client recipe) line 1) had an error: Chef::Exceptions::User: Couldn't lookup integer GID for group name blah" }
54 | it "returns a correct CHEFCCR002" do
55 | expect(subject.exception_args_from_cause).to eq(
56 | ["CHEFCCR002", "Couldn't lookup integer GID for group name blah"]
57 | )
58 | end
59 | end
60 | end
61 | end
62 |
63 | context "when resource is not a known Chef resource" do
64 | let(:cause_line) { "NoMethodError: undefined method `useraaa' for cookbook: (chef-client cookbook), recipe: (chef-client recipe) :Chef::Recipe" }
65 | let(:resource) { "useraaa" }
66 | it "returns a correct CHEFCCR005" do
67 | expect(subject.exception_args_from_cause).to eq(["CHEFCCR005", resource])
68 | end
69 | end
70 |
71 | context "when a resource property does not exist for the given resource" do
72 | let(:cause_line) { "NoMethodError: undefined method `badresourceprop' for Chef::Resource::User::LinuxUser" }
73 | it "returns a correct CHEFCCR006 " do
74 | expect(subject.exception_args_from_cause).to eq(
75 | ["CHEFCCR006", "badresourceprop", "Chef::Resource::User::LinuxUser"]
76 | )
77 | end
78 | end
79 | end
80 |
81 | describe "#raise_mapped_exception!" do
82 | context "when no cause is provided" do
83 | let(:cause_line) { nil }
84 | it "raises a RemoteChefRunFailedToResolveError" do
85 | expect { subject.raise_mapped_exception! }.to raise_error(ChefApply::Action::ConvergeTarget::CCRFailureMapper::RemoteChefRunFailedToResolveError)
86 |
87 | end
88 | end
89 |
90 | context "when a cause is provided" do
91 | context "but can't resolve it" do
92 | let(:cause_line) { "unparseable mess" }
93 | it "raises a RemoteChefClientRunFailedUnknownReason" do
94 | expect { subject.raise_mapped_exception! }.to raise_error(ChefApply::Action::ConvergeTarget::CCRFailureMapper::RemoteChefClientRunFailedUnknownReason)
95 | end
96 | end
97 |
98 | context "and can resolve the cause" do
99 | let(:cause_line) { "NoMethodError: undefined method `badresourceprop' for Chef::Resource::User::LinuxUser" }
100 | it "raises a RemoteChefClientRunFailed" do
101 | expect { subject.raise_mapped_exception! }.to raise_error(ChefApply::Action::ConvergeTarget::CCRFailureMapper::RemoteChefClientRunFailed)
102 | end
103 | end
104 | end
105 | end
106 | end
107 |
--------------------------------------------------------------------------------
/spec/unit/action/generate_local_policy_spec.rb:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright:: Copyright (c) 2017 Chef Software Inc.
3 | # License:: Apache License, Version 2.0
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 | # http://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 "spec_helper"
18 | require "chef_apply/action/generate_local_policy"
19 | require "chef-cli/policyfile_services/install"
20 | require "chef-cli/ui"
21 | require "chef-cli/policyfile_services/export_repo"
22 |
23 | RSpec.describe ChefApply::Action::GenerateLocalPolicy do
24 | subject { ChefApply::Action::GenerateLocalPolicy.new(cookbook: cookbook) }
25 | let(:cookbook) do
26 | double("TempCookbook",
27 | path: "/my/temp/cookbook",
28 | export_path: "/my/temp/cookbook/export",
29 | policyfile_lock_path: "/my/temp/cookbook/policyfile.lock")
30 | end
31 |
32 | let(:installer_double) do
33 | instance_double(ChefCLI::PolicyfileServices::Install, run: :ok)
34 | end
35 |
36 | let(:exporter_double) do
37 | instance_double(ChefCLI::PolicyfileServices::ExportRepo,
38 | archive_file_location: "/path/to/export",
39 | run: :ok)
40 | end
41 |
42 | before do
43 | allow(subject).to receive(:notify)
44 | end
45 |
46 | describe "#perform_action" do
47 | context "in the normal case" do
48 | it "exports the policy notifying caller of progress, setting archive_file_location" do
49 | expect(subject).to receive(:notify).ordered.with(:generating)
50 | expect(subject).to receive(:installer).ordered.and_return installer_double
51 | expect(installer_double).to receive(:run).ordered
52 | expect(subject).to receive(:notify).ordered.with(:exporting)
53 | expect(subject).to receive(:exporter).ordered.and_return exporter_double
54 | expect(exporter_double).to receive(:run).ordered
55 | expect(subject).to receive(:exporter).ordered.and_return exporter_double
56 | expect(subject).to receive(:notify).ordered.with(:success)
57 | subject.perform_action
58 | expect(subject.archive_file_location).to eq("/path/to/export")
59 | end
60 | end
61 |
62 | context "when PolicyfileServices raises an error" do
63 | it "reraises as PolicyfileInstallError" do
64 | expect(subject).to receive(:installer).and_return installer_double
65 | expect(installer_double).to receive(:run).and_raise(ChefCLI::PolicyfileInstallError.new("", nil))
66 | expect { subject.perform_action }.to raise_error(ChefApply::Action::PolicyfileInstallError)
67 | end
68 | end
69 |
70 | context "when the path name is too long" do
71 | let(:name) { "THIS_IS_A_REALLY_LONG_STRING111111111111111111111111111111111111111111111111111111" }
72 |
73 | # There is an issue with policyfile generation where, if we have a cookbook with too long
74 | # of a name or directory name the policyfile will not generate. This is because the tar
75 | # library that ChefCLI uses comes from the Rubygems package and is meant for packaging
76 | # gems up, so it can impose a 100 character limit. We attempt to solve this by ensuring
77 | # that the paths/names we generate with `TempCookbook` are short.
78 | #
79 | # This is here for documentation
80 | # 2018-05-18 mp addendum: this cna take upwards of 15s to run on ci nodes, pending
81 | # for now since it's not testing any Chef Apply functionality.
82 | xit "fails to create when there is a long path name" do
83 | err = ChefCLI::PolicyfileExportRepoError
84 | expect { subject.perform_action }.to raise_error(err) do |e|
85 | expect(e.cause.class).to eq(Gem::Package::TooLongFileName)
86 | expect(e.cause.message).to match(/should be 100 or less/)
87 | end
88 | end
89 | end
90 | end
91 |
92 | describe "#exporter" do
93 |
94 | it "returns a correctly constructed ExportRepo" do
95 | expect(ChefCLI::PolicyfileServices::ExportRepo).to receive(:new)
96 | .with(policyfile: cookbook.policyfile_lock_path,
97 | root_dir: cookbook.path,
98 | export_dir: cookbook.export_path,
99 | archive: true, force: true)
100 | .and_return exporter_double
101 | expect(subject.exporter).to eq exporter_double
102 | end
103 | end
104 |
105 | describe "#installer" do
106 | it "returns a correctly constructed Install service" do
107 | expect(ChefCLI::PolicyfileServices::Install).to receive(:new)
108 | .with(ui: ChefCLI::UI, root_dir: cookbook.path)
109 | .and_return(installer_double)
110 | expect(subject.installer).to eq installer_double
111 | end
112 | end
113 |
114 | end
115 |
--------------------------------------------------------------------------------
/spec/unit/action/generate_temp_cookbook/recipe_lookup_spec.rb:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright:: Copyright (c) 2018 Chef Software Inc.
3 | # License:: Apache License, Version 2.0
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 | # http://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 |
18 | require "spec_helper"
19 | require "chef_apply/action/generate_temp_cookbook/recipe_lookup"
20 | require "chef/exceptions"
21 | require "chef/cookbook/cookbook_version_loader"
22 | require "chef/cookbook_version"
23 | require "chef/cookbook_loader"
24 |
25 | RSpec.describe ChefApply::Action::GenerateTempCookbook::RecipeLookup do
26 | let(:repo_path) { "repo_path" }
27 | subject(:rp) { ChefApply::Action::GenerateTempCookbook::RecipeLookup.new([repo_path]) }
28 | VL = Chef::Cookbook::CookbookVersionLoader
29 | let(:version_loader) { instance_double(VL) }
30 | let(:cookbook_version) { instance_double(Chef::CookbookVersion, root_dir: "dir", name: "name") }
31 | let(:cookbook_loader) { instance_double(Chef::CookbookLoader, load_cookbooks: nil) }
32 |
33 | describe "#split" do
34 | it "splits a customer provided specifier into a cookbook part and possible recipe part" do
35 | expect(rp.split("/some/path")).to eq(%w{/some/path})
36 | expect(rp.split("cookbook::recipe")).to eq(%w{cookbook recipe})
37 | end
38 | end
39 |
40 | describe "#load_cookbook" do
41 | context "when a directory is provided" do
42 | let(:recipe_specifier) { "/some/directory" }
43 | let(:default_recipe) { File.join(recipe_specifier, "default.rb") }
44 | let(:recipes_by_name) { { "default" => default_recipe } }
45 | before do
46 | expect(File).to receive(:directory?).with(recipe_specifier).and_return(true)
47 | expect(VL).to receive(:new).with(recipe_specifier).and_return(version_loader)
48 | end
49 |
50 | it "loads the cookbook and returns the path to the default recipe" do
51 | expect(version_loader).to receive(:load!)
52 | expect(version_loader).to receive(:cookbook_version).and_return(cookbook_version)
53 | expect(rp.load_cookbook(recipe_specifier)).to eq(cookbook_version)
54 | end
55 |
56 | context "the directory is not a cookbook" do
57 | it "raise an InvalidCookbook error" do
58 | expect(version_loader).to receive(:load!).and_raise(Chef::Exceptions::CookbookNotFoundInRepo.new)
59 | expect { rp.load_cookbook(recipe_specifier) }.to raise_error(ChefApply::Action::GenerateTempCookbook::RecipeLookup::InvalidCookbook)
60 | end
61 | end
62 | end
63 |
64 | context "when a cookbook name is provided" do
65 | let(:recipe_specifier) { "cb" }
66 | before do
67 | expect(File).to receive(:directory?).with(recipe_specifier).and_return(false)
68 | expect(Chef::CookbookLoader).to receive(:new).and_return(cookbook_loader)
69 | end
70 |
71 | context "and a cookbook in the cookbook repository exists with that name" do
72 | it "returns the default cookbook" do
73 | expect(cookbook_loader).to receive(:[]).with(recipe_specifier).and_return(cookbook_version)
74 | expect(rp.load_cookbook(recipe_specifier)).to eq(cookbook_version)
75 | end
76 | end
77 |
78 | context "and a cookbook exists but it is invalid" do
79 | it "raises an InvalidCookbook error" do
80 | expect(cookbook_loader).to receive(:[]).with(recipe_specifier).and_raise(Chef::Exceptions::CookbookNotFoundInRepo.new)
81 | expect(File).to receive(:directory?).with(File.join(repo_path, recipe_specifier)).and_return(true)
82 | expect { rp.load_cookbook(recipe_specifier) }.to raise_error(ChefApply::Action::GenerateTempCookbook::RecipeLookup::InvalidCookbook)
83 | end
84 | end
85 |
86 | context "and a cookbook does not exist" do
87 | it "raises an CookbookNotFound error" do
88 | expect(cookbook_loader).to receive(:[]).with(recipe_specifier).and_raise(Chef::Exceptions::CookbookNotFoundInRepo.new)
89 | expect(File).to receive(:directory?).with(File.join(repo_path, recipe_specifier)).and_return(false)
90 | expect { rp.load_cookbook(recipe_specifier) }.to raise_error(ChefApply::Action::GenerateTempCookbook::RecipeLookup::CookbookNotFound)
91 | end
92 | end
93 | end
94 | end
95 |
96 | describe "#find_recipe" do
97 | let(:recipe) { double("recipe") }
98 |
99 | context "no recipe is specified" do
100 | it "finds a default recipe" do
101 | expect(cookbook_version).to receive(:recipe_filenames_by_name).and_return({ "default" => recipe })
102 | expect(cookbook_version).to receive(:recipe_yml_filenames_by_name).and_return({})
103 | expect(rp.find_recipe(cookbook_version)).to eq(recipe)
104 | end
105 | it "when there is no default recipe it raises a NoDefaultRecipe error" do
106 | expect(cookbook_version).to receive(:recipe_filenames_by_name).and_return({})
107 | expect(cookbook_version).to receive(:recipe_yml_filenames_by_name).and_return({})
108 | expect { rp.find_recipe(cookbook_version) }.to raise_error(ChefApply::Action::GenerateTempCookbook::RecipeLookup::NoDefaultRecipe)
109 | end
110 | end
111 |
112 | context "a recipe is specified" do
113 | let(:desired_recipe) { "a_recipe" }
114 | it "finds the specified recipe" do
115 | expect(cookbook_version).to receive(:recipe_filenames_by_name).and_return({ desired_recipe => recipe })
116 | expect(cookbook_version).to receive(:recipe_yml_filenames_by_name).and_return({})
117 | expect(rp.find_recipe(cookbook_version, desired_recipe)).to eq(recipe)
118 | end
119 | it "when there is no recipe with that name it raises a RecipeNotFound error" do
120 | expect(cookbook_version).to receive(:recipe_filenames_by_name).and_return({})
121 | expect(cookbook_version).to receive(:recipe_yml_filenames_by_name).and_return({})
122 | expect { rp.find_recipe(cookbook_version, desired_recipe) }.to raise_error(ChefApply::Action::GenerateTempCookbook::RecipeLookup::RecipeNotFound)
123 | end
124 | end
125 |
126 | context "a yml recipe is specified" do
127 | let(:desired_recipe) { "a_recipe" }
128 | it "finds the specified recipe" do
129 | expect(cookbook_version).to receive(:recipe_filenames_by_name).and_return({})
130 | expect(cookbook_version).to receive(:recipe_yml_filenames_by_name).and_return({ desired_recipe => recipe })
131 | expect(rp.find_recipe(cookbook_version, desired_recipe)).to eq(recipe)
132 | end
133 | end
134 | end
135 | end
136 |
--------------------------------------------------------------------------------
/spec/unit/action/generate_temp_cookbook/temp_cookbook_spec.rb:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright:: Copyright (c) 2018 Chef Software Inc.
3 | # License:: Apache License, Version 2.0
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 | # http://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 |
18 | require "spec_helper"
19 | require "tempfile"
20 | require "securerandom"
21 | require "chef_apply/action/generate_temp_cookbook/temp_cookbook"
22 | RSpec.describe "ChefApply::Action::GenerateTempCookbook::TempCookbook" do
23 | subject(:tc) { ChefApply::Action::GenerateTempCookbook::TempCookbook.new }
24 | let(:uuid) { SecureRandom.uuid }
25 |
26 | before do
27 | @repo_paths = ChefApply::Config.chef.cookbook_repo_paths
28 | ChefApply::Config.chef.cookbook_repo_paths = []
29 | end
30 |
31 | after do
32 | ChefApply::Config.chef.cookbook_repo_paths = @repo_paths
33 | subject.delete
34 | end
35 |
36 | describe "#from_existing_recipe" do
37 | it "raises an error if the recipe does not have a .rb extension" do
38 | err = ChefApply::Action::GenerateTempCookbook::TempCookbook::UnsupportedExtension
39 | expect { subject.from_existing_recipe("/some/file.chef") }.to raise_error(err)
40 | end
41 |
42 | %w{recipes/default.rb recipes/default.yml}.each do |recipe_path|
43 | context "when there is an existing cookbook with recipe #{recipe_path}" do
44 | let(:cb) do
45 | d = Dir.mktmpdir
46 | File.open(File.join(d, "metadata.rb"), "w+") do |f|
47 | f << "name \"foo\""
48 | end
49 | FileUtils.mkdir(File.join(d, "recipes"))
50 | d
51 | end
52 |
53 | let(:existing_recipe) do
54 | File.open(File.join(cb, recipe_path), "w+") do |f|
55 | f.write(uuid)
56 | f
57 | end
58 | end
59 |
60 | after do
61 | FileUtils.remove_entry cb
62 | end
63 |
64 | it "copies the whole cookbook" do
65 | subject.from_existing_recipe(existing_recipe.path)
66 | expect(File.read(File.join(subject.path, recipe_path))).to eq(uuid)
67 | expect(File.read(File.join(subject.path, "Policyfile.rb"))).to eq <<~EXPECTED_POLICYFILE
68 | name "foo_policy"
69 | default_source :supermarket
70 | run_list "foo::default"
71 | cookbook "foo", path: "."
72 | EXPECTED_POLICYFILE
73 | expect(File.read(File.join(subject.path, "metadata.rb"))).to eq("name \"foo\"")
74 | end
75 | end
76 | end
77 |
78 | context "when there is only a single recipe not in a cookbook" do
79 | let(:existing_recipe) do
80 | t = Tempfile.new(["recipe", ".rb"])
81 | t.write(uuid)
82 | t.close
83 | t
84 | end
85 |
86 | after do
87 | existing_recipe.unlink
88 | end
89 |
90 | it "copies the existing recipe into a new cookbook" do
91 | subject.from_existing_recipe(existing_recipe.path)
92 | recipe_filename = File.basename(existing_recipe.path)
93 | recipe_name = File.basename(recipe_filename, File.extname(recipe_filename))
94 | expect(File.read(File.join(subject.path, "recipes/", recipe_filename))).to eq(uuid)
95 | expect(File.read(File.join(subject.path, "Policyfile.rb"))).to eq <<~EXPECTED_POLICYFILE
96 | name "cw_recipe_policy"
97 | default_source :supermarket
98 | run_list "cw_recipe::#{recipe_name}"
99 | cookbook "cw_recipe", path: "."
100 | EXPECTED_POLICYFILE
101 | expect(File.read(File.join(subject.path, "metadata.rb"))).to eq("name \"cw_recipe\"\n")
102 | end
103 | end
104 | end
105 |
106 | describe "#from_resource" do
107 | it "creates a recipe containing the supplied recipe" do
108 | subject.from_resource("directory", "/tmp/foo", [])
109 | expect(File.read(File.join(subject.path, "recipes/default.rb"))).to eq("directory '/tmp/foo'\n")
110 | end
111 | end
112 |
113 | describe "#generate_metadata" do
114 | it "generates metadata in the temp cookbook" do
115 | f = subject.generate_metadata("foo")
116 | expect(File.read(f)).to eq("name \"foo\"\n")
117 | end
118 | end
119 |
120 | describe "#generate_policyfile" do
121 | context "when there is no existing policyfile" do
122 | it "generates a policyfile in the temp cookbook" do
123 | f = subject.generate_policyfile("foo", "bar")
124 | expect(File.read(f)).to eq <<~EXPECTED_POLICYFILE
125 | name "foo_policy"
126 | default_source :supermarket
127 | run_list "foo::bar"
128 | cookbook "foo", path: "."
129 | EXPECTED_POLICYFILE
130 | end
131 |
132 | context "when there are configured cookbook_repo_paths" do
133 | it "generates a policyfile in the temp cookbook" do
134 | ChefApply::Config.chef.cookbook_repo_paths = %w{one two}
135 | f = subject.generate_policyfile("foo", "bar")
136 | expect(File.read(f)).to eq <<~EXPECTED_POLICYFILE
137 | name "foo_policy"
138 | default_source :chef_repo, "one"
139 | default_source :chef_repo, "two"
140 | default_source :supermarket
141 | run_list "foo::bar"
142 | cookbook "foo", path: "."
143 | EXPECTED_POLICYFILE
144 | end
145 | end
146 | end
147 |
148 | context "when there is an existing policyfile" do
149 | before do
150 | File.open(File.join(subject.path, "Policyfile.rb"), "a") do |f|
151 | f << "this is a policyfile"
152 | end
153 | end
154 | it "only overrides the existing run_list in the policyfile" do
155 | f = subject.generate_policyfile("foo", "bar")
156 | expect(File.read(f)).to eq <<~EXPECTED_POLICYFILE
157 | this is a policyfile
158 | # Overriding run_list with command line specified value
159 | run_list "foo::bar"
160 | EXPECTED_POLICYFILE
161 | end
162 | end
163 | end
164 |
165 | describe "#create_resource_definition" do
166 | let(:r1) { "directory" }
167 | let(:r2) { "/tmp" }
168 | let(:props) { nil }
169 | context "when no properties are provided" do
170 | it "it creates a simple resource" do
171 | expect(subject.create_resource_definition(r1, r2, [])).to eq("directory '/tmp'\n")
172 | end
173 | end
174 |
175 | context "when properties are provided" do
176 | let(:props) do
177 | {
178 | "key1" => "value",
179 | "key2" => 0.1,
180 | "key3" => 100,
181 | "key4" => true,
182 | "key_with_underscore" => "value",
183 | }
184 | end
185 |
186 | it "converts the properties to chef-client args" do
187 | expected = <<~EXPECTED_RESOURCE
188 | directory '/tmp' do
189 | key1 'value'
190 | key2 0.1
191 | key3 100
192 | key4 true
193 | key_with_underscore 'value'
194 | end
195 | EXPECTED_RESOURCE
196 | expect(subject.create_resource_definition(r1, r2, props)).to eq(expected)
197 | end
198 | end
199 | end
200 | end
201 |
--------------------------------------------------------------------------------
/spec/unit/action/generate_temp_cookbook_spec.rb:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright:: Copyright (c) 2018 Chef Software Inc.
3 | # License:: Apache License, Version 2.0
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 | # http://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 |
18 | require "chef_apply/action/generate_temp_cookbook"
19 |
20 | RSpec.describe ChefApply::Action::GenerateTempCookbook do
21 | let(:options) { {} }
22 | subject { ChefApply::Action::GenerateTempCookbook }
23 |
24 | describe ".from_options" do
25 | context "when given options for a recipe" do
26 | let(:options) { { recipe_spec: "some::recipe" } }
27 | it "returns a GenerateCookbookFromRecipe action" do
28 | expect(subject.from_options(options)).to be_a(ChefApply::Action::GenerateCookbookFromRecipe)
29 | end
30 | end
31 |
32 | context "when given options for a resource" do
33 | let(:resource_properties) { {} }
34 | let(:options) do
35 | { resource_name: "user1", resource_type: "user",
36 | resource_properties: resource_properties }
37 | end
38 |
39 | it "returns a GenerateCookbookFromResource action" do
40 | expect(subject.from_options(options)).to be_a ChefApply::Action::GenerateCookbookFromResource
41 | end
42 | end
43 |
44 | context "when not given sufficient options for either" do
45 | let(:options) { {} }
46 | it "raises MissingOptions" do
47 | expect { subject.from_options(options) }.to raise_error ChefApply::Action::MissingOptions
48 | end
49 | end
50 |
51 | end
52 |
53 | describe "#perform_action" do
54 | subject { ChefApply::Action::GenerateTempCookbook.new( {} ) }
55 | it "generates a cookbook, notifies caller, and makes the cookbook available" do
56 | expect(subject).to receive(:notify).ordered.with(:generating)
57 | expect(subject).to receive(:generate)
58 | expect(subject).to receive(:notify).ordered.with(:success)
59 | subject.perform_action
60 | expect(subject.generated_cookbook).to_not be nil
61 | end
62 |
63 | end
64 |
65 | end
66 |
67 | RSpec.describe ChefApply::Action::GenerateCookbookFromRecipe do
68 | xit "#generate", "Please implement me"
69 | end
70 |
71 | RSpec.describe ChefApply::Action::GenerateCookbookFromResource do
72 | xit "#generate", "Please implement me"
73 | end
74 |
--------------------------------------------------------------------------------
/spec/unit/action/install_chef/minimum_chef_version_spec.rb:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright:: Copyright (c) 2018 Chef Software Inc.
3 | # License:: Apache License, Version 2.0
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 | # http://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 |
18 | require "chef_apply/action/install_chef/minimum_chef_version"
19 | require "chef_apply/target_host"
20 | require "spec_helper"
21 |
22 | RSpec.describe ChefApply::Action::InstallChef::MinimumChefVersion do
23 | let(:base_os) { :linux }
24 | let(:version) { 14 }
25 | let(:target) { instance_double(ChefApply::TargetHost, base_os: base_os, installed_chef_version: version) }
26 | subject(:klass) { ChefApply::Action::InstallChef::MinimumChefVersion }
27 |
28 | context "#check!" do
29 | context "when chef is not already installed on target" do
30 | before do
31 | expect(target).to receive(:installed_chef_version)
32 | .and_raise ChefApply::TargetHost::ChefNotInstalled.new
33 | end
34 |
35 | it "should return :client_not_installed" do
36 | actual = klass.check!(target, false)
37 | expect(:client_not_installed).to eq(actual)
38 | end
39 |
40 | context "when config is set to check_only" do
41 | it "raises ClientNotInstalled" do
42 | expect do
43 | klass.check!(target, true)
44 | end.to raise_error(ChefApply::Action::InstallChef::MinimumChefVersion::ClientNotInstalled)
45 | end
46 | end
47 | end
48 |
49 | %i{windows linux}.each do |os|
50 | context "on #{os}" do
51 | let(:base_os) { os }
52 | [13, 14].each do |major_version|
53 | context "when chef is already installed at the correct minimum Chef #{major_version} version" do
54 | let(:version) { ChefApply::Action::InstallChef::MinimumChefVersion::CONSTRAINTS[os][major_version] }
55 | it "should return :minimum_version_met" do
56 | actual = klass.check!(target, false)
57 | expect(:minimum_version_met).to eq(actual)
58 | end
59 | end
60 | end
61 | end
62 | end
63 |
64 | installed_expected = {
65 | windows: {
66 | Gem::Version.new("12.1.1") => ChefApply::Action::InstallChef::MinimumChefVersion::Client13Outdated,
67 | Gem::Version.new("13.9.0") => ChefApply::Action::InstallChef::MinimumChefVersion::Client13Outdated,
68 | Gem::Version.new("14.3.37") => ChefApply::Action::InstallChef::MinimumChefVersion::Client14Outdated,
69 | },
70 | linux: {
71 | Gem::Version.new("12.1.1") => ChefApply::Action::InstallChef::MinimumChefVersion::Client13Outdated,
72 | Gem::Version.new("13.9.0") => ChefApply::Action::InstallChef::MinimumChefVersion::Client13Outdated,
73 | Gem::Version.new("14.1.0") => ChefApply::Action::InstallChef::MinimumChefVersion::Client14Outdated,
74 | },
75 | }
76 | %i{windows linux}.each do |os|
77 | context "on #{os}" do
78 | let(:base_os) { os }
79 | installed_expected[os].each do |installed, expected|
80 | context "when chef is already installed on target at version #{installed}" do
81 | let(:version) { installed }
82 | it "notifies of failure and takes no further action" do
83 | expect { klass.check!(target, false) }.to raise_error(expected)
84 | end
85 | end
86 | end
87 | end
88 | end
89 | end
90 | end
91 |
--------------------------------------------------------------------------------
/spec/unit/action/install_chef_spec.rb:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright:: Copyright (c) 2018 Chef Software Inc.
3 | # License:: Apache License, Version 2.0
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 | # http://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 |
18 | require "spec_helper"
19 | require "chef_apply/action/install_chef"
20 |
21 | RSpec.describe ChefApply::Action::InstallChef do
22 | let(:mock_os_name) { "linux" }
23 | let(:mock_os_family) { "linux" }
24 | let(:mock_os_release ) { "unknown" }
25 | let(:mock_opts) do
26 | {
27 | name: mock_os_name,
28 | family: mock_os_family,
29 | release: mock_os_release,
30 | arch: "x86_64",
31 | }
32 | end
33 | let(:target_host) do
34 | ChefApply::TargetHost.mock_instance("mock://user1:password1@localhost", **mock_opts)
35 | end
36 |
37 | let(:reporter) do
38 | ChefApply::MockReporter.new
39 | end
40 |
41 | subject(:install) do
42 | ChefApply::Action::InstallChef.new(target_host: target_host,
43 | reporter: reporter,
44 | check_only: false)
45 | end
46 |
47 | context "#perform_action" do
48 | context "when chef is already installed on target" do
49 | it "notifies of success and takes no further action" do
50 | expect(ChefApply::Action::InstallChef::MinimumChefVersion).to receive(:check!).with(install.target_host, false)
51 | .and_return(:minimum_version_met)
52 | expect(install).not_to receive(:perform_local_install)
53 | install.perform_action
54 | end
55 | end
56 |
57 | context "when chef is not already installed on target" do
58 | it "should invoke perform_local_install" do
59 | expect(ChefApply::Action::InstallChef::MinimumChefVersion).to receive(:check!).with(install.target_host, false)
60 | .and_return(:client_not_installed)
61 | expect(install).to receive(:perform_local_install)
62 | install.perform_action
63 | end
64 | end
65 | end
66 |
67 | context "#perform_local_install" do
68 | let(:artifact) { double("artifact") }
69 | let(:package_url) { "https://chef.io/download/package/here" }
70 | before do
71 | allow(artifact).to receive(:url).and_return package_url
72 | end
73 |
74 | it "performs the steps necessary to perform an installation" do
75 | expect(install).to receive(:lookup_artifact).and_return artifact
76 | expect(install).to receive(:download_to_workstation).with(package_url) .and_return "/local/path"
77 | expect(install).to receive(:upload_to_target).with("/local/path").and_return("/remote/path")
78 | expect(target_host).to receive(:install_package).with("/remote/path")
79 |
80 | install.perform_local_install
81 | end
82 | end
83 |
84 | context "#train_to_mixlib" do
85 | let(:platform) { double }
86 | before do
87 | allow(platform).to receive(:release).and_return "1234"
88 | allow(platform).to receive(:name).and_return "beos"
89 | allow(platform).to receive(:arch).and_return "ppc"
90 | end
91 |
92 | context "when any flavor of windows" do
93 | before do
94 | allow(platform).to receive(:name).and_return "windows_10_pro_n"
95 | end
96 |
97 | it "sets platform name to 'windows'" do
98 | mixlib_info = install.train_to_mixlib(platform)
99 | expect(mixlib_info[:platform]).to eq "windows"
100 | end
101 | end
102 |
103 | context "when redhat" do
104 | before do
105 | allow(platform).to receive(:name).and_return "redhat"
106 | end
107 |
108 | it "sets platform name to 'el'" do
109 | mixlib_info = install.train_to_mixlib(platform)
110 | expect(mixlib_info[:platform]).to eq "el"
111 | end
112 | end
113 |
114 | context "when centos" do
115 | before do
116 | allow(platform).to receive(:name).and_return "centos"
117 | end
118 |
119 | it "sets platform name to 'el'" do
120 | mixlib_info = install.train_to_mixlib(platform)
121 | expect(mixlib_info[:platform]).to eq "el"
122 | end
123 | end
124 |
125 | context "when suse" do
126 | before do
127 | allow(platform).to receive(:name).and_return "suse"
128 | end
129 |
130 | it "sets platform name to 'sles'" do
131 | mixlib_info = install.train_to_mixlib(platform)
132 | expect(mixlib_info[:platform]).to eq "sles"
133 | end
134 | end
135 | context "when amazon" do
136 | before do
137 | allow(platform).to receive(:name).and_return "amazon"
138 | end
139 |
140 | context "when amazon linux 1.x" do
141 | before do
142 | allow(platform).to receive(:release).and_return "2017.09"
143 | end
144 |
145 | it "sets platform name to 'amazon' and plaform_version to '6'" do
146 | mixlib_info = install.train_to_mixlib(platform)
147 | expect(mixlib_info[:platform]).to eq "el"
148 | expect(mixlib_info[:platform_version]).to eq "6"
149 | end
150 | end
151 | context "when amazon linux 2.x" do
152 | before do
153 | allow(platform).to receive(:release).and_return "2"
154 | end
155 |
156 | it "sets platform name to 'amazon' and plaform_version to '7'" do
157 | mixlib_info = install.train_to_mixlib(platform)
158 | expect(mixlib_info[:platform]).to eq "el"
159 | expect(mixlib_info[:platform_version]).to eq "7"
160 | end
161 | end
162 | end
163 | end
164 | end
165 |
--------------------------------------------------------------------------------
/spec/unit/cli/options_spec.rb:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright:: Copyright (c) 2018 Chef Software Inc.
3 | # License:: Apache License, Version 2.0
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 | # http://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 |
18 | require "spec_helper"
19 | require "mixlib/cli"
20 | require "chef_apply/cli/options"
21 | require "chef-config/config"
22 |
23 | ChefApply::Config.load
24 |
25 | module ChefApply
26 | module CLIOptions
27 | class TestClass
28 | include Mixlib::CLI
29 | include ChefApply::CLI::Options
30 | end
31 |
32 | def parse(argv)
33 | parse_options(argv)
34 | end
35 | end
36 | end
37 |
38 | RSpec.describe ChefApply::CLIOptions do
39 | let(:klass) { ChefApply::CLIOptions::TestClass.new }
40 |
41 | it "contains the specified options" do
42 | expect(klass.options.keys).to eq(%i{
43 | version
44 | help
45 | config_path
46 | identity_file
47 | ssl
48 | ssl_verify
49 | protocol
50 | user
51 | password
52 | cookbook_repo_paths
53 | install
54 | sudo
55 | sudo_command
56 | sudo_password
57 | sudo_options
58 | })
59 | end
60 |
61 | it "persists certain CLI options back to the ChefApply::Config" do
62 | # First we check the default value beforehand
63 | expect(ChefApply::Config.connection.winrm.ssl).to eq(false)
64 | expect(ChefApply::Config.connection.winrm.ssl_verify).to eq(true)
65 | expect(ChefApply::Config.connection.default_protocol).to eq("ssh")
66 | expect(ChefApply::Config.chef.cookbook_repo_paths).to_not be_empty
67 | # Then we set the values and check they are changed
68 | klass.parse_options(["--ssl", "--no-ssl-verify", "--protocol", "winrm", "--cookbook-repo-paths", "a,b"])
69 | expect(ChefApply::Config.connection.winrm.ssl).to eq(true)
70 | expect(ChefApply::Config.connection.winrm.ssl_verify).to eq(false)
71 | expect(ChefApply::Config.connection.default_protocol).to eq("winrm")
72 | expect(ChefApply::Config.chef.cookbook_repo_paths).to eq(%w{a b})
73 | end
74 |
75 | end
76 |
--------------------------------------------------------------------------------
/spec/unit/cli/validation_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 | require "chef_apply/error"
3 | require "chef_apply/cli/validation"
4 |
5 | RSpec.describe ChefApply::CLI::Validation do
6 | class Validator
7 | include ChefApply::CLI::Validation
8 | end
9 | subject { Validator.new }
10 |
11 | context "#validate_params" do
12 | OptionValidationError = ChefApply::CLI::OptionValidationError
13 | it "raises an error if not enough params are specified" do
14 | params = [
15 | [],
16 | %w{one},
17 | ]
18 | params.each do |p|
19 | expect { subject.validate_params(p) }.to raise_error(OptionValidationError) do |e|
20 | e.id == "CHEFVAL002"
21 | end
22 | end
23 | end
24 |
25 | it "succeeds if the second command is a valid file path" do
26 | params = %w{target /some/path}
27 | expect(File).to receive(:exist?).with("/some/path").and_return true
28 | expect { subject.validate_params(params) }.to_not raise_error
29 | end
30 |
31 | it "succeeds if the second argument looks like a cookbook name" do
32 | params = [
33 | %w{target cb},
34 | %w{target cb::recipe},
35 | ]
36 | params.each do |p|
37 | expect { subject.validate_params(p) }.to_not raise_error
38 | end
39 | end
40 |
41 | it "raises an error if the second argument is neither a valid path or a valid cookbook name" do
42 | params = %w{target weird%name}
43 | expect { subject.validate_params(params) }.to raise_error(OptionValidationError) do |e|
44 | e.id == "CHEFVAL004"
45 | end
46 | end
47 |
48 | it "raises an error if properties are not specified as key value pairs" do
49 | params = [
50 | %w{one two three four},
51 | %w{one two three four=value five six=value},
52 | %w{one two three non.word=value},
53 | ]
54 | params.each do |p|
55 | expect { subject.validate_params(p) }.to raise_error(OptionValidationError) do |e|
56 | e.id == "CHEFVAL003"
57 | end
58 | end
59 | end
60 | end
61 | describe "#properties_from_string" do
62 | it "parses properties into a hash" do
63 | provided = %w{key1=value key2=1 key3=true key4=FaLsE key5=0777 key6=https://some.website key7=num1and2digit key_8=underscore key9=127.0.0.1 key10=1. key11=1.1 key12=:symbol}
64 | expected = {
65 | "key1" => "value",
66 | "key2" => 1,
67 | "key3" => true,
68 | "key4" => false,
69 | "key5" => "0777",
70 | "key6" => "https://some.website",
71 | "key7" => "num1and2digit",
72 | "key_8" => "underscore",
73 | "key9" => "127.0.0.1",
74 | "key10" => 1.0,
75 | "key11" => 1.1,
76 | "key12" => :symbol,
77 | }
78 | expect(subject.properties_from_string(provided)).to eq(expected)
79 | end
80 | end
81 |
82 | end
83 |
--------------------------------------------------------------------------------
/spec/unit/config_spec.rb:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright:: Copyright (c) 2018 Chef Software Inc.
3 | # License:: Apache License, Version 2.0
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 | # http://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 |
18 | require "spec_helper"
19 | require "chef_apply/config"
20 |
21 | RSpec.describe ChefApply::Config do
22 | subject(:config) do
23 | ChefApply::Config
24 | end
25 |
26 | before(:each) do
27 | ChefApply::Config.reset
28 | end
29 |
30 | it "raises an error when trying to specify non-existing config location" do
31 | expect { config.custom_location("/does/not/exist") }.to raise_error(RuntimeError, /No config file/)
32 | end
33 |
34 | it "should use default location by default" do
35 | expect(config.using_default_location?).to eq(true)
36 | end
37 |
38 | context "when there is a custom config" do
39 | let(:custom_config) { File.expand_path("../fixtures/custom_config.toml", __dir__) }
40 |
41 | it "successfully loads the config" do
42 | config.custom_location(custom_config)
43 | expect(config.using_default_location?).to eq(false)
44 | expect(config.exist?).to eq(true)
45 | config.load
46 | expect(config.telemetry.dev).to eq(true)
47 | end
48 | end
49 | describe "#load" do
50 | before do
51 | expect(subject).to receive(:exist?).and_return(exists)
52 | end
53 |
54 | context "when the config file exists" do
55 | let(:exists) { true }
56 | it "loads the file" do
57 | expect(subject).to receive(:from_file)
58 | subject.load
59 | end
60 | end
61 |
62 | context "when the config file does not exist" do
63 | let(:exists) { false }
64 | it "does not try to load the file" do
65 | expect(subject).to_not receive(:from_file)
66 | subject.load
67 | end
68 | end
69 | end
70 | end
71 |
--------------------------------------------------------------------------------
/spec/unit/file_fetcher_spec.rb:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright:: Copyright (c) 2018 Chef Software Inc.
3 | # License:: Apache License, Version 2.0
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 | # http://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 |
18 | require "chef_apply/file_fetcher"
19 | require "spec_helper"
20 |
21 | RSpec.describe ChefApply::FileFetcher do
22 | let(:expected_local_location) { File.join(ChefApply::Config.cache.path, "example.txt") }
23 | subject { ChefApply::FileFetcher }
24 | describe ".fetch" do
25 | it "returns the local path when the file is cached" do
26 | allow(FileUtils).to receive(:mkdir)
27 | expect(File).to receive(:exist?).with(expected_local_location).and_return(true)
28 | result = subject.fetch("https://example.com/example.txt")
29 | expect(result).to eq expected_local_location
30 | end
31 |
32 | it "returns the local path when the file is fetched" do
33 | allow(FileUtils).to receive(:mkdir)
34 | expect(File).to receive(:exist?).with(expected_local_location).and_return(false)
35 | expect(subject).to receive(:download_file)
36 | result = subject.fetch("https://example.com/example.txt")
37 | expect(result).to eq expected_local_location
38 | end
39 | end
40 | end
41 |
--------------------------------------------------------------------------------
/spec/unit/fixtures/multi-error.out:
--------------------------------------------------------------------------------
1 | Host: host1 Error: CHEFUPL005: Uploading policy bundle to target failed.
2 | Host: host2 : An unexpected error has occurred: Hello World
3 |
--------------------------------------------------------------------------------
/spec/unit/log_spec.rb:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright:: Copyright (c) 2018 Chef Software Inc.
3 | # License:: Apache License, Version 2.0
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 | # http://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 |
18 | require "spec_helper"
19 | require "chef_apply/log"
20 |
21 | RSpec.describe ChefApply::Log do
22 | Log = ChefApply::Log
23 | let(:output) { StringIO.new }
24 |
25 | before do
26 | Log.setup output, :debug
27 | end
28 |
29 | after do
30 | Log.setup File::NULL, :error
31 | end
32 |
33 | it "correctly logs to stdout" do
34 | Log.debug("test")
35 | expect(output.string).to match(/DEBUG: test$/)
36 | end
37 | end
38 |
--------------------------------------------------------------------------------
/spec/unit/target_host/aix_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 | require "chef_apply/target_host"
3 | require "chef_apply/target_host/aix"
4 |
5 | RSpec.describe ChefApply::TargetHost::Aix do
6 | let(:user) { "testuser" }
7 | let(:host) { "mock://#{user}@example.com" }
8 | let(:family) { "aix" }
9 | let(:name) { "aix" }
10 | let(:path) { "/tmp/blah" }
11 |
12 | subject do
13 | ChefApply::TargetHost.mock_instance(host, family: family, name: name)
14 | end
15 |
16 | context "#make_temp_dir" do
17 | it "creates the directory using a properly formed make_temp_dir" do
18 | expect(subject).to receive(:run_command!)
19 | .with("bash -c '#{ChefApply::TargetHost::Aix::MKTEMP_COMMAND}'")
20 | .and_return(instance_double("result", stdout: "/tmp/blah"))
21 | expect(subject.make_temp_dir).to eq "/tmp/blah"
22 | end
23 | end
24 |
25 | context "#mkdir" do
26 | it "uses a properly formed mkdir to create the directory and changes ownership to connected user" do
27 | expect(subject).to receive(:run_command!).with("mkdir -p /tmp/dir")
28 | subject.mkdir("/tmp/dir")
29 | end
30 | end
31 |
32 | context "#chown" do
33 | # it "uses a properly formed chown to change owning user to the provided user" do
34 | # expect(subject).to receive(:run_command!).with("chown newowner '/tmp/dir'")
35 | # subject.chown("/tmp/dir", "newowner")
36 | # end
37 | xit "Doing nothing for this right now on aix"
38 | end
39 |
40 | context "#install_package" do
41 | context "run the correct pkg run command " do
42 | let(:expected_command) { "installp -aXYgd chef-17.9.26-1.powerpc.bff all" }
43 | it "should run the correct install command" do
44 | expect(subject).to receive(:run_command!).with expected_command
45 | subject.install_package("chef-17.9.26-1.powerpc.bff")
46 | end
47 |
48 | end
49 | end
50 | end
51 |
--------------------------------------------------------------------------------
/spec/unit/target_host/linux_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 | require "chef_apply/target_host"
3 | require "chef_apply/target_host/linux"
4 |
5 | RSpec.describe ChefApply::TargetHost::Linux do
6 | let(:user) { "testuser" }
7 | let(:host) { "mock://#{user}@example.com" }
8 | let(:family) { "linux" }
9 | let(:name) { "linux" }
10 | let(:path) { "/tmp/blah" }
11 |
12 | subject do
13 | ChefApply::TargetHost.mock_instance(host, family: family, name: name)
14 | end
15 |
16 | context "#make_temp_dir" do
17 | it "creates the directory using a properly formed make_temp_dir" do
18 | expect(subject).to receive(:run_command!)
19 | .with("bash -c '#{ChefApply::TargetHost::Linux::MKTEMP_COMMAND}'")
20 | .and_return(instance_double("result", stdout: "/tmp/blah"))
21 | expect(subject.make_temp_dir).to eq "/tmp/blah"
22 | end
23 | end
24 |
25 | context "#mkdir" do
26 | it "uses a properly formed mkdir to create the directory and changes ownership to connected user" do
27 | expect(subject).to receive(:run_command!).with("mkdir -p /tmp/dir")
28 | subject.mkdir("/tmp/dir")
29 | end
30 | end
31 |
32 | context "#chown" do
33 | it "uses a properly formed chown to change owning user to the provided user" do
34 | expect(subject).to receive(:run_command!).with("chown newowner '/tmp/dir'")
35 | subject.chown("/tmp/dir", "newowner")
36 | end
37 | end
38 |
39 | context "#install_package" do
40 | context "when it receives an RPM package" do
41 | let(:expected_command) { "rpm -Uvh /my/package.rpm" }
42 | it "should run the correct rpm command" do
43 | expect(subject).to receive(:run_command!).with expected_command
44 | subject.install_package("/my/package.rpm")
45 |
46 | end
47 |
48 | end
49 | context "when it receives a DEB package" do
50 | let(:expected_command) { "dpkg -i /my/package.deb" }
51 | it "should run the correct dpkg command" do
52 | expect(subject).to receive(:run_command!).with expected_command
53 | subject.install_package("/my/package.deb")
54 | end
55 | end
56 | end
57 | end
58 |
--------------------------------------------------------------------------------
/spec/unit/target_host/macos_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 | require "chef_apply/target_host"
3 | require "chef_apply/target_host/macos"
4 |
5 | RSpec.describe ChefApply::TargetHost::MacOS do
6 | let(:user) { "testuser" }
7 | let(:host) { "mock://#{user}@example.com" }
8 | let(:family) { "darwin" }
9 | let(:name) { "darwin" }
10 |
11 | subject do
12 | ChefApply::TargetHost.mock_instance(host, family: family, name: name)
13 | end
14 |
15 | context "#make_temp_dir" do
16 | it "creates the directory using a properly formed make_temp_dir" do
17 | installer_dir = "/tmp/chef-installer"
18 | expect(subject).to receive(:run_command!)
19 | .with("mkdir -p #{installer_dir}")
20 | .and_return(instance_double("result", stdout: "/tmp/blah"))
21 | expect(subject).to receive(:run_command!)
22 | .with("chmod 777 #{installer_dir}")
23 | .and_return(instance_double("result", stdout: "/tmp/blah"))
24 | expect(subject.make_temp_dir).to eq "/tmp/chef-installer"
25 | end
26 | end
27 |
28 | context "#mkdir" do
29 | it "uses a properly formed mkdir to create the directory and changes ownership to connected user" do
30 | expect(subject).to receive(:run_command!).with("mkdir -p /tmp/dir")
31 | subject.mkdir("/tmp/dir")
32 | end
33 | end
34 |
35 | context "#chown" do
36 | it "uses a properly formed chown to change owning user to the provided user" do
37 | expect(subject).to receive(:run_command!).with("chown newowner '/tmp/dir'")
38 | subject.chown("/tmp/dir", "newowner")
39 | end
40 | end
41 |
42 | context "#install_package" do
43 | it "runs the correct dmg package install command" do
44 | expected_command = <<-EOS
45 | hdiutil detach "/Volumes/chef_software" >/dev/null 2>&1 || true
46 | hdiutil attach /tmp/chef-installer/chef-16.11.7-1.x86_64.dmg -mountpoint "/Volumes/chef_software"
47 | cd / && sudo /usr/sbin/installer -pkg `sudo find "/Volumes/chef_software" -name \\*.pkg` -target /
48 | EOS
49 | expect(subject).to receive(:run_command!).with(expected_command)
50 | subject.install_package("/tmp/chef-installer/chef-16.11.7-1.x86_64.dmg")
51 | end
52 | end
53 | end
54 |
--------------------------------------------------------------------------------
/spec/unit/target_host/solaris_spec.rb:
--------------------------------------------------------------------------------
1 | require "spec_helper"
2 | require "chef_apply/target_host"
3 | require "chef_apply/target_host/solaris"
4 |
5 | RSpec.describe ChefApply::TargetHost::Solaris do
6 | let(:user) { "testuser" }
7 | let(:host) { "mock://#{user}@example.com" }
8 | let(:family) { "solaris" }
9 | let(:name) { "solaris" }
10 | let(:path) { "/tmp/blah" }
11 |
12 | subject do
13 | ChefApply::TargetHost.mock_instance(host, family: family, name: name)
14 | end
15 |
16 | context "#make_temp_dir" do
17 | it "creates the directory using a properly formed make_temp_dir" do
18 | expect(subject).to receive(:run_command!)
19 | .with("bash -c '#{ChefApply::TargetHost::Solaris::MKTEMP_COMMAND}'")
20 | .and_return(instance_double("result", stdout: "/tmp/blah"))
21 | expect(subject.make_temp_dir).to eq "/tmp/blah"
22 | end
23 | end
24 |
25 | context "#mkdir" do
26 | it "uses a properly formed mkdir to create the directory and changes ownership to connected user" do
27 | expect(subject).to receive(:run_command!).with("mkdir -p /tmp/dir")
28 | subject.mkdir("/tmp/dir")
29 | end
30 | end
31 |
32 | context "#chown" do
33 | it "uses a properly formed chown to change owning user to the provided user" do
34 | expect(subject).to receive(:run_command!).with("chown newowner '/tmp/dir'")
35 | subject.chown("/tmp/dir", "newowner")
36 | end
37 | end
38 |
39 | context "#install_package" do
40 | context "run the correct pkg run command " do
41 | let(:expected_command) { "pkg install -g /my/chef-17.3.48-1.i386.p5p chef" }
42 | it "should run the correct install command" do
43 | expect(subject).to receive(:run_command!).with expected_command
44 | subject.install_package("/my/chef-17.3.48-1.i386.p5p")
45 |
46 | end
47 |
48 | end
49 | end
50 | end
51 |
--------------------------------------------------------------------------------
/spec/unit/target_host/windows_spec.rb:
--------------------------------------------------------------------------------
1 |
2 | require "spec_helper"
3 | require "chef_apply/target_host"
4 | require "chef_apply/target_host/windows"
5 |
6 | RSpec.describe ChefApply::TargetHost::Windows do
7 | let(:host) { "mock://user@example.com" }
8 | let(:family) { "windows" }
9 | let(:name) { "windows" }
10 | let(:path) { "C:\\temp\\blah" }
11 |
12 | subject do
13 | ChefApply::TargetHost.mock_instance(host, family: family, name: name)
14 | end
15 |
16 | context "#make_temp_dir" do
17 | it "creates the temporary directory using the correct PowerShell command and returns the path" do
18 | expect(subject).to receive(:run_command!)
19 | .with(ChefApply::TargetHost::Windows::MKTEMP_COMMAND)
20 | .and_return(instance_double("result", stdout: path))
21 | expect(subject.make_temp_dir).to eq(path)
22 | end
23 | end
24 |
25 | context "#mkdir" do
26 | it "creates the directory using the correct command PowerShell command" do
27 | expect(subject).to receive(:run_command!).with("New-Item -ItemType Directory -Force -Path C:\\temp\\dir")
28 | subject.mkdir("C:\\temp\\dir")
29 | end
30 | end
31 |
32 | context "#chown" do
33 | xit "does nothing - this is not implemented on Windows until we need it"
34 | end
35 |
36 | context "#install_package" do
37 | it "runs the correct MSI package install command" do
38 | expected_command = "cmd /c msiexec /package C:\\My\\Package.msi /quiet"
39 | expect(subject).to receive(:run_command!).with(expected_command)
40 | subject.install_package("C:/My/Package.msi")
41 | end
42 | end
43 | end
44 |
--------------------------------------------------------------------------------
/spec/unit/telemeter_spec.rb:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright:: Copyright (c) 2018-2019 Chef Software Inc.
3 | # License:: Apache License, Version 2.0
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 | # http://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 "spec_helper"
18 | require "chef_apply/telemeter"
19 |
20 | RSpec.describe ChefApply::Telemeter do
21 | subject { ChefApply::Telemeter }
22 | let(:host_platform) { "linux" }
23 | context "#timed_action_capture" do
24 | context "when a valid target_host is present" do
25 | it "invokes timed_capture with action and valid target data" do
26 | target = instance_double("TargetHost",
27 | base_os: "windows",
28 | version: "10.0.0",
29 | architecture: "x86_64",
30 | hostname: "My_Host",
31 | transport_type: "winrm")
32 | action = instance_double("Action::Base", name: "test_action",
33 | target_host: target)
34 | expected_data = {
35 | action: "test_action",
36 | target: {
37 | platform: {
38 | name: "windows",
39 | version: "10.0.0",
40 | architecture: "x86_64",
41 | },
42 | hostname_sha1: Digest::SHA1.hexdigest("my_host"),
43 | transport_type: "winrm",
44 | },
45 | }
46 | expect(Chef::Telemeter).to receive(:timed_capture).with(:action, expected_data)
47 | subject.timed_action_capture(action) { :ok }
48 | end
49 |
50 | context "when a valid target_host is not present" do
51 | it "invokes timed_capture with empty target values" do
52 | expected_data = { action: "Base", target: { platform: {},
53 | hostname_sha1: nil,
54 | transport_type: nil } }
55 | expect(Chef::Telemeter).to receive(:timed_capture)
56 | .with(:action, expected_data)
57 | subject.timed_action_capture(
58 | ChefApply::Action::Base.new(target_host: nil)
59 | ) { :ok }
60 | end
61 | end
62 | end
63 | end
64 | end
65 |
--------------------------------------------------------------------------------
/spec/unit/text/error_translation_spec.rb:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright:: Copyright (c) 2017 Chef Software Inc.
3 | # License:: Apache License, Version 2.0
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 | # http://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 |
18 | require "spec_helper"
19 | require "chef_apply/text"
20 |
21 | RSpec.describe ChefApply::Text::ErrorTranslation do
22 |
23 | let(:display_defaults) do
24 | {
25 | decorations: true,
26 | header: true,
27 | footer: true,
28 | stack: false,
29 | log: false }
30 | end
31 |
32 | let(:error_table) do
33 | { display_defaults: display_defaults,
34 | TESTERROR: test_error }
35 | end
36 |
37 | let(:test_error_text) { "This is a test error" }
38 | let(:test_error) { {} }
39 |
40 | subject { ChefApply::Text::ErrorTranslation }
41 | let(:error_mock) do
42 | double("R18n::Translated",
43 | text: test_error_text )
44 | end
45 | let(:translation_mock) do
46 | double("R18n::Translated",
47 | TESTERROR: error_mock,
48 | text: test_error_text )
49 | end
50 | before do
51 | # Mock out the R18n portion - our methods care only that the key exists, and
52 | # the test focus is on the display metadata.
53 | allow(ChefApply::Text).to receive(:errors).and_return translation_mock
54 | allow(ChefApply::Text).to receive(:_error_table).and_return(error_table)
55 | end
56 |
57 | context "when some display attributes are specified" do
58 | let(:test_error) { { display: { stack: true, log: true } } }
59 | it "sets display attributes to specified values and defaults remaining" do
60 | translation = subject.new("TESTERROR")
61 | expect(translation.decorations).to be true
62 | expect(translation.header).to be true
63 | expect(translation.footer).to be true
64 | expect(translation.stack).to be true
65 | expect(translation.log).to be true
66 | expect(translation.message).to eq test_error_text
67 | end
68 | end
69 |
70 | context "when all display attributes are specified" do
71 | let(:test_error) do
72 | { display: { decorations: false, header: false,
73 | footer: false, stack: true, log: true } }
74 | end
75 | it "sets display attributes to specified values with no defaults" do
76 | translation = subject.new("TESTERROR")
77 | expect(translation.decorations).to be false
78 | expect(translation.header).to be false
79 | expect(translation.footer).to be false
80 | expect(translation.stack).to be true
81 | expect(translation.log).to be true
82 | expect(translation.message).to eq test_error_text
83 | end
84 | end
85 |
86 | context "when no attributes for an error are specified" do
87 | let(:test_error) { {} }
88 | it "sets display attribute to default values and references the correct message" do
89 | translation = subject.new("TESTERROR")
90 | expect(translation.decorations).to be true
91 | expect(translation.header).to be true
92 | expect(translation.footer).to be true
93 | expect(translation.stack).to be false
94 | expect(translation.log).to be false
95 | expect(translation.message).to eq test_error_text
96 | end
97 | end
98 |
99 | context "when invalid attributes for an error are specified" do
100 | let(:test_error) { { display: { bad_value: true } } }
101 | it "raises InvalidDisplayAttributes when invalid attributes are specified" do
102 | expect { subject.new("TESTERROR") }
103 | .to raise_error(ChefApply::Text::ErrorTranslation::InvalidDisplayAttributes) do |e|
104 | expect(e.invalid_attrs).to eq({ bad_value: true })
105 | end
106 |
107 | end
108 | end
109 | end
110 |
--------------------------------------------------------------------------------
/spec/unit/ui/terminal_spec.rb:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright:: Copyright (c) 2018 Chef Software Inc.
3 | # License:: Apache License, Version 2.0
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 | # http://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 |
18 | require "spec_helper"
19 | require "chef_apply/ui/terminal"
20 |
21 | RSpec.describe ChefApply::UI::Terminal do
22 | Terminal = ChefApply::UI::Terminal
23 | # Lets send our Terminal output somewhere so it does not clutter the
24 | # test output
25 | Terminal.location = StringIO.new
26 |
27 | it "correctly outputs a message" do
28 | expect { Terminal.output("test") }
29 | .to output("test\n").to_terminal
30 | end
31 |
32 | context "#render_job" do
33 | it "executes the provided block" do
34 | @ran = false
35 | job = Terminal::Job.new("prefix", nil) do
36 | @ran = true
37 | end
38 | Terminal.render_job("a message", job)
39 | expect(@ran).to eq true
40 | end
41 | end
42 |
43 | context "#render_parallel_jobs" do
44 | it "executes the provided job instances" do
45 | @job1ran = false
46 | @job2ran = false
47 | job1 = Terminal::Job.new("prefix", nil) do
48 | @job1ran = true
49 | end
50 | job2 = Terminal::Job.new("prefix", nil) do
51 | @job2ran = true
52 | end
53 | Terminal.render_parallel_jobs("a message", [job1, job2])
54 | expect(@job1ran).to eq true
55 | expect(@job2ran).to eq true
56 | end
57 | it "always resets the cursor to visible" do
58 | job1 = Terminal::Job.new("prefix", nil) do
59 | raise "Cursor should be set visible"
60 | end
61 | expect(Terminal).to receive(:show_cursor)
62 | Terminal.render_parallel_jobs("a message", [job1])
63 | end
64 | end
65 |
66 | describe ChefApply::UI::Terminal::Job do
67 | subject { ChefApply::UI::Terminal::Job }
68 | context "#exception" do
69 | context "when no exception occurs in execution" do
70 | context "and it's been invoked directly" do
71 | it "exception is nil" do
72 | job = subject.new("", nil) { 0 }
73 | job.run(ChefApply::MockReporter.new)
74 | expect(job.exception).to eq nil
75 | end
76 | end
77 | context "and it's running in a thread alongside other jobs" do
78 | it "exception is nil for each job" do
79 | job1 = subject.new("", nil) { 0 }
80 | job2 = subject.new("", nil) { 0 }
81 | threads = []
82 | threads << Thread.new { job1.run(ChefApply::MockReporter.new) }
83 | threads << Thread.new { job2.run(ChefApply::MockReporter.new) }
84 | threads.each(&:join)
85 | expect(job1.exception).to eq nil
86 | expect(job2.exception).to eq nil
87 |
88 | end
89 | end
90 | end
91 | context "when an exception occurs in execution" do
92 | context "and it's been invoked directly" do
93 | it "captures the exception in #exception" do
94 | expected_exception = StandardError.new("exception 1")
95 | job = subject.new("", nil) { |arg| raise expected_exception }
96 | job.run(ChefApply::MockReporter.new)
97 | expect(job.exception).to eq expected_exception
98 | end
99 | end
100 |
101 | context "and it's running in a thread alongside other jobs" do
102 | it "each job holds its own exception" do
103 | e1 = StandardError.new("exception 1")
104 | e2 = StandardError.new("exception 2")
105 |
106 | job1 = subject.new("", nil) { |_| raise e1 }
107 | job2 = subject.new("", nil) { |_| raise e2 }
108 | threads = []
109 | threads << Thread.new { job1.run(ChefApply::MockReporter.new) }
110 | threads << Thread.new { job2.run(ChefApply::MockReporter.new) }
111 | threads.each(&:join)
112 | expect(job1.exception).to eq e1
113 | expect(job2.exception).to eq e2
114 | end
115 | end
116 | end
117 | end
118 | end
119 | end
120 |
--------------------------------------------------------------------------------
/spec/unit/version_spec.rb:
--------------------------------------------------------------------------------
1 | #
2 | # Copyright:: Copyright (c) 2018 Chef Software Inc.
3 | # License:: Apache License, Version 2.0
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 | # http://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 |
18 | require "spec_helper"
19 | require "chef_apply/version"
20 |
21 | RSpec.describe ChefApply::VERSION do
22 | subject(:version) do
23 | ChefApply::VERSION
24 | end
25 |
26 | context "VERSION" do
27 | it "returns the version" do
28 | expect(Gem::Version.correct?(version)).to be_truthy
29 | end
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/warning.txt:
--------------------------------------------------------------------------------
1 | Chef Apply is not meant to be installed as a gem. It is meant to be installed
2 | as part of the Chef Workstation package. Please download and install
3 | that from https://downloads.chef.io/chef-workstation/
4 |
--------------------------------------------------------------------------------