helmsnap 0.4.1 → 0.6.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: bd784fcd78ea76eeb09743cf411b599c9cd6ab2299a1a647a037d0dcc667cdb0
4
- data.tar.gz: a53928f8100827b1cd738840abff6ab43d875c0d44f5c7400023aa185d5ffeb3
3
+ metadata.gz: 00d1a53c6e09960c8a7b6d21e9998aba27e0765959b4450f2e433754440fe65b
4
+ data.tar.gz: 0f9fc47f9897eb76745e817528646ac340e43e39fad563a8370560bde810274f
5
5
  SHA512:
6
- metadata.gz: 9e5f7c1f7516cf65c9bcb3dde36977e739a5973ec7eaf7147380b797c9acc271b889a637fe6b9d3a6764839ed8362270e77c8160cc0fbcab1aee9775421c7c07
7
- data.tar.gz: 12d305a44dfc9c07956aa84ad5c221a7bf2b404888be7750de6bdcd06617c69d13e9c0a1846e2f07157b029a55b23b867a541014e550f6d94e62989a569494a7
6
+ metadata.gz: d014a938da2ea497e3e751f5cb600940ccb77a1d7bd297399a7b3380ac74c2d5a47bb9b4c31f6a540992c72d3e1012540d19b264e49d3498ffc282cb4f63b68a
7
+ data.tar.gz: 17614166a76919b0353d7bf322062047c546c3256826758ef5d0991bf66e30ee7ad0552c117ebad489ecba205e7c4771340dde7c947975cd1101895954eb8fe9
data/Dockerfile CHANGED
@@ -2,8 +2,9 @@ FROM alpine/helm
2
2
 
3
3
  RUN apk add --update --no-cache ruby git colordiff
4
4
 
5
- WORKDIR /app
5
+ WORKDIR /wd
6
6
 
7
+ COPY --from=quay.io/roboll/helmfile:v0.142.0 /usr/local/bin/helmfile /usr/local/bin/helmfile
7
8
  COPY . .
8
9
 
9
10
  RUN gem install colorize && gem build && gem install helmsnap --local
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- helmsnap (0.4.1)
4
+ helmsnap (0.6.0)
5
5
  colorize
6
6
 
7
7
  GEM
@@ -23,7 +23,7 @@ GEM
23
23
  method_source (1.0.0)
24
24
  minitest (5.14.4)
25
25
  parallel (1.21.0)
26
- parser (3.0.3.0)
26
+ parser (3.0.3.1)
27
27
  ast (~> 2.4.1)
28
28
  pry (0.14.1)
29
29
  coderay (~> 1.1)
@@ -31,7 +31,7 @@ GEM
31
31
  rack (2.2.3)
32
32
  rainbow (3.0.0)
33
33
  rake (13.0.6)
34
- regexp_parser (2.1.1)
34
+ regexp_parser (2.2.0)
35
35
  rexml (3.2.5)
36
36
  rspec (3.10.0)
37
37
  rspec-core (~> 3.10.0)
@@ -55,7 +55,7 @@ GEM
55
55
  rubocop-ast (>= 1.7.0, < 2.0)
56
56
  ruby-progressbar (~> 1.7)
57
57
  unicode-display_width (>= 1.4.0, < 3.0)
58
- rubocop-ast (1.13.0)
58
+ rubocop-ast (1.14.0)
59
59
  parser (>= 3.0.1.1)
60
60
  rubocop-config-umbrellio (1.17.0.53)
61
61
  rubocop (= 1.17.0)
data/README.md CHANGED
@@ -4,16 +4,16 @@
4
4
 
5
5
  Helmsnap is a tool for generating and checking helm chart snapshots. Example:
6
6
 
7
- Generate snapshots (uses `helm template` under the hood):
7
+ Generate snapshots (uses `helmfile template` under the hood):
8
8
 
9
9
  ```sh
10
- helmsnap generate -c helm/mychart -s helm/snapshots -v helm/values/production.yaml
10
+ helmsnap generate
11
11
  ```
12
12
 
13
- Generate snapshots in some temp directory and check (diff) them against existing snapshots in `helm/snapshots` directory:
13
+ Generate snapshots in a temporary directory and check (diff) them against existing snapshots in `helm/snapshots` directory:
14
14
 
15
15
  ```sh
16
- helmsnap check -c helm/mychart -s helm/snapshots -v helm/values/production.yaml
16
+ helmsnap check
17
17
  ```
18
18
 
19
19
  Get the full description of possible arguments:
@@ -30,6 +30,23 @@ The typical usage flow:
30
30
 
31
31
  This tool can also be useful when you are developing a new chart or updating an existing one: you can generate snapshots and see what is rendered without need to deploy the chart in your cluster.
32
32
 
33
+ ## Configuration
34
+
35
+ By default, helmsnap will render your helmfile using `default` environment and will place snapshots in `helm/snapshots` directory. If you want to configure that, create a `.helmsnap.yaml` file and put there configuration that looks like this:
36
+
37
+ ```yaml
38
+ envs: [staging, production] # `[default]` by default
39
+ snapshotsPath: somedir/snapshots # `helm/snapshots` by default
40
+ ```
41
+
42
+ You can also override configuration file location using `--config` option.
43
+
44
+ ## Dependencies
45
+
46
+ - Ruby 2.7+.
47
+ - [Helmfile](https://github.com/roboll/helmfile), which in turn relies on [Helm](https://github.com/helm/helm).
48
+ - Colordiff or diff utility.
49
+
33
50
  ## Features
34
51
 
35
52
  ### Helm dependency management
@@ -58,7 +75,7 @@ Example job for Gitlab CI:
58
75
  check-snapshots:
59
76
  stage: test
60
77
  image: ghcr.io/tycooon/helmsnap:latest
61
- script: helmsnap check -c helm/mychart -s helm/snapshots -v helm/values/production.yaml
78
+ script: helmsnap check
62
79
  ```
63
80
 
64
81
  ## Contributing
@@ -1,22 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Helmsnap::ArgsParser
4
- Args = Struct.new(:chart_path, :snapshots_path, :values_path, keyword_init: true)
5
- MissingOption = Class.new(OptionParser::ParseError)
4
+ Args = Struct.new(:config_path)
6
5
 
6
+ DEFAULT_CONFIG_PATH = ".helmsnap.yaml"
7
+ CONFIG_PATH_HELP = %{Path to config (default: "#{DEFAULT_CONFIG_PATH}")}
7
8
  BANNER = "Usage: helmsnap CMD [options]"
8
9
 
9
10
  def initialize(options)
10
11
  self.options = options
11
- self.args = Args.new
12
+ self.args = Args.new(DEFAULT_CONFIG_PATH)
12
13
  self.parser = build_parser
13
14
  end
14
15
 
15
16
  def get_options!
16
17
  parser.parse!(options)
17
- raise MissingOption, "Missing option: CHARTDIR" unless args.chart_path
18
- raise MissingOption, "Missing option: SNAPDIR" unless args.snapshots_path
19
- raise MissingOption, "Missing option: VALUES" unless args.values_path
20
18
  args
21
19
  rescue OptionParser::ParseError => error
22
20
  print_help!(error)
@@ -32,22 +30,14 @@ class Helmsnap::ArgsParser
32
30
 
33
31
  attr_accessor :options, :parser, :args
34
32
 
35
- def build_parser # rubocop:disable Metrics/MethodLength
33
+ def build_parser
36
34
  OptionParser.new(BANNER, 50) do |opts|
37
35
  opts.separator("Supported commands: `generate` and `check`.")
38
36
  opts.separator("")
39
37
  opts.separator("Specific options:")
40
38
 
41
- opts.on("-c", "--chart-dir CHARTDIR", "Chart directory") do |option|
42
- args.chart_path = pn(option)
43
- end
44
-
45
- opts.on("-s", "--snapshots-dir SNAPDIR", "Snapshots directory") do |option|
46
- args.snapshots_path = pn(option)
47
- end
48
-
49
- opts.on("-v", "--values VALUES", "Values file") do |option|
50
- args.values_path = pn(option)
39
+ opts.on("-c", "--config CONFIG_PATH", CONFIG_PATH_HELP) do |val|
40
+ args.config_path = Pathname.new(val)
51
41
  end
52
42
 
53
43
  opts.on("--version", "Show version") do
@@ -61,8 +51,4 @@ class Helmsnap::ArgsParser
61
51
  end
62
52
  end
63
53
  end
64
-
65
- def pn(...)
66
- Pathname.new(...)
67
- end
68
54
  end
@@ -1,31 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class Helmsnap::Check
4
- def self.call(...)
5
- new(...).call
6
- end
7
-
8
- def initialize(chart_path:, snapshots_path:, values_path:)
9
- self.chart_path = chart_path
10
- self.snapshots_path = snapshots_path
11
- self.values_path = values_path
3
+ class Helmsnap::Check < Helmsnap::Service
4
+ def initialize(config)
5
+ super()
6
+ self.config = config
12
7
  end
13
8
 
14
9
  def call
15
10
  temp_dir_path = Pathname.new(Dir.mktmpdir)
16
11
 
17
- Helmsnap::Generate.call(
18
- chart_path: chart_path,
19
- snapshots_path: temp_dir_path,
20
- values_path: values_path,
21
- )
12
+ Helmsnap::Generate.call(config, snapshots_path: temp_dir_path)
22
13
 
23
- result = Helmsnap.run_cmd("which", "colordiff", allow_failure: true)
14
+ result = run_cmd("which", "colordiff", allow_failure: true)
24
15
  util = result.success ? "colordiff" : "diff"
25
16
 
26
- diff = Helmsnap.run_cmd(
27
- util, "--unified", "--recursive", snapshots_path, temp_dir_path, allow_failure: true
28
- ).output
17
+ cmd_parts = [util, "--unified", "--recursive", config.snapshots_path, temp_dir_path]
18
+ diff = run_cmd(*cmd_parts, allow_failure: true).output
29
19
 
30
20
  diff.strip.empty?
31
21
  ensure
@@ -34,5 +24,5 @@ class Helmsnap::Check
34
24
 
35
25
  private
36
26
 
37
- attr_accessor :chart_path, :snapshots_path, :values_path
27
+ attr_accessor :config
38
28
  end
@@ -1,14 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class Helmsnap::Command
3
+ class Helmsnap::Command < Helmsnap::Service
4
4
  Result = Struct.new(:success, :output)
5
5
 
6
- def self.call(...)
7
- new(...).call
8
- end
9
-
10
6
  def initialize(cmd, stdout: $stdout, stderr: $stderr, allow_failure: false)
7
+ super()
11
8
  self.cmd = cmd
9
+ self.output = +""
12
10
  self.stdout = stdout
13
11
  self.stderr = stderr
14
12
  self.allow_failure = allow_failure
@@ -21,12 +19,10 @@ class Helmsnap::Command
21
19
 
22
20
  private
23
21
 
24
- attr_accessor :cmd, :stdout, :stderr, :allow_failure
22
+ attr_accessor :cmd, :output, :stdout, :stderr, :allow_failure
25
23
 
26
24
  def run_command
27
25
  Open3.popen3(cmd) do |_in, out, err, wait_thr|
28
- output = +""
29
-
30
26
  while (chunk = out.gets)
31
27
  Helmsnap::Console.print(stdout, chunk)
32
28
  output << chunk
@@ -34,15 +30,24 @@ class Helmsnap::Command
34
30
 
35
31
  exit_status = wait_thr.value
36
32
  success = exit_status.success?
37
-
38
- if !success && !allow_failure
39
- Helmsnap::Console.error(stderr, err.read)
40
- Helmsnap::Console.error(stderr, "Command failed with status #{exit_status.to_i}")
41
- exit 1
42
- end
33
+ handle_error!(exit_status, err.read) unless success
43
34
 
44
35
  Helmsnap::Console.print(stdout, "\n")
45
36
  Result.new(success, output)
46
37
  end
38
+ rescue SystemCallError => error
39
+ handle_error!(error.errno, error.message)
40
+ Result.new(false, error.message)
41
+ end
42
+
43
+ def handle_error!(exit_status, err_output)
44
+ Helmsnap::Console.error(stderr, err_output)
45
+
46
+ if allow_failure
47
+ output << err_output
48
+ else
49
+ Helmsnap::Console.error(stderr, "Command failed with status #{exit_status.to_i}")
50
+ exit 1
51
+ end
47
52
  end
48
53
  end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Helmsnap::Config
4
+ attr_reader :envs, :snapshots_path
5
+
6
+ DEFAULT_ENV = "default"
7
+
8
+ def initialize(config_path)
9
+ yaml = YAML.load_file(config_path.to_s)
10
+ self.envs = parse_envs(yaml)
11
+ self.snapshots_path = parse_snaphots_path(yaml)
12
+ end
13
+
14
+ private
15
+
16
+ attr_writer :envs, :snapshots_path
17
+
18
+ def parse_envs(yaml)
19
+ # TODO: check value type
20
+ value = yaml.fetch("envs", [DEFAULT_ENV])
21
+ value.map { |x| Helmsnap::Env.new(x) }
22
+ end
23
+
24
+ def parse_snaphots_path(yaml)
25
+ # TODO: chekc value type/presence
26
+ Pathname.new(yaml.fetch("snapshotsPath"))
27
+ end
28
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Helmsnap::Env
4
+ attr_reader :name, :release_paths
5
+
6
+ def initialize(name)
7
+ self.name = name
8
+ self.release_paths = get_release_paths
9
+ end
10
+
11
+ private
12
+
13
+ attr_writer :name, :release_paths
14
+
15
+ def get_release_paths
16
+ json = Helmsnap.run_cmd("helmfile", "--environment", name, "list", "--output", "json").output
17
+ YAML.load(json).map { |x| x.fetch("chart") }
18
+ end
19
+ end
@@ -1,35 +1,31 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class Helmsnap::Generate
4
- def self.call(...)
5
- new(...).call
6
- end
7
-
8
- def initialize(chart_path:, snapshots_path:, values_path:)
9
- self.chart_path = chart_path
10
- self.snapshots_path = snapshots_path
11
- self.values_path = values_path
3
+ class Helmsnap::Generate < Helmsnap::Service
4
+ def initialize(config, snapshots_path: nil)
5
+ super()
6
+ self.config = config
7
+ self.snapshots_path = snapshots_path || config.snapshots_path
12
8
  end
13
9
 
14
10
  def call
15
- dep_list = run_cmd("helm", "dependency", "list", "--max-col-width", 0, chart_path).output
11
+ FileUtils.rmtree(snapshots_path)
16
12
 
17
- dep_list.scan(%r{file://(.+?)\t}) do |dep_path|
18
- run_cmd("helm", "dependency", "update", "--skip-refresh", chart_path.join(dep_path.first))
13
+ config.envs.flat_map(&:release_paths).uniq.each do |release_path|
14
+ Helmsnap::SetupDependencies.call(release_path)
19
15
  end
20
16
 
21
- dep_list.scan(%r{(https?://.+?)\t}) do |dep_path|
22
- run_cmd("helm", "repo", "add", Digest::MD5.hexdigest(dep_path.first), dep_path.first)
17
+ config.envs.each do |env|
18
+ run_cmd(
19
+ "helmfile",
20
+ "--environment",
21
+ env.name,
22
+ "template",
23
+ "--output-dir-template",
24
+ snapshots_path.join(env.name).join("{{ .Release.Name }}"),
25
+ "--skip-deps",
26
+ )
23
27
  end
24
28
 
25
- run_cmd("helm", "dependency", "update", "--skip-refresh", chart_path)
26
-
27
- FileUtils.rmtree(snapshots_path)
28
-
29
- run_cmd(
30
- "helm", "template", chart_path, "--values", values_path, "--output-dir", snapshots_path
31
- )
32
-
33
29
  snapshots_path.glob(["**/*yaml", "**/*.yml"]).each do |path|
34
30
  content = path.read
35
31
  content.gsub!(/\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d\.\d+/, "2022-01-01 00:00:00.000") or next
@@ -39,9 +35,5 @@ class Helmsnap::Generate
39
35
 
40
36
  private
41
37
 
42
- attr_accessor :chart_path, :snapshots_path, :values_path
43
-
44
- def run_cmd(...)
45
- Helmsnap.run_cmd(...)
46
- end
38
+ attr_accessor :config, :snapshots_path
47
39
  end
@@ -1,17 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class Helmsnap::Runner
4
- def self.call(...)
5
- new(...).call
6
- end
7
-
3
+ class Helmsnap::Runner < Helmsnap::Service
8
4
  def initialize(args)
5
+ super()
9
6
  self.args = args.dup
10
7
  end
11
8
 
12
9
  def call
13
10
  parser = Helmsnap::ArgsParser.new(args)
14
11
  self.options = parser.get_options!
12
+ self.config = Helmsnap::Config.new(options.config_path)
15
13
 
16
14
  cmd, *rest = args
17
15
 
@@ -35,25 +33,18 @@ class Helmsnap::Runner
35
33
 
36
34
  private
37
35
 
38
- attr_accessor :args, :options
36
+ attr_accessor :args, :options, :config
39
37
 
40
38
  def generate!
41
- Helmsnap::Generate.call(**options.to_h)
39
+ Helmsnap::Generate.call(config)
42
40
  Helmsnap::Console.info($stdout, "Snapshots generated successfully.")
43
41
  end
44
42
 
45
43
  def check!
46
- if Helmsnap::Check.call(**options.to_h)
44
+ if Helmsnap::Check.call(config)
47
45
  Helmsnap::Console.info($stdout, "Snapshots are up-to-date.")
48
46
  else
49
- example_cmd = Shellwords.join(
50
- [
51
- "helmsnap", "generate",
52
- "--chart-dir", options.chart_path,
53
- "--snapshots-dir", options.snapshots_path,
54
- "--values", options.values_path
55
- ],
56
- )
47
+ example_cmd = Shellwords.join(["helmsnap", "generate", "--config", options.config_path])
57
48
 
58
49
  Helmsnap::Console.error(
59
50
  $stdout,
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Helmsnap::Service
4
+ def self.call(...)
5
+ new(...).call
6
+ end
7
+
8
+ private
9
+
10
+ def run_cmd(...)
11
+ Helmsnap.run_cmd(...)
12
+ end
13
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Helmsnap::SetupDependencies < Helmsnap::Service
4
+ def initialize(chart_path)
5
+ super()
6
+ self.chart_path = Pathname.new(chart_path)
7
+ end
8
+
9
+ def call
10
+ dep_list = run_cmd("helm", "dependency", "list", "--max-col-width", 0, chart_path).output
11
+
12
+ dep_list.scan(%r{file://(.+?)\t}) do |dep_path|
13
+ run_cmd("helm", "dependency", "update", "--skip-refresh", chart_path.join(dep_path.first))
14
+ end
15
+
16
+ dep_list.scan(%r{(https?://.+?)\t}) do |dep_path|
17
+ run_cmd("helm", "repo", "add", Digest::MD5.hexdigest(dep_path.first), dep_path.first)
18
+ end
19
+
20
+ run_cmd("helm", "dependency", "update", "--skip-refresh", chart_path)
21
+ end
22
+
23
+ private
24
+
25
+ attr_accessor :chart_path
26
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Helmsnap
4
- VERSION = "0.4.1"
4
+ VERSION = "0.6.0"
5
5
  end
data/lib/helmsnap.rb CHANGED
@@ -7,17 +7,23 @@ require "optparse"
7
7
  require "pathname"
8
8
  require "shellwords"
9
9
  require "tmpdir"
10
+ require "yaml"
10
11
 
11
12
  require "colorized_string"
12
13
 
13
14
  module Helmsnap
15
+ require_relative "helmsnap/config"
16
+ require_relative "helmsnap/env"
17
+ require_relative "helmsnap/service"
18
+ require_relative "helmsnap/version"
19
+
14
20
  require_relative "helmsnap/args_parser"
15
21
  require_relative "helmsnap/check"
16
- require_relative "helmsnap/console"
17
22
  require_relative "helmsnap/command"
23
+ require_relative "helmsnap/console"
18
24
  require_relative "helmsnap/generate"
19
25
  require_relative "helmsnap/runner"
20
- require_relative "helmsnap/version"
26
+ require_relative "helmsnap/setup_dependencies"
21
27
 
22
28
  class Error < StandardError; end
23
29
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: helmsnap
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.1
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yuri Smirnov
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-12-01 00:00:00.000000000 Z
11
+ date: 2021-12-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: colorize
@@ -63,9 +63,13 @@ files:
63
63
  - lib/helmsnap/args_parser.rb
64
64
  - lib/helmsnap/check.rb
65
65
  - lib/helmsnap/command.rb
66
+ - lib/helmsnap/config.rb
66
67
  - lib/helmsnap/console.rb
68
+ - lib/helmsnap/env.rb
67
69
  - lib/helmsnap/generate.rb
68
70
  - lib/helmsnap/runner.rb
71
+ - lib/helmsnap/service.rb
72
+ - lib/helmsnap/setup_dependencies.rb
69
73
  - lib/helmsnap/version.rb
70
74
  homepage: https://github.com/tycooon/helmsnap
71
75
  licenses: