use auto-generated scripts for non-exe installers if not included in gitops payload (#28680)

> For #28561

# Checklist for submitter

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

<!-- Note that API documentation changes are now addressed by the
product design team. -->

- [x] Added/updated automated tests
- [x] A detailed QA plan exists on the associated ticket (if it isn't
there, work with the product group's QA engineer to add it)
- [x] Manual QA for all new/changed functionality
- [x] For unreleased bug fixes in a release candidate, confirmed that
the fix is not expected to adversely impact load test results or alerted
the release DRI if additional load testing is needed.
This commit is contained in:
Jahziel Villasana-Espinoza 2025-04-30 17:18:55 -04:00 committed by GitHub
parent 95b80482ba
commit 671ef75476
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 102 additions and 10 deletions

View file

@ -1739,6 +1739,7 @@ func (svc *Service) softwareBatchUpload(
installer.BundleIdentifier = *foundInstaller.BundleIdentifier
}
installer.Title = foundInstaller.Title
installer.PackageIDs = foundInstaller.PackageIDs
case !ok && len(teamIDs) > 0:
// Installer exists, but for another team. We should copy it over to this team
// (if we have access to the other team).
@ -1770,6 +1771,7 @@ func (svc *Service) softwareBatchUpload(
}
installer.Title = i.Title
installer.StorageID = p.SHA256
installer.PackageIDs = i.PackageIDs
break
}
}
@ -1814,6 +1816,17 @@ func (svc *Service) softwareBatchUpload(
}
}
// custom scripts only for exe installers
if installer.Extension != "exe" {
if installer.InstallScript == "" {
installer.InstallScript = file.GetInstallScript(installer.Extension)
}
if installer.UninstallScript == "" {
installer.UninstallScript = file.GetUninstallScript(installer.Extension)
}
}
// Update $PACKAGE_ID in uninstall script
preProcessUninstallScript(installer)

View file

@ -2329,7 +2329,8 @@ SELECT
si.platform AS platform,
st.source AS source,
st.bundle_identifier AS bundle_identifier,
st.name AS title
st.name AS title,
si.package_ids AS package_ids
FROM
software_installers si
JOIN software_titles st ON si.title_id = st.id
@ -2359,6 +2360,9 @@ WHERE
if _, ok := set[tmID]; ok {
return nil, ctxerr.New(ctx, fmt.Sprintf("cannot have multiple installers with the same hash %q on one team", sha256))
}
if installer.PackageIDList != "" {
installer.PackageIDs = strings.Split(installer.PackageIDList, ",")
}
set[tmID] = installer
}

View file

@ -383,15 +383,17 @@ type UploadSoftwareInstallerPayload struct {
}
type ExistingSoftwareInstaller struct {
InstallerID uint `db:"installer_id"`
TeamID *uint `db:"team_id"`
Filename string `db:"filename"`
Extension string `db:"extension"`
Version string `db:"version"`
Platform string `db:"platform"`
Source string `db:"source"`
BundleIdentifier *string `db:"bundle_identifier"`
Title string `db:"title"`
InstallerID uint `db:"installer_id"`
TeamID *uint `db:"team_id"`
Filename string `db:"filename"`
Extension string `db:"extension"`
Version string `db:"version"`
Platform string `db:"platform"`
Source string `db:"source"`
BundleIdentifier *string `db:"bundle_identifier"`
Title string `db:"title"`
PackageIDList string `db:"package_ids"`
PackageIDs []string ``
}
type UpdateSoftwareInstallerPayload struct {

View file

@ -17199,6 +17199,13 @@ func (s *integrationEnterpriseTestSuite) TestBatchSoftwareUploadWithSHAs() {
w.Header().Set("Content-Type", "application/vnd.microsoft.portable-executable")
_, err = io.Copy(w, file)
require.NoError(t, err)
case "/app.pkg":
file, err := os.Open(filepath.Join("testdata", "software-installers", "dummy_installer.pkg"))
require.NoError(t, err)
defer file.Close()
w.Header().Set("Content-Type", "application/x-newton-compatible-pkg")
_, err = io.Copy(w, file)
require.NoError(t, err)
default:
w.WriteHeader(http.StatusNotFound)
@ -17389,4 +17396,70 @@ func (s *integrationEnterpriseTestSuite) TestBatchSoftwareUploadWithSHAs() {
s.DoJSON("POST", "/api/latest/fleet/software/batch", batchSetSoftwareInstallersRequest{Software: softwareToInstall}, http.StatusAccepted, &batchResponse, "team_name", team2.Name)
errMsg = waitBatchSetSoftwareInstallersFailed(t, s, team2.Name, batchResponse.RequestUUID)
require.Contains(t, errMsg, "Couldn't edit. Uninstall script is required for .exe packages.")
// add both scripts to get a success
softwareToInstall[1].InstallScript = "echo install 2"
softwareToInstall[1].UninstallScript = "echo uninstall 2"
softwareToInstall[1].SHA256 = exeHash
// add the pkg installer with some custom scripts
pkgURL := srv.URL + "/app.pkg"
softwareToInstall = append(softwareToInstall, &fleet.SoftwareInstallerPayload{
URL: pkgURL,
InstallScript: "some install script",
UninstallScript: "some uninstall script",
})
s.DoJSON("POST", "/api/latest/fleet/software/batch", batchSetSoftwareInstallersRequest{Software: softwareToInstall}, http.StatusAccepted, &batchResponse, "team_name", team2.Name)
packages = waitBatchSetSoftwareInstallersCompleted(t, s, team2.Name, batchResponse.RequestUUID)
require.Len(t, packages, 3)
pkgTitleID := packages[2].TitleID
require.NotNil(t, pkgTitleID)
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/software/titles/%d", *pkgTitleID), getSoftwareTitleRequest{}, http.StatusOK, &stResp, "team_id", fmt.Sprint(team2.ID))
require.NotNil(t, stResp.SoftwareTitle.SoftwarePackage)
require.Equal(t, "DummyApp.app", stResp.SoftwareTitle.Name)
require.Equal(t, pkgURL, stResp.SoftwareTitle.SoftwarePackage.URL)
require.Equal(t, softwareToInstall[2].InstallScript, stResp.SoftwareTitle.SoftwarePackage.InstallScript)
require.Equal(t, softwareToInstall[2].UninstallScript, stResp.SoftwareTitle.SoftwarePackage.UninstallScript)
expectedUninstallScript := `#!/bin/sh
# Fleet extracts and saves package IDs.
pkg_ids=(
"com.example.dummy"
)
# For each package id, get all .app folders associated with the package and remove them.
for pkg_id in "${pkg_ids[@]}"
do
# Get volume and location of the package.
volume=$(pkgutil --pkg-info "$pkg_id" | grep -i "volume" | awk '{if (NF>1) print $NF}')
location=$(pkgutil --pkg-info "$pkg_id" | grep -i "location" | awk '{if (NF>1) print $NF}')
# Check if this package id corresponds to a valid/installed package
if [[ ! -z "$volume" ]]; then
# Remove individual directories that end with ".app" belonging to the package.
# Only process directories that end with ".app" to prevent Fleet from removing top level directories.
pkgutil --only-dirs --files "$pkg_id" | grep "\.app$" | sed -e 's@^@'"$volume""$location"'/@' | tr '\n' '\0' | xargs -n 1 -0 rm -rf
# Remove receipts
pkgutil --forget "$pkg_id"
else
echo "WARNING: volume is empty for package ID $pkg_id"
fi
done
`
// remove the custom scripts from the .pkg. We should get back the auto-generated ones.
softwareToInstall[2].InstallScript = ""
softwareToInstall[2].UninstallScript = ""
s.DoJSON("POST", "/api/latest/fleet/software/batch", batchSetSoftwareInstallersRequest{Software: softwareToInstall}, http.StatusAccepted, &batchResponse, "team_name", team2.Name)
packages = waitBatchSetSoftwareInstallersCompleted(t, s, team2.Name, batchResponse.RequestUUID)
require.Len(t, packages, 3)
pkgTitleID = packages[2].TitleID
require.NotNil(t, pkgTitleID)
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/software/titles/%d", *pkgTitleID), getSoftwareTitleRequest{}, http.StatusOK, &stResp, "team_id", fmt.Sprint(team2.ID))
require.NotNil(t, stResp.SoftwareTitle.SoftwarePackage)
require.Equal(t, "DummyApp.app", stResp.SoftwareTitle.Name)
require.Equal(t, pkgURL, stResp.SoftwareTitle.SoftwarePackage.URL)
require.Equal(t, file.GetInstallScript("pkg"), stResp.SoftwareTitle.SoftwarePackage.InstallScript)
require.Equal(t, expectedUninstallScript, stResp.SoftwareTitle.SoftwarePackage.UninstallScript)
}