mirror of
https://github.com/fleetdm/fleet
synced 2026-05-23 00:49:03 +00:00
allow manually specified install and uninstall scripts for homebrew (#30805)
> Closes #30780 # Checklist for submitter If some of the following don't apply, delete the relevant line. <!-- Note that API documentation changes are now addressed by the product design team. --> - [ ] 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] Added/updated automated tests - [x] Manual QA for all new/changed functionality
This commit is contained in:
parent
db700abb54
commit
aedb8bf78f
2 changed files with 102 additions and 42 deletions
|
|
@ -37,6 +37,10 @@ func IngestApps(ctx context.Context, logger kitlog.Logger, inputsPath, slugFilte
|
|||
var manifestApps []*maintained_apps.FMAManifestApp
|
||||
|
||||
for _, f := range files {
|
||||
if f.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
fileBytes, err := os.ReadFile(path.Join(inputsPath, f.Name()))
|
||||
if err != nil {
|
||||
return nil, ctxerr.WrapWithData(ctx, err, "reading app input file", map[string]any{"fileName": f.Name()})
|
||||
|
|
@ -85,8 +89,8 @@ type brewIngester struct {
|
|||
client *http.Client
|
||||
}
|
||||
|
||||
func (i *brewIngester) ingestOne(ctx context.Context, app inputApp) (*maintained_apps.FMAManifestApp, error) {
|
||||
apiURL := fmt.Sprintf("%scask/%s.json", i.baseURL, app.Token)
|
||||
func (i *brewIngester) ingestOne(ctx context.Context, input inputApp) (*maintained_apps.FMAManifestApp, error) {
|
||||
apiURL := fmt.Sprintf("%scask/%s.json", i.baseURL, input.Token)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, apiURL, nil)
|
||||
if err != nil {
|
||||
|
|
@ -118,55 +122,89 @@ func (i *brewIngester) ingestOne(ctx context.Context, app inputApp) (*maintained
|
|||
|
||||
var cask brewCask
|
||||
if err := json.Unmarshal(body, &cask); err != nil {
|
||||
return nil, ctxerr.Wrapf(ctx, err, "unmarshal brew cask for %s", app.Token)
|
||||
return nil, ctxerr.Wrapf(ctx, err, "unmarshal brew cask for %s", input.Token)
|
||||
}
|
||||
|
||||
out := &maintained_apps.FMAManifestApp{}
|
||||
|
||||
// validate required fields
|
||||
if len(cask.Name) == 0 || cask.Name[0] == "" {
|
||||
return nil, ctxerr.Errorf(ctx, "missing name for cask %s", app.Token)
|
||||
return nil, ctxerr.Errorf(ctx, "missing name for cask %s", input.Token)
|
||||
}
|
||||
if cask.Token == "" {
|
||||
return nil, ctxerr.Errorf(ctx, "missing token for cask %s", app.Token)
|
||||
return nil, ctxerr.Errorf(ctx, "missing token for cask %s", input.Token)
|
||||
}
|
||||
if cask.Version == "" {
|
||||
return nil, ctxerr.Errorf(ctx, "missing version for cask %s", app.Token)
|
||||
return nil, ctxerr.Errorf(ctx, "missing version for cask %s", input.Token)
|
||||
}
|
||||
if cask.URL == "" {
|
||||
return nil, ctxerr.Errorf(ctx, "missing URL for cask %s", app.Token)
|
||||
return nil, ctxerr.Errorf(ctx, "missing URL for cask %s", input.Token)
|
||||
}
|
||||
_, err = url.Parse(cask.URL)
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrapf(ctx, err, "parse URL for cask %s", app.Token)
|
||||
return nil, ctxerr.Wrapf(ctx, err, "parse URL for cask %s", input.Token)
|
||||
}
|
||||
|
||||
out.Name = app.Name
|
||||
out.Name = input.Name
|
||||
out.Version = strings.Split(cask.Version, ",")[0]
|
||||
out.InstallerURL = cask.URL
|
||||
out.UniqueIdentifier = app.UniqueIdentifier
|
||||
out.UniqueIdentifier = input.UniqueIdentifier
|
||||
out.SHA256 = cask.SHA256
|
||||
out.Queries = maintained_apps.FMAQueries{Exists: fmt.Sprintf("SELECT 1 FROM apps WHERE bundle_identifier = '%s';", out.UniqueIdentifier)}
|
||||
out.Slug = app.Slug
|
||||
out.DefaultCategories = app.DefaultCategories
|
||||
if len(app.PreUninstallScripts) != 0 {
|
||||
cask.PreUninstallScripts = app.PreUninstallScripts
|
||||
out.Slug = input.Slug
|
||||
out.DefaultCategories = input.DefaultCategories
|
||||
|
||||
var installScript, uninstallScript string
|
||||
|
||||
switch input.InstallScriptPath {
|
||||
case "":
|
||||
installScript, err = installScriptForApp(input, &cask)
|
||||
if err != nil {
|
||||
return nil, ctxerr.WrapWithData(ctx, err, "generating install script for maintained app", map[string]any{"unique_identifier": input.UniqueIdentifier})
|
||||
}
|
||||
default:
|
||||
scriptBytes, err := os.ReadFile(input.InstallScriptPath)
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "reading provided install script file")
|
||||
}
|
||||
|
||||
installScript = string(scriptBytes)
|
||||
}
|
||||
|
||||
if len(app.PostUninstallScripts) != 0 {
|
||||
cask.PostUninstallScripts = app.PostUninstallScripts
|
||||
switch input.UninstallScriptPath {
|
||||
case "":
|
||||
if len(input.PreUninstallScripts) != 0 {
|
||||
cask.PreUninstallScripts = input.PreUninstallScripts
|
||||
}
|
||||
|
||||
if len(input.PostUninstallScripts) != 0 {
|
||||
cask.PostUninstallScripts = input.PostUninstallScripts
|
||||
}
|
||||
|
||||
uninstallScript = uninstallScriptForApp(&cask)
|
||||
default:
|
||||
if len(input.PreUninstallScripts) != 0 {
|
||||
return nil, ctxerr.New(ctx, "cannot provide pre-uninstall scripts if uninstall script is provided")
|
||||
}
|
||||
|
||||
if len(input.PostUninstallScripts) != 0 {
|
||||
return nil, ctxerr.New(ctx, "cannot provide post-uninstall scripts if uninstall script is provided")
|
||||
}
|
||||
|
||||
scriptBytes, err := os.ReadFile(input.UninstallScriptPath)
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "reading provided uninstall script file")
|
||||
}
|
||||
|
||||
uninstallScript = string(scriptBytes)
|
||||
}
|
||||
|
||||
out.UninstallScript = uninstallScriptForApp(&cask)
|
||||
installScript, err := installScriptForApp(app, &cask)
|
||||
if err != nil {
|
||||
return nil, ctxerr.WrapWithData(ctx, err, "generating install script for maintained app", map[string]any{"unique_identifier": app.UniqueIdentifier})
|
||||
}
|
||||
out.InstallScript = installScript
|
||||
out.UninstallScript = uninstallScript
|
||||
|
||||
out.UninstallScriptRef = maintained_apps.GetScriptRef(out.UninstallScript)
|
||||
out.InstallScriptRef = maintained_apps.GetScriptRef(out.InstallScript)
|
||||
out.Frozen = app.Frozen
|
||||
out.Frozen = input.Frozen
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
|
@ -186,6 +224,8 @@ type inputApp struct {
|
|||
PostUninstallScripts []string `json:"post_uninstall_scripts"`
|
||||
DefaultCategories []string `json:"default_categories"`
|
||||
Frozen bool `json:"frozen"`
|
||||
InstallScriptPath string `json:"install_script_path"`
|
||||
UninstallScriptPath string `json:"uninstall_script_path"`
|
||||
}
|
||||
|
||||
type brewCask struct {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import (
|
|||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"testing"
|
||||
|
|
@ -16,6 +17,14 @@ import (
|
|||
)
|
||||
|
||||
func TestIngestValidations(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
|
||||
testInstallScriptContents := "this is a test install script"
|
||||
require.NoError(t, os.WriteFile(path.Join(tempDir, "install_script.sh"), []byte(testInstallScriptContents), 0644))
|
||||
|
||||
testUninstallScriptContents := "this is a test uninstall script"
|
||||
require.NoError(t, os.WriteFile(path.Join(tempDir, "uninstall_script.sh"), []byte(testUninstallScriptContents), 0644))
|
||||
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var cask brewCask
|
||||
|
||||
|
|
@ -77,7 +86,7 @@ func TestIngestValidations(t *testing.T) {
|
|||
Version: "1.0",
|
||||
}
|
||||
|
||||
case "ok":
|
||||
case "ok", "install_script_path", "uninstall_script_path", "uninstall_script_path_with_pre", "uninstall_script_path_with_post":
|
||||
cask = brewCask{
|
||||
Token: appToken,
|
||||
Name: []string{appToken},
|
||||
|
|
@ -98,36 +107,47 @@ func TestIngestValidations(t *testing.T) {
|
|||
ctx := context.Background()
|
||||
|
||||
cases := []struct {
|
||||
appToken string
|
||||
wantErr string
|
||||
upsertCalled bool
|
||||
wantErr string
|
||||
inputApp inputApp
|
||||
}{
|
||||
{"fail", "brew API returned status 500", false},
|
||||
{"notfound", "app not found in brew API", false},
|
||||
{"noname", "missing name for cask noname", false},
|
||||
{"emptyname", "missing name for cask emptyname", false},
|
||||
{"notoken", "missing token for cask notoken", false},
|
||||
{"noversion", "missing version for cask noversion", false},
|
||||
{"nourl", "missing URL for cask nourl", false},
|
||||
{"invalidurl", "parse URL for cask invalidurl", false},
|
||||
{"ok", "", true},
|
||||
{"brew API returned status 500", inputApp{Token: "fail", UniqueIdentifier: "abc", InstallerFormat: "pkg"}},
|
||||
{"app not found in brew API", inputApp{Token: "notfound", UniqueIdentifier: "abc", InstallerFormat: "pkg"}},
|
||||
{"missing name for cask noname", inputApp{Token: "noname", UniqueIdentifier: "abc", InstallerFormat: "pkg"}},
|
||||
{"missing name for cask emptyname", inputApp{Token: "emptyname", UniqueIdentifier: "abc", InstallerFormat: "pkg"}},
|
||||
{"missing token for cask notoken", inputApp{Token: "notoken", UniqueIdentifier: "abc", InstallerFormat: "pkg"}},
|
||||
{"missing version for cask noversion", inputApp{Token: "noversion", UniqueIdentifier: "abc", InstallerFormat: "pkg"}},
|
||||
{"missing URL for cask nourl", inputApp{Token: "nourl", UniqueIdentifier: "abc", InstallerFormat: "pkg"}},
|
||||
{"parse URL for cask invalidurl", inputApp{Token: "invalidurl", UniqueIdentifier: "abc", InstallerFormat: "pkg"}},
|
||||
{"", inputApp{Token: "ok", UniqueIdentifier: "abc", InstallerFormat: "pkg"}},
|
||||
{"", inputApp{Token: "install_script_path", UniqueIdentifier: "abc", InstallerFormat: "pkg", InstallScriptPath: path.Join(tempDir, "install_script.sh")}},
|
||||
{"", inputApp{Token: "uninstall_script_path", UniqueIdentifier: "abc", InstallerFormat: "pkg", UninstallScriptPath: path.Join(tempDir, "uninstall_script.sh")}},
|
||||
{"cannot provide pre-uninstall scripts if uninstall script is provided", inputApp{Token: "uninstall_script_path_with_pre", UniqueIdentifier: "abc", InstallerFormat: "pkg", UninstallScriptPath: path.Join(tempDir, "uninstall_script.sh"), PreUninstallScripts: []string{"foo", "bar"}}},
|
||||
{"cannot provide post-uninstall scripts if uninstall script is provided", inputApp{Token: "uninstall_script_path_with_post", UniqueIdentifier: "abc", InstallerFormat: "pkg", UninstallScriptPath: path.Join(tempDir, "uninstall_script.sh"), PostUninstallScripts: []string{"foo", "bar"}}},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.appToken, func(t *testing.T) {
|
||||
t.Run(c.inputApp.Token, func(t *testing.T) {
|
||||
i := &brewIngester{
|
||||
logger: log.NewNopLogger(),
|
||||
client: fleethttp.NewClient(fleethttp.WithTimeout(10 * time.Second)),
|
||||
baseURL: srv.URL + "/",
|
||||
}
|
||||
|
||||
inputApp := inputApp{Token: c.appToken, UniqueIdentifier: "abc", InstallerFormat: "pkg"}
|
||||
|
||||
_, err := i.ingestOne(ctx, inputApp)
|
||||
if c.wantErr == "" {
|
||||
require.NoError(t, err)
|
||||
} else {
|
||||
out, err := i.ingestOne(ctx, c.inputApp)
|
||||
if c.wantErr != "" {
|
||||
require.ErrorContains(t, err, c.wantErr)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
|
||||
if c.inputApp.InstallScriptPath != "" {
|
||||
require.Equal(t, testInstallScriptContents, out.InstallScript)
|
||||
}
|
||||
|
||||
if c.inputApp.UninstallScriptPath != "" {
|
||||
require.Equal(t, testUninstallScriptContents, out.UninstallScript)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue