mirror of
https://github.com/fleetdm/fleet
synced 2026-05-24 01:18:42 +00:00
Merge branch 'main' into feat-fleet-app-library
This commit is contained in:
commit
062b138c04
53 changed files with 18814 additions and 3792 deletions
10
.github/ISSUE_TEMPLATE/story.md
vendored
10
.github/ISSUE_TEMPLATE/story.md
vendored
|
|
@ -32,15 +32,15 @@ What else should contributors [keep in mind](https://fleetdm.com/handbook/compan
|
|||
## Changes
|
||||
|
||||
### Product
|
||||
- [ ] Reference documentation changes: TODO <!-- Specify references documentation changes at fleetdm.com/docs -->
|
||||
- [ ] UI changes: TODO <!-- Insert the link to the relevant Figma cover page. Put "No changes" if there are no changes to the user interface. -->
|
||||
- [ ] CLI (fleetctl) usage changes: TODO <!-- Insert the link to the relevant Figma cover page. Put "No changes" if there are no changes to the CLI. -->
|
||||
- [ ] YAML changes: TODO <!-- Specify changes as a PR to the YAML files doc page. Put "No changes" if there are no changes necessary. -->
|
||||
- [ ] REST API changes: TODO <!-- Specify changes as a PR to the REST API doc page. Put "No changes" if there are no changes necessary. Move this item to the engineering list below if engineering will design the API changes. -->
|
||||
- [ ] YAML changes: TODO <!-- Specify changes in the YAML files doc page as a PR to the reference docs release branch. Put "No changes" if there are no changes necessary. -->
|
||||
- [ ] REST API changes: TODO <!-- Specify changes in the the REST API doc page as a PR to reference docs release branch. Put "No changes" if there are no changes necessary. Move this item to the engineering list below if engineering will design the API changes. -->
|
||||
- [ ] Fleet's agent (fleetd) changes: TODO <!-- Specify changes to fleetd. If the change requires a new Fleet (server) version, consider specifying to only enable this change in new Fleet versions. Put "No changes" if there are no changes necessary. -->
|
||||
- [ ] Activity changes: TODO <!-- Specify changes to Fleet's activity feed as a draft PR to the Audit log page in the contributor docs: https://github.com/fleetdm/fleet/blob/main/docs/Contributing/Audit-logs.md This PR will be closed before release because the Audit log page is automatically generated: https://fleetdm.com/handbook/company/communications#audit-logs Put "No changes" if there are no changes necessary. -->
|
||||
- [ ] Permissions changes: TODO <!-- Specify changes as a PR to the Manage access doc page. If doc changes aren't necessary, explicitly mention no changes to the doc page. Put "No changes" if there are no permissions changes. -->
|
||||
- [ ] Changes to paid features or tiers: TODO <!-- Specify changes as a PR to fleetdm.com/pricing (pricing-features-table.yml). Remove this checkbox and specify "Fleet Free" or "Fleet Premium" if there are no changes to the pricing page necessary. -->
|
||||
- [ ] Permissions changes: TODO <!-- Specify changes in the Manage access doc page as a PR to the reference docs release branch. If doc changes aren't necessary, explicitly mention no changes to the doc page. Put "No changes" if there are no permissions changes. -->
|
||||
- [ ] Changes to paid features or tiers: TODO <!-- Specify changes in pricing-features-table.yml as a PR to reference docs release branch. Remove this checkbox and specify "Fleet Free" or "Fleet Premium" if there are no changes to the pricing page necessary. -->
|
||||
- [ ] Other reference documentation changes: TODO <!-- Any other reference doc changes? Specify changes as a PR to reference docs release branch. Put "No changes" if there are no changes necessary. -->
|
||||
- [ ] Once shipped, requester has been notified
|
||||
|
||||
### Engineering
|
||||
|
|
|
|||
|
|
@ -13,18 +13,13 @@ on:
|
|||
- '.github/workflows/generate-desktop-targets.yml'
|
||||
workflow_dispatch:
|
||||
|
||||
# This allows a subsequently queued workflow run to interrupt previous runs
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id}}
|
||||
cancel-in-progress: true
|
||||
|
||||
defaults:
|
||||
run:
|
||||
# fail-fast using bash -eo pipefail. See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#exit-codes-and-error-action-preference
|
||||
shell: bash
|
||||
|
||||
env:
|
||||
FLEET_DESKTOP_VERSION: 1.32.0
|
||||
FLEET_DESKTOP_VERSION: 1.33.0
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
|
|
|||
5
.github/workflows/goreleaser-orbit.yaml
vendored
5
.github/workflows/goreleaser-orbit.yaml
vendored
|
|
@ -5,11 +5,6 @@ on:
|
|||
tags:
|
||||
- "orbit-*" # For testing, use a pre-release tag like 'orbit-1.24.0-1'
|
||||
|
||||
# This allows a subsequently queued workflow run to interrupt previous runs
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id}}
|
||||
cancel-in-progress: true
|
||||
|
||||
defaults:
|
||||
run:
|
||||
# fail-fast using bash -eo pipefail. See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#exit-codes-and-error-action-preference
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ In this case, `jq` is used to locate and read the value of the `tab_organization
|
|||
|
||||
### Step 3: Query the JSON file with Fleet
|
||||
|
||||
To detect Chrome AI features in Fleet, use SQL query like the following:
|
||||
To detect Chrome AI features in Fleet, use a SQL query like the following:
|
||||
|
||||
```
|
||||
SELECT fullkey,path FROM parse_json WHERE path LIKE '/Users/%/Library/Application Support/Google/Chrome/Default/Preferences' AND fullkey='optimization_guide/tab_organization_setting_state';
|
||||
|
|
|
|||
|
|
@ -6,186 +6,103 @@ This guide provides instructions for migrating devices from your current MDM sol
|
|||
|
||||
## Requirements
|
||||
|
||||
|
||||
- A [deployed Fleet instance](https://fleetdm.com/docs/deploy/deploy-fleet)
|
||||
- Fleet is connected to Apple Push Notification service (APNs) and Apple Business Manager (ABM). [See macOS MDM setup](https://fleetdm.com/guides/macos-mdm-setup)
|
||||
|
||||
## Migrate hosts
|
||||
|
||||
## Migrate manually enrolled hosts
|
||||
To migrate hosts, we will do the following steps:
|
||||
|
||||
1. [Enroll](https://fleetdm.com/guides/enroll-hosts) your hosts to Fleet with [Fleetd and Fleet Desktop](https://fleetdm.com/guides/enroll-hosts#fleet-desktop)
|
||||
1. Enroll hosts to Fleet
|
||||
2. Assign hosts in Apple Business Manager (ABM) to Fleet
|
||||
3. Choose migration workflow and migrate hosts
|
||||
|
||||
### Step 1: enroll hosts to Fleet
|
||||
|
||||
1. First, enroll your hosts to Fleet by installing Fleet's agent (fleetd). Learn how [here](https://fleetdm.com/guides/enroll-hosts).
|
||||
2. Ensure your end users have access to an admin account on their Mac. End users won't be able to migrate on their own if they have a standard account.
|
||||
3. In your old MDM solution, unenroll the hosts to be migrated. MacOS does not allow multiple MDMs to be installed at once.
|
||||
4. Send [these guided instructions](#how-to-turn-on-mdm) to your end users to complete the final few steps via Fleet Desktop.
|
||||
* Note that there will be a gap in MDM coverage between when the host is unenrolled from the old MDM and when the host turns on MDM in Fleet.
|
||||
|
||||
### End user experience
|
||||
### Step 2: assign hosts in Apple Business Manager (ABM) to Fleet
|
||||
|
||||
1. On their **My device** page, once an end user's device is unenrolled from the old MDM solution, the end user will be given the option to manually download the MDM enrollment profile.
|
||||
|
||||
2. Once downloaded, the user will receive a system notification that the Device Enrollment profile needs to be installed in their **System Settings > Profiles** section.
|
||||
|
||||
3. After installation, the MDM enrollment profile can be removed by the end user at any time.
|
||||
1. In ABM, unassign your hosts from your current MDM solution by selecting **Devices** and then selecting **All Devices**. Then, select **Edit** next to **Edit MDM Server**, select **Unassign from the current MDM**, and select **Continue**.
|
||||
|
||||
### How to turn on MDM
|
||||
2. Assign these hosts to Fleet: select **Devices** and then select **All Devices**. Then, select **Edit** next to **Edit MDM Server**, select **Assign to the following MDM:**, select your Fleet server in the dropdown, and select **Continue**.
|
||||
|
||||
1. Select the Fleet icon in your menu bar and select **My device**.
|
||||
|
||||

|
||||
|
||||
2. On your **My device** page, select the **Turn on MDM** button in the yellow banner and follow the instructions.
|
||||
- If you don’t see the yellow banner or the **Turn on MDM** button, select the purple **Refetch** button at the top of the page.
|
||||
- If you still don't see the **Turn on MDM** button or the **My device** page presents you with an error, please contact your IT administrator.
|
||||
|
||||
<img width="1400" alt="My device page - turn on MDM" src="https://user-images.githubusercontent.com/5359586/229950406-98343bf7-9653-4117-a8f5-c03359ba0d86.png">
|
||||
|
||||
## Migrate automatically enrolled (ADE) hosts
|
||||
|
||||
> Automatic enrollment is available in Fleet Premium or Ultimate
|
||||
|
||||
To migrate automatically enrolled hosts, we will do the following steps:
|
||||
|
||||
1. Prepare to migrate hosts
|
||||
2. Choose migration workflow and migrate hosts
|
||||
|
||||
### Step 1: prepare to migrate hosts
|
||||
|
||||
1. Connect Fleet to Apple Business Manager (ABM). Learn how [here](https://fleetdm.com/guides/macos-mdm-setup#apple-business-manager-abm).
|
||||
2. [Enroll](https://fleetdm.com/guides/enroll-hosts) your hosts to Fleet with [Fleetd and Fleet Desktop](https://fleetdm.com/guides/enroll-hosts#fleet-desktop)
|
||||
3. Ensure your end users have access to an admin account on their Mac. End users won't be able to migrate on their own if they have a standard account.
|
||||
4. Migrate your hosts to Fleet in ABM:
|
||||
1. In ABM, unassign the existing hosts' MDM server from the old MDM solution: In ABM, select **Devices** and then select **All Devices**. Then, select **Edit** next to **Edit MDM Server**, select **Unassign from the current MDM**, and select **Continue**.
|
||||
2. In ABM, assign these hosts' MDM server to Fleet: In ABM, select **Devices** and then select **All Devices**. Then, select **Edit** next to **Edit MDM Server**, select **Assign to the following MDM:**, select your Fleet server in the dropdown, and select **Continue**.
|
||||
|
||||
### Step 2: choose migration workflow and migrate hosts
|
||||
### Step 3: choose migration workflow and migrate hosts
|
||||
|
||||
There are two migration workflows in Fleet: default and end user.
|
||||
|
||||
The default migration workflow requires that the IT admin unenrolls hosts from the old MDM solution before the end user can complete migration. This will result in a gap in MDM coverage until the end user completes migration.
|
||||
|
||||
The end user migration workflow allows the end user to kick-off migration by unenrolling from the old MDM solution on their own. Once the user is unenrolled, they're prompted to turn on MDM features in Fleet. This reduces the gap in MDM coverage.
|
||||
|
||||
Configuring the end user migration workflow requires a few additional steps.
|
||||
The end user migration workflow allows the user to kick off migration by unenrolling from the old MDM solution on their own. Once the user is unenrolled, they're prompted to turn on MDM features in Fleet, reducing the gap in MDM coverage.
|
||||
|
||||
#### Default workflow
|
||||
|
||||
1. In your old MDM solution, unenroll the hosts to be migrated. MacOS does not allow multiple MDMs to be installed at once.
|
||||
|
||||
2. Send [these guided instructions](#how-to-turn-on-mdm-default) to your end users to complete the final few steps via Fleet Desktop.
|
||||
* Note that there will be a gap in MDM coverage between when the host is unenrolled from the old MDM and when the host turns on MDM in Fleet.
|
||||
|
||||
##### End user experience
|
||||
|
||||
1. The end user will receive a "Device Enrollment: <organization> can automatically configure your Mac." system notification within the macOS Notifications Center.
|
||||
|
||||
2. After the end user clicks on the system notification, macOS will open the **System Setting > Profiles** and ask the user to "Allow Device Enrollment: <organization> can automatically configure your Mac based on settings provided by your System Administrator."
|
||||
|
||||
3. If the end user does not install the profile, the system notification will continue to prompt the end user until the setting has been allowed.
|
||||
|
||||
4. Once this setting has been approved, the MDM enrollment profile cannot be removed by the end user.
|
||||
|
||||
##### How to turn on MDM (default)
|
||||
|
||||
1. Select the Fleet icon in your menu bar and select **My device**.
|
||||
|
||||

|
||||
|
||||
2. On your **My device** page, select the **Turn on MDM** button in the yellow banner and follow the instructions.
|
||||
* If you don’t see the yellow banner or the **Turn on MDM** button, select the purple **Refetch** button at the top of the page.
|
||||
* If you still don't see the **Turn on MDM** button or the **My device** page presents you with an error, please contact your IT administrator.
|
||||
End user experience:
|
||||
|
||||
- After a host is unenrolled from your current MDM solution, the end user will be prompted with Apple's **Remote Management** full-screen popup if the host is assigned to Fleet in ABM.
|
||||
<img width="1400" alt="macOS Remote Management popup" src="https://github.com/user-attachments/assets/084946a5-1658-4d8c-852d-3cf5f5d58655">
|
||||
- If the host is not assigned to Fleet in ABM (manual enrollment), the end user will be given the option to download the MDM enrollment profile on their **My device page**.
|
||||
<img width="1600" alt="Fleet icon in menu bar" src="https://raw.githubusercontent.com/fleetdm/fleet/main/website/assets/images/articles/fleet-desktop-says-hello-world-cover-1600x900@2x.jpg">
|
||||
<img width="1400" alt="My device page - turn on MDM" src="https://user-images.githubusercontent.com/5359586/229950406-98343bf7-9653-4117-a8f5-c03359ba0d86.png">
|
||||
|
||||
Configuration:
|
||||
|
||||
- To kick off the default workflow, unenroll the hosts to be migrated in your current MDM solution. MacOS does not allow a host to be connected to multiple MDM solutions at once.
|
||||
|
||||
#### End user workflow
|
||||
|
||||
> Available in Fleet Premium or Ultimate
|
||||
> Available in Fleet Premium
|
||||
|
||||
The end user migration workflow is supported for automatically enrolled (ADE) hosts.
|
||||
End user experience:
|
||||
|
||||
To watch a GIF that walks through the end user experience during the migration workflow, in the Fleet UI, head to **Settings > Integrations > Mobile device management (MDM)**, and scroll down to the **End user migration workflow** section.
|
||||
- To watch an animation of the end user experience during the migration workflow, head to **Settings > Integrations > Mobile device management (MDM)** in the Fleet UI, and scroll down to the **End user migration workflow** section.
|
||||
|
||||
In Fleet, you can configure the end user workflow using the Fleet UI or fleetctl command-line tool.
|
||||
Configuration:
|
||||
|
||||
Fleet UI:
|
||||
- In Fleet, you can configure the end user workflow using the Fleet UI, Fleet API, or Fleet's GitOps workflow.
|
||||
|
||||
- After configuring the end user workflow, instruct your end users to select the Fleet icon in their menu bar, select **Migrate to Fleet** and follow the on-screen instructions to migrate to Fleet.
|
||||
|
||||
- Fleet UI:
|
||||
1. Select the avatar on the right side of the top navigation and select **Settings > Integrations > Mobile device management (MDM)**.
|
||||
|
||||
2. Scroll down to the **End user migration workflow** section and select the toggle to enable the workflow.
|
||||
|
||||
3. Under **Mode** choose a mode and enter the webhook URL for you automation tool (ex. Tines) under **Webhook URL** and select **Save**.
|
||||
|
||||
4. During the end user migration workflow, an end user's device will have their selected system theme (light or dark) applied. If your logo is not easy to see on both light and dark backgrounds, you can optionally set a logo for each theme:
|
||||
Head to **Settings** > **Organization settings** >
|
||||
**Organization info**, add URLs to your logos in the **Organization avatar URL (for dark backgrounds)** and **Organization avatar URL (for light backgrounds)** fields, and select **Save**.
|
||||
|
||||
fleetctl CLI:
|
||||
|
||||
1. Create `fleet-config.yaml` file or add to your existing `config` YAML file:
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: config
|
||||
spec:
|
||||
mdm:
|
||||
macos_migration:
|
||||
enable: true
|
||||
mode: "voluntary"
|
||||
webhook_url: "https://example.com"
|
||||
...
|
||||
```
|
||||
|
||||
2. Fill in the above keys under the `mdm.macos_migration` key.
|
||||
|
||||
To learn about each option, in the Fleet UI, select the avatar on the right side of the top navigation, select **Settings > Integrations > Mobile device management (MDM)**, and scroll down to the **End user migration workflow** section.
|
||||
|
||||
3. During the end user migration workflow, the window will show the Fleet logo on top of a dark and light background (appearance configured by end user).
|
||||
|
||||
If want to add a your organization's logo, you can optionally set a logo for each background:
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: config
|
||||
spec:
|
||||
org_info:
|
||||
org_logo_url: https://fleetdm.com/images/press-kit/fleet-blue-logo.png
|
||||
org_logo_url_light_background: https://fleetdm.com/images/press-kit/fleet-white-logo.png
|
||||
...
|
||||
```
|
||||
|
||||
Add URLs to your logos that are visible on a dark background and light background in the `org_logo_url` and `org_logo_url_light_background` keys respectively. If you only set a logo for one, the Fleet logo will be used for the other.
|
||||
|
||||
4. Run the fleetctl `apply -f fleet-config.yml` command to add your configuration.
|
||||
|
||||
5. Confirm that your configuration was saved by running `fleetctl get config`.
|
||||
|
||||
6. Send [these guided instructions](#how-to-turn-on-mdm-end-user) to your end users to complete the final few steps via Fleet Desktop.
|
||||
|
||||
##### How to turn on MDM (end user)
|
||||
|
||||
1. Select the Fleet icon in your menu bar and select **Migrate to Fleet**.
|
||||
|
||||
2. Select **Start** in the **Migrate to Fleet** popup.
|
||||
|
||||
2. On your **My device** page, select the **Turn on MDM** button in the yellow banner and follow the instructions.
|
||||
* If you don’t see the yellow banner or the **Turn on MDM** button, select the purple **Refetch** button at the top of the page.
|
||||
* If you still don't see the **Turn on MDM** button or the **My device** page presents you with an error, please contact your IT administrator.
|
||||
3. Under **Mode**, choose a mode, enter the webhook URL for your automation tool (e.g., Tines) under **Webhook URL**, and select **Save**.
|
||||
4. During the end user migration workflow, an end user's device will have its selected system theme (light or dark) applied. If your logo is not easy to see on both light and dark backgrounds, you can optionally set a logo for each theme:
|
||||
Head to **Settings** > **Organization settings** > **Organization info**, add URLs to your logos in the **Organization avatar URL (for dark backgrounds)** and **Organization avatar URL (for light backgrounds)** fields, and select **Save**.
|
||||
- Fleet API: API documentation is [here](https://fleetdm.com/docs/rest-api/rest-api#mdm-macos-migration)
|
||||
- GitOps:
|
||||
- To manage macOS MDM migration configuration using Fleet's best practice GitOps, check out the `macos_migration` key in the [GitOps reference documentation](https://fleetdm.com/docs/configuration/yaml-files#macos-migration).
|
||||
- To manage your organization's logo for dark and light backgrounds using Fleet's best practice GitOps, check out the `org_info` key in the [GitOps reference documentation](https://fleetdm.com/docs/configuration/yaml-files#org-info).
|
||||
|
||||
## Check migration progress
|
||||
|
||||
To see a report of which hosts have successfully migrated to Fleet, have MDM features off, or are still enrolled to your old MDM solution head to the **Dashboard** page by clicking the icon on the left side of the top navigation bar.
|
||||
To see a report of which hosts have successfully migrated to Fleet, have MDM features off, or are still enrolled to your old MDM solution head to the **Dashboard** page by clicking the icon on the left side of the top navigation bar.
|
||||
|
||||
Then, scroll down to the **Mobile device management (MDM)** section.
|
||||
Then, scroll down to the **Mobile device management (MDM)** section of the Dashboard. You'll see a breakdown of which hosts have successfully migrated to Fleet, which have MDM features disabled, and which are still enrolled in the previous MDM solution.
|
||||
|
||||
## FileVault recovery keys
|
||||
|
||||
_Available in Fleet Premium_
|
||||
|
||||
When migrating from a previous MDM, end users need to restart or logout of their device to escrow FileVault keys to Fleet. The **My device** page in Fleet Desktop will present users with instructions to reset their key.
|
||||
When migrating from a previous MDM, end users must restart or log out of their device to escrow FileVault keys to Fleet. The **My device** page in Fleet Desktop will present users with instructions on how to reset their key.
|
||||
|
||||
To start, enforce FileVault (disk encryption) and escrow in Fleet. Learn how [here](https://fleetdm.com/guides/enforce-disk-encryption).
|
||||
To start, enforce FileVault disk encryption and escrow recovery keys in Fleet. Learn how [here](https://fleetdm.com/guides/enforce-disk-encryption).
|
||||
|
||||
After turning on disk encryption in Fleet, share [these guided instructions](#how-to-turn-on-disk-encryption) with your end users.
|
||||
|
||||
### How to turn on disk encryption
|
||||
|
||||
1. Select the Fleet icon in your menu bar and select **My device**.
|
||||
|
||||

|
||||
|
||||
2. On your **My device** page, follow the disk encryption instructions in the yellow banner.
|
||||
- If you don’t see the yellow banner, select the purple **Refetch** button at the top of the page.
|
||||
- If you still don't see the yellow banner after a couple minutes or if the **My device** page presents you with an error, please contact your IT administrator.
|
||||
|
||||
<img width="1399" alt="My device page - turn on disk encryption" src="https://user-images.githubusercontent.com/5359586/229950451-cfcd2314-a993-48db-aecf-11aac576d297.png">
|
||||
|
||||
## Activation Lock
|
||||
|
||||
In Fleet, the [Activation Lock](https://support.apple.com/en-us/HT208987) feature is disabled by default for automatically enrolled (ADE) hosts.
|
||||
|
|
@ -194,21 +111,9 @@ In 2024, Apple added the ability to manage activation lock in Apple Business Man
|
|||
|
||||
If a device is not available in ABM and has Activation Lock enabled, we recommend asking the end user to follow these instructions to disable Activation Lock before migrating the device to Fleet: https://support.apple.com/en-us/HT208987.
|
||||
|
||||
This is because if the Activation Lock is enabled, you will need the Activation Lock bypass code to successfully wipe and reuse the Mac.
|
||||
If the Activation Lock is enabled, you will need the Activation Lock bypass code to wipe and reuse the Mac successfully.
|
||||
|
||||
However, Activation Lock bypass codes can only be retrieved from the Mac up to 30 days after the device is enrolled. This means that when migrating from your old MDM solution, it’s likely that you’ll be unable to retrieve the Activation Lock bypass code.
|
||||
|
||||
### How to turn on disk encryption
|
||||
|
||||
1. Select the Fleet icon in your menu bar and select **My device**.
|
||||
|
||||

|
||||
|
||||
2. On your **My device** page, follow the disk encryption instructions in the yellow banner.
|
||||
- If you don’t see the yellow banner, select the purple **Refetch** button at the top of the page.
|
||||
- If you still don't see the yellow banner after a couple minutes or if the **My device** page presents you with an error, please contact your IT administrator.
|
||||
|
||||
<img width="1399" alt="My device page - turn on disk encryption" src="https://user-images.githubusercontent.com/5359586/229950451-cfcd2314-a993-48db-aecf-11aac576d297.png">
|
||||
|
||||
<meta name="category" value="guides">
|
||||
<meta name="authorGitHubUsername" value="zhumo">
|
||||
|
|
|
|||
1
changes/20959-query-host-flow-fix-observer
Normal file
1
changes/20959-query-host-flow-fix-observer
Normal file
|
|
@ -0,0 +1 @@
|
|||
- Fix UI flow for observers to easily query hosts from the host details page
|
||||
1
changes/22069-gitops-async-software-batch
Normal file
1
changes/22069-gitops-async-software-batch
Normal file
|
|
@ -0,0 +1 @@
|
|||
* Modified `POST /api/latest/fleet/software/batch` endpoint to be asynchronous and added a new endpoint `GET /api/latest/fleet/software/batch/{request_uuid}` to retrieve the result of the batch upload.
|
||||
1
changes/22097-mdm-migration-guide
Normal file
1
changes/22097-mdm-migration-guide
Normal file
|
|
@ -0,0 +1 @@
|
|||
- Updates the guide for MDM migration to include the new UX in fleetd.
|
||||
|
|
@ -49,6 +49,7 @@ import (
|
|||
"github.com/fleetdm/fleet/v4/server/pubsub"
|
||||
"github.com/fleetdm/fleet/v4/server/service"
|
||||
"github.com/fleetdm/fleet/v4/server/service/async"
|
||||
"github.com/fleetdm/fleet/v4/server/service/redis_key_value"
|
||||
"github.com/fleetdm/fleet/v4/server/service/redis_lock"
|
||||
"github.com/fleetdm/fleet/v4/server/service/redis_policy_set"
|
||||
"github.com/fleetdm/fleet/v4/server/sso"
|
||||
|
|
@ -798,6 +799,7 @@ the way that the Fleet server works.
|
|||
softwareInstallStore,
|
||||
bootstrapPackageStore,
|
||||
distributedLock,
|
||||
redis_key_value.New(redisPool),
|
||||
)
|
||||
if err != nil {
|
||||
initFatal(err, "initial Fleet Premium service")
|
||||
|
|
|
|||
|
|
@ -2320,8 +2320,8 @@ func TestGetTeamsYAMLAndApply(t *testing.T) {
|
|||
declaration.DeclarationUUID = uuid.NewString()
|
||||
return declaration, nil
|
||||
}
|
||||
ds.BatchSetSoftwareInstallersFunc = func(ctx context.Context, tmID *uint, installers []*fleet.UploadSoftwareInstallerPayload) ([]fleet.SoftwarePackageResponse, error) {
|
||||
return nil, nil
|
||||
ds.BatchSetSoftwareInstallersFunc = func(ctx context.Context, tmID *uint, installers []*fleet.UploadSoftwareInstallerPayload) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
actualYaml := runAppForTest(t, []string{"get", "teams", "--yaml"})
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import (
|
|||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
|
|
@ -182,7 +183,8 @@ func TestGitOpsBasicGlobalPremium(t *testing.T) {
|
|||
license := &fleet.LicenseInfo{Tier: fleet.TierPremium, Expiration: time.Now().Add(24 * time.Hour)}
|
||||
_, ds := runServerWithMockedDS(
|
||||
t, &service.TestServerOpts{
|
||||
License: license,
|
||||
License: license,
|
||||
KeyValueStore: newMemKeyValueStore(),
|
||||
},
|
||||
)
|
||||
|
||||
|
|
@ -229,7 +231,10 @@ func TestGitOpsBasicGlobalPremium(t *testing.T) {
|
|||
ds.NewJobFunc = func(ctx context.Context, job *fleet.Job) (*fleet.Job, error) {
|
||||
return &fleet.Job{}, nil
|
||||
}
|
||||
ds.BatchSetSoftwareInstallersFunc = func(ctx context.Context, teamID *uint, installers []*fleet.UploadSoftwareInstallerPayload) ([]fleet.SoftwarePackageResponse, error) {
|
||||
ds.BatchSetSoftwareInstallersFunc = func(ctx context.Context, teamID *uint, installers []*fleet.UploadSoftwareInstallerPayload) error {
|
||||
return nil
|
||||
}
|
||||
ds.GetSoftwareInstallersFunc = func(ctx context.Context, tmID uint) ([]fleet.SoftwarePackageResponse, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
|
|
@ -285,7 +290,8 @@ func TestGitOpsBasicTeam(t *testing.T) {
|
|||
license := &fleet.LicenseInfo{Tier: fleet.TierPremium, Expiration: time.Now().Add(24 * time.Hour)}
|
||||
_, ds := runServerWithMockedDS(
|
||||
t, &service.TestServerOpts{
|
||||
License: license,
|
||||
License: license,
|
||||
KeyValueStore: newMemKeyValueStore(),
|
||||
},
|
||||
)
|
||||
|
||||
|
|
@ -373,7 +379,10 @@ func TestGitOpsBasicTeam(t *testing.T) {
|
|||
ds.DeleteMDMAppleDeclarationByNameFunc = func(ctx context.Context, teamID *uint, name string) error {
|
||||
return nil
|
||||
}
|
||||
ds.BatchSetSoftwareInstallersFunc = func(ctx context.Context, teamID *uint, installers []*fleet.UploadSoftwareInstallerPayload) ([]fleet.SoftwarePackageResponse, error) {
|
||||
ds.BatchSetSoftwareInstallersFunc = func(ctx context.Context, teamID *uint, installers []*fleet.UploadSoftwareInstallerPayload) error {
|
||||
return nil
|
||||
}
|
||||
ds.GetSoftwareInstallersFunc = func(ctx context.Context, tmID uint) ([]fleet.SoftwarePackageResponse, error) {
|
||||
return nil, nil
|
||||
}
|
||||
ds.ApplyEnrollSecretsFunc = func(ctx context.Context, teamID *uint, secrets []*fleet.EnrollSecret) error {
|
||||
|
|
@ -644,6 +653,7 @@ func TestGitOpsFullTeam(t *testing.T) {
|
|||
MDMPusher: mockPusher{},
|
||||
FleetConfig: &fleetCfg,
|
||||
NoCacheDatastore: true,
|
||||
KeyValueStore: newMemKeyValueStore(),
|
||||
},
|
||||
)
|
||||
|
||||
|
|
@ -804,8 +814,11 @@ func TestGitOpsFullTeam(t *testing.T) {
|
|||
return nil
|
||||
}
|
||||
var appliedSoftwareInstallers []*fleet.UploadSoftwareInstallerPayload
|
||||
ds.BatchSetSoftwareInstallersFunc = func(ctx context.Context, teamID *uint, installers []*fleet.UploadSoftwareInstallerPayload) ([]fleet.SoftwarePackageResponse, error) {
|
||||
ds.BatchSetSoftwareInstallersFunc = func(ctx context.Context, teamID *uint, installers []*fleet.UploadSoftwareInstallerPayload) error {
|
||||
appliedSoftwareInstallers = installers
|
||||
return nil
|
||||
}
|
||||
ds.GetSoftwareInstallersFunc = func(ctx context.Context, tmID uint) ([]fleet.SoftwarePackageResponse, error) {
|
||||
return nil, nil
|
||||
}
|
||||
ds.SetTeamVPPAppsFunc = func(ctx context.Context, teamID *uint, adamIDs []fleet.VPPAppTeam) error {
|
||||
|
|
@ -937,7 +950,8 @@ func TestGitOpsBasicGlobalAndTeam(t *testing.T) {
|
|||
license := &fleet.LicenseInfo{Tier: fleet.TierPremium, Expiration: time.Now().Add(24 * time.Hour)}
|
||||
_, ds := runServerWithMockedDS(
|
||||
t, &service.TestServerOpts{
|
||||
License: license,
|
||||
License: license,
|
||||
KeyValueStore: newMemKeyValueStore(),
|
||||
},
|
||||
)
|
||||
|
||||
|
|
@ -1055,7 +1069,10 @@ func TestGitOpsBasicGlobalAndTeam(t *testing.T) {
|
|||
savedTeam = team
|
||||
return team, nil
|
||||
}
|
||||
ds.BatchSetSoftwareInstallersFunc = func(ctx context.Context, teamID *uint, installers []*fleet.UploadSoftwareInstallerPayload) ([]fleet.SoftwarePackageResponse, error) {
|
||||
ds.BatchSetSoftwareInstallersFunc = func(ctx context.Context, teamID *uint, installers []*fleet.UploadSoftwareInstallerPayload) error {
|
||||
return nil
|
||||
}
|
||||
ds.GetSoftwareInstallersFunc = func(ctx context.Context, tmID uint) ([]fleet.SoftwarePackageResponse, error) {
|
||||
return nil, nil
|
||||
}
|
||||
ds.ListSoftwareTitlesFunc = func(ctx context.Context, opt fleet.SoftwareTitleListOptions, tmFilter fleet.TeamFilter) ([]fleet.SoftwareTitleListResult, int, *fleet.PaginationMetadata, error) {
|
||||
|
|
@ -1201,7 +1218,8 @@ func TestGitOpsBasicGlobalAndNoTeam(t *testing.T) {
|
|||
license := &fleet.LicenseInfo{Tier: fleet.TierPremium, Expiration: time.Now().Add(24 * time.Hour)}
|
||||
_, ds := runServerWithMockedDS(
|
||||
t, &service.TestServerOpts{
|
||||
License: license,
|
||||
License: license,
|
||||
KeyValueStore: newMemKeyValueStore(),
|
||||
},
|
||||
)
|
||||
// Mock appConfig
|
||||
|
|
@ -1317,7 +1335,10 @@ func TestGitOpsBasicGlobalAndNoTeam(t *testing.T) {
|
|||
savedTeam = team
|
||||
return team, nil
|
||||
}
|
||||
ds.BatchSetSoftwareInstallersFunc = func(ctx context.Context, teamID *uint, installers []*fleet.UploadSoftwareInstallerPayload) ([]fleet.SoftwarePackageResponse, error) {
|
||||
ds.BatchSetSoftwareInstallersFunc = func(ctx context.Context, teamID *uint, installers []*fleet.UploadSoftwareInstallerPayload) error {
|
||||
return nil
|
||||
}
|
||||
ds.GetSoftwareInstallersFunc = func(ctx context.Context, tmID uint) ([]fleet.SoftwarePackageResponse, error) {
|
||||
return nil, nil
|
||||
}
|
||||
ds.ListSoftwareTitlesFunc = func(ctx context.Context, opt fleet.SoftwareTitleListOptions, tmFilter fleet.TeamFilter) ([]fleet.SoftwareTitleListResult, int, *fleet.PaginationMetadata, error) {
|
||||
|
|
@ -1634,9 +1655,9 @@ func TestGitOpsTeamSofwareInstallers(t *testing.T) {
|
|||
file string
|
||||
wantErr string
|
||||
}{
|
||||
{"testdata/gitops/team_software_installer_not_found.yml", "Please make sure that URLs are publicy accessible to the internet."},
|
||||
{"testdata/gitops/team_software_installer_not_found.yml", "Please make sure that URLs are reachable from your Fleet server."},
|
||||
{"testdata/gitops/team_software_installer_unsupported.yml", "The file should be .pkg, .msi, .exe or .deb."},
|
||||
{"testdata/gitops/team_software_installer_too_large.yml", "The maximum file size is 500 MB"},
|
||||
{"testdata/gitops/team_software_installer_too_large.yml", "The maximum file size is 500 MiB"},
|
||||
{"testdata/gitops/team_software_installer_valid.yml", ""},
|
||||
{"testdata/gitops/team_software_installer_valid_apply.yml", ""},
|
||||
{"testdata/gitops/team_software_installer_pre_condition_multiple_queries.yml", "should have only one query."},
|
||||
|
|
@ -1668,10 +1689,13 @@ func TestGitOpsTeamSoftwareInstallersQueryEnv(t *testing.T) {
|
|||
|
||||
t.Setenv("QUERY_VAR", "IT_WORKS")
|
||||
|
||||
ds.BatchSetSoftwareInstallersFunc = func(ctx context.Context, tmID *uint, installers []*fleet.UploadSoftwareInstallerPayload) ([]fleet.SoftwarePackageResponse, error) {
|
||||
ds.BatchSetSoftwareInstallersFunc = func(ctx context.Context, tmID *uint, installers []*fleet.UploadSoftwareInstallerPayload) error {
|
||||
if installers[0].PreInstallQuery != "select IT_WORKS" {
|
||||
return nil, fmt.Errorf("Missing env var, got %s", installers[0].PreInstallQuery)
|
||||
return fmt.Errorf("Missing env var, got %s", installers[0].PreInstallQuery)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
ds.GetSoftwareInstallersFunc = func(ctx context.Context, tmID uint) ([]fleet.SoftwarePackageResponse, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
|
|
@ -1686,9 +1710,9 @@ func TestGitOpsNoTeamSoftwareInstallers(t *testing.T) {
|
|||
noTeamFile string
|
||||
wantErr string
|
||||
}{
|
||||
{"testdata/gitops/no_team_software_installer_not_found.yml", "Please make sure that URLs are publicy accessible to the internet."},
|
||||
{"testdata/gitops/no_team_software_installer_not_found.yml", "Please make sure that URLs are reachable from your Fleet server."},
|
||||
{"testdata/gitops/no_team_software_installer_unsupported.yml", "The file should be .pkg, .msi, .exe or .deb."},
|
||||
{"testdata/gitops/no_team_software_installer_too_large.yml", "The maximum file size is 500 MB"},
|
||||
{"testdata/gitops/no_team_software_installer_too_large.yml", "The maximum file size is 500 MiB"},
|
||||
{"testdata/gitops/no_team_software_installer_valid.yml", ""},
|
||||
{"testdata/gitops/no_team_software_installer_pre_condition_multiple_queries.yml", "should have only one query."},
|
||||
{"testdata/gitops/no_team_software_installer_pre_condition_not_found.yml", "no such file or directory"},
|
||||
|
|
@ -2050,6 +2074,7 @@ func setupFullGitOpsPremiumServer(t *testing.T) (*mock.Store, **fleet.AppConfig,
|
|||
FleetConfig: &fleetCfg,
|
||||
License: license,
|
||||
NoCacheDatastore: true,
|
||||
KeyValueStore: newMemKeyValueStore(),
|
||||
},
|
||||
)
|
||||
|
||||
|
|
@ -2181,7 +2206,10 @@ func setupFullGitOpsPremiumServer(t *testing.T) (*mock.Store, **fleet.AppConfig,
|
|||
declaration.DeclarationUUID = uuid.NewString()
|
||||
return declaration, nil
|
||||
}
|
||||
ds.BatchSetSoftwareInstallersFunc = func(ctx context.Context, teamID *uint, installers []*fleet.UploadSoftwareInstallerPayload) ([]fleet.SoftwarePackageResponse, error) {
|
||||
ds.BatchSetSoftwareInstallersFunc = func(ctx context.Context, teamID *uint, installers []*fleet.UploadSoftwareInstallerPayload) error {
|
||||
return nil
|
||||
}
|
||||
ds.GetSoftwareInstallersFunc = func(ctx context.Context, tmID uint) ([]fleet.SoftwarePackageResponse, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
|
|
@ -2890,3 +2918,25 @@ software:
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
type memKeyValueStore struct {
|
||||
m sync.Map
|
||||
}
|
||||
|
||||
func newMemKeyValueStore() *memKeyValueStore {
|
||||
return &memKeyValueStore{}
|
||||
}
|
||||
|
||||
func (m *memKeyValueStore) Set(ctx context.Context, key string, value string, expireTime time.Duration) error {
|
||||
m.m.Store(key, value)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *memKeyValueStore) Get(ctx context.Context, key string) (*string, error) {
|
||||
v, ok := m.m.Load(key)
|
||||
if !ok {
|
||||
return nil, nil
|
||||
}
|
||||
vAsString := v.(string)
|
||||
return &vAsString, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -585,16 +585,44 @@ Can only be configured for all teams (`org_settings`).
|
|||
|
||||
#### mdm
|
||||
|
||||
The `mdm` section lets you enable MDM features in Fleet.
|
||||
##### apple_business_manager
|
||||
|
||||
- `apple_bm_default_team` - is name of the team that macOS hosts in Apple Business Manager automatically enroll to when they're first set up. If empty, hosts will enroll to "No team" (default: `""`).
|
||||
- `organization_name` is the organization name associated with the Apple Business Manager account.
|
||||
- `macos_team` is the team where macOS hosts are automatically added when they appear in Apple Business Manager.
|
||||
- `ios_team` is the the team where iOS hosts are automatically added when they appear in Apple Business Manager.
|
||||
- `ipados_team` is the team where iPadOS hosts are automatically added when they appear in Apple Business Manager.
|
||||
|
||||
##### Example
|
||||
|
||||
```yaml
|
||||
org_settings:
|
||||
mdm:
|
||||
apple_bm_default_team: "Workstations" # Available in Fleet Premium
|
||||
apple_business_manager: # Available in Fleet Premium
|
||||
- organization_name: Fleet Device Management Inc.
|
||||
macos_team: "💻 Workstations"
|
||||
ios_team: "📱🏢 Company-owned iPhones"
|
||||
ipados_team: "🔳🏢 Company-owned iPads"
|
||||
```
|
||||
|
||||
> Apple Business Manager settings can only be configured for all teams (`org_settings`).
|
||||
|
||||
##### volume_purchasing_program
|
||||
|
||||
- `location` is the name of the location in the Apple Business Manager account.
|
||||
- `teams` is a list of team names. If you choose specific teams, App Store apps in this VPP account will only be available to install on hosts in these teams. If not specified, App Store apps are available to install on hosts in all teams.
|
||||
|
||||
##### Example
|
||||
|
||||
```yaml
|
||||
org_settings:
|
||||
mdm:
|
||||
volume_purchasing_program: # Available in Fleet Premium
|
||||
- location: Fleet Device Management Inc.
|
||||
teams:
|
||||
- "💻 Workstations"
|
||||
- "💻🐣 Workstations (canary)"
|
||||
- "📱🏢 Company-owned iPhones"
|
||||
- "🔳🏢 Company-owned iPads"
|
||||
```
|
||||
|
||||
Can only be configured for all teams (`org_settings`).
|
||||
|
|
|
|||
|
|
@ -531,9 +531,15 @@ The MDM endpoints exist to support the related command-line interface sub-comman
|
|||
- [Generate Apple Business Manager public key (ADE)](#generate-apple-business-manager-public-key-ade)
|
||||
- [Request Certificate Signing Request (CSR)](#request-certificate-signing-request-csr)
|
||||
- [Upload APNS certificate](#upload-apns-certificate)
|
||||
- [Upload ABM Token](#upload-abm-token)
|
||||
- [Add ABM token](#add-abm-token)
|
||||
- [Turn off Apple MDM](#turn-off-apple-mdm)
|
||||
- [Disable automatic enrollment (ADE)](#disable-automatic-enrollment-ade)
|
||||
- [Update ABM token's teams](#update-abm-tokens-teams)
|
||||
- [Renew ABM token](#renew-abm-token)
|
||||
- [Delete ABM token](#delete-abm-token)
|
||||
- [Add VPP token](#add-VPP-token)
|
||||
- [Update VPP token's teams](#update-vpp-tokens-teams)
|
||||
- [Renew VPP token](#renew-vpp-token)
|
||||
- [Delete VPP token](#delete-vpp-token)
|
||||
- [Batch-apply MDM custom settings](#batch-apply-mdm-custom-settings)
|
||||
- [Initiate SSO during DEP enrollment](#initiate-sso-during-dep-enrollment)
|
||||
- [Complete SSO during DEP enrollment](#complete-sso-during-dep-enrollment)
|
||||
|
|
@ -620,9 +626,9 @@ Content-Type: application/octet-stream
|
|||
|
||||
`Status: 200`
|
||||
|
||||
### Upload ABM Token
|
||||
### Add ABM token
|
||||
|
||||
`POST /api/v1/fleet/mdm/apple/abm_token`
|
||||
`POST /api/v1/fleet/abm_tokens`
|
||||
|
||||
#### Parameters
|
||||
|
||||
|
|
@ -632,7 +638,7 @@ Content-Type: application/octet-stream
|
|||
|
||||
#### Example
|
||||
|
||||
`POST /api/v1/fleet/mdm/apple/abm_token`
|
||||
`POST /api/v1/fleet/abm_tokens`
|
||||
|
||||
##### Request header
|
||||
|
||||
|
|
@ -653,11 +659,23 @@ Content-Type: application/octet-stream
|
|||
--------------------------f02md47480und42y
|
||||
```
|
||||
|
||||
|
||||
##### Default response
|
||||
|
||||
`Status: 200`
|
||||
|
||||
```json
|
||||
"abm_token": {
|
||||
"id": 1,
|
||||
"apple_id": "apple@example.com",
|
||||
"org_name": "Fleet Device Management Inc.",
|
||||
"mdm_server_url": "https://example.com/mdm/apple/mdm",
|
||||
"renew_date": "2024-10-20T00:00:00Z",
|
||||
"terms_expired": false,
|
||||
"macos_team": null,
|
||||
"ios_team": null,
|
||||
"ipados_team": null
|
||||
}
|
||||
```
|
||||
|
||||
### Turn off Apple MDM
|
||||
|
||||
|
|
@ -671,19 +689,265 @@ Content-Type: application/octet-stream
|
|||
|
||||
`Status: 204`
|
||||
|
||||
### Update ABM token's teams
|
||||
|
||||
### Disable automatic enrollment (ADE)
|
||||
`PATCH /api/v1/fleet/abm_tokens/:id/teams`
|
||||
|
||||
`DELETE /api/v1/fleet/mdm/apple/abm_token`
|
||||
#### Parameters
|
||||
|
||||
| Name | Type | In | Description |
|
||||
| ---- | ---- | -- | ----------- |
|
||||
| id | integer | path | *Required* The ABM token's ID |
|
||||
| macos_team_id | integer | body | macOS hosts are automatically added to this team in Fleet when they appear in Apple Business Manager. If not specified, defaults to "No team" |
|
||||
| ios_team_id | integer | body | iOS hosts are automatically added to this team in Fleet when they appear in Apple Business Manager. If not specified, defaults to "No team" |
|
||||
| ipados_team_id | integer | body | iPadOS hosts are automatically added to this team in Fleet when they appear in Apple Business Manager. If not specified, defaults to "No team" |
|
||||
|
||||
#### Example
|
||||
|
||||
`DELETE /api/v1/fleet/mdm/apple/abm_token`
|
||||
`PATCH /api/v1/fleet/abm_tokens/1/teams`
|
||||
|
||||
##### Request body
|
||||
|
||||
```json
|
||||
{
|
||||
"macos_team_id": 1,
|
||||
"ios_team_id": 2,
|
||||
"ipados_team_id": 3
|
||||
}
|
||||
```
|
||||
|
||||
##### Default response
|
||||
|
||||
`Status: 200`
|
||||
|
||||
```json
|
||||
"abm_token": {
|
||||
"id": 1,
|
||||
"apple_id": "apple@example.com",
|
||||
"org_name": "Fleet Device Management Inc.",
|
||||
"mdm_server_url": "https://example.com/mdm/apple/mdm",
|
||||
"renew_date": "2024-11-29T00:00:00Z",
|
||||
"terms_expired": false,
|
||||
"macos_team": 1,
|
||||
"ios_team": 2,
|
||||
"ipados_team": 3
|
||||
}
|
||||
```
|
||||
|
||||
### Renew ABM token
|
||||
|
||||
`PATCH /api/v1/fleet/abm_tokens/:id/renew`
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | Type | In | Description |
|
||||
| ---- | ---- | -- | ----------- |
|
||||
| id | integer | path | *Required* The ABM token's ID |
|
||||
|
||||
#### Example
|
||||
|
||||
`PATCH /api/v1/fleet/abm_tokens/1/renew`
|
||||
|
||||
##### Request header
|
||||
|
||||
```http
|
||||
Content-Length: 850
|
||||
Content-Type: multipart/form-data; boundary=------------------------f02md47480und42y
|
||||
```
|
||||
|
||||
##### Request body
|
||||
|
||||
```http
|
||||
--------------------------f02md47480und42y
|
||||
Content-Disposition: form-data; name="token"; filename="server_token_abm.p7m"
|
||||
Content-Type: application/octet-stream
|
||||
|
||||
<TOKEN_DATA>
|
||||
|
||||
--------------------------f02md47480und42y
|
||||
```
|
||||
|
||||
##### Default response
|
||||
|
||||
`Status: 200`
|
||||
|
||||
```json
|
||||
"abm_token": {
|
||||
"id": 1,
|
||||
"apple_id": "apple@example.com",
|
||||
"org_name": "Fleet Device Management Inc.",
|
||||
"mdm_server_url": "https://example.com/mdm/apple/mdm",
|
||||
"renew_date": "2025-10-20T00:00:00Z",
|
||||
"terms_expired": false,
|
||||
"macos_team": null,
|
||||
"ios_team": null,
|
||||
"ipados_team": null
|
||||
}
|
||||
```
|
||||
|
||||
### Delete ABM token
|
||||
|
||||
`DELETE /api/v1/fleet/abm_tokens/:id`
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | Type | In | Description |
|
||||
| ---- | ---- | -- | ----------- |
|
||||
| id | integer | path | *Required* The ABM token's ID |
|
||||
|
||||
#### Example
|
||||
|
||||
`DELETE /api/v1/fleet/abm_tokens/1`
|
||||
|
||||
##### Default response
|
||||
|
||||
`Status: 204`
|
||||
|
||||
### Add VPP token
|
||||
|
||||
`POST /api/v1/fleet/vpp_tokens`
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | Type | In | Description |
|
||||
| ---- | ---- | -- | ----------- |
|
||||
| token | file | form | *Required* The file containing the content token (.vpptoken) from Apple Business Manager |
|
||||
|
||||
#### Example
|
||||
|
||||
`POST /api/v1/fleet/vpp_tokens`
|
||||
|
||||
##### Request header
|
||||
|
||||
```http
|
||||
Content-Length: 850
|
||||
Content-Type: multipart/form-data; boundary=------------------------f02md47480und42y
|
||||
```
|
||||
|
||||
##### Request body
|
||||
|
||||
```http
|
||||
--------------------------f02md47480und42y
|
||||
Content-Disposition: form-data; name="token"; filename="sToken_for_Acme.vpptoken"
|
||||
Content-Type: application/octet-stream
|
||||
<TOKEN_DATA>
|
||||
--------------------------f02md47480und42y
|
||||
```
|
||||
|
||||
##### Default response
|
||||
|
||||
`Status: 200`
|
||||
|
||||
```json
|
||||
"vpp_token": {
|
||||
"id": 1,
|
||||
"org_name": "Fleet Device Management Inc.",
|
||||
"location": "https://example.com/mdm/apple/mdm",
|
||||
"renew_date": "2024-10-20T00:00:00Z",
|
||||
"terms_expired": false,
|
||||
"teams": null
|
||||
}
|
||||
```
|
||||
|
||||
### Update VPP token's teams
|
||||
|
||||
`PATCH /api/v1/fleet/vpp_tokens/:id/teams`
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | Type | In | Description |
|
||||
| ---- | ---- | -- | ----------- |
|
||||
| id | integer | path | *Required* The ABM token's ID |
|
||||
| team_ids | list | body | If you choose specific teams, App Store apps in this VPP account will only be available to install on hosts in these teams. If not specified, defaults to all teams. |
|
||||
|
||||
#### Example
|
||||
|
||||
`PATCH /api/v1/fleet/vpp_tokens/1/teams`
|
||||
|
||||
##### Request body
|
||||
|
||||
```json
|
||||
{
|
||||
"team_ids": [1, 2, 3]
|
||||
}
|
||||
```
|
||||
|
||||
##### Default response
|
||||
|
||||
`Status: 200`
|
||||
|
||||
```json
|
||||
"vpp_token": {
|
||||
"id": 1,
|
||||
"org_name": "Fleet Device Management Inc.",
|
||||
"location": "https://example.com/mdm/apple/mdm",
|
||||
"renew_date": "2024-10-20T00:00:00Z",
|
||||
"terms_expired": false,
|
||||
"teams": [1, 2, 3]
|
||||
}
|
||||
```
|
||||
|
||||
### Renew VPP token
|
||||
|
||||
`PATCH /api/v1/fleet/vpp_tokens/:id/renew`
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | Type | In | Description |
|
||||
| ---- | ---- | -- | ----------- |
|
||||
| id | integer | path | *Required* The VPP token's ID |
|
||||
|
||||
##### Request header
|
||||
|
||||
```http
|
||||
Content-Length: 850
|
||||
Content-Type: multipart/form-data; boundary=------------------------f02md47480und42y
|
||||
```
|
||||
|
||||
##### Request body
|
||||
|
||||
```http
|
||||
--------------------------f02md47480und42y
|
||||
Content-Disposition: form-data; name="token"; filename="sToken_for_Acme.vpptoken"
|
||||
Content-Type: application/octet-stream
|
||||
|
||||
<TOKEN_DATA>
|
||||
|
||||
--------------------------f02md47480und42y
|
||||
```
|
||||
|
||||
##### Default response
|
||||
|
||||
`Status: 200`
|
||||
|
||||
```json
|
||||
"vpp_token": {
|
||||
"id": 1,
|
||||
"org_name": "Fleet Device Management Inc.",
|
||||
"location": "https://example.com/mdm/apple/mdm",
|
||||
"renew_date": "2025-10-20T00:00:00Z",
|
||||
"terms_expired": false,
|
||||
"teams": [1, 2, 3]
|
||||
}
|
||||
```
|
||||
|
||||
### Delete VPP token
|
||||
|
||||
`DELETE /api/v1/fleet/vpp_token/:id`
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | Type | In | Description |
|
||||
| ---- | ---- | -- | ----------- |
|
||||
| id | integer | path | *Required* The VPP token's ID |
|
||||
|
||||
#### Example
|
||||
|
||||
`DELETE /api/v1/fleet/vpp_tokens/1`
|
||||
|
||||
##### Default response
|
||||
|
||||
`Status: 204`
|
||||
|
||||
### Batch-apply MDM custom settings
|
||||
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ Render is a cloud hosting service that makes it easy to get up and running fast,
|
|||
|
||||
2. Give the Blueprint a unique name like `yourcompany-fleet`.
|
||||
|
||||
3. Click "**Apply.**" Render will provision your services, which should take less than five minutes.
|
||||
3. Click "**Deploy Blueprint.**" Render will provision your services, which should take less than five minutes.
|
||||
|
||||
4. Click the "**Dashboard**" tab in Render when provisioning is complete to see your new services.
|
||||
|
||||
|
|
|
|||
|
|
@ -878,9 +878,6 @@ None.
|
|||
"additional_queries": null
|
||||
},
|
||||
"mdm": {
|
||||
"apple_bm_default_team": "",
|
||||
"apple_bm_terms_expired": false,
|
||||
"enabled_and_configured": true,
|
||||
"windows_enabled_and_configured": true,
|
||||
"enable_disk_encryption": true,
|
||||
"macos_updates": {
|
||||
|
|
@ -1170,9 +1167,6 @@ Modifies the Fleet's configuration with the supplied information.
|
|||
"expiration": "0001-01-01T00:00:00Z"
|
||||
},
|
||||
"mdm": {
|
||||
"apple_bm_default_team": "",
|
||||
"apple_bm_terms_expired": false,
|
||||
"apple_bm_enabled_and_configured": false,
|
||||
"enabled_and_configured": false,
|
||||
"windows_enabled_and_configured": false,
|
||||
"enable_disk_encryption": true,
|
||||
|
|
@ -1694,7 +1688,6 @@ _Available in Fleet Premium._
|
|||
|
||||
| Name | Type | Description |
|
||||
| --------------------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| apple_bm_default_team | string | _Available in Fleet Premium._ The default team to use with Apple Business Manager. |
|
||||
| windows_enabled_and_configured | boolean | Enables Windows MDM support. |
|
||||
| enable_disk_encryption | boolean | _Available in Fleet Premium._ Hosts that belong to no team will have disk encryption enabled if set to true. |
|
||||
| macos_updates | object | See [`mdm.macos_updates`](#mdm-macos-updates). |
|
||||
|
|
@ -1811,7 +1804,6 @@ _Available in Fleet Premium._
|
|||
```json
|
||||
{
|
||||
"mdm": {
|
||||
"apple_bm_default_team": "",
|
||||
"windows_enabled_and_configured": false,
|
||||
"enable_disk_encryption": true,
|
||||
"macos_updates": {
|
||||
|
|
@ -6261,8 +6253,8 @@ This endpoint returns the list of custom MDM commands that have been executed.
|
|||
## Integrations
|
||||
|
||||
- [Get Apple Push Notification service (APNs)](#get-apple-push-notification-service-apns)
|
||||
- [Get Apple Business Manager (ABM)](#get-apple-business-manager-abm)
|
||||
- [Get Volume Purchasing Program (VPP)](#get-volume-purchasing-program-vpp)
|
||||
- [List Apple Business Manager (ABM) tokens](#list-apple-business-manager-abm-tokens)
|
||||
- [List Volume Purchasing Program (VPP) tokens](#list-volume-purchasing-program-vpp-tokens)
|
||||
|
||||
### Get Apple Push Notification service (APNs)
|
||||
|
||||
|
|
@ -6289,11 +6281,11 @@ None.
|
|||
}
|
||||
```
|
||||
|
||||
### Get Apple Business Manager (ABM)
|
||||
### List Apple Business Manager (ABM) tokens
|
||||
|
||||
_Available in Fleet Premium_
|
||||
|
||||
`GET /api/v1/fleet/abm`
|
||||
`GET /api/v1/fleet/abm_tokens`
|
||||
|
||||
#### Parameters
|
||||
|
||||
|
|
@ -6301,20 +6293,82 @@ None.
|
|||
|
||||
#### Example
|
||||
|
||||
`GET /api/v1/fleet/abm`
|
||||
`GET /api/v1/fleet/abm_tokens`
|
||||
|
||||
##### Default response
|
||||
|
||||
`Status: 200`
|
||||
|
||||
```json
|
||||
{
|
||||
"apple_id": "apple@example.com",
|
||||
"org_name": "Fleet Device Management",
|
||||
"mdm_server_url": "https://example.com/mdm/apple/mdm",
|
||||
"renew_date": "2023-11-29T00:00:00Z",
|
||||
"default_team": ""
|
||||
}
|
||||
"abm_tokens": [
|
||||
{
|
||||
"id": 1,
|
||||
"apple_id": "apple@example.com",
|
||||
"org_name": "Fleet Device Management Inc.",
|
||||
"mdm_server_url": "https://example.com/mdm/apple/mdm",
|
||||
"renew_date": "2023-11-29T00:00:00Z",
|
||||
"terms_expired": false,
|
||||
"macos_team": {
|
||||
"name": "💻 Workstations",
|
||||
"id" 1
|
||||
},
|
||||
"ios_team": {
|
||||
"name": "📱🏢 Company-owned iPhones",
|
||||
"id": 2
|
||||
},
|
||||
"ipados_team": {
|
||||
"name": "🔳🏢 Company-owned iPads",
|
||||
"id": 3
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### List Volume Purchasing Program (VPP) tokens
|
||||
|
||||
_Available in Fleet Premium_
|
||||
|
||||
`GET /api/v1/fleet/vpp_tokens`
|
||||
|
||||
#### Parameters
|
||||
|
||||
None.
|
||||
|
||||
#### Example
|
||||
|
||||
`GET /api/v1/fleet/vpp_tokens`
|
||||
|
||||
##### Default response
|
||||
|
||||
`Status: 200`
|
||||
|
||||
```json
|
||||
"vpp_tokens": [
|
||||
{
|
||||
"id": 1,
|
||||
"org_name": "Fleet Device Management Inc.",
|
||||
"location": "https://example.com/mdm/apple/mdm",
|
||||
"renew_date": "2023-11-29T00:00:00Z",
|
||||
"teams": [
|
||||
{
|
||||
"name": "💻 Workstations",
|
||||
"id": 1
|
||||
},
|
||||
{
|
||||
"name": "💻🐣 Workstations (canary)",
|
||||
"id": 2
|
||||
},
|
||||
{
|
||||
"name": "📱🏢 Company-owned iPhones",
|
||||
"id": 3
|
||||
},
|
||||
{
|
||||
"name": "🔳🏢 Company-owned iPads",
|
||||
"id" 4
|
||||
}
|
||||
],
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
Get Volume Purchasing Program (VPP)
|
||||
|
|
|
|||
|
|
@ -109,6 +109,7 @@ func setupMockDatastorePremiumService(t testing.TB) (*mock.Store, *eeservice.Ser
|
|||
nil,
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ type Service struct {
|
|||
softwareInstallStore fleet.SoftwareInstallerStore
|
||||
bootstrapPackageStore fleet.MDMBootstrapPackageStore
|
||||
distributedLock fleet.Lock
|
||||
keyValueStore fleet.KeyValueStore
|
||||
}
|
||||
|
||||
func NewService(
|
||||
|
|
@ -46,6 +47,7 @@ func NewService(
|
|||
softwareInstallStore fleet.SoftwareInstallerStore,
|
||||
bootstrapPackageStore fleet.MDMBootstrapPackageStore,
|
||||
distributedLock fleet.Lock,
|
||||
keyValueStore fleet.KeyValueStore,
|
||||
) (*Service, error) {
|
||||
authorizer, err := authz.NewAuthorizer()
|
||||
if err != nil {
|
||||
|
|
@ -67,6 +69,7 @@ func NewService(
|
|||
softwareInstallStore: softwareInstallStore,
|
||||
bootstrapPackageStore: bootstrapPackageStore,
|
||||
distributedLock: distributedLock,
|
||||
keyValueStore: keyValueStore,
|
||||
}
|
||||
|
||||
// Override methods that can't be easily overriden via
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import (
|
|||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/pkg/file"
|
||||
"github.com/fleetdm/fleet/v4/pkg/fleethttp"
|
||||
|
|
@ -24,6 +25,7 @@ import (
|
|||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
"github.com/fleetdm/fleet/v4/server/mdm/apple/vpp"
|
||||
"github.com/fleetdm/fleet/v4/server/ptr"
|
||||
"github.com/go-kit/log"
|
||||
kitlog "github.com/go-kit/log"
|
||||
"github.com/go-kit/log/level"
|
||||
"github.com/google/uuid"
|
||||
|
|
@ -1112,13 +1114,21 @@ func (svc *Service) addMetadataToSoftwarePayload(ctx context.Context, payload *f
|
|||
return meta.Extension, nil
|
||||
}
|
||||
|
||||
const maxInstallerSizeBytes int64 = 1024 * 1024 * 500
|
||||
const (
|
||||
maxInstallerSizeBytes int64 = 1024 * 1024 * 500
|
||||
batchSoftwarePrefix = "software_batch_"
|
||||
)
|
||||
|
||||
func (svc *Service) BatchSetSoftwareInstallers(
|
||||
ctx context.Context, tmName string, payloads []fleet.SoftwareInstallerPayload, dryRun bool,
|
||||
) ([]fleet.SoftwarePackageResponse, error) {
|
||||
) (string, error) {
|
||||
if err := svc.authz.Authorize(ctx, &fleet.Team{}, fleet.ActionRead); err != nil {
|
||||
return nil, err
|
||||
return "", err
|
||||
}
|
||||
|
||||
vc, ok := viewer.FromContext(ctx)
|
||||
if !ok {
|
||||
return "", fleet.ErrNoContext
|
||||
}
|
||||
|
||||
var teamID *uint
|
||||
|
|
@ -1127,32 +1137,153 @@ func (svc *Service) BatchSetSoftwareInstallers(
|
|||
if err != nil {
|
||||
// If this is a dry run, the team may not have been created yet
|
||||
if dryRun && fleet.IsNotFound(err) {
|
||||
return nil, nil
|
||||
return "", nil
|
||||
}
|
||||
return nil, err
|
||||
return "", err
|
||||
}
|
||||
teamID = &tm.ID
|
||||
}
|
||||
|
||||
if err := svc.authz.Authorize(ctx, &fleet.SoftwareInstaller{TeamID: teamID}, fleet.ActionWrite); err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "validating authorization")
|
||||
return "", ctxerr.Wrap(ctx, err, "validating authorization")
|
||||
}
|
||||
|
||||
// Verify payloads first, to prevent starting the download+upload process if the data is invalid.
|
||||
for _, payload := range payloads {
|
||||
if len(payload.URL) > fleet.SoftwareInstallerURLMaxLength {
|
||||
return nil, fleet.NewInvalidArgumentError(
|
||||
return "", fleet.NewInvalidArgumentError(
|
||||
"software.url",
|
||||
"software URL is too long, must be less than 256 characters",
|
||||
)
|
||||
}
|
||||
if _, err := url.ParseRequestURI(payload.URL); err != nil {
|
||||
return "", fleet.NewInvalidArgumentError(
|
||||
"software.url",
|
||||
fmt.Sprintf("Couldn't edit software. URL (%q) is invalid", payload.URL),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
vc, ok := viewer.FromContext(ctx)
|
||||
if !ok {
|
||||
return nil, fleet.ErrNoContext
|
||||
// keyExpireTime is the current maximum time supported for retrieving
|
||||
// the result of a software by batch operation.
|
||||
const keyExpireTime = 24 * time.Hour
|
||||
|
||||
requestUUID := uuid.NewString()
|
||||
if err := svc.keyValueStore.Set(ctx, batchSoftwarePrefix+requestUUID, batchSetProcessing, keyExpireTime); err != nil {
|
||||
return "", ctxerr.Wrapf(ctx, err, "failed to set key as %s", batchSetProcessing)
|
||||
}
|
||||
|
||||
g, workerCtx := errgroup.WithContext(ctx)
|
||||
svc.logger.Log(
|
||||
"msg", "software batch start",
|
||||
"request_uuid", requestUUID,
|
||||
"team_id", teamID,
|
||||
"payloads", len(payloads),
|
||||
)
|
||||
|
||||
go svc.softwareBatchUpload(
|
||||
requestUUID,
|
||||
teamID,
|
||||
vc.UserID(),
|
||||
payloads,
|
||||
dryRun,
|
||||
)
|
||||
|
||||
return requestUUID, nil
|
||||
}
|
||||
|
||||
const (
|
||||
batchSetProcessing = "processing"
|
||||
batchSetCompleted = "completed"
|
||||
batchSetFailedPrefix = "failed:"
|
||||
)
|
||||
|
||||
func (svc *Service) softwareBatchUpload(
|
||||
requestUUID string,
|
||||
teamID *uint,
|
||||
userID uint,
|
||||
payloads []fleet.SoftwareInstallerPayload,
|
||||
dryRun bool,
|
||||
) {
|
||||
var batchErr error
|
||||
|
||||
// We do not use the request ctx on purpose because this method runs in the background.
|
||||
ctx := context.Background()
|
||||
|
||||
defer func(start time.Time) {
|
||||
status := batchSetCompleted
|
||||
if batchErr != nil {
|
||||
status = fmt.Sprintf("%s%s", batchSetFailedPrefix, batchErr)
|
||||
}
|
||||
logger := log.With(svc.logger,
|
||||
"request_uuid", requestUUID,
|
||||
"team_id", teamID,
|
||||
"payloads", len(payloads),
|
||||
"status", status,
|
||||
"took", time.Since(start),
|
||||
)
|
||||
logger.Log("msg", "software batch done")
|
||||
// Give 10m for the client to read the result (it overrides the previos expiration time).
|
||||
if err := svc.keyValueStore.Set(ctx, batchSoftwarePrefix+requestUUID, status, 10*time.Minute); err != nil {
|
||||
logger.Log("msg", "failed to set result", "err", err)
|
||||
}
|
||||
}(time.Now())
|
||||
|
||||
downloadURLFn := func(ctx context.Context, url string) (http.Header, []byte, error) {
|
||||
client := fleethttp.NewClient()
|
||||
client.Transport = fleethttp.NewSizeLimitTransport(maxInstallerSizeBytes)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("creating request for URL %q: %w", url, err)
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
var maxBytesErr *http.MaxBytesError
|
||||
if errors.Is(err, fleethttp.ErrMaxSizeExceeded) || errors.As(err, &maxBytesErr) {
|
||||
return nil, nil, fleet.NewInvalidArgumentError(
|
||||
"software.url",
|
||||
fmt.Sprintf("Couldn't edit software. URL (%q). The maximum file size is %d MiB", url, maxInstallerSizeBytes/(1024*1024)),
|
||||
)
|
||||
}
|
||||
|
||||
return nil, nil, fmt.Errorf("performing request for URL %q: %w", url, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusNotFound {
|
||||
return nil, nil, fleet.NewInvalidArgumentError(
|
||||
"software.url",
|
||||
fmt.Sprintf("Couldn't edit software. URL (%q) returned \"Not Found\". Please make sure that URLs are reachable from your Fleet server.", url),
|
||||
)
|
||||
}
|
||||
|
||||
// Allow all 2xx and 3xx status codes in this pass.
|
||||
if resp.StatusCode >= 400 {
|
||||
return nil, nil, fleet.NewInvalidArgumentError(
|
||||
"software.url",
|
||||
fmt.Sprintf("Couldn't edit software. URL (%q) received response status code %d.", url, resp.StatusCode),
|
||||
)
|
||||
}
|
||||
|
||||
bodyBytes, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
// the max size error can be received either at client.Do or here when
|
||||
// reading the body if it's caught via a limited body reader.
|
||||
var maxBytesErr *http.MaxBytesError
|
||||
if errors.Is(err, fleethttp.ErrMaxSizeExceeded) || errors.As(err, &maxBytesErr) {
|
||||
return nil, nil, fleet.NewInvalidArgumentError(
|
||||
"software.url",
|
||||
fmt.Sprintf("Couldn't edit software. URL (%q). The maximum file size is %d MiB", url, maxInstallerSizeBytes/(1024*1024)),
|
||||
)
|
||||
}
|
||||
return nil, nil, fmt.Errorf("reading installer %q contents: %w", url, err)
|
||||
}
|
||||
|
||||
return resp.Header, bodyBytes, nil
|
||||
}
|
||||
|
||||
var g errgroup.Group
|
||||
g.SetLimit(3)
|
||||
// critical to avoid data race, the slice is pre-allocated and each
|
||||
// goroutine only writes to its index.
|
||||
|
|
@ -1162,63 +1293,9 @@ func (svc *Service) BatchSetSoftwareInstallers(
|
|||
i, p := i, p
|
||||
|
||||
g.Go(func() error {
|
||||
// validate the URL before doing the request
|
||||
_, err := url.ParseRequestURI(p.URL)
|
||||
headers, bodyBytes, err := downloadURLFn(ctx, p.URL)
|
||||
if err != nil {
|
||||
return fleet.NewInvalidArgumentError(
|
||||
"software.url",
|
||||
fmt.Sprintf("Couldn't edit software. URL (%q) is invalid", p.URL),
|
||||
)
|
||||
}
|
||||
client := fleethttp.NewClient()
|
||||
client.Transport = fleethttp.NewSizeLimitTransport(maxInstallerSizeBytes)
|
||||
|
||||
req, err := http.NewRequestWithContext(workerCtx, http.MethodGet, p.URL, nil)
|
||||
if err != nil {
|
||||
return ctxerr.Wrapf(ctx, err, "creating request for URL %s", p.URL)
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
var maxBytesErr *http.MaxBytesError
|
||||
if errors.Is(err, fleethttp.ErrMaxSizeExceeded) || errors.As(err, &maxBytesErr) {
|
||||
return fleet.NewInvalidArgumentError(
|
||||
"software.url",
|
||||
fmt.Sprintf("Couldn't edit software. URL (%q). The maximum file size is %d MB", p.URL, maxInstallerSizeBytes/(1024*1024)),
|
||||
)
|
||||
}
|
||||
|
||||
return ctxerr.Wrapf(ctx, err, "performing request for URL %s", p.URL)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusNotFound {
|
||||
return fleet.NewInvalidArgumentError(
|
||||
"software.url",
|
||||
fmt.Sprintf("Couldn't edit software. URL (%q) doesn't exist. Please make sure that URLs are publicy accessible to the internet.", p.URL),
|
||||
)
|
||||
}
|
||||
|
||||
// Allow all 2xx and 3xx status codes in this pass.
|
||||
if resp.StatusCode > 400 {
|
||||
return fleet.NewInvalidArgumentError(
|
||||
"software.url",
|
||||
fmt.Sprintf("Couldn't edit software. URL (%q) received response status code %d.", p.URL, resp.StatusCode),
|
||||
)
|
||||
}
|
||||
|
||||
bodyBytes, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
// the max size error can be received either at client.Do or here when
|
||||
// reading the body if it's caught via a limited body reader.
|
||||
var maxBytesErr *http.MaxBytesError
|
||||
if errors.Is(err, fleethttp.ErrMaxSizeExceeded) || errors.As(err, &maxBytesErr) {
|
||||
return fleet.NewInvalidArgumentError(
|
||||
"software.url",
|
||||
fmt.Sprintf("Couldn't edit software. URL (%q). The maximum file size is %d MB", p.URL, maxInstallerSizeBytes/(1024*1024)),
|
||||
)
|
||||
}
|
||||
return ctxerr.Wrapf(ctx, err, "reading installer %q contents", p.URL)
|
||||
return err
|
||||
}
|
||||
|
||||
installer := &fleet.UploadSoftwareInstallerPayload{
|
||||
|
|
@ -1229,13 +1306,13 @@ func (svc *Service) BatchSetSoftwareInstallers(
|
|||
UninstallScript: p.UninstallScript,
|
||||
InstallerFile: bytes.NewReader(bodyBytes),
|
||||
SelfService: p.SelfService,
|
||||
UserID: vc.UserID(),
|
||||
UserID: userID,
|
||||
URL: p.URL,
|
||||
}
|
||||
|
||||
// set the filename before adding metadata, as it is used as fallback
|
||||
var filename string
|
||||
cdh, ok := resp.Header["Content-Disposition"]
|
||||
cdh, ok := headers["Content-Disposition"]
|
||||
if ok && len(cdh) > 0 {
|
||||
_, params, err := mime.ParseMediaType(cdh[0])
|
||||
if err == nil {
|
||||
|
|
@ -1273,30 +1350,88 @@ func (svc *Service) BatchSetSoftwareInstallers(
|
|||
}
|
||||
|
||||
if err := g.Wait(); err != nil {
|
||||
// NOTE: intentionally not wrapping to avoid polluting user
|
||||
// errors.
|
||||
return nil, err
|
||||
// NOTE: intentionally not wrapping to avoid polluting user errors.
|
||||
batchErr = err
|
||||
return
|
||||
}
|
||||
|
||||
if dryRun {
|
||||
return nil, nil
|
||||
return
|
||||
}
|
||||
|
||||
for _, payload := range installers {
|
||||
if err := svc.storeSoftware(ctx, payload); err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "storing software installer")
|
||||
batchErr = fmt.Errorf("storing software installer %q: %w", payload.Filename, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
insertedSoftwareInstallers, err := svc.ds.BatchSetSoftwareInstallers(ctx, teamID, installers)
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "batch set software installers")
|
||||
if err := svc.ds.BatchSetSoftwareInstallers(ctx, teamID, installers); err != nil {
|
||||
batchErr = fmt.Errorf("batch set software installers: %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Note: per @noahtalerman we don't want activity items for CLI actions
|
||||
// anymore, so that's intentionally skipped.
|
||||
}
|
||||
|
||||
return insertedSoftwareInstallers, nil
|
||||
func (svc *Service) GetBatchSetSoftwareInstallersResult(ctx context.Context, tmName string, requestUUID string, dryRun bool) (string, string, []fleet.SoftwarePackageResponse, error) {
|
||||
// We've already authorized in the POST /api/latest/fleet/software/batch,
|
||||
// but adding it here so we don't need to worry about a special case endpoint.
|
||||
if err := svc.authz.Authorize(ctx, &fleet.Team{}, fleet.ActionRead); err != nil {
|
||||
return "", "", nil, err
|
||||
}
|
||||
|
||||
result, err := svc.keyValueStore.Get(ctx, batchSoftwarePrefix+requestUUID)
|
||||
if err != nil {
|
||||
return "", "", nil, ctxerr.Wrap(ctx, err, "failed to get result")
|
||||
}
|
||||
if result == nil {
|
||||
return "", "", nil, ctxerr.Wrap(ctx, notFoundError{}, "request_uuid not found")
|
||||
}
|
||||
|
||||
switch {
|
||||
case *result == batchSetCompleted:
|
||||
if dryRun {
|
||||
return fleet.BatchSetSoftwareInstallersStatusCompleted, "", nil, nil
|
||||
} // this will fall through to retrieving software packages if not a dry run.
|
||||
case *result == batchSetProcessing:
|
||||
return fleet.BatchSetSoftwareInstallersStatusProcessing, "", nil, nil
|
||||
case strings.HasPrefix(*result, batchSetFailedPrefix):
|
||||
message := strings.TrimPrefix(*result, batchSetFailedPrefix)
|
||||
return fleet.BatchSetSoftwareInstallersStatusFailed, message, nil, nil
|
||||
default:
|
||||
return "", "", nil, ctxerr.New(ctx, "invalid status")
|
||||
}
|
||||
|
||||
var (
|
||||
teamID uint // GetSoftwareInstallers uses 0 for "No team"
|
||||
ptrTeamID *uint // Authorize uses *uint for "No team" teamID
|
||||
)
|
||||
if tmName != "" {
|
||||
team, err := svc.ds.TeamByName(ctx, tmName)
|
||||
if err != nil {
|
||||
return "", "", nil, ctxerr.Wrap(ctx, err, "load team by name")
|
||||
}
|
||||
teamID = team.ID
|
||||
ptrTeamID = &team.ID
|
||||
}
|
||||
|
||||
// We've already authorized in the POST /api/latest/fleet/software/batch,
|
||||
// but adding it here so we don't need to worry about a special case endpoint.
|
||||
//
|
||||
// We use fleet.ActionWrite because this method is the counterpart of the POST
|
||||
// /api/latest/fleet/software/batch.
|
||||
if err := svc.authz.Authorize(ctx, &fleet.SoftwareInstaller{TeamID: ptrTeamID}, fleet.ActionWrite); err != nil {
|
||||
return "", "", nil, ctxerr.Wrap(ctx, err, "validating authorization")
|
||||
}
|
||||
|
||||
softwarePackages, err := svc.ds.GetSoftwareInstallers(ctx, teamID)
|
||||
if err != nil {
|
||||
return "", "", nil, ctxerr.Wrap(ctx, err, "get software installers")
|
||||
}
|
||||
|
||||
return fleet.BatchSetSoftwareInstallersStatusCompleted, "", softwarePackages, nil
|
||||
}
|
||||
|
||||
func (svc *Service) SelfServiceInstallSoftwareTitle(ctx context.Context, host *fleet.Host, softwareTitleID uint) error {
|
||||
|
|
|
|||
|
|
@ -166,18 +166,6 @@ const AppleOSTargetForm = ({
|
|||
setDeadline(val);
|
||||
};
|
||||
|
||||
const getMinimumVersionPlaceholder = (platform: ApplePlatform) => {
|
||||
switch (platform) {
|
||||
case "darwin":
|
||||
return "13.0.1";
|
||||
case "ios":
|
||||
case "ipados":
|
||||
return "17.5.1";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
||||
const getMinimumVersionTooltip = () => {
|
||||
return (
|
||||
<>
|
||||
|
|
@ -210,7 +198,6 @@ const AppleOSTargetForm = ({
|
|||
label="Minimum version"
|
||||
tooltip={getMinimumVersionTooltip()}
|
||||
helpText="Version number only (e.g., “13.0.1” not “Ventura 13” or “13.0.1 (22A400)”)"
|
||||
placeholder={getMinimumVersionPlaceholder(applePlatform)}
|
||||
value={minOsVersion}
|
||||
error={minOsVersionError}
|
||||
onChange={handleMinVersionChange}
|
||||
|
|
@ -219,7 +206,6 @@ const AppleOSTargetForm = ({
|
|||
label="Deadline"
|
||||
tooltip={getDeadlineTooltip(applePlatform)}
|
||||
helpText="YYYY-MM-DD format only (e.g., “2024-07-01”)."
|
||||
placeholder="2024-07-01"
|
||||
value={deadline}
|
||||
error={deadlineError}
|
||||
onChange={handleDeadlineChange}
|
||||
|
|
|
|||
|
|
@ -158,7 +158,6 @@ const WindowsTargetForm = ({
|
|||
label="Deadline"
|
||||
tooltip="Number of days the end user has before updates are installed and the host is forced to restart."
|
||||
helpText="Number of days from 0 to 30."
|
||||
placeholder="5"
|
||||
value={deadlineDays}
|
||||
error={deadlineDaysError}
|
||||
onChange={handleDeadlineDaysChange}
|
||||
|
|
@ -167,7 +166,6 @@ const WindowsTargetForm = ({
|
|||
label="Grace period"
|
||||
tooltip="Number of days after the deadline the end user has before the host is forced to restart (only if end user was offline when deadline passed)."
|
||||
helpText="Number of days from 0 to 7."
|
||||
placeholder="2"
|
||||
value={gracePeriodDays}
|
||||
error={gracePeriodDaysError}
|
||||
onChange={handleGracePeriodDays}
|
||||
|
|
|
|||
|
|
@ -282,6 +282,36 @@ const removeUnavailableOptions = (
|
|||
return options;
|
||||
};
|
||||
|
||||
// Available tooltips for disabled options
|
||||
export const getDropdownOptionTooltipContent = (
|
||||
value: string | number,
|
||||
isHostOnline?: boolean
|
||||
) => {
|
||||
const tooltipAction: Record<string, string> = {
|
||||
runScript: "run scripts on",
|
||||
wipe: "wipe",
|
||||
lock: "lock",
|
||||
unlock: "unlock",
|
||||
installSoftware: "install software on", // Host software dropdown option
|
||||
uninstallSoftware: "uninstall software on", // Host software dropdown option
|
||||
};
|
||||
if (tooltipAction[value]) {
|
||||
return (
|
||||
<>
|
||||
To {tooltipAction[value]} this host, deploy the
|
||||
<br />
|
||||
fleetd agent with --enable-scripts and
|
||||
<br />
|
||||
refetch host vitals
|
||||
</>
|
||||
);
|
||||
}
|
||||
if (!isHostOnline && value === "query") {
|
||||
return <>You can't query an offline host.</>;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const modifyOptions = (
|
||||
options: IDropdownOption[],
|
||||
{
|
||||
|
|
@ -291,34 +321,13 @@ const modifyOptions = (
|
|||
hostPlatform,
|
||||
}: IHostActionConfigOptions
|
||||
) => {
|
||||
// Available tooltips for disabled options
|
||||
const getDropdownOptionTooltipContent = (value: string | number) => {
|
||||
const tooltipAction: Record<string, string> = {
|
||||
runScript: "run scripts on",
|
||||
wipe: "wipe",
|
||||
lock: "lock",
|
||||
unlock: "unlock",
|
||||
};
|
||||
if (tooltipAction[value]) {
|
||||
return (
|
||||
<>
|
||||
To {tooltipAction[value]} this host, deploy the
|
||||
<br />
|
||||
fleetd agent with --enable-scripts and
|
||||
<br />
|
||||
refetch host vitals
|
||||
</>
|
||||
);
|
||||
}
|
||||
if (!isHostOnline && value === "query") {
|
||||
return <>You can't query an offline host.</>;
|
||||
}
|
||||
};
|
||||
|
||||
const disableOptions = (optionsToDisable: IDropdownOption[]) => {
|
||||
optionsToDisable.forEach((option) => {
|
||||
option.disabled = true;
|
||||
option.tooltipContent = getDropdownOptionTooltipContent(option.value);
|
||||
option.tooltipContent = getDropdownOptionTooltipContent(
|
||||
option.value,
|
||||
isHostOnline
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -946,6 +946,7 @@ const HostDetailsPage = ({
|
|||
platform={host.platform}
|
||||
softwareUpdatedAt={host.software_updated_at}
|
||||
hostCanWriteSoftware={!!host.orbit_version || isIosOrIpadosHost}
|
||||
hostScriptsEnabled={host.scripts_enabled || false}
|
||||
isSoftwareEnabled={featuresConfig?.enable_software_inventory}
|
||||
router={router}
|
||||
queryParams={parseHostSoftwareQueryParams(location.query)}
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ interface IHostSoftwareProps {
|
|||
hostTeamId: number;
|
||||
onShowSoftwareDetails?: (software: IHostSoftware) => void;
|
||||
isSoftwareEnabled?: boolean;
|
||||
hostScriptsEnabled?: boolean;
|
||||
isMyDevicePage?: boolean;
|
||||
}
|
||||
|
||||
|
|
@ -87,6 +88,7 @@ const HostSoftware = ({
|
|||
platform,
|
||||
softwareUpdatedAt,
|
||||
hostCanWriteSoftware,
|
||||
hostScriptsEnabled,
|
||||
router,
|
||||
queryParams,
|
||||
pathname,
|
||||
|
|
@ -249,6 +251,7 @@ const HostSoftware = ({
|
|||
router,
|
||||
softwareIdActionPending,
|
||||
userHasSWWritePermission,
|
||||
hostScriptsEnabled,
|
||||
onSelectAction,
|
||||
teamId: hostTeamId,
|
||||
hostCanWriteSoftware,
|
||||
|
|
@ -258,6 +261,7 @@ const HostSoftware = ({
|
|||
router,
|
||||
softwareIdActionPending,
|
||||
userHasSWWritePermission,
|
||||
hostScriptsEnabled,
|
||||
onSelectAction,
|
||||
hostTeamId,
|
||||
hostCanWriteSoftware,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,83 @@
|
|||
import {
|
||||
generateActions,
|
||||
DEFAULT_ACTION_OPTIONS,
|
||||
generateActionsProps,
|
||||
} from "./HostSoftwareTableConfig";
|
||||
|
||||
describe("generateActions", () => {
|
||||
const defaultProps: generateActionsProps = {
|
||||
userHasSWWritePermission: true,
|
||||
hostScriptsEnabled: true,
|
||||
hostCanWriteSoftware: true,
|
||||
softwareIdActionPending: null,
|
||||
softwareId: 1,
|
||||
status: null,
|
||||
software_package: null,
|
||||
app_store_app: null,
|
||||
};
|
||||
|
||||
it("returns default actions when user has write permission and scripts are enabled", () => {
|
||||
const actions = generateActions(defaultProps);
|
||||
expect(actions).toEqual(DEFAULT_ACTION_OPTIONS);
|
||||
});
|
||||
|
||||
it("removes install and uninstall actions when user has no write permission", () => {
|
||||
const props = { ...defaultProps, userHasSWWritePermission: false };
|
||||
const actions = generateActions(props);
|
||||
expect(actions.find((a) => a.value === "install")).toBeUndefined();
|
||||
expect(actions.find((a) => a.value === "uninstall")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("disables install and uninstall actions when host scripts are disabled", () => {
|
||||
const props = { ...defaultProps, hostScriptsEnabled: false };
|
||||
const actions = generateActions(props);
|
||||
expect(actions.find((a) => a.value === "install")?.disabled).toBe(true);
|
||||
expect(actions.find((a) => a.value === "uninstall")?.disabled).toBe(true);
|
||||
});
|
||||
|
||||
it("disables install and uninstall actions when locally pending (waiting for API response)", () => {
|
||||
const props = {
|
||||
...defaultProps,
|
||||
softwareIdActionPending: 1,
|
||||
softwareId: 1,
|
||||
};
|
||||
const actions = generateActions(props);
|
||||
expect(actions.find((a) => a.value === "install")?.disabled).toBe(true);
|
||||
expect(actions.find((a) => a.value === "uninstall")?.disabled).toBe(true);
|
||||
});
|
||||
|
||||
it("disables install and uninstall actions when pending install status", () => {
|
||||
const props: generateActionsProps = {
|
||||
...defaultProps,
|
||||
status: "pending_install",
|
||||
};
|
||||
const actions = generateActions(props);
|
||||
expect(actions.find((a) => a.value === "install")?.disabled).toBe(true);
|
||||
expect(actions.find((a) => a.value === "uninstall")?.disabled).toBe(true);
|
||||
});
|
||||
|
||||
it("disables install and uninstall actions when pending uninstall status", () => {
|
||||
const props: generateActionsProps = {
|
||||
...defaultProps,
|
||||
status: "pending_uninstall",
|
||||
};
|
||||
const actions = generateActions(props);
|
||||
expect(actions.find((a) => a.value === "install")?.disabled).toBe(true);
|
||||
expect(actions.find((a) => a.value === "uninstall")?.disabled).toBe(true);
|
||||
});
|
||||
|
||||
it("removes uninstall action for VPP apps", () => {
|
||||
const props: generateActionsProps = {
|
||||
...defaultProps,
|
||||
app_store_app: {
|
||||
app_store_id: "1",
|
||||
self_service: false,
|
||||
icon_url: "",
|
||||
version: "",
|
||||
last_install: { command_uuid: "", installed_at: "" },
|
||||
},
|
||||
};
|
||||
const actions = generateActions(props);
|
||||
expect(actions.find((a) => a.value === "uninstall")).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
|
@ -29,8 +29,9 @@ import VersionCell from "pages/SoftwarePage/components/VersionCell";
|
|||
import { getVulnerabilities } from "pages/SoftwarePage/SoftwareTitles/SoftwareTable/SoftwareTitlesTableConfig";
|
||||
|
||||
import InstallStatusCell from "./InstallStatusCell";
|
||||
import { getDropdownOptionTooltipContent } from "../../HostDetailsPage/HostActionsDropdown/helpers";
|
||||
|
||||
const DEFAULT_ACTION_OPTIONS: IDropdownOption[] = [
|
||||
export const DEFAULT_ACTION_OPTIONS: IDropdownOption[] = [
|
||||
{ value: "showDetails", label: "Show details", disabled: false },
|
||||
{ value: "install", label: "Install", disabled: false },
|
||||
{ value: "uninstall", label: "Uninstall", disabled: false },
|
||||
|
|
@ -50,24 +51,25 @@ type IInstalledVersionsCellProps = CellProps<
|
|||
>;
|
||||
type IVulnerabilitiesCellProps = IInstalledVersionsCellProps;
|
||||
|
||||
const generateActions = ({
|
||||
userHasSWWritePermission,
|
||||
// Commenting below in case there is a quick decision to use these conditions after all
|
||||
// hostCanWriteSoftware,
|
||||
// software_package,
|
||||
softwareIdActionPending,
|
||||
softwareId,
|
||||
status,
|
||||
app_store_app,
|
||||
}: {
|
||||
export interface generateActionsProps {
|
||||
userHasSWWritePermission: boolean;
|
||||
hostScriptsEnabled: boolean;
|
||||
hostCanWriteSoftware: boolean;
|
||||
softwareIdActionPending: number | null;
|
||||
softwareId: number;
|
||||
status: SoftwareInstallStatus | null;
|
||||
software_package: IHostSoftwarePackage | null;
|
||||
app_store_app: IHostAppStoreApp | null;
|
||||
}) => {
|
||||
}
|
||||
|
||||
export const generateActions = ({
|
||||
userHasSWWritePermission,
|
||||
hostScriptsEnabled,
|
||||
softwareIdActionPending,
|
||||
softwareId,
|
||||
status,
|
||||
app_store_app,
|
||||
}: generateActionsProps) => {
|
||||
// this gives us a clean slate of the default actions so we can modify
|
||||
// the options.
|
||||
const actions = cloneDeep(DEFAULT_ACTION_OPTIONS);
|
||||
|
|
@ -88,15 +90,29 @@ const generateActions = ({
|
|||
}
|
||||
|
||||
if (!userHasSWWritePermission) {
|
||||
actions.splice(indexInstallAction, 1);
|
||||
// Reverse order to not change index of subsequent array element before removal
|
||||
actions.splice(indexUninstallAction, 1);
|
||||
actions.splice(indexInstallAction, 1);
|
||||
} else {
|
||||
// if host's scripts are disabled, disable install/uninstall with tooltip
|
||||
if (!hostScriptsEnabled) {
|
||||
actions[indexInstallAction].disabled = true;
|
||||
actions[indexUninstallAction].disabled = true;
|
||||
|
||||
actions[
|
||||
indexInstallAction
|
||||
].tooltipContent = getDropdownOptionTooltipContent("installSoftware");
|
||||
actions[
|
||||
indexUninstallAction
|
||||
].tooltipContent = getDropdownOptionTooltipContent("uninstallSoftware");
|
||||
}
|
||||
|
||||
// user has software write permission for host
|
||||
const pendingStatuses = ["pending_install", "pending_uninstall"];
|
||||
|
||||
// if locally pending (waiting for API response) or pending install/uninstall,
|
||||
// disable both install and uninstall
|
||||
if (
|
||||
// if locally pending (waiting for API response) or pending install/uninstall, disable both
|
||||
// install and uninstall
|
||||
softwareId === softwareIdActionPending ||
|
||||
pendingStatuses.includes(status || "")
|
||||
) {
|
||||
|
|
@ -114,6 +130,7 @@ const generateActions = ({
|
|||
|
||||
interface ISoftwareTableHeadersProps {
|
||||
userHasSWWritePermission: boolean;
|
||||
hostScriptsEnabled?: boolean;
|
||||
hostCanWriteSoftware: boolean;
|
||||
softwareIdActionPending: number | null;
|
||||
router: InjectedRouter;
|
||||
|
|
@ -125,6 +142,7 @@ interface ISoftwareTableHeadersProps {
|
|||
// more info here https://react-table.tanstack.com/docs/api/useTable#cell-properties
|
||||
export const generateSoftwareTableHeaders = ({
|
||||
userHasSWWritePermission,
|
||||
hostScriptsEnabled = false,
|
||||
hostCanWriteSoftware,
|
||||
softwareIdActionPending,
|
||||
router,
|
||||
|
|
@ -217,6 +235,7 @@ export const generateSoftwareTableHeaders = ({
|
|||
placeholder="Actions"
|
||||
options={generateActions({
|
||||
userHasSWWritePermission,
|
||||
hostScriptsEnabled,
|
||||
hostCanWriteSoftware,
|
||||
softwareIdActionPending,
|
||||
softwareId,
|
||||
|
|
|
|||
|
|
@ -49,7 +49,12 @@ interface IQueryDetailsPageProps {
|
|||
params: Params;
|
||||
location: {
|
||||
pathname: string;
|
||||
query: { team_id?: string; order_key?: string; order_direction?: string };
|
||||
query: {
|
||||
team_id?: string;
|
||||
order_key?: string;
|
||||
order_direction?: string;
|
||||
host_id?: string;
|
||||
};
|
||||
search: string;
|
||||
};
|
||||
}
|
||||
|
|
@ -67,6 +72,12 @@ const QueryDetailsPage = ({
|
|||
}
|
||||
const queryParams = location.query;
|
||||
|
||||
// Present when observer is redirected from host details > query
|
||||
// since observer does not have access to edit page
|
||||
const hostId = queryParams?.host_id
|
||||
? parseInt(queryParams.host_id, 10)
|
||||
: undefined;
|
||||
|
||||
const { currentTeamId } = useTeamIdParam({
|
||||
location,
|
||||
router,
|
||||
|
|
@ -295,7 +306,7 @@ const QueryDetailsPage = ({
|
|||
onClick={() => {
|
||||
queryId &&
|
||||
router.push(
|
||||
PATHS.LIVE_QUERY(queryId, currentTeamId)
|
||||
PATHS.LIVE_QUERY(queryId, currentTeamId, hostId)
|
||||
);
|
||||
}}
|
||||
disabled={isLiveQueryDisabled}
|
||||
|
|
|
|||
|
|
@ -208,7 +208,14 @@ const EditQueryPage = ({
|
|||
queryId > 0 &&
|
||||
!canEditExistingQuery
|
||||
) {
|
||||
router.push(PATHS.QUERY_DETAILS(queryId));
|
||||
// Reroute to query report page still maintains query params for live query purposes
|
||||
const baseUrl = PATHS.QUERY_DETAILS(queryId);
|
||||
const queryParams = buildQueryStringFromParams({
|
||||
host_id: location.query.host_id,
|
||||
team_id: location.query.team_id,
|
||||
});
|
||||
|
||||
router.push(queryParams ? `${baseUrl}?${queryParams}` : baseUrl);
|
||||
}
|
||||
}, [queryId, isTeamMaintainerOrTeamAdmin, isStoredQueryLoading]);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { buildQueryStringFromParams } from "utilities/url";
|
||||
|
||||
import { IPolicy } from "../interfaces/policy";
|
||||
import URL_PREFIX from "./url_prefix";
|
||||
|
||||
|
|
@ -100,10 +102,17 @@ export default {
|
|||
teamId ? `?team_id=${teamId}` : ""
|
||||
}`;
|
||||
},
|
||||
LIVE_QUERY: (queryId: number | null, teamId?: number): string => {
|
||||
return `${URL_PREFIX}/queries/${queryId || "new"}/live${
|
||||
teamId ? `?team_id=${teamId}` : ""
|
||||
}`;
|
||||
LIVE_QUERY: (
|
||||
queryId: number | null,
|
||||
teamId?: number,
|
||||
hostId?: number
|
||||
): string => {
|
||||
const baseUrl = `${URL_PREFIX}/queries/${queryId || "new"}/live`;
|
||||
const queryParams = buildQueryStringFromParams({
|
||||
team_id: teamId,
|
||||
host_id: hostId,
|
||||
});
|
||||
return queryParams ? `${baseUrl}?${queryParams}` : baseUrl;
|
||||
},
|
||||
QUERY_DETAILS: (queryId: number, teamId?: number): string => {
|
||||
return `${URL_PREFIX}/queries/${queryId}${
|
||||
|
|
|
|||
|
|
@ -1359,10 +1359,10 @@ Each heading needs two lines of empty space separating it from the previous sect
|
|||
|
||||
```
|
||||
...previous content.
|
||||
|
||||
|
||||
<!-- Empty space -->
|
||||
<!-- Empty space -->
|
||||
### New heading
|
||||
|
||||
<!-- Empty space -->
|
||||
Related content...
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -360,54 +360,16 @@ After receiving the interview packet, the Head of Digital Experience uses the fo
|
|||
4. **Send offer:** 🐈⬛ CEO reviews and sends the offer to the candidate:
|
||||
- _Grant the candidate "edit" access_ to their "exit scenarios" spreadsheet.
|
||||
- _Send_ the email.
|
||||
5. **Process the offer response** The Head of Digital Experience will process the offer response by either creating a new ["Teammate pre-onboarding" issue](https://github.com/fleetdm/confidential/issues/new?assignees=&labels=%23g-digital-experience&projects=&template=pre-onboarding.md&title=Pre-onboarding%3A+______________________) and following the steps if the offer is accepted or notifying the stakeholders that the offer was not accepted and we should continue the search.
|
||||
|
||||
#### Steps after an offer is accepted
|
||||
Once the new team member replies and accepts their offer in writing, 🌐 Head of Digital Experience follows these steps:
|
||||
1. **Verify, track, and reply:** Reply to the candidate:
|
||||
- _Verify the candidate replied with their physical address… or else keep asking._ If they did not reply with their physical address, then we are not done. No offer is "accepted" until we've received a physical address.
|
||||
- _Review and update the team database_ to be sure everything is accurate, **one last time**. Remember to read the column headers and precisely follow the instructions about how to format the data:
|
||||
- The new team member's role in ["🧑🚀 Fleeties"](https://docs.google.com/spreadsheets/d/1OSLn-ZCbGSjPusHPiR5dwQhheH1K8-xqyZdsOe9y7qc/edit#gid=0) now includes:
|
||||
- **Start date** _(The new fleetie's first day, YYYY-MM-DD)_
|
||||
- **Location** _(Derive this from the physical address)_
|
||||
- **GitHub username** _(Username of 2FA-enabled GitHub account)_
|
||||
- **@fleetdm.com email** _(Set this to whatever email you think this person should have)_
|
||||
- The new team member's row in ["🥧 Equity plan"](https://docs.google.com/spreadsheets/d/1_GJlqnWWIQBiZFOoyl9YbTr72bg5qdSSp4O3kuKm1Jc/edit#gid=0) now includes:
|
||||
- **OTE** _("On-target earnings", i.e. anticipated total annual cash compensation)_
|
||||
- **Equity** _(Stock options)_
|
||||
- **"Notes"** _(Track base salary here, as well as a very short explanation of commission or bonus structure.)_
|
||||
- **Physical address** _(The full street address of the location where work will typically be performed.)_
|
||||
- **Personal email** _(Use the personal email they're replying from, e.g. `@gmail.com`)_
|
||||
- **"Offer accepted?"** _(Set this to `TRUE`)_
|
||||
- _[Create a "Hiring" issue](https://github.com/fleetdm/confidential/issues/new/choose)_ for the new team member. (This issue will keep track of the hiring tasks for the new team member.)
|
||||
- _Send a reply_ welcoming the team member to Fleet and letting them know to expect a separate email with next steps for getting the team member's laptop, Yubikeys, and agreement going ASAP so they can start on time. For example:
|
||||
#### After an offer is accepted
|
||||
|
||||
```
|
||||
\o/ It's official!
|
||||
|
||||
Be on the lookout for an email in a separate thread with next steps for quickly signing the paperwork and getting your company laptop and hardware 2FA keys (Yubikeys), which we recommend setting up ASAP.
|
||||
|
||||
Thanks, and welcome to the team!
|
||||
|
||||
Cheers,
|
||||
Sam
|
||||
```
|
||||
|
||||
2. **Ask hiring manager to send rejections:** Post to the `hiring-xxxxx-yyyy` Slack channel to let folks know the offer was accepted, and at-mention the _hiring manager_ using the following template:
|
||||
|
||||
```
|
||||
@HIRING_MANAGER, :astronaut: TEAM_MEMBER_NAME has accepted the offer :fleet: and this position is now filled :white_check_mark:. Please inform any other interviewees who are still in the running and let them know that we chose a different person. :thankyou-ty:
|
||||
```
|
||||
|
||||
3. **Close Slack channel:** Then archive the channel.
|
||||
|
||||
>_**Note:** Send rejection emails quickly, within 1 business day. It only gets harder if you wait._
|
||||
4. **Remove open position:** Ensure the hiring manager removes the newly-filled position from the fleetdm.com website by [making a pull request](https://fleetdm.com/handbook/company/communications#making-a-pull-request) to delete it from the [open-positions.yml](https://github.com/fleetdm/fleet/blob/main/handbook/company/open-positions.yml) file.
|
||||
5. **Create 30-60-90 day plan:** 🧑🚀 Hiring manager creates a 30-60-90 day plan outlining key role objectives. The plan is reviewed weekly in 1:1 meetings during the first three months of employment, ensuring continuous support and alignment with company goals. To create the 30-60-90 day plan, hiring manager will:
|
||||
- Create a copy of the [30-60-90 day plan template](https://docs.google.com/document/d/1EztmPBuMFXbVoy4ZToXcxasNO38ooOh8Gh5hPXFvJhI/copy) and rename the copied file using the naming convention `[start date] - 30-60-90 day plan - [teammate full name]` and move it to the [30-60-90 day plan folder](https://drive.google.com/drive/u/0/folders/1QWiAbgBFuuofT_3M8oIoBsbEBmubQAj7) in Google Drive.
|
||||
- Follow the prompts in the template to fill out the 30-60-90 day plan for the new teammate before they start.
|
||||
The Head of Digital Experience will then follow the steps in the ["Teammate pre-onboarding"](https://github.com/fleetdm/confidential/issues/new?assignees=&labels=%23g-digital-experience&projects=&template=pre-onboarding.md&title=Pre-onboarding%3A+______________________) issue, which includes reaching out to the new team member within 1 business day from a separate email thread to get additional information as needed, prepare their agreement, add them to the company's payroll system, and get their new laptop and hardware security keys ordered so that everything is ready for them to start on their first day.
|
||||
|
||||
|
||||
Now what happens? 🌐 Head of Digital Experience will then follow the steps in the "Hiring" issue, which includes reaching out to the new team member within 1 business day from a separate email thread to get additional information as needed, prepare their agreement, add them to the company's payroll system, and get their new laptop and hardware security keys ordered so that everything is ready for them to start on their first day.
|
||||
## Create a 30-60-90 day plan
|
||||
|
||||
The hiring manager creates a 30-60-90 day plan outlining key role objectives to be reviewed in 1:1 meetings during the first three months of employment. To create the 30-60-90 day plan, use the prompts in the "Vision" section of the new teammates [1:1 meeting doc (TEMPLATE)](https://docs.google.com/document/d/1IkGQJ4PPU0MyW35Xo8BuvoUPKpStsmcw_nHWt71W2yE/edit#heading=h.uzxntzlyyaou) to ensure continuous support and alignment with company goals.
|
||||
|
||||
|
||||
## CEO shadow program
|
||||
|
|
|
|||
|
|
@ -36,9 +36,9 @@ At Fleet, like [GitLab](https://about.gitlab.com/handbook/product-development-fl
|
|||
- **Ready.** Use this page to communicate designs reviews and development.
|
||||
- **Scratchpad.** Use this page for work in progress and design that might be useful in the future.
|
||||
|
||||
- If the story requires API or YAML file changes, open a draft PR with the proposed design.
|
||||
- If the story requires API or YAML file changes, open a pull request to the reference docs release branch (e.g. `docs-v4.58.0`) with the proposed design. Mark the PR ready for review as soon as it's ready for feedback from the [API design DRI](https://fleetdm.com/handbook/company/communications#directly-responsible-individuals-dris).
|
||||
|
||||
- Add links to the Figma file's cover page and draft PRs in the user story.
|
||||
- Add links to the user story as specified in the [issue template](https://github.com/fleetdm/fleet/issues/new?template=story.md).
|
||||
|
||||
- Draft changes to the Fleet product that solve the problem specified in the story. Constantly place yourself in the shoes of a user while drafting changes. Place these drafts in the appropriate Figma file in Fleet product project.
|
||||
|
||||
|
|
@ -97,28 +97,6 @@ What happens during expedited drafting?
|
|||
5. UI changes [are approved](https://fleetdm.com/handbook/company/development-groups#drafting-process), and the UI changes are brought back into the sprint or are estimated.
|
||||
|
||||
|
||||
### Correctly prioritize a bug
|
||||
|
||||
Bugs are always prioritized. (Fleet takes quality and stability [very seriously](https://fleetdm.com/handbook/company/why-this-way#why-spend-so-much-energy-responding-to-every-potential-production-incident).) Bugs should be prioritized in the following order:
|
||||
1. Quality: product does what it's supposed to (what is documented).
|
||||
2. Common-sense user criticality: If no one can load any page, that's obviously important.
|
||||
3. Age of bugs: Long-open bugs are open wounds bleeding quality out of the product. They must be closed quickly.
|
||||
4. Customer criticality: How important it is to a customer use case.
|
||||
|
||||
|
||||
If a bug is unreleased or [critical](https://fleetdm.com/handbook/engineering#critical-bugs), it is addressed in the current sprint. Otherwise, it may be prioritized and estimated for the next sprint. If a bug [requires drafting](https://fleetdm.com/handbook/engineering#in-product-drafting-as-needed) to determine the expected functionality, the bug should undergo [expedited drafting](#expedited-drafting).
|
||||
|
||||
If a bug is not addressed within six weeks, it is [sent to the product team for triage](https://fleetdm.com/handbook/engineering#in-engineering). Each sprint, the Head of Product Design reviews these bugs. Bugs are categorized as follows:
|
||||
- **Schedule**: the bug should be prioritized in the next sprint if there's engineering capacity for it.
|
||||
- **De-prioritized**: the issue will be closed and the necessary subsequent steps will be initiated. This might include updating documentation and informing the community.
|
||||
|
||||
The Head of Product Design meets with the Director of Product Development to discuss and finalize the outcomes for the churned bugs.
|
||||
|
||||
After aligning with the Director of Product Development on the outcomes, The Head of Product Design will clean up churned bugs. Below are the steps for each category:
|
||||
- **Schedule**: Remove the `:product` label, move the bug ticket to the 'Sprint backlog' column on the bug board, and assign it to the appropriate group's Engineering Manager so that it can be prioritized into the sprint backlog.
|
||||
- **De-prioritized**: The Head of Product Design should close the issue and, as the DRI, ensure all follow-up actions are finalized.
|
||||
|
||||
|
||||
### Write a user story
|
||||
|
||||
Product Managers [write user stories](https://fleetdm.com/handbook/company/product-groups#writing-a-good-user-story) in the [drafting board](https://app.zenhub.com/workspaces/-product-backlog-coming-soon-6192dd66ea2562000faea25c/board). The drafting board is shared by every [product group](https://fleetdm.com/handbook/company/development-groups).
|
||||
|
|
@ -133,26 +111,6 @@ If an issue's title or user story summary (_"as a…I want to…so that"_) does
|
|||
Engineering Managers estimate user stories. They are responsible for delivering planned work in the current sprint (0-3 weeks) while quickly getting user stories estimated for the next sprint (3-6 weeks). Only work that is slated to be released into the hands of users within ≤six weeks will be estimated. Estimation is run by each group's Engineering Manager and occurs on the [drafting board](https://app.zenhub.com/workspaces/-product-backlog-coming-soon-6192dd66ea2562000faea25c/board).
|
||||
|
||||
|
||||
### Rank features before release
|
||||
|
||||
These measures exist to keep all contributors (including other departments besides engineering and product) up to date with improvements and changes to the Fleet product. This helps folks plan and communicate with customers and users more effectively.
|
||||
|
||||
After the kickoff of a product sprint, the demand and product teams decide which improvements are most important to highlight in this release, whether that's through social media "drumbeat" tweets, collaboration with partners, or emphasized [content blocks](https://about.gitlab.com/handbook/marketing/blog/release-posts/#3rd-to-10th) within the release blog post.
|
||||
|
||||
When an improvement gets scheduled for release, the Head of Product sets its "echelon" to determine the emphasis the company will place on it. This leveling is based on the improvement's desirability and timeliness, and will affect demand effort for the feature.
|
||||
|
||||
- **Echelon 1: A major product feature announcement.** The most important release types, these require a specific and custom demand package. Usually including an individual blog post, a demo video and potentially a press release or official product demand launch. There is a maximum of one _echelon 1_ product announcement per release sprint.
|
||||
- **Echelon 2: A highlighted feature in the release notes.** This product feature will be highlighted at the top of the Sprint Release blog post. Depending on the feature specifics this will include: a 1-2 paragraph write-up of the feature, a demo video (if applicable) and a link to the docs. Ideally there would be no more than three _echelon 2_ features in a release post, otherwise the top features will be crowded.
|
||||
- **Echelon 3: A notable feature to mention in the [changelog](https://github.com/fleetdm/fleet/blob/main/CHANGELOG.md)**. Most product improvements fit into this echelon. This includes 1-2 sentences in the changelog and [release blog post](https://fleetdm.com/releases).
|
||||
|
||||
|
||||
### Create release issue
|
||||
|
||||
Before each release, the Head of Product [creates a "Release" issue](https://github.com/fleetdm/confidential/issues/new/choose), which includes a list of all improvements included in the upcoming release. Each improvement links to the relevant bug or user story issue on GitHub so it is easy to read the related discussion and history.
|
||||
|
||||
The product team is responsible for providing the demand team with the necessary information for writing the release blog post. Every three weeks after the sprint is kicked off, the product team meets with the relevant demand team members to go over the features for that sprint and recommend items to highlight as _echelon 2_ features and provide relevant context for other features to help demand decide which features to highlight.
|
||||
|
||||
|
||||
### Consider a feature eligible to be flagged
|
||||
|
||||
At Fleet, features are placed behind feature flags if the changes could affect Fleet's availability of existing functionalities. The following highlights should be considered when deciding if we should leverage feature flags:
|
||||
|
|
@ -167,20 +125,6 @@ At Fleet, features are placed behind feature flags if the changes could affect F
|
|||
> Fleet's feature flag guidelines is borrowed from GitLab's ["When to use feature flags" section](https://about.gitlab.com/handbook/product-development-flow/feature-flag-lifecycle/#when-to-use-feature-flags) of their handbook. Check out [GitLab's "Feature flags only when needed" video](https://www.youtube.com/watch?v=DQaGqyolOd8) for an explanation of the costs of introducing feature flags.
|
||||
|
||||
|
||||
### Consider promoting a feature as "beta"
|
||||
|
||||
At Fleet, features are advertised as "beta" if there are concerns that the feature may not work as intended in certain Fleet
|
||||
deployments. For example, these concerns could be related to the feature's performance in Fleet
|
||||
deployments with hundreds of thousands of hosts.
|
||||
|
||||
The following highlights should be considered when deciding if we promote a feature as "beta:"
|
||||
|
||||
- The feature will not be advertised as "beta" permanently. This means that the Directly
|
||||
Responsible Individual (DRI) who decides a feature is advertised as "beta" is also responsible for creating an issue that
|
||||
explains why the feature is advertised as "beta" and tracking the feature's progress towards advertising the feature as "stable."
|
||||
- The feature will be advertised as "beta" in the documentation on fleetdm.com/docs, release notes, release blog posts, and Twitter.
|
||||
|
||||
|
||||
### View Fleet usage statistics
|
||||
|
||||
In order to understand the usage of the Fleet product, we [collect statistics](https://fleetdm.com/docs/using-fleet/usage-statistics) from installations where this functionality is enabled.
|
||||
|
|
@ -276,9 +220,6 @@ Please see [handbook/product#create-release-issue](https://fleetdm.com/handbook/
|
|||
##### Feature flags
|
||||
Please see [handbook/product#consider-a-feature-eligible-to-be-flagged](https://fleetdm.com/handbook/product#consider-a-feature-eligible-to-be-flagged)
|
||||
|
||||
##### Beta features
|
||||
Please see [handbook/product#consider-promoting-a-feature-as-beta](https://fleetdm.com/handbook/product#consider-promoting-a-feature-as-beta)
|
||||
|
||||
##### Feature fest
|
||||
Please see [handbook/product-groups#feature-fest](https://fleetdm.com/handbook/product-groups#feature-fest)
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
task: "Design sprint kickoff" # 2024-03-06 TODO: Link to responsibility or corresponding "how to" info e.g. https://fleetdm.com/handbook/company/product-groups#making-changes
|
||||
startedOn: "2024-03-07"
|
||||
frequency: "Triweekly"
|
||||
description: "Add stories prioritized during Feature fest to Drafting board, assign stories to product designers, and align on priorities."
|
||||
description: "Add stories prioritized during Feature fest to Drafting board, assign stories to product designers, create upcoming reference docs release branch, and align on priorities."
|
||||
moreInfoUrl:
|
||||
dri: "noahtalerman"
|
||||
-
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,44 +0,0 @@
|
|||
name: "Explore data (fleetdm.com)"
|
||||
team_settings:
|
||||
features:
|
||||
enable_host_users: true
|
||||
enable_software_inventory: true
|
||||
host_expiry_settings:
|
||||
host_expiry_enabled: false
|
||||
host_expiry_window: 0
|
||||
secrets:
|
||||
- secret: $DOGFOOD_EXPLORE_DATA_ENROLL_SECRET
|
||||
agent_options:
|
||||
config:
|
||||
decorators:
|
||||
load:
|
||||
- SELECT uuid AS host_uuid FROM system_info;
|
||||
- SELECT hostname AS hostname FROM system_info;
|
||||
options:
|
||||
disable_distributed: false
|
||||
distributed_interval: 5
|
||||
distributed_plugin: tls
|
||||
distributed_tls_max_attempts: 3
|
||||
logger_tls_endpoint: /api/v1/osquery/log
|
||||
pack_delimiter: /
|
||||
controls:
|
||||
enable_disk_encryption: false
|
||||
macos_settings:
|
||||
custom_settings:
|
||||
macos_setup:
|
||||
bootstrap_package: null
|
||||
enable_end_user_authentication: false
|
||||
macos_setup_assistant: null
|
||||
macos_updates:
|
||||
deadline: null
|
||||
minimum_version: null
|
||||
windows_settings:
|
||||
custom_settings: null
|
||||
windows_updates:
|
||||
deadline_days: null
|
||||
grace_period_days: null
|
||||
scripts:
|
||||
policies:
|
||||
queries:
|
||||
- path: ../lib/explore-data.queries.yml
|
||||
software:
|
||||
|
|
@ -1,3 +1,9 @@
|
|||
## Orbit 1.33.0 (Sep 20, 2024)
|
||||
|
||||
* Added support to run the configured uninstall script when installer's post-install script fails.
|
||||
|
||||
* Updated Go to go1.23.1
|
||||
|
||||
## Orbit 1.32.0 (Aug 29, 2024)
|
||||
|
||||
* Bumped macadmins extension to use SOFA feed sofafeed.macadmins.io
|
||||
|
|
|
|||
|
|
@ -18,8 +18,8 @@ Following are the currently deployed versions of fleetd components on the `stabl
|
|||
|
||||
| Component\OS | macOS | Linux | Windows | Linux (arm64) |
|
||||
|--------------|--------|--------|---------|---------------|
|
||||
| orbit | 1.32.0 | 1.32.0 | 1.32.0 | 1.32.0 |
|
||||
| desktop | 1.32.0 | 1.32.0 | 1.32.0 | 1.32.0 |
|
||||
| orbit | 1.33.0 | 1.33.0 | 1.33.0 | 1.33.0 |
|
||||
| desktop | 1.33.0 | 1.33.0 | 1.33.0 | 1.33.0 |
|
||||
| osqueryd | 5.13.1 | 5.13.1 | 5.13.1 | 5.13.1 |
|
||||
| nudge | - | - | - | - |
|
||||
| swiftDialog | - | - | - | - |
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
During software install flow, if installer's post-install script fails, run the uninstall script to attempt to roll back.
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
* Updated Go to go1.23.1
|
||||
|
||||
|
|
@ -771,7 +771,7 @@ func (ds *Datastore) CleanupUnusedSoftwareInstallers(ctx context.Context, softwa
|
|||
return ctxerr.Wrap(ctx, err, "cleanup unused software installers")
|
||||
}
|
||||
|
||||
func (ds *Datastore) BatchSetSoftwareInstallers(ctx context.Context, tmID *uint, installers []*fleet.UploadSoftwareInstallerPayload) ([]fleet.SoftwarePackageResponse, error) {
|
||||
func (ds *Datastore) BatchSetSoftwareInstallers(ctx context.Context, tmID *uint, installers []*fleet.UploadSoftwareInstallerPayload) error {
|
||||
const upsertSoftwareTitles = `
|
||||
INSERT INTO software_titles
|
||||
(name, source, browser)
|
||||
|
|
@ -881,23 +881,12 @@ ON DUPLICATE KEY UPDATE
|
|||
url = VALUES(url)
|
||||
`
|
||||
|
||||
const loadInsertedSoftwareInstallers = `
|
||||
SELECT
|
||||
team_id,
|
||||
title_id,
|
||||
url
|
||||
FROM
|
||||
software_installers
|
||||
WHERE global_or_team_id = ?
|
||||
`
|
||||
|
||||
// use a team id of 0 if no-team
|
||||
var globalOrTeamID uint
|
||||
if tmID != nil {
|
||||
globalOrTeamID = *tmID
|
||||
}
|
||||
|
||||
var insertedSoftwareInstallers []fleet.SoftwarePackageResponse
|
||||
if err := ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
|
||||
// if no installers are provided, just delete whatever was in
|
||||
// the table
|
||||
|
|
@ -1043,15 +1032,11 @@ WHERE global_or_team_id = ?
|
|||
}
|
||||
}
|
||||
|
||||
if err := sqlx.SelectContext(ctx, tx, &insertedSoftwareInstallers, loadInsertedSoftwareInstallers, globalOrTeamID); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "load inserted software installers")
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
return insertedSoftwareInstallers, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ds *Datastore) HasSelfServiceSoftwareInstallers(ctx context.Context, hostPlatform string, hostTeamID *uint) (bool, error) {
|
||||
|
|
@ -1138,3 +1123,21 @@ func (ds *Datastore) UpdateSoftwareInstallerWithoutPackageIDs(ctx context.Contex
|
|||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ds *Datastore) GetSoftwareInstallers(ctx context.Context, teamID uint) ([]fleet.SoftwarePackageResponse, error) {
|
||||
const loadInsertedSoftwareInstallers = `
|
||||
SELECT
|
||||
team_id,
|
||||
title_id,
|
||||
url
|
||||
FROM
|
||||
software_installers
|
||||
WHERE global_or_team_id = ?
|
||||
`
|
||||
var softwarePackages []fleet.SoftwarePackageResponse
|
||||
// Using ds.writer(ctx) on purpose because this method is to be called after applying software.
|
||||
if err := sqlx.SelectContext(ctx, ds.writer(ctx), &softwarePackages, loadInsertedSoftwareInstallers, teamID); err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "get software installers")
|
||||
}
|
||||
return softwarePackages, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -630,11 +630,15 @@ func testBatchSetSoftwareInstallers(t *testing.T, ds *Datastore) {
|
|||
}
|
||||
|
||||
// batch set with everything empty
|
||||
softwareInstallers, err := ds.BatchSetSoftwareInstallers(ctx, &team.ID, nil)
|
||||
err = ds.BatchSetSoftwareInstallers(ctx, &team.ID, nil)
|
||||
require.NoError(t, err)
|
||||
softwareInstallers, err := ds.GetSoftwareInstallers(ctx, team.ID)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, softwareInstallers)
|
||||
assertSoftware(nil)
|
||||
softwareInstallers, err = ds.BatchSetSoftwareInstallers(ctx, &team.ID, []*fleet.UploadSoftwareInstallerPayload{})
|
||||
err = ds.BatchSetSoftwareInstallers(ctx, &team.ID, []*fleet.UploadSoftwareInstallerPayload{})
|
||||
require.NoError(t, err)
|
||||
softwareInstallers, err = ds.GetSoftwareInstallers(ctx, team.ID)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, softwareInstallers)
|
||||
assertSoftware(nil)
|
||||
|
|
@ -642,7 +646,7 @@ func testBatchSetSoftwareInstallers(t *testing.T, ds *Datastore) {
|
|||
// add a single installer
|
||||
ins0 := "installer0"
|
||||
ins0File := bytes.NewReader([]byte("installer0"))
|
||||
softwareInstallers, err = ds.BatchSetSoftwareInstallers(ctx, &team.ID, []*fleet.UploadSoftwareInstallerPayload{{
|
||||
err = ds.BatchSetSoftwareInstallers(ctx, &team.ID, []*fleet.UploadSoftwareInstallerPayload{{
|
||||
InstallScript: "install",
|
||||
InstallerFile: ins0File,
|
||||
StorageID: ins0,
|
||||
|
|
@ -656,6 +660,8 @@ func testBatchSetSoftwareInstallers(t *testing.T, ds *Datastore) {
|
|||
URL: "https://example.com",
|
||||
}})
|
||||
require.NoError(t, err)
|
||||
softwareInstallers, err = ds.GetSoftwareInstallers(ctx, team.ID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, softwareInstallers, 1)
|
||||
require.NotNil(t, softwareInstallers[0].TeamID)
|
||||
require.Equal(t, team.ID, *softwareInstallers[0].TeamID)
|
||||
|
|
@ -668,7 +674,7 @@ func testBatchSetSoftwareInstallers(t *testing.T, ds *Datastore) {
|
|||
// add a new installer + ins0 installer
|
||||
ins1 := "installer1"
|
||||
ins1File := bytes.NewReader([]byte("installer1"))
|
||||
softwareInstallers, err = ds.BatchSetSoftwareInstallers(ctx, &team.ID, []*fleet.UploadSoftwareInstallerPayload{
|
||||
err = ds.BatchSetSoftwareInstallers(ctx, &team.ID, []*fleet.UploadSoftwareInstallerPayload{
|
||||
{
|
||||
InstallScript: "install",
|
||||
InstallerFile: ins0File,
|
||||
|
|
@ -698,6 +704,8 @@ func testBatchSetSoftwareInstallers(t *testing.T, ds *Datastore) {
|
|||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
softwareInstallers, err = ds.GetSoftwareInstallers(ctx, team.ID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, softwareInstallers, 2)
|
||||
require.NotNil(t, softwareInstallers[0].TitleID)
|
||||
require.NotNil(t, softwareInstallers[0].TeamID)
|
||||
|
|
@ -713,7 +721,7 @@ func testBatchSetSoftwareInstallers(t *testing.T, ds *Datastore) {
|
|||
})
|
||||
|
||||
// remove ins0
|
||||
softwareInstallers, err = ds.BatchSetSoftwareInstallers(ctx, &team.ID, []*fleet.UploadSoftwareInstallerPayload{
|
||||
err = ds.BatchSetSoftwareInstallers(ctx, &team.ID, []*fleet.UploadSoftwareInstallerPayload{
|
||||
{
|
||||
InstallScript: "install",
|
||||
PostInstallScript: "post-install",
|
||||
|
|
@ -728,6 +736,8 @@ func testBatchSetSoftwareInstallers(t *testing.T, ds *Datastore) {
|
|||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
softwareInstallers, err = ds.GetSoftwareInstallers(ctx, team.ID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, softwareInstallers, 1)
|
||||
require.NotNil(t, softwareInstallers[0].TitleID)
|
||||
require.NotNil(t, softwareInstallers[0].TeamID)
|
||||
|
|
@ -737,7 +747,9 @@ func testBatchSetSoftwareInstallers(t *testing.T, ds *Datastore) {
|
|||
})
|
||||
|
||||
// remove everything
|
||||
softwareInstallers, err = ds.BatchSetSoftwareInstallers(ctx, &team.ID, []*fleet.UploadSoftwareInstallerPayload{})
|
||||
err = ds.BatchSetSoftwareInstallers(ctx, &team.ID, []*fleet.UploadSoftwareInstallerPayload{})
|
||||
require.NoError(t, err)
|
||||
softwareInstallers, err = ds.GetSoftwareInstallers(ctx, team.ID)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, softwareInstallers)
|
||||
assertSoftware([]fleet.SoftwareTitle{})
|
||||
|
|
|
|||
|
|
@ -1711,7 +1711,8 @@ type Datastore interface {
|
|||
CleanupUnusedSoftwareInstallers(ctx context.Context, softwareInstallStore SoftwareInstallerStore, removeCreatedBefore time.Time) error
|
||||
|
||||
// BatchSetSoftwareInstallers sets the software installers for the given team or no team.
|
||||
BatchSetSoftwareInstallers(ctx context.Context, tmID *uint, installers []*UploadSoftwareInstallerPayload) ([]SoftwarePackageResponse, error)
|
||||
BatchSetSoftwareInstallers(ctx context.Context, tmID *uint, installers []*UploadSoftwareInstallerPayload) error
|
||||
GetSoftwareInstallers(ctx context.Context, tmID uint) ([]SoftwarePackageResponse, error)
|
||||
|
||||
// HasSelfServiceSoftwareInstallers returns true if self-service software installers are available for the team or globally.
|
||||
HasSelfServiceSoftwareInstallers(ctx context.Context, platform string, teamID *uint) (bool, error)
|
||||
|
|
|
|||
|
|
@ -643,9 +643,15 @@ type Service interface {
|
|||
// GetSoftwareInstallResults gets the results for a particular software install attempt.
|
||||
GetSoftwareInstallResults(ctx context.Context, installUUID string) (*HostSoftwareInstallerResult, error)
|
||||
|
||||
// BatchSetSoftwareInstallers replaces the software installers for a specified team.
|
||||
// Returns the metadata of inserted software installers.
|
||||
BatchSetSoftwareInstallers(ctx context.Context, tmName string, payloads []SoftwareInstallerPayload, dryRun bool) ([]SoftwarePackageResponse, error)
|
||||
// BatchSetSoftwareInstallers asynchronously replaces the software installers for a specified team.
|
||||
// Returns a request UUID that can be used to track an ongoing batch request (with GetBatchSetSoftwareInstallersResult).
|
||||
BatchSetSoftwareInstallers(ctx context.Context, tmName string, payloads []SoftwareInstallerPayload, dryRun bool) (string, error)
|
||||
// GetBatchSetSoftwareInstallersResult polls for the status of a batch-apply started by BatchSetSoftwareInstallers.
|
||||
// Return values:
|
||||
// - 'status': status of the batch-apply which can be "processing", "completed" or "failed".
|
||||
// - 'message': which contains error information when the status is "failed".
|
||||
// - 'packages': Contains the list of the applied software packages (when status is "completed"). This is always empty for a dry run.
|
||||
GetBatchSetSoftwareInstallersResult(ctx context.Context, tmName string, requestUUID string, dryRun bool) (status string, message string, packages []SoftwarePackageResponse, err error)
|
||||
|
||||
// SelfServiceInstallSoftwareTitle installs a software title
|
||||
// initiated by the user
|
||||
|
|
@ -1130,3 +1136,17 @@ type Service interface {
|
|||
// CalendarWebhook handles incoming calendar callback requests.
|
||||
CalendarWebhook(ctx context.Context, eventUUID string, channelID string, resourceState string) error
|
||||
}
|
||||
|
||||
type KeyValueStore interface {
|
||||
Set(ctx context.Context, key string, value string, expireTime time.Duration) error
|
||||
Get(ctx context.Context, key string) (*string, error)
|
||||
}
|
||||
|
||||
const (
|
||||
// BatchSetSoftwareInstallerStatusProcessing is the value returned for an ongoing BatchSetSoftwareInstallers operation.
|
||||
BatchSetSoftwareInstallersStatusProcessing = "processing"
|
||||
// BatchSetSoftwareInstallerStatusCompleted is the value returned for a completed BatchSetSoftwareInstallers operation.
|
||||
BatchSetSoftwareInstallersStatusCompleted = "completed"
|
||||
// BatchSetSoftwareInstallerStatusFailed is the value returned for a failed BatchSetSoftwareInstallers operation.
|
||||
BatchSetSoftwareInstallersStatusFailed = "failed"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1072,7 +1072,9 @@ type GetSoftwareInstallResultsFunc func(ctx context.Context, resultsUUID string)
|
|||
|
||||
type CleanupUnusedSoftwareInstallersFunc func(ctx context.Context, softwareInstallStore fleet.SoftwareInstallerStore, removeCreatedBefore time.Time) error
|
||||
|
||||
type BatchSetSoftwareInstallersFunc func(ctx context.Context, tmID *uint, installers []*fleet.UploadSoftwareInstallerPayload) ([]fleet.SoftwarePackageResponse, error)
|
||||
type BatchSetSoftwareInstallersFunc func(ctx context.Context, tmID *uint, installers []*fleet.UploadSoftwareInstallerPayload) error
|
||||
|
||||
type GetSoftwareInstallersFunc func(ctx context.Context, tmID uint) ([]fleet.SoftwarePackageResponse, error)
|
||||
|
||||
type HasSelfServiceSoftwareInstallersFunc func(ctx context.Context, platform string, teamID *uint) (bool, error)
|
||||
|
||||
|
|
@ -2676,6 +2678,9 @@ type DataStore struct {
|
|||
BatchSetSoftwareInstallersFunc BatchSetSoftwareInstallersFunc
|
||||
BatchSetSoftwareInstallersFuncInvoked bool
|
||||
|
||||
GetSoftwareInstallersFunc GetSoftwareInstallersFunc
|
||||
GetSoftwareInstallersFuncInvoked bool
|
||||
|
||||
HasSelfServiceSoftwareInstallersFunc HasSelfServiceSoftwareInstallersFunc
|
||||
HasSelfServiceSoftwareInstallersFuncInvoked bool
|
||||
|
||||
|
|
@ -6391,13 +6396,20 @@ func (s *DataStore) CleanupUnusedSoftwareInstallers(ctx context.Context, softwar
|
|||
return s.CleanupUnusedSoftwareInstallersFunc(ctx, softwareInstallStore, removeCreatedBefore)
|
||||
}
|
||||
|
||||
func (s *DataStore) BatchSetSoftwareInstallers(ctx context.Context, tmID *uint, installers []*fleet.UploadSoftwareInstallerPayload) ([]fleet.SoftwarePackageResponse, error) {
|
||||
func (s *DataStore) BatchSetSoftwareInstallers(ctx context.Context, tmID *uint, installers []*fleet.UploadSoftwareInstallerPayload) error {
|
||||
s.mu.Lock()
|
||||
s.BatchSetSoftwareInstallersFuncInvoked = true
|
||||
s.mu.Unlock()
|
||||
return s.BatchSetSoftwareInstallersFunc(ctx, tmID, installers)
|
||||
}
|
||||
|
||||
func (s *DataStore) GetSoftwareInstallers(ctx context.Context, tmID uint) ([]fleet.SoftwarePackageResponse, error) {
|
||||
s.mu.Lock()
|
||||
s.GetSoftwareInstallersFuncInvoked = true
|
||||
s.mu.Unlock()
|
||||
return s.GetSoftwareInstallersFunc(ctx, tmID)
|
||||
}
|
||||
|
||||
func (s *DataStore) HasSelfServiceSoftwareInstallers(ctx context.Context, platform string, teamID *uint) (bool, error) {
|
||||
s.mu.Lock()
|
||||
s.HasSelfServiceSoftwareInstallersFuncInvoked = true
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
)
|
||||
|
|
@ -29,14 +32,38 @@ func (c *Client) ListSoftwareTitles(query string) ([]fleet.SoftwareTitleListResu
|
|||
}
|
||||
|
||||
func (c *Client) ApplyNoTeamSoftwareInstallers(softwareInstallers []fleet.SoftwareInstallerPayload, opts fleet.ApplySpecOptions) ([]fleet.SoftwarePackageResponse, error) {
|
||||
verb, path := "POST", "/api/latest/fleet/software/batch"
|
||||
query, err := url.ParseQuery(opts.RawQuery())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c.applySoftwareInstallers(softwareInstallers, query, opts.DryRun)
|
||||
}
|
||||
|
||||
func (c *Client) applySoftwareInstallers(softwareInstallers []fleet.SoftwareInstallerPayload, query url.Values, dryRun bool) ([]fleet.SoftwarePackageResponse, error) {
|
||||
path := "/api/latest/fleet/software/batch"
|
||||
var resp batchSetSoftwareInstallersResponse
|
||||
if err := c.authenticatedRequestWithQuery(map[string]interface{}{"software": softwareInstallers}, verb, path, &resp, query.Encode()); err != nil {
|
||||
if err := c.authenticatedRequestWithQuery(map[string]interface{}{"software": softwareInstallers}, "POST", path, &resp, query.Encode()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp.Packages, nil
|
||||
if dryRun && resp.RequestUUID == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
requestUUID := resp.RequestUUID
|
||||
for {
|
||||
var resp batchSetSoftwareInstallersResultResponse
|
||||
if err := c.authenticatedRequestWithQuery(nil, "GET", path+"/"+requestUUID, &resp, query.Encode()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch {
|
||||
case resp.Status == fleet.BatchSetSoftwareInstallersStatusProcessing:
|
||||
time.Sleep(5 * time.Second)
|
||||
case resp.Status == fleet.BatchSetSoftwareInstallersStatusFailed:
|
||||
return nil, errors.New(resp.Message)
|
||||
case resp.Status == fleet.BatchSetSoftwareInstallersStatusCompleted:
|
||||
return resp.Packages, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown status: %q", resp.Status)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -94,17 +94,12 @@ func (c *Client) ApplyTeamScripts(tmName string, scripts []fleet.ScriptPayload,
|
|||
}
|
||||
|
||||
func (c *Client) ApplyTeamSoftwareInstallers(tmName string, softwareInstallers []fleet.SoftwareInstallerPayload, opts fleet.ApplySpecOptions) ([]fleet.SoftwarePackageResponse, error) {
|
||||
verb, path := "POST", "/api/latest/fleet/software/batch"
|
||||
query, err := url.ParseQuery(opts.RawQuery())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
query.Add("team_name", tmName)
|
||||
var resp batchSetSoftwareInstallersResponse
|
||||
if err := c.authenticatedRequestWithQuery(map[string]interface{}{"software": softwareInstallers}, verb, path, &resp, query.Encode()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp.Packages, nil
|
||||
return c.applySoftwareInstallers(softwareInstallers, query, opts.DryRun)
|
||||
}
|
||||
|
||||
func (c *Client) ApplyTeamAppStoreAppsAssociation(tmName string, vppBatchPayload []fleet.VPPBatchPayload, opts fleet.ApplySpecOptions) error {
|
||||
|
|
|
|||
|
|
@ -381,7 +381,10 @@ func attachFleetAPIRoutes(r *mux.Router, svc fleet.Service, config config.FleetC
|
|||
ue.DELETE("/api/_version_/fleet/software/titles/{title_id:[0-9]+}/available_for_install", deleteSoftwareInstallerEndpoint, deleteSoftwareInstallerRequest{})
|
||||
ue.GET("/api/_version_/fleet/software/install/{install_uuid}/results", getSoftwareInstallResultsEndpoint,
|
||||
getSoftwareInstallResultsRequest{})
|
||||
// POST /api/_version_/fleet/software/batch is asynchronous, meaning it will start the process of software download+upload in the background
|
||||
// and will return a request UUID to be used in GET /api/_version_/fleet/software/batch/{request_uuid} to query for the status of the operation.
|
||||
ue.POST("/api/_version_/fleet/software/batch", batchSetSoftwareInstallersEndpoint, batchSetSoftwareInstallersRequest{})
|
||||
ue.GET("/api/_version_/fleet/software/batch/{request_uuid}", batchSetSoftwareInstallersResultEndpoint, batchSetSoftwareInstallersResultRequest{})
|
||||
|
||||
// App store software
|
||||
ue.GET("/api/_version_/fleet/software/app_store_apps", getAppStoreAppsEndpoint, getAppStoreAppsRequest{})
|
||||
|
|
|
|||
|
|
@ -10907,6 +10907,10 @@ func (s *integrationEnterpriseTestSuite) TestBatchSetSoftwareInstallers() {
|
|||
|
||||
// create an HTTP server to host the software installer
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/ruby.deb" {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
file, err := os.Open(filepath.Join("testdata", "software-installers", "ruby.deb"))
|
||||
require.NoError(t, err)
|
||||
defer file.Close()
|
||||
|
|
@ -10918,11 +10922,28 @@ func (s *integrationEnterpriseTestSuite) TestBatchSetSoftwareInstallers() {
|
|||
srv := httptest.NewServer(handler)
|
||||
t.Cleanup(srv.Close)
|
||||
|
||||
// do a request with a valid URL
|
||||
// do a request with a URL that returns a 404.
|
||||
softwareToInstall = []fleet.SoftwareInstallerPayload{
|
||||
{URL: srv.URL},
|
||||
{URL: srv.URL + "/not_found.pkg"},
|
||||
}
|
||||
s.Do("POST", "/api/latest/fleet/software/batch", batchSetSoftwareInstallersRequest{Software: softwareToInstall}, http.StatusOK, "team_name", tm.Name)
|
||||
var batchResponse batchSetSoftwareInstallersResponse
|
||||
s.DoJSON("POST", "/api/latest/fleet/software/batch", batchSetSoftwareInstallersRequest{Software: softwareToInstall}, http.StatusOK, &batchResponse, "team_name", tm.Name)
|
||||
message := waitBatchSetSoftwareInstallersFailed(t, s, tm.Name, batchResponse.RequestUUID)
|
||||
require.NotEmpty(t, message)
|
||||
require.Contains(t, message, fmt.Sprintf("validation failed: software.url Couldn't edit software. URL (\"%s/not_found.pkg\") returned \"Not Found\". Please make sure that URLs are reachable from your Fleet server.", srv.URL))
|
||||
|
||||
// do a request with a valid URL
|
||||
rubyURL := srv.URL + "/ruby.deb"
|
||||
softwareToInstall = []fleet.SoftwareInstallerPayload{
|
||||
{URL: rubyURL},
|
||||
}
|
||||
s.DoJSON("POST", "/api/latest/fleet/software/batch", batchSetSoftwareInstallersRequest{Software: softwareToInstall}, http.StatusOK, &batchResponse, "team_name", tm.Name)
|
||||
packages := waitBatchSetSoftwareInstallersCompleted(t, s, tm.Name, batchResponse.RequestUUID)
|
||||
require.Len(t, packages, 1)
|
||||
require.NotNil(t, packages[0].TitleID)
|
||||
require.Equal(t, rubyURL, packages[0].URL)
|
||||
require.NotNil(t, packages[0].TeamID)
|
||||
require.Equal(t, tm.ID, *packages[0].TeamID)
|
||||
|
||||
// TODO(roberto): test with a variety of response codes
|
||||
|
||||
|
|
@ -10933,7 +10954,7 @@ func (s *integrationEnterpriseTestSuite) TestBatchSetSoftwareInstallers() {
|
|||
require.Len(t, titlesResp.SoftwareTitles, 1)
|
||||
// Check that the URL is set to software installers uploaded via batch.
|
||||
require.NotNil(t, titlesResp.SoftwareTitles[0].SoftwarePackage.PackageURL)
|
||||
require.Equal(t, srv.URL, *titlesResp.SoftwareTitles[0].SoftwarePackage.PackageURL)
|
||||
require.Equal(t, rubyURL, *titlesResp.SoftwareTitles[0].SoftwarePackage.PackageURL)
|
||||
|
||||
// check that platform is set when the installer is created
|
||||
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
|
||||
|
|
@ -10946,14 +10967,26 @@ func (s *integrationEnterpriseTestSuite) TestBatchSetSoftwareInstallers() {
|
|||
})
|
||||
|
||||
// same payload doesn't modify anything
|
||||
s.Do("POST", "/api/latest/fleet/software/batch", batchSetSoftwareInstallersRequest{Software: softwareToInstall}, http.StatusOK, "team_name", tm.Name)
|
||||
s.DoJSON("POST", "/api/latest/fleet/software/batch", batchSetSoftwareInstallersRequest{Software: softwareToInstall}, http.StatusOK, &batchResponse, "team_name", tm.Name)
|
||||
packages = waitBatchSetSoftwareInstallersCompleted(t, s, tm.Name, batchResponse.RequestUUID)
|
||||
require.Len(t, packages, 1)
|
||||
require.NotNil(t, packages[0].TitleID)
|
||||
require.Equal(t, rubyURL, packages[0].URL)
|
||||
require.NotNil(t, packages[0].TeamID)
|
||||
require.Equal(t, tm.ID, *packages[0].TeamID)
|
||||
newTitlesResp := listSoftwareTitlesResponse{}
|
||||
s.DoJSON("GET", "/api/v1/fleet/software/titles", nil, http.StatusOK, &newTitlesResp, "available_for_install", "true", "team_id", strconv.Itoa(int(tm.ID)))
|
||||
require.Equal(t, titlesResp, newTitlesResp)
|
||||
|
||||
// setting self-service to true updates the software title metadata
|
||||
softwareToInstall[0].SelfService = true
|
||||
s.Do("POST", "/api/latest/fleet/software/batch", batchSetSoftwareInstallersRequest{Software: softwareToInstall}, http.StatusOK, "team_name", tm.Name)
|
||||
s.DoJSON("POST", "/api/latest/fleet/software/batch", batchSetSoftwareInstallersRequest{Software: softwareToInstall}, http.StatusOK, &batchResponse, "team_name", tm.Name)
|
||||
packages = waitBatchSetSoftwareInstallersCompleted(t, s, tm.Name, batchResponse.RequestUUID)
|
||||
require.Len(t, packages, 1)
|
||||
require.NotNil(t, packages[0].TitleID)
|
||||
require.Equal(t, rubyURL, packages[0].URL)
|
||||
require.NotNil(t, packages[0].TeamID)
|
||||
require.Equal(t, tm.ID, *packages[0].TeamID)
|
||||
newTitlesResp = listSoftwareTitlesResponse{}
|
||||
s.DoJSON("GET", "/api/v1/fleet/software/titles", nil, http.StatusOK, &newTitlesResp, "available_for_install", "true", "team_id", strconv.Itoa(int(tm.ID)))
|
||||
titlesResp.SoftwareTitles[0].SoftwarePackage.SelfService = ptr.Bool(true)
|
||||
|
|
@ -10961,7 +10994,9 @@ func (s *integrationEnterpriseTestSuite) TestBatchSetSoftwareInstallers() {
|
|||
|
||||
// empty payload cleans the software items
|
||||
softwareToInstall = []fleet.SoftwareInstallerPayload{}
|
||||
s.Do("POST", "/api/latest/fleet/software/batch", batchSetSoftwareInstallersRequest{Software: softwareToInstall}, http.StatusOK, "team_name", tm.Name)
|
||||
s.DoJSON("POST", "/api/latest/fleet/software/batch", batchSetSoftwareInstallersRequest{Software: softwareToInstall}, http.StatusOK, &batchResponse, "team_name", tm.Name)
|
||||
packages = waitBatchSetSoftwareInstallersCompleted(t, s, tm.Name, batchResponse.RequestUUID)
|
||||
require.Empty(t, packages)
|
||||
titlesResp = listSoftwareTitlesResponse{}
|
||||
s.DoJSON("GET", "/api/v1/fleet/software/titles", nil, http.StatusOK, &titlesResp, "available_for_install", "true", "team_id", strconv.Itoa(int(tm.ID)))
|
||||
require.Equal(t, 0, titlesResp.Count)
|
||||
|
|
@ -10971,9 +11006,14 @@ func (s *integrationEnterpriseTestSuite) TestBatchSetSoftwareInstallers() {
|
|||
// Do a request with a valid URL with no team
|
||||
//////////////////////////
|
||||
softwareToInstall = []fleet.SoftwareInstallerPayload{
|
||||
{URL: srv.URL},
|
||||
{URL: rubyURL},
|
||||
}
|
||||
s.Do("POST", "/api/latest/fleet/software/batch", batchSetSoftwareInstallersRequest{Software: softwareToInstall}, http.StatusOK)
|
||||
s.DoJSON("POST", "/api/latest/fleet/software/batch", batchSetSoftwareInstallersRequest{Software: softwareToInstall}, http.StatusOK, &batchResponse)
|
||||
packages = waitBatchSetSoftwareInstallersCompleted(t, s, "", batchResponse.RequestUUID)
|
||||
require.Len(t, packages, 1)
|
||||
require.NotNil(t, packages[0].TitleID)
|
||||
require.Equal(t, rubyURL, packages[0].URL)
|
||||
require.Nil(t, packages[0].TeamID)
|
||||
|
||||
// check the application status on team 0
|
||||
titlesResp = listSoftwareTitlesResponse{}
|
||||
|
|
@ -10982,14 +11022,24 @@ func (s *integrationEnterpriseTestSuite) TestBatchSetSoftwareInstallers() {
|
|||
require.Len(t, titlesResp.SoftwareTitles, 1)
|
||||
|
||||
// same payload doesn't modify anything
|
||||
s.Do("POST", "/api/latest/fleet/software/batch", batchSetSoftwareInstallersRequest{Software: softwareToInstall}, http.StatusOK)
|
||||
s.DoJSON("POST", "/api/latest/fleet/software/batch", batchSetSoftwareInstallersRequest{Software: softwareToInstall}, http.StatusOK, &batchResponse)
|
||||
packages = waitBatchSetSoftwareInstallersCompleted(t, s, "", batchResponse.RequestUUID)
|
||||
require.Len(t, packages, 1)
|
||||
require.NotNil(t, packages[0].TitleID)
|
||||
require.Equal(t, rubyURL, packages[0].URL)
|
||||
require.Nil(t, packages[0].TeamID)
|
||||
newTitlesResp = listSoftwareTitlesResponse{}
|
||||
s.DoJSON("GET", "/api/v1/fleet/software/titles", nil, http.StatusOK, &newTitlesResp, "available_for_install", "true", "team_id", strconv.Itoa(int(0)))
|
||||
require.Equal(t, titlesResp, newTitlesResp)
|
||||
|
||||
// setting self-service to true updates the software title metadata
|
||||
softwareToInstall[0].SelfService = true
|
||||
s.Do("POST", "/api/latest/fleet/software/batch", batchSetSoftwareInstallersRequest{Software: softwareToInstall}, http.StatusOK)
|
||||
s.DoJSON("POST", "/api/latest/fleet/software/batch", batchSetSoftwareInstallersRequest{Software: softwareToInstall}, http.StatusOK, &batchResponse)
|
||||
packages = waitBatchSetSoftwareInstallersCompleted(t, s, "", batchResponse.RequestUUID)
|
||||
require.Len(t, packages, 1)
|
||||
require.NotNil(t, packages[0].TitleID)
|
||||
require.Equal(t, rubyURL, packages[0].URL)
|
||||
require.Nil(t, packages[0].TeamID)
|
||||
newTitlesResp = listSoftwareTitlesResponse{}
|
||||
s.DoJSON("GET", "/api/v1/fleet/software/titles", nil, http.StatusOK, &newTitlesResp, "available_for_install", "true", "team_id", strconv.Itoa(int(0)))
|
||||
titlesResp.SoftwareTitles[0].SoftwarePackage.SelfService = ptr.Bool(true)
|
||||
|
|
@ -10997,13 +11047,50 @@ func (s *integrationEnterpriseTestSuite) TestBatchSetSoftwareInstallers() {
|
|||
|
||||
// empty payload cleans the software items
|
||||
softwareToInstall = []fleet.SoftwareInstallerPayload{}
|
||||
s.Do("POST", "/api/latest/fleet/software/batch", batchSetSoftwareInstallersRequest{Software: softwareToInstall}, http.StatusOK)
|
||||
s.DoJSON("POST", "/api/latest/fleet/software/batch", batchSetSoftwareInstallersRequest{Software: softwareToInstall}, http.StatusOK, &batchResponse)
|
||||
packages = waitBatchSetSoftwareInstallersCompleted(t, s, "", batchResponse.RequestUUID)
|
||||
require.Empty(t, packages)
|
||||
titlesResp = listSoftwareTitlesResponse{}
|
||||
s.DoJSON("GET", "/api/v1/fleet/software/titles", nil, http.StatusOK, &titlesResp, "available_for_install", "true", "team_id", strconv.Itoa(int(0)))
|
||||
require.Equal(t, 0, titlesResp.Count)
|
||||
require.Len(t, titlesResp.SoftwareTitles, 0)
|
||||
}
|
||||
|
||||
func waitBatchSetSoftwareInstallersCompleted(t *testing.T, s *integrationEnterpriseTestSuite, teamName string, requestUUID string) []fleet.SoftwarePackageResponse {
|
||||
timeout := time.After(1 * time.Minute)
|
||||
for {
|
||||
var batchResultResponse batchSetSoftwareInstallersResultResponse
|
||||
s.DoJSON("GET", "/api/latest/fleet/software/batch/"+requestUUID, nil, http.StatusOK, &batchResultResponse, "team_name", teamName)
|
||||
if batchResultResponse.Status == fleet.BatchSetSoftwareInstallersStatusCompleted {
|
||||
return batchResultResponse.Packages
|
||||
}
|
||||
select {
|
||||
case <-timeout:
|
||||
t.Fatalf("timeout: %s, %s", teamName, requestUUID)
|
||||
case <-time.After(500 * time.Millisecond):
|
||||
// OK, continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func waitBatchSetSoftwareInstallersFailed(t *testing.T, s *integrationEnterpriseTestSuite, teamName string, requestUUID string) string {
|
||||
timeout := time.After(1 * time.Minute)
|
||||
for {
|
||||
var batchResultResponse batchSetSoftwareInstallersResultResponse
|
||||
s.DoJSON("GET", "/api/latest/fleet/software/batch/"+requestUUID, nil, http.StatusOK, &batchResultResponse, "team_name", teamName)
|
||||
if batchResultResponse.Status == fleet.BatchSetSoftwareInstallersStatusFailed {
|
||||
require.Empty(t, batchResultResponse.Packages)
|
||||
return batchResultResponse.Message
|
||||
}
|
||||
select {
|
||||
case <-timeout:
|
||||
t.Fatalf("timeout: %s, %s", teamName, requestUUID)
|
||||
case <-time.After(500 * time.Millisecond):
|
||||
// OK, continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *integrationEnterpriseTestSuite) TestBatchSetSoftwareInstallersSideEffects() {
|
||||
t := s.T()
|
||||
|
||||
|
|
@ -11034,7 +11121,14 @@ func (s *integrationEnterpriseTestSuite) TestBatchSetSoftwareInstallersSideEffec
|
|||
softwareToInstall := []fleet.SoftwareInstallerPayload{
|
||||
{URL: srv.URL},
|
||||
}
|
||||
s.Do("POST", "/api/latest/fleet/software/batch", batchSetSoftwareInstallersRequest{Software: softwareToInstall}, http.StatusOK, "team_name", tm.Name)
|
||||
var batchResponse batchSetSoftwareInstallersResponse
|
||||
s.DoJSON("POST", "/api/latest/fleet/software/batch", batchSetSoftwareInstallersRequest{Software: softwareToInstall}, http.StatusOK, &batchResponse, "team_name", tm.Name)
|
||||
packages := waitBatchSetSoftwareInstallersCompleted(t, s, tm.Name, batchResponse.RequestUUID)
|
||||
require.Len(t, packages, 1)
|
||||
require.NotNil(t, packages[0].TitleID)
|
||||
require.NotNil(t, packages[0].TeamID)
|
||||
require.Equal(t, tm.ID, *packages[0].TeamID)
|
||||
require.Equal(t, srv.URL, packages[0].URL)
|
||||
titlesResp := listSoftwareTitlesResponse{}
|
||||
s.DoJSON("GET", "/api/v1/fleet/software/titles", nil, http.StatusOK, &titlesResp, "available_for_install", "true", "team_id", strconv.Itoa(int(tm.ID)))
|
||||
titleResponse := getSoftwareTitleResponse{}
|
||||
|
|
@ -11072,7 +11166,13 @@ func (s *integrationEnterpriseTestSuite) TestBatchSetSoftwareInstallersSideEffec
|
|||
|
||||
// Switch self-service flag
|
||||
softwareToInstall[0].SelfService = true
|
||||
s.Do("POST", "/api/latest/fleet/software/batch", batchSetSoftwareInstallersRequest{Software: softwareToInstall}, http.StatusOK, "team_name", tm.Name)
|
||||
s.DoJSON("POST", "/api/latest/fleet/software/batch", batchSetSoftwareInstallersRequest{Software: softwareToInstall}, http.StatusOK, &batchResponse, "team_name", tm.Name)
|
||||
packages = waitBatchSetSoftwareInstallersCompleted(t, s, tm.Name, batchResponse.RequestUUID)
|
||||
require.Len(t, packages, 1)
|
||||
require.NotNil(t, packages[0].TitleID)
|
||||
require.NotNil(t, packages[0].TeamID)
|
||||
require.Equal(t, tm.ID, *packages[0].TeamID)
|
||||
require.Equal(t, srv.URL, packages[0].URL)
|
||||
newTitlesResp := listSoftwareTitlesResponse{}
|
||||
s.DoJSON("GET", "/api/v1/fleet/software/titles", nil, http.StatusOK, &newTitlesResp, "available_for_install", "true", "team_id", strconv.Itoa(int(tm.ID)))
|
||||
require.Equal(t, true, *newTitlesResp.SoftwareTitles[0].SoftwarePackage.SelfService)
|
||||
|
|
@ -11086,7 +11186,13 @@ func (s *integrationEnterpriseTestSuite) TestBatchSetSoftwareInstallersSideEffec
|
|||
withUpdatedPreinstallQuery := []fleet.SoftwareInstallerPayload{
|
||||
{URL: srv.URL, PreInstallQuery: "SELECT * FROM os_version"},
|
||||
}
|
||||
s.Do("POST", "/api/latest/fleet/software/batch", batchSetSoftwareInstallersRequest{Software: withUpdatedPreinstallQuery}, http.StatusOK, "team_name", tm.Name)
|
||||
s.DoJSON("POST", "/api/latest/fleet/software/batch", batchSetSoftwareInstallersRequest{Software: withUpdatedPreinstallQuery}, http.StatusOK, &batchResponse, "team_name", tm.Name)
|
||||
packages = waitBatchSetSoftwareInstallersCompleted(t, s, tm.Name, batchResponse.RequestUUID)
|
||||
require.Len(t, packages, 1)
|
||||
require.NotNil(t, packages[0].TitleID)
|
||||
require.NotNil(t, packages[0].TeamID)
|
||||
require.Equal(t, tm.ID, *packages[0].TeamID)
|
||||
require.Equal(t, srv.URL, packages[0].URL)
|
||||
titleResponse = getSoftwareTitleResponse{}
|
||||
s.DoJSON("GET", fmt.Sprintf("/api/v1/fleet/software/titles/%d", newTitlesResp.SoftwareTitles[0].ID), nil, http.StatusOK, &titleResponse, "team_id", strconv.Itoa(int(tm.ID)))
|
||||
require.Equal(t, "SELECT * FROM os_version", titleResponse.SoftwareTitle.SoftwarePackage.PreInstallQuery)
|
||||
|
|
@ -11123,7 +11229,13 @@ func (s *integrationEnterpriseTestSuite) TestBatchSetSoftwareInstallersSideEffec
|
|||
withUpdatedInstallScript := []fleet.SoftwareInstallerPayload{
|
||||
{URL: srv.URL, InstallScript: "apt install ruby"},
|
||||
}
|
||||
s.Do("POST", "/api/latest/fleet/software/batch", batchSetSoftwareInstallersRequest{Software: withUpdatedInstallScript}, http.StatusOK, "team_name", tm.Name)
|
||||
s.DoJSON("POST", "/api/latest/fleet/software/batch", batchSetSoftwareInstallersRequest{Software: withUpdatedInstallScript}, http.StatusOK, &batchResponse, "team_name", tm.Name)
|
||||
packages = waitBatchSetSoftwareInstallersCompleted(t, s, tm.Name, batchResponse.RequestUUID)
|
||||
require.Len(t, packages, 1)
|
||||
require.NotNil(t, packages[0].TitleID)
|
||||
require.NotNil(t, packages[0].TeamID)
|
||||
require.Equal(t, tm.ID, *packages[0].TeamID)
|
||||
require.Equal(t, srv.URL, packages[0].URL)
|
||||
|
||||
// ensure install count is the same, and uploaded_at hasn't changed
|
||||
s.DoJSON("GET", fmt.Sprintf("/api/v1/fleet/software/titles/%d", newTitlesResp.SoftwareTitles[0].ID), nil, http.StatusOK, &titleResponse, "team_id", strconv.Itoa(int(tm.ID)))
|
||||
|
|
@ -11138,7 +11250,13 @@ func (s *integrationEnterpriseTestSuite) TestBatchSetSoftwareInstallersSideEffec
|
|||
|
||||
trailer = " " // add a character to the response for the installer HTTP call to ensure the file hashes differently
|
||||
// update package
|
||||
s.Do("POST", "/api/latest/fleet/software/batch", batchSetSoftwareInstallersRequest{Software: withUpdatedInstallScript}, http.StatusOK, "team_name", tm.Name)
|
||||
s.DoJSON("POST", "/api/latest/fleet/software/batch", batchSetSoftwareInstallersRequest{Software: withUpdatedInstallScript}, http.StatusOK, &batchResponse, "team_name", tm.Name)
|
||||
packages = waitBatchSetSoftwareInstallersCompleted(t, s, tm.Name, batchResponse.RequestUUID)
|
||||
require.Len(t, packages, 1)
|
||||
require.NotNil(t, packages[0].TitleID)
|
||||
require.NotNil(t, packages[0].TeamID)
|
||||
require.Equal(t, tm.ID, *packages[0].TeamID)
|
||||
require.Equal(t, srv.URL, packages[0].URL)
|
||||
|
||||
// ensure install count is zeroed and uploaded_at HAS changed
|
||||
s.DoJSON("GET", fmt.Sprintf("/api/v1/fleet/software/titles/%d", newTitlesResp.SoftwareTitles[0].ID), nil, http.StatusOK, &titleResponse, "team_id", strconv.Itoa(int(tm.ID)))
|
||||
|
|
@ -11202,7 +11320,15 @@ func (s *integrationEnterpriseTestSuite) TestBatchSetSoftwareInstallersWithPolic
|
|||
URL: srv.URL + "/ruby.deb",
|
||||
},
|
||||
}
|
||||
s.Do("POST", "/api/latest/fleet/software/batch", batchSetSoftwareInstallersRequest{Software: softwareToInstall}, http.StatusOK, "team_name", team1.Name)
|
||||
var batchResponse batchSetSoftwareInstallersResponse
|
||||
s.DoJSON("POST", "/api/latest/fleet/software/batch", batchSetSoftwareInstallersRequest{Software: softwareToInstall}, http.StatusOK, &batchResponse, "team_name", team1.Name)
|
||||
packages := waitBatchSetSoftwareInstallersCompleted(t, s, team1.Name, batchResponse.RequestUUID)
|
||||
require.Len(t, packages, 1)
|
||||
require.NotNil(t, packages[0].TitleID)
|
||||
require.NotNil(t, packages[0].TeamID)
|
||||
require.Equal(t, team1.ID, *packages[0].TeamID)
|
||||
require.Equal(t, srv.URL+"/ruby.deb", packages[0].URL)
|
||||
|
||||
// team2 has dummy_installer.pkg and ruby.deb.
|
||||
softwareToInstall = []fleet.SoftwareInstallerPayload{
|
||||
{
|
||||
|
|
@ -11212,7 +11338,20 @@ func (s *integrationEnterpriseTestSuite) TestBatchSetSoftwareInstallersWithPolic
|
|||
URL: srv.URL + "/ruby.deb",
|
||||
},
|
||||
}
|
||||
s.Do("POST", "/api/latest/fleet/software/batch", batchSetSoftwareInstallersRequest{Software: softwareToInstall}, http.StatusOK, "team_name", team2.Name)
|
||||
s.DoJSON("POST", "/api/latest/fleet/software/batch", batchSetSoftwareInstallersRequest{Software: softwareToInstall}, http.StatusOK, &batchResponse, "team_name", team2.Name)
|
||||
packages = waitBatchSetSoftwareInstallersCompleted(t, s, team2.Name, batchResponse.RequestUUID)
|
||||
sort.Slice(packages, func(i, j int) bool {
|
||||
return packages[i].URL < packages[j].URL
|
||||
})
|
||||
require.Len(t, packages, 2)
|
||||
require.NotNil(t, packages[0].TitleID)
|
||||
require.NotNil(t, packages[0].TeamID)
|
||||
require.Equal(t, team2.ID, *packages[0].TeamID)
|
||||
require.Equal(t, srv.URL+"/dummy_installer.pkg", packages[0].URL)
|
||||
require.NotNil(t, packages[1].TitleID)
|
||||
require.NotNil(t, packages[1].TeamID)
|
||||
require.Equal(t, team2.ID, *packages[1].TeamID)
|
||||
require.Equal(t, srv.URL+"/ruby.deb", packages[1].URL)
|
||||
|
||||
// Associate ruby.deb to policy1Team1.
|
||||
resp := listSoftwareTitlesResponse{}
|
||||
|
|
@ -11242,7 +11381,9 @@ func (s *integrationEnterpriseTestSuite) TestBatchSetSoftwareInstallersWithPolic
|
|||
|
||||
// Get rid of all installers in team1.
|
||||
softwareToInstall = []fleet.SoftwareInstallerPayload{}
|
||||
s.Do("POST", "/api/latest/fleet/software/batch", batchSetSoftwareInstallersRequest{Software: softwareToInstall}, http.StatusOK, "team_name", team1.Name)
|
||||
s.DoJSON("POST", "/api/latest/fleet/software/batch", batchSetSoftwareInstallersRequest{Software: softwareToInstall}, http.StatusOK, &batchResponse, "team_name", team1.Name)
|
||||
packages = waitBatchSetSoftwareInstallersCompleted(t, s, team1.Name, batchResponse.RequestUUID)
|
||||
require.Len(t, packages, 0)
|
||||
|
||||
// policy1Team1 should not be associated to any installer.
|
||||
policy1Team1, err = s.ds.Policy(ctx, policy1Team1.ID)
|
||||
|
|
|
|||
58
server/service/redis_key_value/redis_key_value.go
Normal file
58
server/service/redis_key_value/redis_key_value.go
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
// Package redis_key_value implements a most basic SET & GET key/value store
|
||||
// where both the key and the value are strings.
|
||||
package redis_key_value
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
|
||||
"github.com/fleetdm/fleet/v4/server/datastore/redis"
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
redigo "github.com/gomodule/redigo/redis"
|
||||
)
|
||||
|
||||
// RedisKeyValue is a basic key/value store with SET and GET operations
|
||||
// Items are removed via expiration (defined in the SET operation).
|
||||
type RedisKeyValue struct {
|
||||
pool fleet.RedisPool
|
||||
testPrefix string // for tests, the key prefix to use to avoid conflicts
|
||||
}
|
||||
|
||||
// New creates a new RedisKeyValue store.
|
||||
func New(pool fleet.RedisPool) *RedisKeyValue {
|
||||
return &RedisKeyValue{pool: pool}
|
||||
}
|
||||
|
||||
// prefix is used to not collide with other key domains (like live queries or calendar locks).
|
||||
const prefix = "key_value_"
|
||||
|
||||
// Set creates or overrides the given key with the given value.
|
||||
// Argument expireTime is used to set the expiration of the item
|
||||
// (when updating, the expiration of the item is updated).
|
||||
func (r *RedisKeyValue) Set(ctx context.Context, key string, value string, expireTime time.Duration) error {
|
||||
conn := redis.ConfigureDoer(r.pool, r.pool.Get())
|
||||
defer conn.Close()
|
||||
|
||||
if _, err := redigo.String(conn.Do("SET", r.testPrefix+prefix+key, value, "PX", expireTime.Milliseconds())); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "redis failed to set")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get returns the value for a given key.
|
||||
// It returns (nil, nil) if the key doesn't exist.
|
||||
func (r *RedisKeyValue) Get(ctx context.Context, key string) (*string, error) {
|
||||
conn := redis.ConfigureDoer(r.pool, r.pool.Get())
|
||||
defer conn.Close()
|
||||
|
||||
res, err := redigo.String(conn.Do("GET", r.testPrefix+prefix+key))
|
||||
if errors.Is(err, redigo.ErrNil) {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "redis failed to get")
|
||||
}
|
||||
return &res, nil
|
||||
}
|
||||
92
server/service/redis_key_value/redis_key_value_test.go
Normal file
92
server/service/redis_key_value/redis_key_value_test.go
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
package redis_key_value
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/server/datastore/redis/redistest"
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
"github.com/fleetdm/fleet/v4/server/test"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestRedisKeyValue(t *testing.T) {
|
||||
for _, f := range []func(*testing.T, *RedisKeyValue){
|
||||
testSetGet,
|
||||
} {
|
||||
t.Run(test.FunctionName(f), func(t *testing.T) {
|
||||
t.Run("standalone", func(t *testing.T) {
|
||||
kv := setupRedis(t, false, false)
|
||||
f(t, kv)
|
||||
})
|
||||
t.Run("cluster", func(t *testing.T) {
|
||||
kv := setupRedis(t, true, true)
|
||||
f(t, kv)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func setupRedis(t testing.TB, cluster, redir bool) *RedisKeyValue {
|
||||
pool := redistest.SetupRedis(t, t.Name(), cluster, redir, true)
|
||||
return newRedisKeyValueForTest(t, pool)
|
||||
}
|
||||
|
||||
type testName interface {
|
||||
Name() string
|
||||
}
|
||||
|
||||
func newRedisKeyValueForTest(t testName, pool fleet.RedisPool) *RedisKeyValue {
|
||||
return &RedisKeyValue{
|
||||
pool: pool,
|
||||
testPrefix: t.Name() + ":",
|
||||
}
|
||||
}
|
||||
|
||||
func testSetGet(t *testing.T, kv *RedisKeyValue) {
|
||||
ctx := context.Background()
|
||||
|
||||
result, err := kv.Get(ctx, "foo")
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, result)
|
||||
|
||||
err = kv.Set(ctx, "foo", "bar", 5*time.Second)
|
||||
require.NoError(t, err)
|
||||
|
||||
result, err = kv.Get(ctx, "foo")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, result)
|
||||
require.Equal(t, "bar", *result)
|
||||
|
||||
err = kv.Set(ctx, "foo", "zoo", 5*time.Second)
|
||||
require.NoError(t, err)
|
||||
|
||||
result, err = kv.Get(ctx, "foo")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, result)
|
||||
require.Equal(t, "zoo", *result)
|
||||
|
||||
err = kv.Set(ctx, "boo", "bar", 2*time.Second)
|
||||
require.NoError(t, err)
|
||||
result, err = kv.Get(ctx, "boo")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, result)
|
||||
require.Equal(t, "bar", *result)
|
||||
|
||||
time.Sleep(3 * time.Second)
|
||||
result, err = kv.Get(ctx, "boo")
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, result)
|
||||
|
||||
// Updating an item, updates the expiration time.
|
||||
err = kv.Set(ctx, "test", "foo", 2*time.Second)
|
||||
require.NoError(t, err)
|
||||
err = kv.Set(ctx, "test", "foo", 10*time.Second)
|
||||
require.NoError(t, err)
|
||||
time.Sleep(5 * time.Second)
|
||||
result, err = kv.Get(ctx, "test")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, result)
|
||||
require.Equal(t, "foo", *result)
|
||||
}
|
||||
|
|
@ -546,27 +546,64 @@ type batchSetSoftwareInstallersRequest struct {
|
|||
}
|
||||
|
||||
type batchSetSoftwareInstallersResponse struct {
|
||||
Packages []fleet.SoftwarePackageResponse `json:"packages"`
|
||||
Err error `json:"error,omitempty"`
|
||||
RequestUUID string `json:"request_uuid"`
|
||||
Err error `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
func (r batchSetSoftwareInstallersResponse) error() error { return r.Err }
|
||||
|
||||
func batchSetSoftwareInstallersEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
|
||||
req := request.(*batchSetSoftwareInstallersRequest)
|
||||
packages, err := svc.BatchSetSoftwareInstallers(ctx, req.TeamName, req.Software, req.DryRun)
|
||||
requestUUID, err := svc.BatchSetSoftwareInstallers(ctx, req.TeamName, req.Software, req.DryRun)
|
||||
if err != nil {
|
||||
return batchSetSoftwareInstallersResponse{Err: err}, nil
|
||||
}
|
||||
return batchSetSoftwareInstallersResponse{Packages: packages}, nil
|
||||
return batchSetSoftwareInstallersResponse{RequestUUID: requestUUID}, nil
|
||||
}
|
||||
|
||||
func (svc *Service) BatchSetSoftwareInstallers(ctx context.Context, tmName string, payloads []fleet.SoftwareInstallerPayload, dryRun bool) ([]fleet.SoftwarePackageResponse, error) {
|
||||
func (svc *Service) BatchSetSoftwareInstallers(ctx context.Context, tmName string, payloads []fleet.SoftwareInstallerPayload, dryRun bool) (string, error) {
|
||||
// skipauth: No authorization check needed due to implementation returning
|
||||
// only license error.
|
||||
svc.authz.SkipAuthorization(ctx)
|
||||
|
||||
return nil, fleet.ErrMissingLicense
|
||||
return "", fleet.ErrMissingLicense
|
||||
}
|
||||
|
||||
type batchSetSoftwareInstallersResultRequest struct {
|
||||
RequestUUID string `url:"request_uuid"`
|
||||
TeamName string `query:"team_name,optional"`
|
||||
DryRun bool `query:"dry_run,optional"` // if true, apply validation but do not save changes
|
||||
}
|
||||
|
||||
type batchSetSoftwareInstallersResultResponse struct {
|
||||
Status string `json:"status"`
|
||||
Message string `json:"message"`
|
||||
Packages []fleet.SoftwarePackageResponse `json:"packages"`
|
||||
|
||||
Err error `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
func (r batchSetSoftwareInstallersResultResponse) error() error { return r.Err }
|
||||
|
||||
func batchSetSoftwareInstallersResultEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
|
||||
req := request.(*batchSetSoftwareInstallersResultRequest)
|
||||
status, message, packages, err := svc.GetBatchSetSoftwareInstallersResult(ctx, req.TeamName, req.RequestUUID, req.DryRun)
|
||||
if err != nil {
|
||||
return batchSetSoftwareInstallersResultResponse{Err: err}, nil
|
||||
}
|
||||
return batchSetSoftwareInstallersResultResponse{
|
||||
Status: status,
|
||||
Message: message,
|
||||
Packages: packages,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (svc *Service) GetBatchSetSoftwareInstallersResult(ctx context.Context, tmName string, requestUUID string, dryRun bool) (string, string, []fleet.SoftwarePackageResponse, error) {
|
||||
// skipauth: No authorization check needed due to implementation returning
|
||||
// only license error.
|
||||
svc.authz.SkipAuthorization(ctx)
|
||||
|
||||
return "", "", nil, fleet.ErrMissingLicense
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ import (
|
|||
"github.com/fleetdm/fleet/v4/server/ptr"
|
||||
"github.com/fleetdm/fleet/v4/server/service/async"
|
||||
"github.com/fleetdm/fleet/v4/server/service/mock"
|
||||
"github.com/fleetdm/fleet/v4/server/service/redis_key_value"
|
||||
"github.com/fleetdm/fleet/v4/server/service/redis_lock"
|
||||
"github.com/fleetdm/fleet/v4/server/sso"
|
||||
"github.com/fleetdm/fleet/v4/server/test"
|
||||
|
|
@ -72,6 +73,7 @@ func newTestServiceWithConfig(t *testing.T, ds fleet.Datastore, fleetConfig conf
|
|||
softwareInstallStore fleet.SoftwareInstallerStore
|
||||
bootstrapPackageStore fleet.MDMBootstrapPackageStore
|
||||
distributedLock fleet.Lock
|
||||
keyValueStore fleet.KeyValueStore
|
||||
)
|
||||
if len(opts) > 0 {
|
||||
if opts[0].Clock != nil {
|
||||
|
|
@ -79,6 +81,10 @@ func newTestServiceWithConfig(t *testing.T, ds fleet.Datastore, fleetConfig conf
|
|||
}
|
||||
}
|
||||
|
||||
if len(opts) > 0 && opts[0].KeyValueStore != nil {
|
||||
keyValueStore = opts[0].KeyValueStore
|
||||
}
|
||||
|
||||
task := async.NewTask(ds, nil, c, config.OsqueryConfig{})
|
||||
if len(opts) > 0 {
|
||||
if opts[0].Task != nil {
|
||||
|
|
@ -99,6 +105,7 @@ func newTestServiceWithConfig(t *testing.T, ds fleet.Datastore, fleetConfig conf
|
|||
ssoStore = sso.NewSessionStore(opts[0].Pool)
|
||||
profMatcher = apple_mdm.NewProfileMatcher(opts[0].Pool)
|
||||
distributedLock = redis_lock.NewLock(opts[0].Pool)
|
||||
keyValueStore = redis_key_value.New(opts[0].Pool)
|
||||
}
|
||||
if opts[0].ProfileMatcher != nil {
|
||||
profMatcher = opts[0].ProfileMatcher
|
||||
|
|
@ -203,6 +210,7 @@ func newTestServiceWithConfig(t *testing.T, ds fleet.Datastore, fleetConfig conf
|
|||
softwareInstallStore,
|
||||
bootstrapPackageStore,
|
||||
distributedLock,
|
||||
keyValueStore,
|
||||
)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
|
|
@ -317,6 +325,7 @@ type TestServerOpts struct {
|
|||
NoCacheDatastore bool
|
||||
SoftwareInstallStore fleet.SoftwareInstallerStore
|
||||
BootstrapPackageStore fleet.MDMBootstrapPackageStore
|
||||
KeyValueStore fleet.KeyValueStore
|
||||
}
|
||||
|
||||
func RunServerForTestsWithDS(t *testing.T, ds fleet.Datastore, opts ...*TestServerOpts) (map[string]fleet.User, *httptest.Server) {
|
||||
|
|
|
|||
17395
website/.sailsrc
vendored
17395
website/.sailsrc
vendored
File diff suppressed because it is too large
Load diff
3
website/config/routes.js
vendored
3
website/config/routes.js
vendored
|
|
@ -559,6 +559,9 @@ module.exports.routes = {
|
|||
'GET /learn-more-about/host-identifiers': '/docs/rest-api/rest-api#get-host-by-identifier',
|
||||
'GET /learn-more-about/uninstall-fleetd': '/docs/using-fleet/faq#how-can-i-uninstall-fleetd',
|
||||
'GET /learn-more-about/vulnerability-processing': '/docs/using-fleet/vulnerability-processing',
|
||||
'GET /learn-more-about/apple-business-manager-tokens-api': '/docs/rest-api/rest-api#list-apple-business-manager-abm-tokens',
|
||||
'GET /learn-more-about/apple-business-manager-teams-api': 'https://github.com/fleetdm/fleet/blob/main/docs/Contributing/API-for-contributors.md#update-abm-tokens-teams',
|
||||
'GET /learn-more-about/apple-business-manager-gitops': '/docs/using-fleet/gitops#apple-business-manager',
|
||||
'GET /learn-more-about/s3-bootstrap-package': '/docs/configuration/fleet-server-configuration#s-3-software-installers-bucket',
|
||||
|
||||
// Sitemap
|
||||
|
|
|
|||
Loading…
Reference in a new issue