Files
fleet/server/service/integration_install_test.go
T
Victor Lyuboslavsky aaac4b1dfe Changes needed before gokit/log to slog transition. (#39527)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #38889

PLEASE READ BELOW before looking at file changes

Before converting individual files/packages to slog, we generally need
to make these 2 changes to make the conversion easier:
- Replace uses of `kitlog.With` since they are not fully compatible with
our kitlog adapter
- Directly use the kitlog adapter logger type instead of the kitlog
interface, which will let us have direct access to the underlying slog
logger: `*logging.Logger`

Note: that I did not replace absolutely all uses of `kitlog.Logger`, but
I did remove all uses of `kitlog.With` except for these due to
complexity:
- server/logging/filesystem.go and the other log writers (webhook,
firehose, kinesis, lambda, pubsub, nats)
- server/datastore/mysql/nanomdm_storage.go (adapter pattern)
- server/vulnerabilities/nvd/* (cascades to CLI tools)
- server/service/osquery_utils/queries.go (callback type signatures
cascade broadly)
- cmd/maintained-apps/ (standalone, so can be transitioned later all at
once)

Most of the changes in this PR follow these patterns:
- `kitlog.Logger` type → `*logging.Logger`
- `kitlog.With(logger, ...)` → `logger.With(...)`
- `kitlog.NewNopLogger() → logging.NewNopLogger()`, including similar
variations such as `logging.NewLogfmtLogger(w)` and
`logging.NewJSONLogger(w)`
- removed many now-unused kitlog imports

Unique changes that the PR review should focus on:
- server/platform/logging/kitlog_adapter.go: Core adapter changes
- server/platform/logging/logging.go: New convenience functions
- server/service/integration_logger_test.go: Test changes for slog

# Checklist for submitter

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

- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
  - Was added in previous PR

## Testing

- [x] Added/updated automated tests
- [x] QA'd all new/changed functionality manually


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Refactor**
* Migrated the codebase to a unified internal structured logging system
for more consistent, reliable logs and observability.
* No user-facing functionality changed; runtime behavior and APIs remain
compatible.
* **Tests**
* Updated tests to use the new logging helpers to ensure consistent test
logging and validation.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-02-11 10:08:33 -06:00

339 lines
13 KiB
Go

package service
import (
"context"
"crypto/rand"
"crypto/rsa"
"errors"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strings"
"testing"
"time"
"github.com/fleetdm/fleet/v4/server/config"
"github.com/fleetdm/fleet/v4/server/datastore/mysql"
"github.com/fleetdm/fleet/v4/server/datastore/s3"
"github.com/fleetdm/fleet/v4/server/fleet"
software_mock "github.com/fleetdm/fleet/v4/server/mock/software"
"github.com/fleetdm/fleet/v4/server/platform/logging"
"github.com/fleetdm/fleet/v4/server/ptr"
"github.com/jmoiron/sqlx"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
)
func TestIntegrationsInstall(t *testing.T) {
testingSuite := new(integrationInstallTestSuite)
testingSuite.withServer.s = &testingSuite.Suite
suite.Run(t, testingSuite)
}
type integrationInstallTestSuite struct {
withServer
suite.Suite
softwareInstallStore *software_mock.SoftwareInstallerStore
}
func (s *integrationInstallTestSuite) SetupSuite() {
s.withDS.SetupSuite("integrationInstallTestSuite")
// Create a mock S3 software install store
softwareInstallStore := &software_mock.SoftwareInstallerStore{}
s.softwareInstallStore = softwareInstallStore
fleetConfig := config.TestConfig()
signer, _ := rsa.GenerateKey(rand.Reader, 2048)
fleetConfig.S3.SoftwareInstallersCloudFrontSigner = signer
installConfig := TestServerOpts{
License: &fleet.LicenseInfo{
Tier: fleet.TierPremium,
},
Logger: logging.NewLogfmtLogger(os.Stdout),
EnableCachedDS: true,
SoftwareInstallStore: softwareInstallStore,
FleetConfig: &fleetConfig,
}
if os.Getenv("FLEET_INTEGRATION_TESTS_DISABLE_LOG") != "" {
installConfig.Logger = logging.NewNopLogger()
}
users, server := RunServerForTestsWithDS(s.T(), s.ds, &installConfig)
s.server = server
s.users = users
s.token = s.getTestAdminToken()
s.cachedTokens = make(map[string]string)
}
func (s *integrationInstallTestSuite) TearDownTest() {
s.withServer.commonTearDownTest(s.T())
}
// TestSoftwareInstallerSignedURL tests that the software installer signed URL is returned.
// We test using both mock and real fleet.SoftwareInstallerStore.Sign functions.
func (s *integrationInstallTestSuite) TestSoftwareInstallerSignedURL() {
t := s.T()
openFile := func(name string) *os.File {
f, err := os.Open(filepath.Join("testdata", "software-installers", name))
require.NoError(t, err)
return f
}
filename := "ruby.deb"
var expectBytes []byte
var expectLen int
f := openFile(filename)
st, err := f.Stat()
require.NoError(t, err)
expectLen = int(st.Size())
require.Equal(t, expectLen, 11340)
expectBytes = make([]byte, expectLen)
n, err := f.Read(expectBytes)
require.NoError(t, err)
require.Equal(t, n, expectLen)
f.Close()
// Set up mocks
var myInstallerID string
s.softwareInstallStore.ExistsFunc = func(ctx context.Context, installerID string) (bool, error) {
return installerID == myInstallerID, nil
}
s.softwareInstallStore.PutFunc = func(ctx context.Context, installerID string, content io.ReadSeeker) error {
myInstallerID = installerID
return nil
}
s.softwareInstallStore.SignFunc = func(ctx context.Context, fileID string, expiresIn time.Duration) (string, error) {
return "https://example.com/signed", nil
}
var createTeamResp teamResponse
s.DoJSON("POST", "/api/latest/fleet/teams", &fleet.Team{
Name: t.Name(),
}, http.StatusOK, &createTeamResp)
require.NotZero(t, createTeamResp.Team.ID)
payload := &fleet.UploadSoftwareInstallerPayload{
TeamID: &createTeamResp.Team.ID,
InstallScript: "another install script",
PreInstallQuery: "another pre install query",
PostInstallScript: "another post install script",
Filename: filename,
// additional fields below are pre-populated so we can re-use the payload later for the test assertions
Title: "ruby",
Version: "1:2.5.1",
Source: "deb_packages",
StorageID: "df06d9ce9e2090d9cb2e8cd1f4d7754a803dc452bf93e3204e3acd3b95508628",
Platform: "linux",
SelfService: true,
}
s.uploadSoftwareInstaller(t, payload, http.StatusOK, "")
// check the software installer
var id uint
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
return sqlx.GetContext(context.Background(), q, &id,
`SELECT id FROM software_installers WHERE global_or_team_id = ? AND filename = ?`, payload.TeamID, payload.Filename)
})
require.NotZero(t, id)
meta, err := s.ds.GetSoftwareInstallerMetadataByID(context.Background(), id)
require.NoError(t, err)
titleID := *meta.TitleID
// create an orbit host, assign to team
hostInTeam := createOrbitEnrolledHost(t, "linux", "orbit-host-team", s.ds)
require.NoError(t, s.ds.AddHostsToTeam(context.Background(), fleet.NewAddHostsToTeamParams(&createTeamResp.Team.ID, []uint{hostInTeam.ID})))
// Create a software installation request
s.Do("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/software/%d/install", hostInTeam.ID, titleID), installSoftwareRequest{},
http.StatusAccepted)
// Get the InstallerUUID
installUUID := getLatestSoftwareInstallExecID(t, s.ds, hostInTeam.ID)
// Fetch installer details
var orbitSoftwareResp orbitGetSoftwareInstallResponse
s.DoJSON("POST", "/api/fleet/orbit/software_install/details", orbitGetSoftwareInstallRequest{
InstallUUID: installUUID,
OrbitNodeKey: *hostInTeam.OrbitNodeKey,
}, http.StatusOK, &orbitSoftwareResp)
assert.Equal(t, meta.InstallerID, orbitSoftwareResp.InstallerID)
require.NotNil(t, orbitSoftwareResp.SoftwareInstallerURL)
assert.Equal(t, "https://example.com/signed", orbitSoftwareResp.SoftwareInstallerURL.URL)
require.Equal(t, filename, orbitSoftwareResp.SoftwareInstallerURL.Filename)
// Error in signing -- we simply don't return the URL
s.softwareInstallStore.SignFunc = func(ctx context.Context, fileID string, expiresIn time.Duration) (string, error) {
return "", errors.New("error signing")
}
orbitSoftwareResp = orbitGetSoftwareInstallResponse{}
s.DoJSON("POST", "/api/fleet/orbit/software_install/details", orbitGetSoftwareInstallRequest{
InstallUUID: installUUID,
OrbitNodeKey: *hostInTeam.OrbitNodeKey,
}, http.StatusOK, &orbitSoftwareResp)
assert.Equal(t, meta.InstallerID, orbitSoftwareResp.InstallerID)
assert.Nil(t, orbitSoftwareResp.SoftwareInstallerURL)
// Now test with the real sign function
signer, _ := rsa.GenerateKey(rand.Reader, 2048)
s3Config := config.S3Config{
SoftwareInstallersCloudFrontURL: "https://example.cloudfront.net",
SoftwareInstallersCloudFrontURLSigningPublicKeyID: "ABC123XYZ",
SoftwareInstallersCloudFrontSigner: signer,
}
s3Store, err := s3.NewTestSoftwareInstallerStore(s3Config)
require.NoError(t, err)
s.softwareInstallStore.SignFunc = func(ctx context.Context, fileID string, expiresIn time.Duration) (string, error) {
return s3Store.Sign(ctx, fileID, fleet.SoftwareInstallerSignedURLExpiry)
}
s.DoJSON("POST", "/api/fleet/orbit/software_install/details", orbitGetSoftwareInstallRequest{
InstallUUID: installUUID,
OrbitNodeKey: *hostInTeam.OrbitNodeKey,
}, http.StatusOK, &orbitSoftwareResp)
assert.Equal(t, meta.InstallerID, orbitSoftwareResp.InstallerID)
require.NotNil(t, orbitSoftwareResp.SoftwareInstallerURL)
assert.True(t,
strings.HasPrefix(orbitSoftwareResp.SoftwareInstallerURL.URL,
s3Config.SoftwareInstallersCloudFrontURL+"/software-installers/"+payload.StorageID+"?Expires="),
orbitSoftwareResp.SoftwareInstallerURL.URL)
assert.Contains(t, orbitSoftwareResp.SoftwareInstallerURL.URL, "&Signature=")
assert.Contains(t, orbitSoftwareResp.SoftwareInstallerURL.URL,
"&Key-Pair-Id="+s3Config.SoftwareInstallersCloudFrontURLSigningPublicKeyID)
require.Equal(t, filename, orbitSoftwareResp.SoftwareInstallerURL.Filename)
}
func getLatestSoftwareInstallExecID(t *testing.T, ds *mysql.Datastore, hostID uint) string {
var installUUID string
mysql.ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
return sqlx.GetContext(context.Background(), q, &installUUID,
"SELECT execution_id FROM host_software_installs WHERE host_id = ? ORDER BY id desc", hostID)
})
return installUUID
}
// TestShScriptInstallOnDarwin tests that .sh script packages (stored as platform='linux')
// can be installed on darwin (macOS) hosts through the full HTTP API flow.
func (s *integrationInstallTestSuite) TestShScriptInstallOnDarwin() {
t := s.T()
filename := "test-script.sh"
// Create a .sh script file in-memory
tfr, err := fleet.NewTempFileReader(strings.NewReader("#!/bin/bash\necho 'hello world'\n"), t.TempDir)
require.NoError(t, err)
defer tfr.Close()
// Set up mocks
var myInstallerID string
s.softwareInstallStore.ExistsFunc = func(ctx context.Context, installerID string) (bool, error) {
return installerID == myInstallerID, nil
}
s.softwareInstallStore.PutFunc = func(ctx context.Context, installerID string, content io.ReadSeeker) error {
myInstallerID = installerID
return nil
}
s.softwareInstallStore.SignFunc = func(ctx context.Context, fileID string, expiresIn time.Duration) (string, error) {
return "https://example.com/signed-sh", nil
}
// Create a team
var createTeamResp teamResponse
s.DoJSON("POST", "/api/latest/fleet/teams", &fleet.Team{
Name: t.Name(),
}, http.StatusOK, &createTeamResp)
require.NotZero(t, createTeamResp.Team.ID)
// Upload .sh script package
s.uploadSoftwareInstaller(t, &fleet.UploadSoftwareInstallerPayload{
TeamID: &createTeamResp.Team.ID,
Filename: filename,
InstallerFile: tfr,
}, http.StatusOK, "")
// Get the title ID from the database
var id uint
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
return sqlx.GetContext(context.Background(), q, &id,
`SELECT id FROM software_installers WHERE global_or_team_id = ? AND filename = ?`, createTeamResp.Team.ID, filename)
})
require.NotZero(t, id)
meta, err := s.ds.GetSoftwareInstallerMetadataByID(context.Background(), id)
require.NoError(t, err)
require.Equal(t, "linux", meta.Platform, ".sh file should be stored with platform=linux")
titleID := *meta.TitleID
// Create a darwin (macOS) orbit host and assign to team
darwinHost := createOrbitEnrolledHost(t, "darwin", "darwin-sh-host", s.ds)
require.NoError(t, s.ds.AddHostsToTeam(context.Background(), fleet.NewAddHostsToTeamParams(&createTeamResp.Team.ID, []uint{darwinHost.ID})))
// Install .sh on darwin should succeed
s.Do("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/software/%d/install", darwinHost.ID, titleID), installSoftwareRequest{},
http.StatusAccepted)
// Get the install UUID
installUUID := getLatestSoftwareInstallExecID(t, s.ds, darwinHost.ID)
// Fetch installer details via orbit endpoint
var orbitSoftwareResp orbitGetSoftwareInstallResponse
s.DoJSON("POST", "/api/fleet/orbit/software_install/details", orbitGetSoftwareInstallRequest{
InstallUUID: installUUID,
OrbitNodeKey: *darwinHost.OrbitNodeKey,
}, http.StatusOK, &orbitSoftwareResp)
assert.Equal(t, meta.InstallerID, orbitSoftwareResp.InstallerID)
require.NotNil(t, orbitSoftwareResp.SoftwareInstallerURL)
assert.Equal(t, "https://example.com/signed-sh", orbitSoftwareResp.SoftwareInstallerURL.URL)
require.Equal(t, filename, orbitSoftwareResp.SoftwareInstallerURL.Filename)
}
func (s *integrationInstallTestSuite) TestGetInHouseAppManifestSignedURL() {
// Test that the signed URL is used if cloudfrontsigner is configured
t := s.T()
teamID := ptr.Uint(0)
signURL := `https://example.cloudfront.net/software-installers/storage_id?Expires=1766462733&Signature=some_signature&Key-Pair-Id=ABC123XYZ`
// Set up mocks
var myInstallerID string
s.softwareInstallStore.ExistsFunc = func(ctx context.Context, installerID string) (bool, error) {
return installerID == myInstallerID, nil
}
s.softwareInstallStore.PutFunc = func(ctx context.Context, installerID string, content io.ReadSeeker) error {
myInstallerID = installerID
return nil
}
s.softwareInstallStore.SignFunc = func(ctx context.Context, fileID string, expiresIn time.Duration) (string, error) {
return signURL, nil
}
s.uploadSoftwareInstaller(t, &fleet.UploadSoftwareInstallerPayload{Filename: "ipa_test.ipa"}, http.StatusOK, "")
var titleResp listSoftwareTitlesResponse
s.DoJSON("GET", "/api/latest/fleet/software/titles", listSoftwareTitlesRequest{
SoftwareTitleListOptions: fleet.SoftwareTitleListOptions{Platform: "ios"},
}, http.StatusOK, &titleResp, "team_id", "0")
require.Len(t, titleResp.SoftwareTitles, 1)
require.Equal(t, "ipa_test", titleResp.SoftwareTitles[0].Name)
titleID := titleResp.SoftwareTitles[0].ID
readManifest := func(res *http.Response) []byte {
buf, err := io.ReadAll(res.Body)
require.NoError(t, err)
res.Body.Close()
return buf
}
res := s.DoRawNoAuth("GET", fmt.Sprintf("/api/latest/fleet/software/titles/%d/in_house_app/manifest?team_id=%d", titleID, *teamID),
jsonMustMarshal(t, getInHouseAppManifestRequest{TitleID: titleID, TeamID: teamID}), http.StatusOK)
manifest := readManifest(res)
require.NotNil(t, manifest)
escapedURL := `https://example.cloudfront.net/software-installers/storage_id?Expires=1766462733&amp;Signature=some_signature&amp;Key-Pair-Id=ABC123XYZ`
require.Contains(t, string(manifest), escapedURL)
}