Files
Nico 3f5944626c Fix Fleet startup crash on read-only filesystem without S3 bucket (#47099)
**Related issue:** Resolves #47090

Fleet crashes into `CrashLoopBackOff` on startup when deployed on
Kubernetes with `readOnlyRootFilesystem: true` and **no** S3 software
installers bucket configured:

```
Failed to start: initializing filesystem org logo store: mkdir /tmp/org-logos: read-only file system
```

I realised I was calling `initFatal` when failing to create a directory
on the filesystem which doesn't match the pattern of `logging` +
`creating a "failing" store` (one that is initialized but fails all
operations) as we do for e.g. software title icons (see
NewFailingSoftwareTitleIconStore).

Per this slack conversation:
https://fleetdm.slack.com/archives/C084F4MKYSJ/p1780931127976389, we
decided to fall back to a database-backed storage:

<img width="737" height="114" alt="Screenshot 2026-06-08 at 3 16 28 PM"
src="https://github.com/user-attachments/assets/2a6ff75f-b382-40ba-81d9-3be3cfbd648a"
/>


# Checklist for submitter

- [x] Changes file added for user-visible changes in `changes/`.

## Testing

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

Commented out this line to force filesystem usage:

<img width="615" height="71" alt="Screenshot 2026-06-08 at 1 18 53 PM"
src="https://github.com/user-attachments/assets/85043c88-5c8c-48a0-8145-098fba9513bd"
/>


#### Before

Server crashes

<img width="1278" height="124" alt="Screenshot 2026-06-08 at 1 18 17 PM"
src="https://github.com/user-attachments/assets/7b788a24-131a-47a3-8580-fcd9fda8b449"
/>


#### After

Server starts and logo upload works

- Without --dev_license



https://github.com/user-attachments/assets/58c5ebf9-cf52-4ba0-ac98-9675e7eef92c



- With --dev_license




https://github.com/user-attachments/assets/117bb812-31bd-4849-927c-93cafd1a71d7








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

## Release Notes

* **New Features**
- Organization logos now support database storage as the fallback option
when S3 software installers bucket is not configured, replacing local
filesystem storage for improved reliability.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-06-08 17:15:14 -03:00

67 lines
2.0 KiB
Go

package mysql
import (
"bytes"
"context"
"database/sql"
"errors"
"io"
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/jmoiron/sqlx"
)
type orgLogoStore struct {
ds *Datastore
}
var _ fleet.OrgLogoStore = (*orgLogoStore)(nil)
func (ds *Datastore) NewOrgLogoStore() fleet.OrgLogoStore {
return &orgLogoStore{ds: ds}
}
func (s *orgLogoStore) Put(ctx context.Context, mode fleet.OrgLogoMode, content io.ReadSeeker) error {
data, err := io.ReadAll(content)
if err != nil {
return ctxerr.Wrap(ctx, err, "reading org logo content")
}
const stmt = `
INSERT INTO org_logo (mode, data, uploaded_at)
VALUES (?, ?, NOW(6))
ON DUPLICATE KEY UPDATE data = ?, uploaded_at = NOW(6)`
if _, err := s.ds.writer(ctx).ExecContext(ctx, stmt, string(mode), data, data); err != nil {
return ctxerr.Wrap(ctx, err, "storing org logo")
}
return nil
}
func (s *orgLogoStore) Get(ctx context.Context, mode fleet.OrgLogoMode) (io.ReadCloser, int64, error) {
var data []byte
err := sqlx.GetContext(ctx, s.ds.reader(ctx), &data, `SELECT data FROM org_logo WHERE mode = ?`, string(mode))
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, 0, ctxerr.Wrap(ctx, notFound("OrgLogo").WithName(string(mode)), "get org logo")
}
return nil, 0, ctxerr.Wrap(ctx, err, "get org logo")
}
return io.NopCloser(bytes.NewReader(data)), int64(len(data)), nil
}
func (s *orgLogoStore) Delete(ctx context.Context, mode fleet.OrgLogoMode) error {
if _, err := s.ds.writer(ctx).ExecContext(ctx, `DELETE FROM org_logo WHERE mode = ?`, string(mode)); err != nil {
return ctxerr.Wrap(ctx, err, "deleting org logo")
}
return nil
}
func (s *orgLogoStore) Exists(ctx context.Context, mode fleet.OrgLogoMode) (bool, error) {
var exists bool
err := sqlx.GetContext(ctx, s.ds.reader(ctx), &exists, `SELECT EXISTS(SELECT 1 FROM org_logo WHERE mode = ?)`, string(mode))
if err != nil {
return false, ctxerr.Wrap(ctx, err, "checking org logo existence")
}
return exists, nil
}