├── .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 | [![Gem Version](https://badge.fury.io/rb/chef-apply.svg)](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 | Chef Workstation 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 | --------------------------------------------------------------------------------