├── lib └── kitchen │ └── verifier │ ├── goss_version.rb │ └── goss.rb ├── test └── integration │ └── simple │ └── goss │ ├── common.yml │ ├── test2.yml │ └── test1.yml ├── Gemfile ├── .gitignore ├── LICENSE ├── kitchen-goss.gemspec └── Readme.md /lib/kitchen/verifier/goss_version.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | 3 | module Kitchen 4 | module Verifier 5 | GOSS_VERSION = '0.1.1'.freeze 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /test/integration/simple/goss/common.yml: -------------------------------------------------------------------------------- 1 | ## 2 | # common info 3 | ## 4 | user_uid : 1122 5 | group_gid : 1122 6 | username : "test123" 7 | groupname : "test123" -------------------------------------------------------------------------------- /test/integration/simple/goss/test2.yml: -------------------------------------------------------------------------------- 1 | file: 2 | /blaaa: 3 | exists: true 4 | mode: "1777" 5 | owner: root 6 | group: root 7 | filetype: directory 8 | contains: [] 9 | process: 10 | sshds: 11 | running: true -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # A sample Gemfile 2 | source "https://rubygems.org" 3 | 4 | gemspec 5 | group :development do 6 | # Use Test Kitchen with Vagrant for converging the build environment 7 | # bundle install --binstubs 8 | gem 'test-kitchen' 9 | gem 'kitchen-vagrant' 10 | gem 'kitchen-localhost' 11 | end -------------------------------------------------------------------------------- /test/integration/simple/goss/test1.yml: -------------------------------------------------------------------------------- 1 | file: 2 | /tmp: 3 | exists: true 4 | mode: "1777" 5 | owner: root 6 | group: root 7 | filetype: directory 8 | contains: [] 9 | user: 10 | vagrant: 11 | exists: true 12 | uid: 1000 13 | gid: 1000 14 | groups: 15 | - vagrant 16 | home: /home/vagrant 17 | {{ .Vars.username }}: 18 | exists: true 19 | uid: {{ .Vars.user_uid }} 20 | gid: {{ .Vars.group_gid }} 21 | test: 22 | exists: true 23 | uid: {{ .Env.test_uid }} 24 | process: 25 | sshd: 26 | running: true -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | /.config 4 | /coverage/ 5 | /InstalledFiles 6 | /pkg/ 7 | /spec/reports/ 8 | /test/tmp/ 9 | /test/version_tmp/ 10 | /tmp/ 11 | 12 | ## Specific to RubyMotion: 13 | .dat* 14 | .repl_history 15 | build/ 16 | 17 | ## Documentation cache and generated files: 18 | /.yardoc/ 19 | /_yardoc/ 20 | /doc/ 21 | /rdoc/ 22 | 23 | ## Environment normalisation: 24 | /.bundle/ 25 | /vendor/bundle 26 | /lib/bundler/man/ 27 | 28 | # for a library or gem, you might want to ignore these files since the code is 29 | # intended to run in multiple environments; otherwise, check them in: 30 | # Gemfile.lock 31 | # .ruby-version 32 | # .ruby-gemset 33 | 34 | # unless supporting rvm < 1.11.0 or doing something fancy, ignore this: 35 | .rvmrc 36 | .project 37 | build.sh 38 | .kitchen/* 39 | .idea 40 | Gemfile.lock 41 | .bundler 42 | bundler_gems 43 | *.pyc 44 | 45 | # 46 | bin/* -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Adham Helal 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /kitchen-goss.gemspec: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | $:.unshift File.expand_path('../lib', __FILE__) 4 | require 'kitchen/verifier/goss_version' 5 | 6 | Gem::Specification.new do |gem| 7 | gem.name = "kitchen-goss" 8 | gem.version = Kitchen::Verifier::GOSS_VERSION 9 | gem.authors = ["Adham Helal"] 10 | gem.email = ["adham.helal@gmail.com"] 11 | gem.licenses = ['MIT'] 12 | gem.homepage = "https://github.com/ahelal/kitchen-goss" 13 | gem.summary = "A test-kitchen verifier plugin for GOSS" 14 | candidates = Dir.glob("{lib}/**/*") + ['README.md', 'kitchen-goss.gemspec'] 15 | gem.files = candidates.sort 16 | gem.platform = Gem::Platform::RUBY 17 | gem.require_paths = ['lib'] 18 | gem.rubyforge_project = '[none]' 19 | gem.description = <<-EOF 20 | == DESCRIPTION: 21 | 22 | GOSS is a tool for validating a server's configuration. 23 | This kitchen plugin adds Goss support as a validation to test-kitchen. 24 | 25 | == FEATURES: 26 | 27 | 28 | EOF 29 | 30 | gem.add_runtime_dependency 'test-kitchen' 31 | gem.add_development_dependency 'rspec' 32 | gem.add_development_dependency 'pry' 33 | 34 | end -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # kitchen-goss 2 | [![Gem Version](https://badge.fury.io/rb/kitchen-goss.svg)](https://badge.fury.io/rb/kitchen-goss) 3 | [![Gem Downloads](http://ruby-gem-downloads-badge.herokuapp.com/kitchen-goss?type=total&color=brightgreen)](https://rubygems.org/gems/kitchen-goss) 4 | 5 | A test-kitchen verifier plugin for GOSS 6 | 7 | ## Intro 8 | [GOSS](https://github.com/aelsabbahy/goss.git) is a tool for validating a server's configuration. 9 | This kitchen plugin adds Goss support as a validation to kitchen. Since GOSS is written in GO lang. This plugin use sftp to push tests to remote machines no ruby is needed to run verify. 10 | 11 | 12 | ## How to install 13 | 14 | ### Ruby gem 15 | ``` 16 | gem install kitchen-goss 17 | ``` 18 | 19 | ### To install from code or develop 20 | ``` 21 | git clone git@github.com:ahelal/kitchen-goss.git 22 | cd kitchen-goss 23 | gem build kitchen-goss.gemspec 24 | gem install kitchen-goss-.gem 25 | ``` 26 | 27 | ## kitchen.yml configuration 28 | ```yaml 29 | verifier : 30 | name : "goss" 31 | sleep : 0 32 | use_sudo : true 33 | goss_version : "v0.3.6" 34 | goss_link : "https://github.com/aelsabbahy/goss/releases/download/$VERSION/goss-linux-${ARCH}" 35 | goss_var_path : "common.yml" 36 | env_vars : {"test_uid": 123} 37 | ``` 38 | 39 | ## kitchen.yml options 40 | Besides the normal config in kitchen.yml goss validation can accept the following options. 41 | 42 | ```ruby 43 | default_config :sleep, 0 44 | default_config :use_sudo, false 45 | default_config :env_vars, {} 46 | default_config :goss_version, "v0.1.5" 47 | default_config :validate_output, "documentation" 48 | default_config :custom_install_command, nil 49 | default_config :goss_link, "https://github.com/aelsabbahy/goss/releases/download/$VERSION/goss-${DISTRO}-${ARCH}" 50 | default_config :goss_download_path, "/tmp/goss-${VERSION}-${DISTRO}-${ARCH}" 51 | default_config :goss_var_path, nil 52 | ``` 53 | 54 | ## Test structure 55 | 56 | Lets say you have a suite name **simple** all yaml files will be uses for testing. 57 | 58 | 59 | ```bash 60 | .kitchen.yml 61 | test/ 62 | \_integration/ 63 | \_simple/ 64 | \_goss/ 65 | \_test1.yml 66 | |_test2.yml 67 | |_common.yml # --vars common.yml with .Vars render, not for goss test 68 | ``` 69 | 70 | 71 | ## License 72 | 73 | MIT 74 | -------------------------------------------------------------------------------- /lib/kitchen/verifier/goss.rb: -------------------------------------------------------------------------------- 1 | require 'kitchen/verifier/base' 2 | 3 | module Kitchen 4 | module Verifier 5 | class Goss < Kitchen::Verifier::Base 6 | require 'mixlib/shellout' 7 | require 'kitchen/util' 8 | require 'pathname' 9 | 10 | kitchen_verifier_api_version 1 11 | plugin_version Kitchen::VERSION 12 | 13 | # 14 | default_config :sleep, 0 15 | default_config :use_sudo, false 16 | default_config :env_vars, {} 17 | default_config :goss_version, 'v0.3.6' 18 | default_config :validate_output, 'documentation' 19 | default_config :custom_install_command, nil 20 | default_config :goss_link, 'https://github.com/aelsabbahy/goss/releases/download/$VERSION/goss-${DISTRO}-${ARCH}' 21 | default_config :goss_download_path, '/tmp/goss-${VERSION}-${DISTRO}-${ARCH}' 22 | default_config :goss_var_path, nil 23 | 24 | def install_command 25 | # If cutom install 26 | info('Installing with custom install command') if config[:custom_install_command] 27 | return config[:custom_install_command] if config[:custom_install_command] 28 | 29 | info('Checking/Installing GOSS') 30 | prefix_command(wrap_shell_code(Util.outdent!(<<-CMD))) 31 | ## Get helper 32 | #{Kitchen::Util.shell_helpers} 33 | 34 | #{goss_filename_flags} 35 | download_url="#{config[:goss_link]}" 36 | goss_download_path="#{config[:goss_download_path]}" 37 | 38 | ## Check do we need to download GOSS 39 | if [ -f "/${goss_download_path}" ]; then 40 | echo "GOSS is installed in ${goss_download_path}" 41 | else 42 | echo "Checking compatibility" 43 | distro="$(uname)" 44 | if [ "x${distro}" != "xLinux" ]; then 45 | echo "Your distro '${distro}' is not supported." 46 | exit 1 47 | fi 48 | echo "Trying to download GOSS to ${goss_download_path}" 49 | do_download ${download_url} ${goss_download_path} 50 | chmod +x ${goss_download_path} 51 | fi 52 | CMD 53 | end 54 | 55 | # (see Base#init_command) 56 | def init_command 57 | return if local_suite_files.empty? 58 | debug('Remove root_path on remote server.') 59 | <<-CMD 60 | suite_dir="#{config[:root_path]}" 61 | if [ "${suite_dir}" = "x" ]; then 62 | echo "root_path is not configured." 63 | exit 1 64 | fi 65 | ## Remove root_path 66 | rm -rf #{config[:root_path]} 67 | ## Create root_path 68 | mkdir -p #{config[:root_path]} 69 | CMD 70 | end 71 | 72 | # Runs the verifier on the instance. 73 | # 74 | # @param state [Hash] mutable instance state 75 | # @raise [ActionFailed] if the action could not be completed 76 | def call(state) 77 | create_sandbox 78 | sandbox_dirs = Dir.glob(File.join(sandbox_path, '*')) 79 | 80 | instance.transport.connection(state) do |conn| 81 | conn.execute(install_command) 82 | conn.execute(init_command) 83 | info("Transferring files to #{instance.to_str}") 84 | conn.upload(sandbox_dirs, config[:root_path]) 85 | debug('Transfer complete') 86 | conn.execute(prepare_command) 87 | conn.execute(run_command) 88 | end 89 | rescue Kitchen::Transport::TransportFailed => ex 90 | if ex.message .include? '' 91 | raise ActionFailed, "Action #verify failed for #{instance.to_str}." 92 | else 93 | raise ActionFailed, ex.message 94 | end 95 | ensure 96 | cleanup_sandbox 97 | end 98 | 99 | # (see Base#run_command) 100 | def run_command 101 | return if local_suite_files.empty? 102 | 103 | debug('Running tests') 104 | prefix_command(wrap_shell_code(Util.outdent!(<<-CMD))) 105 | set +e 106 | #{goss_filename_flags} 107 | command_validate_opts="validate --format #{config[:validate_output]}" 108 | #{run_test_command} 109 | CMD 110 | end 111 | 112 | # Copies all test suite files into the suites directory in the sandbox. 113 | # 114 | # @api private 115 | def prepare_suites 116 | base = File.join(config[:test_base_path], config[:suite_name]) 117 | 118 | local_suite_files.each do |src| 119 | dest = File.join(sandbox_suites_dir, src.sub("#{base}/", '')) 120 | FileUtils.mkdir_p(File.dirname(dest)) 121 | FileUtils.cp(src, dest, preserve: true) 122 | end 123 | end 124 | 125 | # Returns an Array of test suite filenames for the related suite currently 126 | # residing on the local workstation. Any special provisioner-specific 127 | # directories (such as a Chef roles/ directory) are excluded. 128 | # 129 | # @return [Array] array of suite files 130 | # @api private 131 | def local_suite_files 132 | base = File.join(config[:test_base_path], config[:suite_name]) 133 | glob = File.join(base, 'goss/**/*') 134 | # testfiles = Dir.glob(glob).reject { |f| File.directory?(f) } 135 | Dir.glob(glob).reject { |f| File.directory?(f) } 136 | end 137 | 138 | # (see Base#create_sandbox) 139 | def create_sandbox 140 | super 141 | prepare_suites 142 | end 143 | 144 | # @return [String] path to suites directory under sandbox path 145 | # @api private 146 | def sandbox_suites_dir 147 | File.join(sandbox_path, 'suites') 148 | end 149 | 150 | def env_vars 151 | return nil if config[:env_vars].none? 152 | config[:env_vars].map { |k, v| "#{k}=#{v}" }.join(' ') 153 | end 154 | 155 | def remote_var_file 156 | base_path = File.join(config[:test_base_path], config[:suite_name]) 157 | remote_base_path = File.join(config[:root_path], 'suites') 158 | result = '' 159 | local_suite_files.each do |src| 160 | if File.basename(src) == config[:goss_var_path] 161 | result = src.sub(base_path, remote_base_path) 162 | end 163 | end 164 | result 165 | end 166 | 167 | # @return [String] the run command to execute tests 168 | # @api private 169 | def run_test_command 170 | command = config[:goss_download_path] 171 | command = "sudo -E #{command}" if !config[:use_sudo] == true 172 | command = "#{env_vars} #{command}" if config[:env_vars].any? 173 | command = "#{command} --vars #{remote_var_file}" if config[:goss_var_path] 174 | puts command 175 | 176 | <<-CMD 177 | if [ ! -x "#{config[:goss_download_path]}" ]; then 178 | echo "Something failed cant execute '${command}'" 179 | exit 1 180 | fi 181 | 182 | test_failed=0 183 | for VARIABLE in #{get_test_name} 184 | do 185 | #{command} -g ${VARIABLE} ${command_validate_opts} 186 | if [ "$?" -ne 0 ]; then 187 | test_failed=1 188 | fi 189 | done 190 | 191 | # Check exit code 192 | if [ "$test_failed" -ne 0 ]; then 193 | test_failed=1 194 | echo "" 195 | fi 196 | exit ${test_failed} 197 | CMD 198 | end 199 | 200 | def goss_filename_flags 201 | <<-CMD 202 | ## Set the flags for GOSS command path 203 | VERSION="#{config[:goss_version]}" 204 | DISTRO="$(uname)" 205 | ## Need improvements 206 | if [ "$(uname -m)" = "x86_64" ]; then 207 | ARCH="amd64" 208 | else 209 | ARCH="386" 210 | fi 211 | 212 | if [ -f /etc/os-release ]; then 213 | 214 | if [ "$(grep -i 'ubuntu' /etc/os-release)" != "" ]; then 215 | OS="ubuntu" 216 | fi 217 | if [ "$(grep -i 'centos' /etc/os-release)" != "" ]; then 218 | OS="centos" 219 | fi 220 | if [ "$(grep -i '7' /etc/os-release)" != "" ]; then 221 | VER='7' 222 | fi 223 | if [ "$(grep -i '16.04' /etc/os-release)" != "" ]; then 224 | VER='16.04' 225 | fi 226 | else 227 | OS="centos" 228 | VER="6" 229 | fi 230 | 231 | OS_VERSION=${OS}${VER} 232 | echo $OS_VERSION 233 | CMD 234 | end 235 | 236 | def get_test_name 237 | base_path = File.join(config[:test_base_path], config[:suite_name]) 238 | remote_base_path = File.join(config[:root_path], 'suites') 239 | all_tests = '' 240 | local_suite_files.each do |test_file| 241 | if File.basename(test_file) != config[:goss_var_path] && File.basename(test_file).end_with?('.yml') 242 | all_tests += ' ' + test_file.sub(base_path, remote_base_path) 243 | end 244 | end 245 | all_tests 246 | end 247 | 248 | # Sleep for a period of time, if a value is set in the config. 249 | # 250 | # @api private 251 | def sleep_if_set 252 | config[:sleep].to_i.times do 253 | print '.' 254 | sleep 1 255 | end 256 | end 257 | end 258 | end 259 | end 260 | --------------------------------------------------------------------------------