Module: LocalVault::EnvProjection

Defined in:
lib/localvault/env_projection.rb

Defined Under Namespace

Classes: Entry, InvalidMapping, UnknownProfile

Constant Summary collapse

ENV_NAME_PATTERN =
/\A[A-Za-z_][A-Za-z0-9_]*\z/
PROFILES =
{
  "aws" => {
    only: ["AWS_IAM.*"],
    map: {
      "AWS_IAM.access_key_id" => "AWS_ACCESS_KEY_ID",
      "AWS_IAM.secret_access_key" => "AWS_SECRET_ACCESS_KEY",
      "AWS_IAM.session_token" => "AWS_SESSION_TOKEN"
    }
  }
}.freeze

Class Method Summary collapse

Class Method Details

.apply_mapping(entry, mappings, on_skip:) ⇒ Object



117
118
119
120
121
122
123
124
125
# File 'lib/localvault/env_projection.rb', line 117

def self.apply_mapping(entry, mappings, on_skip:)
  mapped_name = mappings.fetch(entry.key, entry.env_name)
  unless safe_env_name?(mapped_name)
    on_skip&.call(entry.key)
    return nil
  end

  Entry.new(key: entry.key, env_name: mapped_name, value: entry.value)
end

.entries(secrets, project: nil, only: nil, except: nil, map: nil, profile: nil, on_skip: nil) ⇒ Object



21
22
23
24
25
26
27
28
29
30
31
32
# File 'lib/localvault/env_projection.rb', line 21

def self.entries(secrets, project: nil, only: nil, except: nil, map: nil, profile: nil, on_skip: nil)
  profile_config = profile_config(profile)
  selectors = parse_selectors(only) || profile_config[:only]
  exclusions = parse_selectors(except) || []
  mappings = profile_config[:map].merge(parse_map(map))

  flatten(secrets, project: project, on_skip: on_skip)
    .select { |entry| include_entry?(entry.key, selectors) }
    .reject { |entry| selector_match?(entry.key, exclusions) }
    .map { |entry| apply_mapping(entry, mappings, on_skip: on_skip) }
    .compact
end

.flatten(secrets, project:, on_skip:) ⇒ Object



60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
# File 'lib/localvault/env_projection.rb', line 60

def self.flatten(secrets, project:, on_skip:)
  if project
    group = secrets[project]
    return [] unless group.is_a?(Hash)

    group.filter_map do |key, value|
      if safe_env_name?(key)
        Entry.new(key: key, env_name: key, value: value.to_s)
      else
        on_skip&.call(key)
        nil
      end
    end
  else
    secrets.flat_map do |key, value|
      if value.is_a?(Hash)
        flatten_group(key, value, on_skip: on_skip)
      elsif safe_env_name?(key)
        [Entry.new(key: key, env_name: key, value: value.to_s)]
      else
        on_skip&.call(key)
        []
      end
    end
  end
end

.flatten_group(group, pairs, on_skip:) ⇒ Object



87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
# File 'lib/localvault/env_projection.rb', line 87

def self.flatten_group(group, pairs, on_skip:)
  unless safe_env_name?(group)
    on_skip&.call(group)
    return []
  end

  pairs.filter_map do |key, value|
    if safe_env_name?(key)
      Entry.new(key: "#{group}.#{key}", env_name: "#{group.upcase}__#{key}", value: value.to_s)
    else
      on_skip&.call("#{group}.#{key}")
      nil
    end
  end
end

.include_entry?(key, selectors) ⇒ Boolean

Returns:

  • (Boolean)


103
104
105
# File 'lib/localvault/env_projection.rb', line 103

def self.include_entry?(key, selectors)
  selectors.nil? || selector_match?(key, selectors)
end

.parse_map(value) ⇒ Object



40
41
42
43
44
45
46
47
48
49
50
# File 'lib/localvault/env_projection.rb', line 40

def self.parse_map(value)
  Array(value).compact.flat_map { |v| v.to_s.split(",") }.each_with_object({}) do |pair, hash|
    next if pair.strip.empty?

    key, env_name = pair.split("=", 2).map(&:strip)
    raise InvalidMapping, "Invalid map '#{pair}'. Use KEY=ENV_NAME" if key.to_s.empty? || env_name.to_s.empty?
    raise InvalidMapping, "Invalid environment variable name '#{env_name}'" unless safe_env_name?(env_name)

    hash[key] = env_name
  end
end

.parse_selectors(value) ⇒ Object



34
35
36
37
38
# File 'lib/localvault/env_projection.rb', line 34

def self.parse_selectors(value)
  values = Array(value).compact.flat_map { |v| v.to_s.split(",") }
  selectors = values.map(&:strip).reject(&:empty?)
  selectors.empty? ? nil : selectors
end

.profile_config(profile) ⇒ Object



52
53
54
55
56
57
58
# File 'lib/localvault/env_projection.rb', line 52

def self.profile_config(profile)
  return { only: nil, map: {} } if profile.nil? || profile.to_s.empty?

  PROFILES.fetch(profile.to_s) do
    raise UnknownProfile, "Unknown env profile '#{profile}'"
  end
end

.safe_env_name?(name) ⇒ Boolean

Returns:

  • (Boolean)


127
128
129
# File 'lib/localvault/env_projection.rb', line 127

def self.safe_env_name?(name)
  name.is_a?(String) && name.match?(ENV_NAME_PATTERN)
end

.selector_match?(key, selectors) ⇒ Boolean

Returns:

  • (Boolean)


107
108
109
110
111
112
113
114
115
# File 'lib/localvault/env_projection.rb', line 107

def self.selector_match?(key, selectors)
  selectors.any? do |selector|
    if selector.end_with?(".*")
      key.start_with?("#{selector.delete_suffix(".*")}.")
    else
      key == selector
    end
  end
end