Skip to content

Commit

Permalink
WIP configure
Browse files Browse the repository at this point in the history
  • Loading branch information
XiaoConstantine committed Jul 4, 2024
1 parent bb246b1 commit f4a408d
Show file tree
Hide file tree
Showing 5 changed files with 391 additions and 1 deletion.
199 changes: 199 additions & 0 deletions pkg/commands/configure/configure.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
package configure

import (
"context"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strings"
"time"

"mycli/pkg/iostreams"
"mycli/pkg/utils"

"github.com/AlecAivazis/survey/v2"
"github.com/olekukonko/tablewriter"
"github.com/spf13/cobra"
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"
)

func NewConfigureCmd(iostream *iostreams.IOStreams) *cobra.Command {
cs := iostream.ColorScheme()
var configFile string
var force bool

cmd := &cobra.Command{
Use: "configure",
Short: "Configure tools from a YAML configuration file",
Long: `Reads a list of tools to configure from a YAML file and applies their configurations.`,
Annotations: map[string]string{
"group": "configure",
},
RunE: func(cmd *cobra.Command, args []string) error {
span, ctx := tracer.StartSpanFromContext(cmd.Context(), "configure_tools")
defer span.Finish()

var configPath string
var force bool
// Prompt for config file path
configPrompt := &survey.Input{
Message: "Enter the path to the config file:",
Default: "config.yaml",
}
if err := survey.AskOne(configPrompt, &configPath); err != nil {
return os.ErrExist
}
configPath = os.ExpandEnv(configPath)
// Replace ~ with home directory
if strings.HasPrefix(configPath, "~") {
home, err := os.UserHomeDir()
if err == nil {
configPath = filepath.Join(home, configPath[1:])
}
}

// Get the absolute path
absPath, err := filepath.Abs(configPath)
if err == nil {
configPath = absPath
}
fmt.Println(configPath)
// Validate the file path
if _, err := os.Stat(configPath); os.IsNotExist(err) {
fmt.Fprintf(iostream.ErrOut, "Error: Config file does not exist at path: %s\n", configPath)
return err
}
if err := cmd.Flags().Set("config", configPath); err != nil {
fmt.Fprintf(iostream.ErrOut, "failed to set config flag: %s\n", err)
return err
}
// Prompt for force flag
forcePrompt := &survey.Confirm{
Message: "Do you want to force overwrite existing configs?",
Default: false,
}
if err := survey.AskOne(forcePrompt, &force); err != nil {
return os.ErrExist
}
config, err := utils.LoadToolsConfig(configFile)
if err != nil {
fmt.Fprintf(iostream.ErrOut, cs.Red("Error loading configuration: %v\n"), err)
return utils.ConfigNotFoundError
}

if force {
if err := cmd.Flags().Set("force", "true"); err != nil {
fmt.Fprintf(iostream.ErrOut, "failed to set force flag: %s\n", err)
return err
}
}

return ConfigureToolsFromConfig(iostream, config, ctx, force)
},
}

cmd.Flags().StringVarP(&configFile, "config", "c", "config.yaml", "Path to the configuration file")
cmd.Flags().BoolVarP(&force, "force", "f", false, "Force reconfiguration of tools")

return cmd
}

func ConfigureToolsFromConfig(iostream *iostreams.IOStreams, config *utils.ToolConfig, ctx context.Context, force bool) error {
cs := iostream.ColorScheme()
parentSpan, ctx := tracer.StartSpanFromContext(ctx, "configure_tools")
defer parentSpan.Finish()

var configuredTools [][]string
startTime := time.Now()

for _, item := range config.Configure {
toolSpan, toolCtx := tracer.StartSpanFromContext(ctx, fmt.Sprintf("configure_%s", item.Name))
toolStartTime := time.Now()

fmt.Fprintf(iostream.Out, cs.Green("Configuring %s...\n"), item.Name)

if err := configureTool(item, toolCtx, force); err != nil {
fmt.Fprintf(iostream.ErrOut, cs.Red("Failed to configure %s: %v\n"), item.Name, err)
toolSpan.SetTag("status", "failed")
toolSpan.SetTag("error", err)
return err
}

toolDuration := time.Since(toolStartTime)
configuredTools = append(configuredTools, []string{item.Name, toolDuration.String(), "succeed"})

toolSpan.SetTag("status", "success")
toolSpan.Finish()
}

totalDuration := time.Since(startTime)
configuredTools = append(configuredTools, []string{"Total", totalDuration.String()})

// Print summary of configured tools in a table
table := tablewriter.NewWriter(iostream.Out)
table.SetHeader([]string{"Tool", "Duration", "Status"})
for _, v := range configuredTools {
table.Append(v)
}
table.Render()

fmt.Fprintln(iostream.Out, cs.GreenBold("All requested tools have been configured successfully."))
return nil
}

func configureTool(item utils.ConfigureItem, ctx context.Context, force bool) error {
span, _ := tracer.StartSpanFromContext(ctx, "configure_tool")
defer span.Finish()

installPath := expandTilde(item.InstallPath)

// Check if file already exists and force flag is not set
if _, err := os.Stat(installPath); err == nil && !force {
return fmt.Errorf("configuration file already exists at %s. Use --force to overwrite", installPath)
}

// Create the directory if it doesn't exist
err := os.MkdirAll(filepath.Dir(installPath), 0755)
if err != nil {
return fmt.Errorf("failed to create directory: %v", err)
}

// Download the configuration file
resp, err := http.Get(item.ConfigURL)
if err != nil {
return fmt.Errorf("failed to download configuration: %v", err)
}
defer resp.Body.Close()

// Create the configuration file
out, err := os.Create(installPath)
if err != nil {
return fmt.Errorf("failed to create configuration file: %v", err)
}
defer out.Close()

// Write the content to the file
_, err = io.Copy(out, resp.Body)
if err != nil {
return fmt.Errorf("failed to write configuration: %v", err)
}

return nil
}

func expandTilde(path string) string {
if len(path) == 0 || path[0] != '~' {
return path
}
if len(path) > 1 && path[1] != '/' {
// If it's something like ~user/test, don't expand it
return path
}
home, err := os.UserHomeDir()
if err != nil {
return path
}
return filepath.Join(home, path[1:])
}
165 changes: 165 additions & 0 deletions pkg/commands/configure/configure_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
package configure

import (
"context"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"

"mycli/pkg/iostreams"
"mycli/pkg/utils"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestNewConfigureToolsCmd(t *testing.T) {
ios, _, _, _ := iostreams.Test()
cmd := NewConfigureCmd(ios)

assert.NotNil(t, cmd)
assert.Equal(t, "configure", cmd.Use)
assert.Equal(t, "configure", cmd.Annotations["group"])
}

func TestConfigureToolsFromConfig(t *testing.T) {
// Create a temporary directory for test files
tempDir, err := os.MkdirTemp("", "test-configure")
require.NoError(t, err)
defer os.RemoveAll(tempDir)

// Create a test server to serve configuration files
testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, err := w.Write([]byte("test configuration content"))
if err != nil {
t.Errorf("Failed to write response: %v", err)
}
}))
defer testServer.Close()

// Create a test configuration
config := &utils.ToolConfig{
Configure: []utils.ConfigureItem{
{
Name: "test-tool",
ConfigURL: testServer.URL,
InstallPath: filepath.Join(tempDir, "test-tool-config"),
},
},
}

ios, _, stdout, stderr := iostreams.Test()
err = ConfigureToolsFromConfig(ios, config, context.Background(), false)

assert.NoError(t, err)
assert.Contains(t, stdout.String(), "Configuring test-tool...")
assert.Contains(t, stdout.String(), "All requested tools have been configured successfully.")
assert.Empty(t, stderr.String())

// Check if the file was created and has the correct content
content, err := os.ReadFile(filepath.Join(tempDir, "test-tool-config"))
require.NoError(t, err)
assert.Equal(t, "test configuration content", string(content))
}

func TestConfigureTool(t *testing.T) {
// Create a temporary directory for test files
tempDir, err := os.MkdirTemp("", "test-configure-tool")
require.NoError(t, err)
defer os.RemoveAll(tempDir)

// Create a test server to serve configuration files
testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, err := w.Write([]byte("test configuration content"))
if err != nil {
t.Errorf("Failed to write response: %v", err)
}
}))
defer testServer.Close()

testCases := []struct {
name string
item utils.ConfigureItem
force bool
expectError bool
}{
{
name: "Successful configuration",
item: utils.ConfigureItem{
Name: "test-tool",
ConfigURL: testServer.URL,
InstallPath: filepath.Join(tempDir, "test-tool-config"),
},
force: false,
expectError: false,
},
{
name: "File already exists - no force",
item: utils.ConfigureItem{
Name: "existing-tool",
ConfigURL: testServer.URL,
InstallPath: filepath.Join(tempDir, "existing-tool-config"),
},
force: false,
expectError: true,
},
{
name: "File already exists - with force",
item: utils.ConfigureItem{
Name: "existing-tool",
ConfigURL: testServer.URL,
InstallPath: filepath.Join(tempDir, "existing-tool-config"),
},
force: true,
expectError: false,
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// For the "existing file" tests, create the file first
if tc.item.Name == "existing-tool" {
err := os.WriteFile(tc.item.InstallPath, []byte("existing content"), 0644)
require.NoError(t, err)
}

err := configureTool(tc.item, context.Background(), tc.force)

if tc.expectError {
assert.Error(t, err)
} else {
assert.NoError(t, err)

// Check if the file was created and has the correct content
content, err := os.ReadFile(tc.item.InstallPath)
require.NoError(t, err)
assert.Equal(t, "test configuration content", string(content))
}
})
}
}

func TestExpandTilde(t *testing.T) {
home, err := os.UserHomeDir()
require.NoError(t, err)

testCases := []struct {
input string
expected string
}{
{"~/test", filepath.Join(home, "test")},
{"/absolute/path", "/absolute/path"},
{"relative/path", "relative/path"},
{"~user/test", "~user/test"}, // should not expand for other users
{"~", home}, // just ~ should expand to home
}

for _, tc := range testCases {
t.Run(tc.input, func(t *testing.T) {
result := expandTilde(tc.input)
assert.Equal(t, tc.expected, result)
})
}
}
Empty file.
9 changes: 9 additions & 0 deletions pkg/commands/root/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"context"
"errors"
"fmt"
"mycli/pkg/commands/configure"
"mycli/pkg/commands/install"
"mycli/pkg/iostreams"
"mycli/pkg/utils"
Expand Down Expand Up @@ -47,9 +48,17 @@ func NewRootCmd(iostream *iostreams.IOStreams) (*cobra.Command, error) {
Title: "Install commands",
})

rootCmd.AddGroup(
&cobra.Group{
ID: "configure",
Title: "Configure commands",
})

installCmd := install.NewInstallCmd(iostream)
configureCmd := configure.NewConfigureCmd(iostream)

rootCmd.AddCommand(installCmd)
rootCmd.AddCommand(configureCmd)
rootCmd.PersistentFlags().Bool("help", false, "Show help for command")
return rootCmd, nil
}
Expand Down
Loading

0 comments on commit f4a408d

Please sign in to comment.