bindan 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.
- checksums.yaml +7 -0
- data/.editorconfig +5 -0
- data/.envrc +2 -0
- data/.standard.yml +3 -0
- data/CHANGELOG.md +9 -0
- data/LICENSE +9 -0
- data/README.md +136 -0
- data/Rakefile +14 -0
- data/Steepfile +17 -0
- data/compose.yml +12 -0
- data/lib/bindan/emulators/firestore_controller.rb +152 -0
- data/lib/bindan/emulators/gcs_server_controller.rb +119 -0
- data/lib/bindan/error.rb +3 -0
- data/lib/bindan/providers/envvar.rb +27 -0
- data/lib/bindan/providers/firestore.rb +87 -0
- data/lib/bindan/providers/storage.rb +96 -0
- data/lib/bindan/providers.rb +1 -0
- data/lib/bindan/version.rb +5 -0
- data/lib/bindan.rb +24 -0
- data/rbs_collection.lock.yaml +328 -0
- data/rbs_collection.yaml +19 -0
- data/sig/bindan/emulators/firestore_controller.rbs +76 -0
- data/sig/bindan/emulators/gcs_server_controller.rbs +71 -0
- data/sig/bindan/error.rbs +4 -0
- data/sig/bindan/providers/envvar.rbs +22 -0
- data/sig/bindan/providers/firestore.rbs +47 -0
- data/sig/bindan/providers/storage.rbs +61 -0
- data/sig/bindan/version.rbs +3 -0
- data/sig/bindan.rbs +8 -0
- metadata +79 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 41a496e6f91cd22b627b887d5b0cb209cd3c6d277dedbf527dd38c774d444273
|
|
4
|
+
data.tar.gz: 6dea83b8c98edfa91989d2b488180e250710336cfe7b6470e257cec75c292c89
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 0ffd5b03d4a1c9a76c696a514fbaa5bac9d323820dc19c88229b0ba88c6e6e893b6bd6b2e8d1659d325e4f235f906b87d14e37741ed0ba7045e11bcf8be4dcf7
|
|
7
|
+
data.tar.gz: 5a46f6c97809b06dc6dc4aa7b75a2e5093059b71f6baeb6fd75450f303471b7f005fee21f995787866d2341b1440a1f3d5fedd8dcfb3be98e77f81e0bf000a0e
|
data/.editorconfig
ADDED
data/.envrc
ADDED
data/.standard.yml
ADDED
data/CHANGELOG.md
ADDED
data/LICENSE
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
Copyright 2025 wtnabe
|
|
2
|
+
|
|
3
|
+
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
|
4
|
+
|
|
5
|
+
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
|
|
6
|
+
|
|
7
|
+
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
|
8
|
+
|
|
9
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
data/README.md
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# Bindan
|
|
2
|
+
[](https://badge.fury.io/rb/bindan)
|
|
3
|
+
[](https://github.com/wtnabe/bindan/actions/workflows/main.yml)
|
|
4
|
+
|
|
5
|
+
Bindan is a Ruby gem for building single configuration object from various sources with providers (that is bundled Google Cloud Storage and Firestore platform by default). It provides a flexible way to manage application settings, supporting lazy initialization and seamless switching between development (using emulators) and production (eg. actual GCP services) environments.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **Environment Transparency**: Use the same code for development (with emulators) and production.
|
|
10
|
+
- **Multiple Providers**: Fetch configuration from:
|
|
11
|
+
- Environment Variables
|
|
12
|
+
- Google Cloud Storage
|
|
13
|
+
- Google Cloud Firestore
|
|
14
|
+
- your custom provider
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
Add this line to your application's Gemfile:
|
|
19
|
+
|
|
20
|
+
```ruby
|
|
21
|
+
gem 'bindan'
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
And then execute:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
$ bundle install
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Or install it yourself as:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
$ gem install bindan
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Usage
|
|
37
|
+
|
|
38
|
+
Here is a basic example of how to use Bindan.
|
|
39
|
+
|
|
40
|
+
### Dead Simple Use
|
|
41
|
+
|
|
42
|
+
Envvar built-in provider does not need extra code.
|
|
43
|
+
|
|
44
|
+
```ruby
|
|
45
|
+
require "bindan"
|
|
46
|
+
|
|
47
|
+
config = Bindan.configure do |c, pr|
|
|
48
|
+
c.database_host = pr.env["DATABASE_HOST"] || "localhost"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# unless $DATABASE_HOST environment variable
|
|
52
|
+
puts config.database_host # => "localhost"
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### with Multiple Providers
|
|
56
|
+
|
|
57
|
+
in Gemfile
|
|
58
|
+
|
|
59
|
+
```ruby
|
|
60
|
+
gem "bindan"
|
|
61
|
+
gem "google-cloud-storage"
|
|
62
|
+
gem "google-cloud-firestore"
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
and
|
|
66
|
+
|
|
67
|
+
```ruby
|
|
68
|
+
require "bindan"
|
|
69
|
+
|
|
70
|
+
# Define your configuration providers
|
|
71
|
+
providers = {
|
|
72
|
+
env: Bindan::Provider::Envvar.new,
|
|
73
|
+
storage: Bindan::Provider::Storage.new(bucket: "my-config-bucket"),
|
|
74
|
+
firestore: Bindan::Provider::Firestore.new(collection: "app-settings")
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
# Configure Bindan
|
|
78
|
+
config = Bindan.configure(providers: providers) do |c, pr|
|
|
79
|
+
# Define configuration keys and how they are loaded from providers and fallback
|
|
80
|
+
c.database_host = pr.env["DATABASE_HOST"] || "localhost"
|
|
81
|
+
c.api_key = pr.storage["api_key"]
|
|
82
|
+
c.feature_flags = pr.firestore["feature_flags_document"]
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Access your configuration values
|
|
86
|
+
puts config.database_host # => "localhost" (or value from ENV["DATABASE_HOST"])
|
|
87
|
+
puts config.api_key # => (Value loaded from Google Cloud Storage)
|
|
88
|
+
puts config.feature_flags # => (Value loaded from Firestore)
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
In the `configure` block, you define how each configuration key is mapped to a provider. The provider name is `providers` Hash key.
|
|
92
|
+
|
|
93
|
+
## Development
|
|
94
|
+
|
|
95
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
|
96
|
+
|
|
97
|
+
### Prerequisites for Development
|
|
98
|
+
|
|
99
|
+
This gem is designed to interacts with Google Cloud services as a first-class citizen. For local development and testing, it uses emulators. Please install the following tools:
|
|
100
|
+
|
|
101
|
+
1. **Google Cloud CLI:** [Installation the gcloud CLI](https://cloud.google.com/sdk/docs/install)
|
|
102
|
+
2. **Docker:** [Get Docker](https://docs.docker.com/get-docker/)
|
|
103
|
+
3. **Firestore Emulator:**
|
|
104
|
+
```bash
|
|
105
|
+
gcloud components install cloud-firestore-emulator
|
|
106
|
+
```
|
|
107
|
+
4. **fake-gcs-server:**
|
|
108
|
+
```bash
|
|
109
|
+
docker pull fsouza/fake-gcs-server
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Custom Provider
|
|
113
|
+
|
|
114
|
+
implement `[]` method.
|
|
115
|
+
|
|
116
|
+
```ruby
|
|
117
|
+
class CustomProvider
|
|
118
|
+
def [](key)
|
|
119
|
+
...
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Running Tests
|
|
125
|
+
|
|
126
|
+
The test suite is configured to automatically start the emulators (including `fake-gcs-server` via Docker), run the tests against them, and shut them down. Simply run:
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
bundle exec rake spec
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
|
133
|
+
|
|
134
|
+
## Contributing
|
|
135
|
+
|
|
136
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/wtnabe/bindan.
|
data/Rakefile
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bundler/gem_tasks"
|
|
4
|
+
require "minitest/test_task"
|
|
5
|
+
|
|
6
|
+
Minitest::TestTask.create(:spec) do |t|
|
|
7
|
+
t.libs << "spec"
|
|
8
|
+
t.test_globs = "spec/**/*_spec.rb"
|
|
9
|
+
t.warning = false
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
require "standard/rake"
|
|
13
|
+
|
|
14
|
+
task default: %i[spec standard]
|
data/Steepfile
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# D = Steep::Diagnostic
|
|
2
|
+
#
|
|
3
|
+
target :lib do
|
|
4
|
+
signature "sig"
|
|
5
|
+
|
|
6
|
+
check "lib" # Directory name
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
# target :test do
|
|
10
|
+
# unreferenced! # Skip type checking the `lib` code when types in `test` target is changed
|
|
11
|
+
# signature "sig/test" # Put RBS files for tests under `sig/test`
|
|
12
|
+
# check "test" # Type check Ruby scripts under `test`
|
|
13
|
+
#
|
|
14
|
+
# configure_code_diagnostics(D::Ruby.lenient) # Weak type checking for test code
|
|
15
|
+
#
|
|
16
|
+
# # library "pathname" # Standard libraries
|
|
17
|
+
# end
|
data/compose.yml
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
services:
|
|
2
|
+
storage:
|
|
3
|
+
image: fsouza/fake-gcs-server:latest
|
|
4
|
+
command: -scheme http -port 4443 -host 0.0.0.0
|
|
5
|
+
ports:
|
|
6
|
+
- "4443:4443"
|
|
7
|
+
healthcheck:
|
|
8
|
+
# The fake-gcs-server is ready when we can query the bucket list endpoint.
|
|
9
|
+
test: ["CMD", "curl", "-f", "http://localhost:4443/storage/v1/b"]
|
|
10
|
+
interval: 1s
|
|
11
|
+
timeout: 5s
|
|
12
|
+
retries: 20
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
require_relative "../error"
|
|
2
|
+
require "fileutils"
|
|
3
|
+
require "open3"
|
|
4
|
+
require "socket"
|
|
5
|
+
require "timeout"
|
|
6
|
+
require "uri"
|
|
7
|
+
|
|
8
|
+
module Bindan
|
|
9
|
+
module Emulator
|
|
10
|
+
#
|
|
11
|
+
# Represents and controls a single instance of the Google Cloud Firestore emulator process.
|
|
12
|
+
# This class encapsulates the logic for starting, stopping, and waiting for the emulator,
|
|
13
|
+
# separating process management from the test execution flow.
|
|
14
|
+
#
|
|
15
|
+
class FirestoreController
|
|
16
|
+
WAIT_TIMEOUT = 15 # seconds
|
|
17
|
+
INITIAL_BACKOFF_KEY_SEC = 0.1 # seconds
|
|
18
|
+
BACKOFF_MULTIPLIER = 1.5
|
|
19
|
+
|
|
20
|
+
class << self
|
|
21
|
+
#
|
|
22
|
+
# @return [Array<String, Integer>]
|
|
23
|
+
#
|
|
24
|
+
def host_and_port(host = nil, port = nil)
|
|
25
|
+
host, port = ENV["FIRESTORE_EMULATOR_HOST"].to_s.split(":") if ENV["FIRESTORE_EMULATOR_HOST"]
|
|
26
|
+
|
|
27
|
+
host ||= ENV["CI"] ? "0.0.0.0" : "localhost"
|
|
28
|
+
port ||= 8080
|
|
29
|
+
|
|
30
|
+
[host, port]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
#
|
|
34
|
+
# Initiates the emulator process and returns a new instance.
|
|
35
|
+
#
|
|
36
|
+
# @param [String] import
|
|
37
|
+
# @param [String] export
|
|
38
|
+
# @param [String] host
|
|
39
|
+
# @param [Integer] port
|
|
40
|
+
# @param [bool] close_io
|
|
41
|
+
# @raise [Errno]
|
|
42
|
+
# @return [FirestoreEmulatorController] An instance to manage the emulator lifecycle.
|
|
43
|
+
#
|
|
44
|
+
def start(import: nil, export: nil, host: nil, port: nil, close_io: true)
|
|
45
|
+
host, port = host_and_port(host, port)
|
|
46
|
+
|
|
47
|
+
puts " Starting Firestore emulator ..." unless close_io
|
|
48
|
+
kill_process_if_already_exists(host: host, port: port, with_message: !close_io)
|
|
49
|
+
|
|
50
|
+
#
|
|
51
|
+
# This command is known to launch a background Java process and then exit.
|
|
52
|
+
# Trying keep process info with process group
|
|
53
|
+
#
|
|
54
|
+
cmd = "gcloud emulators firestore start --host-port=#{host}:#{port}"
|
|
55
|
+
cmd += " --import-data=#{import}" if import
|
|
56
|
+
cmd += " --export-on-exit=#{export}" if export
|
|
57
|
+
opts = close_io ? {out: "/dev/null", err: "/dev/null"} : {}
|
|
58
|
+
pid = Process.spawn(*cmd, pgroup: true, **opts) # steep:ignore
|
|
59
|
+
|
|
60
|
+
puts " Emulator process initiated." unless close_io
|
|
61
|
+
new(pid, host, port)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
#
|
|
65
|
+
# kill process group
|
|
66
|
+
#
|
|
67
|
+
# @param [Integer] pid
|
|
68
|
+
# @param [bool] with_message
|
|
69
|
+
# @return [void]
|
|
70
|
+
#
|
|
71
|
+
def stop(pid, with_message: true)
|
|
72
|
+
puts "\n=> Stopping Firestore emulator..." if with_message
|
|
73
|
+
|
|
74
|
+
begin
|
|
75
|
+
Process.kill("TERM", -pid)
|
|
76
|
+
Process.wait(-pid)
|
|
77
|
+
rescue Errno::ESRCH, Errno::ECHILD
|
|
78
|
+
# Process was already killed or doesn't exist, which is fine.
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
puts " Emulator process terminated." if with_message
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
#
|
|
85
|
+
# @param [Integer] host
|
|
86
|
+
# @param [Integer] port
|
|
87
|
+
#
|
|
88
|
+
def kill_process_if_already_exists(host:, port:, with_message: true)
|
|
89
|
+
TCPSocket.new(host, port).close
|
|
90
|
+
|
|
91
|
+
# `lsof -Fg -i:PORT` returns PIDs of the process listening on the port.
|
|
92
|
+
process_ids, = Open3.capture3(*"lsof -Fg -g -s TCP:LISTEN -i :#{port}".split(" "))
|
|
93
|
+
process_group = process_ids.lines.map(&:chomp).find { |id| id =~ /^g([0-9]+)/ }
|
|
94
|
+
|
|
95
|
+
if process_group
|
|
96
|
+
id = process_group[1..].to_i # strip first letter
|
|
97
|
+
puts " Found emulator process group #{id} on port #{port}. Terminating." if with_message
|
|
98
|
+
stop(id, with_message: false)
|
|
99
|
+
end
|
|
100
|
+
rescue Errno::ECONNREFUSED
|
|
101
|
+
# noop
|
|
102
|
+
end
|
|
103
|
+
end # of class methods
|
|
104
|
+
|
|
105
|
+
#
|
|
106
|
+
# @param [Integer] pid - process group id
|
|
107
|
+
#
|
|
108
|
+
def initialize(pid, host, port)
|
|
109
|
+
@pid = pid
|
|
110
|
+
@host = host
|
|
111
|
+
@port = port
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
#
|
|
115
|
+
# waits to accessable
|
|
116
|
+
#
|
|
117
|
+
# @raise [Timeout::Error] if the emulator does not become available within the configured timeout.
|
|
118
|
+
# @param [number] timeout
|
|
119
|
+
# @param [number] backoff
|
|
120
|
+
# @param [number] multiplier
|
|
121
|
+
# @return [void]
|
|
122
|
+
#
|
|
123
|
+
def wait_available(timeout: WAIT_TIMEOUT, backoff: INITIAL_BACKOFF_KEY_SEC, multiplier: BACKOFF_MULTIPLIER, with_message: true)
|
|
124
|
+
last_rescue_error = nil
|
|
125
|
+
|
|
126
|
+
puts " Waiting for emulator to be ready on #{@host}:#{@port}..." if with_message
|
|
127
|
+
count = 1
|
|
128
|
+
Timeout.timeout(timeout) do
|
|
129
|
+
loop do
|
|
130
|
+
TCPSocket.new(@host, @port).close
|
|
131
|
+
puts " Emulator is up and listening." if with_message
|
|
132
|
+
return
|
|
133
|
+
rescue Errno::ECONNREFUSED, Errno::EADDRNOTAVAIL => e
|
|
134
|
+
last_rescue_error = e
|
|
135
|
+
sleep backoff
|
|
136
|
+
count += 1
|
|
137
|
+
backoff *= multiplier**count
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
rescue Timeout::Error
|
|
141
|
+
raise last_rescue_error # steep:ignore
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
#
|
|
145
|
+
# @return [void]
|
|
146
|
+
#
|
|
147
|
+
def stop(with_message: true)
|
|
148
|
+
self.class.stop(@pid, with_message: with_message)
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
require_relative "../error"
|
|
2
|
+
require "fileutils"
|
|
3
|
+
require "timeout"
|
|
4
|
+
require "open3"
|
|
5
|
+
|
|
6
|
+
module Bindan
|
|
7
|
+
module Emulator
|
|
8
|
+
class GcsServerController
|
|
9
|
+
class ContainerCannotOpenError < Error; end
|
|
10
|
+
|
|
11
|
+
CONTAINER_NAME = "gcs-server"
|
|
12
|
+
WAIT_TIMEOUT = 15 # seconds
|
|
13
|
+
INITIAL_BACKOFF_KEY_SEC = 0.1 # seconds
|
|
14
|
+
BACKOFF_MULTIPLIER = 1.5
|
|
15
|
+
|
|
16
|
+
class << self
|
|
17
|
+
#
|
|
18
|
+
# @raise [Errno]
|
|
19
|
+
# @param [String] folder
|
|
20
|
+
# @param [Integer] [port]
|
|
21
|
+
# @param [String] [name]
|
|
22
|
+
# @return [GcsServerController]
|
|
23
|
+
#
|
|
24
|
+
def start(folder:, port: 4443, name: CONTAINER_NAME, close_io: true)
|
|
25
|
+
FileUtils.mkdir_p(folder)
|
|
26
|
+
unless running?(name: name)
|
|
27
|
+
opts = close_io ? {out: "/dev/null", err: "/dev/null"} : {}
|
|
28
|
+
Process.spawn( # steep:ignore
|
|
29
|
+
*"docker run --rm --name #{name} -p #{port}:4443 -v #{folder}:/data fsouza/fake-gcs-server -scheme http".split(" "),
|
|
30
|
+
**opts
|
|
31
|
+
)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
new(name: name, folder: folder)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
#
|
|
38
|
+
# @raise [IOError]
|
|
39
|
+
# @param [String] name
|
|
40
|
+
# @return [bool]
|
|
41
|
+
#
|
|
42
|
+
def running?(name:)
|
|
43
|
+
_stdin, stdout, stderr, = Open3.popen3("docker container list -f name=#{name}")
|
|
44
|
+
|
|
45
|
+
begin
|
|
46
|
+
# container list not only headers
|
|
47
|
+
stdout.readlines.size > 1
|
|
48
|
+
rescue IOError => e
|
|
49
|
+
warn "in #{self}"
|
|
50
|
+
warn " " + stderr.read
|
|
51
|
+
raise ContainerCannotOpenError.new e
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
#
|
|
56
|
+
# @param [String] name
|
|
57
|
+
# @raise [Errno]
|
|
58
|
+
# @return [void]
|
|
59
|
+
#
|
|
60
|
+
def stop(name:)
|
|
61
|
+
`docker container stop #{name}`
|
|
62
|
+
end
|
|
63
|
+
end # of class methods
|
|
64
|
+
|
|
65
|
+
#
|
|
66
|
+
# @param [String] name
|
|
67
|
+
# @param [String] folder
|
|
68
|
+
#
|
|
69
|
+
def initialize(name:, folder:)
|
|
70
|
+
@container_name = name
|
|
71
|
+
@folder = folder
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
#
|
|
75
|
+
# @return
|
|
76
|
+
#
|
|
77
|
+
def stop
|
|
78
|
+
self.class.stop(name: @container_name)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
#
|
|
82
|
+
# @param [String] name
|
|
83
|
+
# @return [bool]
|
|
84
|
+
#
|
|
85
|
+
def running?
|
|
86
|
+
self.class.running?(name: @container_name)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
#
|
|
90
|
+
# waits container to be running
|
|
91
|
+
#
|
|
92
|
+
# @raise [ContainerCannotOpenError] if the container does not become available within the configured timeout.
|
|
93
|
+
# @param [number] timeout
|
|
94
|
+
# @param [number] backoff
|
|
95
|
+
# @param [number] multiplier
|
|
96
|
+
# @return [void]
|
|
97
|
+
#
|
|
98
|
+
def wait_available(timeout: WAIT_TIMEOUT, backoff: INITIAL_BACKOFF_KEY_SEC, multiplier: BACKOFF_MULTIPLIER)
|
|
99
|
+
last_rescued_error = nil
|
|
100
|
+
|
|
101
|
+
count = 1
|
|
102
|
+
Timeout.timeout(timeout) do
|
|
103
|
+
loop do
|
|
104
|
+
if running?
|
|
105
|
+
return
|
|
106
|
+
end
|
|
107
|
+
rescue ContainerCannotOpenError => e
|
|
108
|
+
last_rescued_error = e
|
|
109
|
+
sleep backoff
|
|
110
|
+
count += 1
|
|
111
|
+
backoff *= multiplier**count
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
rescue Timeout::Error
|
|
115
|
+
raise last_rescued_error # steep:ignore
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
data/lib/bindan/error.rb
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
module Bindan
|
|
2
|
+
module Provider
|
|
3
|
+
class Envvar
|
|
4
|
+
#
|
|
5
|
+
# @param [Hash] env
|
|
6
|
+
# @param [Hash] options
|
|
7
|
+
#
|
|
8
|
+
def initialize(env = ENV.to_h, options = {})
|
|
9
|
+
@_env = env
|
|
10
|
+
@_options = options
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
#
|
|
14
|
+
# @raise KeyError
|
|
15
|
+
# @param [String] key
|
|
16
|
+
# @return [String]
|
|
17
|
+
#
|
|
18
|
+
def [](key)
|
|
19
|
+
if @_options[:raise_error]
|
|
20
|
+
@_env.fetch(key)
|
|
21
|
+
else
|
|
22
|
+
@_env[key]
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
require_relative "../error"
|
|
2
|
+
begin
|
|
3
|
+
require "google/cloud/firestore"
|
|
4
|
+
rescue LoadError => e
|
|
5
|
+
warn e
|
|
6
|
+
module Google
|
|
7
|
+
module Cloud
|
|
8
|
+
class Firestore
|
|
9
|
+
def initialize(**kwargs)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def doc(*args)
|
|
13
|
+
Class.new do
|
|
14
|
+
def create(*args)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def get
|
|
18
|
+
Class.new do
|
|
19
|
+
def data
|
|
20
|
+
end
|
|
21
|
+
end.new
|
|
22
|
+
end
|
|
23
|
+
end.new
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
module Bindan
|
|
31
|
+
module Provider
|
|
32
|
+
class Firestore
|
|
33
|
+
class ColOrDocNotExist < Error; end
|
|
34
|
+
|
|
35
|
+
#
|
|
36
|
+
# @param [String] collection
|
|
37
|
+
# @param [bool] raise_error
|
|
38
|
+
# @param [Google::Cloud::Firestore] sdk
|
|
39
|
+
#
|
|
40
|
+
def initialize(collection = nil, project_id: nil, raise_error: false, sdk: ::Google::Cloud::Firestore, **kwargs)
|
|
41
|
+
if ENV["FIRESTORE_EMULATOR_HOST"]
|
|
42
|
+
project_id ||= "project"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
@_options = {}
|
|
46
|
+
@collection = collection
|
|
47
|
+
|
|
48
|
+
@_options[:raise_error] = raise_error
|
|
49
|
+
@project_id = project_id
|
|
50
|
+
|
|
51
|
+
@_firestore = sdk.new(project_id: project_id, **kwargs)
|
|
52
|
+
end
|
|
53
|
+
attr_reader :project_id
|
|
54
|
+
|
|
55
|
+
#
|
|
56
|
+
# @param [Hash] pair
|
|
57
|
+
#
|
|
58
|
+
def _prepare(pair)
|
|
59
|
+
path, doc = pair.to_a.flatten
|
|
60
|
+
|
|
61
|
+
@_firestore.doc(path).create(doc)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
#
|
|
65
|
+
# @raise ColOrDocNotExist
|
|
66
|
+
# @param [String] key
|
|
67
|
+
# @return [Hash]
|
|
68
|
+
#
|
|
69
|
+
def [](key)
|
|
70
|
+
doc_path =
|
|
71
|
+
if @collection.nil? && key.include?("/")
|
|
72
|
+
key
|
|
73
|
+
else
|
|
74
|
+
[@collection, key].join("/")
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
doc = @_firestore.doc(doc_path).get
|
|
78
|
+
|
|
79
|
+
if @_options[:raise_error] && !doc.data
|
|
80
|
+
raise ColOrDocNotExist.new doc_path
|
|
81
|
+
else
|
|
82
|
+
doc.data
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|