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/41566-policy-report-labels-gitops
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Wires labels_include_all to GitOps/fleetctl for policies and reports.
24 changes: 18 additions & 6 deletions cmd/fleetctl/fleetctl/get.go
Original file line number Diff line number Diff line change
Expand Up @@ -345,11 +345,16 @@ func queryToTableRow(query fleet.Query, teamName string) []string {

if len(query.LabelsIncludeAny) > 0 {
scheduleInfo += "\nlabels_include_any:"

for _, label := range query.LabelsIncludeAny {
scheduleInfo += fmt.Sprintf("\n - %s", label.LabelName)
}
}
if len(query.LabelsIncludeAll) > 0 {
scheduleInfo += "\nlabels_include_all:"
for _, label := range query.LabelsIncludeAll {
scheduleInfo += fmt.Sprintf("\n - %s", label.LabelName)
}
}

teamNameOut := teamName
if teamName == "" {
Expand Down Expand Up @@ -478,11 +483,17 @@ func getReportsCommand() *cli.Command {
}

if c.Bool(yamlFlagName) || c.Bool(jsonFlagName) {
for _, query := range queries {
labelsAny := []string{}
for _, label := range query.LabelsIncludeAny {
labelsAny = append(labelsAny, label.LabelName)
identsToNames := func(idents []fleet.LabelIdent) []string {
if len(idents) == 0 {
return nil
}
out := make([]string, 0, len(idents))
for _, ident := range idents {
out = append(out, ident.LabelName)
}
return out
}
for _, query := range queries {
if err := printQuerySpec(c, &fleet.QuerySpec{
Name: query.Name,
Description: query.Description,
Expand All @@ -496,7 +507,8 @@ func getReportsCommand() *cli.Command {
AutomationsEnabled: query.AutomationsEnabled,
Logging: query.Logging,
DiscardData: query.DiscardData,
LabelsIncludeAny: labelsAny,
LabelsIncludeAny: identsToNames(query.LabelsIncludeAny),
LabelsIncludeAll: identsToNames(query.LabelsIncludeAll),
}); err != nil {
return fmt.Errorf("unable to print query: %w", err)
}
Expand Down
110 changes: 110 additions & 0 deletions cmd/fleetctl/fleetctl/get_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1834,6 +1834,116 @@ spec:
assert.Equal(t, expectedJson, RunAppForTest(t, []string{"get", "report", "--json", "--fleet", "1", "teamQuery1"}))
}

func TestGetQueryLabelsIncludeAll(t *testing.T) {
_, ds := testing_utils.RunServerWithMockedDS(t, &service.TestServerOpts{
License: &fleet.LicenseInfo{
Tier: fleet.TierPremium,
Expiration: time.Now().Add(24 * time.Hour),
},
})

ds.QueryByNameFunc = func(ctx context.Context, teamID *uint, name string) (*fleet.Query, error) {
if teamID != nil || name != "qall" {
return nil, &notFoundError{}
}
return &fleet.Query{
ID: 77,
Name: "qall",
Description: "include_all roundtrip",
Query: "select 1;",
Saved: true,
ObserverCanRun: false,
LabelsIncludeAll: []fleet.LabelIdent{
{LabelName: "labelA", LabelID: 1},
{LabelName: "labelB", LabelID: 2},
},
}, nil
}

expectedYaml := `---
apiVersion: v1
kind: report
spec:
automations_enabled: false
description: include_all roundtrip
discard_data: false
fleet: ""
interval: 0
labels_include_all:
- labelA
- labelB
logging: ""
min_osquery_version: ""
name: qall
observer_can_run: false
platform: ""
query: select 1;
team: ""
`
expectedJson := `{"kind":"report","apiVersion":"v1","spec":{"automations_enabled":false,"description":"include_all roundtrip","discard_data":false,"fleet":"","interval":0,"labels_include_all":["labelA","labelB"],"logging":"","min_osquery_version":"","name":"qall","observer_can_run":false,"platform":"","query":"select 1;","team":""}}
`

assert.YAMLEq(t, expectedYaml, RunAppForTest(t, []string{"get", "query", "qall"}))
assert.YAMLEq(t, expectedYaml, RunAppForTest(t, []string{"get", "query", "--yaml", "qall"}))
assert.JSONEq(t, expectedJson, RunAppForTest(t, []string{"get", "query", "--json", "qall"}))
}

func TestGetReportsLabelsIncludeAll(t *testing.T) {
_, ds := testing_utils.RunServerWithMockedDS(t, &service.TestServerOpts{
License: &fleet.LicenseInfo{
Tier: fleet.TierPremium,
Expiration: time.Now().Add(24 * time.Hour),
},
})

ds.TeamsSummaryFunc = func(ctx context.Context) ([]*fleet.TeamSummary, error) {
return []*fleet.TeamSummary{}, nil
}
ds.ListQueriesFunc = func(ctx context.Context, opt fleet.ListQueryOptions) ([]*fleet.Query, int, int, *fleet.PaginationMetadata, error) {
if opt.TeamID != nil {
return nil, 0, 0, nil, errors.New("unexpected team scope")
}
return []*fleet.Query{
{
ID: 78,
Name: "qall-bulk",
Query: "select 1;",
Saved: true,
LabelsIncludeAll: []fleet.LabelIdent{
{LabelName: "labelA", LabelID: 1},
{LabelName: "labelB", LabelID: 2},
},
},
}, 1, 0, nil, nil
}

expectedYaml := `---
apiVersion: v1
kind: report
spec:
automations_enabled: false
description: ""
discard_data: false
fleet: ""
interval: 0
labels_include_all:
- labelA
- labelB
logging: ""
min_osquery_version: ""
name: qall-bulk
observer_can_run: false
platform: ""
query: select 1;
team: ""
`
expectedJson := `{"kind":"report","apiVersion":"v1","spec":{"automations_enabled":false,"description":"","discard_data":false,"fleet":"","interval":0,"labels_include_all":["labelA","labelB"],"logging":"","min_osquery_version":"","name":"qall-bulk","observer_can_run":false,"platform":"","query":"select 1;","team":""}}
`

assert.YAMLEq(t, expectedYaml, RunAppForTest(t, []string{"get", "reports", "--yaml"}))
assert.JSONEq(t, expectedJson, RunAppForTest(t, []string{"get", "reports", "--json"}))
}

// TestGetQueriesAsObservers tests that when observers run `fleectl get queries` they
// only get queries that they can execute.
func TestGetQueriesAsObserver(t *testing.T) {
Expand Down
27 changes: 23 additions & 4 deletions cmd/fleetctl/fleetctl/gitops.go
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,9 @@ func gitopsCommand() *cli.Command {
if len(query.LabelsIncludeAny) > 0 {
return fmt.Errorf("report %q uses 'labels_include_any', which is only available in Fleet Premium", query.Name)
}
if len(query.LabelsIncludeAll) > 0 {
return fmt.Errorf("report %q uses 'labels_include_all', which is only available in Fleet Premium", query.Name)
}
}
}

Expand Down Expand Up @@ -933,20 +936,36 @@ func getLabelUsage(config *spec.GitOps) (map[string][]LabelUsage, error) {
updateLabelUsage(labels, maintainedApp.Slug, "Fleet Maintained App", result)
}

// Get query label usage
// Get query label usage.
for _, query := range config.Queries {
updateLabelUsage(query.LabelsIncludeAny, query.Name, "Query", result)
var labels []string
if len(query.LabelsIncludeAny) > 0 {
labels = query.LabelsIncludeAny
}
if len(query.LabelsIncludeAll) > 0 {
if len(labels) > 0 {
return nil, fmt.Errorf("Query '%s' has multiple label keys; please choose one of `labels_include_any` or `labels_include_all`.", query.Name)
}
labels = query.LabelsIncludeAll
}
updateLabelUsage(labels, query.Name, "Query", result)
}

// Get policy label usage
// Get policy label usage.
for _, policy := range config.Policies {
var labels []string
if len(policy.LabelsIncludeAny) > 0 {
labels = policy.LabelsIncludeAny
}
if len(policy.LabelsIncludeAll) > 0 {
if len(labels) > 0 {
return nil, fmt.Errorf("Policy '%s' has multiple label keys; please choose one of `labels_include_any`, `labels_include_all`, or `labels_exclude_any`.", policy.Name)
}
labels = policy.LabelsIncludeAll
}
if len(policy.LabelsExcludeAny) > 0 {
if len(labels) > 0 {
return nil, fmt.Errorf("Policy '%s' has multiple label keys; please choose one of `labels_include_any`, `labels_exclude_any`.", policy.Name)
return nil, fmt.Errorf("Policy '%s' has multiple label keys; please choose one of `labels_include_any`, `labels_include_all`, or `labels_exclude_any`.", policy.Name)
}
labels = policy.LabelsExcludeAny
}
Expand Down
141 changes: 141 additions & 0 deletions cmd/fleetctl/fleetctl/gitops_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,147 @@ queries:
assert.ErrorContains(t, err, "Fleet Premium")
}

func TestGitOpsQueryLabelsMutexRejection(t *testing.T) {
// Cannot run t.Parallel() because it sets environment variables

license := &fleet.LicenseInfo{Tier: fleet.TierPremium, Expiration: time.Now().Add(24 * time.Hour)}
_, ds := testing_utils.RunServerWithMockedDS(t, &service.TestServerOpts{License: license})
setupEmptyGitOpsMocks(ds)

tmpFile, err := os.CreateTemp(t.TempDir(), "*.yml")
require.NoError(t, err)
_, err = tmpFile.WriteString(`
controls:
policies:
agent_options:
labels:
- name: lbl-a
description: A
label_membership_type: dynamic
query: SELECT 1
- name: lbl-b
description: B
label_membership_type: dynamic
query: SELECT 2
org_settings:
server_settings:
server_url: https://fleet.example.com
org_info:
contact_url: https://example.com/contact
org_logo_url: ""
org_logo_url_light_background: ""
org_name: GitOps Test
secrets:
queries:
- name: bad-query
description: invalid mutex
query: SELECT 1
labels_include_any:
- lbl-a
labels_include_all:
- lbl-b
`)
require.NoError(t, err)

_, err = RunAppNoChecks([]string{"gitops", "-f", tmpFile.Name()})
require.Error(t, err)
require.ErrorContains(t, err, "bad-query")
require.ErrorContains(t, err, "labels_include_any")
require.ErrorContains(t, err, "labels_include_all")
}

func TestGitOpsQueryLabelsIncludeAllUnknownLabel(t *testing.T) {
// Cannot run t.Parallel() because it sets environment variables

license := &fleet.LicenseInfo{Tier: fleet.TierPremium, Expiration: time.Now().Add(24 * time.Hour)}
_, ds := testing_utils.RunServerWithMockedDS(t, &service.TestServerOpts{License: license})
setupEmptyGitOpsMocks(ds)

tmpFile, err := os.CreateTemp(t.TempDir(), "*.yml")
require.NoError(t, err)
_, err = tmpFile.WriteString(`
controls:
policies:
agent_options:
labels:
- name: known-label
description: known
label_membership_type: dynamic
query: SELECT 1
org_settings:
server_settings:
server_url: https://fleet.example.com
org_info:
contact_url: https://example.com/contact
org_logo_url: ""
org_logo_url_light_background: ""
org_name: GitOps Test
secrets:
queries:
- name: refs-missing
description: references undefined label
query: SELECT 1
labels_include_all:
- does-not-exist
`)
require.NoError(t, err)

_, err = RunAppNoChecks([]string{"gitops", "-f", tmpFile.Name()})
require.Error(t, err)
require.ErrorContains(t, err, "missing labels")
}

func TestGitOpsPolicyLabelsIncludeAllMutexRejection(t *testing.T) {
// Cannot run t.Parallel() because it sets environment variables

license := &fleet.LicenseInfo{Tier: fleet.TierPremium, Expiration: time.Now().Add(24 * time.Hour)}
_, ds := testing_utils.RunServerWithMockedDS(t, &service.TestServerOpts{License: license})
setupEmptyGitOpsMocks(ds)

tmpFile, err := os.CreateTemp(t.TempDir(), "*.yml")
require.NoError(t, err)
_, err = tmpFile.WriteString(`
controls:
queries:
agent_options:
labels:
- name: lbl-a
description: A
label_membership_type: dynamic
query: SELECT 1
- name: lbl-b
description: B
label_membership_type: dynamic
query: SELECT 2
org_settings:
server_settings:
server_url: https://fleet.example.com
org_info:
contact_url: https://example.com/contact
org_logo_url: ""
org_logo_url_light_background: ""
org_name: GitOps Test
secrets:
policies:
- name: bad-policy
description: invalid mutex
query: SELECT 1
resolution: ""
platform: linux
labels_include_all:
- lbl-a
labels_exclude_any:
- lbl-b
`)
require.NoError(t, err)

_, err = RunAppNoChecks([]string{"gitops", "-f", tmpFile.Name()})
require.Error(t, err)
require.ErrorContains(t, err, "bad-policy")
require.ErrorContains(t, err, "labels_include_all")
require.ErrorContains(t, err, "labels_exclude_any")
}

func TestGitOpsBasicGlobalPremium(t *testing.T) {
// Cannot run t.Parallel() because it sets environment variables

Expand Down
1 change: 1 addition & 0 deletions server/datastore/mysql/policies.go
Original file line number Diff line number Diff line change
Expand Up @@ -1619,6 +1619,7 @@ func (ds *Datastore) ApplyPolicySpecs(ctx context.Context, authorID uint, specs
PolicyData: fleet.PolicyData{
ID: policyID,
LabelsIncludeAny: fleet.LabelNamesToIdents(spec.LabelsIncludeAny),
LabelsIncludeAll: fleet.LabelNamesToIdents(spec.LabelsIncludeAll),
LabelsExcludeAny: fleet.LabelNamesToIdents(spec.LabelsExcludeAny),
},
})
Expand Down
Loading
Loading