diff --git a/changes/issue-1688-get-hosts-output b/changes/issue-1688-get-hosts-output new file mode 100644 index 0000000000..4f3483255b --- /dev/null +++ b/changes/issue-1688-get-hosts-output @@ -0,0 +1 @@ +* fix `fleetctl get host ` to properly output JSON when the command line flag is supplied i.e `fleetctl get host --json foobar` \ No newline at end of file diff --git a/cmd/fleetctl/get.go b/cmd/fleetctl/get.go index 286a7d85d3..cb68e2add1 100644 --- a/cmd/fleetctl/get.go +++ b/cmd/fleetctl/get.go @@ -132,6 +132,16 @@ func printHost(c *cli.Context, host *service.HostResponse) error { return printSpec(c, spec) } +func printHostDetail(c *cli.Context, host *service.HostDetailResponse) error { + spec := specGeneric{ + Kind: fleet.HostKind, + Version: fleet.ApiVersion, + Spec: host, + } + + return printSpec(c, spec) +} + func printConfig(c *cli.Context, config *fleet.AppConfig) error { spec := specGeneric{ Kind: fleet.AppConfigKind, @@ -368,7 +378,7 @@ func getPacksCommand() *cli.Command { return errors.Wrap(err, "could not list packs") } - if c.Bool(yamlFlagName) { + if c.Bool(yamlFlagName) || c.Bool(jsonFlagName) { for _, pack := range packs { if err := printPack(c, pack); err != nil { return errors.Wrap(err, "unable to print pack") @@ -625,12 +635,10 @@ func getHostsCommand() *cli.Command { if err != nil { return errors.Wrap(err, "could not get host") } - b, err := yaml.Marshal(host) + err = printHostDetail(c, host) if err != nil { return err } - - fmt.Print(string(b)) } return nil }, diff --git a/cmd/fleetctl/get_test.go b/cmd/fleetctl/get_test.go index 4ce226586e..aeb1f42718 100644 --- a/cmd/fleetctl/get_test.go +++ b/cmd/fleetctl/get_test.go @@ -1,7 +1,12 @@ package main import ( + "bufio" + "bytes" "encoding/json" + "github.com/ghodss/yaml" + "io/ioutil" + "path/filepath" "testing" "time" @@ -192,6 +197,7 @@ func TestGetHosts(t *testing.T) { server, ds := runServerWithMockedDS(t) defer server.Close() + // this func is called when no host is specified i.e. `fleetctl get hosts --json` ds.ListHostsFunc = func(filter fleet.TeamFilter, opt fleet.HostListOptions) ([]*fleet.Host, error) { hosts := []*fleet.Host{ { @@ -207,67 +213,131 @@ func TestGetHosts(t *testing.T) { ComputerName: "test_host", Hostname: "test_host", }, + { + UpdateCreateTimestamps: fleet.UpdateCreateTimestamps{ + CreateTimestamp: fleet.CreateTimestamp{CreatedAt: time.Time{}}, + UpdateTimestamp: fleet.UpdateTimestamp{UpdatedAt: time.Time{}}, + }, + HostSoftware: fleet.HostSoftware{}, + DetailUpdatedAt: time.Time{}, + LabelUpdatedAt: time.Time{}, + LastEnrolledAt: time.Time{}, + SeenTime: time.Time{}, + ComputerName: "test_host2", + Hostname: "test_host2", + }, } return hosts, nil } - expectedText := `+------+-----------+----------+-----------------+--------+ -| UUID | HOSTNAME | PLATFORM | OSQUERY VERSION | STATUS | -+------+-----------+----------+-----------------+--------+ -| | test_host | | | mia | -+------+-----------+----------+-----------------+--------+ + // these are run when host is specified `fleetctl get hosts --json test_host` + ds.HostByIdentifierFunc = func(identifier string) (*fleet.Host, error) { + require.NotEmpty(t, identifier) + return &fleet.Host{ + UpdateCreateTimestamps: fleet.UpdateCreateTimestamps{ + CreateTimestamp: fleet.CreateTimestamp{CreatedAt: time.Time{}}, + UpdateTimestamp: fleet.UpdateTimestamp{UpdatedAt: time.Time{}}, + }, + HostSoftware: fleet.HostSoftware{}, + DetailUpdatedAt: time.Time{}, + LabelUpdatedAt: time.Time{}, + LastEnrolledAt: time.Time{}, + SeenTime: time.Time{}, + ComputerName: "test_host", + Hostname: "test_host"}, nil + } + + ds.LoadHostSoftwareFunc = func(host *fleet.Host) error { + return nil + } + ds.ListLabelsForHostFunc = func(hid uint) ([]*fleet.Label, error) { + return make([]*fleet.Label, 0), nil + } + ds.ListPacksForHostFunc = func(hid uint) (packs []*fleet.Pack, err error) { + return make([]*fleet.Pack, 0), nil + } + + expectedText := `+------+------------+----------+-----------------+--------+ +| UUID | HOSTNAME | PLATFORM | OSQUERY VERSION | STATUS | ++------+------------+----------+-----------------+--------+ +| | test_host | | | mia | ++------+------------+----------+-----------------+--------+ +| | test_host2 | | | mia | ++------+------------+----------+-----------------+--------+ ` - expectedYaml := `--- -apiVersion: v1 -kind: host -spec: - build: "" - code_name: "" - computer_name: test_host - config_tls_refresh: 0 - cpu_brand: "" - cpu_logical_cores: 0 - cpu_physical_cores: 0 - cpu_subtype: "" - cpu_type: "" - created_at: "0001-01-01T00:00:00Z" - detail_updated_at: "0001-01-01T00:00:00Z" - display_text: test_host - distributed_interval: 0 - gigs_disk_space_available: 0 - hardware_model: "" - hardware_serial: "" - hardware_vendor: "" - hardware_version: "" - hostname: test_host - id: 0 - label_updated_at: "0001-01-01T00:00:00Z" - last_enrolled_at: "0001-01-01T00:00:00Z" - logger_tls_period: 0 - memory: 0 - os_version: "" - osquery_version: "" - pack_stats: null - percent_disk_space_available: 0 - platform: "" - platform_like: "" - primary_ip: "" - primary_mac: "" - refetch_requested: false - seen_time: "0001-01-01T00:00:00Z" - status: mia - team_id: null - team_name: null - updated_at: "0001-01-01T00:00:00Z" - uptime: 0 - uuid: "" -` - expectedJson := "{\"kind\":\"host\",\"apiVersion\":\"v1\",\"spec\":{\"created_at\":\"0001-01-01T00:00:00Z\",\"updated_at\":\"0001-01-01T00:00:00Z\",\"id\":0,\"detail_updated_at\":\"0001-01-01T00:00:00Z\",\"label_updated_at\":\"0001-01-01T00:00:00Z\",\"last_enrolled_at\":\"0001-01-01T00:00:00Z\",\"seen_time\":\"0001-01-01T00:00:00Z\",\"refetch_requested\":false,\"hostname\":\"test_host\",\"uuid\":\"\",\"platform\":\"\",\"osquery_version\":\"\",\"os_version\":\"\",\"build\":\"\",\"platform_like\":\"\",\"code_name\":\"\",\"uptime\":0,\"memory\":0,\"cpu_type\":\"\",\"cpu_subtype\":\"\",\"cpu_brand\":\"\",\"cpu_physical_cores\":0,\"cpu_logical_cores\":0,\"hardware_vendor\":\"\",\"hardware_model\":\"\",\"hardware_version\":\"\",\"hardware_serial\":\"\",\"computer_name\":\"test_host\",\"primary_ip\":\"\",\"primary_mac\":\"\",\"distributed_interval\":0,\"config_tls_refresh\":0,\"logger_tls_period\":0,\"team_id\":null,\"pack_stats\":null,\"team_name\":null,\"gigs_disk_space_available\":0,\"percent_disk_space_available\":0,\"status\":\"mia\",\"display_text\":\"test_host\"}}\n" + tests := []struct { + name string + goldenFile string + unmarshaler func(data []byte, v interface{}) error + scanner func(s string) []string + args []string + }{ + { + name: "get hosts --json", + goldenFile: "expectedListHostsJson.json", + unmarshaler: json.Unmarshal, + scanner: func(s string) []string { + var parts []string + scanner := bufio.NewScanner(bytes.NewBufferString(s)) + for scanner.Scan() { + parts = append(parts, scanner.Text()) + } + return parts + }, + args: []string{"get", "hosts", "--json"}, + }, + { + name: "get hosts --json test_host", + goldenFile: "expectedHostDetailResponseJson.json", + unmarshaler: json.Unmarshal, + scanner: func(s string) []string { + return []string{s} + }, + args: []string{"get", "hosts", "--json", "test_host"}, + }, + { + name: "get hosts --yaml", + goldenFile: "expectedListHostsYaml.yml", + unmarshaler: yaml.Unmarshal, + scanner: func(s string) []string { + return []string{s} + }, + args: []string{"get", "hosts", "--yaml"}, + }, + { + name: "get hosts --yaml test_host", + goldenFile: "expectedHostDetailResponseYaml.yml", + unmarshaler: yaml.Unmarshal, + scanner: func(s string) []string { + return splitYaml(s) + }, + args: []string{"get", "hosts", "--yaml", "test_host"}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + expected, err := ioutil.ReadFile(filepath.Join("testdata", tt.goldenFile)) + require.NoError(t, err) + expectedResults := tt.scanner(string(expected)) + expectedSpecs := make([]specGeneric, len(expectedResults)) + for i, result := range expectedResults { + var got specGeneric + require.NoError(t, tt.unmarshaler([]byte(result), &got)) + expectedSpecs[i] = got + } + actualResult := tt.scanner(runAppForTest(t, tt.args)) + actualSpecs := make([]specGeneric, len(actualResult)) + for i, result := range actualResult { + var spec specGeneric + require.NoError(t, tt.unmarshaler([]byte(result), &spec)) + actualSpecs[i] = spec + } + require.Equal(t, expectedSpecs, actualSpecs) + }) + } assert.Equal(t, expectedText, runAppForTest(t, []string{"get", "hosts"})) - assert.Equal(t, expectedYaml, runAppForTest(t, []string{"get", "hosts", "--yaml"})) - assert.Equal(t, expectedJson, runAppForTest(t, []string{"get", "hosts", "--json"})) } func TestGetConfig(t *testing.T) { diff --git a/cmd/fleetctl/testdata/expectedHostDetailResponseJson.json b/cmd/fleetctl/testdata/expectedHostDetailResponseJson.json new file mode 100644 index 0000000000..8f4906c363 --- /dev/null +++ b/cmd/fleetctl/testdata/expectedHostDetailResponseJson.json @@ -0,0 +1 @@ +{"kind":"host","apiVersion":"v1","spec":{"created_at":"0001-01-01T00:00:00Z","updated_at":"0001-01-01T00:00:00Z","id":0,"detail_updated_at":"0001-01-01T00:00:00Z","label_updated_at":"0001-01-01T00:00:00Z","last_enrolled_at":"0001-01-01T00:00:00Z","seen_time":"0001-01-01T00:00:00Z","refetch_requested":false,"hostname":"test_host","uuid":"","platform":"","osquery_version":"","os_version":"","build":"","platform_like":"","code_name":"","uptime":0,"memory":0,"cpu_type":"","cpu_subtype":"","cpu_brand":"","cpu_physical_cores":0,"cpu_logical_cores":0,"hardware_vendor":"","hardware_model":"","hardware_version":"","hardware_serial":"","computer_name":"test_host","primary_ip":"","primary_mac":"","distributed_interval":0,"config_tls_refresh":0,"logger_tls_period":0,"team_id":null,"pack_stats":null,"team_name":null,"gigs_disk_space_available":0,"percent_disk_space_available":0,"labels":[],"packs":[],"status":"mia","display_text":"test_host"}} \ No newline at end of file diff --git a/cmd/fleetctl/testdata/expectedHostDetailResponseYaml.yml b/cmd/fleetctl/testdata/expectedHostDetailResponseYaml.yml new file mode 100644 index 0000000000..caf6b03972 --- /dev/null +++ b/cmd/fleetctl/testdata/expectedHostDetailResponseYaml.yml @@ -0,0 +1,46 @@ +--- +apiVersion: v1 +kind: host +spec: + build: "" + code_name: "" + computer_name: test_host + config_tls_refresh: 0 + cpu_brand: "" + cpu_logical_cores: 0 + cpu_physical_cores: 0 + cpu_subtype: "" + cpu_type: "" + created_at: "0001-01-01T00:00:00Z" + detail_updated_at: "0001-01-01T00:00:00Z" + display_text: test_host + distributed_interval: 0 + gigs_disk_space_available: 0 + hardware_model: "" + hardware_serial: "" + hardware_vendor: "" + hardware_version: "" + hostname: test_host + id: 0 + label_updated_at: "0001-01-01T00:00:00Z" + labels: [] + last_enrolled_at: "0001-01-01T00:00:00Z" + logger_tls_period: 0 + memory: 0 + os_version: "" + osquery_version: "" + pack_stats: null + packs: [] + percent_disk_space_available: 0 + platform: "" + platform_like: "" + primary_ip: "" + primary_mac: "" + refetch_requested: false + seen_time: "0001-01-01T00:00:00Z" + status: mia + team_id: null + team_name: null + updated_at: "0001-01-01T00:00:00Z" + uptime: 0 + uuid: "" \ No newline at end of file diff --git a/cmd/fleetctl/testdata/expectedListHostsJson.json b/cmd/fleetctl/testdata/expectedListHostsJson.json new file mode 100644 index 0000000000..9d90c3914f --- /dev/null +++ b/cmd/fleetctl/testdata/expectedListHostsJson.json @@ -0,0 +1,2 @@ +{"kind":"host","apiVersion":"v1","spec":{"created_at":"0001-01-01T00:00:00Z","updated_at":"0001-01-01T00:00:00Z","id":0,"detail_updated_at":"0001-01-01T00:00:00Z","label_updated_at":"0001-01-01T00:00:00Z","last_enrolled_at":"0001-01-01T00:00:00Z","seen_time":"0001-01-01T00:00:00Z","refetch_requested":false,"hostname":"test_host","uuid":"","platform":"","osquery_version":"","os_version":"","build":"","platform_like":"","code_name":"","uptime":0,"memory":0,"cpu_type":"","cpu_subtype":"","cpu_brand":"","cpu_physical_cores":0,"cpu_logical_cores":0,"hardware_vendor":"","hardware_model":"","hardware_version":"","hardware_serial":"","computer_name":"test_host","primary_ip":"","primary_mac":"","distributed_interval":0,"config_tls_refresh":0,"logger_tls_period":0,"team_id":null,"pack_stats":null,"team_name":null,"gigs_disk_space_available":0,"percent_disk_space_available":0,"status":"mia","display_text":"test_host"}} +{"kind":"host","apiVersion":"v1","spec":{"created_at":"0001-01-01T00:00:00Z","updated_at":"0001-01-01T00:00:00Z","id":0,"detail_updated_at":"0001-01-01T00:00:00Z","label_updated_at":"0001-01-01T00:00:00Z","last_enrolled_at":"0001-01-01T00:00:00Z","seen_time":"0001-01-01T00:00:00Z","refetch_requested":false,"hostname":"test_host2","uuid":"","platform":"","osquery_version":"","os_version":"","build":"","platform_like":"","code_name":"","uptime":0,"memory":0,"cpu_type":"","cpu_subtype":"","cpu_brand":"","cpu_physical_cores":0,"cpu_logical_cores":0,"hardware_vendor":"","hardware_model":"","hardware_version":"","hardware_serial":"","computer_name":"test_host2","primary_ip":"","primary_mac":"","distributed_interval":0,"config_tls_refresh":0,"logger_tls_period":0,"team_id":null,"pack_stats":null,"team_name":null,"gigs_disk_space_available":0,"percent_disk_space_available":0,"status":"mia","display_text":"test_host2"}} \ No newline at end of file diff --git a/cmd/fleetctl/testdata/expectedListHostsYaml.yml b/cmd/fleetctl/testdata/expectedListHostsYaml.yml new file mode 100644 index 0000000000..2b75527a0f --- /dev/null +++ b/cmd/fleetctl/testdata/expectedListHostsYaml.yml @@ -0,0 +1,88 @@ +--- +apiVersion: v1 +kind: host +spec: + build: "" + code_name: "" + computer_name: test_host + config_tls_refresh: 0 + cpu_brand: "" + cpu_logical_cores: 0 + cpu_physical_cores: 0 + cpu_subtype: "" + cpu_type: "" + created_at: "0001-01-01T00:00:00Z" + detail_updated_at: "0001-01-01T00:00:00Z" + display_text: test_host + distributed_interval: 0 + gigs_disk_space_available: 0 + hardware_model: "" + hardware_serial: "" + hardware_vendor: "" + hardware_version: "" + hostname: test_host + id: 0 + label_updated_at: "0001-01-01T00:00:00Z" + last_enrolled_at: "0001-01-01T00:00:00Z" + logger_tls_period: 0 + memory: 0 + os_version: "" + osquery_version: "" + pack_stats: null + percent_disk_space_available: 0 + platform: "" + platform_like: "" + primary_ip: "" + primary_mac: "" + refetch_requested: false + seen_time: "0001-01-01T00:00:00Z" + status: mia + team_id: null + team_name: null + updated_at: "0001-01-01T00:00:00Z" + uptime: 0 + uuid: "" +--- +apiVersion: v1 +kind: host +spec: + build: "" + code_name: "" + computer_name: test_host2 + config_tls_refresh: 0 + cpu_brand: "" + cpu_logical_cores: 0 + cpu_physical_cores: 0 + cpu_subtype: "" + cpu_type: "" + created_at: "0001-01-01T00:00:00Z" + detail_updated_at: "0001-01-01T00:00:00Z" + display_text: test_host2 + distributed_interval: 0 + gigs_disk_space_available: 0 + hardware_model: "" + hardware_serial: "" + hardware_vendor: "" + hardware_version: "" + hostname: test_host2 + id: 0 + label_updated_at: "0001-01-01T00:00:00Z" + last_enrolled_at: "0001-01-01T00:00:00Z" + logger_tls_period: 0 + memory: 0 + os_version: "" + osquery_version: "" + pack_stats: null + percent_disk_space_available: 0 + platform: "" + platform_like: "" + primary_ip: "" + primary_mac: "" + refetch_requested: false + seen_time: "0001-01-01T00:00:00Z" + status: mia + team_id: null + team_name: null + updated_at: "0001-01-01T00:00:00Z" + uptime: 0 + uuid: "" \ No newline at end of file diff --git a/server/service/endpoint_hosts.go b/server/service/endpoint_hosts.go index cebd3c4250..682a716b4f 100644 --- a/server/service/endpoint_hosts.go +++ b/server/service/endpoint_hosts.go @@ -26,7 +26,7 @@ func hostResponseForHost(ctx context.Context, svc fleet.Service, host *fleet.Hos }, nil } -// HostDetailresponse is the response struct that contains the full host information +// HostDetailResponse is the response struct that contains the full host information // with the HostDetail details. type HostDetailResponse struct { fleet.HostDetail