diff --git a/assets/images/icon-apple-black-24x24@2x.png b/assets/images/icon-apple-black-24x24@2x.png
new file mode 100644
index 0000000000..50f389ab87
Binary files /dev/null and b/assets/images/icon-apple-black-24x24@2x.png differ
diff --git a/assets/images/icon-apple-vibrant-blue-24x24@2x.png b/assets/images/icon-apple-vibrant-blue-24x24@2x.png
new file mode 100644
index 0000000000..4dd07cfd50
Binary files /dev/null and b/assets/images/icon-apple-vibrant-blue-24x24@2x.png differ
diff --git a/assets/images/icon-circle-check-blue-48x48@2x.png b/assets/images/icon-circle-check-blue-48x48@2x.png
new file mode 100644
index 0000000000..67a2b7b533
Binary files /dev/null and b/assets/images/icon-circle-check-blue-48x48@2x.png differ
diff --git a/assets/images/icon-linux-black-24x24@2x.png b/assets/images/icon-linux-black-24x24@2x.png
new file mode 100644
index 0000000000..93663b4991
Binary files /dev/null and b/assets/images/icon-linux-black-24x24@2x.png differ
diff --git a/assets/images/icon-linux-vibrant-blue-24x24@2x.png b/assets/images/icon-linux-vibrant-blue-24x24@2x.png
new file mode 100644
index 0000000000..2da7e23c0d
Binary files /dev/null and b/assets/images/icon-linux-vibrant-blue-24x24@2x.png differ
diff --git a/assets/images/icon-windows-black-24x24@2x.png b/assets/images/icon-windows-black-24x24@2x.png
new file mode 100644
index 0000000000..f0aa38c98f
Binary files /dev/null and b/assets/images/icon-windows-black-24x24@2x.png differ
diff --git a/assets/images/icon-windows-vibrant-blue-24x24@2x.png b/assets/images/icon-windows-vibrant-blue-24x24@2x.png
new file mode 100644
index 0000000000..5de3e9280f
Binary files /dev/null and b/assets/images/icon-windows-vibrant-blue-24x24@2x.png differ
diff --git a/changes/issue-5757-download-orbit-installer-sandbox b/changes/issue-5757-download-orbit-installer-sandbox
new file mode 100644
index 0000000000..80fb1c9a86
--- /dev/null
+++ b/changes/issue-5757-download-orbit-installer-sandbox
@@ -0,0 +1 @@
+- Added Fleet Sandbox UI to download pre-packaged installers
\ No newline at end of file
diff --git a/docs/Contributing/Testing.md b/docs/Contributing/Testing.md
index 2f63c85c7c..42b0ef2d5b 100644
--- a/docs/Contributing/Testing.md
+++ b/docs/Contributing/Testing.md
@@ -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!`.
diff --git a/frontend/components/AddHostsModal/AddHostsModal.tsx b/frontend/components/AddHostsModal/AddHostsModal.tsx
index c867539b78..13cad6defe 100644
--- a/frontend/components/AddHostsModal/AddHostsModal.tsx
+++ b/frontend/components/AddHostsModal/AddHostsModal.tsx
@@ -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 ;
+ }
+ if (!enrollSecret) {
+ return ;
+ }
+
+ // 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 ? (
+
+ ) : (
+
+ );
+ };
+
return (
-
+ {renderModalContent()}
);
};
diff --git a/frontend/components/AddHostsModal/DownloadInstallers/DownloadInstallers.tsx b/frontend/components/AddHostsModal/DownloadInstallers/DownloadInstallers.tsx
new file mode 100644
index 0000000000..316e5d4a85
--- /dev/null
+++ b/frontend/components/AddHostsModal/DownloadInstallers/DownloadInstallers.tsx
@@ -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 (
+
+ );
+ case "macOS":
+ return (
+
+ );
+ case "Windows":
+ return (
+
+ );
+ 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 (
+
+
+
+ );
+ }
+
+ if (isDownloadSuccess) {
+ const installerPlatform =
+ (selectedInstaller &&
+ `${INSTALLER_PLATFORM_BY_TYPE[selectedInstaller]} `) ||
+ "";
+ return (
+
+
+
You’re almost there
+
{`Run the installer on a ${installerPlatform}laptop, workstation, or sever to add it to Fleet.`}
+
Got it
+
+ );
+ }
+
+ return (
+
+
Which platform is your host running?
+
+ {displayOrder.map((platform) => {
+ const installerType = INSTALLER_TYPE_BY_PLATFORM[platform];
+ const isSelected = selectedInstaller === installerType;
+ return (
+
onClickSelector(installerType)}
+ >
+
+ {displayIcon(platform, isSelected)}
+ {platform}
+
+
+ );
+ })}
+
+
setIncludeDesktop(value)}
+ value={includeDesktop}
+ >
+ <>
+ Include
+ Lightweight application that allows end users to see information about their device."
+ }
+ >
+ Fleet Desktop
+
+ >
+
+
downloadInstaller(selectedInstaller)}
+ >
+ {isDownloading ? : "Download installer"}
+
+
+ );
+};
+
+export default DownloadInstallers;
diff --git a/frontend/components/AddHostsModal/DownloadInstallers/_styles.scss b/frontend/components/AddHostsModal/DownloadInstallers/_styles.scss
new file mode 100644
index 0000000000..ae45b4a810
--- /dev/null
+++ b/frontend/components/AddHostsModal/DownloadInstallers/_styles.scss
@@ -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;
+ }
+ }
+}
diff --git a/frontend/components/AddHostsModal/PlatformWrapper/PlatformWrapper.tsx b/frontend/components/AddHostsModal/PlatformWrapper/PlatformWrapper.tsx
index 2560c5408a..63e2e8230f 100644
--- a/frontend/components/AddHostsModal/PlatformWrapper/PlatformWrapper.tsx
+++ b/frontend/components/AddHostsModal/PlatformWrapper/PlatformWrapper.tsx
@@ -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>({});
- const [includeFleetDesktop, setIncludeFleetDesktop] = useState(
- false
- );
+ const [includeFleetDesktop, setIncludeFleetDesktop] = useState(true);
const [showPlainOsquery, setShowPlainOsquery] = useState(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 = ({
<>
setIncludeFleetDesktop(!includeFleetDesktop)}
+ onChange={(value: boolean) => setIncludeFleetDesktop(value)}
value={includeFleetDesktop}
>
<>
diff --git a/frontend/interfaces/installer.ts b/frontend/interfaces/installer.ts
new file mode 100644
index 0000000000..341dac572a
--- /dev/null
+++ b/frontend/interfaces/installer.ts
@@ -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;
diff --git a/frontend/interfaces/team.ts b/frontend/interfaces/team.ts
index 6f7f14ed78..20a462ba88 100644
--- a/frontend/interfaces/team.ts
+++ b/frontend/interfaces/team.ts
@@ -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;
diff --git a/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/TeamDetailsWrapper.tsx b/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/TeamDetailsWrapper.tsx
index 5e47b86509..7a4e28be9d 100644
--- a/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/TeamDetailsWrapper.tsx
+++ b/frontend/pages/admin/TeamManagementPage/TeamDetailsWrapper/TeamDetailsWrapper.tsx
@@ -487,11 +487,15 @@ const TeamDetailsWrapper = ({
{showAddHostsModal && (
)}
{showManageEnrollSecretsModal && (
diff --git a/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tsx b/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tsx
index e52a9c7d4e..ae8fd3b4dc 100644
--- a/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tsx
+++ b/frontend/pages/hosts/ManageHostsPage/ManageHostsPage.tsx
@@ -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 = () => (
-
- );
+ 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 (
+
+ );
+ };
const renderTransferHostModal = () => {
if (!teams) {
@@ -1344,7 +1353,9 @@ const ManageHostsPage = ({
isHostCountLoading ? "count-loading" : ""
}`}
>
- {`${count} host${count === 1 ? "" : "s"}`}
+ {count !== undefined && (
+ {`${count} host${count === 1 ? "" : "s"}`}
+ )}
{count ? (
=> {
+ const path = `${ENDPOINTS.DOWNLOAD_INSTALLER}/${encodeURIComponent(
+ enrollSecret
+ )}/${installerType}?desktop=${includeDesktop}`;
+ console.log("path: ", path);
+
+ return sendRequest("GET", path, undefined, "blob");
+ },
+};
diff --git a/frontend/services/index.ts b/frontend/services/index.ts
index 037e0b4e4e..0cad1f853f 100644
--- a/frontend/services/index.ts
+++ b/frontend/services/index.ts
@@ -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 => {
const { origin } = global.window.location;
@@ -17,6 +22,7 @@ const sendRequest = async (
method,
url,
data,
+ responseType,
headers: {
Authorization: `Bearer ${token}`,
},
diff --git a/frontend/utilities/endpoints.ts b/frontend/utilities/endpoints.ts
index 5bb1e35d3b..67e506a691 100644
--- a/frontend/utilities/endpoints.ts
+++ b/frontend/utilities/endpoints.ts
@@ -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`;
},
diff --git a/tools/installerstore/README.md b/tools/installerstore/README.md
index 1bb136bf48..5ecaf95f2a 100644
--- a/tools/installerstore/README.md
+++ b/tools/installerstore/README.md
@@ -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
```