Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changes/41470-python-script-only-packages
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Added support for Python (`.py`) script-only software packages, which can be uploaded as custom packages (the file contents become the install script) and installed on macOS and Linux hosts, via the UI, REST API, and GitOps.
56 changes: 54 additions & 2 deletions cmd/fleetctl/fleetctl/generate_gitops_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1812,10 +1812,10 @@ func TestGenerateSoftwareScriptPackages(t *testing.T) {

packages, ok := software["packages"].([]interface{})
require.True(t, ok, "packages should be an array")
require.Len(t, packages, 3, "should have 3 packages: 1 regular + 2 scripts (.sh and .ps1)")
require.Len(t, packages, 4, "should have 4 packages: 1 regular + 3 scripts (.sh, .ps1, and .py)")

// Identify by URL since hash_sha256 includes comment tokens
var shScriptPkg, ps1ScriptPkg, regularPkg map[string]interface{}
var shScriptPkg, ps1ScriptPkg, pyScriptPkg, regularPkg map[string]any
for _, pkg := range packages {
p := pkg.(map[string]interface{})
url, ok := p["url"].(string)
Expand All @@ -1827,13 +1827,16 @@ func TestGenerateSoftwareScriptPackages(t *testing.T) {
shScriptPkg = p
case "https://example.com/download/setup.ps1":
ps1ScriptPkg = p
case "https://example.com/download/install.py":
pyScriptPkg = p
case "https://example.com/download/regular-package.deb":
regularPkg = p
}
}

require.NotNil(t, shScriptPkg, ".sh script package should exist")
require.NotNil(t, ps1ScriptPkg, ".ps1 script package should exist")
require.NotNil(t, pyScriptPkg, ".py script package should exist")
require.NotNil(t, regularPkg, "regular package should exist")

_, hasInstallScript := shScriptPkg["install_script"]
Expand All @@ -1860,10 +1863,24 @@ func TestGenerateSoftwareScriptPackages(t *testing.T) {
_, hasPreInstallQuery = ps1ScriptPkg["pre_install_query"]
require.False(t, hasPreInstallQuery, ".ps1 script package should NOT have pre_install_query in YAML output")

_, hasInstallScript = pyScriptPkg["install_script"]
require.False(t, hasInstallScript, ".py script package should NOT have install_script in YAML output")

_, hasPostInstallScript = pyScriptPkg["post_install_script"]
require.False(t, hasPostInstallScript, ".py script package should NOT have post_install_script in YAML output")

_, hasUninstallScript = pyScriptPkg["uninstall_script"]
require.False(t, hasUninstallScript, ".py script package should NOT have uninstall_script in YAML output")

_, hasPreInstallQuery = pyScriptPkg["pre_install_query"]
require.False(t, hasPreInstallQuery, ".py script package should NOT have pre_install_query in YAML output")

require.Contains(t, shScriptPkg, "url", ".sh script package should have url")
require.Contains(t, shScriptPkg, "hash_sha256", ".sh script package should have hash_sha256")
require.Contains(t, ps1ScriptPkg, "url", ".ps1 script package should have url")
require.Contains(t, ps1ScriptPkg, "hash_sha256", ".ps1 script package should have hash_sha256")
require.Contains(t, pyScriptPkg, "url", ".py script package should have url")
require.Contains(t, pyScriptPkg, "hash_sha256", ".py script package should have hash_sha256")

require.Contains(t, regularPkg, "install_script", "regular package should have install_script")
require.Contains(t, regularPkg, "post_install_script", "regular package should have post_install_script")
Expand All @@ -1881,6 +1898,7 @@ func TestGenerateSoftwareScriptPackages(t *testing.T) {
}
require.NotContains(t, commentFor("my-script.sh"), "version", ".sh script package comment should not mention version")
require.NotContains(t, commentFor("setup.ps1"), "version", ".ps1 script package comment should not mention version")
require.NotContains(t, commentFor("install.py"), "version", ".py script package comment should not mention version")
require.Contains(t, commentFor("regular-package.deb"), "version", "regular package comment should still mention version")

for filename := range cmd.FilesToWrite {
Expand All @@ -1893,6 +1911,11 @@ func TestGenerateSoftwareScriptPackages(t *testing.T) {
require.NotContains(t, filename, "powershell-script-windows-postinstall", "should not write post-install script file for .ps1 script package")
require.NotContains(t, filename, "powershell-script-windows-uninstall", "should not write uninstall script file for .ps1 script package")
require.NotContains(t, filename, "powershell-script-windows-preinstallquery", "should not write pre-install query file for .ps1 script package")

require.NotContains(t, filename, "python-script-linux-install", "should not write install script file for .py script package")
require.NotContains(t, filename, "python-script-linux-postinstall", "should not write post-install script file for .py script package")
require.NotContains(t, filename, "python-script-linux-uninstall", "should not write uninstall script file for .py script package")
require.NotContains(t, filename, "python-script-linux-preinstallquery", "should not write pre-install query file for .py script package")
}
}

Expand Down Expand Up @@ -1934,6 +1957,16 @@ func (c *MockClientWithScriptPackage) ListSoftwareTitles(query string) ([]fleet.
Version: "1.5",
},
},
{
ID: 6,
Name: "Python Script",
HashSHA256: new("py-script-hash"),
SoftwarePackage: &fleet.SoftwarePackageOrApp{
Name: "install.py",
Platform: "linux",
Version: "1.2",
},
},
}, nil
default:
return c.MockClient.ListSoftwareTitles(query)
Expand Down Expand Up @@ -1997,6 +2030,25 @@ func (c *MockClientWithScriptPackage) GetSoftwareTitleByID(id uint, teamID *uint
Name: "setup.ps1",
},
}, nil
case 6:
if *teamID != 2 {
return nil, errors.New("team ID mismatch")
}
// InstallScript is populated internally from file contents, but these fields
// should NOT be output in GitOps YAML
return &fleet.SoftwareTitle{
ID: 6,
SoftwarePackage: &fleet.SoftwareInstaller{
InstallScript: "#!/usr/bin/env python3\nprint('This is the Python script content')",
PostInstallScript: "",
UninstallScript: "",
PreInstallQuery: "",
SelfService: true,
Platform: "linux",
URL: "https://example.com/download/install.py",
Name: "install.py",
},
}, nil
default:
return c.MockClient.GetSoftwareTitleByID(id, teamID)
}
Expand Down
4 changes: 2 additions & 2 deletions cmd/fleetctl/integrationtest/gitops/software_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ func TestGitOpsTeamSoftwareInstallers(t *testing.T) {
}{
{"testdata/gitops/team_software_installer_not_found.yml", "Please make sure that URLs are reachable from your Fleet server."},
{"testdata/gitops/team_software_installer_install_script_secret.yml", "environment variable \"FLEET_SECRET_NAME\" not set"},
{"testdata/gitops/team_software_installer_unsupported.yml", "The file should be .pkg, .msi, .exe, .zip, .deb, .rpm, .tar.gz, .sh, .ipa or .ps1."},
{"testdata/gitops/team_software_installer_unsupported.yml", "The file should be .pkg, .msi, .exe, .zip, .deb, .rpm, .tar.gz, .sh, .py, .ipa or .ps1."},
{"testdata/gitops/team_software_installer_too_large.yml", "The maximum file size is 513MiB"},
{"testdata/gitops/team_software_installer_valid.yml", ""},
{"testdata/gitops/team_software_installer_subdir.yml", ""},
Expand Down Expand Up @@ -427,7 +427,7 @@ func TestGitOpsNoTeamSoftwareInstallers(t *testing.T) {
wantErr string
}{
{"testdata/gitops/no_team_software_installer_not_found.yml", "Please make sure that URLs are reachable from your Fleet server."},
{"testdata/gitops/no_team_software_installer_unsupported.yml", "The file should be .pkg, .msi, .exe, .zip, .deb, .rpm, .tar.gz, .sh, .ipa or .ps1."},
{"testdata/gitops/no_team_software_installer_unsupported.yml", "The file should be .pkg, .msi, .exe, .zip, .deb, .rpm, .tar.gz, .sh, .py, .ipa or .ps1."},
{"testdata/gitops/no_team_software_installer_too_large.yml", "The maximum file size is 513MiB"},
{"testdata/gitops/no_team_software_installer_valid.yml", ""},
{"testdata/gitops/no_team_software_installer_subdir.yml", ""},
Expand Down
12 changes: 7 additions & 5 deletions ee/server/service/software_installers.go
Original file line number Diff line number Diff line change
Expand Up @@ -1799,8 +1799,8 @@ func (svc *Service) installSoftwareTitleUsingInstaller(ctx context.Context, host
}

if host.FleetPlatform() != requiredPlatform {
// Allow .sh scripts for any unix-like platform (linux and darwin)
if !(ext == ".sh" && fleet.IsUnixLike(host.Platform)) {
// Allow .sh and .py scripts for any unix-like platform (linux and darwin)
if !((ext == ".sh" || ext == ".py") && fleet.IsUnixLike(host.Platform)) {
return &fleet.BadRequestError{
Message: fmt.Sprintf("Package (%s) can be installed only on %s hosts.", ext, requiredPlatform),
InternalErr: ctxerr.NewWithData(
Comment thread
cdcme marked this conversation as resolved.
Expand Down Expand Up @@ -2097,7 +2097,7 @@ func (svc *Service) addMetadataToSoftwarePayload(ctx context.Context, payload *f
if err != nil {
if errors.Is(err, file.ErrUnsupportedType) {
return "", &fleet.BadRequestError{
Message: "Couldn't edit software. File type not supported. The file should be .pkg, .msi, .exe, .zip, .deb, .rpm, .tar.gz, .sh, .ipa or .ps1.",
Message: "Couldn't edit software. File type not supported. The file should be .pkg, .msi, .exe, .zip, .deb, .rpm, .tar.gz, .sh, .py, .ipa or .ps1.",
InternalErr: ctxerr.Wrap(ctx, err, "extracting metadata from installer"),
}
}
Expand Down Expand Up @@ -2271,6 +2271,8 @@ func (svc *Service) addScriptPackageMetadata(ctx context.Context, payload *fleet
payload.Source = "sh_packages"
case "ps1":
payload.Source = "ps1_packages"
case "py":
payload.Source = "py_packages"
}

platform, err := fleet.SoftwareInstallerPlatformFromExtension(extension)
Expand Down Expand Up @@ -2956,7 +2958,7 @@ func (svc *Service) softwareBatchUpload(
ext = strings.TrimPrefix(ext, ".")

if !fleet.IsScriptPackage(ext) {
return fmt.Errorf("script:// URL must reference a .sh or .ps1 file, got: %s", filename)
return fmt.Errorf("script:// URL must reference a .sh, .py, or .ps1 file, got: %s", filename)
}

if p.InstallScript == "" {
Expand Down Expand Up @@ -3728,7 +3730,7 @@ func packageExtensionToPlatform(ext string) string {
requiredPlatform = "windows"
case ".pkg", ".dmg":
requiredPlatform = "darwin"
case ".deb", ".rpm", ".gz", ".tgz", ".sh":
case ".deb", ".rpm", ".gz", ".tgz", ".sh", ".py":
requiredPlatform = "linux"
default:
return ""
Expand Down
Loading
Loading