bauble_core 0.1.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.
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bauble
4
+ module Cli
5
+ # Builds a docker command
6
+ class DockerCommandBuilder
7
+ def initialize
8
+ @command = 'docker run '
9
+ end
10
+
11
+ def with_rm
12
+ @command += '--rm '
13
+ self
14
+ end
15
+
16
+ def with_volume(volume)
17
+ @command += "-v #{volume} "
18
+ self
19
+ end
20
+
21
+ def with_workdir(workdir)
22
+ @command += "-w #{workdir} "
23
+ self
24
+ end
25
+
26
+ def with_entrypoint(entrypoint)
27
+ @command += "--entrypoint #{entrypoint} "
28
+ self
29
+ end
30
+
31
+ def with_platform(platform)
32
+ @command += "--platform #{platform} "
33
+ self
34
+ end
35
+
36
+ def with_image(image)
37
+ @command += "#{image} "
38
+ self
39
+ end
40
+
41
+ def with_command(cmd)
42
+ @command += "-c \"#{cmd}\""
43
+ self
44
+ end
45
+
46
+ def with_env(key, value)
47
+ @command += "-e #{key}=#{value} "
48
+ self
49
+ end
50
+
51
+ def build
52
+ @command
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'colorize'
4
+
5
+ module Bauble
6
+ module Cli
7
+ # cli logger
8
+ module Logger
9
+ class << self
10
+ def log(message)
11
+ print "[ Bauble ] #{message}".green
12
+ end
13
+
14
+ def block_log(message)
15
+ Logger.nl
16
+ Logger.log message
17
+ Logger.nl
18
+ end
19
+
20
+ def pulumi(message)
21
+ print "[ Pulumi ] #{message}".blue
22
+ end
23
+
24
+ def docker(message)
25
+ print "[ Docker ] #{message}".magenta
26
+ end
27
+
28
+ def nl(times = 1)
29
+ times.times { puts }
30
+ end
31
+
32
+ def debug(message)
33
+ puts "[ Bauble DEBUG ] #{message}".orange if ENV['BAUBLE_DEBUG']
34
+ end
35
+
36
+ def error(message)
37
+ nl
38
+ puts "[ Bauble Error ] #{message}".red
39
+ nl
40
+ end
41
+
42
+ def logo
43
+ puts <<-LOGO
44
+
45
+ ██████╗ █████╗ ██╗ ██╗██████╗ ██╗ ███████╗
46
+ ██╔══██╗██╔══██╗██║ ██║██╔══██╗██║ ██╔════╝
47
+ ██████╔╝███████║██║ ██║██████╔╝██║ █████╗
48
+ ██╔══██╗██╔══██║██║ ██║██╔══██╗██║ ██╔══╝
49
+ ██████╔╝██║ ██║╚██████╔╝██████╔╝███████╗███████╗
50
+ ╚═════╝ ╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚══════╝╚══════╝ v#{Bauble::VERSION}
51
+
52
+ LOGO
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'logger'
4
+ require 'English'
5
+
6
+ # pulumi wrapper
7
+ module Bauble
8
+ module Cli
9
+ # Pulumi class
10
+ class Pulumi
11
+ attr_accessor :config
12
+
13
+ def initialize(config:)
14
+ @config = config
15
+ end
16
+
17
+ def create_pulumi_yml(template)
18
+ Logger.debug 'Creating Pulumi.yaml...'
19
+ FileUtils.mkdir_p(@config.pulumi_home) unless Dir.exist?(@config.pulumi_home)
20
+ File.write("#{@config.pulumi_home}/Pulumi.yaml", template, mode: 'w')
21
+ end
22
+
23
+ def init!
24
+ init_pulumi unless pulumi_initialized?
25
+ end
26
+
27
+ def preview
28
+ Logger.debug "Running pulumi preview...\n"
29
+ output_command('preview')
30
+ end
31
+
32
+ def up(target = nil)
33
+ Logger.debug "Running pulumi up...\n"
34
+ output_command("up --yes#{target ? " --target #{target}" : ''}")
35
+ end
36
+
37
+ def destroy
38
+ Logger.debug "Running pulumi destroy...\n"
39
+ output_command('destroy --yes')
40
+ end
41
+
42
+ def create_or_select_stack(stack_name)
43
+ if stack_initialized?(stack_name)
44
+ Logger.debug "Selecting stack #{stack_name}"
45
+ select_stack(stack_name)
46
+ else
47
+ Logger.debug "Initializing stack #{stack_name}"
48
+ init_stack(stack_name)
49
+ end
50
+ end
51
+
52
+ private
53
+
54
+ def pulumi_initialized?
55
+ return false unless pulumi_yml_exists?
56
+
57
+ pulumi_logged_in?
58
+ end
59
+
60
+ def output_command(command)
61
+ Logger.nl
62
+ IO.popen("#{build_command(command)} 2>&1") do |io|
63
+ io.each do |line|
64
+ Logger.pulumi(line)
65
+ end
66
+ end
67
+ Logger.nl
68
+ end
69
+
70
+ def run_command(command)
71
+ `#{build_command(command)}#{silent_mode}`
72
+ end
73
+
74
+ def silent_mode
75
+ @config.debug ? '' : ' 2>/dev/null'
76
+ end
77
+
78
+ def build_command(command)
79
+ "pulumi #{command} #{global_flags.join(' ')}"
80
+ end
81
+
82
+ def global_flags
83
+ [
84
+ '--non-interactive',
85
+ "--cwd #{@config.pulumi_home}"
86
+ ]
87
+ end
88
+
89
+ def init_pulumi
90
+ login
91
+ end
92
+
93
+ def pulumi_yml_exists?
94
+ Logger.debug "Checking for Pulumi.yaml... #{File.exist?("#{@config.pulumi_home}/Pulumi.yaml")}"
95
+ File.exist?("#{@config.pulumi_home}/Pulumi.yaml")
96
+ end
97
+
98
+ def login
99
+ Logger.debug 'Logging into pulumi locally...'
100
+ run_command('login --local')
101
+ end
102
+
103
+ def pulumi_logged_in?
104
+ run_command('whoami')
105
+ Logger.debug "Checking pulumi login status... #{$CHILD_STATUS.success?}"
106
+ $CHILD_STATUS.success?
107
+ end
108
+
109
+ def init_stack(stack_name)
110
+ run_command("stack init --stack #{stack_name}")
111
+ end
112
+
113
+ def select_stack(stack_name)
114
+ run_command("stack select --stack #{stack_name}")
115
+ end
116
+
117
+ def stack_initialized?(stack_name)
118
+ Logger.debug "Checking if stack #{stack_name} is initialized..."
119
+ run_command('stack ls').include?(stack_name)
120
+ end
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'resource'
4
+
5
+ # Api Gateway v2
6
+ module Bauble
7
+ module Resources
8
+ # AWS API Gateway v2
9
+ class ApiGatewayV2 < Resource
10
+ attr_accessor :name, :routes, :app
11
+
12
+ def initialize(app, name:)
13
+ super(app)
14
+ @app = app
15
+ @name = name
16
+ @routes = []
17
+ end
18
+
19
+ def synthesize
20
+ base_hash = api_hash
21
+ base_hash.merge!(routes_hash)
22
+ base_hash.merge!(deploymeny_hash)
23
+ base_hash.merge!(synthesize_permissions)
24
+ base_hash
25
+ end
26
+
27
+ def add_route(route_key:, function:)
28
+ routes << { route_key: route_key, target_lambda_arn: "${#{function.name}.arn}", function_name: function.name }
29
+ end
30
+
31
+ def bundle
32
+ true
33
+ end
34
+
35
+ private
36
+
37
+ def routes_hash
38
+ routes.each_with_index.each_with_object({}) do |(route, index), route_hash|
39
+ route_name = "#{name}-route-#{index}"
40
+ route_hash[route_name] = {
41
+ 'type' => 'aws:apigatewayv2:Route',
42
+ 'properties' => {
43
+ 'apiId' => "${#{name}.id}",
44
+ 'routeKey' => route[:route_key],
45
+ 'target' => "integrations/${#{route_name}-integration.id}"
46
+ }
47
+ }
48
+ route_hash.merge!(synthesize_integration(route, route_name))
49
+ end
50
+ end
51
+
52
+ def synthesize_integration(route, route_name)
53
+ {
54
+ "#{route_name}-integration" => {
55
+ 'type' => 'aws:apigatewayv2:Integration',
56
+ 'properties' => {
57
+ 'apiId' => "${#{name}.id}",
58
+ 'integrationUri' => route[:target_lambda_arn],
59
+ 'integrationType' => 'AWS_PROXY',
60
+ 'payloadFormatVersion' => '2.0'
61
+ }
62
+ }
63
+ }
64
+ end
65
+
66
+ def synthesize_permissions
67
+ routes.uniq { |route| route[:function_name] }.each_with_object({}) do |route, permissions_hash|
68
+ permissions_hash.merge!(function_permission_hash(route[:function_name]))
69
+ end
70
+ end
71
+
72
+ def function_permission_hash(function_name)
73
+ {
74
+ "#{function_name}-api-gateway-permission" => {
75
+ 'type' => 'aws:lambda:Permission',
76
+ 'properties' => {
77
+ 'action' => 'lambda:InvokeFunction',
78
+ 'function' => "${#{function_name}.name}",
79
+ 'principal' => 'apigateway.amazonaws.com',
80
+ 'sourceArn' => "${#{name}.executionArn}/*/*"
81
+ }
82
+ }
83
+ }
84
+ end
85
+
86
+ def deploymeny_hash
87
+ {
88
+ "#{name}-deployment" => {
89
+ 'type' => 'aws:apigatewayv2:Deployment',
90
+ 'properties' => {
91
+ 'apiId' => "${#{name}.id}"
92
+ },
93
+ 'options' => {
94
+ 'dependsOn' => routes_hash.keys.map { |route_name| "${#{route_name}}" }
95
+ }
96
+ }
97
+ }
98
+ end
99
+
100
+ def api_hash
101
+ {
102
+ name => {
103
+ 'type' => 'aws:apigatewayv2:Api',
104
+ 'properties' => {
105
+ 'name' => name,
106
+ 'protocolType' => 'HTTP'
107
+ }
108
+ },
109
+ "#{name}-stage" => {
110
+ 'type' => 'aws:apigatewayv2:Stage',
111
+ 'properties' => {
112
+ 'apiId' => "${#{name}.id}",
113
+ 'name' => app.current_stack.name,
114
+ 'autoDeploy' => true
115
+ }
116
+ }
117
+ }
118
+ end
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'resource'
4
+
5
+ module Bauble
6
+ module Resources
7
+ # EventBridgeRule class
8
+ class EventBridgeRule < Resource
9
+ def initialize(app, rule_name:, **kwargs)
10
+ super(app)
11
+ @rule_name = rule_name
12
+ @description = kwargs.fetch(:description, 'Bauble EventBridge Rule')
13
+ @event_pattern = kwargs.fetch(:event_pattern, nil)
14
+ @schedule_expression = kwargs.fetch(:schedule_expression, nil)
15
+ @state = kwargs.fetch(:state, 'ENABLED')
16
+ @event_bus_name = kwargs.fetch(:event_bus_name, 'default')
17
+ @targets = []
18
+
19
+ validate_inputs
20
+ end
21
+
22
+ def synthesize
23
+ event_rule_hash = {
24
+ @rule_name => {
25
+ 'type' => 'aws:cloudwatch:EventRule',
26
+ 'properties' => {
27
+ 'name' => @rule_name,
28
+ 'description' => @description,
29
+ 'eventBusName' => @event_bus_name,
30
+ 'state' => @state
31
+ }
32
+ }
33
+ }
34
+
35
+ event_rule_hash[@rule_name]['properties']['eventPattern'] = @event_pattern.to_json if @event_pattern
36
+ event_rule_hash[@rule_name]['properties']['scheduleExpression'] = @schedule_expression if @schedule_expression
37
+
38
+ @targets.each do |target|
39
+ event_rule_hash.merge!(target)
40
+ end
41
+
42
+ event_rule_hash
43
+ end
44
+
45
+ def bundle
46
+ true
47
+ end
48
+
49
+ def add_target(function)
50
+ @targets << {
51
+ "#{@rule_name}-#{function.name}-target" => {
52
+ 'type' => 'aws:cloudwatch:EventTarget',
53
+ 'properties' => {
54
+ 'rule' => "${#{@rule_name}.name}",
55
+ 'arn' => "${#{function.name}.arn}"
56
+ }
57
+ },
58
+ "#{function.name}-permission" => {
59
+ 'type' => 'aws:lambda:Permission',
60
+ 'properties' => {
61
+ 'action' => 'lambda:InvokeFunction',
62
+ 'function' => "${#{function.name}.name}",
63
+ 'principal' => 'events.amazonaws.com',
64
+ 'sourceArn' => "${#{@rule_name}.arn}"
65
+ }
66
+ }
67
+ }
68
+ end
69
+
70
+ private
71
+
72
+ def validate_inputs
73
+ name_present?
74
+ pattern_or_schedule?
75
+ pattern_and_schedule?
76
+ end
77
+
78
+ def pattern_or_schedule?
79
+ return if @event_pattern || @schedule_expression
80
+
81
+ raise 'EventBridgeRule must have an event_pattern or a schedule_expression'
82
+ end
83
+
84
+ def pattern_and_schedule?
85
+ return unless @event_pattern && @schedule_expression
86
+
87
+ raise 'EventBridgeRule cannot have both an event_pattern and a schedule_expression'
88
+ end
89
+
90
+ def name_present?
91
+ return if @rule_name
92
+
93
+ raise 'EventBridgeRule must have a rule_name'
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'zip'
4
+ require_relative 'resource'
5
+ require_relative '../cli/code_bundler'
6
+
7
+ # Ruby function
8
+ module Bauble
9
+ module Resources
10
+ # a ruby lambda function
11
+ class GemLayer < Resource
12
+ def bundle
13
+ Bauble::Cli::CodeBundler.docker_bundle_gems(bundle_hash: @app.bundle_hash)
14
+ end
15
+
16
+ def synthesize
17
+ {
18
+ 'gemLayer' => {
19
+ 'type' => 'aws:lambda:LayerVersion',
20
+ 'name' => 'gem_layer',
21
+ 'properties' => {
22
+ 'code' => {
23
+ 'fn::fileArchive' => "#{@app.config.asset_dir}/#{@app.bundle_hash}/gem-layer"
24
+ },
25
+ 'layerName' => "#{@app.config.app_name}-gem-layer",
26
+ 'compatibleRuntimes' => %w[ruby3.2]
27
+ }
28
+ }
29
+ }
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'resource'
4
+
5
+ # Lambda role
6
+ module Bauble
7
+ module Resources
8
+ # aws lambda role
9
+ class LambdaRole < Resource
10
+ attr_accessor :role_name, :policy_statements
11
+
12
+ def initialize(app, role_name:, policy_statements: [])
13
+ super(app)
14
+ @role_name = role_name
15
+ @policy_statements = policy_statements
16
+ end
17
+
18
+ def synthesize
19
+ role_hash = {
20
+ role_name => {
21
+ 'type' => 'aws:iam:Role',
22
+ 'properties' => {
23
+ 'assumeRolePolicy' => assume_role_policy,
24
+ 'managedPolicyArns' => ['arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole']
25
+ }
26
+ }
27
+ }
28
+
29
+ return role_hash.merge(role_policy) if policy_statements.any?
30
+
31
+ role_hash
32
+ end
33
+
34
+ def add_policy_statement(effect:, actions:, resources:)
35
+ policy_statements << { effect: effect, actions: actions, resources: resources }
36
+ end
37
+
38
+ def bundle
39
+ true
40
+ end
41
+
42
+ private
43
+
44
+ def role_policy
45
+ {
46
+ "#{role_name}-policy" => {
47
+ 'type' => 'aws:iam:RolePolicy',
48
+ 'properties' => {
49
+ 'name' => "#{role_name}-policy",
50
+ 'role' => "${#{role_name}}",
51
+ 'policy' => synth_policies
52
+ }
53
+ }
54
+ }
55
+ end
56
+
57
+ def synth_policies
58
+ {
59
+ Version: '2012-10-17',
60
+ Statement: policy_statements.map do |statement|
61
+ {
62
+ Effect: statement[:effect].downcase == 'allow' ? 'Allow' : 'Deny',
63
+ Action: statement[:actions],
64
+ Resource: statement[:resources]
65
+ }
66
+ end
67
+ }.to_json
68
+ end
69
+
70
+ def assume_role_policy
71
+ {
72
+ 'Version' => '2012-10-17',
73
+ 'Statement' => [
74
+ {
75
+ 'Action' => 'sts:AssumeRole',
76
+ 'Principal' => {
77
+ 'Service' => 'lambda.amazonaws.com'
78
+ },
79
+ 'Effect' => 'Allow'
80
+ }
81
+ ]
82
+ }.to_json
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'resource'
4
+ require_relative '../cli/docker_command_builder'
5
+ require_relative '../cli/logger'
6
+
7
+ module Bauble
8
+ module Resources
9
+ # Postgres layer
10
+ class PostgresLayer < Resource
11
+ def bundle
12
+ IO.popen("docker build -t bauble_postgres_layer #{__dir__}/../cli/Dockerfile.postgres } 2>&1") do |io|
13
+ io.each do |line|
14
+ Bauble::Cli::Logger.docker(line)
15
+ end
16
+ end
17
+
18
+ IO.popen("#{docker_command} 2>&1") do |io|
19
+ io.each do |line|
20
+ Bauble::Cli::Logger.docker(line)
21
+ end
22
+ end
23
+ end
24
+
25
+ def synthesize
26
+ {}
27
+ end
28
+
29
+ private
30
+
31
+ def docker_command
32
+ Bauble::Cli::DockerCommandBuilder
33
+ .new
34
+ .with_rm
35
+ .with_volume('pg-layer:/opt/pgsql')
36
+ .with_platform('linux/amd64')
37
+ .with_image('bauble_postgres_layer')
38
+ .build
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bauble
4
+ module Resources
5
+ # Base resource
6
+ # TODO: this should just be Resource not base resource
7
+ class Resource
8
+ attr_accessor :app
9
+
10
+ def initialize(app)
11
+ @app = app
12
+ app.add_resource(self)
13
+ end
14
+
15
+ def synthesize
16
+ raise 'Not implemented'
17
+ end
18
+
19
+ def bundle
20
+ raise 'Not implemented'
21
+ end
22
+ end
23
+ end
24
+ end