internal/platform/effectiveconfig/config.go (255 lines of code) (raw):
/*
* Copyright 2021-2024 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package effectiveconfig
import (
"errors"
"fmt"
"os"
"path/filepath"
"github.com/JetBrains/qodana-cli/internal/platform/msg"
"github.com/JetBrains/qodana-cli/internal/platform/qdyaml"
"github.com/JetBrains/qodana-cli/internal/platform/strutil"
"github.com/JetBrains/qodana-cli/internal/platform/utils"
"github.com/JetBrains/qodana-cli/internal/tooling"
log "github.com/sirupsen/logrus"
)
// Files – effective configuration files, constructed by calling config-loader-cli.jar,
// all paths are absolute
// + also profile files are stored in config dir
type Files struct {
ConfigDir string
EffectiveQodanaYamlPath string
LocalQodanaYamlPath string
QodanaConfigJsonPath string
}
func CreateEffectiveConfigFiles(
localQodanaYamlFullPath string,
globalConfigurationsDir string,
globalConfigId string,
jrePath string,
effectiveConfigDir string,
logDir string,
) (Files, error) {
if globalConfigId != "" && globalConfigurationsDir == "" {
return Files{}, fmt.Errorf(
"global configuration id %s is defined without global cofigurations directory",
globalConfigId,
)
}
if globalConfigurationsDir != "" && globalConfigId == "" {
return Files{}, fmt.Errorf(
"global configurations directory %s is defined without global configuration id",
globalConfigurationsDir,
)
}
globalConfigurationsFile := ""
if globalConfigurationsDir != "" {
globalConfigurationsYamlFilename := "qodana-global-configurations.yaml"
globalConfigurationsFile = filepath.Join(globalConfigurationsDir, globalConfigurationsYamlFilename)
if !isFileExists(globalConfigurationsFile) {
msg.ErrorMessage(
"%s file not found in global configuration directory %s",
globalConfigurationsYamlFilename,
globalConfigurationsDir,
)
return Files{}, fmt.Errorf("%s doesn't exist", globalConfigurationsFile)
}
}
configLoaderTempDir, cleanup, err := utils.CreateTempDir("config-loader-cli")
if err != nil {
return Files{}, err
}
defer cleanup()
configLoaderCliJar, err := createConfigLoaderCliJar(configLoaderTempDir)
if err != nil {
return Files{}, err
}
defer func(name string) {
err := os.Remove(name)
if err != nil {
log.Warnf("Failed to delete config-loader-cli.jar: %v", err)
}
}(configLoaderCliJar)
args, err := configurationLoaderCliArgs(
jrePath,
configLoaderCliJar,
localQodanaYamlFullPath,
globalConfigurationsFile,
globalConfigId,
effectiveConfigDir,
)
if err != nil {
return Files{}, err
}
log.Debugf("Creating effective configuration in '%s' directory, args: %v", effectiveConfigDir, args)
if _, _, res, err := utils.LaunchAndLog(logDir, "config-loader-cli", args...); res > 0 || err != nil {
if err == nil {
err = errors.New("failed to create effective configuration")
} else {
err = fmt.Errorf("failed to create effective configuration: %v", err)
}
msg.ErrorMessage("Failed to create effective configuration.")
return Files{}, err
}
effectiveQodanaYamlData, err := getEffectiveQodanaYamlData(effectiveConfigDir)
if err != nil {
return Files{}, err
}
err = verifyEffectiveQodanaYamlIdeAndLinterMatchLocal(effectiveQodanaYamlData, localQodanaYamlFullPath)
if err != nil {
return Files{}, err
}
msg.SuccessMessage("Loaded Qodana Configuration")
return effectiveQodanaYamlData, nil
}
func createConfigLoaderCliJar(dir string) (string, error) {
configLoaderCliJarPath := filepath.Join(dir, "config-loader-cli.jar")
if isFileExists(configLoaderCliJarPath) {
err := os.Remove(configLoaderCliJarPath)
if err != nil {
return "", fmt.Errorf("failed to delete existing config-loader-cli.jar: %v", err)
}
}
err := os.MkdirAll(filepath.Dir(configLoaderCliJarPath), 0755)
if err != nil {
return "", fmt.Errorf("failed to create directory for config-loader-cli.jar: %v", err)
}
log.Debugf("creating config-loader-cli.jar at '%s'", configLoaderCliJarPath)
err = os.WriteFile(configLoaderCliJarPath, tooling.ConfigLoaderCli, 0644)
if err != nil {
return "", fmt.Errorf("failed to write config-loader-cli.jar content to %s: %v", configLoaderCliJarPath, err)
}
return configLoaderCliJarPath, nil
}
func configurationLoaderCliArgs(
jrePath string,
configLoaderCliJarPath string,
localQodanaYamlPath string,
globalConfigurationsFile string,
globalConfigId string,
effectiveConfigDir string,
) ([]string, error) {
if jrePath == "" {
return nil, fmt.Errorf(
"JRE not found. Required for effective configuration creation. " +
"See requirements in our documentation: https://www.jetbrains.com/help/qodana/deploy-qodana.html",
)
}
if configLoaderCliJarPath == "" {
return nil, fmt.Errorf("config-loader-cli.jar not found. Required for effective configuration creation")
}
var err error
args := []string{
strutil.QuoteIfSpace(strutil.QuoteForWindows(jrePath)),
"-jar",
strutil.QuoteForWindows(configLoaderCliJarPath),
}
effectiveConfigDirAbs, err := filepath.Abs(effectiveConfigDir)
if err != nil {
err := fmt.Errorf(
"failed to compute absolute path of effective configuration directory %s: %v",
effectiveConfigDir,
err,
)
return nil, err
}
args = append(args, "--effective-config-out-dir", strutil.QuoteForWindows(effectiveConfigDirAbs))
if localQodanaYamlPath != "" {
localQodanaYamlPathAbs, err := filepath.Abs(localQodanaYamlPath)
if err != nil {
err := fmt.Errorf(
"failed to compute absolute path of local qodana.yaml file %s: %v",
localQodanaYamlPath,
err,
)
return nil, err
}
args = append(
args,
"--local-qodana-yaml",
strutil.QuoteIfSpace(strutil.QuoteForWindows(localQodanaYamlPathAbs)),
)
}
if globalConfigurationsFile != "" {
globalConfigurationsFileAbs, err := filepath.Abs(globalConfigurationsFile)
if err != nil {
err := fmt.Errorf(
"failed to compute absolute path of global configurations file %s: %v",
globalConfigurationsFile,
err,
)
return nil, err
}
args = append(
args,
"--global-configs-file",
strutil.QuoteIfSpace(strutil.QuoteForWindows(globalConfigurationsFileAbs)),
)
}
if globalConfigId != "" {
args = append(args, "--global-config-id", strutil.QuoteIfSpace(strutil.QuoteForWindows(globalConfigId)))
}
return args, nil
}
func getEffectiveQodanaYamlData(effectiveConfigDir string) (Files, error) {
effectiveQodanaYamlPath := filepath.Join(effectiveConfigDir, "effective.qodana.yaml")
if !isFileExists(effectiveQodanaYamlPath) {
effectiveQodanaYamlPath = ""
}
localQodanaYamlPath := filepath.Join(effectiveConfigDir, "qodana.yaml")
if !isFileExists(localQodanaYamlPath) {
localQodanaYamlPath = ""
}
qodanaConfigJsonPath := filepath.Join(effectiveConfigDir, "qodana-config.json")
if !isFileExists(qodanaConfigJsonPath) {
qodanaConfigJsonPath = ""
}
if effectiveQodanaYamlPath != "" && qodanaConfigJsonPath == "" {
return Files{}, errors.New("effective.qodana.yaml file doesn't have a qodana-config.json file")
}
if localQodanaYamlPath != "" && effectiveQodanaYamlPath == "" {
return Files{}, errors.New("local qodana.yaml file doesn't have an effective.qodana.yaml file")
}
return Files{
ConfigDir: effectiveConfigDir,
EffectiveQodanaYamlPath: effectiveQodanaYamlPath,
LocalQodanaYamlPath: localQodanaYamlPath,
QodanaConfigJsonPath: qodanaConfigJsonPath,
}, nil
}
//goland:noinspection GoRedundantElseInIf
func isFileExists(path string) bool {
if _, err := os.Stat(path); err == nil {
return true
} else if os.IsNotExist(err) {
return false
} else {
log.Fatalf("Failed to verify existence of file %s: %s", path, err)
}
return false
}
func verifyEffectiveQodanaYamlIdeAndLinterMatchLocal(
effectiveQodanaYamlData Files,
localQodanaYamlPathFromRoot string,
) error {
effectiveYaml := qdyaml.LoadQodanaYamlByFullPath(effectiveQodanaYamlData.EffectiveQodanaYamlPath)
effectiveLinter := effectiveYaml.Linter
effectiveIde := effectiveYaml.Ide
if effectiveLinter == "" && effectiveIde == "" {
return nil
}
isLocalQodanaYamlPresent := effectiveQodanaYamlData.LocalQodanaYamlPath != ""
if isLocalQodanaYamlPresent {
localQodanaYaml := qdyaml.LoadQodanaYamlByFullPath(effectiveQodanaYamlData.LocalQodanaYamlPath)
failedToCreateEffectiveConfigurationMessage := "Failed to create effective configuration"
topMessageTemplate := "'%s: %s' is specified in one of files provided by 'imports' from " + localQodanaYamlPathFromRoot + " '%s' is required in root qodana.yaml"
bottomMessageTemplate := "Add `%s: %s` to " + localQodanaYamlPathFromRoot
if effectiveIde != localQodanaYaml.Ide {
msg.ErrorMessage(failedToCreateEffectiveConfigurationMessage)
msg.ErrorMessage(topMessageTemplate, "ide", effectiveIde, "ide")
msg.ErrorMessage(bottomMessageTemplate, "ide", effectiveIde)
return errors.New("effective.qodana.yaml `ide` doesn't match root qodana.yaml `ide`")
}
//goland:noinspection GoDfaConstantCondition
if effectiveLinter != localQodanaYaml.Linter {
msg.ErrorMessage(failedToCreateEffectiveConfigurationMessage)
msg.ErrorMessage(topMessageTemplate, "linter", effectiveLinter, "linter")
msg.ErrorMessage(bottomMessageTemplate, "linter", effectiveLinter)
return errors.New("effective.qodana.yaml `linter` doesn't match root qodana.yaml `linter`")
}
}
return nil
}