cuber 0.0.0 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f3c804748670e9820f373a93a7eee22f5f3347e6fbb92f249f1db5be08dac352
4
- data.tar.gz: c551a4ae6c9edd75814d893ee8e259d7c045f6464d942f5920d632b47e836ade
3
+ metadata.gz: b5561ce42263c62d390dc44cff4a723add20afd04bf432511f16d28259889c6e
4
+ data.tar.gz: e0740fa2bee7dc18e9bc3d6304fcb73b592a5c114bab3df8349cbd9e82b2547d
5
5
  SHA512:
6
- metadata.gz: 22bf3a47e78f9a9cc71b04fa3a3cf3abb2672014598fa67cf2186f4943db5d67ccbd47c89c256ae8ddc14386efc8405842acdea2fecda0c8be82f765e937429a
7
- data.tar.gz: ac57faaf701389648745991c4dac3eaa580c7c4e609b2141e9b07b0ba55a75593794c6faa27d0242839accd2b541c296b4ebc872da7f6910400c099a82dba4f3
6
+ metadata.gz: 1a3877f6bcfda0e789a3b9f906fd89026976bc7402491b82e106a80965376011e86425f208abc0ebb13cc56ed8272addd2b1511125a8911504bcb7a206d10e0e
7
+ data.tar.gz: d9faec4d34f5a3c756168e4b57898cc61411da003ad103b0911b24af39996bd1d936cbd40cdd49141e4caae3ab261f2ef37b0fa0a6b7b6b24551bb8c8cce8118
data/LICENSE.md ADDED
@@ -0,0 +1,56 @@
1
+ # Cuber License
2
+
3
+ Copyright 2021 Cuber (AbstractBrain srls)
4
+
5
+ *This software end-user license agreement (EULA) is a legal agreement between you, the customer who has obtained a copy of the software, as an individual or as an organization, and Cuber (AbstractBrain srls).*
6
+
7
+ ## License Grant
8
+ This EULA grants you a non-exclusive, non-transferable, limited license to install and use this software. You can use any number of copies of this software inside your organization.
9
+
10
+ ## Ownership
11
+ The ownership and copyright of the software belongs to Cuber and is not transferred in any way by this license.
12
+
13
+ ## Restricted Uses
14
+ You are not allowed to distribute, publish, sublicense or sell copies of this software.
15
+ It is your responsibility to never share your license key.
16
+ You are also not allowed to use the software as part of a product or service that provides similar functionality to the software itself.
17
+
18
+ ## Modifications
19
+ You are allowed to inspect and make changes to this software for private use, excluding any modification made to circumvent licenses or copyright. You are also allowed to make temporary forks of the project in order to contribute to it. This EULA also applies to modified versions of the software.
20
+
21
+ ## Contributions and Copyright Transfer
22
+ In case you contribute to the project, for example with suggestions or pull requests, you assign all rights, including copyright, from yourself to Cuber. You also confirm that your employer, if you have one, has given you the right to do so.
23
+
24
+ ## No Warranty
25
+ This software is provided “as is” without warranty of any kind, either express or implied.
26
+
27
+ ## Limitation of Liability
28
+ In no event shall the author of this software be liable for any damages (including, without limitation, damages for loss of business profits, business interruption, loss of data, or any other pecuniary loss), even if the author of this software is aware of the possibility of such damages and known defects.
29
+ In no event will Cuber liability exceed the software license price as indicated in the invoice.
30
+
31
+ ## Use of Data
32
+ Cuber does not collect any kind of data, except for what is strictly necessary for billing purposes.
33
+
34
+ ## Software Updates
35
+ You can receive software updates, if any, for the duration of the license that you have purchased.
36
+
37
+ ## Fees and Payment
38
+ You can use this software for free, within the limits of the free tier, if present. When you purchase a license key, the software license fees will be due and payable in full at the time of purchase or when you decide to renew the license because it is expiring.
39
+
40
+ ## Expiration of License Key
41
+ A license key has a limited duration and must be renewed after that billing period if you want to continue to use the software with all its functionalities.
42
+
43
+ ## Termination
44
+ If the license expires or it is terminated due to violation of these terms or because one of the parties requests the termination of this contract, you must destroy all the copies of the software, including any modified version.
45
+
46
+ ## Third Party Software
47
+ Third party software that is used by this software is subject to the respective license and is not subject to this EULA.
48
+
49
+ ## Governing Law
50
+ This agreement is governed by the Italian law and you agree that any dispute arising from or relating to the subject matter of this EULA shall be governed by the exclusive jurisdiction and venue of the Italian courts.
51
+
52
+ ## Amendment
53
+ Cuber reserves the right, in its sole discretion, to amend this EULA. You can find an updated version of this EULA by visiting <https://github.com/cuber-cloud/cuber-gem/blob/master/LICENSE.md>
54
+
55
+ ## License and Copyright Notice
56
+ This license and the copyright notice must be retained on any copy of the software.
data/README.md ADDED
@@ -0,0 +1,5 @@
1
+ # Cuber
2
+
3
+ Deploy your apps on Kubernetes easily.
4
+
5
+ [cuber.cloud →](https://cuber.cloud)
data/bin/cuber ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env ruby
2
+ require 'cuber'
3
+ Cuber::CLI.new
data/cuber.gemspec ADDED
@@ -0,0 +1,13 @@
1
+ require_relative 'lib/cuber/version'
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = 'cuber'
5
+ s.version = Cuber::VERSION
6
+ s.summary = 'Deploy your apps on Kubernetes easily.'
7
+ s.author = 'Cuber'
8
+ s.homepage = 'https://cuber.cloud'
9
+ s.license = 'LicenseRef-LICENSE.md'
10
+ s.executables = ['cuber']
11
+ s.files = `git ls-files`.split("\n")
12
+ s.add_dependency 'jwt'
13
+ end
data/lib/cuber/cli.rb ADDED
@@ -0,0 +1,55 @@
1
+ require 'optparse'
2
+ require 'fileutils'
3
+ require 'open3'
4
+ require 'erb'
5
+ require 'base64'
6
+ require 'yaml'
7
+ require 'json'
8
+ require 'shellwords'
9
+ require 'time'
10
+ require 'openssl'
11
+ require 'jwt'
12
+
13
+ module Cuber
14
+ class CLI
15
+
16
+ def initialize
17
+ @options = {}
18
+ parse_command!
19
+ parse_cuberfile
20
+ validate_cuberfile
21
+ execute
22
+ end
23
+
24
+ private
25
+
26
+ def parse_command!
27
+ @options[:cmd] = ARGV.shift&.to_sym
28
+ end
29
+
30
+ def parse_cuberfile
31
+ abort 'Cuberfile not found in current directory' unless File.exists? 'Cuberfile'
32
+ content = File.read 'Cuberfile'
33
+ parser = CuberfileParser.new
34
+ parser.instance_eval(content)
35
+ cuberfile_options = parser.instance_variables.map do |name|
36
+ [name[1..-1].to_sym, parser.instance_variable_get(name)]
37
+ end.to_h
38
+ @options.merge! cuberfile_options
39
+ end
40
+
41
+ def validate_cuberfile
42
+ validator = CuberfileValidator.new @options
43
+ errors = validator.validate
44
+ errors.each { |err| $stderr.puts "Cuberfile: #{err}" }
45
+ abort unless errors.empty?
46
+ end
47
+
48
+ def execute
49
+ command_class = @options[:cmd]&.capitalize
50
+ abort "Cuber: \"#{@options[:cmd]}\" is not a command" unless command_class && Cuber::Commands.const_defined?(command_class)
51
+ Cuber::Commands.const_get(command_class).new(@options).execute
52
+ end
53
+
54
+ end
55
+ end
@@ -0,0 +1,98 @@
1
+ module Cuber::Commands
2
+ class Deploy
3
+ include Cuber::Utils
4
+
5
+ def initialize options
6
+ @options = options
7
+ end
8
+
9
+ def execute
10
+ if @options[:release]
11
+ print_step 'Deploying a past release'
12
+ else
13
+ checkout
14
+ set_release_name
15
+ if @options[:buildpacks]
16
+ pack
17
+ else
18
+ build
19
+ push
20
+ end
21
+ end
22
+ configure
23
+ apply
24
+ rollout
25
+ end
26
+
27
+ private
28
+
29
+ def print_step desc
30
+ puts
31
+ puts "\e[34m-----> #{desc}\e[0m"
32
+ end
33
+
34
+ def checkout
35
+ print_step 'Cloning Git repository'
36
+ path = '.cuber/repo'
37
+ FileUtils.mkdir_p path
38
+ FileUtils.rm_rf path, secure: true
39
+ system('git', 'clone', '--depth', '1', @options[:repo], path) || abort('Cuber: git clone failed')
40
+ end
41
+
42
+ def commit_hash
43
+ out, status = Open3.capture2 'git', 'rev-parse', '--short', 'HEAD', chdir: '.cuber/repo'
44
+ abort 'Cuber: cannot get commit hash' unless status.success?
45
+ out.strip
46
+ end
47
+
48
+ def set_release_name
49
+ @options[:release] = "#{commit_hash}-#{Time.now.utc.iso8601.delete('^0-9')}"
50
+ end
51
+
52
+ def pack
53
+ print_step 'Building image using buildpacks'
54
+ tag = "#{@options[:image]}:#{@options[:release]}"
55
+ cmd = ['pack', 'build', tag, '--builder', @options[:buildpacks], '--publish']
56
+ cmd += ['--pull-policy', 'always', '--clear-cache'] if @options[:cache] == false
57
+ system(*cmd, chdir: '.cuber/repo') || abort('Cuber: pack build failed')
58
+ end
59
+
60
+ def build
61
+ print_step 'Building image from Dockerfile'
62
+ dockerfile = @options[:dockerfile] || 'Dockerfile'
63
+ tag = "#{@options[:image]}:#{@options[:release]}"
64
+ cmd = ['docker', 'build']
65
+ cmd += ['--pull', '--no-cache'] if @options[:cache] == false
66
+ cmd += ['--progress', 'plain', '-f', dockerfile, '-t', tag, '.']
67
+ system(*cmd, chdir: '.cuber/repo') || abort('Cuber: docker build failed')
68
+ end
69
+
70
+ def push
71
+ print_step 'Pushing image to Docker registry'
72
+ tag = "#{@options[:image]}:#{@options[:release]}"
73
+ system('docker', 'push', tag) || abort('Cuber: docker push failed')
74
+ end
75
+
76
+ def configure
77
+ print_step 'Generating Kubernetes configuration'
78
+ @options[:instance] = "#{@options[:app]}-#{Time.now.utc.iso8601.delete('^0-9')}"
79
+ @options[:dockerconfigjson] = Base64.strict_encode64 File.read File.expand_path(@options[:dockerconfig] || '~/.docker/config.json')
80
+ render 'deployment.yml', '.cuber/kubernetes/deployment.yml'
81
+ end
82
+
83
+ def apply
84
+ print_step 'Applying configuration to Kubernetes cluster'
85
+ kubectl 'apply',
86
+ '-f', '.cuber/kubernetes/deployment.yml',
87
+ '--prune', '-l', "app.kubernetes.io/name=#{@options[:app]},app.kubernetes.io/managed-by=cuber"
88
+ end
89
+
90
+ def rollout
91
+ print_step 'Verifying deployment status'
92
+ @options[:procs].each_key do |procname|
93
+ kubectl 'rollout', 'status', "deployment/#{procname}"
94
+ end
95
+ end
96
+
97
+ end
98
+ end
@@ -0,0 +1,134 @@
1
+ module Cuber::Commands
2
+ class Info
3
+ include Cuber::Utils
4
+
5
+ def initialize options
6
+ @options = options
7
+ @namespace = nil
8
+ end
9
+
10
+ def execute
11
+ set_namespace
12
+ print_app_version
13
+ print_public_ip
14
+ print_env
15
+ print_migration
16
+ print_proc
17
+ print_cron
18
+ print_pods
19
+ end
20
+
21
+ private
22
+
23
+ def set_namespace
24
+ @namespace = kubeget 'namespace', @options[:app]
25
+ abort 'Cuber: app not found' if @namespace.dig('metadata', 'labels', 'app.kubernetes.io/managed-by') != 'cuber'
26
+ end
27
+
28
+ def print_section title
29
+ puts
30
+ puts "\e[34m=== #{title}\e[0m"
31
+ end
32
+
33
+ def print_app_version
34
+ print_section 'App'
35
+ puts "#{@namespace['metadata']['labels']['app.kubernetes.io/name']}"
36
+ puts "version #{@namespace['metadata']['labels']['app.kubernetes.io/version']}"
37
+ end
38
+
39
+ def print_public_ip
40
+ print_section 'Public IP'
41
+ if @namespace['metadata']['annotations']['ingress'] == 'true'
42
+ json = kubeget 'ingress', 'web-ingress'
43
+ else
44
+ json = kubeget 'service', 'load-balancer'
45
+ end
46
+ ip = json.dig 'status', 'loadBalancer', 'ingress', 0, 'ip'
47
+ if ip
48
+ puts "#{ip}"
49
+ else
50
+ puts "None detected"
51
+ end
52
+ end
53
+
54
+ def print_env
55
+ print_section 'Env'
56
+ json = kubeget 'configmap', 'env'
57
+ json['data']&.each do |key, value|
58
+ puts "#{key}=#{value}"
59
+ end
60
+ json = kubeget 'secrets', 'app-secrets'
61
+ json['data']&.each do |key, value|
62
+ puts "#{key}=#{Base64.decode64(value)[0...5] + '***'}"
63
+ end
64
+ end
65
+
66
+ def print_migration
67
+ print_section 'Migration'
68
+ migration = "migrate-#{@namespace['metadata']['labels']['app.kubernetes.io/instance']}"
69
+ json = kubeget 'job', migration, '--ignore-not-found'
70
+ if json
71
+ migration_command = json['spec']['template']['spec']['containers'][0]['command'].shelljoin
72
+ migration_status = json['status']['succeeded'].to_i.zero? ? 'Pending' : 'Completed'
73
+ puts "migrate: #{migration_command} (#{migration_status})"
74
+ else
75
+ puts "None detected"
76
+ end
77
+ end
78
+
79
+ def print_proc
80
+ print_section 'Proc'
81
+ json = kubeget 'deployments'
82
+ json['items'].each do |proc|
83
+ name = proc['metadata']['name']
84
+ command = proc['spec']['template']['spec']['containers'][0]['command'].shelljoin
85
+ available = proc['status']['availableReplicas'].to_i
86
+ updated = proc['status']['updatedReplicas'].to_i
87
+ replicas = proc['status']['replicas'].to_i
88
+ scale = proc['spec']['replicas'].to_i
89
+ puts "#{name}: #{command} (#{available}/#{scale}) #{'OUT-OF-DATE' if replicas - updated > 0}"
90
+ end
91
+ end
92
+
93
+ def print_cron
94
+ print_section 'Cron'
95
+ json = kubeget 'cronjobs'
96
+ json['items'].each do |cron|
97
+ name = cron['metadata']['name']
98
+ schedule = cron['spec']['schedule']
99
+ command = cron['spec']['jobTemplate']['spec']['template']['spec']['containers'][0]['command'].shelljoin
100
+ last = cron['status']['lastScheduleTime']
101
+ puts "#{name}: #{schedule} #{command} (#{time_ago_in_words last})"
102
+ end
103
+ end
104
+
105
+ def print_pods
106
+ print_section 'Pods'
107
+ json = kubeget 'pods'
108
+ json['items'].each do |pod|
109
+ name = pod['metadata']['name']
110
+ created_at = pod['metadata']['creationTimestamp']
111
+ pod_status = pod['status']['phase']
112
+ container_ready = pod['status']['containerStatuses'][0]['ready']
113
+ container_status = pod['status']['containerStatuses'][0]['state'].values.first['reason']
114
+ if pod_status == 'Succeeded' || (pod_status == 'Running' && container_ready)
115
+ puts "#{name}: \e[32m#{container_status || pod_status}\e[0m (#{time_ago_in_words created_at})"
116
+ else
117
+ puts "#{name}: \e[31m#{container_status || pod_status}\e[0m (#{time_ago_in_words created_at})"
118
+ end
119
+ end
120
+ end
121
+
122
+ def time_ago_in_words time
123
+ time = Time.parse time unless time.is_a? Time
124
+ seconds = (Time.now - time).round
125
+ case
126
+ when seconds < 60 then "#{seconds}s"
127
+ when seconds < 60*60 then "#{(seconds / 60)}m"
128
+ when seconds < 60*60*24 then "#{(seconds / 60 / 60)}h"
129
+ else "#{(seconds / 60 / 60 / 24)}d"
130
+ end
131
+ end
132
+
133
+ end
134
+ end
@@ -0,0 +1,18 @@
1
+ module Cuber::Commands
2
+ class Logs
3
+ include Cuber::Utils
4
+
5
+ def initialize options
6
+ @options = options
7
+ end
8
+
9
+ def execute
10
+ pod = ARGV.first
11
+ cmd = ['logs']
12
+ cmd += pod ? [pod] : ['-l', "app.kubernetes.io/name=#{@options[:app]}"]
13
+ cmd += ['--all-containers']
14
+ kubectl *cmd
15
+ end
16
+
17
+ end
18
+ end
@@ -0,0 +1,14 @@
1
+ module Cuber::Commands
2
+ class Restart
3
+ include Cuber::Utils
4
+
5
+ def initialize options
6
+ @options = options
7
+ end
8
+
9
+ def execute
10
+ kubectl 'rollout', 'restart', 'deploy'
11
+ end
12
+
13
+ end
14
+ end
@@ -0,0 +1,48 @@
1
+ module Cuber::Commands
2
+ class Run
3
+ include Cuber::Utils
4
+
5
+ def initialize options
6
+ @options = options
7
+ end
8
+
9
+ def execute
10
+ set_current_release
11
+ kubeexec command
12
+ end
13
+
14
+ private
15
+
16
+ def set_current_release
17
+ json = kubeget 'namespace', @options[:app]
18
+ @options[:app] = json['metadata']['labels']['app.kubernetes.io/name']
19
+ @options[:release] = json['metadata']['labels']['app.kubernetes.io/version']
20
+ @options[:image] = json['metadata']['annotations']['image']
21
+ @options[:buildpacks] = json['metadata']['annotations']['buildpacks']
22
+ end
23
+
24
+ def command
25
+ if ARGV.length == 0
26
+ 'sh'
27
+ elsif ARGV.length == 1
28
+ ARGV.first
29
+ else
30
+ ARGV.shelljoin
31
+ end
32
+ end
33
+
34
+ def kubeexec command
35
+ @options[:pod] = "pod-#{command.downcase.gsub(/[^a-z0-9]+/, '-')}-#{Time.now.utc.iso8601.delete('^0-9')}"
36
+ path = ".cuber/kubernetes/#{@options[:pod]}.yml"
37
+ full_command = command.shellsplit
38
+ full_command.unshift 'launcher' if @options[:buildpacks]
39
+ render 'pod.yml', path
40
+ kubectl 'apply', '-f', path
41
+ kubectl 'wait', '--for', 'condition=ready', "pod/#{@options[:pod]}"
42
+ kubectl 'exec', '-it', @options[:pod], '--', *full_command
43
+ kubectl 'delete', 'pod', @options[:pod], '--wait=false'
44
+ File.delete path
45
+ end
46
+
47
+ end
48
+ end
@@ -0,0 +1,13 @@
1
+ module Cuber::Commands
2
+ class Version
3
+
4
+ def initialize options
5
+ @options = options
6
+ end
7
+
8
+ def execute
9
+ puts "Cuber v#{Cuber::VERSION}"
10
+ end
11
+
12
+ end
13
+ end
@@ -0,0 +1,91 @@
1
+ module Cuber
2
+ class CuberfileParser
3
+ def initialize
4
+ @app = nil
5
+ @release = nil
6
+ @repo = nil
7
+ @buildpacks = nil
8
+ @dockerfile = nil
9
+ @image = nil
10
+ @cache = nil
11
+ @dockerconfig = nil
12
+ @kubeconfig = nil
13
+ @migrate = nil
14
+ @procs = {}
15
+ @cron = {}
16
+ @secrets = {}
17
+ @env = {}
18
+ @lb = {}
19
+ @ingress = nil
20
+ @ssl = nil
21
+ end
22
+
23
+ def method_missing m, *args
24
+ abort "Cuberfile: \"#{m}\" is not a command"
25
+ end
26
+
27
+ def app name
28
+ @app = name
29
+ end
30
+
31
+ def release version
32
+ @release = version
33
+ end
34
+
35
+ def repo uri
36
+ @repo = uri
37
+ end
38
+
39
+ def buildpacks builder
40
+ @buildpacks = builder
41
+ end
42
+
43
+ def dockerfile path
44
+ @dockerfile = path
45
+ end
46
+
47
+ def image name
48
+ @image = name
49
+ end
50
+
51
+ def cache enabled
52
+ @cache = enabled
53
+ end
54
+
55
+ def dockerconfig path
56
+ @dockerconfig = path
57
+ end
58
+
59
+ def kubeconfig path
60
+ @kubeconfig = path
61
+ end
62
+
63
+ def migrate cmd, check: nil
64
+ @migrate = { cmd: cmd, check: check }
65
+ end
66
+
67
+ def proc name, cmd, scale: 1, env: {}
68
+ @procs[name] = { cmd: cmd, scale: scale, env: env }
69
+ end
70
+
71
+ def cron name, schedule, cmd
72
+ @cron[name] = { schedule: schedule, cmd: cmd }
73
+ end
74
+
75
+ def env key, value, secret: false
76
+ secret ? (@secrets[key] = value) : (@env[key] = value)
77
+ end
78
+
79
+ def lb key, value
80
+ @lb[key] = value
81
+ end
82
+
83
+ def ingress enabled
84
+ @ingress = enabled
85
+ end
86
+
87
+ def ssl crt, key
88
+ @ssl = { crt: crt, key: key }
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,146 @@
1
+ module Cuber
2
+ class CuberfileValidator
3
+
4
+ def initialize options
5
+ @options = options
6
+ @errors = []
7
+ end
8
+
9
+ def validate
10
+ validate_app
11
+ validate_release
12
+ validate_repo
13
+ validate_buildpacks
14
+ validate_dockerfile
15
+ validate_image
16
+ validate_cache
17
+ validate_dockerconfig
18
+ validate_kubeconfig
19
+ validate_migrate
20
+ validate_procs
21
+ validate_cron
22
+ validate_env
23
+ validate_lb
24
+ validate_ingress
25
+ validate_ssl
26
+ validate_key
27
+ validate_limits
28
+ @errors
29
+ end
30
+
31
+ private
32
+
33
+ def validate_app
34
+ @errors << 'app name must be present' if @options[:app].to_s.strip.empty?
35
+ @errors << 'app name can only include lowercase letters, digits or dashes' if @options[:app] !~ /\A[a-z0-9\-]+\z/
36
+ end
37
+
38
+ def validate_release
39
+ return unless @options[:release]
40
+ @errors << 'release has an invalid format' if @options[:release] !~ /\A[a-zA-Z0-9_\-\.]+\z/
41
+ end
42
+
43
+ def validate_repo
44
+ @errors << 'repo must be present' if @options[:repo].to_s.strip.empty?
45
+ end
46
+
47
+ def validate_buildpacks
48
+ return unless @options[:buildpacks]
49
+ @errors << 'buildpacks is not compatible with the dockerfile option' if @options[:dockerfile]
50
+ end
51
+
52
+ def validate_dockerfile
53
+ return unless @options[:dockerfile]
54
+ @errors << 'dockerfile must be a file' unless File.exists? @options[:dockerfile]
55
+ end
56
+
57
+ def validate_image
58
+ @errors << 'image must be present' if @options[:image].to_s.strip.empty?
59
+ end
60
+
61
+ def validate_cache
62
+ return unless @options[:cache]
63
+ @errors << 'cache must be true or false' if @options[:cache] != true && @options[:cache] != false
64
+ end
65
+
66
+ def validate_dockerconfig
67
+ return unless @options[:dockerconfig]
68
+ @errors << 'dockerconfig must be a file' unless File.exists? @options[:dockerconfig]
69
+ end
70
+
71
+ def validate_kubeconfig
72
+ @errors << 'kubeconfig must be present' if @options[:kubeconfig].to_s.strip.empty?
73
+ @errors << 'kubeconfig must be a file' unless File.exists? @options[:kubeconfig]
74
+ end
75
+
76
+ def validate_migrate
77
+ return unless @options[:migrate]
78
+ @errors << 'migrate command must be present' if @options[:migrate][:cmd].to_s.strip.empty?
79
+ end
80
+
81
+ def validate_procs
82
+ @options[:procs].each do |procname, proc|
83
+ @errors << "proc \"#{procname}\" name can only include lowercase letters" if procname !~ /\A[a-z]+\z/
84
+ @errors << "proc \"#{procname}\" command must be present" if proc[:cmd].to_s.strip.empty?
85
+ @errors << "proc \"#{procname}\" scale must be a positive number" unless proc[:scale].is_a?(Integer) && proc[:scale] > 0
86
+ proc[:env].each do |key, value|
87
+ @errors << "proc \"#{procname}\" env name can only include uppercase letters, digits or underscores" if key !~ /\A[A-Z_]+[A-Z0-9_]*\z/
88
+ end
89
+ end
90
+ end
91
+
92
+ def validate_cron
93
+ @options[:cron].each do |jobname, cron|
94
+ @errors << "cron \"#{jobname}\" name can only include lowercase letters" if jobname !~ /\A[a-z]+\z/
95
+ @errors << "cron \"#{jobname}\" schedule must be present" if cron[:schedule].to_s.strip.empty?
96
+ @errors << "cron \"#{jobname}\" command must be present" if cron[:cmd].to_s.strip.empty?
97
+ end
98
+ end
99
+
100
+ def validate_env
101
+ @options[:env].merge(@options[:secrets]).each do |key, value|
102
+ @errors << "env \"#{key}\" name can only include uppercase letters, digits or underscores" if key !~ /\A[A-Z_]+[A-Z0-9_]*\z/
103
+ end
104
+ end
105
+
106
+ def validate_lb
107
+ @options[:lb].each do |key, value|
108
+ @errors << "lb \"#{key}\" key can only include letters, digits, underscores, dashes, dots or slash" if key !~ /\A[a-zA-Z0-9_\-\.\/]+\z/
109
+ end
110
+ end
111
+
112
+ def validate_ingress
113
+ return unless @options[:ingress]
114
+ @errors << 'ingress must be true or false' if @options[:ingress] != true && @options[:ingress] != false
115
+ end
116
+
117
+ def validate_ssl
118
+ return unless @options[:ssl]
119
+ @errors << 'ssl crt must be a file' unless File.exists? @options[:ssl][:crt]
120
+ @errors << 'ssl key must be a file' unless File.exists? @options[:ssl][:key]
121
+ end
122
+
123
+ def validate_key
124
+ return unless File.exists? File.expand_path '~/.cuber.key'
125
+ token = File.read File.expand_path '~/.cuber.key'
126
+ ecdsa_public = OpenSSL::PKey.read <<~PEM
127
+ -----BEGIN PUBLIC KEY-----
128
+ MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAERAh4uT9yojc06y5wgU6CY6sr0Hrv
129
+ P8AUw6uw2PgUdbm7DKkJwFvQYMj3g+TmrxmPV3KQ8uzegfYRbHr6DyonNQ==
130
+ -----END PUBLIC KEY-----
131
+ PEM
132
+ JWT.decode token, ecdsa_public, true, { iss: 'Cuber', verify_iss: true, algorithm: 'ES256' }
133
+ rescue JWT::DecodeError
134
+ @errors << 'your license key is invalid or expired'
135
+ end
136
+
137
+ def validate_limits
138
+ return if File.exists? File.expand_path '~/.cuber.key'
139
+ scale = @options[:procs].collect { |procname, proc| proc[:scale].to_i }.sum
140
+ @errors << 'please purchase a license key or reduce the number of procs' if scale > 5
141
+ end
142
+
143
+ end
144
+ end
145
+
146
+ Cuber::CuberfileValidator.freeze
@@ -0,0 +1,332 @@
1
+ kind: Namespace
2
+ apiVersion: v1
3
+ metadata:
4
+ name: <%= @options[:app] %>
5
+ labels:
6
+ app.kubernetes.io/name: <%= @options[:app] %>
7
+ app.kubernetes.io/instance: <%= @options[:instance] %>
8
+ app.kubernetes.io/version: <%= @options[:release] %>
9
+ app.kubernetes.io/managed-by: cuber
10
+ annotations:
11
+ image: <%= @options[:image].to_s.to_json %>
12
+ buildpacks: <%= @options[:buildpacks].to_s.to_json %>
13
+ ingress: <%= @options[:ingress].to_s.to_json %>
14
+
15
+ ---
16
+ apiVersion: v1
17
+ kind: Secret
18
+ metadata:
19
+ name: regcred
20
+ namespace: <%= @options[:app] %>
21
+ labels:
22
+ app.kubernetes.io/name: <%= @options[:app] %>
23
+ app.kubernetes.io/instance: <%= @options[:instance] %>
24
+ app.kubernetes.io/version: <%= @options[:release] %>
25
+ app.kubernetes.io/managed-by: cuber
26
+ data:
27
+ .dockerconfigjson: <%= @options[:dockerconfigjson] %>
28
+ type: kubernetes.io/dockerconfigjson
29
+
30
+ ---
31
+ apiVersion: v1
32
+ kind: Secret
33
+ metadata:
34
+ name: app-secrets
35
+ namespace: <%= @options[:app] %>
36
+ labels:
37
+ app.kubernetes.io/name: <%= @options[:app] %>
38
+ app.kubernetes.io/instance: <%= @options[:instance] %>
39
+ app.kubernetes.io/version: <%= @options[:release] %>
40
+ app.kubernetes.io/managed-by: cuber
41
+ data:
42
+ <%- @options[:secrets].each do |key, value| -%>
43
+ <%= key %>: <%= Base64.strict_encode64 value %>
44
+ <%- end -%>
45
+
46
+ ---
47
+ apiVersion: v1
48
+ kind: ConfigMap
49
+ metadata:
50
+ name: env
51
+ namespace: <%= @options[:app] %>
52
+ labels:
53
+ app.kubernetes.io/name: <%= @options[:app] %>
54
+ app.kubernetes.io/instance: <%= @options[:instance] %>
55
+ app.kubernetes.io/version: <%= @options[:release] %>
56
+ app.kubernetes.io/managed-by: cuber
57
+ data:
58
+ <%- @options[:env].each do |key, value| -%>
59
+ <%= key %>: <%= value.to_s.to_json %>
60
+ <%- end -%>
61
+
62
+ <%- if @options[:migrate] -%>
63
+ ---
64
+ apiVersion: batch/v1
65
+ kind: Job
66
+ metadata:
67
+ name: migrate-<%= @options[:instance] %>
68
+ namespace: <%= @options[:app] %>
69
+ labels:
70
+ app.kubernetes.io/name: <%= @options[:app] %>
71
+ app.kubernetes.io/instance: <%= @options[:instance] %>
72
+ app.kubernetes.io/version: <%= @options[:release] %>
73
+ app.kubernetes.io/managed-by: cuber
74
+ spec:
75
+ template:
76
+ metadata:
77
+ labels:
78
+ app.kubernetes.io/name: <%= @options[:app] %>
79
+ app.kubernetes.io/instance: <%= @options[:instance] %>
80
+ app.kubernetes.io/version: <%= @options[:release] %>
81
+ app.kubernetes.io/managed-by: cuber
82
+ spec:
83
+ containers:
84
+ - name: migration
85
+ image: <%= @options[:image] %>:<%= @options[:release] %>
86
+ imagePullPolicy: Always
87
+ <%- if @options[:buildpacks] -%>
88
+ command: ["launcher"]
89
+ args: <%= @options[:migrate][:cmd].shellsplit %>
90
+ <%- else -%>
91
+ command: <%= @options[:migrate][:cmd].shellsplit %>
92
+ <%- end -%>
93
+ envFrom:
94
+ - configMapRef:
95
+ name: env
96
+ - secretRef:
97
+ name: app-secrets
98
+ imagePullSecrets:
99
+ - name: regcred
100
+ restartPolicy: Never
101
+ <%- end -%>
102
+
103
+ <%- @options[:procs].each do |procname, proc| -%>
104
+ ---
105
+ apiVersion: apps/v1
106
+ kind: Deployment
107
+ metadata:
108
+ name: <%= procname %>
109
+ namespace: <%= @options[:app] %>
110
+ labels:
111
+ app.kubernetes.io/name: <%= @options[:app] %>
112
+ app.kubernetes.io/instance: <%= @options[:instance] %>
113
+ app.kubernetes.io/version: <%= @options[:release] %>
114
+ app.kubernetes.io/managed-by: cuber
115
+ spec:
116
+ revisionHistoryLimit: 0
117
+ replicas: <%= proc[:scale] %>
118
+ selector:
119
+ matchLabels:
120
+ app: <%= procname %>-proc
121
+ template:
122
+ metadata:
123
+ labels:
124
+ app.kubernetes.io/name: <%= @options[:app] %>
125
+ app.kubernetes.io/instance: <%= @options[:instance] %>
126
+ app.kubernetes.io/version: <%= @options[:release] %>
127
+ app.kubernetes.io/managed-by: cuber
128
+ app: <%= procname %>-proc
129
+ spec:
130
+ containers:
131
+ - name: <%= procname %>-proc
132
+ image: <%= @options[:image] %>:<%= @options[:release] %>
133
+ imagePullPolicy: Always
134
+ <%- if @options[:buildpacks] -%>
135
+ command: ["launcher"]
136
+ args: <%= proc[:cmd].shellsplit %>
137
+ <%- else -%>
138
+ command: <%= proc[:cmd].shellsplit %>
139
+ <%- end -%>
140
+ envFrom:
141
+ - configMapRef:
142
+ name: env
143
+ - secretRef:
144
+ name: app-secrets
145
+ env:
146
+ <%- proc[:env].each do |key, value| -%>
147
+ - name: <%= key %>
148
+ value: <%= value.to_s.to_json %>
149
+ <%- end -%>
150
+ <%- if procname.to_s == 'web' -%>
151
+ - name: PORT
152
+ value: "8080"
153
+ ports:
154
+ - containerPort: 8080
155
+ readinessProbe:
156
+ httpGet:
157
+ path: /
158
+ port: 8080
159
+ <%- end -%>
160
+ <%- if @options[:migrate] && @options[:migrate][:check] -%>
161
+ initContainers:
162
+ - name: migration-check
163
+ image: <%= @options[:image] %>:<%= @options[:release] %>
164
+ imagePullPolicy: Always
165
+ <%- if @options[:buildpacks] -%>
166
+ command: ["launcher"]
167
+ args: <%= @options[:migrate][:check].shellsplit %>
168
+ <%- else -%>
169
+ command: <%= @options[:migrate][:check].shellsplit %>
170
+ <%- end -%>
171
+ envFrom:
172
+ - configMapRef:
173
+ name: env
174
+ - secretRef:
175
+ name: app-secrets
176
+ <%- end -%>
177
+ imagePullSecrets:
178
+ - name: regcred
179
+ <%- end -%>
180
+
181
+ <%- @options[:cron].each do |jobname, cron| -%>
182
+ ---
183
+ apiVersion: batch/v1
184
+ kind: CronJob
185
+ metadata:
186
+ name: cron-<%= jobname %>
187
+ namespace: <%= @options[:app] %>
188
+ labels:
189
+ app.kubernetes.io/name: <%= @options[:app] %>
190
+ app.kubernetes.io/instance: <%= @options[:instance] %>
191
+ app.kubernetes.io/version: <%= @options[:release] %>
192
+ app.kubernetes.io/managed-by: cuber
193
+ spec:
194
+ schedule: <%= cron[:schedule].to_s.to_json %>
195
+ concurrencyPolicy: Forbid
196
+ successfulJobsHistoryLimit: 1
197
+ failedJobsHistoryLimit: 1
198
+ jobTemplate:
199
+ metadata:
200
+ labels:
201
+ app.kubernetes.io/name: <%= @options[:app] %>
202
+ app.kubernetes.io/instance: <%= @options[:instance] %>
203
+ app.kubernetes.io/version: <%= @options[:release] %>
204
+ app.kubernetes.io/managed-by: cuber
205
+ spec:
206
+ backoffLimit: 0
207
+ template:
208
+ metadata:
209
+ labels:
210
+ app.kubernetes.io/name: <%= @options[:app] %>
211
+ app.kubernetes.io/instance: <%= @options[:instance] %>
212
+ app.kubernetes.io/version: <%= @options[:release] %>
213
+ app.kubernetes.io/managed-by: cuber
214
+ spec:
215
+ containers:
216
+ - name: task
217
+ image: <%= @options[:image] %>:<%= @options[:release] %>
218
+ imagePullPolicy: Always
219
+ <%- if @options[:buildpacks] -%>
220
+ command: ["launcher"]
221
+ args: <%= cron[:cmd].shellsplit %>
222
+ <%- else -%>
223
+ command: <%= cron[:cmd].shellsplit %>
224
+ <%- end -%>
225
+ envFrom:
226
+ - configMapRef:
227
+ name: env
228
+ - secretRef:
229
+ name: app-secrets
230
+ imagePullSecrets:
231
+ - name: regcred
232
+ restartPolicy: Never
233
+ <%- end -%>
234
+
235
+ <%- if @options[:ssl] -%>
236
+ ---
237
+ apiVersion: v1
238
+ kind: Secret
239
+ metadata:
240
+ name: ssl
241
+ namespace: <%= @options[:app] %>
242
+ labels:
243
+ app.kubernetes.io/name: <%= @options[:app] %>
244
+ app.kubernetes.io/instance: <%= @options[:instance] %>
245
+ app.kubernetes.io/version: <%= @options[:release] %>
246
+ app.kubernetes.io/managed-by: cuber
247
+ data:
248
+ tls.crt: <%= Base64.strict_encode64 File.read @options[:ssl][:crt] %>
249
+ tls.key: <%= Base64.strict_encode64 File.read @options[:ssl][:key] %>
250
+ type: kubernetes.io/tls
251
+ <%- end -%>
252
+
253
+ <%- if @options[:ingress] -%>
254
+ ---
255
+ apiVersion: v1
256
+ kind: Service
257
+ metadata:
258
+ name: web-service
259
+ namespace: <%= @options[:app] %>
260
+ labels:
261
+ app.kubernetes.io/name: <%= @options[:app] %>
262
+ app.kubernetes.io/instance: <%= @options[:instance] %>
263
+ app.kubernetes.io/version: <%= @options[:release] %>
264
+ app.kubernetes.io/managed-by: cuber
265
+ spec:
266
+ selector:
267
+ app: web-proc
268
+ ports:
269
+ - protocol: TCP
270
+ port: 80
271
+ targetPort: 8080
272
+
273
+ ---
274
+ apiVersion: networking.k8s.io/v1
275
+ kind: Ingress
276
+ metadata:
277
+ name: web-ingress
278
+ namespace: <%= @options[:app] %>
279
+ labels:
280
+ app.kubernetes.io/name: <%= @options[:app] %>
281
+ app.kubernetes.io/instance: <%= @options[:instance] %>
282
+ app.kubernetes.io/version: <%= @options[:release] %>
283
+ app.kubernetes.io/managed-by: cuber
284
+ annotations:
285
+ <%- @options[:lb].each do |key, value| -%>
286
+ <%= key %>: <%= value.to_s.to_json %>
287
+ <%- end -%>
288
+ spec:
289
+ <%- if @options[:ssl] -%>
290
+ tls:
291
+ - secretName: ssl
292
+ <%- end -%>
293
+ rules:
294
+ - http:
295
+ paths:
296
+ - path: /
297
+ pathType: Prefix
298
+ backend:
299
+ service:
300
+ name: web-service
301
+ port:
302
+ number: 80
303
+ <%- else -%>
304
+ ---
305
+ apiVersion: v1
306
+ kind: Service
307
+ metadata:
308
+ name: load-balancer
309
+ namespace: <%= @options[:app] %>
310
+ labels:
311
+ app.kubernetes.io/name: <%= @options[:app] %>
312
+ app.kubernetes.io/instance: <%= @options[:instance] %>
313
+ app.kubernetes.io/version: <%= @options[:release] %>
314
+ app.kubernetes.io/managed-by: cuber
315
+ annotations:
316
+ <%- @options[:lb].each do |key, value| -%>
317
+ <%= key %>: <%= value.to_s.to_json %>
318
+ <%- end -%>
319
+ spec:
320
+ type: LoadBalancer
321
+ selector:
322
+ app: web-proc
323
+ ports:
324
+ - name: http
325
+ protocol: TCP
326
+ port: 80
327
+ targetPort: 8080
328
+ - name: https
329
+ protocol: TCP
330
+ port: 443
331
+ targetPort: 8080
332
+ <%- end -%>
@@ -0,0 +1,22 @@
1
+ apiVersion: v1
2
+ kind: Pod
3
+ metadata:
4
+ name: <%= @options[:pod] %>
5
+ namespace: <%= @options[:app] %>
6
+ labels:
7
+ app.kubernetes.io/name: <%= @options[:app] %>
8
+ app.kubernetes.io/version: <%= @options[:release] %>
9
+ app.kubernetes.io/managed-by: cuber
10
+ spec:
11
+ containers:
12
+ - name: pod-proc
13
+ image: <%= @options[:image] %>:<%= @options[:release] %>
14
+ imagePullPolicy: Always
15
+ command: ["sleep", "infinity"]
16
+ envFrom:
17
+ - configMapRef:
18
+ name: env
19
+ - secretRef:
20
+ name: app-secrets
21
+ imagePullSecrets:
22
+ - name: regcred
@@ -0,0 +1,24 @@
1
+ module Cuber::Utils
2
+
3
+ def kubectl *args
4
+ cmd = ['kubectl', '--kubeconfig', @options[:kubeconfig], '-n', @options[:app]] + args
5
+ system(*cmd) || abort("Cuber: \"#{cmd.shelljoin}\" failed")
6
+ end
7
+
8
+ def kubeget type, name = nil, *args
9
+ cmd = ['kubectl', 'get', type, name, '-o', 'json', '--kubeconfig', @options[:kubeconfig], '-n', @options[:app], *args].compact
10
+ out, status = Open3.capture2 *cmd
11
+ abort "Cuber: \"#{cmd.shelljoin}\" failed" unless status.success?
12
+ out.empty? ? nil : JSON.parse(out)
13
+ end
14
+
15
+ def render template, target_file = nil
16
+ template = File.join __dir__, 'templates', "#{template}.erb"
17
+ renderer = ERB.new File.read(template), trim_mode: '-'
18
+ content = renderer.result binding
19
+ return content unless target_file
20
+ FileUtils.mkdir_p File.dirname target_file
21
+ File.write target_file, content
22
+ end
23
+
24
+ end
@@ -0,0 +1,3 @@
1
+ module Cuber
2
+ VERSION = '1.1.0'.freeze
3
+ end
data/lib/cuber.rb ADDED
@@ -0,0 +1,15 @@
1
+ require 'cuber/version'
2
+ require 'cuber/cli'
3
+ require 'cuber/cuberfile_parser'
4
+ require 'cuber/cuberfile_validator'
5
+ require 'cuber/utils'
6
+ require 'cuber/commands/version'
7
+ require 'cuber/commands/info'
8
+ require 'cuber/commands/logs'
9
+ require 'cuber/commands/restart'
10
+ require 'cuber/commands/run'
11
+ require 'cuber/commands/deploy'
12
+
13
+ module Cuber
14
+
15
+ end
metadata CHANGED
@@ -1,23 +1,57 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cuber
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.0
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Cuber
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-10-26 00:00:00.000000000 Z
12
- dependencies: []
11
+ date: 2022-02-19 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: jwt
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'
13
27
  description:
14
28
  email:
15
- executables: []
29
+ executables:
30
+ - cuber
16
31
  extensions: []
17
32
  extra_rdoc_files: []
18
- files: []
19
- homepage:
20
- licenses: []
33
+ files:
34
+ - LICENSE.md
35
+ - README.md
36
+ - bin/cuber
37
+ - cuber.gemspec
38
+ - lib/cuber.rb
39
+ - lib/cuber/cli.rb
40
+ - lib/cuber/commands/deploy.rb
41
+ - lib/cuber/commands/info.rb
42
+ - lib/cuber/commands/logs.rb
43
+ - lib/cuber/commands/restart.rb
44
+ - lib/cuber/commands/run.rb
45
+ - lib/cuber/commands/version.rb
46
+ - lib/cuber/cuberfile_parser.rb
47
+ - lib/cuber/cuberfile_validator.rb
48
+ - lib/cuber/templates/deployment.yml.erb
49
+ - lib/cuber/templates/pod.yml.erb
50
+ - lib/cuber/utils.rb
51
+ - lib/cuber/version.rb
52
+ homepage: https://cuber.cloud
53
+ licenses:
54
+ - LicenseRef-LICENSE.md
21
55
  metadata: {}
22
56
  post_install_message:
23
57
  rdoc_options: []
@@ -34,8 +68,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
34
68
  - !ruby/object:Gem::Version
35
69
  version: '0'
36
70
  requirements: []
37
- rubygems_version: 3.2.22
71
+ rubygems_version: 3.2.32
38
72
  signing_key:
39
73
  specification_version: 4
40
- summary: Cuber
74
+ summary: Deploy your apps on Kubernetes easily.
41
75
  test_files: []