pagoda 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.
@@ -0,0 +1,149 @@
1
+ module Pagoda::Command
2
+ class Auth < Base
3
+ attr_accessor :credentials
4
+
5
+ def initialize(args)
6
+ @args = args
7
+ check_for_credentials
8
+ end
9
+
10
+
11
+ def client
12
+ @client ||= init_client
13
+ end
14
+
15
+ def init_client
16
+ client = Pagoda::Client.new(user, password)
17
+ client.on_warning { |message| self.display("\n#{message}\n\n") }
18
+ client
19
+ end
20
+
21
+ # just a stub; will raise if not authenticated
22
+ def check
23
+ client.app_list
24
+ end
25
+
26
+ def check_for_credentials
27
+ if option_value("-u", "--username") && option_value("-p", "--password")
28
+ reauthorize
29
+ end
30
+ end
31
+
32
+ def reauthorize
33
+ @credentials = ask_for_credentials
34
+ write_credentials
35
+ end
36
+
37
+ def user # :nodoc:
38
+ get_credentials
39
+ @credentials[0]
40
+ end
41
+
42
+ def password # :nodoc:
43
+ get_credentials
44
+ @credentials[1]
45
+ end
46
+
47
+ def credentials_file
48
+ "#{home_directory}/.pagoda/credentials"
49
+ end
50
+
51
+ def get_credentials # :nodoc:
52
+ return if @credentials
53
+ unless @credentials = read_credentials
54
+ @credentials = ask_for_credentials
55
+ save_credentials
56
+ end
57
+ @credentials
58
+ end
59
+
60
+ def read_credentials
61
+ File.exists?(credentials_file) and File.read(credentials_file).split("\n")
62
+ end
63
+
64
+ def echo_off
65
+ system "stty -echo"
66
+ end
67
+
68
+ def echo_on
69
+ system "stty echo"
70
+ end
71
+
72
+ def ask_for_credentials
73
+ unless username = option_value("-u", "--username")
74
+ username = ask "Username: "
75
+ end
76
+ unless password = option_value("-p", "--password")
77
+ display "Password: ", false
78
+ password = running_on_windows? ? ask_for_password_on_windows : ask_for_password
79
+ end
80
+ [username, password] # return
81
+ end
82
+
83
+ def ask_for_password
84
+ echo_off
85
+ password = ask
86
+ puts
87
+ echo_on
88
+ return password
89
+ end
90
+
91
+ def ask_for_password_on_windows
92
+ require "Win32API"
93
+ char = nil
94
+ password = ''
95
+
96
+ while char = Win32API.new("crtdll", "_getch", [ ], "L").Call do
97
+ break if char == 10 || char == 13 # received carriage return or newline
98
+ if char == 127 || char == 8 # backspace and delete
99
+ password.slice!(-1, 1)
100
+ else
101
+ # windows might throw a -1 at us so make sure to handle RangeError
102
+ (password << char.chr) rescue RangeError
103
+ end
104
+ end
105
+ return password
106
+ end
107
+
108
+ def save_credentials
109
+ begin
110
+ write_credentials
111
+ Pagoda::Command.run_internal('auth:check', args)
112
+ rescue RestClient::Unauthorized => e
113
+ delete_credentials
114
+ raise e unless retry_login?
115
+
116
+ error "Authentication failed"
117
+ @credentials = ask_for_credentials
118
+ @client = init_client
119
+ retry
120
+ rescue Exception => e
121
+ delete_credentials
122
+ raise e
123
+ end
124
+ end
125
+
126
+ def retry_login?
127
+ @login_attempts ||= 0
128
+ @login_attempts += 1
129
+ @login_attempts < 3
130
+ end
131
+
132
+ def write_credentials
133
+ FileUtils.mkdir_p(File.dirname(credentials_file))
134
+ File.open(credentials_file, 'w') do |file|
135
+ file.puts self.credentials
136
+ end
137
+ set_credentials_permissions
138
+ end
139
+
140
+ def set_credentials_permissions
141
+ FileUtils.chmod 0700, File.dirname(credentials_file)
142
+ FileUtils.chmod 0600, credentials_file
143
+ end
144
+
145
+ def delete_credentials
146
+ FileUtils.rm_f(credentials_file)
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,184 @@
1
+ require 'iniparse'
2
+
3
+ module Pagoda
4
+ module Command
5
+ class Base
6
+ include Pagoda::Helpers
7
+
8
+ attr_accessor :args
9
+
10
+ def initialize(args)
11
+ @args = args
12
+ end
13
+
14
+ def client
15
+ @client ||= Pagoda::Command.run_internal('auth:client', args)
16
+ end
17
+
18
+ def shell(cmd)
19
+ FileUtils.cd(Dir.pwd) {|d| return `#{cmd}`}
20
+ end
21
+
22
+ def app(soft_fail=false)
23
+ if override = option_value("-a", "--app")
24
+ return override
25
+ else
26
+ if name = find_app
27
+ return name
28
+ else
29
+ if locate_app_root
30
+ if extract_git_clone_url
31
+ return false if soft_fail
32
+ errors = []
33
+ errors << "This repo is either not launched, or not paired with a launched app"
34
+ errors << ""
35
+ errors << "To launch this app run 'pagoda launch <app-name>'"
36
+ errors << ""
37
+ errors << "To pair this project with a deployed app, run 'pagoda pair <app-name>'"
38
+ errors << ""
39
+ errors << "To see a list of currently deployed apps, run 'pagoda list'"
40
+ error errors
41
+ else
42
+ return false if soft_fail
43
+ errors = []
44
+ errors << "It appears you are using git (fantastic)."
45
+ errors << "However we only support git repos hosted with github."
46
+ errors << "Please ensure your repo is hosted with github."
47
+ errors << ""
48
+ errors << "If you are trying to reference a specific app, try argument: -a <app-name>"
49
+ error errors
50
+ end
51
+ else
52
+ return false if soft_fail
53
+ errors = []
54
+ errors << "Unable to find git config in this directory or in any parent directory"
55
+ errors << ""
56
+ errors << "If you are trying to reference a specific app, try argument: -a <app-name>"
57
+ error errors
58
+ end
59
+ end
60
+ end
61
+ name
62
+ end
63
+
64
+ def parse_branch
65
+ option_value("-b", "--branch") || find_branch
66
+ end
67
+
68
+ def parse_commit
69
+ option_value("-c", "--commit") || find_commit
70
+ end
71
+
72
+ def find_app
73
+ read_apps.each do |line|
74
+ app = line.split(" ")
75
+ return app[0] if app[2] == locate_app_root
76
+ end
77
+ false
78
+ end
79
+
80
+ def find_branch
81
+ begin
82
+ line = File.new("#{locate_app_root}/.git/HEAD").gets
83
+ line.strip.split(' ').last.split("/").last
84
+ rescue
85
+ nil
86
+ end
87
+ end
88
+
89
+ def find_commit
90
+ begin
91
+ File.new("#{locate_app_root}/.git/refs/heads/#{parse_branch}").gets.strip
92
+ rescue
93
+ nil
94
+ end
95
+ end
96
+
97
+ def read_apps
98
+ return [] if !File.exists?(apps_file)
99
+ File.read(apps_file).split(/\n/).inject([]) {|apps, line| apps << line if line.include?("git@github.com"); apps}
100
+ end
101
+
102
+ def write_app(name, git_url=nil, app_root=nil)
103
+ git_url = extract_git_clone_url unless git_url
104
+ app_root = locate_app_root unless app_root
105
+ FileUtils.mkdir_p(File.dirname(apps_file)) if !File.exists?(apps_file)
106
+ current_apps = read_apps
107
+ File.open(apps_file, 'w') do |file|
108
+ current_apps.each do |app|
109
+ file.puts app
110
+ end
111
+ file.puts "#{name} #{git_url} #{app_root}"
112
+ end
113
+ set_apps_file_permissions
114
+ end
115
+ alias :add_app :write_app
116
+
117
+ def remove_app(name)
118
+ current_apps = read_apps
119
+ current_apps.delete_if do |app|
120
+ app.split(" ")[0] == name
121
+ end
122
+ File.open(apps_file, 'w') do |file|
123
+ current_apps.each do |app|
124
+ file.puts app
125
+ end
126
+ end
127
+ end
128
+
129
+ def set_apps_file_permissions
130
+ FileUtils.chmod 0700, File.dirname(apps_file)
131
+ FileUtils.chmod 0600, apps_file
132
+ end
133
+
134
+ def apps_file
135
+ "#{home_directory}/.pagoda/apps"
136
+ end
137
+
138
+ def extract_possible_name
139
+ cleanup_name(extract_git_clone_url.split(":")[1].split("/")[1].split(".")[0])
140
+ end
141
+
142
+ def cleanup_name(name)
143
+ name.gsub(/-/, '').gsub(/_/, '').gsub(/ /, '').downcase
144
+ end
145
+
146
+ def extract_git_clone_url(soft=false)
147
+ begin
148
+ url = IniParse.parse( File.read("#{locate_app_root}/.git/config") )['remote "origin"']["url"]
149
+ raise unless url.match(/^git@github.com:.+\.git$/)
150
+ url
151
+ rescue Exception => e
152
+ return false
153
+ end
154
+ end
155
+
156
+ def version
157
+ display Client.gem_version_string
158
+ end
159
+
160
+ def locate_app_root(dir=Dir.pwd)
161
+ return dir if File.exists? "#{dir}/.git/config"
162
+ parent = dir.split('/')[0..-2].join('/')
163
+ return false if parent.empty?
164
+ locate_app_root(parent)
165
+ end
166
+
167
+ def extract_option(options, default=true)
168
+ values = options.is_a?(Array) ? options : [options]
169
+ return unless opt_index = args.select { |a| values.include? a }.first
170
+ opt_position = args.index(opt_index) + 1
171
+ if args.size > opt_position && opt_value = args[opt_position]
172
+ if opt_value.include?('--')
173
+ opt_value = nil
174
+ else
175
+ args.delete_at(opt_position)
176
+ end
177
+ end
178
+ opt_value ||= default
179
+ args.delete(opt_index)
180
+ block_given? ? yield(opt_value) : opt_value
181
+ end
182
+ end
183
+ end
184
+ end
@@ -0,0 +1,100 @@
1
+ module Pagoda::Command
2
+ class Help < Base
3
+ def index
4
+ display %{
5
+ Pagoda
6
+
7
+ NAME
8
+ pagoda -- command line utility for pagodabox.com
9
+
10
+ SYNOPSIS
11
+ pagoda [command] [parameters]
12
+
13
+ DESCRIPTION
14
+
15
+ If no operands are given, we will attempt to pull data from the current
16
+ directory. If more than one operand is given, non-directory operands are
17
+ displayed first.
18
+
19
+ The following options are available:
20
+
21
+ COMMANDS
22
+
23
+ list # list your apps
24
+ deploy # Deploy your current state to pagoda
25
+ launch <name> # create (register) a new app
26
+ info # display info about an app
27
+ destroy # remove app
28
+ rollback # rollback app
29
+ tunnel # create a tunnel to your database on pagoda
30
+
31
+
32
+ PARAMETERS
33
+
34
+ ---------------------------
35
+ GLOBAL :
36
+ ---------------------------
37
+ -a <name> | --app=<name>
38
+ Set the application name (Only necessary when not in repo dir).
39
+
40
+ -u <username> | --username=<username>
41
+ When set, will not attempt to save your username. Also over-rides
42
+ any saved username.
43
+
44
+ -p <password> | --password=<password>
45
+ When set, will not attempt to save your password. Also over-rides
46
+ any saved password.
47
+
48
+ -f
49
+ Executes all commands without confirmation request.
50
+
51
+ ---------------------------
52
+ DEPLOYING - pagoda deploy :
53
+ ---------------------------
54
+ -b <branch> | --branch=<branch>
55
+ Specify the branch name. By default uses the branch
56
+ your local repo is on.
57
+
58
+ -c <commit> | --commit=<commit>
59
+ Specify the commit id. By default uses the commit HEAD is set to.
60
+
61
+ --latest
62
+ Will attempt to deploy to the latest commit on github rather than
63
+ your local repo's current commit.
64
+
65
+ ---------------------------
66
+ TUNNELING - pagoda tunnel :
67
+ ---------------------------
68
+
69
+ -t <type> | --type=<type>
70
+ Specify the tunnel type. (ex:mysql)
71
+
72
+ -n <instance> | --name=<instance>
73
+ Specify the instance name you want to operate on used for
74
+ database instance
75
+
76
+
77
+ EXAMPLES
78
+ launch an application on pagoda from inside the clone folder:
79
+ (must be done inside your repo folder)
80
+ pagoda launch <app name>
81
+
82
+ list your applications:
83
+
84
+ pagoda list
85
+
86
+ create tunnel to your database:
87
+ (must be inside your repo folder or specify app)
88
+
89
+ pagoda tunnel -a <app name> -t mysql -n <database name>
90
+
91
+ destroy an application:
92
+ (must be inside your repo folder or specify app)
93
+
94
+ pagoda destroy
95
+
96
+
97
+ }
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,49 @@
1
+ module Pagoda::Command
2
+ class Tunnel < Auth
3
+
4
+ def index
5
+ app
6
+ type = option_value("-t", "--type") || 'mysql'
7
+ Pagoda::Command.run_internal("tunnel:#{type}", args)
8
+ end
9
+
10
+ def mysql
11
+ instance_name = option_value("-n", "--name")
12
+ unless instance_name
13
+ # try to find mysql instances here
14
+ dbs = client.app_databases(app)
15
+ if dbs.length == 0
16
+ errors = []
17
+ errors << "It looks like you don't have any MySQL instances for #{app}"
18
+ errors << "Feel free to add one in the admin panel (10 MB Free)"
19
+ error errors
20
+ elsif dbs.length == 1
21
+ instance_name = dbs.first[:name]
22
+ else
23
+ errors = []
24
+ errors << "Multiple MySQL instances found"
25
+ errors << ""
26
+ dbs.each do |instance|
27
+ errors << "-> #{instance[:name]}"
28
+ end
29
+ errors << ""
30
+ errors << "Please specify which instance you would like to use."
31
+ errors << ""
32
+ errors << "ex: pagoda tunnel -n #{dbs[0][:name]}"
33
+ error errors
34
+ end
35
+ end
36
+ display
37
+ display "+> Authenticating Database Ownership"
38
+
39
+ if client.database_exists?(app, instance_name)
40
+ Pagoda::TunnelProxy.new(:mysql, user, password, app, instance_name).start
41
+ else
42
+ errors = []
43
+ errors << "Security exception -"
44
+ errors << "Either the MySQL instance doesn't exist or you are unauthorized"
45
+ error errors
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,127 @@
1
+ require 'crack'
2
+
3
+ module Pagoda
4
+ module Helpers
5
+ INDENT = " "
6
+
7
+ def home_directory
8
+ running_on_windows? ? ENV['USERPROFILE'] : ENV['HOME']
9
+ end
10
+
11
+ def running_on_windows?
12
+ RUBY_PLATFORM =~ /mswin32|mingw32/
13
+ end
14
+
15
+ def running_on_a_mac?
16
+ RUBY_PLATFORM =~ /-darwin\d/
17
+ end
18
+
19
+ def display(msg="", newline=true, level=1)
20
+ indent = build_indent(level)
21
+ if newline
22
+ puts("#{indent}#{msg}")
23
+ else
24
+ print("#{indent}#{msg}")
25
+ STDOUT.flush
26
+ end
27
+ end
28
+
29
+ def option_value(short_hand = nil, long_hand = nil)
30
+ match = false
31
+ value = nil
32
+
33
+ if short_hand
34
+ if args.include?(short_hand)
35
+ value = args[args.index(short_hand) + 1]
36
+ match = true
37
+ end
38
+ end
39
+ if long_hand && !match
40
+ if match = args.grep(/#{long_hand}.*/).first
41
+ if match.include? "="
42
+ value = match.split("=").last
43
+ else
44
+ value = true
45
+ end
46
+ end
47
+ end
48
+
49
+ value
50
+ end
51
+
52
+ def format_date(date)
53
+ date = Time.parse(date) if date.is_a?(String)
54
+ date.strftime("%Y-%m-%d %H:%M %Z")
55
+ end
56
+
57
+ def ask(message=nil, level=1)
58
+ display("#{message}", false, level) if message
59
+ gets.strip
60
+ end
61
+
62
+ def confirm(message="Are you sure you wish to continue? (y/n)?", level=1)
63
+ return true if args.include? "-f"
64
+ case message
65
+ when Array
66
+ count = message.length
67
+ iteration = 0
68
+ message.each do |m|
69
+ if iteration == count - 1
70
+ display("#{m} ", false, level)
71
+ else
72
+ display("#{m} ", true, level)
73
+ end
74
+ iteration += 1
75
+ end
76
+ when String
77
+ display("#{message} ", false, level)
78
+ end
79
+ ask.downcase == 'y'
80
+ end
81
+
82
+ def error(msg, exit=true, level=1)
83
+ indent = build_indent(level)
84
+ STDERR.puts
85
+ case msg
86
+ when Array
87
+ STDERR.puts("#{indent}** Error:")
88
+ msg.each do |m|
89
+ STDERR.puts("#{indent}** #{m}")
90
+ end
91
+ when String
92
+ STDERR.puts("#{indent}** Error: #{msg}")
93
+ end
94
+ STDERR.puts
95
+ exit 1 if exit
96
+ end
97
+
98
+ def loop_transaction(app_name = nil)
99
+ finished = false
100
+ until finished
101
+ display ".", false, 0
102
+ sleep 1
103
+ if client.app_info(app_name || app)[:transactions].count < 1
104
+ finished = true
105
+ display
106
+ end
107
+ end
108
+ end
109
+
110
+ def build_indent(level=1)
111
+ indent = ""
112
+ level.times do
113
+ indent += INDENT
114
+ end
115
+ indent
116
+ end
117
+
118
+ end
119
+ end
120
+
121
+ unless String.method_defined?(:shellescape)
122
+ class String
123
+ def shellescape
124
+ empty? ? "''" : gsub(/([^A-Za-z0-9_\-.,:\/@\n])/n, '\\\\\\1').gsub(/\n/, "'\n'")
125
+ end
126
+ end
127
+ end