Skip to content

Commit

Permalink
Key search (#83)
Browse files Browse the repository at this point in the history
* Large refactoring #4

With the old funtionality it was OK to always need a node,
because a user had to choose one after choosing an environment.

With #4 we are introducing a different funtionality that
should work indepently of a node.

This is in preparation of #4 and tries to disentangle
environment, nodes and everything else.

To accomplish this, "proper" abstractions for all relevant
parts of the hiera data model are introduced (hierarchies,
data_files and values).

Due to the git backend, (data_)file and value handling still
need to be able to work with a node's facts, but this has
been made optional.

* First draft of key search #4

* Disable misguided cop #4

* Collapse search results by default #4

Lists of files may get very long. This way, a user can
open just the hierarchy she wants.

* Cleanup and some tests #4
  • Loading branch information
oneiros committed Oct 18, 2022
1 parent e01ae92 commit 379d30b
Show file tree
Hide file tree
Showing 39 changed files with 557 additions and 258 deletions.
4 changes: 4 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,9 @@ AllCops:
- config/environments/*
- vendor/**/*
- .vendor/**/*
Naming/PredicateName:
ForbiddenPrefixes:
- "is_"
- "have_"
Rails/I18nLocaleTexts:
Enabled: false
4 changes: 2 additions & 2 deletions app/controllers/decrypted_values_controller.rb
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
class DecryptedValuesController < ApplicationController
include EnvironmentAndNodeConcern
before_action :load_environments

def create
authorize! :decrypt, Value
hierarchy = Hierarchy.find(@node, params[:hierarchy])
hierarchy = Hierarchy.find(@environment, params[:hierarchy_id])
@decrypted_value = hierarchy.decrypt_value(params[:value].chomp)

render plain: @decrypted_value
Expand Down
4 changes: 2 additions & 2 deletions app/controllers/encrypted_values_controller.rb
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
class EncryptedValuesController < ApplicationController
include EnvironmentAndNodeConcern
before_action :load_environments

def create
authorize! :encrypt, Value
hierarchy = Hierarchy.find(@node, params[:hierarchy])
hierarchy = Hierarchy.find(@environment, params[:hierarchy_id])
@encrypted_value = hierarchy.encrypt_value(params[:value])

render plain: @encrypted_value
Expand Down
20 changes: 20 additions & 0 deletions app/controllers/files_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
class FilesController < ApplicationController
before_action :load_environments
before_action :load_key

add_breadcrumb "Home", :root_path
add_breadcrumb "Environments", :environments_path

def index
@files_and_values_by_hierarchy = DataFile.search(@environment, @key)

add_breadcrumb @environment, environment_nodes_path(@environment)
add_breadcrumb @key, environment_key_files_path(@environment, @key)
end

private

def load_key
@key = Key.new(environment: @environment, name: params[:key_id].squish)
end
end
6 changes: 3 additions & 3 deletions app/controllers/keys_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,16 @@ class KeysController < ApplicationController

def index
authorize! :show, Key
@keys = Key.all(@node)
@keys = Key.all_for(@node)

add_breadcrumb @environment, environment_nodes_path(@environment)
add_breadcrumb @node, environment_node_keys_path(@environment, @node)
end

def show
authorize! :show, Key
@keys = Key.all(@node)
@key = Key.new(@node, params[:id])
@keys = Key.all_for(@node)
@key = Key.new(environment: @environment, name: params[:id])

add_breadcrumb @environment, environment_nodes_path(@environment)
add_breadcrumb @node, environment_node_keys_path(@environment, @node)
Expand Down
33 changes: 33 additions & 0 deletions app/controllers/values_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
class ValuesController < ApplicationController
before_action :instantiate_models

def update
authorize! :update, Key

@value.update(params[:value], node: @node)

redirect_to environment_node_key_path(@environment, @node, @key),
notice: "Value was saved successfully"
end

def destroy
authorize! :destroy, Key

@value.destroy(node: @node)

redirect_to environment_node_key_path(@environment, @node, @key),
status: :see_other,
notice: "Value was removed successfully"
end

private

def instantiate_models
@environment = Environment.find(params[:environment_id])
@node = Node.new(hostname: params[:node_id], environment: @environment)
@hierarchy = Hierarchy.find(@environment, params[:hierarchy_id])
@data_file = DataFile.new(hierarchy: @hierarchy, path: params[:data_file_id])
@key = Key.new(environment: @environment, name: params[:key_id])
@value = @data_file.value_for(key: @key)
end
end
8 changes: 4 additions & 4 deletions app/helpers/application_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,16 @@ def flash_class(level)
end
end

def format_path(value)
def format_path(file, key)
tag_name, classes =
if value.key_present?
if file.has_key?(key) # rubocop:disable Style/PreferredHashMethods
[:b, nil]
elsif value.file_present?
elsif file.exist?
[:span, "text-dark"]
else
[:em, "text-muted"]
end
tag.send(tag_name, value.path, class: classes)
tag.send(tag_name, file.path, class: classes)
end

def icon(name)
Expand Down
14 changes: 14 additions & 0 deletions app/javascript/controllers/text_input_navigation_controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Controller } from "@hotwired/stimulus"
import { Turbo } from "@hotwired/turbo-rails"

// Connects to data-controller="text-input-navigation"
export default class extends Controller {
static targets = ["input"]
static values = {placeholder: String, url: String}

navigate() {
const trimmedValue = this.inputTarget.value.trim()
const url = this.urlValue.replace(this.placeholderValue, trimmedValue);
Turbo.visit(url);
}
}
69 changes: 69 additions & 0 deletions app/models/data_file.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
class DataFile < HieraModel
attribute :hierarchy
attribute :node
attribute :path, :string
attribute :exist, :boolean, default: false
attribute :writable, :boolean, default: false
attribute :replaced_from_git, :boolean, default: false

delegate :environment, to: :hierarchy

def self.search(environment, key)
hierarchies = {}
result = {}
HieraData.new(environment.name).files_including(key.name).each do |file|
hierarchies[file[:hierarchy_name]] ||= Hierarchy.new(
environment: environment,
name: file[:hierarchy_name],
backend: file[:hierarchy_backend]
)
data_file = new(
hierarchy: hierarchies[file[:hierarchy_name]],
path: file[:path]
)
value = Value.new(data_file: data_file, key: key, value: file[:value])
result[hierarchies[file[:hierarchy_name]]] ||= {}
result[hierarchies[file[:hierarchy_name]]][data_file] = value
end
result
end

def initialize(attributes = {})
super(attributes)
file_attributes = hiera_data.file_attributes(hierarchy.name, path, facts: node&.facts)
self.exist = file_attributes[:exist]
self.writable = file_attributes[:writable]
self.replaced_from_git = file_attributes[:replaced_from_git]
end

def keys
@keys ||= hiera_data.keys_in_file(hierarchy.name, path).map do |key_name|
Key.new(environment: environment, name: key_name)
end
end

def has_key?(key)
keys.include?(key)
end

def value_for(key:)
raw_value = (hiera_data.value_in_file(hierarchy.name, path, key.name, facts: node&.facts) if has_key?(key)) # rubocop:disable Style/PreferredHashMethods
Value.new(data_file: self, key: key, value: raw_value)
end

def writable?
!Rails.configuration.hdm.read_only && attributes["writable"]
end

def exist?
exist
end

def replaced_from_git?
replaced_from_git
end

def to_param
path
end
end
14 changes: 6 additions & 8 deletions app/models/environment.rb
Original file line number Diff line number Diff line change
@@ -1,22 +1,20 @@
class Environment
class NotFound < StandardError; end

attr_reader :name
class Environment < HieraModel
attribute :name, :string

def self.all_names
PuppetDbClient.environments
end

def self.all
all_names.map { |e| new(e) }
all_names.map { |e| new(name: e) }
end

def self.find(name)
new(name)
new(name: name)
end

def initialize(name)
@name = name
def hierarchies
Hierarchy.all(self)
end

def ==(other)
Expand Down
50 changes: 48 additions & 2 deletions app/models/hiera_data.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# rubocop:disable Metrics/ClassLength
class HieraData
class EnvironmentNotFound < StandardError; end

Expand All @@ -21,10 +22,38 @@ def all_keys(facts)
keys.sort.uniq
end

def search_key(datadir, files, key, facts: {})
def files_for(hierarchy_name, facts: {})
hierarchy = find_hierarchy(hierarchy_name)
hierarchy.resolved_paths(facts: facts)
end

def file_attributes(hierarchy_name, path, facts: nil)
hierarchy = find_hierarchy(hierarchy_name)
file = DataFile.new(path: hierarchy.datadir.join(path), facts: facts)
{
exist: file.exist?,
writable: file.writable?,
replaced_from_git: file.replaced_from_git?
}
end

def keys_in_file(hierarchy_name, path)
hierarchy = find_hierarchy(hierarchy_name)
DataFile.new(path: hierarchy.datadir.join(path)).keys
end

def value_in_file(hierarchy_name, path, key, facts: {})
hierarchy = find_hierarchy(hierarchy_name)
file = DataFile.new(path: hierarchy.datadir.join(path), facts: facts)
file.content_for_key(key)
end

def search_key(hierarchy_name, key, facts: nil)
hierarchy = find_hierarchy(hierarchy_name)
files = facts ? hierarchy.resolved_paths(facts: facts) : hierarchy.candidate_files
search_results = {}
files.each do |path|
file = DataFile.new(path: datadir.join(path), facts: facts)
file = DataFile.new(path: hierarchy.datadir.join(path), facts: facts)
search_results[path] = {
file_present: file.exist?,
file_writable: file.writable?,
Expand All @@ -36,6 +65,22 @@ def search_key(datadir, files, key, facts: {})
search_results
end

def files_including(key)
hierarchies.flat_map do |hierarchy|
search_results = search_key(hierarchy.name, key)
search_results.map do |path, search_result|
next unless search_result[:key_present]

{
path: path,
hierarchy_name: hierarchy.name,
hierarchy_backend: hierarchy.backend,
value: search_result[:value]
}
end
end.compact!
end

def write_key(hierarchy_name, path, key, value, facts: {})
hierarchy = find_hierarchy(hierarchy_name)
read_file = DataFile.new(path: hierarchy.datadir.join(path), facts: facts)
Expand Down Expand Up @@ -92,3 +137,4 @@ def find_hierarchy(name)
config.hierarchies.find { |h| h.name == name }
end
end
# rubocop:enable Metrics/ClassLength
7 changes: 7 additions & 0 deletions app/models/hiera_data/hierarchy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,13 @@ def resolved_paths(facts:)
end
end

def candidate_files
paths.flat_map do |path|
globbed_path = Interpolation.replace_variables_with_globs(path)
Interpolation.interpolate_globs(path: globbed_path, datadir: datadir)
end
end

private

def setup_paths
Expand Down
4 changes: 4 additions & 0 deletions app/models/hiera_data/interpolation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,9 @@ def interpolate_facts(path:, facts:)
value || variable_string
end
end

def replace_variables_with_globs(path)
path.gsub(VARIABLE_REGEXP, "*")
end
end
end
10 changes: 10 additions & 0 deletions app/models/hiera_model.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
class HieraModel
include ActiveModel::Model
include ActiveModel::Attributes

private

def hiera_data
@hiera_data ||= HieraData.new(environment.name) if respond_to?(:environment)
end
end
Loading

0 comments on commit 379d30b

Please sign in to comment.