rascal 0.1.0 → 0.2.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: 814d874743694d11422907bf02213100735789bb30c4f6a04d5bf57d3a686541
4
- data.tar.gz: 77a43969306a5d45e84a8f7a4e3518604b09989701afc4829188ae6385ef464d
3
+ metadata.gz: 5cac8b4564154cac2c8e002da1cb8e2874509331ad381cb6b8cc60b196954ab0
4
+ data.tar.gz: a577bd8374a055453a3db8a73bf211d69973fcab2fe74a3117aadd56149e644a
5
5
  SHA512:
6
- metadata.gz: f076c894c0bebe868de8f56764ce47d2bd54ed8052550aa4595c48238515db324f5582f0f0f612976b81e06f99597bd76d303f8b2171ada400b221280c7ca4ae
7
- data.tar.gz: e2ac5cf785f08ab3f1adfafde4c2bd8e6e0a2bb72ebb740b279d584379341915f4e595c5b987a1b8ecb00236cc7f50321e449074c83223cb62a3618ce6d88c7a
6
+ metadata.gz: 7d0770a88072f36e7848b0fb46ef11599e32759d3c1e17b0de8eebb2fd298b32a97f7aedc9ea191e4d996bddc27d12b313457a5adba7905465b03e68fd11fa87
7
+ data.tar.gz: 2113fab40ec414ce8d724a7cf34ef94f12246f7f93d4a6509b32292c6757b96e2993286c69ba53a155bb7a1485ab5fe3a39b5d41abcd40d9763f4c7b44b0c25b
@@ -5,6 +5,13 @@ All notable changes to this project will be documented here.
5
5
  Rascal follows semantic versioning. This has little consequence pre 1.0, so expect breaking changes.
6
6
 
7
7
 
8
+ ## 0.2.0 (2019-04-10)
9
+
10
+ - Add `--all` flag for `rascal clean`.
11
+ - Add `rascal update` command.
12
+ - Allow rascal specific per job config.
13
+
14
+
8
15
  ## 0.1.0 (2019-03-21)
9
16
 
10
17
  initial release
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- rascal (0.1.0)
4
+ rascal (0.2.0)
5
5
  thor (~> 0.20.3)
6
6
 
7
7
  GEM
data/README.md CHANGED
@@ -30,14 +30,18 @@ You need to add some extra information to your `.gitlab-ci.yml`. A working versi
30
30
 
31
31
  ```
32
32
  # settings here override job settings
33
- .rascal:
34
- repo_dir: /repo
35
- variables:
33
+ .rascal: # add rascal specific config here
34
+ repo_dir: /repo # /repo is the default
35
+ variables: # extra env variables
36
36
  BUNDLE_PATH: /cache/bundle
37
- volumes:
38
- cache: /cache
37
+ volumes: # mount these volumes
38
+ cache: /cache # we will always mount a /builds volume
39
39
  before_shell:
40
- - bundle check
40
+ - bundle check # run this when starting a shell
41
+ jobs:
42
+ rspec: # override settings for a specific job
43
+ variables:
44
+ BUNDLE_GEMFILE = /repo/Gemfile
41
45
 
42
46
  .environment: &environment
43
47
  image: registry.makandra.de/makandra/ci-images/test-env:2.5
@@ -53,7 +57,6 @@ You need to add some extra information to your `.gitlab-ci.yml`. A working versi
53
57
  variables:
54
58
  BUNDLE_PATH: ./bundle/vendor
55
59
  DATABASE_URL: postgresql://pg_user@pg-db/test-db
56
- DATABASE_CLEANER_ALLOW_REMOTE_DATABASE_URL: "true"
57
60
  REDIS_URL: redis://redis
58
61
  PROMPT: CI env
59
62
  cache:
@@ -73,6 +76,27 @@ rspec:
73
76
  Then, in your project root, run `rascal shell rspec`.
74
77
 
75
78
 
79
+ ### Commands
80
+
81
+ #### rascal shell <job>
82
+
83
+ Start a docker container (plus required services) and open an interactive shell.
84
+
85
+ Currently requires a "bash" to exist.
86
+
87
+
88
+ #### rascal clean <job> | --all [--volumes]
89
+
90
+ Stop and remove all created containers, services, networks for either the given or all jobs.
91
+
92
+ If `--volumes` is given, also remove all cached volumes.
93
+
94
+
95
+ #### rascal update <job> | --all
96
+
97
+ Update all images for the given job, or all jobs.
98
+
99
+
76
100
  ## License
77
101
 
78
102
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -2,9 +2,10 @@ require 'thor'
2
2
 
3
3
  module Rascal
4
4
  module CLI
5
- autoload :Base, 'rascal/cli/base'
6
- autoload :Clean, 'rascal/cli/clean'
7
- autoload :Main, 'rascal/cli/main'
8
- autoload :Shell, 'rascal/cli/shell'
5
+ autoload :Base, 'rascal/cli/base'
6
+ autoload :Clean, 'rascal/cli/clean'
7
+ autoload :Main, 'rascal/cli/main'
8
+ autoload :Shell, 'rascal/cli/shell'
9
+ autoload :Update, 'rascal/cli/update'
9
10
  end
10
11
  end
@@ -19,19 +19,38 @@ module Rascal
19
19
  end
20
20
 
21
21
  def find_environment(environment_name)
22
- if (definition = EnvironmentsDefinition.detect(config_location))
23
- if (environment = definition.environment(environment_name))
24
- return environment
22
+ definition = environment_definition
23
+ if (environment = definition.environment(environment_name))
24
+ return environment
25
+ else
26
+ available_environments = definition.available_environment_names.join(', ')
27
+ if environment_name
28
+ fail_with_error("Unknown environment #{environment_name}. Available: #{available_environments}.")
25
29
  else
26
- available_environments = definition.available_environment_names.join(', ')
27
- if environment_name
28
- fail_with_error("Unknown environment #{environment_name}. Available: #{available_environments}.")
29
- else
30
- fail_with_error("Missing environment. Available: #{available_environments}.")
31
- end
30
+ fail_with_error("Missing environment. Available: #{available_environments}.")
31
+ end
32
+ end
33
+ end
34
+
35
+ def each_environment(name, &block)
36
+ return enum_for(:each_environment) unless block_given?
37
+
38
+ if name == :all
39
+ definition = environment_definition
40
+ definition.available_environment_names.each do |environment_name|
41
+ yield definition.environment(environment_name)
32
42
  end
43
+ else
44
+ yield find_environment(name)
45
+ end
46
+ end
47
+
48
+ def environment_definition
49
+ if (definition = EnvironmentsDefinition.detect(config_location))
50
+ definition
33
51
  else
34
52
  fail_with_error("Could not find an environment definition in current working directory.")
53
+ nil
35
54
  end
36
55
  end
37
56
  end
@@ -4,12 +4,19 @@ module Rascal
4
4
  module CLI
5
5
  class Clean < Base
6
6
  def initialize(thor, options, environment_name)
7
- @environment_name = environment_name
7
+ @environment_name = if options[:all]
8
+ fail_with_error('Cannot give --all and an environment name!') if environment_name
9
+ :all
10
+ else
11
+ environment_name
12
+ end
8
13
  super(thor, options)
9
14
  end
10
15
 
11
16
  def run
12
- find_environment(@environment_name)&.clean
17
+ each_environment(@environment_name) do |environment|
18
+ environment.clean(clean_volumes: @options[:volumes])
19
+ end
13
20
  end
14
21
  end
15
22
  end
@@ -31,15 +31,37 @@ module Rascal
31
31
  map "shell" => "_shell"
32
32
  desc 'shell ENVIRONMENT', 'Start a docker shell for the given environment'
33
33
  def _shell(environment_name = nil)
34
- Shell.new(self, options, environment_name).run
34
+ handle_error do
35
+ Shell.new(self, options, environment_name).run
36
+ end
35
37
  end
36
38
 
37
39
  desc 'clean ENVIRONMENT', 'Stop and remove docker containers for the given environment'
40
+ method_option :volumes, type: :boolean, default: false, desc: 'Remove (cache) volumes'
41
+ method_option :all, type: :boolean, default: false, desc: 'Clean all environments'
38
42
  def clean(environment_name = nil)
39
- Clean.new(self, options, environment_name).run
43
+ handle_error do
44
+ Clean.new(self, options, environment_name).run
45
+ end
46
+ end
47
+
48
+ desc 'update ENVIRONMENT', 'Update all docker images'
49
+ method_option :all, type: :boolean, default: false, desc: 'update all available environments'
50
+ def update(environment_name = nil)
51
+ handle_error do
52
+ Update.new(self, options, environment_name).run
53
+ end
40
54
  end
41
55
 
42
- class_option :config_file, aliases: ['-c'], default: '.', required: true, banner: 'path to configuration file or directory containing it'
56
+ class_option :config_file, aliases: ['-c'], default: '.', required: true, desc: 'path to configuration file or directory containing it'
57
+
58
+ private
59
+
60
+ def handle_error
61
+ yield
62
+ rescue Rascal::Error => e
63
+ raise Thor::Error, e.message
64
+ end
43
65
  end
44
66
  end
45
67
  end
@@ -0,0 +1,24 @@
1
+ require 'rascal'
2
+
3
+ module Rascal
4
+ module CLI
5
+ class Update < Base
6
+ def initialize(thor, options, environment_name)
7
+ @environment_name = if options[:all]
8
+ fail_with_error('Cannot give --all and an environment name!') if environment_name
9
+ :all
10
+ else
11
+ environment_name
12
+ end
13
+ super(thor, options)
14
+ end
15
+
16
+ def run
17
+ images = []
18
+ each_environment(@environment_name) do |environment|
19
+ images += environment.update(skip: images)
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -3,6 +3,8 @@ module Rascal
3
3
  class Container
4
4
  include IOHelper
5
5
 
6
+ attr_reader :image
7
+
6
8
  def initialize(name, image)
7
9
  @name = name
8
10
  @prefixed_name = "#{NAME_PREFIX}#{name}"
@@ -26,7 +28,7 @@ module Rascal
26
28
  'container',
27
29
  'inspect',
28
30
  id,
29
- output: :json
31
+ output: :json,
30
32
  ).first
31
33
  !!container_info.dig('State', 'Running')
32
34
  else
@@ -56,26 +58,55 @@ module Rascal
56
58
  *(['--network', network.id] if network),
57
59
  *(['--network-alias', network_alias] if network_alias),
58
60
  @image,
59
- output: :id
61
+ output: :id,
60
62
  )
61
63
  end
62
64
 
63
65
  def run_and_attach(*command, env: {}, network: nil, volumes: [], working_dir: nil, allow_failure: false)
64
- Docker.interface.run_and_attach(@image, *command,
65
- env: env,
66
- stdout: stdout,
67
- stderr: stderr,
68
- stdin: stdin,
69
- network: network&.id,
70
- volumes: volumes,
71
- working_dir: working_dir,
72
- allow_failure: allow_failure
66
+ Docker.interface.run_and_attach(
67
+ 'container',
68
+ 'run',
69
+ '--rm',
70
+ '-a', 'STDOUT',
71
+ '-a', 'STDERR',
72
+ '-a', 'STDIN',
73
+ '--interactive',
74
+ '--tty',
75
+ *(['-w', working_dir] if working_dir),
76
+ *(volumes.flat_map { |v| ['-v', v.to_param] }),
77
+ *(env.flat_map { |key, value| ['-e', "#{key}=#{value}"] }),
78
+ *(['--network', network.id] if network),
79
+ @image,
80
+ *command,
81
+ redirect_io: {
82
+ out: stdout,
83
+ err: stderr,
84
+ in: stdin,
85
+ },
86
+ allow_failure: allow_failure,
73
87
  )
74
88
  end
75
89
 
76
90
  def clean
77
- stop_container if running?
78
- remove_container if exists?
91
+ if running?
92
+ say "Stopping container for #{@name}"
93
+ stop_container
94
+ end
95
+ if exists?
96
+ say "Removing container for #{@name}"
97
+ remove_container
98
+ end
99
+ end
100
+
101
+ def update(skip: [])
102
+ return if skip.include?(@image)
103
+ say "Updating image #{@image}"
104
+ Docker.interface.run(
105
+ 'pull',
106
+ @image,
107
+ stdout: stdout,
108
+ )
109
+ @image
79
110
  end
80
111
 
81
112
  private
@@ -87,7 +118,7 @@ module Rascal
87
118
  '--all',
88
119
  '--quiet',
89
120
  '--filter', "name=^/#{@prefixed_name}$",
90
- output: :id
121
+ output: :id,
91
122
  )
92
123
  end
93
124
 
@@ -6,7 +6,7 @@ module Rascal
6
6
  class Interface
7
7
  class Error < Rascal::Error; end
8
8
 
9
- def run(*command, output: :ignore, stdout: nil, redirect_io: {}, allow_failure: false)
9
+ def run(*command, output: :ignore, stdout: nil, allow_failure: false)
10
10
  save_stdout = ''
11
11
  save_stderr = ''
12
12
  exit_status = nil
@@ -22,60 +22,11 @@ module Rascal
22
22
  unless allow_failure || exit_status.success?
23
23
  raise Error, "docker command '#{command.join(' ')}' failed with error:\n#{save_stderr}"
24
24
  end
25
- case output
26
- when :json
27
- begin
28
- JSON.parse(save_stdout)
29
- rescue JSON::ParserError
30
- raise Error, "could not parse output of docker command '#{command.join(' ')}':\n#{save_stdout}"
31
- end
32
- when :id
33
- save_stdout[/[0-9a-f]+/]
34
- when :ignore
35
- nil
36
- else
37
- raise ArgumentError, 'unknown option for :output'
38
- end
25
+ parse_output(output, save_stdout, save_stderr, command: command.join(' '))
39
26
  end
40
27
 
41
- def run_and_attach(image, *command, stdout: nil, stderr: nil, stdin: nil, env: {}, network: nil, volumes: [], working_dir: nil, allow_failure: false)
42
- process_redirections = {}
43
- args = []
44
- if stdout
45
- process_redirections[:out] = stdout
46
- args += ['-a', 'STDOUT']
47
- end
48
- if stderr
49
- process_redirections[:err] = stderr
50
- args += ['-a', 'STDERR']
51
- end
52
- if stdin
53
- process_redirections[:in] = stdin
54
- args += ['-a', 'STDIN', '--interactive', '--tty']
55
- end
56
- if working_dir
57
- args += ['-w', working_dir.to_s]
58
- end
59
- volumes.each do |volume|
60
- args += ['-v', volume.to_param]
61
- end
62
- env.each do |key, value|
63
- args += ['-e', "#{key}=#{value}"]
64
- end
65
- if network
66
- args += ['--network', network.to_s]
67
- end
68
- exit_status = spawn(
69
- env,
70
- 'docker',
71
- 'container',
72
- 'run',
73
- '--rm',
74
- *args,
75
- image.to_s,
76
- *stringify_command(command),
77
- process_redirections,
78
- )
28
+ def run_and_attach(*command, stdout: nil, stderr: nil, stdin: nil, env: {}, network: nil, volumes: [], working_dir: nil, redirect_io: {}, allow_failure: false)
29
+ exit_status = spawn(env, 'docker', *stringify_command(command), redirect_io)
79
30
  unless allow_failure || exit_status.success?
80
31
  raise Error, "docker container run failed"
81
32
  end
@@ -106,6 +57,23 @@ module Rascal
106
57
  rescue IOError
107
58
  end
108
59
  end
60
+
61
+ def parse_output(output, stdout, stderr, command:)
62
+ case output
63
+ when :json
64
+ begin
65
+ JSON.parse(stdout)
66
+ rescue JSON::ParserError
67
+ raise Error, "could not parse output of docker command '#{command}':\n#{stdout}"
68
+ end
69
+ when :id
70
+ stdout[/[0-9a-f]+/]
71
+ when :ignore
72
+ nil
73
+ else
74
+ raise ArgumentError, 'unknown option for :output'
75
+ end
76
+ end
109
77
  end
110
78
  end
111
79
  end
@@ -2,6 +2,7 @@ module Rascal
2
2
  module Docker
3
3
  module Volume
4
4
  class Base
5
+ include IOHelper
5
6
  end
6
7
 
7
8
  class Named < Base
@@ -15,10 +16,23 @@ module Rascal
15
16
  end
16
17
 
17
18
  def clean
19
+ if exists?
20
+ say "Removing volume #{@prefixed_name}"
21
+ Docker.interface.run(
22
+ 'volume',
23
+ 'rm',
24
+ @prefixed_name,
25
+ )
26
+ end
27
+ end
28
+
29
+ def exists?
18
30
  Docker.interface.run(
19
31
  'volume',
20
- 'rm',
21
- @prefixed_name,
32
+ 'ls',
33
+ '--quiet',
34
+ '--filter', "name=^#{@prefixed_name}$",
35
+ output: :id,
22
36
  )
23
37
  end
24
38
  end
@@ -1,6 +1,6 @@
1
1
  module Rascal
2
2
  class Environment
3
- attr_reader :name
3
+ attr_reader :name, :network, :container, :env_variables, :services, :volumes, :working_dir, :before_shell
4
4
 
5
5
  def initialize(name, image:, env_variables: {}, services: [], volumes: [], before_shell: [], working_dir: nil)
6
6
  @name = name
@@ -26,10 +26,17 @@ module Rascal
26
26
  )
27
27
  end
28
28
 
29
- def clean
29
+ def clean(clean_volumes: false)
30
30
  @services.each(&:clean)
31
31
  @network.clean
32
- @volumes.each(&:clean)
32
+ @volumes.each(&:clean) if clean_volumes
33
+ end
34
+
35
+ def update(skip: [])
36
+ [
37
+ *@services.collect { |s| s.update(skip: skip) },
38
+ *@container.update(skip: skip),
39
+ ]
33
40
  end
34
41
 
35
42
  private
@@ -36,6 +36,7 @@ module Rascal
36
36
  @info = parse_definition(config_path.read)
37
37
  @repo_dir = config_path.parent
38
38
  @rascal_config = @info.fetch('.rascal', {})
39
+ @rascal_environment_config = @rascal_config.delete('jobs') || {}
39
40
  end
40
41
 
41
42
  def environment(name)
@@ -51,20 +52,28 @@ module Rascal
51
52
  private
52
53
 
53
54
  def parse_definition(yaml)
54
- YAML.safe_load(yaml, [], [], true)
55
+ if Psych::VERSION >= '3.1'
56
+ YAML.safe_load(yaml, aliases: true)
57
+ else
58
+ YAML.safe_load(yaml, [], [], true)
59
+ end
55
60
  end
56
61
 
57
62
  def environments
58
63
  @environments ||= begin
59
64
  @info.collect do |key, environment_config|
60
- config = Config.new(deep_merge(environment_config, @rascal_config), key)
61
- docker_repo_dir = config.get('repo_dir')
65
+ config = Config.new(deep_merge(environment_config, @rascal_config, @rascal_environment_config[key] || {}), key)
66
+ docker_repo_dir = config.get('repo_dir', '/repo')
62
67
  unless key.start_with?('.')
63
68
  Environment.new(key,
64
69
  image: config.get('image'),
65
70
  env_variables: (config.get('variables', {})),
66
71
  services: build_services(key, config.get('services', [])),
67
- volumes: [build_repo_volume(docker_repo_dir), *build_volumes(key, config.get('volumes', {}))],
72
+ volumes: [
73
+ build_repo_volume(docker_repo_dir),
74
+ build_builds_volume(key),
75
+ *build_volumes(key, config.get('volumes', {}))
76
+ ],
68
77
  before_shell: config.get('before_shell', []),
69
78
  working_dir: docker_repo_dir,
70
79
  )
@@ -73,9 +82,9 @@ module Rascal
73
82
  end
74
83
  end
75
84
 
76
- def deep_merge(hash1, hash2)
85
+ def deep_merge(hash1, hash2, *other)
86
+ result = {}
77
87
  if hash1.is_a?(Hash) && hash2.is_a?(Hash)
78
- result = {}
79
88
  hash1.each do |key1, value1|
80
89
  if hash2.has_key?(key1)
81
90
  result[key1] = deep_merge(value1, hash2[key1])
@@ -86,9 +95,13 @@ module Rascal
86
95
  hash2.each do |key2, value2|
87
96
  result[key2] ||= value2
88
97
  end
89
- result
90
98
  else
91
- hash2
99
+ result = hash2
100
+ end
101
+ if other.any?
102
+ deep_merge(result, *other)
103
+ else
104
+ result
92
105
  end
93
106
  end
94
107
 
@@ -106,6 +119,10 @@ module Rascal
106
119
  Docker::Volume::Bind.new(@repo_dir, docker_repo_dir)
107
120
  end
108
121
 
122
+ def build_builds_volume(name)
123
+ Docker::Volume::Named.new("#{name}-builds", '/builds')
124
+ end
125
+
109
126
  def build_volumes(name, volume_config)
110
127
  volume_config.collect do |volume_name, docker_path|
111
128
  Docker::Volume::Named.new("#{name}-#{volume_name}", docker_path)
@@ -1,6 +1,6 @@
1
1
  module Rascal
2
2
  class Service
3
- attr_reader :name
3
+ attr_reader :name, :container, :alias
4
4
 
5
5
  def initialize(name, image:, alias_name:)
6
6
  @name = name
@@ -21,5 +21,9 @@ module Rascal
21
21
  def clean
22
22
  @container.clean
23
23
  end
24
+
25
+ def update(**args)
26
+ @container.update(**args)
27
+ end
24
28
  end
25
29
  end
@@ -1,3 +1,3 @@
1
1
  module Rascal
2
- VERSION = "0.1.0"
2
+ VERSION = "0.2.0"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rascal
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tobias Kraze
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2019-03-21 00:00:00.000000000 Z
11
+ date: 2019-04-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: thor
@@ -56,6 +56,7 @@ files:
56
56
  - lib/rascal/cli/clean.rb
57
57
  - lib/rascal/cli/main.rb
58
58
  - lib/rascal/cli/shell.rb
59
+ - lib/rascal/cli/update.rb
59
60
  - lib/rascal/docker.rb
60
61
  - lib/rascal/docker/container.rb
61
62
  - lib/rascal/docker/interface.rb