mirror of
https://github.com/fleetdm/fleet
synced 2026-04-21 13:37:30 +00:00
🤖 Add fallback for FMA manifest URL pulls (#43312)
**Related issue:** Resolves #42754 # Checklist for submitter If some of the following don't apply, delete the relevant line. - [x] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. See [Changes files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files) for more information. - [x] Input data is properly validated, `SELECT *` is avoided, SQL injection is prevented (using placeholders for values in statements), JS inline code is prevented especially for url redirects, and untrusted data interpolated into shell scripts/commands is validated against shell metacharacters. ## 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 * **Bug Fixes** * Improved app manifest retrieval with automatic fallback to hosted copies when the primary source is unavailable, reducing sync failures. * **Documentation** * Clarified that Fleet will fall back to hosted manifest copies if the new manifest site is inaccessible. * **New Features** * Streamlined maintained-app synchronization to use a simpler sync entrypoint and unified primary/fallback fetch logic. * **Tests** * Added comprehensive tests for primary/fallback fetch flows, error handling, large-response truncation, and environment-based overrides. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
parent
58563852f0
commit
8509b18c46
6 changed files with 413 additions and 77 deletions
|
|
@ -1 +1 @@
|
|||
* Switched Fleet-maintained apps serving location from GitHub to https://maintained-apps.fleetdm.com/manifests. If you limit outbound Fleet server traffic, make sure it can access the new FMA manifests location.
|
||||
* Switched Fleet-maintained apps serving location from GitHub to https://maintained-apps.fleetdm.com/manifests. If this site is inaccessible, Fleet will fall back to the previous GitHub-hosted copies of manifest files.
|
||||
|
|
|
|||
|
|
@ -2005,7 +2005,7 @@ func newMaintainedAppSchedule(
|
|||
// ensures it runs a few seconds after Fleet is started
|
||||
schedule.WithDefaultPrevRunCreatedAt(time.Now().Add(priorJobDiff)),
|
||||
schedule.WithJob("refresh_maintained_apps", func(ctx context.Context) error {
|
||||
return maintained_apps.Refresh(ctx, ds, logger)
|
||||
return maintained_apps.SyncAppsList(ctx, ds)
|
||||
}),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -4,9 +4,9 @@ import (
|
|||
"context"
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
|
|
@ -30,10 +30,80 @@ type AppsList struct {
|
|||
}
|
||||
|
||||
const fmaOutputsBase = "https://maintained-apps.fleetdm.com/manifests"
|
||||
const fmaOutputsFallbackBase = "https://raw.githubusercontent.com/fleetdm/fleet/refs/heads/main/ee/maintained-apps/outputs"
|
||||
|
||||
// Refresh fetches the latest information about maintained apps from FMA's
|
||||
// apps list on GitHub and updates the Fleet database with the new information.
|
||||
func Refresh(ctx context.Context, ds fleet.Datastore, logger *slog.Logger) error {
|
||||
// resolveBaseURLs returns the primary and fallback base URLs for FMA manifests,
|
||||
// taking into account any dev-mode env var overrides.
|
||||
func resolveBaseURLs() (primary, fallback string) {
|
||||
primary = fmaOutputsBase
|
||||
if baseFromEnvVar := dev_mode.Env("FLEET_DEV_MAINTAINED_APPS_BASE_URL"); baseFromEnvVar != "" {
|
||||
primary = baseFromEnvVar
|
||||
}
|
||||
|
||||
fallback = fmaOutputsFallbackBase
|
||||
if fallbackFromEnvVar := dev_mode.Env("FLEET_DEV_MAINTAINED_APPS_FALLBACK_BASE_URL"); fallbackFromEnvVar != "" {
|
||||
fallback = fallbackFromEnvVar
|
||||
}
|
||||
|
||||
return primary, fallback
|
||||
}
|
||||
|
||||
// fetchManifestFile fetches a manifest file from the primary FMA CDN, falling back to the
|
||||
// fallback CDN if the primary fails.
|
||||
func fetchManifestFile(ctx context.Context, path string) ([]byte, error) {
|
||||
primaryBase, fallbackBase := resolveBaseURLs()
|
||||
|
||||
body, primaryErr := doFetch(ctx, primaryBase, path)
|
||||
if primaryErr == nil {
|
||||
return body, nil
|
||||
}
|
||||
|
||||
// Primary failed; try fallback.
|
||||
body, fallbackErr := doFetch(ctx, fallbackBase, path)
|
||||
if fallbackErr == nil {
|
||||
return body, nil
|
||||
}
|
||||
|
||||
return nil, ctxerr.Errorf(ctx, "fetching FMA manifest file %q: primary (%s) failed: %v; fallback (%s) also failed: %v",
|
||||
path, primaryBase, primaryErr, fallbackBase, fallbackErr)
|
||||
}
|
||||
|
||||
// doFetch performs a single HTTP GET for baseURL+path and returns the response
|
||||
// body on success (HTTP 200). Any other outcome is returned as an error.
|
||||
func doFetch(ctx context.Context, baseURL, path string) ([]byte, error) {
|
||||
httpClient := fleethttp.NewClient(fleethttp.WithTimeout(10 * time.Second))
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("%s%s", baseURL, path), nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create http request: %w", err)
|
||||
}
|
||||
|
||||
res, err := httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("execute http request: %w", err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read http response body: %w", err)
|
||||
}
|
||||
|
||||
switch res.StatusCode {
|
||||
case http.StatusOK:
|
||||
return body, nil
|
||||
case http.StatusNotFound:
|
||||
return nil, errors.New("not found (HTTP 404)")
|
||||
default:
|
||||
if len(body) > 512 {
|
||||
body = body[:512]
|
||||
}
|
||||
return nil, fmt.Errorf("HTTP status %d: %s", res.StatusCode, string(body))
|
||||
}
|
||||
}
|
||||
|
||||
// SyncAppsList fetches the latest FMA apps list and updates the apps list copy cached in the DB
|
||||
func SyncAppsList(ctx context.Context, ds fleet.Datastore) error {
|
||||
appsList, err := FetchAppsList(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -43,38 +113,9 @@ func Refresh(ctx context.Context, ds fleet.Datastore, logger *slog.Logger) error
|
|||
}
|
||||
|
||||
func FetchAppsList(ctx context.Context) (*AppsList, error) {
|
||||
httpClient := fleethttp.NewClient(fleethttp.WithTimeout(10 * time.Second))
|
||||
baseURL := fmaOutputsBase
|
||||
if baseFromEnvVar := dev_mode.Env("FLEET_DEV_MAINTAINED_APPS_BASE_URL"); baseFromEnvVar != "" {
|
||||
baseURL = baseFromEnvVar
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("%s/apps.json", baseURL), nil)
|
||||
body, err := fetchManifestFile(ctx, "/apps.json")
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "create http request")
|
||||
}
|
||||
|
||||
res, err := httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "execute http request")
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "read http response body")
|
||||
}
|
||||
|
||||
switch res.StatusCode {
|
||||
case http.StatusOK:
|
||||
// success, go on
|
||||
case http.StatusNotFound:
|
||||
return nil, ctxerr.New(ctx, "maintained apps list not found")
|
||||
default:
|
||||
if len(body) > 512 {
|
||||
body = body[:512]
|
||||
}
|
||||
return nil, ctxerr.Errorf(ctx, "apps list returned HTTP status %d: %s", res.StatusCode, string(body))
|
||||
return nil, ctxerr.Wrap(ctx, err, "fetch apps list")
|
||||
}
|
||||
|
||||
var appsList AppsList
|
||||
|
|
@ -163,38 +204,9 @@ func Hydrate(ctx context.Context, app *fleet.MaintainedApp, version string, team
|
|||
return app, nil
|
||||
}
|
||||
|
||||
httpClient := fleethttp.NewClient(fleethttp.WithTimeout(10 * time.Second))
|
||||
baseURL := fmaOutputsBase
|
||||
if baseFromEnvVar := dev_mode.Env("FLEET_DEV_MAINTAINED_APPS_BASE_URL"); baseFromEnvVar != "" {
|
||||
baseURL = baseFromEnvVar
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fmt.Sprintf("%s/%s.json", baseURL, app.Slug), nil)
|
||||
body, err := fetchManifestFile(ctx, fmt.Sprintf("/%s.json", app.Slug))
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "create http request")
|
||||
}
|
||||
|
||||
res, err := httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "execute http request")
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "read http response body")
|
||||
}
|
||||
|
||||
switch res.StatusCode {
|
||||
case http.StatusOK:
|
||||
// success, go on
|
||||
case http.StatusNotFound:
|
||||
return nil, ctxerr.New(ctx, "app not found in Fleet manifests")
|
||||
default:
|
||||
if len(body) > 512 {
|
||||
body = body[:512]
|
||||
}
|
||||
return nil, ctxerr.Errorf(ctx, "manifest retrieval returned HTTP status %d: %s", res.StatusCode, string(body))
|
||||
return nil, ctxerr.Wrap(ctx, err, "fetch app manifest")
|
||||
}
|
||||
|
||||
var manifest ma.FMAManifestFile
|
||||
|
|
|
|||
305
server/mdm/maintainedapps/sync_test.go
Normal file
305
server/mdm/maintainedapps/sync_test.go
Normal file
|
|
@ -0,0 +1,305 @@
|
|||
package maintained_apps
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/server/dev_mode"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// newTestAppsJSON returns a valid apps.json payload for testing.
|
||||
func newTestAppsJSON() []byte {
|
||||
data, _ := json.Marshal(AppsList{
|
||||
Version: 2,
|
||||
Apps: []appListing{
|
||||
{Name: "Test App", Slug: "test-app", Platform: "darwin", UniqueIdentifier: "com.test.app"},
|
||||
},
|
||||
})
|
||||
return data
|
||||
}
|
||||
|
||||
func TestFetchAppsListPrimarySuccess(t *testing.T) {
|
||||
primary := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, "/apps.json", r.URL.Path)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write(newTestAppsJSON())
|
||||
}))
|
||||
t.Cleanup(primary.Close)
|
||||
|
||||
fallback := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Error("fallback should not be called when primary succeeds")
|
||||
}))
|
||||
t.Cleanup(fallback.Close)
|
||||
|
||||
dev_mode.SetOverride("FLEET_DEV_MAINTAINED_APPS_BASE_URL", primary.URL, t)
|
||||
dev_mode.SetOverride("FLEET_DEV_MAINTAINED_APPS_FALLBACK_BASE_URL", fallback.URL, t)
|
||||
|
||||
apps, err := FetchAppsList(t.Context())
|
||||
require.NoError(t, err)
|
||||
require.Len(t, apps.Apps, 1)
|
||||
assert.Equal(t, "test-app", apps.Apps[0].Slug)
|
||||
}
|
||||
|
||||
func TestFetchAppsListFallbackOnPrimaryFailure(t *testing.T) {
|
||||
var fallbackWasCalled atomic.Bool
|
||||
primary := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
_, _ = w.Write([]byte("primary is down"))
|
||||
}))
|
||||
t.Cleanup(primary.Close)
|
||||
|
||||
fallback := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, "/apps.json", r.URL.Path)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write(newTestAppsJSON())
|
||||
fallbackWasCalled.Store(true)
|
||||
}))
|
||||
t.Cleanup(fallback.Close)
|
||||
|
||||
dev_mode.SetOverride("FLEET_DEV_MAINTAINED_APPS_BASE_URL", primary.URL, t)
|
||||
dev_mode.SetOverride("FLEET_DEV_MAINTAINED_APPS_FALLBACK_BASE_URL", fallback.URL, t)
|
||||
|
||||
apps, err := FetchAppsList(t.Context())
|
||||
require.NoError(t, err)
|
||||
require.Len(t, apps.Apps, 1)
|
||||
assert.Equal(t, "test-app", apps.Apps[0].Slug)
|
||||
assert.True(t, fallbackWasCalled.Load())
|
||||
}
|
||||
|
||||
func TestFetchAppsListFallbackOnPrimary404(t *testing.T) {
|
||||
primary := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}))
|
||||
t.Cleanup(primary.Close)
|
||||
|
||||
fallback := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write(newTestAppsJSON())
|
||||
}))
|
||||
t.Cleanup(fallback.Close)
|
||||
|
||||
dev_mode.SetOverride("FLEET_DEV_MAINTAINED_APPS_BASE_URL", primary.URL, t)
|
||||
dev_mode.SetOverride("FLEET_DEV_MAINTAINED_APPS_FALLBACK_BASE_URL", fallback.URL, t)
|
||||
|
||||
apps, err := FetchAppsList(t.Context())
|
||||
require.NoError(t, err)
|
||||
require.Len(t, apps.Apps, 1)
|
||||
}
|
||||
|
||||
func TestFetchAppsListBothFail(t *testing.T) {
|
||||
primary := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusServiceUnavailable)
|
||||
_, _ = w.Write([]byte("primary down"))
|
||||
}))
|
||||
t.Cleanup(primary.Close)
|
||||
|
||||
fallback := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusBadGateway)
|
||||
_, _ = w.Write([]byte("fallback down"))
|
||||
}))
|
||||
t.Cleanup(fallback.Close)
|
||||
|
||||
dev_mode.SetOverride("FLEET_DEV_MAINTAINED_APPS_BASE_URL", primary.URL, t)
|
||||
dev_mode.SetOverride("FLEET_DEV_MAINTAINED_APPS_FALLBACK_BASE_URL", fallback.URL, t)
|
||||
|
||||
_, err := FetchAppsList(t.Context())
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "primary")
|
||||
assert.Contains(t, err.Error(), "fallback")
|
||||
}
|
||||
|
||||
func TestFetchAppsListFallbackOnPrimaryNetworkError(t *testing.T) {
|
||||
// Start and immediately close the primary to simulate a connection-refused error.
|
||||
primary := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
|
||||
primary.Close()
|
||||
|
||||
fallback := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write(newTestAppsJSON())
|
||||
}))
|
||||
t.Cleanup(fallback.Close)
|
||||
|
||||
dev_mode.SetOverride("FLEET_DEV_MAINTAINED_APPS_BASE_URL", primary.URL, t)
|
||||
dev_mode.SetOverride("FLEET_DEV_MAINTAINED_APPS_FALLBACK_BASE_URL", fallback.URL, t)
|
||||
|
||||
apps, err := FetchAppsList(t.Context())
|
||||
require.NoError(t, err)
|
||||
require.Len(t, apps.Apps, 1)
|
||||
}
|
||||
|
||||
func TestFetchAppsListOnlyFallbackFails(t *testing.T) {
|
||||
// Primary works; fallback is broken. Nothing should break.
|
||||
primary := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write(newTestAppsJSON())
|
||||
}))
|
||||
t.Cleanup(primary.Close)
|
||||
|
||||
// Closed server as broken fallback.
|
||||
fallback := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
|
||||
fallback.Close()
|
||||
|
||||
dev_mode.SetOverride("FLEET_DEV_MAINTAINED_APPS_BASE_URL", primary.URL, t)
|
||||
dev_mode.SetOverride("FLEET_DEV_MAINTAINED_APPS_FALLBACK_BASE_URL", fallback.URL, t)
|
||||
|
||||
apps, err := FetchAppsList(t.Context())
|
||||
require.NoError(t, err)
|
||||
require.Len(t, apps.Apps, 1)
|
||||
}
|
||||
|
||||
func TestFetchManifestDataFallbackUsedForSlugPath(t *testing.T) {
|
||||
var primaryHits, fallbackHits atomic.Int32
|
||||
|
||||
primary := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
primaryHits.Add(1)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}))
|
||||
t.Cleanup(primary.Close)
|
||||
|
||||
fallback := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
fallbackHits.Add(1)
|
||||
assert.Equal(t, "/test-app.json", r.URL.Path)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte(`{"test": true}`))
|
||||
}))
|
||||
t.Cleanup(fallback.Close)
|
||||
|
||||
dev_mode.SetOverride("FLEET_DEV_MAINTAINED_APPS_BASE_URL", primary.URL, t)
|
||||
dev_mode.SetOverride("FLEET_DEV_MAINTAINED_APPS_FALLBACK_BASE_URL", fallback.URL, t)
|
||||
|
||||
body, err := fetchManifestFile(t.Context(), "/test-app.json")
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, string(body), `"test"`)
|
||||
assert.Equal(t, int32(1), primaryHits.Load(), "primary should have been tried once")
|
||||
assert.Equal(t, int32(1), fallbackHits.Load(), "fallback should have been tried once")
|
||||
}
|
||||
|
||||
func TestFetchManifestDataPrimarySucceedsSkipsFallback(t *testing.T) {
|
||||
primary := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte(`{"ok": true}`))
|
||||
}))
|
||||
t.Cleanup(primary.Close)
|
||||
|
||||
var fallbackHits atomic.Int32
|
||||
fallback := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
fallbackHits.Add(1)
|
||||
}))
|
||||
t.Cleanup(fallback.Close)
|
||||
|
||||
dev_mode.SetOverride("FLEET_DEV_MAINTAINED_APPS_BASE_URL", primary.URL, t)
|
||||
dev_mode.SetOverride("FLEET_DEV_MAINTAINED_APPS_FALLBACK_BASE_URL", fallback.URL, t)
|
||||
|
||||
body, err := fetchManifestFile(t.Context(), "/something.json")
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, string(body), `"ok"`)
|
||||
assert.Equal(t, int32(0), fallbackHits.Load(), "fallback must not be contacted when primary succeeds")
|
||||
}
|
||||
|
||||
func TestResolveBaseURLsDefaults(t *testing.T) {
|
||||
dev_mode.SetOverride("FLEET_DEV_MAINTAINED_APPS_BASE_URL", "", t)
|
||||
dev_mode.SetOverride("FLEET_DEV_MAINTAINED_APPS_FALLBACK_BASE_URL", "", t)
|
||||
primary, fallback := resolveBaseURLs()
|
||||
assert.Equal(t, fmaOutputsBase, primary)
|
||||
assert.Equal(t, fmaOutputsFallbackBase, fallback)
|
||||
}
|
||||
|
||||
func TestResolveBaseURLsWithOverrides(t *testing.T) {
|
||||
dev_mode.SetOverride("FLEET_DEV_MAINTAINED_APPS_BASE_URL", "http://custom-primary", t)
|
||||
dev_mode.SetOverride("FLEET_DEV_MAINTAINED_APPS_FALLBACK_BASE_URL", "http://custom-fallback", t)
|
||||
|
||||
primary, fallback := resolveBaseURLs()
|
||||
assert.Equal(t, "http://custom-primary", primary)
|
||||
assert.Equal(t, "http://custom-fallback", fallback)
|
||||
}
|
||||
|
||||
func TestDoFetchSuccess(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
assert.Equal(t, "/test.json", r.URL.Path)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte("ok"))
|
||||
}))
|
||||
t.Cleanup(srv.Close)
|
||||
|
||||
body, err := doFetch(t.Context(), srv.URL, "/test.json")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "ok", string(body))
|
||||
}
|
||||
|
||||
func TestDoFetchNotFound(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}))
|
||||
t.Cleanup(srv.Close)
|
||||
|
||||
_, err := doFetch(t.Context(), srv.URL, "/missing.json")
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "404")
|
||||
}
|
||||
|
||||
func TestDoFetchServerError(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusBadGateway)
|
||||
_, _ = w.Write([]byte("bad gateway"))
|
||||
}))
|
||||
t.Cleanup(srv.Close)
|
||||
|
||||
_, err := doFetch(t.Context(), srv.URL, "/broken.json")
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "502")
|
||||
}
|
||||
|
||||
func TestDoFetchNetworkError(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
|
||||
srv.Close() // immediately close to force connection errors
|
||||
|
||||
_, err := doFetch(t.Context(), srv.URL, "/anything.json")
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestDoFetchTruncatesLargeBodyInErrorMessage(t *testing.T) {
|
||||
largeBody := make([]byte, 1024)
|
||||
for i := range largeBody {
|
||||
largeBody[i] = 'x'
|
||||
}
|
||||
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
_, _ = w.Write(largeBody)
|
||||
}))
|
||||
t.Cleanup(srv.Close)
|
||||
|
||||
_, err := doFetch(t.Context(), srv.URL, "/big.json")
|
||||
require.Error(t, err)
|
||||
// The error message should contain at most 512 bytes of body.
|
||||
assert.LessOrEqual(t, len(err.Error()), 600) // 512 body + status prefix
|
||||
}
|
||||
|
||||
func TestFetchAppsListFallbackOverrideViaEnvVar(t *testing.T) {
|
||||
primary := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusServiceUnavailable)
|
||||
}))
|
||||
t.Cleanup(primary.Close)
|
||||
|
||||
var fallbackWasCalled atomic.Bool
|
||||
customFallback := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write(newTestAppsJSON())
|
||||
fallbackWasCalled.Store(true)
|
||||
}))
|
||||
t.Cleanup(customFallback.Close)
|
||||
|
||||
dev_mode.SetOverride("FLEET_DEV_MAINTAINED_APPS_BASE_URL", primary.URL, t)
|
||||
dev_mode.SetOverride("FLEET_DEV_MAINTAINED_APPS_FALLBACK_BASE_URL", customFallback.URL, t)
|
||||
|
||||
apps, err := FetchAppsList(t.Context())
|
||||
require.NoError(t, err)
|
||||
require.Len(t, apps.Apps, 1)
|
||||
assert.Equal(t, "test-app", apps.Apps[0].Slug)
|
||||
assert.True(t, fallbackWasCalled.Load())
|
||||
}
|
||||
|
|
@ -3,7 +3,6 @@ package maintained_apps
|
|||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
|
|
@ -44,8 +43,10 @@ func SyncApps(t *testing.T, ds fleet.Datastore) []fleet.MaintainedApp {
|
|||
// this call
|
||||
dev_mode.SetOverride("FLEET_DEV_MAINTAINED_APPS_BASE_URL", srv.URL)
|
||||
defer dev_mode.ClearOverride("FLEET_DEV_MAINTAINED_APPS_BASE_URL")
|
||||
dev_mode.SetOverride("FLEET_DEV_MAINTAINED_APPS_FALLBACK_BASE_URL", srv.URL)
|
||||
defer dev_mode.ClearOverride("FLEET_DEV_MAINTAINED_APPS_FALLBACK_BASE_URL")
|
||||
|
||||
err := Refresh(context.Background(), ds, slog.New(slog.DiscardHandler))
|
||||
err := SyncAppsList(context.Background(), ds)
|
||||
require.NoError(t, err)
|
||||
|
||||
apps, _, err := ds.ListAvailableFleetMaintainedApps(context.Background(), nil, fleet.ListOptions{
|
||||
|
|
@ -101,7 +102,7 @@ func SyncAndRemoveApps(t *testing.T, ds fleet.Datastore) {
|
|||
dev_mode.SetOverride("FLEET_DEV_MAINTAINED_APPS_BASE_URL", srv.URL)
|
||||
defer dev_mode.ClearOverride("FLEET_DEV_MAINTAINED_APPS_BASE_URL")
|
||||
|
||||
err = Refresh(context.Background(), ds, slog.New(slog.DiscardHandler))
|
||||
err = SyncAppsList(context.Background(), ds)
|
||||
require.NoError(t, err)
|
||||
|
||||
originalApps, _, err := ds.ListAvailableFleetMaintainedApps(context.Background(), nil, fleet.ListOptions{})
|
||||
|
|
@ -113,7 +114,7 @@ func SyncAndRemoveApps(t *testing.T, ds fleet.Datastore) {
|
|||
removedApp := appsFile.Apps[0]
|
||||
appsFile.Apps = appsFile.Apps[1:]
|
||||
|
||||
err = Refresh(context.Background(), ds, slog.New(slog.DiscardHandler))
|
||||
err = SyncAppsList(context.Background(), ds)
|
||||
require.NoError(t, err)
|
||||
|
||||
modifiedApps, _, err := ds.ListAvailableFleetMaintainedApps(context.Background(), nil, fleet.ListOptions{})
|
||||
|
|
@ -128,7 +129,7 @@ func SyncAndRemoveApps(t *testing.T, ds fleet.Datastore) {
|
|||
// remove all apps from upstream.
|
||||
appsFile.Apps = []appListing{}
|
||||
|
||||
err = Refresh(context.Background(), ds, slog.New(slog.DiscardHandler))
|
||||
err = SyncAppsList(context.Background(), ds)
|
||||
require.NoError(t, err)
|
||||
|
||||
modifiedApps, _, err = ds.ListAvailableFleetMaintainedApps(context.Background(), nil, fleet.ListOptions{})
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ import (
|
|||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
|
|
@ -1494,12 +1496,25 @@ func startFMAServers(t *testing.T, ds fleet.Datastore, states map[string]*fmaTes
|
|||
_, _ = w.Write(state.installerBytes)
|
||||
}))
|
||||
|
||||
// call Refresh directly (instead of SyncApps) since we're using the server above and not the file server
|
||||
// created in SyncApps
|
||||
err := maintained_apps.Refresh(t.Context(), ds, slog.New(slog.DiscardHandler))
|
||||
require.NoError(t, err)
|
||||
// Locate the repo's apps.json so the manifest server can serve it.
|
||||
_, thisFile, _, ok := runtime.Caller(0)
|
||||
if !ok {
|
||||
t.Fatalf("could not locate myself to serve apps.json file")
|
||||
}
|
||||
repoRoot := filepath.Dir(filepath.Dir(filepath.Dir(thisFile)))
|
||||
appsJSONPath := filepath.Join(repoRoot, "ee", "maintained-apps", "outputs", "apps.json")
|
||||
|
||||
manifestServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/apps.json" {
|
||||
b, err := os.ReadFile(appsJSONPath)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
_, _ = w.Write(b)
|
||||
return
|
||||
}
|
||||
|
||||
var state *fmaTestState
|
||||
state, found := states[r.URL.Path]
|
||||
if !found {
|
||||
|
|
@ -1532,6 +1547,9 @@ func startFMAServers(t *testing.T, ds fleet.Datastore, states map[string]*fmaTes
|
|||
installerServer.Close()
|
||||
})
|
||||
dev_mode.SetOverride("FLEET_DEV_MAINTAINED_APPS_BASE_URL", manifestServer.URL, t)
|
||||
dev_mode.SetOverride("FLEET_DEV_MAINTAINED_APPS_FALLBACK_BASE_URL", manifestServer.URL, t)
|
||||
|
||||
require.NoError(t, maintained_apps.SyncAppsList(t.Context(), ds))
|
||||
}
|
||||
|
||||
// acmeCSRSigner adapts a depot.Signer to the acme.CSRSigner interface.
|
||||
|
|
|
|||
Loading…
Reference in a new issue