envirobly 0.1.0 → 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 28058fa832ce9f45998bcd08d3d3c03306523487eae0d7dba1a2ef7059fc99d4
4
- data.tar.gz: d1f6a72c17c55275d67e927a140e4545e1c502474225285c7f346a26aaf83b4e
3
+ metadata.gz: 0baeb0d203dc1d0eb6b45c63b8ef52517d80d4ec7ebf22f0c8b0b5f460dfe955
4
+ data.tar.gz: 444b48b695e768e704e9fec2bf486062cddf39214fd8857a711011d1ac610b89
5
5
  SHA512:
6
- metadata.gz: 5fe021d182f01bb34d5af882c65e188df0b299308f0187aa90b01e1e0d4a0ae180dfd6e50cc7640c52740fe7eda1b917564d78300c7f3569c2a761d6c7e7033c
7
- data.tar.gz: 4cd9e4b06eb62600ce1c8e4e9102ef7005a09a4214623009b0f5e6698d9f793cb1e5d7c26dd011c51ab274f949a5b27d435c602149844e000de7fa85ac935dd3
6
+ metadata.gz: 7027f768d962e9cf28366a19b8fe7587876b939b2daee02da281dda61b35d9dddecaa11f23067350fb15dcb0ba3331294c4cef9c9f0ce4d93cedb9275daedca5
7
+ data.tar.gz: e1ad2ea6499d5999bf264030e9bb1bf654461534d46717adc0ce351de7d92c9dde7145c46f5b211c4802d2f57ddee02cb022d4324b9e53d59bba9bb752ce5f48
@@ -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,29 @@
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
+ abort_if_aws_cli_is_missing
13
+ Envirobly::Deployment.new environment, options
14
+ end
15
+
16
+ desc "set_access_token TOKEN", "Save and use an access token generated at Envirobly"
17
+ def set_access_token(token)
18
+ Envirobly::AccessToken.new(token).save
19
+ end
20
+
21
+ private
22
+ def abort_if_aws_cli_is_missing
23
+ `command -v aws`
24
+ unless $?.success?
25
+ $stderr.puts "AWS CLI is missing. Please install it first: https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html"
26
+ exit 1
27
+ end
28
+ end
6
29
  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.1"
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.1
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: {}