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:
Lucas Manuel Rodriguez
2023-02-02 14:46:50 -03:00
committed by GitHub
parent 424b370e63
commit dfe6b53963
4 changed files with 172 additions and 51 deletions
@@ -0,0 +1 @@
* Make `fleetctl get teams --yaml` output compatible with `fleetctl apply -f`.
+27 -5
View File
@@ -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
View File
@@ -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
View File
@@ -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
}