Use list FMA endpoint in generate-gitops to match FMAs by ID (#42483)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** Resolves #39842 Replaces the call to `maintained_apps.FetchAppsList(context.Background())` which downloads the apps.json list from github to the list Fleet-maintained apps endpoint. This is so we can match apps by their Fleet-maintained app ID instead of by name which can cause problems when a Windows FMA is associated to a title with the wrong name (e.g. title is `7-Zip 23.01` instead of the current FMA's name `7-Zip 25.01`). # Checklist for submitter If some of the following don't apply, delete the relevant line. - [x] 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. - [ ] Input data is properly validated, `SELECT *` is avoided, SQL injection is prevented (using placeholders for values in statements), JS inline code is prevented especially for url redirects, and untrusted data interpolated into shell scripts/commands is validated against shell metacharacters. - [ ] If paths of existing endpoints are modified without backwards compatibility, checked the frontend/CLI for any necessary changes ## Testing - [x] Added/updated automated tests - [ ] Where appropriate, [automated tests simulate multiple hosts and test for host isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing) (updates to one hosts's records do not affect another) - [x] QA'd all new/changed functionality manually
This commit is contained in:
@@ -0,0 +1 @@
|
||||
- Fixed a bug where `fleetctl generate-gitops` failed if a Fleet-maintained app was associated to a software title with a different name (e.g. names with different versions).
|
||||
@@ -89,6 +89,7 @@ type generateGitopsClient interface {
|
||||
GetCertificateAuthoritiesSpec(includeSecrets bool) (*fleet.GroupedCertificateAuthorities, error)
|
||||
GetCertificateTemplates(teamID string) ([]*fleet.CertificateTemplateResponseSummary, error)
|
||||
GetFleetMaintainedApp(id uint) (*fleet.MaintainedApp, error)
|
||||
ListFleetMaintainedApps(teamID uint) ([]fleet.MaintainedApp, error)
|
||||
}
|
||||
|
||||
// Given a struct type and a field name, return the JSON field name.
|
||||
@@ -1687,13 +1688,13 @@ func (cmd *GenerateGitopsCommand) generateSoftware(filePath string, teamID uint,
|
||||
packages := make([]map[string]any, 0)
|
||||
appStoreApps := make([]map[string]any, 0)
|
||||
fmas := make([]map[string]any, 0)
|
||||
var appsList *maintained_apps.AppsList
|
||||
var appsList []fleet.MaintainedApp
|
||||
|
||||
// in-house apps generate two software titles for the same gitops entry: one
|
||||
// for iOS and one for iPadOS. Use this set to deduplicate them (by filename,
|
||||
// which is unique for a given team and platform).
|
||||
dedupeInHouseAppsByFilename := make(map[string]struct{})
|
||||
var byUniqueID map[string]string
|
||||
var byFMAID map[uint]string
|
||||
for _, sw := range software {
|
||||
softwareSpec := make(map[string]interface{})
|
||||
switch {
|
||||
@@ -1760,28 +1761,22 @@ func (cmd *GenerateGitopsCommand) generateSoftware(filePath string, teamID uint,
|
||||
|
||||
var fmaInstallScriptModified, fmaUninstallScriptModified bool
|
||||
if softwareTitle.SoftwarePackage.FleetMaintainedAppID != nil {
|
||||
if byUniqueID == nil {
|
||||
if byFMAID == nil {
|
||||
if appsList == nil {
|
||||
var err error
|
||||
appsList, err = maintained_apps.FetchAppsList(context.Background())
|
||||
// currently, the list FMA endpoint has no default pagination
|
||||
appsList, err = cmd.Client.ListFleetMaintainedApps(teamID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
byUniqueID = make(map[string]string, len(appsList.Apps))
|
||||
for _, a := range appsList.Apps {
|
||||
byUniqueID[a.UniqueIdentifier] = a.Slug
|
||||
byFMAID = make(map[uint]string, len(appsList))
|
||||
for _, a := range appsList {
|
||||
byFMAID[a.ID] = a.Slug
|
||||
}
|
||||
}
|
||||
|
||||
// Look up slug by bundle identifier (macOS) or software title name (Windows).
|
||||
// Windows FMAs don't have a bundle identifier; their unique_identifier
|
||||
// in the manifest is the program name (e.g., "1Password").
|
||||
lookupKey := ptr.ValOrZero(softwareTitle.BundleIdentifier)
|
||||
if lookupKey == "" {
|
||||
lookupKey = softwareTitle.Name
|
||||
}
|
||||
slug = byUniqueID[lookupKey]
|
||||
slug = byFMAID[*softwareTitle.SoftwarePackage.FleetMaintainedAppID]
|
||||
fma, err := maintained_apps.Hydrate(context.Background(), &fleet.MaintainedApp{
|
||||
ID: *softwareTitle.SoftwarePackage.FleetMaintainedAppID,
|
||||
Slug: slug,
|
||||
|
||||
@@ -311,6 +311,17 @@ func (MockClient) ListSoftwareTitles(query string) ([]fleet.SoftwareTitleListRes
|
||||
FleetMaintainedAppID: ptr.Uint(2),
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: 10,
|
||||
Name: "Version Locked Name 0.1",
|
||||
HashSHA256: ptr.String("win-fma-3-package-hash"),
|
||||
SoftwarePackage: &fleet.SoftwarePackageOrApp{
|
||||
Name: "my-fma.msi",
|
||||
Platform: "windows",
|
||||
Version: "1",
|
||||
FleetMaintainedAppID: ptr.Uint(3),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
case "available_for_install=1&fleet_id=0":
|
||||
return []fleet.SoftwareTitleListResult{}, nil
|
||||
@@ -323,6 +334,14 @@ func (MockClient) GetFleetMaintainedApp(id uint) (*fleet.MaintainedApp, error) {
|
||||
return &fleet.MaintainedApp{Slug: "foo/darwin"}, nil
|
||||
}
|
||||
|
||||
func (MockClient) ListFleetMaintainedApps(teamID uint) ([]fleet.MaintainedApp, error) {
|
||||
return []fleet.MaintainedApp{
|
||||
{ID: 1, Slug: "fma1/darwin", Name: "My FMA", Platform: "darwin", UniqueIdentifier: "com.my.fma"},
|
||||
{ID: 2, Slug: "fma2/windows", Name: "My Windows FMA", Platform: "windows", UniqueIdentifier: "My Windows FMA"},
|
||||
{ID: 3, Slug: "fma3/windows", Name: "Version Locked Name 2.0", Platform: "windows", UniqueIdentifier: "Version Locked Name 2.0"},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (MockClient) GetPolicies(teamID *uint) ([]*fleet.Policy, error) {
|
||||
if teamID == nil {
|
||||
return []*fleet.Policy{
|
||||
@@ -567,6 +586,18 @@ func (MockClient) GetSoftwareTitleByID(ID uint, teamID *uint) (*fleet.SoftwareTi
|
||||
},
|
||||
IconUrl: ptr.String("/api/icon5.png"),
|
||||
}, nil
|
||||
case 10:
|
||||
return &fleet.SoftwareTitle{
|
||||
ID: 9,
|
||||
Name: "Version Locked Name 0.1",
|
||||
SoftwarePackage: &fleet.SoftwareInstaller{
|
||||
InstallScript: "install",
|
||||
UninstallScript: "uninstall",
|
||||
SelfService: true,
|
||||
Platform: "windows",
|
||||
FleetMaintainedAppID: ptr.Uint(3),
|
||||
},
|
||||
}, nil
|
||||
default:
|
||||
return nil, errors.New("software title not found")
|
||||
}
|
||||
@@ -834,7 +865,7 @@ func compareDirs(t *testing.T, sourceDir, targetDir string) {
|
||||
func configureFMAManifestServer(t *testing.T) {
|
||||
manifestServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if strings.Contains(r.URL.Path, "apps.json") {
|
||||
data := json.RawMessage(`{"version": 2, "apps": [{"name": "My FMA", "slug": "fma1/darwin", "platform": "darwin", "unique_identifier": "com.my.fma"}, {"name": "My Windows FMA", "slug": "fma2/windows", "platform": "windows", "unique_identifier": "My Windows FMA"}]}`)
|
||||
data := json.RawMessage(`{"version": 2, "apps": [{"name": "My FMA", "slug": "fma1/darwin", "platform": "darwin", "unique_identifier": "com.my.fma"}, {"name": "My Windows FMA", "slug": "fma2/windows", "platform": "windows", "unique_identifier": "My Windows FMA"}, {"name": "Version Locked Name 2.0", "slug": "fma3/windows", "platform": "windows", "unique_identifier": "Version Locked Name 2.0"}]}`)
|
||||
err := json.NewEncoder(w).Encode(data)
|
||||
require.NoError(t, err)
|
||||
return
|
||||
|
||||
@@ -34,6 +34,8 @@ fleet_maintained_apps:
|
||||
- Label A
|
||||
- Label B
|
||||
self_service: true
|
||||
- slug: fma3/windows
|
||||
self_service: true
|
||||
app_store_apps:
|
||||
- app_store_id: "1234567890"
|
||||
labels_exclude_any:
|
||||
|
||||
+2
@@ -171,6 +171,8 @@ software:
|
||||
- Label B
|
||||
self_service: true
|
||||
slug: fma2/windows
|
||||
- self_service: true
|
||||
slug: fma3/windows
|
||||
packages:
|
||||
- categories:
|
||||
- Browsers
|
||||
|
||||
@@ -267,3 +267,15 @@ func (c *Client) GetFleetMaintainedApp(id uint) (*fleet.MaintainedApp, error) {
|
||||
}
|
||||
return responseBody.FleetMaintainedApp, nil
|
||||
}
|
||||
|
||||
func (c *Client) ListFleetMaintainedApps(teamID uint) ([]fleet.MaintainedApp, error) {
|
||||
verb, path := "GET", "/api/latest/fleet/software/fleet_maintained_apps"
|
||||
query := fmt.Sprintf("fleet_id=%d", teamID)
|
||||
|
||||
var responseBody listFleetMaintainedAppsResponse
|
||||
err := c.authenticatedRequestWithQuery(nil, verb, path, &responseBody, query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return responseBody.FleetMaintainedApps, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user