Skip to content

Commit

Permalink
Adds automated PRs to apply scaling recommendations (#1734)
Browse files Browse the repository at this point in the history
  • Loading branch information
michaeljguarino authored Jan 8, 2025
1 parent b094ebf commit d875d06
Show file tree
Hide file tree
Showing 20 changed files with 331 additions and 31 deletions.
2 changes: 1 addition & 1 deletion AGENT_VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
v0.5.5
v0.5.6
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ ARG TARGETARCH=amd64
# ENV TERRAFORM_VERSION=v1.9.8

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

# renovate: datasource=github-tags depName=kubernetes/kubernetes
# ENV KUBECTL_VERSION=v1.31.3
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { Button, LinkoutIcon, PrOpenIcon, Toast } from '@pluralsh/design-system'
import { createColumnHelper } from '@tanstack/react-table'
import { StackedText } from 'components/utils/table/StackedText'
import { Body2P } from 'components/utils/typography/Text'
import { ClusterScalingRecommendationFragment } from 'generated/graphql'
import {
ClusterScalingRecommendationFragment,
useApplyScalingRecommendationMutation,
} from 'generated/graphql'
import styled from 'styled-components'

const columnHelper = createColumnHelper<ClusterScalingRecommendationFragment>()
Expand Down Expand Up @@ -53,6 +57,57 @@ export const ColMemoryChange = columnHelper.accessor((rec) => rec, {
},
})

export const ColScalingPr = columnHelper.accessor((rec) => rec, {
id: 'scalingPr',
header: 'Create PR',
cell: function Cell({ getValue }) {
const rec = getValue()
const [mutation, { data, loading, error }] =
useApplyScalingRecommendationMutation({ variables: { id: rec.id } })

if (!rec.service) {
return null
}

return (
<Body2P css={{ whiteSpace: 'pre-wrap' }}>
{error && (
<Toast
severity="danger"
position="top-right"
margin="large"
heading="PR Creation Failed"
>
{error.message}
</Toast>
)}
{data?.applyScalingRecommendation?.id ? (
<Button
primary
type="button"
endIcon={<LinkoutIcon />}
as="a"
href={data?.applyScalingRecommendation?.url}
target="_blank"
rel="noopener noreferrer"
>
View PR
</Button>
) : (
<Button
secondary
startIcon={<PrOpenIcon />}
onClick={mutation}
loading={loading}
>
Create PR
</Button>
)}
</Body2P>
)
},
})

const BoldTextSC = styled.strong(({ theme }) => ({
color: theme.colors.text,
}))
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
ColCpuChange,
ColMemoryChange,
ColName,
ColScalingPr,
} from './ClusterScalingRecsTableCols'
import { CMContextType } from './CostManagementDetails'
import { useMemo } from 'react'
Expand Down Expand Up @@ -111,4 +112,4 @@ export function CostManagementDetailsRecommendations() {
)
}

const cols = [ColName, ColCpuChange, ColMemoryChange]
const cols = [ColName, ColCpuChange, ColMemoryChange, ColScalingPr]
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
82 changes: 66 additions & 16 deletions 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 @@ -5238,6 +5238,7 @@ export type RootMutationType = {
addClusterAuditLog?: Maybe<Scalars['Boolean']['output']>;
addRunLogs?: Maybe<RunLogs>;
aiFixPr?: Maybe<PullRequest>;
applyScalingRecommendation?: Maybe<PullRequest>;
/** approves an approval pipeline gate */
approveGate?: Maybe<PipelineGate>;
approveStackRun?: Maybe<StackRun>;
Expand Down Expand Up @@ -5456,6 +5457,11 @@ export type RootMutationTypeAiFixPrArgs = {
};


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


export type RootMutationTypeApproveGateArgs = {
id: Scalars['ID']['input'];
};
Expand Down Expand Up @@ -10550,7 +10556,7 @@ export type ClusterUsageTinyFragment = { __typename?: 'ClusterUsage', id: string

export type ClusterNamespaceUsageFragment = { __typename?: 'ClusterNamespaceUsage', id: string, namespace?: string | null, storage?: number | null, cpuCost?: number | null, cpuUtil?: number | null, cpu?: number | null, memoryCost?: number | null, memUtil?: number | null, memory?: number | null, ingressCost?: number | null, loadBalancerCost?: number | null, egressCost?: number | null };

export type ClusterScalingRecommendationFragment = { __typename?: 'ClusterScalingRecommendation', id: string, namespace?: string | null, name?: string | null, container?: string | null, cpuCost?: number | null, cpuRequest?: number | null, cpuRecommendation?: number | null, memoryCost?: number | null, memoryRequest?: number | null, memoryRecommendation?: number | null, type?: ScalingRecommendationType | null };
export type ClusterScalingRecommendationFragment = { __typename?: 'ClusterScalingRecommendation', id: string, namespace?: string | null, name?: string | null, type?: ScalingRecommendationType | null, container?: string | null, cpuCost?: number | null, cpuRequest?: number | null, cpuRecommendation?: number | null, memoryCost?: number | null, memoryRequest?: number | null, memoryRecommendation?: number | null, service?: { __typename?: 'ServiceDeployment', id: string, name: string, cluster?: { __typename?: 'Cluster', id: string, name: string, handle?: string | null } | null } | null };

export type ClusterUsagesQueryVariables = Exact<{
after?: InputMaybe<Scalars['String']['input']>;
Expand Down Expand Up @@ -10588,7 +10594,14 @@ export type ClusterUsageScalingRecommendationsQueryVariables = Exact<{
}>;


export type ClusterUsageScalingRecommendationsQuery = { __typename?: 'RootQueryType', clusterUsage?: { __typename?: 'ClusterUsage', id: string, cluster?: { __typename?: 'Cluster', id: string, name: string } | null, recommendations?: { __typename?: 'ClusterScalingRecommendationConnection', pageInfo: { __typename?: 'PageInfo', hasNextPage: boolean, endCursor?: string | null, hasPreviousPage: boolean, startCursor?: string | null }, edges?: Array<{ __typename?: 'ClusterScalingRecommendationEdge', node?: { __typename?: 'ClusterScalingRecommendation', id: string, namespace?: string | null, name?: string | null, container?: string | null, cpuCost?: number | null, cpuRequest?: number | null, cpuRecommendation?: number | null, memoryCost?: number | null, memoryRequest?: number | null, memoryRecommendation?: number | null, type?: ScalingRecommendationType | null } | null } | null> | null } | null } | null };
export type ClusterUsageScalingRecommendationsQuery = { __typename?: 'RootQueryType', clusterUsage?: { __typename?: 'ClusterUsage', id: string, cluster?: { __typename?: 'Cluster', id: string, name: string } | null, recommendations?: { __typename?: 'ClusterScalingRecommendationConnection', pageInfo: { __typename?: 'PageInfo', hasNextPage: boolean, endCursor?: string | null, hasPreviousPage: boolean, startCursor?: string | null }, edges?: Array<{ __typename?: 'ClusterScalingRecommendationEdge', node?: { __typename?: 'ClusterScalingRecommendation', id: string, namespace?: string | null, name?: string | null, type?: ScalingRecommendationType | null, container?: string | null, cpuCost?: number | null, cpuRequest?: number | null, cpuRecommendation?: number | null, memoryCost?: number | null, memoryRequest?: number | null, memoryRecommendation?: number | null, service?: { __typename?: 'ServiceDeployment', id: string, name: string, cluster?: { __typename?: 'Cluster', id: string, name: string, handle?: string | null } | null } | null } | null } | null> | null } | null } | null };

export type ApplyScalingRecommendationMutationVariables = Exact<{
id: Scalars['ID']['input'];
}>;


export type ApplyScalingRecommendationMutation = { __typename?: 'RootMutationType', applyScalingRecommendation?: { __typename?: 'PullRequest', id: string, title?: string | null, url: string, labels?: Array<string | null> | null, creator?: string | null, status?: PrStatus | null, insertedAt?: string | null, updatedAt?: string | null, service?: { __typename?: 'ServiceDeployment', id: string, name: string, protect?: boolean | null, deletedAt?: string | null } | null, cluster?: { __typename?: 'Cluster', handle?: string | null, protect?: boolean | null, deletedAt?: string | null, version?: string | null, currentVersion?: string | null, self?: boolean | null, virtual?: boolean | null, id: string, name: string, distro?: ClusterDistro | null, upgradePlan?: { __typename?: 'ClusterUpgradePlan', compatibilities?: boolean | null, deprecations?: boolean | null, incompatibilities?: boolean | null } | null, provider?: { __typename?: 'ClusterProvider', cloud: string } | null } | null } | null };

export type GroupMemberFragment = { __typename?: 'GroupMember', user?: { __typename?: 'User', id: string, pluralId?: string | null, name: string, email: string, profile?: string | null, backgroundColor?: string | null, readTimestamp?: string | null, emailSettings?: { __typename?: 'EmailSettings', digest?: boolean | null } | null, roles?: { __typename?: 'UserRoles', admin?: boolean | null } | null, personas?: Array<{ __typename?: 'Persona', id: string, name: string, description?: string | null, bindings?: Array<{ __typename?: 'PolicyBinding', id?: string | null, user?: { __typename?: 'User', id: string, name: string, email: string } | null, group?: { __typename?: 'Group', id: string, name: string } | null } | null> | null, configuration?: { __typename?: 'PersonaConfiguration', all?: boolean | null, deployments?: { __typename?: 'PersonaDeployment', addOns?: boolean | null, clusters?: boolean | null, pipelines?: boolean | null, providers?: boolean | null, repositories?: boolean | null, services?: boolean | null } | null, home?: { __typename?: 'PersonaHome', manager?: boolean | null, security?: boolean | null } | null, sidebar?: { __typename?: 'PersonaSidebar', audits?: boolean | null, kubernetes?: boolean | null, pullRequests?: boolean | null, settings?: boolean | null, backups?: boolean | null, stacks?: boolean | null } | null } | null } | null> | null } | null, group?: { __typename?: 'Group', id: string, name: string, description?: string | null, insertedAt?: string | null, updatedAt?: string | null } | null };

Expand Down Expand Up @@ -13037,17 +13050,6 @@ export const ServiceDeploymentRevisionsFragmentDoc = gql`
}
}
${ServiceDeploymentRevisionFragmentDoc}`;
export const ServiceDeploymentTinyFragmentDoc = gql`
fragment ServiceDeploymentTiny on ServiceDeployment {
id
name
cluster {
id
name
handle
}
}
`;
export const ApiDeprecationFragmentDoc = gql`
fragment ApiDeprecation on ApiDeprecation {
availableIn
Expand Down Expand Up @@ -13263,21 +13265,35 @@ export const ClusterNamespaceUsageFragmentDoc = gql`
egressCost
}
`;
export const ServiceDeploymentTinyFragmentDoc = gql`
fragment ServiceDeploymentTiny on ServiceDeployment {
id
name
cluster {
id
name
handle
}
}
`;
export const ClusterScalingRecommendationFragmentDoc = gql`
fragment ClusterScalingRecommendation on ClusterScalingRecommendation {
id
namespace
name
type
container
cpuCost
cpuRequest
cpuRecommendation
memoryCost
memoryRequest
memoryRecommendation
type
service {
...ServiceDeploymentTiny
}
}
`;
${ServiceDeploymentTinyFragmentDoc}`;
export const GroupFragmentDoc = gql`
fragment Group on Group {
id
Expand Down Expand Up @@ -20602,6 +20618,39 @@ export type ClusterUsageScalingRecommendationsQueryHookResult = ReturnType<typeo
export type ClusterUsageScalingRecommendationsLazyQueryHookResult = ReturnType<typeof useClusterUsageScalingRecommendationsLazyQuery>;
export type ClusterUsageScalingRecommendationsSuspenseQueryHookResult = ReturnType<typeof useClusterUsageScalingRecommendationsSuspenseQuery>;
export type ClusterUsageScalingRecommendationsQueryResult = Apollo.QueryResult<ClusterUsageScalingRecommendationsQuery, ClusterUsageScalingRecommendationsQueryVariables>;
export const ApplyScalingRecommendationDocument = gql`
mutation ApplyScalingRecommendation($id: ID!) {
applyScalingRecommendation(id: $id) {
...PullRequest
}
}
${PullRequestFragmentDoc}`;
export type ApplyScalingRecommendationMutationFn = Apollo.MutationFunction<ApplyScalingRecommendationMutation, ApplyScalingRecommendationMutationVariables>;

/**
* __useApplyScalingRecommendationMutation__
*
* To run a mutation, you first call `useApplyScalingRecommendationMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useApplyScalingRecommendationMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [applyScalingRecommendationMutation, { data, loading, error }] = useApplyScalingRecommendationMutation({
* variables: {
* id: // value for 'id'
* },
* });
*/
export function useApplyScalingRecommendationMutation(baseOptions?: Apollo.MutationHookOptions<ApplyScalingRecommendationMutation, ApplyScalingRecommendationMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<ApplyScalingRecommendationMutation, ApplyScalingRecommendationMutationVariables>(ApplyScalingRecommendationDocument, options);
}
export type ApplyScalingRecommendationMutationHookResult = ReturnType<typeof useApplyScalingRecommendationMutation>;
export type ApplyScalingRecommendationMutationResult = Apollo.MutationResult<ApplyScalingRecommendationMutation>;
export type ApplyScalingRecommendationMutationOptions = Apollo.BaseMutationOptions<ApplyScalingRecommendationMutation, ApplyScalingRecommendationMutationVariables>;
export const GroupsDocument = gql`
query Groups($q: String, $first: Int = 20, $after: String) {
groups(q: $q, first: $first, after: $after) {
Expand Down Expand Up @@ -24959,6 +25008,7 @@ export const namedOperations = {
UpdateRbac: 'UpdateRbac',
SelfManage: 'SelfManage',
KickService: 'KickService',
ApplyScalingRecommendation: 'ApplyScalingRecommendation',
CreateGroupMember: 'CreateGroupMember',
DeleteGroupMember: 'DeleteGroupMember',
CreateGroup: 'CreateGroup',
Expand Down
11 changes: 10 additions & 1 deletion assets/src/graph/costManagement.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,17 @@ fragment ClusterScalingRecommendation on ClusterScalingRecommendation {
id
namespace
name
type
container
cpuCost
cpuRequest
cpuRecommendation
memoryCost
memoryRequest
memoryRecommendation
type
service {
...ServiceDeploymentTiny
}
}

query ClusterUsages(
Expand Down Expand Up @@ -147,3 +150,9 @@ query ClusterUsageScalingRecommendations(
}
}
}

mutation ApplyScalingRecommendation($id: ID!) {
applyScalingRecommendation(id: $id) {
...PullRequest
}
}
14 changes: 8 additions & 6 deletions lib/console/ai/fixer.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ defmodule Console.AI.Fixer do
alias Console.AI.Fixer.Stack, as: StackFixer
alias Console.AI.{Provider, Tools.Pr}

@type pr_resp :: {:ok, PullRequest.t} | Console.error

@prompt """
Please provide the most straightforward code or configuration change available based on the information I've already provided above to fix this issue.
Expand Down Expand Up @@ -44,7 +46,7 @@ defmodule Console.AI.Fixer do
@doc """
Generate a fix recommendation from an ai insight struct
"""
@spec pr(AiInsight.t, Provider.history) :: {:ok, PullRequest.t} | Console.error
@spec pr(AiInsight.t, Provider.history) :: pr_resp
def pr(%AiInsight{service: svc, stack: stack} = insight, history) when is_map(svc) or is_map(stack) do
with {:ok, prompt} <- pr_prompt(insight, history) do
ask(prompt, @tool)
Expand All @@ -58,7 +60,7 @@ defmodule Console.AI.Fixer do
@doc """
Spawns a pr given a fix recommendation
"""
@spec pr(binary, Provider.history, User.t) :: {:ok, PullRequest.t} | Console.error
@spec pr(binary, Provider.history, User.t) :: pr_resp
def pr(id, history, %User{} = user) do
Console.AI.Tool.set_actor(user)

Expand All @@ -79,14 +81,14 @@ defmodule Console.AI.Fixer do
|> when_ok(&fix/1)
end

defp handle_tool_call({:ok, [%{create_pr: %{result: pr_attrs}} | _]}, additional) do
def handle_tool_call({:ok, [%{create_pr: %{result: pr_attrs}} | _]}, additional) do
%PullRequest{}
|> PullRequest.changeset(Map.merge(pr_attrs, additional))
|> Repo.insert()
end
defp handle_tool_call({:ok, [%{create_pr: %{error: err}} | _]}, _), do: {:error, err}
defp handle_tool_call({:ok, msg}, _), do: {:error, msg}
defp handle_tool_call(err, _), do: err
def handle_tool_call({:ok, [%{create_pr: %{error: err}} | _]}, _), do: {:error, err}
def handle_tool_call({:ok, msg}, _), do: {:error, msg}
def handle_tool_call(err, _), do: err

defp ask(prompt, task \\ @prompt), do: prompt ++ [{:user, task}]

Expand Down
20 changes: 20 additions & 0 deletions lib/console/ai/tools/pr.ex
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ defmodule Console.AI.Tools.Pr do
field :file_name, :string
field :content, :string
end

embeds_one :confidence, Confidence, on_replace: :update do
field :confident, :boolean
field :reason, :string
end
end

@valid ~w(repo_url branch_name commit_message pr_title pr_description)a
Expand All @@ -25,7 +30,9 @@ defmodule Console.AI.Tools.Pr do
model
|> cast(attrs, @valid)
|> cast_embed(:file_updates, with: &file_update_changeset/2)
|> cast_embed(:confidence, with: &confidence_changeset/2)
|> validate_required(@valid)
|> ensure_confident()
end

@json_schema Console.priv_file!("tools/pr.json") |> Jason.decode!()
Expand Down Expand Up @@ -73,4 +80,17 @@ defmodule Console.AI.Tools.Pr do
|> cast(attrs, ~w(file_name content)a)
|> validate_required(~w(file_name content)a)
end

defp confidence_changeset(model, attrs) do
model
|> cast(attrs, ~w(confident reason)a)
end

defp ensure_confident(cs) do
case get_field(cs, :confidence) do
%__MODULE__.Confidence{confident: false, reason: reason} ->
add_error(cs, :confidence, "There's not sufficient confidence to apply this PR, the reason is: #{reason}")
_ -> cs
end
end
end
17 changes: 17 additions & 0 deletions lib/console/cost/dimensions.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
defmodule Console.Cost.Dimensions do
@kb 1024
@mb @kb * @kb

def memory(mem) when mem > @mb, do: "#{unit(mem, @mb)}Mi"
def memory(mem) when mem > @kb, do: "#{unit(mem, @kb)}Ki"
def memory(mem), do: mem

def cpu(cpu) when cpu > 1, do: cpu
def cpu(cpu), do: "#{cpu * 1000}m"

def maybe_quote(val) when is_binary(val), do: ~s("#{val}")
def maybe_quote(val), do: val

defp round(v, mult), do: round(v / mult) * mult
defp unit(v, unit), do: round(round(v, unit), 10)
end
Loading

0 comments on commit d875d06

Please sign in to comment.