Add UI for Fleet Sandbox to download prepackaged installers (#6721)

This commit is contained in:
gillespi314 2022-07-19 14:28:06 -05:00 committed by GitHub
parent 1c1bec12d2
commit 4792d7a759
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 430 additions and 38 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 517 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 527 B

View file

@ -0,0 +1 @@
- Added Fleet Sandbox UI to download pre-packaged installers

View file

@ -409,7 +409,8 @@ awslocal kinesis get-records --shard-iterator AAAAAAAAAAERtiUrWGI0sq99TtpKnmDu6h
Pre-built installers are kept in a blob storage like AWS S3. As part of your your local development there's a [MinIO](https://min.io/) instance running on http://localhost:9000. To test the pre-built installers functionality locally:
1. Build the installers you want using `fleetctl package`.
1. Build the installers you want using `fleetctl package`. Be sure to include the `--insecure` flag
for local testing.
2. Use the [installerstore](../../tools/installerstore/README.md) tool to upload them to your MinIO instance.
3. Configure your fleet server setting `FLEET_PACKAGING_GLOBAL_ENROLL_SECRET` to match your global enroll secret.
4. Set `FLEET_DEMO=1`, as the endpoint to retrieve the installer is only available in the sandbox.
@ -418,4 +419,7 @@ Pre-built installers are kept in a blob storage like AWS S3. As part of your you
FLEET_DEMO=1 FLEET_PACKAGING_GLOBAL_ENROLL_SECRET=xyz ./build/fleet serve --dev
```
Be sure to replace the `FLEET_PACKAGING_GLOBAL_ENROLL_SECRET` value above with the global enroll
secret from the `fleetctl package` command used to build the installers.
MinIO also offers a web interface at http://localhost:9001. Credentials are `minio` / `minio123!`.

View file

@ -1,24 +1,52 @@
import React from "react";
import { ITeamSummary } from "interfaces/team";
import DataError from "components/DataError";
import Modal from "components/Modal";
import { ITeam } from "interfaces/team";
import { IEnrollSecret } from "interfaces/enroll_secret";
import Spinner from "components/Spinner";
import PlatformWrapper from "./PlatformWrapper/PlatformWrapper";
import DownloadInstallers from "./DownloadInstallers/DownloadInstallers";
const baseClass = "add-hosts-modal";
interface IAddHostsModal {
currentTeam?: ITeamSummary;
enrollSecret?: string;
isLoading: boolean;
isSandboxMode?: boolean;
onCancel: () => void;
selectedTeam: ITeam | { name: string; secrets: IEnrollSecret[] | null };
}
const AddHostsModal = ({
currentTeam,
enrollSecret,
isLoading,
isSandboxMode,
onCancel,
selectedTeam,
}: IAddHostsModal): JSX.Element => {
const renderModalContent = () => {
if (isLoading) {
return <Spinner />;
}
if (!enrollSecret) {
return <DataError />;
}
// TODO: Currently, prepacked installers in Fleet Sandbox use the global enroll secret,
// and Fleet Sandbox runs Fleet Free so the currentTeam check here is an
// additional precaution/reminder to revisit this in connection with future changes.
// See https://github.com/fleetdm/fleet/issues/4970#issuecomment-1187679407.
return isSandboxMode && !currentTeam ? (
<DownloadInstallers onCancel={onCancel} enrollSecret={enrollSecret} />
) : (
<PlatformWrapper onCancel={onCancel} enrollSecret={enrollSecret} />
);
};
return (
<Modal onExit={onCancel} title={"Add hosts"} className={baseClass}>
<PlatformWrapper onCancel={onCancel} selectedTeam={selectedTeam} />
{renderModalContent()}
</Modal>
);
};

View file

@ -0,0 +1,185 @@
import React, { useState } from "react";
import FileSaver from "file-saver";
import {
IInstallerPlatform,
IInstallerType,
INSTALLER_PLATFORM_BY_TYPE,
INSTALLER_TYPE_BY_PLATFORM,
} from "interfaces/installer";
import installerAPI from "services/entities/installers";
import Button from "components/buttons/Button";
import Checkbox from "components/forms/fields/Checkbox";
import DataError from "components/DataError";
import Spinner from "components/Spinner";
import TooltipWrapper from "components/TooltipWrapper";
import AppleIcon from "./../../../../assets/images/icon-apple-black-24x24@2x.png";
import AppleIconVibrant from "./../../../../assets/images/icon-apple-vibrant-blue-24x24@2x.png";
import LinuxIcon from "./../../../../assets/images/icon-linux-black-24x24@2x.png";
import LinuxIconVibrant from "./../../../../assets/images/icon-linux-vibrant-blue-24x24@2x.png";
import WindowsIcon from "./../../../../assets/images/icon-windows-black-24x24@2x.png";
import WindowsIconVibrant from "./../../../../assets/images/icon-windows-vibrant-blue-24x24@2x.png";
import SuccessIcon from "./../../../../assets/images/icon-circle-check-blue-48x48@2x.png";
interface IDownloadInstallersProps {
enrollSecret: string;
onCancel: () => void;
}
const baseClass = "download-installers";
const displayOrder = [
"macOS",
"Windows",
"Linux (RPM)",
"Linux (deb)",
] as const;
const displayIcon = (platform: IInstallerPlatform, isSelected: boolean) => {
switch (platform) {
case "Linux (RPM)":
case "Linux (deb)":
return (
<img src={isSelected ? LinuxIconVibrant : LinuxIcon} alt={platform} />
);
case "macOS":
return (
<img src={isSelected ? AppleIconVibrant : AppleIcon} alt={platform} />
);
case "Windows":
return (
<img
src={isSelected ? WindowsIconVibrant : WindowsIcon}
alt={platform}
/>
);
default:
return null;
}
};
const DownloadInstallers = ({
enrollSecret,
onCancel,
}: IDownloadInstallersProps): JSX.Element => {
const [includeDesktop, setIncludeDesktop] = useState(true);
const [isDownloadError, setIsDownloadError] = useState(false);
const [isDownloading, setIsDownloading] = useState(false);
const [isDownloadSuccess, setIsDownloadSuccess] = useState(false);
const [selectedInstaller, setSelectedInstaller] = useState<
IInstallerType | undefined
>();
const downloadInstaller = async (installerType?: IInstallerType) => {
if (!installerType) {
// do nothing
return;
}
setIsDownloading(true);
try {
const blob: BlobPart = await installerAPI.downloadInstaller({
enrollSecret,
installerType,
includeDesktop,
});
const filename = `fleet-osquery.${installerType}`;
const file = new global.window.File([blob], filename, {
type: "application/octet-stream",
});
FileSaver.saveAs(file);
setIsDownloadSuccess(true);
} catch {
setIsDownloadError(true);
} finally {
setIsDownloading(false);
}
};
const onClickSelector = (type: IInstallerType) => {
if (isDownloading) {
// do nothing
return;
}
if (type === selectedInstaller) {
setSelectedInstaller(undefined);
return;
}
setSelectedInstaller(type);
};
if (isDownloadError) {
return (
<div className={`${baseClass}__error`}>
<DataError />
</div>
);
}
if (isDownloadSuccess) {
const installerPlatform =
(selectedInstaller &&
`${INSTALLER_PLATFORM_BY_TYPE[selectedInstaller]} `) ||
"";
return (
<div className={`${baseClass}__success`}>
<img src={SuccessIcon} alt="download successful" />
<h2>You&rsquo;re almost there</h2>
<p>{`Run the installer on a ${installerPlatform}laptop, workstation, or sever to add it to Fleet.`}</p>
<Button onClick={onCancel}>Got it</Button>
</div>
);
}
return (
<div className={`${baseClass}`}>
<p>Which platform is your host running?</p>
<div className={`${baseClass}__select-installer`}>
{displayOrder.map((platform) => {
const installerType = INSTALLER_TYPE_BY_PLATFORM[platform];
const isSelected = selectedInstaller === installerType;
return (
<div
key={installerType}
className={`${baseClass}__selector ${
isSelected ? `${baseClass}__selector--selected` : ""
}`}
onClick={() => onClickSelector(installerType)}
>
<span>
{displayIcon(platform, isSelected)}
{platform}
</span>
</div>
);
})}
</div>
<Checkbox
name="include-fleet-desktop"
onChange={(value: boolean) => setIncludeDesktop(value)}
value={includeDesktop}
>
<>
Include&nbsp;
<TooltipWrapper
tipContent={
"<p>Lightweight application that allows end users to see information about their device.</p>"
}
>
Fleet Desktop
</TooltipWrapper>
</>
</Checkbox>
<Button
className={`${baseClass}__button--download`}
disabled={!selectedInstaller}
onClick={() => downloadInstaller(selectedInstaller)}
>
{isDownloading ? <Spinner /> : "Download installer"}
</Button>
</div>
);
};
export default DownloadInstallers;

View file

@ -0,0 +1,104 @@
.download-installers {
display: flex;
flex-direction: column;
padding: 0;
padding-bottom: 20px;
p {
padding-top: $pad-small;
padding-bottom: $pad-medium;
margin: 0;
}
.component__tooltip-wrapper__tip-text {
p {
padding: 0;
}
}
.form-field.form-field--checkbox {
padding-bottom: $pad-large;
margin-bottom: 0;
}
&__select-installer {
display: flex;
flex-direction: row;
flex-wrap: wrap;
align-items: flex-start;
gap: 8px;
padding-bottom: $pad-large;
}
&__selector {
cursor: pointer;
font-size: $small;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
padding: 16px;
gap: 16px;
width: 286px;
border: 1px solid #c5c7d1;
border-radius: 4px;
span {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
img {
height: 24px;
width: auto;
padding-right: 8px;
}
}
&:hover {
border-color: $core-vibrant-blue;
}
&--selected {
color: $core-vibrant-blue;
background-color: $ui-vibrant-blue-10;
border-color: $core-vibrant-blue;
}
}
&__button--download {
width: 154px;
height: 38px;
}
&__success {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 48px 0 70px;
img {
height: 48px;
width: 48px;
}
h2 {
margin: 0;
padding: 16px 0 8px;
}
p {
margin: 0;
padding-bottom: 24px;
}
}
&__error {
.data-error__inner {
margin: 0;
padding-bottom: 20px;
}
}
}

View file

@ -53,23 +53,21 @@ const platformSubNav: IPlatformSubNav[] = [
},
];
interface IPlatformWrapperProp {
selectedTeam: ITeam | { name: string; secrets: IEnrollSecret[] | null };
interface IPlatformWrapperProps {
enrollSecret: string;
onCancel: () => void;
}
const baseClass = "platform-wrapper";
const PlatformWrapper = ({
selectedTeam,
enrollSecret,
onCancel,
}: IPlatformWrapperProp): JSX.Element => {
}: IPlatformWrapperProps): JSX.Element => {
const { config, isPreviewMode } = useContext(AppContext);
const { renderFlash } = useContext(NotificationContext);
const [copyMessage, setCopyMessage] = useState<Record<string, string>>({});
const [includeFleetDesktop, setIncludeFleetDesktop] = useState<boolean>(
false
);
const [includeFleetDesktop, setIncludeFleetDesktop] = useState<boolean>(true);
const [showPlainOsquery, setShowPlainOsquery] = useState<boolean>(false);
const {
@ -127,11 +125,6 @@ const PlatformWrapper = ({
--carver_continue_endpoint=/api/v1/osquery/carve/block
--carver_block_size=2000000`;
let enrollSecret: string;
if (selectedTeam.secrets) {
enrollSecret = selectedTeam.secrets[0].secret;
}
const onDownloadEnrollSecret = (evt: React.MouseEvent) => {
evt.preventDefault();
@ -407,7 +400,7 @@ const PlatformWrapper = ({
<>
<Checkbox
name="include-fleet-desktop"
onChange={() => setIncludeFleetDesktop(!includeFleetDesktop)}
onChange={(value: boolean) => setIncludeFleetDesktop(value)}
value={includeFleetDesktop}
>
<>

View file

@ -0,0 +1,27 @@
export type IInstallerType = "pkg" | "msi" | "rpm" | "deb";
export type IInstallerPlatform =
| "Windows"
| "macOS"
| "Linux (RPM)"
| "Linux (deb)";
export const INSTALLER_TYPE_BY_PLATFORM: Record<
IInstallerPlatform,
IInstallerType
> = {
macOS: "pkg",
Windows: "msi",
"Linux (RPM)": "rpm",
"Linux (deb)": "deb",
} as const;
export const INSTALLER_PLATFORM_BY_TYPE: Record<
IInstallerType,
IInstallerPlatform
> = {
pkg: "macOS",
msi: "Windows",
rpm: "Linux (RPM)",
deb: "Linux (deb)",
} as const;

View file

@ -16,7 +16,7 @@ export default PropTypes.shape({
});
/**
* The id, name, and optional description for a team entity
* The id, name, description, and host count for a team entity
*/
export interface ITeamSummary {
id: number;

View file

@ -487,11 +487,15 @@ const TeamDetailsWrapper = ({
</TabsWrapper>
{showAddHostsModal && (
<AddHostsModal
currentTeam={currentTeam}
enrollSecret={teamSecrets?.[0]?.secret}
isLoading={isLoadingTeams}
// TODO: Currently, prepacked installers in Fleet Sandbox use the global enroll secret,
// and Fleet Sandbox runs Fleet Free so explicitly setting isSandboxMode here is an
// additional precaution/reminder to revisit this in connection with future changes.
// See https://github.com/fleetdm/fleet/issues/4970#issuecomment-1187679407.
isSandboxMode={false}
onCancel={toggleAddHostsModal}
selectedTeam={{
name: currentTeam.name,
secrets: teamSecrets || null,
}}
/>
)}
{showManageEnrollSecretsModal && (

View file

@ -314,13 +314,6 @@ const ManageHostsPage = ({
}
);
const addHostsTeam = currentTeam
? { name: currentTeam.name, secrets: teamSecrets || null }
: {
name: "No team",
secrets: globalSecrets || null,
};
const {
data: teams,
isLoading: isLoadingTeams,
@ -1187,9 +1180,25 @@ const ManageHostsPage = ({
/>
);
const renderAddHostsModal = () => (
<AddHostsModal onCancel={toggleAddHostsModal} selectedTeam={addHostsTeam} />
);
const renderAddHostsModal = () => {
const enrollSecret =
// TODO: Currently, prepacked installers in Fleet Sandbox use the global enroll secret,
// and Fleet Sandbox runs Fleet Free so the isSandboxMode check here is an
// additional precaution/reminder to revisit this in connection with future changes.
// See https://github.com/fleetdm/fleet/issues/4970#issuecomment-1187679407.
currentTeam && !isSandboxMode
? teamSecrets?.[0].secret
: globalSecrets?.[0].secret;
return (
<AddHostsModal
currentTeam={currentTeam}
enrollSecret={enrollSecret}
isLoading={isLoadingTeams || isGlobalSecretsLoading}
isSandboxMode={!!isSandboxMode}
onCancel={toggleAddHostsModal}
/>
);
};
const renderTransferHostModal = () => {
if (!teams) {
@ -1344,7 +1353,9 @@ const ManageHostsPage = ({
isHostCountLoading ? "count-loading" : ""
}`}
>
<span>{`${count} host${count === 1 ? "" : "s"}`}</span>
{count !== undefined && (
<span>{`${count} host${count === 1 ? "" : "s"}`}</span>
)}
{count ? (
<Button
className={`${baseClass}__export-btn`}

View file

@ -0,0 +1,25 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
import { IInstallerType } from "interfaces/installer";
import sendRequest from "services";
import ENDPOINTS from "utilities/endpoints";
export interface IDownloadInstallerRequestParams {
enrollSecret: string;
includeDesktop: boolean;
installerType: IInstallerType;
}
export default {
downloadInstaller: ({
enrollSecret,
includeDesktop,
installerType,
}: IDownloadInstallerRequestParams): Promise<BlobPart> => {
const path = `${ENDPOINTS.DOWNLOAD_INSTALLER}/${encodeURIComponent(
enrollSecret
)}/${installerType}?desktop=${includeDesktop}`;
console.log("path: ", path);
return sendRequest("GET", path, undefined, "blob");
},
};

View file

@ -1,11 +1,16 @@
import axios, { AxiosError, AxiosResponse } from "axios";
import axios, {
AxiosError,
AxiosResponse,
ResponseType as AxiosResponseType,
} from "axios";
import local from "utilities/local";
import URL_PREFIX from "router/url_prefix";
const sendRequest = async (
method: "GET" | "POST" | "PATCH" | "DELETE",
path: string,
data?: unknown
data?: unknown,
responseType: AxiosResponseType = "json"
): Promise<any> => {
const { origin } = global.window.location;
@ -17,6 +22,7 @@ const sendRequest = async (
method,
url,
data,
responseType,
headers: {
Authorization: `Bearer ${token}`,
},

View file

@ -8,6 +8,7 @@ export default {
return `/${API_VERSION}/fleet/email/change/${token}`;
},
DEVICE_USER_DETAILS: `/${API_VERSION}/fleet/device`,
DOWNLOAD_INSTALLER: `/${API_VERSION}/fleet/download_installer`,
ENABLE_USER: (id: number): string => {
return `/${API_VERSION}/fleet/users/${id}/enable`;
},

View file

@ -32,7 +32,9 @@ GLOBAL OPTIONS:
### Example
To upload a file for testing to your local MinIO server, you can run this
command from the root of the repo:
command from the root of the repo (be sure to replace the `--enroll-secret`
string with the value you wish to test and set the `--fleet-desktop` boolean
to your desired value):
```
go run tools/installerstore/main.go \
@ -46,6 +48,7 @@ go run tools/installerstore/main.go \
--disable-ssl=true \
--force-s3-path-style=true \
--create-bucket=true \
--fleet-desktop=true \
fleet-osquery.pkg
```