cuber 0.0.0 → 1.2.1

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: 26ed60418289b2d4b6e39d87691661c9c5d9c57cac883d4d21a224d7e7fc7271
4
+ data.tar.gz: c4093343c2fac80c5adabba7cfe285e60e30fc722d4dbb01e24df840144bbbbf
5
5
  SHA512:
6
- metadata.gz: 22bf3a47e78f9a9cc71b04fa3a3cf3abb2672014598fa67cf2186f4943db5d67ccbd47c89c256ae8ddc14386efc8405842acdea2fecda0c8be82f765e937429a
7
- data.tar.gz: ac57faaf701389648745991c4dac3eaa580c7c4e609b2141e9b07b0ba55a75593794c6faa27d0242839accd2b541c296b4ebc872da7f6910400c099a82dba4f3
6
+ metadata.gz: 168df66b846819cda138affc0bdbbd95185815367aeaadc210ef215404b514239ed812ed2f18885255c9909a1a3345e4819b96709fd2387e5586c6037025327c
7
+ data.tar.gz: ea4fffbab3fd2c3ba50d2ff587a029472ea826fad9deb33c95190b54c331ca97ea1f1cbafb1e4fc5c4627540bcfc256c6b00e31286087879df366701ca76df41
data/README.md ADDED
@@ -0,0 +1,86 @@
1
+ <a href="https://cuber.cloud"><img src="https://cuber.cloud/assets/images/logo.svg" alt="Cuber" height="80" width="80"></a>
2
+
3
+ # CUBER
4
+
5
+ [![Gem Version](https://badge.fury.io/rb/cuber.svg)](https://badge.fury.io/rb/cuber)
6
+
7
+ Deploy your apps on Kubernetes easily.
8
+
9
+ ## What is Cuber?
10
+
11
+ Cuber is an automation tool (written in Ruby) that can package and deploy your apps (written in any language and framework) on Kubernetes.
12
+
13
+ Unlike other tools that add more options and more complexity to Kubernetes, Cuber is made to simplify and reduce the complexity.
14
+ You just need to create a `Cuberfile`, with ~10 lines of code, and then type `cuber deploy` to package and deploy your app on any Kubernetes cluster.
15
+
16
+ Kubernetes is up to 80% cheaper compared to PaaS like Heroku and you can choose between different cloud providers (no lock-in).
17
+ It is also reliable and it can scale enterprise applications at any size.
18
+ The only downside is that it's difficult to master...
19
+ Cuber makes Kubernetes simple!
20
+ In this way you have the simplicity of a PaaS, at the cost of bare infrastructure and without the additional cost of a DevOps team.
21
+
22
+ [Read more](https://cuber.cloud/docs/overview)
23
+
24
+ ## Installation
25
+
26
+ First you need to [install the prerequisites](https://cuber.cloud/docs/installation): `ruby`, `git`, `docker`, `pack`, `kubectl`.
27
+
28
+ Then install Cuber:
29
+
30
+ ```
31
+ $ gem install cuber
32
+ ```
33
+
34
+ ## Quickstart
35
+
36
+ Open your application folder and create a `Cuberfile`, for example:
37
+
38
+ ```ruby
39
+ # Give a name to your app
40
+ app 'myapp'
41
+
42
+ # Get the code from this Git repository
43
+ repo '.'
44
+
45
+ # Build the Docker image automatically (or provide a Dockerfile)
46
+ buildpacks 'heroku/buildpacks:20'
47
+
48
+ # Publish the Docker image in a registry
49
+ image 'username/myapp'
50
+
51
+ # Connect to this Kubernetes cluster
52
+ kubeconfig 'path/to/kubeconfig.yml'
53
+
54
+ # Run and scale any command on Kubernetes
55
+ proc :web, 'your web server command'
56
+ ```
57
+
58
+ You can also see [a more complete example](https://cuber.cloud/docs/quickstart).
59
+
60
+ Then in your terminal:
61
+
62
+ ```
63
+ $ cuber deploy
64
+ ```
65
+
66
+ Finally you can also monitor the status of your application:
67
+
68
+ ```
69
+ $ cuber info
70
+ ```
71
+
72
+ Check out the [Cuberfile configuration](https://cuber.cloud/docs/cuberfile) and the [Cuber CLI commands](https://cuber.cloud/docs/cli) for more information.
73
+
74
+ ## License
75
+
76
+ Cuber is released under a source-available license:
77
+ [Standard Source Available License (SSAL)](https://github.com/collimarco/Standard-Source-Available-License)
78
+
79
+ Cuber is completely free up to 5 procs per app (and you can publish unlimited apps). If you are a large customer, and you need more procs to scale your applications, please [purchase a license](https://cuber.cloud/buy) (it also includes dedicated support).
80
+
81
+ Contributions are welcome: you can fork this project on GitHub and submit a pull request. If you submit a change / fix, we can use it without restrictions and you transfer the copyright of your contribution to Cuber.
82
+
83
+ ## Learn more
84
+
85
+ You can find more information and documentation on [cuber.cloud →](https://cuber.cloud)
86
+
data/SSAL-LICENSE ADDED
@@ -0,0 +1,9 @@
1
+ Copyright 2022 Cuber (AbstractBrain srls)
2
+
3
+ Use and modifications are permitted under the following conditions:
4
+
5
+ 1. The above copyright notice and this permission must be retained in all copies.
6
+ 2. The modifications to the software must not circumvent license key validations or other license-related code.
7
+
8
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED.
9
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY DAMAGES OR OTHER LIABILITY.
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-SSAL-LICENSE'
10
+ s.executables = ['cuber']
11
+ s.files = `git ls-files`.split("\n")
12
+ s.add_dependency 'jwt', '~> 2.3.0'
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,101 @@
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
+ cmd = ['git', 'clone']
40
+ cmd += ['--branch', @options[:repo][:branch]] if @options[:repo][:branch]
41
+ cmd += ['--depth', '1', @options[:repo][:url], path]
42
+ system(*cmd) || abort('Cuber: git clone failed')
43
+ end
44
+
45
+ def commit_hash
46
+ out, status = Open3.capture2 'git', 'rev-parse', '--short', 'HEAD', chdir: '.cuber/repo'
47
+ abort 'Cuber: cannot get commit hash' unless status.success?
48
+ out.strip
49
+ end
50
+
51
+ def set_release_name
52
+ @options[:release] = "#{commit_hash}-#{Time.now.utc.iso8601.delete('^0-9')}"
53
+ end
54
+
55
+ def pack
56
+ print_step 'Building image using buildpacks'
57
+ tag = "#{@options[:image]}:#{@options[:release]}"
58
+ cmd = ['pack', 'build', tag, '--builder', @options[:buildpacks], '--publish']
59
+ cmd += ['--pull-policy', 'always', '--clear-cache'] if @options[:cache] == false
60
+ system(*cmd, chdir: '.cuber/repo') || abort('Cuber: pack build failed')
61
+ end
62
+
63
+ def build
64
+ print_step 'Building image from Dockerfile'
65
+ dockerfile = @options[:dockerfile] || 'Dockerfile'
66
+ tag = "#{@options[:image]}:#{@options[:release]}"
67
+ cmd = ['docker', 'build']
68
+ cmd += ['--pull', '--no-cache'] if @options[:cache] == false
69
+ cmd += ['--platform', 'linux/amd64', '--progress', 'plain', '-f', dockerfile, '-t', tag, '.']
70
+ system(*cmd, chdir: '.cuber/repo') || abort('Cuber: docker build failed')
71
+ end
72
+
73
+ def push
74
+ print_step 'Pushing image to Docker registry'
75
+ tag = "#{@options[:image]}:#{@options[:release]}"
76
+ system('docker', 'push', tag) || abort('Cuber: docker push failed')
77
+ end
78
+
79
+ def configure
80
+ print_step 'Generating Kubernetes configuration'
81
+ @options[:instance] = "#{@options[:app]}-#{Time.now.utc.iso8601.delete('^0-9')}"
82
+ @options[:dockerconfigjson] = Base64.strict_encode64 File.read File.expand_path(@options[:dockerconfig] || '~/.docker/config.json')
83
+ render 'deployment.yml', '.cuber/kubernetes/deployment.yml'
84
+ end
85
+
86
+ def apply
87
+ print_step 'Applying configuration to Kubernetes cluster'
88
+ kubectl 'apply',
89
+ '-f', '.cuber/kubernetes/deployment.yml',
90
+ '--prune', '-l', "app.kubernetes.io/name=#{@options[:app]},app.kubernetes.io/managed-by=cuber"
91
+ end
92
+
93
+ def rollout
94
+ print_step 'Verifying deployment status'
95
+ @options[:procs].each_key do |procname|
96
+ kubectl 'rollout', 'status', "deployment/#{procname}"
97
+ end
98
+ end
99
+
100
+ end
101
+ 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' unless @options[:buildpacks].to_s.strip.empty?
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 url, branch: nil
36
+ @repo = { url: url, branch: branch }
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].nil? || @options[:repo][:url].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.2.1'.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.2.1
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-06-08 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: 2.3.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 2.3.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
+ - README.md
35
+ - SSAL-LICENSE
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-SSAL-LICENSE
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.33
38
72
  signing_key:
39
73
  specification_version: 4
40
- summary: Cuber
74
+ summary: Deploy your apps on Kubernetes easily.
41
75
  test_files: []