Skip to content

Commit

Permalink
feat: Added troubleshoot command
Browse files Browse the repository at this point in the history
Signed-off-by: Thomas Kosiewski <[email protected]>
  • Loading branch information
Thomas Kosiewski authored and pascalbreuninger committed Nov 29, 2024
1 parent da85f9f commit ad437c8
Show file tree
Hide file tree
Showing 30 changed files with 1,466 additions and 80 deletions.
2 changes: 1 addition & 1 deletion cmd/provider/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ func (cmd *ListCmd) Run(ctx context.Context) error {
continue
}

srcOptions := mergeDynamicOptions(entry.Config.Options, configuredProviders[entry.Config.Name].DynamicOptions)
srcOptions := MergeDynamicOptions(entry.Config.Options, configuredProviders[entry.Config.Name].DynamicOptions)
entry.Config.Options = srcOptions
retMap[k] = ProviderWithDefault{
ProviderWithOptions: *entry,
Expand Down
4 changes: 2 additions & 2 deletions cmd/provider/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ func (cmd *OptionsCmd) Run(ctx context.Context, args []string) error {
func printOptions(devPodConfig *config.Config, provider *workspace.ProviderWithOptions, format string, showHidden bool) error {
entryOptions := devPodConfig.ProviderOptions(provider.Config.Name)
dynamicOptions := devPodConfig.DynamicProviderOptionDefinitions(provider.Config.Name)
srcOptions := mergeDynamicOptions(provider.Config.Options, dynamicOptions)
srcOptions := MergeDynamicOptions(provider.Config.Options, dynamicOptions)
if format == "plain" {
tableEntries := [][]string{}
for optionName, entry := range srcOptions {
Expand Down Expand Up @@ -133,7 +133,7 @@ func printOptions(devPodConfig *config.Config, provider *workspace.ProviderWithO
}

// mergeOptions merges the static provider options and dynamic options
func mergeDynamicOptions(options map[string]*types.Option, dynamicOptions config.OptionDefinitions) map[string]*types.Option {
func MergeDynamicOptions(options map[string]*types.Option, dynamicOptions config.OptionDefinitions) map[string]*types.Option {
retOptions := map[string]*types.Option{}
for k, option := range options {
retOptions[k] = option
Expand Down
5 changes: 2 additions & 3 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,7 @@ import (
"golang.org/x/crypto/ssh"
)

var (
globalFlags *flags.GlobalFlags
)
var globalFlags *flags.GlobalFlags

// NewRootCmd returns a new root command
func NewRootCmd() *cobra.Command {
Expand Down Expand Up @@ -142,5 +140,6 @@ func BuildRoot() *cobra.Command {
rootCmd.AddCommand(NewImportCmd(globalFlags))
rootCmd.AddCommand(NewLogsCmd(globalFlags))
rootCmd.AddCommand(NewUpgradeCmd())
rootCmd.AddCommand(NewTroubleshootCmd(globalFlags))
return rootCmd
}
2 changes: 1 addition & 1 deletion cmd/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ func NewStatusCmd(flags *flags.GlobalFlags) *cobra.Command {
RunE: func(cobraCmd *cobra.Command, args []string) error {
_, err := clientimplementation.DecodeOptionsFromEnv(clientimplementation.DevPodFlagsStatus, &cmd.StatusOptions)
if err != nil {
return fmt.Errorf("decode up options: %w", err)
return fmt.Errorf("decode status options: %w", err)
}

ctx := cobraCmd.Context()
Expand Down
241 changes: 241 additions & 0 deletions cmd/troubleshoot.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
package cmd

import (
"context"
"encoding/json"
"errors"
"fmt"

managementv1 "github.com/loft-sh/api/v4/pkg/apis/management/v1"
"github.com/loft-sh/devpod/cmd/flags"
"github.com/loft-sh/devpod/cmd/provider"
"github.com/loft-sh/devpod/pkg/client"
"github.com/loft-sh/devpod/pkg/config"
"github.com/loft-sh/devpod/pkg/platform"
pkgprovider "github.com/loft-sh/devpod/pkg/provider"
"github.com/loft-sh/devpod/pkg/version"
"github.com/loft-sh/devpod/pkg/workspace"
"github.com/loft-sh/log"
"github.com/spf13/cobra"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

type TroubleshootCmd struct {
*flags.GlobalFlags
}

func NewTroubleshootCmd(flags *flags.GlobalFlags) *cobra.Command {
cmd := &TroubleshootCmd{
GlobalFlags: flags,
}
troubleshootCmd := &cobra.Command{
Use: "troubleshoot [workspace-path|workspace-name]",
Short: "Prints the workspaces troubleshooting information",
Run: func(cobraCmd *cobra.Command, args []string) {
cmd.Run(cobraCmd.Context(), args)
},
Hidden: true,
}

return troubleshootCmd
}

func (cmd *TroubleshootCmd) Run(ctx context.Context, args []string) {
// (ThomasK33): We're creating an anonymous struct here, so that we group
// everything and then we can serialize it in one call.
var info struct {
CLIVersion string
Config *config.Config
Providers map[string]provider.ProviderWithDefault
DevPodProInstances []DevPodProInstance
Workspace *pkgprovider.Workspace
WorkspaceStatus client.Status
WorkspaceTroubleshoot *managementv1.DevPodWorkspaceInstanceTroubleshoot

Errors []PrintableError `json:",omitempty"`
}
info.CLIVersion = version.GetVersion()

// (ThomasK33): We are defering the printing here, as we want to make sure
// that we will always print, even in the case of a panic.
defer func() {
out, err := json.MarshalIndent(info, "", " ")
if err == nil {
fmt.Print(string(out))
} else {
fmt.Print(err)
fmt.Print(info)
}
}()

// NOTE(ThomasK33): Since this is a troubleshooting command, we want to
// collect as many relevant information as possible.
// For this reason we may not return with an error early.
// We are fine with a partially filled TrbouelshootInfo struct, as this
// already provides us with more information then before.
var err error
info.Config, err = config.LoadConfig(cmd.Context, cmd.Provider)
if err != nil {
info.Errors = append(info.Errors, PrintableError{fmt.Errorf("load config: %w", err)})
// (ThomasK33): It's fine to return early here, as without the devpod config
// we cannot do any further troubleshooting.
return
}

logger := log.Default.ErrorStreamOnly()
info.Providers, err = collectProviders(info.Config, logger)
if err != nil {
info.Errors = append(info.Errors, PrintableError{fmt.Errorf("collect providers: %w", err)})
}

info.DevPodProInstances, err = collectPlatformInfo(info.Config, logger)
if err != nil {
info.Errors = append(info.Errors, PrintableError{fmt.Errorf("collect platform info: %w", err)})
}

workspaceClient, err := workspace.Get(ctx, info.Config, args, false, logger)
if err == nil {
info.Workspace = workspaceClient.WorkspaceConfig()
info.WorkspaceStatus, err = workspaceClient.Status(ctx, client.StatusOptions{})
if err != nil {
info.Errors = append(info.Errors, PrintableError{fmt.Errorf("workspace status: %w", err)})
}

if info.Workspace.Pro != nil {
// (ThomasK33): As there can be multiple pro instances configured
// we want to iterate over all and find the host that this workspace belongs to.
var proInstance DevPodProInstance

for _, instance := range info.DevPodProInstances {
if instance.ProviderName == info.Workspace.Provider.Name {
proInstance = instance
break
}
}

if proInstance.ProviderName != "" {
info.WorkspaceTroubleshoot, err = collectProWorkspaceInfo(
ctx,
info.Config,
proInstance.Host,
logger,
info.Workspace.UID,
info.Workspace.Pro.Project,
)
if err != nil {
info.Errors = append(info.Errors, PrintableError{fmt.Errorf("collect pro workspace info: %w", err)})
}
}
}
} else {
info.Errors = append(info.Errors, PrintableError{fmt.Errorf("get workspace: %w", err)})
}
}

// collectProWorkspaceInfo collects troubleshooting information for a DevPod Pro instance.
// It initializes a client from the host, finds the workspace instance in the project, and retrieves
// troubleshooting information using the management client.
func collectProWorkspaceInfo(
ctx context.Context,
devPodConfig *config.Config,
host string,
logger log.Logger,
workspaceUID string,
project string,
) (*managementv1.DevPodWorkspaceInstanceTroubleshoot, error) {
baseClient, err := platform.InitClientFromHost(ctx, devPodConfig, host, logger)
if err != nil {
return nil, fmt.Errorf("init client from host: %w", err)
}

workspace, err := platform.FindInstanceInProject(ctx, baseClient, workspaceUID, project)
if err != nil {
return nil, err
} else if workspace == nil {
return nil, fmt.Errorf("couldn't find workspace")
}

managementClient, err := baseClient.Management()
if err != nil {
return nil, fmt.Errorf("management: %w", err)
}

troubleshoot, err := managementClient.
Loft().
ManagementV1().
DevPodWorkspaceInstances(workspace.Namespace).
Troubleshoot(ctx, workspace.Name, metav1.GetOptions{})
if err != nil {
return nil, fmt.Errorf("troubleshoot: %w", err)
}

return troubleshoot, nil
}

// collectProviders collects and configures providers based on the given devPodConfig.
// It returns a map of providers with their default settings and an error if any occurs.
func collectProviders(devPodConfig *config.Config, logger log.Logger) (map[string]provider.ProviderWithDefault, error) {
providers, err := workspace.LoadAllProviders(devPodConfig, logger)
if err != nil {
return nil, err
}

configuredProviders := devPodConfig.Current().Providers
if configuredProviders == nil {
configuredProviders = map[string]*config.ProviderConfig{}
}

retMap := map[string]provider.ProviderWithDefault{}
for k, entry := range providers {
if configuredProviders[entry.Config.Name] == nil {
continue
}

srcOptions := provider.MergeDynamicOptions(entry.Config.Options, configuredProviders[entry.Config.Name].DynamicOptions)
entry.Config.Options = srcOptions
retMap[k] = provider.ProviderWithDefault{
ProviderWithOptions: *entry,
Default: devPodConfig.Current().DefaultProvider == entry.Config.Name,
}
}

return retMap, nil
}

type DevPodProInstance struct {
Host string
ProviderName string
Version string
}

// collectPlatformInfo collects information about all platform instances in a given devPodConfig.
// It iterates over the pro instances, retrieves their versions, and appends them to the ProInstance slice.
// Any errors encountered during this process are combined and returned along with the ProInstance slice.
// This means that even when an error value is returned, the pro instance slice will contain valid values.
func collectPlatformInfo(devPodConfig *config.Config, logger log.Logger) ([]DevPodProInstance, error) {
proInstanceList, err := workspace.ListProInstances(devPodConfig, logger)
if err != nil {
return nil, fmt.Errorf("list pro instances: %w", err)
}

var proInstances []DevPodProInstance
var combinedErrs error

for _, proInstance := range proInstanceList {
version, err := platform.GetProInstanceDevPodVersion(&pkgprovider.ProInstance{Host: proInstance.Host})
combinedErrs = errors.Join(combinedErrs, err)
proInstances = append(proInstances, DevPodProInstance{
Host: proInstance.Host,
ProviderName: proInstance.Provider,
Version: version,
})
}

return proInstances, combinedErrs
}

// (ThomasK33): Little type embedding here, so that we can
// serialize the error strings when invoking json.Marshal.
type PrintableError struct{ error }

func (p PrintableError) MarshalJSON() ([]byte, error) { return json.Marshal(p.Error()) }
8 changes: 4 additions & 4 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ require (
github.com/google/uuid v1.6.0
github.com/gorilla/websocket v1.5.3
github.com/joho/godotenv v1.5.1
github.com/loft-sh/agentapi/v4 v4.2.0-alpha.3
github.com/loft-sh/api/v4 v4.2.0-alpha.3
github.com/loft-sh/agentapi/v4 v4.2.0-alpha.6
github.com/loft-sh/api/v4 v4.0.0-alpha.6.0.20241129074910-a24d4104d586
github.com/loft-sh/log v0.0.0-20240219160058-26d83ffb46ac
github.com/loft-sh/programming-language-detection v0.0.5
github.com/loft-sh/ssh v0.0.4
Expand Down Expand Up @@ -154,7 +154,7 @@ require (
github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect
github.com/loft-sh/admin-apis v0.0.0-20241105163154-88dd686aaba0 // indirect
github.com/loft-sh/admin-apis v0.0.0-20241127134028-9cfb6b23ec44 // indirect
github.com/loft-sh/apiserver v0.0.0-20241008120650-f17d504a4d0d // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
Expand All @@ -175,7 +175,7 @@ require (
github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/opencontainers/image-spec v1.1.0 // indirect
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
github.com/prometheus/client_golang v1.20.4 // indirect
github.com/prometheus/client_golang v1.20.5 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.60.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
Expand Down
16 changes: 8 additions & 8 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -348,12 +348,12 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0=
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE=
github.com/loft-sh/admin-apis v0.0.0-20241105163154-88dd686aaba0 h1:fAn1LUVsxpkRasDJcSq7wRncxET+zeA8MVtGyxL2rSU=
github.com/loft-sh/admin-apis v0.0.0-20241105163154-88dd686aaba0/go.mod h1:MWczNwKvWssHo1KaeZKaWDdRLYSNbWqQBGsTLoCNd7U=
github.com/loft-sh/agentapi/v4 v4.2.0-alpha.3 h1:pygw90oyxSa+BySv8VpG0MrLhjToAYkdEEJdaxkKT8A=
github.com/loft-sh/agentapi/v4 v4.2.0-alpha.3/go.mod h1:yqbIMmyXqbzZcK0DlwldRLy0xb3lYnH4NoI3K+iETlM=
github.com/loft-sh/api/v4 v4.2.0-alpha.3 h1:arrcAnYPO42vyIICuON2SF8D2JGH58OiaPWJuAs4GsU=
github.com/loft-sh/api/v4 v4.2.0-alpha.3/go.mod h1:awpCyxnrEoKqvRwDRmhZuA/D197Ap4GtC4qr7HOKgdY=
github.com/loft-sh/admin-apis v0.0.0-20241127134028-9cfb6b23ec44 h1:Sq6qEsKSiZHYTzWbnFvnWrzMBFIC3XxFoXtnHdJE9P8=
github.com/loft-sh/admin-apis v0.0.0-20241127134028-9cfb6b23ec44/go.mod h1:MWczNwKvWssHo1KaeZKaWDdRLYSNbWqQBGsTLoCNd7U=
github.com/loft-sh/agentapi/v4 v4.2.0-alpha.6 h1:eVIzaW+EvIygxNXl5163c1+WcUr8c95OP6lj8FcJHUc=
github.com/loft-sh/agentapi/v4 v4.2.0-alpha.6/go.mod h1:yqbIMmyXqbzZcK0DlwldRLy0xb3lYnH4NoI3K+iETlM=
github.com/loft-sh/api/v4 v4.0.0-alpha.6.0.20241129074910-a24d4104d586 h1:nBLJCtuGQH0Cq4lkaUJsDqSUYueWT874YVuW66BQ9S0=
github.com/loft-sh/api/v4 v4.0.0-alpha.6.0.20241129074910-a24d4104d586/go.mod h1:bPDJ1+vZBBEIoPgykfy+TzOwLHtTvWAbTSHexnj4tJA=
github.com/loft-sh/apiserver v0.0.0-20241008120650-f17d504a4d0d h1:73wE8wtsnJm4bXtFbTDRG1EgN4LonpPdgzF3HFhP7kA=
github.com/loft-sh/apiserver v0.0.0-20241008120650-f17d504a4d0d/go.mod h1:jmxtfco3FHrInOVcVcUH0TjE76M6bsNgin5B+84D7IQ=
github.com/loft-sh/log v0.0.0-20240219160058-26d83ffb46ac h1:Gz/7Lb7WgdgIv+KJz87ORA1zvQW52tUqKPGyunlp4dQ=
Expand Down Expand Up @@ -462,8 +462,8 @@ github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.20.4 h1:Tgh3Yr67PaOv/uTqloMsCEdeuFTatm5zIq5+qNN23vI=
github.com/prometheus/client_golang v1.20.4/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/common v0.60.0 h1:+V9PAREWNvJMAuJ1x1BaWl9dewMW4YrHZQbx0sJNllA=
Expand Down
3 changes: 1 addition & 2 deletions pkg/platform/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import (
"github.com/loft-sh/devpod/pkg/provider"
"github.com/loft-sh/devpod/pkg/workspace"
"github.com/loft-sh/log"
"github.com/pkg/errors"
)

func InitClientFromHost(ctx context.Context, devPodConfig *config.Config, devPodProHost string, log log.Logger) (client.Client, error) {
Expand Down Expand Up @@ -39,7 +38,7 @@ func ProviderFromHost(ctx context.Context, devPodConfig *config.Config, devPodPr

provider, err := workspace.FindProvider(devPodConfig, proInstanceConfig.Provider, log)
if err != nil {
return nil, errors.Wrap(err, "find provider")
return nil, fmt.Errorf("find provider: %w", err)
} else if !provider.Config.IsProxyProvider() {
return nil, fmt.Errorf("provider is not a proxy provider")
}
Expand Down
5 changes: 4 additions & 1 deletion pkg/workspace/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@ import (
"github.com/pkg/errors"
)

var errProvideWorkspaceArg = errors.New("please provide a workspace name. E.g. 'devpod up ./my-folder', 'devpod up github.com/my-org/my-repo' or 'devpod up ubuntu'")
var (
ErrNoWorkspaceFound = errors.New("no workspace found")
errProvideWorkspaceArg = errors.New("please provide a workspace name. E.g. 'devpod up ./my-folder', 'devpod up github.com/my-org/my-repo' or 'devpod up ubuntu'")
)

type ProviderWithOptions struct {
Config *provider2.ProviderConfig `json:"config,omitempty"`
Expand Down
Loading

0 comments on commit ad437c8

Please sign in to comment.