knife-wip 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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: []
|