Skip to content

Commit

Permalink
Implement stack destruction (#813)
Browse files Browse the repository at this point in the history
  • Loading branch information
michaeljguarino authored Apr 7, 2024
1 parent f508fc8 commit 576cff8
Show file tree
Hide file tree
Showing 17 changed files with 186 additions and 13 deletions.
2 changes: 1 addition & 1 deletion AGENT_VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
v0.4.17
v0.4.18
8 changes: 8 additions & 0 deletions assets/src/generated/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1888,6 +1888,8 @@ export type InfrastructureStack = {
cluster?: Maybe<Cluster>;
/** version/image config for the tool you're using */
configuration: StackConfiguration;
/** whether this stack was previously deleted and is pending cleanup */
deletedAt?: Maybe<Scalars['DateTime']['output']>;
/** environment variables for this stack */
environment?: Maybe<Array<Maybe<StackEnvironment>>>;
/** files bound to a run of this stack */
Expand Down Expand Up @@ -3775,6 +3777,7 @@ export type RootMutationType = {
detachCluster?: Maybe<Cluster>;
/** removes a service from storage, but bypasses waiting for the agent to fully drain it from its hosting cluster */
detachServiceDeployment?: Maybe<ServiceDeployment>;
detachStack?: Maybe<InfrastructureStack>;
enableDeployments?: Maybe<DeploymentSettings>;
executeRunbook?: Maybe<RunbookActionResponse>;
/** forces a pipeline gate to be in open state */
Expand Down Expand Up @@ -4226,6 +4229,11 @@ export type RootMutationTypeDetachServiceDeploymentArgs = {
};


export type RootMutationTypeDetachStackArgs = {
id: Scalars['ID']['input'];
};


export type RootMutationTypeExecuteRunbookArgs = {
input: RunbookActionInput;
name: Scalars['String']['input'];
Expand Down
1 change: 1 addition & 0 deletions lib/console/deployments/events.ex
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ defmodule Console.PubSub.ManagedNamespaceDeleted, do: use Piazza.PubSub.Event
defmodule Console.PubSub.StackCreated, do: use Piazza.PubSub.Event
defmodule Console.PubSub.StackUpdated, do: use Piazza.PubSub.Event
defmodule Console.PubSub.StackDeleted, do: use Piazza.PubSub.Event
defmodule Console.PubSub.StackDetached, do: use Piazza.PubSub.Event

defmodule Console.PubSub.StackRunCreated, do: use Piazza.PubSub.Event
defmodule Console.PubSub.StackRunUpdated, do: use Piazza.PubSub.Event
Expand Down
3 changes: 2 additions & 1 deletion lib/console/deployments/global.ex
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,7 @@ defmodule Console.Deployments.Global do
{:ok, dest_secrets} <- Services.configuration(dest),
{:diff, true} <- {:diff, diff?(source, dest, source_secrets, dest_secrets)} do
Services.update_service(%{
templated: source.templated,
namespace: source.namespace,
configuration: Enum.map(Map.merge(dest_secrets, source_secrets), fn {k, v} -> %{name: k, value: v} end),
repository_id: source.repository_id,
Expand Down Expand Up @@ -373,7 +374,7 @@ defmodule Console.Deployments.Global do
def diff?(_, _), do: false

defp diff?(%Service{} = s, %Service{} = d, source, dest) do
missing_source?(source, dest) || specs_different?(s, d) || s.repository_id != d.repository_id || s.namespace != d.namespace
missing_source?(source, dest) || specs_different?(s, d) || s.repository_id != d.repository_id || s.namespace != d.namespace || s.templated != d.templated
end

defp ensure_revision(%ServiceTemplate{} = template, config) do
Expand Down
16 changes: 13 additions & 3 deletions lib/console/deployments/pubsub/recurse.ex
Original file line number Diff line number Diff line change
Expand Up @@ -137,11 +137,21 @@ defimpl Console.PubSub.Recurse, for: [Console.PubSub.StackCreated, Console.PubSu
def process(%{item: stack}), do: Stacks.poll(stack)
end

defimpl Console.PubSub.Recurse, for: Console.PubSub.StackDeleted do
alias Console.Deployments.Stacks

def process(%{item: stack}), do: Stacks.create_run(stack, stack.sha)
end

defimpl Console.PubSub.Recurse, for: [Console.PubSub.StackRunCompleted] do
alias Console.Schema.{Stack, StackRun}
alias Console.Deployments.Stacks

def process(%{item: run}) do
Stacks.get_stack!(run.stack_id)
|> Stacks.dequeue()
def process(%{item: %{id: id} = run}) do
case {Stacks.get_stack!(run.stack_id), run} do
{%Stack{delete_run_id: ^id} = stack, %StackRun{status: :successful}} ->
Console.Repo.delete(stack)
{stack, _} -> Stacks.dequeue(stack)
end
end
end
2 changes: 1 addition & 1 deletion lib/console/deployments/services.ex
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,7 @@ defmodule Console.Deployments.Services do
do: {:ok, merge_configuration(secrets, attrs[:configuration])}
end)
|> add_operation(:create, fn %{source: source, config: config} ->
Map.take(source, [:repository_id, :sha, :name, :namespace])
Map.take(source, [:repository_id, :sha, :name, :namespace, :templated])
|> Console.dedupe(:git, Console.mapify(source.git))
|> Console.dedupe(:helm, Console.mapify(source.helm))
|> Console.dedupe(:kustomize, Console.mapify(source.kustomize))
Expand Down
30 changes: 26 additions & 4 deletions lib/console/deployments/stacks.ex
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,23 @@ defmodule Console.Deployments.Stacks do
@doc """
It can delete a stack if the user has write perms
"""
@spec detach_stack(binary, User.t) :: stack_resp
def detach_stack(id, %User{} = user) do
get_stack!(id)
|> allow(user, :write)
|> when_ok(:delete)
|> notify(:detach, user)
end

@doc """
Schedules a stack to be destroyed
"""
@spec delete_stack(binary, User.t) :: stack_resp
def delete_stack(id, %User{} = user) do
get_stack!(id)
|> Stack.delete_changeset(%{deleted_at: Timex.now()})
|> allow(user, :write)
|> when_ok(:delete)
|> when_ok(:update)
|> notify(:delete, user)
end

Expand Down Expand Up @@ -164,6 +176,9 @@ defmodule Console.Deployments.Stacks do
Polls a stack's git repo and creates a run if there's a new commit
"""
@spec poll(Stack.t) :: run_resp
def poll(%Stack{delete_run_id: id}) when is_binary(id),
do: {:error, "stack is deleting"}

def poll(%Stack{sha: sha} = stack) do
%{repository: repo} = stack = Repo.preload(stack, [:repository, :environment])
case Discovery.sha(repo, stack.git.ref) do
Expand All @@ -182,23 +197,28 @@ defmodule Console.Deployments.Stacks do
|> add_operation(:run, fn _ ->
%StackRun{stack_id: stack.id, status: :queued}
|> StackRun.changeset(
stack
|> Map.take(~w(approval configuration type environment job_spec repository_id cluster_id)a)
Repo.preload(stack, [:environment, :files])
|> Map.take(~w(approval configuration type environment files job_spec repository_id cluster_id)a)
|> Console.clean()
|> Map.put(:git, %{ref: sha, folder: stack.git.folder})
|> Map.put(:steps, commands(stack, !!attrs[:dry_run]))
|> Map.merge(attrs)
)
|> Repo.insert()
end)
|> add_operation(:stack, fn _ ->
|> add_operation(:stack, fn %{run: run} ->
Ecto.Changeset.change(stack, %{sha: sha})
|> Stack.delete_changeset(delete_run(stack, run))
|> Repo.update()
end)
|> execute(extract: :run)
|> notify(:create)
end

defp delete_run(%Stack{deleted_at: d}, %{id: id}) when not is_nil(d),
do: %{delete_run_id: id}
defp delete_run(_, _), do: %{}

@doc """
Fetches a file handle to the tarball for a stack run
"""
Expand Down Expand Up @@ -261,6 +281,8 @@ defmodule Console.Deployments.Stacks do
do: handle_notify(PubSub.StackUpdated, stack, actor: actor)
defp notify({:ok, %Stack{} = stack}, :delete, actor),
do: handle_notify(PubSub.StackDeleted, stack, actor: actor)
defp notify({:ok, %Stack{} = stack}, :detach, actor),
do: handle_notify(PubSub.StackDetached, stack, actor: actor)
defp notify(pass, _, _), do: pass

defp notify({:ok, %StackRun{} = stack}, :create),
Expand Down
7 changes: 7 additions & 0 deletions lib/console/deployments/stacks/commands.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@ defmodule Console.Deployments.Stacks.Commands do
])
end

defp terraform_commands(%Stack{deleted_at: d}, _) when not is_nil(d) do
indexed([
cmd("init", "terraform", ["init", "-upgrade"], :plan),
cmd("destroy", "terraform", ["destroy", "-auto-approve"], :apply)
])
end

defp terraform_commands(%Stack{}, _) do
indexed([
cmd("init", "terraform", ["init", "-upgrade"], :plan),
Expand Down
8 changes: 8 additions & 0 deletions lib/console/graphql/deployments/stack.ex
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ defmodule Console.GraphQl.Deployments.Stack do
field :job_spec, :job_gate_spec, description: "optional k8s job configuration for the job that will apply this stack"
field :configuration, non_null(:stack_configuration), description: "version/image config for the tool you're using"
field :approval, :boolean, description: "whether to require approval"
field :deleted_at, :datetime, description: "whether this stack was previously deleted and is pending cleanup"

connection field :runs, node_type: :stack_run do
resolve &Deployments.list_stack_runs/3
Expand Down Expand Up @@ -281,6 +282,13 @@ defmodule Console.GraphQl.Deployments.Stack do
resolve &Deployments.delete_stack/2
end

field :detach_stack, :infrastructure_stack do
middleware Authenticated
arg :id, non_null(:id)

resolve &Deployments.detach_stack/2
end

field :approve_stack_run, :stack_run do
middleware Authenticated
arg :id, non_null(:id)
Expand Down
3 changes: 3 additions & 0 deletions lib/console/graphql/resolvers/deployments/stack.ex
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ defmodule Console.GraphQl.Resolvers.Deployments.Stack do
def delete_stack(%{id: id}, %{context: %{current_user: user}}),
do: Stacks.delete_stack(id, user)

def detach_stack(%{id: id}, %{context: %{current_user: user}}),
do: Stacks.detach_stack(id, user)

def update_stack_run(%{id: id, attributes: attrs}, ctx),
do: Stacks.update_stack_run(attrs, id, actor(ctx))

Expand Down
7 changes: 7 additions & 0 deletions lib/console/schema/stack.ex
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ defmodule Console.Schema.Stack do
field :approval, :boolean
field :sha, :string
field :last_successful, :string
field :deleted_at, :utc_datetime_usec

field :write_policy_id, :binary_id
field :read_policy_id, :binary_id
Expand All @@ -50,6 +51,7 @@ defmodule Console.Schema.Stack do

belongs_to :repository, GitRepository
belongs_to :cluster, Cluster
belongs_to :delete_run, StackRun

has_one :state, StackState, on_replace: :update

Expand Down Expand Up @@ -114,4 +116,9 @@ defmodule Console.Schema.Stack do
|> cast_assoc(:state)
|> validate_required(~w(status)a)
end

def delete_changeset(model, attrs) do
model
|> cast(attrs, ~w(deleted_at delete_run_id)a)
end
end
10 changes: 10 additions & 0 deletions priv/repo/migrations/20240406003015_add_stack_destroy.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
defmodule Console.Repo.Migrations.AddStackDestroy do
use Ecto.Migration

def change do
alter table(:stacks) do
add :deleted_at, :utc_datetime_usec
add :delete_run_id, references(:stack_runs, type: :uuid)
end
end
end
5 changes: 5 additions & 0 deletions schema/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -691,6 +691,8 @@ type RootMutationType {

deleteStack(id: ID!): InfrastructureStack

detachStack(id: ID!): InfrastructureStack

approveStackRun(id: ID!): StackRun

"a reusable mutation for updating rbac settings on core services"
Expand Down Expand Up @@ -987,6 +989,9 @@ type InfrastructureStack {
"whether to require approval"
approval: Boolean

"whether this stack was previously deleted and is pending cleanup"
deletedAt: DateTime

runs(after: String, first: Int, before: String, last: Int): StackRunConnection

"files bound to a run of this stack"
Expand Down
19 changes: 19 additions & 0 deletions test/console/deployments/git/agent_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,25 @@ defmodule Console.Deployments.Git.AgentTest do
assert Process.alive?(pid)
end

test "it can fetch constraints from the bootstrap repo" do
git = insert(:git_repository, url: "https://github.com/pluralsh/bootstrap.git")
svc = insert(:service, repository: git, git: %{ref: "main", folder: "resources/policy/constraints"})

{:ok, pid} = Discovery.start(git)

{:ok, f} = Agent.fetch(pid, svc)
{:ok, tmp} = Briefly.create()

IO.binstream(f, 1024)
|> Enum.into(File.stream!(tmp))
File.close(f)

{:ok, res} = :erl_tar.extract(tmp, [:compressed, :memory])
files = Enum.into(res, %{}, fn {name, content} -> {to_string(name), to_string(content)} end)
IO.inspect(files)
assert map_size(files) > 0
end

test "busted credentials fail as expected" do
git = insert(:git_repository, auth_method: :ssh, url: "[email protected]:pluralsh/test-repo.git", private_key: "busted")

Expand Down
36 changes: 35 additions & 1 deletion test/console/deployments/pubsub/recurse_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ defmodule Console.Deployments.PubSub.RecurseTest do
use Console.DataCase, async: true
use Mimic
alias Console.PubSub
alias Console.Deployments.{Clusters, Services, Global}
alias Console.Deployments.{Clusters, Services, Global, Stacks}
alias Console.Deployments.Git.Discovery
alias Console.PubSub.Consumers.Recurse

Expand Down Expand Up @@ -386,6 +386,28 @@ defmodule Console.Deployments.PubSub.RecurseTest do
end
end

describe "StackDeleted" do
test "it will create a delete run" do
stack = insert(:stack, deleted_at: Timex.now(), sha: "last-sha")

event = %PubSub.StackDeleted{item: stack}
{:ok, run} = Recurse.handle_event(event)

assert run.git.ref == "last-sha"
assert run.stack_id == stack.id

%{steps: steps} = Console.Repo.preload(run, [:steps])
%{"init" => init, "destroy" => destroy} = Map.new(steps, & {&1.name, &1})
assert init.cmd == "terraform"
assert init.args == ["init", "-upgrade"]

assert destroy.cmd == "terraform"
assert destroy.args == ["destroy", "-auto-approve"]

assert refetch(stack).delete_run_id == run.id
end
end

describe "StackRunCompleted" do
test "it can dequeue a stack run" do
stack = insert(:stack)
Expand All @@ -399,5 +421,17 @@ defmodule Console.Deployments.PubSub.RecurseTest do
assert dequeued.id == run.id
assert dequeued.status == :pending
end

test "it can delete a stack if it is in deleting stack" do
stack = insert(:stack, deleted_at: Timex.now(), sha: "last-sha")

{:ok, run} = Stacks.create_run(stack, stack.sha)

event = %PubSub.StackRunCompleted{item: %{run | status: :successful}}
{:ok, deleted} = Recurse.handle_event(event)

assert deleted.id == stack.id
refute refetch(stack)
end
end
end
22 changes: 21 additions & 1 deletion test/console/deployments/stacks_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ defmodule Console.Deployments.StacksTest do
{:ok, deleted} = Stacks.delete_stack(stack.id, user)

assert deleted.id == stack.id
refute refetch(stack)
assert deleted.deleted_at

assert_receive {:event, %PubSub.StackDeleted{item: ^deleted}}
end
Expand All @@ -121,6 +121,26 @@ defmodule Console.Deployments.StacksTest do
end
end

describe "#detach_stack/2" do
test "stack writers can delete" do
user = insert(:user)
stack = insert(:stack, write_bindings: [%{user_id: user.id}])

{:ok, deleted} = Stacks.detach_stack(stack.id, user)

assert deleted.id == stack.id
refute refetch(stack)

assert_receive {:event, %PubSub.StackDetached{item: ^deleted}}
end

test "random users cannot delete" do
stack = insert(:stack)

{:error, _} = Stacks.detach_stack(stack.id, insert(:user))
end
end

describe "#poll/1" do
test "it can create a new run when the sha changes" do
stack = insert(:stack)
Expand Down
Loading

0 comments on commit 576cff8

Please sign in to comment.