Class: Vkit::CLI::PolicyPack::Manager

Inherits:
Object
  • Object
show all
Defined in:
lib/vkit/cli/policy_pack/manager.rb

Defined Under Namespace

Classes: DependencyMissing, Error, InvalidPack, PackAlreadyInstalled, PackNotFound, PackNotInstalled, UnsafeOverwrite

Constant Summary collapse

PACKS_DIR =

Packs shipped with CLI (in the gem)

File.expand_path("../../policy/packs", __dir__)
STATE_DIR_NAME =

Project-local state

".vkit"
TRACKING_FILE_NAME =
"packs.yaml"
DEFAULT_POLICIES_DIR =
File.join("config", "policies")

Instance Method Summary collapse

Constructor Details

#initialize(project_root: Dir.pwd, policies_dir: DEFAULT_POLICIES_DIR) ⇒ Manager

Returns a new instance of Manager.



29
30
31
32
# File 'lib/vkit/cli/policy_pack/manager.rb', line 29

def initialize(project_root: Dir.pwd, policies_dir: DEFAULT_POLICIES_DIR)
  @project_root = File.expand_path(project_root)
  @policies_dir = File.expand_path(policies_dir, @project_root)
end

Instance Method Details

#available_packsObject



34
35
36
37
38
39
40
# File 'lib/vkit/cli/policy_pack/manager.rb', line 34

def available_packs
  return [] unless Dir.exist?(PACKS_DIR)

  Dir.children(PACKS_DIR)
     .select { |name| File.directory?(File.join(PACKS_DIR, name)) }
     .sort
end

#install!(pack_name, force: false, dry_run: false) ⇒ Object

Install pack policies into project’s policies_dir.

Options:

  • force: overwrite pack-installed files if they exist (refuses to overwrite non-pack files)

  • dry_run: compute actions but do not write



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
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
# File 'lib/vkit/cli/policy_pack/manager.rb', line 60

def install!(pack_name, force: false, dry_run: false)
  meta, policies = read_pack!(pack_name)

  if installed?(pack_name)
    raise PackAlreadyInstalled, "Pack '#{pack_name}' is already installed"
  end

  ensure_dependencies!(meta)

  FileUtils.mkdir_p(@policies_dir) unless dry_run

  validate_pack_policies!(pack_name, meta, policies)

  install_record = {
    "name" => pack_name,
    "version" => meta["version"],
    "layer" => meta["layer"],
    "installed_at" => Time.now.utc.iso8601,
    "pack_checksum" => pack_checksum(pack_name),
    "files" => []
  }

  policies.each_with_index do |policy, idx|
    policy_id = policy.fetch("id")
    ext = "yaml"

    filename = format("%<pack>s__%<idx>02d__%<id>s.%<ext>s",
                      pack: pack_name,
                      idx: (idx + 1),
                      id: safe_slug(policy_id),
                      ext: ext)

    dest = File.join(@policies_dir, filename)

    # Overwrite rules:
    # - if dest exists and is NOT tracked by this pack -> refuse
    # - if dest exists and force=false -> refuse
    # - if dest exists and force=true -> allow overwrite
    if File.exist?(dest)
      raise UnsafeOverwrite, "Refusing to overwrite existing file: #{dest}" unless force
      # force is allowed, but this file isn't yet tracked because pack isn't installed.
      # Still, we only allow overwriting if the file looks like our namespace.
      unless File.basename(dest).start_with?("#{pack_name}__")
        raise UnsafeOverwrite, "Refusing to overwrite non-pack file (missing namespace): #{dest}"
      end
    end

    content = render_policy_file(pack_name, meta, policy)

    unless dry_run
      File.write(dest, content)
    end

    install_record["files"] << {
      "path" => relative_to_root(dest),
      "policy_id" => policy_id
    }
  end

  write_state_add!(pack_name, install_record) unless dry_run

  install_record["files"].length
end

#install_with_deps!(pack_name, force: false, dry_run: false, visited: {}) ⇒ Object



211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
# File 'lib/vkit/cli/policy_pack/manager.rb', line 211

def install_with_deps!(pack_name, force: false, dry_run: false, visited: {})
  return 0 if installed?(pack_name)

  raise InvalidPack, "Circular dependency detected at '#{pack_name}'" if visited[pack_name]
  visited[pack_name] = true

  meta = (pack_name)
  deps = Array(meta["dependencies"])

  # Install dependencies first
  deps.each do |dep|
    install_with_deps!(dep, force: force, dry_run: dry_run, visited: visited)
  end

  # Enforce layering monotonicity
  deps.each do |dep|
    dep_layer = (dep)["layer"].to_i
    my_layer  = meta["layer"].to_i
    if dep_layer > my_layer
      raise InvalidPack, "Invalid layering: '#{pack_name}' layer #{my_layer} depends on '#{dep}' layer #{dep_layer}"
    end
  end

  install!(pack_name, force: force, dry_run: dry_run)
ensure
  visited.delete(pack_name)
end

#installed?(pack_name) ⇒ Boolean

Returns:

  • (Boolean)


46
47
48
# File 'lib/vkit/cli/policy_pack/manager.rb', line 46

def installed?(pack_name)
  installed_packs.key?(pack_name)
end

#installed_packsObject



42
43
44
# File 'lib/vkit/cli/policy_pack/manager.rb', line 42

def installed_packs
  state.dig("installed_packs") || {}
end

#list_statusObject



154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
# File 'lib/vkit/cli/policy_pack/manager.rb', line 154

def list_status
  avail = available_packs
  installed = installed_packs

  avail.map do |name|
    shipped_meta = (name)
    entry = installed[name]

    shipped_version = shipped_meta["version"]
    installed_version = entry && entry["version"]

    {
      "name" => name,
      "layer" => shipped_meta["layer"],
      "shipped_version" => shipped_version,
      "installed" => !entry.nil?,
      "installed_version" => installed_version,
      "drift" => entry && shipped_version && installed_version && shipped_version != installed_version
    }
  end
end

#pack_metadata(pack_name) ⇒ Object



50
51
52
53
# File 'lib/vkit/cli/policy_pack/manager.rb', line 50

def (pack_name)
  meta, _policies = read_pack!(pack_name)
  meta
end

#remove!(pack_name, force: false) ⇒ Object

Remove pack-installed policy files.

Options:

  • force: remove files even if missing (still won’t delete non-tracked files)



128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
# File 'lib/vkit/cli/policy_pack/manager.rb', line 128

def remove!(pack_name, force: false)
  unless installed?(pack_name)
    raise PackNotInstalled, "Pack '#{pack_name}' is not installed"
  end

  pack_entry = installed_packs[pack_name]
  files = Array(pack_entry["files"])

  removed = 0

  files.each do |f|
    abs = File.expand_path(f.fetch("path"), @project_root)

    if File.exist?(abs)
      FileUtils.rm_f(abs)
      removed += 1
    else
      raise Error, "Expected pack file missing: #{abs} (use --force to ignore)" unless force
    end
  end

  write_state_remove!(pack_name)

  removed
end

#upgrade!(pack_name, force: false, dry_run: false) ⇒ Object



176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
# File 'lib/vkit/cli/policy_pack/manager.rb', line 176

def upgrade!(pack_name, force: false, dry_run: false)
  unless installed?(pack_name)
    raise PackNotInstalled, "Pack '#{pack_name}' is not installed"
  end

  installed_entry = installed_packs[pack_name]
  installed_version = installed_entry["version"]

  shipped_meta = (pack_name)
  shipped_version = shipped_meta["version"]

  if installed_version == shipped_version
    return :up_to_date
  end

  remove!(pack_name, force: force) unless dry_run

  count = install!(pack_name, force: force, dry_run: dry_run)

  {
    old_version: installed_version,
    new_version: shipped_version,
    policies: count
  }
end

#upgrade_all!(force: false, dry_run: false) ⇒ Object



202
203
204
205
206
207
208
209
# File 'lib/vkit/cli/policy_pack/manager.rb', line 202

def upgrade_all!(force: false, dry_run: false)
  # installed packs ordered by layer (low -> high)
  packs = installed_packs.keys.sort_by { |name| (name)["layer"].to_i }

  packs.each_with_object([]) do |pack, results|
    results << [pack, upgrade!(pack, force: force, dry_run: dry_run)]
  end
end