Files
fleet/server/service/client_teams.go
T
Scott Gress 4e35de2ac3 Update fleetctl client urls and params (#41463)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #41385 

# Details

This PR updates `fleetctl` to use the new API urls and params when
communicating with Fleet server. This avoids deprecation warnings
showing up on the server that users won't be able to fix.

Most of the changes are straightforward `team_id` -> `fleet_id`. A
couple of code changes have been pointed out. The most interesting is in
icon URLs, which can be persisted in the database (so we'll need to do a
migration in Fleet 5 if we want to drop support for `team_id`.

Similarly the FMA download urls are briefly persisted in the db for the
purpose of sending MDM commands. If we drop team_id support in Fleet 5
there could be a brief window where there are unprocessed commands in
the db still with `team_id` in them, so we'll probably want to migrate
those as well.

# Checklist for submitter

If some of the following don't apply, delete the relevant line.

- [ ] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files)
for more information.
n/a - all internal

## Testing

- [X] Added/updated automated tests
- [X] QA'd all new/changed functionality manually
- [X] ran `fleetctl gitops` on main and saw a bunch of deprecation
warnings, ran it on this branch and the warnings were gone 💨
  - [X] same with `fleetctl generate-gitops`
- [X] ran `fleetctl get` commands and verified that the new URLs and
params were used
- [X] ran `fleetctl apply` commands and verified that the new URLs and
params were used
2026-03-13 08:38:55 -05:00

171 lines
6.2 KiB
Go

package service
import (
"encoding/json"
"fmt"
"net/url"
"strconv"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/platform/endpointer"
)
// ListTeams retrieves the list of teams.
func (c *Client) ListTeams(query string) ([]fleet.Team, error) {
verb, path := "GET", "/api/latest/fleet/fleets"
var responseBody listTeamsResponse
err := c.authenticatedRequestWithQuery(nil, verb, path, &responseBody, query)
if err != nil {
return nil, err
}
return responseBody.Teams, nil
}
// CreateTeam creates a new team.
func (c *Client) CreateTeam(teamPayload fleet.TeamPayload) (*fleet.Team, error) {
req := createTeamRequest{
TeamPayload: teamPayload,
}
verb, path := "POST", "/api/latest/fleet/fleets"
var responseBody teamResponse
err := c.authenticatedRequest(req, verb, path, &responseBody)
if err != nil {
return nil, err
}
return responseBody.Team, nil
}
func (c *Client) GetTeam(teamID uint) (*fleet.Team, error) {
verb, path := "GET", fmt.Sprintf("/api/latest/fleet/fleets/%d", teamID)
var responseBody getTeamResponse
if err := c.authenticatedRequest(nil, verb, path, &responseBody); err != nil {
return nil, err
}
return responseBody.Team, nil
}
// DeleteTeam deletes a team.
func (c *Client) DeleteTeam(teamID uint) error {
verb, path := "DELETE", "/api/latest/fleet/fleets/"+strconv.FormatUint(uint64(teamID), 10)
var responseBody deleteTeamResponse
return c.authenticatedRequest(nil, verb, path, &responseBody)
}
// ApplyTeams sends the list of Teams to be applied to the
// Fleet instance.
func (c *Client) ApplyTeams(specs []json.RawMessage, opts fleet.ApplyTeamSpecOptions) (map[string]uint, error) {
verb, path := "POST", "/api/latest/fleet/spec/fleets"
var responseBody applyTeamSpecsResponse
// Rewrite deprecated key names in each team spec to use the new names.
rules := endpointer.ExtractAliasRules(fleet.TeamSpec{})
rewritten := make([]json.RawMessage, len(specs))
for i, spec := range specs {
updated, err := endpointer.RewriteOldToNewKeys(spec, rules)
if err != nil {
return nil, err
}
rewritten[i] = updated
}
params := map[string]any{"specs": rewritten}
if opts.DryRun && opts.DryRunAssumptions != nil {
params["dry_run_assumptions"] = opts.DryRunAssumptions
}
err := c.authenticatedRequestWithQuery(params, verb, path, &responseBody, opts.RawQuery())
if err != nil {
return nil, err
}
return responseBody.TeamIDsByName, nil
}
// PatchFleet sends a partial update to the specified team.
func (c *Client) PatchFleet(teamID uint, payload fleet.TeamPayload) error {
verb, path := "PATCH", "/api/latest/fleet/fleets/"+strconv.FormatUint(uint64(teamID), 10)
var resp teamResponse
return c.authenticatedRequest(payload, verb, path, &resp)
}
// ApplyTeamProfiles sends the list of profiles to be applied for the specified
// team.
func (c *Client) ApplyTeamProfiles(tmName string, profiles []fleet.MDMProfileBatchPayload, opts fleet.ApplyTeamSpecOptions) error {
verb, path := "POST", "/api/latest/fleet/mdm/profiles/batch"
query, err := url.ParseQuery(opts.RawQuery())
if err != nil {
return err
}
query.Add("fleet_name", tmName)
if opts.DryRunAssumptions != nil && opts.DryRunAssumptions.WindowsEnabledAndConfigured.Valid {
query.Add("assume_enabled", strconv.FormatBool(opts.DryRunAssumptions.WindowsEnabledAndConfigured.Value))
}
return c.authenticatedRequestWithQuery(map[string]interface{}{"profiles": profiles}, verb, path, nil, query.Encode())
}
// ApplyTeamScripts sends the list of scripts to be applied for the specified
// team.
func (c *Client) ApplyTeamScripts(tmName string, scripts []fleet.ScriptPayload, opts fleet.ApplySpecOptions) ([]fleet.ScriptResponse, error) {
verb, path := "POST", "/api/latest/fleet/scripts/batch"
query, err := url.ParseQuery(opts.RawQuery())
if err != nil {
return nil, err
}
query.Add("fleet_name", tmName)
var resp batchSetScriptsResponse
err = c.authenticatedRequestWithQuery(map[string]interface{}{"scripts": scripts}, verb, path, &resp, query.Encode())
return resp.Scripts, err
}
func (c *Client) ApplyTeamSoftwareInstallers(tmName string, softwareInstallers []fleet.SoftwareInstallerPayload, opts fleet.ApplySpecOptions) ([]fleet.SoftwarePackageResponse, error) {
query, err := url.ParseQuery(opts.RawQuery())
if err != nil {
return nil, err
}
query.Add("fleet_name", tmName)
return c.applySoftwareInstallers(softwareInstallers, query, opts.DryRun)
}
func (c *Client) ApplyTeamAppStoreAppsAssociation(tmName string, vppBatchPayload []fleet.VPPBatchPayload, opts fleet.ApplySpecOptions) ([]fleet.VPPAppResponse, error) {
query, err := url.ParseQuery(opts.RawQuery())
if err != nil {
return nil, err
}
query.Add("fleet_name", tmName)
return c.applyAppStoreAppsAssociation(vppBatchPayload, query)
}
func (c *Client) ApplyNoTeamAppStoreAppsAssociation(vppBatchPayload []fleet.VPPBatchPayload, opts fleet.ApplySpecOptions) ([]fleet.VPPAppResponse, error) {
query, err := url.ParseQuery(opts.RawQuery())
if err != nil {
return nil, err
}
return c.applyAppStoreAppsAssociation(vppBatchPayload, query)
}
func (c *Client) applyAppStoreAppsAssociation(vppBatchPayload []fleet.VPPBatchPayload, query url.Values) ([]fleet.VPPAppResponse, error) {
verb, path := "POST", "/api/latest/fleet/software/app_store_apps/batch"
var appsResponse batchAssociateAppStoreAppsResponse
err := c.authenticatedRequestWithQuery(map[string]interface{}{"app_store_apps": vppBatchPayload}, verb, path, &appsResponse, query.Encode())
if err != nil {
return nil, err
}
return matchAppStoreAppCustomIcons(vppBatchPayload, appsResponse.Apps), nil
}
// matchAppStoreAppCustomIcons hydrates VPP responses with references to icons in the request payload, so we can track
// which API calls to make to add/update/delete icons
func matchAppStoreAppCustomIcons(request []fleet.VPPBatchPayload, response []fleet.VPPAppResponse) []fleet.VPPAppResponse {
byAdamID := make(map[string]fleet.VPPBatchPayload)
for _, clientSide := range request {
byAdamID[clientSide.AppStoreID] = clientSide
}
for i := range response {
serverSide := &response[i]
if clientSide, ok := byAdamID[serverSide.AppStoreID]; ok {
serverSide.LocalIconHash = clientSide.IconHash
serverSide.LocalIconPath = clientSide.IconPath
}
}
return response
}