Skip to content

Commit

Permalink
Merge pull request #1 from jgrer/new-features
Browse files Browse the repository at this point in the history
feat: Initial implementation of copa extension
  • Loading branch information
ashnamehrotra authored Jun 26, 2024
2 parents a9b40a1 + f762c23 commit d97bb89
Show file tree
Hide file tree
Showing 8 changed files with 249 additions and 605 deletions.
4 changes: 3 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ COPY ui/package-lock.json /ui/package-lock.json
RUN --mount=type=cache,target=/usr/src/app/.npm \
npm set cache /usr/src/app/.npm && \
npm ci
# install
# install mui icons
RUN npm install @mui/icons-material

COPY ui /ui
RUN npm run build

Expand Down
2 changes: 2 additions & 0 deletions container/entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ case "$connection_format" in
;;
esac


# run trivy to generate scan for image
trivy image --vuln-type os --ignore-unfixed -f json -o scan.json $image

# run copa to patch image
Expand Down
486 changes: 8 additions & 478 deletions copa-color.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 7 additions & 7 deletions package-lock.json

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

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"dependencies": {
"@mui/icons-material": "^5.15.19"
"@mui/icons-material": "^5.15.20"
}
}
192 changes: 119 additions & 73 deletions ui/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,29 +13,43 @@ import {
DialogTitle,
DialogContent,
DialogContentText,
DialogActions
DialogActions,
IconButton,
Grow,
Collapse
} from '@mui/material';
import { createDockerDesktopClient } from '@docker/extension-api-client';
import { CopaInput } from './copainput';
import { CommandLine } from './commandline';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import ChevronRightIcon from '@mui/icons-material/ChevronRight';

export function App() {
const ddClient = createDockerDesktopClient();
const learnMoreLink = "https://project-copacetic.github.io/copacetic/website/";

const [selectedImage, setSelectedImage] = React.useState<string | null>(null);
const [selectedScanner, setSelectedScanner] = React.useState<string | undefined>(undefined);
const [selectedImageTag, setSelectedImageTag] = React.useState<string | undefined>(undefined);
const [selectedTimeout, setSelectedTimeout] = React.useState<string | undefined>(undefined);
const [totalStdout, setTotalStdout] = React.useState("");
// The correct image name of the currently selected image. The latest tag is added if there is no tag.
const [imageName, setImageName] = useState("");


const [inSettings, setInSettings] = React.useState(false);
const [showPreload, setShowPreload] = React.useState(true);
const [showLoading, setShowLoading] = React.useState(false);
const [showSuccess, setShowSuccess] = React.useState(false);
const [showFailure, setShowFailure] = React.useState(false);
const [showCopaOutputModal, setShowCopaOutputModal] = React.useState(false);
const [selectedImage, setSelectedImage] = useState<string | null>(null);
const [selectedScanner, setSelectedScanner] = useState<string | undefined>("trivy");
const [selectedImageTag, setSelectedImageTag] = useState<string | undefined>(undefined);
const [selectedTimeout, setSelectedTimeout] = useState<string | undefined>(undefined);
const [totalOutput, setTotalOutput] = useState("");
const [actualImageTag, setActualImageTag] = useState("");
const [errorText, setErrorText] = useState("");
const [useContainerdChecked, setUseContainerdChecked] = React.useState(false);


const [inSettings, setInSettings] = useState(false);
const [showPreload, setShowPreload] = useState(true);
const [showLoading, setShowLoading] = useState(false);
const [showSuccess, setShowSuccess] = useState(false);
const [showFailure, setShowFailure] = useState(false);
const [showCopaOutputModal, setShowCopaOutputModal] = useState(false);
const [showCommandLine, setShowCommandLine] = useState(false);


const patchImage = () => {
setShowPreload(false);
Expand All @@ -48,55 +62,86 @@ export function App() {
setSelectedScanner(undefined);
setSelectedImageTag(undefined);
setSelectedTimeout(undefined);
setTotalOutput("");
setImageName("");
setActualImageTag("");
}

const processError = (error: string) => {
if (error.indexOf("unknown tag") >= 0) {
setErrorText("Unknown image tag.")
} else if (error.indexOf("No such image") >= 0) {
setErrorText("Image does not exist.");
} else {
setErrorText("An unexpected error occurred.");
}
}

async function triggerCopa() {
let stdout = "";
let stderr = "";


let imageTag = "";
// Create the correct tag for the image
if (selectedImage !== null) {
let imageSplit = selectedImage.split(':');
if (selectedImageTag !== undefined) {
imageTag = selectedImageTag;
} else if (imageSplit?.length === 1) {
imageTag = `latest-patched`;
} else {
imageTag = `${imageSplit[1]}-patched`;
}
}
setActualImageTag(imageTag);

if (selectedImage != null) {
let commandParts: string[] = [
"--mount",
"type=bind,source=/var/run/docker.sock,target=/var/run/docker.sock",
// "--name=copa-extension",
"copa-extension",
`${selectedImage}`,
`${selectedImageTag === undefined ? `${selectedImage.split(':')[1]}-patched` : selectedImageTag}`,
`${imageTag}`,
`${selectedTimeout === undefined ? "5m" : selectedTimeout}`,
"buildx",
`${useContainerdChecked ? 'custom-socket' : 'buildx'}`,
"openvex"
];
({ stdout, stderr } = await runCopa(commandParts, stdout, stderr));
}
}

async function runCopa(commandParts: string[], stdout: string, stderr: string) {
let latestStdout: string = "";
let latestStderr: string = "";
let tOutput = "";
await ddClient.docker.cli.exec(
"run", commandParts,
{
stream: {
onOutput(data: any) {
stdout += (data.stdout + "\n");

if (data.stdout) {
stdout += data.stdout;
tOutput += data.stdout;

}
if (data.stderr) {
stderr += data.stderr;
latestStdout = data.stderr;
tOutput += data.stderr;
latestStderr = data.stderr;
}
},
onError(error: any) {
setTotalStdout(stdout);
console.error(error);
setTotalOutput(tOutput);
},
onClose(exitCode: number) {
setShowLoading(false);
setTotalStdout(stdout);
var res = { stdout: stdout, stderr: stderr };
if (exitCode == 0) {
processResult(res);
setShowSuccess(true);
ddClient.desktopUI.toast.success(`Copacetic - Created new patched image ${selectedImage}-patched`);
ddClient.desktopUI.toast.success(`Copacetic - Created new patched image ${selectedImage}-${actualImageTag}`);
} else {
setShowFailure(true);
ddClient.desktopUI.toast.error(`Copacetic - Failed to patch ${selectedImage}: ${latestStdout}`);
ddClient.desktopUI.toast.error(`Copacetic - Failed to patch image ${imageName}`);
processError(latestStderr);
}
},
},
Expand All @@ -105,19 +150,27 @@ export function App() {
return { stdout, stderr };
}

const processResult = (res: object) => {

}
const showCommandLineButton = (
<IconButton aria-label="show-command-line" onClick={() => { setShowCommandLine(!showCommandLine) }}>
{showCommandLine ? <ExpandMoreIcon /> : <ChevronRightIcon />}
</IconButton>
)

const loadingPage = (
<Stack direction="row" alignContent="center" alignItems="center">
<Box
width={80}
>
</Box>
<Stack>
<Stack sx={{ alignItems: 'center' }}>
<CircularProgress size={100} />
<Typography align='center' variant="h6" sx={{ maxWidth: 400 }}>Patching Image...</Typography>
<Stack direction="row">
{showCommandLineButton}
<Typography variant="h6" sx={{ maxWidth: 400 }}>Patching Image...</Typography>
</Stack>
<Collapse in={showCommandLine}>
<CommandLine totalOutput={totalOutput}></CommandLine>
</Collapse>
</Stack>
</Stack>
)
Expand All @@ -129,18 +182,24 @@ export function App() {
alt="celebration icon"
src="celebration-icon.png"
/>
<Box>
<Stack sx={{ alignItems: 'center' }}>
<Typography align='center' variant="h6">Successfully patched image</Typography>
<Typography align='center' variant="h6">{selectedImage}!</Typography>
</Box>
<Stack direction="row" spacing={2}>
<Button onClick={() => {
<Stack direction="row">
{showCommandLineButton}
<Typography align='center' variant="h6">{imageName}!</Typography>
</Stack>
</Stack>
<Button
onClick={() => {
clearInput();
setShowSuccess(false);
setShowPreload(true);
}}>Return</Button>
<Button onClick={() => { setShowCopaOutputModal(true) }}>Show Copa Output</Button>
</Stack>
}}>
Return
</Button>
<Collapse in={showCommandLine}>
<CommandLine totalOutput={totalOutput}></CommandLine>
</Collapse>
</Stack>
);

Expand All @@ -151,18 +210,21 @@ export function App() {
alt="error icon"
src="error-icon.png"
/>
<Box>
<Typography align='center' variant="h6">Failed to patch {selectedImage}:</Typography>
<Typography align='center' variant="h6">error here</Typography>
</Box>
<Stack direction="row" spacing={2}>
<Button onClick={() => {
clearInput();
setShowFailure(false);
setShowPreload(true);
}}>Return</Button>
<Button onClick={() => { setShowCopaOutputModal(true) }}>Show Copa Output</Button>
<Stack sx={{ alignItems: 'center' }} >
<Typography align='center' variant="h6">Failed to patch {imageName}</Typography>
<Stack direction="row">
{showCommandLineButton}
<Typography align='center' variant="h6">{errorText}</Typography>
</Stack>
</Stack>
<Button onClick={() => {
clearInput();
setShowFailure(false);
setShowPreload(true);
}}>Return</Button>
<Collapse in={showCommandLine}>
<CommandLine totalOutput={totalOutput}></CommandLine>
</Collapse>
</Stack>
)

Expand All @@ -184,7 +246,9 @@ export function App() {
<Typography align='center' variant="h6">Directly patch containers quickly</Typography>
<Typography align='center' variant="h6">without going upstream for a full rebuild.</Typography>
</Stack>
<Link href="https://project-copacetic.github.io/copacetic/website/">LEARN MORE</Link>
<Link onClick={() => {
ddClient.host.openExternal(learnMoreLink)
}}>LEARN MORE</Link>
</Stack>
<Divider orientation="vertical" variant="middle" flexItem />
{showPreload &&
Expand All @@ -200,32 +264,14 @@ export function App() {
inSettings={inSettings}
setInSettings={setInSettings}
patchImage={patchImage}
useContainerdChecked={useContainerdChecked}
setUseContainerdChecked={setUseContainerdChecked}
imageName={imageName}
setImageName={setImageName}
/>}
{showLoading && loadingPage}
{showSuccess && successPage}
{showFailure && failurePage}
<Dialog
open={showCopaOutputModal}
onClose={() => { setShowCopaOutputModal(false) }}
scroll='paper'
aria-labelledby="scroll-dialog-title"
aria-describedby="scroll-dialog-description"
maxWidth='xl'
fullWidth
>
<DialogTitle id="scroll-dialog-title">Copa Output</DialogTitle>
<DialogContent dividers={true}>
<DialogContentText
id="scroll-dialog-description"
tabIndex={-1}
>
{totalStdout}
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={() => { setShowCopaOutputModal(false) }}>Back</Button>
</DialogActions>
</Dialog>
</Stack>
</Box>
);
Expand Down
Loading

0 comments on commit d97bb89

Please sign in to comment.