Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions cmd/fleetctl/fleetctl/get_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1015,6 +1015,7 @@ spec:
id: 0
name: foo
software_package: null
packages: null
source: chrome_extensions
extension_for: chrome
display_name: ""
Expand All @@ -1040,6 +1041,7 @@ spec:
id: 0
name: bar
software_package: null
packages: null
source: deb_packages
extension_for: ""
display_name: ""
Expand Down Expand Up @@ -1091,6 +1093,7 @@ spec:
}
],
"software_package": null,
"packages": null,
"app_store_app": null
},
{
Expand All @@ -1111,6 +1114,7 @@ spec:
}
],
"software_package": null,
"packages": null,
"app_store_app": null
}
]
Expand Down
99 changes: 88 additions & 11 deletions ee/server/service/software_installers.go
Original file line number Diff line number Diff line change
Expand Up @@ -212,15 +212,16 @@ func (svc *Service) UploadSoftwareInstaller(ctx context.Context, payload *fleet.
return addedInstaller, nil
}

addedInstaller, err := svc.ds.GetSoftwareInstallerMetadataByTeamAndTitleID(ctxdb.RequirePrimary(ctx, true), &tmID, titleID, true)
// Return the package just added, not the title's first-added one.
addedInstaller, err := svc.ds.GetSoftwareInstallerMetadataByTeamTitleAndInstallerID(ctxdb.RequirePrimary(ctx, true), &tmID, titleID, installerID, true)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "getting added software installer")
}
Comment thread
cdcme marked this conversation as resolved.

if payload.AutomaticInstall {
if payload.AutomaticInstall && payload.AddedAutomaticInstallPolicy != nil {
policyAct := fleet.ActivityTypeCreatedPolicy{
ID: addedInstaller.AutomaticInstallPolicies[0].ID,
Name: addedInstaller.AutomaticInstallPolicies[0].Name,
ID: payload.AddedAutomaticInstallPolicy.ID,
Name: payload.AddedAutomaticInstallPolicy.Name,
}

if err := svc.NewActivity(ctx, authz.UserFromContext(ctx), policyAct); err != nil {
Expand Down Expand Up @@ -411,19 +412,51 @@ func (svc *Service) UpdateSoftwareInstaller(ctx context.Context, payload *fleet.
return svc.updateInHouseAppInstaller(ctx, payload, vc, teamName, software)
}

// With more than one installer on the title, this edits the first-added one.
// Choosing a specific package to edit is handled by the precedence work.
if software.SoftwareInstallersCount < 1 {
return nil, &fleet.BadRequestError{
Message: "There are no software installers defined yet for this title and team. Please add an installer instead of attempting to edit.",
}
}

// Defaults to the first-added package; a specific installer_id overrides it below.
existingInstaller, err := svc.ds.GetSoftwareInstallerMetadataByTeamAndTitleID(ctx, payload.TeamID, payload.TitleID, true)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "getting existing installer")
}

// siblings is reused for both installer targeting and the hash-collision check below.
var siblings []*fleet.SoftwareInstaller
if software.SoftwareInstallersCount > 1 || payload.InstallerID != 0 {
siblings, err = svc.ds.GetSoftwarePackagesByTeamAndTitleID(ctx, payload.TeamID, payload.TitleID)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "getting title packages")
}

switch {
case payload.InstallerID == 0 && software.SoftwareInstallersCount > 1:
return nil, &fleet.BadRequestError{
Message: "installer_id is required when the title has multiple packages.",
}
case payload.InstallerID != 0:
var found bool
for _, p := range siblings {
if p.InstallerID == payload.InstallerID {
found = true
break
}
}
if !found {
return nil, ctxerr.Wrapf(ctx, &notFoundError{},
"installer %d does not belong to this title and team", payload.InstallerID)
}
// hydrate the targeted package the same way as the first-added default
existingInstaller, err = svc.ds.GetSoftwareInstallerMetadataByTeamTitleAndInstallerID(ctx, payload.TeamID, payload.TitleID, payload.InstallerID, true)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "getting targeted installer")
}
}
}
Comment thread
cdcme marked this conversation as resolved.

if payload.IsNoopPayload(software) {
return existingInstaller, nil // no payload, noop
}
Expand Down Expand Up @@ -500,6 +533,15 @@ func (svc *Service) UpdateSoftwareInstaller(ctx context.Context, payload *fleet.
}

if payloadForNewInstallerFile.StorageID != existingInstaller.StorageID {
// Catch a sibling hash match for a friendly 409; the dedup_token key would otherwise raise a raw 1062.
for _, p := range siblings {
if p.InstallerID != existingInstaller.InstallerID && p.StorageID == payloadForNewInstallerFile.StorageID {
return nil, ctxerr.Wrap(ctx, fleet.ConflictError{
Message: fmt.Sprintf(fleet.SoftwarePackageHashConflictMessage, payloadForNewInstallerFile.Filename),
}, "edit collides with sibling package hash")
}
}

activity.SoftwarePackage = &payload.Filename
payload.StorageID = payloadForNewInstallerFile.StorageID
payload.Filename = payloadForNewInstallerFile.Filename
Expand Down Expand Up @@ -726,6 +768,9 @@ func (svc *Service) UpdateSoftwareInstaller(ctx context.Context, payload *fleet.
return nil, ctxerr.Wrap(ctx, err, "processing side effects for version pin")
}
}

// the pinned version is now the active installer; return it, not the one we pinned away from
payload.InstallerID = activeInstallerID
default:
if payloadForNewInstallerFile != nil {
if err := svc.storeSoftware(ctx, payloadForNewInstallerFile); err != nil {
Expand Down Expand Up @@ -825,8 +870,9 @@ func (svc *Service) UpdateSoftwareInstaller(ctx context.Context, payload *fleet.
}
}

// re-pull installer from database to ensure any side effects are accounted for; may be able to optimize this out later
updatedInstaller, err := svc.ds.GetSoftwareInstallerMetadataByTeamAndTitleID(ctxdb.RequirePrimary(ctx, true), payload.TeamID, payload.TitleID, true)
// re-pull the edited installer to reflect side effects; return that specific
// package, not the title's first-added one. May be able to optimize this out later.
updatedInstaller, err := svc.ds.GetSoftwareInstallerMetadataByTeamTitleAndInstallerID(ctxdb.RequirePrimary(ctx, true), payload.TeamID, payload.TitleID, payload.InstallerID, true)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "re-hydrating updated installer metadata")
}
Comment thread
cdcme marked this conversation as resolved.
Expand Down Expand Up @@ -917,7 +963,7 @@ func ValidateSoftwareLabelsForUpdate(ctx context.Context, svc fleet.Service, exi
return false, nil, nil
}

func (svc *Service) DeleteSoftwareInstaller(ctx context.Context, titleID uint, teamID *uint) error {
func (svc *Service) DeleteSoftwareInstaller(ctx context.Context, titleID uint, teamID *uint, installerID *uint) error {
if teamID == nil {
return fleet.NewInvalidArgumentError("fleet_id", "is required")
}
Expand All @@ -928,7 +974,7 @@ func (svc *Service) DeleteSoftwareInstaller(ctx context.Context, titleID uint, t
return err
}

// first, look for a software installer
// metaInstaller is fully hydrated (incl. the title-level icon) which the per-package reads below lack.
metaInstaller, errInstaller := svc.ds.GetSoftwareInstallerMetadataByTeamAndTitleID(ctx, teamID, titleID, false)
metaVPP, errVPP := svc.ds.GetVPPAppMetadataByTeamAndTitleID(ctx, teamID, titleID)
metaInHouse, errInHouse := svc.ds.GetInHouseAppMetadataByTeamAndTitleID(ctx, teamID, titleID)
Expand All @@ -942,9 +988,40 @@ func (svc *Service) DeleteSoftwareInstaller(ctx context.Context, titleID uint, t
return ctxerr.Wrap(ctx, errInHouse, "getting in house app metadata")
}

// An installer id always refers to a software installer, never a VPP or in-house app.
if installerID != nil {
if metaInstaller == nil {
return ctxerr.Wrapf(ctx, &notFoundError{}, "installer %d does not belong to this title and team", *installerID)
}
pkgs, err := svc.ds.GetSoftwarePackagesByTeamAndTitleID(ctx, teamID, titleID)
if err != nil {
return ctxerr.Wrap(ctx, err, "getting title packages")
}
for _, pkg := range pkgs {
if pkg.InstallerID == *installerID {
pkg.IconUrl = metaInstaller.IconUrl // title-level icon for cleanup + activity
return svc.deleteSoftwareInstaller(ctx, pkg)
Comment thread
cdcme marked this conversation as resolved.
}
}
return ctxerr.Wrapf(ctx, &notFoundError{}, "installer %d does not belong to this title and team", *installerID)
}
Comment thread
cdcme marked this conversation as resolved.

switch {
case metaInstaller != nil:
return svc.deleteSoftwareInstaller(ctx, metaInstaller)
// Delete every package on the title. FMA titles keep one active row, so this
// matches prior behavior for them. Per-package deletes mean a guarded package
// (setup experience / patch policy) fails the title delete partway.
pkgs, err := svc.ds.GetSoftwarePackagesByTeamAndTitleID(ctx, teamID, titleID)
if err != nil {
return ctxerr.Wrap(ctx, err, "getting title packages to delete")
}
for _, pkg := range pkgs {
pkg.IconUrl = metaInstaller.IconUrl // title-level icon for cleanup + activity
if err := svc.deleteSoftwareInstaller(ctx, pkg); err != nil {
return err
}
}
return nil
case metaVPP != nil:
return svc.deleteVPPApp(ctx, teamID, metaVPP)
case metaInHouse != nil:
Expand Down
38 changes: 38 additions & 0 deletions server/datastore/mysql/software.go
Original file line number Diff line number Diff line change
Expand Up @@ -7165,3 +7165,41 @@ WHERE

return ret, nil
}

// GetCategoriesForSoftwareInstallers returns categories keyed by installer id,
// unmerged (unlike GetCategoriesForSoftwareTitles) so packages keep their own.
func (ds *Datastore) GetCategoriesForSoftwareInstallers(ctx context.Context, installerIDs []uint) (map[uint][]string, error) {
if len(installerIDs) == 0 {
return map[uint][]string{}, nil
}

stmt := `
SELECT
sisc.software_installer_id AS installer_id,
sc.name AS software_category_name
FROM
software_installer_software_categories sisc
JOIN software_categories sc ON sc.id = sisc.software_category_id
WHERE
sisc.software_installer_id IN (?)
ORDER BY sc.name`

stmt, args, err := sqlx.In(stmt, installerIDs)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "sqlx.In for get categories for software installers by id")
}
var categories []struct {
InstallerID uint `db:"installer_id"`
CategoryName string `db:"software_category_name"`
}
if err := sqlx.SelectContext(ctx, ds.reader(ctx), &categories, stmt, args...); err != nil {
return nil, ctxerr.Wrap(ctx, err, "get categories for software installers by id")
}

ret := make(map[uint][]string, len(categories))
for _, c := range categories {
ret[c.InstallerID] = append(ret[c.InstallerID], c.CategoryName)
}

return ret, nil
}
83 changes: 62 additions & 21 deletions server/datastore/mysql/software_installers.go
Original file line number Diff line number Diff line change
Expand Up @@ -1257,6 +1257,17 @@ WHERE
}

func (ds *Datastore) GetSoftwareInstallerMetadataByTeamAndTitleID(ctx context.Context, teamID *uint, titleID uint, withScriptContents bool) (*fleet.SoftwareInstaller, error) {
return ds.getSoftwareInstallerMetadata(ctx, teamID, titleID, nil, withScriptContents)
}

// GetSoftwareInstallerMetadataByTeamTitleAndInstallerID returns the fully-hydrated
// metadata for a specific installer (rather than the first-added one), so add/edit
// responses can echo the affected package.
func (ds *Datastore) GetSoftwareInstallerMetadataByTeamTitleAndInstallerID(ctx context.Context, teamID *uint, titleID uint, installerID uint, withScriptContents bool) (*fleet.SoftwareInstaller, error) {
return ds.getSoftwareInstallerMetadata(ctx, teamID, titleID, &installerID, withScriptContents)
}
Comment thread
cdcme marked this conversation as resolved.

func (ds *Datastore) getSoftwareInstallerMetadata(ctx context.Context, teamID *uint, titleID uint, installerID *uint, withScriptContents bool) (*fleet.SoftwareInstaller, error) {
var scriptContentsSelect, scriptContentsFrom string
if withScriptContents {
scriptContentsSelect = ` , inst.contents AS install_script, COALESCE(pinst.contents, '') AS post_install_script, uninst.contents AS uninstall_script `
Expand All @@ -1265,6 +1276,22 @@ func (ds *Datastore) GetSoftwareInstallerMetadataByTeamAndTitleID(ctx context.Co
LEFT OUTER JOIN script_contents uninst ON uninst.id = si.uninstall_script_content_id`
}

var tmID uint
if teamID != nil {
tmID = *teamID
}

// nil installerID selects the first-added active package; otherwise that specific one.
whereClause := `si.title_id = ? AND si.global_or_team_id = ?
AND si.is_active = 1
ORDER BY si.id ASC
LIMIT 1`
args := []any{titleID, tmID}
if installerID != nil {
whereClause = `si.id = ? AND si.title_id = ? AND si.global_or_team_id = ?`
args = []any{*installerID, titleID, tmID}
}

query := fmt.Sprintf(`
SELECT
si.id,
Expand Down Expand Up @@ -1295,19 +1322,11 @@ FROM
LEFT JOIN fleet_maintained_apps fma ON fma.id = si.fleet_maintained_app_id
%s
WHERE
si.title_id = ? AND si.global_or_team_id = ?
AND si.is_active = 1
ORDER BY si.id ASC
LIMIT 1`,
scriptContentsSelect, scriptContentsFrom)

var tmID uint
if teamID != nil {
tmID = *teamID
}
%s`,
scriptContentsSelect, scriptContentsFrom, whereClause)

var dest fleet.SoftwareInstaller
err := sqlx.GetContext(ctx, ds.reader(ctx), &dest, query, titleID, tmID)
err := sqlx.GetContext(ctx, ds.reader(ctx), &dest, query, args...)
if err != nil {
if err == sql.ErrNoRows {
return nil, ctxerr.Wrap(ctx, notFound("SoftwareInstaller"), "get software installer metadata")
Expand All @@ -1322,13 +1341,21 @@ LIMIT 1`,
return nil, err
}

categoryMap, err := ds.GetCategoriesForSoftwareTitles(ctx, []uint{titleID}, teamID)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "getting categories for software installer metadata")
}

if categories, ok := categoryMap[titleID]; ok {
dest.Categories = categories
if installerID != nil {
// a specific package returns its own categories, not the title-merged set
categoryMap, err := ds.GetCategoriesForSoftwareInstallers(ctx, []uint{dest.InstallerID})
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "getting categories for software installer metadata")
}
dest.Categories = categoryMap[dest.InstallerID]
} else {
categoryMap, err := ds.GetCategoriesForSoftwareTitles(ctx, []uint{titleID}, teamID)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "getting categories for software installer metadata")
}
if categories, ok := categoryMap[titleID]; ok {
dest.Categories = categories
}
}

displayName, err := ds.getSoftwareTitleDisplayName(ctx, tmID, titleID)
Expand Down Expand Up @@ -1358,6 +1385,7 @@ LIMIT 1`,
}

func (ds *Datastore) GetSoftwarePackagesByTeamAndTitleID(ctx context.Context, teamID *uint, titleID uint) ([]*fleet.SoftwareInstaller, error) {
// Join script contents so the detail shape and the edit path get the full package.
const query = `
SELECT
si.id,
Expand All @@ -1380,10 +1408,16 @@ SELECT
si.url,
COALESCE(st.name, '') AS software_title,
COALESCE(st.bundle_identifier, '') AS bundle_identifier,
si.patch_query
si.patch_query,
inst.contents AS install_script,
COALESCE(pinst.contents, '') AS post_install_script,
uninst.contents AS uninstall_script
FROM
software_installers si
JOIN software_titles st ON st.id = si.title_id
LEFT OUTER JOIN script_contents inst ON inst.id = si.install_script_content_id
LEFT OUTER JOIN script_contents pinst ON pinst.id = si.post_install_script_content_id
LEFT OUTER JOIN script_contents uninst ON uninst.id = si.uninstall_script_content_id
WHERE
si.title_id = ? AND si.global_or_team_id = ?
AND si.is_active = 1
Expand Down Expand Up @@ -1490,8 +1524,15 @@ func (ds *Datastore) DeleteSoftwareInstaller(ctx context.Context, id uint) error
}
activateAffectedHostIDs = affectedHostIDs

if _, err := tx.ExecContext(ctx, `DELETE FROM software_title_display_names WHERE (software_title_id, team_id) IN
(SELECT title_id, global_or_team_id FROM software_installers WHERE id = ?)`, id); err != nil {
// The display name is title-level (shared across sibling packages), so only remove it
// when this is the last installer on the title/team.
if _, err := tx.ExecContext(ctx, `DELETE dn FROM software_title_display_names dn
JOIN software_installers si ON si.title_id = dn.software_title_id AND si.global_or_team_id = dn.team_id
WHERE si.id = ?
AND NOT EXISTS (
SELECT 1 FROM software_installers other
WHERE other.title_id = si.title_id AND other.global_or_team_id = si.global_or_team_id AND other.id != si.id
)`, id); err != nil {
return ctxerr.Wrap(ctx, err, "delete software title display name for installer being deleted")
}

Expand Down
Loading
Loading