Skip to content

Commit

Permalink
Merge pull request #694 from FabianKramm/driver-refactor
Browse files Browse the repository at this point in the history
refactor: devpod pro import functionality
  • Loading branch information
FabianKramm authored Sep 13, 2023
2 parents 259c23c + c1f48a2 commit 6818b37
Show file tree
Hide file tree
Showing 6 changed files with 79 additions and 146 deletions.
13 changes: 13 additions & 0 deletions cmd/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,19 @@ func (cmd *DeleteCmd) Run(ctx context.Context, devPodConfig *config.Config, args
return nil
}

// skip deletion if imported
workspaceConfig := client.WorkspaceConfig()
if !cmd.Force && workspaceConfig.Imported {
// delete workspace folder
err = clientimplementation.DeleteWorkspaceFolder(devPodConfig.DefaultContext, client.Workspace(), log.Default)
if err != nil {
return err
}

log.Default.Donef("Skip remote deletion of workspace %s as it is imported, if you really want to delete this workspace also remotely, run with --force", client.Workspace())
return nil
}

// get instance status
if !cmd.Force {
// lock workspace only if we don't force deletion
Expand Down
138 changes: 63 additions & 75 deletions cmd/pro/import_workspace.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import (
"fmt"

"github.com/loft-sh/devpod/cmd/flags"
"github.com/loft-sh/devpod/pkg/client/clientimplementation"
"github.com/loft-sh/devpod/pkg/config"
provider2 "github.com/loft-sh/devpod/pkg/provider"
"github.com/loft-sh/devpod/pkg/random"
"github.com/loft-sh/devpod/pkg/workspace"
"github.com/loft-sh/log"
"github.com/pkg/errors"
Expand All @@ -19,19 +19,18 @@ type ImportCmd struct {

WorkspaceId string
WorkspaceUid string
WorkspaceContext string
WorkspaceOptions []string
providerResolver *ProviderResolver
log log.Logger
WorkspaceProject string

Own bool
log log.Logger
}

// NewImportCmd creates a new command
func NewImportCmd(globalFlags *flags.GlobalFlags) *cobra.Command {
logger := log.GetInstance()
cmd := &ImportCmd{
GlobalFlags: globalFlags,
providerResolver: &ProviderResolver{log: logger},
log: logger,
GlobalFlags: globalFlags,
log: logger,
}

importCmd := &cobra.Command{
Expand All @@ -44,114 +43,103 @@ func NewImportCmd(globalFlags *flags.GlobalFlags) *cobra.Command {

importCmd.Flags().StringVar(&cmd.WorkspaceId, "workspace-id", "", "ID of a workspace to import")
importCmd.Flags().StringVar(&cmd.WorkspaceUid, "workspace-uid", "", "UID of a workspace to import")
// optional
importCmd.Flags().StringVar(&cmd.WorkspaceContext, "workspace-context", "", "Target context for a workspace")
importCmd.Flags().StringArrayVarP(
&cmd.WorkspaceOptions, "option", "o", []string{}, "Workspace option in the form KEY=VALUE")

_ = importCmd.MarkFlagRequired("workspace-id")
importCmd.Flags().StringVar(&cmd.WorkspaceProject, "workspace-project", "", "Project of the workspace to import")
importCmd.Flags().BoolVar(&cmd.Own, "own", false, "If true, will behave as if workspace was not imported")
_ = importCmd.MarkFlagRequired("workspace-uid")

return importCmd
}

func (cmd *ImportCmd) Run(ctx context.Context, args []string) error {
if len(args) != 1 {
return fmt.Errorf("usage: devpod pro import-workspace <devpod-pro-host>")
}

devPodProHost := args[0]
devPodConfig, err := config.LoadConfig(cmd.Context, "")
if err != nil {
return err
}

provider, err := cmd.providerResolver.Resolve(devPodConfig, devPodProHost)
if err != nil {
return errors.Wrap(err, "resolve provider")
// set uid as id
if cmd.WorkspaceId == "" {
cmd.WorkspaceId = cmd.WorkspaceUid
}

options, err := provider2.ParseOptions(cmd.WorkspaceOptions)
if err != nil {
return errors.Wrap(err, "parse options")
// check if workspace already exists
if provider2.WorkspaceExists(devPodConfig.DefaultContext, cmd.WorkspaceId) {
workspaceConfig, err := provider2.LoadWorkspaceConfig(devPodConfig.DefaultContext, cmd.WorkspaceId)
if err != nil {
return fmt.Errorf("load workspace: %w", err)
} else if workspaceConfig.UID == cmd.WorkspaceUid {
cmd.log.Infof("Workspace %s already imported", cmd.WorkspaceId)
return nil
}

newWorkspaceId := cmd.WorkspaceId + "-" + random.String(5)
if provider2.WorkspaceExists(devPodConfig.DefaultContext, newWorkspaceId) {
return fmt.Errorf("workspace %s already exists", cmd.WorkspaceId)
}

cmd.log.Infof("Workspace %s already exists, will use name %s instead", cmd.WorkspaceId, newWorkspaceId)
cmd.WorkspaceId = newWorkspaceId
}

workspaceDefinition, err := cmd.prepareWorkspaceToImportDefinition(devPodConfig, provider)
provider, err := resolveProInstance(devPodConfig, devPodProHost, cmd.log)
if err != nil {
return errors.Wrap(err, "prepare workspace to import definition")
return errors.Wrap(err, "resolve provider")
}

workspaceClient, err := clientimplementation.NewProxyClient(
devPodConfig, provider, workspaceDefinition, cmd.log)
err = cmd.writeWorkspaceDefinition(devPodConfig, provider)
if err != nil {
return err
return errors.Wrap(err, "prepare workspace to import definition")
}

return workspaceClient.ImportWorkspace(ctx, options)
cmd.log.Infof("Successfully imported workspace %s", cmd.WorkspaceId)
return nil
}

func (cmd *ImportCmd) context(devPodConfig *config.Config) (string, error) {
if cmd.WorkspaceContext == "" {
return devPodConfig.DefaultContext, nil
func (cmd *ImportCmd) writeWorkspaceDefinition(devPodConfig *config.Config, provider *provider2.ProviderConfig) error {
workspaceFolder, err := provider2.GetWorkspaceDir(devPodConfig.DefaultContext, cmd.WorkspaceId)
if err != nil {
return errors.Wrap(err, "get workspace dir")
}

if devPodConfig.Contexts[cmd.WorkspaceContext] != nil {
return cmd.WorkspaceContext, nil
workspaceObj := &provider2.Workspace{
ID: cmd.WorkspaceId,
UID: cmd.WorkspaceUid,
Folder: workspaceFolder,
Provider: provider2.WorkspaceProviderConfig{
Name: provider.Name,
Options: map[string]config.OptionValue{},
},
Context: devPodConfig.DefaultContext,
Imported: !cmd.Own,
}

return "", fmt.Errorf("context '%s' doesn't exist", cmd.WorkspaceContext)
}

func (cmd *ImportCmd) prepareWorkspaceToImportDefinition(
devPodConfig *config.Config, provider *provider2.ProviderConfig) (*provider2.Workspace, error) {
workspaceContext, err := cmd.context(devPodConfig)
if err != nil {
return nil, err
if cmd.WorkspaceProject != "" {
workspaceObj.Provider.Options["LOFT_PROJECT"] = config.OptionValue{
Value: cmd.WorkspaceProject,
UserProvided: true,
}
}

workspaceFolder, err := provider2.GetWorkspaceDir(workspaceContext, cmd.WorkspaceId)
err = provider2.SaveWorkspaceConfig(workspaceObj)
if err != nil {
return nil, errors.Wrap(err, "get workspace dir")
return err
}

return &provider2.Workspace{
ID: cmd.WorkspaceId,
UID: cmd.WorkspaceUid,
Folder: workspaceFolder,
Provider: provider2.WorkspaceProviderConfig{Name: provider.Name},
Context: workspaceContext,
}, nil
}

type ProviderResolver struct {
log log.Logger
return nil
}

func (r *ProviderResolver) proInstance(
devPodConfig *config.Config, devPodProHost string) (*provider2.ProInstance, error) {
instances, err := workspace.ListProInstances(devPodConfig, r.log)
func resolveProInstance(devPodConfig *config.Config, devPodProHost string, log log.Logger) (*provider2.ProviderConfig, error) {
proInstanceConfig, err := provider2.LoadProInstanceConfig(devPodConfig.DefaultContext, devPodProHost)
if err != nil {
return nil, errors.Wrap(err, "list pro instances")
}
for _, instance := range instances {
if instance.Host == devPodProHost {
return instance, nil
}
}
return nil, fmt.Errorf("pro instance with host '%s' doesn't exist", devPodProHost)
}

func (r *ProviderResolver) Resolve(devPodConfig *config.Config, devPodProHost string) (*provider2.ProviderConfig, error) {
instance, err := r.proInstance(devPodConfig, devPodProHost)
if err != nil {
return nil, errors.Wrap(err, "pro instance")
return nil, fmt.Errorf("load pro instance %s: %w", devPodProHost, err)
}

provider, err := workspace.FindProvider(devPodConfig, instance.Provider, r.log)
provider, err := workspace.FindProvider(devPodConfig, proInstanceConfig.Provider, log)
if err != nil {
return nil, errors.Wrap(err, "find provider")
}

if !provider.Config.IsProxyProvider() {
} else if !provider.Config.IsProxyProvider() {
return nil, fmt.Errorf("provider is not a proxy provider")
}

Expand Down
3 changes: 0 additions & 3 deletions pkg/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,6 @@ type ProxyClient interface {

// Ssh starts an ssh tunnel to the workspace container
Ssh(ctx context.Context, options SshOptions) error

// ImportWorkspace imports the workspace from the remote
ImportWorkspace(ctx context.Context, options ImportWorkspaceOptions) error
}

type MachineClient interface {
Expand Down
65 changes: 0 additions & 65 deletions pkg/client/clientimplementation/proxy_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -333,56 +333,6 @@ func (s *proxyClient) Status(ctx context.Context, options client.StatusOptions)
return client.ParseStatus(status.State)
}

func (s *proxyClient) ImportWorkspace(ctx context.Context, options client.ImportWorkspaceOptions) error {
s.m.Lock()
defer s.m.Unlock()

reader, writer := io.Pipe()
defer writer.Close()
go func() {
readLogStream(reader, s.log)
}()

buf := &bytes.Buffer{}

cmd := s.config.Exec.Proxy.ImportWorkspace
if !isCommandSpecified(cmd) {
return fmt.Errorf("import command not configured")
}

err := RunCommandWithBinaries(
ctx,
"import",
cmd,
s.workspace.Context,
s.workspace,
nil,
s.devPodConfig.ProviderOptions(s.config.Name),
s.config,
options,
nil,
io.MultiWriter(writer, buf),
io.MultiWriter(writer, buf),
s.log.ErrorStreamOnly(),
)
if err != nil {
cmdOutput := buf.String()
msg, parsingError := parseLogEntry(cmdOutput)
if parsingError != nil {
s.log.Warnf("Error parsing log entry (%v): %v", cmdOutput, parsingError)
return fmt.Errorf("error importing a workspace: %w", err)
}

return fmt.Errorf("error importing a workspace: %s", msg)
}

return nil
}

func isCommandSpecified(cmd []string) bool {
return len(cmd) > 0 && cmd[0] != ""
}

func EncodeOptions(options any, name string) map[string]string {
raw, _ := json.Marshal(options)
return map[string]string{
Expand Down Expand Up @@ -426,18 +376,3 @@ func readLogStream(reader io.Reader, logger log.Logger) {
}
}
}

type LogEntry struct {
Time string `json:"time"`
Message string `json:"message"`
Level string `json:"level"`
}

func parseLogEntry(rawEntry string) (string, error) {
var entry LogEntry
err := json.Unmarshal([]byte(rawEntry), &entry)
if err != nil {
return "", err
}
return entry.Message, nil
}
3 changes: 0 additions & 3 deletions pkg/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -247,9 +247,6 @@ type ProxyCommands struct {

// Status proxies the status command
Status types.StrArray `json:"status,omitempty"`

// ImportWorkspace proxies the import command
ImportWorkspace types.StrArray `json:"import,omitempty"`
}

type SubOptions struct {
Expand Down
3 changes: 3 additions & 0 deletions pkg/provider/workspace.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ type Workspace struct {
// Context is the context where this config file was loaded from
Context string `json:"context,omitempty"`

// Imported signals that this workspace was imported
Imported bool `json:"imported,omitempty"`

// Origin is the place where this config file was loaded from
Origin string `json:"-"`
}
Expand Down

0 comments on commit 6818b37

Please sign in to comment.