Skip to content

Commit

Permalink
Group expectations evaluation (#4)
Browse files Browse the repository at this point in the history
  • Loading branch information
fabriziosestito authored Aug 4, 2022
1 parent f446928 commit 6d5120f
Show file tree
Hide file tree
Showing 22 changed files with 700 additions and 264 deletions.
34 changes: 28 additions & 6 deletions lib/wanda/catalog.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,46 @@ defmodule Wanda.Catalog do
Function to interact with the checks catalog.
"""

alias Wanda.Catalog.Expectation

@catalog_path Application.compile_env!(:wanda, [__MODULE__, :catalog_path])

def get_facts_names(check_id) do
@doc """
Get a list of expectations for a given check.
"""
@spec get_expectations(String.t()) :: [Expectation.t()]
def get_expectations(check_id) do
check_id
|> get_check()
|> Map.get("facts")
|> Enum.map(& &1["name"])
|> Map.fetch!("expectations")
|> Enum.map(&map_expectation/1)
end

def get_expectations(check_id) do
def get_expectation(check_id, name) do
check_id
|> get_check()
|> Map.fetch!("expectations")
|> get_expectations()
|> Enum.find(&(&1.name == name))
end

defp get_check(check_id) do
@catalog_path
|> Path.join("#{check_id}.yaml")
|> YamlElixir.read_from_file!()
end

defp map_expectation(%{"name" => name, "expect" => expression}) do
%Expectation{
name: name,
type: :expect,
expression: expression
}
end

defp map_expectation(%{"name" => name, "expect_same" => expression}) do
%Expectation{
name: name,
type: :expect_same,
expression: expression
}
end
end
13 changes: 13 additions & 0 deletions lib/wanda/catalog/expectation.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
defmodule Wanda.Catalog.Expectation do
@moduledoc """
Represents an expectation.
"""

defstruct [:name, :type, :expression]

@type t :: %__MODULE__{
name: String.t(),
type: :expect | :expect_same,
expression: String.t()
}
end
18 changes: 13 additions & 5 deletions lib/wanda/execution.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ defmodule Wanda.Execution do

use GenServer

alias Wanda.Execution.{Expectations, Gathering, State}
alias Wanda.Execution.{Evaluation, Gathering, State}
alias Wanda.Messaging.Publisher

require Logger
Expand Down Expand Up @@ -63,15 +63,23 @@ defmodule Wanda.Execution do
end

defp continue_or_complete_execution(
%State{gathered_facts: gathered_facts, targets: targets} = state,
%State{
execution_id: execution_id,
group_id: group_id,
gathered_facts: gathered_facts,
targets: targets,
agents_gathered: agents_gathered
} = state,
agent_id,
facts
) do
gathered_facts = Gathering.put_gathered_facts(gathered_facts, agent_id, facts)
state = %State{state | gathered_facts: gathered_facts}
agents_gathered = [agent_id | agents_gathered]

if Gathering.all_agents_sent_facts?(gathered_facts, targets) do
Expectations.eval(gathered_facts)
state = %State{state | gathered_facts: gathered_facts, agents_gathered: agents_gathered}

if Gathering.all_agents_sent_facts?(agents_gathered, targets) do
Evaluation.execute(execution_id, group_id, gathered_facts)

Publisher.send_execution_results("", "", "")
{:stop, :normal, state}
Expand Down
19 changes: 19 additions & 0 deletions lib/wanda/execution/agent_check_result.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
defmodule Wanda.Execution.AgentCheckResult do
@moduledoc """
Represents the result of a check on a specific agent.
"""

alias Wanda.Execution.{ExpectationEvaluation, ExpectationEvaluationError}

defstruct [
:agent_id,
:facts,
:expectation_evaluations
]

@type t :: %__MODULE__{
agent_id: String.t(),
facts: %{(name :: String.t()) => result :: any()},
expectation_evaluations: [ExpectationEvaluation.t() | ExpectationEvaluationError.t()]
}
end
12 changes: 6 additions & 6 deletions lib/wanda/execution/check_result.ex
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
defmodule Wanda.Execution.CheckResult do
@moduledoc """
Represents the result of check.
Represents the result of a check.
"""

alias Wanda.Execution.ExpectationResult
alias Wanda.Execution.{AgentCheckResult, ExpectationResult}

defstruct [
:check_id,
:expectations_results,
:facts,
:expectation_results,
:agents_check_results,
:result
]

@type t :: %__MODULE__{
check_id: String.t(),
expectations_results: [ExpectationResult.t()],
facts: %{(name :: String.t()) => result :: any()},
expectation_results: [ExpectationResult.t()],
agents_check_results: [AgentCheckResult.t()],
result: :passing | :warning | :critical
}
end
171 changes: 171 additions & 0 deletions lib/wanda/execution/evaluation.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
defmodule Wanda.Execution.Evaluation do
@moduledoc """
Evaluation functional core.
"""

alias Wanda.Catalog
alias Wanda.Catalog.Expectation

alias Wanda.Execution.{
AgentCheckResult,
CheckResult,
ExpectationEvaluation,
ExpectationEvaluationError,
ExpectationResult,
Result
}

# Abacus spec is wrong
@dialyzer {:nowarn_function, eval_expectation: 2}

@spec execute(String.t(), String.t(), map()) :: Result.t()
def execute(execution_id, group_id, gathered_facts) do
%Result{
execution_id: execution_id,
group_id: group_id
}
|> add_checks_result(gathered_facts)
|> aggregate_execution_result()
end

defp add_checks_result(%Result{} = result, gathered_facts) do
check_results =
Enum.map(gathered_facts, fn {check_id, agents_facts} ->
build_check_result(check_id, agents_facts)
end)

%Result{result | check_results: check_results}
end

defp build_check_result(check_id, agents_facts) do
%CheckResult{
check_id: check_id
}
|> add_agents_results(agents_facts)
|> add_expectation_evaluations()
|> aggregate_check_result()
end

defp add_agents_results(%CheckResult{check_id: check_id} = check_result, agents_facts) do
agents_results =
Enum.map(agents_facts, fn {agent_id, facts} ->
%AgentCheckResult{agent_id: agent_id, facts: facts}
|> add_agent_expectation_result(check_id)
end)

%CheckResult{check_result | agents_check_results: agents_results}
end

defp add_agent_expectation_result(
%AgentCheckResult{facts: facts} = agent_check_result,
check_id
) do
expectation_evaluations =
check_id
|> Catalog.get_expectations()
|> Enum.map(&eval_expectation(&1, facts))

%AgentCheckResult{
agent_check_result
| expectation_evaluations: expectation_evaluations
}
end

defp eval_expectation(%Expectation{name: name, type: type, expression: expression}, facts) do
case Abacus.eval(expression, facts) do
{:ok, return_value} ->
%ExpectationEvaluation{name: name, type: type, return_value: return_value}

{:error, :einkey} ->
%ExpectationEvaluationError{
name: name,
message: "Fact is not present.",
type: :fact_missing_error
}

_ ->
%ExpectationEvaluationError{
name: name,
message: "Illegal expression provided, check expression syntax.",
type: :illegal_expression_error
}
end
end

defp add_expectation_evaluations(
%CheckResult{check_id: check_id, agents_check_results: agents_check_results} = result
) do
expectation_results =
agents_check_results
|> Enum.flat_map(fn %AgentCheckResult{expectation_evaluations: expectation_evaluations} ->
expectation_evaluations
end)
|> Enum.group_by(& &1.name)
|> Enum.map(fn {name, expectation_evaluations} ->
%{type: type} = Catalog.get_expectation(check_id, name)

%ExpectationResult{
name: name,
type: type,
result: eval_expectation_result_or_error(type, expectation_evaluations)
}
end)

%CheckResult{result | expectation_results: expectation_results}
end

defp eval_expectation_result_or_error(type, expectation_evaluations) do
if has_error?(expectation_evaluations) do
false
else
eval_expectation_result(type, expectation_evaluations)
end
end

defp has_error?(expectation_evaluations) do
Enum.any?(expectation_evaluations, fn
%ExpectationEvaluationError{} -> true
_ -> false
end)
end

defp eval_expectation_result(:expect_same, expectation_evaluations) do
expectation_evaluations
|> Enum.uniq_by(& &1.return_value)
|> Kernel.length() == 1
end

defp eval_expectation_result(:expect, expectations_evaluations) do
Enum.all?(expectations_evaluations, &(&1.return_value == true))
end

defp aggregate_check_result(
%CheckResult{expectation_results: expectation_results} = check_result
) do
result =
if Enum.all?(expectation_results, &(&1.result == true)) do
:passing
else
:critical
end

%CheckResult{check_result | result: result}
end

defp aggregate_execution_result(%Result{check_results: check_results} = execution_result) do
result =
check_results
|> Enum.map(& &1.result)
|> Enum.map(&{&1, result_weight(&1)})
|> Enum.max_by(fn {_, weight} -> weight end)
|> elem(0)

%Result{execution_result | result: result}
end

# TODO: is unknown needed?
# defp result_weight(:unknown), do: 3
defp result_weight(:critical), do: 2
defp result_weight(:warning), do: 1
defp result_weight(:passing), do: 0
end
17 changes: 17 additions & 0 deletions lib/wanda/execution/expectation_evaluation.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
defmodule Wanda.Execution.ExpectationEvaluation do
@moduledoc """
Represents the evaluation of an expectation.
"""

defstruct [
:name,
:return_value,
:type
]

@type t :: %__MODULE__{
name: String.t(),
return_value: number() | boolean() | String.t(),
type: :expect | :expect_same
}
end
17 changes: 17 additions & 0 deletions lib/wanda/execution/expectation_evaluation_error.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
defmodule Wanda.Execution.ExpectationEvaluationError do
@moduledoc """
Represents an error occurred during the evaluation of an expectation.
"""

defstruct [
:name,
:message,
:type
]

@type t :: %__MODULE__{
name: String.t(),
message: String.t(),
type: :fact_missing_error | :illegal_expression_error
}
end
6 changes: 4 additions & 2 deletions lib/wanda/execution/expectation_result.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ defmodule Wanda.Execution.ExpectationResult do

defstruct [
:name,
:result
:result,
:type
]

@type t :: %__MODULE__{
name: String.t(),
result: boolean() | :fact_missing_error | :illegal_expression_error
result: boolean(),
type: :expect | :expect_same
}
end
Loading

0 comments on commit 6d5120f

Please sign in to comment.