consist 0.1.0 → 0.1.2

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.
data/lib/consist/cli.rb CHANGED
@@ -1,4 +1,17 @@
1
1
  require "thor"
2
+ require "sshkit"
3
+ require "sshkit/dsl"
4
+
5
+ require "consist/recipe"
6
+ require "consist/recipes"
7
+ require "consist/step"
8
+ require "consist/consistfile"
9
+ require "consist/commands/includes/stream_logger"
10
+ require "consist/commands/includes/erbable"
11
+ require "consist/commands/exec"
12
+ require "consist/commands/upload"
13
+ require "consist/commands/mutate"
14
+ require "consist/commands/check"
2
15
 
3
16
  module Consist
4
17
  class CLI < Thor
@@ -6,9 +19,33 @@ module Consist
6
19
 
7
20
  map %w[-v --version] => "version"
8
21
 
9
- desc "version", "Display consist version", hide: true
22
+ desc "version", "Display consist version"
10
23
  def version
11
24
  say "consist/#{VERSION} #{RUBY_DESCRIPTION}"
12
25
  end
26
+
27
+ desc "lightup", "Attempt to connect to a server and execute an idempotent statement."
28
+ def lightup(user, server)
29
+ puts "---> Attempting to connect to #{server} as #{user}"
30
+ on("#{user}@#{server}") do
31
+ as user do
32
+ execute "true"
33
+ end
34
+ end
35
+ end
36
+
37
+ desc "scaffold", "Apply a given recipe to (a) server(s)"
38
+ def scaffold(_recipe, server_ip)
39
+ Consist::Recipes.new(server_ip)
40
+ end
41
+
42
+ option :step, type: :string
43
+ option :consistfile, type: :string
44
+ desc "up", "Run a Consistfile against a server"
45
+ def up(server_ip)
46
+ specified_step = options[:step]
47
+ consistfile = options[:consistfile]
48
+ Consist::Consistfile.new(server_ip, consistfile:, specified_step:)
49
+ end
13
50
  end
14
51
  end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Consist
4
+ module Commands
5
+ class Check
6
+ def initialize(command)
7
+ @command = command
8
+ end
9
+
10
+ def perform!(executor)
11
+ status = @command[:status]
12
+
13
+ flag, val = if @command.has_key?(:path) && !@command[:path].nil?
14
+ ["d", @command[:path]]
15
+ else
16
+ ["f", @command[:file]]
17
+ end
18
+
19
+ exists = executor.test("[ -#{flag} #{val} ]")
20
+
21
+ if status == :exist && !exists
22
+ @command[:block].call
23
+ elsif status == :nonexistant && !exists
24
+ @command[:block].call
25
+ else
26
+ tense = if status == :exist
27
+ "should"
28
+ elsif status == :nonexistant
29
+ "shoudlnt"
30
+ end
31
+ puts "Checking path `#{status}` - `#{val}` - #{exists ? "exists" : "doesn't exist"} and #{tense} - skipping"
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Consist
4
+ module Commands
5
+ class Exec
6
+ include Erbable
7
+
8
+ def initialize(command)
9
+ @command = command
10
+ end
11
+
12
+ def perform!(executor)
13
+ @command[:commands].each do
14
+ executor.execute(erb_template(_1), interaction_handler: Consist::Commands::StreamLogger.new,
15
+ **@command[:params])
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "erb"
4
+
5
+ module Consist
6
+ module Commands
7
+ module Erbable
8
+ def self.included(klass)
9
+ klass.extend ClassMethods
10
+ end
11
+
12
+ def erb_template(contents)
13
+ b = binding
14
+ Consist.config.keys.each do |key|
15
+ b.local_variable_set(key, Consist.config[key])
16
+ end
17
+ ERB.new(contents).result(b)
18
+ end
19
+
20
+ module ClassMethods
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Consist
4
+ module Commands
5
+ class StreamLogger
6
+ def initialize(log_level = :info)
7
+ @log_level = log_level
8
+ end
9
+
10
+ def on_data(_command, _stream_name, data, _channel)
11
+ log(data)
12
+ end
13
+
14
+ private
15
+
16
+ def log(message)
17
+ SSHKit.config.output.send(@log_level, message) unless @log_level.nil?
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Consist
4
+ module Commands
5
+ class Mutate
6
+ include Erbable
7
+
8
+ def initialize(command)
9
+ @command = command
10
+ end
11
+
12
+ def perform!(executor)
13
+ delim = @command[:delim]
14
+ target_file = @command[:target_file]
15
+ target_string = erb_template(@command[:target_string])
16
+ match = erb_template(@command[:match])
17
+
18
+ case @command[:mode]
19
+ when :replace
20
+ cmd = "sed -i -E 's#{delim}#{match}#{delim}#{target_string}#{delim}' #{target_file} "
21
+ end
22
+
23
+ executor.execute(cmd, interaction_handler: Consist::Commands::StreamLogger.new)
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Consist
4
+ module Commands
5
+ class Upload
6
+ include Erbable
7
+
8
+ def initialize(command)
9
+ @command = command
10
+ end
11
+
12
+ def perform!(executor)
13
+ # rubocop:disable Style/ClassEqualityComparison
14
+ if @command[:local_file].class == Symbol
15
+ puts "---> Uploading defined file #{@command[:local_file]}"
16
+ target_file = Consist.files.detect { |f| f[:id] == @command[:local_file] }
17
+ raise "\n\nNo declared file of ID `#{@command[:local_file]}`" unless target_file
18
+
19
+ contents = StringIO.new(erb_template(target_file[:contents]))
20
+ upload_defined_file(executor, contents, @command[:remote_path])
21
+ else
22
+ local_path = File.expand_path("../steps/#{@id}/#{@command[:local_file]}", __dir__)
23
+ upload(executor, local_path, @command[:remote_path])
24
+ end
25
+ # rubocop:enable Style/ClassEqualityComparison
26
+ end
27
+
28
+ def upload(executor, local_path, remote_path)
29
+ executor.send(:upload!, local_path, remote_path, interaction_handler: Consist::Commands::StreamLogger.new)
30
+ end
31
+
32
+ def upload_defined_file(executor, contents, remote_path)
33
+ executor.upload! contents, remote_path, interaction_handler: Consist::Commands::StreamLogger.new
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true37236
2
+
3
+ module Consist
4
+ class << self
5
+ attr_accessor :files, :config
6
+ end
7
+
8
+ @files = []
9
+ @config = {}
10
+
11
+ class Consistfile
12
+ include SSHKit::DSL
13
+
14
+ def initialize(server_ip, specified_step:, consistfile:)
15
+ @server_ip = server_ip
16
+ @specified_step = specified_step
17
+ consistfile_path = if consistfile
18
+ File.expand_path(consistfile, Dir.pwd)
19
+ else
20
+ File.expand_path("Consistfile", Dir.pwd)
21
+ end
22
+
23
+ consistfile_contents = File.read(consistfile_path)
24
+ instance_eval(consistfile_contents)
25
+ end
26
+
27
+ def consist(&definition)
28
+ instance_eval(&definition)
29
+ end
30
+
31
+ def recipe(id, &definition)
32
+ recipe = Consist::Recipe.new(id, &definition)
33
+
34
+ puts "Executing Recipe: #{recipe.name}"
35
+
36
+ if @specified_step.nil?
37
+ recipe.steps.each { exec_step(_1) }
38
+ else
39
+ puts "Specific step targeted: #{@specified_step.to_sym}"
40
+ specified_step, *_rest = recipe.steps.select { _1.id === @specified_step.to_sym }
41
+ raise "Step with id #{@specified_step.to_sym} not found." unless specified_step
42
+
43
+ exec_step(specified_step)
44
+ end
45
+
46
+ puts "Execution of #{recipe.name} has completed."
47
+ end
48
+
49
+ def file(id, &definition)
50
+ return unless definition
51
+
52
+ contents = yield
53
+
54
+ Consist.files << {id:, contents:}
55
+ end
56
+
57
+ def config(id, val)
58
+ Consist.config[id] = val
59
+ end
60
+
61
+ private
62
+
63
+ def exec_step(specified_step)
64
+ puts "Executing Step: #{specified_step.name}"
65
+
66
+ on("#{specified_step.required_user}@#{@server_ip}") do
67
+ specified_step.perform(self)
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Consist
4
+ class Recipe
5
+ def initialize(_id = nil, &definition)
6
+ @steps = []
7
+ instance_eval(&definition)
8
+ end
9
+
10
+ def name(name = nil)
11
+ @name = name if name
12
+ @name
13
+ end
14
+
15
+ def description(description = nil)
16
+ @description = description if description
17
+ @description
18
+ end
19
+
20
+ def user(user = nil)
21
+ @user = user if @user
22
+ @user
23
+ end
24
+
25
+ def steps(&block)
26
+ instance_eval(&block) if block
27
+ @steps
28
+ end
29
+
30
+ def step(step_name, &block)
31
+ if block
32
+ @steps << Step.new(id: step_name, &block)
33
+ return
34
+ end
35
+
36
+ target_path = File.join("../../", "steps", step_name.to_s, "step.rb")
37
+ step_path = File.expand_path(target_path, __FILE__)
38
+ step_content = File.read(step_path)
39
+ @steps << Step.new(id: step_name) { instance_eval(step_content) }
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Consist
4
+ class Recipes
5
+ include SSHKit::DSL
6
+
7
+ def initialize(server_ip)
8
+ recipe_directory = File.expand_path("../recipes", __dir__)
9
+ recipes = Dir[File.join(recipe_directory, "*.rb")]
10
+
11
+ recipes.each do |recipe_file|
12
+ recipe_content = File.read(recipe_file)
13
+ recipe = Recipe.new { instance_eval(recipe_content) }
14
+
15
+ puts "Executing Recipe: #{recipe.name}"
16
+ recipe.steps.each do |step|
17
+ puts "Executing Step: #{step.name}"
18
+
19
+ on("#{step.required_user}@#{server_ip}") do
20
+ step.perform(self)
21
+ end
22
+ end
23
+
24
+ puts "Execution of #{recipe.name} has completed."
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "erb"
4
+
5
+ module Consist
6
+ class Step
7
+ include SSHKit::DSL
8
+
9
+ def initialize(id:, &block)
10
+ @commands = []
11
+ @id = id
12
+ @required_user = :root
13
+ instance_eval(&block)
14
+ end
15
+
16
+ def id(id = nil)
17
+ @id = id if id
18
+ @id
19
+ end
20
+
21
+ def name(name = nil)
22
+ @name = name if name
23
+ @name
24
+ end
25
+
26
+ def required_user(user = nil)
27
+ @required_user = user if user
28
+ @required_user
29
+ end
30
+
31
+ def shell(message = "", params: {})
32
+ return unless block_given?
33
+
34
+ command = yield
35
+ commands = command.split(/(?<!\\)\n/).select { !_1.start_with?("#") }.compact
36
+
37
+ @commands << {message:, type: :exec, commands:, params:}
38
+ end
39
+
40
+ def mutate_file(mode:, target_file:, match:, target_string:, delim: "/", message: "")
41
+ @commands << {type: :mutate, mode:, message:, match:, target_file:, delim:, target_string:}
42
+ end
43
+
44
+ def upload_file(local_file:, remote_path:, message: "")
45
+ @commands << {message:, type: :upload, local_file:, remote_path:}
46
+ end
47
+
48
+ def check(status:, path: nil, file: nil, message: "", &block)
49
+ @commands << {type: :check, message:, status:, file:, path:, block: -> { instance_eval(&block) }}
50
+ end
51
+
52
+ def perform(executor)
53
+ @commands.each do |command|
54
+ banner(command[:message]) unless command[:message].empty?
55
+
56
+ execable = Object.const_get("Consist::Commands::#{command[:type].capitalize}").new(command)
57
+ executor.as @required_user do
58
+ execable.perform!(executor)
59
+ end
60
+ end
61
+ end
62
+
63
+ private
64
+
65
+ def banner(message)
66
+ return if message.empty?
67
+
68
+ msg = "********* #{message} ********"
69
+ puts "*" * msg.length
70
+ puts msg
71
+ puts "*" * msg.length
72
+ end
73
+ end
74
+ end
@@ -28,12 +28,12 @@ module Consist
28
28
  base.check_unknown_options!
29
29
  end
30
30
 
31
- def start(given_args=ARGV, config={})
31
+ def start(given_args = ARGV, config = {})
32
32
  config[:shell] ||= Thor::Base.shell.new
33
33
  handle_help_switches(given_args) do |args|
34
34
  dispatch(nil, args, nil, config)
35
35
  end
36
- rescue StandardError => e
36
+ rescue => e
37
37
  handle_exception_on_start(e, config)
38
38
  end
39
39
 
@@ -1,3 +1,3 @@
1
1
  module Consist
2
- VERSION = "0.1.0".freeze
2
+ VERSION = "0.1.2".freeze
3
3
  end
@@ -0,0 +1,8 @@
1
+ name "Kamal Single Server"
2
+ description "Sets up a single server to run Kamal"
3
+ user :root
4
+
5
+ steps do
6
+ step :update_apt_packages
7
+ step :install_apt_packages
8
+ end
@@ -0,0 +1,19 @@
1
+ name "Install APT packages"
2
+ required_user :root
3
+
4
+ shell "Installing essential packages" do
5
+ <<~EOS
6
+ apt-get -y remove systemd-timesyncd
7
+ timedatectl set-ntp no
8
+ apt-get -y install build-essential curl fail2ban git ntp vim
9
+ apt-get autoremove
10
+ apt-get autoclean
11
+ EOS
12
+ end
13
+
14
+ shell "Start NTP and Fail2Ban" do
15
+ <<~EOS
16
+ service ntp restart
17
+ service fail2ban restart
18
+ EOS
19
+ end
@@ -0,0 +1,3 @@
1
+ APT::Periodic::AutocleanInterval "7";
2
+ APT::Periodic::Update-Package-Lists "1";
3
+ APT::Periodic::Unattended-Upgrade "1";
@@ -0,0 +1,11 @@
1
+ name "Update the APT packages"
2
+ required_user :root
3
+
4
+ upload_file message: "Uploading APT config...", local_file: "apt_auto_upgrades",
5
+ remote_path: "/etc/apt/apt.conf.d/20auto-upgrades"
6
+
7
+ shell do
8
+ <<~EOS
9
+ apt-get update && apt-get upgrade -y
10
+ EOS
11
+ end
metadata CHANGED
@@ -1,15 +1,43 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: consist
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - John McDowall
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-11-20 00:00:00.000000000 Z
11
+ date: 2023-11-30 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: net-ssh
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '7.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '7.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: sshkit
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.21'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.21'
13
41
  - !ruby/object:Gem::Dependency
14
42
  name: thor
15
43
  requirement: !ruby/object:Gem::Requirement
@@ -26,7 +54,7 @@ dependencies:
26
54
  version: '1.2'
27
55
  description:
28
56
  email:
29
- - j@jmd.fm
57
+ - john@kantan.io
30
58
  executables:
31
59
  - consist
32
60
  extensions: []
@@ -37,16 +65,30 @@ files:
37
65
  - exe/consist
38
66
  - lib/consist.rb
39
67
  - lib/consist/cli.rb
68
+ - lib/consist/commands/check.rb
69
+ - lib/consist/commands/exec.rb
70
+ - lib/consist/commands/includes/erbable.rb
71
+ - lib/consist/commands/includes/stream_logger.rb
72
+ - lib/consist/commands/mutate.rb
73
+ - lib/consist/commands/upload.rb
74
+ - lib/consist/consistfile.rb
75
+ - lib/consist/recipe.rb
76
+ - lib/consist/recipes.rb
77
+ - lib/consist/step.rb
40
78
  - lib/consist/thor_ext.rb
41
79
  - lib/consist/version.rb
42
- homepage: https://github.com/johnmcdowall/consist
80
+ - lib/recipes/kamal_single_server.rb
81
+ - lib/steps/install_apt_packages/step.rb
82
+ - lib/steps/update_apt_packages/apt_auto_upgrades
83
+ - lib/steps/update_apt_packages/step.rb
84
+ homepage: https://github.com/consist-sh/consist
43
85
  licenses:
44
- - MIT
86
+ - LGPL-3.0
45
87
  metadata:
46
- bug_tracker_uri: https://github.com/johnmcdowall/consist/issues
47
- changelog_uri: https://github.com/johnmcdowall/consist/releases
48
- source_code_uri: https://github.com/johnmcdowall/consist
49
- homepage_uri: https://github.com/johnmcdowall/consist
88
+ bug_tracker_uri: https://github.com/consist-sh/consist/issues
89
+ changelog_uri: https://github.com/consist-sh/consist/releases
90
+ source_code_uri: https://github.com/consist-sh/consist
91
+ homepage_uri: https://github.com/consist-sh/consist
50
92
  rubygems_mfa_required: 'true'
51
93
  post_install_message:
52
94
  rdoc_options: []