pantry 0.0.0 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +9 -0
- data/.ruby-version +1 -0
- data/.travis.yml +19 -0
- data/Gemfile +15 -0
- data/Guardfile +16 -0
- data/LICENSE +20 -0
- data/README.md +53 -0
- data/Rakefile +18 -0
- data/Vagrantfile +86 -0
- data/bin/pantry +11 -0
- data/bin/pantry-client +38 -0
- data/bin/pantry-server +33 -0
- data/dist/client.yml +79 -0
- data/dist/server.yml +56 -0
- data/dist/upstart/pantry-client.conf +12 -0
- data/dist/upstart/pantry-server.conf +12 -0
- data/doc/message_packet.dot +19 -0
- data/doc/message_packet.dot.png +0 -0
- data/doc/network_topology.dot +42 -0
- data/doc/network_topology.dot.png +0 -0
- data/lib/celluloid_zmq_patches.rb +16 -0
- data/lib/opt_parse_plus.rb +184 -0
- data/lib/pantry.rb +197 -0
- data/lib/pantry/cli.rb +154 -0
- data/lib/pantry/client.rb +131 -0
- data/lib/pantry/client_info.rb +34 -0
- data/lib/pantry/client_registry.rb +104 -0
- data/lib/pantry/command.rb +194 -0
- data/lib/pantry/command_handler.rb +53 -0
- data/lib/pantry/command_line.rb +115 -0
- data/lib/pantry/commands/create_client.rb +30 -0
- data/lib/pantry/commands/download_directory.rb +35 -0
- data/lib/pantry/commands/echo.rb +32 -0
- data/lib/pantry/commands/edit_application.rb +60 -0
- data/lib/pantry/commands/register_client.rb +38 -0
- data/lib/pantry/commands/status.rb +78 -0
- data/lib/pantry/commands/sync_directory.rb +50 -0
- data/lib/pantry/commands/update_application.rb +45 -0
- data/lib/pantry/commands/upload_file.rb +68 -0
- data/lib/pantry/communication.rb +20 -0
- data/lib/pantry/communication/client.rb +75 -0
- data/lib/pantry/communication/client_filter.rb +117 -0
- data/lib/pantry/communication/file_service.rb +125 -0
- data/lib/pantry/communication/file_service/file_progress.rb +164 -0
- data/lib/pantry/communication/file_service/receive_file.rb +97 -0
- data/lib/pantry/communication/file_service/send_file.rb +74 -0
- data/lib/pantry/communication/publish_socket.rb +20 -0
- data/lib/pantry/communication/reading_socket.rb +89 -0
- data/lib/pantry/communication/receive_socket.rb +23 -0
- data/lib/pantry/communication/security.rb +44 -0
- data/lib/pantry/communication/security/authentication.rb +98 -0
- data/lib/pantry/communication/security/curve_key_store.rb +120 -0
- data/lib/pantry/communication/security/curve_security.rb +70 -0
- data/lib/pantry/communication/security/null_security.rb +32 -0
- data/lib/pantry/communication/send_socket.rb +19 -0
- data/lib/pantry/communication/serialize_message.rb +84 -0
- data/lib/pantry/communication/server.rb +97 -0
- data/lib/pantry/communication/subscribe_socket.rb +33 -0
- data/lib/pantry/communication/wait_list.rb +45 -0
- data/lib/pantry/communication/writing_socket.rb +46 -0
- data/lib/pantry/config.rb +182 -0
- data/lib/pantry/file_editor.rb +67 -0
- data/lib/pantry/logger.rb +78 -0
- data/lib/pantry/message.rb +134 -0
- data/lib/pantry/multi_command.rb +36 -0
- data/lib/pantry/server.rb +132 -0
- data/lib/pantry/test/acceptance.rb +83 -0
- data/lib/pantry/test/support/fake_fs.rb +31 -0
- data/lib/pantry/test/support/matchers.rb +13 -0
- data/lib/pantry/test/support/minitest.rb +13 -0
- data/lib/pantry/test/support/mock_ui.rb +23 -0
- data/lib/pantry/test/unit.rb +13 -0
- data/lib/pantry/ui.rb +68 -0
- data/lib/pantry/version.rb +3 -0
- data/pantry.gemspec +40 -0
- data/test/acceptance/cli/error_handling_test.rb +7 -0
- data/test/acceptance/cli/execute_command_on_clients_test.rb +32 -0
- data/test/acceptance/cli/request_info_from_server_test.rb +44 -0
- data/test/acceptance/communication/client_requests_info_from_server_test.rb +28 -0
- data/test/acceptance/communication/heartbeat_test.rb +19 -0
- data/test/acceptance/communication/pub_sub_communication_test.rb +53 -0
- data/test/acceptance/communication/security_test.rb +117 -0
- data/test/acceptance/communication/server_requests_info_from_client_test.rb +41 -0
- data/test/acceptance/test_helper.rb +25 -0
- data/test/fixtures/config.yml +22 -0
- data/test/fixtures/empty.yml +2 -0
- data/test/fixtures/file_to_upload +3 -0
- data/test/root_dir/.gitkeep +0 -0
- data/test/unit/cli_test.rb +173 -0
- data/test/unit/client_registry_test.rb +61 -0
- data/test/unit/client_test.rb +128 -0
- data/test/unit/command_handler_test.rb +79 -0
- data/test/unit/command_line_test.rb +5 -0
- data/test/unit/command_test.rb +206 -0
- data/test/unit/commands/create_client_test.rb +25 -0
- data/test/unit/commands/download_directory_test.rb +58 -0
- data/test/unit/commands/echo_test.rb +22 -0
- data/test/unit/commands/edit_application_test.rb +84 -0
- data/test/unit/commands/register_client_test.rb +41 -0
- data/test/unit/commands/status_test.rb +81 -0
- data/test/unit/commands/sync_directory_test.rb +75 -0
- data/test/unit/commands/update_application_test.rb +35 -0
- data/test/unit/commands/upload_file_test.rb +51 -0
- data/test/unit/communication/client_filter_test.rb +262 -0
- data/test/unit/communication/client_test.rb +99 -0
- data/test/unit/communication/file_service/receive_file_test.rb +214 -0
- data/test/unit/communication/file_service/send_file_test.rb +110 -0
- data/test/unit/communication/file_service_test.rb +56 -0
- data/test/unit/communication/publish_socket_test.rb +19 -0
- data/test/unit/communication/reading_socket_test.rb +110 -0
- data/test/unit/communication/receive_socket_test.rb +20 -0
- data/test/unit/communication/security/authentication_test.rb +97 -0
- data/test/unit/communication/security/curve_key_store_test.rb +110 -0
- data/test/unit/communication/security/curve_security_test.rb +44 -0
- data/test/unit/communication/security/null_security_test.rb +15 -0
- data/test/unit/communication/security_test.rb +49 -0
- data/test/unit/communication/send_socket_test.rb +19 -0
- data/test/unit/communication/serialize_message_test.rb +128 -0
- data/test/unit/communication/server_test.rb +106 -0
- data/test/unit/communication/subscribe_socket_test.rb +46 -0
- data/test/unit/communication/wait_list_test.rb +60 -0
- data/test/unit/communication/writing_socket_test.rb +46 -0
- data/test/unit/config_test.rb +150 -0
- data/test/unit/logger_test.rb +79 -0
- data/test/unit/message_test.rb +179 -0
- data/test/unit/multi_command_test.rb +45 -0
- data/test/unit/opt_parse_plus_test.rb +218 -0
- data/test/unit/pantry_test.rb +82 -0
- data/test/unit/server_test.rb +166 -0
- data/test/unit/test_helper.rb +25 -0
- data/test/unit/ui_test.rb +58 -0
- metadata +389 -13
@@ -0,0 +1,97 @@
|
|
1
|
+
module Pantry
|
2
|
+
module Communication
|
3
|
+
|
4
|
+
# The communication layer of a Pantry::Server
|
5
|
+
# This class manages all of the ZeroMQ sockets and underlying
|
6
|
+
# communication systems, handling the sending and receiving of messages.
|
7
|
+
class Server
|
8
|
+
include Celluloid
|
9
|
+
|
10
|
+
#
|
11
|
+
# +listener+ must respond to the #receive_message method
|
12
|
+
def initialize(listener)
|
13
|
+
@listener = listener
|
14
|
+
@response_wait_list = Communication::WaitList.new
|
15
|
+
end
|
16
|
+
|
17
|
+
# Start up the networking layer, opening up sockets and getting
|
18
|
+
# ready for client communication.
|
19
|
+
def run
|
20
|
+
@security = Communication::Security.new_server
|
21
|
+
@security.link_to(self)
|
22
|
+
|
23
|
+
@publish_socket = Communication::PublishSocket.new_link(
|
24
|
+
Pantry.config.server_host,
|
25
|
+
Pantry.config.pub_sub_port,
|
26
|
+
@security
|
27
|
+
)
|
28
|
+
@publish_socket.open
|
29
|
+
|
30
|
+
@receive_socket = Communication::ReceiveSocket.new_link(
|
31
|
+
Pantry.config.server_host,
|
32
|
+
Pantry.config.receive_port,
|
33
|
+
@security
|
34
|
+
)
|
35
|
+
@receive_socket.add_listener(self)
|
36
|
+
@receive_socket.open
|
37
|
+
|
38
|
+
@file_service = Communication::FileService.new_link(
|
39
|
+
Pantry.config.server_host,
|
40
|
+
Pantry.config.file_service_port,
|
41
|
+
@security
|
42
|
+
)
|
43
|
+
@file_service.start_server
|
44
|
+
end
|
45
|
+
|
46
|
+
# Ask Security to generate a new set of credentials as necessary
|
47
|
+
# for a new Client to connect to this Server
|
48
|
+
def create_client
|
49
|
+
@security.create_client
|
50
|
+
end
|
51
|
+
|
52
|
+
# Send a request to all clients, expecting a result. Returns a Future
|
53
|
+
# which can be queried later for the client response.
|
54
|
+
def send_request(message)
|
55
|
+
@response_wait_list.wait_for(message).tap do
|
56
|
+
publish_message(message)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# Send a message to all connected subscribers without modifying the package.
|
61
|
+
# Used when handling requests meant for other clients (say from the CLI). The source
|
62
|
+
# is untouched so the Client(s) handling know how to respond.
|
63
|
+
def forward_message(message)
|
64
|
+
message.forwarded!
|
65
|
+
publish_message(message)
|
66
|
+
end
|
67
|
+
|
68
|
+
# Send a message to all clients who match the given filter.
|
69
|
+
def publish_message(message)
|
70
|
+
message.from ||= @listener
|
71
|
+
@publish_socket.send_message(message)
|
72
|
+
end
|
73
|
+
|
74
|
+
# Listener callback from ReceiveSocket. See if we need to match this response
|
75
|
+
# with a previous request or if it's a new message entirely.
|
76
|
+
def handle_message(message)
|
77
|
+
if message.forwarded?
|
78
|
+
forward_message(message)
|
79
|
+
elsif @response_wait_list.waiting_for?(message)
|
80
|
+
@response_wait_list.received(message)
|
81
|
+
else
|
82
|
+
@listener.receive_message(message)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def receive_file(file_size, file_checksum)
|
87
|
+
@file_service.receive_file(file_size, file_checksum)
|
88
|
+
end
|
89
|
+
|
90
|
+
def send_file(file_path, receiver_uuid, file_uuid)
|
91
|
+
@file_service.send_file(file_path, receiver_uuid, file_uuid)
|
92
|
+
end
|
93
|
+
|
94
|
+
end
|
95
|
+
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Pantry
|
2
|
+
module Communication
|
3
|
+
|
4
|
+
# The SubscribeSocket manages the Subscription side of the Pub/Sub channel,
|
5
|
+
# using a 0MQ PUB socket. This socket can subscribe to any number of streams
|
6
|
+
# depending on the filtering given. Messages received by this socket are passed
|
7
|
+
# to the configured listener as Messages.
|
8
|
+
class SubscribeSocket < ReadingSocket
|
9
|
+
|
10
|
+
def initialize(host, port, security)
|
11
|
+
super
|
12
|
+
@filter = ClientFilter.new
|
13
|
+
end
|
14
|
+
|
15
|
+
def filter_on(client_filter)
|
16
|
+
@filter = client_filter
|
17
|
+
end
|
18
|
+
|
19
|
+
def build_socket
|
20
|
+
Celluloid::ZMQ::SubSocket.new
|
21
|
+
end
|
22
|
+
|
23
|
+
def open_socket(socket)
|
24
|
+
socket.connect("tcp://#{host}:#{port}")
|
25
|
+
|
26
|
+
@filter.streams.each do |stream|
|
27
|
+
socket.subscribe(stream)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Pantry
|
2
|
+
module Communication
|
3
|
+
|
4
|
+
# The WaitList manages futures for asynchronously waiting for responses
|
5
|
+
# from either the Client or the Server. Given an identity and a message,
|
6
|
+
# WaitList returns a Future that will be filled when the handler in question
|
7
|
+
# receives a message of the same Message type from that identity.
|
8
|
+
class WaitList
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
@futures = Hash.new {|hash, key| hash[key] = []}
|
12
|
+
end
|
13
|
+
|
14
|
+
# Given a +message+ being sent, create a Future for a response to this message.
|
15
|
+
# This keys off of the Message's UUID, which must be kept in tact as it
|
16
|
+
# passes through the system.
|
17
|
+
def wait_for(message)
|
18
|
+
future = Celluloid::Future.new
|
19
|
+
@futures[ message.uuid ] << future
|
20
|
+
future
|
21
|
+
end
|
22
|
+
|
23
|
+
# Is there a future waiting for this response message?
|
24
|
+
def waiting_for?(message)
|
25
|
+
!@futures[ message.uuid ].empty?
|
26
|
+
end
|
27
|
+
|
28
|
+
# Internal to Celluloid::Future, using #signal ends up in a Result object
|
29
|
+
# in which calling #value then calls #value on the saved data which in our
|
30
|
+
# case is Message. We just want the Message so wrap up our messages in this
|
31
|
+
# object to work around this oddity.
|
32
|
+
#
|
33
|
+
# https://github.com/celluloid/celluloid/blob/master/lib/celluloid/future.rb#L89
|
34
|
+
FutureResultWrapper = Struct.new(:value)
|
35
|
+
|
36
|
+
def received(message)
|
37
|
+
if future = @futures[ message.uuid ].shift
|
38
|
+
future.signal(FutureResultWrapper.new(message))
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module Pantry
|
2
|
+
module Communication
|
3
|
+
|
4
|
+
# Base class of all sockets that write messages through ZMQ.
|
5
|
+
# Not meant for direct use, please use one of the subclasses for specific
|
6
|
+
# functionality.
|
7
|
+
class WritingSocket
|
8
|
+
include Celluloid::ZMQ
|
9
|
+
|
10
|
+
attr_reader :host, :port
|
11
|
+
|
12
|
+
def initialize(host, port, security)
|
13
|
+
@host = host
|
14
|
+
@port = port
|
15
|
+
@security = security
|
16
|
+
end
|
17
|
+
|
18
|
+
def open
|
19
|
+
@socket = build_socket
|
20
|
+
Communication.configure_socket(@socket)
|
21
|
+
@security.configure_socket(@socket)
|
22
|
+
|
23
|
+
open_socket(@socket)
|
24
|
+
end
|
25
|
+
|
26
|
+
def build_socket
|
27
|
+
raise "Implement the socket setup."
|
28
|
+
end
|
29
|
+
|
30
|
+
def open_socket(socket)
|
31
|
+
raise "Connect / Bind the socket built in #build_socket"
|
32
|
+
end
|
33
|
+
|
34
|
+
def close
|
35
|
+
@socket.close if @socket
|
36
|
+
end
|
37
|
+
|
38
|
+
def send_message(message)
|
39
|
+
@socket.write(
|
40
|
+
SerializeMessage.to_zeromq(message)
|
41
|
+
)
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,182 @@
|
|
1
|
+
module Pantry
|
2
|
+
|
3
|
+
# Retrieve the current configuration set
|
4
|
+
def self.config
|
5
|
+
@@config ||= Config.new
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.reset_config!
|
9
|
+
@@config = nil
|
10
|
+
end
|
11
|
+
|
12
|
+
# Global configuration values for running all of Pantry.
|
13
|
+
class Config
|
14
|
+
##
|
15
|
+
# Global Configuration
|
16
|
+
##
|
17
|
+
|
18
|
+
# Where does Pantry log to?
|
19
|
+
# Can be "stdout", "syslog", or a file system path
|
20
|
+
# Defaults to STDOUT
|
21
|
+
# When using syslog, program name will be "pantry"
|
22
|
+
attr_accessor :log_to
|
23
|
+
|
24
|
+
# After what level are logs dropped and ignored?
|
25
|
+
# Can be any of: "fatal", "error", "warn", "info", "debug"
|
26
|
+
# Each level will include the logs of all levels above it.
|
27
|
+
# Defaults to "info"
|
28
|
+
attr_accessor :log_level
|
29
|
+
|
30
|
+
# If logging to Syslog, set the program-name Pantry will
|
31
|
+
# use when sending logs to syslog.
|
32
|
+
# Defaults to "pantry"
|
33
|
+
attr_accessor :syslog_program_name
|
34
|
+
|
35
|
+
# Location on the file system Pantry will store any persistent data
|
36
|
+
# Default: /var/lib/pantry
|
37
|
+
attr_accessor :root_dir
|
38
|
+
|
39
|
+
##
|
40
|
+
# Communication Configuration
|
41
|
+
##
|
42
|
+
|
43
|
+
# Host name of the Pantry Server
|
44
|
+
attr_accessor :server_host
|
45
|
+
|
46
|
+
# Port used for Pub/Sub communication
|
47
|
+
attr_accessor :pub_sub_port
|
48
|
+
|
49
|
+
# Port clients use to send information to the Server
|
50
|
+
attr_accessor :receive_port
|
51
|
+
|
52
|
+
# Port through which files are sent and received
|
53
|
+
attr_accessor :file_service_port
|
54
|
+
|
55
|
+
# How often, in seconds, the client pings the Server
|
56
|
+
attr_accessor :client_heartbeat_interval
|
57
|
+
|
58
|
+
# What type of security will Pantry be employing?
|
59
|
+
# Available types are nil (no security) and "curve" (ZMQ4 Curve security)
|
60
|
+
#
|
61
|
+
# Defaults to nil because curve security has not yet been fully
|
62
|
+
# vetted by the crypto-community
|
63
|
+
attr_accessor :security
|
64
|
+
|
65
|
+
##
|
66
|
+
# Client Identification
|
67
|
+
##
|
68
|
+
|
69
|
+
# Unique identification of this Client
|
70
|
+
attr_accessor :client_identity
|
71
|
+
|
72
|
+
# Application this Client serves
|
73
|
+
attr_accessor :client_application
|
74
|
+
|
75
|
+
# Environment of the Application this Client runs
|
76
|
+
attr_accessor :client_environment
|
77
|
+
|
78
|
+
# Roles this Client serves
|
79
|
+
attr_accessor :client_roles
|
80
|
+
|
81
|
+
##
|
82
|
+
# Testing configuration helpers
|
83
|
+
##
|
84
|
+
|
85
|
+
# Time in seconds the CLI will wait for a response from the server
|
86
|
+
# By default this is nil, meaning unlimited timeout. Used mainly in tests.
|
87
|
+
attr_accessor :response_timeout
|
88
|
+
|
89
|
+
def initialize
|
90
|
+
|
91
|
+
# Logging defaults
|
92
|
+
@log_level = "info"
|
93
|
+
@syslog_program_name = "pantry"
|
94
|
+
|
95
|
+
# Default connectivity settings
|
96
|
+
@server_host = "127.0.0.1"
|
97
|
+
@pub_sub_port = 23001
|
98
|
+
@receive_port = 23002
|
99
|
+
@file_service_port = 23003
|
100
|
+
|
101
|
+
# Default client heartbeat to every 5 minutes
|
102
|
+
@client_heartbeat_interval = 300
|
103
|
+
|
104
|
+
# Default Client identificiation values
|
105
|
+
@client_identity = nil
|
106
|
+
@client_application = nil
|
107
|
+
@client_environment = nil
|
108
|
+
@client_roles = []
|
109
|
+
|
110
|
+
end
|
111
|
+
|
112
|
+
# Given a YAML config file, read in config values
|
113
|
+
def load_file(config_file)
|
114
|
+
configs = SafeYAML.load_file(config_file)
|
115
|
+
load_global_configs(configs)
|
116
|
+
load_networking_configs(configs["networking"])
|
117
|
+
load_client_configs(configs["client"])
|
118
|
+
refresh
|
119
|
+
end
|
120
|
+
|
121
|
+
def refresh
|
122
|
+
apply_configuration
|
123
|
+
end
|
124
|
+
|
125
|
+
protected
|
126
|
+
|
127
|
+
def load_global_configs(configs)
|
128
|
+
@log_to = configs["log_to"]
|
129
|
+
|
130
|
+
if configs["log_level"]
|
131
|
+
@log_level = configs["log_level"]
|
132
|
+
end
|
133
|
+
|
134
|
+
if configs["syslog_program_name"]
|
135
|
+
@syslog_program_name = configs["syslog_program_name"]
|
136
|
+
end
|
137
|
+
|
138
|
+
@root_dir = configs["root_dir"]
|
139
|
+
end
|
140
|
+
|
141
|
+
def load_networking_configs(configs)
|
142
|
+
return unless configs
|
143
|
+
|
144
|
+
if configs["server_host"]
|
145
|
+
@server_host = configs["server_host"]
|
146
|
+
end
|
147
|
+
|
148
|
+
if configs["pub_sub_port"]
|
149
|
+
@pub_sub_port = configs["pub_sub_port"]
|
150
|
+
end
|
151
|
+
|
152
|
+
if configs["receive_port"]
|
153
|
+
@receive_port = configs["receive_port"]
|
154
|
+
end
|
155
|
+
|
156
|
+
if configs["file_service_port"]
|
157
|
+
@file_service_port = configs["file_service_port"]
|
158
|
+
end
|
159
|
+
|
160
|
+
@security = configs["security"]
|
161
|
+
end
|
162
|
+
|
163
|
+
def load_client_configs(configs)
|
164
|
+
return unless configs
|
165
|
+
|
166
|
+
@client_identity = configs["identity"]
|
167
|
+
@client_application = configs["application"]
|
168
|
+
@client_environment = configs["environment"]
|
169
|
+
@client_roles = configs["roles"]
|
170
|
+
|
171
|
+
if configs["heartbeat_interval"]
|
172
|
+
@client_heartbeat_interval = configs["heartbeat_interval"]
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
def apply_configuration
|
177
|
+
# Reset our logger knowledge so the next call picks up the
|
178
|
+
# new configs
|
179
|
+
Pantry.logger = nil
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
module Pantry
|
2
|
+
|
3
|
+
# Use EDITOR to edit the contents of a remote file locally
|
4
|
+
# The editor can validate the updated content to be YAML (more to be added as needed)
|
5
|
+
# and will show errors and re-edit the file if validation fails.
|
6
|
+
#
|
7
|
+
# If the user chooses to cancel editing, #edit will return the original
|
8
|
+
# content given to it.
|
9
|
+
#
|
10
|
+
# Usage is simple:
|
11
|
+
#
|
12
|
+
# editor = FileEditor.new
|
13
|
+
# updated_content = editor.edit(file_contents, file_type)
|
14
|
+
#
|
15
|
+
class FileEditor
|
16
|
+
|
17
|
+
def initialize
|
18
|
+
@editor = ENV['EDITOR']
|
19
|
+
raise "Please set EDITOR environment variable to a text editor." unless @editor
|
20
|
+
end
|
21
|
+
|
22
|
+
def edit(original_content, file_type)
|
23
|
+
file = create_temp_file(original_content, file_type)
|
24
|
+
new_content = ""
|
25
|
+
|
26
|
+
loop do
|
27
|
+
new_content = edit_file(file)
|
28
|
+
|
29
|
+
is_valid, message = validate_content(new_content, file_type)
|
30
|
+
break if is_valid
|
31
|
+
|
32
|
+
Pantry.ui.say(message)
|
33
|
+
if !Pantry.ui.continue?("Continue editing?")
|
34
|
+
new_content = original_content
|
35
|
+
break
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
file.unlink
|
40
|
+
new_content
|
41
|
+
end
|
42
|
+
|
43
|
+
protected
|
44
|
+
|
45
|
+
def create_temp_file(file_contents, file_type)
|
46
|
+
tempfile = Tempfile.new(["edit-in-line", ".#{file_type}"])
|
47
|
+
tempfile.write(file_contents)
|
48
|
+
tempfile.close
|
49
|
+
tempfile
|
50
|
+
end
|
51
|
+
|
52
|
+
def edit_file(tempfile)
|
53
|
+
system("#{@editor} #{tempfile.path}")
|
54
|
+
File.read(tempfile.path)
|
55
|
+
end
|
56
|
+
|
57
|
+
def validate_content(content, file_type)
|
58
|
+
begin
|
59
|
+
Psych.parse(content, "config.yml")
|
60
|
+
return true, nil
|
61
|
+
rescue => ex
|
62
|
+
return false, ex.message
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|