Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .codacy/codacy.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ runtimes:
- node@22.2.0
tools:
- eslint@9.3.0
- trivy@0.50.0
154 changes: 145 additions & 9 deletions config/tools-installer.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@
import (
"bytes"
"codacy/cli-v2/plugins"
"codacy/cli-v2/utils"
"fmt"
"io"
"log"
"os"
"os/exec"
"path/filepath"
"strings"
"text/template"
)
Expand All @@ -30,18 +33,26 @@
return nil
}

// Get the runtime for this tool
runtimeInfo, ok := Config.Runtimes()[toolInfo.Runtime]
if !ok {
return fmt.Errorf("required runtime %s not found for tool %s", toolInfo.Runtime, name)
}

// Make sure the installation directory exists
err := os.MkdirAll(toolInfo.InstallDir, 0755)
if err != nil {
return fmt.Errorf("failed to create installation directory: %w", err)
}

// Check if this is a download-based tool (like trivy) or a runtime-based tool (like eslint)
if toolInfo.DownloadURL != "" {
// This is a download-based tool
return installDownloadBasedTool(toolInfo)
Copy link
Contributor Author

@andrzej-janczak andrzej-janczak Mar 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return ❤️ 🥲

}

// This is a runtime-based tool, proceed with regular installation

// Get the runtime for this tool
runtimeInfo, ok := Config.Runtimes()[toolInfo.Runtime]
if !ok {
return fmt.Errorf("required runtime %s not found for tool %s", toolInfo.Runtime, name)
}

// Prepare template data
templateData := map[string]string{
"InstallDir": toolInfo.InstallDir,
Expand Down Expand Up @@ -80,7 +91,7 @@

// Execute the installation command using the package manager
cmd := exec.Command(packageManagerBinary, strings.Split(installCmd, " ")...)

log.Printf("Installing %s v%s...\n", toolInfo.Name, toolInfo.Version)
output, err := cmd.CombinedOutput()
if err != nil {
Expand All @@ -91,6 +102,131 @@
return nil
}

// installDownloadBasedTool installs a tool by downloading and extracting it
func installDownloadBasedTool(toolInfo *plugins.ToolInfo) error {

Check warning on line 106 in config/tools-installer.go

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

config/tools-installer.go#L106

Method installDownloadBasedTool has 77 lines of code (limit is 50)

Check failure on line 106 in config/tools-installer.go

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

config/tools-installer.go#L106

Method installDownloadBasedTool has a cyclomatic complexity of 18 (limit is 10)
// Create a file name for the downloaded archive
fileName := filepath.Base(toolInfo.DownloadURL)
downloadPath := filepath.Join(Config.ToolsDirectory(), fileName)

// Check if the file already exists
_, err := os.Stat(downloadPath)
if os.IsNotExist(err) {
// Download the file
log.Printf("Downloading %s v%s...\n", toolInfo.Name, toolInfo.Version)
downloadPath, err = utils.DownloadFile(toolInfo.DownloadURL, Config.ToolsDirectory())
if err != nil {
return fmt.Errorf("failed to download tool: %w", err)
}
} else if err != nil {
return fmt.Errorf("error checking for existing download: %w", err)
} else {
log.Printf("Using existing download for %s v%s\n", toolInfo.Name, toolInfo.Version)
}

// Open the downloaded file
file, err := os.Open(downloadPath)
if err != nil {
return fmt.Errorf("failed to open downloaded file: %w", err)
}
defer file.Close()

// Create a temporary extraction directory
tempExtractDir := filepath.Join(Config.ToolsDirectory(), fmt.Sprintf("%s-%s-temp", toolInfo.Name, toolInfo.Version))

// Clean up any previous extraction attempt
os.RemoveAll(tempExtractDir)

// Create the temporary extraction directory
err = os.MkdirAll(tempExtractDir, 0755)

Check warning on line 140 in config/tools-installer.go

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

config/tools-installer.go#L140

Detected file permissions that are set to more than `0600` (user/owner can read and write). Setting file permissions to higher than `0600` is most likely unnecessary and violates the principle of least privilege.

Check warning on line 140 in config/tools-installer.go

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

config/tools-installer.go#L140

The application was found setting directory permissions to overly permissive values.
if err != nil {
return fmt.Errorf("failed to create temporary extraction directory: %w", err)
}

// Extract to the temporary directory first
log.Printf("Extracting %s v%s...\n", toolInfo.Name, toolInfo.Version)
if strings.HasSuffix(fileName, ".zip") {
err = utils.ExtractZip(file.Name(), tempExtractDir)
} else {
err = utils.ExtractTarGz(file, tempExtractDir)
}

if err != nil {
return fmt.Errorf("failed to extract tool: %w", err)
}

// Create the final installation directory
err = os.MkdirAll(toolInfo.InstallDir, 0755)

Check warning on line 158 in config/tools-installer.go

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

config/tools-installer.go#L158

Detected file permissions that are set to more than `0600` (user/owner can read and write). Setting file permissions to higher than `0600` is most likely unnecessary and violates the principle of least privilege.

Check warning on line 158 in config/tools-installer.go

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

config/tools-installer.go#L158

The application was found setting directory permissions to overly permissive values.
if err != nil {
return fmt.Errorf("failed to create installation directory: %w", err)
}

// Find and copy the tool binaries
for binName, binPath := range toolInfo.Binaries {
// Get the base name of the binary (without the path)
binBaseName := filepath.Base(binPath)

// Try to find the binary in the extracted files
foundPath := ""

// First check if it's at the expected location directly
expectedPath := filepath.Join(tempExtractDir, binBaseName)
if _, err := os.Stat(expectedPath); err == nil {
foundPath = expectedPath
} else {
// Look for the binary anywhere in the extracted directory
err := filepath.Walk(tempExtractDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() && filepath.Base(path) == binBaseName {
foundPath = path
return io.EOF // Stop the walk
}
return nil
})

// io.EOF is expected when we find the file and stop the walk
if err != nil && err != io.EOF {
return fmt.Errorf("error searching for %s binary: %w", binName, err)
}
}

if foundPath == "" {
return fmt.Errorf("could not find %s binary in extracted files", binName)
}

// Make sure the destination directory exists
err = os.MkdirAll(filepath.Dir(binPath), 0755)

Check warning on line 199 in config/tools-installer.go

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

config/tools-installer.go#L199

Detected file permissions that are set to more than `0600` (user/owner can read and write). Setting file permissions to higher than `0600` is most likely unnecessary and violates the principle of least privilege.

Check warning on line 199 in config/tools-installer.go

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

config/tools-installer.go#L199

The application was found setting directory permissions to overly permissive values.
if err != nil {
return fmt.Errorf("failed to create directory for binary: %w", err)
}

// Copy the binary to the installation directory
input, err := os.Open(foundPath)
if err != nil {
return fmt.Errorf("failed to open %s binary: %w", binName, err)
}
defer input.Close()

output, err := os.OpenFile(binPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755)

Check warning on line 211 in config/tools-installer.go

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

config/tools-installer.go#L211

Detected file permissions that are set to more than `0600` (user/owner can read and write). Setting file permissions to higher than `0600` is most likely unnecessary and violates the principle of least privilege.

Check warning on line 211 in config/tools-installer.go

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

config/tools-installer.go#L211

The application was found setting file permissions to overly permissive values.
if err != nil {
return fmt.Errorf("failed to create destination file for %s: %w", binName, err)
}
defer output.Close()

_, err = io.Copy(output, input)
if err != nil {
return fmt.Errorf("failed to copy %s binary: %w", binName, err)
}
}

// Clean up the temporary directory
os.RemoveAll(tempExtractDir)

log.Printf("Successfully installed %s v%s\n", toolInfo.Name, toolInfo.Version)
return nil
}

// isToolInstalled checks if a tool is already installed by checking for the binary
func isToolInstalled(toolInfo *plugins.ToolInfo) bool {
// If there are no binaries, check the install directory
Expand All @@ -116,12 +252,12 @@
if err != nil {
return "", err
}

var buf bytes.Buffer
err = tmpl.Execute(&buf, data)
if err != nil {
return "", err
}

return buf.String(), nil
}
77 changes: 66 additions & 11 deletions config/tools-installer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,40 +13,40 @@ func TestAddTools(t *testing.T) {
// Set up a temporary config for testing
originalConfig := Config
defer func() { Config = originalConfig }() // Restore original config after test

tempDir, err := os.MkdirTemp("", "codacy-tools-test")
assert.NoError(t, err)
defer os.RemoveAll(tempDir)

// Initialize config with test directories
Config = ConfigType{
toolsDirectory: tempDir,
tools: make(map[string]*plugins.ToolInfo),
}

// Create a list of tool configs for testing
configs := []plugins.ToolConfig{
{
Name: "eslint",
Version: "8.38.0",
},
}

// Add tools to the config
err = Config.AddTools(configs)
assert.NoError(t, err)

// Assert we have the expected tool in the config
assert.Contains(t, Config.Tools(), "eslint")

// Get the eslint tool info
eslintInfo := Config.Tools()["eslint"]

// Assert the basic tool info is correct
assert.Equal(t, "eslint", eslintInfo.Name)
assert.Equal(t, "8.38.0", eslintInfo.Version)
assert.Equal(t, "node", eslintInfo.Runtime)

// Assert the install directory is correct
expectedInstallDir := filepath.Join(tempDir, "eslint@8.38.0")
assert.Equal(t, expectedInstallDir, eslintInfo.InstallDir)
Expand All @@ -67,18 +67,73 @@ func TestExecuteToolTemplate(t *testing.T) {

// Test conditional registry template
registryTemplateStr := "{{if .Registry}}config set registry {{.Registry}}{{end}}"

// With registry
dataWithRegistry := map[string]string{
"Registry": "https://registry.npmjs.org/",
}
resultWithRegistry, err := executeToolTemplate(registryTemplateStr, dataWithRegistry)
assert.NoError(t, err)
assert.Equal(t, "config set registry https://registry.npmjs.org/", resultWithRegistry)

// Without registry
dataWithoutRegistry := map[string]string{}
resultWithoutRegistry, err := executeToolTemplate(registryTemplateStr, dataWithoutRegistry)
assert.NoError(t, err)
assert.Equal(t, "", resultWithoutRegistry)
}
}

func TestAddDownloadBasedTool(t *testing.T) {
// Set up a temporary config for testing
originalConfig := Config
defer func() { Config = originalConfig }() // Restore original config after test

tempDir, err := os.MkdirTemp("", "codacy-tools-test")
assert.NoError(t, err)
defer os.RemoveAll(tempDir)

// Initialize config with test directories
Config = ConfigType{
toolsDirectory: tempDir,
tools: make(map[string]*plugins.ToolInfo),
}

// Create a list of tool configs for testing
configs := []plugins.ToolConfig{
{
Name: "trivy",
Version: "0.37.3",
},
}

// Add tools to the config
err = Config.AddTools(configs)
assert.NoError(t, err)

// Assert we have the expected tool in the config
assert.Contains(t, Config.Tools(), "trivy")

// Get the trivy tool info
trivyInfo := Config.Tools()["trivy"]

// Assert the basic tool info is correct
assert.Equal(t, "trivy", trivyInfo.Name)
assert.Equal(t, "0.37.3", trivyInfo.Version)

// Make sure it has a download URL
assert.NotEmpty(t, trivyInfo.DownloadURL)
assert.Contains(t, trivyInfo.DownloadURL, "aquasecurity/trivy/releases/download")
assert.Contains(t, trivyInfo.DownloadURL, "0.37.3")

// Assert the install directory is correct
expectedInstallDir := filepath.Join(tempDir, "trivy@0.37.3")
assert.Equal(t, expectedInstallDir, trivyInfo.InstallDir)

// Assert binary paths are set
assert.NotEmpty(t, trivyInfo.Binaries)
assert.Contains(t, trivyInfo.Binaries, "trivy")

// Assert the binary path is correct (should point to the extracted binary)
expectedBinaryPath := filepath.Join(expectedInstallDir, "trivy")
assert.Equal(t, expectedBinaryPath, trivyInfo.Binaries["trivy"])
}
Loading