Add gitops support for in house apps (#35423)

This commit is contained in:
Martin Angers
2025-11-11 16:38:54 -05:00
committed by GitHub
parent f8ba3211d8
commit 1253b64528
25 changed files with 2337 additions and 125 deletions
@@ -0,0 +1 @@
* Added support for in-house (".ipa") apps to `fleetctl gitops`.
+14
View File
@@ -7,6 +7,7 @@ import (
"math"
"os"
pathUtils "path"
"path/filepath"
"reflect"
"regexp"
"slices"
@@ -1404,10 +1405,23 @@ func (cmd *GenerateGitopsCommand) generateSoftware(filePath string, teamID uint,
result := make(map[string]interface{})
packages := make([]map[string]interface{}, 0)
appStoreApps := make([]map[string]interface{}, 0)
// 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{})
for _, sw := range software {
softwareSpec := make(map[string]interface{})
switch {
case sw.SoftwarePackage != nil:
if isInHouseApp := filepath.Ext(sw.SoftwarePackage.Name) == ".ipa"; isInHouseApp {
if _, ok := dedupeInHouseAppsByFilename[sw.SoftwarePackage.Name]; ok {
// ignore duplicate in-house app
continue
}
dedupeInHouseAppsByFilename[sw.SoftwarePackage.Name] = struct{}{}
}
pkgName := ""
if sw.SoftwarePackage.Name != "" {
pkgName = fmt.Sprintf(" (%s)", sw.SoftwarePackage.Name)
+3
View File
@@ -2472,6 +2472,9 @@ func TestGetTeamsYAMLAndApply(t *testing.T) {
ds.BatchSetSoftwareInstallersFunc = func(ctx context.Context, tmID *uint, installers []*fleet.UploadSoftwareInstallerPayload) error {
return nil
}
ds.BatchSetInHouseAppsInstallersFunc = func(ctx context.Context, tmID *uint, installers []*fleet.UploadSoftwareInstallerPayload) error {
return nil
}
actualYaml := RunAppForTest(t, []string{"get", "teams", "--yaml"})
yamlFilePath := writeTmpYml(t, actualYaml)
+19 -4
View File
@@ -316,6 +316,9 @@ func TestGitOpsBasicGlobalPremium(t *testing.T) {
ds.BatchSetSoftwareInstallersFunc = func(ctx context.Context, teamID *uint, installers []*fleet.UploadSoftwareInstallerPayload) error {
return nil
}
ds.BatchSetInHouseAppsInstallersFunc = func(ctx context.Context, teamID *uint, installers []*fleet.UploadSoftwareInstallerPayload) error {
return nil
}
ds.GetSoftwareInstallersFunc = func(ctx context.Context, tmID uint) ([]fleet.SoftwarePackageResponse, error) {
return nil, nil
}
@@ -684,6 +687,9 @@ func TestGitOpsBasicTeam(t *testing.T) {
ds.BatchSetSoftwareInstallersFunc = func(ctx context.Context, teamID *uint, installers []*fleet.UploadSoftwareInstallerPayload) error {
return nil
}
ds.BatchSetInHouseAppsInstallersFunc = func(ctx context.Context, teamID *uint, installers []*fleet.UploadSoftwareInstallerPayload) error {
return nil
}
ds.GetSoftwareInstallersFunc = func(ctx context.Context, tmID uint) ([]fleet.SoftwarePackageResponse, error) {
return nil, nil
}
@@ -1205,8 +1211,8 @@ func TestGitOpsFullTeam(t *testing.T) {
return team, nil
}
ds.GetTeamsWithInstallerByHashFunc = func(ctx context.Context, sha256, url string) (map[uint]*fleet.ExistingSoftwareInstaller, error) {
return map[uint]*fleet.ExistingSoftwareInstaller{}, nil
ds.GetTeamsWithInstallerByHashFunc = func(ctx context.Context, sha256, url string) (map[uint][]*fleet.ExistingSoftwareInstaller, error) {
return map[uint][]*fleet.ExistingSoftwareInstaller{}, nil
}
// Policies
@@ -1284,6 +1290,9 @@ func TestGitOpsFullTeam(t *testing.T) {
}
return nil
}
ds.BatchSetInHouseAppsInstallersFunc = func(ctx context.Context, teamID *uint, installers []*fleet.UploadSoftwareInstallerPayload) error {
return nil
}
ds.GetSoftwareInstallersFunc = func(ctx context.Context, tmID uint) ([]fleet.SoftwarePackageResponse, error) {
return nil, nil
}
@@ -1637,6 +1646,9 @@ func TestGitOpsBasicGlobalAndTeam(t *testing.T) {
ds.BatchSetSoftwareInstallersFunc = func(ctx context.Context, teamID *uint, installers []*fleet.UploadSoftwareInstallerPayload) error {
return nil
}
ds.BatchSetInHouseAppsInstallersFunc = func(ctx context.Context, teamID *uint, installers []*fleet.UploadSoftwareInstallerPayload) error {
return nil
}
ds.GetSoftwareInstallersFunc = func(ctx context.Context, tmID uint) ([]fleet.SoftwarePackageResponse, error) {
return nil, nil
}
@@ -2010,6 +2022,9 @@ func TestGitOpsBasicGlobalAndNoTeam(t *testing.T) {
ds.BatchSetSoftwareInstallersFunc = func(ctx context.Context, teamID *uint, installers []*fleet.UploadSoftwareInstallerPayload) error {
return nil
}
ds.BatchSetInHouseAppsInstallersFunc = func(ctx context.Context, teamID *uint, installers []*fleet.UploadSoftwareInstallerPayload) error {
return nil
}
ds.GetSoftwareInstallersFunc = func(ctx context.Context, tmID uint) ([]fleet.SoftwarePackageResponse, error) {
return nil, nil
}
@@ -2379,8 +2394,8 @@ func TestGitOpsFullGlobalAndTeam(t *testing.T) {
ds.GetABMTokenCountFunc = func(ctx context.Context) (int, error) {
return 0, nil
}
ds.GetTeamsWithInstallerByHashFunc = func(ctx context.Context, sha256, url string) (map[uint]*fleet.ExistingSoftwareInstaller, error) {
return map[uint]*fleet.ExistingSoftwareInstaller{}, nil
ds.GetTeamsWithInstallerByHashFunc = func(ctx context.Context, sha256, url string) (map[uint][]*fleet.ExistingSoftwareInstaller, error) {
return map[uint][]*fleet.ExistingSoftwareInstaller{}, nil
}
ds.GetSoftwareCategoryIDsFunc = func(ctx context.Context, names []string) ([]uint, error) {
return []uint{}, nil
@@ -0,0 +1,6 @@
name: No team
controls:
policies:
software:
packages:
- path: subdir/installer_ipa.yml
@@ -0,0 +1,6 @@
name: No team
controls:
policies:
software:
packages:
- url: ${SOFTWARE_INSTALLER_URL}/ipa_test.ipa
@@ -0,0 +1 @@
url: ${SOFTWARE_INSTALLER_URL}/ipa_test.ipa
@@ -0,0 +1,17 @@
name: "${TEST_TEAM_NAME}"
team_settings:
secrets:
- secret: "ABC"
features:
enable_host_users: true
enable_software_inventory: true
host_expiry_settings:
host_expiry_enabled: true
host_expiry_window: 30
agent_options:
controls:
policies:
queries:
software:
packages:
- path: ./subdir/installer_ipa.yml
@@ -0,0 +1,18 @@
name: "${TEST_TEAM_NAME}"
team_settings:
secrets:
- secret: "ABC"
features:
enable_host_users: true
enable_software_inventory: true
host_expiry_settings:
host_expiry_enabled: true
host_expiry_window: 30
agent_options:
controls:
policies:
queries:
software:
packages:
- url: ${SOFTWARE_INSTALLER_URL}/ipa_test.ipa
self_service: true
@@ -181,10 +181,14 @@ func ServeMDMBootstrapPackage(t *testing.T, pkgPath, pkgName string) (*httptest.
}
func StartSoftwareInstallerServer(t *testing.T) {
// start the web server that will serve the installer
// load the ruby installer to use as base bytes to repeat for the "too large" case
b, err := os.ReadFile(getPathRelative("../../../../server/service/testdata/software-installers/ruby.deb"))
require.NoError(t, err)
// get the base dir of all installers
baseDir := getPathRelative("../../../../server/service/testdata/software-installers/")
// start the web server that will serve the installer
srv := httptest.NewServer(
http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
@@ -203,9 +207,12 @@ func StartSoftwareInstallerServer(t *testing.T) {
n, _ := w.Write(b)
sz += n
}
default:
case strings.Contains(r.URL.Path, "other.deb"):
// serve same content as ruby.deb
w.Header().Set("Content-Type", "application/vnd.debian.binary-package")
_, _ = w.Write(b)
default:
http.ServeFile(w, r, filepath.Join(baseDir, filepath.Base(r.URL.Path)))
}
},
),
@@ -403,6 +410,9 @@ func SetupFullGitOpsPremiumServer(t *testing.T) (*mock.Store, **fleet.AppConfig,
ds.BatchSetSoftwareInstallersFunc = func(ctx context.Context, teamID *uint, installers []*fleet.UploadSoftwareInstallerPayload) error {
return nil
}
ds.BatchSetInHouseAppsInstallersFunc = func(ctx context.Context, teamID *uint, installers []*fleet.UploadSoftwareInstallerPayload) error {
return nil
}
ds.GetSoftwareInstallersFunc = func(ctx context.Context, tmID uint) ([]fleet.SoftwarePackageResponse, error) {
return nil, nil
}
@@ -144,6 +144,10 @@ func (s *enterpriseIntegrationGitopsTestSuite) TearDownTest() {
_, err := tx.ExecContext(ctx, "DELETE FROM vpp_apps;")
return err
})
mysql.ExecAdhocSQL(t, s.DS, func(tx sqlx.ExtContext) error {
_, err := tx.ExecContext(ctx, "DELETE FROM in_house_apps;")
return err
})
lbls, err := s.DS.ListLabels(ctx, fleet.TeamFilter{User: test.UserAdmin}, fleet.ListOptions{})
require.NoError(t, err)
@@ -1948,7 +1952,7 @@ queries:
labels:
- name: my-label
label_membership_type: manual
hosts:
hosts:
- %s
- %s
- %d
@@ -1978,3 +1982,222 @@ labels:
// Verify the correct hosts were added to the label
require.ElementsMatch(t, labelHostIDs, []uint{host1.ID, host2.ID, host3.ID, host5.ID})
}
func (s *enterpriseIntegrationGitopsTestSuite) TestIPASoftwareInstallers() {
t := s.T()
ctx := context.Background()
user := s.createGitOpsUser(t)
fleetctlConfig := s.createFleetctlConfig(t, user)
lbl, err := s.DS.NewLabel(ctx, &fleet.Label{Name: "Label1", Query: "SELECT 1"})
require.NoError(t, err)
require.NotZero(t, lbl.ID)
const (
globalTemplate = `
agent_options:
controls:
org_settings:
server_settings:
server_url: $FLEET_URL
org_info:
org_name: Fleet
secrets:
policies:
queries:
labels:
- name: Label1
label_membership_type: dynamic
query: SELECT 1
`
noTeamTemplate = `name: No team
controls:
policies:
software:
packages:
%s
`
teamTemplate = `
controls:
software:
packages:
%s
queries:
policies:
agent_options:
name: %s
team_settings:
secrets: [{"secret":"enroll_secret"}]
`
)
globalFile, err := os.CreateTemp(t.TempDir(), "*.yml")
require.NoError(t, err)
_, err = globalFile.WriteString(globalTemplate)
require.NoError(t, err)
err = globalFile.Close()
require.NoError(t, err)
// create an .ipa software for the no-team config
noTeamFile, err := os.CreateTemp(t.TempDir(), "*.yml")
require.NoError(t, err)
_, err = noTeamFile.WriteString(fmt.Sprintf(noTeamTemplate, `
- url: ${SOFTWARE_INSTALLER_URL}/ipa_test.ipa
self_service: true
`))
require.NoError(t, err)
err = noTeamFile.Close()
require.NoError(t, err)
noTeamFilePath := filepath.Join(filepath.Dir(noTeamFile.Name()), "no-team.yml")
err = os.Rename(noTeamFile.Name(), noTeamFilePath)
require.NoError(t, err)
// Set the required environment variables
t.Setenv("FLEET_URL", s.Server.URL)
testing_utils.StartSoftwareInstallerServer(t)
_ = fleetctl.RunAppForTest(t,
[]string{"gitops", "--config", fleetctlConfig.Name(), "-f", globalFile.Name(), "-f", noTeamFilePath, "--dry-run"})
_ = fleetctl.RunAppForTest(t,
[]string{"gitops", "--config", fleetctlConfig.Name(), "-f", globalFile.Name(), "-f", noTeamFilePath})
// the ipa installer was created for no team
titles, _, _, err := s.DS.ListSoftwareTitles(ctx, fleet.SoftwareTitleListOptions{AvailableForInstall: true, TeamID: ptr.Uint(0)},
fleet.TeamFilter{User: test.UserAdmin})
require.NoError(t, err)
require.Len(t, titles, 2)
var sources, platforms []string
for _, title := range titles {
require.Equal(t, "ipa_test", title.Name)
require.NotNil(t, title.BundleIdentifier)
require.Equal(t, "com.ipa-test.ipa-test", *title.BundleIdentifier)
sources = append(sources, title.Source)
require.NotNil(t, title.SoftwarePackage)
platforms = append(platforms, title.SoftwarePackage.Platform)
require.Equal(t, "ipa_test.ipa", title.SoftwarePackage.Name)
meta, err := s.DS.GetInHouseAppMetadataByTeamAndTitleID(ctx, nil, title.ID)
require.NoError(t, err)
require.True(t, meta.SelfService)
require.Empty(t, meta.LabelsExcludeAny)
require.Empty(t, meta.LabelsIncludeAny)
}
require.ElementsMatch(t, []string{"ios_apps", "ipados_apps"}, sources)
require.ElementsMatch(t, []string{"ios", "ipados"}, platforms)
// create a dummy install script, should be ignored for ipa apps
scriptFile, err := os.CreateTemp(t.TempDir(), "*.sh")
require.NoError(t, err)
_, err = scriptFile.WriteString(`echo "dummy install script"`)
require.NoError(t, err)
err = scriptFile.Close()
require.NoError(t, err)
// create an .ipa software for the team config
teamName := uuid.NewString()
teamFile, err := os.CreateTemp(t.TempDir(), "*.yml")
require.NoError(t, err)
_, err = teamFile.WriteString(fmt.Sprintf(teamTemplate, `
- url: ${SOFTWARE_INSTALLER_URL}/ipa_test.ipa
self_service: false
install_script:
path: `+scriptFile.Name()+`
labels_include_any:
- Label1
`, teamName))
require.NoError(t, err)
err = teamFile.Close()
require.NoError(t, err)
_ = fleetctl.RunAppForTest(t,
[]string{"gitops", "--config", fleetctlConfig.Name(), "-f", globalFile.Name(), "-f", teamFile.Name(), "--dry-run"})
_ = fleetctl.RunAppForTest(t,
[]string{"gitops", "--config", fleetctlConfig.Name(), "-f", globalFile.Name(), "-f", teamFile.Name()})
// get the team ID
team, err := s.DS.TeamByName(ctx, teamName)
require.NoError(t, err)
// the ipa installer was created for the team
titles, _, _, err = s.DS.ListSoftwareTitles(ctx, fleet.SoftwareTitleListOptions{AvailableForInstall: true, TeamID: &team.ID},
fleet.TeamFilter{User: test.UserAdmin})
require.NoError(t, err)
require.Len(t, titles, 2)
sources, platforms = []string{}, []string{}
for _, title := range titles {
require.Equal(t, "ipa_test", title.Name)
require.NotNil(t, title.BundleIdentifier)
require.Equal(t, "com.ipa-test.ipa-test", *title.BundleIdentifier)
sources = append(sources, title.Source)
require.NotNil(t, title.SoftwarePackage)
platforms = append(platforms, title.SoftwarePackage.Platform)
require.Equal(t, "ipa_test.ipa", title.SoftwarePackage.Name)
meta, err := s.DS.GetInHouseAppMetadataByTeamAndTitleID(ctx, &team.ID, title.ID)
require.NoError(t, err)
require.False(t, meta.SelfService)
require.Empty(t, meta.LabelsExcludeAny)
require.Len(t, meta.LabelsIncludeAny, 1)
require.Equal(t, lbl.ID, meta.LabelsIncludeAny[0].LabelID)
require.Empty(t, meta.InstallScript) // install script should be ignored for ipa apps
}
require.ElementsMatch(t, []string{"ios_apps", "ipados_apps"}, sources)
require.ElementsMatch(t, []string{"ios", "ipados"}, platforms)
// update the team config to clear the label condition
err = os.WriteFile(teamFile.Name(), []byte(fmt.Sprintf(teamTemplate, `
- url: ${SOFTWARE_INSTALLER_URL}/ipa_test.ipa
labels_include_any:
`, teamName)), 0o644)
require.NoError(t, err)
_ = fleetctl.RunAppForTest(t,
[]string{"gitops", "--config", fleetctlConfig.Name(), "-f", globalFile.Name(), "-f", teamFile.Name(), "--dry-run"})
_ = fleetctl.RunAppForTest(t,
[]string{"gitops", "--config", fleetctlConfig.Name(), "-f", globalFile.Name(), "-f", teamFile.Name()})
// the ipa installer was created for the team
titles, _, _, err = s.DS.ListSoftwareTitles(ctx, fleet.SoftwareTitleListOptions{AvailableForInstall: true, TeamID: &team.ID},
fleet.TeamFilter{User: test.UserAdmin})
require.NoError(t, err)
require.Len(t, titles, 2)
sources, platforms = []string{}, []string{}
for _, title := range titles {
require.Equal(t, "ipa_test", title.Name)
require.NotNil(t, title.BundleIdentifier)
require.Equal(t, "com.ipa-test.ipa-test", *title.BundleIdentifier)
sources = append(sources, title.Source)
require.NotNil(t, title.SoftwarePackage)
platforms = append(platforms, title.SoftwarePackage.Platform)
require.Equal(t, "ipa_test.ipa", title.SoftwarePackage.Name)
meta, err := s.DS.GetInHouseAppMetadataByTeamAndTitleID(ctx, &team.ID, title.ID)
require.NoError(t, err)
require.False(t, meta.SelfService)
require.Empty(t, meta.LabelsExcludeAny)
require.Empty(t, meta.LabelsIncludeAny)
}
require.ElementsMatch(t, []string{"ios_apps", "ipados_apps"}, sources)
require.ElementsMatch(t, []string{"ios", "ipados"}, platforms)
// update the team config to clear all installers
err = os.WriteFile(teamFile.Name(), []byte(fmt.Sprintf(teamTemplate, "", teamName)), 0o644)
require.NoError(t, err)
_ = fleetctl.RunAppForTest(t,
[]string{"gitops", "--config", fleetctlConfig.Name(), "-f", globalFile.Name(), "-f", teamFile.Name(), "--dry-run"})
_ = fleetctl.RunAppForTest(t,
[]string{"gitops", "--config", fleetctlConfig.Name(), "-f", globalFile.Name(), "-f", teamFile.Name()})
titles, _, _, err = s.DS.ListSoftwareTitles(ctx, fleet.SoftwareTitleListOptions{AvailableForInstall: true, TeamID: &team.ID},
fleet.TeamFilter{User: test.UserAdmin})
require.NoError(t, err)
require.Len(t, titles, 0)
}
@@ -34,7 +34,7 @@ func TestGitOpsTeamSoftwareInstallers(t *testing.T) {
}{
{"testdata/gitops/team_software_installer_not_found.yml", "Please make sure that URLs are reachable from your Fleet server."},
{"testdata/gitops/team_software_installer_install_script_secret.yml", "environment variable \"FLEET_SECRET_NAME\" not set"},
{"testdata/gitops/team_software_installer_unsupported.yml", "The file should be .pkg, .msi, .exe, .deb, .rpm, .tar.gz, .sh, or .ps1."},
{"testdata/gitops/team_software_installer_unsupported.yml", "The file should be .pkg, .msi, .exe, .deb, .rpm, .tar.gz, .sh, .ipa or .ps1."},
// commenting out, results in the process getting killed on CI and on some machines
// {"testdata/gitops/team_software_installer_too_large.yml", "The maximum file size is 3 GB"},
{"testdata/gitops/team_software_installer_valid.yml", ""},
@@ -65,6 +65,8 @@ func TestGitOpsTeamSoftwareInstallers(t *testing.T) {
{"testdata/gitops/team_setup_software_invalid_script.yml", "no_such_script.sh: no such file"},
{"testdata/gitops/team_setup_software_invalid_software_package.yml", "no_such_software.yml\" does not exist for that team"},
{"testdata/gitops/team_setup_software_invalid_vpp_app.yml", "\"no_such_app\" does not exist for that team"},
{"testdata/gitops/team_software_installer_valid_ipa.yml", ""},
{"testdata/gitops/team_software_installer_subdir_ipa.yml", ""},
}
for _, c := range cases {
c.file = filepath.Join("../../fleetctl", c.file)
@@ -127,8 +129,8 @@ func TestGitOpsTeamSoftwareInstallers(t *testing.T) {
}
return ret, nil
}
ds.GetTeamsWithInstallerByHashFunc = func(ctx context.Context, sha256, url string) (map[uint]*fleet.ExistingSoftwareInstaller, error) {
return map[uint]*fleet.ExistingSoftwareInstaller{}, nil
ds.GetTeamsWithInstallerByHashFunc = func(ctx context.Context, sha256, url string) (map[uint][]*fleet.ExistingSoftwareInstaller, error) {
return map[uint][]*fleet.ExistingSoftwareInstaller{}, nil
}
ds.GetSoftwareCategoryIDsFunc = func(ctx context.Context, names []string) ([]uint, error) {
return []uint{}, nil
@@ -156,11 +158,14 @@ func TestGitOpsTeamSoftwareInstallersQueryEnv(t *testing.T) {
}
return nil
}
ds.BatchSetInHouseAppsInstallersFunc = func(ctx context.Context, tmID *uint, installers []*fleet.UploadSoftwareInstallerPayload) error {
return nil
}
ds.GetSoftwareInstallersFunc = func(ctx context.Context, tmID uint) ([]fleet.SoftwarePackageResponse, error) {
return nil, nil
}
ds.GetTeamsWithInstallerByHashFunc = func(ctx context.Context, sha256, url string) (map[uint]*fleet.ExistingSoftwareInstaller, error) {
return map[uint]*fleet.ExistingSoftwareInstaller{}, nil
ds.GetTeamsWithInstallerByHashFunc = func(ctx context.Context, sha256, url string) (map[uint][]*fleet.ExistingSoftwareInstaller, error) {
return map[uint][]*fleet.ExistingSoftwareInstaller{}, nil
}
ds.GetSoftwareCategoryIDsFunc = func(ctx context.Context, names []string) ([]uint, error) {
return []uint{}, nil
@@ -295,7 +300,7 @@ func TestGitOpsNoTeamSoftwareInstallers(t *testing.T) {
wantErr string
}{
{"testdata/gitops/no_team_software_installer_not_found.yml", "Please make sure that URLs are reachable from your Fleet server."},
{"testdata/gitops/no_team_software_installer_unsupported.yml", "The file should be .pkg, .msi, .exe, .deb, .rpm, .tar.gz, .sh, or .ps1."},
{"testdata/gitops/no_team_software_installer_unsupported.yml", "The file should be .pkg, .msi, .exe, .deb, .rpm, .tar.gz, .sh, .ipa or .ps1."},
// commenting out, results in the process getting killed on CI and on some machines
// {"testdata/gitops/no_team_software_installer_too_large.yml", "The maximum file size is 3 GB"},
{"testdata/gitops/no_team_software_installer_valid.yml", ""},
@@ -320,6 +325,8 @@ func TestGitOpsNoTeamSoftwareInstallers(t *testing.T) {
{"testdata/gitops/no_team_setup_software_invalid_script.yml", "no_such_script.sh: no such file"},
{"testdata/gitops/no_team_setup_software_invalid_software_package.yml", "no_such_software.yml\" does not exist for that team"},
{"testdata/gitops/no_team_setup_software_invalid_vpp_app.yml", "\"no_such_app\" does not exist for that team"},
{"testdata/gitops/no_team_software_installer_valid_ipa.yml", ""},
{"testdata/gitops/no_team_software_installer_subdir_ipa.yml", ""},
}
for _, c := range cases {
c.noTeamFile = filepath.Join("../../fleetctl", c.noTeamFile)
@@ -380,8 +387,8 @@ func TestGitOpsNoTeamSoftwareInstallers(t *testing.T) {
}
return ret, nil
}
ds.GetTeamsWithInstallerByHashFunc = func(ctx context.Context, sha256, url string) (map[uint]*fleet.ExistingSoftwareInstaller, error) {
return map[uint]*fleet.ExistingSoftwareInstaller{}, nil
ds.GetTeamsWithInstallerByHashFunc = func(ctx context.Context, sha256, url string) (map[uint][]*fleet.ExistingSoftwareInstaller, error) {
return map[uint][]*fleet.ExistingSoftwareInstaller{}, nil
}
ds.GetSoftwareCategoryIDsFunc = func(ctx context.Context, names []string) ([]uint, error) {
return []uint{}, nil
+144 -56
View File
@@ -1645,7 +1645,7 @@ func (svc *Service) addMetadataToSoftwarePayload(ctx context.Context, payload *f
if err != nil {
if errors.Is(err, file.ErrUnsupportedType) {
return "", &fleet.BadRequestError{
Message: "Couldn't edit software. File type not supported. The file should be .pkg, .msi, .exe, .deb, .rpm, .tar.gz, .sh, or .ps1.",
Message: "Couldn't edit software. File type not supported. The file should be .pkg, .msi, .exe, .deb, .rpm, .tar.gz, .sh, .ipa or .ps1.",
InternalErr: ctxerr.Wrap(ctx, err, "extracting metadata from installer"),
}
}
@@ -1687,7 +1687,7 @@ func (svc *Service) addMetadataToSoftwarePayload(ctx context.Context, payload *f
}
// Software edits validate non-empty scripts later, so set failOnBlankScript to false
if payload.InstallScript == "" && failOnBlankScript {
if payload.InstallScript == "" && failOnBlankScript && payload.Extension != "ipa" {
return "", &fleet.BadRequestError{
Message: fmt.Sprintf("Couldn't add. Install script is required for .%s packages.", strings.ToLower(payload.Extension)),
}
@@ -1700,28 +1700,35 @@ func (svc *Service) addMetadataToSoftwarePayload(ctx context.Context, payload *f
payload.UninstallScript = file.UninstallMsiWithUpgradeCodeScript
}
}
if payload.UninstallScript == "" && failOnBlankScript {
if payload.UninstallScript == "" && failOnBlankScript && payload.Extension != "ipa" {
return "", &fleet.BadRequestError{
Message: fmt.Sprintf("Couldn't add. Uninstall script is required for .%s packages.", strings.ToLower(payload.Extension)),
}
}
if payload.BundleIdentifier != "" {
payload.Source = "apps"
} else {
source, err := fleet.SofwareInstallerSourceFromExtensionAndName(meta.Extension, meta.Name)
if err != nil {
return "", ctxerr.Wrap(ctx, err, "determining source from extension and name")
}
payload.Source = source
}
platform, err := fleet.SoftwareInstallerPlatformFromExtension(meta.Extension)
if err != nil {
return "", ctxerr.Wrap(ctx, err, "determining platform from extension")
}
payload.Platform = platform
switch {
case payload.Extension == "ipa":
if payload.Platform == "ipados" {
payload.Source = "ipados_apps"
} else {
payload.Source = "ios_apps"
}
case payload.BundleIdentifier != "":
payload.Source = "apps"
default:
source, err := fleet.SofwareInstallerSourceFromExtensionAndName(meta.Extension, meta.Name)
if err != nil {
return "", ctxerr.Wrap(ctx, err, "determining source from extension and name")
}
payload.Source = source
}
return meta.Extension, nil
}
@@ -1944,6 +1951,11 @@ func (svc *Service) softwareBatchUpload(
) {
var batchErr error
// TODO: this might be a little drastic to drop back to Background context,
// consider using ctx.WithoutCancel to keep all but the cancellation of the
// parent: https://pkg.go.dev/context#WithoutCancel
// e.g. for telemetry and such.
// We do not use the request ctx on purpose because this method runs in the background.
ctx := context.Background()
@@ -2023,9 +2035,23 @@ func (svc *Service) softwareBatchUpload(
var g errgroup.Group
g.SetLimit(1) // TODO: consider whether we can increase this limit, see https://github.com/fleetdm/fleet/issues/22704#issuecomment-2397407837
// critical to avoid data race, the slice is pre-allocated and each
// the reason for this struct with extra installers support is that:
// - ih-house apps match multiple installers to a single source installer
// payload (because an .ipa creates entries for iOS and iPadOS)
// - the for loop over each entry in the payload is executed in a goroutine
// that can only write to its pre-allocated index in the installers slice, so
// any extra installer for a given payload must be part of a single value
// inserted in that slice.
type installerPayloadWithExtras struct {
*fleet.UploadSoftwareInstallerPayload
ExtraInstallers []*fleet.UploadSoftwareInstallerPayload
}
// critical to avoid data race, the slices are pre-allocated and each
// goroutine only writes to its index.
installers := make([]*fleet.UploadSoftwareInstallerPayload, len(payloads))
installers := make([]*installerPayloadWithExtras, len(payloads))
toBeClosedTFRs := make([]*fleet.TempFileReader, len(payloads))
for i, p := range payloads {
i, p := i, p
@@ -2033,8 +2059,8 @@ func (svc *Service) softwareBatchUpload(
g.Go(func() error {
// NOTE: cannot defer tfr.Close() here because the reader needs to be
// available after the goroutine completes. Instead, all temp file
// readers will have their Close deferred after the join/wait of
// goroutines.
// readers are collected in toBeClosedTFRs and will have their Close
// deferred after the join/wait of goroutines.
installer := &fleet.UploadSoftwareInstallerPayload{
TeamID: teamID,
InstallScript: p.InstallScript,
@@ -2051,6 +2077,8 @@ func (svc *Service) softwareBatchUpload(
Categories: p.Categories,
}
var extraInstallers []*fleet.UploadSoftwareInstallerPayload
p.Categories = server.RemoveDuplicatesFromSlice(p.Categories)
catIDs, err := svc.ds.GetSoftwareCategoryIDs(ctx, p.Categories)
if err != nil {
@@ -2077,12 +2105,11 @@ func (svc *Service) softwareBatchUpload(
tmID = *teamID
}
foundInstaller, ok := teamIDs[tmID]
foundInstallers, ok := teamIDs[tmID]
switch {
case ok:
// Perfect match: existing installer on the same team
installer.StorageID = p.SHA256
foundInstaller := foundInstallers[0]
if foundInstaller.Extension == "exe" || foundInstaller.Extension == "tar.gz" {
if p.InstallScript == "" {
@@ -2093,18 +2120,19 @@ func (svc *Service) softwareBatchUpload(
return fmt.Errorf("Couldn't edit. Uninstall script is required for .%s packages.", foundInstaller.Extension)
}
}
installer.Extension = foundInstaller.Extension
installer.Filename = foundInstaller.Filename
installer.Version = foundInstaller.Version
installer.Platform = foundInstaller.Platform
installer.Source = foundInstaller.Source
if foundInstaller.BundleIdentifier != nil {
installer.BundleIdentifier = *foundInstaller.BundleIdentifier
// make a copy of the installer without filled fields in case we add
// extra installers
extraInstallerBase := *installer
fillSoftwareInstallerPayloadFromExisting(installer, foundInstaller, p.SHA256)
for _, extraInstaller := range foundInstallers[1:] {
extraPayload := extraInstallerBase
fillSoftwareInstallerPayloadFromExisting(&extraPayload, extraInstaller, p.SHA256)
extraInstallers = append(extraInstallers, &extraPayload)
}
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
// Installer(s) exists, but for another team. We should copy it over to this team
// (if we have access to the other team).
user, err := svc.ds.UserByID(ctx, userID)
if err != nil {
@@ -2113,7 +2141,7 @@ func (svc *Service) softwareBatchUpload(
userctx := viewer.NewContext(ctx, viewer.Viewer{User: user})
for tmID, i := range teamIDs {
for tmID, teamInstallers := range teamIDs {
// use the first one to which this user has access; the specific one shouldn't
// matter because they're all the same installer bytes
var tmIDPtr *uint
@@ -2124,7 +2152,8 @@ func (svc *Service) softwareBatchUpload(
continue
}
if i.Extension == "exe" {
teamInstaller := teamInstallers[0]
if teamInstaller.Extension == "exe" {
if p.InstallScript == "" {
return errors.New("Couldn't edit. Install script is required for .exe packages.")
}
@@ -2134,17 +2163,16 @@ func (svc *Service) softwareBatchUpload(
}
}
installer.Extension = i.Extension
installer.Filename = i.Filename
installer.Version = i.Version
installer.Platform = i.Platform
installer.Source = i.Source
if i.BundleIdentifier != nil {
installer.BundleIdentifier = *i.BundleIdentifier
// make a copy of the installer without filled fields in case we add
// extra installers
extraInstallerBase := *installer
fillSoftwareInstallerPayloadFromExisting(installer, teamInstaller, p.SHA256)
for _, extraInstaller := range teamInstallers[1:] {
extraPayload := extraInstallerBase
fillSoftwareInstallerPayloadFromExisting(&extraPayload, extraInstaller, p.SHA256)
extraInstallers = append(extraInstallers, &extraPayload)
}
installer.Title = i.Title
installer.StorageID = p.SHA256
installer.PackageIDs = i.PackageIDs
break
}
}
@@ -2170,17 +2198,25 @@ func (svc *Service) softwareBatchUpload(
}
installer.InstallerFile = tfr
toBeClosedTFRs[i] = tfr
filename = maintained_apps.FilenameFromResponse(resp)
installer.Filename = filename
// For script packages (.sh and .ps1), clear unsupported fields early.
// Determine extension from filename to validate before metadata extraction.
// For script packages (.sh and .ps1) and in-house apps (.ipa), clear
// unsupported fields early. Determine extension from filename to
// validate before metadata extraction.
ext := strings.ToLower(filepath.Ext(filename))
ext = strings.TrimPrefix(ext, ".")
if fleet.IsScriptPackage(ext) {
installer.PostInstallScript = ""
installer.UninstallScript = ""
installer.PreInstallQuery = ""
} else if ext == "ipa" {
installer.InstallScript = ""
installer.PostInstallScript = ""
installer.UninstallScript = ""
installer.PreInstallQuery = ""
}
}
@@ -2244,7 +2280,6 @@ func (svc *Service) softwareBatchUpload(
if installer.FleetMaintainedAppID == nil && installer.InstallerFile != nil {
ext, err = svc.addMetadataToSoftwarePayload(ctx, installer, true)
if err != nil {
_ = installer.InstallerFile.Close() // closing the temp file here since it will not be available after the goroutine completes
return err
}
@@ -2254,14 +2289,17 @@ func (svc *Service) softwareBatchUpload(
}
}
// For script packages (.sh and .ps1), clear unsupported fields
// The file contents become the install script, so post_install_script,
// uninstall_script, and pre_install_query are not supported.
if fleet.IsScriptPackage(installer.Extension) {
// For script packages (.sh and .ps1) and in-house apps (.ipa), clear
// unsupported fields. For script packages, the file contents become the
// install script, so post_install_script, uninstall_script, and
// pre_install_query are not supported.
switch {
case fleet.IsScriptPackage(installer.Extension):
installer.PostInstallScript = ""
installer.UninstallScript = ""
installer.PreInstallQuery = ""
} else if installer.Extension != "exe" {
case installer.Extension != "exe":
// custom scripts only for exe installers and non-script packages
if installer.InstallScript == "" {
installer.InstallScript = file.GetInstallScript(installer.Extension)
@@ -2270,6 +2308,12 @@ func (svc *Service) softwareBatchUpload(
if installer.UninstallScript == "" {
installer.UninstallScript = file.GetUninstallScript(installer.Extension)
}
case installer.Extension == "ipa":
installer.PostInstallScript = ""
installer.UninstallScript = ""
installer.PreInstallQuery = ""
installer.InstallScript = ""
}
// Update $PACKAGE_ID/$UPGRADE_CODE in uninstall script
@@ -2290,7 +2334,24 @@ func (svc *Service) softwareBatchUpload(
installer.Title = installer.Filename
}
installers[i] = installer
// if this is an .ipa and there is no extra installer, create it here
if installer.Extension == "ipa" && len(extraInstallers) == 0 {
extraPayload := *installer
switch installer.Platform {
case string(fleet.IOSPlatform):
extraPayload.Platform = string(fleet.IPadOSPlatform)
extraPayload.Source = "ipados_apps"
case string(fleet.IPadOSPlatform):
extraPayload.Platform = string(fleet.IOSPlatform)
extraPayload.Source = "ios_apps"
}
extraInstallers = append(extraInstallers, &extraPayload)
}
installers[i] = &installerPayloadWithExtras{
UploadSoftwareInstallerPayload: installer,
ExtraInstallers: extraInstallers,
}
return nil
})
@@ -2299,9 +2360,9 @@ func (svc *Service) softwareBatchUpload(
waitErr := g.Wait()
// defer close for any valid temp file reader
for _, payload := range installers {
if payload != nil && payload.InstallerFile != nil {
defer payload.InstallerFile.Close()
for _, tfr := range toBeClosedTFRs {
if tfr != nil {
defer tfr.Close()
}
}
@@ -2315,22 +2376,49 @@ func (svc *Service) softwareBatchUpload(
return
}
for _, payload := range installers {
var inHouseInstallers, softwareInstallers []*fleet.UploadSoftwareInstallerPayload
for _, payloadWithExtras := range installers {
payload := payloadWithExtras.UploadSoftwareInstallerPayload
if err := svc.storeSoftware(ctx, payload); err != nil {
batchErr = fmt.Errorf("storing software installer %q: %w", payload.Filename, err)
return
}
if payload.Extension == "ipa" {
inHouseInstallers = append(inHouseInstallers, payload)
inHouseInstallers = append(inHouseInstallers, payloadWithExtras.ExtraInstallers...)
} else {
softwareInstallers = append(softwareInstallers, payload)
softwareInstallers = append(softwareInstallers, payloadWithExtras.ExtraInstallers...)
}
}
if err := svc.ds.BatchSetSoftwareInstallers(ctx, teamID, installers); err != nil {
if err := svc.ds.BatchSetSoftwareInstallers(ctx, teamID, softwareInstallers); err != nil {
batchErr = fmt.Errorf("batch set software installers: %w", err)
return
}
if err := svc.ds.BatchSetInHouseAppsInstallers(ctx, teamID, inHouseInstallers); err != nil {
batchErr = fmt.Errorf("batch set in-house apps installers: %w", err)
return
}
// Note: per @noahtalerman we don't want activity items for CLI actions
// anymore, so that's intentionally skipped.
}
func fillSoftwareInstallerPayloadFromExisting(payload *fleet.UploadSoftwareInstallerPayload, existing *fleet.ExistingSoftwareInstaller, sha256Hash string) {
payload.Extension = existing.Extension
payload.Filename = existing.Filename
payload.Version = existing.Version
payload.Platform = existing.Platform
payload.Source = existing.Source
if existing.BundleIdentifier != nil {
payload.BundleIdentifier = *existing.BundleIdentifier
}
payload.Title = existing.Title
payload.StorageID = sha256Hash
payload.PackageIDs = existing.PackageIDs
}
func (svc *Service) GetBatchSetSoftwareInstallersResult(ctx context.Context, tmName string, requestUUID string, dryRun bool) (string, string, []fleet.SoftwarePackageResponse, error) {
// We've already authorized in the POST /api/latest/fleet/software/batch,
// but adding it here so we don't need to worry about a special case endpoint.
+629 -10
View File
@@ -196,7 +196,8 @@ SELECT
iha.created_at AS uploaded_at,
st.bundle_identifier AS bundle_identifier,
COALESCE(st.name, '') AS software_title,
iha.self_service
iha.self_service,
iha.url
FROM
in_house_apps iha
JOIN software_titles st ON st.id = iha.title_id
@@ -314,13 +315,13 @@ func (ds *Datastore) RemovePendingInHouseAppInstalls(ctx context.Context, inHous
}
var installs []ipaInstall
err := sqlx.SelectContext(ctx, ds.reader(ctx), &installs, `
SELECT
host_id,
command_uuid
FROM
host_in_house_software_installs
WHERE
in_house_app_id = ? AND
SELECT
host_id,
command_uuid
FROM
host_in_house_software_installs
WHERE
in_house_app_id = ? AND
canceled = 0 AND
verification_at IS NULL AND
verification_failed_at IS NULL
@@ -367,16 +368,21 @@ upcoming AS (
),
-- select most recent past activities for each host
-- NOTE if you change this logic make sure to change inHouseAppHostStatusNamedQuery accordingly
past AS (
SELECT
hihsi.host_id,
CASE
WHEN ncr.status = :mdm_status_acknowledged THEN
WHEN hihsi.verification_at IS NOT NULL THEN
:software_status_installed
WHEN hihsi.verification_failed_at IS NOT NULL THEN
:software_status_failed
WHEN ncr.status = :mdm_status_error OR ncr.status = :mdm_status_format_error THEN
:software_status_failed
WHEN ncr.status = :mdm_status_acknowledged THEN
:software_status_pending
ELSE
NULL -- either pending or not installed
NULL -- either pending or not installed via in-house App
END AS status
FROM
host_in_house_software_installs hihsi
@@ -684,3 +690,616 @@ WHERE
return user, act, nil
}
func (ds *Datastore) BatchSetInHouseAppsInstallers(ctx context.Context, tmID *uint, installers []*fleet.UploadSoftwareInstallerPayload) error {
const upsertSoftwareTitles = `
INSERT INTO software_titles
(name, source, extension_for, bundle_identifier)
VALUES
%s
ON DUPLICATE KEY UPDATE
name = VALUES(name),
source = VALUES(source),
extension_for = VALUES(extension_for),
bundle_identifier = VALUES(bundle_identifier)
`
const loadSoftwareTitles = `
SELECT
id
FROM
software_titles
WHERE (unique_identifier, source, extension_for) IN (%s)
`
const cancelAllPendingInHouseInstalls = `
UPDATE
host_in_house_software_installs
SET
canceled = 1
WHERE
verification_at IS NULL AND
verification_failed_at IS NULL AND
in_house_app_id IN (
SELECT id FROM in_house_apps WHERE global_or_team_id = ?
)
`
const cancelAllPendingInHouseNanoCmds = `
UPDATE
nano_enrollment_queue
SET
active = 0
WHERE
command_uuid IN (
SELECT command_uuid
FROM host_in_house_software_installs hihsi
INNER JOIN in_house_apps iha ON hihsi.in_house_app_id = iha.id
WHERE
hihsi.verification_at IS NULL AND
hihsi.verification_failed_at IS NULL AND
iha.global_or_team_id = ?
)
`
const loadAffectedHostsPendingInHouseInstallsUA = `
SELECT
DISTINCT host_id
FROM
upcoming_activities ua
INNER JOIN in_house_app_upcoming_activities ihua
ON ua.id = ihua.upcoming_activity_id
WHERE
ua.activity_type = 'in_house_app_install' AND
ua.activated_at IS NOT NULL AND
ihua.in_house_app_id IN (
SELECT id FROM in_house_apps WHERE global_or_team_id = ?
)
`
const deleteAllPendingInHouseInstallsUA = `
DELETE FROM upcoming_activities
USING upcoming_activities
INNER JOIN in_house_app_upcoming_activities ihua
ON upcoming_activities.id = ihua.upcoming_activity_id
WHERE
activity_type = 'in_house_app_install' AND
ihua.in_house_app_id IN (
SELECT id FROM in_house_apps WHERE global_or_team_id = ?
)
`
const markAllInHouseInstallsAsRemoved = `
UPDATE host_in_house_software_installs SET removed = TRUE
WHERE in_house_app_id IN (
SELECT id FROM in_house_apps WHERE global_or_team_id = ?
)
`
const deleteAllInHouseInstallersInTeam = `
DELETE FROM
in_house_apps
WHERE
global_or_team_id = ?
`
const cancelPendingInHouseInstallsNotInList = `
UPDATE
host_in_house_software_installs
SET
canceled = 1
WHERE
verification_at IS NULL AND
verification_failed_at IS NULL AND
in_house_app_id IN (
SELECT id FROM in_house_apps WHERE global_or_team_id = ? AND title_id NOT IN (?)
)
`
const cancelPendingInHouseNanoCmdsNotInList = `
UPDATE
nano_enrollment_queue
SET
active = 0
WHERE
command_uuid IN (
SELECT command_uuid
FROM host_in_house_software_installs hihsi
INNER JOIN in_house_apps iha ON hihsi.in_house_app_id = iha.id
WHERE
hihsi.verification_at IS NULL AND
hihsi.verification_failed_at IS NULL AND
iha.global_or_team_id = ? AND
iha.title_id NOT IN (?)
)
`
const loadAffectedHostsPendingInHouseInstallsNotInListUA = `
SELECT
DISTINCT host_id
FROM
upcoming_activities ua
INNER JOIN in_house_app_upcoming_activities ihua
ON ua.id = ihua.upcoming_activity_id
WHERE
ua.activity_type = 'in_house_app_install' AND
ua.activated_at IS NOT NULL AND
ihua.in_house_app_id IN (
SELECT id FROM in_house_apps WHERE global_or_team_id = ? AND title_id NOT IN (?)
)
`
const deletePendingInHouseInstallsNotInListUA = `
DELETE FROM upcoming_activities
USING upcoming_activities
INNER JOIN in_house_app_upcoming_activities ihua
ON upcoming_activities.id = ihua.upcoming_activity_id
WHERE
activity_type = 'in_house_app_install' AND
ihua.in_house_app_id IN (
SELECT id FROM in_house_apps WHERE global_or_team_id = ? AND title_id NOT IN (?)
)
`
const markInHouseInstallsNotInListAsRemoved = `
UPDATE host_in_house_software_installs SET removed = TRUE
WHERE in_house_app_id IN (
SELECT id FROM in_house_apps WHERE global_or_team_id = ? AND title_id NOT IN (?)
)
`
const deleteInHouseInstallersNotInList = `
DELETE FROM
in_house_apps
WHERE
global_or_team_id = ? AND
title_id NOT IN (?)
`
const checkExistingInstaller = `
SELECT
id,
storage_id != ? is_package_modified
FROM
in_house_apps
WHERE
global_or_team_id = ? AND
title_id IN (SELECT id FROM software_titles WHERE unique_identifier = ? AND source = ? AND extension_for = '')
`
const insertNewOrEditedInstaller = `
INSERT INTO in_house_apps (
title_id,
team_id,
global_or_team_id,
filename,
version,
storage_id,
platform,
bundle_identifier,
self_service,
url
) VALUES (
(SELECT id FROM software_titles WHERE unique_identifier = ? AND source = ? AND extension_for = ''),
?, ?, ?, ?, ?, ?, ?, ?, ?
)
ON DUPLICATE KEY UPDATE
filename = VALUES(filename),
version = VALUES(version),
storage_id = VALUES(storage_id),
platform = VALUES(platform),
bundle_identifier = VALUES(bundle_identifier),
self_service = VALUES(self_service),
url = VALUES(url)
`
const loadInHouseInstallerID = `
SELECT
id
FROM
in_house_apps
WHERE
-- this is guaranteed to select a single in-house installer, due to unique index
global_or_team_id = ? AND
filename = ? AND
platform = ?
`
const deleteInHouseLabelsNotInList = `
DELETE FROM
in_house_app_labels
WHERE
in_house_app_id = ? AND
label_id NOT IN (?)
`
const deleteAllInHouseLabels = `
DELETE FROM
in_house_app_labels
WHERE
in_house_app_id = ?
`
const upsertInHouseLabels = `
INSERT INTO
in_house_app_labels (
in_house_app_id,
label_id,
exclude
)
VALUES
%s
ON DUPLICATE KEY UPDATE
exclude = VALUES(exclude)
`
const loadExistingInHouseLabels = `
SELECT
label_id,
exclude
FROM
in_house_app_labels
WHERE
in_house_app_id = ?
`
// use a team id of 0 if no-team
var globalOrTeamID uint
if tmID != nil {
globalOrTeamID = *tmID
}
// NOTE: at the time of implementation, in-house apps do not support install
// during setup, automatic install (via policies), categories, and
// uninstalls, so the related validations and updates that are done in
// BatchSetSoftwareInstallers are removed here.
var activateAffectedHostIDs []uint
err := ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
// if no installers are provided, just delete whatever was in the table
if len(installers) == 0 {
if _, err := tx.ExecContext(ctx, cancelAllPendingInHouseInstalls, globalOrTeamID); err != nil {
return ctxerr.Wrap(ctx, err, "cancel all pending host in-house install records")
}
if _, err := tx.ExecContext(ctx, cancelAllPendingInHouseNanoCmds, globalOrTeamID); err != nil {
return ctxerr.Wrap(ctx, err, "cancel all pending in-house nano commands")
}
var affectedHostIDs []uint
if err := sqlx.SelectContext(ctx, tx, &affectedHostIDs,
loadAffectedHostsPendingInHouseInstallsUA, globalOrTeamID); err != nil {
return ctxerr.Wrap(ctx, err, "load affected hosts for upcoming in-house installs")
}
activateAffectedHostIDs = affectedHostIDs
if _, err := tx.ExecContext(ctx, deleteAllPendingInHouseInstallsUA, globalOrTeamID); err != nil {
return ctxerr.Wrap(ctx, err, "delete all upcoming pending in-house install records")
}
if _, err := tx.ExecContext(ctx, markAllInHouseInstallsAsRemoved, globalOrTeamID); err != nil {
return ctxerr.Wrap(ctx, err, "mark all host in-house installs as removed")
}
if _, err := tx.ExecContext(ctx, deleteAllInHouseInstallersInTeam, globalOrTeamID); err != nil {
return ctxerr.Wrap(ctx, err, "delete obsolete in-house installers")
}
return nil
}
var args []any
for _, installer := range installers {
args = append(
args,
strings.TrimSuffix(installer.Filename, ".ipa"),
installer.Source,
"",
func() *string {
if strings.TrimSpace(installer.BundleIdentifier) != "" {
return &installer.BundleIdentifier
}
return nil
}(),
)
}
values := strings.TrimSuffix(strings.Repeat("(?,?,?,?),", len(installers)), ",")
if _, err := tx.ExecContext(ctx, fmt.Sprintf(upsertSoftwareTitles, values), args...); err != nil {
return ctxerr.Wrap(ctx, err, "insert new/edited software titles")
}
var titleIDs []uint
args = []any{}
for _, installer := range installers {
args = append(
args,
BundleIdentifierOrName(installer.BundleIdentifier, strings.TrimSuffix(installer.Filename, ".ipa")),
installer.Source,
"",
)
}
values = strings.TrimSuffix(strings.Repeat("(?,?,?),", len(installers)), ",")
if err := sqlx.SelectContext(ctx, tx, &titleIDs, fmt.Sprintf(loadSoftwareTitles, values), args...); err != nil {
return ctxerr.Wrap(ctx, err, "load existing titles")
}
stmt, args, err := sqlx.In(cancelPendingInHouseInstallsNotInList, globalOrTeamID, titleIDs)
if err != nil {
return ctxerr.Wrap(ctx, err, "build statement to cancel pending in-house installs")
}
if _, err := tx.ExecContext(ctx, stmt, args...); err != nil {
return ctxerr.Wrap(ctx, err, "cancel obsolete pending host in-house install records")
}
stmt, args, err = sqlx.In(cancelPendingInHouseNanoCmdsNotInList, globalOrTeamID, titleIDs)
if err != nil {
return ctxerr.Wrap(ctx, err, "build statement to cancel pending in-house nano commands")
}
if _, err := tx.ExecContext(ctx, stmt, args...); err != nil {
return ctxerr.Wrap(ctx, err, "cancel obsolete pending host in-house install nano commands")
}
stmt, args, err = sqlx.In(loadAffectedHostsPendingInHouseInstallsNotInListUA, globalOrTeamID, titleIDs)
if err != nil {
return ctxerr.Wrap(ctx, err, "build statement to load affected hosts for upcoming in-house installs")
}
var affectedHostIDs []uint
if err := sqlx.SelectContext(ctx, tx, &affectedHostIDs, stmt, args...); err != nil {
return ctxerr.Wrap(ctx, err, "load affected hosts for upcoming in-house installs")
}
activateAffectedHostIDs = affectedHostIDs
stmt, args, err = sqlx.In(deletePendingInHouseInstallsNotInListUA, globalOrTeamID, titleIDs)
if err != nil {
return ctxerr.Wrap(ctx, err, "build statement to delete upcoming pending in-house installs")
}
if _, err := tx.ExecContext(ctx, stmt, args...); err != nil {
return ctxerr.Wrap(ctx, err, "delete obsolete upcoming pending host in-house install records")
}
stmt, args, err = sqlx.In(markInHouseInstallsNotInListAsRemoved, globalOrTeamID, titleIDs)
if err != nil {
return ctxerr.Wrap(ctx, err, "build statement to mark obsolete host in-house installs as removed")
}
if _, err := tx.ExecContext(ctx, stmt, args...); err != nil {
return ctxerr.Wrap(ctx, err, "mark obsolete host in-house installs as removed")
}
stmt, args, err = sqlx.In(deleteInHouseInstallersNotInList, globalOrTeamID, titleIDs)
if err != nil {
return ctxerr.Wrap(ctx, err, "build statement to delete obsolete in-house installers")
}
if _, err := tx.ExecContext(ctx, stmt, args...); err != nil {
return ctxerr.Wrap(ctx, err, "delete obsolete in-house installers")
}
for _, installer := range installers {
if installer.ValidatedLabels == nil {
return ctxerr.Errorf(ctx, "labels have not been validated for in-house app with name %s", installer.Filename)
}
wasUpdatedArgs := []any{
// package update
installer.StorageID,
// WHERE clause
globalOrTeamID,
BundleIdentifierOrName(installer.BundleIdentifier, strings.TrimSuffix(installer.Filename, ".ipa")),
installer.Source,
}
// pull existing installer state if it exists so we can diff for side effects post-update
type existingInstallerUpdateCheckResult struct {
InstallerID uint `db:"id"`
IsPackageModified bool `db:"is_package_modified"`
IsMetadataModified bool
}
var existing []existingInstallerUpdateCheckResult
err = sqlx.SelectContext(ctx, tx, &existing, checkExistingInstaller, wasUpdatedArgs...)
if err != nil {
return ctxerr.Wrapf(ctx, err, "checking for existing installer with name %q", installer.Filename)
}
args := []any{
BundleIdentifierOrName(installer.BundleIdentifier, strings.TrimSuffix(installer.Filename, ".ipa")),
installer.Source,
tmID,
globalOrTeamID,
installer.Filename,
installer.Version,
installer.StorageID,
installer.Platform,
installer.BundleIdentifier,
installer.SelfService,
installer.URL,
}
upsertQuery := insertNewOrEditedInstaller
if len(existing) > 0 && existing[0].IsPackageModified { // update uploaded_at for updated installer package
upsertQuery = fmt.Sprintf("%s, updated_at = NOW()", upsertQuery)
}
if _, err := tx.ExecContext(ctx, upsertQuery, args...); err != nil {
return ctxerr.Wrapf(ctx, err, "insert new/edited in-house app with name %q", installer.Filename)
}
// now that the software installer is created/updated, load its installer
// ID (cannot use res.LastInsertID due to the upsert statement, won't
// give the id in case of update)
var installerID uint
if err := sqlx.GetContext(ctx, tx, &installerID, loadInHouseInstallerID, globalOrTeamID, installer.Filename, installer.Platform); err != nil {
return ctxerr.Wrapf(ctx, err, "load id of new/edited in-house app with name %q", installer.Filename)
}
// process the labels associated with that in-house installer
if len(installer.ValidatedLabels.ByName) == 0 {
// no label to apply, so just delete all existing labels if any
res, err := tx.ExecContext(ctx, deleteAllInHouseLabels, installerID)
if err != nil {
return ctxerr.Wrapf(ctx, err, "delete in-house labels for %s", installer.Filename)
}
if n, _ := res.RowsAffected(); n > 0 && len(existing) > 0 {
// if it did delete a row, then the target changed so pending
// installs/uninstalls must be deleted
existing[0].IsMetadataModified = true
}
} else {
// there are new labels to apply, delete only the obsolete ones
labelIDs := make([]uint, 0, len(installer.ValidatedLabels.ByName))
for _, lbl := range installer.ValidatedLabels.ByName {
labelIDs = append(labelIDs, lbl.LabelID)
}
stmt, args, err := sqlx.In(deleteInHouseLabelsNotInList, installerID, labelIDs)
if err != nil {
return ctxerr.Wrap(ctx, err, "build statement to delete in-house labels not in list")
}
res, err := tx.ExecContext(ctx, stmt, args...)
if err != nil {
return ctxerr.Wrapf(ctx, err, "delete in-house labels not in list for %s", installer.Filename)
}
if n, _ := res.RowsAffected(); n > 0 && len(existing) > 0 {
// if it did delete a row, then the target changed so pending
// installs/uninstalls must be deleted
existing[0].IsMetadataModified = true
}
excludeLabels := installer.ValidatedLabels.LabelScope == fleet.LabelScopeExcludeAny
if len(existing) > 0 && !existing[0].IsMetadataModified {
// load the remaining labels for that installer, so that we can detect
// if any label changed (if the counts differ, then labels did change,
// otherwise if the exclude bool changed, the target did change).
var existingLabels []struct {
LabelID uint `db:"label_id"`
Exclude bool `db:"exclude"`
}
if err := sqlx.SelectContext(ctx, tx, &existingLabels, loadExistingInHouseLabels, installerID); err != nil {
return ctxerr.Wrapf(ctx, err, "load existing labels for in-house with name %q", installer.Filename)
}
if len(existingLabels) != len(labelIDs) {
existing[0].IsMetadataModified = true
}
if len(existingLabels) > 0 && existingLabels[0].Exclude != excludeLabels {
// same labels are provided, but the include <-> exclude changed
existing[0].IsMetadataModified = true
}
}
// upsert the new labels now that obsolete ones have been deleted
var upsertLabelArgs []any
for _, lblID := range labelIDs {
upsertLabelArgs = append(upsertLabelArgs, installerID, lblID, excludeLabels)
}
upsertLabelValues := strings.TrimSuffix(strings.Repeat("(?,?,?),", len(installer.ValidatedLabels.ByName)), ",")
_, err = tx.ExecContext(ctx, fmt.Sprintf(upsertInHouseLabels, upsertLabelValues), upsertLabelArgs...)
if err != nil {
return ctxerr.Wrapf(ctx, err, "insert new/edited labels for in-house with name %q", installer.Filename)
}
}
// perform side effects if this was an update (related to pending install requests)
if len(existing) > 0 {
affectedHostIDs, err := ds.runInHouseUpdateSideEffectsInTransaction(
ctx,
tx,
existing[0].InstallerID,
existing[0].IsMetadataModified,
existing[0].IsPackageModified,
)
if err != nil {
return ctxerr.Wrapf(ctx, err, "processing side-effects for in-house with name %q", installer.Filename)
}
activateAffectedHostIDs = append(activateAffectedHostIDs, affectedHostIDs...)
}
}
return nil
})
if err != nil {
return err
}
return ds.activateNextUpcomingActivityForBatchOfHosts(ctx, activateAffectedHostIDs)
}
func (ds *Datastore) runInHouseUpdateSideEffectsInTransaction(ctx context.Context, tx sqlx.ExtContext, installerID uint, wasMetadataUpdated bool, wasPackageUpdated bool) (affectedHostIDs []uint, err error) {
if wasMetadataUpdated || wasPackageUpdated { // cancel pending installs
const cancelInHouseInstalls = `
UPDATE
host_in_house_software_installs
SET
canceled = 1
WHERE
verification_at IS NULL AND
verification_failed_at IS NULL AND
in_house_app_id = ?
`
_, err = tx.ExecContext(ctx, cancelInHouseInstalls, installerID)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "cancel pending host in-house installs")
}
const cancelInHouseCmds = `
UPDATE
nano_enrollment_queue
SET
active = 0
WHERE
command_uuid IN (
SELECT command_uuid
FROM host_in_house_software_installs
WHERE
verification_at IS NULL AND
verification_failed_at IS NULL AND
in_house_app_id = ?
)
`
_, err = tx.ExecContext(ctx, cancelInHouseCmds, installerID)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "cancel pending host in-house commands")
}
const loadAffectedHosts = `
SELECT
DISTINCT host_id
FROM
upcoming_activities ua
INNER JOIN in_house_app_upcoming_activities ihua
ON ua.id = ihua.upcoming_activity_id
WHERE
ua.activity_type = 'in_house_app_install' AND
ua.activated_at IS NOT NULL AND
ihua.in_house_app_id = ?
`
if err := sqlx.SelectContext(ctx, tx, &affectedHostIDs, loadAffectedHosts, installerID); err != nil {
return nil, ctxerr.Wrap(ctx, err, "select affected host IDs for in-house installs")
}
const deleteUpcomingInHouse = `
DELETE FROM upcoming_activities
USING upcoming_activities
INNER JOIN in_house_app_upcoming_activities ihua
ON upcoming_activities.id = ihua.upcoming_activity_id
WHERE
activity_type = 'in_house_app_install' AND
ihua.in_house_app_id = ?
`
_, err = tx.ExecContext(ctx, deleteUpcomingInHouse, installerID)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "delete upcoming host in-house installs")
}
}
if wasPackageUpdated { // hide existing install counts
const markInHouseRemoved = `
UPDATE host_in_house_software_installs SET removed = TRUE
WHERE in_house_app_id = ?
`
_, err := tx.ExecContext(ctx, markInHouseRemoved, installerID)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "hide existing install counts")
}
}
return affectedHostIDs, nil
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,25 @@
package tables
import (
"database/sql"
"fmt"
)
func init() {
MigrationClient.AddMigration(Up_20251111153133, Down_20251111153133)
}
func Up_20251111153133(tx *sql.Tx) error {
_, err := tx.Exec(`
ALTER TABLE in_house_apps
ADD COLUMN url varchar(4095) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT ''
`)
if err != nil {
return fmt.Errorf("failed to alter in_house_apps url: %w", err)
}
return nil
}
func Down_20251111153133(tx *sql.Tx) error {
return nil
}
@@ -0,0 +1,13 @@
package tables
import "testing"
func TestUp_20251111153133(t *testing.T) {
db := applyUpToPrev(t)
// Just a new column, so no logic to test here.
// Leaving it in because it's nice to validate that the migration applies successfully.
// Apply current migration.
applyNext(t, db)
}
File diff suppressed because one or more lines are too long
+63 -12
View File
@@ -267,7 +267,8 @@ func (ds *Datastore) MatchOrCreateSoftwareInstaller(ctx context.Context, payload
// If the existing installer has the same title and source, allow the insert to proceed
// so that the existing UNIQUE (global_or_team_id, title_id) constraint yields a
// Conflict error with the expected message.
if !(found.Title == payload.Title && found.Source == payload.Source) {
// Since this is not an in-house app, only one installer per team can exist.
if !(found[0].Title == payload.Title && found[0].Source == payload.Source) {
return 0, 0, fleet.NewInvalidArgumentError(
"software",
"Couldn't add software. An installer with identical contents already exists on this team.",
@@ -2745,6 +2746,10 @@ func (ds *Datastore) UpdateSoftwareInstallerWithoutPackageIDs(ctx context.Contex
return nil
}
// GetSoftwareInstallers returns all software installers, including in-house
// apps, for the specified team. The reason why installers and in-house apps
// are returned together is that this is used in the gitops flow, where both
// types of installers are specified in the same "packages" key in the yaml.
func (ds *Datastore) GetSoftwareInstallers(ctx context.Context, teamID uint) ([]fleet.SoftwarePackageResponse, error) {
const loadInsertedSoftwareInstallers = `
SELECT
@@ -2755,13 +2760,34 @@ SELECT
si.fleet_maintained_app_id,
COALESCE(icons.filename, '') AS icon_filename,
COALESCE(icons.storage_id, '') AS icon_hash_sha256
FROM software_installers si
LEFT JOIN software_title_icons icons ON icons.software_title_id = si.title_id AND icons.team_id = si.global_or_team_id
WHERE global_or_team_id = ?
FROM
software_installers si
LEFT JOIN software_title_icons icons ON
icons.software_title_id = si.title_id AND icons.team_id = si.global_or_team_id
WHERE
global_or_team_id = ?
UNION ALL
SELECT
iha.team_id,
iha.title_id,
iha.url,
iha.storage_id as hash_sha256,
NULL as fleet_maintained_app_id,
COALESCE(icons.filename, '') AS icon_filename,
COALESCE(icons.storage_id, '') AS icon_hash_sha256
FROM
in_house_apps iha
LEFT JOIN software_title_icons icons ON
icons.software_title_id = iha.title_id AND icons.team_id = iha.global_or_team_id
WHERE
iha.global_or_team_id = ?
`
var softwarePackages []fleet.SoftwarePackageResponse
// Using ds.writer(ctx) on purpose because this method is to be called after applying software.
if err := sqlx.SelectContext(ctx, ds.writer(ctx), &softwarePackages, loadInsertedSoftwareInstallers, teamID); err != nil {
if err := sqlx.SelectContext(ctx, ds.writer(ctx), &softwarePackages,
loadInsertedSoftwareInstallers, teamID, teamID); err != nil {
return nil, ctxerr.Wrap(ctx, err, "get software installers")
}
return softwarePackages, nil
@@ -2964,7 +2990,11 @@ WHERE
return res, nil
}
func (ds *Datastore) GetTeamsWithInstallerByHash(ctx context.Context, sha256, url string) (map[uint]*fleet.ExistingSoftwareInstaller, error) {
// GetTeamsWithInstallerByHash retrieves all software installers and in-house apps
// matching the given sha256 hash (storage_id) and optional URL, grouped by team ID.
// Software installers can only have at most 1 installer per team for the given hash,
// while in-house apps can have multiple (1 for ios and 1 for ipados).
func (ds *Datastore) GetTeamsWithInstallerByHash(ctx context.Context, sha256, url string) (map[uint][]*fleet.ExistingSoftwareInstaller, error) {
stmt := `
SELECT
si.id AS installer_id,
@@ -2981,7 +3011,27 @@ FROM
software_installers si
JOIN software_titles st ON si.title_id = st.id
WHERE
si.storage_id = ?%s`
si.storage_id = ? %s
UNION ALL
SELECT
iha.id AS installer_id,
iha.team_id AS team_id,
iha.filename AS filename,
'ipa' AS extension,
iha.version AS version,
iha.platform AS platform,
st.source AS source,
st.bundle_identifier AS bundle_identifier,
st.name AS title,
'' AS package_ids
FROM
in_house_apps iha
JOIN software_titles st ON iha.title_id = st.id
WHERE
iha.storage_id = ? %s
`
var urlFilter string
args := []any{sha256}
@@ -2989,28 +3039,29 @@ WHERE
urlFilter = " AND url = ?"
args = append(args, url)
}
stmt = fmt.Sprintf(stmt, urlFilter)
stmt = fmt.Sprintf(stmt, urlFilter, urlFilter)
args = append(args, args...)
var installers []*fleet.ExistingSoftwareInstaller
if err := sqlx.SelectContext(ctx, ds.writer(ctx), &installers, stmt, args...); err != nil {
return nil, ctxerr.Wrap(ctx, err, "get software installer by hash")
}
set := make(map[uint]*fleet.ExistingSoftwareInstaller, len(installers))
byTeam := make(map[uint][]*fleet.ExistingSoftwareInstaller, len(installers))
for _, installer := range installers {
// team ID 0 is No team in this context
var tmID uint
if installer.TeamID != nil {
tmID = *installer.TeamID
}
if _, ok := set[tmID]; ok {
if _, ok := byTeam[tmID]; ok && installer.Extension != "ipa" {
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
byTeam[tmID] = append(byTeam[tmID], installer)
}
return set, nil
return byTeam, nil
}
@@ -2983,6 +2983,20 @@ func testGetTeamsWithInstallerByHash(t *testing.T, ds *Datastore) {
})
require.NoError(t, err)
// add an in-house app to the team
_, _, err = ds.MatchOrCreateSoftwareInstaller(ctx, &fleet.UploadSoftwareInstallerPayload{
TeamID: &team1.ID,
UserID: user.ID,
Title: "inhouse",
Filename: "inhouse.ipa",
BundleIdentifier: "com.inhouse",
StorageID: "inhouse",
Extension: "ipa",
Version: "1.2.3",
ValidatedLabels: &fleet.LabelIdentsWithScope{},
})
require.NoError(t, err)
// get installer IDs from added installers
var installer1NoTeam, installer1Team1, installer2NoTeam uint
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
@@ -3010,14 +3024,17 @@ func testGetTeamsWithInstallerByHash(t *testing.T, ds *Datastore) {
require.NoError(t, err)
require.Len(t, installers, 2)
require.Equal(t, installer1NoTeam, installers[0].InstallerID)
require.Nil(t, installers[0].TeamID)
require.Len(t, installers[0], 1)
require.Equal(t, installer1NoTeam, installers[0][0].InstallerID)
require.Nil(t, installers[0][0].TeamID)
require.Equal(t, installer1Team1, installers[1].InstallerID)
require.NotNil(t, installers[1].TeamID)
require.Equal(t, team1.ID, *installers[1].TeamID)
require.Len(t, installers[1], 1)
require.Equal(t, installer1Team1, installers[1][0].InstallerID)
require.NotNil(t, installers[1][0].TeamID)
require.Equal(t, team1.ID, *installers[1][0].TeamID)
for _, i := range installers {
for _, is := range installers {
i := is[0]
require.Equal(t, "installer1", i.Title)
require.Equal(t, "pkg", i.Extension)
require.Equal(t, "1.0", i.Version)
@@ -3027,7 +3044,26 @@ func testGetTeamsWithInstallerByHash(t *testing.T, ds *Datastore) {
installers, err = ds.GetTeamsWithInstallerByHash(ctx, hash2, "https://example.com/2")
require.NoError(t, err)
require.Len(t, installers, 1)
require.Equal(t, installers[0].InstallerID, installer2NoTeam)
require.Len(t, installers[0], 1)
require.Equal(t, installers[0][0].InstallerID, installer2NoTeam)
// in-house hash with invalid url
installers, err = ds.GetTeamsWithInstallerByHash(ctx, "inhouse", "https://no-such-match")
require.NoError(t, err)
require.Len(t, installers, 0)
// in-house hash without url match
installers, err = ds.GetTeamsWithInstallerByHash(ctx, "inhouse", "")
require.NoError(t, err)
require.Len(t, installers, 1)
require.Len(t, installers[team1.ID], 2) // ios and ipados
require.Equal(t, "inhouse.ipa", installers[team1.ID][0].Filename)
require.Equal(t, "inhouse.ipa", installers[team1.ID][1].Filename)
var foundPlatforms []string
for _, inst := range installers[team1.ID] {
foundPlatforms = append(foundPlatforms, inst.Platform)
}
require.ElementsMatch(t, []string{"ios", "ipados"}, foundPlatforms)
}
func testEditDeleteSoftwareInstallersActivateNextActivity(t *testing.T, ds *Datastore) {
+1
View File
@@ -445,6 +445,7 @@ func TruncateTables(t testing.TB, ds *Datastore, tables ...string) {
"mdm_operation_types": true,
"migration_status_tables": true,
"osquery_options": true,
"software_categories": true,
}
testing_utils.TruncateTables(t, ds.writer(context.Background()), ds.logger, nonEmptyTables, tables...)
}
+3 -1
View File
@@ -2100,6 +2100,8 @@ type Datastore interface {
// BatchSetSoftwareInstallers sets the software installers for the given team or no team.
BatchSetSoftwareInstallers(ctx context.Context, tmID *uint, installers []*UploadSoftwareInstallerPayload) error
// BatchSetInHouseAppsInstallers sets the in-house apps installers for the given team or no team.
BatchSetInHouseAppsInstallers(ctx context.Context, tmID *uint, installers []*UploadSoftwareInstallerPayload) error
GetSoftwareInstallers(ctx context.Context, tmID uint) ([]SoftwarePackageResponse, error)
// HasSelfServiceSoftwareInstallers returns true if self-service software installers are available for the team or globally.
@@ -2158,7 +2160,7 @@ type Datastore interface {
// GetTeamsWithInstallerByHash gets a map of teamIDs (0 for No team) to software installers
// metadata by the installer's hash.
GetTeamsWithInstallerByHash(ctx context.Context, sha256, url string) (map[uint]*ExistingSoftwareInstaller, error)
GetTeamsWithInstallerByHash(ctx context.Context, sha256, url string) (map[uint][]*ExistingSoftwareInstaller, error)
// TeamIDsWithSetupExperienceIdPEnabled returns the list of team IDs that
// have the setup experience IdP (End user authentication) enabled. It uses
+11 -11
View File
@@ -520,17 +520,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"`
PackageIDList string `db:"package_ids"`
PackageIDs []string ``
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 {
+14 -2
View File
@@ -1329,6 +1329,8 @@ type CleanupUnusedSoftwareTitleIconsFunc func(ctx context.Context, softwareTitle
type BatchSetSoftwareInstallersFunc func(ctx context.Context, tmID *uint, installers []*fleet.UploadSoftwareInstallerPayload) error
type BatchSetInHouseAppsInstallersFunc func(ctx context.Context, tmID *uint, installers []*fleet.UploadSoftwareInstallerPayload) error
type GetSoftwareInstallersFunc func(ctx context.Context, tmID uint) ([]fleet.SoftwarePackageResponse, error)
type HasSelfServiceSoftwareInstallersFunc func(ctx context.Context, platform string, teamID *uint) (bool, error)
@@ -1381,7 +1383,7 @@ type SetHostAwaitingConfigurationFunc func(ctx context.Context, hostUUID string,
type GetHostAwaitingConfigurationFunc func(ctx context.Context, hostUUID string) (bool, error)
type GetTeamsWithInstallerByHashFunc func(ctx context.Context, sha256 string, url string) (map[uint]*fleet.ExistingSoftwareInstaller, error)
type GetTeamsWithInstallerByHashFunc func(ctx context.Context, sha256 string, url string) (map[uint][]*fleet.ExistingSoftwareInstaller, error)
type TeamIDsWithSetupExperienceIdPEnabledFunc func(ctx context.Context) ([]uint, error)
@@ -3561,6 +3563,9 @@ type DataStore struct {
BatchSetSoftwareInstallersFunc BatchSetSoftwareInstallersFunc
BatchSetSoftwareInstallersFuncInvoked bool
BatchSetInHouseAppsInstallersFunc BatchSetInHouseAppsInstallersFunc
BatchSetInHouseAppsInstallersFuncInvoked bool
GetSoftwareInstallersFunc GetSoftwareInstallersFunc
GetSoftwareInstallersFuncInvoked bool
@@ -8543,6 +8548,13 @@ func (s *DataStore) BatchSetSoftwareInstallers(ctx context.Context, tmID *uint,
return s.BatchSetSoftwareInstallersFunc(ctx, tmID, installers)
}
func (s *DataStore) BatchSetInHouseAppsInstallers(ctx context.Context, tmID *uint, installers []*fleet.UploadSoftwareInstallerPayload) error {
s.mu.Lock()
s.BatchSetInHouseAppsInstallersFuncInvoked = true
s.mu.Unlock()
return s.BatchSetInHouseAppsInstallersFunc(ctx, tmID, installers)
}
func (s *DataStore) GetSoftwareInstallers(ctx context.Context, tmID uint) ([]fleet.SoftwarePackageResponse, error) {
s.mu.Lock()
s.GetSoftwareInstallersFuncInvoked = true
@@ -8725,7 +8737,7 @@ func (s *DataStore) GetHostAwaitingConfiguration(ctx context.Context, hostUUID s
return s.GetHostAwaitingConfigurationFunc(ctx, hostUUID)
}
func (s *DataStore) GetTeamsWithInstallerByHash(ctx context.Context, sha256 string, url string) (map[uint]*fleet.ExistingSoftwareInstaller, error) {
func (s *DataStore) GetTeamsWithInstallerByHash(ctx context.Context, sha256 string, url string) (map[uint][]*fleet.ExistingSoftwareInstaller, error) {
s.mu.Lock()
s.GetTeamsWithInstallerByHashFuncInvoked = true
s.mu.Unlock()
Binary file not shown.