Pin FMA major version in GitOps (#43053)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** Resolves #38988 # 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. - [ ] Timeouts are implemented and retries are limited to avoid infinite loops - [ ] 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) - [ ] QA'd all new/changed functionality manually
This commit is contained in:
@@ -0,0 +1 @@
|
||||
- Added ability to pin Fleet-maintained apps to a specific major version in GitOps.
|
||||
@@ -960,7 +960,7 @@ func (svc *Service) deleteSoftwareInstaller(ctx context.Context, meta *fleet.Sof
|
||||
// delete them. GetFleetMaintainedVersionsByTitleID queries the live DB, so
|
||||
// it will not return the row we just deleted.
|
||||
if meta.TitleID != nil {
|
||||
cachedVersions, err := svc.ds.GetFleetMaintainedVersionsByTitleID(ctx, meta.TeamID, *meta.TitleID)
|
||||
cachedVersions, err := svc.ds.GetFleetMaintainedVersionsByTitleID(ctx, meta.TeamID, *meta.TitleID, false)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "getting cached FMA versions for cleanup")
|
||||
}
|
||||
@@ -2161,13 +2161,20 @@ func (svc *Service) BatchSetSoftwareInstallers(
|
||||
return requestUUID, nil
|
||||
}
|
||||
|
||||
var (
|
||||
errNonMajorVersion = errors.New("only the major version can be specified with a caret (^), without including minor and patch versions. For example, \"^32\".")
|
||||
errMajorVersionNotFound = errors.New("specified major version is not available. Available versions are listed in the Fleet UI under Actions > Edit software.")
|
||||
)
|
||||
|
||||
func (svc *Service) softwareInstallerPayloadFromSlug(ctx context.Context, payload *fleet.SoftwareInstallerPayload, teamID *uint) error {
|
||||
slug := payload.Slug
|
||||
if slug == nil || *slug == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
app, err := svc.ds.GetMaintainedAppBySlug(ctx, *slug, teamID)
|
||||
// convert nil teamID to 0 to get correct titleID
|
||||
tmID := ptr.ValOrZero(teamID)
|
||||
app, err := svc.ds.GetMaintainedAppBySlug(ctx, *slug, &tmID)
|
||||
if err != nil {
|
||||
// Return user-friendly message for generic not found error
|
||||
if fleet.IsNotFound(err) {
|
||||
@@ -2179,11 +2186,63 @@ func (svc *Service) softwareInstallerPayloadFromSlug(ctx context.Context, payloa
|
||||
}
|
||||
return err
|
||||
}
|
||||
fma, err := maintained_apps.Hydrate(ctx, app, payload.RollbackVersion, teamID, svc.ds)
|
||||
|
||||
majorVersionString, usesCaret := strings.CutPrefix(payload.RollbackVersion, "^")
|
||||
if usesCaret {
|
||||
if len(majorVersionString) == 0 {
|
||||
return ctxerr.Wrap(ctx, errors.New("no version number provided"), "reading Fleet-maintained app pinned version")
|
||||
}
|
||||
if parts := strings.Split(payload.RollbackVersion, "."); len(parts) > 1 {
|
||||
return fleet.NewUserMessageError(errNonMajorVersion, http.StatusBadRequest)
|
||||
}
|
||||
// unset rollback version to avoid getting a cached installer
|
||||
payload.RollbackVersion = ""
|
||||
}
|
||||
|
||||
_, err = maintained_apps.Hydrate(ctx, app, payload.RollbackVersion, teamID, svc.ds)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if usesCaret {
|
||||
downloadedSemVer, err := fleet.VersionToSemverVersion(app.Version)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "extracting semver version")
|
||||
}
|
||||
|
||||
majorVersion, err := fleet.VersionToSemverVersion(majorVersionString)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "extracting pinnged major version")
|
||||
}
|
||||
|
||||
if downloadedSemVer.Major() != majorVersion.Major() {
|
||||
// We cannot use the FMA we just got the manifest for since it is on a different major
|
||||
// version, so we try to find the latest cached version and use that instead.
|
||||
if app.TitleID == nil {
|
||||
return fleet.NewUserMessageError(errMajorVersionNotFound, http.StatusNotFound)
|
||||
}
|
||||
versions, err := svc.ds.GetFleetMaintainedVersionsByTitleID(ctx, teamID, *app.TitleID, true)
|
||||
if err != nil {
|
||||
return fleet.NewUserMessageError(errMajorVersionNotFound, http.StatusNotFound)
|
||||
}
|
||||
|
||||
// This is a bit inefficient as we are duplicating strings for categories and install/uninstall scripts,
|
||||
// but it can be optimized in softwareBatchUpload if it accepted only passing category and script content IDs.
|
||||
installer, err := svc.ds.GetCachedFMAInstallerMetadata(ctx, teamID, app.ID, versions[0].Version)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "getting software installer")
|
||||
}
|
||||
|
||||
app.Version = installer.Version
|
||||
app.InstallerURL = installer.InstallerURL
|
||||
app.SHA256 = installer.SHA256
|
||||
app.InstallScript = installer.InstallScript
|
||||
app.UninstallScript = installer.UninstallScript
|
||||
app.Categories = installer.Categories
|
||||
app.PatchQuery = installer.PatchQuery
|
||||
}
|
||||
}
|
||||
|
||||
payload.URL = app.InstallerURL
|
||||
if app.SHA256 != noCheckHash {
|
||||
payload.SHA256 = app.SHA256
|
||||
@@ -2199,7 +2258,7 @@ func (svc *Service) softwareInstallerPayloadFromSlug(ctx context.Context, payloa
|
||||
if len(payload.Categories) == 0 {
|
||||
payload.Categories = app.Categories
|
||||
}
|
||||
payload.MaintainedApp.PatchQuery = fma.PatchQuery
|
||||
payload.MaintainedApp.PatchQuery = app.PatchQuery
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -477,6 +477,65 @@ func TestSoftwareInstallerPayloadFromSlug(t *testing.T) {
|
||||
assert.Empty(t, payload.InstallScript)
|
||||
assert.Empty(t, payload.UninstallScript)
|
||||
assert.False(t, payload.FleetMaintained)
|
||||
|
||||
ds.GetMaintainedAppBySlugFunc = func(ctx context.Context, slug string, teamID *uint) (*fleet.MaintainedApp, error) {
|
||||
return &fleet.MaintainedApp{
|
||||
ID: 1,
|
||||
Name: "1Password",
|
||||
Platform: "darwin",
|
||||
UniqueIdentifier: "com.1password.1password",
|
||||
Slug: "1password/darwin",
|
||||
TitleID: new(uint(1)),
|
||||
}, nil
|
||||
}
|
||||
|
||||
ds.GetFleetMaintainedVersionsByTitleIDFunc = func(ctx context.Context, teamID *uint, titleID uint, byVersion bool) ([]fleet.FleetMaintainedVersion, error) {
|
||||
return []fleet.FleetMaintainedVersion{{ID: 1, Version: "26.0.0"}}, nil
|
||||
}
|
||||
|
||||
ds.GetCachedFMAInstallerMetadataFunc = func(ctx context.Context, teamID *uint, fmaID uint, version string) (*fleet.MaintainedApp, error) {
|
||||
return &fleet.MaintainedApp{
|
||||
ID: 1,
|
||||
Name: "1Password",
|
||||
Platform: "darwin",
|
||||
UniqueIdentifier: "com.1password.1password",
|
||||
Slug: "1password/darwin",
|
||||
}, nil
|
||||
}
|
||||
|
||||
versionPinValidationTests := []struct {
|
||||
name string
|
||||
version string
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "valid",
|
||||
version: "^26",
|
||||
},
|
||||
{
|
||||
name: "no version",
|
||||
version: "^",
|
||||
wantErr: "no version number provided",
|
||||
},
|
||||
{
|
||||
name: "invalid version",
|
||||
version: "^26.0",
|
||||
wantErr: errNonMajorVersion.Error(),
|
||||
},
|
||||
}
|
||||
|
||||
for _, vt := range versionPinValidationTests {
|
||||
t.Run(vt.name, func(t *testing.T) {
|
||||
payload := fleet.SoftwareInstallerPayload{Slug: ptr.String("1password/darwin"), RollbackVersion: vt.version}
|
||||
err = svc.softwareInstallerPayloadFromSlug(context.Background(), &payload, nil)
|
||||
if vt.wantErr != "" {
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, vt.wantErr)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetInHouseAppManifest(t *testing.T) {
|
||||
|
||||
@@ -2883,7 +2883,7 @@ WHERE
|
||||
// Evict old FMA versions beyond the max per title per team.
|
||||
// Always keep the active installer; fill remaining slots with
|
||||
// the most recent versions, evict everything else.
|
||||
fmaVersions, err := ds.getFleetMaintainedVersionsByTitleIDs(ctx, tx, []uint{titleID}, globalOrTeamID)
|
||||
fmaVersions, err := ds.getFleetMaintainedVersionsByTitleIDs(ctx, tx, []uint{titleID}, globalOrTeamID, false)
|
||||
if err != nil {
|
||||
return ctxerr.Wrapf(ctx, err, "list FMA installer versions for eviction for %q", installer.Filename)
|
||||
}
|
||||
|
||||
@@ -513,7 +513,7 @@ func (ds *Datastore) processSoftwareTitleResults(
|
||||
}
|
||||
}
|
||||
if len(fmaTitleIDs) > 0 {
|
||||
fmaVersions, err := ds.getFleetMaintainedVersionsByTitleIDs(ctx, ds.reader(ctx), fmaTitleIDs, *opt.TeamID)
|
||||
fmaVersions, err := ds.getFleetMaintainedVersionsByTitleIDs(ctx, ds.reader(ctx), fmaTitleIDs, *opt.TeamID, false)
|
||||
if err != nil {
|
||||
return nil, 0, nil, ctxerr.Wrap(ctx, err, "get fleet maintained versions")
|
||||
}
|
||||
@@ -964,8 +964,8 @@ func countSoftwareTitlesOptimized(opts fleet.SoftwareTitleListOptions) string {
|
||||
|
||||
// GetFleetMaintainedVersionsByTitleID returns all cached versions of a fleet-maintained app
|
||||
// for the given title and team.
|
||||
func (ds *Datastore) GetFleetMaintainedVersionsByTitleID(ctx context.Context, teamID *uint, titleID uint) ([]fleet.FleetMaintainedVersion, error) {
|
||||
result, err := ds.getFleetMaintainedVersionsByTitleIDs(ctx, ds.reader(ctx), []uint{titleID}, ptr.ValOrZero(teamID))
|
||||
func (ds *Datastore) GetFleetMaintainedVersionsByTitleID(ctx context.Context, teamID *uint, titleID uint, byVersion bool) ([]fleet.FleetMaintainedVersion, error) {
|
||||
result, err := ds.getFleetMaintainedVersionsByTitleIDs(ctx, ds.reader(ctx), []uint{titleID}, ptr.ValOrZero(teamID), byVersion)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -974,17 +974,23 @@ func (ds *Datastore) GetFleetMaintainedVersionsByTitleID(ctx context.Context, te
|
||||
|
||||
// getFleetMaintainedVersionsByTitleIDs returns all cached versions of fleet-maintained apps
|
||||
// for the given title IDs and team, keyed by title ID.
|
||||
func (ds *Datastore) getFleetMaintainedVersionsByTitleIDs(ctx context.Context, q sqlx.QueryerContext, titleIDs []uint, teamID uint) (map[uint][]fleet.FleetMaintainedVersion, error) {
|
||||
func (ds *Datastore) getFleetMaintainedVersionsByTitleIDs(ctx context.Context, q sqlx.QueryerContext, titleIDs []uint, teamID uint, byVersion bool) (map[uint][]fleet.FleetMaintainedVersion, error) {
|
||||
if len(titleIDs) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
query, args, err := sqlx.In(`
|
||||
query := `
|
||||
SELECT si.id, si.version, si.title_id
|
||||
FROM software_installers si
|
||||
WHERE si.title_id IN (?) AND si.global_or_team_id = ? AND si.fleet_maintained_app_id IS NOT NULL
|
||||
ORDER BY si.title_id, si.uploaded_at DESC
|
||||
`, titleIDs, teamID)
|
||||
`
|
||||
if byVersion {
|
||||
query += ` ORDER BY si.version DESC`
|
||||
} else {
|
||||
query += ` ORDER BY si.title_id, si.uploaded_at DESC`
|
||||
}
|
||||
|
||||
query, args, err := sqlx.In(query, titleIDs, teamID)
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "build fleet maintained versions query")
|
||||
}
|
||||
|
||||
@@ -2213,8 +2213,9 @@ type Datastore interface {
|
||||
GetSoftwareInstallerMetadataByTeamAndTitleID(ctx context.Context, teamID *uint, titleID uint, withScriptContents bool) (*SoftwareInstaller, error)
|
||||
|
||||
// GetFleetMaintainedVersionsByTitleID returns all cached versions of a
|
||||
// fleet-maintained app for the given title and team.
|
||||
GetFleetMaintainedVersionsByTitleID(ctx context.Context, teamID *uint, titleID uint) ([]FleetMaintainedVersion, error)
|
||||
// fleet-maintained app for the given title and team. If byVersion is true
|
||||
// the versions will be sorted by the version string.
|
||||
GetFleetMaintainedVersionsByTitleID(ctx context.Context, teamID *uint, titleID uint, byVersion bool) ([]FleetMaintainedVersion, error)
|
||||
|
||||
// HasFMAInstallerVersion returns true if the given FMA version is already
|
||||
// cached as a software installer for the given team.
|
||||
|
||||
@@ -1401,7 +1401,7 @@ type ValidateOrbitSoftwareInstallerAccessFunc func(ctx context.Context, hostID u
|
||||
|
||||
type GetSoftwareInstallerMetadataByTeamAndTitleIDFunc func(ctx context.Context, teamID *uint, titleID uint, withScriptContents bool) (*fleet.SoftwareInstaller, error)
|
||||
|
||||
type GetFleetMaintainedVersionsByTitleIDFunc func(ctx context.Context, teamID *uint, titleID uint) ([]fleet.FleetMaintainedVersion, error)
|
||||
type GetFleetMaintainedVersionsByTitleIDFunc func(ctx context.Context, teamID *uint, titleID uint, byVersion bool) ([]fleet.FleetMaintainedVersion, error)
|
||||
|
||||
type HasFMAInstallerVersionFunc func(ctx context.Context, teamID *uint, fmaID uint, version string) (bool, error)
|
||||
|
||||
@@ -9430,11 +9430,11 @@ func (s *DataStore) GetSoftwareInstallerMetadataByTeamAndTitleID(ctx context.Con
|
||||
return s.GetSoftwareInstallerMetadataByTeamAndTitleIDFunc(ctx, teamID, titleID, withScriptContents)
|
||||
}
|
||||
|
||||
func (s *DataStore) GetFleetMaintainedVersionsByTitleID(ctx context.Context, teamID *uint, titleID uint) ([]fleet.FleetMaintainedVersion, error) {
|
||||
func (s *DataStore) GetFleetMaintainedVersionsByTitleID(ctx context.Context, teamID *uint, titleID uint, byVersion bool) ([]fleet.FleetMaintainedVersion, error) {
|
||||
s.mu.Lock()
|
||||
s.GetFleetMaintainedVersionsByTitleIDFuncInvoked = true
|
||||
s.mu.Unlock()
|
||||
return s.GetFleetMaintainedVersionsByTitleIDFunc(ctx, teamID, titleID)
|
||||
return s.GetFleetMaintainedVersionsByTitleIDFunc(ctx, teamID, titleID, byVersion)
|
||||
}
|
||||
|
||||
func (s *DataStore) HasFMAInstallerVersion(ctx context.Context, teamID *uint, fmaID uint, version string) (bool, error) {
|
||||
|
||||
@@ -27945,3 +27945,133 @@ func getFleetMaintainedAppID(t *testing.T, ds *mysql.Datastore, slug string) uin
|
||||
})
|
||||
return id
|
||||
}
|
||||
|
||||
func (s *integrationEnterpriseTestSuite) TestPinMajorVersion() {
|
||||
t := s.T()
|
||||
|
||||
resetFMAState := func(state *fmaTestState, version string, installerBytes []byte, patchQuery string) {
|
||||
state.version = version
|
||||
state.installerBytes = installerBytes
|
||||
state.ComputeSHA(installerBytes)
|
||||
state.patchQuery = patchQuery
|
||||
}
|
||||
|
||||
t.Run("pin major version", func(t *testing.T) {
|
||||
teamName := ""
|
||||
states := make(map[string]*fmaTestState, 1)
|
||||
states["/zoom/windows.json"] = &fmaTestState{
|
||||
version: "1.0",
|
||||
installerBytes: []byte("xyz"),
|
||||
installerPath: "/zoom.msi",
|
||||
}
|
||||
startFMAServers(t, s.ds, states)
|
||||
|
||||
var resp batchSetSoftwareInstallersResponse
|
||||
s.DoJSON("POST", "/api/latest/fleet/software/batch",
|
||||
batchSetSoftwareInstallersRequest{Software: []*fleet.SoftwareInstallerPayload{{Slug: ptr.String("zoom/windows")}}, TeamName: teamName},
|
||||
http.StatusAccepted, &resp,
|
||||
"team_name", teamName, "team_id", "0",
|
||||
)
|
||||
waitBatchSetSoftwareInstallersCompleted(t, &s.withServer, teamName, resp.RequestUUID)
|
||||
|
||||
var listTitlesResp listSoftwareTitlesResponse
|
||||
s.DoJSON("GET", "/api/latest/fleet/software/titles", nil, http.StatusOK, &listTitlesResp, "team_id", "0")
|
||||
require.Len(t, listTitlesResp.SoftwareTitles, 1)
|
||||
require.Equal(t, "zoom.msi", listTitlesResp.SoftwareTitles[0].SoftwarePackage.Name)
|
||||
require.Equal(t, "1.0", listTitlesResp.SoftwareTitles[0].SoftwarePackage.Version)
|
||||
titleID := listTitlesResp.SoftwareTitles[0].ID
|
||||
|
||||
// pin version to ^1, call batch, should get 1.0
|
||||
s.DoJSON("POST", "/api/latest/fleet/software/batch",
|
||||
batchSetSoftwareInstallersRequest{Software: []*fleet.SoftwareInstallerPayload{{Slug: ptr.String("zoom/windows"), RollbackVersion: "^1"}}, TeamName: teamName},
|
||||
http.StatusAccepted, &resp,
|
||||
"team_name", teamName, "team_id", "0",
|
||||
)
|
||||
waitBatchSetSoftwareInstallersCompleted(t, &s.withServer, teamName, resp.RequestUUID)
|
||||
|
||||
var titleResp getSoftwareTitleResponse
|
||||
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/software/titles/%d", titleID), nil, http.StatusOK, &titleResp, "team_id", "0")
|
||||
require.NotNil(t, titleResp.SoftwareTitle)
|
||||
require.Equal(t, "1.0", titleResp.SoftwareTitle.SoftwarePackage.Version)
|
||||
|
||||
// update manifest to 1.1, FleetMaintainedVersions should have 1.0, 1.1
|
||||
// installer version should be 1.1, while still pinned to ^1
|
||||
resetFMAState(states["/zoom/windows.json"], "1.1", []byte("abc"), "")
|
||||
|
||||
s.DoJSON("POST", "/api/latest/fleet/software/batch",
|
||||
batchSetSoftwareInstallersRequest{Software: []*fleet.SoftwareInstallerPayload{{Slug: ptr.String("zoom/windows"), RollbackVersion: "^1"}}, TeamName: teamName},
|
||||
http.StatusAccepted, &resp,
|
||||
"team_name", teamName, "team_id", "0",
|
||||
)
|
||||
waitBatchSetSoftwareInstallersCompleted(t, &s.withServer, teamName, resp.RequestUUID)
|
||||
|
||||
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/software/titles/%d", titleID), nil, http.StatusOK, &titleResp, "team_id", "0")
|
||||
require.NotNil(t, titleResp.SoftwareTitle)
|
||||
require.Len(t, titleResp.SoftwareTitle.SoftwarePackage.FleetMaintainedVersions, 2)
|
||||
require.Equal(t, "1.1", titleResp.SoftwareTitle.SoftwarePackage.FleetMaintainedVersions[0].Version)
|
||||
require.Equal(t, "1.0", titleResp.SoftwareTitle.SoftwarePackage.FleetMaintainedVersions[1].Version)
|
||||
require.Equal(t, "1.1", titleResp.SoftwareTitle.SoftwarePackage.Version)
|
||||
|
||||
// pin version to 1.0? then pin to ^1, installer should be 1.1. if fleet keeps enough versions we need to test this.
|
||||
// maybe unnecessary test, can remove to speed it up
|
||||
|
||||
s.DoJSON("POST", "/api/latest/fleet/software/batch",
|
||||
batchSetSoftwareInstallersRequest{Software: []*fleet.SoftwareInstallerPayload{{Slug: ptr.String("zoom/windows"), RollbackVersion: "1.0"}}, TeamName: teamName},
|
||||
http.StatusAccepted, &resp,
|
||||
"team_name", teamName, "team_id", "0",
|
||||
)
|
||||
waitBatchSetSoftwareInstallersCompleted(t, &s.withServer, teamName, resp.RequestUUID)
|
||||
|
||||
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/software/titles/%d", titleID), nil, http.StatusOK, &titleResp, "team_id", "0")
|
||||
require.NotNil(t, titleResp.SoftwareTitle)
|
||||
require.Equal(t, "1.0", titleResp.SoftwareTitle.SoftwarePackage.Version)
|
||||
|
||||
s.DoJSON("POST", "/api/latest/fleet/software/batch",
|
||||
batchSetSoftwareInstallersRequest{Software: []*fleet.SoftwareInstallerPayload{{Slug: ptr.String("zoom/windows"), RollbackVersion: "^1"}}, TeamName: teamName},
|
||||
http.StatusAccepted, &resp,
|
||||
"team_name", teamName, "team_id", "0",
|
||||
)
|
||||
waitBatchSetSoftwareInstallersCompleted(t, &s.withServer, teamName, resp.RequestUUID)
|
||||
|
||||
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/software/titles/%d", titleID), nil, http.StatusOK, &titleResp, "team_id", "0")
|
||||
require.NotNil(t, titleResp.SoftwareTitle)
|
||||
require.Equal(t, "1.1", titleResp.SoftwareTitle.SoftwarePackage.Version)
|
||||
|
||||
// update manifesst to 2.0, installer should be 1.1
|
||||
resetFMAState(states["/zoom/windows.json"], "2.0", []byte("abc"), "")
|
||||
|
||||
s.DoJSON("POST", "/api/latest/fleet/software/batch",
|
||||
batchSetSoftwareInstallersRequest{Software: []*fleet.SoftwareInstallerPayload{{Slug: ptr.String("zoom/windows"), RollbackVersion: "^1"}}, TeamName: teamName},
|
||||
http.StatusAccepted, &resp,
|
||||
"team_name", teamName, "team_id", "0",
|
||||
)
|
||||
waitBatchSetSoftwareInstallersCompleted(t, &s.withServer, teamName, resp.RequestUUID)
|
||||
|
||||
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/software/titles/%d", titleID), nil, http.StatusOK, &titleResp, "team_id", "0")
|
||||
require.NotNil(t, titleResp.SoftwareTitle)
|
||||
require.Len(t, titleResp.SoftwareTitle.SoftwarePackage.FleetMaintainedVersions, 2)
|
||||
require.Equal(t, "1.1", titleResp.SoftwareTitle.SoftwarePackage.FleetMaintainedVersions[0].Version)
|
||||
require.Equal(t, "1.0", titleResp.SoftwareTitle.SoftwarePackage.FleetMaintainedVersions[1].Version)
|
||||
require.Equal(t, "1.1", titleResp.SoftwareTitle.SoftwarePackage.Version)
|
||||
})
|
||||
|
||||
// Test pinning ^1 for the first time when only 2.0 is available from manifest
|
||||
t.Run("add installer first time with version pin", func(t *testing.T) {
|
||||
teamName := ""
|
||||
states := make(map[string]*fmaTestState, 1)
|
||||
// use a different installer that isn't available yet
|
||||
states["/1password/darwin.json"] = &fmaTestState{
|
||||
version: "2.0",
|
||||
installerBytes: []byte("xyz"),
|
||||
installerPath: "/1password.pkg",
|
||||
}
|
||||
startFMAServers(t, s.ds, states)
|
||||
|
||||
var resp batchSetSoftwareInstallersResponse
|
||||
s.DoJSON("POST", "/api/latest/fleet/software/batch",
|
||||
batchSetSoftwareInstallersRequest{Software: []*fleet.SoftwareInstallerPayload{{Slug: ptr.String("1password/darwin"), RollbackVersion: "^1"}}, TeamName: teamName},
|
||||
http.StatusNotFound, &resp,
|
||||
"team_name", teamName, "team_id", "0",
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -203,7 +203,7 @@ func (svc *Service) SoftwareTitleByID(ctx context.Context, id uint, teamID *uint
|
||||
|
||||
// Populate FleetMaintainedVersions if this is an FMA
|
||||
if meta != nil && meta.FleetMaintainedAppID != nil {
|
||||
fmaVersions, err := svc.ds.GetFleetMaintainedVersionsByTitleID(ctx, teamID, id)
|
||||
fmaVersions, err := svc.ds.GetFleetMaintainedVersionsByTitleID(ctx, teamID, id, false)
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "get fleet maintained versions")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user