diff --git a/app/package.json b/app/package.json index 80413f28f..38b5ed111 100644 --- a/app/package.json +++ b/app/package.json @@ -5,6 +5,7 @@ "proxy": "http://localhost:15220", "dependencies": { "@kobsio/plugin-applications": "*", + "@kobsio/plugin-azure": "*", "@kobsio/plugin-core": "*", "@kobsio/plugin-dashboards": "*", "@kobsio/plugin-elasticsearch": "*", diff --git a/app/src/index.tsx b/app/src/index.tsx index 5b7453574..9480e3c14 100644 --- a/app/src/index.tsx +++ b/app/src/index.tsx @@ -7,6 +7,7 @@ import './index.css'; // first party plugins from the /plugins folder. import { App } from '@kobsio/plugin-core'; import applicationsPlugin from '@kobsio/plugin-applications'; +import azurePlugin from '@kobsio/plugin-azure'; import dashboardsPlugin from '@kobsio/plugin-dashboards'; import elasticsearchPlugin from '@kobsio/plugin-elasticsearch'; import fluxPlugin from '@kobsio/plugin-flux'; @@ -31,6 +32,7 @@ ReactDOM.render( = 200 && resp.StatusCode <= 299 { + var containerGroupListResult ContainerGroupListResult + + if err := json.NewDecoder(resp.Body).Decode(&containerGroupListResult); err != nil { + return nil, err + } + + return containerGroupListResult.Value, nil + } + + errBody, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + return nil, fmt.Errorf("could not list container groups: %s", string(errBody)) +} + +// GetContainerGroup returns a single container group. +func (c *Client) GetContainerGroup(ctx context.Context, resourceGroup, containerGroup string) (map[string]interface{}, error) { + req, err := http.NewRequestWithContext(context.Background(), "GET", containerinstance.DefaultBaseURI+"/subscriptions/"+c.subscriptionID+"/resourceGroups/"+resourceGroup+"/providers/Microsoft.ContainerInstance/containerGroups/"+containerGroup+"?api-version=2021-07-01", nil) + if err != nil { + return nil, err + } + + resp, err := c.baseClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode >= 200 && resp.StatusCode <= 299 { + var containerGroup map[string]interface{} + + if err := json.NewDecoder(resp.Body).Decode(&containerGroup); err != nil { + return nil, err + } + + return containerGroup, nil + } + + errBody, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + return nil, fmt.Errorf("could not get container group: %s", string(errBody)) +} + +// GetContainerGroupMetrics returns the metrisc for a container group. +func (c *Client) GetContainerGroupMetrics(ctx context.Context, resourceGroup, containerGroup, metricname string, timeStart, timeEnd int64) (*[]insights.Metric, error) { + interval := getInterval(timeStart, timeEnd) + top := int32(500) + + timeStartISO := time.Unix(timeStart, 0).UTC() + timeEndISO := time.Unix(timeEnd, 0).UTC() + timespan := timeStartISO.Format("2006-01-02T15:04:05") + "/" + timeEndISO.Format("2006-01-02T15:04:05") + + res, err := c.metricsClient.List( + ctx, + "/subscriptions/"+c.subscriptionID+"/resourceGroups/"+resourceGroup+"/providers/Microsoft.ContainerInstance/containerGroups/"+containerGroup, + timespan, + &interval, + metricname, + "", + &top, + "", + "", + insights.Data, + "", + ) + if err != nil { + return nil, err + } + + return res.Value, nil +} + +// GetContainerLogs returns the logs for a container. +func (c *Client) GetContainerLogs(ctx context.Context, resourceGroup, containerGroup, container string, tail *int32, timestamps *bool) (*string, error) { + res, err := c.containersClient.ListLogs(ctx, resourceGroup, containerGroup, container, tail, timestamps) + if err != nil { + return nil, err + } + + return res.Content, nil +} + +// RestartContainerGroup restarts a container group. +func (c *Client) RestartContainerGroup(ctx context.Context, resourceGroup, containerGroup string) error { + _, err := c.containerGroupsClient.Restart(ctx, resourceGroup, containerGroup) + if err != nil { + return err + } + + return nil +} + +// New returns a new client to interact with the container instances API. +func New(subscriptionID string, authorizer autorest.Authorizer) *Client { + baseClient := containerinstance.NewWithBaseURI(containerinstance.DefaultBaseURI, subscriptionID) + baseClient.Authorizer = authorizer + + containerGroupsClient := containerinstance.NewContainerGroupsClient(subscriptionID) + containerGroupsClient.Authorizer = authorizer + + containersClient := containerinstance.NewContainersClient(subscriptionID) + containersClient.Authorizer = authorizer + + metricsClient := insights.NewMetricsClient(subscriptionID) + metricsClient.Authorizer = authorizer + + return &Client{ + subscriptionID: subscriptionID, + baseClient: baseClient, + containerGroupsClient: containerGroupsClient, + containersClient: containersClient, + metricsClient: metricsClient, + } +} diff --git a/plugins/azure/pkg/instance/containerinstances/helpers.go b/plugins/azure/pkg/instance/containerinstances/helpers.go new file mode 100644 index 000000000..69d0fd3e9 --- /dev/null +++ b/plugins/azure/pkg/instance/containerinstances/helpers.go @@ -0,0 +1,23 @@ +package containerinstances + +// getInterval returns the duration for the Prometheus resolution for a given start and end time. +func getInterval(start, end int64) string { + switch seconds := end - start; { + case seconds <= 21600: + return "PT1M" + case seconds <= 86400: + return "PT5M" + case seconds <= 259200: + return "PT15M" + case seconds <= 518400: + return "PT30M" + case seconds <= 1036800: + return "PT1H" + case seconds <= 2073600: + return "PT6H" + case seconds <= 4147200: + return "PT12H" + default: + return "P1D" + } +} diff --git a/plugins/azure/pkg/instance/instance.go b/plugins/azure/pkg/instance/instance.go new file mode 100644 index 000000000..b29e17dc5 --- /dev/null +++ b/plugins/azure/pkg/instance/instance.go @@ -0,0 +1,44 @@ +package instance + +import ( + "os" + + "github.com/Azure/go-autorest/autorest/azure/auth" + "github.com/kobsio/kobs/plugins/azure/pkg/instance/containerinstances" + + "github.com/sirupsen/logrus" +) + +var ( + log = logrus.WithFields(logrus.Fields{"package": "azure"}) +) + +// Config is the structure of the configuration for a single Azure instance. +type Config struct { + Name string `json:"name"` + DisplayName string `json:"displayName"` + Description string `json:"description"` +} + +// Instance represents a single Azure instance, which can be added via the configuration file. +type Instance struct { + Name string + ContainerInstances *containerinstances.Client +} + +// New returns a new Elasticsearch instance for the given configuration. +func New(config Config) (*Instance, error) { + subscriptionID := os.Getenv("AZURE_SUBSCRIPTION_ID") + + authorizer, err := auth.NewAuthorizerFromEnvironment() + if err != nil { + return nil, err + } + + containerInstances := containerinstances.New(subscriptionID, authorizer) + + return &Instance{ + Name: config.Name, + ContainerInstances: containerInstances, + }, nil +} diff --git a/plugins/azure/src/assets/azure.css b/plugins/azure/src/assets/azure.css new file mode 100644 index 000000000..5f16fb3c4 --- /dev/null +++ b/plugins/azure/src/assets/azure.css @@ -0,0 +1,3 @@ +.kobsio-azure-tab-content .pf-c-tab-content { + min-height: calc(100% - 56px); +} diff --git a/plugins/azure/src/assets/icon.png b/plugins/azure/src/assets/icon.png new file mode 100644 index 000000000..b962fe363 Binary files /dev/null and b/plugins/azure/src/assets/icon.png differ diff --git a/plugins/azure/src/assets/services/container-instances.svg b/plugins/azure/src/assets/services/container-instances.svg new file mode 100644 index 000000000..28e406dc0 --- /dev/null +++ b/plugins/azure/src/assets/services/container-instances.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/plugins/azure/src/components/containerinstances/ContainerGroups.tsx b/plugins/azure/src/components/containerinstances/ContainerGroups.tsx new file mode 100644 index 000000000..0c7592fdd --- /dev/null +++ b/plugins/azure/src/components/containerinstances/ContainerGroups.tsx @@ -0,0 +1,84 @@ +import { Alert, AlertActionLink, AlertVariant, Menu, MenuContent, MenuList, Spinner } from '@patternfly/react-core'; +import { QueryObserverResult, useQuery } from 'react-query'; +import React from 'react'; + +import ContainerGroupsItem from './ContainerGroupsItem'; +import { IContainerGroup } from './interfaces'; + +interface IContainerGroupsProps { + name: string; + setDetails?: (details: React.ReactNode) => void; +} + +const ContainerGroups: React.FunctionComponent = ({ + name, + setDetails, +}: IContainerGroupsProps) => { + const { isError, isLoading, error, data, refetch } = useQuery( + ['azure/containergroups/containergroups', name], + async () => { + try { + const response = await fetch(`/api/plugins/azure/containerinstances/containergroups/${name}`, { + method: 'get', + }); + const json = await response.json(); + + if (response.status >= 200 && response.status < 300) { + return json; + } else { + if (json.error) { + throw new Error(json.error); + } else { + throw new Error('An unknown error occured'); + } + } + } catch (err) { + throw err; + } + }, + ); + + if (isLoading) { + return ( +
+ +
+ ); + } + + if (isError) { + return ( + + > => refetch()}> + Retry + + + } + > +

{error?.message}

+
+ ); + } + + if (!data || data.length === 0) { + return null; + } + + return ( + + + + {data.map((containerGroup, index) => ( + + ))} + + + + ); +}; + +export default ContainerGroups; diff --git a/plugins/azure/src/components/containerinstances/ContainerGroupsItem.tsx b/plugins/azure/src/components/containerinstances/ContainerGroupsItem.tsx new file mode 100644 index 000000000..7347b4302 --- /dev/null +++ b/plugins/azure/src/components/containerinstances/ContainerGroupsItem.tsx @@ -0,0 +1,79 @@ +import { MenuItem } from '@patternfly/react-core'; +import React from 'react'; + +import { IContainer, IContainerGroup } from './interfaces'; +import Details from './Details'; +import { getResourceGroupFromID } from '../../utils/helpers'; + +const getContainers = (containers?: IContainer[]): string[] => { + const names: string[] = []; + + if (containers) { + for (const container of containers) { + if (container.name) { + names.push(container.name); + } + } + } + + return names; +}; + +interface IAlertsItemProps { + name: string; + containerGroup: IContainerGroup; + setDetails?: (details: React.ReactNode) => void; +} + +const AlertsItem: React.FunctionComponent = ({ + name, + containerGroup, + setDetails, +}: IAlertsItemProps) => { + const resourceGroup = containerGroup.id ? getResourceGroupFromID(containerGroup.id) : ''; + + return ( + + + Resource Group: + {resourceGroup || '-'} + + + Location: + {containerGroup.location || '-'} + + + Provisioning State: + {containerGroup.properties?.provisioningState || '-'} + + + Containers: + + {containerGroup.properties?.containers?.map((container) => container.name).join(', ')} + + + + } + onClick={ + setDetails + ? (): void => + setDetails( +
setDetails(undefined)} + />, + ) + : undefined + } + > + {containerGroup.name} + + ); +}; + +export default AlertsItem; diff --git a/plugins/azure/src/components/containerinstances/Details.tsx b/plugins/azure/src/components/containerinstances/Details.tsx new file mode 100644 index 000000000..1198718b3 --- /dev/null +++ b/plugins/azure/src/components/containerinstances/Details.tsx @@ -0,0 +1,108 @@ +import { + Card, + CardBody, + DrawerActions, + DrawerCloseButton, + DrawerHead, + DrawerPanelBody, + DrawerPanelContent, + Tab, + TabTitleText, + Tabs, +} from '@patternfly/react-core'; +import React, { useRef, useState } from 'react'; + +import { Title, useDimensions } from '@kobsio/plugin-core'; +import DetailsContainerGroup from './DetailsContainerGroup'; +import DetailsContainerGroupActions from './DetailsContainerGroupActions'; +import DetailsLogs from './DetailsLogs'; +import DetailsMetrics from './DetailsMetrics'; + +interface IDetailsProps { + name: string; + resourceGroup: string; + containerGroup: string; + containers: string[]; + close: () => void; +} + +const Details: React.FunctionComponent = ({ + name, + resourceGroup, + containerGroup, + containers, + close, +}: IDetailsProps) => { + const [activeTab, setActiveTab] = useState('details'); + const refTabsWrapper = useRef(null); + const tabsWrapperSize = useDimensions(refTabsWrapper); + + return ( + + + + <DrawerActions style={{ padding: 0 }}> + <DetailsContainerGroupActions + name={name} + resourceGroup={resourceGroup} + containerGroup={containerGroup} + isPanelAction={false} + /> + <DrawerCloseButton onClose={close} /> + </DrawerActions> + </DrawerHead> + + <DrawerPanelBody className="kobsio-azure-tab-content"> + <div style={{ height: '100%' }} ref={refTabsWrapper}> + <Tabs + activeKey={activeTab} + onSelect={(event, tabIndex): void => setActiveTab(tabIndex.toString())} + className="pf-u-mt-md" + isFilled={true} + mountOnEnter={true} + > + <Tab eventKey="details" title={<TabTitleText>Details</TabTitleText>}> + <div style={{ maxWidth: '100%', overflowX: 'scroll', padding: '24px 24px' }}> + <Card isCompact={true}> + <CardBody> + <DetailsContainerGroup name={name} resourceGroup={resourceGroup} containerGroup={containerGroup} /> + </CardBody> + </Card> + </div> + </Tab> + + <Tab eventKey="metrics" title={<TabTitleText>Metrics</TabTitleText>}> + <div style={{ maxWidth: '100%', overflowX: 'scroll', padding: '24px 24px' }}> + <DetailsMetrics name={name} resourceGroup={resourceGroup} containerGroup={containerGroup} /> + </div> + </Tab> + + <Tab eventKey="logs" title={<TabTitleText>Logs</TabTitleText>}> + <div + style={{ + height: `${tabsWrapperSize.height - 32}px`, + maxWidth: '100%', + overflowX: 'scroll', + padding: '24px 24px', + }} + > + <Card isCompact={true} style={{ height: '100%' }}> + <CardBody> + <DetailsLogs + name={name} + resourceGroup={resourceGroup} + containerGroup={containerGroup} + containers={containers} + /> + </CardBody> + </Card> + </div> + </Tab> + </Tabs> + </div> + </DrawerPanelBody> + </DrawerPanelContent> + ); +}; + +export default Details; diff --git a/plugins/azure/src/components/containerinstances/DetailsContainerGroup.tsx b/plugins/azure/src/components/containerinstances/DetailsContainerGroup.tsx new file mode 100644 index 000000000..a54f63320 --- /dev/null +++ b/plugins/azure/src/components/containerinstances/DetailsContainerGroup.tsx @@ -0,0 +1,212 @@ +import { + Alert, + AlertActionLink, + AlertVariant, + DescriptionList, + DescriptionListDescription, + DescriptionListGroup, + DescriptionListTerm, + Spinner, + Title, +} from '@patternfly/react-core'; +import { QueryObserverResult, useQuery } from 'react-query'; +import { TableComposable, TableVariant, Tbody, Th, Thead, Tr } from '@patternfly/react-table'; +import React from 'react'; + +import DetailsContainerGroupContainer from './DetailsContainerGroupContainer'; +import DetailsContainerGroupEvent from './DetailsContainerGroupEvent'; +import DetailsContainerGroupInitContainer from './DetailsContainerGroupInitContainer'; +import { IContainerGroup } from './interfaces'; + +interface IDetailsContainerGroupProps { + name: string; + resourceGroup: string; + containerGroup: string; +} + +const DetailsContainerGroup: React.FunctionComponent<IDetailsContainerGroupProps> = ({ + name, + resourceGroup, + containerGroup, +}: IDetailsContainerGroupProps) => { + const { isError, isLoading, error, data, refetch } = useQuery<IContainerGroup, Error>( + ['azure/containergroups/containergroup/details', name, resourceGroup, containerGroup], + async () => { + try { + const response = await fetch( + `/api/plugins/azure/containerinstances/containergroup/details/${name}?resourceGroup=${resourceGroup}&containerGroup=${containerGroup}`, + { + method: 'get', + }, + ); + const json = await response.json(); + + if (response.status >= 200 && response.status < 300) { + return json; + } else { + if (json.error) { + throw new Error(json.error); + } else { + throw new Error('An unknown error occured'); + } + } + } catch (err) { + throw err; + } + }, + ); + + if (isLoading) { + return ( + <div className="pf-u-text-align-center"> + <Spinner /> + </div> + ); + } + + if (isError) { + return ( + <Alert + variant={AlertVariant.danger} + isInline={true} + title="Could not get container group details" + actionLinks={ + <React.Fragment> + <AlertActionLink onClick={(): Promise<QueryObserverResult<IContainerGroup, Error>> => refetch()}> + Retry + </AlertActionLink> + </React.Fragment> + } + > + <p>{error?.message}</p> + </Alert> + ); + } + + if (!data) { + return null; + } + + return ( + <div style={{ maxWidth: '100%', overflow: 'scroll' }}> + <DescriptionList className="pf-u-text-break-word" isHorizontal={true}> + <DescriptionListGroup> + <DescriptionListTerm>OS Type</DescriptionListTerm> + <DescriptionListDescription>{data.properties?.osType || '-'}</DescriptionListDescription> + </DescriptionListGroup> + <DescriptionListGroup> + <DescriptionListTerm>Restart Policy</DescriptionListTerm> + <DescriptionListDescription>{data.properties?.restartPolicy || '-'}</DescriptionListDescription> + </DescriptionListGroup> + <DescriptionListGroup> + <DescriptionListTerm>FQDN</DescriptionListTerm> + <DescriptionListDescription>{data.properties?.ipAddress?.fqdn || '-'}</DescriptionListDescription> + </DescriptionListGroup> + <DescriptionListGroup> + <DescriptionListTerm> + IP Address {data.properties?.ipAddress?.type ? `(${data.properties?.ipAddress?.type})` : ''} + </DescriptionListTerm> + <DescriptionListDescription>{data.properties?.ipAddress?.ip || '-'}</DescriptionListDescription> + </DescriptionListGroup> + <DescriptionListGroup> + <DescriptionListTerm>Ports</DescriptionListTerm> + <DescriptionListDescription> + {data.properties?.ipAddress?.ports.map((port) => `${port.port} (${port.protocol || '-'})`).join(', ') || + '-'} + </DescriptionListDescription> + </DescriptionListGroup> + <DescriptionListGroup> + <DescriptionListTerm>Volumes</DescriptionListTerm> + <DescriptionListDescription> + {data.properties?.volumes?.map((volume) => volume.name).join(', ') || '-'} + </DescriptionListDescription> + </DescriptionListGroup> + <DescriptionListGroup> + <DescriptionListTerm>Provisioning State</DescriptionListTerm> + <DescriptionListDescription>{data.properties?.provisioningState || '-'}</DescriptionListDescription> + </DescriptionListGroup> + <DescriptionListGroup> + <DescriptionListTerm>State</DescriptionListTerm> + <DescriptionListDescription>{data.properties?.instanceView?.state || '-'}</DescriptionListDescription> + </DescriptionListGroup> + <DescriptionListGroup> + <DescriptionListTerm>State</DescriptionListTerm> + <DescriptionListDescription>{data.properties?.instanceView?.state || '-'}</DescriptionListDescription> + </DescriptionListGroup> + + {data.properties?.instanceView?.events && data.properties?.instanceView?.events.length > 0 && ( + <React.Fragment> + <Title headingLevel="h4" size="lg"> + Events + + + + + Name + Type + Count + First Seen + Last Seen + Message + + + + {data.properties?.instanceView?.events?.map((event, index) => ( + + ))} + + + + )} + + {data.properties?.initContainers && data.properties?.initContainers.length > 0 && ( + + + Init Containers + + + + + Name + Restarts + Current State + Previous State + + + + {data.properties?.initContainers?.map((initContainer) => ( + + ))} + + + + )} + + {data.properties?.containers && data.properties?.containers.length > 0 && ( + + + Containers + + + + + Name + Restarts + Current State + Previous State + + + + {data.properties?.containers?.map((container) => ( + + ))} + + + + )} + + + ); +}; + +export default DetailsContainerGroup; diff --git a/plugins/azure/src/components/containerinstances/DetailsContainerGroupActions.tsx b/plugins/azure/src/components/containerinstances/DetailsContainerGroupActions.tsx new file mode 100644 index 000000000..251d37e37 --- /dev/null +++ b/plugins/azure/src/components/containerinstances/DetailsContainerGroupActions.tsx @@ -0,0 +1,92 @@ +import { + Alert, + AlertActionCloseButton, + AlertGroup, + AlertVariant, + CardActions, + Dropdown, + DropdownItem, + KebabToggle, +} from '@patternfly/react-core'; +import React, { useState } from 'react'; + +interface IDetailsContainerGroupActionsProps { + name: string; + resourceGroup: string; + containerGroup: string; + isPanelAction: boolean; +} + +const DetailsContainerGroupActions: React.FunctionComponent = ({ + name, + resourceGroup, + containerGroup, + isPanelAction, +}: IDetailsContainerGroupActionsProps) => { + const [showDropdown, setShowDropdown] = useState(false); + const [alert, setAlert] = useState<{ title: string; variant: AlertVariant } | undefined>(undefined); + + const restart = async (): Promise => { + try { + const response = await fetch( + `/api/plugins/azure/containerinstances/containergroup/restart/${name}?resourceGroup=${resourceGroup}&containerGroup=${containerGroup}`, + { + method: 'get', + }, + ); + const json = await response.json(); + + if (response.status >= 200 && response.status < 300) { + setShowDropdown(false); + setAlert({ title: `${containerGroup} (${resourceGroup}) was restarted`, variant: AlertVariant.success }); + } else { + if (json.error) { + throw new Error(json.error); + } else { + throw new Error('An unknown error occured'); + } + } + } catch (err) { + setShowDropdown(false); + setAlert({ title: err.message, variant: AlertVariant.danger }); + } + }; + + const dropdown = ( + setShowDropdown(!showDropdown)} />} + isOpen={showDropdown} + isPlain={true} + position="right" + dropdownItems={[ + + Restart + , + ]} + /> + ); + + if (isPanelAction) { + return {dropdown}; + } + + return ( + + {alert ? ( + + setAlert(undefined)} />} + /> + + ) : null} + + {dropdown} + + ); +}; + +export default DetailsContainerGroupActions; diff --git a/plugins/azure/src/components/containerinstances/DetailsContainerGroupContainer.tsx b/plugins/azure/src/components/containerinstances/DetailsContainerGroupContainer.tsx new file mode 100644 index 000000000..62e7ab723 --- /dev/null +++ b/plugins/azure/src/components/containerinstances/DetailsContainerGroupContainer.tsx @@ -0,0 +1,96 @@ +import { + DescriptionList, + DescriptionListDescription, + DescriptionListGroup, + DescriptionListTerm, +} from '@patternfly/react-core'; +import { ExpandableRowContent, TableComposable, TableVariant, Tbody, Td, Tr } from '@patternfly/react-table'; +import React, { useState } from 'react'; + +import DetailsContainerGroupEvent from './DetailsContainerGroupEvent'; +import { IContainer } from './interfaces'; + +interface IDetailsContainerGroupContainerProps { + container: IContainer; +} + +const DetailsContainerGroupContainer: React.FunctionComponent = ({ + container, +}: IDetailsContainerGroupContainerProps) => { + const [isExpanded, setIsExpaned] = useState(false); + + return ( + + setIsExpaned(!isExpanded)}> + {container.name} + {container.properties?.instanceView?.restartCount || '-'} + {container.properties?.instanceView?.currentState?.state || '-'} + {container.properties?.instanceView?.previousState?.state || '-'} + + + + + + {container.properties?.image && ( + + Image + {container.properties?.image} + + )} + {container.properties?.command && container.properties?.command.length > 0 && ( + + Command + {container.properties.command.join(' ')} + + )} + {container.properties?.environmentVariables && ( + + Environment + + {container.properties?.environmentVariables?.map((env, index) => ( +
+ {env.name}: + + {env.value ? env.value : env.secureValue} + +
+ ))} +
+
+ )} + {container.properties?.volumeMounts && ( + + Mounts + + {container.properties?.volumeMounts.map((mount, index) => ( +
+ {mount.name}: + {mount.mountPath} +
+ ))} +
+
+ )} + {container.properties?.instanceView?.events && container.properties?.instanceView?.events.length > 0 && ( + + Events + + + + {container.properties?.instanceView?.events.map((event, index) => ( + + ))} + + + + + )} +
+
+ + +
+ ); +}; + +export default DetailsContainerGroupContainer; diff --git a/plugins/azure/src/components/containerinstances/DetailsContainerGroupEvent.tsx b/plugins/azure/src/components/containerinstances/DetailsContainerGroupEvent.tsx new file mode 100644 index 000000000..861841b4a --- /dev/null +++ b/plugins/azure/src/components/containerinstances/DetailsContainerGroupEvent.tsx @@ -0,0 +1,28 @@ +import { Td, Tr } from '@patternfly/react-table'; +import { ContainerInstanceManagementModels } from '@azure/arm-containerinstance'; +import React from 'react'; + +import { formatTime } from '../../utils/helpers'; + +interface IDetailsContainerGroupEventProps { + event: ContainerInstanceManagementModels.Event; +} + +const DetailsContainerGroupEvent: React.FunctionComponent = ({ + event, +}: IDetailsContainerGroupEventProps) => { + return ( + + {event.name} + {event.type} + {event.count} + + {event.firstTimestamp ? formatTime(event.firstTimestamp as unknown as string) : '-'} + + {event.lastTimestamp ? formatTime(event.lastTimestamp as unknown as string) : '-'} + {event.message} + + ); +}; + +export default DetailsContainerGroupEvent; diff --git a/plugins/azure/src/components/containerinstances/DetailsContainerGroupInitContainer.tsx b/plugins/azure/src/components/containerinstances/DetailsContainerGroupInitContainer.tsx new file mode 100644 index 000000000..ebda19ffe --- /dev/null +++ b/plugins/azure/src/components/containerinstances/DetailsContainerGroupInitContainer.tsx @@ -0,0 +1,97 @@ +import { + DescriptionList, + DescriptionListDescription, + DescriptionListGroup, + DescriptionListTerm, +} from '@patternfly/react-core'; +import { ExpandableRowContent, TableComposable, TableVariant, Tbody, Td, Tr } from '@patternfly/react-table'; +import React, { useState } from 'react'; + +import DetailsContainerGroupEvent from './DetailsContainerGroupEvent'; +import { IInitContainer } from './interfaces'; + +interface IDetailsContainerGroupInitContainerProps { + initContainer: IInitContainer; +} + +const DetailsContainerGroupInitContainer: React.FunctionComponent = ({ + initContainer, +}: IDetailsContainerGroupInitContainerProps) => { + const [isExpanded, setIsExpaned] = useState(false); + + return ( + + setIsExpaned(!isExpanded)}> + {initContainer.name} + {initContainer.properties?.instanceView?.restartCount || '-'} + {initContainer.properties?.instanceView?.currentState?.state || '-'} + {initContainer.properties?.instanceView?.previousState?.state || '-'} + + + + + + {initContainer.properties?.image && ( + + Image + {initContainer.properties?.image} + + )} + {initContainer.properties?.command && initContainer.properties?.command.length > 0 && ( + + Command + {initContainer.properties.command.join(' ')} + + )} + {initContainer.properties?.environmentVariables && ( + + Environment + + {initContainer.properties?.environmentVariables?.map((env, index) => ( +
+ {env.name}: + + {env.value ? env.value : env.secureValue} + +
+ ))} +
+
+ )} + {initContainer.properties?.volumeMounts && ( + + Mounts + + {initContainer.properties?.volumeMounts.map((mount, index) => ( +
+ {mount.name}: + {mount.mountPath} +
+ ))} +
+
+ )} + {initContainer.properties?.instanceView?.events && + initContainer.properties?.instanceView?.events.length > 0 && ( + + Events + + + + {initContainer.properties?.instanceView?.events.map((event, index) => ( + + ))} + + + + + )} +
+
+ + +
+ ); +}; + +export default DetailsContainerGroupInitContainer; diff --git a/plugins/azure/src/components/containerinstances/DetailsLogs.tsx b/plugins/azure/src/components/containerinstances/DetailsLogs.tsx new file mode 100644 index 000000000..735df20fe --- /dev/null +++ b/plugins/azure/src/components/containerinstances/DetailsLogs.tsx @@ -0,0 +1,113 @@ +import { + Alert, + AlertActionLink, + AlertVariant, + Select, + SelectOption, + SelectVariant, + Spinner, +} from '@patternfly/react-core'; +import { QueryObserverResult, useQuery } from 'react-query'; +import React, { useRef, useState } from 'react'; +import { LogViewer } from '@patternfly/react-log-viewer'; + +import { IContainerLogs } from './interfaces'; +import { useDimensions } from '@kobsio/plugin-core'; + +interface IDetailsLogsProps { + name: string; + resourceGroup: string; + containerGroup: string; + containers: string[]; +} + +const DetailsLogs: React.FunctionComponent = ({ + name, + resourceGroup, + containerGroup, + containers, +}: IDetailsLogsProps) => { + const [container, setContainer] = useState(containers.length > 0 ? containers[0] : ''); + const [showSelect, setShowSelect] = useState(false); + const refWrapper = useRef(null); + const wrapperSize = useDimensions(refWrapper); + + const { isError, isLoading, error, data, refetch } = useQuery( + ['azure/containergroups/containergroup/logs', name, resourceGroup, containerGroup, container], + async () => { + try { + if (container !== '') { + const response = await fetch( + `/api/plugins/azure/containerinstances/containergroup/logs/${name}?resourceGroup=${resourceGroup}&containerGroup=${containerGroup}&container=${container}`, + { + method: 'get', + }, + ); + const json = await response.json(); + + if (response.status >= 200 && response.status < 300) { + return json; + } else { + if (json.error) { + throw new Error(json.error); + } else { + throw new Error('An unknown error occured'); + } + } + } + } catch (err) { + throw err; + } + }, + ); + + console.log(wrapperSize); + + return ( +
+ + +

 

+ + {isLoading ? ( +
+ +
+ ) : isError ? ( + + > => refetch()}> + Retry + + + } + > +

{error?.message}

+
+ ) : data && data.logs ? ( + + ) : ( +

 

+ )} +

 

+
+ ); +}; + +export default DetailsLogs; diff --git a/plugins/azure/src/components/containerinstances/DetailsMetric.tsx b/plugins/azure/src/components/containerinstances/DetailsMetric.tsx new file mode 100644 index 000000000..22aea66aa --- /dev/null +++ b/plugins/azure/src/components/containerinstances/DetailsMetric.tsx @@ -0,0 +1,128 @@ +import { Alert, AlertActionLink, AlertVariant, Spinner } from '@patternfly/react-core'; +import { QueryObserverResult, useQuery } from 'react-query'; +import React from 'react'; +import { ResponsiveLineCanvas } from '@nivo/line'; + +import { CHART_THEME, COLOR_SCALE, ChartTooltip } from '@kobsio/plugin-core'; +import { formatAxisBottom, formatMetrics } from '../../utils/helpers'; +import { IMetric } from '../../utils/interfaces'; +import { IPluginTimes } from '@kobsio/plugin-core'; + +interface IDetailsMetricProps { + name: string; + resourceGroup: string; + containerGroup: string; + metricName: string; + times: IPluginTimes; +} + +const DetailsMetric: React.FunctionComponent = ({ + name, + resourceGroup, + containerGroup, + metricName, + times, +}: IDetailsMetricProps) => { + const { isError, isLoading, error, data, refetch } = useQuery( + ['azure/containergroups/containergroup/metrics', name, resourceGroup, containerGroup, metricName, times], + async () => { + try { + const response = await fetch( + `/api/plugins/azure/containerinstances/containergroup/metrics/${name}?resourceGroup=${resourceGroup}&containerGroup=${containerGroup}&metricName=${metricName}&timeStart=${times.timeStart}&timeEnd=${times.timeEnd}`, + { + method: 'get', + }, + ); + const json = await response.json(); + + if (response.status >= 200 && response.status < 300) { + return json; + } else { + if (json.error) { + throw new Error(json.error); + } else { + throw new Error('An unknown error occured'); + } + } + } catch (err) { + throw err; + } + }, + ); + + if (isLoading) { + return ( +
+ +
+ ); + } + + if (isError) { + return ( + + > => refetch()}> + Retry + + + } + > +

{error?.message}

+
+ ); + } + + if (!data || data.length !== 1) { + return null; + } + + return ( + -.2f', + legend: data[0].unit, + legendOffset: -40, + legendPosition: 'middle', + }} + colors={COLOR_SCALE} + curve="monotoneX" + data={formatMetrics(data[0])} + enableArea={true} + enableGridX={false} + enableGridY={true} + enablePoints={false} + xFormat="time:%Y-%m-%d %H:%M:%S" + lineWidth={1} + margin={{ bottom: 25, left: 50, right: 0, top: 0 }} + theme={CHART_THEME} + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type + tooltip={(tooltip) => { + const isFirstHalf = + new Date(tooltip.point.data.x).getTime() < (times.timeStart * 1000 + times.timeEnd * 1000) / 2; + + return ( + + ); + }} + xScale={{ type: 'time' }} + yScale={{ max: 'auto', min: 'auto', stacked: false, type: 'linear' }} + yFormat=" >-.4f" + /> + ); +}; + +export default DetailsMetric; diff --git a/plugins/azure/src/components/containerinstances/DetailsMetrics.tsx b/plugins/azure/src/components/containerinstances/DetailsMetrics.tsx new file mode 100644 index 000000000..ad0bc4710 --- /dev/null +++ b/plugins/azure/src/components/containerinstances/DetailsMetrics.tsx @@ -0,0 +1,102 @@ +import { Card, CardBody, CardHeader, CardTitle } from '@patternfly/react-core'; +import React, { useState } from 'react'; + +import DetailsMetric from './DetailsMetric'; +import DetailsMetricsToolbar from './DetailsMetricsToolbar'; +import { IPluginTimes } from '@kobsio/plugin-core'; + +interface IDetailsMetricsProps { + name: string; + resourceGroup: string; + containerGroup: string; +} + +const DetailsMetrics: React.FunctionComponent = ({ + name, + resourceGroup, + containerGroup, +}: IDetailsMetricsProps) => { + const [times, setTimes] = useState({ + timeEnd: Math.floor(Date.now() / 1000), + timeStart: Math.floor(Date.now() / 1000) - 900, + }); + + return ( +
+ +

 

+ + + CPU Usage + + +
+ +
+
+
+ +

 

+ + + Memory Usage + + +
+ +
+
+
+ +

 

+ + + Network Bytes Received Per Second + + +
+ +
+
+
+ +

 

+ + + Network Bytes Transmitted Per Second + + +
+ +
+
+
+
+ ); +}; + +export default DetailsMetrics; diff --git a/plugins/azure/src/components/containerinstances/DetailsMetricsToolbar.tsx b/plugins/azure/src/components/containerinstances/DetailsMetricsToolbar.tsx new file mode 100644 index 000000000..26fbcec2d --- /dev/null +++ b/plugins/azure/src/components/containerinstances/DetailsMetricsToolbar.tsx @@ -0,0 +1,42 @@ +import { Card, Toolbar, ToolbarContent, ToolbarGroup, ToolbarItem, ToolbarToggleGroup } from '@patternfly/react-core'; +import { FilterIcon } from '@patternfly/react-icons'; +import React from 'react'; + +import { IOptionsAdditionalFields, IPluginTimes, Options } from '@kobsio/plugin-core'; + +interface IDetailsMetricsToolbarProps { + times: IPluginTimes; + setTimes: (times: IPluginTimes) => void; +} + +const DetailsMetricsToolbar: React.FunctionComponent = ({ + times, + setTimes, +}: IDetailsMetricsToolbarProps) => { + return ( + + + + } breakpoint="lg"> + + + setTimes({ timeEnd: timeEnd, timeStart: timeStart })} + /> + + + + + + + ); +}; + +export default DetailsMetricsToolbar; diff --git a/plugins/azure/src/components/containerinstances/Page.tsx b/plugins/azure/src/components/containerinstances/Page.tsx new file mode 100644 index 000000000..caeb6ee91 --- /dev/null +++ b/plugins/azure/src/components/containerinstances/Page.tsx @@ -0,0 +1,41 @@ +import { Drawer, DrawerContent, DrawerContentBody, PageSection, PageSectionVariants } from '@patternfly/react-core'; +import React, { useState } from 'react'; + +import ContainerGroups from './ContainerGroups'; +import { Title } from '@kobsio/plugin-core'; +import { services } from '../../utils/services'; + +const service = 'containerinstances'; + +interface IContainerInstancesPageProps { + name: string; + displayName: string; +} + +const ContainerInstancesPage: React.FunctionComponent = ({ + name, + displayName, +}: IContainerInstancesPageProps) => { + const [details, setDetails] = useState(undefined); + + return ( + + + + <p>{services[service].description}</p> + </PageSection> + + <Drawer isExpanded={details !== undefined}> + <DrawerContent panelContent={details}> + <DrawerContentBody> + <PageSection style={{ minHeight: '100%' }} variant={PageSectionVariants.default}> + <ContainerGroups name={name} setDetails={setDetails} /> + </PageSection> + </DrawerContentBody> + </DrawerContent> + </Drawer> + </React.Fragment> + ); +}; + +export default ContainerInstancesPage; diff --git a/plugins/azure/src/components/containerinstances/interfaces.ts b/plugins/azure/src/components/containerinstances/interfaces.ts new file mode 100644 index 000000000..8bc8a95a6 --- /dev/null +++ b/plugins/azure/src/components/containerinstances/interfaces.ts @@ -0,0 +1,25 @@ +import { ContainerInstanceManagementModels } from '@azure/arm-containerinstance'; + +export interface IContainerGroup extends ContainerInstanceManagementModels.Resource { + properties?: IContainerGroupProperties; +} + +export interface IContainerGroupProperties + extends Omit<ContainerInstanceManagementModels.ContainerGroup, 'containers' | 'initContainers'> { + containers?: IContainer[]; + initContainers?: IInitContainer[]; +} + +export interface IContainer { + name?: string; + properties?: ContainerInstanceManagementModels.Container; +} + +export interface IInitContainer { + name?: string; + properties?: ContainerInstanceManagementModels.InitContainerDefinition; +} + +export interface IContainerLogs { + logs: string; +} diff --git a/plugins/azure/src/components/page/OverviewPage.tsx b/plugins/azure/src/components/page/OverviewPage.tsx new file mode 100644 index 000000000..23e378482 --- /dev/null +++ b/plugins/azure/src/components/page/OverviewPage.tsx @@ -0,0 +1,72 @@ +import { + Avatar, + Card, + CardBody, + CardHeader, + CardTitle, + Drawer, + DrawerContent, + DrawerContentBody, + Gallery, + GalleryItem, + PageSection, + PageSectionVariants, + Title, +} from '@patternfly/react-core'; +import React from 'react'; + +import { LinkWrapper } from '@kobsio/plugin-core'; +import { services } from '../../utils/services'; + +interface IOverviewPageProps { + name: string; + displayName: string; + description: string; +} + +const OverviewPage: React.FunctionComponent<IOverviewPageProps> = ({ + name, + displayName, + description, +}: IOverviewPageProps) => { + return ( + <React.Fragment> + <PageSection variant={PageSectionVariants.light}> + <Title headingLevel="h6" size="xl"> + {displayName} + +

{description}

+
+ + + + + + + {Object.keys(services).map((service) => ( + + + + + + {services[service].name} + + {services[service].description} + + + + ))} + + + + + +
+ ); +}; + +export default OverviewPage; diff --git a/plugins/azure/src/components/page/Page.tsx b/plugins/azure/src/components/page/Page.tsx new file mode 100644 index 000000000..ea245157b --- /dev/null +++ b/plugins/azure/src/components/page/Page.tsx @@ -0,0 +1,21 @@ +import { Route, Switch } from 'react-router-dom'; +import React from 'react'; + +import ContainerInstancesPage from '../containerinstances/Page'; +import { IPluginPageProps } from '@kobsio/plugin-core'; +import OverviewPage from './OverviewPage'; + +const Page: React.FunctionComponent = ({ name, displayName, description }: IPluginPageProps) => { + return ( + + + + + + + + + ); +}; + +export default Page; diff --git a/plugins/azure/src/components/panel/Panel.tsx b/plugins/azure/src/components/panel/Panel.tsx new file mode 100644 index 000000000..f6b2fa06b --- /dev/null +++ b/plugins/azure/src/components/panel/Panel.tsx @@ -0,0 +1,127 @@ +import React, { memo } from 'react'; + +import { IPluginPanelProps, PluginCard, PluginOptionsMissing } from '@kobsio/plugin-core'; +import { IPanelOptions } from '../../utils/interfaces'; + +import CIContainerGroups from '../containerinstances/ContainerGroups'; +import CIDetailMetric from '../containerinstances/DetailsMetric'; +import CIDetailsContainerGroup from '../containerinstances/DetailsContainerGroup'; +import CIDetailsContainerGroupActions from '../containerinstances/DetailsContainerGroupActions'; +import CIDetailsLogs from '../containerinstances/DetailsLogs'; + +interface IPanelProps extends IPluginPanelProps { + options?: IPanelOptions; +} + +export const Panel: React.FunctionComponent = ({ + name, + title, + description, + times, + options, + showDetails, +}: IPanelProps) => { + if ( + options?.type && + options?.type === 'containerinstances' && + options.containerinstances && + options.containerinstances.type === 'list' + ) { + return ( + + + + ); + } + + if ( + options?.type && + options?.type === 'containerinstances' && + options.containerinstances && + options.containerinstances.type === 'details' && + options.containerinstances.resourceGroup && + options.containerinstances.containerGroup + ) { + return ( + + } + > + + + ); + } + + if ( + options?.type && + options?.type === 'containerinstances' && + options.containerinstances && + options.containerinstances.type === 'logs' && + options.containerinstances.resourceGroup && + options.containerinstances.containerGroup && + options.containerinstances.containers + ) { + return ( + + + + ); + } + + if ( + options?.type && + options?.type === 'containerinstances' && + options.containerinstances && + options.containerinstances.type === 'metrics' && + options.containerinstances.resourceGroup && + options.containerinstances.containerGroup && + options.containerinstances.metric && + times + ) { + return ( + + + + ); + } + + return ( + + ); +}; + +export default memo(Panel, (prevProps, nextProps) => { + if (JSON.stringify(prevProps) === JSON.stringify(nextProps)) { + return true; + } + + return false; +}); diff --git a/plugins/azure/src/index.ts b/plugins/azure/src/index.ts new file mode 100644 index 000000000..3a89ea2a5 --- /dev/null +++ b/plugins/azure/src/index.ts @@ -0,0 +1,18 @@ +import { IPluginComponents } from '@kobsio/plugin-core'; + +import './assets/azure.css'; + +import icon from './assets/icon.png'; + +import Page from './components/page/Page'; +import Panel from './components/panel/Panel'; + +const azurePlugin: IPluginComponents = { + azure: { + icon: icon, + page: Page, + panel: Panel, + }, +}; + +export default azurePlugin; diff --git a/plugins/azure/src/utils/helpers.ts b/plugins/azure/src/utils/helpers.ts new file mode 100644 index 000000000..fc6d37b6e --- /dev/null +++ b/plugins/azure/src/utils/helpers.ts @@ -0,0 +1,51 @@ +import { Serie } from '@nivo/line'; + +import { IMetric } from './interfaces'; +import { formatTime as formatTimeCore } from '@kobsio/plugin-core'; + +export const getResourceGroupFromID = (id: string): string => { + const parts = id.split('/'); + const index = parts.indexOf('resourceGroups'); + + if (index > -1 && parts.length >= index + 1) { + return parts[index + 1]; + } + + return ''; +}; + +export const formatTime = (time: string): string => { + return formatTimeCore(Math.floor(new Date(time).getTime() / 1000)); +}; + +// formatAxisBottom calculates the format for the bottom axis based on the specified start and end time. +export const formatAxisBottom = (timeStart: number, timeEnd: number): string => { + timeStart = Math.floor(timeStart / 1000); + timeEnd = Math.floor(timeEnd / 1000); + + if (timeEnd - timeStart < 3600) { + return '%H:%M:%S'; + } else if (timeEnd - timeStart < 86400) { + return '%H:%M'; + } else if (timeEnd - timeStart < 604800) { + return '%m-%d %H:%M'; + } + + return '%m-%d'; +}; + +// formatMetrics returns the metrics from the Azure API in the format required for nivo charts. +export const formatMetrics = (metrics: IMetric): Serie[] => { + const series: Serie[] = []; + + if (metrics.timeseries.length === 1) { + series.push({ + data: metrics.timeseries[0].data.map((datum) => { + return { x: new Date(datum.timeStamp), y: datum.average === undefined ? null : datum.average }; + }), + id: 'metric', + }); + } + + return series; +}; diff --git a/plugins/azure/src/utils/interfaces.ts b/plugins/azure/src/utils/interfaces.ts new file mode 100644 index 000000000..762f9f85d --- /dev/null +++ b/plugins/azure/src/utils/interfaces.ts @@ -0,0 +1,34 @@ +// IPanelOptions is the interface for the options property for the Azure panel component. +export interface IPanelOptions { + type?: string; + containerinstances?: { + type?: string; + resourceGroup?: string; + containerGroup?: string; + containers?: string[]; + metric?: string; + }; +} + +// IMetric is the interface for all metrics returned by Azure. +export interface IMetric { + id: string; + type: string; + name: IMetricName; + unit: string; + timeseries: IMetricTimeseries[]; +} + +export interface IMetricName { + value: string; + localizedValue: string; +} + +export interface IMetricTimeseries { + data: IMetricDatum[]; +} + +export interface IMetricDatum { + timeStamp: string; + average?: number; +} diff --git a/plugins/azure/src/utils/services.ts b/plugins/azure/src/utils/services.ts new file mode 100644 index 000000000..9b8ed72be --- /dev/null +++ b/plugins/azure/src/utils/services.ts @@ -0,0 +1,20 @@ +import containerInstancesIcon from '../assets/services/container-instances.svg'; + +export interface IServices { + [key: string]: IService; +} + +export interface IService { + description: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + icon: any; + name: string; +} + +export const services: IServices = { + containerinstances: { + description: 'Easily run containers on Azure without managing servers', + icon: containerInstancesIcon, + name: 'Container Instances', + }, +}; diff --git a/plugins/azure/tsconfig.esm.json b/plugins/azure/tsconfig.esm.json new file mode 100644 index 000000000..acbc1eff8 --- /dev/null +++ b/plugins/azure/tsconfig.esm.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig.json", + "include": ["src"], + "compilerOptions": { + "outDir": "lib-esm", + "module": "esnext", + "target": "esnext", + "moduleResolution": "node", + "lib": ["dom", "esnext"], + "declaration": false + } +} diff --git a/plugins/azure/tsconfig.json b/plugins/azure/tsconfig.json new file mode 100644 index 000000000..09365d619 --- /dev/null +++ b/plugins/azure/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "../../tsconfig.json", + "exclude": ["node_modules", "lib-esm", "lib"], + "include": ["src"], + "compilerOptions": { + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "outDir": "lib", + "declaration": true + } +} diff --git a/typings.d.ts b/typings.d.ts index 1cc546e20..95d5b88f4 100644 --- a/typings.d.ts +++ b/typings.d.ts @@ -10,6 +10,12 @@ declare module '*.png' { export default value; } +declare module '*.svg' { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const content: any; + export default content; +} + declare module '*.css' { interface IClassNames { [className: string]: string; diff --git a/yarn.lock b/yarn.lock index 6de76cee9..77e871239 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,6 +2,55 @@ # yarn lockfile v1 +"@azure/abort-controller@^1.0.0": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@azure/abort-controller/-/abort-controller-1.0.4.tgz#fd3c4d46c8ed67aace42498c8e2270960250eafd" + integrity sha512-lNUmDRVGpanCsiUN3NWxFTdwmdFI53xwhkTFfHDGTYk46ca7Ind3nanJc+U6Zj9Tv+9nTCWRBscWEW1DyKOpTw== + dependencies: + tslib "^2.0.0" + +"@azure/arm-containerinstance@^7.1.0": + version "7.1.0" + resolved "https://registry.yarnpkg.com/@azure/arm-containerinstance/-/arm-containerinstance-7.1.0.tgz#261e63373999e382526d921735bd6607fb6d1f4d" + integrity sha512-FF6pJZMXC2sIeyFrA+bDAP2nZQ0XIOIXEKRiM/dG4IMuvU5j+YXBpzEQO0+PVzll8GVVWhhm22+OrZZBLvgxkA== + dependencies: + "@azure/core-auth" "^1.1.4" + "@azure/ms-rest-azure-js" "^2.1.0" + "@azure/ms-rest-js" "^2.2.0" + tslib "^1.10.0" + +"@azure/core-auth@^1.1.4": + version "1.3.2" + resolved "https://registry.yarnpkg.com/@azure/core-auth/-/core-auth-1.3.2.tgz#6a2c248576c26df365f6c7881ca04b7f6d08e3d0" + integrity sha512-7CU6DmCHIZp5ZPiZ9r3J17lTKMmYsm/zGvNkjArQwPkrLlZ1TZ+EUYfGgh2X31OLMVAQCTJZW4cXHJi02EbJnA== + dependencies: + "@azure/abort-controller" "^1.0.0" + tslib "^2.2.0" + +"@azure/ms-rest-azure-js@^2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@azure/ms-rest-azure-js/-/ms-rest-azure-js-2.1.0.tgz#8c90b31468aeca3146b06c7144b386fd4827f64c" + integrity sha512-CjZjB8apvXl5h97Ck6SbeeCmU0sk56YPozPtTyGudPp1RGoHXNjFNtoOvwOG76EdpmMpxbK10DqcygI16Lu60Q== + dependencies: + "@azure/core-auth" "^1.1.4" + "@azure/ms-rest-js" "^2.2.0" + tslib "^1.10.0" + +"@azure/ms-rest-js@^2.2.0": + version "2.6.0" + resolved "https://registry.yarnpkg.com/@azure/ms-rest-js/-/ms-rest-js-2.6.0.tgz#ec06e6a0b704567ea9b2c8044821cf47148c586b" + integrity sha512-4C5FCtvEzWudblB+h92/TYYPiq7tuElX8icVYToxOdggnYqeec4Se14mjse5miInKtZahiFHdl8lZA/jziEc5g== + dependencies: + "@azure/core-auth" "^1.1.4" + abort-controller "^3.0.0" + form-data "^2.5.0" + node-fetch "^2.6.0" + tough-cookie "^3.0.1" + tslib "^1.10.0" + tunnel "0.0.6" + uuid "^8.3.2" + xml2js "^0.4.19" + "@babel/code-frame@7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.10.4.tgz#168da1a36e90da68ae8d49c0f1b48c7c6249213a" @@ -2583,6 +2632,17 @@ resolved "https://registry.yarnpkg.com/@patternfly/react-icons/-/react-icons-4.26.4.tgz#3cd6c2d8405027e25b847e24d8a000d4af88537a" integrity sha512-h1NJixtNVK+nYxLON3HOO0xguA9qXQ5rkGUo4TZJ/kRehFCJMH8PPeebdOAKgAo38NjS2/06+atNKjDRl6zYcA== +"@patternfly/react-log-viewer@^4.20.4": + version "4.20.4" + resolved "https://registry.yarnpkg.com/@patternfly/react-log-viewer/-/react-log-viewer-4.20.4.tgz#fac3e1e126c21fbc6359a511f8b82b1f1c93a406" + integrity sha512-mkSrCntx24lgh+r68DiW9HSiA0PbOHTFz5Fyx8kUcYnAq9J7ODsH7dVqdV3bzHggC9UBxIxb6DS9Ms23uVfIDQ== + dependencies: + "@patternfly/react-core" "^4.175.4" + "@patternfly/react-icons" "^4.26.4" + "@patternfly/react-styles" "^4.25.4" + memoize-one "^5.1.0" + resize-observer-polyfill "^1.5.1" + "@patternfly/react-styles@^4.25.4": version "4.25.4" resolved "https://registry.yarnpkg.com/@patternfly/react-styles/-/react-styles-4.25.4.tgz#e49c9b290d1e48a12e9d62f3e57079fccf758392" @@ -3581,6 +3641,13 @@ abbrev@1: resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== +abort-controller@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" + integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg== + dependencies: + event-target-shim "^5.0.0" + accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.7: version "1.3.7" resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd" @@ -6829,6 +6896,11 @@ etag@~1.8.1: resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= +event-target-shim@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" + integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== + eventemitter3@^4.0.0, eventemitter3@^4.0.4: version "4.0.7" resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" @@ -10093,6 +10165,11 @@ media-typer@0.3.0: resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= +memoize-one@^5.1.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e" + integrity sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q== + memory-fs@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.4.1.tgz#3a9a20b8462523e447cfbc7e8bb80ed667bfc552" @@ -10823,7 +10900,7 @@ no-case@^3.0.4: lower-case "^2.0.2" tslib "^2.0.3" -node-fetch@^2.6.1: +node-fetch@^2.6.0, node-fetch@^2.6.1: version "2.6.6" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.6.tgz#1751a7c01834e8e1697758732e9efb6eeadfaf89" integrity sha512-Z8/6vRlTUChSdIgMa51jxQ4lrw/Jy5SOW10ObaA47/RElsAN2c5Pn8bTgFGWn/ibwzXTE8qwr1Yzx28vsecXEA== @@ -13759,7 +13836,7 @@ sass-loader@^10.0.5: schema-utils "^3.0.0" semver "^7.3.2" -sax@~1.2.4: +sax@>=0.6.0, sax@~1.2.4: version "1.2.4" resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== @@ -14931,6 +15008,15 @@ totalist@^2.0.0: resolved "https://registry.yarnpkg.com/totalist/-/totalist-2.0.0.tgz#db6f1e19c0fa63e71339bbb8fba89653c18c7eec" integrity sha512-+Y17F0YzxfACxTyjfhnJQEe7afPA0GSpYlFkl2VFMxYP7jshQf9gXV7cH47EfToBumFThfKBvfAcoUn6fdNeRQ== +tough-cookie@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-3.0.1.tgz#9df4f57e739c26930a018184887f4adb7dca73b2" + integrity sha512-yQyJ0u4pZsv9D4clxO69OEjLWYw+jbgspjTue4lTQZLfV0c5l1VmK2y1JK8E9ahdpltPOaAThPcp5nKPUgSnsg== + dependencies: + ip-regex "^2.1.0" + psl "^1.1.28" + punycode "^2.1.1" + tough-cookie@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-4.0.0.tgz#d822234eeca882f991f0f908824ad2622ddbece4" @@ -14990,12 +15076,12 @@ tsconfig-paths@^3.11.0: minimist "^1.2.0" strip-bom "^3.0.0" -tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3: +tslib@^1.10.0, tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3: version "1.14.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== -tslib@^2.0.0, tslib@^2.0.1, tslib@^2.0.3: +tslib@^2.0.0, tslib@^2.0.1, tslib@^2.0.3, tslib@^2.2.0: version "2.3.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01" integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw== @@ -15019,6 +15105,11 @@ tunnel-agent@^0.6.0: dependencies: safe-buffer "^5.0.1" +tunnel@0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/tunnel/-/tunnel-0.0.6.tgz#72f1314b34a5b192db012324df2cc587ca47f92c" + integrity sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg== + tweetnacl@^0.14.3, tweetnacl@~0.14.0: version "0.14.5" resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" @@ -15435,7 +15526,7 @@ uuid@^3.3.2, uuid@^3.4.0: resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== -uuid@^8.3.0: +uuid@^8.3.0, uuid@^8.3.2: version "8.3.2" resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== @@ -16277,6 +16368,19 @@ xml-name-validator@^3.0.0: resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a" integrity sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw== +xml2js@^0.4.19: + version "0.4.23" + resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.23.tgz#a0c69516752421eb2ac758ee4d4ccf58843eac66" + integrity sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug== + dependencies: + sax ">=0.6.0" + xmlbuilder "~11.0.0" + +xmlbuilder@~11.0.0: + version "11.0.1" + resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-11.0.1.tgz#be9bae1c8a046e76b31127726347d0ad7002beb3" + integrity sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA== + xmlchars@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb"