mirror of
https://github.com/fleetdm/fleet
synced 2026-04-21 13:37:30 +00:00
Add support to upload RPM packages (#22502)
#22473 - [X] 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/Committing-Changes.md#changes-files) for more information. - [X] Added/updated tests - [X] Manual QA for all new/changed functionality - For Orbit and Fleet Desktop changes: - [x] Manual QA must be performed in the three main OSs, macOS, Windows and Linux. --------- Co-authored-by: RachelElysia <71795832+RachelElysia@users.noreply.github.com> Co-authored-by: Ian Littman <iansltx@gmail.com>
This commit is contained in:
parent
f7fc22d766
commit
f8f24e0a80
33 changed files with 328 additions and 31 deletions
1
changes/20537-add-rpm-support
Normal file
1
changes/20537-add-rpm-support
Normal file
|
|
@ -0,0 +1 @@
|
|||
* Added support for uploading RPM packages.
|
||||
|
|
@ -1727,7 +1727,7 @@ func TestGitOpsTeamSofwareInstallers(t *testing.T) {
|
|||
wantErr string
|
||||
}{
|
||||
{"testdata/gitops/team_software_installer_not_found.yml", "Please make sure that URLs are reachable from your Fleet server."},
|
||||
{"testdata/gitops/team_software_installer_unsupported.yml", "The file should be .pkg, .msi, .exe or .deb."},
|
||||
{"testdata/gitops/team_software_installer_unsupported.yml", "The file should be .pkg, .msi, .exe, .deb or .rpm."},
|
||||
{"testdata/gitops/team_software_installer_too_large.yml", "The maximum file size is 500 MiB"},
|
||||
{"testdata/gitops/team_software_installer_valid.yml", ""},
|
||||
{"testdata/gitops/team_software_installer_valid_apply.yml", ""},
|
||||
|
|
@ -1782,7 +1782,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 or .deb."},
|
||||
{"testdata/gitops/no_team_software_installer_unsupported.yml", "The file should be .pkg, .msi, .exe, .deb or .rpm."},
|
||||
{"testdata/gitops/no_team_software_installer_too_large.yml", "The maximum file size is 500 MiB"},
|
||||
{"testdata/gitops/no_team_software_installer_valid.yml", ""},
|
||||
{"testdata/gitops/no_team_software_installer_pre_condition_multiple_queries.yml", "should have only one query."},
|
||||
|
|
|
|||
|
|
@ -1054,7 +1054,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 or .deb.",
|
||||
Message: "Couldn't edit software. File type not supported. The file should be .pkg, .msi, .exe, .deb or .rpm.",
|
||||
InternalErr: ctxerr.Wrap(ctx, err, "extracting metadata from installer"),
|
||||
}
|
||||
}
|
||||
|
|
@ -1517,7 +1517,7 @@ func packageExtensionToPlatform(ext string) string {
|
|||
requiredPlatform = "windows"
|
||||
case ".pkg":
|
||||
requiredPlatform = "darwin"
|
||||
case ".deb":
|
||||
case ".deb", ".rpm":
|
||||
requiredPlatform = "linux"
|
||||
default:
|
||||
return ""
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
const unixPackageTypes = ["pkg", "deb"] as const;
|
||||
const unixPackageTypes = ["pkg", "deb", "rpm"] as const;
|
||||
const windowsPackageTypes = ["msi", "exe"] as const;
|
||||
export const packageTypes = [
|
||||
...unixPackageTypes,
|
||||
|
|
|
|||
|
|
@ -270,7 +270,7 @@ export interface ISoftwareInstallResults {
|
|||
|
||||
// ISoftwareInstallerType defines the supported installer types for
|
||||
// software uploaded by the IT admin.
|
||||
export type ISoftwareInstallerType = "pkg" | "msi" | "deb" | "exe";
|
||||
export type ISoftwareInstallerType = "pkg" | "msi" | "deb" | "rpm" | "exe";
|
||||
|
||||
export interface ISoftwareLastInstall {
|
||||
install_uuid: string;
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ const getSupportedScriptTypeText = (pkgType: PackageType) => {
|
|||
const PKG_TYPE_TO_ID_TEXT = {
|
||||
pkg: "package IDs",
|
||||
deb: "package name",
|
||||
rpm: "package name",
|
||||
msi: "product code",
|
||||
exe: "software name",
|
||||
} as const;
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ interface IPackageFormProps {
|
|||
defaultSelfService?: boolean;
|
||||
}
|
||||
|
||||
const ACCEPTED_EXTENSIONS = ".pkg,.msi,.exe,.deb";
|
||||
const ACCEPTED_EXTENSIONS = ".pkg,.msi,.exe,.deb,.rpm";
|
||||
|
||||
const PackageForm = ({
|
||||
isUploading,
|
||||
|
|
@ -173,7 +173,7 @@ const PackageForm = ({
|
|||
canEdit={isEditingSoftware}
|
||||
graphicName={"file-pkg"}
|
||||
accept={ACCEPTED_EXTENSIONS}
|
||||
message=".pkg, .msi, .exe, or .deb"
|
||||
message=".pkg, .msi, .exe, .deb, or .rpm"
|
||||
onFileUpload={onFileSelect}
|
||||
buttonMessage="Choose file"
|
||||
buttonType="link"
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ const getPlatformDisplayFromPackageSuffix = (packageName: string) => {
|
|||
case "pkg":
|
||||
return "macOS";
|
||||
case "deb":
|
||||
case "rpm":
|
||||
return "Linux";
|
||||
case "exe":
|
||||
return "Windows";
|
||||
|
|
|
|||
|
|
@ -2,15 +2,22 @@ import { getPlatformDisplayName } from "./fileUtils";
|
|||
|
||||
describe("fileUtils", () => {
|
||||
describe("getPlatformDisplayName", () => {
|
||||
it("should return the correct platform display name depending on the file extension", () => {
|
||||
const file = new File([""], "test.pkg");
|
||||
expect(getPlatformDisplayName(file)).toEqual("macOS");
|
||||
const testCases = [
|
||||
{ extension: "pkg", platform: "macOS" },
|
||||
{ extension: "json", platform: "macOS" },
|
||||
{ extension: "mobileconfig", platform: "macOS" },
|
||||
{ extension: "exe", platform: "Windows" },
|
||||
{ extension: "msi", platform: "Windows" },
|
||||
{ extension: "xml", platform: "Windows" },
|
||||
{ extension: "deb", platform: "Linux" },
|
||||
{ extension: "rpm", platform: "Linux" },
|
||||
];
|
||||
|
||||
const file2 = new File([""], "test.exe");
|
||||
expect(getPlatformDisplayName(file2)).toEqual("Windows");
|
||||
|
||||
const file3 = new File([""], "test.deb");
|
||||
expect(getPlatformDisplayName(file3)).toEqual("linux");
|
||||
testCases.forEach(({ extension, platform }) => {
|
||||
it(`should return ${platform} for .${extension} files`, () => {
|
||||
const file = new File([""], `test.${extension}`);
|
||||
expect(getPlatformDisplayName(file)).toEqual(platform);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
type IPlatformDisplayName = "macOS" | "Windows" | "linux";
|
||||
type IPlatformDisplayName = "macOS" | "Windows" | "Linux";
|
||||
|
||||
const getFileExtension = (file: File) => {
|
||||
const nameParts = file.name.split(".");
|
||||
|
|
@ -15,7 +15,8 @@ export const FILE_EXTENSIONS_TO_PLATFORM_DISPLAY_NAME: Record<
|
|||
exe: "Windows",
|
||||
msi: "Windows",
|
||||
xml: "Windows",
|
||||
deb: "linux",
|
||||
deb: "Linux",
|
||||
rpm: "Linux",
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ import installMsi from "../../pkg/file/scripts/install_msi.ps1";
|
|||
import installExe from "../../pkg/file/scripts/install_exe.ps1";
|
||||
// @ts-ignore
|
||||
import installDeb from "../../pkg/file/scripts/install_deb.sh";
|
||||
// @ts-ignore
|
||||
import installRPM from "../../pkg/file/scripts/install_rpm.sh";
|
||||
|
||||
/*
|
||||
* getInstallScript returns a string with a script to install the
|
||||
|
|
@ -20,6 +22,8 @@ const getDefaultInstallScript = (fileName: string): string => {
|
|||
return installMsi;
|
||||
case "deb":
|
||||
return installDeb;
|
||||
case "rpm":
|
||||
return installRPM;
|
||||
case "exe":
|
||||
return installExe;
|
||||
default:
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ import uninstallMsi from "../../pkg/file/scripts/uninstall_msi.ps1";
|
|||
import uninstallExe from "../../pkg/file/scripts/uninstall_exe.ps1";
|
||||
// @ts-ignore
|
||||
import uninstallDeb from "../../pkg/file/scripts/uninstall_deb.sh";
|
||||
// @ts-ignore
|
||||
import uninstallRPM from "../../pkg/file/scripts/uninstall_rpm.sh";
|
||||
|
||||
/*
|
||||
* getUninstallScript returns a string with a script to uninstall the
|
||||
|
|
@ -20,6 +22,8 @@ const getDefaultUninstallScript = (fileName: string): string => {
|
|||
return uninstallMsi;
|
||||
case "deb":
|
||||
return uninstallDeb;
|
||||
case "rpm":
|
||||
return uninstallRPM;
|
||||
case "exe":
|
||||
return uninstallExe;
|
||||
default:
|
||||
|
|
|
|||
2
go.mod
2
go.mod
|
|
@ -199,6 +199,8 @@ require (
|
|||
github.com/caarlos0/env/v6 v6.7.0 // indirect
|
||||
github.com/caarlos0/go-shellwords v1.0.12 // indirect
|
||||
github.com/cavaliercoder/go-cpio v0.0.0-20180626203310-925f9528c45e // indirect
|
||||
github.com/cavaliergopher/cpio v1.0.1 // indirect
|
||||
github.com/cavaliergopher/rpm v1.2.0 // indirect
|
||||
github.com/cespare/xxhash v1.1.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/cloudflare/circl v1.3.8 // indirect
|
||||
|
|
|
|||
4
go.sum
4
go.sum
|
|
@ -320,6 +320,10 @@ github.com/caarlos0/testfs v0.4.3 h1:q1zEM5hgsssqWanAfevJYYa0So60DdK6wlJeTc/yfUE
|
|||
github.com/caarlos0/testfs v0.4.3/go.mod h1:bRN55zgG4XCUVVHZCeU+/Tz1Q6AxEJOEJTliBy+1DMk=
|
||||
github.com/cavaliercoder/go-cpio v0.0.0-20180626203310-925f9528c45e h1:hHg27A0RSSp2Om9lubZpiMgVbvn39bsUmW9U5h0twqc=
|
||||
github.com/cavaliercoder/go-cpio v0.0.0-20180626203310-925f9528c45e/go.mod h1:oDpT4efm8tSYHXV5tHSdRvBet/b/QzxZ+XyyPehvm3A=
|
||||
github.com/cavaliergopher/cpio v1.0.1 h1:KQFSeKmZhv0cr+kawA3a0xTQCU4QxXF1vhU7P7av2KM=
|
||||
github.com/cavaliergopher/cpio v1.0.1/go.mod h1:pBdaqQjnvXxdS/6CvNDwIANIFSP0xRKI16PX4xejRQc=
|
||||
github.com/cavaliergopher/rpm v1.2.0 h1:s0h+QeVK252QFTolkhGiMeQ1f+tMeIMhGl8B1HUmGUc=
|
||||
github.com/cavaliergopher/rpm v1.2.0/go.mod h1:R0q3vTqa7RUvPofAZYrnjJ63hh2vngjFfphuXiExVos=
|
||||
github.com/cenkalti/backoff v2.1.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
|
||||
github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
|
||||
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
|
||||
|
|
|
|||
|
|
@ -22,8 +22,10 @@ import (
|
|||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type QueryResponse = osquery_gen.ExtensionResponse
|
||||
type QueryResponseStatus = osquery_gen.ExtensionStatus
|
||||
type (
|
||||
QueryResponse = osquery_gen.ExtensionResponse
|
||||
QueryResponseStatus = osquery_gen.ExtensionStatus
|
||||
)
|
||||
|
||||
// Client defines the methods required for the API requests to the server. The
|
||||
// fleet.OrbitClient type satisfies this interface.
|
||||
|
|
@ -202,7 +204,7 @@ func (r *Runner) installSoftware(ctx context.Context, installID string) (*fleet.
|
|||
return payload, fmt.Errorf("creating temporary directory: %w", err)
|
||||
}
|
||||
|
||||
log.Debug().Msgf("about to download software installer")
|
||||
log.Debug().Str("install_id", installID).Msgf("about to download software installer")
|
||||
installerPath, err := r.OrbitClient.DownloadSoftwareInstaller(installer.InstallerID, tmpDir)
|
||||
if err != nil {
|
||||
return payload, err
|
||||
|
|
@ -233,7 +235,7 @@ func (r *Runner) installSoftware(ctx context.Context, installID string) (*fleet.
|
|||
}
|
||||
|
||||
if installer.PostInstallScript != "" {
|
||||
log.Debug().Msgf("about to run post-install script")
|
||||
log.Debug().Msgf("about to run post-install script for %s", installerPath)
|
||||
postOutput, postExitCode, postErr := r.runInstallerScript(ctx, installer.PostInstallScript, installerPath, "post-install-script"+scriptExtension)
|
||||
payload.PostInstallScriptOutput = &postOutput
|
||||
payload.PostInstallScriptExitCode = &postExitCode
|
||||
|
|
|
|||
|
|
@ -42,6 +42,8 @@ func ExtractInstallerMetadata(r io.Reader) (*InstallerMetadata, error) {
|
|||
switch extension {
|
||||
case "deb":
|
||||
meta, err = ExtractDebMetadata(br)
|
||||
case "rpm":
|
||||
meta, err = ExtractRPMMetadata(br)
|
||||
case "exe":
|
||||
meta, err = ExtractPEMetadata(br)
|
||||
case "pkg":
|
||||
|
|
@ -59,12 +61,16 @@ func ExtractInstallerMetadata(r io.Reader) (*InstallerMetadata, error) {
|
|||
return meta, err
|
||||
}
|
||||
|
||||
// typeFromBytes deduces the type from the magic bytes.
|
||||
// See https://en.wikipedia.org/wiki/List_of_file_signatures.
|
||||
func typeFromBytes(br *bufio.Reader) (string, error) {
|
||||
switch {
|
||||
case hasPrefix(br, []byte{0x78, 0x61, 0x72, 0x21}):
|
||||
return "pkg", nil
|
||||
case hasPrefix(br, []byte("!<arch>\ndebian")):
|
||||
return "deb", nil
|
||||
case hasPrefix(br, []byte{0xed, 0xab, 0xee, 0xdb}):
|
||||
return "rpm", nil
|
||||
case hasPrefix(br, []byte{0xd0, 0xcf}):
|
||||
return "msi", nil
|
||||
case hasPrefix(br, []byte("MZ")):
|
||||
|
|
|
|||
|
|
@ -16,6 +16,9 @@ var installExeScript string
|
|||
//go:embed scripts/install_deb.sh
|
||||
var installDebScript string
|
||||
|
||||
//go:embed scripts/install_rpm.sh
|
||||
var installRPMScript string
|
||||
|
||||
// GetInstallScript returns a script that can be used to install the given extension
|
||||
func GetInstallScript(extension string) string {
|
||||
switch extension {
|
||||
|
|
@ -23,6 +26,8 @@ func GetInstallScript(extension string) string {
|
|||
return installMsiScript
|
||||
case "deb":
|
||||
return installDebScript
|
||||
case "rpm":
|
||||
return installRPMScript
|
||||
case "pkg":
|
||||
return installPkgScript
|
||||
case "exe":
|
||||
|
|
@ -44,6 +49,9 @@ var removeMsiScript string
|
|||
//go:embed scripts/remove_deb.sh
|
||||
var removeDebScript string
|
||||
|
||||
//go:embed scripts/remove_rpm.sh
|
||||
var removeRPMScript string
|
||||
|
||||
// GetRemoveScript returns a script that can be used to remove an
|
||||
// installer with the given extension.
|
||||
func GetRemoveScript(extension string) string {
|
||||
|
|
@ -52,6 +60,8 @@ func GetRemoveScript(extension string) string {
|
|||
return removeMsiScript
|
||||
case "deb":
|
||||
return removeDebScript
|
||||
case "rpm":
|
||||
return removeRPMScript
|
||||
case "pkg":
|
||||
return removePkgScript
|
||||
case "exe":
|
||||
|
|
@ -73,6 +83,9 @@ var uninstallMsiScript string
|
|||
//go:embed scripts/uninstall_deb.sh
|
||||
var uninstallDebScript string
|
||||
|
||||
//go:embed scripts/uninstall_rpm.sh
|
||||
var uninstallRPMScript string
|
||||
|
||||
// GetUninstallScript returns a script that can be used to uninstall a
|
||||
// software item with the given extension.
|
||||
func GetUninstallScript(extension string) string {
|
||||
|
|
@ -81,6 +94,8 @@ func GetUninstallScript(extension string) string {
|
|||
return uninstallMsiScript
|
||||
case "deb":
|
||||
return uninstallDebScript
|
||||
case "rpm":
|
||||
return uninstallRPMScript
|
||||
case "pkg":
|
||||
return uninstallPkgScript
|
||||
case "exe":
|
||||
|
|
|
|||
|
|
@ -11,9 +11,7 @@ import (
|
|||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var (
|
||||
update = flag.Bool("update", false, "update the golden files of this test")
|
||||
)
|
||||
var update = flag.Bool("update", false, "update the golden files of this test")
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
flag.Parse()
|
||||
|
|
@ -41,6 +39,11 @@ func TestGetInstallAndRemoveScript(t *testing.T) {
|
|||
"remove": "./scripts/remove_deb.sh",
|
||||
"uninstall": "./scripts/uninstall_deb.sh",
|
||||
},
|
||||
"rpm": {
|
||||
"install": "./scripts/install_rpm.sh",
|
||||
"remove": "./scripts/remove_rpm.sh",
|
||||
"uninstall": "./scripts/uninstall_rpm.sh",
|
||||
},
|
||||
"exe": {
|
||||
"install": "./scripts/install_exe.ps1",
|
||||
"remove": "./scripts/remove_exe.ps1",
|
||||
|
|
@ -64,7 +67,7 @@ func assertGoldenMatches(t *testing.T, goldenFile string, actual string, update
|
|||
t.Helper()
|
||||
goldenPath := filepath.Join("testdata", goldenFile+".golden")
|
||||
|
||||
f, err := os.OpenFile(goldenPath, os.O_RDWR|os.O_CREATE, 0644)
|
||||
f, err := os.OpenFile(goldenPath, os.O_RDWR|os.O_CREATE, 0o644)
|
||||
require.NoError(t, err)
|
||||
defer f.Close()
|
||||
|
||||
|
|
|
|||
33
pkg/file/rpm.go
Normal file
33
pkg/file/rpm.go
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
package file
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/cavaliergopher/rpm"
|
||||
)
|
||||
|
||||
func ExtractRPMMetadata(r io.Reader) (*InstallerMetadata, error) {
|
||||
h := sha256.New()
|
||||
r = io.TeeReader(r, h)
|
||||
|
||||
// Read the package headers
|
||||
pkg, err := rpm.Read(r)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read headers: %w", err)
|
||||
}
|
||||
// r is now positioned at the RPM payload.
|
||||
|
||||
// Ensure the whole file is read to get the correct hash
|
||||
if _, err := io.Copy(io.Discard, r); err != nil {
|
||||
return nil, fmt.Errorf("read all RPM content: %w", err)
|
||||
}
|
||||
|
||||
return &InstallerMetadata{
|
||||
Name: pkg.Name(),
|
||||
Version: pkg.Version(),
|
||||
SHASum: h.Sum(nil),
|
||||
PackageIDs: []string{pkg.Name()},
|
||||
}, nil
|
||||
}
|
||||
94
pkg/file/rpm_test.go
Normal file
94
pkg/file/rpm_test.go
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
package file
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/orbit/pkg/constant"
|
||||
"github.com/goreleaser/nfpm/v2"
|
||||
"github.com/goreleaser/nfpm/v2/files"
|
||||
"github.com/goreleaser/nfpm/v2/rpm"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestExtractRPMMetadata(t *testing.T) {
|
||||
//
|
||||
// Build an RPM package on the fly with nfpm.
|
||||
//
|
||||
tmpDir := t.TempDir()
|
||||
err := os.WriteFile(filepath.Join(tmpDir, "foo.sh"), []byte("#!/bin/sh\n\necho \"Foo!\"\n"), constant.DefaultFileMode)
|
||||
require.NoError(t, err)
|
||||
contents := files.Contents{
|
||||
&files.Content{
|
||||
Source: filepath.Join(tmpDir, "**"),
|
||||
Destination: "/",
|
||||
},
|
||||
}
|
||||
postInstallPath := filepath.Join(t.TempDir(), "postinstall.sh")
|
||||
err = os.WriteFile(postInstallPath, []byte("#!/bin/sh\n\necho \"Hello world!\"\n"), constant.DefaultFileMode)
|
||||
require.NoError(t, err)
|
||||
info := &nfpm.Info{
|
||||
Name: "foobar",
|
||||
Version: "1.2.3",
|
||||
Description: "Foo bar",
|
||||
Arch: "x86_64",
|
||||
Maintainer: "Fleet Device Management",
|
||||
Vendor: "Fleet Device Management",
|
||||
License: "LICENSE",
|
||||
Homepage: "https://example.com",
|
||||
Overridables: nfpm.Overridables{
|
||||
Contents: contents,
|
||||
Scripts: nfpm.Scripts{
|
||||
PostInstall: postInstallPath,
|
||||
},
|
||||
},
|
||||
}
|
||||
rpmPath := filepath.Join(t.TempDir(), "foobar.rpm")
|
||||
out, err := os.OpenFile(rpmPath, os.O_CREATE|os.O_RDWR, constant.DefaultFileMode)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
out.Close()
|
||||
})
|
||||
err = rpm.Default.Package(info, out)
|
||||
require.NoError(t, err)
|
||||
err = out.Close()
|
||||
require.NoError(t, err)
|
||||
|
||||
//
|
||||
// Test ExtractRPMMetadata with the generated package.
|
||||
// Using ExtractInstallerMetadata for broader testing (for a file
|
||||
// with rpm extension it will call ExtractRPMMetadata).
|
||||
//
|
||||
f, err := os.Open(rpmPath)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
f.Close()
|
||||
})
|
||||
m, err := ExtractInstallerMetadata(f)
|
||||
require.NoError(t, err)
|
||||
err = f.Close()
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, m.BundleIdentifier)
|
||||
require.Equal(t, "rpm", m.Extension)
|
||||
require.Equal(t, "foobar", m.Name)
|
||||
require.Equal(t, []string{"foobar"}, m.PackageIDs)
|
||||
require.Equal(t, sha256FilePath(t, rpmPath), m.SHASum)
|
||||
require.Equal(t, "1.2.3", m.Version)
|
||||
}
|
||||
|
||||
func sha256FilePath(t *testing.T, path string) []byte {
|
||||
f, err := os.Open(path)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
f.Close()
|
||||
})
|
||||
h := sha256.New()
|
||||
_, err = io.Copy(h, f)
|
||||
require.NoError(t, err)
|
||||
err = f.Close()
|
||||
require.NoError(t, err)
|
||||
return h.Sum(nil)
|
||||
}
|
||||
|
|
@ -3,10 +3,15 @@
|
|||
This folder contains scripts to install/remove software for different types of installers.
|
||||
|
||||
Scripts are stored on their own files for two reasons:
|
||||
|
||||
1. Some of them are read and displayed in the UI.
|
||||
2. It's helpful to have good syntax highlighting and easy ways to run them.
|
||||
|
||||
#### Scripts
|
||||
|
||||
- `install_*.*`: Default installer scripts for each platform.
|
||||
- `uninstall_*.*`: Default uinstaller scripts for each platform.
|
||||
- `remove_*.*`: Uninstaller scripts used when the uninstall script is not set (for packages added before the uninstall feature was released) or empty uninstaller scripts.
|
||||
|
||||
#### Variables
|
||||
|
||||
The scripts in this folder accept variables like `$VAR_NAME` that will be replaced/populated by `fleetd` when they run.
|
||||
|
|
@ -14,4 +19,3 @@ The scripts in this folder accept variables like `$VAR_NAME` that will be replac
|
|||
Supported variables are:
|
||||
|
||||
- `$INSTALLER_PATH` path to the installer file.
|
||||
|
||||
|
|
|
|||
3
pkg/file/scripts/install_rpm.sh
Normal file
3
pkg/file/scripts/install_rpm.sh
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
#!/bin/sh
|
||||
|
||||
dnf install --assumeyes "$INSTALLER_PATH"
|
||||
6
pkg/file/scripts/remove_rpm.sh
Normal file
6
pkg/file/scripts/remove_rpm.sh
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
#!/bin/sh
|
||||
|
||||
package_name=$PACKAGE_ID
|
||||
|
||||
# Fleet uninstalls app using product name that's extracted on upload
|
||||
dnf remove --assumeyes "$package_name"
|
||||
4
pkg/file/scripts/uninstall_rpm.sh
Normal file
4
pkg/file/scripts/uninstall_rpm.sh
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
package_name=$PACKAGE_ID
|
||||
|
||||
# Fleet uninstalls app using product name that's extracted on upload
|
||||
dnf remove --assumeyes "$package_name"
|
||||
3
pkg/file/testdata/scripts/install_rpm.sh.golden
vendored
Normal file
3
pkg/file/testdata/scripts/install_rpm.sh.golden
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
#!/bin/sh
|
||||
|
||||
dnf install --assumeyes "$INSTALLER_PATH"
|
||||
6
pkg/file/testdata/scripts/remove_rpm.sh.golden
vendored
Normal file
6
pkg/file/testdata/scripts/remove_rpm.sh.golden
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
#!/bin/sh
|
||||
|
||||
package_name=$PACKAGE_ID
|
||||
|
||||
# Fleet uninstalls app using product name that's extracted on upload
|
||||
dnf remove --assumeyes "$package_name"
|
||||
4
pkg/file/testdata/scripts/uninstall_rpm.sh.golden
vendored
Normal file
4
pkg/file/testdata/scripts/uninstall_rpm.sh.golden
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
package_name=$PACKAGE_ID
|
||||
|
||||
# Fleet uninstalls app using product name that's extracted on upload
|
||||
dnf remove --assumeyes "$package_name"
|
||||
|
|
@ -557,6 +557,7 @@ SELECT
|
|||
hsi.self_service,
|
||||
hsi.host_deleted_at,
|
||||
hsi.created_at as created_at,
|
||||
hsi.updated_at as updated_at,
|
||||
si.user_id AS software_installer_user_id,
|
||||
si.user_name AS software_installer_user_name,
|
||||
si.user_email AS software_installer_user_email
|
||||
|
|
|
|||
|
|
@ -511,8 +511,18 @@ func testGetSoftwareInstallResult(t *testing.T, ds *Datastore) {
|
|||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
beforeInstallRequest := time.Now()
|
||||
installUUID, err := ds.InsertSoftwareInstallRequest(ctx, host.ID, installerID, false)
|
||||
require.NoError(t, err)
|
||||
|
||||
res, err := ds.GetSoftwareInstallResults(ctx, installUUID)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, res.UpdatedAt)
|
||||
require.Less(t, beforeInstallRequest, res.CreatedAt)
|
||||
createdAt := res.CreatedAt
|
||||
require.Less(t, beforeInstallRequest, *res.UpdatedAt)
|
||||
|
||||
beforeInstallResult := time.Now()
|
||||
err = ds.SetHostSoftwareInstallResult(ctx, &fleet.HostSoftwareInstallResultPayload{
|
||||
HostID: host.ID,
|
||||
InstallUUID: installUUID,
|
||||
|
|
@ -524,7 +534,7 @@ func testGetSoftwareInstallResult(t *testing.T, ds *Datastore) {
|
|||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
res, err := ds.GetSoftwareInstallResults(ctx, installUUID)
|
||||
res, err = ds.GetSoftwareInstallResults(ctx, installUUID)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, installUUID, res.InstallUUID)
|
||||
|
|
@ -534,6 +544,10 @@ func testGetSoftwareInstallResult(t *testing.T, ds *Datastore) {
|
|||
require.Equal(t, tc.preInstallQueryOutput, res.PreInstallQueryOutput)
|
||||
require.Equal(t, tc.postInstallScriptOutput, res.PostInstallScriptOutput)
|
||||
require.Equal(t, tc.installScriptOutput, res.Output)
|
||||
require.NotNil(t, res.CreatedAt)
|
||||
require.Equal(t, createdAt, res.CreatedAt)
|
||||
require.NotNil(t, res.UpdatedAt)
|
||||
require.Less(t, beforeInstallResult, *res.UpdatedAt)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -363,6 +363,8 @@ func SofwareInstallerSourceFromExtensionAndName(ext, name string) (string, error
|
|||
switch ext {
|
||||
case "deb":
|
||||
return "deb_packages", nil
|
||||
case "rpm":
|
||||
return "rpm_packages", nil
|
||||
case "exe", "msi":
|
||||
return "programs", nil
|
||||
case "pkg":
|
||||
|
|
@ -378,7 +380,7 @@ func SofwareInstallerSourceFromExtensionAndName(ext, name string) (string, error
|
|||
func SofwareInstallerPlatformFromExtension(ext string) (string, error) {
|
||||
ext = strings.TrimPrefix(ext, ".")
|
||||
switch ext {
|
||||
case "deb":
|
||||
case "deb", "rpm":
|
||||
return "linux", nil
|
||||
case "exe", "msi":
|
||||
return "windows", nil
|
||||
|
|
|
|||
|
|
@ -14404,3 +14404,74 @@ func (s *integrationEnterpriseTestSuite) TestSoftwareInstallersWithoutBundleIden
|
|||
}
|
||||
s.uploadSoftwareInstaller(payload, http.StatusOK, "")
|
||||
}
|
||||
|
||||
func (s *integrationEnterpriseTestSuite) TestSoftwareUploadRPM() {
|
||||
ctx := context.Background()
|
||||
t := s.T()
|
||||
|
||||
// Fedora and RHEL have hosts.platform = 'rhel'.
|
||||
host := createOrbitEnrolledHost(t, "rhel", "", s.ds)
|
||||
|
||||
// Upload an RPM package.
|
||||
payload := &fleet.UploadSoftwareInstallerPayload{
|
||||
InstallScript: "install script",
|
||||
PreInstallQuery: "pre install query",
|
||||
PostInstallScript: "post install script",
|
||||
Filename: "ruby.rpm",
|
||||
Title: "ruby",
|
||||
}
|
||||
s.uploadSoftwareInstaller(payload, http.StatusOK, "")
|
||||
titleID := getSoftwareTitleID(t, s.ds, payload.Title, "rpm_packages")
|
||||
|
||||
latestInstallUUID := func() string {
|
||||
var id string
|
||||
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
|
||||
return sqlx.GetContext(ctx, q, &id, `SELECT execution_id FROM host_software_installs ORDER BY id DESC LIMIT 1`)
|
||||
})
|
||||
return id
|
||||
}
|
||||
|
||||
// Send a request to the host to install the RPM package.
|
||||
var installSoftwareResp installSoftwareResponse
|
||||
beforeInstallRequest := time.Now()
|
||||
s.DoJSON("POST", fmt.Sprintf("/api/v1/fleet/hosts/%d/software/%d/install", host.ID, titleID), nil, http.StatusAccepted, &installSoftwareResp)
|
||||
installUUID := latestInstallUUID()
|
||||
|
||||
// Simulate host installing the RPM package.
|
||||
beforeInstallResult := time.Now()
|
||||
s.Do("POST", "/api/fleet/orbit/software_install/result",
|
||||
json.RawMessage(fmt.Sprintf(`{
|
||||
"orbit_node_key": %q,
|
||||
"install_uuid": %q,
|
||||
"pre_install_condition_output": "1",
|
||||
"install_script_exit_code": 1,
|
||||
"install_script_output": "failed"
|
||||
}`, *host.OrbitNodeKey, installUUID)),
|
||||
http.StatusNoContent,
|
||||
)
|
||||
|
||||
var resp getSoftwareInstallResultsResponse
|
||||
s.DoJSON("GET", fmt.Sprintf("/api/v1/fleet/software/install/%s/results", installUUID), nil, http.StatusOK, &resp)
|
||||
assert.Equal(t, host.ID, resp.Results.HostID)
|
||||
assert.Equal(t, installUUID, resp.Results.InstallUUID)
|
||||
assert.Equal(t, fleet.SoftwareInstallFailed, resp.Results.Status)
|
||||
assert.NotNil(t, resp.Results.PreInstallQueryOutput)
|
||||
assert.Equal(t, fleet.SoftwareInstallerQuerySuccessCopy, *resp.Results.PreInstallQueryOutput)
|
||||
assert.NotNil(t, resp.Results.Output)
|
||||
assert.Equal(t, fmt.Sprintf(fleet.SoftwareInstallerInstallFailCopy, "failed"), *resp.Results.Output)
|
||||
assert.Empty(t, resp.Results.PostInstallScriptOutput)
|
||||
assert.Less(t, beforeInstallRequest, resp.Results.CreatedAt)
|
||||
assert.Greater(t, time.Now(), resp.Results.CreatedAt)
|
||||
assert.NotNil(t, resp.Results.UpdatedAt)
|
||||
assert.Less(t, beforeInstallResult, *resp.Results.UpdatedAt)
|
||||
|
||||
wantAct := fleet.ActivityTypeInstalledSoftware{
|
||||
HostID: host.ID,
|
||||
HostDisplayName: host.DisplayName(),
|
||||
SoftwareTitle: payload.Title,
|
||||
SoftwarePackage: payload.Filename,
|
||||
InstallUUID: installUUID,
|
||||
Status: string(fleet.SoftwareInstallFailed),
|
||||
}
|
||||
s.lastActivityMatches(wantAct.ActivityName(), string(jsonMustMarshal(t, wantAct)), 0)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
# testdata
|
||||
|
||||
- `fleet-osquery.msi` is a dummy MSI installer created by `packaging.BuildMSI` with a fake `orbit.exe` that just has `hello world` in it. Its software title is `Fleet osquery` and its version is `1.0.0`.
|
||||
- `fleet-osquery.msi` is a dummy MSI installer created by `packaging.BuildMSI` with a fake `orbit.exe` that just has `hello world` in it. Its software title is `Fleet osquery` and its version is `1.0.0`.
|
||||
- `ruby.rpm` was downloaded from https://rpmfind.net/linux/fedora/linux/development/rawhide/Everything/x86_64/os/Packages/r/ruby-3.3.5-15.fc42.x86_64.rpm.
|
||||
BIN
server/service/testdata/software-installers/ruby.rpm
vendored
Normal file
BIN
server/service/testdata/software-installers/ruby.rpm
vendored
Normal file
Binary file not shown.
Loading…
Reference in a new issue