Skip to content

Commit

Permalink
initial working
Browse files Browse the repository at this point in the history
  • Loading branch information
tfenster committed Aug 14, 2022
1 parent 748ae73 commit c60e61b
Show file tree
Hide file tree
Showing 10 changed files with 269 additions and 147 deletions.
15 changes: 1 addition & 14 deletions Dockerfile
@@ -1,15 +1,3 @@
FROM golang:1.17-alpine AS builder
ENV CGO_ENABLED=0
WORKDIR /backend
COPY vm/go.* .
RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
go mod download
COPY vm/. .
RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
go build -trimpath -ldflags="-s -w" -o bin/service

FROM --platform=$BUILDPLATFORM node:17.7-alpine3.14 AS client-builder
WORKDIR /ui
# cache packages in layer
Expand All @@ -33,9 +21,8 @@ LABEL org.opencontainers.image.title="image-size-extension" \
com.docker.extension.additional-urls="" \
com.docker.extension.changelog=""

COPY --from=builder /backend/bin/service /
COPY docker-compose.yaml .
COPY metadata.json .
COPY docker.svg .
COPY tfe.svg .
COPY --from=client-builder /ui/build ui
CMD /service -socket /run/guest-services/extension-image-size-extension.sock
4 changes: 2 additions & 2 deletions metadata.json
@@ -1,5 +1,5 @@
{
"icon": "docker.svg",
"icon": "tfe.svg",
"vm": {
"composefile": "docker-compose.yaml",
"exposes": {
Expand All @@ -8,7 +8,7 @@
},
"ui": {
"dashboard-tab": {
"title": "Image-Size-Extension",
"title": "Image size",
"src": "index.html",
"root": "ui",
"backend": {
Expand Down
Binary file added tfe.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
25 changes: 25 additions & 0 deletions tfe.svg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
89 changes: 89 additions & 0 deletions ui/package-lock.json

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

2 changes: 2 additions & 0 deletions ui/package.json
Expand Up @@ -7,6 +7,8 @@
"@docker/extension-api-client": "^0.2.3",
"@emotion/react": "^11.9.0",
"@emotion/styled": "^11.8.1",
"@mui/icons-material": "^5.8.4",
"@mui/lab": "^5.0.0-alpha.94",
"@mui/material": "^5.6.1",
"cra-template": "1.1.3",
"react": "^17.0.2",
Expand Down
171 changes: 150 additions & 21 deletions ui/src/App.tsx
Expand Up @@ -2,6 +2,10 @@ import React from 'react';
import Button from '@mui/material/Button';
import { createDockerDesktopClient } from '@docker/extension-api-client';
import { Stack, TextField, Typography } from '@mui/material';
import TreeView from '@mui/lab/TreeView';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import ChevronRightIcon from '@mui/icons-material/ChevronRight';
import TreeItem from '@mui/lab/TreeItem';

// Note: This line relies on Docker Desktop's presence as a host application.
// If you're running this React app in a browser, it won't work properly.
Expand All @@ -11,42 +15,167 @@ function useDockerDesktopClient() {
return client;
}

function formatBytes(bytes, decimals = 2) {
// from https://stackoverflow.com/questions/15900485/correct-way-to-convert-size-in-bytes-to-kb-mb-gb-in-javascript
if (bytes === 0) return '0 Bytes';

const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];

const i = Math.floor(Math.log(bytes) / Math.log(k));

return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
}

interface Manifest {
Ref: string;
Descriptor: Descriptor;
SchemaV2Manifest: SchemaManifest;
}

interface Descriptor {
mediaType: string;
digest: string;
size: number;
platform: Platform;
}

interface Platform {
architecture: string;
os: string;
osversion: string;
}

interface Layer {
mediaType: string;
size: number;
digest: string;
urls: string[];
}

interface Config {
mediaType: string;
size: number;
digest: string;
}

interface SchemaManifest {
schemaVersion: number;
mediaType: string;
config: Config;
layers: Layer[];
}

export function App() {
const [response, setResponse] = React.useState<string>();
const [buttonText, setButtonText] = React.useState<string>();
const [imagename, setImagename] = React.useState<string>();
const [manifests, setManifests] = React.useState<Manifest[]>();
const ddClient = useDockerDesktopClient();

const fetchAndDisplayResponse = async () => {
const result = await ddClient.extension.vm?.service?.get('/hello');
setResponse(JSON.stringify(result));
setButtonText("Loading ...")
try {
const manifestInfo = await ddClient.docker.cli.exec("manifest", [
"inspect",
imagename,
"-v"
]);

const parsedManifestInfo: Manifest | Manifest[] = manifestInfo.parseJsonObject();
var localManifests: Manifest[];
if (!Array.isArray(parsedManifestInfo)) {
localManifests = [parsedManifestInfo];
} else {
localManifests = parsedManifestInfo;
}
setManifests(localManifests);
} catch (e) {
setManifests(undefined);
console.log(e.stderr);
ddClient.desktopUI.toast.error(e.stderr);
}
setButtonText(undefined);
};

function RenderResultTree() {
if (manifests !== undefined) {
return (
<TreeView
aria-label="image size view"
defaultCollapseIcon={<ExpandMoreIcon />}
defaultExpandIcon={<ChevronRightIcon />}
sx={{ flexGrow: 1, overflowY: 'auto' }}
disableSelection={true}
>
{
manifests.map((manifest: Manifest, index: number) => {
var localRef = manifest.Ref;
if (localRef.indexOf("@") > 0)
localRef = localRef.substring(0, localRef.indexOf("@"));

localRef += ` (${manifest.Descriptor.platform.os} - ${manifest.Descriptor.platform.architecture}`;
if (manifest.Descriptor.platform["os.version"] !== undefined)
localRef += ` - ${manifest.Descriptor.platform["os.version"]}`;
localRef += ")";

var configSize = manifest.SchemaV2Manifest.config.size;
var totalLayerSize = configSize;
manifest.SchemaV2Manifest.layers.forEach((layer: Layer) => {
totalLayerSize += layer.size;
});
var totalSize = configSize + totalLayerSize;

return (
<TreeItem nodeId={`${index}`} key={`${index}`} label={localRef}>
<TreeItem nodeId={`${index}-total`} key={`${index}-total`} label={`Total size: ${formatBytes(totalSize)}`} />
<TreeItem nodeId={`${index}-config`} key={`${index}-config`} label={`Config size: ${formatBytes(configSize)}`} />
<TreeItem nodeId={`${index}-layers`} key={`${index}-layers`} label={`Layers size: ${formatBytes(totalLayerSize)}`} >
{
manifest.SchemaV2Manifest.layers.map((layer: Layer, indexLayer: number) => {
return (
<TreeItem nodeId={`${index}-${indexLayer}-layer`} key={`${index}-${indexLayer}-layer`} label={`Layer size: ${formatBytes(layer.size)} ${layer.urls !== undefined ? " - external" : ""}`} />
);
})
}
</TreeItem>
</TreeItem>
);
})
}
</TreeView>
);
}
}

const handleImagenameChange = event => {
setImagename(event.target.value);
};

return (
<>
<Typography variant="h3">Docker extension demo</Typography>
<Typography variant="h3">Docker image size</Typography>
<Typography variant="body1" color="text.secondary" sx={{ mt: 2 }}>
This is a basic page rendered with MUI, using Docker's theme. Read the
MUI documentation to learn more. Using MUI in a conventional way and
avoiding custom styling will help make sure your extension continues to
look great as Docker's theme evolves.
This extension allows you to query any publicly available Docker image for its compressed size.
</Typography>
<Typography variant="body1" color="text.secondary" sx={{ mt: 2 }}>
Pressing the below button will trigger a request to the backend. Its
response will appear in the textarea.
Entering the name (and tag) of a Docker image in the entry field below and then pressing the button will retrieve and calculate the size.
</Typography>
<Stack direction="row" alignItems="start" spacing={2} sx={{ mt: 4 }}>
<Button variant="contained" onClick={fetchAndDisplayResponse}>
Call backend
</Button>

<Stack direction="column" spacing={2}>
<Stack direction="row" alignItems="center" spacing={2} sx={{ mt: 4 }}>
<TextField
label="Backend response"
sx={{ width: 480 }}
disabled
multiline
label="Image name"
sx={{ width: 480 }}
variant="outlined"
minRows={5}
value={response ?? ''}
onChange={handleImagenameChange}
value={imagename ?? ''}
/>

<Button variant="contained" onClick={fetchAndDisplayResponse} disabled={buttonText !== undefined || imagename === undefined}>
{buttonText === undefined ? "Get image size" : buttonText}
</Button>
</Stack>
{RenderResultTree()}
</Stack>
</>
);
Expand Down

0 comments on commit c60e61b

Please sign in to comment.