cloud-sh 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,144 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "cloud/sh/commands/base"
4
+
5
+ module Cloud
6
+ module Sh
7
+ module Commands
8
+ class Refresh < Base
9
+ attr_reader :aliases
10
+
11
+ def execute
12
+ Cloud::Sh::Providers::DigitalOcean.refresh_k8s_configs
13
+ build_aliases
14
+ save_aliases
15
+ end
16
+
17
+ def build_aliases
18
+ @aliases = {}
19
+ config.accounts.each do |account|
20
+ puts "Refreshing account #{account.name}"
21
+ build_ssh_aliases(account)
22
+ build_db_aliases(account)
23
+ build_k8s_aliases(account)
24
+ end
25
+ end
26
+
27
+ def save_aliases
28
+ print "Saving aliases ... "
29
+ aliases_text = aliases.map do |alias_name, cmd|
30
+ "alias #{alias_name}=\"#{cmd}\""
31
+ end.join("\n")
32
+ File.write(config.aliases_file, aliases_text)
33
+ puts "DONE"
34
+ end
35
+
36
+ def build_ssh_aliases(account)
37
+ print " Refreshing SSH aliases ... "
38
+ provider = cloud_provider(account)
39
+ provider.servers do |server|
40
+ add_alias(:do, account, :ssh, server.name, "ssh #{server.ip}")
41
+ end
42
+ puts "DONE"
43
+ end
44
+
45
+ def build_db_aliases(account)
46
+ print " Refreshing DB aliases ... "
47
+ provider = cloud_provider(account)
48
+ provider.databases do |database|
49
+ next if database.cluster.ignore
50
+ if database.cluster.engine == "pg"
51
+ add_alias(:do, account, :psql, database.cluster, database.name, "psql \\\"#{database.uri}\\\"")
52
+ add_alias(:do, account, :pgdump, database.cluster, database.name, pgdump_command(database))
53
+ add_alias(:do, account, :pgcli, database.cluster, database.name, "pgcli \\\"#{database.uri}\\\"")
54
+ elsif database.cluster.engine == "mysql"
55
+ add_alias(:do, account, :mysql, database.cluster, database.name, mysql_command(database))
56
+ add_alias(:do, account, :mysqldump, database.cluster, database.name, mysqldump_command(database))
57
+ add_alias(:do, account, :mycli, database.cluster, database.name, "pgcli \\\"#{database.uri}\\\"")
58
+ elsif database.cluster.engine == "redis"
59
+ add_alias(:do, account, :redis, database.cluster, database.name, "redli -u \\\"#{database.uri}\\\"")
60
+ else
61
+ puts "Don't know how to handle database engine #{database.cluster.engine}"
62
+ end
63
+ end
64
+ puts "DONE"
65
+ end
66
+
67
+ def build_k8s_aliases(account)
68
+ print " Refreshing K8S aliases ... "
69
+ add_alias(:k8s, account, :ctl, kubectl)
70
+ provider = cloud_provider(account)
71
+ provider.clusters do |cluster|
72
+ add_alias(:k8s, account, :switch, :to, cluster, kubectl("config use-context", cluster.context))
73
+ add_alias(:k8s, account, :ctl, cluster, kubectl("--context #{cluster.context}"))
74
+ cluster.pods.each do |namespace, pods|
75
+ add_alias(:k8s, account, :tail, namespace, :all, "cloud-sh k8s tail --context #{cluster.context} --namespace #{namespace}")
76
+ pods.each do |pod|
77
+ add_alias(:k8s, account, :tail, namespace, pod.name, "cloud-sh k8s tail --context #{cluster.context} --namespace #{namespace} --pod #{pod.name}") unless pod.name == "console"
78
+ add_alias(:k8s, account, :exec, namespace, pod.name, "cloud-sh k8s exec --context #{cluster.context} --namespace #{namespace} --pod #{pod.name}")
79
+ add_alias(:k8s, account, namespace, :rails, :console, "cloud-sh k8s exec --context #{cluster.context} --namespace #{namespace} --pod #{pod.name} --cmd 'bundle exec rails console'") if pod.name == "console"
80
+ end
81
+ end
82
+ end
83
+ puts "DONE"
84
+ end
85
+
86
+ def kubectl(*parts)
87
+ [
88
+ "kubectl",
89
+ "--kubeconfig=#{Cloud::Sh::Providers::DigitalOcean.kube_config}",
90
+ parts
91
+ ].flatten.join(" ")
92
+ end
93
+
94
+ def mysql_command(database)
95
+ uri = URI.parse(database.uri)
96
+ [ :mysql, mysql_connection_params(uri), uri.path.delete("/")].join(" ")
97
+ end
98
+
99
+ def mysqldump_command(database)
100
+ uri = URI.parse(database.uri)
101
+ dump_name = "#{database.db}-`date +%s`.sql"
102
+ [ :mysqldump, mysql_connection_params(uri), uri.path.delete("/"), "> #{dump_name}"].join(" ")
103
+ end
104
+
105
+ def mysql_connection_params(uri)
106
+ [
107
+ "--host=#{uri.host}",
108
+ "--user=#{uri.user}",
109
+ "--password=#{uri.password}",
110
+ "--port=#{uri.port}",
111
+ "--ssl-mode=REQUIRED"
112
+ ].join(" ")
113
+ end
114
+
115
+ def pgdump_command(database)
116
+ dump_name = "#{database.db}-`date +%s`.sql"
117
+ "pg_dump \\\"#{database.uri}\\\" -f #{dump_name}"
118
+ end
119
+
120
+ def add_alias(*parts, cmd)
121
+ alias_name = parts.map { |part| normalize_alias_part(part) }.compact.join("-")
122
+ aliases[alias_name] = cmd
123
+ end
124
+
125
+ def normalize_alias_part(part)
126
+ return nil if part.respond_to?(:default) && part.default
127
+ if part.respond_to?(:alias)
128
+ part = part.alias
129
+ elsif part.respond_to?(:name)
130
+ part = part.name
131
+ elsif part.respond_to?(:to_s)
132
+ part = part.to_s
133
+ end
134
+ part.tr("._", "-")
135
+ end
136
+
137
+ def cloud_provider(account)
138
+ @cloud_providers ||= {}
139
+ @cloud_providers[account] ||= Cloud::Sh::Providers.build(account)
140
+ end
141
+ end
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cloud
4
+ module Sh
5
+ class Config
6
+ attr_reader :accounts
7
+
8
+ def initialize
9
+ @accounts = []
10
+ read_config
11
+ end
12
+
13
+ def read_config
14
+ return unless File.exist?(config_file)
15
+ config = YAML.safe_load(File.read(config_file))
16
+ config.each do |account_config|
17
+ accounts << Account.new(account_config)
18
+ end
19
+ end
20
+
21
+ def config_file
22
+ File.expand_path(".config/cloud-sh.yml", "~")
23
+ end
24
+
25
+ def aliases_file
26
+ File.expand_path(".cloud_sh_aliases", "~/")
27
+ end
28
+ end
29
+
30
+ class Account
31
+ attr_reader :name, :kind, :context, :default, :clusters, :databases
32
+
33
+ def initialize(config)
34
+ @name = config["name"]
35
+ @kind = config["kind"]
36
+ @context = config["context"]
37
+ @default = config.key?("default") && !!config["default"]
38
+ @clusters = []
39
+ @databases = []
40
+ load_clusters(config)
41
+ load_databases(config)
42
+ end
43
+
44
+ def load_clusters(config)
45
+ return unless config.key?("clusters")
46
+ config["clusters"].each do |cluster_config|
47
+ clusters << Cluster.new(cluster_config)
48
+ end
49
+ end
50
+
51
+ def load_databases(config)
52
+ return unless config.key?("databases")
53
+ config["databases"].each do |database_config|
54
+ databases << Database.new(database_config)
55
+ end
56
+ end
57
+
58
+ def find_cluster(name)
59
+ clusters.find { |cluster| cluster.name == name }
60
+ end
61
+
62
+ def find_database(name)
63
+ databases.find { |database| database.name == name }
64
+ end
65
+
66
+ def ignore_database?(name)
67
+ databases.any? do |database|
68
+ database.name == name && database.ignore
69
+ end
70
+ end
71
+ end
72
+ class Cluster
73
+ attr_reader :name, :alias, :default, :ignore
74
+
75
+ def initialize(config)
76
+ @name = config["name"]
77
+ @alias = config["alias"] || @name
78
+ @default = config.key?("default") && !!config["default"]
79
+ @ignore = config.key?("ignore") && !!config["ignore"]
80
+ end
81
+
82
+ def enrich(object)
83
+ object.alias = @alias
84
+ object.default = default
85
+ object.ignore = ignore
86
+ end
87
+ end
88
+
89
+ class Database
90
+ attr_reader :name, :alias, :default, :ignore
91
+
92
+ def initialize(config)
93
+ @name = config["name"]
94
+ @alias = config["alias"] || @name
95
+ @default = config.key?("default") && !!config["default"]
96
+ @ignore = config.key?("ignore") && !!config["ignore"]
97
+ end
98
+
99
+ def enrich(object)
100
+ object.alias = @alias
101
+ object.default = default
102
+ object.ignore = ignore
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cloud
4
+ module Sh
5
+ module Helpers
6
+ module Commands
7
+
8
+ def command_chain(base)
9
+ CmdChain.new(base)
10
+ end
11
+
12
+ class CmdChain
13
+ def initialize(base)
14
+ @cmd = [base]
15
+ end
16
+
17
+ def with(val)
18
+ @cmd << val
19
+ self
20
+ end
21
+
22
+ def method_missing(name, *args)
23
+ if args.empty?
24
+ @cmd << name.to_s.tr("_", "-")
25
+ elsif args.first.is_a?(TrueClass)
26
+ @cmd << "--#{name.to_s.tr("_", "-")}"
27
+ else
28
+ @cmd << "--#{name.to_s.tr("_", "-")}=#{args.first}"
29
+ end
30
+ self
31
+ end
32
+
33
+ def map(*fields)
34
+ execute.lines.map do |line|
35
+ values = line.split.first(fields.size)
36
+ OpenStruct.new(fields.zip(values).to_h)
37
+ end
38
+ end
39
+
40
+ def replace_current_process
41
+ exec(@cmd.join(" "))
42
+ end
43
+
44
+ def execute
45
+ cloud_sh_exec(@cmd)
46
+ end
47
+
48
+ def to_s
49
+ @cmd.join(" ")
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cloud
4
+ module Sh
5
+ module Providers
6
+ def self.providers
7
+ @providers ||= {}
8
+ end
9
+
10
+ def self.add_provider(name, klass)
11
+ providers[name] = klass
12
+ end
13
+
14
+ def self.build(account)
15
+ return providers[account.kind].new(account) if providers.key?(account.kind)
16
+ raise ArgumentError, "Don't know account kind #{account.kind} for account #{account.inspect}"
17
+ end
18
+
19
+ class Base
20
+ include Cloud::Sh::Helpers::Commands
21
+
22
+ attr_reader :account
23
+
24
+ def initialize(account)
25
+ @account = account
26
+ end
27
+
28
+ def servers
29
+ raise NotImplementedError
30
+ end
31
+
32
+ def databases
33
+ raise NotImplementedError
34
+ end
35
+
36
+ def clusters
37
+ raise NotImplementedError
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "cloud/sh/providers/base"
4
+ require "open3"
5
+
6
+ module Cloud
7
+ module Sh
8
+ module Providers
9
+ class DigitalOcean < Base
10
+ def self.refresh_k8s_configs
11
+ return if File.exist?(kube_config) && (Time.now.to_i - File.mtime(kube_config).to_i) < 3600
12
+ configs = Cloud::Sh.config.accounts.map { |account| new(account).k8s_configs }.flatten.compact
13
+ config = configs.shift
14
+ configs.each do |cfg|
15
+ config["clusters"] += cfg["clusters"]
16
+ config["contexts"] += cfg["contexts"]
17
+ config["users"] += cfg["users"]
18
+ end
19
+ config["current-context"] = config["contexts"].first["name"]
20
+ File.write(kube_config, YAML.dump(config))
21
+ end
22
+
23
+ def self.kube_config
24
+ File.expand_path(".kube/cloud_sh_config", "~/")
25
+ end
26
+
27
+ def servers
28
+ doctl.compute.droplet.list.format("Name,PublicIPv4").no_header(true).map(:name, :ip).each do |server|
29
+ yield server if block_given?
30
+ end
31
+ end
32
+
33
+ def databases
34
+ list = []
35
+ doctl.db.list.no_header(true).map(:id, :name, :engine).each do |cluster|
36
+ account.find_database(cluster.name)&.enrich(cluster)
37
+ defaultdb_uri = doctl.db.conn.with(cluster.id).format("URI").no_header(true).map(:uri).first.uri
38
+ dbs = [OpenStruct.new(name: "")] if cluster.engine == "redis"
39
+ dbs ||= doctl.db.db.list.with(cluster.id).no_header(true).map(:name)
40
+ dbs.each do |db|
41
+ uri = URI.parse(defaultdb_uri).tap { |uri| uri.path = "/#{db.name}" }.to_s
42
+ database = OpenStruct.new(cluster: cluster, name: db.name, uri: uri)
43
+ yield database if block_given?
44
+ list << database
45
+ end
46
+ end
47
+ list
48
+ end
49
+
50
+ def clusters
51
+ doctl.k8s.cluster.list.no_header(true).map(:id, :name).map do |cluster|
52
+ account.find_cluster(cluster.name)&.enrich(cluster)
53
+ next if cluster.ignore
54
+ cluster.context = k8s_context(account, cluster)
55
+ cluster.pods = kubectl.context(cluster.context).get.pod.all_namespaces(true).no_headers(true).map(:namespace, :name).map do |pod|
56
+ pod.name = k8s_pod_name(pod.name)
57
+ pod
58
+ end.group_by(&:itself).keys.group_by(&:namespace)
59
+ yield cluster if block_given?
60
+ end.compact
61
+ end
62
+
63
+ def k8s_configs
64
+ doctl.k8s.cluster.list.no_header(true).map(:id, :name).map do |cluster|
65
+ account.find_cluster(cluster.name)&.enrich(cluster)
66
+ next if cluster.ignore
67
+ cluster_config = YAML.load(doctl.k8s.cluster.config.show.with(cluster.id).execute)
68
+ cluster_config["contexts"].first["name"] = k8s_context(account, cluster)
69
+ cluster_config
70
+ end
71
+ end
72
+
73
+ def k8s_context(account, cluster)
74
+ [
75
+ account.name,
76
+ (cluster.alias unless cluster.default)
77
+ ].compact.join("-").tr("._", "-")
78
+ end
79
+
80
+ def k8s_pod_name(name)
81
+ parts = name.split("-")
82
+ parts.pop if parts.last =~ /^[a-z0-9]{5}$/
83
+ parts.pop if parts.last =~ /^[a-f0-9]{8,10}$/
84
+ parts.pop if parts.last =~ /^[a-f0-9]{8,10}$/
85
+ parts.join("-")
86
+ end
87
+
88
+ def doctl
89
+ command_chain("doctl").context(account.context)
90
+ end
91
+
92
+ def kubectl
93
+ command_chain("kubectl").kubeconfig(Cloud::Sh::Providers::DigitalOcean.kube_config)
94
+ end
95
+ end
96
+
97
+ add_provider("do", DigitalOcean)
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cloud
4
+ module Sh
5
+ VERSION = "1.0.0"
6
+ end
7
+ end
data/lib/cloud/sh.rb ADDED
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "cloud/sh/version"
4
+ require "gli"
5
+
6
+ require "cloud/sh/config"
7
+
8
+ require "cloud/sh/helpers/commands"
9
+
10
+ require "cloud/sh/commands/refresh"
11
+ require "cloud/sh/commands/k8s_tail"
12
+ require "cloud/sh/commands/k8s_exec"
13
+
14
+ require "cloud/sh/providers/digital_ocean"
15
+
16
+ module Cloud
17
+ module Sh
18
+ class Error < StandardError; end
19
+ module_function
20
+
21
+ def config
22
+ @config ||= Cloud::Sh::Config.new
23
+ end
24
+ end
25
+ end
26
+
27
+ module Kernel
28
+ def cloud_sh_exec(*cmd, env: nil)
29
+ cmd = cmd.flatten.map(&:to_s).join(" ")
30
+ args = env ? [env, cmd] : [cmd]
31
+ message = "Executing: #{cmd}"
32
+ message << " (#{env.inspect})" if env
33
+ stdout, stderr, status = Open3.capture3(*args)
34
+ unless status.success?
35
+ puts "Command: #{cmd}"
36
+ puts "ENV: #{env.inspect}" if env
37
+ puts "Stdout:\n#{stdout}\n"
38
+ puts "Stderr:\n#{stderr}\n"
39
+ raise "Command failed!!!"
40
+ end
41
+ stdout
42
+ end
43
+ end
44
+
45
+ class OpenStruct
46
+ def merge(other)
47
+ other.each_pair do |k, v|
48
+ self[k] = v
49
+ end
50
+ end
51
+ end
metadata ADDED
@@ -0,0 +1,128 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: cloud-sh
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Cristian Bica
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2019-08-30 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: gli
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: bundler
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.17'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.17'
41
+ - !ruby/object:Gem::Dependency
42
+ name: minitest
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '5.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '5.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '10.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '10.0'
69
+ description: Cloud shell helpers.
70
+ email:
71
+ - cristian.bica@gmail.com
72
+ executables:
73
+ - cloud-sh
74
+ - kubetail
75
+ extensions: []
76
+ extra_rdoc_files: []
77
+ files:
78
+ - ".github/workflows/gempush.yml"
79
+ - ".gitignore"
80
+ - ".rubocop.yml"
81
+ - ".travis.yml"
82
+ - Gemfile
83
+ - Gemfile.lock
84
+ - LICENSE.txt
85
+ - README.md
86
+ - Rakefile
87
+ - bin/console
88
+ - bin/setup
89
+ - cloud-sh.gemspec
90
+ - exe/cloud-sh
91
+ - exe/kubetail
92
+ - lib/cloud/sh.rb
93
+ - lib/cloud/sh/cli.rb
94
+ - lib/cloud/sh/commands/base.rb
95
+ - lib/cloud/sh/commands/k8s_exec.rb
96
+ - lib/cloud/sh/commands/k8s_tail.rb
97
+ - lib/cloud/sh/commands/refresh.rb
98
+ - lib/cloud/sh/config.rb
99
+ - lib/cloud/sh/helpers/commands.rb
100
+ - lib/cloud/sh/providers/base.rb
101
+ - lib/cloud/sh/providers/digital_ocean.rb
102
+ - lib/cloud/sh/version.rb
103
+ homepage: https://github.com/cristianbica/cloud-sh
104
+ licenses:
105
+ - MIT
106
+ metadata:
107
+ homepage_uri: https://github.com/cristianbica/cloud-sh
108
+ source_code_uri: https://github.com/cristianbica/cloud-sh
109
+ post_install_message:
110
+ rdoc_options: []
111
+ require_paths:
112
+ - lib
113
+ required_ruby_version: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ required_rubygems_version: !ruby/object:Gem::Requirement
119
+ requirements:
120
+ - - ">="
121
+ - !ruby/object:Gem::Version
122
+ version: '0'
123
+ requirements: []
124
+ rubygems_version: 3.0.3
125
+ signing_key:
126
+ specification_version: 4
127
+ summary: Cloud shell helpers.
128
+ test_files: []