pagoda 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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