Skip to content

Updates schema for simplified calling convention #70

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
May 9, 2016
67 changes: 59 additions & 8 deletions lib/spanner/config.ex
Original file line number Diff line number Diff line change
@@ -1,7 +1,17 @@
defmodule Spanner.Config do
alias Spanner.Config.SyntaxValidator
alias Spanner.Config.SemanticValidator
alias Spanner.Config.Upgrader

@current_config_version 3
@old_config_version @current_config_version - 1
@config_extensions [".yaml", ".yml", ".json"]
@config_file "config"

@doc "Returns the current supported config version"
def current_config_version,
do: @current_config_version

@doc "Returns a list of valid config extensions"
def config_extensions,
do: @config_extensions
Expand Down Expand Up @@ -40,21 +50,62 @@ defmodule Spanner.Config do
def fixup_rules(config),
do: config

@doc "Validate bundle configs"
def validate(config) do
case Spanner.Config.SyntaxValidator.validate(config) do
@doc """
Validates bundle configs. Returns {:ok, config} | {:error, errors, warnings} |
{:warning, config, warnings}
"""
@spec validate(Map.t) ::
{:ok, Map.t} | {:error, List.t, List.t} | {:warning, Map.t, List.t}
def validate(%{"cog_bundle_version" => @current_config_version}=config) do
case SyntaxValidator.validate(config) do
:ok ->
config = fixup_rules(config)
case Spanner.Config.SemanticValidator.validate(config) do
case SemanticValidator.validate(config) do
:ok ->
{:ok, config}
error ->
error
{:error, errors} ->
{:error, errors, []}
end
error ->
error
{:error, errors} ->
{:error, errors, []}
end
end
def validate(%{"cog_bundle_version" => @old_config_version}=config) do
# Upgrader will return an upgraded config and a list of warnings
# or an error
case Upgrader.upgrade(config) do
{:ok, upgraded_config, warnings} ->
# We still need to validate the upgraded config
case validate(upgraded_config) do
{:ok, validated_config} ->
# If everything goes well, we return the validated config
# and a list of warnings.
{:warning, validated_config, warnings}
{:error, errors, _} ->
{:error, errors, warnings}
end
{:error, errors, warnings} ->
{:error, errors, warnings}
end
end
def validate(%{"cog_bundle_version" => version}) do
{:error,
[{"""
cog_bundle_version #{version} is not supported. \
Please update your bundle config to version #{@current_config_version}.\
""",
"#/cog_bundle_version"}],
[]}
end
def validate(_) do
{:error,
[{"""
cog_bundle_version not specified. You must specify a valid bundle \
version. The current version is #{@current_config_version}.\
""",
"#/cog_bundle_version"}],
[]}
end

defp fix_rules(bundle_command, rules) do
Enum.map(rules, &fix_rule(bundle_command, &1))
Expand Down
33 changes: 17 additions & 16 deletions lib/spanner/config/syntax_validator.ex
Original file line number Diff line number Diff line change
@@ -1,27 +1,27 @@
defmodule Spanner.Config.SyntaxValidator do

@schema_file Path.join([:code.priv_dir(:spanner), "schemas", "bundle_config_schema.yaml"])
@current_config_version Spanner.Config.current_config_version
@old_config_version @current_config_version - 1

@external_resource @schema_file
@current_schema_file Path.join([:code.priv_dir(:spanner), "schemas", "bundle_config_schema_v#{@current_config_version}.yaml"])
@old_schema_file Path.join([:code.priv_dir(:spanner), "schemas", "bundle_config_schema_v#{@old_config_version}.yaml"])

@schema File.read!(@schema_file)
@external_resource @current_schema_file
@external_resource @old_schema_file

@current_schema File.read!(@current_schema_file)
@old_schema File.read!(@old_schema_file)

@moduledoc """
Validates bundle config syntax leveraging JsonSchema.
"""

@doc """
Accepts a config map and validates syntax. Validate does three major checks.
An error can be returned during any one of these. First it does some basic
validation on the config using JsonSchema. Last we validate that all rules
at least parse.
Accepts a config map and validates syntax.
"""
@spec validate(Map.t) :: :ok | {:ok, [{String.t, String.t}]}
def validate(config) do
# Note: We could validate command calling convention with ExJsonEchema
# but the error that it returned was less than informative so instead
# we just do it manually. It may be worth revisiting in the future.
with {:ok, schema} <- load_schema("bundle_config_schema"),
@spec validate(Map.t, integer()) :: :ok | {:error, [{String.t, String.t}]}
def validate(config, version \\ @current_config_version) do
with {:ok, schema} <- load_schema(version),
{:ok, resolved_schema} <- resolve_schema(schema),
:ok <- ExJsonSchema.Validator.validate(resolved_schema, config),
do: :ok
Expand All @@ -42,7 +42,8 @@ defmodule Spanner.Config.SyntaxValidator do
end
end

defp load_schema(_name) do
Spanner.Config.Parser.read_from_string(@schema)
end
defp load_schema(@old_config_version),
do: Spanner.Config.Parser.read_from_string(@old_schema)
defp load_schema(_),
do: Spanner.Config.Parser.read_from_string(@current_schema)
end
119 changes: 119 additions & 0 deletions lib/spanner/config/upgrader.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
defmodule Spanner.Config.Upgrader do
alias Spanner.Config.SyntaxValidator

@moduledoc """
Attempts to upgrade bundle config to current version.
"""

@current_version Spanner.Config.current_config_version
@upgradable_version @current_version - 1

@doc """
When possible upgrades config to the current version. Returns the upgraded
config and a list of warnings for deprecated config options or an error if
the upgrade fails.
"""
@spec upgrade(Map.t) :: {:ok, Map.t, List.t} | {:error, List.t}
def upgrade(%{"cog_bundle_version" => @upgradable_version}=config) do
deprecation_msg =
{"""
Bundle config version #{@upgradable_version} has been deprecated. \
Please update to version #{@current_version}.\
""",
"#/cog_bundle_version"}
# We run the validator for the old version here. So if the user passes an
# old version that is also invalid, we don't crash trying to access fields
# that don't exist.
case SyntaxValidator.validate(config, @upgradable_version) do
:ok ->
do_upgrade(config)
|> insert_deprecation_msg(deprecation_msg)
{:error, errors} ->
{:error, errors, [deprecation_msg]}
end
end

defp do_upgrade(config) do
case execution_once?(config["commands"]) do
{true, {warnings, errors}} ->
{:error, errors, warnings}
{false, warnings} ->
{enforce_warnings, updated_commands} = update_enforcing(config["commands"])
updated_config = %{config | "commands" => updated_commands}
|> Map.put("cog_bundle_version", @current_version)
{:ok, updated_config, warnings ++ enforce_warnings}
end
end

# Checks to see if the execution field exists in the bundle config. If it
# does but contains "multiple" we just return a warning. If it contains
# "once" we return an error.
defp execution_once?(commands) do
{warnings, errors} = Enum.reduce(commands, {[],[]},
fn
({cmd_name, cmd}, {warnings, errors}) ->
case Map.get(cmd, "execution", nil) do
"once" ->
msg = "Execution 'once' commands are no longer supported"
location = "#/commands/#{cmd_name}/execution"
updated_errors = [{msg, location} | errors]
{warnings, updated_errors}
"multiple" ->
msg = """
Execution type has been deprecated. \
Please update your bundle config to version #{@current_version}.\
"""
location = "#/commands/#{cmd_name}/execution"
updated_warnings = [{msg, location} | warnings]
{updated_warnings, errors}
nil ->
{warnings, errors}
end
end)

if length(errors) > 0 do
{true, {warnings, errors}}
else
{false, warnings}
end

end

# Updates for non enforcing commands. If 'enforcing: false' is specified we
# add the "allow" rule, delete the enforcing field and return a warning. If
# 'enforcing: true' is specified we return a warning and delete the field.
defp update_enforcing(commands) do
Enum.reduce(commands, {[], commands},
fn
({cmd_name, %{"enforcing" => enforcing}=cmd}, {warnings, commands}) when not(enforcing) ->
msg = """
Non-enforcing commands have been deprecated. \
Please update your bundle config to version #{@current_version}.\
"""
updated_warnings = [{msg, "#/commands/#{cmd_name}/enforcing"} | warnings]

updated_cmd = Map.put(cmd, "rules", ["allow"])
|> Map.delete("enforcing")
updated_commands = Map.put(commands, cmd_name, updated_cmd)

{updated_warnings, updated_commands}
({cmd_name, %{"enforcing" => _}=cmd}, {warnings, commands}) ->
msg = """
The 'enforcing' field has been deprecated. \
Please update your bundle config to version #{@current_version}.\
"""
updated_warnings = [{msg, "#/commands/#{cmd_name}/enforcing"} | warnings]

updated_cmd = Map.delete(cmd, "enforcing")
updated_commands = Map.put(commands, cmd_name, updated_cmd)

{updated_warnings, updated_commands}
(_, acc) ->
acc
end)
end

defp insert_deprecation_msg({status, errors, warnings}, msg) do
{status, errors, [msg | warnings]}
end
end
102 changes: 102 additions & 0 deletions priv/schemas/bundle_config_schema_v3.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
---
"$schema": http://json-schema.org/draft-04/schema#
title: Bundle Config v3
description: A config schema for bundles
type: object
required:
- cog_bundle_version
- name
- version
- commands
additionalProperties: false
properties:
cog_bundle_version:
type: number
enum:
- 3
name:
type: string
version:
type: string
pattern: ^\d+\.\d+($|\.\d+$)
permissions:
type: array
items:
type: string
docker:
type: object
required:
- image
- tag
properties:
image:
type: string
tag:
type: string
templates:
type: object
additionalProperties:
"$ref": "#/definitions/template"
commands:
type: object
additionalProperties:
"$ref": "#/definitions/command"

############# DEFINITIONS #################
definitions:
template:
type: object
additionalProperties: false
properties:
slack:
type: string
hipchat:
type: string
command:
type: object
additionalProperties: false
required:
- executable
- rules
properties:
executable:
type: string
documentation:
type: string
rules:
type: array
items:
type: string
env_vars:
type: object
additionalProperties:
type:
- string
- boolean
- number
options:
type: object
additionalProperties:
"$ref": "#/definitions/command_option"
command_option:
type: object
required:
- type
additionalProperties: false
properties:
type:
type: string
enum:
- int
- float
- bool
- string
- incr
- list
description:
type: string
required:
type: boolean
short_flag:
type: string

Loading