Skip to content

Commit c12c198

Browse files
committed
Implement upsert of maintained apps
1 parent 9f7bbfe commit c12c198

File tree

5 files changed

+143
-3
lines changed

5 files changed

+143
-3
lines changed
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package mysql
2+
3+
import (
4+
"context"
5+
6+
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
7+
"github.com/fleetdm/fleet/v4/server/fleet"
8+
"github.com/jmoiron/sqlx"
9+
)
10+
11+
func (ds *Datastore) UpsertMaintainedApp(ctx context.Context, app *fleet.MaintainedApp) error {
12+
const upsertStmt = `
13+
INSERT INTO
14+
fleet_library_apps (
15+
name, token, version, platform, installer_url, filename,
16+
sha256, bundle_identifier, install_script_content_id, uninstall_script_content_id
17+
)
18+
VALUES
19+
( ?, ?, ?, ?, ?, ?,
20+
?, ?, ?, ? )
21+
ON DUPLICATE KEY UPDATE
22+
name = VALUES(name),
23+
version = VALUES(version),
24+
platform = VALUES(platform),
25+
installer_url = VALUES(installer_url),
26+
filename = VALUES(filename),
27+
sha256 = VALUES(sha256),
28+
bundle_identifier = VALUES(bundle_identifier),
29+
install_script_content_id = VALUES(install_script_content_id),
30+
uninstall_script_content_id = VALUES(uninstall_script_content_id)
31+
`
32+
33+
return ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
34+
var err error
35+
36+
// ensure the install script exists
37+
installRes, err := insertScriptContents(ctx, tx, app.InstallScript)
38+
if err != nil {
39+
return ctxerr.Wrap(ctx, err, "insert install script content")
40+
}
41+
installScriptID, _ := installRes.LastInsertId()
42+
43+
// ensure the uninstall script exists
44+
uninstallRes, err := insertScriptContents(ctx, tx, app.UninstallScript)
45+
if err != nil {
46+
return ctxerr.Wrap(ctx, err, "insert uninstall script content")
47+
}
48+
uninstallScriptID, _ := uninstallRes.LastInsertId()
49+
50+
// upsert the maintained app
51+
_, err = tx.ExecContext(ctx, upsertStmt, app.Name, app.Token, app.Version, app.Platform, app.InstallerURL, app.Filename,
52+
app.SHA256, app.BundleIdentifier, installScriptID, uninstallScriptID)
53+
return ctxerr.Wrap(ctx, err, "upsert maintained app")
54+
})
55+
}

server/fleet/datastore.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1685,6 +1685,10 @@ type Datastore interface {
16851685
GetPastActivityDataForVPPAppInstall(ctx context.Context, commandResults *mdm.CommandResults) (*User, *ActivityInstalledAppStoreApp, error)
16861686

16871687
GetVPPTokenByLocation(ctx context.Context, loc string) (*VPPTokenDB, error)
1688+
1689+
// UpsertMaintainedApp inserts or updates a maintained app using the updated
1690+
// metadata provided via app.
1691+
UpsertMaintainedApp(ctx context.Context, app *MaintainedApp) error
16881692
}
16891693

16901694
// MDMAppleStore wraps nanomdm's storage and adds methods to deal with

server/fleet/maintained_apps.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package fleet
2+
3+
// MaintainedApp represets an app in the Fleet library of maintained apps,
4+
// as stored in the fleet_library_apps table.
5+
type MaintainedApp struct {
6+
ID uint `json:"id" db:"id"`
7+
Name string `json:"name" db:"name"`
8+
Token string `json:"-" db:"name"`
9+
Version string `json:"version" db:"version"`
10+
Platform AppleDevicePlatform `json:"platform" db:"platform"`
11+
InstallerURL string `json:"-" db:"installer_url"`
12+
Filename string `json:"filename" db:"filename"`
13+
SHA256 string `json:"-" db:"sha256"`
14+
BundleIdentifier string `json:"-" db:"bundle_identifier"`
15+
16+
// InstallScript and UninstallScript are not stored directly in the table, they
17+
// must be filled via a JOIN on script_contents. On insert/update/upsert, these
18+
// fields are used to provide the content of those scripts.
19+
InstallScript string `json:"install_script" db:"install_script"`
20+
UninstallScript string `json:"uninstall_script" db:"uninstall_script"`
21+
}

server/mdm/maintainedapps/ingest.go

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ import (
77
"fmt"
88
"io"
99
"net/http"
10+
"net/url"
1011
"os"
12+
"path"
1113
"time"
1214

1315
"github.com/fleetdm/fleet/v4/pkg/fleethttp"
@@ -29,6 +31,8 @@ type maintainedApp struct {
2931

3032
const baseBrewAPIURL = "https://formulae.brew.sh/api/"
3133

34+
// Refresh fetches the latest information about maintained apps from the brew
35+
// API and updates the Fleet database with the new information.
3236
func Refresh(ctx context.Context, ds fleet.Datastore, logger kitlog.Logger) error {
3337
var apps []maintainedApp
3438
if err := json.Unmarshal(appsJSON, &apps); err != nil {
@@ -60,7 +64,7 @@ func (i ingester) ingest(ctx context.Context, apps []maintainedApp) error {
6064

6165
client := fleethttp.NewClient(fleethttp.WithTimeout(10 * time.Second))
6266

63-
// run at most 3 concurrent requests
67+
// run at most 3 concurrent requests to avoid overwhelming the brew API
6468
g.SetLimit(3)
6569
for _, app := range apps {
6670
app := app // capture loop variable, not required in Go 1.23+
@@ -108,8 +112,52 @@ func (i ingester) ingestOne(ctx context.Context, app maintainedApp, client *http
108112
return ctxerr.Wrapf(ctx, err, "unmarshal brew cask for %s", app.Identifier)
109113
}
110114

111-
//i.ds.UpsertMaintainedApp(ctx, &fleet.MaintainedApp{})
112-
panic("unimplemented")
115+
// validate required fields
116+
if len(cask.Name) == 0 || cask.Name[0] == "" {
117+
return ctxerr.Errorf(ctx, "missing name for cask %s", app.Identifier)
118+
}
119+
if cask.Token == "" {
120+
return ctxerr.Errorf(ctx, "missing token for cask %s", app.Identifier)
121+
}
122+
if cask.Version == "" {
123+
return ctxerr.Errorf(ctx, "missing version for cask %s", app.Identifier)
124+
}
125+
if cask.URL == "" {
126+
return ctxerr.Errorf(ctx, "missing URL for cask %s", app.Identifier)
127+
}
128+
parsedURL, err := url.Parse(cask.URL)
129+
if err != nil {
130+
return ctxerr.Wrapf(ctx, err, "parse URL for cask %s", app.Identifier)
131+
}
132+
filename := path.Base(parsedURL.Path)
133+
134+
installScript := installScriptForApp(app, &cask)
135+
uninstallScript := uninstallScriptForApp(app, &cask)
136+
137+
err = i.ds.UpsertMaintainedApp(ctx, &fleet.MaintainedApp{
138+
Name: cask.Name[0],
139+
Token: cask.Token,
140+
Version: cask.Version,
141+
// for now, maintained apps are always macOS (darwin)
142+
Platform: fleet.MacOSPlatform,
143+
InstallerURL: cask.URL,
144+
Filename: filename,
145+
SHA256: cask.SHA256,
146+
BundleIdentifier: app.BundleIdentifier,
147+
InstallScript: installScript,
148+
UninstallScript: uninstallScript,
149+
})
150+
return ctxerr.Wrap(ctx, err, "upsert maintained app")
151+
}
152+
153+
func installScriptForApp(app maintainedApp, cask *brewCask) string {
154+
// TODO: implement install script based on cask and app installer format
155+
return "install"
156+
}
157+
158+
func uninstallScriptForApp(app maintainedApp, cask *brewCask) string {
159+
// TODO: implement uninstall script based on cask and app installer format
160+
return "uninstall"
113161
}
114162

115163
type brewCask struct {

server/mock/datastore_mock.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1068,6 +1068,8 @@ type GetPastActivityDataForVPPAppInstallFunc func(ctx context.Context, commandRe
10681068

10691069
type GetVPPTokenByLocationFunc func(ctx context.Context, loc string) (*fleet.VPPTokenDB, error)
10701070

1071+
type UpsertMaintainedAppFunc func(ctx context.Context, app *fleet.MaintainedApp) error
1072+
10711073
type DataStore struct {
10721074
HealthCheckFunc HealthCheckFunc
10731075
HealthCheckFuncInvoked bool
@@ -2641,6 +2643,9 @@ type DataStore struct {
26412643
GetVPPTokenByLocationFunc GetVPPTokenByLocationFunc
26422644
GetVPPTokenByLocationFuncInvoked bool
26432645

2646+
UpsertMaintainedAppFunc UpsertMaintainedAppFunc
2647+
UpsertMaintainedAppFuncInvoked bool
2648+
26442649
mu sync.Mutex
26452650
}
26462651

@@ -6311,3 +6316,10 @@ func (s *DataStore) GetVPPTokenByLocation(ctx context.Context, loc string) (*fle
63116316
s.mu.Unlock()
63126317
return s.GetVPPTokenByLocationFunc(ctx, loc)
63136318
}
6319+
6320+
func (s *DataStore) UpsertMaintainedApp(ctx context.Context, app *fleet.MaintainedApp) error {
6321+
s.mu.Lock()
6322+
s.UpsertMaintainedAppFuncInvoked = true
6323+
s.mu.Unlock()
6324+
return s.UpsertMaintainedAppFunc(ctx, app)
6325+
}

0 commit comments

Comments
 (0)