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

feat: Initial implementation of copa extension #1

Merged
merged 14 commits into from
Jun 26, 2024
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
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