Fix script-only packages not setting install script file (#44299)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** Resolves #43659 # 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) - [x] QA'd all new/changed functionality manually <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Preserves install scripts for script-only software installers when using hash-based references in GitOps, preventing self-service installs from silently no‑opping. * **Tests** * Added an integration regression test to verify batch installer resolution by hash preserves uploaded install script contents. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -0,0 +1 @@
|
||||
- Fixed a bug where applying GitOps to a script-only package by `hash_sha256` reference would wipe the install script, causing self-service installs to silently no-op.
|
||||
@@ -2539,10 +2539,14 @@ func (svc *Service) softwareBatchUpload(
|
||||
// make a copy of the installer without filled fields in case we add
|
||||
// extra installers
|
||||
extraInstallerBase := *installer
|
||||
fillSoftwareInstallerPayloadFromExisting(installer, foundInstaller, p.SHA256)
|
||||
if err := svc.fillSoftwareInstallerPayloadFromExisting(ctx, installer, foundInstaller, p.SHA256); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, extraInstaller := range foundInstallers[1:] {
|
||||
extraPayload := extraInstallerBase
|
||||
fillSoftwareInstallerPayloadFromExisting(&extraPayload, extraInstaller, p.SHA256)
|
||||
if err := svc.fillSoftwareInstallerPayloadFromExisting(ctx, &extraPayload, extraInstaller, p.SHA256); err != nil {
|
||||
return err
|
||||
}
|
||||
extraInstallers = append(extraInstallers, &extraPayload)
|
||||
}
|
||||
|
||||
@@ -2583,10 +2587,14 @@ func (svc *Service) softwareBatchUpload(
|
||||
// make a copy of the installer without filled fields in case we add
|
||||
// extra installers
|
||||
extraInstallerBase := *installer
|
||||
fillSoftwareInstallerPayloadFromExisting(installer, teamInstaller, p.SHA256)
|
||||
if err := svc.fillSoftwareInstallerPayloadFromExisting(ctx, installer, teamInstaller, p.SHA256); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, extraInstaller := range teamInstallers[1:] {
|
||||
extraPayload := extraInstallerBase
|
||||
fillSoftwareInstallerPayloadFromExisting(&extraPayload, extraInstaller, p.SHA256)
|
||||
if err := svc.fillSoftwareInstallerPayloadFromExisting(ctx, &extraPayload, extraInstaller, p.SHA256); err != nil {
|
||||
return err
|
||||
}
|
||||
extraInstallers = append(extraInstallers, &extraPayload)
|
||||
}
|
||||
|
||||
@@ -2690,7 +2698,9 @@ func (svc *Service) softwareBatchUpload(
|
||||
if resp != nil && resp.StatusCode == http.StatusNotModified && existingForCache != nil {
|
||||
bytesExist, existErr := svc.softwareInstallStore.Exists(ctx, existingForCache.StorageID)
|
||||
if existErr == nil && bytesExist {
|
||||
fillSoftwareInstallerPayloadFromExisting(installer, existingForCache, existingForCache.StorageID)
|
||||
if err := svc.fillSoftwareInstallerPayloadFromExisting(ctx, installer, existingForCache, existingForCache.StorageID); err != nil {
|
||||
return err
|
||||
}
|
||||
installer.HTTPETag = existingForCache.HTTPETag
|
||||
// Propagate the existing hash so FMA hydration below
|
||||
// doesn't try to recompute it from the (nil) file
|
||||
@@ -2975,7 +2985,7 @@ func (svc *Service) softwareBatchUpload(
|
||||
// anymore, so that's intentionally skipped.
|
||||
}
|
||||
|
||||
func fillSoftwareInstallerPayloadFromExisting(payload *fleet.UploadSoftwareInstallerPayload, existing *fleet.ExistingSoftwareInstaller, sha256Hash string) {
|
||||
func (svc *Service) fillSoftwareInstallerPayloadFromExisting(ctx context.Context, payload *fleet.UploadSoftwareInstallerPayload, existing *fleet.ExistingSoftwareInstaller, sha256Hash string) error {
|
||||
payload.Extension = existing.Extension
|
||||
payload.Filename = existing.Filename
|
||||
payload.Version = existing.Version
|
||||
@@ -2987,6 +2997,16 @@ func fillSoftwareInstallerPayloadFromExisting(payload *fleet.UploadSoftwareInsta
|
||||
payload.Title = existing.Title
|
||||
payload.StorageID = sha256Hash
|
||||
payload.PackageIDs = existing.PackageIDs
|
||||
|
||||
if fleet.IsScriptPackage(existing.Extension) {
|
||||
contents, err := svc.ds.GetAnyScriptContents(ctx, existing.InstallScriptContentID)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "fetch install script for hash-matched script package")
|
||||
}
|
||||
payload.InstallScript = string(contents)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// validETag checks if an ETag value is a strong ETag per RFC 7232
|
||||
|
||||
@@ -3590,7 +3590,8 @@ SELECT
|
||||
st.source,
|
||||
st.bundle_identifier,
|
||||
st.name AS title,
|
||||
si.package_ids
|
||||
si.package_ids,
|
||||
si.install_script_content_id
|
||||
FROM
|
||||
software_installers si
|
||||
JOIN software_titles st ON si.title_id = st.id
|
||||
@@ -3611,7 +3612,8 @@ SELECT
|
||||
st.source,
|
||||
st.bundle_identifier,
|
||||
st.name AS title,
|
||||
'' AS package_ids
|
||||
'' AS package_ids,
|
||||
0 AS install_script_content_id
|
||||
FROM
|
||||
in_house_apps iha
|
||||
JOIN software_titles st ON iha.title_id = st.id
|
||||
@@ -3666,7 +3668,8 @@ SELECT
|
||||
st.bundle_identifier AS bundle_identifier,
|
||||
st.name AS title,
|
||||
si.package_ids AS package_ids,
|
||||
si.http_etag AS http_etag
|
||||
si.http_etag AS http_etag,
|
||||
si.install_script_content_id AS install_script_content_id
|
||||
FROM
|
||||
software_installers si
|
||||
JOIN software_titles st ON si.title_id = st.id
|
||||
|
||||
@@ -586,19 +586,20 @@ func (p UploadSoftwareInstallerPayload) GetUpgradeCodeForDB() *string {
|
||||
}
|
||||
|
||||
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"`
|
||||
PackageIDList string `db:"package_ids"`
|
||||
PackageIDs []string
|
||||
StorageID string `db:"storage_id"`
|
||||
HTTPETag *string `db:"http_etag"`
|
||||
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
|
||||
StorageID string `db:"storage_id"`
|
||||
HTTPETag *string `db:"http_etag"`
|
||||
InstallScriptContentID uint `db:"install_script_content_id"`
|
||||
}
|
||||
|
||||
type UpdateSoftwareInstallerPayload struct {
|
||||
|
||||
@@ -29132,3 +29132,59 @@ func (s *integrationEnterpriseTestSuite) TestAPIOnlyUserEndpointMiddleware() {
|
||||
s.Do("GET", "/api/latest/fleet/hosts", nil, http.StatusOK)
|
||||
})
|
||||
}
|
||||
|
||||
// Regression test for fleet#43659.
|
||||
func (s *integrationEnterpriseTestSuite) TestBatchSetInstallersScriptByHash() {
|
||||
t := s.T()
|
||||
ctx := context.Background()
|
||||
|
||||
tm, err := s.ds.NewTeam(ctx, &fleet.Team{Name: t.Name(), Description: "desc"})
|
||||
require.NoError(t, err)
|
||||
|
||||
scriptBytes, err := os.ReadFile(filepath.Join("testdata", "software-installers", "script.sh"))
|
||||
require.NoError(t, err)
|
||||
|
||||
s.uploadSoftwareInstaller(t, &fleet.UploadSoftwareInstallerPayload{
|
||||
TeamID: &tm.ID,
|
||||
Filename: "script.sh",
|
||||
SelfService: true,
|
||||
}, http.StatusOK, "")
|
||||
|
||||
var listResp listSoftwareTitlesResponse
|
||||
s.DoJSON("GET", "/api/latest/fleet/software/titles", nil, http.StatusOK, &listResp,
|
||||
"team_id", fmt.Sprintf("%d", tm.ID), "available_for_install", "true")
|
||||
|
||||
var titleID uint
|
||||
for _, sw := range listResp.SoftwareTitles {
|
||||
if sw.SoftwarePackage != nil && sw.SoftwarePackage.Name == "script.sh" {
|
||||
titleID = sw.ID
|
||||
break
|
||||
}
|
||||
}
|
||||
require.NotZero(t, titleID)
|
||||
|
||||
var storageID string
|
||||
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
|
||||
return sqlx.GetContext(ctx, q, &storageID,
|
||||
"SELECT storage_id FROM software_installers WHERE title_id = ? AND global_or_team_id = ?",
|
||||
titleID, tm.ID)
|
||||
})
|
||||
require.NotEmpty(t, storageID)
|
||||
|
||||
var batchResponse batchSetSoftwareInstallersResponse
|
||||
s.DoJSON("POST", "/api/latest/fleet/software/batch",
|
||||
batchSetSoftwareInstallersRequest{Software: []*fleet.SoftwareInstallerPayload{{SHA256: storageID}}},
|
||||
http.StatusAccepted, &batchResponse, "team_name", tm.Name)
|
||||
waitBatchSetSoftwareInstallersCompleted(t, &s.withServer, tm.Name, batchResponse.RequestUUID)
|
||||
|
||||
var installScript string
|
||||
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
|
||||
return sqlx.GetContext(ctx, q, &installScript, `
|
||||
SELECT sc.contents
|
||||
FROM software_installers si
|
||||
JOIN script_contents sc ON sc.id = si.install_script_content_id
|
||||
WHERE si.title_id = ? AND si.global_or_team_id = ?`,
|
||||
titleID, tm.ID)
|
||||
})
|
||||
require.Equal(t, string(scriptBytes), installScript)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user