Skip to content

Commit 7209462

Browse files
feat: Add trivy along with Add support for download-based tools and enhance installation logic
1 parent 03133b3 commit 7209462

File tree

7 files changed

+497
-57
lines changed

7 files changed

+497
-57
lines changed

.codacy/codacy.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ runtimes:
22
33
tools:
44
5+

config/tools-installer.go

Lines changed: 145 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,13 @@ package config
33
import (
44
"bytes"
55
"codacy/cli-v2/plugins"
6+
"codacy/cli-v2/utils"
67
"fmt"
8+
"io"
79
"log"
810
"os"
911
"os/exec"
12+
"path/filepath"
1013
"strings"
1114
"text/template"
1215
)
@@ -30,18 +33,26 @@ func InstallTool(name string, toolInfo *plugins.ToolInfo) error {
3033
return nil
3134
}
3235

33-
// Get the runtime for this tool
34-
runtimeInfo, ok := Config.Runtimes()[toolInfo.Runtime]
35-
if !ok {
36-
return fmt.Errorf("required runtime %s not found for tool %s", toolInfo.Runtime, name)
37-
}
38-
3936
// Make sure the installation directory exists
4037
err := os.MkdirAll(toolInfo.InstallDir, 0755)
4138
if err != nil {
4239
return fmt.Errorf("failed to create installation directory: %w", err)
4340
}
4441

42+
// Check if this is a download-based tool (like trivy) or a runtime-based tool (like eslint)
43+
if toolInfo.DownloadURL != "" {
44+
// This is a download-based tool
45+
return installDownloadBasedTool(toolInfo)
46+
}
47+
48+
// This is a runtime-based tool, proceed with regular installation
49+
50+
// Get the runtime for this tool
51+
runtimeInfo, ok := Config.Runtimes()[toolInfo.Runtime]
52+
if !ok {
53+
return fmt.Errorf("required runtime %s not found for tool %s", toolInfo.Runtime, name)
54+
}
55+
4556
// Prepare template data
4657
templateData := map[string]string{
4758
"InstallDir": toolInfo.InstallDir,
@@ -80,7 +91,7 @@ func InstallTool(name string, toolInfo *plugins.ToolInfo) error {
8091

8192
// Execute the installation command using the package manager
8293
cmd := exec.Command(packageManagerBinary, strings.Split(installCmd, " ")...)
83-
94+
8495
log.Printf("Installing %s v%s...\n", toolInfo.Name, toolInfo.Version)
8596
output, err := cmd.CombinedOutput()
8697
if err != nil {
@@ -91,6 +102,131 @@ func InstallTool(name string, toolInfo *plugins.ToolInfo) error {
91102
return nil
92103
}
93104

105+
// installDownloadBasedTool installs a tool by downloading and extracting it
106+
func installDownloadBasedTool(toolInfo *plugins.ToolInfo) error {
107+
// Create a file name for the downloaded archive
108+
fileName := filepath.Base(toolInfo.DownloadURL)
109+
downloadPath := filepath.Join(Config.ToolsDirectory(), fileName)
110+
111+
// Check if the file already exists
112+
_, err := os.Stat(downloadPath)
113+
if os.IsNotExist(err) {
114+
// Download the file
115+
log.Printf("Downloading %s v%s...\n", toolInfo.Name, toolInfo.Version)
116+
downloadPath, err = utils.DownloadFile(toolInfo.DownloadURL, Config.ToolsDirectory())
117+
if err != nil {
118+
return fmt.Errorf("failed to download tool: %w", err)
119+
}
120+
} else if err != nil {
121+
return fmt.Errorf("error checking for existing download: %w", err)
122+
} else {
123+
log.Printf("Using existing download for %s v%s\n", toolInfo.Name, toolInfo.Version)
124+
}
125+
126+
// Open the downloaded file
127+
file, err := os.Open(downloadPath)
128+
if err != nil {
129+
return fmt.Errorf("failed to open downloaded file: %w", err)
130+
}
131+
defer file.Close()
132+
133+
// Create a temporary extraction directory
134+
tempExtractDir := filepath.Join(Config.ToolsDirectory(), fmt.Sprintf("%s-%s-temp", toolInfo.Name, toolInfo.Version))
135+
136+
// Clean up any previous extraction attempt
137+
os.RemoveAll(tempExtractDir)
138+
139+
// Create the temporary extraction directory
140+
err = os.MkdirAll(tempExtractDir, 0755)
141+
if err != nil {
142+
return fmt.Errorf("failed to create temporary extraction directory: %w", err)
143+
}
144+
145+
// Extract to the temporary directory first
146+
log.Printf("Extracting %s v%s...\n", toolInfo.Name, toolInfo.Version)
147+
if strings.HasSuffix(fileName, ".zip") {
148+
err = utils.ExtractZip(file.Name(), tempExtractDir)
149+
} else {
150+
err = utils.ExtractTarGz(file, tempExtractDir)
151+
}
152+
153+
if err != nil {
154+
return fmt.Errorf("failed to extract tool: %w", err)
155+
}
156+
157+
// Create the final installation directory
158+
err = os.MkdirAll(toolInfo.InstallDir, 0755)
159+
if err != nil {
160+
return fmt.Errorf("failed to create installation directory: %w", err)
161+
}
162+
163+
// Find and copy the tool binaries
164+
for binName, binPath := range toolInfo.Binaries {
165+
// Get the base name of the binary (without the path)
166+
binBaseName := filepath.Base(binPath)
167+
168+
// Try to find the binary in the extracted files
169+
foundPath := ""
170+
171+
// First check if it's at the expected location directly
172+
expectedPath := filepath.Join(tempExtractDir, binBaseName)
173+
if _, err := os.Stat(expectedPath); err == nil {
174+
foundPath = expectedPath
175+
} else {
176+
// Look for the binary anywhere in the extracted directory
177+
err := filepath.Walk(tempExtractDir, func(path string, info os.FileInfo, err error) error {
178+
if err != nil {
179+
return err
180+
}
181+
if !info.IsDir() && filepath.Base(path) == binBaseName {
182+
foundPath = path
183+
return io.EOF // Stop the walk
184+
}
185+
return nil
186+
})
187+
188+
// io.EOF is expected when we find the file and stop the walk
189+
if err != nil && err != io.EOF {
190+
return fmt.Errorf("error searching for %s binary: %w", binName, err)
191+
}
192+
}
193+
194+
if foundPath == "" {
195+
return fmt.Errorf("could not find %s binary in extracted files", binName)
196+
}
197+
198+
// Make sure the destination directory exists
199+
err = os.MkdirAll(filepath.Dir(binPath), 0755)
200+
if err != nil {
201+
return fmt.Errorf("failed to create directory for binary: %w", err)
202+
}
203+
204+
// Copy the binary to the installation directory
205+
input, err := os.Open(foundPath)
206+
if err != nil {
207+
return fmt.Errorf("failed to open %s binary: %w", binName, err)
208+
}
209+
defer input.Close()
210+
211+
output, err := os.OpenFile(binPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755)
212+
if err != nil {
213+
return fmt.Errorf("failed to create destination file for %s: %w", binName, err)
214+
}
215+
defer output.Close()
216+
217+
_, err = io.Copy(output, input)
218+
if err != nil {
219+
return fmt.Errorf("failed to copy %s binary: %w", binName, err)
220+
}
221+
}
222+
223+
// Clean up the temporary directory
224+
os.RemoveAll(tempExtractDir)
225+
226+
log.Printf("Successfully installed %s v%s\n", toolInfo.Name, toolInfo.Version)
227+
return nil
228+
}
229+
94230
// isToolInstalled checks if a tool is already installed by checking for the binary
95231
func isToolInstalled(toolInfo *plugins.ToolInfo) bool {
96232
// If there are no binaries, check the install directory
@@ -116,12 +252,12 @@ func executeToolTemplate(tmplStr string, data map[string]string) (string, error)
116252
if err != nil {
117253
return "", err
118254
}
119-
255+
120256
var buf bytes.Buffer
121257
err = tmpl.Execute(&buf, data)
122258
if err != nil {
123259
return "", err
124260
}
125-
261+
126262
return buf.String(), nil
127263
}

config/tools-installer_test.go

Lines changed: 66 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,40 +13,40 @@ func TestAddTools(t *testing.T) {
1313
// Set up a temporary config for testing
1414
originalConfig := Config
1515
defer func() { Config = originalConfig }() // Restore original config after test
16-
16+
1717
tempDir, err := os.MkdirTemp("", "codacy-tools-test")
1818
assert.NoError(t, err)
1919
defer os.RemoveAll(tempDir)
20-
20+
2121
// Initialize config with test directories
2222
Config = ConfigType{
2323
toolsDirectory: tempDir,
2424
tools: make(map[string]*plugins.ToolInfo),
2525
}
26-
26+
2727
// Create a list of tool configs for testing
2828
configs := []plugins.ToolConfig{
2929
{
3030
Name: "eslint",
3131
Version: "8.38.0",
3232
},
3333
}
34-
34+
3535
// Add tools to the config
3636
err = Config.AddTools(configs)
3737
assert.NoError(t, err)
38-
38+
3939
// Assert we have the expected tool in the config
4040
assert.Contains(t, Config.Tools(), "eslint")
41-
41+
4242
// Get the eslint tool info
4343
eslintInfo := Config.Tools()["eslint"]
44-
44+
4545
// Assert the basic tool info is correct
4646
assert.Equal(t, "eslint", eslintInfo.Name)
4747
assert.Equal(t, "8.38.0", eslintInfo.Version)
4848
assert.Equal(t, "node", eslintInfo.Runtime)
49-
49+
5050
// Assert the install directory is correct
5151
expectedInstallDir := filepath.Join(tempDir, "[email protected]")
5252
assert.Equal(t, expectedInstallDir, eslintInfo.InstallDir)
@@ -67,18 +67,73 @@ func TestExecuteToolTemplate(t *testing.T) {
6767

6868
// Test conditional registry template
6969
registryTemplateStr := "{{if .Registry}}config set registry {{.Registry}}{{end}}"
70-
70+
7171
// With registry
7272
dataWithRegistry := map[string]string{
7373
"Registry": "https://registry.npmjs.org/",
7474
}
7575
resultWithRegistry, err := executeToolTemplate(registryTemplateStr, dataWithRegistry)
7676
assert.NoError(t, err)
7777
assert.Equal(t, "config set registry https://registry.npmjs.org/", resultWithRegistry)
78-
78+
7979
// Without registry
8080
dataWithoutRegistry := map[string]string{}
8181
resultWithoutRegistry, err := executeToolTemplate(registryTemplateStr, dataWithoutRegistry)
8282
assert.NoError(t, err)
8383
assert.Equal(t, "", resultWithoutRegistry)
84-
}
84+
}
85+
86+
func TestAddDownloadBasedTool(t *testing.T) {
87+
// Set up a temporary config for testing
88+
originalConfig := Config
89+
defer func() { Config = originalConfig }() // Restore original config after test
90+
91+
tempDir, err := os.MkdirTemp("", "codacy-tools-test")
92+
assert.NoError(t, err)
93+
defer os.RemoveAll(tempDir)
94+
95+
// Initialize config with test directories
96+
Config = ConfigType{
97+
toolsDirectory: tempDir,
98+
tools: make(map[string]*plugins.ToolInfo),
99+
}
100+
101+
// Create a list of tool configs for testing
102+
configs := []plugins.ToolConfig{
103+
{
104+
Name: "trivy",
105+
Version: "0.37.3",
106+
},
107+
}
108+
109+
// Add tools to the config
110+
err = Config.AddTools(configs)
111+
assert.NoError(t, err)
112+
113+
// Assert we have the expected tool in the config
114+
assert.Contains(t, Config.Tools(), "trivy")
115+
116+
// Get the trivy tool info
117+
trivyInfo := Config.Tools()["trivy"]
118+
119+
// Assert the basic tool info is correct
120+
assert.Equal(t, "trivy", trivyInfo.Name)
121+
assert.Equal(t, "0.37.3", trivyInfo.Version)
122+
123+
// Make sure it has a download URL
124+
assert.NotEmpty(t, trivyInfo.DownloadURL)
125+
assert.Contains(t, trivyInfo.DownloadURL, "aquasecurity/trivy/releases/download")
126+
assert.Contains(t, trivyInfo.DownloadURL, "0.37.3")
127+
128+
// Assert the install directory is correct
129+
expectedInstallDir := filepath.Join(tempDir, "[email protected]")
130+
assert.Equal(t, expectedInstallDir, trivyInfo.InstallDir)
131+
132+
// Assert binary paths are set
133+
assert.NotEmpty(t, trivyInfo.Binaries)
134+
assert.Contains(t, trivyInfo.Binaries, "trivy")
135+
136+
// Assert the binary path is correct (should point to the extracted binary)
137+
expectedBinaryPath := filepath.Join(expectedInstallDir, "trivy")
138+
assert.Equal(t, expectedBinaryPath, trivyInfo.Binaries["trivy"])
139+
}

0 commit comments

Comments
 (0)