Make fleetctl get teams --yaml output compatible with fleetctl apply -f (#9626)
#9535 - [X] Changes file added for user-visible changes in `changes/` or `orbit/changes/`. See [Changes files](https://fleetdm.com/docs/contributing/committing-changes#changes-files) for more information. - ~[ ] Documented any API changes (docs/Using-Fleet/REST-API.md or docs/Contributing/API-for-contributors.md)~ - ~[ ] Documented any permissions changes~ - ~[ ] Input data is properly validated, `SELECT *` is avoided, SQL injection is prevented (using placeholders for values in statements)~ - ~[ ] Added support on fleet's osquery simulator `cmd/osquery-perf` for new osquery data ingestion features.~ - [X] Added/updated tests - [X] Manual QA for all new/changed functionality - ~For Orbit and Fleet Desktop changes:~ - ~[ ] Manual QA must be performed in the three main OSs, macOS, Windows and Linux.~ - ~[ ] Auto-update manual QA, from released version of component to new version (see [tools/tuf/test](../tools/tuf/test/README.md)).~
This commit is contained in:
@@ -0,0 +1 @@
|
||||
* Make `fleetctl get teams --yaml` output compatible with `fleetctl apply -f`.
|
||||
+27
-5
@@ -237,11 +237,19 @@ func printUserRoles(c *cli.Context, users []fleet.User) error {
|
||||
|
||||
func printTeams(c *cli.Context, teams []fleet.Team) error {
|
||||
for _, team := range teams {
|
||||
var teamItem interface{} = team
|
||||
if c.Bool(yamlFlagName) {
|
||||
teamSpec, err := fleet.TeamSpecFromTeam(&team)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
teamItem = teamSpec
|
||||
}
|
||||
spec := specGeneric{
|
||||
Kind: fleet.TeamKind,
|
||||
Version: fleet.ApiVersion,
|
||||
Spec: map[string]interface{}{
|
||||
"team": team,
|
||||
"team": teamItem,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -907,14 +915,28 @@ func printKeyValueTable(c *cli.Context, rows [][]string) {
|
||||
table.Render()
|
||||
}
|
||||
|
||||
func getTeamsJSONFlag() cli.Flag {
|
||||
return &cli.BoolFlag{
|
||||
Name: jsonFlagName,
|
||||
Usage: "Output all team information in JSON format",
|
||||
}
|
||||
}
|
||||
|
||||
func getTeamsYAMLFlag() cli.Flag {
|
||||
return &cli.BoolFlag{
|
||||
Name: yamlFlagName,
|
||||
Usage: "Output team configuration in yaml format. Intended for use with \"fleetctl apply -f\"",
|
||||
}
|
||||
}
|
||||
|
||||
func getTeamsCommand() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "teams",
|
||||
Aliases: []string{"t"},
|
||||
Usage: "List teams",
|
||||
Flags: []cli.Flag{
|
||||
jsonFlag(),
|
||||
yamlFlag(),
|
||||
getTeamsJSONFlag(),
|
||||
getTeamsYAMLFlag(),
|
||||
configFlag(),
|
||||
contextFlag(),
|
||||
debugFlag(),
|
||||
@@ -959,11 +981,11 @@ func getTeamsCommand() *cli.Command {
|
||||
for _, team := range teams {
|
||||
data = append(data, []string{
|
||||
team.Name,
|
||||
team.Description,
|
||||
fmt.Sprintf("%d", team.HostCount),
|
||||
fmt.Sprintf("%d", team.UserCount),
|
||||
})
|
||||
}
|
||||
columns := []string{"Team Name", "Description", "User count"}
|
||||
columns := []string{"Team name", "Host count", "User count"}
|
||||
printTable(c, columns, data)
|
||||
|
||||
return nil
|
||||
|
||||
+116
-45
@@ -3,6 +3,7 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -130,6 +131,7 @@ func TestGetTeams(t *testing.T) {
|
||||
Name: "team1",
|
||||
Description: "team1 description",
|
||||
UserCount: 99,
|
||||
HostCount: 42,
|
||||
Config: fleet.TeamConfig{
|
||||
Features: fleet.Features{
|
||||
EnableHostUsers: true,
|
||||
@@ -143,6 +145,7 @@ func TestGetTeams(t *testing.T) {
|
||||
Name: "team2",
|
||||
Description: "team2 description",
|
||||
UserCount: 87,
|
||||
HostCount: 43,
|
||||
Config: fleet.TeamConfig{
|
||||
AgentOptions: &agentOpts,
|
||||
Features: fleet.Features{
|
||||
@@ -159,41 +162,27 @@ func TestGetTeams(t *testing.T) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
expectedText := `+-----------+-------------------+------------+
|
||||
| TEAM NAME | DESCRIPTION | USER COUNT |
|
||||
+-----------+-------------------+------------+
|
||||
| team1 | team1 description | 99 |
|
||||
+-----------+-------------------+------------+
|
||||
| team2 | team2 description | 87 |
|
||||
+-----------+-------------------+------------+
|
||||
expectedText := `+-----------+------------+------------+
|
||||
| TEAM NAME | HOST COUNT | USER COUNT |
|
||||
+-----------+------------+------------+
|
||||
| team1 | 42 | 99 |
|
||||
+-----------+------------+------------+
|
||||
| team2 | 43 | 87 |
|
||||
+-----------+------------+------------+
|
||||
`
|
||||
expectedYaml := `---
|
||||
apiVersion: v1
|
||||
kind: team
|
||||
spec:
|
||||
team:
|
||||
created_at: "1999-03-10T02:45:06.371Z"
|
||||
description: team1 description
|
||||
features:
|
||||
enable_host_users: true
|
||||
enable_software_inventory: true
|
||||
host_count: 0
|
||||
id: 42
|
||||
integrations:
|
||||
jira: null
|
||||
zendesk: null
|
||||
mdm:
|
||||
macos_updates:
|
||||
minimum_version: ""
|
||||
deadline: ""
|
||||
name: team1
|
||||
user_count: 99
|
||||
webhook_settings:
|
||||
failing_policies_webhook:
|
||||
destination_url: ""
|
||||
enable_failing_policies_webhook: false
|
||||
host_batch_size: 0
|
||||
policy_ids: null
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: team
|
||||
@@ -206,43 +195,35 @@ spec:
|
||||
platforms:
|
||||
darwin:
|
||||
foo: override
|
||||
created_at: "1999-03-10T02:45:06.371Z"
|
||||
description: team2 description
|
||||
features:
|
||||
additional_queries:
|
||||
foo: bar
|
||||
enable_host_users: false
|
||||
enable_software_inventory: false
|
||||
host_count: 0
|
||||
id: 43
|
||||
integrations:
|
||||
jira: null
|
||||
zendesk: null
|
||||
mdm:
|
||||
macos_updates:
|
||||
minimum_version: "12.3.1"
|
||||
deadline: "2021-12-14"
|
||||
name: team2
|
||||
user_count: 87
|
||||
webhook_settings:
|
||||
failing_policies_webhook:
|
||||
destination_url: ""
|
||||
enable_failing_policies_webhook: false
|
||||
host_batch_size: 0
|
||||
policy_ids: null
|
||||
`
|
||||
expectedJson := `{"kind":"team","apiVersion":"v1","spec":{"team":{"id":42,"created_at":"1999-03-10T02:45:06.371Z","name":"team1","description":"team1 description","webhook_settings":{"failing_policies_webhook":{"enable_failing_policies_webhook":false,"destination_url":"","policy_ids":null,"host_batch_size":0}},"integrations":{"jira":null,"zendesk":null},"features":{"enable_host_users":true,"enable_software_inventory":true},"mdm":{"macos_updates":{"minimum_version":"","deadline":""}},"user_count":99,"host_count":0}}}
|
||||
{"kind":"team","apiVersion":"v1","spec":{"team":{"id":43,"created_at":"1999-03-10T02:45:06.371Z","name":"team2","description":"team2 description","agent_options":{"config":{"foo":"bar"},"overrides":{"platforms":{"darwin":{"foo":"override"}}}},"webhook_settings":{"failing_policies_webhook":{"enable_failing_policies_webhook":false,"destination_url":"","policy_ids":null,"host_batch_size":0}},"integrations":{"jira":null,"zendesk":null},"features":{"enable_host_users":false,"enable_software_inventory":false,"additional_queries":{"foo":"bar"}},"mdm":{"macos_updates":{"minimum_version":"12.3.1","deadline":"2021-12-14"}},"user_count":87,"host_count":0}}}
|
||||
expectedJson := `{"kind":"team","apiVersion":"v1","spec":{"team":{"id":42,"created_at":"1999-03-10T02:45:06.371Z","name":"team1","description":"team1 description","webhook_settings":{"failing_policies_webhook":{"enable_failing_policies_webhook":false,"destination_url":"","policy_ids":null,"host_batch_size":0}},"integrations":{"jira":null,"zendesk":null},"features":{"enable_host_users":true,"enable_software_inventory":true},"mdm":{"macos_updates":{"minimum_version":"","deadline":""}},"user_count":99,"host_count":42}}}
|
||||
{"kind":"team","apiVersion":"v1","spec":{"team":{"id":43,"created_at":"1999-03-10T02:45:06.371Z","name":"team2","description":"team2 description","agent_options":{"config":{"foo":"bar"},"overrides":{"platforms":{"darwin":{"foo":"override"}}}},"webhook_settings":{"failing_policies_webhook":{"enable_failing_policies_webhook":false,"destination_url":"","policy_ids":null,"host_batch_size":0}},"integrations":{"jira":null,"zendesk":null},"features":{"enable_host_users":false,"enable_software_inventory":false,"additional_queries":{"foo":"bar"}},"mdm":{"macos_updates":{"minimum_version":"12.3.1","deadline":"2021-12-14"}},"user_count":87,"host_count":43}}}
|
||||
`
|
||||
|
||||
if tt.shouldHaveExpiredBanner {
|
||||
expectedJson = expiredBanner.String() + expectedJson
|
||||
expectedYaml = expiredBanner.String() + expectedYaml
|
||||
expectedText = expiredBanner.String() + expectedText
|
||||
}
|
||||
|
||||
assert.YAMLEq(t, expectedText, runAppForTest(t, []string{"get", "teams"}))
|
||||
assert.YAMLEq(t, expectedYaml, runAppForTest(t, []string{"get", "teams", "--yaml"}))
|
||||
assert.Equal(t, expectedText, runAppForTest(t, []string{"get", "teams"}))
|
||||
assert.Equal(t, expectedJson, runAppForTest(t, []string{"get", "teams", "--json"}))
|
||||
|
||||
actualYaml := runAppForTest(t, []string{"get", "teams", "--yaml"})
|
||||
if tt.shouldHaveExpiredBanner {
|
||||
require.True(t, strings.HasPrefix(actualYaml, expiredBanner.String()))
|
||||
actualYaml = strings.TrimPrefix(actualYaml, expiredBanner.String())
|
||||
}
|
||||
assert.YAMLEq(t, expectedYaml, actualYaml)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -262,15 +243,16 @@ func TestGetTeamsByName(t *testing.T) {
|
||||
Name: "team1",
|
||||
Description: "team1 description",
|
||||
UserCount: 99,
|
||||
HostCount: 43,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
expectedText := `+-----------+-------------------+------------+
|
||||
| TEAM NAME | DESCRIPTION | USER COUNT |
|
||||
+-----------+-------------------+------------+
|
||||
| team1 | team1 description | 99 |
|
||||
+-----------+-------------------+------------+
|
||||
expectedText := `+-----------+------------+------------+
|
||||
| TEAM NAME | HOST COUNT | USER COUNT |
|
||||
+-----------+------------+------------+
|
||||
| team1 | 43 | 99 |
|
||||
+-----------+------------+------------+
|
||||
`
|
||||
assert.Equal(t, expectedText, runAppForTest(t, []string{"get", "teams", "--name", "test1"}))
|
||||
}
|
||||
@@ -1665,3 +1647,92 @@ func TestGetCarveWithError(t *testing.T) {
|
||||
|
||||
runAppCheckErr(t, []string{"get", "carve", "1"}, "test error")
|
||||
}
|
||||
|
||||
// TestGetTeamsYAMLAndApply checks that the output of `get teams --yaml` can be applied
|
||||
// via the `apply` command.
|
||||
func TestGetTeamsYAMLAndApply(t *testing.T) {
|
||||
_, ds := runServerWithMockedDS(t, &service.TestServerOpts{
|
||||
License: &fleet.LicenseInfo{Tier: fleet.TierPremium, Expiration: time.Now().Add(24 * time.Hour)},
|
||||
})
|
||||
|
||||
created_at, err := time.Parse(time.RFC3339, "1999-03-10T02:45:06.371Z")
|
||||
require.NoError(t, err)
|
||||
agentOpts := json.RawMessage(`
|
||||
{
|
||||
"config": {
|
||||
"options": {
|
||||
"distributed_interval": 10
|
||||
}
|
||||
},
|
||||
"overrides": {
|
||||
"platforms": {
|
||||
"darwin": {
|
||||
"options": {
|
||||
"distributed_interval": 5
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`)
|
||||
additionalQueries := json.RawMessage(`{"time":"SELECT * FROM time;"}`)
|
||||
team1 := &fleet.Team{
|
||||
ID: 42,
|
||||
CreatedAt: created_at,
|
||||
Name: "team1",
|
||||
Description: "team1 description",
|
||||
UserCount: 99,
|
||||
Config: fleet.TeamConfig{
|
||||
Features: fleet.Features{
|
||||
EnableHostUsers: true,
|
||||
EnableSoftwareInventory: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
team2 := &fleet.Team{
|
||||
ID: 43,
|
||||
CreatedAt: created_at,
|
||||
Name: "team2",
|
||||
Description: "team2 description",
|
||||
UserCount: 87,
|
||||
Config: fleet.TeamConfig{
|
||||
AgentOptions: &agentOpts,
|
||||
Features: fleet.Features{
|
||||
AdditionalQueries: &additionalQueries,
|
||||
},
|
||||
MDM: fleet.TeamMDM{
|
||||
MacOSUpdates: fleet.MacOSUpdates{
|
||||
MinimumVersion: "12.3.1",
|
||||
Deadline: "2021-12-14",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
ds.ListTeamsFunc = func(ctx context.Context, filter fleet.TeamFilter, opt fleet.ListOptions) ([]*fleet.Team, error) {
|
||||
return []*fleet.Team{team1, team2}, nil
|
||||
}
|
||||
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
||||
return &fleet.AppConfig{AgentOptions: &agentOpts}, nil
|
||||
}
|
||||
ds.SaveTeamFunc = func(ctx context.Context, team *fleet.Team) (*fleet.Team, error) {
|
||||
return team, nil
|
||||
}
|
||||
ds.ApplyEnrollSecretsFunc = func(ctx context.Context, teamID *uint, secrets []*fleet.EnrollSecret) error {
|
||||
return nil
|
||||
}
|
||||
ds.NewActivityFunc = func(ctx context.Context, user *fleet.User, activity fleet.ActivityDetails) error {
|
||||
return nil
|
||||
}
|
||||
ds.TeamByNameFunc = func(ctx context.Context, name string) (*fleet.Team, error) {
|
||||
if name == "team1" {
|
||||
return team1, nil
|
||||
} else if name == "team2" {
|
||||
return team2, nil
|
||||
}
|
||||
return nil, fmt.Errorf("team not found: %s", name)
|
||||
}
|
||||
|
||||
actualYaml := runAppForTest(t, []string{"get", "teams", "--yaml"})
|
||||
yamlFilePath := writeTmpYml(t, actualYaml)
|
||||
|
||||
require.Equal(t, "[+] applied 2 teams\n", runAppForTest(t, []string{"apply", "-f", yamlFilePath}))
|
||||
}
|
||||
|
||||
+28
-1
@@ -268,7 +268,34 @@ type TeamSpec struct {
|
||||
// set to the agent options JSON object.
|
||||
AgentOptions json.RawMessage `json:"agent_options,omitempty"` // marshals as "null" if omitempty is not set
|
||||
|
||||
Secrets []EnrollSecret `json:"secrets"`
|
||||
Secrets []EnrollSecret `json:"secrets,omitempty"`
|
||||
Features *json.RawMessage `json:"features"`
|
||||
MDM TeamMDM `json:"mdm"`
|
||||
}
|
||||
|
||||
// TeamSpecFromTeam returns a TeamSpec constructed from the given Team.
|
||||
func TeamSpecFromTeam(t *Team) (*TeamSpec, error) {
|
||||
features, err := json.Marshal(t.Config.Features)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
featuresJSON := json.RawMessage(features)
|
||||
var secrets []EnrollSecret
|
||||
if len(t.Secrets) > 0 {
|
||||
secrets = make([]EnrollSecret, 0, len(t.Secrets))
|
||||
for _, secret := range t.Secrets {
|
||||
secrets = append(secrets, *secret)
|
||||
}
|
||||
}
|
||||
var agentOptions json.RawMessage
|
||||
if t.Config.AgentOptions != nil {
|
||||
agentOptions = *t.Config.AgentOptions
|
||||
}
|
||||
return &TeamSpec{
|
||||
Name: t.Name,
|
||||
AgentOptions: agentOptions,
|
||||
Features: &featuresJSON,
|
||||
Secrets: secrets,
|
||||
MDM: t.Config.MDM,
|
||||
}, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user