Skip to content
Draft
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/44088-linux-label-platform
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Added `linux` as a label platform option, which targets hosts on any Linux distribution.
6 changes: 6 additions & 0 deletions cmd/fleetctl/fleetctl/generate_gitops_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -735,6 +735,12 @@ func (MockClient) GetLabels(teamID uint) ([]*fleet.LabelSpec, error) {
Description: "Label C description",
LabelMembershipType: fleet.LabelMembershipTypeHostVitals,
HostVitalsCriteria: ptr.RawMessage(json.RawMessage(`{"vital": "end_user_idp_group", "value": "some-group"}`)),
}, {
Name: "Label D",
Description: "Label D description",
Platform: "linux",
LabelMembershipType: fleet.LabelMembershipTypeDynamic,
Query: "SELECT 1",
}}, nil
}

Expand Down
4 changes: 2 additions & 2 deletions cmd/fleetctl/fleetctl/gitops_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -388,7 +388,7 @@ queries:
labels:
- name: Test - Linux invalid platform
query: SELECT 1;
platform: linux
platform: bados
org_settings:
server_settings:
server_url: https://fleet.example.com
Expand All @@ -401,7 +401,7 @@ org_settings:

_, err = runAppNoChecks([]string{"gitops", "-f", tmpFile.Name(), "--dry-run"})
require.ErrorContains(t, err, "invalid platform")
require.ErrorContains(t, err, "linux")
require.ErrorContains(t, err, "bados")
}

func TestGitOpsBasicGlobalPremium(t *testing.T) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,9 @@
label_membership_type: host_vitals
criteria:
vital: end_user_idp_group
value: some-group
value: some-group
- name: Label D
description: Label D description
label_membership_type: dynamic
query: SELECT 1
platform: linux
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ labels:
description: Label C description
label_membership_type: host_vitals
name: Label C
- description: Label D description
label_membership_type: dynamic
name: Label D
platform: linux
query: SELECT 1
org_settings:
activity_expiry_settings:
activity_expiry_enabled: false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ labels:
description: Label C description
label_membership_type: host_vitals
name: Label C
- description: Label D description
label_membership_type: dynamic
name: Label D
platform: linux
query: SELECT 1
org_settings:
activity_expiry_settings:
activity_expiry_enabled: false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -941,6 +941,7 @@ agent_options:
labels:
- name: Label1
query: select 1
platform: linux
controls:
apple_settings:
configuration_profiles:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import React from "react";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";

import PlatformField from "./PlatformField";

describe("PlatformField component", () => {
it("renders all platform options, including generic Linux", async () => {
const onChange = jest.fn();
render(<PlatformField platform="" onChange={onChange} />);

// Open the dropdown.
await userEvent.click(screen.getByText(/all platforms/i));

expect(screen.getByText("macOS")).toBeInTheDocument();
expect(screen.getByText("Windows")).toBeInTheDocument();
expect(screen.getByText("Linux")).toBeInTheDocument();
expect(screen.getByText("Ubuntu (Linux)")).toBeInTheDocument();
expect(screen.getByText("CentOS (Linux)")).toBeInTheDocument();
});

it("calls onChange with the platform value when Linux is selected", async () => {
const onChange = jest.fn();
render(<PlatformField platform="" onChange={onChange} />);

await userEvent.click(screen.getByText(/all platforms/i));
await userEvent.click(screen.getByText("Linux"));

expect(onChange).toHaveBeenCalledWith("linux");
});

it("renders read-only display text when editing", () => {
render(<PlatformField platform="linux" isEditing />);

expect(screen.getByText("Linux")).toBeInTheDocument();
});

it("renders updated display text for ubuntu and centos when editing", () => {
const { rerender } = render(<PlatformField platform="ubuntu" isEditing />);
expect(screen.getByText("Ubuntu (Linux)")).toBeInTheDocument();

rerender(<PlatformField platform="centos" isEditing />);
expect(screen.getByText("CentOS (Linux)")).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,23 @@ import DropdownWrapper, {
} from "components/forms/fields/DropdownWrapper/DropdownWrapper";
import FormField from "components/forms/FormField";

// Used to display the platform of an existing label on the edit label page
// (platform is not editable after creation).
const PLATFORM_STRINGS: { [key: string]: string } = {
darwin: "macOS",
windows: "MS Windows",
ubuntu: "Ubuntu Linux",
zorin: "Zorin OS",
centos: "CentOS Linux",
windows: "Windows",
linux: "Linux",
ubuntu: "Ubuntu (Linux)",
centos: "CentOS (Linux)",
};

const platformOptions: CustomOptionType[] = [
{ label: "All platforms", value: "" },
{ label: "macOS", value: "darwin" },
{ label: "Windows", value: "windows" },
{ label: "Ubuntu", value: "ubuntu" },
{ label: "Centos", value: "centos" },
{ label: "Linux", value: "linux" },
{ label: "Ubuntu (Linux)", value: "ubuntu" },
{ label: "CentOS (Linux)", value: "centos" },
];

const baseClass = "platform-field";
Expand Down
33 changes: 20 additions & 13 deletions server/datastore/mysql/labels.go
Original file line number Diff line number Diff line change
Expand Up @@ -935,25 +935,32 @@ func applyLabelTeamFilter(query string, filter fleet.TeamFilter, initialParams .
return maybeIn(query)
}

func platformForHost(host *fleet.Host) string {
if host.Platform != "rhel" {
return host.Platform
}
if strings.Contains(strings.ToLower(host.OSVersion), "centos") {
return "centos"
}
return host.Platform
// platformsForHost returns the label platform values that match the given
// host. A host matches labels with its specific platform (with rhel hosts
// running CentOS matching "centos"), and Linux hosts additionally match the
// generic "linux" platform regardless of distribution.
func platformsForHost(host *fleet.Host) []string {
specific := host.Platform
if host.Platform == "rhel" && strings.Contains(strings.ToLower(host.OSVersion), "centos") {
specific = "centos"
}
if fleet.IsLinux(specific) && specific != "linux" {
return []string{specific, "linux"}
}
return []string{specific}
}

func (ds *Datastore) LabelQueriesForHost(ctx context.Context, host *fleet.Host) (map[string]string, error) {
var rows *sql.Rows
var err error
platform := platformForHost(host)
platforms := platformsForHost(host)
query := `SELECT id, query FROM labels WHERE
(platform = ? OR platform = '') AND
(platform IN (?) OR platform = '') AND
label_membership_type = ? AND
(team_id IS NULL OR team_id = ?)`
rows, err = ds.reader(ctx).QueryContext(ctx, query, platform, fleet.LabelMembershipTypeDynamic, host.TeamID)
query, args, err := sqlx.In(query, platforms, fleet.LabelMembershipTypeDynamic, host.TeamID)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "building label queries for host")
}
Comment thread
lucasmrod marked this conversation as resolved.
rows, err := ds.reader(ctx).QueryContext(ctx, query, args...)
if err != nil && err != sql.ErrNoRows {
return nil, ctxerr.Wrap(ctx, err, "selecting label queries for host")
}
Expand Down
84 changes: 84 additions & 0 deletions server/datastore/mysql/labels_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ func TestLabels(t *testing.T) {
{"SingleByName", testLabelByName},
{"Save", testLabelsSave},
{"QueriesForCentOSHost", testLabelsQueriesForCentOSHost},
{"QueriesForLinuxPlatformLabel", testLabelsQueriesForLinuxPlatformLabel},
{"RecordNonExistentQueryLabelExecution", testLabelsRecordNonexistentQueryLabelExecution},
{"RecordLabelQueryExecutionsQueryErrorKeepsMembership", testRecordLabelQueryExecutionsQueryErrorKeepsMembership},
{"DeleteLabel", testDeleteLabel},
Expand Down Expand Up @@ -1240,6 +1241,89 @@ func testLabelsQueriesForCentOSHost(t *testing.T, db *Datastore) {
assert.Equal(t, "select 1;", queries[fmt.Sprint(label.ID)])
}

func testLabelsQueriesForLinuxPlatformLabel(t *testing.T, db *Datastore) {
ctx := t.Context()

// A label with the generic "linux" platform matches hosts on any Linux
// distribution.
linuxLabel, err := db.NewLabel(ctx, &fleet.Label{
UpdateCreateTimestamps: fleet.UpdateCreateTimestamps{
CreateTimestamp: fleet.CreateTimestamp{CreatedAt: time.Now()},
UpdateTimestamp: fleet.UpdateTimestamp{UpdatedAt: time.Now()},
},
Name: "linux label",
Query: "select 1;",
Platform: "linux",
LabelType: fleet.LabelTypeRegular,
LabelMembershipType: fleet.LabelMembershipTypeDynamic,
})
require.NoError(t, err)

// A distro-specific label keeps matching only that distro.
ubuntuLabel, err := db.NewLabel(ctx, &fleet.Label{
UpdateCreateTimestamps: fleet.UpdateCreateTimestamps{
CreateTimestamp: fleet.CreateTimestamp{CreatedAt: time.Now()},
UpdateTimestamp: fleet.UpdateTimestamp{UpdatedAt: time.Now()},
},
Name: "ubuntu label",
Query: "select 2;",
Platform: "ubuntu",
LabelType: fleet.LabelTypeRegular,
LabelMembershipType: fleet.LabelMembershipTypeDynamic,
})
require.NoError(t, err)

cases := []struct {
name string
platform string
osVersion string
wantLinux bool
wantUbuntu bool
}{
{"ubuntu host matches linux and ubuntu labels", "ubuntu", "Ubuntu 22.04", true, true},
{"debian host matches linux label only", "debian", "Debian GNU/Linux 12", true, false},
{"rhel host matches linux label only", "rhel", "Red Hat Enterprise Linux 9", true, false},
// CentOS reports platform "rhel"; matching centos-specific labels is
// covered by testLabelsQueriesForCentOSHost.
{"centos host matches linux label", "rhel", "CentOS 7", true, false},
{"generic linux host matches linux label", "linux", "Linux 6.1", true, false},
{"darwin host matches neither", "darwin", "macOS 14.0", false, false},
{"windows host matches neither", "windows", "Windows 11", false, false},
}

for i, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
host, err := db.EnrollOsquery(ctx,
fleet.WithEnrollOsqueryHostID(fmt.Sprint(i)),
fleet.WithEnrollOsqueryNodeKey(fmt.Sprint(i)),
)
require.NoError(t, err, "enrollment should succeed")

host.Platform = tc.platform
host.OSVersion = tc.osVersion
err = db.UpdateHost(ctx, host)
require.NoError(t, err)

queries, err := db.LabelQueriesForHost(ctx, host)
require.NoError(t, err)

linuxKey := fmt.Sprint(linuxLabel.ID)
ubuntuKey := fmt.Sprint(ubuntuLabel.ID)

if tc.wantLinux {
assert.Contains(t, queries, linuxKey, "expected linux label for %s host", tc.platform)
} else {
assert.NotContains(t, queries, linuxKey, "did not expect linux label for %s host", tc.platform)
}
if tc.wantUbuntu {
assert.Contains(t, queries, ubuntuKey, "expected ubuntu label for %s host", tc.platform)
} else {
assert.NotContains(t, queries, ubuntuKey, "did not expect ubuntu label for %s host", tc.platform)
}
})
}
}

func testLabelsRecordNonexistentQueryLabelExecution(t *testing.T, db *Datastore) {
h1, err := db.NewHost(context.Background(), &fleet.Host{
DetailUpdatedAt: time.Now(),
Expand Down
1 change: 1 addition & 0 deletions server/fleet/labels.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ var ValidLabelPlatformVariants = map[string]struct{}{
"": {}, // empty platform is valid value
"darwin": {},
"windows": {},
"linux": {}, // matches hosts on any Linux distribution
"ubuntu": {},
"centos": {},
}
Expand Down
Loading
Loading