Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve overview page for Pods #85

Merged
merged 1 commit into from
Jul 17, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ NOTE: As semantic versioning states all 0.y.z releases can contain breaking chan
### Changed

- [#82](https://github.com/kobsio/kobs/pull/82): Improve error handling for our API.
- [#85](https://github.com/kobsio/kobs/pull/85): Improve overview page for Pods, by displaying all Containers in an expandable table and by including the current resource usage of all Containers.

## [v0.4.0](https://github.com/kobsio/kobs/releases/tag/v0.4.0) (2021-07-14)

Expand Down
15 changes: 13 additions & 2 deletions pkg/api/clusters/cluster/cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,19 @@ func (c *Cluster) GetNamespaces(ctx context.Context, cacheDuration time.Duration
}

// GetResources returns a list for the given resource in the given namespace. The resource is identified by the
// Kubernetes API path and the name of the resource.
func (c *Cluster) GetResources(ctx context.Context, namespace, path, resource, paramName, param string) ([]byte, error) {
// Kubernetes API path and the resource. The name is optional and can be used to get a single resource, instead of a
// list of resources.
func (c *Cluster) GetResources(ctx context.Context, namespace, name, path, resource, paramName, param string) ([]byte, error) {
if name != "" {
res, err := c.clientset.RESTClient().Get().AbsPath(path).Namespace(namespace).Resource(resource).Name(name).DoRaw(ctx)
if err != nil {
log.WithError(err).WithFields(logrus.Fields{"cluster": c.name, "namespace": namespace, "name": name, "path": path, "resource": resource}).Errorf("GetResources")
return nil, err
}

return res, nil
}

res, err := c.clientset.RESTClient().Get().AbsPath(path).Namespace(namespace).Resource(resource).Param(paramName, param).DoRaw(ctx)
if err != nil {
log.WithError(err).WithFields(logrus.Fields{"cluster": c.name, "namespace": namespace, "path": path, "resource": resource}).Errorf("GetResources")
Expand Down
7 changes: 4 additions & 3 deletions plugins/resources/resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,12 +61,13 @@ func (router *Router) isForbidden(resource string) bool {
func (router *Router) getResources(w http.ResponseWriter, r *http.Request) {
clusterNames := r.URL.Query()["cluster"]
namespaces := r.URL.Query()["namespace"]
name := r.URL.Query().Get("name")
resource := r.URL.Query().Get("resource")
path := r.URL.Query().Get("path")
paramName := r.URL.Query().Get("paramName")
param := r.URL.Query().Get("param")

log.WithFields(logrus.Fields{"clusters": clusterNames, "namespaces": namespaces, "resource": resource, "path": path, "paramName": paramName, "param": param}).Tracef("getResources")
log.WithFields(logrus.Fields{"clusters": clusterNames, "namespaces": namespaces, "name": name, "resource": resource, "path": path, "paramName": paramName, "param": param}).Tracef("getResources")

var resources []Resources

Expand All @@ -88,7 +89,7 @@ func (router *Router) getResources(w http.ResponseWriter, r *http.Request) {
// provided we loop through all the namespaces and return the resources for these namespaces. All results are
// added to the resources slice, which is then returned by the api.
if namespaces == nil {
list, err := cluster.GetResources(r.Context(), "", path, resource, paramName, param)
list, err := cluster.GetResources(r.Context(), "", name, path, resource, paramName, param)
if err != nil {
errresponse.Render(w, r, err, http.StatusBadRequest, "Could not get resources")
return
Expand All @@ -108,7 +109,7 @@ func (router *Router) getResources(w http.ResponseWriter, r *http.Request) {
})
} else {
for _, namespace := range namespaces {
list, err := cluster.GetResources(r.Context(), namespace, path, resource, paramName, param)
list, err := cluster.GetResources(r.Context(), namespace, name, path, resource, paramName, param)
if err != nil {
errresponse.Render(w, r, err, http.StatusBadRequest, "Could not get resources")
return
Expand Down
9 changes: 8 additions & 1 deletion plugins/resources/src/components/panel/details/Overview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,14 @@ const Overview: React.FunctionComponent<IOverviewProps> = ({ resource }: IOvervi
// Overwrite the additions for several resources.
if (resource.props && resource.props.apiVersion && resource.props.kind) {
if (resource.props.apiVersion === 'v1' && resource.props.kind === 'Pod') {
additions = <Pod pod={resource.props} />;
additions = (
<Pod
cluster={resource.cluster?.title}
namespace={resource.namespace?.title}
name={resource.name?.title}
pod={resource.props}
/>
);
} else if (resource.props.apiVersion === 'apps/v1' && resource.props.kind === 'Deployment') {
additions = (
<Deployment
Expand Down
214 changes: 128 additions & 86 deletions plugins/resources/src/components/panel/details/overview/Container.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
import { DescriptionListDescription, DescriptionListGroup, DescriptionListTerm, Title } from '@patternfly/react-core';
import {
DescriptionList,
DescriptionListDescription,
DescriptionListGroup,
DescriptionListTerm,
} from '@patternfly/react-core';
import { ExpandableRowContent, Td, Tr } from '@patternfly/react-table';
import React, { useState } from 'react';
import { V1Container, V1ContainerState, V1ContainerStatus, V1EnvVarSource, V1Probe } from '@kubernetes/client-node';
import React from 'react';

import { IMetricContainer } from '../../../../utils/interfaces';
import { formatResourceValue } from '../../../../utils/helpers';

const getContainerStatus = (state: V1ContainerState): string => {
if (state.running) {
Expand Down Expand Up @@ -30,7 +39,7 @@ const getValueFrom = (valueFrom: V1EnvVarSource): string => {
return '-';
};

const getPrope = (title: string, probe: V1Probe): JSX.Element => {
const getProbe = (title: string, probe: V1Probe): JSX.Element => {
return (
<DescriptionListGroup>
<DescriptionListTerm>{title}</DescriptionListTerm>
Expand Down Expand Up @@ -94,94 +103,127 @@ const getPrope = (title: string, probe: V1Probe): JSX.Element => {
interface IContainerProps {
container: V1Container;
containerStatus?: V1ContainerStatus;
containerMetric?: IMetricContainer;
}

const Container: React.FunctionComponent<IContainerProps> = ({ container, containerStatus }: IContainerProps) => {
const Container: React.FunctionComponent<IContainerProps> = ({
container,
containerStatus,
containerMetric,
}: IContainerProps) => {
const [isExpanded, setIsExpaned] = useState<boolean>(false);

return (
<React.Fragment>
<Title headingLevel="h4" size="lg">
{container.name}
</Title>
<DescriptionListGroup>
<DescriptionListTerm>Status</DescriptionListTerm>
<DescriptionListDescription>
<Tr onClick={(): void => setIsExpaned(!isExpanded)}>
<Td dataLabel="Name">{container.name}</Td>
<Td dataLabel="Ready">{containerStatus && containerStatus.ready ? 'True' : 'False'}</Td>
<Td dataLabel="Restarts">{containerStatus ? containerStatus.restartCount : 0}</Td>
<Td dataLabel="Status">
{containerStatus && containerStatus.state ? getContainerStatus(containerStatus.state) : '-'}
</DescriptionListDescription>
</DescriptionListGroup>
<DescriptionListGroup>
<DescriptionListTerm>Ready</DescriptionListTerm>
<DescriptionListDescription>
{containerStatus && containerStatus.ready ? 'True' : 'False'}
</DescriptionListDescription>
</DescriptionListGroup>
<DescriptionListGroup>
<DescriptionListTerm>Image</DescriptionListTerm>
<DescriptionListDescription>{container.image}</DescriptionListDescription>
</DescriptionListGroup>
{container.command && (
<DescriptionListGroup>
<DescriptionListTerm>Command</DescriptionListTerm>
<DescriptionListDescription>{container.command}</DescriptionListDescription>
</DescriptionListGroup>
)}
{container.command && (
<DescriptionListGroup>
<DescriptionListTerm>Command</DescriptionListTerm>
<DescriptionListDescription>{container.command.join(' ')}</DescriptionListDescription>
</DescriptionListGroup>
)}
{container.args && (
<DescriptionListGroup>
<DescriptionListTerm>Command</DescriptionListTerm>
<DescriptionListDescription>{container.args.join(' ')}</DescriptionListDescription>
</DescriptionListGroup>
)}
{container.ports && (
<DescriptionListGroup>
<DescriptionListTerm>Ports</DescriptionListTerm>
<DescriptionListDescription>
{container.ports.map((port, index) => (
<div key={index} className="pf-c-chip pf-u-mr-md pf-u-mb-sm" style={{ maxWidth: '100%' }}>
<span className="pf-c-chip__text" style={{ maxWidth: '100%' }}>
{port.containerPort}
{port.protocol ? `/${port.protocol}` : ''}
{port.name ? ` (${port.name})` : ''}
</span>
</div>
))}
</DescriptionListDescription>
</DescriptionListGroup>
)}
{container.env && (
<DescriptionListGroup>
<DescriptionListTerm>Environment</DescriptionListTerm>
<DescriptionListDescription>
{container.env.map((env, index) => (
<div key={index}>
{env.name}:
<span className="pf-u-pl-sm pf-u-font-size-sm pf-u-color-400">
{env.value ? env.value : env.valueFrom ? getValueFrom(env.valueFrom) : '-'}
</span>
</div>
))}
</DescriptionListDescription>
</DescriptionListGroup>
)}
{container.volumeMounts && (
<DescriptionListGroup>
<DescriptionListTerm>Mounts</DescriptionListTerm>
<DescriptionListDescription>
{container.volumeMounts.map((mount, index) => (
<div key={index}>
{mount.name}:<span className="pf-u-pl-sm pf-u-font-size-sm pf-u-color-400">{mount.mountPath}</span>
</div>
))}
</DescriptionListDescription>
</DescriptionListGroup>
)}
{container.livenessProbe && getPrope('Liveness Probe', container.livenessProbe)}
{container.readinessProbe && getPrope('Readiness Probe', container.readinessProbe)}
{container.startupProbe && getPrope('Startup Probe', container.startupProbe)}
</Td>
<Td dataLabel="CPU Usage">
{containerMetric && containerMetric.usage && containerMetric.usage.cpu
? formatResourceValue('cpu', containerMetric.usage.cpu)
: '-'}
</Td>
<Td dataLabel="CPU Requests">
{container.resources && container.resources.requests
? formatResourceValue('cpu', container.resources.requests['cpu'])
: '-'}
</Td>
<Td dataLabel="CPU Limits">
{container.resources && container.resources.limits
? formatResourceValue('cpu', container.resources.limits['cpu'])
: '-'}
</Td>
<Td dataLabel="Memory Usage">
{containerMetric && containerMetric.usage && containerMetric.usage.memory
? formatResourceValue('memory', containerMetric.usage.memory)
: '-'}
</Td>
<Td dataLabel="Memory Requests">
{container.resources && container.resources.requests
? formatResourceValue('memory', container.resources.requests['memory'])
: '-'}
</Td>
<Td dataLabel="Memory Limits">
{container.resources && container.resources.limits
? formatResourceValue('memory', container.resources.limits['memory'])
: '-'}
</Td>
</Tr>
<Tr isExpanded={isExpanded}>
<Td colSpan={10}>
<ExpandableRowContent>
<DescriptionList className="pf-u-text-break-word" isHorizontal={true}>
<DescriptionListGroup>
<DescriptionListTerm>Image</DescriptionListTerm>
<DescriptionListDescription>{container.image}</DescriptionListDescription>
</DescriptionListGroup>
{container.command && (
<DescriptionListGroup>
<DescriptionListTerm>Command</DescriptionListTerm>
<DescriptionListDescription>{container.command.join(' ')}</DescriptionListDescription>
</DescriptionListGroup>
)}
{container.args && (
<DescriptionListGroup>
<DescriptionListTerm>Command</DescriptionListTerm>
<DescriptionListDescription>{container.args.join(' ')}</DescriptionListDescription>
</DescriptionListGroup>
)}
{container.ports && (
<DescriptionListGroup>
<DescriptionListTerm>Ports</DescriptionListTerm>
<DescriptionListDescription>
{container.ports.map((port, index) => (
<div key={index} className="pf-c-chip pf-u-mr-md pf-u-mb-sm" style={{ maxWidth: '100%' }}>
<span className="pf-c-chip__text" style={{ maxWidth: '100%' }}>
{port.containerPort}
{port.protocol ? `/${port.protocol}` : ''}
{port.name ? ` (${port.name})` : ''}
</span>
</div>
))}
</DescriptionListDescription>
</DescriptionListGroup>
)}
{container.env && (
<DescriptionListGroup>
<DescriptionListTerm>Environment</DescriptionListTerm>
<DescriptionListDescription>
{container.env.map((env, index) => (
<div key={index}>
{env.name}:
<span className="pf-u-pl-sm pf-u-font-size-sm pf-u-color-400">
{env.value ? env.value : env.valueFrom ? getValueFrom(env.valueFrom) : '-'}
</span>
</div>
))}
</DescriptionListDescription>
</DescriptionListGroup>
)}
{container.volumeMounts && (
<DescriptionListGroup>
<DescriptionListTerm>Mounts</DescriptionListTerm>
<DescriptionListDescription>
{container.volumeMounts.map((mount, index) => (
<div key={index}>
{mount.name}:
<span className="pf-u-pl-sm pf-u-font-size-sm pf-u-color-400">{mount.mountPath}</span>
</div>
))}
</DescriptionListDescription>
</DescriptionListGroup>
)}
{container.livenessProbe && getProbe('Liveness Probe', container.livenessProbe)}
{container.readinessProbe && getProbe('Readiness Probe', container.readinessProbe)}
{container.startupProbe && getProbe('Startup Probe', container.startupProbe)}
</DescriptionList>
</ExpandableRowContent>
</Td>
</Tr>
</React.Fragment>
);
};
Expand Down
Loading