invar 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/.rspec +2 -0
- data/.rubocop.yml +26 -0
- data/.ruby-version +1 -0
- data/CODE_OF_CONDUCT.md +49 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +21 -0
- data/README.md +324 -0
- data/RELEASE_NOTES.md +75 -0
- data/Rakefile +15 -0
- data/invar.gemspec +41 -0
- data/lib/invar/file_locator.rb +86 -0
- data/lib/invar/rake.rb +47 -0
- data/lib/invar/rake_tasks.rb +178 -0
- data/lib/invar/scope.rb +58 -0
- data/lib/invar/test.rb +16 -0
- data/lib/invar/version.rb +6 -0
- data/lib/invar.rb +273 -0
- metadata +176 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 58a64d36feebf42e3d2e44e3cee20197683055dbb276f38701344db45a050e27
|
4
|
+
data.tar.gz: f39265a13a4ae8a49cb2b7944599c75bab6723422d3b92f5838b777d7fa8dfab
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: c8d134ac530b0e9a2046922cc1055621d4120e92840eedee5fd5d527b1cd5a6880577a78cc4cbfa065dc751d48d9883a0edd36e661dcd83151b6967f1501cf6d
|
7
|
+
data.tar.gz: e27ee5476e9efcf5871f78e50dd428398306a2dc48092b56c55a04be3e98c30fd2fad2d899ed7d35fff70ef297d2ede51941dd22197eb56222155f8b5d93be48
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.rubocop.yml
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
inherit_from: ../.rubocop.yml
|
2
|
+
|
3
|
+
AllCops:
|
4
|
+
Exclude:
|
5
|
+
- 'bin/*'
|
6
|
+
|
7
|
+
TargetRubyVersion: 2.7
|
8
|
+
|
9
|
+
Layout/LineLength:
|
10
|
+
Exclude:
|
11
|
+
- 'spec/**/*.rb'
|
12
|
+
|
13
|
+
# setting to 6 to match RubyMine autoformat
|
14
|
+
Layout/FirstArrayElementIndentation:
|
15
|
+
IndentationWidth: 6
|
16
|
+
|
17
|
+
|
18
|
+
# rspec blocks are huge by design
|
19
|
+
Metrics/BlockLength:
|
20
|
+
Exclude:
|
21
|
+
- 'spec/**/*.rb'
|
22
|
+
|
23
|
+
Metrics/ModuleLength:
|
24
|
+
Exclude:
|
25
|
+
- 'spec/**/*.rb'
|
26
|
+
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
ruby-2.7.1
|
data/CODE_OF_CONDUCT.md
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
# Contributor Code of Conduct
|
2
|
+
|
3
|
+
As contributors and maintainers of this project, and in the interest of
|
4
|
+
fostering an open and welcoming community, we pledge to respect all people who
|
5
|
+
contribute through reporting issues, posting feature requests, updating
|
6
|
+
documentation, submitting pull requests or patches, and other activities.
|
7
|
+
|
8
|
+
We are committed to making participation in this project a harassment-free
|
9
|
+
experience for everyone, regardless of level of experience, gender, gender
|
10
|
+
identity and expression, sexual orientation, disability, personal appearance,
|
11
|
+
body size, race, ethnicity, age, religion, or nationality.
|
12
|
+
|
13
|
+
Examples of unacceptable behavior by participants include:
|
14
|
+
|
15
|
+
* The use of sexualized language or imagery
|
16
|
+
* Personal attacks
|
17
|
+
* Trolling or insulting/derogatory comments
|
18
|
+
* Public or private harassment
|
19
|
+
* Publishing other's private information, such as physical or electronic
|
20
|
+
addresses, without explicit permission
|
21
|
+
* Other unethical or unprofessional conduct
|
22
|
+
|
23
|
+
Project maintainers have the right and responsibility to remove, edit, or
|
24
|
+
reject comments, commits, code, wiki edits, issues, and other contributions
|
25
|
+
that are not aligned to this Code of Conduct, or to ban temporarily or
|
26
|
+
permanently any contributor for other behaviors that they deem inappropriate,
|
27
|
+
threatening, offensive, or harmful.
|
28
|
+
|
29
|
+
By adopting this Code of Conduct, project maintainers commit themselves to
|
30
|
+
fairly and consistently applying these principles to every aspect of managing
|
31
|
+
this project. Project maintainers who do not follow or enforce the Code of
|
32
|
+
Conduct may be permanently removed from the project team.
|
33
|
+
|
34
|
+
This code of conduct applies both within project spaces and in public spaces
|
35
|
+
when an individual is representing the project or its community.
|
36
|
+
|
37
|
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
38
|
+
reported by contacting a project maintainer at robin@tenjin.ca. All
|
39
|
+
complaints will be reviewed and investigated and will result in a response that
|
40
|
+
is deemed necessary and appropriate to the circumstances. Maintainers are
|
41
|
+
obligated to maintain confidentiality with regard to the reporter of an
|
42
|
+
incident.
|
43
|
+
|
44
|
+
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
45
|
+
version 1.3.0, available at
|
46
|
+
[http://contributor-covenant.org/version/1/3/0/][version]
|
47
|
+
|
48
|
+
[homepage]: http://contributor-covenant.org
|
49
|
+
[version]: http://contributor-covenant.org/version/1/3/0/
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2016 Robin Miller
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,324 @@
|
|
1
|
+
# Invar
|
2
|
+
|
3
|
+
Single source of immutable truth for managing application configs, secrets, and environment variable data.
|
4
|
+
|
5
|
+
## Big Picture
|
6
|
+
|
7
|
+
Invar's main purpose is to enhance and simplify how applications know about their system environment and config.
|
8
|
+
|
9
|
+
Using it in code looks like this:
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
invar = Invar.new(namespace: 'my-app')
|
13
|
+
|
14
|
+
db_host = invar / :config / :database / :host
|
15
|
+
```
|
16
|
+
|
17
|
+
### Background
|
18
|
+
|
19
|
+
Managing application config in a [12 Factor](http://12factor.net/config) style is a good idea, but simple environment
|
20
|
+
variables have some downsides:
|
21
|
+
|
22
|
+
* They are not groupable / nestable
|
23
|
+
* Secrets might be leaked to untrustable subprocesses or 3rd party logging services (eg. an error dump including the
|
24
|
+
whole ENV)
|
25
|
+
* Secrets might be stored in plaintext (eg. in cron or scripts). It's better to store secrets as encrypted.
|
26
|
+
* Cannot be easily checked against a schema for early error detection
|
27
|
+
* Ruby's core ENV does not accept symbols as keys (a minor nuisance, but it counts)
|
28
|
+
|
29
|
+
> **Fun Fact:** Invar is named for an [alloy used in clockmaking](https://en.wikipedia.org/wiki/Invar) - it's short for "**invar**iable".
|
30
|
+
|
31
|
+
### Features
|
32
|
+
|
33
|
+
Here's what this Gem provides:
|
34
|
+
|
35
|
+
* File location defaults from
|
36
|
+
the [XDG Base Directory](https://en.wikipedia.org/wiki/Freedesktop.org#Base_Directory_Specification)
|
37
|
+
file location standard
|
38
|
+
* File schema using [dry-schema](https://dry-rb.org/gems/dry-schema/main/)
|
39
|
+
* Distinction between configs and secrets
|
40
|
+
* Secrets encrypted using [Lockbox](https://github.com/ankane/lockbox)
|
41
|
+
* Access configs and ENV variables using symbols or case-insensitive strings.
|
42
|
+
* Enforced key uniqueness
|
43
|
+
* Helpful Rake tasks
|
44
|
+
* Meaningful error messages with suggestions
|
45
|
+
* Immutable
|
46
|
+
|
47
|
+
### Anti-Features
|
48
|
+
|
49
|
+
Things that Invar intentionally does **not** support:
|
50
|
+
|
51
|
+
* Multiple config sources
|
52
|
+
* No subtle overrides
|
53
|
+
* No frustration about file edits not working... because you're editing the wrong file
|
54
|
+
* No remembering finicky precedence order
|
55
|
+
* Modes
|
56
|
+
* No forgetting to set the mode before running rake, etc
|
57
|
+
* No proliferation of files irrelevant to the current situation
|
58
|
+
* Config file code interpretation (eg. ERB in YAML)
|
59
|
+
* Reduced security hazard
|
60
|
+
* No value ambiguity
|
61
|
+
|
62
|
+
### But That's Bonkers
|
63
|
+
|
64
|
+
It might be! This is a bit of an experiment. Some things may appear undesirable at first glance, but it's usually for a
|
65
|
+
reason.
|
66
|
+
|
67
|
+
Some situations might legitimately need a more complex configuration setup. But perhaps reflect on whether it's a code
|
68
|
+
smell nudging you to:
|
69
|
+
|
70
|
+
* Reduce your application into smaller parts (eg. microservices etc)
|
71
|
+
* Reduce the number of service providers
|
72
|
+
* Improve your collaboration or deployment procedures and automation
|
73
|
+
|
74
|
+
You know your situation better than this README can.
|
75
|
+
|
76
|
+
## Concepts and Jargon
|
77
|
+
|
78
|
+
### Configurations
|
79
|
+
|
80
|
+
Configurations are values that depend on the *local system environment*. They do not generally change depending on what
|
81
|
+
you're *doing*. Examples include:
|
82
|
+
|
83
|
+
* Database name
|
84
|
+
* API options
|
85
|
+
* Gem or library configurations
|
86
|
+
|
87
|
+
### Secrets
|
88
|
+
|
89
|
+
This is stuff you don't want to be read by anyone. Rails calls this concept "credentials." Examples include:
|
90
|
+
|
91
|
+
* Usernames and passwords
|
92
|
+
* API keys
|
93
|
+
|
94
|
+
**This is not a replacement for a password-manager**. Use a
|
95
|
+
proper [password sharing tool](https://en.wikipedia.org/wiki/List_of_password_managers) as the primary method for
|
96
|
+
sharing passwords within your team. This is especially true for the master encryption key used to secure the secrets
|
97
|
+
file.
|
98
|
+
|
99
|
+
Similarly, you should use a unique encryption key for each environment (eg. your development laptop vs a server).
|
100
|
+
|
101
|
+
### XDG Base Directory
|
102
|
+
|
103
|
+
This is a standard that defines where applications should store their files (config, data, etc). The relevant part is
|
104
|
+
that it looks in a couple of places for config files, declared in a pair of environment variables:
|
105
|
+
|
106
|
+
1. `XDG_CONFIG_HOME`
|
107
|
+
- Note: Ignored when `$HOME` directory is undefined. Often the case for system daemons.
|
108
|
+
- Default: `~/.config/`
|
109
|
+
2. `XDG_CONFIG_DIRS`
|
110
|
+
- Fallback locations, declared as a colon-separated list in priority order.
|
111
|
+
- Default: `/etc/xdg/`
|
112
|
+
|
113
|
+
You can check your current values by running this in a terminal:
|
114
|
+
|
115
|
+
env | grep XDG
|
116
|
+
|
117
|
+
### Namespace
|
118
|
+
|
119
|
+
The name of the app's subdirectory under the relevant XDG Base Directory.
|
120
|
+
|
121
|
+
eg:
|
122
|
+
|
123
|
+
`~/.config/name-of-app`
|
124
|
+
|
125
|
+
or
|
126
|
+
|
127
|
+
`/etc/xdg/name-of-app`
|
128
|
+
|
129
|
+
## Installation
|
130
|
+
|
131
|
+
Add this line to your application's Gemfile:
|
132
|
+
|
133
|
+
```ruby
|
134
|
+
gem 'invar'
|
135
|
+
```
|
136
|
+
|
137
|
+
And then run in a terminal:
|
138
|
+
|
139
|
+
bundle install
|
140
|
+
|
141
|
+
## Usage
|
142
|
+
|
143
|
+
### Testing
|
144
|
+
|
145
|
+
Invar automatically loads the normal runtime configuration from the config files created by the Rake tasks (details in
|
146
|
+
next section), but tests may need to override some of those values.
|
147
|
+
|
148
|
+
Call `#pretend` on the relevant selector:
|
149
|
+
|
150
|
+
```ruby
|
151
|
+
# Your application require Invar as normal:
|
152
|
+
require 'invar'
|
153
|
+
|
154
|
+
invar = Invar.new(namespace: 'my-app')
|
155
|
+
|
156
|
+
# ... then, in your test suite:
|
157
|
+
require 'invar/test'
|
158
|
+
|
159
|
+
# Usually this would be in a test suite hook,
|
160
|
+
# like Cucumber's `BeforeAll` or RSpec's `before(:all)`
|
161
|
+
invar[:config][:theme].pretend dark_mode: true
|
162
|
+
```
|
163
|
+
|
164
|
+
Calling `#pretend` without requiring `invar/test` will raise an `ImmutableRealityError`.
|
165
|
+
|
166
|
+
To override values immediately after the config files are read, use an `Invar.after_load` block:
|
167
|
+
|
168
|
+
```ruby
|
169
|
+
Invar.after_load do |invar|
|
170
|
+
invar[:config][:database].pretend name: 'my_app_test'
|
171
|
+
end
|
172
|
+
|
173
|
+
# This Invar will return database name 'my_app_test'
|
174
|
+
invar = Invar.new(namespace: 'my-app')
|
175
|
+
|
176
|
+
puts invar / :config / :database
|
177
|
+
```
|
178
|
+
|
179
|
+
### Rake Tasks
|
180
|
+
|
181
|
+
In your `Rakefile`, add:
|
182
|
+
|
183
|
+
```ruby
|
184
|
+
require 'invar/rake'
|
185
|
+
```
|
186
|
+
|
187
|
+
Then you can use the rake tasks as reported by `rake -T`
|
188
|
+
|
189
|
+
#### Show Search Paths
|
190
|
+
|
191
|
+
This will show you the XDG search locations.
|
192
|
+
|
193
|
+
bundle exec rake invar:paths
|
194
|
+
|
195
|
+
#### Create Config File
|
196
|
+
|
197
|
+
bundle exec rake invar:create:configs
|
198
|
+
|
199
|
+
If a config file already exists in any of the search path locations, it will yell at you.
|
200
|
+
|
201
|
+
#### Edit Config File
|
202
|
+
|
203
|
+
bundle exec rake invar:edit:configs
|
204
|
+
|
205
|
+
#### Create Secrets File
|
206
|
+
|
207
|
+
To create the config file, run this in a terminal:
|
208
|
+
|
209
|
+
bundle exec rake invar:create:secrets
|
210
|
+
|
211
|
+
It will print out the generated master key. Save it to a password manager.
|
212
|
+
|
213
|
+
If you do not want it to be displayed (eg. you're in public), you can pipe it to a file:
|
214
|
+
|
215
|
+
bundle exec rake invar:create:secrets > master_key
|
216
|
+
|
217
|
+
Then handle the `master_key` file as needed.
|
218
|
+
|
219
|
+
#### Edit Secrets File
|
220
|
+
|
221
|
+
To edit the secrets file, run this and provide the file's encryption key:
|
222
|
+
|
223
|
+
bundle exec rake invar:edit:secrets
|
224
|
+
|
225
|
+
The file will be decrypted and opened in your default editor (eg. nano). Once you have exited the editor, it will be
|
226
|
+
re-encrypted (remember to save, too!).
|
227
|
+
|
228
|
+
### Code
|
229
|
+
|
230
|
+
Assuming file `~/.config/my-app/config.yml` with:
|
231
|
+
|
232
|
+
```yml
|
233
|
+
---
|
234
|
+
database:
|
235
|
+
name: 'my_app_development'
|
236
|
+
host: 'localhost'
|
237
|
+
```
|
238
|
+
|
239
|
+
And an encrypted file `~/.config/my-app/secrets.yml` with:
|
240
|
+
|
241
|
+
```yml
|
242
|
+
---
|
243
|
+
database:
|
244
|
+
user: 'my_app'
|
245
|
+
pass: 'sekret'
|
246
|
+
```
|
247
|
+
|
248
|
+
Then in `my-app.rb`, you can fetch those values:
|
249
|
+
|
250
|
+
```ruby
|
251
|
+
require 'invar'
|
252
|
+
|
253
|
+
invar = Invar.new 'my-app'
|
254
|
+
|
255
|
+
# Use the slash operator to fetch values (sort of like Pathname)
|
256
|
+
puts invar / :config / :database / :host
|
257
|
+
|
258
|
+
# String keys are okay, too. Also it's case-insensitive
|
259
|
+
puts invar / 'config' / 'database' / 'host'
|
260
|
+
|
261
|
+
# Secrets are kept in a separate tree
|
262
|
+
puts invar / :secret / :database / :username
|
263
|
+
|
264
|
+
# And you can get ENV variables. This should print your HOME directory.
|
265
|
+
puts invar / :config / :home
|
266
|
+
|
267
|
+
# You can also use [] notation, which may be nicer in some situations (like #pretend)
|
268
|
+
puts invar[:config][:database][:host]
|
269
|
+
```
|
270
|
+
|
271
|
+
> **FAQ**: Why not support a dot syntax like `invar.config.database.host`?
|
272
|
+
>
|
273
|
+
> **A**: Because key names could collide with method names, like `inspect`, `dup`, or `tap`.
|
274
|
+
|
275
|
+
### Custom Locations
|
276
|
+
|
277
|
+
You can customize the search paths by setting the environment variables `XDG_CONFIG_HOME` and/or `XDG_CONFIG_DIRS` any
|
278
|
+
time you run a Rake task or your application.
|
279
|
+
|
280
|
+
# Looks in /tmp instead of ~/.config/
|
281
|
+
XDG_CONFIG_HOME=/tmp bundle exec rake invar:paths
|
282
|
+
|
283
|
+
## Alternatives
|
284
|
+
|
285
|
+
Some other gems with different approaches:
|
286
|
+
|
287
|
+
- [RubyConfig](https://github.com/rubyconfig/config)
|
288
|
+
- [Anyway Config](https://github.com/palkan/anyway_config)
|
289
|
+
- [AppConfig](https://github.com/Oshuma/app_config)
|
290
|
+
- [Fiagro](https://github.com/laserlemon/figaro)
|
291
|
+
|
292
|
+
## Contributing
|
293
|
+
|
294
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/TenjinInc/invar.
|
295
|
+
|
296
|
+
Valued topics:
|
297
|
+
|
298
|
+
* Error messages (clarity, hinting)
|
299
|
+
* Documentation
|
300
|
+
* API
|
301
|
+
* Security correctness
|
302
|
+
|
303
|
+
This project is intended to be a friendly space for collaboration, and contributors are expected to adhere to the
|
304
|
+
[Contributor Covenant](http://contributor-covenant.org) code of conduct. Play nice.
|
305
|
+
|
306
|
+
### Core Developers
|
307
|
+
|
308
|
+
After checking out the repo, run `bundle install` to install dependencies. Then, run `rake spec` to run the tests. You
|
309
|
+
can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
310
|
+
|
311
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the
|
312
|
+
version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version,
|
313
|
+
push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
314
|
+
|
315
|
+
Documentation is produced by Yard. Run `bundle exec rake yard`. The goal is to have 100% documentation coverage and 100%
|
316
|
+
test coverage.
|
317
|
+
|
318
|
+
Release notes are provided in `RELEASE_NOTES.md`, and should vaguely
|
319
|
+
follow [Keep A Changelog](https://keepachangelog.com/en/1.0.0/) recommendations.
|
320
|
+
|
321
|
+
## License
|
322
|
+
|
323
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
324
|
+
|
data/RELEASE_NOTES.md
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
# Release Notes
|
2
|
+
|
3
|
+
## [Unreleased]
|
4
|
+
|
5
|
+
### Major Changes
|
6
|
+
|
7
|
+
* none
|
8
|
+
|
9
|
+
### Minor Changes
|
10
|
+
|
11
|
+
* none
|
12
|
+
|
13
|
+
### Bugfixes
|
14
|
+
|
15
|
+
* none
|
16
|
+
|
17
|
+
## [0.4.0] - 2022-12-08
|
18
|
+
|
19
|
+
### Major Changes
|
20
|
+
|
21
|
+
* Renamed project to Invar
|
22
|
+
|
23
|
+
### Minor Changes
|
24
|
+
|
25
|
+
* none
|
26
|
+
|
27
|
+
### Bugfixes
|
28
|
+
|
29
|
+
* `#pretend` symbolizes provided key(s)
|
30
|
+
* `Scope#to_h` recursively calls `.to_h` on subscopes
|
31
|
+
|
32
|
+
## [0.3.0] - Unreleased beta
|
33
|
+
|
34
|
+
### Major Changes
|
35
|
+
|
36
|
+
* none
|
37
|
+
|
38
|
+
### Minor Changes
|
39
|
+
|
40
|
+
* Added support for validation using dry-schema
|
41
|
+
* Added #key? to Scope
|
42
|
+
|
43
|
+
### Bugfixes
|
44
|
+
|
45
|
+
* none
|
46
|
+
|
47
|
+
## [0.2.0] - Unreleased beta
|
48
|
+
|
49
|
+
### Major Changes
|
50
|
+
|
51
|
+
* Overrides are now done with #pretend
|
52
|
+
|
53
|
+
### Minor Changes
|
54
|
+
|
55
|
+
* Master key is stripped of whitespace when read from file
|
56
|
+
* Added known keys to KeyError message
|
57
|
+
* Docs improvements
|
58
|
+
|
59
|
+
### Bugfixes
|
60
|
+
|
61
|
+
* none
|
62
|
+
|
63
|
+
## [0.1.0] - Unreleased beta
|
64
|
+
|
65
|
+
### Major Changes
|
66
|
+
|
67
|
+
* Initial prototype
|
68
|
+
|
69
|
+
### Minor Changes
|
70
|
+
|
71
|
+
* none
|
72
|
+
|
73
|
+
### Bugfixes
|
74
|
+
|
75
|
+
* none
|
data/Rakefile
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bundler/gem_tasks'
|
4
|
+
require 'rspec/core/rake_task'
|
5
|
+
require 'yard'
|
6
|
+
|
7
|
+
RSpec::Core::RakeTask.new(:spec)
|
8
|
+
|
9
|
+
task default: :spec
|
10
|
+
|
11
|
+
YARD::Rake::YardocTask.new do |t|
|
12
|
+
t.files = %w[lib/**/*.rb]
|
13
|
+
# t.options = %w[--some-option]
|
14
|
+
t.stats_options = ['--list-undoc']
|
15
|
+
end
|
data/invar.gemspec
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
lib = File.expand_path('lib', __dir__)
|
4
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
5
|
+
|
6
|
+
require 'invar/version'
|
7
|
+
|
8
|
+
Gem::Specification.new do |spec|
|
9
|
+
spec.name = 'invar'
|
10
|
+
spec.version = Invar::VERSION
|
11
|
+
spec.authors = ['Robin Miller']
|
12
|
+
spec.email = ['robin@tenjin.ca']
|
13
|
+
|
14
|
+
spec.summary = 'Single source of truth for environmental configuration.'
|
15
|
+
spec.description = <<~DESC
|
16
|
+
Locates and loads config YAML files based on XDG standard with the encrypted secrets file kept separately.
|
17
|
+
Includes useful rake tasks to make management easier. No code execution in config. Rails-independent. Gluten free.
|
18
|
+
DESC
|
19
|
+
spec.homepage = 'https://github.com/TenjinInc/invar'
|
20
|
+
spec.license = 'MIT'
|
21
|
+
spec.metadata = {
|
22
|
+
'rubygems_mfa_required' => 'true'
|
23
|
+
}
|
24
|
+
|
25
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
26
|
+
spec.bindir = 'exe'
|
27
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
28
|
+
spec.require_paths = ['lib']
|
29
|
+
|
30
|
+
spec.required_ruby_version = '>= 2.7'
|
31
|
+
|
32
|
+
spec.add_dependency 'dry-schema', '>= 1.0'
|
33
|
+
spec.add_dependency 'lockbox', '>= 1.0'
|
34
|
+
|
35
|
+
spec.add_development_dependency 'bundler', '~> 2.3'
|
36
|
+
spec.add_development_dependency 'fakefs', '~> 1.9'
|
37
|
+
spec.add_development_dependency 'rake', '~> 13.0'
|
38
|
+
spec.add_development_dependency 'rspec', '~> 3.12'
|
39
|
+
spec.add_development_dependency 'simplecov', '~> 0.21'
|
40
|
+
spec.add_development_dependency 'yard', '~> 0.9'
|
41
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'invar/version'
|
4
|
+
require 'invar/scope'
|
5
|
+
|
6
|
+
require 'yaml'
|
7
|
+
require 'lockbox'
|
8
|
+
require 'pathname'
|
9
|
+
|
10
|
+
module Invar
|
11
|
+
# XDG values based on https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html
|
12
|
+
module XDG
|
13
|
+
# Default values for various XDG variables
|
14
|
+
module Defaults
|
15
|
+
# Default XDG config directory within the user's $HOME.
|
16
|
+
CONFIG_HOME = '~/.config'
|
17
|
+
|
18
|
+
# Default XDG config direction within the broader system paths
|
19
|
+
CONFIG_DIRS = '/etc/xdg'
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# Common file extension constants, excluding the dot.
|
24
|
+
module EXT
|
25
|
+
# File extension for YAML files
|
26
|
+
YAML = 'yml'
|
27
|
+
end
|
28
|
+
|
29
|
+
# Locates a config file within XDG standard location(s) and namespace subdirectory.
|
30
|
+
class FileLocator
|
31
|
+
attr_reader :search_paths
|
32
|
+
|
33
|
+
# Builds a new instance that will search in the namespace.
|
34
|
+
#
|
35
|
+
# @param [String] namespace Name of the subdirectory within the XDG standard location(s)
|
36
|
+
# @raise [InvalidNamespaceError] if the namespace is nil or empty string
|
37
|
+
def initialize(namespace)
|
38
|
+
raise InvalidNamespaceError, 'namespace cannot be nil' if namespace.nil?
|
39
|
+
raise InvalidNamespaceError, 'namespace cannot be an empty string' if namespace.empty?
|
40
|
+
|
41
|
+
@namespace = namespace
|
42
|
+
|
43
|
+
home_config_dir = ENV.fetch('XDG_CONFIG_HOME', XDG::Defaults::CONFIG_HOME)
|
44
|
+
alt_config_dirs = ENV.fetch('XDG_CONFIG_DIRS', XDG::Defaults::CONFIG_DIRS).split(':')
|
45
|
+
|
46
|
+
source_dirs = alt_config_dirs
|
47
|
+
source_dirs.unshift(home_config_dir) if ENV.key? 'HOME'
|
48
|
+
@search_paths = source_dirs.collect { |path| Pathname.new(path) / @namespace }.collect(&:expand_path)
|
49
|
+
|
50
|
+
freeze
|
51
|
+
end
|
52
|
+
|
53
|
+
# Locates the file with the given same. You may optionally provide an extension as a second argument.
|
54
|
+
#
|
55
|
+
# These are equivalent:
|
56
|
+
# find('config.yml')
|
57
|
+
# find('config', 'yml')
|
58
|
+
#
|
59
|
+
# @param [String] basename The file's basename
|
60
|
+
# @param [String] ext the file extension, excluding the dot.
|
61
|
+
# @return [Pathname] the path of the located file
|
62
|
+
# @raise [AmbiguousSourceError] if the file is found in multiple locations
|
63
|
+
# @raise [FileNotFoundError] if the file cannot be found
|
64
|
+
def find(basename, ext = nil)
|
65
|
+
basename = [basename, ext].join('.') if ext
|
66
|
+
|
67
|
+
full_paths = search_paths.collect { |dir| dir / basename }
|
68
|
+
files = full_paths.select(&:exist?)
|
69
|
+
|
70
|
+
if files.size > 1
|
71
|
+
msg = "Found more than 1 #{ basename } file: #{ files.join(', ') }."
|
72
|
+
raise AmbiguousSourceError, "#{ msg } #{ AmbiguousSourceError::HINT }"
|
73
|
+
end
|
74
|
+
|
75
|
+
files.first || raise(FileNotFoundError, "Could not find #{ basename }")
|
76
|
+
end
|
77
|
+
|
78
|
+
# Raised when the file cannot be found in any of the XDG search locations.
|
79
|
+
class FileNotFoundError < RuntimeError
|
80
|
+
end
|
81
|
+
|
82
|
+
# Raised when the provided namespace is invalid.
|
83
|
+
class InvalidNamespaceError < ArgumentError
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|