feat(api): add EC2 Auto Scaling groups to the cloud explorer
CI / checks (pull_request) Successful in 26m54s
CI / checks (pull_request) Successful in 26m54s
Create groups from a launch template (in the default subnets unless given), change their minimum, desired and maximum capacity, replace or remove an instance, and follow the launches and terminations TheCloud's reconciler makes. Deleting a group terminates its instances. Scaling policies are not offered: the runtime stores them but its alarms only drive ECS scaling.
This commit is contained in:
@@ -155,6 +155,10 @@ export const apiEndpointKeys = {
|
||||
export: "clouds.services.certificates.export",
|
||||
revoke: "clouds.services.certificates.revoke",
|
||||
},
|
||||
autoscaling: {
|
||||
capacity: "clouds.services.autoscaling.capacity",
|
||||
terminate: "clouds.services.autoscaling.instances.terminate",
|
||||
},
|
||||
cdn: {
|
||||
enabled: "clouds.services.cdn.enabled",
|
||||
invalidations: "clouds.services.cdn.invalidations",
|
||||
@@ -952,6 +956,20 @@ export const endpointRegistry: EndpointRegistry = new Map([
|
||||
method: "POST",
|
||||
},
|
||||
],
|
||||
[
|
||||
apiEndpointKeys.clouds.autoscaling.capacity,
|
||||
{
|
||||
path: "/clouds/:cloud/services/autoscaling/resources/:id/capacity",
|
||||
method: "PUT",
|
||||
},
|
||||
],
|
||||
[
|
||||
apiEndpointKeys.clouds.autoscaling.terminate,
|
||||
{
|
||||
path: "/clouds/:cloud/services/autoscaling/resources/:id/instances/:instanceId/terminate",
|
||||
method: "POST",
|
||||
},
|
||||
],
|
||||
[
|
||||
apiEndpointKeys.clouds.cdn.enabled,
|
||||
{
|
||||
|
||||
@@ -33,6 +33,7 @@ import type {
|
||||
UserPoolSignInTestResult,
|
||||
UserPoolClientSettings,
|
||||
DistributionInvalidation,
|
||||
AutoScalingCapacityInput,
|
||||
CertificatePem,
|
||||
CertificateRevocationReason,
|
||||
ExportedCertificate,
|
||||
@@ -1720,6 +1721,32 @@ export async function revokeCertificate(
|
||||
return res.data;
|
||||
}
|
||||
|
||||
export async function setAutoScalingCapacity(
|
||||
cloud: CloudProvider,
|
||||
groupName: string,
|
||||
input: AutoScalingCapacityInput,
|
||||
): Promise<CloudResource> {
|
||||
const res = await apiClient.call<CloudResource, AutoScalingCapacityInput>(
|
||||
apiEndpointKeys.clouds.autoscaling.capacity,
|
||||
requestOptions(cloud, "autoscaling", { body: input }),
|
||||
{ cloud, id: groupName },
|
||||
);
|
||||
return res.data;
|
||||
}
|
||||
|
||||
export async function terminateAutoScalingInstance(
|
||||
cloud: CloudProvider,
|
||||
groupName: string,
|
||||
instanceId: string,
|
||||
replace: boolean,
|
||||
): Promise<void> {
|
||||
await apiClient.call<void, { replace: boolean }>(
|
||||
apiEndpointKeys.clouds.autoscaling.terminate,
|
||||
requestOptions(cloud, "autoscaling", { body: { replace } }),
|
||||
{ cloud, id: groupName, instanceId },
|
||||
);
|
||||
}
|
||||
|
||||
export async function setDistributionEnabled(
|
||||
cloud: CloudProvider,
|
||||
distributionId: string,
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import {QueryClient, QueryClientProvider} from '@tanstack/react-query'
|
||||
import {render, screen, within} from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import {beforeEach, expect, test, vi} from 'vitest'
|
||||
import {AutoScalingPanel} from './AutoScalingPanel'
|
||||
import type {CloudResource} from '@/types/resource'
|
||||
|
||||
const api = vi.hoisted(() => ({
|
||||
getCloudResource: vi.fn(),
|
||||
setAutoScalingCapacity: vi.fn(),
|
||||
terminateAutoScalingInstance: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/api/cloudProxyClient', () => api)
|
||||
vi.mock('@/lib/accountStore', () => ({useAccountId: () => '000000000000'}))
|
||||
|
||||
function group(metadata: Record<string, unknown> = {}): CloudResource {
|
||||
return {
|
||||
id: 'web-asg', name: 'web-asg', cloud: 'aws', service: 'autoscaling', type: 'autoScalingGroup', region: 'us-east-1', createdAt: null, status: null,
|
||||
metadata: {
|
||||
minSize: 1, desiredCapacity: 2, maxSize: 3, capacity: '2 in service',
|
||||
instances: [
|
||||
{id: 'i-b8736c8b4a8c129a3', availabilityZone: 'us-east-1a', lifecycleState: 'InService', health: 'Healthy', instanceType: 't3.micro'},
|
||||
{id: 'i-a3155c876867976b1', availabilityZone: 'us-east-1b', lifecycleState: 'InService', health: 'Healthy', instanceType: 't3.micro'},
|
||||
],
|
||||
activities: [{id: 'a1', description: 'Launching a new EC2 instance: i-b8736c8b4a8c129a3', cause: 'An instance was started in response to a desired capacity change.', status: 'Successful', statusMessage: null, startTime: null}],
|
||||
...metadata,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function renderPanel(resource: CloudResource) {
|
||||
api.getCloudResource.mockResolvedValue(resource)
|
||||
const client = new QueryClient({defaultOptions: {queries: {retry: false}}})
|
||||
return render(<QueryClientProvider client={client}><AutoScalingPanel resource={resource} runtimeReachable/></QueryClientProvider>)
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
api.setAutoScalingCapacity.mockImplementation(async (_cloud, _id, input) => group({desiredCapacity: input.desiredCapacity}))
|
||||
api.terminateAutoScalingInstance.mockResolvedValue(undefined)
|
||||
})
|
||||
|
||||
test('shows the instances and what launched them', async () => {
|
||||
renderPanel(group())
|
||||
|
||||
expect(await screen.findByText('i-a3155c876867976b1')).toBeInTheDocument()
|
||||
expect(screen.getByText('Launching a new EC2 instance: i-b8736c8b4a8c129a3')).toBeInTheDocument()
|
||||
expect(screen.getByText(/never runs them/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
test('changes the desired capacity', async () => {
|
||||
const user = userEvent.setup()
|
||||
renderPanel(group())
|
||||
|
||||
const form = await screen.findByRole('form', {name: 'Group capacity'})
|
||||
const update = within(form).getByRole('button', {name: 'Update capacity'})
|
||||
expect(update).toBeDisabled()
|
||||
const desired = within(form).getByLabelText('Desired')
|
||||
await user.clear(desired)
|
||||
await user.type(desired, '3')
|
||||
await user.click(update)
|
||||
|
||||
expect(api.setAutoScalingCapacity).toHaveBeenCalledWith('aws', 'web-asg', {minSize: 1, desiredCapacity: 3, maxSize: 3})
|
||||
expect(await screen.findByText(/Capacity set to 3/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
test('replaces an instance, or removes one while above the minimum', async () => {
|
||||
const user = userEvent.setup()
|
||||
vi.spyOn(window, 'confirm').mockReturnValue(true)
|
||||
renderPanel(group())
|
||||
|
||||
await user.click(await screen.findByRole('button', {name: 'Replace i-b8736c8b4a8c129a3'}))
|
||||
expect(api.terminateAutoScalingInstance).toHaveBeenCalledWith('aws', 'web-asg', 'i-b8736c8b4a8c129a3', true)
|
||||
expect(await screen.findByText(/launches a replacement/)).toBeInTheDocument()
|
||||
|
||||
await user.click(screen.getByRole('button', {name: 'Remove i-a3155c876867976b1'}))
|
||||
expect(api.terminateAutoScalingInstance).toHaveBeenLastCalledWith('aws', 'web-asg', 'i-a3155c876867976b1', false)
|
||||
})
|
||||
|
||||
test('cannot remove an instance from a group at its minimum', async () => {
|
||||
renderPanel(group({minSize: 2}))
|
||||
expect(await screen.findByRole('button', {name: 'Remove i-b8736c8b4a8c129a3'})).toBeDisabled()
|
||||
expect(screen.getByRole('button', {name: 'Replace i-b8736c8b4a8c129a3'})).toBeEnabled()
|
||||
})
|
||||
@@ -0,0 +1,146 @@
|
||||
import {RefreshCw} from 'lucide-react'
|
||||
import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'
|
||||
import {useEffect, useState} from 'react'
|
||||
import {getCloudResource, setAutoScalingCapacity, terminateAutoScalingInstance} from '@/api/cloudProxyClient'
|
||||
import {cloudQueryKeys} from '@/api/queries/cloudQueries'
|
||||
import {useAccountId} from '@/lib/accountStore'
|
||||
import {formatDateTime} from '@/lib/format'
|
||||
import type {CloudResource} from '@/types/resource'
|
||||
|
||||
type Props = {
|
||||
resource?: CloudResource
|
||||
runtimeReachable: boolean
|
||||
}
|
||||
|
||||
type Instance = {id: string; availabilityZone: string | null; lifecycleState: string | null; health: string | null; instanceType: string | null}
|
||||
type Activity = {id: string; description: string; cause: string; status: string | null; statusMessage: string | null; startTime: string | null}
|
||||
type Capacity = {minSize: string; desiredCapacity: string; maxSize: string}
|
||||
|
||||
/** The runtime reconciles every 10 seconds, so a group that is still moving is re-read this often. */
|
||||
const SETTLING_POLL_MS = 4000
|
||||
|
||||
export function AutoScalingPanel({resource, runtimeReachable}: Props) {
|
||||
const accountId = useAccountId()
|
||||
const queryClient = useQueryClient()
|
||||
const [capacity, setCapacity] = useState<Capacity>({minSize: '', desiredCapacity: '', maxSize: ''})
|
||||
const [notice, setNotice] = useState('')
|
||||
const groupKey = ['autoscaling-group', accountId, resource?.id]
|
||||
|
||||
const groupQuery = useQuery({
|
||||
queryKey: groupKey,
|
||||
queryFn: ({signal}) => getCloudResource('aws', 'autoscaling', resource?.id ?? '', signal),
|
||||
enabled: Boolean(resource) && runtimeReachable,
|
||||
refetchInterval: (query) => query.state.data && !settled(query.state.data) ? SETTLING_POLL_MS : false,
|
||||
})
|
||||
|
||||
const group = groupQuery.data ?? resource
|
||||
useEffect(() => {
|
||||
setNotice('')
|
||||
}, [resource?.id])
|
||||
// The form follows the group's saved capacity whenever it changes, here or elsewhere.
|
||||
const savedMin = String(group?.metadata.minSize ?? '')
|
||||
const savedDesired = String(group?.metadata.desiredCapacity ?? '')
|
||||
const savedMax = String(group?.metadata.maxSize ?? '')
|
||||
useEffect(() => {
|
||||
setCapacity({minSize: savedMin, desiredCapacity: savedDesired, maxSize: savedMax})
|
||||
}, [group?.id, savedMin, savedDesired, savedMax])
|
||||
|
||||
const refresh = () => Promise.all([
|
||||
queryClient.invalidateQueries({queryKey: groupKey}),
|
||||
queryClient.invalidateQueries({queryKey: cloudQueryKeys.resources('aws', 'autoscaling')}),
|
||||
])
|
||||
|
||||
const capacityMutation = useMutation({
|
||||
mutationFn: () => setAutoScalingCapacity('aws', resource?.id ?? '', {
|
||||
minSize: Number(capacity.minSize),
|
||||
desiredCapacity: Number(capacity.desiredCapacity),
|
||||
maxSize: Number(capacity.maxSize),
|
||||
}),
|
||||
onSuccess: (updated) => {
|
||||
setNotice(`Capacity set to ${String(updated.metadata.desiredCapacity)}. TheCloud launches or terminates instances within about 10 seconds.`)
|
||||
return refresh()
|
||||
},
|
||||
})
|
||||
const terminateMutation = useMutation({
|
||||
mutationFn: ({instanceId, replace}: {instanceId: string; replace: boolean}) => terminateAutoScalingInstance('aws', resource?.id ?? '', instanceId, replace),
|
||||
onSuccess: (_result, {instanceId, replace}) => {
|
||||
setNotice(replace ? `Terminating ${instanceId}. The group launches a replacement to stay at its desired capacity.` : `Terminating ${instanceId} and lowering the desired capacity by one.`)
|
||||
return refresh()
|
||||
},
|
||||
})
|
||||
|
||||
if (!resource || !group) {
|
||||
return <section className="table-panel"><div className="empty compact"><h3>Select an Auto Scaling group</h3><p>Choose a group to change its capacity, see its instances and follow its scaling activity.</p></div></section>
|
||||
}
|
||||
|
||||
const metadata = group.metadata
|
||||
const instances = Array.isArray(metadata.instances) ? metadata.instances as Instance[] : []
|
||||
const activities = Array.isArray(metadata.activities) ? metadata.activities as Activity[] : []
|
||||
const desired = Number(metadata.desiredCapacity ?? 0)
|
||||
const min = Number(metadata.minSize ?? 0)
|
||||
const pending = capacityMutation.isPending || terminateMutation.isPending
|
||||
const disabled = !runtimeReachable || pending
|
||||
const error = groupQuery.error ?? capacityMutation.error ?? terminateMutation.error
|
||||
const changed = capacity.minSize !== String(metadata.minSize) || capacity.desiredCapacity !== String(metadata.desiredCapacity) || capacity.maxSize !== String(metadata.maxSize)
|
||||
const field = (name: keyof Capacity, label: string) => <label className="dynamic-field"><span>{label}</span>
|
||||
<input className="input" type="number" min={0} max={20} step={1} value={capacity[name]} onChange={(event) => setCapacity({...capacity, [name]: event.target.value})} disabled={disabled} required/>
|
||||
</label>
|
||||
|
||||
return <section className="table-panel" style={{marginTop: 16}} aria-label={`Auto Scaling group ${group.name}`}>
|
||||
<div className="input-row resource-table-bar">
|
||||
<div><p className="eyebrow">Auto Scaling group</p><span className="muted">{String(metadata.capacity ?? '')} · desired {desired}{settled(group) ? '' : ' · scaling'}{group.status ? ` · ${group.status}` : ''}</span></div>
|
||||
<div className="cdn-actions">
|
||||
<button className="button" type="button" disabled={!runtimeReachable || groupQuery.isFetching} onClick={() => { void refresh() }}><RefreshCw size={14}/>{groupQuery.isFetching ? 'Loading' : 'Refresh'}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form className="dynamic-form autoscaling-capacity" aria-label="Group capacity" onSubmit={(event) => { event.preventDefault(); setNotice(''); capacityMutation.mutate() }}>
|
||||
{field('minSize', 'Minimum')}
|
||||
{field('desiredCapacity', 'Desired')}
|
||||
{field('maxSize', 'Maximum')}
|
||||
<div><button className="button primary" type="submit" disabled={disabled || !changed}>{capacityMutation.isPending ? 'Updating' : 'Update capacity'}</button></div>
|
||||
</form>
|
||||
<p className="muted compact-text autoscaling-note">Scaling policies are not offered: TheCloud stores EC2 scaling policies but never runs them, so set the capacity here. Alarm-driven scaling works for ECS services through Application Auto Scaling.</p>
|
||||
|
||||
<div className="input-row resource-table-bar"><div><p className="eyebrow">Instances</p><span className="muted">Each is a real EC2 instance in TheCloud, launched from the group's template.</span></div></div>
|
||||
{instances.length === 0 ? <p className="muted compact-text autoscaling-note">No instances. {desired > 0 ? 'The group is launching them.' : 'Raise the desired capacity to launch some.'}</p>
|
||||
: <div className="cognito-table"><table className="table"><thead><tr><th>Instance</th><th>State</th><th>Health</th><th>Zone</th><th>Type</th><th aria-label="Actions"/></tr></thead><tbody>{instances.map((instance) => (
|
||||
<tr key={instance.id}>
|
||||
<td className="mono">{instance.id}</td>
|
||||
<td><code>{instance.lifecycleState ?? '—'}</code></td>
|
||||
<td>{instance.health ?? '—'}</td>
|
||||
<td>{instance.availabilityZone ?? '—'}</td>
|
||||
<td>{instance.instanceType ?? '—'}</td>
|
||||
<td className="autoscaling-instance-actions">
|
||||
<button className="button compact" type="button" disabled={disabled || instance.lifecycleState !== 'InService'} aria-label={`Replace ${instance.id}`} onClick={() => { setNotice(''); terminateMutation.mutate({instanceId: instance.id, replace: true}) }}>Replace</button>
|
||||
<button className="button compact danger" type="button" disabled={disabled || instance.lifecycleState !== 'InService' || desired - 1 < min} title={desired - 1 < min ? 'The group is at its minimum size' : undefined} aria-label={`Remove ${instance.id}`} onClick={() => {
|
||||
setNotice('')
|
||||
if (window.confirm(`Terminate ${instance.id} and lower the desired capacity to ${desired - 1}?`)) terminateMutation.mutate({instanceId: instance.id, replace: false})
|
||||
}}>Remove</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}</tbody></table></div>}
|
||||
|
||||
{activities.length > 0 && <>
|
||||
<div className="input-row resource-table-bar"><div><p className="eyebrow">Activity</p><span className="muted">The most recent launches and terminations.</span></div></div>
|
||||
<div className="cognito-table"><table className="table"><thead><tr><th>Started</th><th>Status</th><th>Activity</th></tr></thead><tbody>{activities.map((activity) => (
|
||||
<tr key={activity.id}>
|
||||
<td>{formatDateTime(activity.startTime) ?? '—'}</td>
|
||||
<td><code>{activity.status ?? '—'}</code></td>
|
||||
<td>{activity.description}<br/><small className="muted">{activity.statusMessage || activity.cause}</small></td>
|
||||
</tr>
|
||||
))}</tbody></table></div>
|
||||
</>}
|
||||
{metadata.activitiesUnavailable === true && <p className="muted compact-text autoscaling-note">The scaling activity could not be read just now.</p>}
|
||||
|
||||
{notice && <p className="cognito-outcome cognito-outcome--ok" role="status">{notice}</p>}
|
||||
{error instanceof Error && <p className="error-text compact-text" role="alert">{error.message}</p>}
|
||||
</section>
|
||||
}
|
||||
|
||||
/** Settled once every desired instance is in service and none is still starting or stopping. */
|
||||
function settled(group: CloudResource): boolean {
|
||||
const instances = Array.isArray(group.metadata.instances) ? group.metadata.instances as Instance[] : []
|
||||
const inService = instances.filter((instance) => instance.lifecycleState === 'InService').length
|
||||
return inService === Number(group.metadata.desiredCapacity ?? 0) && instances.every((instance) => instance.lifecycleState === 'InService')
|
||||
}
|
||||
@@ -68,6 +68,7 @@ import { LoadBalancerListenerPanel } from "@/components/LoadBalancerListenerPane
|
||||
import { CognitoUsersPanel } from "@/components/CognitoUsersPanel";
|
||||
import { Route53RecordsPanel } from "@/components/Route53RecordsPanel";
|
||||
import { CloudFrontPanel } from "@/components/CloudFrontPanel";
|
||||
import { AutoScalingPanel } from "@/components/AutoScalingPanel";
|
||||
import { CertificatePanel } from "@/components/CertificatePanel";
|
||||
import { PipePanel } from "@/components/PipePanel";
|
||||
import { CloudWatchAlarmPanel } from "@/components/CloudWatchAlarmPanel";
|
||||
@@ -995,6 +996,9 @@ export function DynamicResourceView({
|
||||
{service === "pipes" && cloud === "aws" && (
|
||||
<PipePanel resource={activeSelected} runtimeReachable={canUseRuntime} />
|
||||
)}
|
||||
{service === "autoscaling" && cloud === "aws" && (
|
||||
<AutoScalingPanel resource={activeSelected} runtimeReachable={canUseRuntime} />
|
||||
)}
|
||||
{service === "cdn" && cloud === "aws" && (
|
||||
<CloudFrontPanel resource={activeSelected} runtimeReachable={canUseRuntime} />
|
||||
)}
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
MessageSquare,
|
||||
Network,
|
||||
Radio,
|
||||
Scaling,
|
||||
ScrollText,
|
||||
Scale,
|
||||
Server,
|
||||
@@ -37,6 +38,7 @@ import codeDeployIcon from '../../../../aws-icons-svg/Developer-Tools/CodeDeploy
|
||||
import cognitoIcon from '../../../../aws-icons-svg/Security-Identity-Compliance/Cognito.svg'
|
||||
import route53Icon from '../../../../aws-icons-svg/Networking-Content-Delivery/Route-53.svg'
|
||||
import cloudFrontIcon from '../../../../aws-icons-svg/Networking-Content-Delivery/CloudFront.svg'
|
||||
import autoScalingIcon from '../../../../aws-icons-svg/Compute/EC2-Auto-Scaling.svg'
|
||||
import certificateManagerIcon from '../../../../aws-icons-svg/Security-Identity-Compliance/Certificate-Manager.svg'
|
||||
import cloudWatchIcon from '../../../../aws-icons-svg/Management-Governance/CloudWatch.svg'
|
||||
import cloudFormationIcon from '@/assets/aws-services/cloudformation.svg'
|
||||
@@ -98,6 +100,7 @@ const SERVICE_ICONS: Record<string, LucideIcon> = {
|
||||
userpools: UsersRound,
|
||||
dns: Globe,
|
||||
cdn: Earth,
|
||||
autoscaling: Scaling,
|
||||
certificates: FileBadge,
|
||||
monitoring: Activity,
|
||||
loadbalancing: Scale,
|
||||
@@ -147,6 +150,7 @@ const AWS_SERVICE_ICON_ASSETS: Record<string, string> = {
|
||||
userpools: cognitoIcon,
|
||||
dns: route53Icon,
|
||||
cdn: cloudFrontIcon,
|
||||
autoscaling: autoScalingIcon,
|
||||
certificates: certificateManagerIcon,
|
||||
monitoring: cloudWatchIcon,
|
||||
workflows: stepFunctionsIcon,
|
||||
|
||||
@@ -6625,3 +6625,8 @@ tr[aria-current="true"] td { background: var(--hover-md); }
|
||||
.pipe-arn, .pipe-code { overflow-wrap: anywhere; }
|
||||
.pipe-code { font-family: var(--mono); font-size: 11px; white-space: pre-wrap; }
|
||||
.pipe-role, .pipe-reason { padding: 0 16px 12px; margin: 0; overflow-wrap: anywhere; }
|
||||
|
||||
/* EC2 Auto Scaling: capacity fields side by side, instance actions in one row. */
|
||||
.autoscaling-capacity { grid-template-columns: repeat(auto-fit, minmax(110px, 1fr)); align-items: end; padding: 4px 16px 12px; }
|
||||
.autoscaling-note { padding: 0 16px 12px; margin: 0; }
|
||||
.autoscaling-instance-actions { display: flex; flex-wrap: wrap; gap: 6px; justify-content: flex-end; }
|
||||
|
||||
@@ -356,6 +356,12 @@ export type CertificateRevocationReason =
|
||||
| 'CESSATION_OF_OPERATION'
|
||||
| 'PRIVILEGE_WITHDRAWN'
|
||||
|
||||
export interface AutoScalingCapacityInput {
|
||||
minSize?: number
|
||||
desiredCapacity?: number
|
||||
maxSize?: number
|
||||
}
|
||||
|
||||
export interface DistributionInvalidation {
|
||||
id: string
|
||||
status: string
|
||||
|
||||
@@ -28,6 +28,7 @@ export type KubernetesActionName =
|
||||
| 'deleteFargateProfile'
|
||||
export type ItemActionName = 'list' | 'get' | 'put' | 'delete' | 'query'
|
||||
export type DistributionActionName = 'setEnabled' | 'createInvalidation'
|
||||
export type AutoScalingActionName = 'setCapacity' | 'terminateInstance'
|
||||
export type CertificateActionName = 'download' | 'export' | 'revoke'
|
||||
export type MonitoringActionName = 'listMetrics' | 'getMetricData' | 'alarmHistory' | 'setAlarmState' | 'setAlarmActions'
|
||||
export type UserPoolActionName = 'updateUser' | 'setUserGroups' | 'listGroups' | 'createGroup' | 'deleteGroup' | 'testSignIn' | 'describeClient' | 'updateClientOAuth'
|
||||
@@ -92,6 +93,7 @@ export interface ServiceSchema {
|
||||
itemActions?: Array<CapabilitySchema<ItemActionName> | ItemActionName>
|
||||
userPoolActions?: Array<CapabilitySchema<UserPoolActionName>>
|
||||
distributionActions?: Array<CapabilitySchema<DistributionActionName>>
|
||||
autoScalingActions?: Array<CapabilitySchema<AutoScalingActionName>>
|
||||
certificateActions?: Array<CapabilitySchema<CertificateActionName>>
|
||||
monitoringActions?: Array<CapabilitySchema<MonitoringActionName>>
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user