-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
bb246b1
commit f4a408d
Showing
5 changed files
with
391 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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:]) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.