Skip to content

Commit fb9966e

Browse files
authored
Python script only packages BE support (#48942)
**Related issue:** Resolves #48393 Adds `.py` as an accepted script-only software package on the server — mirroring `.sh`, assigned the new `py_packages` source and installable on macOS and Linux hosts. # Checklist for submitter - [x] Input data is properly validated, `SELECT *` is avoided, SQL injection is prevented (using placeholders for values in statements), JS inline code is prevented especially for url redirects, and untrusted data interpolated into shell scripts/commands is validated against shell metacharacters. ## Testing - [x] Added/updated automated tests - [x] QA'd all new/changed functionality manually
1 parent 646ebd0 commit fb9966e

8 files changed

Lines changed: 417 additions & 12 deletions

File tree

cmd/fleetctl/fleetctl/generate_gitops_test.go

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1812,10 +1812,10 @@ func TestGenerateSoftwareScriptPackages(t *testing.T) {
18121812

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

18171817
// Identify by URL since hash_sha256 includes comment tokens
1818-
var shScriptPkg, ps1ScriptPkg, regularPkg map[string]interface{}
1818+
var shScriptPkg, ps1ScriptPkg, pyScriptPkg, regularPkg map[string]any
18191819
for _, pkg := range packages {
18201820
p := pkg.(map[string]interface{})
18211821
url, ok := p["url"].(string)
@@ -1827,13 +1827,16 @@ func TestGenerateSoftwareScriptPackages(t *testing.T) {
18271827
shScriptPkg = p
18281828
case "https://example.com/download/setup.ps1":
18291829
ps1ScriptPkg = p
1830+
case "https://example.com/download/install.py":
1831+
pyScriptPkg = p
18301832
case "https://example.com/download/regular-package.deb":
18311833
regularPkg = p
18321834
}
18331835
}
18341836

18351837
require.NotNil(t, shScriptPkg, ".sh script package should exist")
18361838
require.NotNil(t, ps1ScriptPkg, ".ps1 script package should exist")
1839+
require.NotNil(t, pyScriptPkg, ".py script package should exist")
18371840
require.NotNil(t, regularPkg, "regular package should exist")
18381841

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

1866+
_, hasInstallScript = pyScriptPkg["install_script"]
1867+
require.False(t, hasInstallScript, ".py script package should NOT have install_script in YAML output")
1868+
1869+
_, hasPostInstallScript = pyScriptPkg["post_install_script"]
1870+
require.False(t, hasPostInstallScript, ".py script package should NOT have post_install_script in YAML output")
1871+
1872+
_, hasUninstallScript = pyScriptPkg["uninstall_script"]
1873+
require.False(t, hasUninstallScript, ".py script package should NOT have uninstall_script in YAML output")
1874+
1875+
_, hasPreInstallQuery = pyScriptPkg["pre_install_query"]
1876+
require.False(t, hasPreInstallQuery, ".py script package should NOT have pre_install_query in YAML output")
1877+
18631878
require.Contains(t, shScriptPkg, "url", ".sh script package should have url")
18641879
require.Contains(t, shScriptPkg, "hash_sha256", ".sh script package should have hash_sha256")
18651880
require.Contains(t, ps1ScriptPkg, "url", ".ps1 script package should have url")
18661881
require.Contains(t, ps1ScriptPkg, "hash_sha256", ".ps1 script package should have hash_sha256")
1882+
require.Contains(t, pyScriptPkg, "url", ".py script package should have url")
1883+
require.Contains(t, pyScriptPkg, "hash_sha256", ".py script package should have hash_sha256")
18671884

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

18861904
for filename := range cmd.FilesToWrite {
@@ -1893,6 +1911,11 @@ func TestGenerateSoftwareScriptPackages(t *testing.T) {
18931911
require.NotContains(t, filename, "powershell-script-windows-postinstall", "should not write post-install script file for .ps1 script package")
18941912
require.NotContains(t, filename, "powershell-script-windows-uninstall", "should not write uninstall script file for .ps1 script package")
18951913
require.NotContains(t, filename, "powershell-script-windows-preinstallquery", "should not write pre-install query file for .ps1 script package")
1914+
1915+
require.NotContains(t, filename, "python-script-linux-install", "should not write install script file for .py script package")
1916+
require.NotContains(t, filename, "python-script-linux-postinstall", "should not write post-install script file for .py script package")
1917+
require.NotContains(t, filename, "python-script-linux-uninstall", "should not write uninstall script file for .py script package")
1918+
require.NotContains(t, filename, "python-script-linux-preinstallquery", "should not write pre-install query file for .py script package")
18961919
}
18971920
}
18981921

@@ -1934,6 +1957,16 @@ func (c *MockClientWithScriptPackage) ListSoftwareTitles(query string) ([]fleet.
19341957
Version: "1.5",
19351958
},
19361959
},
1960+
{
1961+
ID: 6,
1962+
Name: "Python Script",
1963+
HashSHA256: new("py-script-hash"),
1964+
SoftwarePackage: &fleet.SoftwarePackageOrApp{
1965+
Name: "install.py",
1966+
Platform: "linux",
1967+
Version: "1.2",
1968+
},
1969+
},
19371970
}, nil
19381971
default:
19391972
return c.MockClient.ListSoftwareTitles(query)
@@ -1997,6 +2030,25 @@ func (c *MockClientWithScriptPackage) GetSoftwareTitleByID(id uint, teamID *uint
19972030
Name: "setup.ps1",
19982031
},
19992032
}, nil
2033+
case 6:
2034+
if *teamID != 2 {
2035+
return nil, errors.New("team ID mismatch")
2036+
}
2037+
// InstallScript is populated internally from file contents, but these fields
2038+
// should NOT be output in GitOps YAML
2039+
return &fleet.SoftwareTitle{
2040+
ID: 6,
2041+
SoftwarePackage: &fleet.SoftwareInstaller{
2042+
InstallScript: "#!/usr/bin/env python3\nprint('This is the Python script content')",
2043+
PostInstallScript: "",
2044+
UninstallScript: "",
2045+
PreInstallQuery: "",
2046+
SelfService: true,
2047+
Platform: "linux",
2048+
URL: "https://example.com/download/install.py",
2049+
Name: "install.py",
2050+
},
2051+
}, nil
20002052
default:
20012053
return c.MockClient.GetSoftwareTitleByID(id, teamID)
20022054
}

cmd/fleetctl/integrationtest/gitops/software_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ func TestGitOpsTeamSoftwareInstallers(t *testing.T) {
3535
}{
3636
{"testdata/gitops/team_software_installer_not_found.yml", "Please make sure that URLs are reachable from your Fleet server."},
3737
{"testdata/gitops/team_software_installer_install_script_secret.yml", "environment variable \"FLEET_SECRET_NAME\" not set"},
38-
{"testdata/gitops/team_software_installer_unsupported.yml", "The file should be .pkg, .msi, .exe, .zip, .deb, .rpm, .tar.gz, .sh, .ipa or .ps1."},
38+
{"testdata/gitops/team_software_installer_unsupported.yml", "The file should be .pkg, .msi, .exe, .zip, .deb, .rpm, .tar.gz, .sh, .py, .ipa or .ps1."},
3939
{"testdata/gitops/team_software_installer_too_large.yml", "The maximum file size is 513MiB"},
4040
{"testdata/gitops/team_software_installer_valid.yml", ""},
4141
{"testdata/gitops/team_software_installer_subdir.yml", ""},
@@ -427,7 +427,7 @@ func TestGitOpsNoTeamSoftwareInstallers(t *testing.T) {
427427
wantErr string
428428
}{
429429
{"testdata/gitops/no_team_software_installer_not_found.yml", "Please make sure that URLs are reachable from your Fleet server."},
430-
{"testdata/gitops/no_team_software_installer_unsupported.yml", "The file should be .pkg, .msi, .exe, .zip, .deb, .rpm, .tar.gz, .sh, .ipa or .ps1."},
430+
{"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."},
431431
{"testdata/gitops/no_team_software_installer_too_large.yml", "The maximum file size is 513MiB"},
432432
{"testdata/gitops/no_team_software_installer_valid.yml", ""},
433433
{"testdata/gitops/no_team_software_installer_subdir.yml", ""},

ee/server/service/software_installers.go

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1799,8 +1799,8 @@ func (svc *Service) installSoftwareTitleUsingInstaller(ctx context.Context, host
17991799
}
18001800

18011801
if host.FleetPlatform() != requiredPlatform {
1802-
// Allow .sh scripts for any unix-like platform (linux and darwin)
1803-
if !(ext == ".sh" && fleet.IsUnixLike(host.Platform)) {
1802+
// Allow .sh and .py scripts for any unix-like platform (linux and darwin)
1803+
if !((ext == ".sh" || ext == ".py") && fleet.IsUnixLike(host.Platform)) {
18041804
return &fleet.BadRequestError{
18051805
Message: fmt.Sprintf("Package (%s) can be installed only on %s hosts.", ext, requiredPlatform),
18061806
InternalErr: ctxerr.NewWithData(
@@ -2097,7 +2097,7 @@ func (svc *Service) addMetadataToSoftwarePayload(ctx context.Context, payload *f
20972097
if err != nil {
20982098
if errors.Is(err, file.ErrUnsupportedType) {
20992099
return "", &fleet.BadRequestError{
2100-
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.",
2100+
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.",
21012101
InternalErr: ctxerr.Wrap(ctx, err, "extracting metadata from installer"),
21022102
}
21032103
}
@@ -2271,6 +2271,8 @@ func (svc *Service) addScriptPackageMetadata(ctx context.Context, payload *fleet
22712271
payload.Source = "sh_packages"
22722272
case "ps1":
22732273
payload.Source = "ps1_packages"
2274+
case "py":
2275+
payload.Source = "py_packages"
22742276
}
22752277

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

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

29622964
if p.InstallScript == "" {
@@ -3728,7 +3730,7 @@ func packageExtensionToPlatform(ext string) string {
37283730
requiredPlatform = "windows"
37293731
case ".pkg", ".dmg":
37303732
requiredPlatform = "darwin"
3731-
case ".deb", ".rpm", ".gz", ".tgz", ".sh":
3733+
case ".deb", ".rpm", ".gz", ".tgz", ".sh", ".py":
37323734
requiredPlatform = "linux"
37333735
default:
37343736
return ""

0 commit comments

Comments
 (0)