envirobly 0.1.0 → 0.3.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: 28058fa832ce9f45998bcd08d3d3c03306523487eae0d7dba1a2ef7059fc99d4
4
- data.tar.gz: d1f6a72c17c55275d67e927a140e4545e1c502474225285c7f346a26aaf83b4e
3
+ metadata.gz: 1dc280b944283bd549516dd80fab598dff0c4e093f8d16dada2e8f3663c650ab
4
+ data.tar.gz: b5aa5d9dbe07e35b00675830f900fa4a6ec7d29f894a9edb3ec780744bdf5123
5
5
  SHA512:
6
- metadata.gz: 5fe021d182f01bb34d5af882c65e188df0b299308f0187aa90b01e1e0d4a0ae180dfd6e50cc7640c52740fe7eda1b917564d78300c7f3569c2a761d6c7e7033c
7
- data.tar.gz: 4cd9e4b06eb62600ce1c8e4e9102ef7005a09a4214623009b0f5e6698d9f793cb1e5d7c26dd011c51ab274f949a5b27d435c602149844e000de7fa85ac935dd3
6
+ metadata.gz: c5d4f46cc0904a036b5d7c8f16aa9eab588a5cd943bf650d98277a257138295183a6e5769944528044613f990ae9d18024066b07cbf69a6ba40619e43e88189d
7
+ data.tar.gz: bc2f471d2b2e5f372cf52557154fc18ef9147c47592a1f9369bb28fceec129313226cf37668b71c954d5d8a8c92747abb34403b0bed892544f958b4cd2794eab
@@ -0,0 +1,36 @@
1
+ require "fileutils"
2
+ require "pathname"
3
+
4
+ class Envirobly::AccessToken
5
+ def initialize(token = ENV.fetch("ENVIROBLY_ACCESS_TOKEN", nil))
6
+ if token.nil? && File.exist?(access_token_path)
7
+ @token = File.read(access_token_path)
8
+ else
9
+ @token = token
10
+ end
11
+ end
12
+
13
+ def save
14
+ FileUtils.mkdir_p config_root
15
+ File.write access_token_path, @token
16
+ File.chmod 0600, access_token_path
17
+ puts "Access token saved to #{access_token_path}"
18
+ end
19
+
20
+ def as_http_bearer
21
+ "Bearer #{@token}"
22
+ end
23
+
24
+ private
25
+ def config_root
26
+ if ENV["XDG_CONFIG_HOME"]
27
+ Pathname.new(ENV["XDG_CONFIG_HOME"]).join("envirobly")
28
+ else
29
+ Pathname.new(Dir.home).join(".envirobly")
30
+ end
31
+ end
32
+
33
+ def access_token_path
34
+ config_root.join "access_token"
35
+ end
36
+ end
@@ -0,0 +1,82 @@
1
+ require "json"
2
+ require "net/http"
3
+ require "socket"
4
+ require "uri"
5
+
6
+ class Envirobly::Api
7
+ HOST = ENV["ENVIROBLY_API_HOST"] || "envirobly.com"
8
+ USER_AGENT = "Envirobly CLI v#{Envirobly::VERSION}"
9
+ CONTENT_TYPE = "application/json"
10
+
11
+ def initialize
12
+ @access_token = Envirobly::AccessToken.new
13
+ end
14
+
15
+ def create_deployment(params)
16
+ post_as_json(api_v1_deployments_url, params:, headers: authorization_headers).tap do |response|
17
+ unless response.code.to_i == 200
18
+ $stderr.puts "Deployment creation request responded with #{response.code}. Aborting."
19
+ exit 1
20
+ end
21
+ end
22
+ end
23
+
24
+ RETRY_INTERVAL_SECONDS = 3
25
+ MAX_RETRIES = 5
26
+ def get_deployment_with_delay_and_retry(url, tries = 1)
27
+ sleep RETRY_INTERVAL_SECONDS * tries
28
+ response = get_as_json URI(url)
29
+
30
+ if response.code.to_i == 200
31
+ return response
32
+ elsif MAX_RETRIES <= tries
33
+ $stderr.puts "Max retries exhausted while waiting for deployment credentials. Aborting."
34
+ exit 1
35
+ else
36
+ sleep RETRY_INTERVAL_SECONDS * tries
37
+ get_deployment_with_delay_and_retry(url, tries + 1)
38
+ end
39
+ end
40
+
41
+ private
42
+ def get_as_json(uri, headers: {})
43
+ request(uri, type: Net::HTTP::Get, headers:)
44
+ end
45
+
46
+ def post_as_json(uri, params: {}, headers: {})
47
+ request(uri, type: Net::HTTP::Post, headers:) do |request|
48
+ request.body = params.to_json
49
+ end
50
+ end
51
+
52
+ def api_v1_deployments_url
53
+ URI::HTTPS.build(host: HOST, path: "/api/v1/deployments")
54
+ end
55
+
56
+ def request(uri, type:, headers: {})
57
+ http = Net::HTTP.new uri.host, uri.port
58
+ http.use_ssl = true
59
+ http.open_timeout = 10
60
+ http.read_timeout = 10
61
+
62
+ headers = default_headers.merge headers
63
+ request = type.new(uri, headers)
64
+ request.content_type = CONTENT_TYPE
65
+
66
+ yield request if block_given?
67
+
68
+ http.request(request).tap do |response|
69
+ def response.object
70
+ @json_parsed_body ||= JSON.parse body
71
+ end
72
+ end
73
+ end
74
+
75
+ def default_headers
76
+ { "User-Agent" => USER_AGENT, "X-Cli-Host" => Socket.gethostname }
77
+ end
78
+
79
+ def authorization_headers
80
+ { "Authorization" => @access_token.as_http_bearer }
81
+ end
82
+ end
@@ -0,0 +1,17 @@
1
+ class Envirobly::Aws::Credentials
2
+ def initialize(params)
3
+ @params = params
4
+ end
5
+
6
+ def as_env_vars
7
+ [
8
+ %{AWS_ACCESS_KEY_ID="#{@params.fetch("access_key_id")}"},
9
+ %{AWS_SECRET_ACCESS_KEY="#{@params.fetch("secret_access_key")}"},
10
+ %{AWS_SESSION_TOKEN="#{@params.fetch("session_token")}"}
11
+ ]
12
+ end
13
+
14
+ def as_inline_env_vars
15
+ as_env_vars.join " "
16
+ end
17
+ end
@@ -0,0 +1,2 @@
1
+ module Envirobly::Aws
2
+ end
@@ -1,6 +1,19 @@
1
+ # require "debug"
2
+
1
3
  class Envirobly::Cli::Main < Envirobly::Base
2
4
  desc "version", "Show Envirobly CLI version"
3
5
  def version
4
6
  puts Envirobly::VERSION
5
7
  end
8
+
9
+ desc "deploy ENVIRONMENT", "Deploy to environment identified by name or URL"
10
+ method_option :commit, type: :string, default: "HEAD"
11
+ def deploy(environment)
12
+ Envirobly::Deployment.new environment, options
13
+ end
14
+
15
+ desc "set_access_token TOKEN", "Save and use an access token generated at Envirobly"
16
+ def set_access_token(token)
17
+ Envirobly::AccessToken.new(token).save
18
+ end
6
19
  end
data/lib/envirobly/cli.rb CHANGED
@@ -1 +1,2 @@
1
- module Envirobly::Cli; end
1
+ module Envirobly::Cli
2
+ end
@@ -0,0 +1,81 @@
1
+ require "yaml"
2
+ require "json"
3
+ require "digest"
4
+
5
+ class Envirobly::Config
6
+ DIR = ".envirobly"
7
+ PATH = "#{DIR}/project.yml"
8
+
9
+ attr_reader :parsing_error
10
+
11
+ def initialize(commit)
12
+ @commit = commit
13
+ @parsing_error = nil
14
+ @project = parse_config_content_at_commit
15
+
16
+ if @project
17
+ transform_env_var_values!
18
+ append_image_tags!
19
+ end
20
+ end
21
+
22
+ def dig(*args)
23
+ @project.dig *args
24
+ rescue NoMethodError
25
+ nil
26
+ end
27
+
28
+ def to_h
29
+ @project
30
+ end
31
+
32
+ def parsing_error?
33
+ !@parsing_error.nil?
34
+ end
35
+
36
+ def path
37
+ PATH
38
+ end
39
+
40
+ private
41
+ def parse_config_content_at_commit
42
+ YAML.load config_content_at_commit, aliases: true
43
+ rescue Psych::Exception => exception
44
+ @parsing_error = exception.message
45
+ nil
46
+ end
47
+
48
+ def config_content_at_commit
49
+ `git show #{@commit.ref}:#{path}`
50
+ end
51
+
52
+ def transform_env_var_values!
53
+ @project.fetch("services", {}).each do |logical_id, service|
54
+ service.fetch("env", {}).each do |key, value|
55
+ if value.is_a?(Hash) && value.has_key?("file")
56
+ @project["services"][logical_id]["env"][key] = File.read value.fetch("file")
57
+ end
58
+ end
59
+ end
60
+ end
61
+
62
+ NON_BUILDABLE_TYPES = %w[ postgres mysql valkey ]
63
+ def append_image_tags!
64
+ @project.fetch("services", {}).each do |logical_id, service|
65
+ next if NON_BUILDABLE_TYPES.include?(service["type"]) || service["image_uri"]
66
+
67
+ dockerfile = service.fetch("dockerfile", "Dockerfile")
68
+ build_context = service.fetch("build_context", ".")
69
+
70
+ @project["services"][logical_id]["image_tag"] = Digest::SHA1.hexdigest [
71
+ git_path_checksums_at_commit(dockerfile),
72
+ git_path_checksums_at_commit(build_context)
73
+ ].to_json
74
+ end
75
+ end
76
+
77
+ def git_path_checksums_at_commit(path)
78
+ `git ls-tree #{@commit.ref} --format='%(objectname) %(path)' #{path}`.
79
+ lines.reject { _1.split(" ").last == DIR }
80
+ end
81
+ end
@@ -0,0 +1,65 @@
1
+ class Envirobly::Deployment
2
+ URL_MATCHER = /^https:\/\/envirobly\.(test|com)\/(\d+)\/environs\/(\d+)$/
3
+
4
+ def initialize(environment, options)
5
+ @commit = Envirobly::Git::Commit.new options.commit
6
+
7
+ unless @commit.exists?
8
+ $stderr.puts "Commit #{options.commit} doesn't exist in this repository. Aborting."
9
+ exit 1
10
+ end
11
+
12
+ config = Envirobly::Config.new(@commit)
13
+ if config.parsing_error?
14
+ $stderr.puts "Error while parsing #{config.path}"
15
+ $stderr.puts config.parsing_error
16
+ exit 1
17
+ end
18
+
19
+ params = {
20
+ environ: {
21
+ logical_id: environment
22
+ },
23
+ commit: {
24
+ ref: @commit.ref,
25
+ time: @commit.time,
26
+ message: @commit.message
27
+ },
28
+ config: config.to_h
29
+ }
30
+
31
+ puts params.to_json
32
+
33
+ unless environment =~ URL_MATCHER
34
+ if project_url = config.dig("remote", "origin")
35
+ params[:environ][:project_url] = project_url
36
+ else
37
+ $stderr.puts "{remote.origin} is required in .envirobly/project.yml"
38
+ exit 1
39
+ end
40
+ end
41
+
42
+ api = Envirobly::Api.new
43
+ response = api.create_deployment params
44
+ response = api.get_deployment_with_delay_and_retry response.object.fetch("url")
45
+ @credentials = Envirobly::Aws::Credentials.new response.object.fetch("credentials")
46
+ @bucket = response.object.fetch("bucket")
47
+
48
+ if archive_commit_and_upload
49
+ $stderr.puts "Build context exported into #{archive_uri}"
50
+ else
51
+ $stderr.puts "Error exporting build context. Aborting."
52
+ exit 1
53
+ end
54
+ end
55
+
56
+ private
57
+ def archive_uri
58
+ "s3://#{@bucket}/#{@commit.ref}.tar.gz"
59
+ end
60
+
61
+ def archive_commit_and_upload
62
+ `git archive --format=tar.gz #{@commit.ref} | #{@credentials.as_inline_env_vars} aws s3 cp - #{archive_uri}`
63
+ $?.success?
64
+ end
65
+ end
@@ -0,0 +1,23 @@
1
+ require "time"
2
+
3
+ class Envirobly::Git::Commit
4
+ def initialize(ref)
5
+ @ref = ref
6
+ end
7
+
8
+ def exists?
9
+ `git cat-file -t #{@ref}`.chomp("") == "commit"
10
+ end
11
+
12
+ def ref
13
+ @normalized_ref ||= `git rev-parse #{@ref}`.chomp("")
14
+ end
15
+
16
+ def message
17
+ `git log #{@ref} -n1 --pretty=%B`.chomp("")
18
+ end
19
+
20
+ def time
21
+ Time.parse `git log #{@ref} -n1 --date=iso --pretty=format:"%ad"`
22
+ end
23
+ end
@@ -0,0 +1,2 @@
1
+ module Envirobly::Git
2
+ end
@@ -1,3 +1,3 @@
1
1
  module Envirobly
2
- VERSION = "0.1.0"
2
+ VERSION = "0.3.0"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: envirobly
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Robert Starsi
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-07-07 00:00:00.000000000 Z
11
+ date: 2024-09-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: thor
@@ -62,11 +62,19 @@ files:
62
62
  - LICENSE
63
63
  - bin/envirobly
64
64
  - lib/envirobly.rb
65
+ - lib/envirobly/access_token.rb
66
+ - lib/envirobly/api.rb
67
+ - lib/envirobly/aws.rb
68
+ - lib/envirobly/aws/credentials.rb
65
69
  - lib/envirobly/base.rb
66
70
  - lib/envirobly/cli.rb
67
71
  - lib/envirobly/cli/main.rb
72
+ - lib/envirobly/config.rb
73
+ - lib/envirobly/deployment.rb
74
+ - lib/envirobly/git.rb
75
+ - lib/envirobly/git/commit.rb
68
76
  - lib/envirobly/version.rb
69
- homepage: https://klevo.sk
77
+ homepage: https://github.com/envirobly/envirobly-cli
70
78
  licenses:
71
79
  - MIT
72
80
  metadata: {}