Switched Android from go-kit/log to slog (#39785)

<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #39785

# Checklist for submitter

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

- [ ] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
  - Changes file already updated.

## 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

* **Chores**
* Updated internal logging infrastructure across Android MDM services
and background jobs to use standard Go logging.
* Enhanced test coverage for access control and permission enforcement
across various endpoints and user roles.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Victor Lyuboslavsky
2026-02-13 08:45:28 -06:00
committed by GitHub
parent e9f9d5a7d5
commit 44aebdf3a7
17 changed files with 143 additions and 152 deletions
+3 -3
View File
@@ -991,7 +991,7 @@ func newCleanupsAndAggregationSchedule(
return ds.RenewMDMManagedCertificates(ctx)
}),
schedule.WithJob("renew_android_certificate_templates", func(ctx context.Context) error {
return android_svc.RenewCertificateTemplates(ctx, ds, logger)
return android_svc.RenewCertificateTemplates(ctx, ds, logger.SlogLogger())
}),
schedule.WithJob("query_results_cleanup", func(ctx context.Context) error {
config, err := ds.AppConfig(ctx)
@@ -1412,7 +1412,7 @@ func newAndroidMDMProfileManagerSchedule(
ctx, name, instanceID, defaultInterval, ds, ds,
schedule.WithLogger(logger),
schedule.WithJob("manage_android_profiles", func(ctx context.Context) error {
return android_svc.ReconcileProfiles(ctx, ds, logger, licenseKey, androidAgentConfig)
return android_svc.ReconcileProfiles(ctx, ds, logger.SlogLogger(), licenseKey, androidAgentConfig)
}),
)
@@ -1848,7 +1848,7 @@ func newAndroidMDMDeviceReconcilerSchedule(
ctx, name, instanceID, defaultInterval, ds, ds,
schedule.WithLogger(logger),
schedule.WithJob("reconcile_android_devices", func(ctx context.Context) error {
return android_svc.ReconcileAndroidDevices(ctx, ds, logger, licenseKey)
return android_svc.ReconcileAndroidDevices(ctx, ds, logger.SlogLogger(), licenseKey)
}),
)
+1 -1
View File
@@ -850,7 +850,7 @@ the way that the Fleet server works.
config.MDM.AndroidAgent.Validate(initFatal)
androidSvc, err := android_service.NewService(
ctx,
logger,
logger.SlogLogger(),
ds,
config.License.Key,
config.Server.PrivateKey,
@@ -5,7 +5,6 @@ import (
"fmt"
"log/slog"
"net/http"
"os"
"reflect"
"strings"
"time"
@@ -16,8 +15,6 @@ import (
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/mdm/android"
"github.com/go-json-experiment/json"
kitlog "github.com/go-kit/log"
"github.com/go-kit/log/level"
"github.com/google/uuid"
"google.golang.org/api/androidmanagement/v1"
"google.golang.org/api/googleapi"
@@ -27,7 +24,7 @@ import (
// GoogleClient connects directly to Google's Android Management API. It is intended to be used for development/debugging.
// To enable, set the following env vars: FLEET_DEV_ANDROID_GOOGLE_CLIENT=1 and FLEET_DEV_ANDROID_GOOGLE_SERVICE_CREDENTIALS=$(cat credentials.json)
type GoogleClient struct {
logger kitlog.Logger
logger *slog.Logger
mgmt *androidmanagement.Service
androidServiceCredentials string
androidProjectID string
@@ -36,7 +33,7 @@ type GoogleClient struct {
// Compile-time check to ensure that ProxyClient implements Client.
var _ Client = &GoogleClient{}
func NewGoogleClient(ctx context.Context, logger kitlog.Logger, getenv dev_mode.GetEnv) Client {
func NewGoogleClient(ctx context.Context, logger *slog.Logger, getenv dev_mode.GetEnv) Client {
androidServiceCredentials := getenv("FLEET_DEV_ANDROID_GOOGLE_SERVICE_CREDENTIALS")
if androidServiceCredentials == "" {
return nil
@@ -49,17 +46,16 @@ func NewGoogleClient(ctx context.Context, logger kitlog.Logger, getenv dev_mode.
var creds credentials
err := json.Unmarshal([]byte(androidServiceCredentials), &creds)
if err != nil {
level.Error(logger).Log("msg", "unmarshaling android service credentials", "err", err)
logger.ErrorContext(ctx, "unmarshaling android service credentials", "err", err)
return nil
}
slogLogger := slog.New(slog.NewTextHandler(os.Stdout, nil))
mgmt, err := androidmanagement.NewService(ctx,
option.WithCredentialsJSON([]byte(androidServiceCredentials)),
option.WithLogger(slogLogger),
option.WithLogger(logger),
)
if err != nil {
level.Error(logger).Log("msg", "creating android management service", "err", err)
logger.ErrorContext(ctx, "creating android management service", "err", err)
return nil
}
return &GoogleClient{
@@ -202,7 +198,7 @@ func (g *GoogleClient) EnterprisesPoliciesPatch(ctx context.Context, policyName
switch {
case googleapi.IsNotModified(err):
g.logger.Log("msg", "Android policy not modified", "policy_name", policyName)
g.logger.InfoContext(ctx, "Android policy not modified", "policy_name", policyName)
return nil, err
case err != nil:
return nil, fmt.Errorf("patching policy %s: %w", policyName, err)
@@ -214,7 +210,7 @@ func (g *GoogleClient) EnterprisesDevicesPatch(ctx context.Context, deviceName s
ret, err := g.mgmt.Enterprises.Devices.Patch(deviceName, device).Context(ctx).Do()
switch {
case googleapi.IsNotModified(err):
g.logger.Log("msg", "Android device not modified", "device_name", deviceName)
g.logger.InfoContext(ctx, "Android device not modified", "device_name", deviceName)
return nil, err
case err != nil:
return nil, fmt.Errorf("patching device %s: %w", deviceName, err)
@@ -234,7 +230,7 @@ func (g *GoogleClient) EnterprisesDevicesDelete(ctx context.Context, deviceName
_, err := g.mgmt.Enterprises.Devices.Delete(deviceName).Context(ctx).Do()
switch {
case googleapi.IsNotModified(err):
g.logger.Log("msg", "Android device already deleted", "device_name", deviceName)
g.logger.InfoContext(ctx, "Android device already deleted", "device_name", deviceName)
return nil
case err != nil:
return fmt.Errorf("deleting device %s: %w", deviceName, err)
@@ -264,14 +260,14 @@ func (g *GoogleClient) EnterpriseDelete(ctx context.Context, enterpriseName stri
// We can also pull the topic from the DB, but this way is more reliable.
enterprise, err := g.mgmt.Enterprises.Get(enterpriseName).Context(ctx).Do()
if err != nil {
level.Error(g.logger).Log("msg", "getting enterprise; perhaps it was already deleted?", "err", err, "enterprise_name", enterpriseName)
g.logger.ErrorContext(ctx, "getting enterprise; perhaps it was already deleted?", "err", err, "enterprise_name", enterpriseName)
return nil
}
_, err = g.mgmt.Enterprises.Delete(enterpriseName).Do()
switch {
case googleapi.IsNotModified(err):
level.Info(g.logger).Log("msg", "enterprise was already deleted", "enterprise_name", enterpriseName)
g.logger.InfoContext(ctx, "enterprise was already deleted", "enterprise_name", enterpriseName)
return nil
case err != nil:
return fmt.Errorf("deleting enterprise %s: %w", enterpriseName, err)
@@ -284,7 +280,7 @@ func (g *GoogleClient) EnterpriseDelete(ctx context.Context, enterpriseName stri
// PubSub topic and subscription have the same ID (but not name) by convention.
topicAndSubscriptionID, err := getLastPart(ctx, enterprise.PubsubTopic)
if err != nil || len(topicAndSubscriptionID) == 0 {
level.Error(g.logger).Log("msg", "getting last part of PubSub topic", "err", err, "topic", enterprise.PubsubTopic)
g.logger.ErrorContext(ctx, "getting last part of PubSub topic", "err", err, "topic", enterprise.PubsubTopic)
return nil
}
@@ -370,7 +366,7 @@ func (g *GoogleClient) EnterprisesPoliciesModifyPolicyApplications(ctx context.C
ret, err := g.mgmt.Enterprises.Policies.ModifyPolicyApplications(policyName, &req).Context(ctx).Do()
switch {
case googleapi.IsNotModified(err):
g.logger.Log("msg", "Android application policy not modified", "policy_name", policyName)
g.logger.InfoContext(ctx, "Android application policy not modified", "policy_name", policyName)
return nil, err
case err != nil:
return nil, ctxerr.Wrapf(ctx, err, "modifying application policy %s", policyName)
@@ -385,7 +381,7 @@ func (g *GoogleClient) EnterprisesPoliciesRemovePolicyApplications(ctx context.C
ret, err := g.mgmt.Enterprises.Policies.RemovePolicyApplications(policyName, &req).Context(ctx).Do()
switch {
case googleapi.IsNotModified(err):
g.logger.Log("msg", "Android application policy not modified", "policy_name", policyName)
g.logger.InfoContext(ctx, "Android application policy not modified", "policy_name", policyName)
return nil, err
case err != nil:
return nil, ctxerr.Wrapf(ctx, err, "removing packages from application policy %s", policyName)
@@ -16,8 +16,6 @@ import (
"github.com/fleetdm/fleet/v4/server/dev_mode"
"github.com/fleetdm/fleet/v4/server/mdm/android"
"github.com/go-json-experiment/json"
kitlog "github.com/go-kit/log"
"github.com/go-kit/log/level"
"google.golang.org/api/androidmanagement/v1"
"google.golang.org/api/googleapi"
"google.golang.org/api/option"
@@ -27,7 +25,7 @@ const defaultProxyEndpoint = "https://fleetdm.com/api/android/"
// ProxyClient connects to Google's Android Management API via a proxy, which is hosted at fleetdm.com by default.
type ProxyClient struct {
logger kitlog.Logger
logger *slog.Logger
mgmt *androidmanagement.Service
licenseKey string
proxyEndpoint string
@@ -37,7 +35,7 @@ type ProxyClient struct {
// Compile-time check to ensure that ProxyClient implements Client.
var _ Client = &ProxyClient{}
func NewProxyClient(ctx context.Context, logger kitlog.Logger, licenseKey string, getenv dev_mode.GetEnv) Client {
func NewProxyClient(ctx context.Context, logger *slog.Logger, licenseKey string, getenv dev_mode.GetEnv) Client {
proxyEndpoint := getenv("FLEET_DEV_ANDROID_PROXY_ENDPOINT")
if proxyEndpoint == "" {
proxyEndpoint = defaultProxyEndpoint
@@ -62,7 +60,7 @@ func NewProxyClient(ctx context.Context, logger kitlog.Logger, licenseKey string
option.WithHTTPClient(fleethttp.NewClient()),
)
if err != nil {
level.Error(logger).Log("msg", "creating android management service", "err", err)
logger.ErrorContext(ctx, "creating android management service", "err", err)
return nil
}
return &ProxyClient{
@@ -180,7 +178,7 @@ func (p *ProxyClient) EnterprisesPoliciesPatch(ctx context.Context, policyName s
ret, err := call.Do()
switch {
case googleapi.IsNotModified(err):
p.logger.Log("msg", "Android policy not modified", "policy_name", policyName)
p.logger.InfoContext(ctx, "Android policy not modified", "policy_name", policyName)
return nil, err
case err != nil:
return nil, fmt.Errorf("patching policy %s: %w", policyName, err)
@@ -194,7 +192,7 @@ func (p *ProxyClient) EnterprisesDevicesPatch(ctx context.Context, deviceName st
ret, err := call.Do()
switch {
case googleapi.IsNotModified(err):
p.logger.Log("msg", "Android device not modified", "device_name", deviceName)
p.logger.InfoContext(ctx, "Android device not modified", "device_name", deviceName)
return nil, err
case err != nil:
return nil, fmt.Errorf("patching device %s: %w", deviceName, err)
@@ -218,7 +216,7 @@ func (p *ProxyClient) EnterprisesDevicesDelete(ctx context.Context, deviceName s
_, err := call.Do()
switch {
case googleapi.IsNotModified(err) || isErrorCode(err, http.StatusNotFound):
p.logger.Log("msg", "Android device already deleted", "device_name", deviceName)
p.logger.InfoContext(ctx, "Android device already deleted", "device_name", deviceName)
return nil
case err != nil:
return fmt.Errorf("deleting device %s: %w", deviceName, err)
@@ -254,7 +252,7 @@ func (p *ProxyClient) EnterpriseDelete(ctx context.Context, enterpriseName strin
_, err := call.Do()
switch {
case googleapi.IsNotModified(err) || isErrorCode(err, http.StatusNotFound):
level.Info(p.logger).Log("msg", "enterprise was already deleted", "enterprise_name", enterpriseName)
p.logger.InfoContext(ctx, "enterprise was already deleted", "enterprise_name", enterpriseName)
return nil
case err != nil:
return fmt.Errorf("deleting enterprise %s: %w", enterpriseName, err)
@@ -334,7 +332,7 @@ func (p *ProxyClient) EnterprisesPoliciesModifyPolicyApplications(ctx context.Co
ret, err := call.Do()
switch {
case googleapi.IsNotModified(err):
p.logger.Log("msg", "Android application policy not modified", "policy_name", policyName)
p.logger.InfoContext(ctx, "Android application policy not modified", "policy_name", policyName)
return nil, err
case err != nil:
return nil, ctxerr.Wrapf(ctx, err, "modifying application policy %s", policyName)
@@ -352,7 +350,7 @@ func (p *ProxyClient) EnterprisesPoliciesRemovePolicyApplications(ctx context.Co
ret, err := call.Do()
switch {
case googleapi.IsNotModified(err):
p.logger.Log("msg", "Android application policy not modified", "policy_name", policyName)
p.logger.InfoContext(ctx, "Android application policy not modified", "policy_name", policyName)
return nil, err
case err != nil:
return nil, ctxerr.Wrapf(ctx, err, "removing packages from application policy %s", policyName)
@@ -2,16 +2,15 @@ package service
import (
"context"
"log/slog"
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
"github.com/fleetdm/fleet/v4/server/fleet"
kitlog "github.com/go-kit/log"
"github.com/go-kit/log/level"
)
// RenewCertificateTemplates identifies Android certificate templates that are approaching
// expiration and marks them for renewal by updating their status.
func RenewCertificateTemplates(ctx context.Context, ds fleet.Datastore, logger kitlog.Logger) error {
func RenewCertificateTemplates(ctx context.Context, ds fleet.Datastore, logger *slog.Logger) error {
const batchSize = 1000
templates, err := ds.GetAndroidCertificateTemplatesForRenewal(ctx, batchSize)
if err != nil {
@@ -23,6 +22,6 @@ func RenewCertificateTemplates(ctx context.Context, ds fleet.Datastore, logger k
if err := ds.SetAndroidCertificateTemplatesForRenewal(ctx, templates); err != nil {
return ctxerr.Wrap(ctx, err, "set android certificate templates for renewal")
}
level.Info(logger).Log("msg", "marked android certificate templates for renewal", "count", len(templates))
logger.InfoContext(ctx, "marked android certificate templates for renewal", "count", len(templates))
return nil
}
+15 -15
View File
@@ -15,9 +15,9 @@ import (
"github.com/fleetdm/fleet/v4/server/mdm/android"
android_mock "github.com/fleetdm/fleet/v4/server/mdm/android/mock"
ds_mock "github.com/fleetdm/fleet/v4/server/mock"
"github.com/fleetdm/fleet/v4/server/platform/logging"
"github.com/fleetdm/fleet/v4/server/ptr"
"github.com/fleetdm/fleet/v4/server/service/modules/activities"
kitlog "github.com/go-kit/log"
"github.com/jmoiron/sqlx"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -28,10 +28,10 @@ import (
func TestEnterprisesAuth(t *testing.T) {
androidAPIClient := android_mock.Client{}
androidAPIClient.InitCommonMocks()
logger := kitlog.NewLogfmtLogger(os.Stdout)
logger := logging.NewLogfmtLogger(os.Stdout)
fleetDS := InitCommonDSMocks()
activityModule := activities.NewActivityModule(fleetDS, logger)
svc, err := NewServiceWithClient(logger, fleetDS, &androidAPIClient, "test-private-key", &fleetDS.DataStore, activityModule, config.AndroidAgentConfig{})
svc, err := NewServiceWithClient(logger.SlogLogger(), fleetDS, &androidAPIClient, "test-private-key", &fleetDS.DataStore, activityModule, config.AndroidAgentConfig{})
require.NoError(t, err)
testCases := []struct {
@@ -126,11 +126,11 @@ func TestEnterprisesAuth(t *testing.T) {
func TestEnterpriseSignupMissingPrivateKey(t *testing.T) {
androidAPIClient := android_mock.Client{}
androidAPIClient.InitCommonMocks()
logger := kitlog.NewLogfmtLogger(os.Stdout)
logger := logging.NewLogfmtLogger(os.Stdout)
fleetDS := InitCommonDSMocks()
activityModule := activities.NewActivityModule(fleetDS, logger)
svc, err := NewServiceWithClient(logger, fleetDS, &androidAPIClient, "test-private-key", &fleetDS.DataStore, activityModule, config.AndroidAgentConfig{})
svc, err := NewServiceWithClient(logger.SlogLogger(), fleetDS, &androidAPIClient, "test-private-key", &fleetDS.DataStore, activityModule, config.AndroidAgentConfig{})
require.NoError(t, err)
user := &fleet.User{ID: 1, GlobalRole: ptr.String(fleet.RoleAdmin)}
@@ -237,7 +237,7 @@ func (e *notFoundError) Error() string { return "not found" }
func (e *notFoundError) IsNotFound() bool { return true }
func TestGetEnterprise(t *testing.T) {
logger := kitlog.NewLogfmtLogger(os.Stdout)
logger := logging.NewLogfmtLogger(os.Stdout)
user := &fleet.User{ID: 1, GlobalRole: ptr.String(fleet.RoleAdmin)}
ctx := viewer.NewContext(context.Background(), viewer.Viewer{User: user})
@@ -247,7 +247,7 @@ func TestGetEnterprise(t *testing.T) {
fleetDS := InitCommonDSMocks()
activityModule := activities.NewActivityModule(fleetDS, logger)
svc, err := NewServiceWithClient(logger, fleetDS, &androidAPIClient, "test-private-key", &fleetDS.DataStore, activityModule, config.AndroidAgentConfig{})
svc, err := NewServiceWithClient(logger.SlogLogger(), fleetDS, &androidAPIClient, "test-private-key", &fleetDS.DataStore, activityModule, config.AndroidAgentConfig{})
require.NoError(t, err)
enterprise, err := svc.GetEnterprise(ctx)
@@ -267,7 +267,7 @@ func TestGetEnterprise(t *testing.T) {
}
activityModule := activities.NewActivityModule(fleetDS, logger)
svc, err := NewServiceWithClient(logger, fleetDS, &androidAPIClient, "test-private-key", &fleetDS.DataStore, activityModule, config.AndroidAgentConfig{})
svc, err := NewServiceWithClient(logger.SlogLogger(), fleetDS, &androidAPIClient, "test-private-key", &fleetDS.DataStore, activityModule, config.AndroidAgentConfig{})
require.NoError(t, err)
enterprise, err := svc.GetEnterprise(ctx)
@@ -281,7 +281,7 @@ func TestGetEnterprise(t *testing.T) {
}
func TestVerifyExistingEnterpriseIfAny(t *testing.T) {
logger := kitlog.NewLogfmtLogger(os.Stdout)
logger := logging.NewLogfmtLogger(os.Stdout)
user := &fleet.User{ID: 1, GlobalRole: ptr.String(fleet.RoleAdmin)}
ctx := viewer.NewContext(context.Background(), viewer.Viewer{User: user})
@@ -313,7 +313,7 @@ func TestVerifyExistingEnterpriseIfAny(t *testing.T) {
}
activityModule := activities.NewActivityModule(fleetDS, logger)
svc, err := NewServiceWithClient(logger, fleetDS, &androidAPIClient, "test-private-key", &fleetDS.DataStore, activityModule, config.AndroidAgentConfig{})
svc, err := NewServiceWithClient(logger.SlogLogger(), fleetDS, &androidAPIClient, "test-private-key", &fleetDS.DataStore, activityModule, config.AndroidAgentConfig{})
require.NoError(t, err)
err = svc.VerifyExistingEnterpriseIfAny(ctx)
@@ -362,7 +362,7 @@ func TestVerifyExistingEnterpriseIfAny(t *testing.T) {
}
activityModule := activities.NewActivityModule(fleetDS, logger)
svc, err := NewServiceWithClient(logger, fleetDS, &androidAPIClient, "test-private-key", &fleetDS.DataStore, activityModule, config.AndroidAgentConfig{})
svc, err := NewServiceWithClient(logger.SlogLogger(), fleetDS, &androidAPIClient, "test-private-key", &fleetDS.DataStore, activityModule, config.AndroidAgentConfig{})
require.NoError(t, err)
err = svc.VerifyExistingEnterpriseIfAny(ctx)
@@ -403,7 +403,7 @@ func TestVerifyExistingEnterpriseIfAny(t *testing.T) {
}
activityModule := activities.NewActivityModule(fleetDS, logger)
svc, err := NewServiceWithClient(logger, fleetDS, &androidAPIClient, "test-private-key", &fleetDS.DataStore, activityModule, config.AndroidAgentConfig{})
svc, err := NewServiceWithClient(logger.SlogLogger(), fleetDS, &androidAPIClient, "test-private-key", &fleetDS.DataStore, activityModule, config.AndroidAgentConfig{})
require.NoError(t, err)
err = svc.VerifyExistingEnterpriseIfAny(ctx)
@@ -444,7 +444,7 @@ func TestVerifyExistingEnterpriseIfAny(t *testing.T) {
}
activityModule := activities.NewActivityModule(fleetDS, logger)
svc, err := NewServiceWithClient(logger, fleetDS, &androidAPIClient, "test-private-key", &fleetDS.DataStore, activityModule, config.AndroidAgentConfig{})
svc, err := NewServiceWithClient(logger.SlogLogger(), fleetDS, &androidAPIClient, "test-private-key", &fleetDS.DataStore, activityModule, config.AndroidAgentConfig{})
require.NoError(t, err)
err = svc.VerifyExistingEnterpriseIfAny(ctx)
@@ -511,7 +511,7 @@ func TestVerifyExistingEnterpriseIfAny(t *testing.T) {
}
activityModule := activities.NewActivityModule(fleetDS, logger)
svc, err := NewServiceWithClient(logger, fleetDS, &androidAPIClient, "test-private-key", &fleetDS.DataStore, activityModule, config.AndroidAgentConfig{})
svc, err := NewServiceWithClient(logger.SlogLogger(), fleetDS, &androidAPIClient, "test-private-key", &fleetDS.DataStore, activityModule, config.AndroidAgentConfig{})
require.NoError(t, err)
err = svc.VerifyExistingEnterpriseIfAny(ctx)
@@ -541,7 +541,7 @@ func TestVerifyExistingEnterpriseIfAny(t *testing.T) {
}
activityModule := activities.NewActivityModule(fleetDS, logger)
svc, err := NewServiceWithClient(logger, fleetDS, &androidAPIClient, "test-private-key", &fleetDS.DataStore, activityModule, config.AndroidAgentConfig{})
svc, err := NewServiceWithClient(logger.SlogLogger(), fleetDS, &androidAPIClient, "test-private-key", &fleetDS.DataStore, activityModule, config.AndroidAgentConfig{})
require.NoError(t, err)
err = svc.VerifyExistingEnterpriseIfAny(ctx)
+4 -4
View File
@@ -5,6 +5,7 @@ import (
"context"
"encoding/json"
"fmt"
"log/slog"
"maps"
"net/http"
"slices"
@@ -15,17 +16,16 @@ import (
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/mdm/android"
"github.com/fleetdm/fleet/v4/server/mdm/android/service/androidmgmt"
kitlog "github.com/go-kit/log"
"google.golang.org/api/androidmanagement/v1"
)
func ReconcileProfiles(ctx context.Context, ds fleet.Datastore, logger kitlog.Logger, licenseKey string, androidAgentConfig config.AndroidAgentConfig) error {
func ReconcileProfiles(ctx context.Context, ds fleet.Datastore, logger *slog.Logger, licenseKey string, androidAgentConfig config.AndroidAgentConfig) error {
return ReconcileProfilesWithClient(ctx, ds, logger, licenseKey, nil, androidAgentConfig)
}
// ReconcileProfilesWithClient is like ReconcileProfiles but allows injecting a custom client for testing.
// If client is nil, a new AMAPI client will be created.
func ReconcileProfilesWithClient(ctx context.Context, ds fleet.Datastore, logger kitlog.Logger, licenseKey string, client androidmgmt.Client, androidAgentConfig config.AndroidAgentConfig) error {
func ReconcileProfilesWithClient(ctx context.Context, ds fleet.Datastore, logger *slog.Logger, licenseKey string, client androidmgmt.Client, androidAgentConfig config.AndroidAgentConfig) error {
appConfig, err := ds.AppConfig(ctx)
if err != nil {
return ctxerr.Wrap(ctx, err, "get app config")
@@ -71,7 +71,7 @@ type profileReconciler struct {
Enterprise *android.Enterprise
Client androidmgmt.Client
AndroidAgentConfig config.AndroidAgentConfig
Logger kitlog.Logger
Logger *slog.Logger
}
func getClientAuthenticationSecret(ctx context.Context, ds fleet.Datastore) (string, error) {
+4 -3
View File
@@ -5,6 +5,7 @@ import (
"encoding/json"
"errors"
"fmt"
"log/slog"
"strings"
"testing"
"time"
@@ -16,9 +17,9 @@ import (
"github.com/fleetdm/fleet/v4/server/mdm/android"
"github.com/fleetdm/fleet/v4/server/mdm/android/mock"
"github.com/fleetdm/fleet/v4/server/mdm/android/service/androidmgmt"
"github.com/fleetdm/fleet/v4/server/platform/logging"
"github.com/fleetdm/fleet/v4/server/ptr"
"github.com/fleetdm/fleet/v4/server/test"
kitlog "github.com/go-kit/log"
"github.com/google/uuid"
"github.com/jmoiron/sqlx"
"github.com/stretchr/testify/require"
@@ -100,7 +101,7 @@ func TestReconcileProfiles(t *testing.T) {
Package: "com.fleetdm.agent",
SigningSHA256: "abc123def456",
},
Logger: kitlog.NewNopLogger(),
Logger: slog.New(logging.DiscardHandler{}),
}
c.fn(t, ds, client, reconciler)
@@ -1007,7 +1008,7 @@ func testBuildAndSendFleetAgentConfigForEnrollment(t *testing.T, ds fleet.Datast
// Create service and call BuildAndSendFleetAgentConfig with skipHostsWithoutNewCerts=false
// This simulates the enrollment flow from software_worker.go
svc := &Service{
logger: kitlog.NewNopLogger(),
logger: slog.New(logging.DiscardHandler{}),
fleetDS: ds,
ds: ds.(fleet.AndroidDatastore),
androidAPIClient: client,
+37 -43
View File
@@ -16,7 +16,6 @@ import (
"github.com/fleetdm/fleet/v4/server/ptr"
"github.com/fleetdm/fleet/v4/server/worker"
"github.com/go-json-experiment/json"
"github.com/go-kit/log/level"
"golang.org/x/text/cases"
"golang.org/x/text/language"
"google.golang.org/api/androidmanagement/v1"
@@ -45,7 +44,7 @@ func (svc *Service) ProcessPubSubPush(ctx context.Context, token string, message
svc.authz.SkipAuthorization(ctx)
return nil
}
level.Debug(svc.logger).Log("msg", "Received PubSub message", "notification", notificationType)
svc.logger.DebugContext(ctx, "Received PubSub message", "notification", notificationType)
if android.NotificationType(notificationType) == android.PubSubTest {
// Nothing to process
svc.authz.SkipAuthorization(ctx)
@@ -69,7 +68,7 @@ func (svc *Service) ProcessPubSubPush(ctx context.Context, token string, message
return svc.handlePubSubStatusReport(ctx, token, rawData)
default:
// Ignore unknown notification types
level.Debug(svc.logger).Log("msg", "Ignoring PubSub notification type", "notification", notificationType)
svc.logger.DebugContext(ctx, "Ignoring PubSub notification type", "notification", notificationType)
svc.authz.SkipAuthorization(ctx)
return nil
}
@@ -151,7 +150,7 @@ func (svc *Service) handlePubSubStatusReport(ctx context.Context, token string,
}
if isDeleted {
level.Debug(svc.logger).Log("msg", "Android device deleted from MDM", "device.name", device.Name,
svc.logger.DebugContext(ctx, "Android device deleted from MDM", "device.name", device.Name,
"device.enterpriseSpecificId", device.HardwareInfo.EnterpriseSpecificId)
// User-initiated unenroll (work profile removed) or device deleted via AMAPI.
@@ -203,18 +202,18 @@ func (svc *Service) handlePubSubStatusReport(ctx context.Context, token string,
return ctxerr.Wrap(ctx, err, "getting existing Android host")
}
if host == nil {
level.Debug(svc.logger).Log("msg", "Device not found in Fleet. Perhaps it was deleted, "+
svc.logger.DebugContext(ctx, "Device not found in Fleet. Perhaps it was deleted, "+
"but it is still connected via Android MDM. Re-enrolling", "device.name", device.Name,
"device.enterpriseSpecificId", device.HardwareInfo.EnterpriseSpecificId)
err = svc.enrollHost(ctx, &device)
if err != nil {
level.Debug(svc.logger).Log("msg", "Error re-enrolling Android host", "data", rawData)
svc.logger.DebugContext(ctx, "Error re-enrolling Android host", "data", rawData)
return ctxerr.Wrap(ctx, err, "re-enrolling deleted Android host")
}
}
err = svc.updateHost(ctx, &device, host, false)
if err != nil {
level.Debug(svc.logger).Log("msg", "Error updating Android host", "data", rawData)
svc.logger.DebugContext(ctx, "Error updating Android host", "data", rawData)
return ctxerr.Wrap(ctx, err, "enrolling Android host")
}
err = svc.updateHostSoftware(ctx, &device, host)
@@ -327,7 +326,7 @@ func (svc *Service) handlePubSubEnrollment(ctx context.Context, token string, ra
err = svc.enrollHost(ctx, &device)
if err != nil {
level.Debug(svc.logger).Log("msg", "Error enrolling Android host", "data", rawData)
svc.logger.DebugContext(ctx, "Error enrolling Android host", "data", rawData)
return ctxerr.Wrap(ctx, err, "enrolling Android host")
}
return nil
@@ -360,7 +359,7 @@ func (svc *Service) enrollHost(ctx context.Context, device *androidmanagement.De
}
if host != nil {
level.Debug(svc.logger).Log("msg", "The enrolling Android host is already present in Fleet. Updating team if needed",
svc.logger.DebugContext(ctx, "The enrolling Android host is already present in Fleet. Updating team if needed",
"device.name", device.Name, "device.enterpriseSpecificId", device.HardwareInfo.EnterpriseSpecificId)
enrollSecret, err := svc.ds.VerifyEnrollSecret(ctx, enrollmentTokenRequest.EnrollSecret)
if err != nil && !fleet.IsNotFound(err) {
@@ -466,7 +465,7 @@ func (svc *Service) updateHost(ctx context.Context, device *androidmanagement.De
teamID = *host.Host.TeamID
}
if _, err := svc.fleetDS.CreatePendingCertificateTemplatesForNewHost(ctx, host.Host.UUID, teamID); err != nil {
level.Error(svc.logger).Log("msg", "failed to create pending certificate templates for re-enrolled host", "host_uuid", host.Host.UUID, "err", err)
svc.logger.ErrorContext(ctx, "failed to create pending certificate templates for re-enrolled host", "host_uuid", host.Host.UUID, "err", err)
return ctxerr.Wrap(ctx, err, "creating pending certificate templates for re-enrolled host")
}
@@ -574,7 +573,7 @@ func (svc *Service) addNewHost(ctx context.Context, device *androidmanagement.De
}
if enrollmentTokenRequest.IdpUUID != "" {
level.Info(svc.logger).Log("msg", "associating android host with idp account", "host_uuid", host.UUID, "idp_uuid", enrollmentTokenRequest.IdpUUID)
svc.logger.InfoContext(ctx, "associating android host with idp account", "host_uuid", host.UUID, "idp_uuid", enrollmentTokenRequest.IdpUUID)
err := svc.ds.AssociateHostMDMIdPAccount(ctx, host.UUID, enrollmentTokenRequest.IdpUUID)
if err != nil {
return ctxerr.Wrap(ctx, err, "associating host with idp account")
@@ -591,7 +590,7 @@ func (svc *Service) addNewHost(ctx context.Context, device *androidmanagement.De
teamID = *enrollSecret.GetTeamID()
}
if _, err := svc.fleetDS.CreatePendingCertificateTemplatesForNewHost(ctx, fleetHost.Host.UUID, teamID); err != nil {
level.Error(svc.logger).Log("msg", "failed to create pending certificate templates for new host", "host_uuid", fleetHost.Host.UUID, "err", err)
svc.logger.ErrorContext(ctx, "failed to create pending certificate templates for new host", "host_uuid", fleetHost.Host.UUID, "err", err)
return ctxerr.Wrap(ctx, err, "creating pending certificate templates for new host")
}
@@ -640,7 +639,7 @@ func (svc *Service) getPolicyID(ctx context.Context, device *androidmanagement.D
return nil, ctxerr.Errorf(ctx, "invalid Android policy name: %s", device.AppliedPolicyName)
}
if len(nameParts[3]) == 0 {
level.Error(svc.logger).Log("msg", "Empty Android policy ID", "device.name", device.Name,
svc.logger.ErrorContext(ctx, "Empty Android policy ID", "device.name", device.Name,
"device.enterpriseSpecificID", device.HardwareInfo.EnterpriseSpecificId, "device.AppliedPolicyName",
device.AppliedPolicyName)
return nil, nil
@@ -651,7 +650,7 @@ func (svc *Service) getPolicyID(ctx context.Context, device *androidmanagement.D
func (svc *Service) verifyDevicePolicy(ctx context.Context, hostUUID string, device *androidmanagement.Device) {
appliedPolicyVersion := device.AppliedPolicyVersion
level.Debug(svc.logger).Log("msg", "Verifying Android device policy", "host_uuid", hostUUID, "applied_policy_version", appliedPolicyVersion)
svc.logger.DebugContext(ctx, "Verifying Android device policy", "host_uuid", hostUUID, "applied_policy_version", appliedPolicyVersion)
// Get all host_mdm_android_profiles that are pending or failed due to non compliance reasons,
// and included_in_policy_version <= device.AppliedPolicyVersion. That way we can either fully
@@ -660,7 +659,7 @@ func (svc *Service) verifyDevicePolicy(ctx context.Context, hostUUID string, dev
// Get all profiles that are pending or failed install
pendingInstallProfiles, err := svc.ds.ListHostMDMAndroidProfilesPendingOrFailedInstallWithVersion(ctx, hostUUID, appliedPolicyVersion)
if err != nil {
level.Error(svc.logger).Log("msg", "error getting pending profiles", "err", err)
svc.logger.ErrorContext(ctx, "error getting pending profiles", "err", err)
return
}
@@ -684,7 +683,7 @@ func (svc *Service) verifyDevicePolicy(ctx context.Context, hostUUID string, dev
err = svc.ds.BulkUpsertMDMAndroidHostProfiles(ctx, verifiedProfiles)
if err != nil {
level.Error(svc.logger).Log("msg", "error verifying pending install profiles", "err", err)
svc.logger.ErrorContext(ctx, "error verifying pending install profiles", "err", err)
}
} else {
@@ -700,19 +699,19 @@ func (svc *Service) verifyDevicePolicy(ctx context.Context, hostUUID string, dev
// Then re-use the map above, so we can iterate over it again, but now the payload is already unmarshalled.
policyRequest, err := svc.ds.GetAndroidPolicyRequestByUUID(ctx, policyRequestUUID)
if err != nil && !fleet.IsNotFound(err) {
level.Error(svc.logger).Log("msg", "error getting policy request", "err", err, "policy_request_uuid", policyRequestUUID, "host_uuid", hostUUID)
svc.logger.ErrorContext(ctx, "error getting policy request", "err", err, "policy_request_uuid", policyRequestUUID, "host_uuid", hostUUID)
return
}
if fleet.IsNotFound(err) {
level.Error(svc.logger).Log("msg", "policy request not found", "policy_request_uuid", policyRequestUUID, "host_uuid", hostUUID)
svc.logger.ErrorContext(ctx, "policy request not found", "policy_request_uuid", policyRequestUUID, "host_uuid", hostUUID)
return
}
var policyRequestPayload fleet.AndroidPolicyRequestPayload
err = json.Unmarshal(policyRequest.Payload, &policyRequestPayload)
if err != nil {
level.Error(svc.logger).Log("msg", "error unmarshalling policy request payload", "err", err, "policy_request_uuid", policyRequestUUID, "host_uuid", hostUUID)
svc.logger.ErrorContext(ctx, "error unmarshalling policy request payload", "err", err, "policy_request_uuid", policyRequestUUID, "host_uuid", hostUUID)
return
}
@@ -758,7 +757,7 @@ func (svc *Service) verifyDevicePolicy(ctx context.Context, hostUUID string, dev
err = svc.ds.BulkUpsertMDMAndroidHostProfiles(ctx, profiles)
if err != nil {
level.Error(svc.logger).Log("msg", "error upserting android profiles", "err", err, "host_uuid", hostUUID)
svc.logger.ErrorContext(ctx, "error upserting android profiles", "err", err, "host_uuid", hostUUID)
return
}
}
@@ -766,7 +765,7 @@ func (svc *Service) verifyDevicePolicy(ctx context.Context, hostUUID string, dev
// Bulk delete any pending or failed remove profiles.
err = svc.ds.BulkDeleteMDMAndroidHostProfiles(ctx, hostUUID, appliedPolicyVersion)
if err != nil {
level.Error(svc.logger).Log("msg", "error deleting pending or failed remove profiles", "err", err, "host_uuid", hostUUID)
svc.logger.ErrorContext(ctx, "error deleting pending or failed remove profiles", "err", err, "host_uuid", hostUUID)
}
}
@@ -774,14 +773,14 @@ func (svc *Service) verifyDeviceSoftware(ctx context.Context, host *fleet.Host,
appliedPolicyVersion := device.AppliedPolicyVersion
hostUUID := host.UUID
level.Debug(svc.logger).Log("msg", "Verifying Android device software", "host_uuid", hostUUID, "applied_policy_version", appliedPolicyVersion)
svc.logger.DebugContext(ctx, "Verifying Android device software", "host_uuid", hostUUID, "applied_policy_version", appliedPolicyVersion)
// Get all host_vpp_software_installs that are pending, and set in a policy version <= device.AppliedPolicyVersion.
// That way we can either fully verify the app install, or mark as failed if the app is not compliant.
pendingInstallApps, err := svc.ds.ListHostMDMAndroidVPPAppsPendingInstallWithVersion(ctx, hostUUID, appliedPolicyVersion)
if err != nil {
level.Error(svc.logger).Log("msg", "error getting pending vpp installs", "err", err)
svc.logger.ErrorContext(ctx, "error getting pending vpp installs", "err", err)
return
}
if len(pendingInstallApps) == 0 {
@@ -820,11 +819,11 @@ func (svc *Service) verifyDeviceSoftware(ctx context.Context, host *fleet.Host,
if appReport.State == "INSTALLED" {
// definitely installed successfully
markVerified[appReport.PackageName] = true
level.Debug(svc.logger).Log("msg", "Software marked as verified", "host_uuid", hostUUID, "package_name", appReport.PackageName)
svc.logger.DebugContext(ctx, "Software marked as verified", "host_uuid", hostUUID, "package_name", appReport.PackageName)
continue
}
}
level.Debug(svc.logger).Log("msg", "Software not marked as verified, checking if failed", "host_uuid", hostUUID, "package_name", appReport.PackageName)
svc.logger.DebugContext(ctx, "Software not marked as verified, checking if failed", "host_uuid", hostUUID, "package_name", appReport.PackageName)
}
// for the remaining apps, mark as failed if non-conformant
@@ -838,7 +837,7 @@ func (svc *Service) verifyDeviceSoftware(ctx context.Context, host *fleet.Host,
if report.NonComplianceReason == "PENDING" || report.InstallationFailureReason == "IN_PROGRESS" {
// keep as pending, the understanding is that another pub-sub will follow when the app's state
// chances to installed or failed.
level.Debug(svc.logger).Log("msg", "Software not reported as installed yet, will remain pending", "host_uuid", hostUUID, "package_name", packageName,
svc.logger.DebugContext(ctx, "Software not reported as installed yet, will remain pending", "host_uuid", hostUUID, "package_name", packageName,
"non_compliance_reason", report.NonComplianceReason,
"installation_failure_reason", report.InstallationFailureReason)
continue
@@ -846,7 +845,7 @@ func (svc *Service) verifyDeviceSoftware(ctx context.Context, host *fleet.Host,
// otherwise it has failed to install, mark as failed
markVerified[packageName] = false
level.Error(svc.logger).Log("msg", "Software failed to install", "host_uuid", hostUUID, "package_name", packageName,
svc.logger.ErrorContext(ctx, "Software failed to install", "host_uuid", hostUUID, "package_name", packageName,
"non_compliance_reason", report.NonComplianceReason,
"installation_failure_reason", report.InstallationFailureReason,
"specific_non_compliance_reason", report.SpecificNonComplianceReason)
@@ -858,7 +857,7 @@ func (svc *Service) verifyDeviceSoftware(ctx context.Context, host *fleet.Host,
// failed, we don't know how long it might take for the device to receive another
// policy, it may never happen.
markVerified[packageName] = false
level.Error(svc.logger).Log("msg", "Software failed to install without non-compliance report", "host_uuid", hostUUID, "package_name", packageName,
svc.logger.ErrorContext(ctx, "Software failed to install without non-compliance report", "host_uuid", hostUUID, "package_name", packageName,
"installation_failure_reason", "unknown - no non-compliance report received")
}
@@ -875,11 +874,11 @@ func (svc *Service) verifyDeviceSoftware(ctx context.Context, host *fleet.Host,
}
}
if err := svc.ds.BulkSetVPPInstallsAsVerified(ctx, host.ID, toVerifyUUIDs); err != nil {
level.Error(svc.logger).Log("msg", "error marking vpp installs as verified", "err", err, "host_uuid", hostUUID)
svc.logger.ErrorContext(ctx, "error marking vpp installs as verified", "err", err, "host_uuid", hostUUID)
return
}
if err := svc.ds.BulkSetVPPInstallsAsFailed(ctx, host.ID, toFailUUIDs); err != nil {
level.Error(svc.logger).Log("msg", "error marking vpp installs as failed", "err", err, "host_uuid", hostUUID)
svc.logger.ErrorContext(ctx, "error marking vpp installs as failed", "err", err, "host_uuid", hostUUID)
return
}
@@ -891,17 +890,17 @@ func (svc *Service) verifyDeviceSoftware(ctx context.Context, host *fleet.Host,
return false
}
// otherwise it's a DB error and we should fail
level.Error(svc.logger).Log("msg", "error getting past activity for installed software", "err", err, "host_uuid", hostUUID)
svc.logger.ErrorContext(ctx, "error getting past activity for installed software", "err", err, "host_uuid", hostUUID)
return true
}
if act == nil {
// could happen if command is not found, but shouldn't
level.Debug(svc.logger).Log("msg", "getting past activity for installed software did not find the command", "host_uuid", hostUUID)
svc.logger.DebugContext(ctx, "getting past activity for installed software did not find the command", "host_uuid", hostUUID)
return false
}
act.FromSetupExperience = true // currently, all Android app installs are from setup experience
if err := svc.activityModule.NewActivity(ctx, user, act); err != nil {
level.Error(svc.logger).Log("msg", "error creating past activity for installed software", "err", err, "host_uuid", hostUUID)
svc.logger.ErrorContext(ctx, "error creating past activity for installed software", "err", err, "host_uuid", hostUUID)
return true
}
return false
@@ -969,8 +968,7 @@ func (svc *Service) calculateAndroidStorageMetrics(
}
// Log memory events for debugging
level.Debug(svc.logger).Log(
"msg", logPrefix+logSuffix,
svc.logger.DebugContext(ctx, logPrefix+logSuffix,
"device_id", device.HardwareInfo.EnterpriseSpecificId,
"total_internal_storage", totalStorageBytes,
"memory_events_count", len(device.MemoryEvents),
@@ -990,8 +988,7 @@ func (svc *Service) calculateAndroidStorageMetrics(
var latestExternalMeasuredTime time.Time
for _, event := range device.MemoryEvents {
level.Debug(svc.logger).Log(
"msg", "Android memory event"+logSuffix,
svc.logger.DebugContext(ctx, "Android memory event"+logSuffix,
"event_type", event.EventType,
"byte_count", event.ByteCount,
"create_time", event.CreateTime,
@@ -1000,8 +997,7 @@ func (svc *Service) calculateAndroidStorageMetrics(
eventTime, err := time.Parse(time.RFC3339, event.CreateTime)
if err != nil {
// Log parse error but continue processing
level.Debug(svc.logger).Log(
"msg", "Failed to parse event time"+logSuffix,
svc.logger.DebugContext(ctx, "Failed to parse event time"+logSuffix,
"event_type", event.EventType,
"create_time", event.CreateTime,
"error", err,
@@ -1051,8 +1047,7 @@ func (svc *Service) calculateAndroidStorageMetrics(
gigsDiskSpaceAvailable = -1
percentDiskSpaceAvailable = -1
level.Debug(svc.logger).Log(
"msg", "Android storage measurement not supported"+logSuffix,
svc.logger.DebugContext(ctx, "Android storage measurement not supported"+logSuffix,
"device_id", device.HardwareInfo.EnterpriseSpecificId,
"total_storage_bytes", totalStorageBytes,
"reason", "Only DETECTED events, no MEASURED events",
@@ -1061,8 +1056,7 @@ func (svc *Service) calculateAndroidStorageMetrics(
gigsDiskSpaceAvailable = float64(totalAvailableBytes) / (1024 * 1024 * 1024)
percentDiskSpaceAvailable = (float64(totalAvailableBytes) / float64(totalStorageBytes)) * 100
level.Debug(svc.logger).Log(
"msg", "Android storage calculation complete"+logSuffix,
svc.logger.DebugContext(ctx, "Android storage calculation complete"+logSuffix,
"total_storage_bytes", totalStorageBytes,
"total_available_bytes", totalAvailableBytes,
"gigs_total", gigsTotalDiskSpace,
+4 -3
View File
@@ -5,6 +5,7 @@ import (
"encoding/base64"
"encoding/json"
"errors"
"log/slog"
"net/http"
"os"
"strconv"
@@ -16,10 +17,10 @@ import (
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/mdm/android"
android_mock "github.com/fleetdm/fleet/v4/server/mdm/android/mock"
"github.com/fleetdm/fleet/v4/server/platform/logging"
common_mysql "github.com/fleetdm/fleet/v4/server/platform/mysql"
"github.com/fleetdm/fleet/v4/server/ptr"
"github.com/fleetdm/fleet/v4/server/service/modules/activities"
kitlog "github.com/go-kit/log"
"github.com/google/uuid"
"github.com/stretchr/testify/require"
"google.golang.org/api/androidmanagement/v1"
@@ -31,9 +32,9 @@ const testBrandTestSerialHashed = "9c311e05af14f958bd65188796e41fcc8a7b0ff913bfe
func createAndroidService(t *testing.T) (android.Service, *AndroidMockDS) {
androidAPIClient := android_mock.Client{}
androidAPIClient.InitCommonMocks()
logger := kitlog.NewLogfmtLogger(os.Stdout)
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
mockDS := InitCommonDSMocks()
activityModule := activities.NewActivityModule(mockDS, logger)
activityModule := activities.NewActivityModule(mockDS, logging.NewLogger(logger))
svc, err := NewServiceWithClient(logger, mockDS, &androidAPIClient, "test-private-key", &mockDS.DataStore, activityModule, config.AndroidAgentConfig{})
require.NoError(t, err)
@@ -3,18 +3,17 @@ package service
import (
"context"
"fmt"
"log/slog"
"time"
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
"github.com/fleetdm/fleet/v4/server/fleet"
kitlog "github.com/go-kit/log"
"github.com/go-kit/log/level"
)
// ReconcileAndroidDevices polls AMAPI for devices that Fleet still considers enrolled
// and flips them to unenrolled if Google reports them missing (404).
// This complements (does not replace) Pub/Sub DELETED handling.
func ReconcileAndroidDevices(ctx context.Context, ds fleet.Datastore, logger kitlog.Logger, licenseKey string) error {
func ReconcileAndroidDevices(ctx context.Context, ds fleet.Datastore, logger *slog.Logger, licenseKey string) error {
appConfig, err := ds.AppConfig(ctx)
if err != nil {
return ctxerr.Wrap(ctx, err, "get app config")
@@ -80,7 +79,7 @@ func ReconcileAndroidDevices(ctx context.Context, ds fleet.Datastore, logger kit
continue
case !ok:
if _, derr := ds.SetAndroidHostUnenrolled(ctx, dev.HostID); derr != nil {
level.Error(logger).Log("msg", "failed to mark android host unenrolled during reconcile", "host_id", dev.HostID, "err", derr)
logger.ErrorContext(ctx, "failed to mark android host unenrolled during reconcile", "host_id", dev.HostID, "err", derr)
continue
}
// Emit system activity to mirror Pub/Sub DELETED handling.
@@ -95,13 +94,13 @@ func ReconcileAndroidDevices(ctx context.Context, ds fleet.Datastore, logger kit
InstalledFromDEP: false,
Platform: "android",
}, nil, time.Now()); aerr != nil {
level.Debug(logger).Log("msg", "failed to create mdm_unenrolled activity during android reconcile", "host_id", dev.HostID, "err", aerr)
logger.DebugContext(ctx, "failed to create mdm_unenrolled activity during android reconcile", "host_id", dev.HostID, "err", aerr)
}
unenrolled++
level.Debug(logger).Log("msg", "android device missing in Google; marked unenrolled", "host_id", dev.HostID, "device", deviceName)
logger.DebugContext(ctx, "android device missing in Google; marked unenrolled", "host_id", dev.HostID, "device", deviceName)
}
}
level.Debug(logger).Log("msg", "android reconcile complete", "checked", checked, "unenrolled", unenrolled)
logger.DebugContext(ctx, "android reconcile complete", "checked", checked, "unenrolled", unenrolled)
return nil
}
+26 -27
View File
@@ -6,6 +6,7 @@ import (
"encoding/json"
"errors"
"fmt"
"log/slog"
"net/http"
"net/url"
"strings"
@@ -23,8 +24,6 @@ import (
"github.com/fleetdm/fleet/v4/server/mdm/android"
"github.com/fleetdm/fleet/v4/server/mdm/android/service/androidmgmt"
"github.com/fleetdm/fleet/v4/server/service/modules/activities"
kitlog "github.com/go-kit/log"
"github.com/go-kit/log/level"
"google.golang.org/api/androidmanagement/v1"
"google.golang.org/api/googleapi"
)
@@ -38,7 +37,7 @@ const (
)
type Service struct {
logger kitlog.Logger
logger *slog.Logger
authz *authz.Authorizer
ds fleet.AndroidDatastore
fleetDS fleet.Datastore
@@ -57,7 +56,7 @@ type Service struct {
func NewService(
ctx context.Context,
logger kitlog.Logger,
logger *slog.Logger,
ds fleet.AndroidDatastore,
licenseKey string,
serverPrivateKey string,
@@ -70,7 +69,7 @@ func NewService(
}
func NewServiceWithClient(
logger kitlog.Logger,
logger *slog.Logger,
ds fleet.AndroidDatastore,
client androidmgmt.Client,
serverPrivateKey string,
@@ -108,7 +107,7 @@ func NewServiceWithClient(
return svc, nil
}
func newAMAPIClient(ctx context.Context, logger kitlog.Logger, licenseKey string) androidmgmt.Client {
func newAMAPIClient(ctx context.Context, logger *slog.Logger, licenseKey string) androidmgmt.Client {
var client androidmgmt.Client
getEnv := dev_mode.Env
if getEnv("FLEET_DEV_ANDROID_GOOGLE_CLIENT") == "1" || strings.ToUpper(getEnv("FLEET_DEV_ANDROID_GOOGLE_CLIENT")) == "ON" {
@@ -372,7 +371,7 @@ func (svc *Service) EnterpriseSignupCallback(ctx context.Context, signupToken st
switch {
case fleet.IsNotFound(err):
// This should never happen.
level.Error(svc.logger).Log("msg", "User that created the Android enterprise was not found", "user_id", enterprise.UserID)
svc.logger.ErrorContext(ctx, "User that created the Android enterprise was not found", "user_id", enterprise.UserID)
case err != nil:
return ctxerr.Wrap(ctx, err, "getting user")
}
@@ -692,7 +691,7 @@ func (svc *Service) EnterpriseSignupSSE(ctx context.Context) (chan string, error
for {
select {
case <-ctx.Done():
level.Debug(svc.logger).Log("msg", "Context cancelled during Android signup SSE")
svc.logger.DebugContext(ctx, "Context cancelled during Android signup SSE")
return
case <-time.After(svc.SignupSSEInterval):
if svc.signupSSECheck(ctx, done) {
@@ -743,24 +742,24 @@ func (svc *Service) verifyEnterpriseExistsWithGoogle(ctx context.Context, enterp
case http.StatusNotFound:
// Special case: 404 from proxy with deletion confirmation
if strings.Contains(gerr.Message, "PROXY_VERIFIED_DELETED:") {
level.Info(svc.logger).Log("msg", "enterprise confirmed deleted by proxy", "enterpriseID", enterprise.EnterpriseID)
svc.logger.InfoContext(ctx, "enterprise confirmed deleted by proxy", "enterpriseID", enterprise.EnterpriseID)
svc.cleanupDeletedEnterprise(ctx, enterprise, enterprise.EnterpriseID)
return fleet.NewInvalidArgumentError("enterprise", "Android Enterprise has been deleted").WithStatus(http.StatusNotFound)
}
case http.StatusBadRequest:
// Bad request might indicate missing headers or invalid request format
// Don't delete the enterprise in this case
level.Error(svc.logger).Log("msg", "bad request when verifying enterprise", "error", err)
svc.logger.ErrorContext(ctx, "bad request when verifying enterprise", "error", err)
return fmt.Errorf("verifying enterprise with Google: %s (check request headers and format)", err.Error())
case http.StatusUnauthorized, http.StatusForbidden:
// Authentication/authorization issues - don't delete the enterprise
level.Error(svc.logger).Log("msg", "authentication/authorization error when verifying enterprise", "error", err)
svc.logger.ErrorContext(ctx, "authentication/authorization error when verifying enterprise", "error", err)
return fmt.Errorf("verifying enterprise with Google: authentication error: %w", err)
}
}
// LIST failed - this is likely a technical issue, not deletion
// Log the error but don't delete the enterprise
level.Error(svc.logger).Log("msg", "failed to list enterprises", "error", err)
svc.logger.ErrorContext(ctx, "failed to list enterprises", "error", err)
return fmt.Errorf("verifying enterprise with Google: %s", err.Error())
}
@@ -774,7 +773,7 @@ func (svc *Service) verifyEnterpriseExistsWithGoogle(ctx context.Context, enterp
}
// Enterprise NOT in list - it's deleted - perform cleanup
level.Info(svc.logger).Log("msg", "enterprise confirmed deleted via LIST API", "enterpriseID", enterpriseID)
svc.logger.InfoContext(ctx, "enterprise confirmed deleted via LIST API", "enterpriseID", enterpriseID)
svc.cleanupDeletedEnterprise(ctx, enterprise, enterpriseID)
return fleet.NewInvalidArgumentError("enterprise", "Android Enterprise has been deleted").WithStatus(http.StatusNotFound)
}
@@ -799,26 +798,26 @@ func (svc *Service) cleanupDeletedEnterprise(ctx context.Context, enterprise *an
// Clean up proxy database records by calling proxy DELETE endpoint
// This ensures the proxy won't return conflicts when creating new signup URLs
if deleteErr := svc.androidAPIClient.EnterpriseDelete(ctx, enterprise.Name()); deleteErr != nil {
level.Warn(svc.logger).Log("msg", "failed to delete proxy records after enterprise deletion (may not exist)", "err", deleteErr)
svc.logger.WarnContext(ctx, "failed to delete proxy records after enterprise deletion (may not exist)", "err", deleteErr)
}
// Delete local enterprise records
if deleteErr := svc.ds.DeleteAllEnterprises(ctx); deleteErr != nil {
level.Error(svc.logger).Log("msg", "failed to delete local enterprise records after deletion", "err", deleteErr)
svc.logger.ErrorContext(ctx, "failed to delete local enterprise records after deletion", "err", deleteErr)
}
// Turn off Android MDM
if setErr := svc.ds.SetAndroidEnabledAndConfigured(ctx, false); setErr != nil {
level.Error(svc.logger).Log("msg", "failed to turn off Android MDM after enterprise deletion", "err", setErr)
svc.logger.ErrorContext(ctx, "failed to turn off Android MDM after enterprise deletion", "err", setErr)
}
// Unenroll Android hosts
if unenrollErr := svc.ds.BulkSetAndroidHostsUnenrolled(ctx); unenrollErr != nil {
level.Error(svc.logger).Log("msg", "failed to unenroll Android hosts after enterprise deletion", "err", unenrollErr)
svc.logger.ErrorContext(ctx, "failed to unenroll Android hosts after enterprise deletion", "err", unenrollErr)
}
if err := svc.ds.MarkAllPendingAndroidVPPInstallsAsFailed(ctx); err != nil {
level.Error(svc.logger).Log("msg", "failed to mark pending Android VPP installs as failed after enterprise deletion", "err", err)
svc.logger.ErrorContext(ctx, "failed to mark pending Android VPP installs as failed after enterprise deletion", "err", err)
}
}
@@ -835,7 +834,7 @@ func (svc *Service) UnenrollAndroidHost(ctx context.Context, hostID uint) error
}
if !fleet.IsAndroidPlatform(host.Platform) {
level.Debug(svc.logger).Log("msg", "Skipping Android unenrollment for non-Android host", "host_id", host.ID, "platform", host.Platform)
svc.logger.DebugContext(ctx, "Skipping Android unenrollment for non-Android host", "host_id", host.ID, "platform", host.Platform)
return nil // no-op for non-Android hosts
}
@@ -1063,7 +1062,7 @@ func (svc *Service) EnableAppReportsOnDefaultPolicy(ctx context.Context) error {
if err != nil {
if fleet.IsNotFound(err) {
// Then Android MDM isn't setup yet, so no-op
level.Info(svc.logger).Log("msg", "skipping android default policy migration, Android MDM is not turned on")
svc.logger.InfoContext(ctx, "skipping android default policy migration, Android MDM is not turned on")
return nil
}
return ctxerr.Wrap(ctx, err, "getting android enterprise")
@@ -1312,7 +1311,7 @@ func (svc *Service) BuildAndSendFleetAgentConfig(ctx context.Context, enterprise
// This prevents concurrent cron runs from processing the same templates
certTemplates, err := svc.fleetDS.GetAndTransitionCertificateTemplatesToDelivering(ctx, hostUUID)
if err != nil {
level.Error(svc.logger).Log("msg", "failed to get and transition to delivering", "host_uuid", hostUUID, "err", err)
svc.logger.ErrorContext(ctx, "failed to get and transition to delivering", "host_uuid", hostUUID, "err", err)
return ctxerr.Wrapf(ctx, err, "get and transition certificate templates to delivering for host %s", hostUUID)
}
@@ -1325,12 +1324,12 @@ func (svc *Service) BuildAndSendFleetAgentConfig(ctx context.Context, enterprise
// There should be no other certificates either, but including them just in case.
config, err := buildHostConfig(hostUUID, certTemplates.Templates)
if err != nil {
level.Error(svc.logger).Log("msg", "failed to build host config without certs", "host_uuid", hostUUID, "err", err)
svc.logger.ErrorContext(ctx, "failed to build host config without certs", "host_uuid", hostUUID, "err", err)
return ctxerr.Wrapf(ctx, err, "build host config without certs for host %s", hostUUID)
}
hostConfigs := map[string]android.AgentManagedConfiguration{hostUUID: *config}
if err := svc.AddFleetAgentToAndroidPolicy(ctx, enterpriseName, hostConfigs); err != nil {
level.Error(svc.logger).Log("msg", "failed to send AMAPI config without certs", "host_uuid", hostUUID, "err", err)
svc.logger.ErrorContext(ctx, "failed to send AMAPI config without certs", "host_uuid", hostUUID, "err", err)
// Not a critical failure. We will retry installing Fleet Agent when certificates are added to the host's team
}
continue
@@ -1339,16 +1338,16 @@ func (svc *Service) BuildAndSendFleetAgentConfig(ctx context.Context, enterprise
// Step 2: Build and send config to AMAPI with ALL certificate templates
config, err := buildHostConfig(hostUUID, certTemplates.Templates)
if err != nil {
level.Error(svc.logger).Log("msg", "failed to build host config", "host_uuid", hostUUID, "err", err)
svc.logger.ErrorContext(ctx, "failed to build host config", "host_uuid", hostUUID, "err", err)
return ctxerr.Wrapf(ctx, err, "build host config for %s", hostUUID)
}
hostConfigs := map[string]android.AgentManagedConfiguration{hostUUID: *config}
if err := svc.AddFleetAgentToAndroidPolicy(ctx, enterpriseName, hostConfigs); err != nil {
// AMAPI call failed, revert to pending for retry later
level.Error(svc.logger).Log("msg", "failed to send to AMAPI", "host_uuid", hostUUID, "err", err)
svc.logger.ErrorContext(ctx, "failed to send to AMAPI", "host_uuid", hostUUID, "err", err)
if revertErr := svc.fleetDS.RevertHostCertificateTemplatesToPending(ctx, hostUUID, certTemplates.DeliveringTemplateIDs); revertErr != nil {
level.Error(svc.logger).Log("msg", "failed to revert to pending after AMAPI failure", "host_uuid", hostUUID, "err", revertErr)
svc.logger.ErrorContext(ctx, "failed to revert to pending after AMAPI failure", "host_uuid", hostUUID, "err", revertErr)
return ctxerr.Wrapf(ctx, revertErr, "revert certificate templates to pending after AMAPI failure for host %s", hostUUID)
}
continue
@@ -1356,7 +1355,7 @@ func (svc *Service) BuildAndSendFleetAgentConfig(ctx context.Context, enterprise
// Step 3: Transition delivering → delivered
if err := svc.fleetDS.TransitionCertificateTemplatesToDelivered(ctx, hostUUID, certTemplates.DeliveringTemplateIDs); err != nil {
level.Error(svc.logger).Log("msg", "failed to transition to delivered", "host_uuid", hostUUID, "err", err)
svc.logger.ErrorContext(ctx, "failed to transition to delivered", "host_uuid", hostUUID, "err", err)
return ctxerr.Wrap(ctx, err, "transition certificate templates to delivered")
}
}
+7 -5
View File
@@ -2,6 +2,7 @@ package tests
import (
"context"
"log/slog"
"net/http"
"net/http/httptest"
"os"
@@ -18,12 +19,12 @@ import (
"github.com/fleetdm/fleet/v4/server/mdm/android/service/androidmgmt"
ds_mock "github.com/fleetdm/fleet/v4/server/mock"
"github.com/fleetdm/fleet/v4/server/platform/endpointer"
"github.com/fleetdm/fleet/v4/server/platform/logging"
"github.com/fleetdm/fleet/v4/server/ptr"
"github.com/fleetdm/fleet/v4/server/service/middleware/auth"
"github.com/fleetdm/fleet/v4/server/service/middleware/log"
"github.com/fleetdm/fleet/v4/server/service/modules/activities"
kithttp "github.com/go-kit/kit/transport/http"
kitlog "github.com/go-kit/log"
"github.com/gorilla/mux"
"github.com/jmoiron/sqlx"
"github.com/stretchr/testify/assert"
@@ -111,13 +112,14 @@ func (ts *WithServer) SetupSuite(t *testing.T, dbName string) {
ts.AndroidAPIClient = android_mock.Client{}
ts.createCommonProxyMocks(t)
logger := kitlog.NewLogfmtLogger(os.Stdout)
activityModule := activities.NewActivityModule(&ts.DS.DataStore, logger)
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
kitLogger := logging.NewLogger(logger)
activityModule := activities.NewActivityModule(&ts.DS.DataStore, kitLogger)
svc, err := service.NewServiceWithClient(logger, &ts.DS, &ts.AndroidAPIClient, "test-private-key", ts.DS.Datastore, activityModule, config.AndroidAgentConfig{})
require.NoError(t, err)
ts.Svc = svc
ts.Server = runServerForTests(t, logger, &ts.FleetSvc, svc)
ts.Server = runServerForTests(t, kitLogger, &ts.FleetSvc, svc)
}
func (ts *WithServer) CreateCommonDSMocks() {
@@ -206,7 +208,7 @@ func (m *mockService) NewActivity(ctx context.Context, user *fleet.User, details
return m.Called(ctx, user, details).Error(0)
}
func runServerForTests(t *testing.T, logger kitlog.Logger, fleetSvc fleet.Service, androidSvc android.Service) *httptest.Server {
func runServerForTests(t *testing.T, logger *logging.Logger, fleetSvc fleet.Service, androidSvc android.Service) *httptest.Server {
// androidErrorEncoder wraps EncodeError with nil domain encoder for android tests
androidErrorEncoder := func(ctx context.Context, err error, w http.ResponseWriter) {
endpointer.EncodeError(ctx, err, w, nil)
@@ -5,6 +5,7 @@ import (
"encoding/json"
"errors"
"fmt"
"log/slog"
"net/http"
"strings"
"testing"
@@ -13,10 +14,10 @@ import (
"github.com/fleetdm/fleet/v4/server/datastore/mysql"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/mdm/android"
"github.com/fleetdm/fleet/v4/server/platform/logging"
"github.com/fleetdm/fleet/v4/server/ptr"
"github.com/fleetdm/fleet/v4/server/service/contract"
"github.com/fleetdm/fleet/v4/server/worker"
"github.com/go-kit/log"
"github.com/google/uuid"
"github.com/jmoiron/sqlx"
"github.com/stretchr/testify/require"
@@ -431,7 +432,7 @@ func (s *integrationMDMTestSuite) TestCertificateTemplateSpecEndpointAndAMAPIFai
// Step: Queue and run the Android setup experience worker job
// Note: Pending certificate templates were created above (simulating pubsub). The worker will deliver them.
enterpriseName := "enterprises/" + enterpriseID
err = worker.QueueRunAndroidSetupExperience(ctx, s.ds, log.NewNopLogger(), host.UUID, &teamID, enterpriseName)
err = worker.QueueRunAndroidSetupExperience(ctx, s.ds, slog.New(logging.DiscardHandler{}), host.UUID, &teamID, enterpriseName)
require.NoError(t, err)
s.runWorker()
@@ -533,7 +534,7 @@ func (s *integrationMDMTestSuite) TestCertificateTemplateNoTeamWithIDPVariable()
// Step: Queue and run the Android setup experience worker job
// Note: Pending certificate templates were created above (simulating pubsub). The worker will deliver them.
enterpriseName := "enterprises/" + enterpriseID
err = worker.QueueRunAndroidSetupExperience(ctx, s.ds, log.NewNopLogger(), host.UUID, nil, enterpriseName)
err = worker.QueueRunAndroidSetupExperience(ctx, s.ds, slog.New(logging.DiscardHandler{}), host.UUID, nil, enterpriseName)
require.NoError(t, err)
s.runWorker()
+3 -3
View File
@@ -225,7 +225,7 @@ func (s *integrationMDMTestSuite) SetupSuite() {
androidMockClient.SetAuthenticationSecretFunc = func(secret string) error {
return nil
}
androidSvc, err := android_service.NewServiceWithClient(wlog, s.ds, androidMockClient, "test-private-key", s.ds, activityModule, config.AndroidAgentConfig{
androidSvc, err := android_service.NewServiceWithClient(wlog.SlogLogger(), s.ds, androidMockClient, "test-private-key", s.ds, activityModule, config.AndroidAgentConfig{
Package: "com.fleetdm.agent",
SigningSHA256: "abc123def456",
})
@@ -410,7 +410,7 @@ func (s *integrationMDMTestSuite) SetupSuite() {
if s.onCleanupScheduleDone != nil {
defer s.onCleanupScheduleDone()
}
return android_service.RenewCertificateTemplates(ctx, ds, logger)
return android_service.RenewCertificateTemplates(ctx, ds, logger.SlogLogger())
}),
)
return cleanupsSchedule, nil
@@ -432,7 +432,7 @@ func (s *integrationMDMTestSuite) SetupSuite() {
s.onAndroidProfileJobDone()
}()
}
err := android_service.ReconcileProfilesWithClient(ctx, ds, logger, "", androidMockClient, config.AndroidAgentConfig{
err := android_service.ReconcileProfilesWithClient(ctx, ds, logger.SlogLogger(), "", androidMockClient, config.AndroidAgentConfig{
Package: "com.fleetdm.agent",
SigningSHA256: "abc123def456",
})
@@ -28,7 +28,7 @@ func SetUpSuite(t *testing.T, uniqueTestName string) *Suite {
proxy.InitCommonMocks()
activityModule := activities.NewActivityModule(ds, logger)
androidSvc, err := android_service.NewServiceWithClient(
logger,
logger.SlogLogger(),
ds,
&proxy,
"test-private-key",
+3 -2
View File
@@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"fmt"
"log/slog"
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
"github.com/fleetdm/fleet/v4/server/fleet"
@@ -430,7 +431,7 @@ func buildApplicationPolicyWithConfig(ctx context.Context, appIDs []string,
return appPolicies, nil
}
func QueueRunAndroidSetupExperience(ctx context.Context, ds fleet.Datastore, logger kitlog.Logger,
func QueueRunAndroidSetupExperience(ctx context.Context, ds fleet.Datastore, logger *slog.Logger,
hostUUID string, hostEnrollTeamID *uint, enterpriseName string) error {
var enrollTeamID uint
@@ -449,7 +450,7 @@ func QueueRunAndroidSetupExperience(ctx context.Context, ds fleet.Datastore, log
return ctxerr.Wrap(ctx, err, "queueing job")
}
level.Debug(logger).Log("job_id", job.ID, "job_name", softwareWorkerJobName, "task", args.Task)
logger.DebugContext(ctx, "queued android setup experience job", "job_id", job.ID, "job_name", softwareWorkerJobName, "task", args.Task)
return nil
}