Skip to content

Commit

Permalink
Add insights for terraform plans (#1604)
Browse files Browse the repository at this point in the history
  • Loading branch information
michaeljguarino authored Nov 22, 2024
1 parent 3c5e112 commit ce0364b
Show file tree
Hide file tree
Showing 30 changed files with 296 additions and 23 deletions.
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ ENV HELM_VERSION=v3.10.3
ENV TERRAFORM_VERSION=v1.2.9

# renovate: datasource=github-releases depName=pluralsh/plural-cli
ENV CLI_VERSION=v0.10.0
ENV CLI_VERSION=v0.10.1

# renovate: datasource=github-tags depName=kubernetes/kubernetes
ENV KUBECTL_VERSION=v1.25.5
Expand Down
2 changes: 1 addition & 1 deletion assets/src/generated/graphql-kubernetes.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/* eslint-disable */
/* prettier-ignore */
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
Expand Down
2 changes: 1 addition & 1 deletion assets/src/generated/graphql-plural.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/* eslint-disable */
/* prettier-ignore */
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
Expand Down
7 changes: 6 additions & 1 deletion assets/src/generated/graphql.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/* eslint-disable */
/* prettier-ignore */
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
Expand Down Expand Up @@ -197,6 +197,7 @@ export type AiInsight = {
sha?: Maybe<Scalars['String']['output']>;
stack?: Maybe<InfrastructureStack>;
stackRun?: Maybe<StackRun>;
stackState?: Maybe<StackState>;
/** a shortish summary of this insight */
summary?: Maybe<Scalars['String']['output']>;
/** the text of this insight */
Expand Down Expand Up @@ -8858,8 +8859,12 @@ export type StackSettingsAttributes = {
export type StackState = {
__typename?: 'StackState';
id: Scalars['ID']['output'];
insertedAt?: Maybe<Scalars['DateTime']['output']>;
/** an insight explaining the state of this stack state, eg the terraform plan it represents */
insight?: Maybe<AiInsight>;
plan?: Maybe<Scalars['String']['output']>;
state?: Maybe<Array<Maybe<StackStateResource>>>;
updatedAt?: Maybe<Scalars['DateTime']['output']>;
};

export type StackStateAttributes = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,12 @@ spec:
x-kubernetes-validations:
- message: Cluster is immutable
rule: self == oldSelf
configuration:
additionalProperties:
type: string
description: Configuration is a set of non-secret configuration to
apply for lightweight templating of manifests in this service
type: object
configurationRef:
description: ConfigurationRef is a secret reference which should contain
service configuration.
Expand Down
5 changes: 5 additions & 0 deletions go/client/models_gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions go/controller/api/v1alpha1/servicedeployment_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,11 @@ type ServiceSpec struct {
// ConfigurationRef is a secret reference which should contain service configuration.
// +kubebuilder:validation:Optional
ConfigurationRef *corev1.SecretReference `json:"configurationRef,omitempty"`

// Configuration is a set of non-secret configuration to apply for lightweight templating of manifests in this service
// +kubebuilder:validation:Optional
Configuration map[string]string `json:"configuration,omitempty"`

// Bindings contain read and write policies of this cluster
// +kubebuilder:validation:Optional
Bindings *Bindings `json:"bindings,omitempty"`
Expand Down
7 changes: 7 additions & 0 deletions go/controller/api/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,12 @@ spec:
x-kubernetes-validations:
- message: Cluster is immutable
rule: self == oldSelf
configuration:
additionalProperties:
type: string
description: Configuration is a set of non-secret configuration to
apply for lightweight templating of manifests in this service
type: object
configurationRef:
description: ConfigurationRef is a secret reference which should contain
service configuration.
Expand Down
14 changes: 14 additions & 0 deletions go/controller/internal/controller/servicedeployment_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,20 @@ func (r *ServiceReconciler) genServiceAttributes(ctx context.Context, service *v
}
}

if len(service.Spec.Configuration) > 0 {
if attr.Configuration == nil {
attr.Configuration = make([]*console.ConfigAttributes, 0)
}

for k, v := range service.Spec.Configuration {
value := v
attr.Configuration = append(attr.Configuration, &console.ConfigAttributes{
Name: k,
Value: lo.ToPtr(value),
})
}
}

for _, contextName := range service.Spec.Contexts {
sc, err := r.ConsoleClient.GetServiceContext(contextName)
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion lib/console/ai/evidence/stack_run.ex
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ defimpl Console.AI.Evidence, for: Console.Schema.StackRun do

defp fetch_code(%StackRun{} = run) do
with {:ok, f} <- Stacks.tarstream(run),
{:ok, msgs} <- code_prompt(f, run.git.folder, "I'll also include the relevant terraform code below, listed in the format #{file_fmt()}") do
{:ok, msgs} <- code_prompt(f, run.git.folder, "I'll also include the relevant #{run.type} code below, listed in the format #{file_fmt()}") do
msgs
else
_ -> []
Expand Down
41 changes: 41 additions & 0 deletions lib/console/ai/evidence/stack_state.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
defimpl Console.AI.Evidence, for: Console.Schema.StackState do
use Console.AI.Evidence.Base
import Console.AI.Fixer.Base
alias Console.Repo
alias Console.Deployments.Stacks
alias Console.Schema.{StackState, StackRun}

def generate(%StackState{run: %StackRun{} = run} = state),
do: {:ok, [state_description(state) | fetch_code(run)]}

def insight(%StackState{insight: insight}), do: insight

def preload(state), do: Repo.preload(state, [:insight, run: [:stack, :cluster, :errors, :repository]])

defp state_description(%StackState{run: %StackRun{} = run} = state) do
{:user, """
The Plural stack #{run.stack.name} has a terraform plan generated and the user will want to understand what it means, in particular:
* expected blast radius of the change
* if any critical systems can be affected by the change
* whether it's safe to apply
The plan itself is recorded below:
```
#{state.plan}
```
It is sourcing #{run.type} configuration from the git repository at #{run.repository.url} from the folder #{run.git.folder} at ref #{run.git.ref}.
"""}
end

defp fetch_code(%StackRun{} = run) do
with {:ok, f} <- Stacks.tarstream(run),
{:ok, msgs} <- code_prompt(f, run.git.folder, "I'll also include the relevant #{run.type} code below, listed in the format #{file_fmt()}") do
msgs
else
_ -> []
end
end
end
6 changes: 4 additions & 2 deletions lib/console/ai/pubsub/consumer.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ defmodule Console.AI.PubSub.Consumer do
max_demand: 10
import Console.Services.Base, only: [handle_notify: 2]
alias Console.PubSub
alias Console.Schema.{AiInsight, Service, Stack}
alias Console.Schema.{AiInsight, Service, Stack, StackState}
alias Console.AI.{PubSub.Insightful, Cron}
require Logger

Expand All @@ -19,11 +19,13 @@ defmodule Console.AI.PubSub.Consumer do
end

def maybe_send_event({:ok, insight} = res) do
case Console.Repo.preload(insight, [:stack, :service]) do
case Console.Repo.preload(insight, [:stack, :service, :stack_state]) do
%AiInsight{service: %Service{} = svc} ->
handle_notify(PubSub.ServiceInsight, {svc, insight})
%AiInsight{stack: %Stack{} = stack} ->
handle_notify(PubSub.StackInsight, {stack, insight})
%AiInsight{stack_state: %StackState{} = state} ->
handle_notify(PubSub.StackStateInsight, {state, insight})
_ -> :ok
end
res
Expand Down
28 changes: 27 additions & 1 deletion lib/console/ai/pubsub/protocol.ex
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,34 @@ defimpl Console.AI.PubSub.Insightful, for: Console.PubSub.ServiceUpdated do
def resource(_), do: :ok
end

defimpl Console.AI.PubSub.Insightful, for: Console.PubSub.StackRunUpdated do
alias Console.Schema.{StackRun, StackState}

def resource(%@for{item: %StackRun{status: :pending_approval} = run}),
do: get_state(run)
def resource(%@for{item: %StackRun{status: :successful, pull_request_id: id} = run}) when is_binary(id),
do: get_state(run)
def resource(_), do: :ok

defp get_state(run) do
case Console.Repo.preload(run, [:state]) do
%StackRun{state: %StackState{plan: p} = state} when is_binary(p) and byte_size(p) > 0 ->
{:ok, state}
_ -> :ok
end
end
end

defimpl Console.AI.PubSub.Insightful, for: Console.PubSub.StackRunCompleted do
alias Console.Schema.StackRun
alias Console.Schema.{StackState, StackRun}

def resource(%@for{item: %StackRun{status: :successful, pull_request_id: id} = run}) when is_binary(id) do
case Console.Repo.preload(run, [:state]) do
%StackRun{state: %StackState{plan: p} = state} when is_binary(p) and byte_size(p) > 0 ->
{:ok, state}
_ -> :ok
end
end
def resource(%@for{item: %StackRun{status: :failed} = run}), do: {:ok, run}
def resource(_), do: :ok
end
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 @@ -73,5 +73,6 @@ defmodule Console.PubSub.AppNotificationCreated, do: use Piazza.PubSub.Event
defmodule Console.PubSub.ServiceInsight, do: use Piazza.PubSub.Event
defmodule Console.PubSub.StackInsight, do: use Piazza.PubSub.Event
defmodule Console.PubSub.ClusterInsight, do: use Piazza.PubSub.Event
defmodule Console.PubSub.StackStateInsight, do: use Piazza.PubSub.Event

defmodule Console.PubSub.AlertCreated, do: use Piazza.PubSub.Event
2 changes: 1 addition & 1 deletion lib/console/deployments/git/cache.ex
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ defmodule Console.Deployments.Git.Cache do
end

defp new_line(cache, repo, sha, path, filter) do
with {:ok, _} <- git(repo, "checkout", [sha]),
with {:ok, _} <- git(repo, "checkout", ["-f", sha]),
{:ok, msg} <- msg(repo),
{:ok, f} <- tarball(cache, sha, path, filter),
do: {:ok, Line.new(f, sha, msg)}
Expand Down
13 changes: 13 additions & 0 deletions lib/console/deployments/pubsub/recurse.ex
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,19 @@ defimpl Console.PubSub.Recurse, for: Console.PubSub.StackRunUpdated do
def process(_), do: :ok
end

defimpl Console.PubSub.Recurse, for: Console.PubSub.StackStateInsight do
alias Console.Schema.{StackRun, PullRequest, StackState, AiInsight}
alias Console.Deployments.Stacks

def process(%@for{item: {%StackState{} = state, _}}) do
case Console.Repo.preload(state, [run: [:pull_request, state: :insight]]) do
%StackState{run: %StackRun{pull_request: %PullRequest{}, state: %StackState{insight: %AiInsight{}}} = run} ->
Stacks.post_comment(run)
_ -> :ok
end
end
end

defimpl Console.PubSub.Recurse, for: Console.PubSub.StackRunCreated do
alias Console.Schema.{Stack, StackRun, PullRequest}
alias Console.Deployments.Stacks
Expand Down
18 changes: 15 additions & 3 deletions lib/console/deployments/stacks.ex
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ defmodule Console.Deployments.Stacks do
ScmConnection,
CustomStackRun,
StackDefinition,
StackCron
StackCron,
AiInsight
}

@preloads [:environment, :files, :observable_metrics, :cron, :tags, :read_bindings, :write_bindings]
Expand Down Expand Up @@ -260,8 +261,17 @@ defmodule Console.Deployments.Stacks do
Posts a review comment for a completed pr stack run if possible
"""
def post_comment(%StackRun{} = run) do
run = Repo.preload(run, [:pull_request, :state, stack: :connection])
run = Repo.preload(run, [:pull_request, stack: :connection, state: :insight])
case {run, scm_connection(run)} do
{%StackRun{
id: id,
stack_id: stack_id,
status: :successful,
state: %StackState{insight: %AiInsight{} = insight},
pull_request: %PullRequest{} = pr
}, %ScmConnection{} = conn} ->
url = Console.url("/stacks/#{stack_id}/runs/#{id}")
Dispatcher.review(conn, pr, pr_blob("insight", insight: insight, link: url))
{%StackRun{
id: id,
stack_id: stack_id,
Expand Down Expand Up @@ -341,7 +351,9 @@ defmodule Console.Deployments.Stacks do
defp sync_stack_status(_), do: {:ok, %{}}

defp add_stack_state(attrs, %{} = state) do
state = Console.clean(state) |> Map.delete(:run_id)
state =
Console.clean(state)
|> Map.drop(~w(run_id insight)a)
Map.put(attrs, :state, state)
end
defp add_stack_state(attrs, _), do: attrs
Expand Down
3 changes: 2 additions & 1 deletion lib/console/graphql/ai.ex
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,9 @@ defmodule Console.GraphQl.AI do
field :cluster, :cluster, resolve: dataloader(Deployments)
field :stack_run, :stack_run, resolve: dataloader(Deployments)
field :service_component, :service_component, resolve: dataloader(Deployments)
field :stack_state, :stack_state, resolve: dataloader(Deployments)

field :cluster_insight_component, :cluster_insight_component, resolve: dataloader(Deployments)
field :cluster_insight_component, :cluster_insight_component, resolve: dataloader(Deployments)

timestamps()
end
Expand Down
13 changes: 11 additions & 2 deletions lib/console/graphql/deployments/stack.ex
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,9 @@ 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 :output, list_of(:stack_output),
resolve: filter_loader(dataloader(Deployments), &Deployments.safe_stack_outputs/3),
description: "the most recent output for this stack"
field :state, :stack_state, resolve: dataloader(Deployments), description: "the most recent state of this stack"

field :project, :project, resolve: dataloader(Deployments), description: "The project this stack belongs to"
Expand Down Expand Up @@ -276,7 +278,9 @@ defmodule Console.GraphQl.Deployments.Stack do
field :environment, list_of(:stack_environment), resolve: dataloader(Deployments), description: "environment variables for this stack"

field :stack, :infrastructure_stack, resolve: dataloader(Deployments), description: "the stack attached to this run"
field :output, list_of(:stack_output), resolve: dataloader(Deployments), description: "the most recent output for this stack"
field :output, list_of(:stack_output),
resolve: filter_loader(dataloader(Deployments), &Deployments.safe_stack_outputs/3),
description: "the most recent output for this stack"
field :state, :stack_state, resolve: dataloader(Deployments), description: "the most recent state of this stack"
field :errors, list_of(:service_error),
resolve: dataloader(Deployments),
Expand Down Expand Up @@ -332,6 +336,11 @@ defmodule Console.GraphQl.Deployments.Stack do
field :id, non_null(:id)
field :plan, :string
field :state, list_of(:stack_state_resource)

field :insight, :ai_insight, resolve: dataloader(Deployments),
description: "an insight explaining the state of this stack state, eg the terraform plan it represents"

timestamps()
end

object :stack_state_resource do
Expand Down
8 changes: 8 additions & 0 deletions lib/console/graphql/resolvers/deployments/stack.ex
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,14 @@ defmodule Console.GraphQl.Resolvers.Deployments.Stack do
|> paginate(args)
end

def safe_stack_outputs(_, outputs, %{context: %{cluster: %{}}}), do: outputs
def safe_stack_outputs(parent, outputs, %{context: %{current_user: user}}) do
case allow(parent, user, :write) do
{:ok, _} -> outputs
_ -> Enum.filter(outputs, & ! &1.secret)
end
end

defp stack_filters(query, args) do
Enum.reduce(args, query, fn
{:project_id, id}, q -> Stack.for_project(q, id)
Expand Down
Loading

0 comments on commit ce0364b

Please sign in to comment.