lamma 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,61 @@
1
+ require 'pathname'
2
+ require 'lamma/runtime'
3
+
4
+ module Lamma
5
+ class CLI::Init
6
+ include Thor::Actions
7
+ attr_reader :target, :thor, :options, :runtime
8
+
9
+ def initialize(options, function_name, thor)
10
+ @options = options
11
+ @thor = thor
12
+ runtime_str = options.fetch('runtime') { raise ArgumentError.new('runtime must be set.') }
13
+ @runtime = Lamma::Runtime.new(runtime_str)
14
+ @target = Pathname.pwd.join(function_name)
15
+ end
16
+
17
+ def run
18
+ if Dir.exist?(target)
19
+ abort("Directory '#{target}' already exists.")
20
+ end
21
+
22
+ tpath = File.join(File.dirname(__FILE__), '..', 'templates', runtime.to_dirname)
23
+ templates = Dir.glob(File.join(tpath, '**/*.erb')).map do |path|
24
+ tar_file = path[tpath.length..path.length - 5] # /foo/bar/templates/RUNTIME/baz/lambda_function.py.erb => /baz/lambda_function.py
25
+
26
+ [File.expand_path(path), File.join(target, tar_file)]
27
+ end.to_h
28
+
29
+ unless Dir.exists?(target)
30
+ FileUtils.makedirs(target)
31
+ end
32
+
33
+ templates.each do |src, dst|
34
+ File.open(dst, "w") do |f|
35
+ template = File.read(src)
36
+ formatted = ERB.new(template).result(binding)
37
+ f.write(formatted)
38
+ end
39
+ end
40
+
41
+ Dir.chdir(target) do
42
+ `git init`
43
+ `git add .`
44
+ end
45
+ end
46
+ end
47
+
48
+ private
49
+
50
+ def default_region
51
+ 'us-east-1'
52
+ end
53
+
54
+ def default_account_id
55
+ nil
56
+ end
57
+
58
+ def default_role_name
59
+ nil
60
+ end
61
+ end
@@ -0,0 +1,60 @@
1
+ require 'yaml'
2
+
3
+ require 'lamma'
4
+ require 'lamma/function'
5
+ require 'lamma/alias'
6
+
7
+ module Lamma
8
+ class CLI::Rollback
9
+ attr_reader :options, :thor, :path
10
+
11
+ def initialize(options, thor)
12
+ @options = options
13
+ @thor = thor
14
+ @conf_path = options['path'] || Lamma::DEFAULT_CONF_PATH
15
+ end
16
+
17
+ def run
18
+ unless File.exists?(@conf_path)
19
+ abort("Config file #{@conf_path} is missing.")
20
+ end
21
+
22
+ f = Lamma::Function.new(@conf_path)
23
+
24
+ unless f.remote_exists?
25
+ abort("Remote function #{f.name} doesn't seem to be exists. You have to create or deploy it first")
26
+ end
27
+
28
+ unless options['alias']
29
+ abort("You can't rollback with alias (-a) option.")
30
+ end
31
+
32
+ a = Lamma::Alias.new(f, options['alias'])
33
+
34
+ unless a.remote_exists?
35
+ abort("Alias #{a.name} doesn't exist. You have to deploy the function first.")
36
+ end
37
+
38
+ la = Lamma::Alias.new(f, "#{options['alias']}_#{Lamma::LAST_DEPLOY_SUFFIX}")
39
+
40
+ unless la.remote_exists?
41
+ abort("Alias #{la.name} doesn't exist. You have to deploy the function first.")
42
+ end
43
+
44
+ v = a.remote_version
45
+ lv = la.remote_version
46
+
47
+ if v == lv
48
+ abort("Aliases #{a.name} and #{la.name} indicates same version #{v}. Deploy it first?")
49
+ end
50
+
51
+ a.version = lv
52
+ a.update
53
+ end
54
+
55
+ private
56
+ def templates_path
57
+ File.expand_path(File.join(File.dirname(__FILE__), '../templates'))
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,20 @@
1
+ module Lamma
2
+ class CLI::Show
3
+ def initialize(options, function_name, thor)
4
+ @options = options
5
+ @thor = thor
6
+ @function_name = function_name
7
+ @conf_path = options[:path] || Lamma::DEFAULT_CONF_PATH
8
+ end
9
+
10
+ def run
11
+ f = Lamma::Function.new(@function_name)
12
+
13
+ unless f.remote_exists?
14
+ thor.say("Function #{f.name} doesn't exist in remote.")
15
+ end
16
+
17
+ thor.say(f) # XXX:
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,133 @@
1
+ require 'digest/md5'
2
+ require 'zip'
3
+
4
+ require 'lamma/logger'
5
+
6
+ module Lamma
7
+ class Code
8
+ BUILD_FILE_NAME = 'build.zip'
9
+
10
+ def initialize(function, yaml)
11
+ @function = function
12
+ @source_path = yaml.fetch('source_path', '.')
13
+ @prebuild = yaml.fetch('prebuild', nil)
14
+ @build_path = yaml.fetch('build_path', nil)
15
+ end
16
+
17
+ def zip_io
18
+ return @zip_io if @zip_io
19
+
20
+ if h = cached_build_hash
21
+ bp = File.join(build_path, h, BUILD_FILE_NAME)
22
+ Lamma.logger.info "Using cached build: #{bp}"
23
+ @zip_io = File.open(bp, 'rb')
24
+ else
25
+ unless File.directory?(@source_path)
26
+ raise "Source path #{@source_path} doesn't exists"
27
+ end
28
+
29
+ # prebuild script
30
+ if @prebuild
31
+ Lamma.logger.info 'Running prebuild script...'
32
+ raise unless system(@prebuild)
33
+ elsif @function.runtime == Lamma::Runtime::PYTHON_27 \
34
+ && File.exist?(File.join(@source_path, 'requirements.txt'))
35
+ raise unless system("pip", "install", "-r", "requirements.txt", "-t", ".")
36
+ # XXX: verbose?
37
+ elsif [Lamma::Runtime::EDGE_NODE_43, Lamma::Runtime::NODE_43].include? @function.runtime \
38
+ && File.exist?(File.join(@source_path, 'package.json'))
39
+ raise unless system("npm", "install", "--production")
40
+ end
41
+
42
+ io = Zip::OutputStream.write_buffer do |zio|
43
+ active_paths.each do |path|
44
+ next unless File.file?(path)
45
+ File.open(path) do |source_io|
46
+ zio.put_next_entry(path)
47
+ data = source_io.read
48
+ zio.write(data)
49
+ end
50
+ end
51
+ end
52
+
53
+ io.rewind
54
+ if true # XXX: option save_builds?
55
+ File.open(File.join(zipfile_path, BUILD_FILE_NAME), 'w').write(io.read)
56
+ Lamma.logger.info("Saved the build: #{zipfile_path}")
57
+ io.rewind
58
+ end
59
+ @zip_io = io
60
+ end
61
+
62
+ @zip_io
63
+ end
64
+
65
+ def to_h
66
+ {
67
+ zip_file: zip_io
68
+ }
69
+ end
70
+
71
+ private
72
+
73
+ def cached_build_hash
74
+ Dir.glob(File.join(build_path, "*/"))
75
+ .map{|path| File.basename(path)}.each do |h|
76
+ if hash == h
77
+ return h
78
+ end
79
+ end
80
+
81
+ nil
82
+ end
83
+
84
+ def hash
85
+ return @hash if @hash
86
+
87
+ h = Digest::MD5.new
88
+
89
+ active_paths.each do |path|
90
+ next unless File.file?(path)
91
+ h.update(path)
92
+ t = Digest::MD5.file(path)
93
+ h.update(t.digest)
94
+ end
95
+
96
+ @hash = h.to_s
97
+ end
98
+
99
+ def active_paths
100
+ Dir.glob(File.join(@source_path, '**/*')).select do |path|
101
+ !ignores.include?(path)
102
+ end
103
+ end
104
+
105
+ def build_path
106
+ if @build_path
107
+ unless File.directory?(@build_path)
108
+ FileUtils.mkdir_p(@build_path)
109
+ end
110
+ else
111
+ @build_path = File.join(Dir.tmpdir, 'lamma')
112
+ end
113
+
114
+ @build_path
115
+ end
116
+
117
+ def zipfile_path
118
+ return @zipfile_path if @zipfile_path
119
+
120
+ path = File.join(build_path, hash)
121
+ unless File.directory?(path)
122
+ FileUtils.mkdir_p(path)
123
+ end
124
+
125
+ @zipfile_path = path
126
+ end
127
+
128
+ def ignores
129
+ # TODO: impl
130
+ %w||
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,7 @@
1
+ module Lamma
2
+ class DeadLetterConfig
3
+ def initialize(yaml)
4
+ @target_arn = yaml.fetch('target_arn', nil)
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,13 @@
1
+ module Lamma
2
+ class Environment
3
+ def initialize(yaml)
4
+ @variables = yaml.fetch('variables', {})
5
+ end
6
+
7
+ def to_h
8
+ {
9
+ variables: @variables
10
+ }
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,7 @@
1
+ require 'lamma'
2
+
3
+ module Lamma
4
+ class EventSourceMapping
5
+
6
+ end
7
+ end
@@ -0,0 +1,175 @@
1
+ require 'lamma'
2
+ require 'yaml'
3
+
4
+ require 'lamma/runtime'
5
+ require 'lamma/code'
6
+ require 'lamma/vpc_config'
7
+ require 'lamma/dead_letter_config'
8
+ require 'lamma/environment'
9
+
10
+ module Lamma
11
+ class Function
12
+ attr_reader :name, :region
13
+ attr_accessor :publish
14
+
15
+ def initialize(yaml_path)
16
+ path = Pathname.new(yaml_path)
17
+ @id = path.basename.sub_ext('').to_s
18
+ @root_path = path.parent
19
+ yaml = YAML.load(path.open)
20
+
21
+ @conf = yaml.fetch('function', {})
22
+
23
+ @publish = false
24
+ @name = @conf.fetch('name', nil)
25
+ @role_arn = @conf.fetch('role_arn', nil)
26
+ @description = @conf.fetch('description', nil)
27
+ @timeout = @conf.fetch('timeout', 3)
28
+ @memory_size = @conf.fetch('memory_size', 128)
29
+ # @dead_letter_config = dead_letter_config(@conf)
30
+ @region = @conf.fetch('region') { raise ValidationError.new('region must be set.') }
31
+ @kms_key_arn = @conf.fetch('kms_key_arn', nil)
32
+ end
33
+
34
+ def code
35
+ @code ||= Code.new(self, @conf.fetch('code', {}))
36
+ end
37
+
38
+ def create
39
+ Lamma.logger.info("Creating new function #{@name}...")
40
+
41
+ resp = lambda_client.create_function({
42
+ function_name: @name,
43
+ runtime: runtime.to_s,
44
+ role: @role_arn,
45
+ handler: handler,
46
+ code: code.to_h,
47
+ description: @description,
48
+ timeout: @timeout,
49
+ memory_size: @memory_size,
50
+ publish: publish,
51
+ vpc_config: vpc_config.to_h,
52
+ environment: environment.to_h,
53
+ kms_key_arn: @kms_key_arn
54
+ })
55
+
56
+ Lamma.logger.info(resp)
57
+ end
58
+
59
+ def update
60
+ Lamma.logger.info('Updating function configuration...')
61
+
62
+ resp = lambda_client.update_function_configuration({
63
+ function_name: @name,
64
+ runtime: runtime.to_s,
65
+ role: @role_arn,
66
+ handler: handler,
67
+ description: @description,
68
+ timeout: @timeout,
69
+ memory_size: @memory_size,
70
+ vpc_config: vpc_config.to_h,
71
+ environment: environment.to_h,
72
+ kms_key_arn: @kms_key_arn
73
+ })
74
+
75
+ Lamma.logger.info("Updated configuration for function: #{resp.function_arn}")
76
+
77
+ Lamma.logger.info('Updating function code...')
78
+
79
+ resp = lambda_client.update_function_code({
80
+ function_name: @name,
81
+ zip_file: code.to_h[:zip_file],
82
+ publish: publish
83
+ })
84
+
85
+ Lamma.logger.info("Updated code for function: #{resp.function_arn}")
86
+ end
87
+
88
+ def update_or_create
89
+ if remote_exists?
90
+ update
91
+ else
92
+ create
93
+ end
94
+ end
95
+
96
+ def aliases
97
+ lambda_client.list_aliases({
98
+ function_name: @name
99
+ }).aliases.map do |a|
100
+ Lamma::Alias.new(self, a.name, a.description)
101
+ end
102
+ end
103
+
104
+ def versions
105
+ lambda_client.list_versions_by_function({
106
+ function_name: @name
107
+ }).versions
108
+ end
109
+
110
+ def publish_version(v_desc, validate=nil)
111
+ Lamma.logger.info("Publishing...")
112
+ resp = lambda_client.publish_version({
113
+ function_name: @name,
114
+ code_sha_256: validate,
115
+ description: v_desc
116
+ })
117
+ Lamma.logger.info("Published $LATEST version as version #{resp.version} of funtion: #{resp.function_arn}")
118
+ resp
119
+ end
120
+
121
+ def remote_exists?
122
+ begin
123
+ lambda_client.get_function_configuration({
124
+ function_name: @name,
125
+ })
126
+ rescue Aws::Lambda::Errors::ResourceNotFoundException
127
+ false
128
+ end
129
+ end
130
+
131
+ private
132
+
133
+ def runtime
134
+ runtime_str = @conf.fetch('runtime') { raise ValidationError.new('runtime must be set') }
135
+
136
+ @runtime ||= Lamma::Runtime.new(runtime_str)
137
+ end
138
+
139
+ def handler
140
+ @conf.fetch('handler', default_handler)
141
+ end
142
+
143
+ def dead_letter_config
144
+ DeadLetterConfig.new(@conf.fetch('dead_letter_config', {}))
145
+ end
146
+
147
+ def environment
148
+ Environment.new(@conf.fetch('environment', {}))
149
+ end
150
+
151
+ def vpc_config
152
+ VpcConfig.new(@conf.fetch('vpc_config', {}))
153
+ end
154
+
155
+ def default_handler
156
+ case runtime.type
157
+ when Runtime::C_SHARP
158
+ raise ValidationError.new('handler must be set for C# runtime')
159
+ when Runtime::JAVA_8
160
+ raise ValidationError.new('handler must be set for Java8 runtime')
161
+ when Runtime::NODE_43
162
+ 'index.handler'
163
+ when Runtime::EDGE_NODE_43
164
+ 'index.handler'
165
+ when Runtime::PYTHON_27
166
+ 'lambda_function.lambda_handler'
167
+ end
168
+ end
169
+
170
+
171
+ def lambda_client
172
+ @lambda_client ||= Aws::Lambda::Client.new(region: @region)
173
+ end
174
+ end
175
+ end