-
Notifications
You must be signed in to change notification settings - Fork 478
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Maintained Apps: define app list, implement ingestion (#21946)
- Loading branch information
Showing
34 changed files
with
4,903 additions
and
10 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
* Added the definition of the Fleet maintained apps and its ingestion. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
package optjson | ||
|
||
import ( | ||
"bytes" | ||
"encoding/json" | ||
) | ||
|
||
// StringOr is a JSON value that can be a string or a different type of object | ||
// (e.g. somewhat common for a string or an array of strings, but can also be | ||
// a string or an object, etc.). | ||
type StringOr[T any] struct { | ||
String string | ||
Other T | ||
IsOther bool | ||
} | ||
|
||
func (s StringOr[T]) MarshalJSON() ([]byte, error) { | ||
if s.IsOther { | ||
return json.Marshal(s.Other) | ||
} | ||
return json.Marshal(s.String) | ||
} | ||
|
||
func (s *StringOr[T]) UnmarshalJSON(data []byte) error { | ||
if bytes.HasPrefix(data, []byte(`"`)) { | ||
s.IsOther = false | ||
return json.Unmarshal(data, &s.String) | ||
} | ||
s.IsOther = true | ||
return json.Unmarshal(data, &s.Other) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,137 @@ | ||
package optjson | ||
|
||
import ( | ||
"encoding/json" | ||
"testing" | ||
|
||
"github.com/stretchr/testify/require" | ||
) | ||
|
||
func TestStringOr(t *testing.T) { | ||
type child struct { | ||
Name string `json:"name"` | ||
} | ||
|
||
type target struct { | ||
Field StringOr[[]string] `json:"field"` | ||
Array []StringOr[*child] `json:"array"` | ||
} | ||
|
||
type nested struct { | ||
Inception StringOr[*target] `json:"inception"` | ||
} | ||
|
||
cases := []struct { | ||
name string | ||
getVar func() any | ||
src string // json source to unmarshal into the value returned by getVar | ||
marshalAs string // how the value should marshal back to json | ||
unmarshalErr string // if non-empty, unmarshal should fail with this error | ||
}{ | ||
{ | ||
name: "simple string", | ||
getVar: func() any { var s StringOr[int]; return &s }, | ||
src: `"abc"`, | ||
marshalAs: `"abc"`, | ||
}, | ||
{ | ||
name: "simple integer", | ||
getVar: func() any { var s StringOr[int]; return &s }, | ||
src: `123`, | ||
marshalAs: `123`, | ||
}, | ||
{ | ||
name: "invalid bool", | ||
getVar: func() any { var s StringOr[int]; return &s }, | ||
src: `true`, | ||
unmarshalErr: "cannot unmarshal bool into Go value of type int", | ||
}, | ||
{ | ||
name: "field string", | ||
getVar: func() any { var s target; return &s }, | ||
src: `{"field":"abc"}`, | ||
marshalAs: `{"field":"abc", "array": null}`, | ||
}, | ||
{ | ||
name: "field strings", | ||
getVar: func() any { var s target; return &s }, | ||
src: `{"field":["a", "b", "c"]}`, | ||
marshalAs: `{"field":["a", "b", "c"], "array": null}`, | ||
}, | ||
{ | ||
name: "field empty array", | ||
getVar: func() any { var s target; return &s }, | ||
src: `{"field":[]}`, | ||
marshalAs: `{"field":[], "array": null}`, | ||
}, | ||
{ | ||
name: "field invalid object", | ||
getVar: func() any { var s target; return &s }, | ||
src: `{"field":{}}`, | ||
unmarshalErr: "cannot unmarshal object into Go struct field target.field of type []string", | ||
}, | ||
{ | ||
name: "array field null", | ||
getVar: func() any { var s target; return &s }, | ||
src: `{"array":null}`, | ||
marshalAs: `{"array":null, "field": ""}`, | ||
}, | ||
{ | ||
name: "array field empty", | ||
getVar: func() any { var s target; return &s }, | ||
src: `{"array":[]}`, | ||
marshalAs: `{"array":[], "field": ""}`, | ||
}, | ||
{ | ||
name: "array field single", | ||
getVar: func() any { var s target; return &s }, | ||
src: `{"array":["a"]}`, | ||
marshalAs: `{"array":["a"], "field": ""}`, | ||
}, | ||
{ | ||
name: "array field empty child", | ||
getVar: func() any { var s target; return &s }, | ||
src: `{"array":["a", {}]}`, | ||
marshalAs: `{"array":["a", {"name":""}], "field": ""}`, | ||
}, | ||
{ | ||
name: "array field set child", | ||
getVar: func() any { var s target; return &s }, | ||
src: `{"array":["a", {"name": "x"}]}`, | ||
marshalAs: `{"array":["a", {"name":"x"}], "field": ""}`, | ||
}, | ||
{ | ||
name: "inception string", | ||
getVar: func() any { var s nested; return &s }, | ||
src: `{"inception":"a"}`, | ||
marshalAs: `{"inception":"a"}`, | ||
}, | ||
{ | ||
name: "inception target field", | ||
getVar: func() any { var s nested; return &s }, | ||
src: `{"inception":{"field":["a", "b"]}}`, | ||
marshalAs: `{"inception":{"field":["a", "b"], "array": null}}`, | ||
}, | ||
{ | ||
name: "inception target field and array", | ||
getVar: func() any { var s nested; return &s }, | ||
src: `{"inception":{"field":["a", "b"], "array": ["c", {"name": "x"}]}}`, | ||
marshalAs: `{"inception":{"field":["a", "b"], "array": ["c", {"name": "x"}]}}`, | ||
}, | ||
} | ||
for _, c := range cases { | ||
t.Run(c.name, func(t *testing.T) { | ||
target := c.getVar() | ||
err := json.Unmarshal([]byte(c.src), target) | ||
if c.unmarshalErr != "" { | ||
require.ErrorContains(t, err, c.unmarshalErr) | ||
return | ||
} | ||
require.NoError(t, err) | ||
|
||
data, err := json.Marshal(target) | ||
require.NoError(t, err) | ||
require.JSONEq(t, c.marshalAs, string(data)) | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
package mysql | ||
|
||
import ( | ||
"context" | ||
|
||
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr" | ||
"github.com/fleetdm/fleet/v4/server/fleet" | ||
"github.com/jmoiron/sqlx" | ||
) | ||
|
||
func (ds *Datastore) UpsertMaintainedApp(ctx context.Context, app *fleet.MaintainedApp) error { | ||
const upsertStmt = ` | ||
INSERT INTO | ||
fleet_library_apps ( | ||
name, token, version, platform, installer_url, filename, | ||
sha256, bundle_identifier, install_script_content_id, uninstall_script_content_id | ||
) | ||
VALUES | ||
( ?, ?, ?, ?, ?, ?, | ||
?, ?, ?, ? ) | ||
ON DUPLICATE KEY UPDATE | ||
name = VALUES(name), | ||
version = VALUES(version), | ||
platform = VALUES(platform), | ||
installer_url = VALUES(installer_url), | ||
filename = VALUES(filename), | ||
sha256 = VALUES(sha256), | ||
bundle_identifier = VALUES(bundle_identifier), | ||
install_script_content_id = VALUES(install_script_content_id), | ||
uninstall_script_content_id = VALUES(uninstall_script_content_id) | ||
` | ||
|
||
return ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error { | ||
var err error | ||
|
||
// ensure the install script exists | ||
installRes, err := insertScriptContents(ctx, tx, app.InstallScript) | ||
if err != nil { | ||
return ctxerr.Wrap(ctx, err, "insert install script content") | ||
} | ||
installScriptID, _ := installRes.LastInsertId() | ||
|
||
// ensure the uninstall script exists | ||
uninstallRes, err := insertScriptContents(ctx, tx, app.UninstallScript) | ||
if err != nil { | ||
return ctxerr.Wrap(ctx, err, "insert uninstall script content") | ||
} | ||
uninstallScriptID, _ := uninstallRes.LastInsertId() | ||
|
||
// upsert the maintained app | ||
_, err = tx.ExecContext(ctx, upsertStmt, app.Name, app.Token, app.Version, app.Platform, app.InstallerURL, app.Filename, | ||
app.SHA256, app.BundleIdentifier, installScriptID, uninstallScriptID) | ||
return ctxerr.Wrap(ctx, err, "upsert maintained app") | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
package mysql | ||
|
||
import ( | ||
"context" | ||
"os" | ||
"testing" | ||
|
||
"github.com/fleetdm/fleet/v4/server/fleet" | ||
"github.com/fleetdm/fleet/v4/server/mdm/maintainedapps" | ||
"github.com/go-kit/kit/log" | ||
"github.com/jmoiron/sqlx" | ||
"github.com/stretchr/testify/require" | ||
) | ||
|
||
func TestMaintainedApps(t *testing.T) { | ||
ds := CreateMySQLDS(t) | ||
|
||
cases := []struct { | ||
name string | ||
fn func(t *testing.T, ds *Datastore) | ||
}{ | ||
{"UpsertMaintainedApps", testUpsertMaintainedApps}, | ||
{"IngestWithBrew", testIngestWithBrew}, | ||
} | ||
|
||
for _, c := range cases { | ||
t.Run(c.name, func(t *testing.T) { | ||
defer TruncateTables(t, ds) | ||
c.fn(t, ds) | ||
}) | ||
} | ||
} | ||
|
||
func testUpsertMaintainedApps(t *testing.T, ds *Datastore) { | ||
ctx := context.Background() | ||
|
||
listSavedApps := func() []*fleet.MaintainedApp { | ||
var apps []*fleet.MaintainedApp | ||
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error { | ||
return sqlx.SelectContext(ctx, q, &apps, "SELECT name, version, platform FROM fleet_library_apps ORDER BY token") | ||
}) | ||
return apps | ||
} | ||
|
||
expectedApps := maintainedapps.IngestMaintainedApps(t, ds) | ||
require.Equal(t, expectedApps, listSavedApps()) | ||
|
||
// ingesting again results in no changes | ||
maintainedapps.IngestMaintainedApps(t, ds) | ||
require.Equal(t, expectedApps, listSavedApps()) | ||
|
||
// upsert the figma app, changing the version | ||
err := ds.UpsertMaintainedApp(ctx, &fleet.MaintainedApp{ | ||
Name: "Figma", | ||
Token: "figma", | ||
InstallerURL: "https://desktop.figma.com/mac-arm/Figma-999.9.9.zip", | ||
Version: "999.9.9", | ||
Platform: fleet.MacOSPlatform, | ||
}) | ||
require.NoError(t, err) | ||
|
||
// change the expected app data for figma | ||
for _, app := range expectedApps { | ||
if app.Name == "Figma" { | ||
app.Version = "999.9.9" | ||
break | ||
} | ||
} | ||
require.Equal(t, expectedApps, listSavedApps()) | ||
} | ||
|
||
func testIngestWithBrew(t *testing.T, ds *Datastore) { | ||
if os.Getenv("NETWORK_TEST") == "" { | ||
t.Skip("set environment variable NETWORK_TEST=1 to run") | ||
} | ||
|
||
ctx := context.Background() | ||
err := maintainedapps.Refresh(ctx, ds, log.NewNopLogger()) | ||
require.NoError(t, err) | ||
|
||
expectedTokens := maintainedapps.ExpectedAppTokens(t) | ||
var actualTokens []string | ||
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error { | ||
return sqlx.SelectContext(ctx, q, &actualTokens, "SELECT token FROM fleet_library_apps ORDER BY token") | ||
}) | ||
require.ElementsMatch(t, expectedTokens, actualTokens) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
package fleet | ||
|
||
// MaintainedApp represets an app in the Fleet library of maintained apps, | ||
// as stored in the fleet_library_apps table. | ||
type MaintainedApp struct { | ||
ID uint `json:"id" db:"id"` | ||
Name string `json:"name" db:"name"` | ||
Token string `json:"-" db:"name"` | ||
Version string `json:"version" db:"version"` | ||
Platform AppleDevicePlatform `json:"platform" db:"platform"` | ||
InstallerURL string `json:"-" db:"installer_url"` | ||
Filename string `json:"filename" db:"filename"` | ||
SHA256 string `json:"-" db:"sha256"` | ||
BundleIdentifier string `json:"-" db:"bundle_identifier"` | ||
|
||
// InstallScript and UninstallScript are not stored directly in the table, they | ||
// must be filled via a JOIN on script_contents. On insert/update/upsert, these | ||
// fields are used to provide the content of those scripts. | ||
InstallScript string `json:"install_script" db:"install_script"` | ||
UninstallScript string `json:"uninstall_script" db:"uninstall_script"` | ||
} |
Oops, something went wrong.