translator/cmdutil/translatorutil.go (216 lines of code) (raw):
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: MIT
package cmdutil
import (
"fmt"
"log"
"os"
"path/filepath"
"strings"
"github.com/xeipuuv/gojsonschema"
"github.com/aws/amazon-cloudwatch-agent/cfg/envconfig"
"github.com/aws/amazon-cloudwatch-agent/internal/constants"
"github.com/aws/amazon-cloudwatch-agent/internal/mapstructure"
"github.com/aws/amazon-cloudwatch-agent/translator"
"github.com/aws/amazon-cloudwatch-agent/translator/config"
"github.com/aws/amazon-cloudwatch-agent/translator/context"
"github.com/aws/amazon-cloudwatch-agent/translator/jsonconfig"
_ "github.com/aws/amazon-cloudwatch-agent/translator/registerrules"
"github.com/aws/amazon-cloudwatch-agent/translator/tocwconfig/toenvconfig"
"github.com/aws/amazon-cloudwatch-agent/translator/tocwconfig/totomlconfig"
"github.com/aws/amazon-cloudwatch-agent/translator/tocwconfig/toyamlconfig"
"github.com/aws/amazon-cloudwatch-agent/translator/translate"
"github.com/aws/amazon-cloudwatch-agent/translator/translate/otel"
translatorUtil "github.com/aws/amazon-cloudwatch-agent/translator/util"
)
const (
fileMode = 0644
jsonTemplateName_Linux = "default_linux_config.json"
jsonTemplateName_Windows = "default_windows_config.json"
jsonTemplateName_Darwin = "default_darwin_config.json"
defaultTomlConfigName = "CWAgent.conf"
)
// TranslateJsonMapToEnvConfigFile populates env-config.json based on the input json config.
func TranslateJsonMapToEnvConfigFile(jsonConfigValue map[string]interface{}, envConfigPath string) {
if envConfigPath == "" {
return
}
bytes := toenvconfig.ToEnvConfig(jsonConfigValue)
if err := os.WriteFile(envConfigPath, bytes, 0644); err != nil {
log.Panicf("E! Failed to create env config. Reason: %s", err.Error())
}
}
func getCurBinaryPath() string {
ex, err := os.Executable()
if err != nil {
log.Panicf("E! Failed to get executable path because of %v", err)
}
return filepath.Dir(ex)
}
func getJsonConfigMap(jsonConfigFilePath, osType string) (map[string]interface{}, error) {
if jsonConfigFilePath == "" {
curPath := getCurBinaryPath()
if osType == config.OS_TYPE_WINDOWS {
jsonConfigFilePath = filepath.Join(curPath, jsonTemplateName_Windows)
} else if osType == config.OS_TYPE_DARWIN {
jsonConfigFilePath = filepath.Join(curPath, jsonTemplateName_Darwin)
} else {
jsonConfigFilePath = filepath.Join(curPath, jsonTemplateName_Linux)
}
}
log.Printf("Reading json config file path: %v ...", jsonConfigFilePath)
if _, err := os.Stat(jsonConfigFilePath); err != nil {
fmt.Printf("%v does not exist or cannot read. Skipping it.\n", jsonConfigFilePath)
return nil, nil
}
return translatorUtil.GetJsonMapFromFile(jsonConfigFilePath)
}
func GetTomlConfigPath(tomlFilePath string) string {
if tomlFilePath == "" {
curPath := getCurBinaryPath()
return filepath.Join(curPath, defaultTomlConfigName)
}
return tomlFilePath
}
func RunSchemaValidation(inputJsonMap map[string]interface{}) (*gojsonschema.Result, error) {
schemaLoader := gojsonschema.NewStringLoader(config.GetJsonSchema())
jsonInputLoader := gojsonschema.NewGoLoader(inputJsonMap)
return gojsonschema.Validate(schemaLoader, jsonInputLoader)
}
func checkSchema(inputJsonMap map[string]interface{}) {
result, err := RunSchemaValidation(inputJsonMap)
if err != nil {
log.Panicf("E! Failed to run schema validation because of %v", err)
}
if result.Valid() {
log.Print("I! Valid Json input schema.")
} else {
errorDetails := result.Errors()
for _, errorDetail := range errorDetails {
translator.AddErrorMessages(config.GetFormattedPath(errorDetail.Context().String()), errorDetail.Description())
}
log.Panic("E! Invalid Json input schema.")
}
}
func GenerateMergedJsonConfigMap(ctx *context.Context) (map[string]interface{}, error) {
// we use a map instead of an array here because we need to override the config value
// for the append operation when the existing file name and new .tmp file name have diff
// only for the ".tmp" suffix, i.e. it is override operation even it says append.
var jsonConfigMapMap = make(map[string]map[string]interface{})
if ctx.MultiConfig() == "append" || ctx.MultiConfig() == "remove" {
// backwards compatible for the old json config file
// this backwards compatible file can be treated as existing files
jsonConfigMap, err := getJsonConfigMap(ctx.InputJsonFilePath(), ctx.Os())
if err != nil {
return nil, fmt.Errorf("unable to get old json config file with error: %v", err)
}
if jsonConfigMap != nil {
jsonConfigMapMap[ctx.InputJsonFilePath()] = jsonConfigMap
}
}
err := filepath.Walk(
ctx.InputJsonDirPath(),
func(path string, info os.FileInfo, err error) error {
if err != nil {
fmt.Printf("Cannot access %v: %v \n", path, err)
return err
}
if envconfig.IsWindowsHostProcessContainer() {
log.Printf("Skipping checking symbolic link for Windows host process containers %s. \n"+
"These symbolic links are skipped as valuating symlinks is common failures for Windows containers", path)
} else {
if info.Mode()&os.ModeSymlink != 0 {
log.Printf("Find symbolic link %s \n", path)
path, err := filepath.EvalSymlinks(path)
if err != nil {
log.Printf("Symbolic link %v will be ignored due to err: %v. \n", path, err)
return nil
}
info, err = os.Stat(path)
if err != nil {
log.Printf("Path %v will be ignored due to err: %v. \n", path, err)
}
}
if info.IsDir() {
return nil
}
}
ext := filepath.Ext(path)
if ext == constants.FileSuffixTmp {
key := strings.TrimSuffix(path, constants.FileSuffixTmp)
// .tmp files
ext = filepath.Ext(key)
// skip .yaml files
if ext == constants.FileSuffixYAML {
return nil
}
if ctx.MultiConfig() == "default" || ctx.MultiConfig() == "append" {
jsonConfigMap, err := getJsonConfigMap(path, ctx.Os())
if err != nil {
return err
}
if jsonConfigMap != nil {
jsonConfigMapMap[key] = jsonConfigMap
}
}
} else if ext == constants.FileSuffixYAML {
// skip .yaml files
return nil
} else {
// non .tmp / existing files
if ctx.MultiConfig() == "append" || ctx.MultiConfig() == "remove" {
jsonConfigMap, err := getJsonConfigMap(path, ctx.Os())
if err != nil {
return err
}
if jsonConfigMap != nil {
if _, ok := jsonConfigMapMap[path]; !ok {
jsonConfigMapMap[path] = jsonConfigMap
}
}
}
}
return nil
})
if err != nil {
log.Printf("unable to scan config dir %v with error: %v", ctx.InputJsonDirPath(), err)
}
if len(jsonConfigMapMap) == 0 {
// For containerized agent, try to read env variable only when json configuration file is absent
if jsonConfigContent, ok := os.LookupEnv(config.CWConfigContent); ok && os.Getenv(config.RUN_IN_CONTAINER) == config.RUN_IN_CONTAINER_TRUE {
log.Printf("Reading json config from from environment variable %v.", config.CWConfigContent)
jm, err := translatorUtil.GetJsonMapFromJsonBytes([]byte(jsonConfigContent))
if err != nil {
return nil, fmt.Errorf("unable to get json map from environment variable %v with error: %v", config.CWConfigContent, err)
}
jsonConfigMapMap[config.CWConfigContent] = jm
}
}
defaultConfig, err := translatorUtil.GetDefaultJsonConfigMap(ctx.Os(), ctx.Mode())
if err != nil {
return nil, err
}
mergedJsonConfigMap, err := jsonconfig.MergeJsonConfigMaps(jsonConfigMapMap, defaultConfig, ctx.MultiConfig())
if err != nil {
return nil, err
}
// Json Schema Validation by gojsonschema
checkSchema(mergedJsonConfigMap)
return mergedJsonConfigMap, nil
}
func TranslateJsonMapToTomlConfig(jsonConfigValue interface{}) (interface{}, error) {
r := new(translate.Translator)
_, val := r.ApplyRule(jsonConfigValue)
if !translator.IsTranslateSuccess() {
return nil, fmt.Errorf("%v", translator.ErrorMessages)
}
// Translation is valid, log info messages and continue to convert/write to toml
for _, infoMessage := range translator.InfoMessages {
log.Println(infoMessage)
}
return val, nil
}
func TranslateJsonMapToYamlConfig(jsonConfigValue interface{}) (interface{}, error) {
cfg, err := otel.Translate(jsonConfigValue, context.CurrentContext().Os())
if err != nil {
return nil, err
}
return mapstructure.Marshal(cfg)
}
func ConfigToTomlFile(config interface{}, tomlConfigFilePath string) error {
res := totomlconfig.ToTomlConfig(config)
return os.WriteFile(tomlConfigFilePath, []byte(res), fileMode)
}
func ConfigToYamlFile(config interface{}, yamlConfigFilePath string) error {
res := toyamlconfig.ToYamlConfig(config)
// If YAML just contains the word null, then remove the file.
if strings.TrimSpace(res) == "null" {
// Intentionally not checking return value.
_ = os.Remove(yamlConfigFilePath)
return nil
}
return os.WriteFile(yamlConfigFilePath, []byte(res), fileMode)
}