Add gitops support for in house apps (#35423)
This commit is contained in:
@@ -0,0 +1 @@
|
||||
* Added support for in-house (".ipa") apps to `fleetctl gitops`.
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
name: No team
|
||||
controls:
|
||||
policies:
|
||||
software:
|
||||
packages:
|
||||
- path: subdir/installer_ipa.yml
|
||||
+6
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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...)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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.
Reference in New Issue
Block a user