Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support to upload RPM packages #22502

Merged
merged 4 commits into from
Oct 1, 2024
Merged
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/20537-add-rpm-support
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* Added support for uploading RPM packages.
4 changes: 2 additions & 2 deletions cmd/fleetctl/gitops_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1656,7 +1656,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", ""},
Expand Down Expand Up @@ -1711,7 +1711,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."},
Expand Down
4 changes: 2 additions & 2 deletions ee/server/service/software_installers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
}
}
Expand Down Expand Up @@ -1517,7 +1517,7 @@ func packageExtensionToPlatform(ext string) string {
requiredPlatform = "windows"
case ".pkg":
requiredPlatform = "darwin"
case ".deb":
case ".deb", ".rpm":
requiredPlatform = "linux"
default:
return ""
Expand Down
2 changes: 1 addition & 1 deletion frontend/interfaces/package_type.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
2 changes: 1 addition & 1 deletion frontend/interfaces/software.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const getPlatformDisplayFromPackageSuffix = (packageName: string) => {
case "pkg":
return "macOS";
case "deb":
case "rpm":
return "Linux";
case "exe":
return "Windows";
Expand Down
23 changes: 15 additions & 8 deletions frontend/utilities/file/fileUtils.tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
});
});
5 changes: 3 additions & 2 deletions frontend/utilities/file/fileUtils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
type IPlatformDisplayName = "macOS" | "Windows" | "linux";
type IPlatformDisplayName = "macOS" | "Windows" | "Linux";

const getFileExtension = (file: File) => {
const nameParts = file.name.split(".");
Expand All @@ -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",
};

/**
Expand Down
4 changes: 4 additions & 0 deletions frontend/utilities/software_install_scripts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -20,6 +22,8 @@ const getDefaultInstallScript = (fileName: string): string => {
return installMsi;
case "deb":
return installDeb;
case "rpm":
return installRPM;
case "exe":
return installExe;
default:
Expand Down
4 changes: 4 additions & 0 deletions frontend/utilities/software_uninstall_scripts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -20,6 +22,8 @@ const getDefaultUninstallScript = (fileName: string): string => {
return uninstallMsi;
case "deb":
return uninstallDeb;
case "rpm":
return uninstallRPM;
case "exe":
return uninstallExe;
default:
Expand Down
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
10 changes: 6 additions & 4 deletions orbit/pkg/installer/installer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions pkg/file/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -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":
Expand All @@ -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")):
Expand Down
15 changes: 15 additions & 0 deletions pkg/file/management.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,18 @@ 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 {
case "msi":
return installMsiScript
case "deb":
return installDebScript
case "rpm":
return installRPMScript
case "pkg":
return installPkgScript
case "exe":
Expand All @@ -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 {
Expand All @@ -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":
Expand All @@ -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 {
Expand All @@ -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":
Expand Down
11 changes: 7 additions & 4 deletions pkg/file/management_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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",
Expand All @@ -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()

Expand Down
33 changes: 33 additions & 0 deletions pkg/file/rpm.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading
Loading