Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

cliutil: export data about running cluster #9365

Draft
wants to merge 17 commits into
base: main
Choose a base branch
from
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
17 changes: 17 additions & 0 deletions pkg/cliutil/export/default.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package export

import (
"context"
"io"
)

// This file contains standard entry points to our exporter today

// ToLocalDirectory captures an export, and places it in the outputDir
func ToLocalDirectory(ctx context.Context, outputDir string, progressReporter io.Writer, options Options) error {
writer := NewLocalDirWriter(outputDir)

exporter := NewReportExporter(progressReporter)

return exporter.Export(ctx, options, writer)
}
52 changes: 52 additions & 0 deletions pkg/cliutil/export/envoy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package export

import (
"context"
"os"
"path/filepath"

"github.com/solo-io/gloo/pkg/utils/cmdutils"
"github.com/solo-io/gloo/pkg/utils/envoyutils/admincli"
"github.com/solo-io/gloo/pkg/utils/errutils"
"github.com/solo-io/gloo/pkg/utils/kubeutils/kubectl"
"github.com/solo-io/gloo/pkg/utils/kubeutils/portforward"
"github.com/solo-io/gloo/pkg/utils/requestutils/curl"
"github.com/solo-io/gloo/projects/gloo/pkg/defaults"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// CollectEnvoyData queries the Admin API for the provided Envoy Deployment, and emits data to the EnvoyDataDir
func CollectEnvoyData(ctx context.Context, envoyDeployment metav1.ObjectMeta, envoyDataDir string) error {
// 1. Open a port-forward to the Kubernetes Deployment, so that we can query the Envoy Admin API directly
portForwarder, err := kubectl.NewCli().WithReceiver(os.Stdout).StartPortForward(ctx,
portforward.WithDeployment(envoyDeployment.GetName(), envoyDeployment.GetNamespace()),
portforward.WithRemotePort(int(defaults.EnvoyAdminPort)))
if err != nil {
return err
}

// 2. Close the port-forward when we're done accessing data
defer func() {
portForwarder.Close()
portForwarder.WaitForStop()
}()

// 3. Create a CLI that connects to the Envoy Admin API
adminCli := admincli.NewClient().
WithReceiver(os.Stdout).
WithCurlOptions(
curl.WithHostPort(portForwarder.Address()),
)

// 4. Execute parallel requests, emitting output to defined files
return errutils.AggregateConcurrent([]func() error{
cmdutils.RunCommandOutputToFileFunc(
adminCli.ConfigDumpCmd(ctx),
filepath.Join(envoyDataDir, "config_dump.log"),
),
cmdutils.RunCommandOutputToFileFunc(
adminCli.StatsCmd(ctx),
filepath.Join(envoyDataDir, "stats.log"),
),
})
}
93 changes: 93 additions & 0 deletions pkg/cliutil/export/exporter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package export

import (
"context"
"fmt"
"io"
"os"
"path/filepath"

"github.com/solo-io/gloo/pkg/utils/errutils"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

var _ ReportExporter = new(reportExporter)

type ReportExporter interface {
// Export generates a report archive, and then relies on the ArchiveWriter to export that archive to a destination
Export(ctx context.Context, options Options, writer ArchiveWriter) error
}

// Options is the set of parameters that effect what is captured in an export
type Options struct {
// EnvoyDeployments is the list of references to Envoy deployments
EnvoyDeployments []metav1.ObjectMeta
}

// NewReportExporter returns an implementation of a ReportExporter
func NewReportExporter(progressReporter io.Writer) ReportExporter {
return &reportExporter{
progressReporter: progressReporter,
}
}

type reportExporter struct {
// progressReporter is the io.Writer used to write progress updates during the export process
// This is intended to be used so there is feedback to callers, if they want it
// HELP-WANTED: We rely on an io.Writer, but perhaps a better implementation would be to have a more
// intelligent component that can write progress updates (percentages) as well
progressReporter io.Writer

// tmpArchiveDir is the directory where the report archive will be persisted, while it is
// being generated. This is created when Export is invoked, and will be cleaned up by the reportExporter
tmpArchiveDir string
}

// Export generates a report archive, and then relies on the ArchiveWriter to export that archive to a destination
// This implementation relies on a tmp directory to aggregate the report archive
func (r *reportExporter) Export(ctx context.Context, options Options, writer ArchiveWriter) error {
r.reportProgress("starting report export")
if err := r.setTmpArchiveDir(); err != nil {
return err
}
defer func() {
r.reportProgress("finishing report export")
_ = os.RemoveAll(r.tmpArchiveDir)
}()

if err := r.doExport(ctx, options); err != nil {
return err
}

r.reportProgress("Export completed. Uploading report")
return writer.Write(ctx, r.tmpArchiveDir)
}

func (r *reportExporter) setTmpArchiveDir() error {
tmpDir, err := os.MkdirTemp("", "gloo-report-export")
if err != nil {
return err
}
r.reportProgress(fmt.Sprintf("using %s to store export temporarily", tmpDir))
r.tmpArchiveDir = tmpDir
return nil
}

func (r *reportExporter) reportProgress(progressUpdate string) {
_, _ = r.progressReporter.Write([]byte(fmt.Sprintf("%s\n", progressUpdate)))
}

func (r *reportExporter) doExport(ctx context.Context, options Options) error {
var parallelFns []func() error

for _, envoyDeploy := range options.EnvoyDeployments {
deploymentDataDir := filepath.Join(
r.tmpArchiveDir,
fmt.Sprintf("envoy-%s-%s", envoyDeploy.GetNamespace(), envoyDeploy.GetName()))
parallelFns = append(parallelFns, func() error {
return CollectEnvoyData(ctx, envoyDeploy, deploymentDataDir)
})
}

return errutils.AggregateConcurrent(parallelFns)
}
54 changes: 54 additions & 0 deletions pkg/cliutil/export/writer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package export

import (
"context"
"os"
"path/filepath"

"github.com/solo-io/gloo/pkg/utils/fileutils"
)

var _ ArchiveWriter = new(localTarWriter)
var _ ArchiveWriter = new(localDirWriter)

type ArchiveWriter interface {
Write(ctx context.Context, artifactDir string) error
}

func NewLocalTarWriter(targetPath string) ArchiveWriter {
return &localTarWriter{
targetPath: targetPath,
}
}

type localTarWriter struct {
// targetPath is the destination path where the tarball will be written
targetPath string
}

func (l *localTarWriter) Write(ctx context.Context, artifactDir string) error {
if err := os.MkdirAll(filepath.Dir(l.targetPath), os.ModePerm); err != nil {
return err
}

return fileutils.CreateTarFile(artifactDir, l.targetPath)
}

type localDirWriter struct {
// targetDir is the destination directory where the artifact will be written
targetDir string
}

func NewLocalDirWriter(targetDir string) ArchiveWriter {
return &localDirWriter{
targetDir: targetDir,
}
}

func (d *localDirWriter) Write(ctx context.Context, artifactDir string) error {
if err := os.MkdirAll(d.targetDir, os.ModePerm); err != nil {
return err
}

return fileutils.CopyDir(artifactDir, d.targetDir)
}
55 changes: 55 additions & 0 deletions pkg/utils/fileutils/compress.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package fileutils

import (
"archive/tar"
"compress/gzip"
"io"
"os"
"path/filepath"
"strings"
)

// CreateTarFile creates a gzipped tar file from srcDir and writes it to outPath.
func CreateTarFile(srcDir, outPath string) error {
mw, err := os.Create(outPath)
if err != nil {
return err
}
defer mw.Close()
gzw := gzip.NewWriter(mw)
defer gzw.Close()

tw := tar.NewWriter(gzw)
defer tw.Close()

return filepath.Walk(srcDir, func(file string, fi os.FileInfo, err error) error {
if err != nil {
return err
}
if !fi.Mode().IsRegular() {
return nil
}
header, err := tar.FileInfoHeader(fi, fi.Name())
if err != nil {
return err
}
header.Name = strings.TrimPrefix(strings.Replace(file, srcDir, "", -1), string(filepath.Separator))
header.Size = fi.Size()
header.Mode = int64(fi.Mode())
header.ModTime = fi.ModTime()
if err := tw.WriteHeader(header); err != nil {
return err
}

f, err := os.Open(file)
if err != nil {
return err
}
defer f.Close()

if _, err := io.Copy(tw, f); err != nil {
return err
}
return nil
})
}
47 changes: 47 additions & 0 deletions pkg/utils/fileutils/copy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package fileutils

import (
"io"
"os"
"path/filepath"
"strings"
)

// CopyDir copies all the files from srcDir into targetDir.
func CopyDir(srcDir, targetDir string) error {
if err := filepath.Walk(srcDir, func(srcFile string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() {
return nil
}

destFile := filepath.Join(targetDir, strings.TrimPrefix(srcFile, srcDir))

// copy
srcReader, err := os.Open(srcFile)
if err != nil {
return err
}
defer srcReader.Close()

if err := os.MkdirAll(filepath.Dir(destFile), os.ModePerm); err != nil {
return err
}

dstFile, err := os.Create(destFile)
if err != nil {
return err
}
defer dstFile.Close()

_, err = io.Copy(dstFile, srcReader)
return err

}); err != nil {
return err
}

return nil
}
2 changes: 2 additions & 0 deletions pkg/utils/kubeutils/names.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ const (
GlooDeploymentName = "gloo"
GlooServiceName = "gloo"

GatewayProxyDeploymentName = "gateway-proxy"

// The name of the port in the gloo control plane Kubernetes Service that serves xDS config.
GlooXdsPortName = "grpc-xds"
)
Loading