Skip to content

Commit 152c270

Browse files
author
Matthew Peck
committed
Add force installation support for bundles
1 parent 45d7f65 commit 152c270

File tree

4 files changed

+170
-10
lines changed

4 files changed

+170
-10
lines changed

lib/cog/repository/bundles.ex

Lines changed: 43 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ defmodule Cog.Repository.Bundles do
3535

3636
@permanent_site_bundle_version "0.0.0"
3737

38+
@install_types [:normal, :force]
39+
@default_install_type :normal
40+
3841
########################################################################
3942
# CRUD Operations
4043

@@ -48,10 +51,11 @@ defmodule Cog.Repository.Bundles do
4851
#{inspect @reserved_bundle_names}
4952
5053
"""
51-
def install(%{"name" => reserved}) when reserved in @reserved_bundle_names,
54+
def install(install_type \\ @default_install_type, params)
55+
def install(_, %{"name" => reserved}) when reserved in @reserved_bundle_names,
5256
do: {:error, {:reserved_bundle, reserved}}
53-
def install(params),
54-
do: __install(params)
57+
def install(install_type, params) when install_type in @install_types,
58+
do: __install(install_type, params)
5559

5660
def install_from_registry(bundle, version) do
5761
case BundleRegistry.get_config(bundle, version) do
@@ -67,8 +71,12 @@ defmodule Cog.Repository.Bundles do
6771

6872
# TODO: clean this up so we only need the config file part;
6973
# everything else is redundant
70-
defp __install(%{"name" => _name, "version" => _version, "config_file" => _config}=params) do
71-
case install_bundle(params) do
74+
defp __install(install_type \\ @default_install_type, params)
75+
defp __install(install_type, %{"name" => _name,
76+
"version" => _version,
77+
"config_file" => _config}=params)
78+
when install_type in @install_types do
79+
case install_bundle(install_type, params) do
7280
{:ok, bundle_version} ->
7381
{:ok, preload(bundle_version)}
7482
{:error, _}=error ->
@@ -634,7 +642,7 @@ defmodule Cog.Repository.Bundles do
634642
Repo.preload(bundle, [:permissions, :commands])
635643
end
636644

637-
defp new_version!(bundle, params) do
645+
defp new_version!(:normal, bundle, params) do
638646
params = Map.merge(params, Map.take(params["config_file"], ["description", "long_description", "author", "homepage"]))
639647

640648
bundle
@@ -643,6 +651,32 @@ defmodule Cog.Repository.Bundles do
643651
|> Repo.insert!
644652
|> preload
645653
end
654+
defp new_version!(:force, bundle, params) do
655+
# If we are doing a force install we check to see if the version
656+
# already exists.
657+
version_num = params["config_file"]["version"]
658+
result = Repo.one(from bv in BundleVersion,
659+
where: bv.bundle_id == ^bundle.id,
660+
where: bv.version == ^version_num)
661+
662+
# If there is an existing version we remove it before adding the
663+
# new version.
664+
case result do
665+
nil ->
666+
new_version!(:normal, bundle, params)
667+
old_version ->
668+
# If the old version was enabled we enable the new version
669+
if enabled?(old_version) do
670+
Repo.delete!(old_version)
671+
new_version = new_version!(:normal, bundle, params)
672+
set_bundle_version_status(new_version, :enabled)
673+
new_version
674+
else
675+
Repo.delete!(old_version)
676+
new_version!(:normal, bundle, params)
677+
end
678+
end
679+
end
646680

647681
# Consolidate what we need to preload for various things so we stay
648682
# consistent
@@ -668,14 +702,14 @@ defmodule Cog.Repository.Bundles do
668702
alias Cog.Models.CommandOption
669703
alias Cog.Models.Permission
670704

671-
defp install_bundle(bundle_params) do
705+
defp install_bundle(install_type, bundle_params) do
672706
Repo.transaction(fn ->
673707
try do
674708
# Create a bundle record if it doesn't exist yet
675709
bundle = find_or_create_bundle(bundle_params["name"])
676710

677-
# Create a new version record
678-
version = new_version!(bundle, bundle_params)
711+
# Create the new version
712+
version = new_version!(install_type, bundle, bundle_params)
679713

680714
# Add permissions, after deduping
681715
:ok = register_permissions_for_version(bundle, version)

test/cog/repository/bundles_test.exs

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,72 @@ defmodule Cog.Repository.BundlesTest do
145145
assert {:error, {:db_errors, [version: {"has already been taken", []}]}} = Bundles.install(%{"name" => "testing", "version" => "1.0.0", "config_file" => %{}})
146146
end
147147

148+
describe "forced bundle installation" do
149+
150+
setup [:forced_installation_configs]
151+
152+
test "installing the same version overwrites the original version", %{old_config: config} do
153+
{:ok, _version} = Bundles.install(%{"name" => config["name"], "version" => config["version"], "config_file" => config})
154+
155+
assert {:ok, _overwritten_version} = Bundles.install(:force, %{"name" => config["name"], "version" => config["version"], "config_file" => config})
156+
end
157+
158+
test "different configs persist properly", %{old_config: old_config, new_config: new_config} do
159+
160+
{:ok, orig_version} = Bundles.install(%{"name" => old_config["name"],
161+
"version" => old_config["version"],
162+
"config_file" => old_config})
163+
164+
# Make sure the bundle installs
165+
assert {:ok, new_version} = Bundles.install(:force, %{"name" => new_config["name"],
166+
"version" => new_config["version"],
167+
"config_file" => new_config})
168+
169+
new_version = Repo.preload(new_version, :templates)
170+
171+
# Make sure the bundle name and version didn't change
172+
assert orig_version.bundle.name == new_version.bundle.name
173+
assert orig_version.version == new_version.version
174+
175+
# Check the config file
176+
assert new_version.config_file == new_config
177+
178+
# Check commands
179+
expected_command_names = Map.keys(new_config["commands"]) |> Enum.sort
180+
actual_command_names = Enum.map(new_version.commands, &(&1.command.name)) |> Enum.sort
181+
assert actual_command_names == expected_command_names
182+
183+
# Check permissions
184+
expected_permissions = new_config["permissions"] |> Enum.sort
185+
actual_permissions = Enum.map(new_version.permissions, &("test_bundle:#{&1.name}")) |> Enum.sort
186+
assert actual_permissions == expected_permissions
187+
188+
# Check templates
189+
expected_template_names = Map.keys(new_config["templates"]) |> Enum.sort
190+
actual_template_names = Enum.map(new_version.templates, &(&1.name)) |> Enum.sort
191+
assert actual_template_names == expected_template_names
192+
193+
expected_templates = Enum.map(new_config["templates"], fn({_, source}) -> source["body"] end) |> Enum.sort
194+
actual_templates = Enum.map(new_version.templates, &(&1.source)) |> Enum.sort
195+
assert actual_templates == expected_templates
196+
end
197+
198+
test "maintains enabled status", %{old_config: old_config, new_config: new_config} do
199+
{:ok, orig_version} = Bundles.install(%{"name" => old_config["name"],
200+
"version" => old_config["version"],
201+
"config_file" => old_config})
202+
203+
:ok = Bundles.set_bundle_version_status(orig_version, :enabled)
204+
205+
{:ok, new_version} = Bundles.install(:force, %{"name" => new_config["name"],
206+
"version" => new_config["version"],
207+
"config_file" => new_config})
208+
209+
assert Bundles.enabled?(new_version)
210+
end
211+
212+
end
213+
148214
test "deleting the last version of a bundle deletes the bundle itself" do
149215
{:ok, version} = Bundles.install(%{"name" => "testing", "version" => "1.0.0", "config_file" => %{}})
150216

@@ -404,4 +470,50 @@ defmodule Cog.Repository.BundlesTest do
404470
v
405471
end
406472

473+
# Setup function for testing forced bundle installations
474+
defp forced_installation_configs(context) do
475+
old_config =
476+
%{"cog_bundle_version" => 4,
477+
"name" => "test_bundle",
478+
"description" => "A test bundle",
479+
"version" => "0.1.0",
480+
"permissions" => ["test_bundle:date", "test_bundle:time"],
481+
"docker" => %{"image" => "operable-bundle/test_bundle",
482+
"tag" => "v0.1.0"},
483+
"commands" => %{"date" => %{"executable" => "/usr/local/bin/date",
484+
"options" => %{"option1" => %{"type" => "string",
485+
"description" => "An option",
486+
"required" => false,
487+
"short_flag" => "o"}},
488+
"rules" => ["when command is test_bundle:date must have test_bundle:date"]},
489+
"time" => %{"executable" => "/usr/local/bin/time",
490+
"rules" => ["when command is test_bundle:time must have test_bundle:time"]}},
491+
"templates" => %{"time" => %{"body" => "~$results[0].time~"},
492+
"date" => %{"body" => "~$results[0].date~"}}}
493+
494+
new_config =
495+
%{"cog_bundle_version" => 4,
496+
"name" => "test_bundle",
497+
"description" => "An updated test bundle",
498+
"version" => "0.1.0",
499+
"permissions" => ["test_bundle:new_date", "test_bundle:new_time", "test_bundle:another_command"],
500+
"docker" => %{"image" => "operable-bundle/test_bundle_update",
501+
"tag" => "v0.1.1"},
502+
"commands" => %{"new_date" => %{"executable" => "/usr/local/bin/date",
503+
"options" => %{"option1" => %{"type" => "string",
504+
"description" => "An option",
505+
"required" => false,
506+
"short_flag" => "o"}},
507+
"rules" => ["when command is test_bundle:date must have test_bundle:date"]},
508+
"new_time" => %{"executable" => "/usr/local/bin/time",
509+
"rules" => ["when command is test_bundle:time must have test_bundle:time"]},
510+
"another_command" => %{"executable" => "/usr/local/bin/time",
511+
"rules" => ["when command is test_bundle:time must have test_bundle:time"]}},
512+
"templates" => %{"new_time" => %{"body" => "~$results[0].new_time~"},
513+
"new_date" => %{"body" => "~$results[0].new_date~"},
514+
"another" => %{"body" => "~$results[0].another~"}}}
515+
516+
Map.merge(context, %{old_config: old_config, new_config: new_config})
517+
end
518+
407519
end

test/controllers/v1/bundles_controller_test.exs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,14 @@ defmodule Cog.V1.BundlesControllerTest do
134134
"version has already been taken"] = json_response(conn, 409)["errors"]
135135
end
136136

137+
test "force enables installation of an already installed version", %{authed: requestor} do
138+
config = config(:map)
139+
{:ok, _orig_version3} = Bundles.install(%{"name" => config["name"], "version" => config["version"], "config_file" => config})
140+
141+
conn = api_request(requestor, :post, "/v1/bundles", body: %{"bundle" => %{"config" => config, "force" => true}})
142+
assert response(conn, 201)
143+
end
144+
137145
test "fails to install with semantically invalid config", %{authed: requestor} do
138146
# The config includes rules that mention permissions; if we remove
139147
# those permissions, installation should fail

web/controllers/v1/bundles_controller.ex

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,9 +76,10 @@ defmodule Cog.V1.BundlesController do
7676

7777
defp install_bundle(params) do
7878
with {:ok, config} <- parse_config(params),
79+
{:ok, install_type} <- install_type(params),
7980
{:ok, valid_config, warnings} <- validate_config(config),
8081
{:ok, params} <- merge_config(params, valid_config),
81-
{:ok, bundle} <- Repository.Bundles.install(params) do
82+
{:ok, bundle} <- Repository.Bundles.install(install_type, params) do
8283
{:ok, bundle, warnings}
8384
end
8485
end
@@ -92,6 +93,11 @@ defmodule Cog.V1.BundlesController do
9293
defp parse_config(_),
9394
do: {:error, :no_config}
9495

96+
defp install_type(%{"force" => true}),
97+
do: {:ok, :force}
98+
defp install_type(_),
99+
do: {:ok, :normal}
100+
95101
# If we have a file, check to see if the filename has the correct extension
96102
defp validate_file_format(%Plug.Upload{filename: filename}) do
97103
if Config.config_extension?(filename) do

0 commit comments

Comments
 (0)