cuber 0.0.0 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: []