kozo 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Kozo
6
+ module Backends
7
+ class Local < Base
8
+ def state
9
+ Kozo.logger.debug "Reading local state in #{file}"
10
+
11
+ File.write(file, {}.to_json) unless File.exist?(file)
12
+
13
+ JSON.parse(File.read(file), symbolize_names: true)
14
+ rescue JSON::ParserError => e
15
+ Kozo.logger.fatal "Could not read state file: #{e.message}"
16
+ end
17
+
18
+ def state=(value)
19
+ Kozo.logger.debug "Writing local state in #{file}"
20
+ File.write(file, value.to_json)
21
+ end
22
+
23
+ def ==(other)
24
+ directory == other.directory
25
+ end
26
+
27
+ private
28
+
29
+ def file
30
+ @file ||= File.join(directory, "kozo.kzstate")
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "optparse"
4
+
5
+ module Kozo
6
+ class CLI
7
+ attr_reader :parser, :args, :command_args
8
+
9
+ def initialize(args)
10
+ @parser = OptionParser.new("#{File.basename($PROGRAM_NAME)} [global options] command [command options]") do |o|
11
+ o.on("Global options:")
12
+ o.on("-v", "--verbose", "Turn on verbose logging")
13
+ o.on("-h", "--help", "Display this message") { usage }
14
+ o.separator("\n")
15
+ o.on("Commands:")
16
+ commands.each { |(name, description)| o.on(" #{name}#{description.rjust(48)}") }
17
+ o.separator("\n")
18
+ end
19
+
20
+ @args = args
21
+ @command_args = []
22
+
23
+ parse!
24
+ end
25
+
26
+ def parse!
27
+ # Parse command line arguments (in order) and extract non-option arguments
28
+ # (unrecognized option values). Raise for invalid option arguments (unrecognized
29
+ # option keys). "--foo FOO --bar BAR" will result in "--foo" and "FOO" being parsed
30
+ # correctly, "--bar" and "BAR" will be extracted.
31
+ parser.order!(args, into: Kozo.options) { |value| command_args << value }
32
+ rescue OptionParser::InvalidOption => e
33
+ @command_args += e.args
34
+ retry
35
+ end
36
+
37
+ def start
38
+ command = command_args.shift
39
+
40
+ return usage unless command
41
+
42
+ klass = "Kozo::Commands::#{command.camelize}".safe_constantize
43
+
44
+ return usage(tail: "#{File.basename($PROGRAM_NAME)}: unknown command: #{command}") unless klass
45
+
46
+ klass
47
+ .new(command_args)
48
+ .start
49
+ end
50
+
51
+ private
52
+
53
+ def usage(code: 1, tail: nil)
54
+ puts parser.to_s
55
+ puts tail if tail
56
+
57
+ exit code
58
+ end
59
+
60
+ def commands
61
+ Commands::Base.descendants.map { |k| [k.name.demodulize.underscore, k.description] }
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kozo
4
+ module Commands
5
+ class Base
6
+ class_attribute :description
7
+
8
+ def initialize(_args); end
9
+
10
+ def start
11
+ raise NotImplementedError
12
+ end
13
+
14
+ protected
15
+
16
+ def configuration
17
+ @configuration ||= Configuration.new(Kozo.options.directory)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kozo
4
+ module Commands
5
+ class Plan < Base
6
+ self.description = "Show execution plan"
7
+
8
+ def start
9
+ configuration.parse!
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kozo
4
+ class Configuration
5
+ attr_writer :backend
6
+ attr_accessor :directory, :providers, :resources
7
+
8
+ def initialize(directory)
9
+ @directory = directory
10
+ @providers = {}
11
+ @resources = {}
12
+ end
13
+
14
+ def backend
15
+ @backend ||= Backends::Local.new(directory)
16
+ end
17
+
18
+ def parse!
19
+ dsl = DSL.new(self)
20
+ Dir[File.join(directory, "*.kz")]
21
+ .sort
22
+ .each { |file| dsl.instance_eval(File.read(file)) }
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kozo
4
+ class Container
5
+ attr_reader :dependencies
6
+
7
+ def initialize
8
+ @dependencies = {}
9
+ end
10
+
11
+ def register(key, force: false, &block)
12
+ raise DependencyAlreadyRegistered, key unless force || !dependencies[key]
13
+
14
+ dependencies[key] = block
15
+ end
16
+
17
+ def resolve(key, *args, quiet: false, &block)
18
+ return dependencies[key].call(*args, &block) if dependencies[key]
19
+
20
+ raise DependencyNotRegistered, key unless quiet
21
+ end
22
+
23
+ class DependencyAlreadyRegistered < StandardError
24
+ def initialize(message)
25
+ super("Dependency already registered: #{message}")
26
+ end
27
+ end
28
+
29
+ class DependencyNotRegistered < StandardError
30
+ def initialize(message)
31
+ super("Dependency not registered: #{message}")
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kozo
4
+ class DSL
5
+ attr_reader :configuration
6
+
7
+ def initialize(configuration)
8
+ @configuration = configuration
9
+ end
10
+
11
+ def kozo(&_block)
12
+ yield
13
+ end
14
+
15
+ def backend(type)
16
+ backend = resolve(:backend, type)
17
+
18
+ yield backend if block_given?
19
+
20
+ configuration.backend = backend
21
+ end
22
+
23
+ def provider(type)
24
+ provider = resolve(:provider, type)
25
+
26
+ yield provider if block_given?
27
+
28
+ configuration.providers[provider.class.name] = provider
29
+ end
30
+
31
+ def resource(type, name)
32
+ resource = resolve(:resource, type, name)
33
+ resource.provider = configuration.providers[resource.class.provider]
34
+
35
+ Kozo.logger.fatal "Provider #{resource.class.provider}" unless resource.provider
36
+
37
+ yield resource if block_given?
38
+
39
+ configuration.resources[resource.class.name] = resource
40
+ end
41
+
42
+ private
43
+
44
+ def resolve(resource, type, name = nil)
45
+ Kozo.logger.debug "Initializing #{resource} #{type} #{name}"
46
+
47
+ Kozo.container.resolve("#{resource}.#{type}")
48
+ rescue Container::DependencyNotRegistered
49
+ Kozo.logger.fatal "Unknown #{resource} type: #{type}"
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kozo
4
+ class Environment < String
5
+ def initialize
6
+ super(ENV.fetch("KOZO_ENV", "development"))
7
+
8
+ return if %w(development test production).include?(self)
9
+
10
+ raise InvalidEnvironment, "Unknown environment: #{self}"
11
+ end
12
+
13
+ def development?
14
+ self == "development"
15
+ end
16
+
17
+ def test?
18
+ self == "test"
19
+ end
20
+
21
+ def production?
22
+ self == "production"
23
+ end
24
+
25
+ class InvalidEnvironment < StandardError; end
26
+ end
27
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "delegate"
4
+ require "logger"
5
+
6
+ module Kozo
7
+ class Logger < SimpleDelegator
8
+ def initialize
9
+ super(::Logger.new($stdout, level: level, formatter: formatter))
10
+ end
11
+
12
+ private
13
+
14
+ def level
15
+ ENV.fetch("LOG_LEVEL", "info")
16
+ end
17
+
18
+ def formatter
19
+ level == "debug" ? ::Logger::Formatter.new : Formatter.new
20
+ end
21
+
22
+ class Formatter < ::Logger::Formatter
23
+ def call(severity, _time, _progname, msg)
24
+ abort("#{File.basename($PROGRAM_NAME)}: #{msg[0].downcase}#{msg[1..]}") if severity == "FATAL"
25
+
26
+ "#{msg}\n"
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kozo
4
+ class Options
5
+ attr_writer :directory, :verbose
6
+
7
+ def directory
8
+ @directory ||= Dir.pwd
9
+ end
10
+
11
+ def directory?
12
+ directory.present?
13
+ end
14
+
15
+ def verbose
16
+ @verbose ||= false
17
+ end
18
+
19
+ def verbose?
20
+ verbose.present?
21
+ end
22
+
23
+ def [](key)
24
+ send(key)
25
+ end
26
+
27
+ def []=(key, value)
28
+ send(:"#{key}=", value)
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kozo
4
+ class Provider
5
+ class_attribute :name
6
+ end
7
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kozo
4
+ module Providers
5
+ module HCloud
6
+ class Provider < Kozo::Provider
7
+ attr_accessor :key
8
+
9
+ self.name = "hcloud"
10
+
11
+ def ==(other)
12
+ key == other.key
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kozo
4
+ module Providers
5
+ module HCloud
6
+ class Resource < Kozo::Resource
7
+ self.provider = "hcloud"
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kozo
4
+ module Providers
5
+ module HCloud
6
+ module Resources
7
+ class SSHKey < Resource
8
+ attr_accessor :name, :public_key
9
+
10
+ self.name = "hcloud_ssh_key"
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kozo
4
+ module Providers
5
+ module Null
6
+ class Provider < Kozo::Provider
7
+ self.name = "null"
8
+
9
+ def ==(_other)
10
+ true
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kozo
4
+ module Providers
5
+ module Null
6
+ class Resource < Kozo::Resource
7
+ self.provider = "null"
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kozo
4
+ module Providers
5
+ module Null
6
+ module Resources
7
+ class Null < Resource
8
+ self.name = "null"
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end