rascal 0.1.0 → 0.2.0

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: 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