knife-wip 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +3 -0
- data/LICENSE +19 -0
- data/README.md +115 -0
- data/Rakefile +2 -0
- data/knife-wip.gemspec +20 -0
- data/lib/chef/knife/knife-wip.rb +227 -0
- data/lib/knife-wip.rb +3 -0
- data/lib/knife-wip/plugins/eventinator.rb +62 -0
- data/lib/knife-wip/plugins/irccat.rb +44 -0
- metadata +91 -0
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
Copyright (C) 2014 Daniel Schauenberg
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
4
|
+
this software and associated documentation files (the "Software"), to deal in
|
5
|
+
the Software without restriction, including without limitation the rights to
|
6
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
7
|
+
of the Software, and to permit persons to whom the Software is furnished to do
|
8
|
+
so, subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in all
|
11
|
+
copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
19
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,115 @@
|
|
1
|
+
# knife-wip
|
2
|
+
|
3
|
+
## Overview
|
4
|
+
Chef should be the authoritative source of how your infrastructure looks like.
|
5
|
+
Ideally what's in Chef is also what's running on the servers. However
|
6
|
+
sometimes you have to go into a server, stop Chef and debug something by hand.
|
7
|
+
This has all sorts of implications - depending on the length of the work - the
|
8
|
+
server won't get updates, drifts out of sync with app configuration and worst
|
9
|
+
of all nobody might know. This is why knife-wip provides a tag based way to
|
10
|
+
track work in progress in your infrastructure. Knife-wip uses Chef [node
|
11
|
+
tags][tags] to track who is working on which servers. This way it supports all
|
12
|
+
the great ways of searching for tags and testing for them in Chef recipe and
|
13
|
+
doesn't rely on any external setup.
|
14
|
+
|
15
|
+
|
16
|
+
## Tag format
|
17
|
+
In order to provide an easy and standardized way to retrieve information and
|
18
|
+
provide everyone with the maximum amount of information the tag for WIP looks
|
19
|
+
like this:
|
20
|
+
|
21
|
+
```
|
22
|
+
wip:[USERNAME]:[provided description]
|
23
|
+
```
|
24
|
+
|
25
|
+
This should make it easy to immediately see who has added the WIP tag and what
|
26
|
+
they are working on.
|
27
|
+
|
28
|
+
|
29
|
+
## Usage
|
30
|
+
|
31
|
+
```
|
32
|
+
# mark a node as WIP
|
33
|
+
% knife node wip web01.example.com testing php build
|
34
|
+
Created WIP "wip:dschauenberg:testing php build" for node web01.example.com.
|
35
|
+
|
36
|
+
# show all nodes that are marked as WIP
|
37
|
+
% knife wip list
|
38
|
+
1 nodes found with work in progress
|
39
|
+
|
40
|
+
web01.example.com: dschauenberg:testing php build
|
41
|
+
|
42
|
+
# remove the WIP tag
|
43
|
+
% knife node unwip web01.example.com testing php build
|
44
|
+
Deleted WIP description "wip:dschauenberg:testing php build" for node web01.example.com.
|
45
|
+
```
|
46
|
+
|
47
|
+
## Plugins
|
48
|
+
knife-wip has a plugin system that makes it possible to perform custom actions
|
49
|
+
when work is started or stopped.
|
50
|
+
|
51
|
+
|
52
|
+
### Configuration format
|
53
|
+
knife-wip reads its configuration from different files in the following order:
|
54
|
+
|
55
|
+
- `$COOKBOOKPATH/config/knife-wip-config.yml`
|
56
|
+
- `config/knife-wip-config.yml`
|
57
|
+
- `/etc/knife-wip-config.yml`
|
58
|
+
- `~/.chef/knife-wip-config.yml`
|
59
|
+
|
60
|
+
And the file should be YAML and look something like this:
|
61
|
+
|
62
|
+
```
|
63
|
+
plugins:
|
64
|
+
irccat:
|
65
|
+
server: irccat.example.com
|
66
|
+
port: 12345
|
67
|
+
channels:
|
68
|
+
- "#chef"
|
69
|
+
```
|
70
|
+
|
71
|
+
The key of the plugin configuration is what knife-wip uses to try and load the
|
72
|
+
corresponding ruby file under `lib/knife-wip/plugins`. So it needs to exist
|
73
|
+
there first.
|
74
|
+
|
75
|
+
### Plugin format
|
76
|
+
A simple plugin just needs to inherit from `KnifeWip::Plugin` and implement
|
77
|
+
the two methods `wip_start` and `wip_stop`. Those methods get passed in the
|
78
|
+
user, tag and node for the command that was performed. When the plugin gets
|
79
|
+
instantiated it also gets its configuration from the config file passed in and
|
80
|
+
it's available as the `@config` instance variable in there.
|
81
|
+
|
82
|
+
```
|
83
|
+
module KnifeWip
|
84
|
+
module Plugins
|
85
|
+
class LolFormatter < KnifeWip::Plugin
|
86
|
+
|
87
|
+
def wip_start(user, tag, node)
|
88
|
+
end
|
89
|
+
|
90
|
+
def wip_stop(user, tag, node)
|
91
|
+
end
|
92
|
+
|
93
|
+
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
```
|
98
|
+
|
99
|
+
|
100
|
+
|
101
|
+
## Installation
|
102
|
+
|
103
|
+
```
|
104
|
+
gem install knife-wip
|
105
|
+
```
|
106
|
+
|
107
|
+
|
108
|
+
## Bugs
|
109
|
+
Probably. Please file a ticket [here][issues]
|
110
|
+
|
111
|
+
|
112
|
+
|
113
|
+
[tags]: https://docs.getchef.com/knife_tag.html
|
114
|
+
[issues]: https://github.com/mrtazz/knife-wip/issues
|
115
|
+
|
data/Rakefile
ADDED
data/knife-wip.gemspec
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
$:.push File.expand_path('../lib', __FILE__)
|
2
|
+
require 'knife-wip'
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.name = 'knife-wip'
|
6
|
+
gem.version = KnifeWip::VERSION
|
7
|
+
gem.authors = ["Daniel Schauenberg"]
|
8
|
+
gem.email = 'd@unwiredcouch.com'
|
9
|
+
gem.homepage = 'https://github.com/mrtazz/knife-wip'
|
10
|
+
gem.summary = "A workflow plugin to track WIP nodes on a chef server"
|
11
|
+
gem.description = "A workflow plugin to track WIP nodes on a chef server"
|
12
|
+
|
13
|
+
gem.files = `git ls-files`.split($\)
|
14
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
15
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
16
|
+
gem.require_paths = ["lib"]
|
17
|
+
|
18
|
+
gem.add_runtime_dependency 'chef', '>= 0.10.4'
|
19
|
+
gem.add_runtime_dependency 'app_conf', '~> 0.4', '>= 0.4.2'
|
20
|
+
end
|
@@ -0,0 +1,227 @@
|
|
1
|
+
require 'chef/knife'
|
2
|
+
require 'chef/knife/core/node_presenter'
|
3
|
+
require 'app_conf'
|
4
|
+
|
5
|
+
module KnifeWip
|
6
|
+
|
7
|
+
# Public: collection namespace for some helper functions and methods
|
8
|
+
module Helpers
|
9
|
+
|
10
|
+
# Public: load all plugins that are configured
|
11
|
+
#
|
12
|
+
# this method merely tries to require the plugin files, instantiates an
|
13
|
+
# object from them and then stores it in @plugins. That way the knife
|
14
|
+
# commands can just call all plugins however they want from there.
|
15
|
+
#
|
16
|
+
# Returns nothing
|
17
|
+
def load_plugins
|
18
|
+
|
19
|
+
@plugins ||= []
|
20
|
+
|
21
|
+
app_config[:plugins].to_hash.each do |plugin, plugin_config|
|
22
|
+
begin
|
23
|
+
require "knife-wip/plugins/#{plugin.downcase}"
|
24
|
+
# apparently this is the way to dynamically instantiate ruby objects
|
25
|
+
@plugins << KnifeWip::Plugins.const_get(plugin.capitalize).new(plugin_config)
|
26
|
+
rescue LoadError
|
27
|
+
ui.warn "Configured plugin '#{plugin}' doesn't exist."
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
|
34
|
+
# Public: get the configuration loaded from the yaml files
|
35
|
+
#
|
36
|
+
# Returns the appconf object with config loaded
|
37
|
+
def app_config
|
38
|
+
return @app_config unless @app_config.nil?
|
39
|
+
|
40
|
+
@app_config = ::AppConf.new
|
41
|
+
load_paths = []
|
42
|
+
# first look for configuration in the cookbooks folder
|
43
|
+
load_paths << File.expand_path("#{cookbook_path.gsub('cookbooks','')}/config/knife-wip-config.yml")
|
44
|
+
# or see if we are in the cookbooks repo
|
45
|
+
load_paths << File.expand_path('config/knife-wip-config.yml')
|
46
|
+
# global config in /etc has higher priority
|
47
|
+
load_paths << '/etc/knife-wip-config.yml'
|
48
|
+
# finally the user supplied config file is loaded
|
49
|
+
load_paths << File.expand_path('~/.chef/knife-wip-config.yml')
|
50
|
+
|
51
|
+
# load all the paths
|
52
|
+
load_paths.each do |load_path|
|
53
|
+
if File.exists?(load_path)
|
54
|
+
@app_config.load(load_path)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
@app_config
|
59
|
+
end
|
60
|
+
|
61
|
+
def ensure_cookbook_path!
|
62
|
+
if config[:cookbook_path].nil?
|
63
|
+
ui.fatal "No default cookbook_path; Specify with -o or fix your knife.rb."
|
64
|
+
show_usage
|
65
|
+
exit(1)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def cookbook_path
|
70
|
+
ensure_cookbook_path!
|
71
|
+
[config[:cookbook_path] ||= ::Chef::Config.cookbook_path].flatten[0]
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
75
|
+
|
76
|
+
# Public: class to inherit from for plugins. Basically a cheap hack to get
|
77
|
+
# some control over whether or not a plugin implements the correct methods
|
78
|
+
class Plugin
|
79
|
+
include ::KnifeWip::Helpers
|
80
|
+
|
81
|
+
def initialize(config)
|
82
|
+
@config = config
|
83
|
+
end
|
84
|
+
|
85
|
+
def wip_start(user, tag, node)
|
86
|
+
ui.warn "Warning: #{self.class} doesn't implement the 'wip_start' method."
|
87
|
+
ui.warn "No announcements performed for #{self.class}"
|
88
|
+
end
|
89
|
+
|
90
|
+
def wip_stop(user, tag, node)
|
91
|
+
ui.warn "Warning: #{self.class} doesn't implement the 'wip_stop' method."
|
92
|
+
ui.warn "No announcements performed for #{self.class}"
|
93
|
+
end
|
94
|
+
|
95
|
+
end
|
96
|
+
|
97
|
+
|
98
|
+
class NodeWip < Chef::Knife
|
99
|
+
include KnifeWip::Helpers
|
100
|
+
|
101
|
+
deps do
|
102
|
+
require 'chef/node'
|
103
|
+
end
|
104
|
+
|
105
|
+
banner "knife node wip NODE DESCRIPTION"
|
106
|
+
|
107
|
+
def run
|
108
|
+
name = @name_args[0]
|
109
|
+
description = @name_args[1..-1]
|
110
|
+
|
111
|
+
if name.nil? || description.nil? || description.empty?
|
112
|
+
show_usage
|
113
|
+
ui.fatal("You must specify a node name and a description of what you are working on.")
|
114
|
+
exit 1
|
115
|
+
end
|
116
|
+
|
117
|
+
self.config = Chef::Config.merge!(config)
|
118
|
+
|
119
|
+
load_plugins
|
120
|
+
|
121
|
+
wip_tag = "wip:#{ENV["USER"]}:#{description.join(" ")}"
|
122
|
+
|
123
|
+
node = Chef::Node.load name
|
124
|
+
(node.tags << wip_tag).uniq!
|
125
|
+
node.save
|
126
|
+
ui.info("Created WIP \"#{wip_tag}\" for node #{name}.")
|
127
|
+
@plugins.each do |plugin|
|
128
|
+
plugin.wip_start(ENV["USER"], description.join(" "), name)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
end
|
133
|
+
|
134
|
+
class NodeUnwip < Chef::Knife
|
135
|
+
include KnifeWip::Helpers
|
136
|
+
|
137
|
+
deps do
|
138
|
+
require 'chef/node'
|
139
|
+
end
|
140
|
+
|
141
|
+
banner "knife node unwip NODE DESCRIPTION"
|
142
|
+
|
143
|
+
def run
|
144
|
+
name = @name_args[0]
|
145
|
+
description = @name_args[1..-1].join(" ")
|
146
|
+
|
147
|
+
if name.nil? || description.nil? || description.empty?
|
148
|
+
show_usage
|
149
|
+
ui.fatal("You must specify a node name and a WIP description.")
|
150
|
+
exit 1
|
151
|
+
end
|
152
|
+
|
153
|
+
self.config = Chef::Config.merge!(config)
|
154
|
+
|
155
|
+
load_plugins
|
156
|
+
|
157
|
+
node = Chef::Node.load name
|
158
|
+
tag = "wip:#{ENV["USER"]}:#{description}"
|
159
|
+
success = node.tags.delete(tag).nil? ? false : true
|
160
|
+
|
161
|
+
node.save
|
162
|
+
if success == false
|
163
|
+
message = "Nothing has changed. The WIP description requested to be deleted does not exist."
|
164
|
+
else
|
165
|
+
message = "Deleted WIP description \"#{tag}\" for node #{name}."
|
166
|
+
@plugins.each do |plugin|
|
167
|
+
plugin.wip_stop(ENV["USER"], description, name)
|
168
|
+
end
|
169
|
+
end
|
170
|
+
ui.info(message)
|
171
|
+
end
|
172
|
+
|
173
|
+
end
|
174
|
+
|
175
|
+
class WipList < Chef::Knife
|
176
|
+
|
177
|
+
deps do
|
178
|
+
require 'chef/node'
|
179
|
+
require 'chef/environment'
|
180
|
+
require 'chef/api_client'
|
181
|
+
require 'chef/search/query'
|
182
|
+
end
|
183
|
+
|
184
|
+
include Chef::Knife::Core::NodeFormattingOptions
|
185
|
+
|
186
|
+
banner "knife wip list"
|
187
|
+
|
188
|
+
def run
|
189
|
+
q = Chef::Search::Query.new
|
190
|
+
|
191
|
+
query = URI.escape("tags:wip*", Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))
|
192
|
+
|
193
|
+
result_items = []
|
194
|
+
result_count = 0
|
195
|
+
|
196
|
+
begin
|
197
|
+
q.search('node', query) do |item|
|
198
|
+
formatted_item = format_for_display(item)
|
199
|
+
result_items << formatted_item
|
200
|
+
result_count += 1
|
201
|
+
end
|
202
|
+
rescue Net::HTTPServerException => e
|
203
|
+
msg = Chef::JSONCompat.from_json(e.response.body)["error"].first
|
204
|
+
ui.error("knife wip list failed: #{msg}")
|
205
|
+
exit 1
|
206
|
+
end
|
207
|
+
|
208
|
+
if ui.interchange?
|
209
|
+
output({:results => result_count, :rows => result_items})
|
210
|
+
else
|
211
|
+
ui.msg "#{result_count} nodes found with work in progress"
|
212
|
+
ui.msg("\n")
|
213
|
+
result_items.each do |item|
|
214
|
+
normalized_tags = []
|
215
|
+
item.tags.each do |tag|
|
216
|
+
next unless tag.start_with? "wip:"
|
217
|
+
tag = tag.split(":")
|
218
|
+
normalized_tags << tag[1, tag.length].join(":")
|
219
|
+
end
|
220
|
+
output("#{item.name}: #{normalized_tags.join(", ")}")
|
221
|
+
end
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
end
|
226
|
+
|
227
|
+
end
|
data/lib/knife-wip.rb
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'chef/knife'
|
2
|
+
|
3
|
+
module KnifeWip
|
4
|
+
module Plugins
|
5
|
+
class Eventinator < KnifeWip::Plugin
|
6
|
+
|
7
|
+
def wip_start(user, tag, node)
|
8
|
+
event_data = {
|
9
|
+
:tag => 'knife',
|
10
|
+
:username => user,
|
11
|
+
:status => "#{user} started work '#{tag}' on #{node}",
|
12
|
+
:metadata => {
|
13
|
+
:node_name => node,
|
14
|
+
:tag => tag
|
15
|
+
}.to_json
|
16
|
+
}
|
17
|
+
eventinate(event_data)
|
18
|
+
end
|
19
|
+
|
20
|
+
def wip_stop(user, tag, node)
|
21
|
+
event_data = {
|
22
|
+
:tag => 'knife',
|
23
|
+
:username => user,
|
24
|
+
:status => "#{user} stopped work '#{tag}' on #{node}",
|
25
|
+
:metadata => {
|
26
|
+
:node_name => node,
|
27
|
+
:tag => tag
|
28
|
+
}.to_json
|
29
|
+
}
|
30
|
+
eventinate(event_data)
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
def eventinate(event_data)
|
35
|
+
begin
|
36
|
+
uri = URI.parse(@config["url"])
|
37
|
+
rescue Exception => e
|
38
|
+
ui.error 'Could not parse URI for Eventinator.'
|
39
|
+
ui.error e.to_s
|
40
|
+
return
|
41
|
+
end
|
42
|
+
|
43
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
44
|
+
http.read_timeout = @config["read_timeout"] || 5
|
45
|
+
|
46
|
+
request = Net::HTTP::Post.new(uri.request_uri)
|
47
|
+
request.set_form_data(event_data)
|
48
|
+
|
49
|
+
begin
|
50
|
+
response = http.request(request)
|
51
|
+
ui.error "Eventinator at #{@config["url"]} did not receive a good response from the server" if response.code != '200'
|
52
|
+
rescue Timeout::Error
|
53
|
+
ui.error "Eventinator timed out connecting to #{@config["url"]}. Is that URL accessible?"
|
54
|
+
rescue Exception => e
|
55
|
+
ui.error 'Eventinator error.'
|
56
|
+
ui.error e.to_s
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module KnifeWip
|
2
|
+
module Plugins
|
3
|
+
class Irccat < KnifeWip::Plugin
|
4
|
+
|
5
|
+
TEMPLATES = {
|
6
|
+
:start => '#BOLD#PURPLECHEF:#NORMAL %{user} started work #TEAL%{tag}#NORMAL on #RED%{node}#NORMAL',
|
7
|
+
:stop => '#BOLD#PURPLECHEF:#NORMAL %{user} stopped work #TEAL%{tag}#NORMAL on #RED%{node}#NORMAL'
|
8
|
+
}
|
9
|
+
|
10
|
+
def wip_start(user, tag, node)
|
11
|
+
send_to_irccat(TEMPLATES[:start] % {
|
12
|
+
:user => user,
|
13
|
+
:tag => tag,
|
14
|
+
:node => node,
|
15
|
+
})
|
16
|
+
end
|
17
|
+
|
18
|
+
def wip_stop(user, tag, node)
|
19
|
+
send_to_irccat(TEMPLATES[:stop] % {
|
20
|
+
:user => user,
|
21
|
+
:tag => tag,
|
22
|
+
:node => node,
|
23
|
+
})
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
def send_to_irccat(message)
|
28
|
+
@config["channels"].each do |channel|
|
29
|
+
begin
|
30
|
+
# Write the message using a TCP Socket
|
31
|
+
socket = TCPSocket.open(@config["server"], @config["port"])
|
32
|
+
socket.write("#{channel} #{message}")
|
33
|
+
rescue Exception => e
|
34
|
+
ui.error 'Failed to post message with irccat.'
|
35
|
+
ui.error e.to_s
|
36
|
+
ensure
|
37
|
+
socket.close unless socket.nil?
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
metadata
ADDED
@@ -0,0 +1,91 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: knife-wip
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Daniel Schauenberg
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2014-10-03 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: chef
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 0.10.4
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: 0.10.4
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: app_conf
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ~>
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0.4'
|
38
|
+
- - ! '>='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 0.4.2
|
41
|
+
type: :runtime
|
42
|
+
prerelease: false
|
43
|
+
version_requirements: !ruby/object:Gem::Requirement
|
44
|
+
none: false
|
45
|
+
requirements:
|
46
|
+
- - ~>
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
version: '0.4'
|
49
|
+
- - ! '>='
|
50
|
+
- !ruby/object:Gem::Version
|
51
|
+
version: 0.4.2
|
52
|
+
description: A workflow plugin to track WIP nodes on a chef server
|
53
|
+
email: d@unwiredcouch.com
|
54
|
+
executables: []
|
55
|
+
extensions: []
|
56
|
+
extra_rdoc_files: []
|
57
|
+
files:
|
58
|
+
- Gemfile
|
59
|
+
- LICENSE
|
60
|
+
- README.md
|
61
|
+
- Rakefile
|
62
|
+
- knife-wip.gemspec
|
63
|
+
- lib/chef/knife/knife-wip.rb
|
64
|
+
- lib/knife-wip.rb
|
65
|
+
- lib/knife-wip/plugins/eventinator.rb
|
66
|
+
- lib/knife-wip/plugins/irccat.rb
|
67
|
+
homepage: https://github.com/mrtazz/knife-wip
|
68
|
+
licenses: []
|
69
|
+
post_install_message:
|
70
|
+
rdoc_options: []
|
71
|
+
require_paths:
|
72
|
+
- lib
|
73
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
74
|
+
none: false
|
75
|
+
requirements:
|
76
|
+
- - ! '>='
|
77
|
+
- !ruby/object:Gem::Version
|
78
|
+
version: '0'
|
79
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
80
|
+
none: false
|
81
|
+
requirements:
|
82
|
+
- - ! '>='
|
83
|
+
- !ruby/object:Gem::Version
|
84
|
+
version: '0'
|
85
|
+
requirements: []
|
86
|
+
rubyforge_project:
|
87
|
+
rubygems_version: 1.8.23.2
|
88
|
+
signing_key:
|
89
|
+
specification_version: 3
|
90
|
+
summary: A workflow plugin to track WIP nodes on a chef server
|
91
|
+
test_files: []
|