Skip to content

Commit

Permalink
Modify reader code to work on unzipped results as well
Browse files Browse the repository at this point in the history
The code for the results reader was made to read the tarball without
opening it but it should also work on unzipped results. This allows
more reuse of the methods, a centralized place to put results structure
information, and will allow us to expand `results` to process an unzipped
directory as well.

Since the extra work was minimal and it served as a good test, I also
modified the results command to accept a directory.

Signed-off-by: John Schnake <jschnake@vmware.com>
  • Loading branch information
johnSchnake committed Feb 21, 2022
1 parent 6cf7519 commit 0138be0
Show file tree
Hide file tree
Showing 7 changed files with 279 additions and 160 deletions.
28 changes: 16 additions & 12 deletions cmd/sonobuoy/app/results.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import (
"github.com/vmware-tanzu/sonobuoy/pkg/client/results"
"github.com/vmware-tanzu/sonobuoy/pkg/discovery"
"github.com/vmware-tanzu/sonobuoy/pkg/errlog"
yaml "gopkg.in/yaml.v2"
"gopkg.in/yaml.v2"
)

const (
Expand Down Expand Up @@ -100,6 +100,13 @@ func NewCmdResults() *cobra.Command {
// getReader returns a *results.Reader along with a cleanup function to close the
// underlying readers. The cleanup function is guaranteed to never be nil.
func getReader(filepath string) (*results.Reader, func(), error) {
fi, err := os.Stat(filepath)
if err != nil {
return nil, func() {}, err
}
if fi.IsDir() {
return results.NewReaderFromDir(filepath), func() {}, nil
}
f, err := os.Open(filepath)
if err != nil {
return nil, func() {}, errors.Wrapf(err, "could not open sonobuoy archive: %v", filepath)
Expand Down Expand Up @@ -142,7 +149,6 @@ func result(input resultsInput) error {
for i, plugin := range plugins {
input.plugin = plugin


// Load file with a new reader since we can't assume this reader has rewind
// capabilities.
r, cleanup, err := getReader(input.archive)
Expand Down Expand Up @@ -330,7 +336,7 @@ func sortErrors(errorSummary discovery.LogSummary) map[string][]string {
}
//Sort in descending order,
//And use the values in hitCounter for the sorting
isMore := func(i,j int) bool {
isMore := func(i, j int) bool {
valueI := hitCounter[sortedFileNamesList[i]]
valueJ := hitCounter[sortedFileNamesList[j]]
return valueI > valueJ
Expand All @@ -341,22 +347,21 @@ func sortErrors(errorSummary discovery.LogSummary) map[string][]string {
return result
}


// filterAndSortHealthInfoDetails takes a copy of a slice of HealthInfoDetails,
// filterAndSortHealthInfoDetails takes a copy of a slice of HealthInfoDetails,
// discards the ones that are healthy,
// then sorts the remaining entries,
// then sorts the remaining entries,
// and finally sorts them by namespace and name
func filterAndSortHealthInfoDetails(details []discovery.HealthInfoDetails) ([]discovery.HealthInfoDetails) {
func filterAndSortHealthInfoDetails(details []discovery.HealthInfoDetails) []discovery.HealthInfoDetails {
result := make([]discovery.HealthInfoDetails, len(details))
var idx int
for _, detail := range details {
if !detail.Healthy {
result[idx] = detail
result[idx] = detail
idx++
}
}
result = result[:idx]
isLess := func (i,j int) bool {
isLess := func(i, j int) bool {
if result[i].Namespace == result[j].Namespace {
return result[i].Name < result[j].Name
} else {
Expand All @@ -367,7 +372,6 @@ func filterAndSortHealthInfoDetails(details []discovery.HealthInfoDetails) ([]di
return result
}


// printClusterHealthResultsSummary prints the summary of the "fake" plugin for health summary,
// tryingf to emulate the format of printResultsSummary
func printClusterHealthResultsSummary(summary discovery.ClusterSummary) error {
Expand All @@ -377,7 +381,7 @@ func printClusterHealthResultsSummary(summary discovery.ClusterSummary) error {
fmt.Printf("Node health: %d/%d", summary.NodeHealth.Healthy, summary.NodeHealth.Total)
//Print the percentage only if Total is not 0 to avoid division by zero errors
if summary.NodeHealth.Total != 0 {
fmt.Printf(" (%d%%)", 100 * summary.NodeHealth.Healthy / summary.NodeHealth.Total)
fmt.Printf(" (%d%%)", 100*summary.NodeHealth.Healthy/summary.NodeHealth.Total)
}
fmt.Println()
//Details of the failed pods. Checking the slice length to avoid trusting the Total
Expand All @@ -396,7 +400,7 @@ func printClusterHealthResultsSummary(summary discovery.ClusterSummary) error {
fmt.Printf("Pods health: %d/%d", summary.PodHealth.Healthy, summary.PodHealth.Total)
//Print the percentage only if Total is not 0 to avoid division by zero errors
if summary.PodHealth.Total != 0 {
fmt.Printf(" (%d%%)", 100 * summary.PodHealth.Healthy / summary.PodHealth.Total)
fmt.Printf(" (%d%%)", 100*summary.PodHealth.Healthy/summary.PodHealth.Total)
}
fmt.Println()
if summary.PodHealth.Healthy < summary.PodHealth.Total {
Expand Down
116 changes: 98 additions & 18 deletions pkg/client/results/reader.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ import (
"path/filepath"
"strings"

"github.com/sirupsen/logrus"
"gopkg.in/yaml.v3"
k8sver "k8s.io/apimachinery/pkg/version"

"github.com/vmware-tanzu/sonobuoy/pkg/config"

Expand Down Expand Up @@ -62,11 +64,17 @@ const (
defaultServerVersionFile = "serverversion.json"
defaultServerGroupsFile = "servergroups.json"

// CoreNodesFile is the filename of the core nodes json output, relative to nonNamespacedResourcesDir
CoreNodesFile = "core_v1_nodes.json"

// CorePodsFile is the filename of the core pod json output, relative to namespacedResourcesDir
CorePodsFile = "core_v1_pods.json"

// InfoFile contains data not that isn't strictly in another location
// but still relevent to post-processing or understanding the run in some way.
InfoFile = "info.json"

//Filename of the cluster health, relative to metadataDir
// ClusterHealthFile is the filename of the cluster health, relative to metadataDir
ClusterHealthFile = "clusterhealth.json"
)

Expand Down Expand Up @@ -97,10 +105,36 @@ var (
// Reader holds a reader and a version. It uses the version to know where to
// find files within the archive.
type Reader struct {
// Embedded reader assumed to be the *.tar.gz of results. If the tarball has
// been extracted already, set the RootDir instead.
io.Reader

// RootDir, if set, instructs the reader to read as if the tarball of results
// was extracted to the given root directory.
RootDir string

Version string
}

// NewReaderFromDir creates a reader that will process the
// directory `root`. It is assumed this directory contains the
// extracted results.
// Note: Assumes 'VersionFifteen' of results, which is current as of this writing.
// Older versions probably don't even need support at this point.
func NewReaderFromDir(root string) *Reader {
r := &Reader{
RootDir: root,
Version: VersionFifteen,
}
ver, err := r.discoverVersion()
if err != nil {
logrus.Errorf("Failed to read version info from directory, assuming the data is structured according to current formats. Error: %v", err)
} else {
r.Version = ver
}
return r
}

// NewReaderWithVersion creates a results.Reader that interprets a results
// archive of the version passed in.
// Useful if the reader can be read only once and if the version of the data to
Expand Down Expand Up @@ -142,9 +176,11 @@ func DiscoverVersion(reader io.Reader) (string, error) {
r := &Reader{
Reader: reader,
}
return r.discoverVersion()
}

func (r *Reader) discoverVersion() (string, error) {
conf := &config.Config{}

err := r.WalkFiles(func(path string, info os.FileInfo, err error) error {
return ExtractConfig(path, info, conf)
})
Expand Down Expand Up @@ -192,10 +228,42 @@ func (t *tarFileInfo) Sys() interface{} {
return t.Reader
}

func cd(path string) (func(), error) {
pwd, err := os.Getwd()
if err != nil {
return nil, err
}
err = os.Chdir(path)
if err != nil {
return nil, err
}
cleanup := func() {
if err := os.Chdir(pwd); err != nil {
logrus.Errorf("Failed to chdir application pwd, future reads during this command may have errors: %v", err)
}
}
return cleanup, nil
}

// WalkFiles walks all of the files in the archive. Processing stops at the
// first error. The error is returned except in the special case of errStopWalk
// which will stop processing but nil will be returned.
// which will stop processing but nil will be returned. In the case where the reader
// is based on a directory (not a tarball) ensure the WalkFuncs realize that the root
// directory will be part of the path.
func (r *Reader) WalkFiles(walkfn filepath.WalkFunc) error {
if len(r.RootDir) > 0 {
cleanup, err := cd(r.RootDir)
if err != nil {
return err
}
defer cleanup()
err = filepath.Walk(".", walkfn)
if err == errStopWalk || err == io.EOF {
return nil
}
return err
}

tr := tar.NewReader(r)
var err error
var header *tar.Header
Expand Down Expand Up @@ -228,12 +296,11 @@ func (r *Reader) WalkFiles(walkfn filepath.WalkFunc) error {
// ExtractBytes pulls out bytes into a buffer for any path matching file.
func ExtractBytes(file string, path string, info os.FileInfo, buf *bytes.Buffer) error {
if file == path {
reader, ok := info.Sys().(io.Reader)
if !ok {
return errors.New("info.Sys() is not a reader")
}
_, err := buf.ReadFrom(reader)
reader, err := fileInfoToReader(info, path)
if err != nil {
return err
}
if _, err := buf.ReadFrom(reader); err != nil {
return errors.Wrap(err, "could not read from buffer")
}
}
Expand All @@ -245,9 +312,9 @@ func ExtractBytes(file string, path string, info os.FileInfo, buf *bytes.Buffer)
// interface passed in (generally a pointer to a struct/slice).
func ExtractIntoStruct(predicate func(string) bool, path string, info os.FileInfo, object interface{}) error {
if predicate(path) {
reader, ok := info.Sys().(io.Reader)
if !ok {
return errors.New("info.Sys() is not a reader")
reader, err := fileInfoToReader(info, path)
if err != nil {
return err
}
// TODO(chuckha) Perhaps find a more robust way to handle different data formats.
if strings.HasSuffix(path, "xml") {
Expand All @@ -268,7 +335,8 @@ func ExtractIntoStruct(predicate func(string) bool, path string, info os.FileInf
}

// ExtractFileIntoStruct is a helper for a common use case of extracting
// the contents of one file into the object.
// the contents of one file into the object. The first parameter is the desired
// file to extract, the second is the path being considered (by a WalkFn).
func ExtractFileIntoStruct(file, path string, info os.FileInfo, object interface{}) error {
return ExtractIntoStruct(func(p string) bool {
return file == p
Expand Down Expand Up @@ -300,6 +368,19 @@ func (r *Reader) ServerVersionFile() string {
}
}

func (r *Reader) ReadVersion() (string, error) {
k8sInfo := k8sver.Info{}
fileName := r.ServerVersionFile()
err := r.WalkFiles(func(path string, info os.FileInfo, err error) error {
return ExtractFileIntoStruct(fileName, path, info, &k8sInfo)
})
if err != nil {
logrus.Errorf("Failed to read server version: failed to read '%s': %s", fileName, err)
return "", err
}
return k8sInfo.GitVersion, err
}

// NamespacedResources returns the path to the directory that contains
// information about namespaced Kubernetes resources.
func (r *Reader) NamespacedResources() string {
Expand Down Expand Up @@ -382,11 +463,10 @@ func (r *Reader) FileReader(filename string) (io.Reader, error) {

if path == filename {
found = true
reader, ok := info.Sys().(io.Reader)
if !ok {
return errors.New("info.Sys() is not a reader")
returnReader, err = fileInfoToReader(info, path)
if err != nil {
return err
}
returnReader = reader
return errStopWalk
}

Expand All @@ -402,7 +482,7 @@ func (r *Reader) FileReader(filename string) (io.Reader, error) {
return returnReader, nil
}

// Return the full path of the ClusterHealthFile
// ClusterHealthFilePath returns the full path of the ClusterHealthFile
func ClusterHealthFilePath() string {
return path.Join(metadataDir, ClusterHealthFile)
return path.Join(metadataDir, ClusterHealthFile)
}
48 changes: 48 additions & 0 deletions pkg/client/results/reader_unix.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
//go:build aix || darwin || dragonfly || freebsd || (js && wasm) || linux || nacl || netbsd || openbsd || solaris
// +build aix darwin dragonfly freebsd js,wasm linux nacl netbsd openbsd solaris

/*
Copyright 2018 Heptio Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package results

import (
"fmt"
"io"
"os"
"syscall"

"github.com/pkg/errors"
)

// fileInfoToReader takes the given FileInfo object and tries to return a reader
// for the data. In the case of normal FileInfo objects (e.g. from os.Stat())
// you need to provide the full path to the file so it can be opened since the
// FileInfo object only contains the name but not the directory.
func fileInfoToReader(info os.FileInfo, path string) (io.Reader, error) {
switch v := info.Sys().(type) {
case io.Reader:
return info.Sys().(io.Reader), nil
case syscall.Stat_t, *syscall.Stat_t:
f, err := os.Open(path)
if err != nil {
return nil, errors.Wrapf(err, "unable to open path %v", path)
}
return f, nil
default:
return nil, fmt.Errorf("info.Sys() (name=%v) is type %v and unable to be used as an io.Reader", info.Name(), v)
}
}
39 changes: 39 additions & 0 deletions pkg/client/results/reader_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
//go:build windows
// +build windows

/*
Copyright 2018 Heptio Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package results

import (
"fmt"
"io"
"os"
)

// fileInfoToReader takes the given FileInfo object and tries to return a reader
// for the data. In the case of normal FileInfo objects (e.g. from os.Stat())
// you need to provide the full path to the file so it can be opened since the
// FileInfo object only contains the name but not the directory.
func fileInfoToReader(info os.FileInfo, path string) (io.Reader, error) {
switch v := info.Sys().(type) {
case io.Reader:
return info.Sys().(io.Reader), nil
default:
return nil, fmt.Errorf("info.Sys() (name=%v) is type %v and unable to be used as an io.Reader", info.Name(), v)
}
}
Loading

0 comments on commit 0138be0

Please sign in to comment.