Skip to content
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

Expose state urls and plural creds to agent for terraform setup #993

Merged
merged 1 commit into from
May 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions assets/src/generated/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1958,6 +1958,8 @@ export type HttpIngressRule = {

export type InfrastructureStack = {
__typename?: 'InfrastructureStack';
/** the actor of this stack (defaults to root console user) */
actor?: Maybe<User>;
/** whether to require approval */
approval?: Maybe<Scalars['Boolean']['output']>;
/** why this run was cancelled */
Expand All @@ -1966,6 +1968,8 @@ export type InfrastructureStack = {
cluster?: Maybe<Cluster>;
/** version/image config for the tool you're using */
configuration: StackConfiguration;
/** the run that physically destroys the stack */
deleteRun?: Maybe<StackRun>;
/** whether this stack was previously deleted and is pending cleanup */
deletedAt?: Maybe<Scalars['DateTime']['output']>;
/** environment variables for this stack */
Expand Down Expand Up @@ -3145,6 +3149,15 @@ export type PluralContext = {
domains?: Maybe<Array<Maybe<Scalars['String']['output']>>>;
};

/** temporary credentials for the user attached to this stack */
export type PluralCreds = {
__typename?: 'PluralCreds';
/** authentication token to use for gql requests */
token?: Maybe<Scalars['String']['output']>;
/** the api url of this instance */
url?: Maybe<Scalars['String']['output']>;
};

export type PluralGitRepository = {
__typename?: 'PluralGitRepository';
events?: Maybe<Array<Maybe<Event>>>;
Expand Down Expand Up @@ -6583,6 +6596,8 @@ export type Stack = {
};

export type StackAttributes = {
/** user id to use for default Plural authentication in this stack */
actorId?: InputMaybe<Scalars['ID']['input']>;
/** whether to require approval */
approval?: InputMaybe<Scalars['Boolean']['input']>;
/** The cluster on which the terraform will be applied */
Expand Down Expand Up @@ -6691,6 +6706,8 @@ export type StackOutputAttributes = {

export type StackRun = {
__typename?: 'StackRun';
/** the actor of this run (defaults to root console user) */
actor?: Maybe<User>;
/** whether to require approval */
approval?: Maybe<Scalars['Boolean']['output']>;
/** when this run was approved */
Expand All @@ -6711,6 +6728,8 @@ export type StackRun = {
git: GitRef;
id: Scalars['ID']['output'];
insertedAt?: Maybe<Scalars['DateTime']['output']>;
/** the kubernetes job for this run (useful for debugging if issues arise) */
job?: Maybe<Job>;
/** optional k8s job configuration for the job that will apply this stack */
jobSpec?: Maybe<JobGateSpec>;
/** whether you want Plural to manage the state of this stack */
Expand All @@ -6719,12 +6738,15 @@ export type StackRun = {
message?: Maybe<Scalars['String']['output']>;
/** the most recent output for this stack */
output?: Maybe<Array<Maybe<StackOutput>>>;
/** temporary plural creds usable for terraform authentication */
pluralCreds?: Maybe<PluralCreds>;
/** the git repository you're sourcing IaC from */
repository?: Maybe<GitRepository>;
/** the stack attached to this run */
stack?: Maybe<InfrastructureStack>;
/** the most recent state of this stack */
state?: Maybe<StackState>;
stateUrls?: Maybe<StateUrls>;
/** The status of this run */
status: StackStatus;
/** The steps to perform when running this stack */
Expand All @@ -6743,6 +6765,8 @@ export type StackRunAttributes = {
cancellationReason?: InputMaybe<Scalars['String']['input']>;
/** Any errors detected when trying to run this stack */
errors?: InputMaybe<Array<InputMaybe<ServiceErrorAttributes>>>;
/** the reference to the k8s job running this stack */
jobRef?: InputMaybe<NamespacedName>;
/** Output generated by this run */
output?: InputMaybe<Array<InputMaybe<StackOutputAttributes>>>;
/** The state from this runs plan or apply */
Expand Down Expand Up @@ -6840,6 +6864,12 @@ export type StageServiceAttributes = {
serviceId?: InputMaybe<Scalars['ID']['input']>;
};

/** grab-bag of state configuration urls for supported tools */
export type StateUrls = {
__typename?: 'StateUrls';
terraform?: Maybe<TerraformStateUrls>;
};

export type StatefulSet = {
__typename?: 'StatefulSet';
events?: Maybe<Array<Maybe<Event>>>;
Expand Down Expand Up @@ -6980,6 +7010,17 @@ export type TerminatedState = {
startedAt?: Maybe<Scalars['String']['output']>;
};

/** Urls for configuring terraform HTTP remote state */
export type TerraformStateUrls = {
__typename?: 'TerraformStateUrls';
/** GET and POST urls for uploadnig state */
address?: Maybe<Scalars['String']['output']>;
/** POST url to lock state */
lock?: Maybe<Scalars['String']['output']>;
/** POST url to unlock state */
unlock?: Maybe<Scalars['String']['output']>;
};

export enum Tool {
Helm = 'HELM',
Terraform = 'TERRAFORM'
Expand Down
2 changes: 1 addition & 1 deletion lib/console/deployments/pubsub/recurse.ex
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ 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)
def process(%{item: stack}), do: Stacks.create_run(stack, stack.sha, %{message: "destroying stack #{stack.name}"})
end

defimpl Console.PubSub.Recurse, for: Console.PubSub.StackRunUpdated do
Expand Down
47 changes: 47 additions & 0 deletions lib/console/deployments/stacks.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ defmodule Console.Deployments.Stacks do
import Console.Deployments.Policies
import Console.Deployments.Stacks.Commands
alias Console.PubSub
alias Console.Deployments.{Services, Clusters}
alias Console.Deployments.Git.Discovery
alias Console.Deployments.Pr.Dispatcher
alias Kazan.Apis.Batch.V1, as: BatchV1
alias Console.Schema.{
User,
Cluster,
Expand Down Expand Up @@ -46,6 +48,40 @@ defmodule Console.Deployments.Stacks do
|> allow(cluster, :read)
end

@doc """
If a user has write access to the run, fetches temporary plural creds for use in the given stack run.

To be set in the environment by the stack harness process
"""
@spec plural_creds(StackRun.t, User.t | Cluster.t) :: {:ok, %{token: binary, url: binary}}
def plural_creds(%StackRun{} = run, actor) do
with {:ok, run} <- allow(run, actor, :state),
do: plural_creds(run)
end

def plural_creds(%StackRun{} = run) do
case Repo.preload(run, [:actor]) do
%{actor: %User{} = actor} ->
with {:ok, token, _} <- Console.Guardian.encode_and_sign(actor, %{}, ttl: {1, :day}),
do: {:ok, %{token: token, url: Console.graphql_endpoint()}}
_ -> {:ok, nil}
end
end

@doc """
Generates remote state urls for supported tools (only terraform for now)
"""
@spec state_urls(StackRun.t) :: %{terraform: map}
def state_urls(%StackRun{stack_id: id}) do
%{
terraform: %{
address: Services.api_url("v1/states/terraform/#{id}"),
lock: Services.api_url("v1/states/terraform/#{id}/lock"),
unlock: Services.api_url("v1/states/terraform/#{id}/unlock")
}
}
end

@doc """
Creates a new stack if a user can write to its cluster
"""
Expand Down Expand Up @@ -405,6 +441,17 @@ defmodule Console.Deployments.Stacks do
def kick(id, user) when is_binary(id),
do: kick(get_stack!(id), user)

@doc """
Fetches the k8s job resource from the stack runs configured cluster via KAS
"""
@spec run_job(StackRun.t) :: {:ok, BatchV1.Job.t} | error
def run_job(%StackRun{job_ref: %{namespace: ns, name: name}} = run) do
%{cluster: cluster} = Repo.preload(run, [:cluster])
BatchV1.read_namespaced_job!(ns, name)
|> Kazan.run(server: Clusters.control_plane(cluster))
end
def run_job(_), do: {:ok, nil}

defp notify({:ok, %Stack{} = stack}, :create, actor),
do: handle_notify(PubSub.StackCreated, stack, actor: actor)
defp notify({:ok, %Stack{} = stack}, :update, actor),
Expand Down
37 changes: 37 additions & 0 deletions lib/console/graphql/deployments/stack.ex
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
defmodule Console.GraphQl.Deployments.Stack do
use Console.GraphQl.Schema.Base
alias Console.Schema.{Stack, RunStep}
alias Console.Deployments.Stacks
alias Console.GraphQl.Resolvers.{Deployments, User}

ecto_enum :stack_status, Stack.Status
Expand All @@ -19,6 +20,7 @@ defmodule Console.GraphQl.Deployments.Stack do
field :approval, :boolean, description: "whether to require approval"
field :manage_state, :boolean, description: "whether you want Plural to manage your terraform state for this stack"
field :workdir, :string, description: "the subdirectory you want to run the stack's commands w/in"
field :actor_id, :id, description: "user id to use for default Plural authentication in this stack"

field :read_bindings, list_of(:policy_binding_attributes)
field :write_bindings, list_of(:policy_binding_attributes)
Expand All @@ -43,6 +45,7 @@ defmodule Console.GraphQl.Deployments.Stack do

input_object :stack_run_attributes do
field :status, non_null(:stack_status), description: "The status of this run"
field :job_ref, :namespaced_name, description: "the reference to the k8s job running this stack"
field :state, :stack_state_attributes, description: "The state from this runs plan or apply"
field :output, list_of(:stack_output_attributes), description: "Output generated by this run"
field :errors, list_of(:service_error_attributes), description: "Any errors detected when trying to run this stack"
Expand Down Expand Up @@ -115,18 +118,39 @@ defmodule Console.GraphQl.Deployments.Stack do

field :observable_metrics, list_of(:observable_metric), resolve: dataloader(Deployments), description: "a list of metrics to poll to determine if a stack run should be cancelled"

field :delete_run, :stack_run, resolve: dataloader(Deployments), description: "the run that physically destroys the stack"
field :output, list_of(:stack_output), resolve: dataloader(Deployments), description: "the most recent output for this stack"
field :state, :stack_state, resolve: dataloader(Deployments), description: "the most recent state of this stack"

field :cluster, :cluster, resolve: dataloader(Deployments), description: "the cluster this stack runs on"
field :repository, :git_repository, resolve: dataloader(Deployments), description: "the git repository you're sourcing IaC from"

field :actor, :user, resolve: dataloader(User), description: "the actor of this stack (defaults to root console user)"

field :read_bindings, list_of(:policy_binding), resolve: dataloader(Deployments)
field :write_bindings, list_of(:policy_binding), resolve: dataloader(Deployments)

timestamps()
end

@desc "grab-bag of state configuration urls for supported tools"
object :state_urls do
field :terraform, :terraform_state_urls
end

@desc "Urls for configuring terraform HTTP remote state"
object :terraform_state_urls do
field :address, :string, description: "GET and POST urls for uploadnig state"
field :lock, :string, description: "POST url to lock state"
field :unlock, :string, description: "POST url to unlock state"
end

@desc "temporary credentials for the user attached to this stack"
object :plural_creds do
field :token, :string, description: "authentication token to use for gql requests"
field :url, :string, description: "the api url of this instance"
end

object :stack_configuration do
field :image, :string, description: "optional custom image you might want to use"
field :version, non_null(:string), description: "the semver of the tool you wish to use"
Expand All @@ -153,9 +177,22 @@ defmodule Console.GraphQl.Deployments.Stack do
field :workdir, :string, description: "the subdirectory you want to run the stack's commands w/in"
field :manage_state, :boolean, description: "whether you want Plural to manage the state of this stack"

field :state_urls, :state_urls, resolve: fn
run, _, _ -> {:ok, Stacks.state_urls(run)}
end

@desc "the kubernetes job for this run (useful for debugging if issues arise)"
field :job, :job do
resolve fn run, _, _ -> Console.Deployments.Stacks.run_job(run) end
middleware ErrorHandler
end

field :plural_creds, :plural_creds, resolve: &Deployments.plural_creds/3, description: "temporary plural creds usable for terraform authentication"

field :tarball, non_null(:string), resolve: &Deployments.stack_tarball/3, description: "https url to fetch the latest tarball of stack IaC"

field :approver, :user, resolve: dataloader(User), description: "the approver of this job"
field :actor, :user, resolve: dataloader(User), description: "the actor of this run (defaults to root console user)"

field :steps, list_of(:run_step), resolve: dataloader(Deployments), description: "The steps to perform when running this stack"

Expand Down
2 changes: 2 additions & 0 deletions lib/console/graphql/resolvers/deployments/stack.ex
Original file line number Diff line number Diff line change
Expand Up @@ -86,4 +86,6 @@ defmodule Console.GraphQl.Resolvers.Deployments.Stack do
do: Stacks.add_run_logs(attrs, id, cluster)

def stack_tarball(%{id: id}, _, _), do: {:ok, Services.api_url("v1/git/stacks/tarballs?id=#{id}")}

def plural_creds(run, _, ctx), do: Stacks.plural_creds(run, actor(ctx))
end
4 changes: 3 additions & 1 deletion lib/console/schema/stack.ex
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ defmodule Console.Schema.Stack do
belongs_to :cluster, Cluster
belongs_to :delete_run, StackRun
belongs_to :connection, ScmConnection
belongs_to :actor, User

has_one :state, StackState,
on_replace: :update,
Expand Down Expand Up @@ -129,7 +130,7 @@ defmodule Console.Schema.Stack do

def stream(query \\ __MODULE__), do: ordered(query, asc: :id)

@valid ~w(name type paused workdir manage_state status approval connection_id repository_id cluster_id)a
@valid ~w(name type paused actor_id workdir manage_state status approval connection_id repository_id cluster_id)a

def changeset(model, attrs \\ %{}) do
model
Expand All @@ -145,6 +146,7 @@ defmodule Console.Schema.Stack do
|> foreign_key_constraint(:repository_id)
|> foreign_key_constraint(:cluster_id)
|> foreign_key_constraint(:connection_id)
|> foreign_key_constraint(:actor_id)
|> unique_constraint(:name)
|> put_new_change(:write_policy_id, &Ecto.UUID.generate/0)
|> put_new_change(:read_policy_id, &Ecto.UUID.generate/0)
Expand Down
16 changes: 15 additions & 1 deletion lib/console/schema/stack_run.ex
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ defmodule Console.Schema.StackRun do
embeds_one :job_spec, JobSpec, on_replace: :update
embeds_one :configuration, Stack.Configuration, on_replace: :update

embeds_one :job_ref, JobRef, on_replace: :update do
field :name, :string
field :namespace, :string
end

has_one :state, StackState,
on_replace: :update,
foreign_key: :run_id
Expand Down Expand Up @@ -59,6 +64,7 @@ defmodule Console.Schema.StackRun do
belongs_to :stack, Stack
belongs_to :approver, User
belongs_to :pull_request, PullRequest
belongs_to :actor, User

timestamps()
end
Expand Down Expand Up @@ -103,14 +109,15 @@ defmodule Console.Schema.StackRun do
from(r in query, order_by: ^order)
end

@valid ~w(type status workdir manage_state message approval dry_run repository_id pull_request_id cluster_id stack_id)a
@valid ~w(type status workdir actor_id manage_state message approval dry_run repository_id pull_request_id cluster_id stack_id)a

def changeset(model, attrs \\ %{}) do
model
|> cast(attrs, @valid)
|> cast_embed(:git)
|> cast_embed(:job_spec)
|> cast_embed(:configuration)
|> cast_embed(:job_ref, with: &job_ref_changeset/2)
|> cast_assoc(:state)
|> cast_assoc(:environment)
|> cast_assoc(:steps)
Expand All @@ -119,6 +126,7 @@ defmodule Console.Schema.StackRun do
|> foreign_key_constraint(:repository_id)
|> foreign_key_constraint(:cluster_id)
|> foreign_key_constraint(:stack_id)
|> foreign_key_constraint(:actor_id)
|> validate_required(~w(type status)a)
end

Expand All @@ -145,4 +153,10 @@ defmodule Console.Schema.StackRun do
|> cast(attrs, @approve)
|> validate_required(@approve)
end

defp job_ref_changeset(model, attrs) do
model
|> cast(attrs, ~w(name namespace)a)
|> validate_required(~w(name namespace)a)
end
end
10 changes: 10 additions & 0 deletions lib/console_web/plugs/token.ex
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,15 @@ defmodule ConsoleWeb.Plugs.Token do
end
end

def fetch_resource(:bearer, token, {conn, opts}) do
key = Guardian.Plug.Pipeline.fetch_key(conn, opts)
with {:ok, claims} <- Console.Guardian.decode_and_verify(token) do
conn
|> Guardian.Plug.put_current_token(token, key: key)
|> Guardian.Plug.put_current_claims(claims, key: key)
end
end

def broadcast(token) do
Console.PubSub.Broadcaster.notify(%Console.PubSub.AccessTokenUsage{
item: token,
Expand All @@ -49,6 +58,7 @@ defmodule ConsoleWeb.Plugs.Token do
case get_req_header(conn, "authorization") do
["Token deploy-" <> _ = token | _] -> match_and_extract(~r/^Token\:?\s+(.*)$/, String.trim(token), :deploy)
["Token console-" <> _ = token | _] -> match_and_extract(~r/^Token\:?\s+(.*)$/, String.trim(token), :user)
["Token " <> _ = token | _] -> match_and_extract(~r/^Token\:?\s+(.*)$/, String.trim(token), :bearer)
_ -> {:error, :unauthorized}
end
end
Expand Down
Loading
Loading